0

[WIP] Update deps

This commit is contained in:
eleksir
2024-07-24 22:38:25 +03:00
parent 618110a7b2
commit a6960d0e86
253 changed files with 15130 additions and 5831 deletions

33
go.mod
View File

@@ -1,11 +1,11 @@
module aleesa-telegram-go
go 1.19
go 1.22
require (
github.com/NicoNex/echotron/v3 v3.29.0
github.com/cockroachdb/errors v1.11.1
github.com/cockroachdb/pebble v1.1.0
github.com/NicoNex/echotron/v3 v3.33.1
github.com/cockroachdb/errors v1.11.3
github.com/cockroachdb/pebble v1.1.1
github.com/davecgh/go-spew v1.1.1
github.com/go-redis/redis/v8 v8.11.5
github.com/hjson/hjson-go v3.3.0+incompatible
@@ -15,25 +15,28 @@ require (
require (
github.com/DataDog/zstd v1.5.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cockroachdb/fifo v0.0.0-20240616162244-4768e80dfb9a // indirect
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect
github.com/cockroachdb/redact v1.1.5 // indirect
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/getsentry/sentry-go v0.27.0 // indirect
github.com/getsentry/sentry-go v0.28.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/klauspost/compress v1.17.7 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_golang v1.18.0 // indirect
github.com/prometheus/client_model v0.6.0 // indirect
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/prometheus/client_golang v1.19.1 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
golang.org/x/sys v0.17.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/protobuf v1.32.0 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
)

84
go.sum
View File

@@ -1,18 +1,21 @@
github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ=
github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
github.com/NicoNex/echotron/v3 v3.29.0 h1:STzeYNauF+h82vTL9QIqIBEvhke/bsxgYKQMCmJKnLM=
github.com/NicoNex/echotron/v3 v3.29.0/go.mod h1:LpP5IyHw0y+DZUZMBgXEDAF9O8feXrQu7w7nlJzzoZI=
github.com/NicoNex/echotron/v3 v3.33.1 h1:P9bVvmnuVd0C7bR5q9TV6Pece/wBqZ6PSo0RqokZgng=
github.com/NicoNex/echotron/v3 v3.33.1/go.mod h1:7LvjveJmezuUOeaoA3nzQduNlSPQYfq219Z+baKY04Q=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4=
github.com/cockroachdb/errors v1.11.1 h1:xSEW75zKaKCWzR3OfxXUxgrk/NtT4G1MiOv5lWZazG8=
github.com/cockroachdb/errors v1.11.1/go.mod h1:8MUxA3Gi6b25tYlFEBGLf+D8aISL+M4MIpiWMSNRfxw=
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU=
github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I=
github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8=
github.com/cockroachdb/fifo v0.0.0-20240616162244-4768e80dfb9a h1:f52TdbU4D5nozMAhO9TvTJ2ZMCXtN4VIAmfrrZ0JXQ4=
github.com/cockroachdb/fifo v0.0.0-20240616162244-4768e80dfb9a/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M=
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE=
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs=
github.com/cockroachdb/pebble v1.1.0 h1:pcFh8CdCIt2kmEpK0OIatq67Ln9uGDYY3d5XnE0LJG4=
github.com/cockroachdb/pebble v1.1.0/go.mod h1:sEHm5NOXxyiAoKWhoFxT8xMgd/f3RA6qUqQ1BXKrh2E=
github.com/cockroachdb/pebble v1.1.1 h1:XnKU22oiCLy2Xn8vp1re67cXg4SAasg/WDt1NtcRFaw=
github.com/cockroachdb/pebble v1.1.1/go.mod h1:4exszw1r40423ZsmkG/09AFEG83I0uDgfujJdbL6kYU=
github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30=
github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo=
@@ -24,9 +27,11 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/getsentry/sentry-go v0.28.1 h1:zzaSm/vHmGllRM6Tpx1492r0YDzauArdBfkJRtY6P5k=
github.com/getsentry/sentry-go v0.28.1/go.mod h1:1fQZ+7l7eeJ3wYi82q5Hg8GqAPgefRq+FP/QhafYVgg=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
@@ -34,33 +39,40 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/hjson/hjson-go v3.3.0+incompatible h1:Rqr+Ya+0aCJMjaE4s8E9YKvuJLuLVpEvz4ONum52vnI=
github.com/hjson/hjson-go v3.3.0+incompatible/go.mod h1:qsetwF8NlsTsOTwZTApNlTCerV+b2GjYRRcIk4JMFio=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos=
github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8=
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
@@ -68,35 +80,40 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@@ -105,10 +122,13 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,4 +1,4 @@
| <img src="assets/readme_banner.png" alt="logo" width="800"><br/><br/> [![Language](https://img.shields.io/badge/Language-Go-blue.svg)](https://golang.org/) [![PkgGoDev](https://pkg.go.dev/badge/github.com/NicoNex/echotron/v3)](https://pkg.go.dev/github.com/NicoNex/echotron/v3) [![Go Report Card](https://goreportcard.com/badge/github.com/NicoNex/echotron/v3)](https://goreportcard.com/report/github.com/NicoNex/echotron/v3) [![License](http://img.shields.io/badge/license-LGPL3.0-orange.svg?style=flat)](https://github.com/NicoNex/echotron/blob/master/LICENSE) [![Coverage Status](https://coveralls.io/repos/github/NicoNex/echotron/badge.svg?branch=master)](https://coveralls.io/github/NicoNex/echotron?branch=master) [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go) [![Telegram](https://img.shields.io/badge/Echotron%20News-blue?logo=telegram&style=flat)](https://t.me/echotronnews) |
| <img src="assets/readme_banner.png" alt="logo" width="800"><br/><br/> [![Language](https://img.shields.io/badge/Language-Go-blue.svg)](https://golang.org/) [![PkgGoDev](https://pkg.go.dev/badge/github.com/NicoNex/echotron/v3)](https://pkg.go.dev/github.com/NicoNex/echotron/v3) [![Go Report Card](https://goreportcard.com/badge/github.com/NicoNex/echotron/v3)](https://goreportcard.com/report/github.com/NicoNex/echotron/v3) [![codecov](https://codecov.io/gh/NicoNex/echotron/graph/badge.svg?token=LVJGOEYL5M)](https://codecov.io/gh/NicoNex/echotron) [![License](http://img.shields.io/badge/license-LGPL3.0-orange.svg?style=flat)](https://github.com/NicoNex/echotron/blob/master/LICENSE) [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go) [![Telegram](https://img.shields.io/badge/Echotron%20News-blue?logo=telegram&style=flat)](https://t.me/echotronnews) |
| :------: |
**Echotron** is an elegant and concurrent library for the Telegram bot API in Go.

View File

@@ -27,21 +27,32 @@ import (
// API is the object that contains all the functions that wrap those of the Telegram Bot API.
type API struct {
token string
base string
client *client
token string
base string
}
// NewAPI returns a new API object.
func NewAPI(token string) API {
return API{
token: token,
base: fmt.Sprintf("https://api.telegram.org/bot%s/", token),
token: token,
base: fmt.Sprintf("https://api.telegram.org/bot%s/", token),
client: lclient,
}
}
// NewLocalAPI is like NewAPI but allows to use a local API server.
func NewLocalAPI(url, token string) API {
return API{
token: token,
base: url,
client: lclient,
}
}
// GetUpdates is used to receive incoming updates using long polling.
func (a API) GetUpdates(opts *UpdateOptions) (res APIResponseUpdate, err error) {
return get[APIResponseUpdate](a.base, "getUpdates", urlValues(opts))
return res, a.client.get(a.base, "getUpdates", urlValues(opts), &res)
}
// SetWebhook is used to specify a url and receive incoming updates via an outgoing webhook.
@@ -60,7 +71,7 @@ func (a API) SetWebhook(webhookURL string, dropPendingUpdates bool, opts *Webhoo
addValues(vals, opts)
url = fmt.Sprintf("%s?%s", strings.TrimSuffix(url, "/"), vals.Encode())
cnt, err := sendPostForm(url, keyVal)
cnt, err := a.client.doPostForm(url, keyVal)
if err != nil {
return
}
@@ -78,17 +89,17 @@ func (a API) DeleteWebhook(dropPendingUpdates bool) (res APIResponseBase, err er
var vals = make(url.Values)
vals.Set("drop_pending_updates", btoa(dropPendingUpdates))
return get[APIResponseBase](a.base, "deleteWebhook", vals)
return res, a.client.get(a.base, "deleteWebhook", vals, &res)
}
// GetWebhookInfo is used to get current webhook status.
func (a API) GetWebhookInfo() (res APIResponseWebhook, err error) {
return get[APIResponseWebhook](a.base, "getWebhookInfo", nil)
return res, a.client.get(a.base, "getWebhookInfo", nil, &res)
}
// GetMe is a simple method for testing your bot's auth token.
func (a API) GetMe() (res APIResponseUser, err error) {
return get[APIResponseUser](a.base, "getMe", nil)
return res, a.client.get(a.base, "getMe", nil, &res)
}
// LogOut is used to log out from the cloud Bot API server before launching the bot locally.
@@ -96,14 +107,14 @@ func (a API) GetMe() (res APIResponseUser, err error) {
// After a successful call, you can immediately log in on a local server,
// but will not be able to log in back to the cloud Bot API server for 10 minutes.
func (a API) LogOut() (res APIResponseBool, err error) {
return get[APIResponseBool](a.base, "logOut", nil)
return res, a.client.get(a.base, "logOut", nil, &res)
}
// Close is used to close the bot instance before moving it from one local server to another.
// You need to delete the webhook before calling this method to ensure that the bot isn't launched again after server restart.
// The method will return error 429 in the first 10 minutes after the bot is launched.
func (a API) Close() (res APIResponseBool, err error) {
return get[APIResponseBool](a.base, "close", nil)
return res, a.client.get(a.base, "close", nil, &res)
}
// SendMessage is used to send text messages.
@@ -112,7 +123,7 @@ func (a API) SendMessage(text string, chatID int64, opts *MessageOptions) (res A
vals.Set("text", text)
vals.Set("chat_id", itoa(chatID))
return get[APIResponseMessage](a.base, "sendMessage", addValues(vals, opts))
return res, a.client.get(a.base, "sendMessage", addValues(vals, opts), &res)
}
// ForwardMessage is used to forward messages of any kind.
@@ -123,7 +134,7 @@ func (a API) ForwardMessage(chatID, fromChatID int64, messageID int, opts *Forwa
vals.Set("chat_id", itoa(chatID))
vals.Set("from_chat_id", itoa(fromChatID))
vals.Set("message_id", itoa(int64(messageID)))
return get[APIResponseMessage](a.base, "forwardMessage", addValues(vals, opts))
return res, a.client.get(a.base, "forwardMessage", addValues(vals, opts), &res)
}
// ForwardMessages is used to forward multiple messages of any kind.
@@ -141,7 +152,7 @@ func (a API) ForwardMessages(chatID, fromChatID int64, messageIDs []int, opts *F
vals.Set("chat_id", itoa(chatID))
vals.Set("from_chat_id", itoa(fromChatID))
vals.Set("message_ids", string(msgIDs))
return get[APIResponseMessageIDs](a.base, "forwardMessages", addValues(vals, opts))
return res, a.client.get(a.base, "forwardMessages", addValues(vals, opts), &res)
}
// CopyMessage is used to copy messages of any kind.
@@ -154,7 +165,7 @@ func (a API) CopyMessage(chatID, fromChatID int64, messageID int, opts *CopyOpti
vals.Set("chat_id", itoa(chatID))
vals.Set("from_chat_id", itoa(fromChatID))
vals.Set("message_id", itoa(int64(messageID)))
return get[APIResponseMessageID](a.base, "copyMessage", addValues(vals, opts))
return res, a.client.get(a.base, "copyMessage", addValues(vals, opts), &res)
}
// CopyMessages is used to copy messages of any kind.
@@ -174,7 +185,7 @@ func (a API) CopyMessages(chatID, fromChatID int64, messageIDs []int, opts *Copy
vals.Set("chat_id", itoa(chatID))
vals.Set("from_chat_id", itoa(fromChatID))
vals.Set("message_ids", string(msgIDs))
return get[APIResponseMessageIDs](a.base, "copyMessages", addValues(vals, opts))
return res, a.client.get(a.base, "copyMessages", addValues(vals, opts), &res)
}
// SendPhoto is used to send photos.
@@ -182,7 +193,7 @@ func (a API) SendPhoto(file InputFile, chatID int64, opts *PhotoOptions) (res AP
var vals = make(url.Values)
vals.Set("chat_id", itoa(chatID))
return postFile[APIResponseMessage](a.base, "sendPhoto", "photo", file, InputFile{}, addValues(vals, opts))
return res, a.client.postFile(a.base, "sendPhoto", "photo", file, InputFile{}, addValues(vals, opts), &res)
}
// SendAudio is used to send audio files,
@@ -199,7 +210,7 @@ func (a API) SendAudio(file InputFile, chatID int64, opts *AudioOptions) (res AP
}
vals.Set("chat_id", itoa(chatID))
return postFile[APIResponseMessage](a.base, "sendAudio", "audio", file, thumbnail, addValues(vals, opts))
return res, a.client.postFile(a.base, "sendAudio", "audio", file, thumbnail, addValues(vals, opts), &res)
}
// SendDocument is used to send general files.
@@ -214,7 +225,7 @@ func (a API) SendDocument(file InputFile, chatID int64, opts *DocumentOptions) (
}
vals.Set("chat_id", itoa(chatID))
return postFile[APIResponseMessage](a.base, "sendDocument", "document", file, thumbnail, addValues(vals, opts))
return res, a.client.postFile(a.base, "sendDocument", "document", file, thumbnail, addValues(vals, opts), &res)
}
// SendVideo is used to send video files.
@@ -230,7 +241,7 @@ func (a API) SendVideo(file InputFile, chatID int64, opts *VideoOptions) (res AP
}
vals.Set("chat_id", itoa(chatID))
return postFile[APIResponseMessage](a.base, "sendVideo", "video", file, thumbnail, addValues(vals, opts))
return res, a.client.postFile(a.base, "sendVideo", "video", file, thumbnail, addValues(vals, opts), &res)
}
// SendAnimation is used to send animation files (GIF or H.264/MPEG-4 AVC video without sound).
@@ -245,7 +256,7 @@ func (a API) SendAnimation(file InputFile, chatID int64, opts *AnimationOptions)
}
vals.Set("chat_id", itoa(chatID))
return postFile[APIResponseMessage](a.base, "sendAnimation", "animation", file, thumbnail, addValues(vals, opts))
return res, a.client.postFile(a.base, "sendAnimation", "animation", file, thumbnail, addValues(vals, opts), &res)
}
// SendVoice is used to send audio files, if you want Telegram clients to display the file as a playable voice message.
@@ -254,7 +265,7 @@ func (a API) SendVoice(file InputFile, chatID int64, opts *VoiceOptions) (res AP
var vals = make(url.Values)
vals.Set("chat_id", itoa(chatID))
return postFile[APIResponseMessage](a.base, "sendVoice", "voice", file, InputFile{}, addValues(vals, opts))
return res, a.client.postFile(a.base, "sendVoice", "voice", file, InputFile{}, addValues(vals, opts), &res)
}
// SendVideoNote is used to send video messages.
@@ -269,7 +280,7 @@ func (a API) SendVideoNote(file InputFile, chatID int64, opts *VideoNoteOptions)
}
vals.Set("chat_id", itoa(chatID))
return postFile[APIResponseMessage](a.base, "sendVideoNote", "video_note", file, thumbnail, addValues(vals, opts))
return res, a.client.postFile(a.base, "sendVideoNote", "video_note", file, thumbnail, addValues(vals, opts), &res)
}
// SendMediaGroup is used to send a group of photos, videos, documents or audios as an album.
@@ -278,7 +289,7 @@ func (a API) SendMediaGroup(chatID int64, media []GroupableInputMedia, opts *Med
var vals = make(url.Values)
vals.Set("chat_id", itoa(chatID))
return postMedia[APIResponseMessageArray](a.base, "sendMediaGroup", false, addValues(vals, opts), toInputMedia(media)...)
return res, a.client.postMedia(a.base, "sendMediaGroup", false, addValues(vals, opts), &res, toInputMedia(media)...)
}
// SendLocation is used to send point on the map.
@@ -288,7 +299,7 @@ func (a API) SendLocation(chatID int64, latitude, longitude float64, opts *Locat
vals.Set("chat_id", itoa(chatID))
vals.Set("latitude", ftoa(latitude))
vals.Set("longitude", ftoa(longitude))
return get[APIResponseMessage](a.base, "sendLocation", addValues(vals, opts))
return res, a.client.get(a.base, "sendLocation", addValues(vals, opts), &res)
}
// EditMessageLiveLocation is used to edit live location messages.
@@ -298,12 +309,12 @@ func (a API) EditMessageLiveLocation(msg MessageIDOptions, latitude, longitude f
vals.Set("latitude", ftoa(latitude))
vals.Set("longitude", ftoa(longitude))
return get[APIResponseMessage](a.base, "editMessageLiveLocation", addValues(addValues(vals, msg), opts))
return res, a.client.get(a.base, "editMessageLiveLocation", addValues(addValues(vals, msg), opts), &res)
}
// StopMessageLiveLocation is used to stop updating a live location message before `LivePeriod` expires.
func (a API) StopMessageLiveLocation(msg MessageIDOptions, opts *MessageReplyMarkup) (res APIResponseMessage, err error) {
return get[APIResponseMessage](a.base, "stopMessageLiveLocation", addValues(urlValues(msg), opts))
func (a API) StopMessageLiveLocation(msg MessageIDOptions, opts *StopLocationOptions) (res APIResponseMessage, err error) {
return res, a.client.get(a.base, "stopMessageLiveLocation", addValues(urlValues(msg), opts), &res)
}
// SendVenue is used to send information about a venue.
@@ -315,7 +326,7 @@ func (a API) SendVenue(chatID int64, latitude, longitude float64, title, address
vals.Set("longitude", ftoa(longitude))
vals.Set("title", title)
vals.Set("address", address)
return get[APIResponseMessage](a.base, "sendVenue", addValues(vals, opts))
return res, a.client.get(a.base, "sendVenue", addValues(vals, opts), &res)
}
// SendContact is used to send phone contacts.
@@ -325,11 +336,11 @@ func (a API) SendContact(phoneNumber, firstName string, chatID int64, opts *Cont
vals.Set("chat_id", itoa(chatID))
vals.Set("phone_number", phoneNumber)
vals.Set("first_name", firstName)
return get[APIResponseMessage](a.base, "sendContact", addValues(vals, opts))
return res, a.client.get(a.base, "sendContact", addValues(vals, opts), &res)
}
// SendPoll is used to send a native poll.
func (a API) SendPoll(chatID int64, question string, options []string, opts *PollOptions) (res APIResponseMessage, err error) {
func (a API) SendPoll(chatID int64, question string, options []InputPollOption, opts *PollOptions) (res APIResponseMessage, err error) {
var vals = make(url.Values)
pollOpts, err := json.Marshal(options)
@@ -340,7 +351,7 @@ func (a API) SendPoll(chatID int64, question string, options []string, opts *Pol
vals.Set("chat_id", itoa(chatID))
vals.Set("question", question)
vals.Set("options", string(pollOpts))
return get[APIResponseMessage](a.base, "sendPoll", addValues(vals, opts))
return res, a.client.get(a.base, "sendPoll", addValues(vals, opts), &res)
}
// SendDice is used to send an animated emoji that will display a random value.
@@ -349,7 +360,7 @@ func (a API) SendDice(chatID int64, emoji DiceEmoji, opts *BaseOptions) (res API
vals.Set("chat_id", itoa(chatID))
vals.Set("emoji", string(emoji))
return get[APIResponseMessage](a.base, "sendDice", addValues(vals, opts))
return res, a.client.get(a.base, "sendDice", addValues(vals, opts), &res)
}
// SendChatAction is used to tell the user that something is happening on the bot's side.
@@ -359,7 +370,7 @@ func (a API) SendChatAction(action ChatAction, chatID int64, opts *ChatActionOpt
vals.Set("chat_id", itoa(chatID))
vals.Set("action", string(action))
return get[APIResponseBool](a.base, "sendChatAction", addValues(vals, opts))
return res, a.client.get(a.base, "sendChatAction", addValues(vals, opts), &res)
}
// SetMessageReaction is used to change the chosen reactions on a message.
@@ -371,7 +382,7 @@ func (a API) SetMessageReaction(chatID int64, messageID int, opts *MessageReacti
vals.Set("chat_id", itoa(chatID))
vals.Set("message_id", itoa(int64(messageID)))
return get[APIResponseBool](a.base, "setMessageReaction", addValues(vals, opts))
return res, a.client.get(a.base, "setMessageReaction", addValues(vals, opts), &res)
}
// GetUserProfilePhotos is used to get a list of profile pictures for a user.
@@ -379,7 +390,7 @@ func (a API) GetUserProfilePhotos(userID int64, opts *UserProfileOptions) (res A
var vals = make(url.Values)
vals.Set("user_id", itoa(userID))
return get[APIResponseUserProfile](a.base, "getUserProfilePhotos", addValues(vals, opts))
return res, a.client.get(a.base, "getUserProfilePhotos", addValues(vals, opts), &res)
}
// GetFile returns the basic info about a file and prepares it for downloading.
@@ -391,14 +402,14 @@ func (a API) GetFile(fileID string) (res APIResponseFile, err error) {
var vals = make(url.Values)
vals.Set("file_id", fileID)
return get[APIResponseFile](a.base, "getFile", vals)
return res, a.client.get(a.base, "getFile", vals, &res)
}
// DownloadFile returns the bytes of the file corresponding to the given filePath.
// This function is callable for at least 1 hour since the call to GetFile.
// When the download expires a new one can be requested by calling GetFile again.
func (a API) DownloadFile(filePath string) ([]byte, error) {
return sendGetRequest(fmt.Sprintf(
return a.client.doGet(fmt.Sprintf(
"https://api.telegram.org/file/bot%s/%s",
a.token,
filePath,
@@ -414,7 +425,7 @@ func (a API) BanChatMember(chatID, userID int64, opts *BanOptions) (res APIRespo
vals.Set("chat_id", itoa(chatID))
vals.Set("user_id", itoa(userID))
return get[APIResponseBool](a.base, "banChatMember", addValues(vals, opts))
return res, a.client.get(a.base, "banChatMember", addValues(vals, opts), &res)
}
// UnbanChatMember is used to unban a previously banned user in a supergroup or channel.
@@ -428,7 +439,7 @@ func (a API) UnbanChatMember(chatID, userID int64, opts *UnbanOptions) (res APIR
vals.Set("chat_id", itoa(chatID))
vals.Set("user_id", itoa(userID))
return get[APIResponseBool](a.base, "unbanChatMember", addValues(vals, opts))
return res, a.client.get(a.base, "unbanChatMember", addValues(vals, opts), &res)
}
// RestrictChatMember is used to restrict a user in a supergroup.
@@ -436,15 +447,15 @@ func (a API) UnbanChatMember(chatID, userID int64, opts *UnbanOptions) (res APIR
func (a API) RestrictChatMember(chatID, userID int64, permissions ChatPermissions, opts *RestrictOptions) (res APIResponseBool, err error) {
var vals = make(url.Values)
perm, err := serializePerms(permissions)
perm, err := json.Marshal(permissions)
if err != nil {
return
}
vals.Set("chat_id", itoa(chatID))
vals.Set("user_id", itoa(userID))
vals.Set("permissions", perm)
return get[APIResponseBool](a.base, "restrictChatMember", addValues(vals, opts))
vals.Set("permissions", string(perm))
return res, a.client.get(a.base, "restrictChatMember", addValues(vals, opts), &res)
}
// PromoteChatMember is used to promote or demote a user in a supergroup or a channel.
@@ -454,7 +465,7 @@ func (a API) PromoteChatMember(chatID, userID int64, opts *PromoteOptions) (res
vals.Set("chat_id", itoa(chatID))
vals.Set("user_id", itoa(userID))
return get[APIResponseBool](a.base, "promoteChatMember", addValues(vals, opts))
return res, a.client.get(a.base, "promoteChatMember", addValues(vals, opts), &res)
}
// SetChatAdministratorCustomTitle is used to set a custom title for an administrator in a supergroup promoted by the bot.
@@ -464,7 +475,7 @@ func (a API) SetChatAdministratorCustomTitle(chatID, userID int64, customTitle s
vals.Set("chat_id", itoa(chatID))
vals.Set("user_id", itoa(userID))
vals.Set("custom_title", customTitle)
return get[APIResponseBool](a.base, "setChatAdministratorCustomTitle", vals)
return res, a.client.get(a.base, "setChatAdministratorCustomTitle", vals, &res)
}
// BanChatSenderChat is used to ban a channel chat in a supergroup or a channel.
@@ -475,7 +486,7 @@ func (a API) BanChatSenderChat(chatID, senderChatID int64) (res APIResponseBool,
vals.Set("chat_id", itoa(chatID))
vals.Set("sender_chat_id", itoa(senderChatID))
return get[APIResponseBool](a.base, "banChatSenderChat", vals)
return res, a.client.get(a.base, "banChatSenderChat", vals, &res)
}
// UnbanChatSenderChat is used to unban a previously channel chat in a supergroup or channel.
@@ -485,7 +496,7 @@ func (a API) UnbanChatSenderChat(chatID, senderChatID int64) (res APIResponseBoo
vals.Set("chat_id", itoa(chatID))
vals.Set("sender_chat_id", itoa(senderChatID))
return get[APIResponseBool](a.base, "unbanChatSenderChat", vals)
return res, a.client.get(a.base, "unbanChatSenderChat", vals, &res)
}
// SetChatPermissions is used to set default chat permissions for all members.
@@ -493,14 +504,14 @@ func (a API) UnbanChatSenderChat(chatID, senderChatID int64) (res APIResponseBoo
func (a API) SetChatPermissions(chatID int64, permissions ChatPermissions, opts *ChatPermissionsOptions) (res APIResponseBool, err error) {
var vals = make(url.Values)
perm, err := serializePerms(permissions)
perm, err := json.Marshal(permissions)
if err != nil {
return
}
vals.Set("chat_id", itoa(chatID))
vals.Set("permissions", perm)
return get[APIResponseBool](a.base, "setChatPermissions", addValues(vals, opts))
vals.Set("permissions", string(perm))
return res, a.client.get(a.base, "setChatPermissions", addValues(vals, opts), &res)
}
// ExportChatInviteLink is used to generate a new primary invite link for a chat;
@@ -510,7 +521,7 @@ func (a API) ExportChatInviteLink(chatID int64) (res APIResponseString, err erro
var vals = make(url.Values)
vals.Set("chat_id", itoa(chatID))
return get[APIResponseString](a.base, "exportChatInviteLink", vals)
return res, a.client.get(a.base, "exportChatInviteLink", vals, &res)
}
// CreateChatInviteLink is used to create an additional invite link for a chat.
@@ -520,7 +531,7 @@ func (a API) CreateChatInviteLink(chatID int64, opts *InviteLinkOptions) (res AP
var vals = make(url.Values)
vals.Set("chat_id", itoa(chatID))
return get[APIResponseInviteLink](a.base, "createChatInviteLink", addValues(vals, opts))
return res, a.client.get(a.base, "createChatInviteLink", addValues(vals, opts), &res)
}
// EditChatInviteLink is used to edit a non-primary invite link created by the bot.
@@ -530,7 +541,7 @@ func (a API) EditChatInviteLink(chatID int64, inviteLink string, opts *InviteLin
vals.Set("chat_id", itoa(chatID))
vals.Set("invite_link", inviteLink)
return get[APIResponseInviteLink](a.base, "editChatInviteLink", addValues(vals, opts))
return res, a.client.get(a.base, "editChatInviteLink", addValues(vals, opts), &res)
}
// RevokeChatInviteLink is used to revoke an invite link created by the bot.
@@ -541,7 +552,7 @@ func (a API) RevokeChatInviteLink(chatID int64, inviteLink string) (res APIRespo
vals.Set("chat_id", itoa(chatID))
vals.Set("invite_link", inviteLink)
return get[APIResponseInviteLink](a.base, "editChatInviteLink", vals)
return res, a.client.get(a.base, "editChatInviteLink", vals, &res)
}
// ApproveChatJoinRequest is used to approve a chat join request.
@@ -551,7 +562,7 @@ func (a API) ApproveChatJoinRequest(chatID, userID int64) (res APIResponseBool,
vals.Set("chat_id", itoa(chatID))
vals.Set("user_id", itoa(userID))
return get[APIResponseBool](a.base, "approveChatJoinRequest", vals)
return res, a.client.get(a.base, "approveChatJoinRequest", vals, &res)
}
// DeclineChatJoinRequest is used to decline a chat join request.
@@ -561,7 +572,7 @@ func (a API) DeclineChatJoinRequest(chatID, userID int64) (res APIResponseBool,
vals.Set("chat_id", itoa(chatID))
vals.Set("user_id", itoa(userID))
return get[APIResponseBool](a.base, "declineChatJoinRequest", vals)
return res, a.client.get(a.base, "declineChatJoinRequest", vals, &res)
}
// SetChatPhoto is used to set a new profile photo for the chat.
@@ -571,7 +582,7 @@ func (a API) SetChatPhoto(file InputFile, chatID int64) (res APIResponseBool, er
var vals = make(url.Values)
vals.Set("chat_id", itoa(chatID))
return postFile[APIResponseBool](a.base, "setChatPhoto", "photo", file, InputFile{}, vals)
return res, a.client.postFile(a.base, "setChatPhoto", "photo", file, InputFile{}, vals, &res)
}
// DeleteChatPhoto is used to delete a chat photo.
@@ -581,7 +592,7 @@ func (a API) DeleteChatPhoto(chatID int64) (res APIResponseBool, err error) {
var vals = make(url.Values)
vals.Set("chat_id", itoa(chatID))
return get[APIResponseBool](a.base, "deleteChatPhoto", vals)
return res, a.client.get(a.base, "deleteChatPhoto", vals, &res)
}
// SetChatTitle is used to change the title of a chat.
@@ -592,7 +603,7 @@ func (a API) SetChatTitle(chatID int64, title string) (res APIResponseBool, err
vals.Set("chat_id", itoa(chatID))
vals.Set("title", title)
return get[APIResponseBool](a.base, "setChatTitle", vals)
return res, a.client.get(a.base, "setChatTitle", vals, &res)
}
// SetChatDescription is used to change the description of a group, a supergroup or a channel.
@@ -602,7 +613,7 @@ func (a API) SetChatDescription(chatID int64, description string) (res APIRespon
vals.Set("chat_id", itoa(chatID))
vals.Set("description", description)
return get[APIResponseBool](a.base, "setChatDescription", vals)
return res, a.client.get(a.base, "setChatDescription", vals, &res)
}
// PinChatMessage is used to add a message to the list of pinned messages in the chat.
@@ -613,7 +624,7 @@ func (a API) PinChatMessage(chatID int64, messageID int, opts *PinMessageOptions
vals.Set("chat_id", itoa(chatID))
vals.Set("message_id", itoa(int64(messageID)))
return get[APIResponseBool](a.base, "pinChatMessage", addValues(vals, opts))
return res, a.client.get(a.base, "pinChatMessage", addValues(vals, opts), &res)
}
// UnpinChatMessage is used to remove a message from the list of pinned messages in the chat.
@@ -624,7 +635,7 @@ func (a API) UnpinChatMessage(chatID int64, messageID int) (res APIResponseBool,
vals.Set("chat_id", itoa(chatID))
vals.Set("message_id", itoa(int64(messageID)))
return get[APIResponseBool](a.base, "unpinChatMessage", vals)
return res, a.client.get(a.base, "unpinChatMessage", vals, &res)
}
// UnpinAllChatMessages is used to clear the list of pinned messages in a chat.
@@ -634,7 +645,7 @@ func (a API) UnpinAllChatMessages(chatID int64) (res APIResponseBool, err error)
var vals = make(url.Values)
vals.Set("chat_id", itoa(chatID))
return get[APIResponseBool](a.base, "unpinAllChatMessages", vals)
return res, a.client.get(a.base, "unpinAllChatMessages", vals, &res)
}
// LeaveChat is used to make the bot leave a group, supergroup or channel.
@@ -642,7 +653,7 @@ func (a API) LeaveChat(chatID int64) (res APIResponseBool, err error) {
var vals = make(url.Values)
vals.Set("chat_id", itoa(chatID))
return get[APIResponseBool](a.base, "leaveChat", vals)
return res, a.client.get(a.base, "leaveChat", vals, &res)
}
// GetChat is used to get up to date information about the chat.
@@ -651,7 +662,7 @@ func (a API) GetChat(chatID int64) (res APIResponseChat, err error) {
var vals = make(url.Values)
vals.Set("chat_id", itoa(chatID))
return get[APIResponseChat](a.base, "getChat", vals)
return res, a.client.get(a.base, "getChat", vals, &res)
}
// GetChatAdministrators is used to get a list of administrators in a chat.
@@ -659,7 +670,7 @@ func (a API) GetChatAdministrators(chatID int64) (res APIResponseAdministrators,
var vals = make(url.Values)
vals.Set("chat_id", itoa(chatID))
return get[APIResponseAdministrators](a.base, "getChatAdministrators", vals)
return res, a.client.get(a.base, "getChatAdministrators", vals, &res)
}
// GetChatMemberCount is used to get the number of members in a chat.
@@ -667,7 +678,7 @@ func (a API) GetChatMemberCount(chatID int64) (res APIResponseInteger, err error
var vals = make(url.Values)
vals.Set("chat_id", itoa(chatID))
return get[APIResponseInteger](a.base, "getChatMemberCount", vals)
return res, a.client.get(a.base, "getChatMemberCount", vals, &res)
}
// GetChatMember is used to get information about a member of a chat.
@@ -676,7 +687,7 @@ func (a API) GetChatMember(chatID, userID int64) (res APIResponseChatMember, err
vals.Set("chat_id", itoa(chatID))
vals.Set("user_id", itoa(userID))
return get[APIResponseChatMember](a.base, "getChatMember", vals)
return res, a.client.get(a.base, "getChatMember", vals, &res)
}
// SetChatStickerSet is used to set a new group sticker set for a supergroup.
@@ -687,7 +698,7 @@ func (a API) SetChatStickerSet(chatID int64, stickerSetName string) (res APIResp
vals.Set("chat_id", itoa(chatID))
vals.Set("sticker_set_name", stickerSetName)
return get[APIResponseBool](a.base, "setChatStickerSet", vals)
return res, a.client.get(a.base, "setChatStickerSet", vals, &res)
}
// DeleteChatStickerSet is used to delete a group sticker set for a supergroup.
@@ -697,7 +708,7 @@ func (a API) DeleteChatStickerSet(chatID int64) (res APIResponseBool, err error)
var vals = make(url.Values)
vals.Set("chat_id", itoa(chatID))
return get[APIResponseBool](a.base, "deleteChatStickerSet", vals)
return res, a.client.get(a.base, "deleteChatStickerSet", vals, &res)
}
// CreateForumTopic is used to create a topic in a forum supergroup chat.
@@ -707,7 +718,7 @@ func (a API) CreateForumTopic(chatID int64, name string, opts *CreateTopicOption
vals.Set("chat_id", itoa(chatID))
vals.Set("name", name)
return get[APIResponseForumTopic](a.base, "createForumTopic", addValues(vals, opts))
return res, a.client.get(a.base, "createForumTopic", addValues(vals, opts), &res)
}
// EditForumTopic is used to edit name and icon of a topic in a forum supergroup chat.
@@ -717,7 +728,7 @@ func (a API) EditForumTopic(chatID, messageThreadID int64, opts *EditTopicOption
vals.Set("chat_id", itoa(chatID))
vals.Set("message_thread_id", itoa(messageThreadID))
return get[APIResponseBool](a.base, "editForumTopic", addValues(vals, opts))
return res, a.client.get(a.base, "editForumTopic", addValues(vals, opts), &res)
}
// CloseForumTopic is used to close an open topic in a forum supergroup chat.
@@ -727,7 +738,7 @@ func (a API) CloseForumTopic(chatID, messageThreadID int64) (res APIResponseBool
vals.Set("chat_id", itoa(chatID))
vals.Set("message_thread_id", itoa(messageThreadID))
return get[APIResponseBool](a.base, "closeForumTopic", vals)
return res, a.client.get(a.base, "closeForumTopic", vals, &res)
}
// ReopenForumTopic is used to reopen a closed topic in a forum supergroup chat.
@@ -737,7 +748,7 @@ func (a API) ReopenForumTopic(chatID, messageThreadID int64) (res APIResponseBoo
vals.Set("chat_id", itoa(chatID))
vals.Set("message_thread_id", itoa(messageThreadID))
return get[APIResponseBool](a.base, "reopenForumTopic", vals)
return res, a.client.get(a.base, "reopenForumTopic", vals, &res)
}
// DeleteForumTopic is used to delete a forum topic along with all its messages in a forum supergroup chat.
@@ -747,7 +758,7 @@ func (a API) DeleteForumTopic(chatID, messageThreadID int64) (res APIResponseBoo
vals.Set("chat_id", itoa(chatID))
vals.Set("message_thread_id", itoa(messageThreadID))
return get[APIResponseBool](a.base, "deleteForumTopic", vals)
return res, a.client.get(a.base, "deleteForumTopic", vals, &res)
}
// UnpinAllForumTopicMessages is used to clear the list of pinned messages in a forum topic.
@@ -757,7 +768,7 @@ func (a API) UnpinAllForumTopicMessages(chatID, messageThreadID int64) (res APIR
vals.Set("chat_id", itoa(chatID))
vals.Set("message_thread_id", itoa(messageThreadID))
return get[APIResponseBool](a.base, "unpinAllForumTopicMessages", vals)
return res, a.client.get(a.base, "unpinAllForumTopicMessages", vals, &res)
}
// EditGeneralForumTopic is used to edit the name of the 'General' topic in a forum supergroup chat.
@@ -767,7 +778,7 @@ func (a API) EditGeneralForumTopic(chatID int64, name string) (res APIResponseBo
vals.Set("chat_id", itoa(chatID))
vals.Set("name", name)
return get[APIResponseBool](a.base, "editGeneralForumTopic", vals)
return res, a.client.get(a.base, "editGeneralForumTopic", vals, &res)
}
// CloseGeneralForumTopic is used to close an open 'General' topic in a forum supergroup chat.
@@ -776,7 +787,7 @@ func (a API) CloseGeneralForumTopic(chatID int64) (res APIResponseBool, err erro
var vals = make(url.Values)
vals.Set("chat_id", itoa(chatID))
return get[APIResponseBool](a.base, "closeGeneralForumTopic", vals)
return res, a.client.get(a.base, "closeGeneralForumTopic", vals, &res)
}
// ReopenGeneralForumTopic is used to reopen a closed 'General' topic in a forum supergroup chat.
@@ -786,7 +797,7 @@ func (a API) ReopenGeneralForumTopic(chatID int64) (res APIResponseBool, err err
var vals = make(url.Values)
vals.Set("chat_id", itoa(chatID))
return get[APIResponseBool](a.base, "reopenGeneralForumTopic", vals)
return res, a.client.get(a.base, "reopenGeneralForumTopic", vals, &res)
}
// HideGeneralForumTopic is used to hide the 'General' topic in a forum supergroup chat.
@@ -796,7 +807,7 @@ func (a API) HideGeneralForumTopic(chatID int64) (res APIResponseBool, err error
var vals = make(url.Values)
vals.Set("chat_id", itoa(chatID))
return get[APIResponseBool](a.base, "hideGeneralForumTopic", vals)
return res, a.client.get(a.base, "hideGeneralForumTopic", vals, &res)
}
// UnhideGeneralForumTopic is used to unhide the 'General' topic in a forum supergroup chat.
@@ -805,7 +816,7 @@ func (a API) UnhideGeneralForumTopic(chatID int64) (res APIResponseBool, err err
var vals = make(url.Values)
vals.Set("chat_id", itoa(chatID))
return get[APIResponseBool](a.base, "unhideGeneralForumTopic", vals)
return res, a.client.get(a.base, "unhideGeneralForumTopic", vals, &res)
}
// UnpinAllGeneralForumTopicMessages is used to clear the list of pinned messages in a General forum topic.
@@ -814,7 +825,7 @@ func (a API) UnpinAllGeneralForumTopicMessages(chatID int64) (res APIResponseBoo
var vals = make(url.Values)
vals.Set("chat_id", itoa(chatID))
return get[APIResponseBool](a.base, "unpinAllGeneralForumTopicMessages", vals)
return res, a.client.get(a.base, "unpinAllGeneralForumTopicMessages", vals, &res)
}
// AnswerCallbackQuery is used to send answers to callback queries sent from inline keyboards.
@@ -823,7 +834,7 @@ func (a API) AnswerCallbackQuery(callbackID string, opts *CallbackQueryOptions)
var vals = make(url.Values)
vals.Set("callback_query_id", callbackID)
return get[APIResponseBool](a.base, "answerCallbackQuery", addValues(vals, opts))
return res, a.client.get(a.base, "answerCallbackQuery", addValues(vals, opts), &res)
}
// GetUserChatBoosts is used to get the list of boosts added to a chat by a user.
@@ -833,7 +844,15 @@ func (a API) GetUserChatBoosts(chatID, userID int64) (res APIResponseUserChatBoo
vals.Set("chat_id", itoa(chatID))
vals.Set("user_id", itoa(userID))
return get[APIResponseUserChatBoosts](a.base, "getUserChatBoosts", vals)
return res, a.client.get(a.base, "getUserChatBoosts", vals, &res)
}
// GetBusinessConnection is used to get information about the connection of the bot with a business account.
func (a API) GetBusinessConnection(business_connection_id string) (res APIResponseBusinessConnection, err error) {
var vals = make(url.Values)
vals.Set("business_connection_id", business_connection_id)
return res, a.client.get(a.base, "getBusinessConnection", vals, &res)
}
// SetMyCommands is used to change the list of the bot's commands for the given scope and user language.
@@ -842,17 +861,17 @@ func (a API) SetMyCommands(opts *CommandOptions, commands ...BotCommand) (res AP
jsn, _ := json.Marshal(commands)
vals.Set("commands", string(jsn))
return get[APIResponseBool](a.base, "setMyCommands", addValues(vals, opts))
return res, a.client.get(a.base, "setMyCommands", addValues(vals, opts), &res)
}
// DeleteMyCommands is used to delete the list of the bot's commands for the given scope and user language.
func (a API) DeleteMyCommands(opts *CommandOptions) (res APIResponseBool, err error) {
return get[APIResponseBool](a.base, "deleteMyCommands", urlValues(opts))
return res, a.client.get(a.base, "deleteMyCommands", urlValues(opts), &res)
}
// GetMyCommands is used to get the current list of the bot's commands for the given scope and user language.
func (a API) GetMyCommands(opts *CommandOptions) (res APIResponseCommands, err error) {
return get[APIResponseCommands](a.base, "getMyCommands", urlValues(opts))
return res, a.client.get(a.base, "getMyCommands", urlValues(opts), &res)
}
// SetMyName is used to change the bot's name.
@@ -861,7 +880,7 @@ func (a API) SetMyName(name, languageCode string) (res APIResponseBool, err erro
vals.Set("name", name)
vals.Set("language_code", languageCode)
return get[APIResponseBool](a.base, "setMyName", vals)
return res, a.client.get(a.base, "setMyName", vals, &res)
}
// GetMyName is used to get the current bot name for the given user language.
@@ -869,7 +888,7 @@ func (a API) GetMyName(languageCode string) (res APIResponseBotName, err error)
var vals = make(url.Values)
vals.Set("language_code", languageCode)
return get[APIResponseBotName](a.base, "getMyName", vals)
return res, a.client.get(a.base, "getMyName", vals, &res)
}
// SetMyDescription is used to to change the bot's description, which is shown in the chat with the bot if the chat is empty.
@@ -878,7 +897,7 @@ func (a API) SetMyDescription(description, languageCode string) (res APIResponse
vals.Set("description", description)
vals.Set("language_code", languageCode)
return get[APIResponseBool](a.base, "setMyDescription", vals)
return res, a.client.get(a.base, "setMyDescription", vals, &res)
}
// GetMyDescription is used to get the current bot description for the given user language.
@@ -886,7 +905,7 @@ func (a API) GetMyDescription(languageCode string) (res APIResponseBotDescriptio
var vals = make(url.Values)
vals.Set("language_code", languageCode)
return get[APIResponseBotDescription](a.base, "getMyDescription", vals)
return res, a.client.get(a.base, "getMyDescription", vals, &res)
}
// SetMyShortDescription is used to to change the bot's short description,
@@ -896,7 +915,7 @@ func (a API) SetMyShortDescription(shortDescription, languageCode string) (res A
vals.Set("short_description", shortDescription)
vals.Set("language_code", languageCode)
return get[APIResponseBool](a.base, "setMyShortDescription", vals)
return res, a.client.get(a.base, "setMyShortDescription", vals, &res)
}
// GetMyShortDescription is used to get the current bot short description for the given user language.
@@ -904,7 +923,7 @@ func (a API) GetMyShortDescription(languageCode string) (res APIResponseBotShort
var vals = make(url.Values)
vals.Set("language_code", languageCode)
return get[APIResponseBotShortDescription](a.base, "getMyDescription", vals)
return res, a.client.get(a.base, "getMyDescription", vals, &res)
}
// EditMessageText is used to edit text and game messages.
@@ -912,12 +931,12 @@ func (a API) EditMessageText(text string, msg MessageIDOptions, opts *MessageTex
var vals = make(url.Values)
vals.Set("text", text)
return get[APIResponseMessage](a.base, "editMessageText", addValues(addValues(vals, msg), opts))
return res, a.client.get(a.base, "editMessageText", addValues(addValues(vals, msg), opts), &res)
}
// EditMessageCaption is used to edit captions of messages.
func (a API) EditMessageCaption(msg MessageIDOptions, opts *MessageCaptionOptions) (res APIResponseMessage, err error) {
return get[APIResponseMessage](a.base, "editMessageCaption", addValues(urlValues(msg), opts))
return res, a.client.get(a.base, "editMessageCaption", addValues(urlValues(msg), opts), &res)
}
// EditMessageMedia is used to edit animation, audio, document, photo or video messages.
@@ -925,22 +944,22 @@ func (a API) EditMessageCaption(msg MessageIDOptions, opts *MessageCaptionOption
// only to a document for document albums and to a photo or a video otherwise.
// When an inline message is edited, a new file can't be uploaded.
// Use a previously uploaded file via its file_id or specify a URL.
func (a API) EditMessageMedia(msg MessageIDOptions, media InputMedia, opts *MessageReplyMarkup) (res APIResponseMessage, err error) {
return postMedia[APIResponseMessage](a.base, "editMessageMedia", true, addValues(urlValues(msg), opts), media)
func (a API) EditMessageMedia(msg MessageIDOptions, media InputMedia, opts *MessageMediaOptions) (res APIResponseMessage, err error) {
return res, a.client.postMedia(a.base, "editMessageMedia", true, addValues(urlValues(msg), opts), &res, media)
}
// EditMessageReplyMarkup is used to edit only the reply markup of messages.
func (a API) EditMessageReplyMarkup(msg MessageIDOptions, opts *MessageReplyMarkup) (res APIResponseMessage, err error) {
return get[APIResponseMessage](a.base, "editMessageReplyMarkup", addValues(urlValues(msg), opts))
func (a API) EditMessageReplyMarkup(msg MessageIDOptions, opts *MessageReplyMarkupOptions) (res APIResponseMessage, err error) {
return res, a.client.get(a.base, "editMessageReplyMarkup", addValues(urlValues(msg), opts), &res)
}
// StopPoll is used to stop a poll which was sent by the bot.
func (a API) StopPoll(chatID int64, messageID int, opts *MessageReplyMarkup) (res APIResponsePoll, err error) {
func (a API) StopPoll(chatID int64, messageID int, opts *StopPollOptions) (res APIResponsePoll, err error) {
var vals = make(url.Values)
vals.Set("chat_id", itoa(chatID))
vals.Set("message_id", itoa(int64(messageID)))
return get[APIResponsePoll](a.base, "stopPoll", addValues(vals, opts))
return res, a.client.get(a.base, "stopPoll", addValues(vals, opts), &res)
}
// DeleteMessage is used to delete a message, including service messages, with the following limitations:
@@ -956,7 +975,7 @@ func (a API) DeleteMessage(chatID int64, messageID int) (res APIResponseBase, er
vals.Set("chat_id", itoa(chatID))
vals.Set("message_id", itoa(int64(messageID)))
return get[APIResponseBase](a.base, "deleteMessage", vals)
return res, a.client.get(a.base, "deleteMessage", vals, &res)
}
// DeleteMessages is used to delete multiple messages simultaneously.
@@ -971,5 +990,5 @@ func (a API) DeleteMessages(chatID int64, messageIDs []int) (res APIResponseBool
vals.Set("chat_id", itoa(chatID))
vals.Set("message_ids", string(msgIDs))
return get[APIResponseBool](a.base, "deleteMessages", vals)
return res, a.client.get(a.base, "deleteMessages", vals, &res)
}

View File

@@ -54,11 +54,11 @@ type GetMyDefaultAdministratorRightsOptions struct {
// requested by the bot when it's added as an administrator to groups or channels.
// These rights will be suggested to users, but they are are free to modify the list
// before adding the bot.
func (a API) SetMyDefaultAdministratorRights(opts SetMyDefaultAdministratorRightsOptions) (res APIResponseBool, err error) {
return get[APIResponseBool](a.base, "setMyDefaultAdministratorRights", urlValues(opts))
func (a API) SetMyDefaultAdministratorRights(opts *SetMyDefaultAdministratorRightsOptions) (res APIResponseBool, err error) {
return res, a.client.get(a.base, "setMyDefaultAdministratorRights", urlValues(opts), &res)
}
// GetMyDefaultAdministratorRights is used to get the current default administrator rights of the bot.
func (a API) GetMyDefaultAdministratorRights(opts GetMyDefaultAdministratorRightsOptions) (res APIResponseChatAdministratorRights, err error) {
return get[APIResponseChatAdministratorRights](a.base, "getMyDefaultAdministratorRights", urlValues(opts))
func (a API) GetMyDefaultAdministratorRights(opts *GetMyDefaultAdministratorRightsOptions) (res APIResponseChatAdministratorRights, err error) {
return res, a.client.get(a.base, "getMyDefaultAdministratorRights", urlValues(opts), &res)
}

View File

@@ -52,7 +52,7 @@ func (a API) SendGame(gameShortName string, chatID int64, opts *BaseOptions) (re
vals.Set("chat_id", itoa(chatID))
vals.Set("game_short_name", gameShortName)
return get[APIResponseMessage](a.base, "sendGame", addValues(vals, opts))
return res, a.client.get(a.base, "sendGame", addValues(vals, opts), &res)
}
// SetGameScore is used to set the score of the specified user in a game.
@@ -61,7 +61,7 @@ func (a API) SetGameScore(userID int64, score int, msgID MessageIDOptions, opts
vals.Set("user_id", itoa(userID))
vals.Set("score", itoa(int64(score)))
return get[APIResponseMessage](a.base, "setGameScore", addValues(addValues(vals, msgID), opts))
return res, a.client.get(a.base, "setGameScore", addValues(addValues(vals, msgID), opts), &res)
}
// GetGameHighScores is used to get data for high score tables.
@@ -69,5 +69,5 @@ func (a API) GetGameHighScores(userID int64, opts MessageIDOptions) (res APIResp
var vals = make(url.Values)
vals.Set("user_id", itoa(userID))
return get[APIResponseGameHighScore](a.base, "getGameHighScores", addValues(vals, opts))
return res, a.client.get(a.base, "getGameHighScores", addValues(vals, opts), &res)
}

View File

@@ -19,7 +19,6 @@
package echotron
import (
"encoding/json"
"fmt"
"net/url"
"os"
@@ -27,6 +26,13 @@ import (
"strconv"
)
// content contains a file's name, its type and its data.
type content struct {
fname string
ftype string
fdata []byte
}
func check(r APIResponse) error {
if b := r.Base(); !b.Ok {
return &APIError{code: b.ErrorCode, desc: b.Description}
@@ -109,123 +115,6 @@ func readFile(im InputFile) (content []byte, path string, err error) {
return
}
func sendFile(file, thumbnail InputFile, url, fileType string) (res []byte, err error) {
var cnt []content
if file.id != "" {
url = fmt.Sprintf("%s&%s=%s", url, fileType, file.id)
} else if file.url != "" {
url = fmt.Sprintf("%s&%s=%s", url, fileType, file.url)
} else if c, e := toContent(fileType, file); e == nil {
cnt = append(cnt, c)
} else {
err = e
}
if c, e := toContent("thumbnail", thumbnail); e == nil {
cnt = append(cnt, c)
} else {
err = e
}
if len(cnt) > 0 {
res, err = sendPostRequest(url, cnt...)
} else {
res, err = sendGetRequest(url)
}
return
}
func sendMediaFiles(url string, editSingle bool, files ...InputMedia) (res []byte, err error) {
var (
med []mediaEnvelope
cnt []content
jsn []byte
)
for _, file := range files {
var im mediaEnvelope
var cntArr []content
media := file.media()
thumbnail := file.thumbnail()
im, cntArr, err = processMedia(media, thumbnail)
if err != nil {
return
}
im.InputMedia = file
med = append(med, im)
cnt = append(cnt, cntArr...)
}
if editSingle {
jsn, err = json.Marshal(med[0])
} else {
jsn, err = json.Marshal(med)
}
if err != nil {
return
}
url = fmt.Sprintf("%s&media=%s", url, jsn)
if len(cnt) > 0 {
return sendPostRequest(url, cnt...)
}
return sendGetRequest(url)
}
func sendStickers(url string, stickers ...InputSticker) (res []byte, err error) {
var (
sti []stickerEnvelope
cnt []content
jsn []byte
)
for _, s := range stickers {
var se stickerEnvelope
var cntArr []content
se, cntArr, err = processSticker(s.Sticker)
if err != nil {
return
}
se.InputSticker = s
sti = append(sti, se)
cnt = append(cnt, cntArr...)
}
if len(sti) == 1 {
jsn, _ = json.Marshal(sti[0])
url = fmt.Sprintf("%s&sticker=%s", url, jsn)
} else {
jsn, _ = json.Marshal(sti)
url = fmt.Sprintf("%s&stickers=%s", url, jsn)
}
if len(cnt) > 0 {
return sendPostRequest(url, cnt...)
}
return sendGetRequest(url)
}
func serializePerms(permissions ChatPermissions) (string, error) {
perm, err := json.Marshal(permissions)
if err != nil {
return "", err
}
return string(perm), nil
}
func toContent(ftype string, f InputFile) (content, error) {
if f.path != "" && len(f.content) == 0 {
var err error
@@ -247,87 +136,6 @@ func toInputMedia(media []GroupableInputMedia) (ret []InputMedia) {
return ret
}
func get[T APIResponse](base, endpoint string, vals url.Values) (res T, err error) {
url, err := url.JoinPath(base, endpoint)
if err != nil {
return res, err
}
if vals != nil {
if queries := vals.Encode(); queries != "" {
url = fmt.Sprintf("%s?%s", url, queries)
}
}
cnt, err := sendGetRequest(url)
if err != nil {
return res, err
}
if err = json.Unmarshal(cnt, &res); err != nil {
return
}
err = check(res)
return
}
func postFile[T APIResponse](base, endpoint, fileType string, file, thumbnail InputFile, vals url.Values) (res T, err error) {
url, err := joinURL(base, endpoint, vals)
if err != nil {
return res, err
}
cnt, err := sendFile(file, thumbnail, url, fileType)
if err != nil {
return res, err
}
if err = json.Unmarshal(cnt, &res); err != nil {
return
}
err = check(res)
return
}
func postMedia[T APIResponse](base, endpoint string, editSingle bool, vals url.Values, files ...InputMedia) (res T, err error) {
url, err := joinURL(base, endpoint, vals)
if err != nil {
return res, err
}
cnt, err := sendMediaFiles(url, editSingle, files...)
if err != nil {
return res, err
}
if err = json.Unmarshal(cnt, &res); err != nil {
return
}
err = check(res)
return
}
func postStickers[T APIResponse](base, endpoint string, vals url.Values, stickers ...InputSticker) (res T, err error) {
url, err := joinURL(base, endpoint, vals)
if err != nil {
return res, err
}
cnt, err := sendStickers(url, stickers...)
if err != nil {
return res, err
}
if err = json.Unmarshal(cnt, &res); err != nil {
return
}
err = check(res)
return
}
func joinURL(base, endpoint string, vals url.Values) (addr string, err error) {
addr, err = url.JoinPath(base, endpoint)
if err != nil {

View File

@@ -90,19 +90,20 @@ func (i InlineQueryResultArticle) ImplementsInlineQueryResult() {}
// By default, this photo will be sent by the user with optional caption.
// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the photo.
type InlineQueryResultPhoto struct {
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
Title string `json:"title,omitempty"`
ThumbnailURL string `json:"thumbnail_url"`
PhotoURL string `json:"photo_url"`
ParseMode string `json:"parse_mode,omitempty"`
ID string `json:"id"`
Description string `json:"description,omitempty"`
Caption string `json:"caption,omitempty"`
Type InlineQueryType `json:"type"`
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
PhotoHeight int `json:"photo_height,omitempty"`
PhotoWidth int `json:"photo_width,omitempty"`
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
Title string `json:"title,omitempty"`
ThumbnailURL string `json:"thumbnail_url"`
PhotoURL string `json:"photo_url"`
ParseMode string `json:"parse_mode,omitempty"`
ID string `json:"id"`
Description string `json:"description,omitempty"`
Caption string `json:"caption,omitempty"`
Type InlineQueryType `json:"type"`
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
PhotoHeight int `json:"photo_height,omitempty"`
PhotoWidth int `json:"photo_width,omitempty"`
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
}
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
@@ -112,20 +113,21 @@ func (i InlineQueryResultPhoto) ImplementsInlineQueryResult() {}
// By default, this animated GIF file will be sent by the user with optional caption.
// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the animation.
type InlineQueryResultGif struct {
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
Title string `json:"title,omitempty"`
GifURL string `json:"gif_url"`
ParseMode string `json:"parse_mode,omitempty"`
Caption string `json:"caption,omitempty"`
ThumbnailURL string `json:"thumbnail_url"`
ID string `json:"id"`
ThumbnailMimeType string `json:"thumbnail_mime_type,omitempty"`
Type InlineQueryType `json:"type"`
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
GifDuration int `json:"gif_duration,omitempty"`
GifHeight int `json:"gif_height,omitempty"`
GifWidth int `json:"gif_width,omitempty"`
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
Title string `json:"title,omitempty"`
GifURL string `json:"gif_url"`
ParseMode string `json:"parse_mode,omitempty"`
Caption string `json:"caption,omitempty"`
ThumbnailURL string `json:"thumbnail_url"`
ID string `json:"id"`
ThumbnailMimeType string `json:"thumbnail_mime_type,omitempty"`
Type InlineQueryType `json:"type"`
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
GifDuration int `json:"gif_duration,omitempty"`
GifHeight int `json:"gif_height,omitempty"`
GifWidth int `json:"gif_width,omitempty"`
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
}
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
@@ -135,20 +137,21 @@ func (i InlineQueryResultGif) ImplementsInlineQueryResult() {}
// By default, this animated MPEG-4 file will be sent by the user with optional caption.
// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the animation.
type InlineQueryResultMpeg4Gif struct {
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
Title string `json:"title,omitempty"`
Mpeg4URL string `json:"mpeg4_url"`
ParseMode string `json:"parse_mode,omitempty"`
Caption string `json:"caption,omitempty"`
ThumbnailURL string `json:"thumbnail_url"`
ID string `json:"id"`
ThumbnailMimeType string `json:"thumbnail_mime_type,omitempty"`
Type InlineQueryType `json:"type"`
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
Mpeg4Duration int `json:"mpeg4_duration,omitempty"`
Mpeg4Height int `json:"mpeg4_height,omitempty"`
Mpeg4Width int `json:"mpeg4_width,omitempty"`
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
Title string `json:"title,omitempty"`
Mpeg4URL string `json:"mpeg4_url"`
ParseMode string `json:"parse_mode,omitempty"`
Caption string `json:"caption,omitempty"`
ThumbnailURL string `json:"thumbnail_url"`
ID string `json:"id"`
ThumbnailMimeType string `json:"thumbnail_mime_type,omitempty"`
Type InlineQueryType `json:"type"`
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
Mpeg4Duration int `json:"mpeg4_duration,omitempty"`
Mpeg4Height int `json:"mpeg4_height,omitempty"`
Mpeg4Width int `json:"mpeg4_width,omitempty"`
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
}
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
@@ -158,21 +161,22 @@ func (i InlineQueryResultMpeg4Gif) ImplementsInlineQueryResult() {}
// By default, this video file will be sent by the user with an optional caption.
// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the video.
type InlineQueryResultVideo struct {
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
Description string `json:"description,omitempty"`
MimeType string `json:"mime_type"`
ThumbnailURL string `json:"thumbnail_url"`
Title string `json:"title"`
Caption string `json:"caption,omitempty"`
ID string `json:"id"`
VideoURL string `json:"video_url"`
ParseMode string `json:"parse_mode,omitempty"`
Type InlineQueryType `json:"type"`
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
VideoHeight int `json:"video_height,omitempty"`
VideoDuration int `json:"video_duration,omitempty"`
VideoWidth int `json:"video_width,omitempty"`
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
Description string `json:"description,omitempty"`
MimeType string `json:"mime_type"`
ThumbnailURL string `json:"thumbnail_url"`
Title string `json:"title"`
Caption string `json:"caption,omitempty"`
ID string `json:"id"`
VideoURL string `json:"video_url"`
ParseMode string `json:"parse_mode,omitempty"`
Type InlineQueryType `json:"type"`
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
VideoHeight int `json:"video_height,omitempty"`
VideoDuration int `json:"video_duration,omitempty"`
VideoWidth int `json:"video_width,omitempty"`
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
}
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
@@ -323,16 +327,17 @@ func (i InlineQueryResultGame) ImplementsInlineQueryResult() {}
// By default, this photo will be sent by the user with an optional caption.
// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the photo.
type InlineQueryResultCachedPhoto struct {
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
Type InlineQueryType `json:"type"`
ID string `json:"id"`
Description string `json:"description,omitempty"`
Caption string `json:"caption,omitempty"`
ParseMode string `json:"parse_mode,omitempty"`
PhotoFileID string `json:"photo_file_id"`
Title string `json:"title,omitempty"`
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
Type InlineQueryType `json:"type"`
ID string `json:"id"`
Description string `json:"description,omitempty"`
Caption string `json:"caption,omitempty"`
ParseMode string `json:"parse_mode,omitempty"`
PhotoFileID string `json:"photo_file_id"`
Title string `json:"title,omitempty"`
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
}
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
@@ -342,15 +347,16 @@ func (i InlineQueryResultCachedPhoto) ImplementsInlineQueryResult() {}
// By default, this animated GIF file will be sent by the user with an optional caption.
// Alternatively, you can use InputMessageContent to send a message with specified content instead of the animation.
type InlineQueryResultCachedGif struct {
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
Type InlineQueryType `json:"type"`
Title string `json:"title,omitempty"`
Caption string `json:"caption,omitempty"`
ParseMode string `json:"parse_mode,omitempty"`
ID string `json:"id"`
GifFileID string `json:"gif_file_id"`
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
Type InlineQueryType `json:"type"`
Title string `json:"title,omitempty"`
Caption string `json:"caption,omitempty"`
ParseMode string `json:"parse_mode,omitempty"`
ID string `json:"id"`
GifFileID string `json:"gif_file_id"`
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
}
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
@@ -360,15 +366,16 @@ func (i InlineQueryResultCachedGif) ImplementsInlineQueryResult() {}
// By default, this animated MPEG-4 file will be sent by the user with an optional caption.
// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the animation.
type InlineQueryResultCachedMpeg4Gif struct {
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
Type InlineQueryType `json:"type"`
Title string `json:"title,omitempty"`
Caption string `json:"caption,omitempty"`
ParseMode string `json:"parse_mode,omitempty"`
ID string `json:"id"`
Mpeg4FileID string `json:"mpeg4_file_id"`
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
Type InlineQueryType `json:"type"`
Title string `json:"title,omitempty"`
Caption string `json:"caption,omitempty"`
ParseMode string `json:"parse_mode,omitempty"`
ID string `json:"id"`
Mpeg4FileID string `json:"mpeg4_file_id"`
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
}
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
@@ -411,16 +418,17 @@ func (i InlineQueryResultCachedDocument) ImplementsInlineQueryResult() {}
// By default, this video file will be sent by the user with an optional caption.
// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the video.
type InlineQueryResultCachedVideo struct {
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
Type InlineQueryType `json:"type"`
ID string `json:"id"`
Description string `json:"description,omitempty"`
Caption string `json:"caption,omitempty"`
ParseMode string `json:"parse_mode,omitempty"`
VideoFileID string `json:"video_file_id"`
Title string `json:"title"`
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
Type InlineQueryType `json:"type"`
ID string `json:"id"`
Description string `json:"description,omitempty"`
Caption string `json:"caption,omitempty"`
ParseMode string `json:"parse_mode,omitempty"`
VideoFileID string `json:"video_file_id"`
Title string `json:"title"`
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
}
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
@@ -468,10 +476,10 @@ type InputMessageContent interface {
// InputTextMessageContent represents the content of a text message to be sent as the result of an inline query.
type InputTextMessageContent struct {
LinkPreviewOptions *LinkPreviewOptions `json:"link_preview_options,omitempty"`
MessageText string `json:"message_text"`
ParseMode string `json:"parse_mode,omitempty"`
Entities []*MessageEntity `json:"entities,omitempty"`
LinkPreviewOptions *LinkPreviewOptions `json:"link_preview_options,omitempty"`
}
// ImplementsInputMessageContent is used to implement the InputMessageContent interface.
@@ -516,6 +524,33 @@ type InputContactMessageContent struct {
// ImplementsInputMessageContent is used to implement the InputMessageContent interface.
func (i InputContactMessageContent) ImplementsInputMessageContent() {}
// InputInvoiceMessageContent represents the content of an invoice message to be sent as the result of an inline query.
type InputInvoiceMessageContent struct {
SuggestedTipAmounts *[]int `json:"suggested_tip_amounts,omitempty"`
PhotoURL string `json:"photo_url,omitempty"`
Description string `json:"description"`
Payload string `json:"string"`
ProviderToken string `json:"provider_token,omitempty"`
Currency string `json:"currency"`
Title string `json:"title"`
ProviderData string `json:"provider_data,omitempty"`
Prices []LabeledPrice `json:"prices"`
PhotoSize int `json:"photo_size,omitempty"`
MaxTipAmount int `json:"max_tip_amount,omitempty"`
PhotoWidth int `json:"photo_width,omitempty"`
PhotoHeight int `json:"photo_height,omitempty"`
NeedName bool `json:"need_name,omitempty"`
NeedPhoneNumber bool `json:"need_phone_number,omitempty"`
NeedEmail bool `json:"need_email,omitempty"`
NeedShippingAddress bool `json:"need_shipping_address,omitempty"`
SendPhoneNumberToProvider bool `json:"send_phone_number_to_provider,omitempty"`
SendEmailToProvider bool `json:"send_email_to_provider,omitempty"`
IsFlexible bool `json:"is_flexible,omitempty"`
}
// ImplementsInputMessageContent is used to implement the InputMessageContent interface.
func (i InputInvoiceMessageContent) ImplementsInputMessageContent() {}
// InlineQueryResultsButton represents a button to be shown above inline query results.
// You MUST use exactly one of the fields.
type InlineQueryResultsButton struct {
@@ -539,5 +574,5 @@ func (a API) AnswerInlineQuery(inlineQueryID string, results []InlineQueryResult
jsn, _ := json.Marshal(results)
vals.Set("inline_query_id", inlineQueryID)
vals.Set("results", string(jsn))
return get[APIResponseBase](a.base, "answerInlineQuery", addValues(vals, opts))
return res, a.client.get(a.base, "answerInlineQuery", addValues(vals, opts), &res)
}

View File

@@ -47,11 +47,11 @@ type GetChatMenuButtonOptions struct {
}
// SetChatMenuButton is used to change the bot's menu button in a private chat, or the default menu button.
func (a API) SetChatMenuButton(opts SetChatMenuButtonOptions) (res APIResponseBool, err error) {
return get[APIResponseBool](a.base, "setChatMenuButton", urlValues(opts))
func (a API) SetChatMenuButton(opts *SetChatMenuButtonOptions) (res APIResponseBool, err error) {
return res, a.client.get(a.base, "setChatMenuButton", urlValues(opts), &res)
}
// GetChatMenuButton is used to get the current value of the bot's menu button in a private chat, or the default menu button.
func (a API) GetChatMenuButton(opts GetChatMenuButtonOptions) (res APIResponseMenuButton, err error) {
return get[APIResponseMenuButton](a.base, "getChatMenuButton", urlValues(opts))
func (a API) GetChatMenuButton(opts *GetChatMenuButtonOptions) (res APIResponseMenuButton, err error) {
return res, a.client.get(a.base, "getChatMenuButton", urlValues(opts), &res)
}

View File

@@ -20,99 +20,345 @@ package echotron
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"path/filepath"
"strings"
"sync"
"time"
"golang.org/x/time/rate"
)
// content is a struct which contains a file's name, its type and its data.
type content struct {
fname string
ftype string
fdata []byte
type client struct {
*http.Client
*sync.RWMutex
cl map[string]*rate.Limiter // chat based limiter
gl *rate.Limiter // global limiter
climiter func() *rate.Limiter
}
// sendGetRequest is used to send an HTTP GET request.
func sendGetRequest(url string) ([]byte, error) {
resp, err := http.Get(url)
var lclient = newClient()
// SetGlobalRequestLimit sets the global rate limit for requests to the Telegram API.
// A duration of 0 disables the rate limiter, allowing unlimited requests.
// By default the duration of this limiter is set to time.Second/30.
func SetGlobalRequestLimit(d time.Duration) {
lclient.Lock()
lclient.gl = rate.NewLimiter(rate.Every(d), 10)
lclient.Unlock()
}
// SetChatRequestLimit sets the per-chat rate limit for requests to the Telegram API.
// A duration of 0 disables the rate limiter, allowing unlimited requests.
// By default the duration of this limiter is set to time.Minute/20.
func SetChatRequestLimit(d time.Duration) {
lclient.Lock()
lclient.cl = make(map[string]*rate.Limiter)
lclient.climiter = func() *rate.Limiter {
return rate.NewLimiter(rate.Every(d), 1)
}
lclient.Unlock()
}
func newClient() *client {
return &client{
Client: new(http.Client),
RWMutex: new(sync.RWMutex),
cl: make(map[string]*rate.Limiter),
gl: rate.NewLimiter(rate.Every(time.Second/30), 10),
climiter: func() *rate.Limiter {
return rate.NewLimiter(rate.Every(time.Minute/20), 1)
},
}
}
func (c client) wait(chatID string) error {
c.RLock()
defer c.RUnlock()
ctx := context.Background()
// If the chatID is empty, it's a general API call like GetUpdates, GetMe
// and similar, so skip the per-chat request limit wait.
if chatID != "" {
// If no limiter exists for a chat, create one.
l, ok := c.cl[chatID]
if !ok {
l = c.climiter()
c.cl[chatID] = l
}
// Make sure to respect the single chat limit of requests.
if err := l.Wait(ctx); err != nil {
return err
}
}
// Make sure to respect the global limit of requests.
return c.gl.Wait(ctx)
}
func (c client) doGet(reqURL string) ([]byte, error) {
resp, err := c.Get(reqURL)
if err != nil {
return []byte{}, err
return nil, err
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return []byte{}, err
return nil, err
}
return data, nil
}
// sendPostRequest is used to send an HTTP POST request.
func sendPostRequest(url string, files ...content) ([]byte, error) {
var buf = new(bytes.Buffer)
var w = multipart.NewWriter(buf)
func (c client) doPost(reqURL string, files ...content) ([]byte, error) {
var (
buf = new(bytes.Buffer)
w = multipart.NewWriter(buf)
)
for _, f := range files {
part, err := w.CreateFormFile(f.ftype, filepath.Base(f.fname))
if err != nil {
return []byte{}, err
return nil, err
}
part.Write(f.fdata)
}
w.Close()
req, err := http.NewRequest("POST", url, buf)
req, err := http.NewRequest(http.MethodPost, reqURL, buf)
if err != nil {
return []byte{}, err
return nil, err
}
req.Header.Add("Content-Type", w.FormDataContentType())
var client = new(http.Client)
res, err := client.Do(req)
res, err := c.Do(req)
if err != nil {
return []byte{}, err
return nil, err
}
defer res.Body.Close()
cnt, err := io.ReadAll(res.Body)
if err != nil {
return []byte{}, err
}
return cnt, nil
return io.ReadAll(res.Body)
}
// sendPostForm is used to send an "application/x-www-form-urlencoded" through an HTTP POST request.
func sendPostForm(reqURL string, keyVals map[string]string) ([]byte, error) {
func (c client) doPostForm(reqURL string, keyVals map[string]string) ([]byte, error) {
var form = make(url.Values)
for k, v := range keyVals {
form.Add(k, v)
}
request, err := http.NewRequest("POST", reqURL, strings.NewReader(form.Encode()))
req, err := http.NewRequest(http.MethodPost, reqURL, strings.NewReader(form.Encode()))
if err != nil {
return []byte{}, err
return nil, err
}
request.PostForm = form
request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.PostForm = form
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
var client http.Client
response, err := client.Do(request)
res, err := c.Do(req)
if err != nil {
return []byte{}, err
return nil, err
}
defer response.Body.Close()
content, err := io.ReadAll(response.Body)
if err != nil {
return []byte{}, err
}
return content, nil
defer res.Body.Close()
return io.ReadAll(res.Body)
}
func (c client) sendFile(file, thumbnail InputFile, url, fileType string) (res []byte, err error) {
var cnt []content
if file.id != "" {
url = fmt.Sprintf("%s&%s=%s", url, fileType, file.id)
} else if file.url != "" {
url = fmt.Sprintf("%s&%s=%s", url, fileType, file.url)
} else if c, e := toContent(fileType, file); e == nil {
cnt = append(cnt, c)
} else {
err = e
}
if c, e := toContent("thumbnail", thumbnail); e == nil {
cnt = append(cnt, c)
} else {
err = e
}
if len(cnt) > 0 {
res, err = c.doPost(url, cnt...)
} else {
res, err = c.doGet(url)
}
return
}
func (c client) get(base, endpoint string, vals url.Values, v APIResponse) error {
url, err := url.JoinPath(base, endpoint)
if err != nil {
return err
}
if vals != nil {
if queries := vals.Encode(); queries != "" {
url = fmt.Sprintf("%s?%s", url, queries)
}
}
if err := c.wait(vals.Get("chat_id")); err != nil {
return err
}
cnt, err := c.doGet(url)
if err != nil {
return err
}
if err := json.Unmarshal(cnt, v); err != nil {
return err
}
return check(v)
}
func (c client) postFile(base, endpoint, fileType string, file, thumbnail InputFile, vals url.Values, v APIResponse) error {
url, err := joinURL(base, endpoint, vals)
if err != nil {
return err
}
if err := c.wait(vals.Get("chat_id")); err != nil {
return err
}
cnt, err := c.sendFile(file, thumbnail, url, fileType)
if err != nil {
return err
}
if err := json.Unmarshal(cnt, v); err != nil {
return err
}
return check(v)
}
func (c client) postMedia(base, endpoint string, editSingle bool, vals url.Values, v APIResponse, files ...InputMedia) error {
url, err := joinURL(base, endpoint, vals)
if err != nil {
return err
}
if err := c.wait(vals.Get("chat_id")); err != nil {
return err
}
cnt, err := c.sendMediaFiles(url, editSingle, files...)
if err != nil {
return err
}
if err := json.Unmarshal(cnt, v); err != nil {
return err
}
return check(v)
}
func (c client) postStickers(base, endpoint string, vals url.Values, v APIResponse, stickers ...InputSticker) error {
url, err := joinURL(base, endpoint, vals)
if err != nil {
return err
}
if err := c.wait(vals.Get("chat_id")); err != nil {
return err
}
cnt, err := c.sendStickers(url, stickers...)
if err != nil {
return err
}
if err := json.Unmarshal(cnt, v); err != nil {
return err
}
return check(v)
}
func (c client) sendMediaFiles(url string, editSingle bool, files ...InputMedia) (res []byte, err error) {
var (
med []mediaEnvelope
cnt []content
jsn []byte
)
for _, file := range files {
var im mediaEnvelope
var cntArr []content
media := file.media()
thumbnail := file.thumbnail()
im, cntArr, err = processMedia(media, thumbnail)
if err != nil {
return
}
im.InputMedia = file
med = append(med, im)
cnt = append(cnt, cntArr...)
}
if editSingle {
jsn, err = json.Marshal(med[0])
} else {
jsn, err = json.Marshal(med)
}
if err != nil {
return
}
url = fmt.Sprintf("%s&media=%s", url, jsn)
if len(cnt) > 0 {
return c.doPost(url, cnt...)
}
return c.doGet(url)
}
func (c client) sendStickers(url string, stickers ...InputSticker) (res []byte, err error) {
var (
sti []stickerEnvelope
cnt []content
jsn []byte
)
for _, s := range stickers {
var se stickerEnvelope
var cntArr []content
se, cntArr, err = processSticker(s.Sticker)
if err != nil {
return
}
se.InputSticker = s
sti = append(sti, se)
cnt = append(cnt, cntArr...)
}
if len(sti) == 1 {
jsn, _ = json.Marshal(sti[0])
url = fmt.Sprintf("%s&sticker=%s", url, jsn)
} else {
jsn, _ = json.Marshal(sti)
url = fmt.Sprintf("%s&stickers=%s", url, jsn)
}
if len(cnt) > 0 {
return c.doPost(url, cnt...)
}
return c.doGet(url)
}

View File

@@ -74,23 +74,25 @@ type MessageEntityType string
// These are all the possible types for MessageEntityType.
const (
MentionEntity MessageEntityType = "mention"
HashtagEntity = "hashtag"
CashtagEntity = "cashtag"
BotCommandEntity = "bot_command"
UrlEntity = "url"
EmailEntity = "email"
PhoneNumberEntity = "phone_number"
BoldEntity = "bold"
ItalicEntity = "italic"
UnderlineEntity = "underline"
StrikethroughEntity = "strikethrough"
SpoilerEntity = "spoiler"
CodeEntity = "code"
PreEntity = "pre"
TextLinkEntity = "text_link"
TextMentionEntity = "text_mention"
CustomEmojiEntity = "custom_emoji"
MentionEntity MessageEntityType = "mention"
HashtagEntity = "hashtag"
CashtagEntity = "cashtag"
BotCommandEntity = "bot_command"
UrlEntity = "url"
EmailEntity = "email"
PhoneNumberEntity = "phone_number"
BoldEntity = "bold"
ItalicEntity = "italic"
UnderlineEntity = "underline"
StrikethroughEntity = "strikethrough"
SpoilerEntity = "spoiler"
BlockQuoteEntity = "blockquote"
ExpandableBlockQuoteEntity = "expandable_blockquote"
CodeEntity = "code"
PreEntity = "pre"
TextLinkEntity = "text_link"
TextMentionEntity = "text_mention"
CustomEmojiEntity = "custom_emoji"
)
// UpdateType is a custom type for the various update types that a bot can be subscribed to.
@@ -137,10 +139,13 @@ type KeyboardButtonPollType struct {
// KeyboardButtonRequestUsers defines the criteria used to request suitable users.
// The identifiers of the selected users will be shared with the bot when the corresponding button is pressed.
type KeyboardButtonRequestUsers struct {
RequestID int `json:"request_id"`
MaxQuantity int `json:"max_quantity,omitempty"`
UserIsBot bool `json:"user_is_bot,omitempty"`
UserIsPremium bool `json:"user_is_premium,omitempty"`
RequestID int `json:"request_id"`
MaxQuantity int `json:"max_quantity,omitempty"`
UserIsBot bool `json:"user_is_bot,omitempty"`
UserIsPremium bool `json:"user_is_premium,omitempty"`
RequestName bool `json:"request_name,omitempty"`
RequestUsername bool `json:"request_username,omitempty"`
RequestPhoto bool `json:"request_photo,omitempty"`
}
// KeyboardButtonRequestChat defines the criteria used to request a suitable chat.
@@ -154,6 +159,9 @@ type KeyboardButtonRequestChat struct {
ChatHasUsername bool `json:"chat_has_username,omitempty"`
ChatIsCreated bool `json:"chat_is_created,omitempty"`
BotIsMember bool `json:"bot_is_member,omitempty"`
RequestName bool `json:"request_name,omitempty"`
RequestUsername bool `json:"request_username,omitempty"`
RequestPhoto bool `json:"request_photo,omitempty"`
}
// ReplyKeyboardMarkup represents a custom keyboard with reply options.
@@ -233,23 +241,27 @@ type WebhookOptions struct {
// BaseOptions contains the optional parameters used frequently in some Telegram API methods.
type BaseOptions struct {
ReplyMarkup ReplyMarkup `query:"reply_markup"`
ReplyParameters ReplyParameters `query:"reply_parameters"`
MessageThreadID int `query:"message_thread_id"`
DisableNotification bool `query:"disable_notification"`
ProtectContent bool `query:"protect_content"`
BusinessConnectionID string `query:"business_connection_id"`
MessageEffectID string `query:"message_effect_id"`
ReplyMarkup ReplyMarkup `query:"reply_markup"`
ReplyParameters ReplyParameters `query:"reply_parameters"`
MessageThreadID int `query:"message_thread_id"`
DisableNotification bool `query:"disable_notification"`
ProtectContent bool `query:"protect_content"`
}
// MessageOptions contains the optional parameters used by some Telegram API methods.
type MessageOptions struct {
ReplyMarkup ReplyMarkup `query:"reply_markup"`
ReplyParameters ReplyParameters `query:"reply_parameters"`
ParseMode ParseMode `query:"parse_mode"`
LinkPreviewOptions LinkPreviewOptions `query:"link_preview_options"`
Entities []MessageEntity `query:"entities"`
MessageThreadID int64 `query:"message_thread_id"`
DisableNotification bool `query:"disable_notification"`
ProtectContent bool `query:"protect_content"`
ReplyMarkup ReplyMarkup `query:"reply_markup"`
BusinessConnectionID string `query:"business_connection_id"`
MessageEffectID string `query:"message_effect_id"`
ParseMode ParseMode `query:"parse_mode"`
LinkPreviewOptions LinkPreviewOptions `query:"link_preview_options"`
Entities []MessageEntity `query:"entities"`
ReplyParameters ReplyParameters `query:"reply_parameters"`
MessageThreadID int64 `query:"message_thread_id"`
DisableNotification bool `query:"disable_notification"`
ProtectContent bool `query:"protect_content"`
}
// PinMessageOptions contains the optional parameters used by the PinChatMember method.
@@ -266,14 +278,15 @@ type ForwardOptions struct {
// CopyOptions contains the optional parameters used by the CopyMessage method.
type CopyOptions struct {
ReplyMarkup ReplyMarkup `query:"reply_markup"`
ReplyParameters ReplyParameters `query:"reply_parameters"`
ParseMode ParseMode `query:"parse_mode"`
Caption string `query:"caption"`
CaptionEntities []MessageEntity `query:"caption_entities"`
MessageThreadID int `query:"message_thread_id"`
DisableNotification bool `query:"disable_notification"`
ProtectContent bool `query:"protect_content"`
ReplyMarkup ReplyMarkup `query:"reply_markup"`
ParseMode ParseMode `query:"parse_mode"`
Caption string `query:"caption"`
CaptionEntities []MessageEntity `query:"caption_entities"`
ReplyParameters ReplyParameters `query:"reply_parameters"`
MessageThreadID int `query:"message_thread_id"`
DisableNotification bool `query:"disable_notification"`
ProtectContent bool `query:"protect_content"`
ShowCaptionAboveMedia bool `query:"show_caption_above_media"`
}
// CopyMessagesOptions contains the optional parameters used by the CopyMessages methods.
@@ -286,12 +299,14 @@ type CopyMessagesOptions struct {
// StickerOptions contains the optional parameters used by the SendSticker method.
type StickerOptions struct {
Emoji string `query:"emoji"`
ReplyMarkup ReplyMarkup `query:"reply_markup"`
ReplyParameters ReplyParameters `query:"reply_parameters"`
MessageThreadID int `query:"message_thread_id"`
DisableNotification bool `query:"disable_notification"`
ProtectContent bool `query:"protect_content"`
BusinessConnectionID string `query:"business_connection_id"`
Emoji string `query:"emoji"`
MessageEffectID string `query:"message_effect_id"`
ReplyMarkup ReplyMarkup `query:"reply_markup"`
ReplyParameters ReplyParameters `query:"reply_parameters"`
MessageThreadID int `query:"message_thread_id"`
DisableNotification bool `query:"disable_notification"`
ProtectContent bool `query:"protect_content"`
}
// InputFile is a struct which contains data about a file to be sent.
@@ -299,7 +314,6 @@ type InputFile struct {
id string
path string
url string
ftype string
content []byte
}
@@ -325,41 +339,48 @@ func NewInputFileBytes(fileName string, content []byte) InputFile {
// PhotoOptions contains the optional parameters used by the SendPhoto method.
type PhotoOptions struct {
ReplyMarkup ReplyMarkup `query:"reply_markup"`
ReplyParameters ReplyParameters `query:"reply_parameters"`
ParseMode ParseMode `query:"parse_mode"`
Caption string `query:"caption"`
CaptionEntities []MessageEntity `query:"caption_entities"`
MessageThreadID int `query:"message_thread_id"`
HasSpoiler bool `query:"has_spoiler"`
DisableNotification bool `query:"disable_notification"`
ProtectContent bool `query:"protect_content"`
ReplyMarkup ReplyMarkup `query:"reply_markup"`
BusinessConnectionID string `query:"business_connection_id"`
MessageEffectID string `query:"message_effect_id"`
ParseMode ParseMode `query:"parse_mode"`
Caption string `query:"caption"`
CaptionEntities []MessageEntity `query:"caption_entities"`
ReplyParameters ReplyParameters `query:"reply_parameters"`
MessageThreadID int `query:"message_thread_id"`
HasSpoiler bool `query:"has_spoiler"`
DisableNotification bool `query:"disable_notification"`
ProtectContent bool `query:"protect_content"`
ShowCaptionAboveMedia bool `query:"show_caption_above_media"`
}
// AudioOptions contains the optional parameters used by the SendAudio method.
type AudioOptions struct {
ReplyMarkup ReplyMarkup `query:"reply_markup"`
ReplyParameters ReplyParameters `query:"reply_parameters"`
ParseMode ParseMode `query:"parse_mode"`
Caption string `query:"caption"`
Performer string `query:"performer"`
Title string `query:"title"`
Thumbnail InputFile
CaptionEntities []MessageEntity `query:"caption_entities"`
MessageThreadID int `query:"message_thread_id"`
Duration int `query:"duration"`
DisableNotification bool `query:"disable_notification"`
ProtectContent bool `query:"protect_content"`
ReplyMarkup ReplyMarkup `query:"reply_markup"`
Title string `query:"title"`
MessageEffectID string `query:"message_effect_id"`
ParseMode ParseMode `query:"parse_mode"`
Caption string `query:"caption"`
Performer string `query:"performer"`
BusinessConnectionID string `query:"business_connection_id"`
Thumbnail InputFile
CaptionEntities []MessageEntity `query:"caption_entities"`
ReplyParameters ReplyParameters `query:"reply_parameters"`
MessageThreadID int `query:"message_thread_id"`
Duration int `query:"duration"`
DisableNotification bool `query:"disable_notification"`
ProtectContent bool `query:"protect_content"`
}
// DocumentOptions contains the optional parameters used by the SendDocument method.
type DocumentOptions struct {
ReplyMarkup ReplyMarkup `query:"reply_markup"`
ReplyParameters ReplyParameters `query:"reply_parameters"`
ParseMode ParseMode `query:"parse_mode"`
Caption string `query:"caption"`
ReplyMarkup ReplyMarkup `query:"reply_markup"`
BusinessConnectionID string `query:"business_connection_id"`
MessageEffectID string `query:"message_effect_id"`
ParseMode ParseMode `query:"parse_mode"`
Caption string `query:"caption"`
Thumbnail InputFile
CaptionEntities []MessageEntity `query:"caption_entities"`
ReplyParameters ReplyParameters `query:"reply_parameters"`
MessageThreadID int `query:"message_thread_id"`
DisableNotification bool `query:"disable_notification"`
ProtectContent bool `query:"protect_content"`
@@ -368,74 +389,91 @@ type DocumentOptions struct {
// VideoOptions contains the optional parameters used by the SendVideo method.
type VideoOptions struct {
ReplyMarkup ReplyMarkup `query:"reply_markup"`
ReplyParameters ReplyParameters `query:"reply_parameters"`
Caption string `query:"caption"`
ParseMode ParseMode `query:"parse_mode"`
Thumbnail InputFile
CaptionEntities []MessageEntity `query:"caption_entities"`
MessageThreadID int `query:"message_thread_id"`
Duration int `query:"duration"`
Width int `query:"width"`
Height int `query:"height"`
HasSpoiler bool `query:"has_spoiler"`
SupportsStreaming bool `query:"supports_streaming"`
DisableNotification bool `query:"disable_notification"`
ProtectContent bool `query:"protect_content"`
ReplyMarkup ReplyMarkup `query:"reply_markup"`
BusinessConnectionID string `query:"business_connection_id"`
Caption string `query:"caption"`
MessageEffectID string `query:"message_effect_id"`
ParseMode ParseMode `query:"parse_mode"`
Thumbnail InputFile
CaptionEntities []MessageEntity `query:"caption_entities"`
ReplyParameters ReplyParameters `query:"reply_parameters"`
MessageThreadID int `query:"message_thread_id"`
Duration int `query:"duration"`
Width int `query:"width"`
Height int `query:"height"`
HasSpoiler bool `query:"has_spoiler"`
SupportsStreaming bool `query:"supports_streaming"`
DisableNotification bool `query:"disable_notification"`
ProtectContent bool `query:"protect_content"`
ShowCaptionAboveMedia bool `query:"show_caption_above_media"`
}
// AnimationOptions contains the optional parameters used by the SendAnimation method.
type AnimationOptions struct {
ReplyMarkup ReplyMarkup `query:"reply_markup"`
ReplyParameters ReplyParameters `query:"reply_parameters"`
ParseMode ParseMode `query:"parse_mode"`
Caption string `query:"caption"`
Thumbnail InputFile
CaptionEntities []MessageEntity `query:"caption_entities"`
MessageThreadID int `query:"message_thread_id"`
Duration int `query:"duration"`
Width int `query:"width"`
Height int `query:"height"`
HasSpoiler bool `query:"has_spoiler"`
DisableNotification bool `query:"disable_notification"`
ProtectContent bool `query:"protect_content"`
ReplyMarkup ReplyMarkup `query:"reply_markup"`
BusinessConnectionID string `query:"business_connection_id"`
MessageEffectID string `query:"message_effect_id"`
ParseMode ParseMode `query:"parse_mode"`
Caption string `query:"caption"`
Thumbnail InputFile
CaptionEntities []MessageEntity `query:"caption_entities"`
ReplyParameters ReplyParameters `query:"reply_parameters"`
MessageThreadID int `query:"message_thread_id"`
Duration int `query:"duration"`
Width int `query:"width"`
Height int `query:"height"`
HasSpoiler bool `query:"has_spoiler"`
DisableNotification bool `query:"disable_notification"`
ProtectContent bool `query:"protect_content"`
ShowCaptionAboveMedia bool `query:"show_caption_above_media"`
}
// VoiceOptions contains the optional parameters used by the SendVoice method.
type VoiceOptions struct {
ReplyMarkup ReplyMarkup `query:"reply_markup"`
ReplyParameters ReplyParameters `query:"reply_parameters"`
ParseMode ParseMode `query:"parse_mode"`
Caption string `query:"caption"`
CaptionEntities []MessageEntity `query:"caption_entities"`
MessageThreadID int `query:"message_thread_id"`
Duration int `query:"duration"`
DisableNotification bool `query:"disable_notification"`
ProtectContent bool `query:"protect_content"`
ReplyMarkup ReplyMarkup `query:"reply_markup"`
BusinessConnectionID string `query:"business_connection_id"`
MessageEffectID string `query:"message_effect_id"`
ParseMode ParseMode `query:"parse_mode"`
Caption string `query:"caption"`
CaptionEntities []MessageEntity `query:"caption_entities"`
ReplyParameters ReplyParameters `query:"reply_parameters"`
MessageThreadID int `query:"message_thread_id"`
Duration int `query:"duration"`
DisableNotification bool `query:"disable_notification"`
ProtectContent bool `query:"protect_content"`
}
// VideoNoteOptions contains the optional parameters used by the SendVideoNote method.
type VideoNoteOptions struct {
ReplyMarkup ReplyMarkup `query:"reply_markup"`
ReplyParameters ReplyParameters `query:"reply_parameters"`
Thumbnail InputFile
MessageThreadID int `query:"message_thread_id"`
Duration int `query:"duration"`
Length int `query:"length"`
DisableNotification bool `query:"disable_notification"`
ProtectContent bool `query:"protect_content"`
ReplyMarkup ReplyMarkup `query:"reply_markup"`
BusinessConnectionID string `query:"business_connection_id"`
MessageEffectID string `query:"message_effect_id"`
Thumbnail InputFile
ReplyParameters ReplyParameters `query:"reply_parameters"`
MessageThreadID int `query:"message_thread_id"`
Duration int `query:"duration"`
Length int `query:"length"`
DisableNotification bool `query:"disable_notification"`
ProtectContent bool `query:"protect_content"`
}
// MediaGroupOptions contains the optional parameters used by the SendMediaGroup method.
type MediaGroupOptions struct {
ReplyParameters ReplyParameters `query:"reply_parameters"`
MessageThreadID int `query:"message_thread_id"`
DisableNotification bool `query:"disable_notification"`
ProtectContent bool `query:"protect_content"`
BusinessConnectionID string `query:"business_connection_id"`
MessageEffectID string `query:"message_effect_id"`
ReplyParameters ReplyParameters `query:"reply_parameters"`
MessageThreadID int `query:"message_thread_id"`
DisableNotification bool `query:"disable_notification"`
ProtectContent bool `query:"protect_content"`
}
// This is a custom constant to set an infinite live period value in LocationOptions and EditLocationOptions.
const InfiniteLivePeriod = 0x7FFFFFFF
// LocationOptions contains the optional parameters used by the SendLocation method.
type LocationOptions struct {
BusinessConnectionID string `query:"business_connection_id"`
MessageEffectID string `query:"message_effect_id"`
ReplyMarkup ReplyMarkup `query:"reply_markup"`
ReplyParameters ReplyParameters `query:"reply_parameters"`
HorizontalAccuracy float64 `query:"horizontal_accuracy"`
@@ -449,46 +487,62 @@ type LocationOptions struct {
// EditLocationOptions contains the optional parameters used by the EditMessageLiveLocation method.
type EditLocationOptions struct {
BusinessConnectionID string `query:"business_connection_id"`
ReplyMarkup InlineKeyboardMarkup `query:"reply_markup"`
HorizontalAccuracy float64 `query:"horizontal_accuracy"`
Heading int `query:"heading"`
LivePeriod int `query:"live_period"`
ProximityAlertRadius int `query:"proximity_alert_radius"`
}
// StopLocationOptions contains the optional parameters used by the StopMessageLiveLocation method.
type StopLocationOptions struct {
ReplyMarkup ReplyMarkup `query:"reply_markup"`
BusinessConnectionID string `query:"business_connection_id"`
}
// VenueOptions contains the optional parameters used by the SendVenue method.
type VenueOptions struct {
ReplyMarkup ReplyMarkup `query:"reply_markup"`
ReplyParameters ReplyParameters `query:"reply_parameters"`
FoursquareID string `query:"foursquare_id"`
FoursquareType string `query:"foursquare_type"`
GooglePlaceType string `query:"google_place_type"`
GooglePlaceID string `query:"google_place_id"`
MessageThreadID int `query:"message_thread_id"`
DisableNotification bool `query:"disable_notification"`
ProtectContent bool `query:"protect_content"`
ReplyMarkup ReplyMarkup `query:"reply_markup"`
BusinessConnectionID string `query:"business_connection_id"`
FoursquareID string `query:"foursquare_id"`
FoursquareType string `query:"foursquare_type"`
GooglePlaceType string `query:"google_place_type"`
GooglePlaceID string `query:"google_place_id"`
MessageEffectID string `query:"message_effect_id"`
ReplyParameters ReplyParameters `query:"reply_parameters"`
MessageThreadID int `query:"message_thread_id"`
DisableNotification bool `query:"disable_notification"`
ProtectContent bool `query:"protect_content"`
}
// ContactOptions contains the optional parameters used by the SendContact method.
type ContactOptions struct {
ReplyMarkup ReplyMarkup `query:"reply_markup"`
ReplyParameters ReplyParameters `query:"reply_parameters"`
VCard string `query:"vcard"`
LastName string `query:"last_name"`
MessageThreadID int `query:"message_thread_id"`
DisableNotification bool `query:"disable_notification"`
ProtectContent bool `query:"protect_content"`
ReplyMarkup ReplyMarkup `query:"reply_markup"`
BusinessConnectionID string `query:"business_connection_id"`
VCard string `query:"vcard"`
LastName string `query:"last_name"`
MessageEffectID string `query:"message_effect_id"`
ReplyParameters ReplyParameters `query:"reply_parameters"`
MessageThreadID int `query:"message_thread_id"`
DisableNotification bool `query:"disable_notification"`
ProtectContent bool `query:"protect_content"`
}
// PollOptions contains the optional parameters used by the SendPoll method.
type PollOptions struct {
ReplyMarkup ReplyMarkup `query:"reply_markup"`
ReplyParameters ReplyParameters `query:"reply_parameters"`
BusinessConnectionID string `query:"business_connection_id"`
Explanation string `query:"explanation"`
MessageEffectID string `query:"message_effect_id"`
ExplanationParseMode ParseMode `query:"explanation_parse_mode"`
QuestionParseMode ParseMode `query:"question_parse_mode"`
Type PollType `query:"type"`
ExplanationEntities []MessageEntity `query:"explanation_entities"`
MessageThreadID int `query:"message_thread_id"`
QuestionEntities []MessageEntity `query:"question_entities"`
ReplyParameters ReplyParameters `query:"reply_parameters"`
CorrectOptionID int `query:"correct_option_id"`
MessageThreadID int `query:"message_thread_id"`
CloseDate int `query:"close_date"`
OpenPeriod int `query:"open_period"`
IsClosed bool `query:"is_closed"`
@@ -498,6 +552,12 @@ type PollOptions struct {
IsAnonymous bool `query:"is_anonymous"`
}
// StopPollOptions contains the optional parameters used by the StopPoll method.
type StopPollOptions struct {
ReplyMarkup ReplyMarkup `query:"reply_markup"`
BusinessConnectionID string `query:"business_connection_id"`
}
// BanOptions contains the optional parameters used by the BanChatMember method.
type BanOptions struct {
UntilDate int `query:"until_date"`
@@ -580,23 +640,33 @@ func NewInlineMessageID(ID string) MessageIDOptions {
// MessageTextOptions contains the optional parameters used by the EditMessageText method.
type MessageTextOptions struct {
ParseMode ParseMode `query:"parse_mode"`
Entities []MessageEntity `query:"entities"`
ReplyMarkup InlineKeyboardMarkup `query:"reply_markup"`
LinkPreviewOptions LinkPreviewOptions `query:"link_preview_options"`
ParseMode ParseMode `query:"parse_mode"`
BusinessConnectionID string `query:"business_connection_id"`
Entities []MessageEntity `query:"entities"`
ReplyMarkup InlineKeyboardMarkup `query:"reply_markup"`
LinkPreviewOptions LinkPreviewOptions `query:"link_preview_options"`
}
// MessageCaptionOptions contains the optional parameters used by the EditMessageCaption method.
type MessageCaptionOptions struct {
Caption string `query:"caption"`
ParseMode ParseMode `query:"parse_mode"`
CaptionEntities []MessageEntity `query:"caption_entities"`
ReplyMarkup InlineKeyboardMarkup `query:"reply_markup"`
Caption string `query:"caption"`
BusinessConnectionID string `query:"business_connection_id"`
ParseMode ParseMode `query:"parse_mode"`
CaptionEntities []MessageEntity `query:"caption_entities"`
ReplyMarkup InlineKeyboardMarkup `query:"reply_markup"`
ShowCaptionAboveMedia bool `query:"show_caption_above_media"`
}
// MessageReplyMarkup contains the optional parameters used by the method which only require reply_markup.
type MessageReplyMarkup struct {
ReplyMarkup InlineKeyboardMarkup `query:"reply_markup"`
// MessageMediaOptions contains the optional parameters used by the EditMessageMedia method.
type MessageMediaOptions struct {
ReplyMarkup ReplyMarkup `query:"reply_markup"`
BusinessConnectionID string `query:"business_connection_id"`
}
// MessageReplyMarkupOptions contains the optional parameters used by the EditMessageReplyMarkup method.
type MessageReplyMarkupOptions struct {
BusinessConnectionID string `query:"business_connection_id"`
ReplyMarkup InlineKeyboardMarkup `query:"reply_markup"`
}
// CommandOptions contains the optional parameters used by the SetMyCommands, DeleteMyCommands and GetMyCommands methods.
@@ -610,17 +680,19 @@ type InvoiceOptions struct {
StartParameter string `query:"start_parameter"`
ProviderData string `query:"provider_data"`
PhotoURL string `query:"photo_url"`
ProviderToken string `query:"provider_token"`
MessageEffectID string `query:"message_effect_id"`
ReplyMarkup InlineKeyboardMarkup `query:"reply_markup"`
ReplyParameters ReplyParameters `query:"reply_parameters"`
SuggestedTipAmount []int `query:"suggested_tip_amounts"`
MessageThreadID int `query:"message_thread_id"`
ReplyParameters ReplyParameters `query:"reply_parameters"`
MaxTipAmount int `query:"max_tip_amount"`
PhotoSize int `query:"photo_size"`
PhotoWidth int `query:"photo_width"`
PhotoHeight int `query:"photo_height"`
MaxTipAmount int `query:"max_tip_amount"`
NeedPhoneNumber bool `query:"need_phone_number"`
NeepShippingAddress bool `query:"need_shipping_address"`
MessageThreadID int `query:"message_thread_id"`
SendPhoneNumberToProvider bool `query:"send_phone_number_to_provider"`
NeepShippingAddress bool `query:"need_shipping_address"`
NeedPhoneNumber bool `query:"need_phone_number"`
SendEmailToProvider bool `query:"send_email_to_provider"`
IsFlexible bool `query:"is_flexible"`
DisableNotification bool `query:"disable_notification"`
@@ -633,6 +705,7 @@ type InvoiceOptions struct {
type CreateInvoiceLinkOptions struct {
ProviderData string `query:"provider_data"`
PhotoURL string `query:"photo_url"`
ProviderToken string `query:"provider_token"`
SuggestedTipAmounts []int `query:"suggested_tip_amounts"`
PhotoSize int `query:"photo_size"`
PhotoWidth int `query:"photo_width"`
@@ -679,7 +752,8 @@ type EditTopicOptions struct {
// ChatActionOptions contains the optional parameters used by the SendChatAction API method.
type ChatActionOptions struct {
MessageThreadID int `query:"message_thread_id"`
BusinessConnectionID string `query:"business_connection_id"`
MessageThreadID int `query:"message_thread_id"`
}
// MessageReactionOptions contains the optional parameters used by the SetMessageReaction API method.

View File

@@ -224,5 +224,5 @@ func (a API) SetPassportDataErrors(userID int64, errors []PassportElementError)
vals.Set("user_id", itoa(userID))
vals.Set("errors", string(errorsArr))
return get[APIResponseBool](a.base, "setPassportDataErrors", vals)
return res, a.client.get(a.base, "setPassportDataErrors", vals, &res)
}

View File

@@ -96,8 +96,90 @@ type PreCheckoutQuery struct {
TotalAmount int `json:"total_amount"`
}
// RevenueWithdrawalState describes the state of a revenue withdrawal operation.
type RevenueWithdrawalState interface {
ImplementsRevenueWithdrawalState()
}
// RevenueWithdrawalStatePending describes the state of a withdrawal in progress.
type RevenueWithdrawalStatePending struct {
Type string `json:"type"`
}
// ImplementsRevenueWithdrawalState is used to implement the RevenueWithdrawalState interface.
func (r RevenueWithdrawalStatePending) ImplementsRevenueWithdrawalState() {}
// RevenueWithdrawalStateSucceeded describes the state of a succeeded withdrawal.
type RevenueWithdrawalStateSucceeded struct {
Type string `json:"type"`
URL string `json:"url"`
Date int `json:"date"`
}
// ImplementsRevenueWithdrawalState is used to implement the RevenueWithdrawalState interface.
func (r RevenueWithdrawalStateSucceeded) ImplementsRevenueWithdrawalState() {}
// RevenueWithdrawalStateFailed describes the state of a failed withdrawal, in which the transaction was refunded.
type RevenueWithdrawalStateFailed struct {
Type string `json:"type"`
}
// ImplementsRevenueWithdrawalState is used to implement the RevenueWithdrawalState interface.
func (r RevenueWithdrawalStateFailed) ImplementsRevenueWithdrawalState() {}
// TransactionPartner describes the source of a transaction, or its recipient for outgoing transactions.
type TransactionPartner interface {
ImplementsTransactionPartner()
}
// TransactionPartnerFragment describes a withdrawal transaction with Fragment.
type TransactionPartnerFragment struct {
WithdrawalState RevenueWithdrawalState `json:"withdrawal_state"`
Type string `json:"type"`
}
// ImplementsTransactionPartner is used to implement the TransactionPartner interface.
func (t TransactionPartnerFragment) ImplementsTransactionPartner() {}
// TransactionPartnerUser describes a transaction with a user.
type TransactionPartnerUser struct {
Type string `json:"type"`
User User `json:"user"`
}
// ImplementsTransactionPartner is used to implement the TransactionPartner interface.
func (t TransactionPartnerUser) ImplementsTransactionPartner() {}
// TransactionPartnerOther describes a transaction with an unknown source or recipient.
type TransactionPartnerOther struct {
Type string `json:"type"`
}
// ImplementsTransactionPartner is used to implement the TransactionPartner interface.
func (t TransactionPartnerOther) ImplementsTransactionPartner() {}
// StarTransaction describes a Telegram Star transaction.
type StarTransaction struct {
Source TransactionPartner `json:"source"`
Receiver TransactionPartner `json:"receiver"`
ID string `json:"id"`
Amount int `json:"amount"`
Date int `json:"date"`
}
// StarTransactions contains a list of Telegram Star transactions.
type StarTransactions struct {
Transaction []StarTransaction `json:"transaction"`
}
// StarTransactionsOptions contains the optional parameters used by the GetStarTransactions method.
type StarTransactionsOptions struct {
Offset int `query:"offset"`
Limit int `query:"limit"`
}
// SendInvoice is used to send invoices.
func (a API) SendInvoice(chatID int64, title, description, payload, providerToken, currency string, prices []LabeledPrice, opts *InvoiceOptions) (res APIResponseMessage, err error) {
func (a API) SendInvoice(chatID int64, title, description, payload, currency string, prices []LabeledPrice, opts *InvoiceOptions) (res APIResponseMessage, err error) {
var vals = make(url.Values)
p, err := json.Marshal(prices)
@@ -109,10 +191,26 @@ func (a API) SendInvoice(chatID int64, title, description, payload, providerToke
vals.Set("title", title)
vals.Set("description", description)
vals.Set("payload", payload)
vals.Set("provider_token", providerToken)
vals.Set("currency", currency)
vals.Set("prices", string(p))
return get[APIResponseMessage](a.base, "sendInvoice", addValues(vals, opts))
return res, a.client.get(a.base, "sendInvoice", addValues(vals, opts), &res)
}
// CreateInvoiceLink creates a link for an invoice.
func (a API) CreateInvoiceLink(title, description, payload, currency string, prices []LabeledPrice, opts *CreateInvoiceLinkOptions) (res APIResponseBase, err error) {
var vals = make(url.Values)
p, err := json.Marshal(prices)
if err != nil {
return res, err
}
vals.Set("title", title)
vals.Set("description", description)
vals.Set("payload", payload)
vals.Set("currency", currency)
vals.Set("prices", string(p))
return res, a.client.get(a.base, "createInvoiceLink", addValues(vals, opts), &res)
}
// AnswerShippingQuery is used to reply to shipping queries.
@@ -123,7 +221,7 @@ func (a API) AnswerShippingQuery(shippingQueryID string, ok bool, opts *Shipping
vals.Set("shipping_query_id", shippingQueryID)
vals.Set("ok", btoa(ok))
return get[APIResponseBase](a.base, "answerShippingQuery", addValues(vals, opts))
return res, a.client.get(a.base, "answerShippingQuery", addValues(vals, opts), &res)
}
// AnswerPreCheckoutQuery is used to respond to such pre-checkout queries.
@@ -135,23 +233,19 @@ func (a API) AnswerPreCheckoutQuery(preCheckoutQueryID string, ok bool, opts *Pr
vals.Set("pre_checkout_query_id", preCheckoutQueryID)
vals.Set("ok", btoa(ok))
return get[APIResponseBase](a.base, "answerPreCheckoutQuery", addValues(vals, opts))
return res, a.client.get(a.base, "answerPreCheckoutQuery", addValues(vals, opts), &res)
}
// CreateInvoiceLink creates a link for an invoice.
func (a API) CreateInvoiceLink(title, description, payload, providerToken, currency string, prices []LabeledPrice, opts *CreateInvoiceLinkOptions) (res APIResponseBase, err error) {
// GetStarTransactions returns the bot's Telegram Star transactions in chronological order.
func (a API) GetStarTransactions(opts *StarTransactionsOptions) (res APIResponseStarTransactions, err error) {
return res, a.client.get(a.base, "getStarTransactions", urlValues(opts), &res)
}
// RefundStarPayment refunds a successful payment in Telegram Stars.
func (a API) RefundStarPayment(userID int64, telegramPaymentChargeID string) (res APIResponseBool, err error) {
var vals = make(url.Values)
p, err := json.Marshal(prices)
if err != nil {
return res, err
}
vals.Set("title", title)
vals.Set("description", description)
vals.Set("payload", payload)
vals.Set("provider_token", providerToken)
vals.Set("currency", currency)
vals.Set("prices", string(p))
return get[APIResponseBase](a.base, "createInvoiceLink", addValues(vals, opts))
vals.Set("user_id", itoa(userID))
vals.Set("telegram_payment_charge_id", telegramPaymentChargeID)
return res, a.client.get(a.base, "refundStarPayment", vals, &res)
}

View File

@@ -70,10 +70,6 @@ func scan(i any, v url.Values) url.Values {
return v
}
func querify(i any) string {
return scan(i, url.Values{}).Encode()
}
func urlValues(i any) url.Values {
if i == nil {
return nil

View File

@@ -20,7 +20,6 @@ package echotron
import (
"encoding/json"
"fmt"
"net/url"
)
@@ -50,8 +49,6 @@ type StickerSet struct {
Name string `json:"name"`
StickerType StickerSetType `json:"sticker_type"`
Stickers []Sticker `json:"stickers"`
IsAnimated bool `json:"is_animated"`
IsVideo bool `json:"is_video"`
}
// StickerSetType represents the type of a sticker or of the entire set
@@ -100,16 +97,17 @@ type NewStickerSetOptions struct {
// InputSticker is a struct which describes a sticker to be added to a sticker set.
type InputSticker struct {
Sticker InputFile `json:"-"`
EmojiList []string `json:"emoji_list"`
MaskPosition *MaskPosition `json:"mask_position,omitempty"`
Keywords *[]string `json:"keywords,omitempty"`
Format StickerFormat `json:"format"`
Sticker InputFile `json:"-"`
EmojiList []string `json:"emoji_list"`
}
// stickerEnvelope is a generic struct for all the various structs under the InputSticker interface.
type stickerEnvelope struct {
InputSticker
Sticker string `json:"sticker"`
InputSticker
}
// SendSticker is used to send static .WEBP or animated .TGS stickers.
@@ -118,7 +116,7 @@ func (a API) SendSticker(stickerID string, chatID int64, opts *StickerOptions) (
vals.Set("sticker", stickerID)
vals.Set("chat_id", itoa(chatID))
return get[APIResponseMessage](a.base, "sendSticker", addValues(vals, opts))
return res, a.client.get(a.base, "sendSticker", addValues(vals, opts), &res)
}
// GetStickerSet is used to get a sticker set.
@@ -126,30 +124,16 @@ func (a API) GetStickerSet(name string) (res APIResponseStickerSet, err error) {
var vals = make(url.Values)
vals.Set("name", name)
return get[APIResponseStickerSet](a.base, "getStickerSet", vals)
return res, a.client.get(a.base, "getStickerSet", vals, &res)
}
// GetCustomEmojiStickers is used to get information about custom emoji stickers by their identifiers.
func (a API) GetCustomEmojiStickers(customEmojiIDs ...string) (res APIResponseStickers, err error) {
var vals = make(url.Values)
jsn, _ := json.Marshal(customEmojiIDs)
var url = fmt.Sprintf(
"%sgetCustomEmojiStickers?custom_emoji_ids=%s",
a.base,
jsn,
)
cnt, err := sendGetRequest(url)
if err != nil {
return
}
if err = json.Unmarshal(cnt, &res); err != nil {
return
}
err = check(res)
return
vals.Set("custom_emoji_ids", string(jsn))
return res, a.client.get(a.base, "getCustomEmojiStickers", vals, &res)
}
// UploadStickerFile is used to upload a .PNG file with a sticker for later use in
@@ -159,18 +143,17 @@ func (a API) UploadStickerFile(userID int64, sticker InputFile, format StickerFo
vals.Set("user_id", itoa(userID))
vals.Set("sticker_format", string(format))
return postFile[APIResponseFile](a.base, "uploadStickerFile", "sticker", sticker, InputFile{}, vals)
return res, a.client.postFile(a.base, "uploadStickerFile", "sticker", sticker, InputFile{}, vals, &res)
}
// CreateNewStickerSet is used to create a new sticker set owned by a user.
func (a API) CreateNewStickerSet(userID int64, name, title string, stickers []InputSticker, format StickerFormat, opts *NewStickerSetOptions) (res APIResponseBool, err error) {
func (a API) CreateNewStickerSet(userID int64, name, title string, stickers []InputSticker, opts *NewStickerSetOptions) (res APIResponseBool, err error) {
var vals = make(url.Values)
vals.Set("user_id", itoa(userID))
vals.Set("name", name)
vals.Set("title", title)
vals.Set("sticker_format", string(format))
return postStickers[APIResponseBool](a.base, "createNewStickerSet", addValues(vals, opts), stickers...)
return res, a.client.postStickers(a.base, "createNewStickerSet", addValues(vals, opts), &res, stickers...)
}
// AddStickerToSet is used to add a new sticker to a set created by the bot.
@@ -179,7 +162,7 @@ func (a API) AddStickerToSet(userID int64, name string, sticker InputSticker) (r
vals.Set("user_id", itoa(userID))
vals.Set("name", name)
return postStickers[APIResponseBool](a.base, "addStickerToSet", vals, sticker)
return res, a.client.postStickers(a.base, "addStickerToSet", vals, &res, sticker)
}
// SetStickerPositionInSet is used to move a sticker in a set created by the bot to a specific position.
@@ -188,7 +171,7 @@ func (a API) SetStickerPositionInSet(sticker string, position int) (res APIRespo
vals.Set("sticker", sticker)
vals.Set("position", itoa(int64(position)))
return get[APIResponseBase](a.base, "setStickerPositionInSet", vals)
return res, a.client.get(a.base, "setStickerPositionInSet", vals, &res)
}
// DeleteStickerFromSet is used to delete a sticker from a set created by the bot.
@@ -196,7 +179,18 @@ func (a API) DeleteStickerFromSet(sticker string) (res APIResponseBase, err erro
var vals = make(url.Values)
vals.Set("sticker", sticker)
return get[APIResponseBase](a.base, "deleteStickerFromSet", vals)
return res, a.client.get(a.base, "deleteStickerFromSet", vals, &res)
}
// ReplaceStickerInSet is used to replace an existing sticker in a sticker set with a new one.
// The method is equivalent to calling DeleteStickerFromSet, then AddStickerToSet, then SetStickerPositionInSet.
func (a API) ReplaceStickerInSet(userID int64, name string, old_sticker string, sticker InputSticker) (res APIResponseBool, err error) {
var vals = make(url.Values)
vals.Set("user_id", itoa(userID))
vals.Set("name", name)
vals.Set("old_sticker", old_sticker)
return res, a.client.postStickers(a.base, "replaceStickerInSet", vals, &res, sticker)
}
// SetStickerEmojiList is used to change the list of emoji assigned to a regular or custom emoji sticker.
@@ -208,7 +202,7 @@ func (a API) SetStickerEmojiList(sticker string, emojis []string) (res APIRespon
vals.Set("sticker", sticker)
vals.Set("emoji_list", string(jsn))
return get[APIResponseBool](a.base, "setStickerEmojiList", vals)
return res, a.client.get(a.base, "setStickerEmojiList", vals, &res)
}
// SetStickerKeywords is used to change search keywords assigned to a regular or custom emoji sticker.
@@ -220,7 +214,7 @@ func (a API) SetStickerKeywords(sticker string, keywords []string) (res APIRespo
vals.Set("sticker", sticker)
vals.Set("keywords", string(jsn))
return get[APIResponseBool](a.base, "setStickerKeywords", vals)
return res, a.client.get(a.base, "setStickerKeywords", vals, &res)
}
// SetStickerMaskPosition is used to change the mask position of a mask sticker.
@@ -232,7 +226,7 @@ func (a API) SetStickerMaskPosition(sticker string, mask MaskPosition) (res APIR
vals.Set("sticker", sticker)
vals.Set("mask_position", string(jsn))
return get[APIResponseBool](a.base, "setStickerMaskPosition", vals)
return res, a.client.get(a.base, "setStickerMaskPosition", vals, &res)
}
// SetStickerSetTitle is used to set the title of a created sticker set.
@@ -241,16 +235,17 @@ func (a API) SetStickerSetTitle(name, title string) (res APIResponseBool, err er
vals.Set("name", name)
vals.Set("title", title)
return get[APIResponseBool](a.base, "setStickerSetTitle", vals)
return res, a.client.get(a.base, "setStickerSetTitle", vals, &res)
}
// SetStickerSetThumbnail is used to set the thumbnail of a sticker set.
func (a API) SetStickerSetThumbnail(name string, userID int64, thumbnail InputFile) (res APIResponseBase, err error) {
func (a API) SetStickerSetThumbnail(name string, userID int64, thumbnail InputFile, format StickerFormat) (res APIResponseBase, err error) {
var vals = make(url.Values)
vals.Set("name", name)
vals.Set("user_id", itoa(userID))
return postFile[APIResponseBase](a.base, "setStickerSetThumbnail", "thumbnail", thumbnail, InputFile{}, vals)
vals.Set("format", string(format))
return res, a.client.postFile(a.base, "setStickerSetThumbnail", "thumbnail", thumbnail, InputFile{}, vals, &res)
}
// SetCustomEmojiStickerSetThumbnail is used to set the thumbnail of a custom emoji sticker set.
@@ -259,7 +254,7 @@ func (a API) SetCustomEmojiStickerSetThumbnail(name, emojiID string) (res APIRes
vals.Set("name", name)
vals.Set("custom_emoji_id", emojiID)
return get[APIResponseBool](a.base, "setCustomEmojiStickerSetThumbnail", vals)
return res, a.client.get(a.base, "setCustomEmojiStickerSetThumbnail", vals, &res)
}
// DeleteStickerSet is used to delete a sticker set that was created by the bot.
@@ -267,10 +262,10 @@ func (a API) DeleteStickerSet(name string) (res APIResponseBool, err error) {
var vals = make(url.Values)
vals.Set("name", name)
return get[APIResponseBool](a.base, "DeleteStickerSet", vals)
return res, a.client.get(a.base, "DeleteStickerSet", vals, &res)
}
// GetForumTopicIconStickers is used to get custom emoji stickers, which can be used as a forum topic icon by any user.
func (a API) GetForumTopicIconStickers() (res APIResponseStickers, err error) {
return get[APIResponseStickers](a.base, "getForumTopicIconStickers", nil)
return res, a.client.get(a.base, "getForumTopicIconStickers", nil, &res)
}

View File

@@ -23,28 +23,40 @@ import "encoding/json"
// Update represents an incoming update.
// At most one of the optional parameters can be present in any given update.
type Update struct {
ChatJoinRequest *ChatJoinRequest `json:"chat_join_request,omitempty"`
ChatBoost *ChatBoostUpdated `json:"chat_boost,omitempty"`
RemovedChatBoost *ChatBoostRemoved `json:"removed_chat_boost,omitempty"`
Message *Message `json:"message,omitempty"`
EditedMessage *Message `json:"edited_message,omitempty"`
ChannelPost *Message `json:"channel_post,omitempty"`
EditedChannelPost *Message `json:"edited_channel_post,omitempty"`
MessageReaction *MessageReactionUpdated `json:"message_reaction,omitempty"`
MessageReactionCount *MessageReactionCountUpdated `json:"message_reaction_count,omitempty"`
InlineQuery *InlineQuery `json:"inline_query,omitempty"`
ChosenInlineResult *ChosenInlineResult `json:"chosen_inline_result,omitempty"`
CallbackQuery *CallbackQuery `json:"callback_query,omitempty"`
ShippingQuery *ShippingQuery `json:"shipping_query,omitempty"`
PreCheckoutQuery *PreCheckoutQuery `json:"pre_checkout_query,omitempty"`
MyChatMember *ChatMemberUpdated `json:"my_chat_member,omitempty"`
ChatMember *ChatMemberUpdated `json:"chat_member,omitempty"`
ID int `json:"update_id"`
ChatJoinRequest *ChatJoinRequest `json:"chat_join_request,omitempty"`
ChatBoost *ChatBoostUpdated `json:"chat_boost,omitempty"`
RemovedChatBoost *ChatBoostRemoved `json:"removed_chat_boost,omitempty"`
Message *Message `json:"message,omitempty"`
EditedMessage *Message `json:"edited_message,omitempty"`
ChannelPost *Message `json:"channel_post,omitempty"`
EditedChannelPost *Message `json:"edited_channel_post,omitempty"`
BusinessConnection *BusinessConnection `json:"business_connection,omitempty"`
BusinessMessage *Message `json:"business_message,omitempty"`
EditedBusinessMessage *Message `json:"edited_business_message,omitempty"`
DeletedBusinessMessages *BusinessMessagesDeleted `json:"deleted_business_messages,omitempty"`
MessageReaction *MessageReactionUpdated `json:"message_reaction,omitempty"`
MessageReactionCount *MessageReactionCountUpdated `json:"message_reaction_count,omitempty"`
InlineQuery *InlineQuery `json:"inline_query,omitempty"`
ChosenInlineResult *ChosenInlineResult `json:"chosen_inline_result,omitempty"`
CallbackQuery *CallbackQuery `json:"callback_query,omitempty"`
ShippingQuery *ShippingQuery `json:"shipping_query,omitempty"`
PreCheckoutQuery *PreCheckoutQuery `json:"pre_checkout_query,omitempty"`
Poll *Poll `json:"poll,omitempty"`
PollAnswer *PollAnswer `json:"poll_answer,omitempty"`
MyChatMember *ChatMemberUpdated `json:"my_chat_member,omitempty"`
ChatMember *ChatMemberUpdated `json:"chat_member,omitempty"`
ID int `json:"update_id"`
}
// ChatID returns the ID of the chat the update is coming from.
func (u Update) ChatID() int64 {
switch {
case u.ChatJoinRequest != nil:
return u.ChatJoinRequest.Chat.ID
case u.ChatBoost != nil:
return u.ChatBoost.Chat.ID
case u.RemovedChatBoost != nil:
return u.RemovedChatBoost.Chat.ID
case u.Message != nil:
return u.Message.Chat.ID
case u.EditedMessage != nil:
@@ -53,6 +65,18 @@ func (u Update) ChatID() int64 {
return u.ChannelPost.Chat.ID
case u.EditedChannelPost != nil:
return u.EditedChannelPost.Chat.ID
case u.BusinessConnection != nil:
return u.BusinessConnection.User.ID
case u.BusinessMessage != nil:
return u.BusinessMessage.Chat.ID
case u.EditedBusinessMessage != nil:
return u.EditedBusinessMessage.Chat.ID
case u.DeletedBusinessMessages != nil:
return u.DeletedBusinessMessages.Chat.ID
case u.MessageReaction != nil:
return u.MessageReaction.Chat.ID
case u.MessageReactionCount != nil:
return u.MessageReactionCount.Chat.ID
case u.InlineQuery != nil:
return u.InlineQuery.From.ID
case u.ChosenInlineResult != nil:
@@ -63,12 +87,12 @@ func (u Update) ChatID() int64 {
return u.ShippingQuery.From.ID
case u.PreCheckoutQuery != nil:
return u.PreCheckoutQuery.From.ID
case u.PollAnswer != nil:
return u.PollAnswer.User.ID
case u.MyChatMember != nil:
return u.MyChatMember.Chat.ID
case u.ChatMember != nil:
return u.ChatMember.Chat.ID
case u.ChatJoinRequest != nil:
return u.ChatJoinRequest.Chat.ID
default:
return 0
}
@@ -215,9 +239,9 @@ func (a APIResponseString) Base() APIResponseBase {
}
// APIResponseChat represents the incoming response from Telegram servers.
// Used by all methods that return a Chat object on success.
// Used by all methods that return a ChatFullInfo object on success.
type APIResponseChat struct {
Result *Chat `json:"result,omitempty"`
Result *ChatFullInfo `json:"result,omitempty"`
APIResponseBase
}
@@ -443,7 +467,7 @@ func (a APIResponseBotName) Base() APIResponseBase {
}
// APIResponseUserChatBoosts represents the incoming response from Telegram servers.
// Used by all methods that return a BotName object on success.
// Used by all methods that return a UserChatBoosts object on success.
type APIResponseUserChatBoosts struct {
Result *UserChatBoosts `json:"result,omitempty"`
APIResponseBase
@@ -454,6 +478,30 @@ func (a APIResponseUserChatBoosts) Base() APIResponseBase {
return a.APIResponseBase
}
// APIResponseBusinessConnection represents the incoming response from Telegram servers.
// Used by all methods that return a BusinessConnection object on success.
type APIResponseBusinessConnection struct {
Result *BusinessConnection `json:"result,omitempty"`
APIResponseBase
}
// Base returns the contained object of type APIResponseBase.
func (a APIResponseBusinessConnection) Base() APIResponseBase {
return a.APIResponseBase
}
// APIResponseStarTransactions represents the incoming response from Telegram servers.
// Used by all methods that return a StarTransactions object on success.
type APIResponseStarTransactions struct {
Result *StarTransactions `json:"result,omitempty"`
APIResponseBase
}
// Base returns the contained object of type APIResponseBase.
func (a APIResponseStarTransactions) Base() APIResponseBase {
return a.APIResponseBase
}
// User represents a Telegram user or bot.
type User struct {
FirstName string `json:"first_name"`
@@ -465,49 +513,67 @@ type User struct {
CanJoinGroups bool `json:"can_join_groups,omitempty"`
CanReadAllGroupMessages bool `json:"can_read_all_group_messages,omitempty"`
SupportsInlineQueries bool `json:"supports_inline_queries,omitempty"`
CanConnectToBusiness bool `json:"can_connect_to_business,omitempty"`
IsPremium bool `json:"is_premium,omitempty"`
AddedToAttachmentMenu bool `json:"added_to_attachment_menu,omitempty"`
}
// Chat represents a chat.
type Chat struct {
Permissions *ChatPermissions `json:"permissions,omitempty"`
Location *ChatLocation `json:"location,omitempty"`
PinnedMessage *Message `json:"pinned_message,omitempty"`
Photo *ChatPhoto `json:"photo,omitempty"`
ActiveUsernames *[]string `json:"active_usernames,omitempty"`
AvailableReactions *[]ReactionType `json:"available_reactions,omitempty"`
BackgroundCustomEmojiID string `json:"background_custom_emoji_id,omitempty"`
ProfileBackgroundCustomEmojiID string `json:"profile_background_custom_emoji_id,omitempty"`
Bio string `json:"bio,omitempty"`
Username string `json:"username,omitempty"`
Title string `json:"title,omitempty"`
StickerSetName string `json:"sticker_set_name,omitempty"`
Description string `json:"description,omitempty"`
FirstName string `json:"first_name,omitempty"`
LastName string `json:"last_name,omitempty"`
InviteLink string `json:"invite_link,omitempty"`
EmojiStatusCustomEmojiID string `json:"emoji_status_custom_emoji_id,omitempty"`
Type string `json:"type"`
CustomEmojiStickerSetName string `json:"custom_emoji_sticker_set_name,omitempty"`
AccentColorID int `json:"accent_color_id,omitempty"`
ProfileAccentColorID int `json:"profile_accent_color_id,omitempty"`
EmojiStatusExpirationDate int `json:"emoji_status_expiration_date,omitempty"`
MessageAutoDeleteTime int `json:"message_auto_delete_time,omitempty"`
SlowModeDelay int `json:"slow_mode_delay,omitempty"`
UnrestrictBoostCount int `json:"unrestrict_boost_count,omitempty"`
LinkedChatID int64 `json:"linked_chat_id,omitempty"`
ID int64 `json:"id"`
IsForum bool `json:"is_forum,omitempty"`
HasAggressiveAntiSpamEnabled bool `json:"has_aggressive_anti_spam_enabled,omitempty"`
HasHiddenMembers bool `json:"has_hidden_members,omitempty"`
HasProtectedContent bool `json:"has_protected_content,omitempty"`
HasVisibleHistory bool `json:"has_visible_history,omitempty"`
HasPrivateForwards bool `json:"has_private_forwards,omitempty"`
CanSetStickerSet bool `json:"can_set_sticker_set,omitempty"`
JoinToSendMessages bool `json:"join_to_send_messages,omitempty"`
JoinByRequest bool `json:"join_by_request,omitempty"`
HasRestrictedVoiceAndVideoMessages bool `json:"has_restricted_voice_and_video_messages,omitempty"`
Type string `json:"type"`
Title string `json:"title,omitempty"`
Username string `json:"username,omitempty"`
FirstName string `json:"first_name,omitempty"`
LastName string `json:"last_name,omitempty"`
ID int64 `json:"id"`
IsForum bool `json:"is_forum,omitempty"`
}
// ChatFullInfo contains full information about a chat.
type ChatFullInfo struct {
Permissions *ChatPermissions `json:"permissions,omitempty"`
Location *ChatLocation `json:"location,omitempty"`
PinnedMessage *Message `json:"pinned_message,omitempty"`
Photo *ChatPhoto `json:"photo,omitempty"`
ActiveUsernames *[]string `json:"active_usernames,omitempty"`
AvailableReactions *[]ReactionType `json:"available_reactions,omitempty"`
BusinessIntro *BusinessIntro `json:"business_intro,omitempty"`
BusinessLocation *BusinessLocation `json:"business_location,omitempty"`
BusinessOpeningHours *BusinessOpeningHours `json:"business_opening_hours,omitempty"`
PersonalChat *Chat `json:"personal_chat,omitempty"`
Birthdate *Birthdate `json:"birthdate,omitempty"`
BackgroundCustomEmojiID string `json:"background_custom_emoji_id,omitempty"`
ProfileBackgroundCustomEmojiID string `json:"profile_background_custom_emoji_id,omitempty"`
Bio string `json:"bio,omitempty"`
Username string `json:"username,omitempty"`
Title string `json:"title,omitempty"`
StickerSetName string `json:"sticker_set_name,omitempty"`
Description string `json:"description,omitempty"`
FirstName string `json:"first_name,omitempty"`
LastName string `json:"last_name,omitempty"`
InviteLink string `json:"invite_link,omitempty"`
EmojiStatusCustomEmojiID string `json:"emoji_status_custom_emoji_id,omitempty"`
Type string `json:"type"`
CustomEmojiStickerSetName string `json:"custom_emoji_sticker_set_name,omitempty"`
AccentColorID int `json:"accent_color_id,omitempty"`
MaxReactionCount int `json:"max_reaction_count,omitempty"`
ProfileAccentColorID int `json:"profile_accent_color_id,omitempty"`
EmojiStatusExpirationDate int `json:"emoji_status_expiration_date,omitempty"`
MessageAutoDeleteTime int `json:"message_auto_delete_time,omitempty"`
SlowModeDelay int `json:"slow_mode_delay,omitempty"`
UnrestrictBoostCount int `json:"unrestrict_boost_count,omitempty"`
LinkedChatID int64 `json:"linked_chat_id,omitempty"`
ID int64 `json:"id"`
IsForum bool `json:"is_forum,omitempty"`
HasAggressiveAntiSpamEnabled bool `json:"has_aggressive_anti_spam_enabled,omitempty"`
HasHiddenMembers bool `json:"has_hidden_members,omitempty"`
HasProtectedContent bool `json:"has_protected_content,omitempty"`
HasVisibleHistory bool `json:"has_visible_history,omitempty"`
HasPrivateForwards bool `json:"has_private_forwards,omitempty"`
CanSetStickerSet bool `json:"can_set_sticker_set,omitempty"`
JoinToSendMessages bool `json:"join_to_send_messages,omitempty"`
JoinByRequest bool `json:"join_by_request,omitempty"`
HasRestrictedVoiceAndVideoMessages bool `json:"has_restricted_voice_and_video_messages,omitempty"`
}
// Message represents a message.
@@ -561,12 +627,16 @@ type Message struct {
LinkPreviewOptions *LinkPreviewOptions `json:"link_preview_options,omitempty"`
ForwardOrigin *MessageOrigin `json:"forward_origin,omitempty"`
BoostAdded *ChatBoostAdded `json:"boost_added,omitempty"`
ChatBackgroundSet *ChatBackground `json:"chat_background_set,omitempty"`
SenderBusinessBot *User `json:"sender_business_bot,omitempty"`
MediaGroupID string `json:"media_group_id,omitempty"`
ConnectedWebsite string `json:"connected_website,omitempty"`
NewChatTitle string `json:"new_chat_title,omitempty"`
AuthorSignature string `json:"author_signature,omitempty"`
Caption string `json:"caption,omitempty"`
Text string `json:"text,omitempty"`
BusinessConnectionID string `json:"business_connection_id,omitempty"`
EffectID string `json:"effect_id,omitempty"`
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
NewChatPhoto []*PhotoSize `json:"new_chat_photo,omitempty"`
NewChatMembers []*User `json:"new_chat_members,omitempty"`
@@ -588,6 +658,8 @@ type Message struct {
ChannelChatCreated bool `json:"channel_chat_created,omitempty"`
HasProtectedContent bool `json:"has_protected_content,omitempty"`
HasMediaSpoiler bool `json:"has_media_spoiler,omitempty"`
IsFromOffline bool `json:"is_from_offline,omitempty"`
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
}
// MessageID represents a unique message identifier.
@@ -701,8 +773,16 @@ type Dice struct {
// PollOption contains information about one answer option in a poll.
type PollOption struct {
Text string `json:"text"`
VoterCount int `json:"voter_count"`
Text string `json:"text"`
TextEntities []*MessageEntity `json:"text_entities,omitempty"`
VoterCount int `json:"voter_count"`
}
// InputPollOption contains information about one answer option in a poll to send.
type InputPollOption struct {
Text string `json:"text"`
TextParseMode ParseMode `json:"text_parse_mode,omitempty"`
TextEntities []*MessageEntity `json:"text_entities,omitempty"`
}
// PollAnswer represents an answer of a user in a non-anonymous poll.
@@ -720,6 +800,7 @@ type Poll struct {
Explanation string `json:"explanation,omitempty"`
ID string `json:"id"`
ExplanationEntities []*MessageEntity `json:"explanation_entities,omitempty"`
QuestionEntities []*MessageEntity `json:"question_entities,omitempty"`
Options []*PollOption `json:"options"`
OpenPeriod int `json:"open_period,omitempty"`
TotalVoterCount int `json:"total_voter_count"`
@@ -885,12 +966,13 @@ type ChatMember struct {
// ChatMemberUpdated represents changes in the status of a chat member.
type ChatMemberUpdated struct {
InviteLink *ChatInviteLink `json:"invite_link,omitempty"`
Chat Chat `json:"chat"`
From User `json:"from"`
OldChatMember ChatMember `json:"old_chat_member"`
NewChatMember ChatMember `json:"new_chat_member"`
Chat Chat `json:"chat"`
ViaChatFolderInviteLink bool `json:"via_chat_folder_invite_link,omitempty"`
Date int `json:"date"`
ViaChatFolderInviteLink bool `json:"via_chat_folder_invite_link,omitempty"`
ViaJoinRequest bool `json:"via_join_request,omitempty"`
}
// ChatPermissions describes actions that a non-administrator user is allowed to take in a chat.
@@ -911,6 +993,38 @@ type ChatPermissions struct {
CanManageTopics bool `json:"can_manage_topics,omitempty"`
}
// Birthdate
type Birthdate struct {
Day int `json:"day"`
Month int `json:"month"`
Year int `json:"year"`
}
// BusinessIntro
type BusinessIntro struct {
Sticker *Sticker `json:"sticker,omitempty"`
Title string `json:"title,omitempty"`
Message string `json:"message,omitempty"`
}
// BusinessLocation
type BusinessLocation struct {
Location *Location `json:"location,omitempty"`
Address string `json:"address"`
}
// BusinessOpeningHoursInterval
type BusinessOpeningHoursInterval struct {
OpeningMinute int `json:"opening_minute"`
ClosingMinute int `json:"closing_minute"`
}
// BusinessOpeningHours
type BusinessOpeningHours struct {
TimeZoneName string `json:"time_zone_name"`
OpeningHours []BusinessOpeningHoursInterval `json:"opening_hours"`
}
// ChatLocation represents a location to which a chat is connected.
type ChatLocation struct {
Location *Location `json:"location"`
@@ -1025,12 +1139,13 @@ func (i mediaEnvelope) MarshalJSON() (cnt []byte, err error) {
// InputMediaPhoto represents a photo to be sent.
// Type MUST BE "photo".
type InputMediaPhoto struct {
Type InputMediaType `json:"type"`
Media InputFile `json:"-"`
Caption string `json:"caption,omitempty"`
ParseMode ParseMode `json:"parse_mode,omitempty"`
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
HasSpoiler bool `json:"has_spoiler,omitempty"`
Type InputMediaType `json:"type"`
Media InputFile `json:"-"`
Caption string `json:"caption,omitempty"`
ParseMode ParseMode `json:"parse_mode,omitempty"`
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
HasSpoiler bool `json:"has_spoiler,omitempty"`
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
}
// media is a method which allows to obtain the Media (type InputFile) field from the InputMedia* struct.
@@ -1045,17 +1160,18 @@ func (i InputMediaPhoto) groupable() {}
// InputMediaVideo represents a video to be sent.
// Type MUST BE "video".
type InputMediaVideo struct {
Type InputMediaType `json:"type"`
Media InputFile `json:"-"`
Thumbnail InputFile `json:"-"`
Caption string `json:"caption,omitempty"`
ParseMode ParseMode `json:"parse_mode,omitempty"`
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
Duration int `json:"duration,omitempty"`
SupportsStreaming bool `json:"supports_streaming,omitempty"`
HasSpoiler bool `json:"has_spoiler,omitempty"`
Type InputMediaType `json:"type"`
Media InputFile `json:"-"`
Thumbnail InputFile `json:"-"`
Caption string `json:"caption,omitempty"`
ParseMode ParseMode `json:"parse_mode,omitempty"`
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
Duration int `json:"duration,omitempty"`
SupportsStreaming bool `json:"supports_streaming,omitempty"`
HasSpoiler bool `json:"has_spoiler,omitempty"`
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
}
// media is a method which allows to obtain the Media (type InputFile) field from the InputMedia* struct.
@@ -1070,16 +1186,17 @@ func (i InputMediaVideo) groupable() {}
// InputMediaAnimation represents an animation file (GIF or H.264/MPEG-4 AVC video without sound) to be sent.
// Type MUST BE "animation".
type InputMediaAnimation struct {
Type InputMediaType `json:"type"`
Media InputFile `json:"-"`
Thumbnail InputFile `json:"-"`
Caption string `json:"caption,omitempty"`
ParseMode ParseMode `json:"parse_mode,omitempty"`
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
Duration int `json:"duration,omitempty"`
HasSpoiler bool `json:"has_spoiler,omitempty"`
Type InputMediaType `json:"type"`
Media InputFile `json:"-"`
Thumbnail InputFile `json:"-"`
Caption string `json:"caption,omitempty"`
ParseMode ParseMode `json:"parse_mode,omitempty"`
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
Duration int `json:"duration,omitempty"`
HasSpoiler bool `json:"has_spoiler,omitempty"`
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
}
// media is a method which allows to obtain the Media (type InputFile) field from the InputMedia* struct.
@@ -1172,8 +1289,8 @@ type BotName struct {
type ChatJoinRequest struct {
InviteLink *ChatInviteLink `json:"invite_link,omitempty"`
Bio string `json:"bio,omitempty"`
From User `json:"user"`
Chat Chat `json:"chat"`
From User `json:"user"`
Date int `json:"date"`
UserChatID int64 `json:"user_chat_id"`
}
@@ -1183,6 +1300,90 @@ type ChatBoostAdded struct {
BoostCount int `json:"boost_count"`
}
// BackgroundFill describes the way a background is filled based on the selected colors.
type BackgroundFill interface {
ImplementsBackgroundFill()
}
// BackgroundFillSolid is a background filled using the selected color.
// Type MUST be "solid".
type BackgroundFillSolid struct {
Type string `json:"type"`
Color int `json:"color"`
}
func (b BackgroundFillSolid) ImplementsBackgroundFill() {}
// BackgroundFillGradient is a background with a gradient fill.
// Type MUST be "gradient".
type BackgroundFillGradient struct {
Type string `json:"type"`
TopColor int `json:"top_color"`
BottomColor int `json:"bottom_color"`
RotationAngle int `json:"rotation_angle"`
}
func (b BackgroundFillGradient) ImplementsBackgroundFill() {}
// BackgroundFillFreeformGradient is a background with a freeform gradient that rotates after every message in the chat.
// Type MUST be "freeform_gradient".
type BackgroundFillFreeformGradient struct {
Type string `json:"type"`
Colors []int `json:"colors"`
}
func (b BackgroundFillFreeformGradient) ImplementsBackgroundFill() {}
// BackgroundType describes the type of a background.
type BackgroundType interface {
ImplementsBackgroundType()
}
// BackgroundTypeFill is a background which is automatically filled based on the selected colors.
// Type MUST be "fill".
type BackgroundTypeFill struct {
Fill BackgroundFill `json:"fill"`
Type string `json:"type"`
DarkThemeDimming int `json:"dark_theme_dimming"`
}
func (b BackgroundTypeFill) ImplementsBackgroundType() {}
// BackgroundTypeWallpaper is a background which is a wallpaper in the JPEG format.
// Type MUST be "wallpaper".
type BackgroundTypeWallpaper struct {
Type string `json:"type"`
Document Document `json:"document"`
DarkThemeDimming int `json:"dark_theme_dimming"`
IsBlurred bool `json:"is_blurred,omitempty"`
IsMoving bool `json:"is_moving,omitempty"`
}
func (b BackgroundTypeWallpaper) ImplementsBackgroundType() {}
// BackgroundTypePattern is a PNG or TGV (gzipped subset of SVG with MIME type “application/x-tgwallpattern”) pattern
// to be combined with the background fill chosen by the user.
// Type MUST be "pattern".
type BackgroundTypePattern struct {
Fill BackgroundFill `json:"fill"`
Type string `json:"type"`
Document Document `json:"document"`
Intensity int `json:"intensity"`
IsInverted bool `json:"is_inverted,omitempty"`
IsMoving bool `json:"is_moving,omitempty"`
}
func (b BackgroundTypePattern) ImplementsBackgroundType() {}
// BackgroundTypeChatTheme is taken directly from a built-in chat theme.
// Type MUST be "chat_theme".
type BackgroundTypeChatTheme struct {
Type string `json:"type"`
ThemeName string `json:"theme_name"`
}
func (b BackgroundTypeChatTheme) ImplementsBackgroundType() {}
// ForumTopicCreated represents a service message about a new forum topic created in the chat.
type ForumTopicCreated struct {
Name string `json:"name"`
@@ -1190,6 +1391,11 @@ type ForumTopicCreated struct {
IconColor int `json:"icon_color"`
}
// ChatBackground represents a chat background.
type ChatBackground struct {
Type BackgroundType `json:"type"`
}
// ForumTopicClosed represents a service message about a forum topic closed in the chat.
type ForumTopicClosed struct{}
@@ -1210,8 +1416,8 @@ type GeneralForumTopicUnhidden struct{}
// WriteAccessAllowed represents a service message about a user allowing a bot added to the attachment menu to write messages.
type WriteAccessAllowed struct {
FromRequest bool `json:"from_request,omitempty"`
WebAppName string `json:"web_app_name,omitempty"`
FromRequest bool `json:"from_request,omitempty"`
FromAttachmentMenu bool `json:"from_attachment_menu,omitempty"`
}
@@ -1244,8 +1450,11 @@ type UserShared struct {
// ChatShared contains information about the chat whose identifier was shared with the bot using a KeyboardButtonRequestChat button.
type ChatShared struct {
RequestID int `json:"request_id"`
ChatID int64 `json:"chat_id"`
Photo *[]PhotoSize `json:"photo,omitempty"`
Title string `json:"title,omitempty"`
Username string `json:"username,omitempty"`
RequestID int `json:"request_id"`
ChatID int64 `json:"chat_id"`
}
// Story represents a story.
@@ -1254,28 +1463,12 @@ type Story struct {
ID int64 `json:"id"`
}
type ReactionType interface {
ImplementsReactionType()
}
// ReactionTypeEmoji is based on an emoji.
// Type is always "emoji".
type ReactionTypeEmoji struct {
Type string `json:"type"`
Emoji string `json:"emoji"`
}
func (i ReactionTypeEmoji) ImplementsReactionType() {}
// ReactionTypeCustomEmoji is based on a custom emoji.
// Type is always "custom_emoji".
type ReactionTypeCustomEmoji struct {
type ReactionType struct {
Type string `json:"type"`
Emoji string `json:"emoji"`
CustomEmoji string `json:"custom_emoji"`
}
func (i ReactionTypeCustomEmoji) ImplementsReactionType() {}
// ReactionCount represents a reaction added to a message along with the number of times it was added.
type ReactionCount struct {
Type ReactionType `json:"type"`
@@ -1285,71 +1478,71 @@ type ReactionCount struct {
// MessageReactionUpdated represents a change of a reaction on a message performed by a user.
type MessageReactionUpdated struct {
Chat Chat `json:"chat"`
MessageID int `json:"message_id"`
User User `json:"user,omitempty"`
ActorChat Chat `json:"actor_chat,omitempty"`
Date int `json:"date"`
OldReaction []ReactionType `json:"old_reaction"`
NewReaction []ReactionType `json:"new_reaction"`
User User `json:"user,omitempty"`
MessageID int `json:"message_id"`
Date int `json:"date"`
}
// MessageReactionCountUpdated represents reaction changes on a message with anonymous reactions.
type MessageReactionCountUpdated struct {
Reactions []ReactionCount `json:"reactions"`
Chat Chat `json:"chat"`
MessageID int `json:"message_id"`
Date int `json:"date"`
Reactions []ReactionCount `json:"reactions"`
}
// TextQuote contains information about the quoted part of a message that is replied to by the given message.
type TextQuote struct {
Entities *[]MessageEntity `json:"entities,omitempty"`
Text string `json:"text"`
Position int `json:"position"`
IsManual bool `json:"is_manual,omitempty"`
Text string `json:"text"`
Entities *[]MessageEntity `json:"entities,omitempty"`
}
// ExternalReplyInfo contains information about a message that is being replied to, which may come from another chat or forum topic.
type ExternalReplyInfo struct {
Origin MessageOrigin `json:"origin"`
Chat Chat `json:"chat,omitempty"`
MessageID int `json:"message_id,omitempty"`
LinkPreviewOptions LinkPreviewOptions `json:"link_preview_options,omitempty"`
Animation Animation `json:"animation,omitempty"`
Audio Audio `json:"audio,omitempty"`
Document Document `json:"document,omitempty"`
Photo []PhotoSize `json:"photo,omitempty"`
Sticker Sticker `json:"sticker,omitempty"`
Story Story `json:"story,omitempty"`
Video Video `json:"video,omitempty"`
VideoNote VideoNote `json:"video_note,omitempty"`
Voice Voice `json:"voice,omitempty"`
HasMediaSpoiler bool `json:"has_media_spoiler,omitempty"`
Contact Contact `json:"contact,omitempty"`
Dice Dice `json:"dice,omitempty"`
Game Game `json:"game,omitempty"`
Giveaway Giveaway `json:"giveaway,omitempty"`
GiveawayWinners GiveawayWinners `json:"giveaway_winners,omitempty"`
Invoice Invoice `json:"invoice,omitempty"`
Location Location `json:"location,omitempty"`
Poll Poll `json:"poll,omitempty"`
Venue Venue `json:"venue,omitempty"`
Chat Chat `json:"chat,omitempty"`
Document Document `json:"document,omitempty"`
Origin MessageOrigin `json:"origin"`
Contact Contact `json:"contact,omitempty"`
Invoice Invoice `json:"invoice,omitempty"`
Dice Dice `json:"dice,omitempty"`
LinkPreviewOptions LinkPreviewOptions `json:"link_preview_options,omitempty"`
Photo []PhotoSize `json:"photo,omitempty"`
Audio Audio `json:"audio,omitempty"`
Story Story `json:"story,omitempty"`
Voice Voice `json:"voice,omitempty"`
VideoNote VideoNote `json:"video_note,omitempty"`
Game Game `json:"game,omitempty"`
Video Video `json:"video,omitempty"`
Animation Animation `json:"animation,omitempty"`
Sticker Sticker `json:"sticker,omitempty"`
Giveaway Giveaway `json:"giveaway,omitempty"`
Poll Poll `json:"poll,omitempty"`
GiveawayWinners GiveawayWinners `json:"giveaway_winners,omitempty"`
Location Location `json:"location,omitempty"`
MessageID int `json:"message_id,omitempty"`
HasMediaSpoiler bool `json:"has_media_spoiler,omitempty"`
}
// MessageOrigin describes the origin of a message.
type MessageOrigin struct {
Date int `json:"date"`
SenderChat *Chat `json:"sender_chat,omitempty"`
SenderUser *User `json:"sender_user,omitempty"`
Type string `json:"type"`
SenderUserName string `json:"sender_user_name,omitempty"` // Only for MessageOriginHiddenUser
AuthorSignature string `json:"author_signature,omitempty"` // Only for MessageOriginChannel
SenderChat *Chat `json:"sender_chat,omitempty"` // Only for MessageOriginChat
SenderUser *User `json:"sender_user,omitempty"` // Only for MessageOriginUser
SenderUserName string `json:"sender_user_name,omitempty"`
AuthorSignature string `json:"author_signature,omitempty"`
Date int `json:"date"`
}
// LinkPreviewOptions describes the options used for link preview generation.
type LinkPreviewOptions struct {
IsDisabled bool `json:"is_disabled,omitempty"`
URL string `json:"url,omitempty"`
IsDisabled bool `json:"is_disabled,omitempty"`
PreferSmallMedia bool `json:"prefer_small_media,omitempty"`
PreferLargeMedia bool `json:"prefer_large_media,omitempty"`
ShowAboveText bool `json:"show_above_text,omitempty"`
@@ -1357,27 +1550,36 @@ type LinkPreviewOptions struct {
// ReplyParameters describes reply parameters for the message that is being sent.
type ReplyParameters struct {
MessageID int `json:"message_id"`
ChatID int64 `json:"chat_id,omitempty"`
AllowSendingWithoutReply bool `json:"allow_sending_without_reply,omitempty"`
Quote string `json:"quote,omitempty"`
QuoteParseMode string `json:"quote_parse_mode,omitempty"`
QuoteEntities []MessageEntity `json:"quote_entitites,omitempty"`
MessageID int `json:"message_id"`
ChatID int64 `json:"chat_id,omitempty"`
QuotePosition int `json:"quote_position,omitempty"`
AllowSendingWithoutReply bool `json:"allow_sending_without_reply,omitempty"`
}
// SharedUser contains information about a user that was shared with the bot using a KeyboardButtonRequestUser button.
type SharedUser struct {
Photo *[]PhotoSize `json:"photo,omitempty"`
FirstName string `json:"firstname,omitempty"`
LastName string `json:"lastname,omitempty"`
Username string `json:"username,omitempty"`
UserID int64 `json:"user_id"`
}
// UsersShared contains information about the users whose identifiers were shared with the bot using a KeyboardButtonRequestUsers button.
type UsersShared struct {
RequestID int `json:"request_id"`
UserIDs []int64 `json:"user_ids"`
Users []SharedUser `json:"users"`
RequestID int `json:"request_id"`
}
// ChatBoost contains information about a chat boost.
type ChatBoost struct {
BoostID string `json:"boost_id"`
Source ChatBoostSource `json:"source"`
AddDate int `json:"add_date"`
ExpirationDate int `json:"expiration_date"`
Source ChatBoostSource `json:"source"`
}
// ChatBoostSourceType is a custom type for the various chat boost sources.
@@ -1392,8 +1594,8 @@ const (
// ChatBoostSource describes the source of a chat boost.
type ChatBoostSource struct {
Source ChatBoostSourceType `json:"source"`
User *User `json:"user,omitempty"`
Source ChatBoostSourceType `json:"source"`
GiveawayMessageID int `json:"giveaway_message_id,omitempty"`
IsUnclaimed bool `json:"is_unclaimed,omitempty"`
}
@@ -1406,10 +1608,10 @@ type ChatBoostUpdated struct {
// ChatBoostRemoved represents a boost removed from a chat.
type ChatBoostRemoved struct {
Chat Chat `json:"chat"`
BoostID string `json:"boost_id"`
RemoveDate int `json:"remove_date"`
Chat Chat `json:"chat"`
Source ChatBoostSource `json:"source"`
RemoveDate int `json:"remove_date"`
}
// UserChatBoosts represents a list of boosts added to a chat by a user.
@@ -1417,39 +1619,57 @@ type UserChatBoosts struct {
Boosts []ChatBoost `json:"boosts"`
}
// BusinessConnection describes the connection of the bot with a business account.
type BusinessConnection struct {
ID string `json:"id"`
User User `json:"user"`
UserChatID int64 `json:"user_chat_id"`
Date int64 `json:"date"`
CanReply bool `json:"can_reply"`
IsEnabled bool `json:"is_enabled"`
}
// BusinessMessagesDeleted is received when messages are deleted from a connected business account.
type BusinessMessagesDeleted struct {
BusinessConnectionID string `json:"business_connection_id"`
MessageIDs []int `json:"message_ids"`
Chat Chat `json:"chat"`
}
// Giveaway represents a message about a scheduled giveaway.
type Giveaway struct {
CountryCodes *[]string `json:"country_codes,omitempty"`
PrizeDescription string `json:"prize_description,omitempty"`
Chats []Chat `json:"chats"`
WinnersSelectionDate int `json:"winners_selection_date"`
WinnerCount int `json:"winner_count"`
PremiumSubscriptionMonthCount int `json:"premium_subscription_month_count,omitempty"`
OnlyNewMembers bool `json:"only_new_members,omitempty"`
HasPublicWinners bool `json:"has_public_winners,omitempty"`
PrizeDescription string `json:"prize_description,omitempty"`
CountryCodes *[]string `json:"country_codes,omitempty"`
PremiumSubscriptionMonthCount int `json:"premium_subscription_month_count,omitempty"`
}
// GiveawayCreated represents a service message about the creation of a scheduled giveaway. Currently holds no information.
// GiveawayCreated represents a service message about the creation of a scheduled giveaway.
// Currently holds no information.
type GiveawayCreated struct{}
// GiveawayWinners represents a message about the completion of a giveaway with public winners.
type GiveawayWinners struct {
PrizeDescription string `json:"prize_description,omitempty"`
Chats []Chat `json:"chats"`
Winners []User `json:"winners"`
GiveawayMessageID int `json:"giveaway_message_id"`
WinnersSelectionDate int `json:"winners_selection_date"`
WinnerCount int `json:"winner_count"`
Winners []User `json:"winners"`
AdditionalChatCount int `json:"additional_chat_count,omitempty"`
PremiumSubscriptionMonthCount int `json:"premium_subscription_month_count,omitempty"`
UnclaimedPrizeCount int `json:"unclaimed_prize_count,omitempty"`
OnlyNewMembers bool `json:"only_new_members,omitempty"`
WasRefunded bool `json:"was_refunded,omitempty"`
PrizeDescription string `json:"prize_description,omitempty"`
}
// GiveawayCompleted represents a service message about the completion of a giveaway without public winners.
type GiveawayCompleted struct {
GiveawayMessage *Message `json:"giveaway_message,omitempty"`
WinnerCount int `json:"winner_count"`
UnclaimedPrizeCount int `json:"unclaimed_prize_count,omitempty"`
GiveawayMessage *Message `json:"giveaway_message,omitempty"`
}

View File

@@ -53,5 +53,5 @@ func (a API) AnswerWebAppQuery(webAppQueryID string, result InlineQueryResult) (
vals.Set("web_app_query_id", webAppQueryID)
vals.Set("result", string(resultJson))
return get[APIResponseSentWebAppMessage](a.base, "answerWebAppQuery", vals)
return res, a.client.get(a.base, "answerWebAppQuery", vals, &res)
}

View File

@@ -70,3 +70,5 @@ benchstat <(go test -benchtime 500ms -count 15 -bench 'Sum64$')
- [VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics)
- [FreeCache](https://github.com/coocood/freecache)
- [FastCache](https://github.com/VictoriaMetrics/fastcache)
- [Ristretto](https://github.com/dgraph-io/ristretto)
- [Badger](https://github.com/dgraph-io/badger)

View File

@@ -19,10 +19,13 @@ const (
// Store the primes in an array as well.
//
// The consts are used when possible in Go code to avoid MOVs but we need a
// contiguous array of the assembly code.
// contiguous array for the assembly code.
var primes = [...]uint64{prime1, prime2, prime3, prime4, prime5}
// Digest implements hash.Hash64.
//
// Note that a zero-valued Digest is not ready to receive writes.
// Call Reset or create a Digest using New before calling other methods.
type Digest struct {
v1 uint64
v2 uint64
@@ -33,19 +36,31 @@ type Digest struct {
n int // how much of mem is used
}
// New creates a new Digest that computes the 64-bit xxHash algorithm.
// New creates a new Digest with a zero seed.
func New() *Digest {
return NewWithSeed(0)
}
// NewWithSeed creates a new Digest with the given seed.
func NewWithSeed(seed uint64) *Digest {
var d Digest
d.Reset()
d.ResetWithSeed(seed)
return &d
}
// Reset clears the Digest's state so that it can be reused.
// It uses a seed value of zero.
func (d *Digest) Reset() {
d.v1 = primes[0] + prime2
d.v2 = prime2
d.v3 = 0
d.v4 = -primes[0]
d.ResetWithSeed(0)
}
// ResetWithSeed clears the Digest's state so that it can be reused.
// It uses the given seed to initialize the state.
func (d *Digest) ResetWithSeed(seed uint64) {
d.v1 = seed + prime1 + prime2
d.v2 = seed + prime2
d.v3 = seed
d.v4 = seed - prime1
d.total = 0
d.n = 0
}

View File

@@ -6,7 +6,7 @@
package xxhash
// Sum64 computes the 64-bit xxHash digest of b.
// Sum64 computes the 64-bit xxHash digest of b with a zero seed.
//
//go:noescape
func Sum64(b []byte) uint64

View File

@@ -3,7 +3,7 @@
package xxhash
// Sum64 computes the 64-bit xxHash digest of b.
// Sum64 computes the 64-bit xxHash digest of b with a zero seed.
func Sum64(b []byte) uint64 {
// A simpler version would be
// d := New()

View File

@@ -5,7 +5,7 @@
package xxhash
// Sum64String computes the 64-bit xxHash digest of s.
// Sum64String computes the 64-bit xxHash digest of s with a zero seed.
func Sum64String(s string) uint64 {
return Sum64([]byte(s))
}

View File

@@ -33,7 +33,7 @@ import (
//
// See https://github.com/golang/go/issues/42739 for discussion.
// Sum64String computes the 64-bit xxHash digest of s.
// Sum64String computes the 64-bit xxHash digest of s with a zero seed.
// It may be faster than Sum64([]byte(s)) by avoiding a copy.
func Sum64String(s string) uint64 {
b := *(*[]byte)(unsafe.Pointer(&sliceHeader{s, len(s)}))

View File

@@ -25,7 +25,7 @@ import (
// WithContextTags captures the k/v pairs stored in the context via the
// `logtags` package and annotates them on the error.
//
// Only the stromg representation of values remains available. This is
// Only the strong representation of values remains available. This is
// because the library cannot guarantee that the underlying value is
// preserved across the network. To avoid creating a stateful interface
// (where the user code needs to know whether an error has traveled

View File

@@ -14,6 +14,6 @@
// Package errutil combines primitives from the library, the Go errors
// package and github.com/pkg/errors. It aims to serve as drop-in
// replacement to gitub.com/pkg/errors and replace the legacy
// replacement to github.com/pkg/errors and replace the legacy
// implementation of `pgerrors`.
package errutil

View File

@@ -179,6 +179,13 @@ func IsAny(err error, references ...error) bool {
return true
}
}
// Recursively try multi-error causes, if applicable.
for _, me := range errbase.UnwrapMulti(c) {
if IsAny(me, references...) {
return true
}
}
}
// Try harder with marks.

25
vendor/github.com/cockroachdb/fifo/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,25 @@
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
go.work.sum
.idea/

201
vendor/github.com/cockroachdb/fifo/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

21
vendor/github.com/cockroachdb/fifo/invariants_off.go generated vendored Normal file
View File

@@ -0,0 +1,21 @@
// Copyright 2024 The Cockroach Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
// implied. See the License for the specific language governing
// permissions and limitations under the License.
//go:build !fifo_invariants
package fifo
// invariants is false if we were not built with the "fifo_invariants" build
// tag.
const invariants = false

20
vendor/github.com/cockroachdb/fifo/invariants_on.go generated vendored Normal file
View File

@@ -0,0 +1,20 @@
// Copyright 2024 The Cockroach Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
// implied. See the License for the specific language governing
// permissions and limitations under the License.
//go:build fifo_invariants
package fifo
// invariants is true if we were built with the "fifo_invariants" build tag.
const invariants = true

162
vendor/github.com/cockroachdb/fifo/queue.go generated vendored Normal file
View File

@@ -0,0 +1,162 @@
// Copyright 2024 The Cockroach Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
// implied. See the License for the specific language governing
// permissions and limitations under the License.
package fifo
import "sync"
// Queue implements an allocation efficient FIFO queue. It is not safe for
// concurrent access.
//
// Note that the queue provides pointer access to the internal storage (via
// PeekFront and PushBack) so it must be used with care. These pointers must not
// be used once the respective element is popped out of the queue.
//
// -- Implementation --
//
// The queue is implemented as a linked list of nodes, where each node is a
// small ring buffer. The nodes are allocated using a sync.Pool (a single pool
// is created for any given type and is used for all queues of that type).
type Queue[T any] struct {
len int
head, tail *queueNode[T]
pool *QueueBackingPool[T]
}
// MakeQueue constructs a new Queue.
//
// The pool should be a singleton object initialized with MakeQueueBackingPool.
// A single pool can and should be used by all queues of that type.
func MakeQueue[T any](pool *QueueBackingPool[T]) Queue[T] {
return Queue[T]{
pool: pool,
}
}
// Len returns the current length of the queue.
func (q *Queue[T]) Len() int {
return q.len
}
// PushBack adds t to the end of the queue.
//
// The returned pointer can be used to modify the element while it is in the
// queue; it is valid until the element is removed from the queue.
func (q *Queue[T]) PushBack(t T) *T {
if q.head == nil {
q.head = q.pool.get()
q.tail = q.head
} else if q.tail.IsFull() {
newTail := q.pool.get()
q.tail.next = newTail
q.tail = newTail
}
q.len++
return q.tail.PushBack(t)
}
// PeekFront returns the current head of the queue, or nil if the queue is
// empty.
//
// The result is only valid until the next call to PopFront.
func (q *Queue[T]) PeekFront() *T {
if q.len == 0 {
return nil
}
return q.head.PeekFront()
}
// PopFront removes the current head of the queue.
//
// It is illegal to call PopFront on an empty queue.
func (q *Queue[T]) PopFront() {
q.head.PopFront()
// If this is the only node, we don't want to release it; otherwise we would
// allocate/free a node every time we transition between the queue being empty
// and non-empty.
if q.head.len == 0 && q.head.next != nil {
oldHead := q.head
q.head = oldHead.next
q.pool.put(oldHead)
}
q.len--
}
// QueueBackingPool is a sync.Pool that used to allocate internal nodes
// for Queue[T].
type QueueBackingPool[T any] struct {
pool sync.Pool
}
// MakeQueueBackingPool makes a queue backing pool. It is intented to be used to
// initialize a singleton (global) variable. A single pool can and should be
// used by all queues of that type.
func MakeQueueBackingPool[T any]() QueueBackingPool[T] {
return QueueBackingPool[T]{
pool: sync.Pool{
New: func() interface{} { return &queueNode[T]{} },
},
}
}
func (qp *QueueBackingPool[T]) get() *queueNode[T] {
return qp.pool.Get().(*queueNode[T])
}
func (qp *QueueBackingPool[T]) put(n *queueNode[T]) {
*n = queueNode[T]{}
qp.pool.Put(n)
}
// We batch the allocation of this many queue objects. The value was chosen
// without experimentation - it provides a reasonable amount of amortization
// without a very large increase in memory overhead if T is large.
const queueNodeSize = 8
type queueNode[T any] struct {
buf [queueNodeSize]T
head, len int32
next *queueNode[T]
}
func (qn *queueNode[T]) IsFull() bool {
return qn.len == queueNodeSize
}
func (qn *queueNode[T]) PushBack(t T) *T {
if invariants && qn.len >= queueNodeSize {
panic("cannot push back into a full node")
}
i := (qn.head + qn.len) % queueNodeSize
qn.buf[i] = t
qn.len++
return &qn.buf[i]
}
func (qn *queueNode[T]) PeekFront() *T {
return &qn.buf[qn.head]
}
func (qn *queueNode[T]) PopFront() T {
if invariants && qn.len == 0 {
panic("cannot pop from empty queue")
}
t := qn.buf[qn.head]
var zero T
qn.buf[qn.head] = zero
qn.head = (qn.head + 1) % queueNodeSize
qn.len--
return t
}

239
vendor/github.com/cockroachdb/fifo/semaphore.go generated vendored Normal file
View File

@@ -0,0 +1,239 @@
// Copyright 2024 The Cockroach Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
// implied. See the License for the specific language governing
// permissions and limitations under the License.
package fifo
import (
"context"
"errors"
"fmt"
"sync"
)
// Semaphore implements a weighted, dynamically reconfigurable semaphore which
// respects context cancellation.
//
// The semaphore implements a FIFO policy, where Acquire requests are satisfied
// in order. This policy provides fairness and prevents starvation but is
// susceptible to head-of-line blocking, where a large request that can't be
// satisfied blocks many other small requests that could be.
type Semaphore struct {
mu struct {
sync.Mutex
capacity int64
// outstanding can exceed capacity if the capacity is dynamically decreased.
outstanding int64
waiters Queue[semaWaiter]
// numCanceled is the number of waiters in the waiters queue which have been
// canceled. It is used to determine the current number of active waiters in
// the queue which is waiters.Len() minus this value.
numCanceled int
// numHadToWait accumulates the total number of Acquire requests which had
// to wait because the semaphore was exhausted.
numHadToWait int64
}
}
// NewSemaphore creates a new semaphore with the given capacity.
func NewSemaphore(capacity int64) *Semaphore {
if capacity <= 0 {
panic("invalid capacity")
}
s := &Semaphore{}
s.mu.capacity = capacity
s.mu.waiters = MakeQueue[semaWaiter](&semaQueuePool)
return s
}
var semaQueuePool = MakeQueueBackingPool[semaWaiter]()
var ErrRequestExceedsCapacity = errors.New("request exceeds semaphore capacity")
// TryAcquire attempts to acquire n units from the semaphore without waiting. On
// success, returns true and the caller must later Release the units.
func (s *Semaphore) TryAcquire(n int64) bool {
s.mu.Lock()
defer s.mu.Unlock()
if s.numWaitersLocked() == 0 && s.mu.outstanding+n <= s.mu.capacity {
s.mu.outstanding += n
return true
}
return false
}
// Acquire n units from the semaphore, waiting if necessary.
//
// If the context is canceled while we are waiting, returns the context error.
// If n exceeds the current capacity, returns ErrRequestExceedsCapacity.
// On success, the caller must later Release the units.
func (s *Semaphore) Acquire(ctx context.Context, n int64) error {
s.mu.Lock()
// Fast path.
if s.numWaitersLocked() == 0 && s.mu.outstanding+n <= s.mu.capacity {
s.mu.outstanding += n
s.mu.Unlock()
return nil
}
if n > s.mu.capacity {
s.mu.Unlock()
return ErrRequestExceedsCapacity
}
c := chanSyncPool.Get().(chan error)
defer chanSyncPool.Put(c)
w := s.mu.waiters.PushBack(semaWaiter{n: n, c: c})
s.mu.numHadToWait++
s.mu.Unlock()
select {
case <-ctx.Done():
s.mu.Lock()
defer s.mu.Unlock()
// We need to check if we raced with a channel notify (which happens under
// the lock).
select {
case err := <-c:
// We actually fulfilled or failed the request.
return err
default:
}
// Mark the request as canceled.
w.c = nil
s.mu.numCanceled++
// If we are the head of the queue, we may be able to fulfill other waiters.
s.processWaitersLocked()
return ctx.Err()
case err := <-c:
return err
}
}
// Release n units back. These must be units that were acquired by a previous
// Acquire call. It is legal to split up or coalesce units when releasing.
func (s *Semaphore) Release(n int64) {
s.mu.Lock()
defer s.mu.Unlock()
s.mu.outstanding -= n
if s.mu.outstanding < 0 {
panic("releasing more than was acquired")
}
s.processWaitersLocked()
}
// UpdateCapacity changes the capacity of the semaphore. If the new capacity is
// smaller, the already outstanding acquisitions might exceed the new capacity
// until they are released.
//
// If there are Acquire calls that are waiting which are requesting more than
// the new capacity, they will error out with ErrRequestExceedsCapacity.
func (s *Semaphore) UpdateCapacity(capacity int64) {
if capacity <= 0 {
panic("invalid capacity")
}
s.mu.Lock()
defer s.mu.Unlock()
s.mu.capacity = capacity
s.processWaitersLocked()
}
// Stats returns the current state of the semaphore.
func (s *Semaphore) Stats() SemaphoreStats {
s.mu.Lock()
defer s.mu.Unlock()
return SemaphoreStats{
Capacity: s.mu.capacity,
Outstanding: s.mu.outstanding,
NumHadToWait: s.mu.numHadToWait,
}
}
// SemaphoreStats contains information about the current state of the semaphore.
type SemaphoreStats struct {
// Capacity is the current capacity of the semaphore.
Capacity int64
// Outstanding is the number of units that have been acquired. Note that this
// can exceed Capacity if the capacity was recently decreased.
Outstanding int64
// NumHadToWait is the total number of Acquire calls (since the semaphore was
// created) that had to wait because the semaphore was exhausted. Useful for
// cumulative metrics.
NumHadToWait int64
// TODO(radu): consider keeping track of the total amount of time the
// semaphore was exhausted (i.e. there were waiters queued).
}
func (ss SemaphoreStats) String() string {
return fmt.Sprintf("capacity: %d, outstanding: %d, num-had-to-wait: %d",
ss.Capacity, ss.Outstanding, ss.NumHadToWait)
}
type semaWaiter struct {
// n is the amount that the waiter is trying to acquire.
n int64
// c is the channel on which Acquire is blocked. If the request is canceled,
// it is set to nil.
c chan error
}
// numWaitersLocked returns how many requests (that have not been canceled) are
// waiting in the queue.
func (s *Semaphore) numWaitersLocked() int {
return s.mu.waiters.Len() - s.mu.numCanceled
}
// processWaitersLocked processes and notifies as many waiters from the head of
// the queue as possible.
func (s *Semaphore) processWaitersLocked() {
for ; s.mu.waiters.Len() > 0; s.mu.waiters.PopFront() {
switch w := s.mu.waiters.PeekFront(); {
case w.c == nil:
// Request was canceled, we can just clean it up.
s.mu.numCanceled--
if invariants && s.mu.numCanceled < 0 {
panic("negative numCanceled")
}
case s.mu.outstanding+w.n <= s.mu.capacity:
// Request can be fulfilled.
s.mu.outstanding += w.n
w.c <- nil
case w.n > s.mu.capacity:
// Request must be failed. This can happen if the capacity was decreased
// while the element was queued.
w.c <- ErrRequestExceedsCapacity
default:
// Head of the queue needs to wait some more.
return
}
}
}
// chanSyncPool is used to pool allocations of the channels used to notify
// goroutines waiting in Acquire.
var chanSyncPool = sync.Pool{
New: func() interface{} { return make(chan error, 1) },
}

View File

@@ -2142,7 +2142,6 @@ func (d *DB) flush1() (bytesFlushed uint64, err error) {
// calculation even when the WAL is disabled.
metrics.BytesIn = metrics.BytesFlushed
} else {
metrics := c.metrics[0]
for i := 0; i < n; i++ {
metrics.BytesIn += d.mu.mem.queue[i].logSize
}

View File

@@ -377,7 +377,7 @@ type DB struct {
queue []fileInfo
// The number of input bytes to the log. This is the raw size of the
// batches written to the WAL, without the overhead of the record
// envelopes.
// envelopes. Requires DB.mu to be held when read or written.
bytesIn uint64
// The LogWriter is protected by commitPipeline.mu. This allows log
// writes to be performed without holding DB.mu, but requires both
@@ -2471,7 +2471,7 @@ func (d *DB) makeRoomForWrite(b *Batch) error {
var prevLogSize uint64
if !d.opts.DisableWAL {
now := time.Now()
newLogNum, prevLogSize = d.recycleWAL()
newLogNum, prevLogSize = d.rotateWAL()
if b != nil {
b.commitStats.WALRotationDuration += time.Since(now)
}
@@ -2543,15 +2543,29 @@ func (d *DB) rotateMemtable(newLogNum FileNum, logSeqNum uint64, prev *memTable)
var entry *flushableEntry
d.mu.mem.mutable, entry = d.newMemTable(newLogNum, logSeqNum)
d.mu.mem.queue = append(d.mu.mem.queue, entry)
// d.logSize tracks the log size of the WAL file corresponding to the most
// recent flushable. The log size of the previous mutable memtable no longer
// applies to the current mutable memtable.
//
// It's tempting to perform this update in rotateWAL, but that would not be
// atomic with the enqueue of the new flushable. A call to DB.Metrics()
// could acquire DB.mu after the WAL has been rotated but before the new
// memtable has been appended; this would result in omitting the log size of
// the most recent flushable.
d.logSize.Store(0)
d.updateReadStateLocked(nil)
if prev.writerUnref() {
d.maybeScheduleFlush()
}
}
// rotateWAL creates a new write-ahead log, possibly recycling a previous WAL's
// files. It returns the file number assigned to the new WAL, and the size of
// the previous WAL file.
//
// Both DB.mu and commitPipeline.mu must be held by the caller. Note that DB.mu
// may be released and reacquired.
func (d *DB) recycleWAL() (newLogNum FileNum, prevLogSize uint64) {
func (d *DB) rotateWAL() (newLogNum FileNum, prevLogSize uint64) {
if d.opts.DisableWAL {
panic("pebble: invalid function call")
}

View File

@@ -1223,7 +1223,12 @@ func (d *DB) handleIngestAsFlushable(meta []*fileMetadata, seqNum uint64) error
// We create a new WAL for the flushable instead of reusing the end of
// the previous WAL. This simplifies the increment of the minimum
// unflushed log number, and also simplifies WAL replay.
logNum, _ = d.recycleWAL()
var prevLogSize uint64
logNum, prevLogSize = d.rotateWAL()
// As the rotator of the WAL, we're responsible for updating the
// previous flushable queue tail's log size.
d.mu.mem.queue[len(d.mu.mem.queue)-1].logSize = prevLogSize
d.mu.Unlock()
err := d.commit.directWrite(b)
if err != nil {
@@ -1243,13 +1248,14 @@ func (d *DB) handleIngestAsFlushable(meta []*fileMetadata, seqNum uint64) error
// the appropriate value below.
newLogNum := d.mu.mem.queue[len(d.mu.mem.queue)-1].logNum
if !d.opts.DisableWAL {
// This is WAL num of the next mutable memtable which comes after the
// ingestedFlushable in the flushable queue. The mutable memtable
// will be created below.
newLogNum, _ = d.recycleWAL()
if err != nil {
return err
}
// newLogNum will be the WAL num of the next mutable memtable which
// comes after the ingestedFlushable in the flushable queue. The mutable
// memtable will be created below.
//
// The prevLogSize returned by rotateWAL is the WAL to which the
// flushable ingest keys were appended. This intermediary WAL is only
// used to record the flushable ingest and nothing else.
newLogNum, entry.logSize = d.rotateWAL()
}
currMem := d.mu.mem.mutable

View File

@@ -7,6 +7,9 @@ package base
import (
"fmt"
"time"
"github.com/cockroachdb/pebble/internal/humanize"
"github.com/cockroachdb/redact"
)
// InternalIterator iterates over a DB's key/value pairs in key order. Unlike
@@ -407,3 +410,35 @@ func (s *InternalIteratorStats) Merge(from InternalIteratorStats) {
s.SeparatedPointValue.ValueBytes += from.SeparatedPointValue.ValueBytes
s.SeparatedPointValue.ValueBytesFetched += from.SeparatedPointValue.ValueBytesFetched
}
func (s *InternalIteratorStats) String() string {
return redact.StringWithoutMarkers(s)
}
// SafeFormat implements the redact.SafeFormatter interface.
func (s *InternalIteratorStats) SafeFormat(p redact.SafePrinter, verb rune) {
p.Printf("blocks: %s cached",
humanize.Bytes.Uint64(s.BlockBytesInCache),
)
if s.BlockBytes != s.BlockBytesInCache || s.BlockReadDuration != 0 {
p.Printf(", %s not cached (read time: %s)",
humanize.Bytes.Uint64(s.BlockBytes-s.BlockBytesInCache),
humanize.FormattedString(s.BlockReadDuration.String()),
)
}
p.Printf("; points: %s", humanize.Count.Uint64(s.PointCount))
if s.PointsCoveredByRangeTombstones != 0 {
p.Printf("(%s tombstoned)", humanize.Count.Uint64(s.PointsCoveredByRangeTombstones))
}
p.Printf(" (%s keys, %s values)",
humanize.Bytes.Uint64(s.KeyBytes),
humanize.Bytes.Uint64(s.ValueBytes),
)
if s.SeparatedPointValue.Count != 0 {
p.Printf("; separated: %s (%s, %s fetched)",
humanize.Count.Uint64(s.SeparatedPointValue.Count),
humanize.Bytes.Uint64(s.SeparatedPointValue.ValueBytes),
humanize.Bytes.Uint64(s.SeparatedPointValue.ValueBytesFetched))
}
}

View File

@@ -154,6 +154,18 @@ func (s *RangeKeyIteratorStats) Merge(o RangeKeyIteratorStats) {
s.SkippedPoints += o.SkippedPoints
}
func (s *RangeKeyIteratorStats) String() string {
return redact.StringWithoutMarkers(s)
}
// SafeFormat implements the redact.SafeFormatter interface.
func (s *RangeKeyIteratorStats) SafeFormat(p redact.SafePrinter, verb rune) {
p.Printf("range keys: %s, contained points: %s (%s skipped)",
humanize.Count.Uint64(uint64(s.Count)),
humanize.Count.Uint64(uint64(s.ContainedPoints)),
humanize.Count.Uint64(uint64(s.SkippedPoints)))
}
// LazyValue is a lazy value. See the long comment in base.LazyValue.
type LazyValue = base.LazyValue
@@ -2788,44 +2800,44 @@ func (stats *IteratorStats) String() string {
// SafeFormat implements the redact.SafeFormatter interface.
func (stats *IteratorStats) SafeFormat(s redact.SafePrinter, verb rune) {
for i := range stats.ForwardStepCount {
switch IteratorStatsKind(i) {
case InterfaceCall:
s.SafeString("(interface (dir, seek, step): ")
case InternalIterCall:
s.SafeString(", (internal (dir, seek, step): ")
}
s.Printf("(fwd, %d, %d), (rev, %d, %d))",
redact.Safe(stats.ForwardSeekCount[i]), redact.Safe(stats.ForwardStepCount[i]),
redact.Safe(stats.ReverseSeekCount[i]), redact.Safe(stats.ReverseStepCount[i]))
}
if stats.InternalStats != (InternalIteratorStats{}) {
s.SafeString(",\n(internal-stats: ")
s.Printf("(block-bytes: (total %s, cached %s, read-time %s)), "+
"(points: (count %s, key-bytes %s, value-bytes %s, tombstoned %s))",
humanize.Bytes.Uint64(stats.InternalStats.BlockBytes),
humanize.Bytes.Uint64(stats.InternalStats.BlockBytesInCache),
humanize.FormattedString(stats.InternalStats.BlockReadDuration.String()),
humanize.Count.Uint64(stats.InternalStats.PointCount),
humanize.Bytes.Uint64(stats.InternalStats.KeyBytes),
humanize.Bytes.Uint64(stats.InternalStats.ValueBytes),
humanize.Count.Uint64(stats.InternalStats.PointsCoveredByRangeTombstones),
if stats.ReverseSeekCount[InterfaceCall] == 0 && stats.ReverseSeekCount[InternalIterCall] == 0 {
s.Printf("seeked %s times (%s internal)",
humanize.Count.Uint64(uint64(stats.ForwardSeekCount[InterfaceCall])),
humanize.Count.Uint64(uint64(stats.ForwardSeekCount[InternalIterCall])),
)
if stats.InternalStats.SeparatedPointValue.Count != 0 {
s.Printf(", (separated: (count %s, bytes %s, fetched %s)))",
humanize.Count.Uint64(stats.InternalStats.SeparatedPointValue.Count),
humanize.Bytes.Uint64(stats.InternalStats.SeparatedPointValue.ValueBytes),
humanize.Bytes.Uint64(stats.InternalStats.SeparatedPointValue.ValueBytesFetched))
} else {
s.Printf(")")
}
} else {
s.Printf("seeked %s times (%s fwd/%s rev, internal: %s fwd/%s rev)",
humanize.Count.Uint64(uint64(stats.ForwardSeekCount[InterfaceCall]+stats.ReverseSeekCount[InterfaceCall])),
humanize.Count.Uint64(uint64(stats.ForwardSeekCount[InterfaceCall])),
humanize.Count.Uint64(uint64(stats.ReverseSeekCount[InterfaceCall])),
humanize.Count.Uint64(uint64(stats.ForwardSeekCount[InternalIterCall])),
humanize.Count.Uint64(uint64(stats.ReverseSeekCount[InternalIterCall])),
)
}
s.SafeString("; ")
if stats.ReverseStepCount[InterfaceCall] == 0 && stats.ReverseStepCount[InternalIterCall] == 0 {
s.Printf("stepped %s times (%s internal)",
humanize.Count.Uint64(uint64(stats.ForwardStepCount[InterfaceCall])),
humanize.Count.Uint64(uint64(stats.ForwardStepCount[InternalIterCall])),
)
} else {
s.Printf("stepped %s times (%s fwd/%s rev, internal: %s fwd/%s rev)",
humanize.Count.Uint64(uint64(stats.ForwardStepCount[InterfaceCall]+stats.ReverseStepCount[InterfaceCall])),
humanize.Count.Uint64(uint64(stats.ForwardStepCount[InterfaceCall])),
humanize.Count.Uint64(uint64(stats.ReverseStepCount[InterfaceCall])),
humanize.Count.Uint64(uint64(stats.ForwardStepCount[InternalIterCall])),
humanize.Count.Uint64(uint64(stats.ReverseStepCount[InternalIterCall])),
)
}
if stats.InternalStats != (InternalIteratorStats{}) {
s.SafeString("; ")
stats.InternalStats.SafeFormat(s, verb)
}
if stats.RangeKeyStats != (RangeKeyIteratorStats{}) {
s.SafeString(",\n(range-key-stats: ")
s.Printf("(count %d), (contained points: (count %d, skipped %d)))",
stats.RangeKeyStats.Count,
stats.RangeKeyStats.ContainedPoints,
stats.RangeKeyStats.SkippedPoints)
s.SafeString(", ")
stats.RangeKeyStats.SafeFormat(s, verb)
}
}

View File

@@ -92,6 +92,17 @@ type Settings struct {
// out a large chunk of dirty filesystem buffers.
BytesPerSync int
// Local contains fields that are only relevant for files stored on the local
// filesystem.
Local struct {
// TODO(radu): move FSCleaner, NoSyncOnClose, BytesPerSync here.
// ReadaheadConfigFn is a function used to retrieve the current readahead
// mode. This function is run whenever a local object is open for reading.
// If it is nil, DefaultReadaheadConfig is used.
ReadaheadConfigFn func() ReadaheadConfig
}
// Fields here are set only if the provider is to support remote objects
// (experimental).
Remote struct {
@@ -129,6 +140,43 @@ type Settings struct {
}
}
// ReadaheadConfig controls the use of read-ahead.
type ReadaheadConfig struct {
// Informed is the type of read-ahead for operations that are known to read a
// large consecutive chunk of a file.
Informed ReadaheadMode
// Speculative is the type of read-ahead used automatically, when consecutive
// reads are detected.
Speculative ReadaheadMode
}
// DefaultReadaheadConfig is the readahead config used when ReadaheadConfigFn is
// not specified.
var DefaultReadaheadConfig = ReadaheadConfig{
Informed: FadviseSequential,
Speculative: FadviseSequential,
}
// ReadaheadMode indicates the type of read-ahead to use, either for informed
// read-ahead (e.g. compactions) or speculative read-ahead.
type ReadaheadMode uint8
const (
// NoReadahead disables readahead altogether.
NoReadahead ReadaheadMode = iota
// SysReadahead enables the use of SYS_READAHEAD call to prefetch data.
// The prefetch window grows dynamically as consecutive writes are detected.
SysReadahead
// FadviseSequential enables to use of FADV_SEQUENTIAL. For informed
// read-ahead, FADV_SEQUENTIAL is used from the beginning. For speculative
// read-ahead SYS_READAHEAD is first used until the window reaches the maximum
// size, then we siwtch to FADV_SEQUENTIAL.
FadviseSequential
)
// DefaultSettings initializes default settings (with no remote storage),
// suitable for tests and tools.
func DefaultSettings(fs vfs.FS, dirName string) Settings {

View File

@@ -4,6 +4,8 @@
package objstorageprovider
import "github.com/cockroachdb/pebble/internal/invariants"
const (
// Constants for dynamic readahead of data blocks. Note that the size values
// make sense as some multiple of the default block size; and they should
@@ -81,6 +83,9 @@ func (rs *readaheadState) recordCacheHit(offset, blockLength int64) {
// Returns a size value (greater than 0) that should be prefetched if readahead
// would be beneficial.
func (rs *readaheadState) maybeReadahead(offset, blockLength int64) int64 {
if invariants.Enabled && rs.maxReadaheadSize == 0 {
panic("readaheadState not initialized")
}
currentReadEnd := offset + blockLength
if rs.numReads >= minFileReadsForReadahead {
// The minimum threshold of sequential reads to justify reading ahead

View File

@@ -31,7 +31,11 @@ func (p *provider) vfsOpenForReading(
}
return nil, err
}
return newFileReadable(file, p.st.FS, filename)
readaheadConfig := DefaultReadaheadConfig
if f := p.st.Local.ReadaheadConfigFn; f != nil {
readaheadConfig = f()
}
return newFileReadable(file, p.st.FS, readaheadConfig, filename)
}
func (p *provider) vfsCreate(

View File

@@ -27,22 +27,26 @@ type fileReadable struct {
// The following fields are used to possibly open the file again using the
// sequential reads option (see vfsReadHandle).
filename string
fs vfs.FS
filename string
fs vfs.FS
readaheadConfig ReadaheadConfig
}
var _ objstorage.Readable = (*fileReadable)(nil)
func newFileReadable(file vfs.File, fs vfs.FS, filename string) (*fileReadable, error) {
func newFileReadable(
file vfs.File, fs vfs.FS, readaheadConfig ReadaheadConfig, filename string,
) (*fileReadable, error) {
info, err := file.Stat()
if err != nil {
return nil, err
}
r := &fileReadable{
file: file,
size: info.Size(),
filename: filename,
fs: fs,
file: file,
size: info.Size(),
filename: filename,
fs: fs,
readaheadConfig: readaheadConfig,
}
invariants.SetFinalizer(r, func(obj interface{}) {
if obj.(*fileReadable).file != nil {
@@ -76,14 +80,14 @@ func (r *fileReadable) Size() int64 {
// NewReadHandle is part of the objstorage.Readable interface.
func (r *fileReadable) NewReadHandle(_ context.Context) objstorage.ReadHandle {
rh := readHandlePool.Get().(*vfsReadHandle)
rh.r = r
rh.rs = makeReadaheadState(fileMaxReadaheadSize)
rh.init(r)
return rh
}
type vfsReadHandle struct {
r *fileReadable
rs readaheadState
r *fileReadable
rs readaheadState
readaheadMode ReadaheadMode
// sequentialFile holds a file descriptor to the same underlying File,
// except with fadvise(FADV_SEQUENTIAL) called on it to take advantage of
@@ -108,6 +112,14 @@ var readHandlePool = sync.Pool{
},
}
func (rh *vfsReadHandle) init(r *fileReadable) {
*rh = vfsReadHandle{
r: r,
rs: makeReadaheadState(fileMaxReadaheadSize),
readaheadMode: r.readaheadConfig.Speculative,
}
}
// Close is part of the objstorage.ReadHandle interface.
func (rh *vfsReadHandle) Close() error {
var err error
@@ -121,14 +133,17 @@ func (rh *vfsReadHandle) Close() error {
// ReadAt is part of the objstorage.ReadHandle interface.
func (rh *vfsReadHandle) ReadAt(_ context.Context, p []byte, offset int64) error {
var n int
var err error
if rh.sequentialFile != nil {
// Use OS-level read-ahead.
n, err = rh.sequentialFile.ReadAt(p, offset)
} else {
n, err := rh.sequentialFile.ReadAt(p, offset)
if invariants.Enabled && err == nil && n != len(p) {
panic("short read")
}
return err
}
if rh.readaheadMode != NoReadahead {
if readaheadSize := rh.rs.maybeReadahead(offset, int64(len(p))); readaheadSize > 0 {
if readaheadSize >= fileMaxReadaheadSize {
if rh.readaheadMode == FadviseSequential && readaheadSize >= fileMaxReadaheadSize {
// We've reached the maximum readahead size. Beyond this point, rely on
// OS-level readahead.
rh.switchToOSReadahead()
@@ -136,8 +151,8 @@ func (rh *vfsReadHandle) ReadAt(_ context.Context, p []byte, offset int64) error
_ = rh.r.file.Prefetch(offset, readaheadSize)
}
}
n, err = rh.r.file.ReadAt(p, offset)
}
n, err := rh.r.file.ReadAt(p, offset)
if invariants.Enabled && err == nil && n != len(p) {
panic("short read")
}
@@ -146,10 +161,16 @@ func (rh *vfsReadHandle) ReadAt(_ context.Context, p []byte, offset int64) error
// SetupForCompaction is part of the objstorage.ReadHandle interface.
func (rh *vfsReadHandle) SetupForCompaction() {
rh.switchToOSReadahead()
rh.readaheadMode = rh.r.readaheadConfig.Informed
if rh.readaheadMode == FadviseSequential {
rh.switchToOSReadahead()
}
}
func (rh *vfsReadHandle) switchToOSReadahead() {
if invariants.Enabled && rh.readaheadMode != FadviseSequential {
panic("readheadMode not respected")
}
if rh.sequentialFile != nil {
return
}
@@ -164,8 +185,8 @@ func (rh *vfsReadHandle) switchToOSReadahead() {
// RecordCacheHit is part of the objstorage.ReadHandle interface.
func (rh *vfsReadHandle) RecordCacheHit(_ context.Context, offset, size int64) {
if rh.sequentialFile != nil {
// Using OS-level readahead, so do nothing.
if rh.sequentialFile != nil || rh.readaheadMode == NoReadahead {
// Using OS-level or no readahead, so do nothing.
return
}
rh.rs.recordCacheHit(offset, size)
@@ -208,8 +229,7 @@ func UsePreallocatedReadHandle(
ctx context.Context, readable objstorage.Readable, rh *PreallocatedReadHandle,
) objstorage.ReadHandle {
if r, ok := readable.(*fileReadable); ok {
// See fileReadable.NewReadHandle.
rh.vfsReadHandle = vfsReadHandle{r: r}
rh.init(r)
return rh
}
return readable.NewReadHandle(ctx)

View File

@@ -310,6 +310,7 @@ func Open(dirname string, opts *Options) (db *DB, _ error) {
NoSyncOnClose: opts.NoSyncOnClose,
BytesPerSync: opts.BytesPerSync,
}
providerSettings.Local.ReadaheadConfigFn = opts.Local.ReadaheadConfigFn
providerSettings.Remote.StorageFactory = opts.Experimental.RemoteStorage
providerSettings.Remote.CreateOnShared = opts.Experimental.CreateOnShared
providerSettings.Remote.CreateOnSharedLocator = opts.Experimental.CreateOnSharedLocator

View File

@@ -14,11 +14,13 @@ import (
"time"
"github.com/cockroachdb/errors"
"github.com/cockroachdb/fifo"
"github.com/cockroachdb/pebble/internal/base"
"github.com/cockroachdb/pebble/internal/cache"
"github.com/cockroachdb/pebble/internal/humanize"
"github.com/cockroachdb/pebble/internal/keyspan"
"github.com/cockroachdb/pebble/internal/manifest"
"github.com/cockroachdb/pebble/objstorage/objstorageprovider"
"github.com/cockroachdb/pebble/objstorage/remote"
"github.com/cockroachdb/pebble/rangekey"
"github.com/cockroachdb/pebble/sstable"
@@ -481,11 +483,25 @@ type Options struct {
// The default cache size is 8 MB.
Cache *cache.Cache
// LoadBlockSema, if set, is used to limit the number of blocks that can be
// loaded (i.e. read from the filesystem) in parallel. Each load acquires one
// unit from the semaphore for the duration of the read.
LoadBlockSema *fifo.Semaphore
// Cleaner cleans obsolete files.
//
// The default cleaner uses the DeleteCleaner.
Cleaner Cleaner
// Local contains option that pertain to files stored on the local filesystem.
Local struct {
// ReadaheadConfigFn is a function used to retrieve the current readahead
// mode. This function is consulted when a table enters the table cache.
ReadaheadConfigFn func() ReadaheadConfig
// TODO(radu): move BytesPerSync, LoadBlockSema, Cleaner here.
}
// Comparer defines a total ordering over the space of []byte keys: a 'less
// than' relationship. The same comparison algorithm must be used for reads
// and writes over the lifetime of the DB.
@@ -996,6 +1012,9 @@ type Options struct {
}
}
// ReadaheadConfig controls the use of read-ahead.
type ReadaheadConfig = objstorageprovider.ReadaheadConfig
// DebugCheckLevels calls CheckLevels on the provided database.
// It may be set in the DebugCheck field of Options to check
// level invariants whenever a new version is installed.
@@ -1713,6 +1732,7 @@ func (o *Options) MakeReaderOptions() sstable.ReaderOptions {
var readerOpts sstable.ReaderOptions
if o != nil {
readerOpts.Cache = o.Cache
readerOpts.LoadBlockSema = o.LoadBlockSema
readerOpts.Comparer = o.Comparer
readerOpts.Filters = o.Filters
if o.Merger != nil {

View File

@@ -5,6 +5,7 @@
package sstable
import (
"github.com/cockroachdb/fifo"
"github.com/cockroachdb/pebble/internal/base"
"github.com/cockroachdb/pebble/internal/cache"
)
@@ -111,6 +112,11 @@ type ReaderOptions struct {
// The default cache size is a zero-size cache.
Cache *cache.Cache
// LoadBlockSema, if set, is used to limit the number of blocks that can be
// loaded (i.e. read from the filesystem) in parallel. Each load acquires one
// unit from the semaphore for the duration of the read.
LoadBlockSema *fifo.Semaphore
// User properties specified in this map will not be added to sst.Properties.UserProperties.
DeniedUserProperties map[string]struct{}

View File

@@ -539,6 +539,15 @@ func (r *Reader) readBlock(
}
// Cache miss.
if sema := r.opts.LoadBlockSema; sema != nil {
if err := sema.Acquire(ctx, 1); err != nil {
// An error here can only come from the context.
return bufferHandle{}, err
}
defer sema.Release(1)
}
var compressed cacheValueOrBuf
if bufferPool != nil {
compressed = cacheValueOrBuf{
@@ -571,6 +580,7 @@ func (r *Reader) readBlock(
int(bh.Length+blockTrailerLen), readDuration.String())
}
if stats != nil {
stats.BlockBytes += bh.Length
stats.BlockReadDuration += readDuration
}
if err != nil {
@@ -628,9 +638,6 @@ func (r *Reader) readBlock(
decompressed = transformed
}
if stats != nil {
stats.BlockBytes += bh.Length
}
if decompressed.buf.Valid() {
return bufferHandle{b: decompressed.buf}, nil
}

View File

@@ -9,6 +9,7 @@ import (
"fmt"
"github.com/cockroachdb/pebble/internal/base"
"github.com/cockroachdb/pebble/objstorage/objstorageprovider"
"github.com/cockroachdb/pebble/objstorage/objstorageprovider/objiotracing"
)
@@ -177,7 +178,7 @@ func (i *twoLevelIterator) init(
_ = i.topLevelIndex.Close()
return err
}
i.dataRH = r.readable.NewReadHandle(ctx)
i.dataRH = objstorageprovider.UsePreallocatedReadHandle(ctx, r.readable, &i.dataRHPrealloc)
if r.tableFormat >= TableFormatPebblev3 {
if r.Properties.NumValueBlocks > 0 {
i.vbReader = &valueBlockReader{

View File

@@ -5,6 +5,7 @@
package vfs
import (
"fmt"
"io"
"os"
)
@@ -38,7 +39,15 @@ func (fs *loggingFS) Create(name string) (File, error) {
}
func (fs *loggingFS) Open(name string, opts ...OpenOption) (File, error) {
fs.logFn("open: %s", name)
var optsStr string
if len(opts) > 0 {
optsStr = " (options:"
for i := range opts {
optsStr += fmt.Sprintf(" %T", opts[i])
}
optsStr += ")"
}
fs.logFn("open: %s%s", name, optsStr)
f, err := fs.FS.Open(name, opts...)
if err != nil {
return nil, err

View File

@@ -1,5 +1,34 @@
# Changelog
## 0.28.1
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.28.1.
### Bug Fixes
- Implement `http.ResponseWriter` to hook into various parts of the response process ([#837](https://github.com/getsentry/sentry-go/pull/837))
## 0.28.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.28.0.
### Features
- Add a `Fiber` performance tracing & error reporting integration ([#795](https://github.com/getsentry/sentry-go/pull/795))
- Add performance tracing to the `Echo` integration ([#722](https://github.com/getsentry/sentry-go/pull/722))
- Add performance tracing to the `FastHTTP` integration ([#732](https://github.com/getsentry/sentry-go/pull/723))
- Add performance tracing to the `Iris` integration ([#809](https://github.com/getsentry/sentry-go/pull/809))
- Add performance tracing to the `Negroni` integration ([#808](https://github.com/getsentry/sentry-go/pull/808))
- Add `FailureIssueThreshold` & `RecoveryThreshold` to `MonitorConfig` ([#775](https://github.com/getsentry/sentry-go/pull/775))
- Use `errors.Unwrap()` to create exception groups ([#792](https://github.com/getsentry/sentry-go/pull/792))
- Add support for matching on strings for `ClientOptions.IgnoreErrors` & `ClientOptions.IgnoreTransactions` ([#819](https://github.com/getsentry/sentry-go/pull/819))
- Add `http.request.method` attribute for performance span data ([#786](https://github.com/getsentry/sentry-go/pull/786))
- Accept `interface{}` for span data values ([#784](https://github.com/getsentry/sentry-go/pull/784))
### Bug Fixes
- Fix missing stack trace for parsing error in `logrusentry` ([#689](https://github.com/getsentry/sentry-go/pull/689))
## 0.27.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.27.0.

View File

@@ -13,7 +13,6 @@
[![Build Status](https://github.com/getsentry/sentry-go/workflows/go-workflow/badge.svg)](https://github.com/getsentry/sentry-go/actions?query=workflow%3Ago-workflow)
[![Go Report Card](https://goreportcard.com/badge/github.com/getsentry/sentry-go)](https://goreportcard.com/report/github.com/getsentry/sentry-go)
[![Discord](https://img.shields.io/discord/621778831602221064)](https://discord.gg/Ww9hbqr)
[![GoDoc](https://godoc.org/github.com/getsentry/sentry-go?status.svg)](https://godoc.org/github.com/getsentry/sentry-go)
[![go.dev](https://img.shields.io/badge/go.dev-pkg-007d9c.svg?style=flat)](https://pkg.go.dev/github.com/getsentry/sentry-go)
`sentry-go` provides a Sentry client implementation for the Go programming
@@ -85,7 +84,6 @@ checkout the official documentation:
- [Bug Tracker](https://github.com/getsentry/sentry-go/issues)
- [GitHub Project](https://github.com/getsentry/sentry-go)
- [![GoDoc](https://godoc.org/github.com/getsentry/sentry-go?status.svg)](https://godoc.org/github.com/getsentry/sentry-go)
- [![go.dev](https://img.shields.io/badge/go.dev-pkg-007d9c.svg?style=flat)](https://pkg.go.dev/github.com/getsentry/sentry-go)
- [![Documentation](https://img.shields.io/badge/documentation-sentry.io-green.svg)](https://docs.sentry.io/platforms/go/)
- [![Discussions](https://img.shields.io/github/discussions/getsentry/sentry-go.svg)](https://github.com/getsentry/sentry-go/discussions)

View File

@@ -87,6 +87,10 @@ type MonitorConfig struct { //nolint: maligned // prefer readability over optima
// A tz database string representing the timezone which the monitor's execution schedule is in.
// See: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
Timezone string `json:"timezone,omitempty"`
// The number of consecutive failed check-ins it takes before an issue is created.
FailureIssueThreshold int64 `json:"failure_issue_threshold,omitempty"`
// The number of consecutive OK check-ins it takes before an issue is resolved.
RecoveryThreshold int64 `json:"recovery_threshold,omitempty"`
}
type CheckIn struct { //nolint: maligned // prefer readability over optimal memory layout

View File

@@ -90,11 +90,10 @@ func NewDsn(rawURL string) (*Dsn, error) {
// Port
var port int
if parsedURL.Port() != "" {
parsedPort, err := strconv.Atoi(parsedURL.Port())
port, err = strconv.Atoi(parsedURL.Port())
if err != nil {
return nil, &DsnParseError{"invalid port"}
}
port = parsedPort
} else {
port = scheme.defaultPort()
}

View File

@@ -140,7 +140,7 @@ func (iei *ignoreErrorsIntegration) processor(event *Event, _ *EventHint) *Event
for _, suspect := range suspects {
for _, pattern := range iei.ignoreErrors {
if pattern.Match([]byte(suspect)) {
if pattern.Match([]byte(suspect)) || strings.Contains(suspect, pattern.String()) {
Logger.Printf("Event dropped due to being matched by `IgnoreErrors` option."+
"| Value matched: %s | Filter used: %s", suspect, pattern)
return nil
@@ -202,7 +202,7 @@ func (iei *ignoreTransactionsIntegration) processor(event *Event, _ *EventHint)
}
for _, pattern := range iei.ignoreTransactions {
if pattern.Match([]byte(suspect)) {
if pattern.Match([]byte(suspect)) || strings.Contains(suspect, pattern.String()) {
Logger.Printf("Transaction dropped due to being matched by `IgnoreTransactions` option."+
"| Value matched: %s | Filter used: %s", suspect, pattern)
return nil

View File

@@ -3,6 +3,7 @@ package sentry
import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
@@ -24,6 +25,9 @@ const profileType = "profile"
// checkInType is the type of a check in event.
const checkInType = "check_in"
// metricType is the type of a metric event.
const metricType = "statsd"
// Level marks the severity of the event.
type Level string
@@ -36,15 +40,6 @@ const (
LevelFatal Level = "fatal"
)
func getSensitiveHeaders() map[string]bool {
return map[string]bool{
"Authorization": true,
"Cookie": true,
"X-Forwarded-For": true,
"X-Real-Ip": true,
}
}
// SdkInfo contains all metadata about about the SDK being used.
type SdkInfo struct {
Name string `json:"name,omitempty"`
@@ -170,6 +165,13 @@ type Request struct {
Env map[string]string `json:"env,omitempty"`
}
var sensitiveHeaders = map[string]struct{}{
"Authorization": {},
"Cookie": {},
"X-Forwarded-For": {},
"X-Real-Ip": {},
}
// NewRequest returns a new Sentry Request from the given http.Request.
//
// NewRequest avoids operations that depend on network access. In particular, it
@@ -200,7 +202,6 @@ func NewRequest(r *http.Request) *Request {
env = map[string]string{"REMOTE_ADDR": addr, "REMOTE_PORT": port}
}
} else {
sensitiveHeaders := getSensitiveHeaders()
for k, v := range r.Header {
if _, ok := sensitiveHeaders[k]; !ok {
headers[k] = strings.Join(v, ",")
@@ -222,11 +223,15 @@ func NewRequest(r *http.Request) *Request {
// Mechanism is the mechanism by which an exception was generated and handled.
type Mechanism struct {
Type string `json:"type,omitempty"`
Description string `json:"description,omitempty"`
HelpLink string `json:"help_link,omitempty"`
Handled *bool `json:"handled,omitempty"`
Data map[string]interface{} `json:"data,omitempty"`
Type string `json:"type,omitempty"`
Description string `json:"description,omitempty"`
HelpLink string `json:"help_link,omitempty"`
Source string `json:"source,omitempty"`
Handled *bool `json:"handled,omitempty"`
ParentID *int `json:"parent_id,omitempty"`
ExceptionID int `json:"exception_id"`
IsExceptionGroup bool `json:"is_exception_group,omitempty"`
Data map[string]any `json:"data,omitempty"`
}
// SetUnhandled indicates that the exception is an unhandled exception, i.e.
@@ -318,6 +323,7 @@ type Event struct {
Exception []Exception `json:"exception,omitempty"`
DebugMeta *DebugMeta `json:"debug_meta,omitempty"`
Attachments []*Attachment `json:"-"`
Metrics []Metric `json:"-"`
// The fields below are only relevant for transactions.
@@ -339,27 +345,43 @@ type Event struct {
// SetException appends the unwrapped errors to the event's exception list.
//
// maxErrorDepth is the maximum depth of the error chain we will look
// into while unwrapping the errors.
// into while unwrapping the errors. If maxErrorDepth is -1, we will
// unwrap all errors in the chain.
func (e *Event) SetException(exception error, maxErrorDepth int) {
err := exception
if err == nil {
if exception == nil {
return
}
for i := 0; i < maxErrorDepth && err != nil; i++ {
err := exception
for i := 0; err != nil && (i < maxErrorDepth || maxErrorDepth == -1); i++ {
// Add the current error to the exception slice with its details
e.Exception = append(e.Exception, Exception{
Value: err.Error(),
Type: reflect.TypeOf(err).String(),
Stacktrace: ExtractStacktrace(err),
})
switch previous := err.(type) {
case interface{ Unwrap() error }:
err = previous.Unwrap()
case interface{ Cause() error }:
err = previous.Cause()
default:
err = nil
// Attempt to unwrap the error using the standard library's Unwrap method.
// If errors.Unwrap returns nil, it means either there is no error to unwrap,
// or the error does not implement the Unwrap method.
unwrappedErr := errors.Unwrap(err)
if unwrappedErr != nil {
// The error was successfully unwrapped using the standard library's Unwrap method.
err = unwrappedErr
continue
}
cause, ok := err.(interface{ Cause() error })
if !ok {
// We cannot unwrap the error further.
break
}
// The error implements the Cause method, indicating it may have been wrapped
// using the github.com/pkg/errors package.
err = cause.Cause()
}
// Add a trace of the current stack to the most recent error in a chain if
@@ -370,8 +392,23 @@ func (e *Event) SetException(exception error, maxErrorDepth int) {
e.Exception[0].Stacktrace = NewStacktrace()
}
if len(e.Exception) <= 1 {
return
}
// event.Exception should be sorted such that the most recent error is last.
reverse(e.Exception)
for i := range e.Exception {
e.Exception[i].Mechanism = &Mechanism{
IsExceptionGroup: true,
ExceptionID: i,
}
if i == 0 {
continue
}
e.Exception[i].Mechanism.ParentID = Pointer(i - 1)
}
}
// TODO: Event.Contexts map[string]interface{} => map[string]EventContext,
@@ -492,13 +529,12 @@ func (e *Event) checkInMarshalJSON() ([]byte, error) {
// NewEvent creates a new Event.
func NewEvent() *Event {
event := Event{
return &Event{
Contexts: make(map[string]Context),
Extra: make(map[string]interface{}),
Tags: make(map[string]string),
Modules: make(map[string]string),
}
return &event
}
// Thread specifies threads that were running at the time of an event.

View File

@@ -32,15 +32,14 @@ var knownCategories = map[Category]struct{}{
// String returns the category formatted for debugging.
func (c Category) String() string {
switch c {
case "":
if c == "" {
return "CategoryAll"
default:
caser := cases.Title(language.English)
rv := "Category"
for _, w := range strings.Fields(string(c)) {
rv += caser.String(w)
}
return rv
}
caser := cases.Title(language.English)
rv := "Category"
for _, w := range strings.Fields(string(c)) {
rv += caser.String(w)
}
return rv
}

427
vendor/github.com/getsentry/sentry-go/metrics.go generated vendored Normal file
View File

@@ -0,0 +1,427 @@
package sentry
import (
"fmt"
"hash/crc32"
"math"
"regexp"
"sort"
"strings"
)
type (
NumberOrString interface {
int | string
}
void struct{}
)
var (
member void
keyRegex = regexp.MustCompile(`[^a-zA-Z0-9_/.-]+`)
valueRegex = regexp.MustCompile(`[^\w\d\s_:/@\.{}\[\]$-]+`)
unitRegex = regexp.MustCompile(`[^a-z]+`)
)
type MetricUnit struct {
unit string
}
func (m MetricUnit) toString() string {
return m.unit
}
func NanoSecond() MetricUnit {
return MetricUnit{
"nanosecond",
}
}
func MicroSecond() MetricUnit {
return MetricUnit{
"microsecond",
}
}
func MilliSecond() MetricUnit {
return MetricUnit{
"millisecond",
}
}
func Second() MetricUnit {
return MetricUnit{
"second",
}
}
func Minute() MetricUnit {
return MetricUnit{
"minute",
}
}
func Hour() MetricUnit {
return MetricUnit{
"hour",
}
}
func Day() MetricUnit {
return MetricUnit{
"day",
}
}
func Week() MetricUnit {
return MetricUnit{
"week",
}
}
func Bit() MetricUnit {
return MetricUnit{
"bit",
}
}
func Byte() MetricUnit {
return MetricUnit{
"byte",
}
}
func KiloByte() MetricUnit {
return MetricUnit{
"kilobyte",
}
}
func KibiByte() MetricUnit {
return MetricUnit{
"kibibyte",
}
}
func MegaByte() MetricUnit {
return MetricUnit{
"megabyte",
}
}
func MebiByte() MetricUnit {
return MetricUnit{
"mebibyte",
}
}
func GigaByte() MetricUnit {
return MetricUnit{
"gigabyte",
}
}
func GibiByte() MetricUnit {
return MetricUnit{
"gibibyte",
}
}
func TeraByte() MetricUnit {
return MetricUnit{
"terabyte",
}
}
func TebiByte() MetricUnit {
return MetricUnit{
"tebibyte",
}
}
func PetaByte() MetricUnit {
return MetricUnit{
"petabyte",
}
}
func PebiByte() MetricUnit {
return MetricUnit{
"pebibyte",
}
}
func ExaByte() MetricUnit {
return MetricUnit{
"exabyte",
}
}
func ExbiByte() MetricUnit {
return MetricUnit{
"exbibyte",
}
}
func Ratio() MetricUnit {
return MetricUnit{
"ratio",
}
}
func Percent() MetricUnit {
return MetricUnit{
"percent",
}
}
func CustomUnit(unit string) MetricUnit {
return MetricUnit{
unitRegex.ReplaceAllString(unit, ""),
}
}
type Metric interface {
GetType() string
GetTags() map[string]string
GetKey() string
GetUnit() string
GetTimestamp() int64
SerializeValue() string
SerializeTags() string
}
type abstractMetric struct {
key string
unit MetricUnit
tags map[string]string
// A unix timestamp (full seconds elapsed since 1970-01-01 00:00 UTC).
timestamp int64
}
func (am abstractMetric) GetTags() map[string]string {
return am.tags
}
func (am abstractMetric) GetKey() string {
return am.key
}
func (am abstractMetric) GetUnit() string {
return am.unit.toString()
}
func (am abstractMetric) GetTimestamp() int64 {
return am.timestamp
}
func (am abstractMetric) SerializeTags() string {
var sb strings.Builder
values := make([]string, 0, len(am.tags))
for k := range am.tags {
values = append(values, k)
}
sortSlice(values)
for _, key := range values {
val := sanitizeValue(am.tags[key])
key = sanitizeKey(key)
sb.WriteString(fmt.Sprintf("%s:%s,", key, val))
}
s := sb.String()
if len(s) > 0 {
s = s[:len(s)-1]
}
return s
}
// Counter Metric.
type CounterMetric struct {
value float64
abstractMetric
}
func (c *CounterMetric) Add(value float64) {
c.value += value
}
func (c CounterMetric) GetType() string {
return "c"
}
func (c CounterMetric) SerializeValue() string {
return fmt.Sprintf(":%v", c.value)
}
// timestamp: A unix timestamp (full seconds elapsed since 1970-01-01 00:00 UTC).
func NewCounterMetric(key string, unit MetricUnit, tags map[string]string, timestamp int64, value float64) CounterMetric {
am := abstractMetric{
key,
unit,
tags,
timestamp,
}
return CounterMetric{
value,
am,
}
}
// Distribution Metric.
type DistributionMetric struct {
values []float64
abstractMetric
}
func (d *DistributionMetric) Add(value float64) {
d.values = append(d.values, value)
}
func (d DistributionMetric) GetType() string {
return "d"
}
func (d DistributionMetric) SerializeValue() string {
var sb strings.Builder
for _, el := range d.values {
sb.WriteString(fmt.Sprintf(":%v", el))
}
return sb.String()
}
// timestamp: A unix timestamp (full seconds elapsed since 1970-01-01 00:00 UTC).
func NewDistributionMetric(key string, unit MetricUnit, tags map[string]string, timestamp int64, value float64) DistributionMetric {
am := abstractMetric{
key,
unit,
tags,
timestamp,
}
return DistributionMetric{
[]float64{value},
am,
}
}
// Gauge Metric.
type GaugeMetric struct {
last float64
min float64
max float64
sum float64
count float64
abstractMetric
}
func (g *GaugeMetric) Add(value float64) {
g.last = value
g.min = math.Min(g.min, value)
g.max = math.Max(g.max, value)
g.sum += value
g.count++
}
func (g GaugeMetric) GetType() string {
return "g"
}
func (g GaugeMetric) SerializeValue() string {
return fmt.Sprintf(":%v:%v:%v:%v:%v", g.last, g.min, g.max, g.sum, g.count)
}
// timestamp: A unix timestamp (full seconds elapsed since 1970-01-01 00:00 UTC).
func NewGaugeMetric(key string, unit MetricUnit, tags map[string]string, timestamp int64, value float64) GaugeMetric {
am := abstractMetric{
key,
unit,
tags,
timestamp,
}
return GaugeMetric{
value, // last
value, // min
value, // max
value, // sum
value, // count
am,
}
}
// Set Metric.
type SetMetric[T NumberOrString] struct {
values map[T]void
abstractMetric
}
func (s *SetMetric[T]) Add(value T) {
s.values[value] = member
}
func (s SetMetric[T]) GetType() string {
return "s"
}
func (s SetMetric[T]) SerializeValue() string {
_hash := func(s string) uint32 {
return crc32.ChecksumIEEE([]byte(s))
}
values := make([]T, 0, len(s.values))
for k := range s.values {
values = append(values, k)
}
sortSlice(values)
var sb strings.Builder
for _, el := range values {
switch any(el).(type) {
case int:
sb.WriteString(fmt.Sprintf(":%v", el))
case string:
s := fmt.Sprintf("%v", el)
sb.WriteString(fmt.Sprintf(":%d", _hash(s)))
}
}
return sb.String()
}
// timestamp: A unix timestamp (full seconds elapsed since 1970-01-01 00:00 UTC).
func NewSetMetric[T NumberOrString](key string, unit MetricUnit, tags map[string]string, timestamp int64, value T) SetMetric[T] {
am := abstractMetric{
key,
unit,
tags,
timestamp,
}
return SetMetric[T]{
map[T]void{
value: member,
},
am,
}
}
func sanitizeKey(s string) string {
return keyRegex.ReplaceAllString(s, "_")
}
func sanitizeValue(s string) string {
return valueRegex.ReplaceAllString(s, "")
}
type Ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64 | ~string
}
func sortSlice[T Ordered](s []T) {
sort.Slice(s, func(i, j int) bool {
return s[i] < s[j]
})
}

View File

@@ -6,7 +6,7 @@ import (
)
// The version of the SDK.
const SDKVersion = "0.27.0"
const SDKVersion = "0.28.1"
// apiVersion is the minimum version of the Sentry API compatible with the
// sentry-go SDK.
@@ -72,18 +72,17 @@ func Recover() *EventID {
// RecoverWithContext captures a panic and passes relevant context object.
func RecoverWithContext(ctx context.Context) *EventID {
if err := recover(); err != nil {
var hub *Hub
if HasHubOnContext(ctx) {
hub = GetHubFromContext(ctx)
} else {
hub = CurrentHub()
}
return hub.RecoverWithContext(ctx, err)
err := recover()
if err == nil {
return nil
}
return nil
hub := GetHubFromContext(ctx)
if hub == nil {
hub = CurrentHub()
}
return hub.RecoverWithContext(ctx, err)
}
// WithScope is a shorthand for CurrentHub().WithScope.

View File

@@ -7,8 +7,8 @@ import (
// Checks whether the transaction should be profiled (according to ProfilesSampleRate)
// and starts a profiler if so.
func (span *Span) sampleTransactionProfile() {
var sampleRate = span.clientOptions().ProfilesSampleRate
func (s *Span) sampleTransactionProfile() {
var sampleRate = s.clientOptions().ProfilesSampleRate
switch {
case sampleRate < 0.0 || sampleRate > 1.0:
Logger.Printf("Skipping transaction profiling: ProfilesSampleRate out of range [0.0, 1.0]: %f\n", sampleRate)
@@ -19,7 +19,7 @@ func (span *Span) sampleTransactionProfile() {
if globalProfiler == nil {
Logger.Println("Skipping transaction profiling: the profiler couldn't be started")
} else {
span.collectProfile = collectTransactionProfile
s.collectProfile = collectTransactionProfile
}
}
}
@@ -69,22 +69,27 @@ func (info *profileInfo) UpdateFromEvent(event *Event) {
info.Dist = event.Dist
info.Transaction.ID = event.EventID
getStringFromContext := func(context map[string]interface{}, originalValue, key string) string {
v, ok := context[key]
if !ok {
return originalValue
}
if s, ok := v.(string); ok {
return s
}
return originalValue
}
if runtimeContext, ok := event.Contexts["runtime"]; ok {
if value, ok := runtimeContext["name"]; !ok {
info.Runtime.Name = value.(string)
}
if value, ok := runtimeContext["version"]; !ok {
info.Runtime.Version = value.(string)
}
info.Runtime.Name = getStringFromContext(runtimeContext, info.Runtime.Name, "name")
info.Runtime.Version = getStringFromContext(runtimeContext, info.Runtime.Version, "version")
}
if osContext, ok := event.Contexts["os"]; ok {
if value, ok := osContext["name"]; !ok {
info.OS.Name = value.(string)
}
info.OS.Name = getStringFromContext(osContext, info.OS.Name, "name")
}
if deviceContext, ok := event.Contexts["device"]; ok {
if value, ok := deviceContext["arch"]; !ok {
info.Device.Architecture = value.(string)
}
info.Device.Architecture = getStringFromContext(deviceContext, info.Device.Architecture, "arch")
}
}

View File

@@ -169,11 +169,11 @@ func StartSpan(ctx context.Context, operation string, options ...SpanOption) *Sp
span.Sampled = span.sample()
span.recorder = &spanRecorder{}
if hasParent {
span.recorder = parent.spanRecorder()
} else {
span.recorder = &spanRecorder{}
}
span.recorder.record(&span)
hub := hubFromContext(ctx)
@@ -226,7 +226,11 @@ func (s *Span) SetTag(name, value string) {
// SetData sets a data on the span. It is recommended to use SetData instead of
// accessing the data map directly as SetData takes care of initializing the map
// when necessary.
func (s *Span) SetData(name, value string) {
func (s *Span) SetData(name string, value interface{}) {
if value == nil {
return
}
s.mu.Lock()
defer s.mu.Unlock()

View File

@@ -2,6 +2,7 @@ package sentry
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"errors"
@@ -94,6 +95,55 @@ func getRequestBodyFromEvent(event *Event) []byte {
return nil
}
func marshalMetrics(metrics []Metric) []byte {
var b bytes.Buffer
for i, metric := range metrics {
b.WriteString(metric.GetKey())
if unit := metric.GetUnit(); unit != "" {
b.WriteString(fmt.Sprintf("@%s", unit))
}
b.WriteString(fmt.Sprintf("%s|%s", metric.SerializeValue(), metric.GetType()))
if serializedTags := metric.SerializeTags(); serializedTags != "" {
b.WriteString(fmt.Sprintf("|#%s", serializedTags))
}
b.WriteString(fmt.Sprintf("|T%d", metric.GetTimestamp()))
if i < len(metrics)-1 {
b.WriteString("\n")
}
}
return b.Bytes()
}
func encodeMetric(enc *json.Encoder, b io.Writer, metrics []Metric) error {
body := marshalMetrics(metrics)
// Item header
err := enc.Encode(struct {
Type string `json:"type"`
Length int `json:"length"`
}{
Type: metricType,
Length: len(body),
})
if err != nil {
return err
}
// metric payload
if _, err = b.Write(body); err != nil {
return err
}
// "Envelopes should be terminated with a trailing newline."
//
// [1]: https://develop.sentry.dev/sdk/envelopes/#envelopes
if _, err := b.Write([]byte("\n")); err != nil {
return err
}
return err
}
func encodeAttachment(enc *json.Encoder, b io.Writer, attachment *Attachment) error {
// Attachment header
err := enc.Encode(struct {
@@ -175,11 +225,15 @@ func envelopeFromBody(event *Event, dsn *Dsn, sentAt time.Time, body json.RawMes
return nil, err
}
if event.Type == transactionType || event.Type == checkInType {
switch event.Type {
case transactionType, checkInType:
err = encodeEnvelopeItem(enc, event.Type, body)
} else {
case metricType:
err = encodeMetric(enc, &b, event.Metrics)
default:
err = encodeEnvelopeItem(enc, eventType, body)
}
if err != nil {
return nil, err
}
@@ -206,7 +260,7 @@ func envelopeFromBody(event *Event, dsn *Dsn, sentAt time.Time, body json.RawMes
return &b, nil
}
func getRequestFromEvent(event *Event, dsn *Dsn) (r *http.Request, err error) {
func getRequestFromEvent(ctx context.Context, event *Event, dsn *Dsn) (r *http.Request, err error) {
defer func() {
if r != nil {
r.Header.Set("User-Agent", fmt.Sprintf("%s/%s", event.Sdk.Name, event.Sdk.Version))
@@ -233,7 +287,13 @@ func getRequestFromEvent(event *Event, dsn *Dsn) (r *http.Request, err error) {
if err != nil {
return nil, err
}
return http.NewRequest(
if ctx == nil {
ctx = context.Background()
}
return http.NewRequestWithContext(
ctx,
http.MethodPost,
dsn.GetAPIURL().String(),
envelope,
@@ -344,8 +404,13 @@ func (t *HTTPTransport) Configure(options ClientOptions) {
})
}
// SendEvent assembles a new packet out of Event and sends it to remote server.
// SendEvent assembles a new packet out of Event and sends it to the remote server.
func (t *HTTPTransport) SendEvent(event *Event) {
t.SendEventWithContext(context.Background(), event)
}
// SendEventWithContext assembles a new packet out of Event and sends it to the remote server.
func (t *HTTPTransport) SendEventWithContext(ctx context.Context, event *Event) {
if t.dsn == nil {
return
}
@@ -356,7 +421,7 @@ func (t *HTTPTransport) SendEvent(event *Event) {
return
}
request, err := getRequestFromEvent(event, t.dsn)
request, err := getRequestFromEvent(ctx, event, t.dsn)
if err != nil {
return
}
@@ -478,6 +543,13 @@ func (t *HTTPTransport) worker() {
Logger.Printf("There was an issue with sending an event: %v", err)
continue
}
if response.StatusCode >= 400 && response.StatusCode <= 599 {
b, err := io.ReadAll(response.Body)
if err != nil {
Logger.Printf("Error while reading response code: %v", err)
}
Logger.Printf("Sending %s failed with the following error: %s", eventType, string(b))
}
t.mu.Lock()
t.limits.Merge(ratelimit.FromResponse(response))
t.mu.Unlock()
@@ -567,8 +639,13 @@ func (t *HTTPSyncTransport) Configure(options ClientOptions) {
}
}
// SendEvent assembles a new packet out of Event and sends it to remote server.
// SendEvent assembles a new packet out of Event and sends it to the remote server.
func (t *HTTPSyncTransport) SendEvent(event *Event) {
t.SendEventWithContext(context.Background(), event)
}
// SendEventWithContext assembles a new packet out of Event and sends it to the remote server.
func (t *HTTPSyncTransport) SendEventWithContext(ctx context.Context, event *Event) {
if t.dsn == nil {
return
}
@@ -577,15 +654,18 @@ func (t *HTTPSyncTransport) SendEvent(event *Event) {
return
}
request, err := getRequestFromEvent(event, t.dsn)
request, err := getRequestFromEvent(ctx, event, t.dsn)
if err != nil {
return
}
var eventType string
if event.Type == transactionType {
switch {
case event.Type == transactionType:
eventType = "transaction"
} else {
case event.Type == metricType:
eventType = metricType
default:
eventType = fmt.Sprintf("%s event", event.Level)
}
Logger.Printf(
@@ -601,6 +681,14 @@ func (t *HTTPSyncTransport) SendEvent(event *Event) {
Logger.Printf("There was an issue with sending an event: %v", err)
return
}
if response.StatusCode >= 400 && response.StatusCode <= 599 {
b, err := io.ReadAll(response.Body)
if err != nil {
Logger.Printf("Error while reading response code: %v", err)
}
Logger.Printf("Sending %s failed with the following error: %s", eventType, string(b))
}
t.mu.Lock()
t.limits.Merge(ratelimit.FromResponse(response))
t.mu.Unlock()

View File

@@ -112,3 +112,7 @@ func revisionFromBuildInfo(info *debug.BuildInfo) string {
return ""
}
func Pointer[T any](v T) *T {
return &v
}

View File

@@ -55,6 +55,10 @@ https://github.com/klauspost/compress/pull/919 https://github.com/klauspost/comp
* s2: Do 2 overlapping match checks https://github.com/klauspost/compress/pull/839
* flate: Add amd64 assembly matchlen https://github.com/klauspost/compress/pull/837
* gzip: Copy bufio.Reader on Reset by @thatguystone in https://github.com/klauspost/compress/pull/860
<details>
<summary>See changes to v1.16.x</summary>
* July 1st, 2023 - [v1.16.7](https://github.com/klauspost/compress/releases/tag/v1.16.7)
* zstd: Fix default level first dictionary encode https://github.com/klauspost/compress/pull/829
@@ -93,6 +97,7 @@ https://github.com/klauspost/compress/pull/919 https://github.com/klauspost/comp
* s2: Add LZ4 block converter. https://github.com/klauspost/compress/pull/748
* s2: Support io.ReaderAt in ReadSeeker. https://github.com/klauspost/compress/pull/747
* s2c/s2sx: Use concurrent decoding. https://github.com/klauspost/compress/pull/746
</details>
<details>
<summary>See changes to v1.15.x</summary>
@@ -560,6 +565,8 @@ the stateless compress described below.
For compression performance, see: [this spreadsheet](https://docs.google.com/spreadsheets/d/1nuNE2nPfuINCZJRMt6wFWhKpToF95I47XjSsc-1rbPQ/edit?usp=sharing).
To disable all assembly add `-tags=noasm`. This works across all packages.
# Stateless compression
This package offers stateless compression as a special option for gzip/deflate.

View File

@@ -51,7 +51,7 @@ func emitCopy(dst []byte, offset, length int) int {
i := 0
// The maximum length for a single tagCopy1 or tagCopy2 op is 64 bytes. The
// threshold for this loop is a little higher (at 68 = 64 + 4), and the
// length emitted down below is is a little lower (at 60 = 64 - 4), because
// length emitted down below is a little lower (at 60 = 64 - 4), because
// it's shorter to encode a length 67 copy as a length 60 tagCopy2 followed
// by a length 7 tagCopy1 (which encodes as 3+2 bytes) than to encode it as
// a length 64 tagCopy2 followed by a length 3 tagCopy2 (which encodes as

View File

@@ -554,6 +554,9 @@ func (b *blockDec) prepareSequences(in []byte, hist *history) (err error) {
if debugDecoder {
printf("Compression modes: 0b%b", compMode)
}
if compMode&3 != 0 {
return errors.New("corrupt block: reserved bits not zero")
}
for i := uint(0); i < 3; i++ {
mode := seqCompMode((compMode >> (6 - i*2)) & 3)
if debugDecoder {

View File

@@ -427,6 +427,16 @@ func (b *blockEnc) encodeLits(lits []byte, raw bool) error {
return nil
}
// encodeRLE will encode an RLE block.
func (b *blockEnc) encodeRLE(val byte, length uint32) {
var bh blockHeader
bh.setLast(b.last)
bh.setSize(length)
bh.setType(blockTypeRLE)
b.output = bh.appendTo(b.output)
b.output = append(b.output, val)
}
// fuzzFseEncoder can be used to fuzz the FSE encoder.
func fuzzFseEncoder(data []byte) int {
if len(data) > maxSequences || len(data) < 2 {
@@ -479,6 +489,16 @@ func (b *blockEnc) encode(org []byte, raw, rawAllLits bool) error {
if len(b.sequences) == 0 {
return b.encodeLits(b.literals, rawAllLits)
}
if len(b.sequences) == 1 && len(org) > 0 && len(b.literals) <= 1 {
// Check common RLE cases.
seq := b.sequences[0]
if seq.litLen == uint32(len(b.literals)) && seq.offset-3 == 1 {
// Offset == 1 and 0 or 1 literals.
b.encodeRLE(org[0], b.sequences[0].matchLen+zstdMinMatch+seq.litLen)
return nil
}
}
// We want some difference to at least account for the headers.
saved := b.size - len(b.literals) - (b.size >> 6)
if saved < 16 {

View File

@@ -82,7 +82,7 @@ var (
// can run multiple concurrent stateless decodes. It is even possible to
// use stateless decodes while a stream is being decoded.
//
// The Reset function can be used to initiate a new stream, which is will considerably
// The Reset function can be used to initiate a new stream, which will considerably
// reduce the allocations normally caused by NewReader.
func NewReader(r io.Reader, opts ...DOption) (*Decoder, error) {
initPredefined()

View File

@@ -273,6 +273,9 @@ func BuildDict(o BuildDictOptions) ([]byte, error) {
enc.Encode(&block, b)
addValues(&remain, block.literals)
litTotal += len(block.literals)
if len(block.sequences) == 0 {
continue
}
seqs += len(block.sequences)
block.genCodes()
addHist(&ll, block.coders.llEnc.Histogram())
@@ -286,6 +289,9 @@ func BuildDict(o BuildDictOptions) ([]byte, error) {
if offset == 0 {
continue
}
if int(offset) >= len(o.History) {
continue
}
if offset > 3 {
newOffsets[offset-3]++
} else {
@@ -336,6 +342,9 @@ func BuildDict(o BuildDictOptions) ([]byte, error) {
if seqs/nUsed < 512 {
// Use 512 as minimum.
nUsed = seqs / 512
if nUsed == 0 {
nUsed = 1
}
}
copyHist := func(dst *fseEncoder, src *[256]int) ([]byte, error) {
hist := dst.Histogram()
@@ -358,6 +367,28 @@ func BuildDict(o BuildDictOptions) ([]byte, error) {
fakeLength += v
hist[i] = uint32(v)
}
// Ensure we aren't trying to represent RLE.
if maxCount == fakeLength {
for i := range hist {
if uint8(i) == maxSym {
fakeLength++
maxSym++
hist[i+1] = 1
if maxSym > 1 {
break
}
}
if hist[0] == 0 {
fakeLength++
hist[i] = 1
if maxSym > 1 {
break
}
}
}
}
dst.HistogramFinished(maxSym, maxCount)
dst.reUsed = false
dst.useRLE = false

View File

@@ -135,8 +135,20 @@ func (e *bestFastEncoder) Encode(blk *blockEnc, src []byte) {
break
}
// Add block to history
s := e.addBlock(src)
blk.size = len(src)
// Check RLE first
if len(src) > zstdMinMatch {
ml := matchLen(src[1:], src)
if ml == len(src)-1 {
blk.literals = append(blk.literals, src[0])
blk.sequences = append(blk.sequences, seq{litLen: 1, matchLen: uint32(len(src)-1) - zstdMinMatch, offset: 1 + 3})
return
}
}
if len(src) < minNonLiteralBlockSize {
blk.extraLits = len(src)
blk.literals = blk.literals[:len(src)]

View File

@@ -102,9 +102,20 @@ func (e *betterFastEncoder) Encode(blk *blockEnc, src []byte) {
e.cur = e.maxMatchOff
break
}
// Add block to history
s := e.addBlock(src)
blk.size = len(src)
// Check RLE first
if len(src) > zstdMinMatch {
ml := matchLen(src[1:], src)
if ml == len(src)-1 {
blk.literals = append(blk.literals, src[0])
blk.sequences = append(blk.sequences, seq{litLen: 1, matchLen: uint32(len(src)-1) - zstdMinMatch, offset: 1 + 3})
return
}
}
if len(src) < minNonLiteralBlockSize {
blk.extraLits = len(src)
blk.literals = blk.literals[:len(src)]

View File

@@ -162,12 +162,12 @@ finalize:
MOVD h, ret+24(FP)
RET
// func writeBlocks(d *Digest, b []byte) int
// func writeBlocks(s *Digest, b []byte) int
TEXT ·writeBlocks(SB), NOSPLIT|NOFRAME, $0-40
LDP ·primes+0(SB), (prime1, prime2)
// Load state. Assume v[1-4] are stored contiguously.
MOVD d+0(FP), digest
MOVD s+0(FP), digest
LDP 0(digest), (v1, v2)
LDP 16(digest), (v3, v4)

View File

@@ -5,7 +5,6 @@
#include "textflag.h"
// func matchLen(a []byte, b []byte) int
// Requires: BMI
TEXT ·matchLen(SB), NOSPLIT, $0-56
MOVQ a_base+0(FP), AX
MOVQ b_base+24(FP), CX
@@ -17,17 +16,16 @@ TEXT ·matchLen(SB), NOSPLIT, $0-56
JB matchlen_match4_standalone
matchlen_loopback_standalone:
MOVQ (AX)(SI*1), BX
XORQ (CX)(SI*1), BX
TESTQ BX, BX
JZ matchlen_loop_standalone
MOVQ (AX)(SI*1), BX
XORQ (CX)(SI*1), BX
JZ matchlen_loop_standalone
#ifdef GOAMD64_v3
TZCNTQ BX, BX
#else
BSFQ BX, BX
#endif
SARQ $0x03, BX
SHRL $0x03, BX
LEAL (SI)(BX*1), SI
JMP gen_match_len_end

31
vendor/github.com/munnerz/goautoneg/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,31 @@
Copyright (c) 2011, Open Knowledge Foundation Ltd.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
Neither the name of the Open Knowledge Foundation Ltd. nor the
names of its contributors may be used to endorse or promote
products derived from this software without specific prior written
permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

13
vendor/github.com/munnerz/goautoneg/Makefile generated vendored Normal file
View File

@@ -0,0 +1,13 @@
include $(GOROOT)/src/Make.inc
TARG=bitbucket.org/ww/goautoneg
GOFILES=autoneg.go
include $(GOROOT)/src/Make.pkg
format:
gofmt -w *.go
docs:
gomake clean
godoc ${TARG} > README.txt

View File

@@ -1,28 +1,28 @@
/*
Copyright (c) 2011, Open Knowledge Foundation Ltd.
All rights reserved.
HTTP Content-Type Autonegotiation.
The functions in this package implement the behaviour specified in
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
Copyright (c) 2011, Open Knowledge Foundation Ltd.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
Neither the name of the Open Knowledge Foundation Ltd. nor the
names of its contributors may be used to endorse or promote
products derived from this software without specific prior written
permission.
Neither the name of the Open Knowledge Foundation Ltd. nor the
names of its contributors may be used to endorse or promote
products derived from this software without specific prior written
permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
@@ -36,6 +36,7 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package goautoneg
import (
@@ -51,16 +52,14 @@ type Accept struct {
Params map[string]string
}
// For internal use, so that we can use the sort interface
type accept_slice []Accept
// acceptSlice is defined to implement sort interface.
type acceptSlice []Accept
func (accept accept_slice) Len() int {
slice := []Accept(accept)
func (slice acceptSlice) Len() int {
return len(slice)
}
func (accept accept_slice) Less(i, j int) bool {
slice := []Accept(accept)
func (slice acceptSlice) Less(i, j int) bool {
ai, aj := slice[i], slice[j]
if ai.Q > aj.Q {
return true
@@ -74,63 +73,93 @@ func (accept accept_slice) Less(i, j int) bool {
return false
}
func (accept accept_slice) Swap(i, j int) {
slice := []Accept(accept)
func (slice acceptSlice) Swap(i, j int) {
slice[i], slice[j] = slice[j], slice[i]
}
func stringTrimSpaceCutset(r rune) bool {
return r == ' '
}
func nextSplitElement(s, sep string) (item string, remaining string) {
if index := strings.Index(s, sep); index != -1 {
return s[:index], s[index+1:]
}
return s, ""
}
// Parse an Accept Header string returning a sorted list
// of clauses
func ParseAccept(header string) (accept []Accept) {
parts := strings.Split(header, ",")
accept = make([]Accept, 0, len(parts))
for _, part := range parts {
part := strings.Trim(part, " ")
func ParseAccept(header string) acceptSlice {
partsCount := 0
remaining := header
for len(remaining) > 0 {
partsCount++
_, remaining = nextSplitElement(remaining, ",")
}
accept := make(acceptSlice, 0, partsCount)
a := Accept{}
a.Params = make(map[string]string)
a.Q = 1.0
remaining = header
var part string
for len(remaining) > 0 {
part, remaining = nextSplitElement(remaining, ",")
part = strings.TrimFunc(part, stringTrimSpaceCutset)
mrp := strings.Split(part, ";")
media_range := mrp[0]
sp := strings.Split(media_range, "/")
a.Type = strings.Trim(sp[0], " ")
switch {
case len(sp) == 1 && a.Type == "*":
a.SubType = "*"
case len(sp) == 2:
a.SubType = strings.Trim(sp[1], " ")
default:
continue
a := Accept{
Q: 1.0,
}
if len(mrp) == 1 {
sp, remainingPart := nextSplitElement(part, ";")
sp0, spRemaining := nextSplitElement(sp, "/")
a.Type = strings.TrimFunc(sp0, stringTrimSpaceCutset)
switch {
case len(spRemaining) == 0:
if a.Type == "*" {
a.SubType = "*"
} else {
continue
}
default:
var sp1 string
sp1, spRemaining = nextSplitElement(spRemaining, "/")
if len(spRemaining) > 0 {
continue
}
a.SubType = strings.TrimFunc(sp1, stringTrimSpaceCutset)
}
if len(remainingPart) == 0 {
accept = append(accept, a)
continue
}
for _, param := range mrp[1:] {
sp := strings.SplitN(param, "=", 2)
if len(sp) != 2 {
a.Params = make(map[string]string)
for len(remainingPart) > 0 {
sp, remainingPart = nextSplitElement(remainingPart, ";")
sp0, spRemaining = nextSplitElement(sp, "=")
if len(spRemaining) == 0 {
continue
}
token := strings.Trim(sp[0], " ")
var sp1 string
sp1, spRemaining = nextSplitElement(spRemaining, "=")
if len(spRemaining) != 0 {
continue
}
token := strings.TrimFunc(sp0, stringTrimSpaceCutset)
if token == "q" {
a.Q, _ = strconv.ParseFloat(sp[1], 32)
a.Q, _ = strconv.ParseFloat(sp1, 32)
} else {
a.Params[token] = strings.Trim(sp[1], " ")
a.Params[token] = strings.TrimFunc(sp1, stringTrimSpaceCutset)
}
}
accept = append(accept, a)
}
slice := accept_slice(accept)
sort.Sort(slice)
return
sort.Sort(accept)
return accept
}
// Negotiate the most appropriate content_type given the accept header

View File

@@ -75,14 +75,14 @@ func ResponseFormat(h http.Header) Format {
func NewDecoder(r io.Reader, format Format) Decoder {
switch format.FormatType() {
case TypeProtoDelim:
return &protoDecoder{r: r}
return &protoDecoder{r: bufio.NewReader(r)}
}
return &textDecoder{r: r}
}
// protoDecoder implements the Decoder interface for protocol buffers.
type protoDecoder struct {
r io.Reader
r protodelim.Reader
}
// Decode implements the Decoder interface.
@@ -90,7 +90,7 @@ func (d *protoDecoder) Decode(v *dto.MetricFamily) error {
opts := protodelim.UnmarshalOptions{
MaxSize: -1,
}
if err := opts.UnmarshalFrom(bufio.NewReader(d.r), v); err != nil {
if err := opts.UnmarshalFrom(d.r, v); err != nil {
return err
}
if !model.IsValidMetricName(model.LabelValue(v.GetName())) {

View File

@@ -21,9 +21,10 @@ import (
"google.golang.org/protobuf/encoding/protodelim"
"google.golang.org/protobuf/encoding/prototext"
"github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg"
"github.com/prometheus/common/model"
"github.com/munnerz/goautoneg"
dto "github.com/prometheus/client_model/go"
)
@@ -139,7 +140,13 @@ func NegotiateIncludingOpenMetrics(h http.Header) Format {
// interface is kept for backwards compatibility.
// In cases where the Format does not allow for UTF-8 names, the global
// NameEscapingScheme will be applied.
func NewEncoder(w io.Writer, format Format) Encoder {
//
// NewEncoder can be called with additional options to customize the OpenMetrics text output.
// For example:
// NewEncoder(w, FmtOpenMetrics_1_0_0, WithCreatedLines())
//
// Extra options are ignored for all other formats.
func NewEncoder(w io.Writer, format Format, options ...EncoderOption) Encoder {
escapingScheme := format.ToEscapingScheme()
switch format.FormatType() {
@@ -178,7 +185,7 @@ func NewEncoder(w io.Writer, format Format) Encoder {
case TypeOpenMetrics:
return encoderCloser{
encode: func(v *dto.MetricFamily) error {
_, err := MetricFamilyToOpenMetrics(w, model.EscapeMetricFamily(v, escapingScheme))
_, err := MetricFamilyToOpenMetrics(w, model.EscapeMetricFamily(v, escapingScheme), options...)
return err
},
close: func() error {

View File

@@ -15,6 +15,7 @@
package expfmt
import (
"fmt"
"strings"
"github.com/prometheus/common/model"
@@ -63,7 +64,7 @@ const (
type FormatType int
const (
TypeUnknown = iota
TypeUnknown FormatType = iota
TypeProtoCompact
TypeProtoDelim
TypeProtoText
@@ -73,7 +74,8 @@ const (
// NewFormat generates a new Format from the type provided. Mostly used for
// tests, most Formats should be generated as part of content negotiation in
// encode.go.
// encode.go. If a type has more than one version, the latest version will be
// returned.
func NewFormat(t FormatType) Format {
switch t {
case TypeProtoCompact:
@@ -91,13 +93,21 @@ func NewFormat(t FormatType) Format {
}
}
// NewOpenMetricsFormat generates a new OpenMetrics format matching the
// specified version number.
func NewOpenMetricsFormat(version string) (Format, error) {
if version == OpenMetricsVersion_0_0_1 {
return fmtOpenMetrics_0_0_1, nil
}
if version == OpenMetricsVersion_1_0_0 {
return fmtOpenMetrics_1_0_0, nil
}
return fmtUnknown, fmt.Errorf("unknown open metrics version string")
}
// FormatType deduces an overall FormatType for the given format.
func (f Format) FormatType() FormatType {
toks := strings.Split(string(f), ";")
if len(toks) < 2 {
return TypeUnknown
}
params := make(map[string]string)
for i, t := range toks {
if i == 0 {

View File

@@ -22,11 +22,47 @@ import (
"strconv"
"strings"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/prometheus/common/model"
dto "github.com/prometheus/client_model/go"
)
type encoderOption struct {
withCreatedLines bool
withUnit bool
}
type EncoderOption func(*encoderOption)
// WithCreatedLines is an EncoderOption that configures the OpenMetrics encoder
// to include _created lines (See
// https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#counter-1).
// Created timestamps can improve the accuracy of series reset detection, but
// come with a bandwidth cost.
//
// At the time of writing, created timestamp ingestion is still experimental in
// Prometheus and need to be enabled with the feature-flag
// `--feature-flag=created-timestamp-zero-ingestion`, and breaking changes are
// still possible. Therefore, it is recommended to use this feature with caution.
func WithCreatedLines() EncoderOption {
return func(t *encoderOption) {
t.withCreatedLines = true
}
}
// WithUnit is an EncoderOption enabling a set unit to be written to the output
// and to be added to the metric name, if it's not there already, as a suffix.
// Without opting in this way, the unit will not be added to the metric name and,
// on top of that, the unit will not be passed onto the output, even if it
// were declared in the *dto.MetricFamily struct, i.e. even if in.Unit !=nil.
func WithUnit() EncoderOption {
return func(t *encoderOption) {
t.withUnit = true
}
}
// MetricFamilyToOpenMetrics converts a MetricFamily proto message into the
// OpenMetrics text format and writes the resulting lines to 'out'. It returns
// the number of bytes written and any error encountered. The output will have
@@ -59,20 +95,34 @@ import (
// Prometheus to OpenMetrics or vice versa:
//
// - Counters are expected to have the `_total` suffix in their metric name. In
// the output, the suffix will be truncated from the `# TYPE` and `# HELP`
// line. A counter with a missing `_total` suffix is not an error. However,
// the output, the suffix will be truncated from the `# TYPE`, `# HELP` and `# UNIT`
// lines. A counter with a missing `_total` suffix is not an error. However,
// its type will be set to `unknown` in that case to avoid invalid OpenMetrics
// output.
//
// - No support for the following (optional) features: `# UNIT` line, `_created`
// line, info type, stateset type, gaugehistogram type.
// - According to the OM specs, the `# UNIT` line is optional, but if populated,
// the unit has to be present in the metric name as its suffix:
// (see https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#unit).
// However, in order to accommodate any potential scenario where such a change in the
// metric name is not desirable, the users are here given the choice of either explicitly
// opt in, in case they wish for the unit to be included in the output AND in the metric name
// as a suffix (see the description of the WithUnit function above),
// or not to opt in, in case they don't want for any of that to happen.
//
// - No support for the following (optional) features: info type,
// stateset type, gaugehistogram type.
//
// - The size of exemplar labels is not checked (i.e. it's possible to create
// exemplars that are larger than allowed by the OpenMetrics specification).
//
// - The value of Counters is not checked. (OpenMetrics doesn't allow counters
// with a `NaN` value.)
func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily) (written int, err error) {
func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily, options ...EncoderOption) (written int, err error) {
toOM := encoderOption{}
for _, option := range options {
option(&toOM)
}
name := in.GetName()
if name == "" {
return 0, fmt.Errorf("MetricFamily has no name: %s", in)
@@ -95,12 +145,15 @@ func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily) (written int
}
var (
n int
metricType = in.GetType()
shortName = name
n int
metricType = in.GetType()
compliantName = name
)
if metricType == dto.MetricType_COUNTER && strings.HasSuffix(shortName, "_total") {
shortName = name[:len(name)-6]
if metricType == dto.MetricType_COUNTER && strings.HasSuffix(compliantName, "_total") {
compliantName = name[:len(name)-6]
}
if toOM.withUnit && in.Unit != nil && !strings.HasSuffix(compliantName, fmt.Sprintf("_%s", *in.Unit)) {
compliantName = compliantName + fmt.Sprintf("_%s", *in.Unit)
}
// Comments, first HELP, then TYPE.
@@ -110,7 +163,7 @@ func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily) (written int
if err != nil {
return
}
n, err = writeName(w, shortName)
n, err = writeName(w, compliantName)
written += n
if err != nil {
return
@@ -136,7 +189,7 @@ func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily) (written int
if err != nil {
return
}
n, err = writeName(w, shortName)
n, err = writeName(w, compliantName)
written += n
if err != nil {
return
@@ -163,55 +216,89 @@ func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily) (written int
if err != nil {
return
}
if toOM.withUnit && in.Unit != nil {
n, err = w.WriteString("# UNIT ")
written += n
if err != nil {
return
}
n, err = writeName(w, compliantName)
written += n
if err != nil {
return
}
err = w.WriteByte(' ')
written++
if err != nil {
return
}
n, err = writeEscapedString(w, *in.Unit, true)
written += n
if err != nil {
return
}
err = w.WriteByte('\n')
written++
if err != nil {
return
}
}
var createdTsBytesWritten int
// Finally the samples, one line for each.
if metricType == dto.MetricType_COUNTER && strings.HasSuffix(name, "_total") {
compliantName = compliantName + "_total"
}
for _, metric := range in.Metric {
switch metricType {
case dto.MetricType_COUNTER:
if metric.Counter == nil {
return written, fmt.Errorf(
"expected counter in metric %s %s", name, metric,
"expected counter in metric %s %s", compliantName, metric,
)
}
// Note that we have ensured above that either the name
// ends on `_total` or that the rendered type is
// `unknown`. Therefore, no `_total` must be added here.
n, err = writeOpenMetricsSample(
w, name, "", metric, "", 0,
w, compliantName, "", metric, "", 0,
metric.Counter.GetValue(), 0, false,
metric.Counter.Exemplar,
)
if toOM.withCreatedLines && metric.Counter.CreatedTimestamp != nil {
createdTsBytesWritten, err = writeOpenMetricsCreated(w, compliantName, "_total", metric, "", 0, metric.Counter.GetCreatedTimestamp())
n += createdTsBytesWritten
}
case dto.MetricType_GAUGE:
if metric.Gauge == nil {
return written, fmt.Errorf(
"expected gauge in metric %s %s", name, metric,
"expected gauge in metric %s %s", compliantName, metric,
)
}
n, err = writeOpenMetricsSample(
w, name, "", metric, "", 0,
w, compliantName, "", metric, "", 0,
metric.Gauge.GetValue(), 0, false,
nil,
)
case dto.MetricType_UNTYPED:
if metric.Untyped == nil {
return written, fmt.Errorf(
"expected untyped in metric %s %s", name, metric,
"expected untyped in metric %s %s", compliantName, metric,
)
}
n, err = writeOpenMetricsSample(
w, name, "", metric, "", 0,
w, compliantName, "", metric, "", 0,
metric.Untyped.GetValue(), 0, false,
nil,
)
case dto.MetricType_SUMMARY:
if metric.Summary == nil {
return written, fmt.Errorf(
"expected summary in metric %s %s", name, metric,
"expected summary in metric %s %s", compliantName, metric,
)
}
for _, q := range metric.Summary.Quantile {
n, err = writeOpenMetricsSample(
w, name, "", metric,
w, compliantName, "", metric,
model.QuantileLabel, q.GetQuantile(),
q.GetValue(), 0, false,
nil,
@@ -222,7 +309,7 @@ func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily) (written int
}
}
n, err = writeOpenMetricsSample(
w, name, "_sum", metric, "", 0,
w, compliantName, "_sum", metric, "", 0,
metric.Summary.GetSampleSum(), 0, false,
nil,
)
@@ -231,20 +318,24 @@ func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily) (written int
return
}
n, err = writeOpenMetricsSample(
w, name, "_count", metric, "", 0,
w, compliantName, "_count", metric, "", 0,
0, metric.Summary.GetSampleCount(), true,
nil,
)
if toOM.withCreatedLines && metric.Summary.CreatedTimestamp != nil {
createdTsBytesWritten, err = writeOpenMetricsCreated(w, compliantName, "", metric, "", 0, metric.Summary.GetCreatedTimestamp())
n += createdTsBytesWritten
}
case dto.MetricType_HISTOGRAM:
if metric.Histogram == nil {
return written, fmt.Errorf(
"expected histogram in metric %s %s", name, metric,
"expected histogram in metric %s %s", compliantName, metric,
)
}
infSeen := false
for _, b := range metric.Histogram.Bucket {
n, err = writeOpenMetricsSample(
w, name, "_bucket", metric,
w, compliantName, "_bucket", metric,
model.BucketLabel, b.GetUpperBound(),
0, b.GetCumulativeCount(), true,
b.Exemplar,
@@ -259,7 +350,7 @@ func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily) (written int
}
if !infSeen {
n, err = writeOpenMetricsSample(
w, name, "_bucket", metric,
w, compliantName, "_bucket", metric,
model.BucketLabel, math.Inf(+1),
0, metric.Histogram.GetSampleCount(), true,
nil,
@@ -270,7 +361,7 @@ func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily) (written int
}
}
n, err = writeOpenMetricsSample(
w, name, "_sum", metric, "", 0,
w, compliantName, "_sum", metric, "", 0,
metric.Histogram.GetSampleSum(), 0, false,
nil,
)
@@ -279,13 +370,17 @@ func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily) (written int
return
}
n, err = writeOpenMetricsSample(
w, name, "_count", metric, "", 0,
w, compliantName, "_count", metric, "", 0,
0, metric.Histogram.GetSampleCount(), true,
nil,
)
if toOM.withCreatedLines && metric.Histogram.CreatedTimestamp != nil {
createdTsBytesWritten, err = writeOpenMetricsCreated(w, compliantName, "", metric, "", 0, metric.Histogram.GetCreatedTimestamp())
n += createdTsBytesWritten
}
default:
return written, fmt.Errorf(
"unexpected type in metric %s %s", name, metric,
"unexpected type in metric %s %s", compliantName, metric,
)
}
written += n
@@ -350,7 +445,7 @@ func writeOpenMetricsSample(
return written, err
}
}
if exemplar != nil {
if exemplar != nil && len(exemplar.Label) > 0 {
n, err = writeExemplar(w, exemplar)
written += n
if err != nil {
@@ -473,6 +568,49 @@ func writeOpenMetricsNameAndLabelPairs(
return written, nil
}
// writeOpenMetricsCreated writes the created timestamp for a single time series
// following OpenMetrics text format to w, given the metric name, the metric proto
// message itself, optionally a suffix to be removed, e.g. '_total' for counters,
// an additional label name with a float64 value (use empty string as label name if
// not required) and the timestamp that represents the created timestamp.
// The function returns the number of bytes written and any error encountered.
func writeOpenMetricsCreated(w enhancedWriter,
name, suffixToTrim string, metric *dto.Metric,
additionalLabelName string, additionalLabelValue float64,
createdTimestamp *timestamppb.Timestamp,
) (int, error) {
written := 0
n, err := writeOpenMetricsNameAndLabelPairs(
w, strings.TrimSuffix(name, suffixToTrim)+"_created", metric.Label, additionalLabelName, additionalLabelValue,
)
written += n
if err != nil {
return written, err
}
err = w.WriteByte(' ')
written++
if err != nil {
return written, err
}
// TODO(beorn7): Format this directly from components of ts to
// avoid overflow/underflow and precision issues of the float
// conversion.
n, err = writeOpenMetricsFloat(w, float64(createdTimestamp.AsTime().UnixNano())/1e9)
written += n
if err != nil {
return written, err
}
err = w.WriteByte('\n')
written++
if err != nil {
return written, err
}
return written, nil
}
// writeExemplar writes the provided exemplar in OpenMetrics format to w. The
// function returns the number of bytes written and any error encountered.
func writeExemplar(w enhancedWriter, e *dto.Exemplar) (int, error) {

View File

@@ -75,7 +75,12 @@ func (a *Alert) ResolvedAt(ts time.Time) bool {
// Status returns the status of the alert.
func (a *Alert) Status() AlertStatus {
if a.Resolved() {
return a.StatusAt(time.Now())
}
// StatusAt returns the status of the alert at the given timestamp.
func (a *Alert) StatusAt(ts time.Time) AlertStatus {
if a.ResolvedAt(ts) {
return AlertResolved
}
return AlertFiring
@@ -127,6 +132,17 @@ func (as Alerts) HasFiring() bool {
return false
}
// HasFiringAt returns true iff one of the alerts is not resolved
// at the time ts.
func (as Alerts) HasFiringAt(ts time.Time) bool {
for _, a := range as {
if !a.ResolvedAt(ts) {
return true
}
}
return false
}
// Status returns StatusFiring iff at least one of the alerts is firing.
func (as Alerts) Status() AlertStatus {
if as.HasFiring() {
@@ -134,3 +150,12 @@ func (as Alerts) Status() AlertStatus {
}
return AlertResolved
}
// StatusAt returns StatusFiring iff at least one of the alerts is firing
// at the time ts.
func (as Alerts) StatusAt(ts time.Time) AlertStatus {
if as.HasFiringAt(ts) {
return AlertFiring
}
return AlertResolved
}

View File

@@ -17,7 +17,6 @@ import (
"encoding/json"
"fmt"
"sort"
"strings"
)
// A LabelSet is a collection of LabelName and LabelValue pairs. The LabelSet
@@ -129,16 +128,6 @@ func (l LabelSet) Merge(other LabelSet) LabelSet {
return result
}
func (l LabelSet) String() string {
lstrs := make([]string, 0, len(l))
for l, v := range l {
lstrs = append(lstrs, fmt.Sprintf("%s=%q", l, v))
}
sort.Strings(lstrs)
return fmt.Sprintf("{%s}", strings.Join(lstrs, ", "))
}
// Fingerprint returns the LabelSet's fingerprint.
func (ls LabelSet) Fingerprint() Fingerprint {
return labelSetToFingerprint(ls)

View File

@@ -0,0 +1,45 @@
// Copyright 2024 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build go1.21
package model
import (
"bytes"
"slices"
"strconv"
)
// String will look like `{foo="bar", more="less"}`. Names are sorted alphabetically.
func (l LabelSet) String() string {
var lna [32]string // On stack to avoid memory allocation for sorting names.
labelNames := lna[:0]
for name := range l {
labelNames = append(labelNames, string(name))
}
slices.Sort(labelNames)
var bytea [1024]byte // On stack to avoid memory allocation while building the output.
b := bytes.NewBuffer(bytea[:0])
b.WriteByte('{')
for i, name := range labelNames {
if i > 0 {
b.WriteString(", ")
}
b.WriteString(name)
b.WriteByte('=')
b.Write(strconv.AppendQuote(b.AvailableBuffer(), string(l[LabelName(name)])))
}
b.WriteByte('}')
return b.String()
}

View File

@@ -0,0 +1,39 @@
// Copyright 2024 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !go1.21
package model
import (
"fmt"
"sort"
"strings"
)
// String was optimized using functions not available for go 1.20
// or lower. We keep the old implementation for compatibility with client_golang.
// Once client golang drops support for go 1.20 (scheduled for August 2024), this
// file can be removed.
func (l LabelSet) String() string {
labelNames := make([]string, 0, len(l))
for name := range l {
labelNames = append(labelNames, string(name))
}
sort.Strings(labelNames)
lstrs := make([]string, 0, len(l))
for _, name := range labelNames {
lstrs = append(lstrs, fmt.Sprintf("%s=%q", name, l[LabelName(name)]))
}
return fmt.Sprintf("{%s}", strings.Join(lstrs, ", "))
}

View File

@@ -204,6 +204,7 @@ func EscapeMetricFamily(v *dto.MetricFamily, scheme EscapingScheme) *dto.MetricF
out := &dto.MetricFamily{
Help: v.Help,
Type: v.Type,
Unit: v.Unit,
}
// If the name is nil, copy as-is, don't try to escape.

View File

@@ -1,9 +1,16 @@
---
linters:
enable:
- errcheck
- godot
- gosimple
- govet
- ineffassign
- misspell
- revive
- staticcheck
- testifylint
- unused
linter-settings:
godot:

View File

@@ -1,2 +1,3 @@
* Johannes 'fish' Ziemke <github@freigeist.org> @discordianfish
* Paul Gier <pgier@redhat.com> @pgier
* Paul Gier <paulgier@gmail.com> @pgier
* Ben Kochie <superq@gmail.com> @SuperQ

View File

@@ -49,23 +49,23 @@ endif
GOTEST := $(GO) test
GOTEST_DIR :=
ifneq ($(CIRCLE_JOB),)
ifneq ($(shell command -v gotestsum > /dev/null),)
ifneq ($(shell command -v gotestsum 2> /dev/null),)
GOTEST_DIR := test-results
GOTEST := gotestsum --junitfile $(GOTEST_DIR)/unit-tests.xml --
endif
endif
PROMU_VERSION ?= 0.15.0
PROMU_VERSION ?= 0.17.0
PROMU_URL := https://github.com/prometheus/promu/releases/download/v$(PROMU_VERSION)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM).tar.gz
SKIP_GOLANGCI_LINT :=
GOLANGCI_LINT :=
GOLANGCI_LINT_OPTS ?=
GOLANGCI_LINT_VERSION ?= v1.54.2
# golangci-lint only supports linux, darwin and windows platforms on i386/amd64.
GOLANGCI_LINT_VERSION ?= v1.59.0
# golangci-lint only supports linux, darwin and windows platforms on i386/amd64/arm64.
# windows isn't included here because of the path separator being different.
ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux darwin))
ifeq ($(GOHOSTARCH),$(filter $(GOHOSTARCH),amd64 i386))
ifeq ($(GOHOSTARCH),$(filter $(GOHOSTARCH),amd64 i386 arm64))
# If we're in CI and there is an Actions file, that means the linter
# is being run in Actions, so we don't need to run it here.
ifneq (,$(SKIP_GOLANGCI_LINT))
@@ -169,16 +169,20 @@ common-vet:
common-lint: $(GOLANGCI_LINT)
ifdef GOLANGCI_LINT
@echo ">> running golangci-lint"
# 'go list' needs to be executed before staticcheck to prepopulate the modules cache.
# Otherwise staticcheck might fail randomly for some reason not yet explained.
$(GO) list -e -compiled -test=true -export=false -deps=true -find=false -tags= -- ./... > /dev/null
$(GOLANGCI_LINT) run $(GOLANGCI_LINT_OPTS) $(pkgs)
endif
.PHONY: common-lint-fix
common-lint-fix: $(GOLANGCI_LINT)
ifdef GOLANGCI_LINT
@echo ">> running golangci-lint fix"
$(GOLANGCI_LINT) run --fix $(GOLANGCI_LINT_OPTS) $(pkgs)
endif
.PHONY: common-yamllint
common-yamllint:
@echo ">> running yamllint on all YAML files in the repository"
ifeq (, $(shell command -v yamllint > /dev/null))
ifeq (, $(shell command -v yamllint 2> /dev/null))
@echo "yamllint not installed so skipping"
else
yamllint .
@@ -204,6 +208,10 @@ common-tarball: promu
@echo ">> building release tarball"
$(PROMU) tarball --prefix $(PREFIX) $(BIN_DIR)
.PHONY: common-docker-repo-name
common-docker-repo-name:
@echo "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)"
.PHONY: common-docker $(BUILD_DOCKER_ARCHS)
common-docker: $(BUILD_DOCKER_ARCHS)
$(BUILD_DOCKER_ARCHS): common-docker-%:

View File

@@ -55,7 +55,7 @@ type ARPEntry struct {
func (fs FS) GatherARPEntries() ([]ARPEntry, error) {
data, err := os.ReadFile(fs.proc.Path("net/arp"))
if err != nil {
return nil, fmt.Errorf("%s: error reading arp %s: %w", ErrFileRead, fs.proc.Path("net/arp"), err)
return nil, fmt.Errorf("%w: error reading arp %s: %w", ErrFileRead, fs.proc.Path("net/arp"), err)
}
return parseARPEntries(data)
@@ -78,11 +78,11 @@ func parseARPEntries(data []byte) ([]ARPEntry, error) {
} else if width == expectedDataWidth {
entry, err := parseARPEntry(columns)
if err != nil {
return []ARPEntry{}, fmt.Errorf("%s: Failed to parse ARP entry: %v: %w", ErrFileParse, entry, err)
return []ARPEntry{}, fmt.Errorf("%w: Failed to parse ARP entry: %v: %w", ErrFileParse, entry, err)
}
entries = append(entries, entry)
} else {
return []ARPEntry{}, fmt.Errorf("%s: %d columns found, but expected %d: %w", ErrFileParse, width, expectedDataWidth, err)
return []ARPEntry{}, fmt.Errorf("%w: %d columns found, but expected %d: %w", ErrFileParse, width, expectedDataWidth, err)
}
}

View File

@@ -58,8 +58,8 @@ func parseBuddyInfo(r io.Reader) ([]BuddyInfo, error) {
return nil, fmt.Errorf("%w: Invalid number of fields, found: %v", ErrFileParse, parts)
}
node := strings.TrimRight(parts[1], ",")
zone := strings.TrimRight(parts[3], ",")
node := strings.TrimSuffix(parts[1], ",")
zone := strings.TrimSuffix(parts[3], ",")
arraySize := len(parts[4:])
if bucketCount == -1 {
@@ -74,7 +74,7 @@ func parseBuddyInfo(r io.Reader) ([]BuddyInfo, error) {
for i := 0; i < arraySize; i++ {
sizes[i], err = strconv.ParseFloat(parts[i+4], 64)
if err != nil {
return nil, fmt.Errorf("%s: Invalid valid in buddyinfo: %f: %w", ErrFileParse, sizes[i], err)
return nil, fmt.Errorf("%w: Invalid valid in buddyinfo: %f: %w", ErrFileParse, sizes[i], err)
}
}

View File

@@ -194,7 +194,7 @@ func parseCPUInfoARM(info []byte) ([]CPUInfo, error) {
firstLine := firstNonEmptyLine(scanner)
match, err := regexp.MatchString("^[Pp]rocessor", firstLine)
if !match || !strings.Contains(firstLine, ":") {
return nil, fmt.Errorf("%s: Cannot parse line: %q: %w", ErrFileParse, firstLine, err)
return nil, fmt.Errorf("%w: Cannot parse line: %q: %w", ErrFileParse, firstLine, err)
}
field := strings.SplitN(firstLine, ": ", 2)
@@ -386,7 +386,7 @@ func parseCPUInfoLoong(info []byte) ([]CPUInfo, error) {
// find the first "processor" line
firstLine := firstNonEmptyLine(scanner)
if !strings.HasPrefix(firstLine, "system type") || !strings.Contains(firstLine, ":") {
return nil, errors.New("invalid cpuinfo file: " + firstLine)
return nil, fmt.Errorf("%w: %q", ErrFileParse, firstLine)
}
field := strings.SplitN(firstLine, ": ", 2)
cpuinfo := []CPUInfo{}

View File

@@ -55,13 +55,13 @@ func (fs FS) Crypto() ([]Crypto, error) {
path := fs.proc.Path("crypto")
b, err := util.ReadFileNoStat(path)
if err != nil {
return nil, fmt.Errorf("%s: Cannot read file %v: %w", ErrFileRead, b, err)
return nil, fmt.Errorf("%w: Cannot read file %v: %w", ErrFileRead, b, err)
}
crypto, err := parseCrypto(bytes.NewReader(b))
if err != nil {
return nil, fmt.Errorf("%s: Cannot parse %v: %w", ErrFileParse, crypto, err)
return nil, fmt.Errorf("%w: Cannot parse %v: %w", ErrFileParse, crypto, err)
}
return crypto, nil
@@ -84,7 +84,7 @@ func parseCrypto(r io.Reader) ([]Crypto, error) {
kv := strings.Split(text, ":")
if len(kv) != 2 {
return nil, fmt.Errorf("%w: Cannot parae line: %q", ErrFileParse, text)
return nil, fmt.Errorf("%w: Cannot parse line: %q", ErrFileParse, text)
}
k := strings.TrimSpace(kv[0])

View File

@@ -236,7 +236,7 @@ func (fs FS) Fscacheinfo() (Fscacheinfo, error) {
m, err := parseFscacheinfo(bytes.NewReader(b))
if err != nil {
return Fscacheinfo{}, fmt.Errorf("%s: Cannot parse %v: %w", ErrFileParse, m, err)
return Fscacheinfo{}, fmt.Errorf("%w: Cannot parse %v: %w", ErrFileParse, m, err)
}
return *m, nil
@@ -245,7 +245,7 @@ func (fs FS) Fscacheinfo() (Fscacheinfo, error) {
func setFSCacheFields(fields []string, setFields ...*uint64) error {
var err error
if len(fields) < len(setFields) {
return fmt.Errorf("%s: Expected %d, but got %d: %w", ErrFileParse, len(setFields), len(fields), err)
return fmt.Errorf("%w: Expected %d, but got %d: %w", ErrFileParse, len(setFields), len(fields), err)
}
for i := range setFields {

View File

@@ -221,16 +221,16 @@ func parseIPPort(s string) (net.IP, uint16, error) {
case 46:
ip = net.ParseIP(s[1:40])
if ip == nil {
return nil, 0, fmt.Errorf("%s: Invalid IPv6 addr %s: %w", ErrFileParse, s[1:40], err)
return nil, 0, fmt.Errorf("%w: Invalid IPv6 addr %s: %w", ErrFileParse, s[1:40], err)
}
default:
return nil, 0, fmt.Errorf("%s: Unexpected IP:Port %s: %w", ErrFileParse, s, err)
return nil, 0, fmt.Errorf("%w: Unexpected IP:Port %s: %w", ErrFileParse, s, err)
}
portString := s[len(s)-4:]
if len(portString) != 4 {
return nil, 0,
fmt.Errorf("%s: Unexpected port string format %s: %w", ErrFileParse, portString, err)
fmt.Errorf("%w: Unexpected port string format %s: %w", ErrFileParse, portString, err)
}
port, err := strconv.ParseUint(portString, 16, 16)
if err != nil {

View File

@@ -51,7 +51,7 @@ func parseLoad(loadavgBytes []byte) (*LoadAvg, error) {
for i, load := range parts[0:3] {
loads[i], err = strconv.ParseFloat(load, 64)
if err != nil {
return nil, fmt.Errorf("%s: Cannot parse load: %f: %w", ErrFileParse, loads[i], err)
return nil, fmt.Errorf("%w: Cannot parse load: %f: %w", ErrFileParse, loads[i], err)
}
}
return &LoadAvg{

View File

@@ -23,7 +23,7 @@ import (
var (
statusLineRE = regexp.MustCompile(`(\d+) blocks .*\[(\d+)/(\d+)\] \[([U_]+)\]`)
recoveryLineBlocksRE = regexp.MustCompile(`\((\d+)/\d+\)`)
recoveryLineBlocksRE = regexp.MustCompile(`\((\d+/\d+)\)`)
recoveryLinePctRE = regexp.MustCompile(`= (.+)%`)
recoveryLineFinishRE = regexp.MustCompile(`finish=(.+)min`)
recoveryLineSpeedRE = regexp.MustCompile(`speed=(.+)[A-Z]`)
@@ -50,6 +50,8 @@ type MDStat struct {
BlocksTotal int64
// Number of blocks on the device that are in sync.
BlocksSynced int64
// Number of blocks on the device that need to be synced.
BlocksToBeSynced int64
// progress percentage of current sync
BlocksSyncedPct float64
// estimated finishing time for current sync (in minutes)
@@ -70,7 +72,7 @@ func (fs FS) MDStat() ([]MDStat, error) {
}
mdstat, err := parseMDStat(data)
if err != nil {
return nil, fmt.Errorf("%s: Cannot parse %v: %w", ErrFileParse, fs.proc.Path("mdstat"), err)
return nil, fmt.Errorf("%w: Cannot parse %v: %w", ErrFileParse, fs.proc.Path("mdstat"), err)
}
return mdstat, nil
}
@@ -90,7 +92,7 @@ func parseMDStat(mdStatData []byte) ([]MDStat, error) {
deviceFields := strings.Fields(line)
if len(deviceFields) < 3 {
return nil, fmt.Errorf("%s: Expected 3+ lines, got %q", ErrFileParse, line)
return nil, fmt.Errorf("%w: Expected 3+ lines, got %q", ErrFileParse, line)
}
mdName := deviceFields[0] // mdx
state := deviceFields[2] // active or inactive
@@ -105,7 +107,7 @@ func parseMDStat(mdStatData []byte) ([]MDStat, error) {
active, total, down, size, err := evalStatusLine(lines[i], lines[i+1])
if err != nil {
return nil, fmt.Errorf("%s: Cannot parse md device lines: %v: %w", ErrFileParse, active, err)
return nil, fmt.Errorf("%w: Cannot parse md device lines: %v: %w", ErrFileParse, active, err)
}
syncLineIdx := i + 2
@@ -115,7 +117,8 @@ func parseMDStat(mdStatData []byte) ([]MDStat, error) {
// If device is syncing at the moment, get the number of currently
// synced bytes, otherwise that number equals the size of the device.
syncedBlocks := size
blocksSynced := size
blocksToBeSynced := size
speed := float64(0)
finish := float64(0)
pct := float64(0)
@@ -136,11 +139,11 @@ func parseMDStat(mdStatData []byte) ([]MDStat, error) {
// Handle case when resync=PENDING or resync=DELAYED.
if strings.Contains(lines[syncLineIdx], "PENDING") ||
strings.Contains(lines[syncLineIdx], "DELAYED") {
syncedBlocks = 0
blocksSynced = 0
} else {
syncedBlocks, pct, finish, speed, err = evalRecoveryLine(lines[syncLineIdx])
blocksSynced, blocksToBeSynced, pct, finish, speed, err = evalRecoveryLine(lines[syncLineIdx])
if err != nil {
return nil, fmt.Errorf("%s: Cannot parse sync line in md device: %q: %w", ErrFileParse, mdName, err)
return nil, fmt.Errorf("%w: Cannot parse sync line in md device: %q: %w", ErrFileParse, mdName, err)
}
}
}
@@ -154,7 +157,8 @@ func parseMDStat(mdStatData []byte) ([]MDStat, error) {
DisksSpare: spare,
DisksTotal: total,
BlocksTotal: size,
BlocksSynced: syncedBlocks,
BlocksSynced: blocksSynced,
BlocksToBeSynced: blocksToBeSynced,
BlocksSyncedPct: pct,
BlocksSyncedFinishTime: finish,
BlocksSyncedSpeed: speed,
@@ -168,13 +172,13 @@ func parseMDStat(mdStatData []byte) ([]MDStat, error) {
func evalStatusLine(deviceLine, statusLine string) (active, total, down, size int64, err error) {
statusFields := strings.Fields(statusLine)
if len(statusFields) < 1 {
return 0, 0, 0, 0, fmt.Errorf("%s: Unexpected statusline %q: %w", ErrFileParse, statusLine, err)
return 0, 0, 0, 0, fmt.Errorf("%w: Unexpected statusline %q: %w", ErrFileParse, statusLine, err)
}
sizeStr := statusFields[0]
size, err = strconv.ParseInt(sizeStr, 10, 64)
if err != nil {
return 0, 0, 0, 0, fmt.Errorf("%s: Unexpected statusline %q: %w", ErrFileParse, statusLine, err)
return 0, 0, 0, 0, fmt.Errorf("%w: Unexpected statusline %q: %w", ErrFileParse, statusLine, err)
}
if strings.Contains(deviceLine, "raid0") || strings.Contains(deviceLine, "linear") {
@@ -189,65 +193,71 @@ func evalStatusLine(deviceLine, statusLine string) (active, total, down, size in
matches := statusLineRE.FindStringSubmatch(statusLine)
if len(matches) != 5 {
return 0, 0, 0, 0, fmt.Errorf("%s: Could not fild all substring matches %s: %w", ErrFileParse, statusLine, err)
return 0, 0, 0, 0, fmt.Errorf("%w: Could not fild all substring matches %s: %w", ErrFileParse, statusLine, err)
}
total, err = strconv.ParseInt(matches[2], 10, 64)
if err != nil {
return 0, 0, 0, 0, fmt.Errorf("%s: Unexpected statusline %q: %w", ErrFileParse, statusLine, err)
return 0, 0, 0, 0, fmt.Errorf("%w: Unexpected statusline %q: %w", ErrFileParse, statusLine, err)
}
active, err = strconv.ParseInt(matches[3], 10, 64)
if err != nil {
return 0, 0, 0, 0, fmt.Errorf("%s: Unexpected active %d: %w", ErrFileParse, active, err)
return 0, 0, 0, 0, fmt.Errorf("%w: Unexpected active %d: %w", ErrFileParse, active, err)
}
down = int64(strings.Count(matches[4], "_"))
return active, total, down, size, nil
}
func evalRecoveryLine(recoveryLine string) (syncedBlocks int64, pct float64, finish float64, speed float64, err error) {
func evalRecoveryLine(recoveryLine string) (blocksSynced int64, blocksToBeSynced int64, pct float64, finish float64, speed float64, err error) {
matches := recoveryLineBlocksRE.FindStringSubmatch(recoveryLine)
if len(matches) != 2 {
return 0, 0, 0, 0, fmt.Errorf("%s: Unexpected recoveryLine %s: %w", ErrFileParse, recoveryLine, err)
return 0, 0, 0, 0, 0, fmt.Errorf("%w: Unexpected recoveryLine blocks %s: %w", ErrFileParse, recoveryLine, err)
}
syncedBlocks, err = strconv.ParseInt(matches[1], 10, 64)
blocks := strings.Split(matches[1], "/")
blocksSynced, err = strconv.ParseInt(blocks[0], 10, 64)
if err != nil {
return 0, 0, 0, 0, fmt.Errorf("%s: Unexpected parsing of recoveryLine %q: %w", ErrFileParse, recoveryLine, err)
return 0, 0, 0, 0, 0, fmt.Errorf("%w: Unable to parse recovery blocks synced %q: %w", ErrFileParse, matches[1], err)
}
blocksToBeSynced, err = strconv.ParseInt(blocks[1], 10, 64)
if err != nil {
return blocksSynced, 0, 0, 0, 0, fmt.Errorf("%w: Unable to parse recovery to be synced blocks %q: %w", ErrFileParse, matches[2], err)
}
// Get percentage complete
matches = recoveryLinePctRE.FindStringSubmatch(recoveryLine)
if len(matches) != 2 {
return syncedBlocks, 0, 0, 0, fmt.Errorf("%w: Unexpected recoveryLine matching percentage %s", ErrFileParse, recoveryLine)
return blocksSynced, blocksToBeSynced, 0, 0, 0, fmt.Errorf("%w: Unexpected recoveryLine matching percentage %s", ErrFileParse, recoveryLine)
}
pct, err = strconv.ParseFloat(strings.TrimSpace(matches[1]), 64)
if err != nil {
return syncedBlocks, 0, 0, 0, fmt.Errorf("%w: Error parsing float from recoveryLine %q", ErrFileParse, recoveryLine)
return blocksSynced, blocksToBeSynced, 0, 0, 0, fmt.Errorf("%w: Error parsing float from recoveryLine %q", ErrFileParse, recoveryLine)
}
// Get time expected left to complete
matches = recoveryLineFinishRE.FindStringSubmatch(recoveryLine)
if len(matches) != 2 {
return syncedBlocks, pct, 0, 0, fmt.Errorf("%w: Unexpected recoveryLine matching est. finish time: %s", ErrFileParse, recoveryLine)
return blocksSynced, blocksToBeSynced, pct, 0, 0, fmt.Errorf("%w: Unexpected recoveryLine matching est. finish time: %s", ErrFileParse, recoveryLine)
}
finish, err = strconv.ParseFloat(matches[1], 64)
if err != nil {
return syncedBlocks, pct, 0, 0, fmt.Errorf("%w: Unable to parse float from recoveryLine: %q", ErrFileParse, recoveryLine)
return blocksSynced, blocksToBeSynced, pct, 0, 0, fmt.Errorf("%w: Unable to parse float from recoveryLine: %q", ErrFileParse, recoveryLine)
}
// Get recovery speed
matches = recoveryLineSpeedRE.FindStringSubmatch(recoveryLine)
if len(matches) != 2 {
return syncedBlocks, pct, finish, 0, fmt.Errorf("%w: Unexpected recoveryLine value: %s", ErrFileParse, recoveryLine)
return blocksSynced, blocksToBeSynced, pct, finish, 0, fmt.Errorf("%w: Unexpected recoveryLine value: %s", ErrFileParse, recoveryLine)
}
speed, err = strconv.ParseFloat(matches[1], 64)
if err != nil {
return syncedBlocks, pct, finish, 0, fmt.Errorf("%s: Error parsing float from recoveryLine: %q: %w", ErrFileParse, recoveryLine, err)
return blocksSynced, blocksToBeSynced, pct, finish, 0, fmt.Errorf("%w: Error parsing float from recoveryLine: %q: %w", ErrFileParse, recoveryLine, err)
}
return syncedBlocks, pct, finish, speed, nil
return blocksSynced, blocksToBeSynced, pct, finish, speed, nil
}
func evalComponentDevices(deviceFields []string) []string {

View File

@@ -126,6 +126,7 @@ type Meminfo struct {
VmallocUsed *uint64
// largest contiguous block of vmalloc area which is free
VmallocChunk *uint64
Percpu *uint64
HardwareCorrupted *uint64
AnonHugePages *uint64
ShmemHugePages *uint64
@@ -140,6 +141,55 @@ type Meminfo struct {
DirectMap4k *uint64
DirectMap2M *uint64
DirectMap1G *uint64
// The struct fields below are the byte-normalized counterparts to the
// existing struct fields. Values are normalized using the optional
// unit field in the meminfo line.
MemTotalBytes *uint64
MemFreeBytes *uint64
MemAvailableBytes *uint64
BuffersBytes *uint64
CachedBytes *uint64
SwapCachedBytes *uint64
ActiveBytes *uint64
InactiveBytes *uint64
ActiveAnonBytes *uint64
InactiveAnonBytes *uint64
ActiveFileBytes *uint64
InactiveFileBytes *uint64
UnevictableBytes *uint64
MlockedBytes *uint64
SwapTotalBytes *uint64
SwapFreeBytes *uint64
DirtyBytes *uint64
WritebackBytes *uint64
AnonPagesBytes *uint64
MappedBytes *uint64
ShmemBytes *uint64
SlabBytes *uint64
SReclaimableBytes *uint64
SUnreclaimBytes *uint64
KernelStackBytes *uint64
PageTablesBytes *uint64
NFSUnstableBytes *uint64
BounceBytes *uint64
WritebackTmpBytes *uint64
CommitLimitBytes *uint64
CommittedASBytes *uint64
VmallocTotalBytes *uint64
VmallocUsedBytes *uint64
VmallocChunkBytes *uint64
PercpuBytes *uint64
HardwareCorruptedBytes *uint64
AnonHugePagesBytes *uint64
ShmemHugePagesBytes *uint64
ShmemPmdMappedBytes *uint64
CmaTotalBytes *uint64
CmaFreeBytes *uint64
HugepagesizeBytes *uint64
DirectMap4kBytes *uint64
DirectMap2MBytes *uint64
DirectMap1GBytes *uint64
}
// Meminfo returns an information about current kernel/system memory statistics.
@@ -152,7 +202,7 @@ func (fs FS) Meminfo() (Meminfo, error) {
m, err := parseMemInfo(bytes.NewReader(b))
if err != nil {
return Meminfo{}, fmt.Errorf("%s: %w", ErrFileParse, err)
return Meminfo{}, fmt.Errorf("%w: %w", ErrFileParse, err)
}
return *m, nil
@@ -162,114 +212,176 @@ func parseMemInfo(r io.Reader) (*Meminfo, error) {
var m Meminfo
s := bufio.NewScanner(r)
for s.Scan() {
// Each line has at least a name and value; we ignore the unit.
fields := strings.Fields(s.Text())
if len(fields) < 2 {
return nil, fmt.Errorf("%w: Malformed line %q", ErrFileParse, s.Text())
}
var val, valBytes uint64
v, err := strconv.ParseUint(fields[1], 0, 64)
val, err := strconv.ParseUint(fields[1], 0, 64)
if err != nil {
return nil, err
}
switch len(fields) {
case 2:
// No unit present, use the parsed the value as bytes directly.
valBytes = val
case 3:
// Unit present in optional 3rd field, convert it to
// bytes. The only unit supported within the Linux
// kernel is `kB`.
if fields[2] != "kB" {
return nil, fmt.Errorf("%w: Unsupported unit in optional 3rd field %q", ErrFileParse, fields[2])
}
valBytes = 1024 * val
default:
return nil, fmt.Errorf("%w: Malformed line %q", ErrFileParse, s.Text())
}
switch fields[0] {
case "MemTotal:":
m.MemTotal = &v
m.MemTotal = &val
m.MemTotalBytes = &valBytes
case "MemFree:":
m.MemFree = &v
m.MemFree = &val
m.MemFreeBytes = &valBytes
case "MemAvailable:":
m.MemAvailable = &v
m.MemAvailable = &val
m.MemAvailableBytes = &valBytes
case "Buffers:":
m.Buffers = &v
m.Buffers = &val
m.BuffersBytes = &valBytes
case "Cached:":
m.Cached = &v
m.Cached = &val
m.CachedBytes = &valBytes
case "SwapCached:":
m.SwapCached = &v
m.SwapCached = &val
m.SwapCachedBytes = &valBytes
case "Active:":
m.Active = &v
m.Active = &val
m.ActiveBytes = &valBytes
case "Inactive:":
m.Inactive = &v
m.Inactive = &val
m.InactiveBytes = &valBytes
case "Active(anon):":
m.ActiveAnon = &v
m.ActiveAnon = &val
m.ActiveAnonBytes = &valBytes
case "Inactive(anon):":
m.InactiveAnon = &v
m.InactiveAnon = &val
m.InactiveAnonBytes = &valBytes
case "Active(file):":
m.ActiveFile = &v
m.ActiveFile = &val
m.ActiveFileBytes = &valBytes
case "Inactive(file):":
m.InactiveFile = &v
m.InactiveFile = &val
m.InactiveFileBytes = &valBytes
case "Unevictable:":
m.Unevictable = &v
m.Unevictable = &val
m.UnevictableBytes = &valBytes
case "Mlocked:":
m.Mlocked = &v
m.Mlocked = &val
m.MlockedBytes = &valBytes
case "SwapTotal:":
m.SwapTotal = &v
m.SwapTotal = &val
m.SwapTotalBytes = &valBytes
case "SwapFree:":
m.SwapFree = &v
m.SwapFree = &val
m.SwapFreeBytes = &valBytes
case "Dirty:":
m.Dirty = &v
m.Dirty = &val
m.DirtyBytes = &valBytes
case "Writeback:":
m.Writeback = &v
m.Writeback = &val
m.WritebackBytes = &valBytes
case "AnonPages:":
m.AnonPages = &v
m.AnonPages = &val
m.AnonPagesBytes = &valBytes
case "Mapped:":
m.Mapped = &v
m.Mapped = &val
m.MappedBytes = &valBytes
case "Shmem:":
m.Shmem = &v
m.Shmem = &val
m.ShmemBytes = &valBytes
case "Slab:":
m.Slab = &v
m.Slab = &val
m.SlabBytes = &valBytes
case "SReclaimable:":
m.SReclaimable = &v
m.SReclaimable = &val
m.SReclaimableBytes = &valBytes
case "SUnreclaim:":
m.SUnreclaim = &v
m.SUnreclaim = &val
m.SUnreclaimBytes = &valBytes
case "KernelStack:":
m.KernelStack = &v
m.KernelStack = &val
m.KernelStackBytes = &valBytes
case "PageTables:":
m.PageTables = &v
m.PageTables = &val
m.PageTablesBytes = &valBytes
case "NFS_Unstable:":
m.NFSUnstable = &v
m.NFSUnstable = &val
m.NFSUnstableBytes = &valBytes
case "Bounce:":
m.Bounce = &v
m.Bounce = &val
m.BounceBytes = &valBytes
case "WritebackTmp:":
m.WritebackTmp = &v
m.WritebackTmp = &val
m.WritebackTmpBytes = &valBytes
case "CommitLimit:":
m.CommitLimit = &v
m.CommitLimit = &val
m.CommitLimitBytes = &valBytes
case "Committed_AS:":
m.CommittedAS = &v
m.CommittedAS = &val
m.CommittedASBytes = &valBytes
case "VmallocTotal:":
m.VmallocTotal = &v
m.VmallocTotal = &val
m.VmallocTotalBytes = &valBytes
case "VmallocUsed:":
m.VmallocUsed = &v
m.VmallocUsed = &val
m.VmallocUsedBytes = &valBytes
case "VmallocChunk:":
m.VmallocChunk = &v
m.VmallocChunk = &val
m.VmallocChunkBytes = &valBytes
case "Percpu:":
m.Percpu = &val
m.PercpuBytes = &valBytes
case "HardwareCorrupted:":
m.HardwareCorrupted = &v
m.HardwareCorrupted = &val
m.HardwareCorruptedBytes = &valBytes
case "AnonHugePages:":
m.AnonHugePages = &v
m.AnonHugePages = &val
m.AnonHugePagesBytes = &valBytes
case "ShmemHugePages:":
m.ShmemHugePages = &v
m.ShmemHugePages = &val
m.ShmemHugePagesBytes = &valBytes
case "ShmemPmdMapped:":
m.ShmemPmdMapped = &v
m.ShmemPmdMapped = &val
m.ShmemPmdMappedBytes = &valBytes
case "CmaTotal:":
m.CmaTotal = &v
m.CmaTotal = &val
m.CmaTotalBytes = &valBytes
case "CmaFree:":
m.CmaFree = &v
m.CmaFree = &val
m.CmaFreeBytes = &valBytes
case "HugePages_Total:":
m.HugePagesTotal = &v
m.HugePagesTotal = &val
case "HugePages_Free:":
m.HugePagesFree = &v
m.HugePagesFree = &val
case "HugePages_Rsvd:":
m.HugePagesRsvd = &v
m.HugePagesRsvd = &val
case "HugePages_Surp:":
m.HugePagesSurp = &v
m.HugePagesSurp = &val
case "Hugepagesize:":
m.Hugepagesize = &v
m.Hugepagesize = &val
m.HugepagesizeBytes = &valBytes
case "DirectMap4k:":
m.DirectMap4k = &v
m.DirectMap4k = &val
m.DirectMap4kBytes = &valBytes
case "DirectMap2M:":
m.DirectMap2M = &v
m.DirectMap2M = &val
m.DirectMap2MBytes = &valBytes
case "DirectMap1G:":
m.DirectMap1G = &v
m.DirectMap1G = &val
m.DirectMap1GBytes = &valBytes
}
}

View File

@@ -109,7 +109,7 @@ func parseMountInfoString(mountString string) (*MountInfo, error) {
if mountInfo[6] != "" {
mount.OptionalFields, err = mountOptionsParseOptionalFields(mountInfo[6 : mountInfoLength-4])
if err != nil {
return nil, fmt.Errorf("%s: %w", ErrFileParse, err)
return nil, fmt.Errorf("%w: %w", ErrFileParse, err)
}
}
return mount, nil

View File

@@ -88,7 +88,7 @@ type MountStatsNFS struct {
// Statistics broken down by filesystem operation.
Operations []NFSOperationStats
// Statistics about the NFS RPC transport.
Transport NFSTransportStats
Transport []NFSTransportStats
}
// mountStats implements MountStats.
@@ -194,8 +194,6 @@ type NFSOperationStats struct {
CumulativeTotalResponseMilliseconds uint64
// Duration from when a request was enqueued to when it was completely handled.
CumulativeTotalRequestMilliseconds uint64
// The average time from the point the client sends RPC requests until it receives the response.
AverageRTTMilliseconds float64
// The count of operations that complete with tk_status < 0. These statuses usually indicate error conditions.
Errors uint64
}
@@ -434,7 +432,7 @@ func parseMountStatsNFS(s *bufio.Scanner, statVersion string) (*MountStatsNFS, e
return nil, err
}
stats.Transport = *tstats
stats.Transport = append(stats.Transport, *tstats)
}
// When encountering "per-operation statistics", we must break this
@@ -582,9 +580,6 @@ func parseNFSOperationStats(s *bufio.Scanner) ([]NFSOperationStats, error) {
CumulativeTotalResponseMilliseconds: ns[6],
CumulativeTotalRequestMilliseconds: ns[7],
}
if ns[0] != 0 {
opStats.AverageRTTMilliseconds = float64(ns[6]) / float64(ns[0])
}
if len(ns) > 8 {
opStats.Errors = ns[8]
@@ -632,7 +627,7 @@ func parseNFSTransportStats(ss []string, statVersion string) (*NFSTransportStats
return nil, fmt.Errorf("%w: invalid NFS transport stats 1.1 statement: %v, protocol: %v", ErrFileParse, ss, protocol)
}
default:
return nil, fmt.Errorf("%s: Unrecognized NFS transport stats version: %q, protocol: %v", ErrFileParse, statVersion, protocol)
return nil, fmt.Errorf("%w: Unrecognized NFS transport stats version: %q, protocol: %v", ErrFileParse, statVersion, protocol)
}
// Allocate enough for v1.1 stats since zero value for v1.1 stats will be okay

View File

@@ -58,7 +58,7 @@ func readConntrackStat(path string) ([]ConntrackStatEntry, error) {
stat, err := parseConntrackStat(bytes.NewReader(b))
if err != nil {
return nil, fmt.Errorf("%s: Cannot read file: %v: %w", ErrFileRead, path, err)
return nil, fmt.Errorf("%w: Cannot read file: %v: %w", ErrFileRead, path, err)
}
return stat, nil
@@ -86,7 +86,7 @@ func parseConntrackStat(r io.Reader) ([]ConntrackStatEntry, error) {
func parseConntrackStatEntry(fields []string) (*ConntrackStatEntry, error) {
entries, err := util.ParseHexUint64s(fields)
if err != nil {
return nil, fmt.Errorf("%s: Cannot parse entry: %d: %w", ErrFileParse, entries, err)
return nil, fmt.Errorf("%w: Cannot parse entry: %d: %w", ErrFileParse, entries, err)
}
numEntries := len(entries)
if numEntries < 16 || numEntries > 17 {

View File

@@ -50,10 +50,13 @@ type (
// UsedSockets shows the total number of parsed lines representing the
// number of used sockets.
UsedSockets uint64
// Drops shows the total number of dropped packets of all UPD sockets.
Drops *uint64
}
// netIPSocketLine represents the fields parsed from a single line
// in /proc/net/{t,u}dp{,6}. Fields which are not used by IPSocket are skipped.
// Drops is non-nil for udp{,6}, but nil for tcp{,6}.
// For the proc file format details, see https://linux.die.net/man/5/proc.
netIPSocketLine struct {
Sl uint64
@@ -66,6 +69,7 @@ type (
RxQueue uint64
UID uint64
Inode uint64
Drops *uint64
}
)
@@ -77,13 +81,14 @@ func newNetIPSocket(file string) (NetIPSocket, error) {
defer f.Close()
var netIPSocket NetIPSocket
isUDP := strings.Contains(file, "udp")
lr := io.LimitReader(f, readLimit)
s := bufio.NewScanner(lr)
s.Scan() // skip first line with headers
for s.Scan() {
fields := strings.Fields(s.Text())
line, err := parseNetIPSocketLine(fields)
line, err := parseNetIPSocketLine(fields, isUDP)
if err != nil {
return nil, err
}
@@ -104,19 +109,25 @@ func newNetIPSocketSummary(file string) (*NetIPSocketSummary, error) {
defer f.Close()
var netIPSocketSummary NetIPSocketSummary
var udpPacketDrops uint64
isUDP := strings.Contains(file, "udp")
lr := io.LimitReader(f, readLimit)
s := bufio.NewScanner(lr)
s.Scan() // skip first line with headers
for s.Scan() {
fields := strings.Fields(s.Text())
line, err := parseNetIPSocketLine(fields)
line, err := parseNetIPSocketLine(fields, isUDP)
if err != nil {
return nil, err
}
netIPSocketSummary.TxQueueLength += line.TxQueue
netIPSocketSummary.RxQueueLength += line.RxQueue
netIPSocketSummary.UsedSockets++
if isUDP {
udpPacketDrops += *line.Drops
netIPSocketSummary.Drops = &udpPacketDrops
}
}
if err := s.Err(); err != nil {
return nil, err
@@ -130,7 +141,7 @@ func parseIP(hexIP string) (net.IP, error) {
var byteIP []byte
byteIP, err := hex.DecodeString(hexIP)
if err != nil {
return nil, fmt.Errorf("%s: Cannot parse socket field in %q: %w", ErrFileParse, hexIP, err)
return nil, fmt.Errorf("%w: Cannot parse socket field in %q: %w", ErrFileParse, hexIP, err)
}
switch len(byteIP) {
case 4:
@@ -144,12 +155,12 @@ func parseIP(hexIP string) (net.IP, error) {
}
return i, nil
default:
return nil, fmt.Errorf("%s: Unable to parse IP %s: %w", ErrFileParse, hexIP, nil)
return nil, fmt.Errorf("%w: Unable to parse IP %s: %v", ErrFileParse, hexIP, nil)
}
}
// parseNetIPSocketLine parses a single line, represented by a list of fields.
func parseNetIPSocketLine(fields []string) (*netIPSocketLine, error) {
func parseNetIPSocketLine(fields []string, isUDP bool) (*netIPSocketLine, error) {
line := &netIPSocketLine{}
if len(fields) < 10 {
return nil, fmt.Errorf(
@@ -167,7 +178,7 @@ func parseNetIPSocketLine(fields []string) (*netIPSocketLine, error) {
}
if line.Sl, err = strconv.ParseUint(s[0], 0, 64); err != nil {
return nil, fmt.Errorf("%s: Unable to parse sl field in %q: %w", ErrFileParse, line.Sl, err)
return nil, fmt.Errorf("%w: Unable to parse sl field in %q: %w", ErrFileParse, line.Sl, err)
}
// local_address
l := strings.Split(fields[1], ":")
@@ -178,7 +189,7 @@ func parseNetIPSocketLine(fields []string) (*netIPSocketLine, error) {
return nil, err
}
if line.LocalPort, err = strconv.ParseUint(l[1], 16, 64); err != nil {
return nil, fmt.Errorf("%s: Unable to parse local_address port value line %q: %w", ErrFileParse, line.LocalPort, err)
return nil, fmt.Errorf("%w: Unable to parse local_address port value line %q: %w", ErrFileParse, line.LocalPort, err)
}
// remote_address
@@ -190,12 +201,12 @@ func parseNetIPSocketLine(fields []string) (*netIPSocketLine, error) {
return nil, err
}
if line.RemPort, err = strconv.ParseUint(r[1], 16, 64); err != nil {
return nil, fmt.Errorf("%s: Cannot parse rem_address port value in %q: %w", ErrFileParse, line.RemPort, err)
return nil, fmt.Errorf("%w: Cannot parse rem_address port value in %q: %w", ErrFileParse, line.RemPort, err)
}
// st
if line.St, err = strconv.ParseUint(fields[3], 16, 64); err != nil {
return nil, fmt.Errorf("%s: Cannot parse st value in %q: %w", ErrFileParse, line.St, err)
return nil, fmt.Errorf("%w: Cannot parse st value in %q: %w", ErrFileParse, line.St, err)
}
// tx_queue and rx_queue
@@ -208,20 +219,29 @@ func parseNetIPSocketLine(fields []string) (*netIPSocketLine, error) {
)
}
if line.TxQueue, err = strconv.ParseUint(q[0], 16, 64); err != nil {
return nil, fmt.Errorf("%s: Cannot parse tx_queue value in %q: %w", ErrFileParse, line.TxQueue, err)
return nil, fmt.Errorf("%w: Cannot parse tx_queue value in %q: %w", ErrFileParse, line.TxQueue, err)
}
if line.RxQueue, err = strconv.ParseUint(q[1], 16, 64); err != nil {
return nil, fmt.Errorf("%s: Cannot parse trx_queue value in %q: %w", ErrFileParse, line.RxQueue, err)
return nil, fmt.Errorf("%w: Cannot parse trx_queue value in %q: %w", ErrFileParse, line.RxQueue, err)
}
// uid
if line.UID, err = strconv.ParseUint(fields[7], 0, 64); err != nil {
return nil, fmt.Errorf("%s: Cannot parse UID value in %q: %w", ErrFileParse, line.UID, err)
return nil, fmt.Errorf("%w: Cannot parse UID value in %q: %w", ErrFileParse, line.UID, err)
}
// inode
if line.Inode, err = strconv.ParseUint(fields[9], 0, 64); err != nil {
return nil, fmt.Errorf("%s: Cannot parse inode value in %q: %w", ErrFileParse, line.Inode, err)
return nil, fmt.Errorf("%w: Cannot parse inode value in %q: %w", ErrFileParse, line.Inode, err)
}
// drops
if isUDP {
drops, err := strconv.ParseUint(fields[12], 0, 64)
if err != nil {
return nil, fmt.Errorf("%w: Cannot parse drops value in %q: %w", ErrFileParse, drops, err)
}
line.Drops = &drops
}
return line, nil

View File

@@ -69,7 +69,7 @@ func readSockstat(name string) (*NetSockstat, error) {
stat, err := parseSockstat(bytes.NewReader(b))
if err != nil {
return nil, fmt.Errorf("%s: sockstats from %q: %w", ErrFileRead, name, err)
return nil, fmt.Errorf("%w: sockstats from %q: %w", ErrFileRead, name, err)
}
return stat, nil
@@ -89,7 +89,7 @@ func parseSockstat(r io.Reader) (*NetSockstat, error) {
// The remaining fields are key/value pairs.
kvs, err := parseSockstatKVs(fields[1:])
if err != nil {
return nil, fmt.Errorf("%s: sockstat key/value pairs from %q: %w", ErrFileParse, s.Text(), err)
return nil, fmt.Errorf("%w: sockstat key/value pairs from %q: %w", ErrFileParse, s.Text(), err)
}
// The first field is the protocol. We must trim its colon suffix.

Some files were not shown because too many files have changed in this diff Show More