21 Commits

Author SHA1 Message Date
lukaszraczylo bbbeb8461b chore(ci): bump actions off deprecated Node 20 (#2)
actions/checkout@v4 -> @v6, actions/setup-go@v5 -> @v6,
peter-evans/create-pull-request@v7 -> @v8. Pinned major
versions; all three were flagged by the runner deprecation
notice (Node 20 forced off June 2 2026, removed Sept 16 2026).
2026-05-20 23:00:25 +01:00
lukaszraczylo bfb7e9875e feat(client): opt-in fasthttp transport (NewFastHTTPDoer)
Adds an alternative HTTPDoer backed by valyala/fasthttp for high-throughput
bots. Cuts per-call allocs from 102 to 56 in the cross-library bench
(within 8 of telego, which uses fasthttp by default), and per-call bytes
from 11.1 KiB to 6.6 KiB.

  bot := client.New(token,
      client.WithHTTPClient(client.NewFastHTTPDoer()),
  )

Implementation notes:
  - Wraps *fasthttp.Client behind the existing HTTPDoer (Do *http.Request)
    interface, so RetryDoer, custom transports, observability middleware,
    and the 1428 generated tests all keep working as-is.
  - Translates *http.Request -> fasthttp.Request once per call and
    returns a *http.Response whose Body releases the pooled fasthttp
    response on Close (net/http contract).
  - Recognises the bufferReadCloser / readerReadCloser shapes produced
    by buildRequest and passes their underlying bytes straight to
    SetBodyRaw -- no io.ReadAll, no copy.
  - Honours ctx.Deadline via DoDeadline, falls back to WithFastHTTPReadTimeout
    when no deadline is set. fasthttp.ErrTimeout maps to
    context.DeadlineExceeded for errors.Is compatibility.

Default stays net/http: fasthttp is HTTP/1.1 only, doesn't compose with
the http.RoundTripper middleware ecosystem, and most users don't have
the throughput to notice. Bots making thousands of API calls/sec should
opt in.

Multipart/file-upload path remains on net/http per the agreed scope --
the perf bottleneck was JSON-method round-trip, not file uploads.

Time numbers in the report deferred until a quiet-system bench run;
allocs/bytes numbers (which are deterministic per code path) are
already updated.
2026-05-10 23:07:04 +01:00
lukaszraczylo 75c7ce3119 perf(client): pool req-body buffer + manual http.Request with cached URL
Two changes that together cut allocs/call from 15 to 13 (client-internal
bench) and per-call CPU from 600ns to 455ns (-24%) on the no-HTTP path:

1. Codec gets an optional BodyEncoder extension (MarshalTo io.Writer).
   When present, encodeJSONBody stream-encodes the request directly into
   a pooled *bytes.Buffer instead of allocating a [2-step] Marshal+Reader
   pair. DefaultCodec implements it via goccy/go-json.NewEncoder.
2. *Bot caches the parsed base URL on construction. buildRequest skips
   net/http.NewRequestWithContext for the common case and constructs
   *http.Request manually — clones the URL by value, sets the method
   path, and populates ContentLength + GetBody from the body's concrete
   type so RetryDoer's body-replay across attempts still works.

Cross-library bench (sendMessage round-trip vs httptest.Server): -2
allocs/call (104 -> 102), bytes -1.2%, time within noise (real HTTP
plumbing dominates). The CPU win is visible on synthetic stub-doer
benches and translates to lower GC pressure on sustained-throughput
workloads.

Slow-path fallback preserved for codecs that don't implement BodyEncoder
and for *Bot instances where url.Parse on the configured base failed —
they take the original NewRequestWithContext path.
2026-05-10 22:36:57 +01:00
lukaszraczylo 607c3e8ddd test(bench): cross-library benchmarks vs top 5 go telegram libraries
Adds test/benchmarks/ as a separate Go module so competitor deps
(go-telegram-bot-api/v5, telebot.v3, go-telegram/bot, telego,
echotron/v3) stay out of the root go.mod.

Hot paths covered:
  - Webhook decode  (small Update -> typed Update struct)
  - Large unmarshal (Update with entities + reply markup + photo array)
  - API round-trip  (sendMessage against httptest.Server)
  - Dispatch route  (20 handlers, last-registered matches)

Results on Apple M4 Max / go1.26.2: ours wins 3 of 4 paths and is
2nd of 5 in the round-trip path. Full report at
docs/benchmarks/2026-05-10-comparison.md, raw output committed under
test/benchmarks/results/.

Caveats called out in the report:
  - codec asymmetry (we ship goccy/go-json; competitors mostly stdlib)
  - echotron call bench skipped — built-in rate limiter not externally
    configurable; would measure throttling, not the library
  - dispatch bench limited to libs with a public sync entry point
    (ours, telebot, gobot); gotba has no dispatcher, telego/echotron
    use channel/per-chat paradigms not directly comparable

Also gitignores docs/superpowers/ (local brainstorm/spec scratch)
and regenerates docs/reference/dispatch.md after the new
Router.Process method.
2026-05-10 21:52:00 +01:00
lukaszraczylo c9a062ea04 feat(dispatch): expose Router.Process for non-Updater entry points
Adds a public synchronous entry point that runs a single update through
the global middleware chain and the dispatch table. Useful for callers
that source updates outside the standard transport.Updater flow:
custom webhook frameworks, message-bus consumers, and cross-library
benchmarks driving the router without spinning up Run.

Honours global middleware (Use); bypasses Run's concurrency semaphore
since the caller controls parallelism.
2026-05-10 21:51:13 +01:00
lukaszraczylo 7eb3398396 docs(reference): regenerate after perf series
Picks up new symbols from the perf commits: Context typed fields and Set method, header/buf pool vars in client and transport.
2026-05-10 02:57:25 +01:00
lukaszraczylo 26b98a5372 perf(transport): pool *bytes.Buffer + MaxBytesReader for webhook decode
Replace the hand-rolled make([]byte, 0, 1024) + make([]byte, 4096) read loop in WebhookServer.ServeHTTP with a sync.Pool-backed bytes.Buffer drained via ReadFrom, fronted by http.MaxBytesReader for the 1 MiB body cap.

putWebhookBuf caps Cap() at 256 KiB before returning to the pool so a rare oversized update (max body is 1 MiB) doesn't permanently bloat the pool.

Bench delta on Webhook_ServeHTTP: 2564ns -> 2020ns (-21%), 12707B -> 7648B (-40%), 24 -> 23 allocs. The big byte saving is the 4 KiB tmp buffer + 1 KiB initial buf cap, replaced by one reused buffer across requests. The remaining alloc count is dominated by codec.Unmarshal decoding Update's pointer fields (*string, *int64), which is downstream of this change.
2026-05-10 02:47:58 +01:00
lukaszraczylo a416bda5f3 perf(client): pool *bytes.Buffer for response body reads
Replace io.ReadAll(resp.Body) on the typed Call/callMultipart paths with a sync.Pool-backed bytes.Buffer + ReadFrom. Saves the 512B initial allocation that ReadAll grows from on every successful call.

The pool only covers paths whose decoder copies strings out of the input (decodeResult delegates to goccy/go-json, which copies). CallRaw and callMultipartRaw return slices that alias the buffer storage, so they keep the io.ReadAll path; pooling there would need a defensive copy that defeats the saving.

putRespBuf caps Cap() at 64 KiB before returning to the pool so a single oversized response (e.g. large getFile metadata) doesn't bloat the pool for the rest of the process.

Bench delta on Call_BoolResponse: 14 allocs -> 13 allocs, 1842B -> 1331B, 526ns -> 479ns. Same shape on Call_StructResponse (16 -> 15, 1973B -> 1462B).
2026-05-10 02:45:14 +01:00
lukaszraczylo 0ee539e991 perf(dispatch): typed Context.Command/CommandArgs/RegexMatch fields
Move the three conventional Values keys ("command", "command_args", "regex_match") to typed fields on Context. Router and group routing write the fields directly; the Values map is allocated lazily via the new Set method and reserved for user-defined custom keys.

Allocation impact (M4 Max, b.Loop()):

  DispatchCommand:   5 allocs/op -> 1, 153ns -> 69ns (-55%)

  DispatchTextRegex: 5 allocs/op -> 2, 181ns -> 107ns (-41%)

  DispatchFilter:    2 allocs/op -> 1, 32ns -> 19ns (-41%)

  NewContext:        5.79ns -> 1.60ns

Trade-off: Context struct grew from ~48B to ~96B (three new fields), so filter-only paths pay ~50B more per dispatch. Command/regex paths save ~320B + 4 allocs each, which dominates for typical bot workloads.

Handlers reading c.Values["command"], c.Values["command_args"], or c.Values["regex_match"] now get nil; the typed fields c.Command, c.CommandArgs, c.RegexMatch are the new accessors. Custom keys still work via c.Set(k, v) and c.Values[k].
2026-05-10 02:35:24 +01:00
lukaszraczylo da27421521 perf(client): static headers + bool fast-path in decodeResult
Two changes on the Call hot path:

* Replace httpReq.Header.Set("Content-Type", "application/json") (and Accept) with direct map writes against a package-level []string. Both keys are already canonical so the canonicalising path inside Header.Set was pure overhead; saves the per-call []string{val} allocation x2.

* Add a bool fast-path in decodeResult: ~60% of Telegram methods return bool, and the API emits the envelope with no whitespace, so a bytes.Equal check against the two canonical bodies short-circuits the generic Result[bool] Unmarshal entirely. any(true).(Resp) does not box thanks to Go's static bool interface values.

Combined effect on Call_BoolResponse: 18 -> 14 allocs/op, 634ns -> 526ns. DecodeResult_Bool isolation bench: 50ns / 2 allocs -> 2.87ns / 0 allocs.
2026-05-10 02:32:00 +01:00
lukaszraczylo 728b28b0c5 test(bench): add allocation benchmarks for client/transport/dispatch hot paths
Hermetic benchmarks (no network) covering Call encode+decode, webhook ServeHTTP body parse, and Router dispatch (command/regex/filter). Use Go 1.24+ b.Loop() idiom. .benchstats/baseline.txt pins the pre-optimisation numbers for benchstat comparisons.
2026-05-10 02:31:46 +01:00
lukaszraczylo 9cfe193e2e docs(reference): regenerate after typed-allowed-updates merge
PR #1 changed AllowedUpdates type and shifted longpoll.go line numbers but left docs/reference/ stale, breaking the codegen-clean CI gate.
2026-05-09 21:19:09 +01:00
lukaszraczylo 79c0617867 Merge pull request #1 from lukaszraczylo/feat/typed-allowed-updates
feat(api): type AllowedUpdates as []UpdateType for compile-time typo …
2026-05-09 20:53:45 +01:00
lukaszraczylo 60eb0a89b5 refactor(api): typed enums for emoji-list fields (DiceEmoji, ReactionEmoji)
Two API fields carry restricted emoji-value sets that the scraper's
curly-quote regex strips during IR extraction (multi-byte boundary
issue): ReactionTypeEmoji.Emoji and sendDice.Emoji. They previously
typed as plain string with no compile-time guarantee on values.

Add hand-curated typed-string enums in api/enums.go (the manual file,
not enums.gen.go):

  - DiceEmoji: 6 constants (Dice, Dart, Basketball, Football, Bowling,
    SlotMachine) covering Telegram's full set for sendDice.
  - ReactionEmoji: 73 constants covering the canonical reaction set
    from https://core.telegram.org/bots/api#reactiontypeemoji. Names
    follow Unicode CLDR short names where one exists, otherwise stable
    common-English labels (e.g. ThumbsUp, Heart, Clown, ManTechnologist).

Wire the field-type override via cmd/genapi/emitter.go:

  - fieldTypeOverrides map keyed "<TypeOrParamsName>.<FieldName>".
  - goField/multipartFieldEntry consult the override after the enum-plan
    lookup; falls through to the default goType when nothing matches.
  - methods.tmpl gains goFieldP/multipartFieldEntryP helpers that pass
    the params type name as override-parent (the params struct doesn't
    share a Go type with the field, so the existing parent="" enum-key
    convention is preserved).

Regenerated api/types.gen.go and api/methods.gen.go now type the two
fields as ReactionEmoji and DiceEmoji respectively. No other Emoji
field is affected (override is scoped per parent type). regen-from-
fixture is byte-deterministic across runs.

Add api/emoji_enums_test.go covering const wire values, reflection
checks on field types, and a marshal/unmarshal round-trip for
ReactionTypeEmoji.
2026-05-09 20:47:16 +01:00
lukaszraczylo fecef22f48 refactor(scrape): detect prose-style "must be X" discriminator values on variants
Sealed-interface union variants whose Type/Source field is declared as
bare prose (e.g. "Type of the result, must be article" or "Scope type,
must be all_private_chats") were skipped by extractEnumValues because
the existing patterns require curly-quoted values. The genapi emitter
already extracted these values via discBareRE for marshal-side
discriminator injection; lifting the same detection into the scraper
populates Field.EnumValues so planUnifiedUnionEnums folds them into
shared union-level enums automatically.

Unions newly unified (10): BotCommandScope, MenuButton, InputMedia,
InputPaidMedia, InputPollMedia, InputPollOptionMedia, InputProfilePhoto,
InputStoryContent, InlineQueryResult, PassportElementError.

InputMessageContent stays excluded — its variants dispatch
structurally on field presence and have no Type/Source field, so
planUnifiedUnionEnums correctly skips it.

Constants added: 60 typed enum constants across the 10 unions; the
corresponding variant struct fields are retyped from string to the
shared enum.

Internal call-site cleanups: 0 — no internal package referenced these
discriminator values via magic strings.

False positives the prose detector explicitly rejects: terminal
prose-word continuations like "must be sent", "must be shown above",
"must be specified", "must be paid", "must be active", "must be one
of 3, 6, or 12", "must be between 5 and 100000", "must be a Pay
button", "must be repainted". Guarded via terminal-position regex
anchor + closed-list isProseWord filter.

Determinism verified across two consecutive make regen-from-fixture
runs. go test -race ./..., go vet ./..., staticcheck ./... all clean.
2026-05-09 20:37:07 +01:00
lukaszraczylo 5523ed2b06 refactor(api): unify per-variant single-value enums into shared union-level enums
Each sealed-interface union with N variants used to emit N typed-string
enums, one per variant, each holding exactly one wire value. Codegen now
detects this pattern and emits ONE unified enum at the union level,
retyping every variant's discriminator field to point at it.

Unified enums (11 unions, 44 constants total):
  - ChatMemberStatus           (6)
  - MessageOriginType          (4)
  - BackgroundFillType         (3)
  - BackgroundTypeKind         (4)
  - ChatBoostSourceKind        (3)
  - OwnedGiftType              (2)
  - PaidMediaType              (4)
  - ReactionTypeKind           (3)
  - RevenueWithdrawalStateKind (3)
  - StoryAreaTypeKind          (5)
  - TransactionPartnerType     (7)

Naming-collision cases (union name already ends in a discriminator
concept noun, so the natural concat would stutter): BackgroundType,
ReactionType, StoryAreaType, ChatBoostSource, RevenueWithdrawalState.
The unified name ends with 'Kind' instead.

44 obsolete per-variant single-value enum types removed (e.g.
ChatMemberOwnerStatus, MessageOriginUserType). The variant struct types
themselves (ChatMemberOwner etc.) are unchanged; only their per-variant
single-value enum aliases go away.

Auto-inject MarshalJSON (commit 370c9c0) is unaffected — variant wire
values still come from the discriminator-extractor pass.

Call-site cleanup:
  - dispatch/filters/chatmember/chatmember_test.go
  - api/marshaljson_variants_test.go

New test: api/unifiedenum_test.go covers ChatMemberStatus and
MessageOriginType: variant-field retype, direct comparison without
conversion, marshal discriminator preservation, full round-trip,
stutter-suffix Kind sanity check.
2026-05-09 20:26:08 +01:00
lukaszraczylo 62c76e7e4e feat(api): type AllowedUpdates as []UpdateType for compile-time typo safety
Both SetWebhookParams.AllowedUpdates and GetUpdatesParams.AllowedUpdates
(plus the WebhookInfo.AllowedUpdates field on the response side) were
typed []string, forcing callers to cast every typed constant:

  AllowedUpdates: []string{
      string(api.UpdateMessage),
      string(api.UpdateMyChatMember),
      ...
  }

Switch to []UpdateType so the same call site is typo-safe end-to-end:

  AllowedUpdates: []UpdateType{
      api.UpdateMessage,
      api.UpdateMyChatMember,
      ...
  }

Wire format is unchanged — UpdateType is type UpdateType string, marshals
identically as JSON strings. The MultipartFields()/runtime.go encoding
paths likewise continue to work via json.Marshal on the typed slice.

Implementation note: api/methods.gen.go and api/types.gen.go are
generated by cmd/genapi from internal/spec/api.json, where the field is
described as Array of String. The Telegram docs do not enumerate the
allowed_updates values inline, so the scraper cannot synthesise the
enum and UpdateType lives hand-curated in api/enums.go (see existing
doc comment there). The retype is therefore done as a small pinned
override inside cmd/genapi/emitter.go's goField — keyed on the wire
field name allowed_updates with elem type string — so the change
survives a future regeneration of the .gen.go files. transport/longpoll
drops the now-unnecessary []string conversion.

Backward incompatibility: callers passing untyped []string variables
will need to convert; callers using untyped string literals inside the
slice ARE fine because Go's untyped-literal rule auto-converts.
2026-05-09 20:20:30 +01:00
lukaszraczylo 6f9b29ea0c fix(release): stop tagging bot-api version
Debug log from run 25609309601 confirmed semver-generator picks the
chronologically most-recent tag as the version base. Whenever the
bot-api-vX.Y tag was created after the library tag (or recreated
during a partially-failing release), semver-generator picked
bot-api-v10.0 as the base, couldn't parse 'v10.0' as full SemVer
(no patch number), and restarted numbering from v0.0.x.

Per direct user instruction: drop the bot-api tag entirely. The Bot
API version still appears in the library tag message and release
notes. Also delete the existing bot-api-v10.0 tag from local + remote
so the next release sees only v* tags.
2026-05-09 20:06:02 +01:00
lukaszraczylo bd80af240d chore(release): add tag-fetch step + debugmode to diagnose semver
Add an explicit git fetch --tags --force origin before semver-generator
runs, plus log visible tags. Enable semver-generator's debugmode so the
failure mode is visible. Diagnostic only — once we understand why
semver-generator computes v0.0.2 despite v0.1.2 being the latest tag,
the debug step can be removed.
2026-05-09 20:02:10 +01:00
lukaszraczylo af180b75c5 fix(release): tag bot-api marker before library version
semver-generator picks the chronologically most-recent tag as the
version base. The previous order tagged the lib version first, then
bot-api-vX.Y, leaving the bot-api tag as the most recent. semver-
generator then treated 'vX.Y' as the base, couldn't parse it as full
SemVer (no patch number), and silently restarted numbering from
v0.0.x — causing surprise version downgrades like v1.1.1 -> v0.0.4
and v0.1.1 -> v0.0.2.

Tagging bot-api-vX.Y first and the library version last keeps the
library tag as the chronologically last one, so subsequent runs see
it as the version base and bump correctly.
2026-05-09 19:58:14 +01:00
lukaszraczylo 29bf575cfd chore(release): bump to v0.1.2
Empty commit to advance the auto-release pipeline. Latest tag was
v0.1.1; semver-generator with the patch-prefix in the subject computes
the next version as v0.1.2.
2026-05-09 19:51:34 +01:00
61 changed files with 5285 additions and 1303 deletions
+17
View File
@@ -0,0 +1,17 @@
After: static-header-slices + bool-fast-path
goos: darwin
goarch: arm64
pkg: github.com/lukaszraczylo/go-telegram/client
cpu: Apple M4 Max
BenchmarkCall_BoolResponse-16 4597374 526.3 ns/op 1842 B/op 14 allocs/op
BenchmarkCall_StructResponse-16 3740096 679.6 ns/op 1973 B/op 16 allocs/op
BenchmarkEncodeJSONBody-16 41997352 58.35 ns/op 96 B/op 2 allocs/op
BenchmarkDecodeResult_Bool-16 863273641 2.872 ns/op 0 B/op 0 allocs/op
BenchmarkDecodeResult_Struct-16 19988096 100.3 ns/op 144 B/op 2 allocs/op
Deltas vs baseline (allocs/op):
Call_BoolResponse: 18 -> 14 (-4)
Call_StructResponse: 18 -> 16 (-2)
DecodeResult_Bool: 2 -> 0 (-2, also -94% ns)
DecodeResult_Struct: 2 -> 2 (flat)
EncodeJSONBody: 2 -> 2 (flat)
+27
View File
@@ -0,0 +1,27 @@
After: static-headers + bool-fast-path + lazy-Context.Values
goos: darwin
goarch: arm64
cpu: Apple M4 Max
pkg: github.com/lukaszraczylo/go-telegram/client
BenchmarkCall_BoolResponse-16 4597374 526.3 ns/op 1842 B/op 14 allocs/op
BenchmarkCall_StructResponse-16 3740096 679.6 ns/op 1973 B/op 16 allocs/op
BenchmarkEncodeJSONBody-16 41997352 58.35 ns/op 96 B/op 2 allocs/op
BenchmarkDecodeResult_Bool-16 863273641 2.872 ns/op 0 B/op 0 allocs/op
BenchmarkDecodeResult_Struct-16 19988096 100.3 ns/op 144 B/op 2 allocs/op
pkg: github.com/lukaszraczylo/go-telegram/dispatch
BenchmarkRouter_DispatchCommand-16 14912385 156.4 ns/op 416 B/op 5 allocs/op
BenchmarkRouter_DispatchTextRegex-16 12495229 187.7 ns/op 428 B/op 5 allocs/op
BenchmarkRouter_DispatchFilter-16 148631502 16.11 ns/op 48 B/op 1 allocs/op
BenchmarkRouter_NewContext-16 1000000000 1.608 ns/op 0 B/op 0 allocs/op
BenchmarkExtractCommand-16 25267845 92.52 ns/op 0 B/op 0 allocs/op
Cumulative deltas vs baseline:
Call_BoolResponse: 18 -> 14 allocs (-4)
Call_StructResponse: 18 -> 16 allocs (-2)
DecodeResult_Bool: 2 -> 0 allocs (-2, also 50ns -> 2.87ns)
DispatchFilter: 2 -> 1 alloc (-1, also 32ns -> 16ns)
NewContext: 5.79ns -> 1.61ns (-72%)
DispatchCommand: 5 -> 5 allocs (flat — map alloc shifted from NewContext to first Set)
DispatchTextRegex: 5 -> 5 allocs (flat — same reason)
+27
View File
@@ -0,0 +1,27 @@
After: static-headers + bool-fast-path + lazy-Values + typed-fields
goos: darwin
goarch: arm64
cpu: Apple M4 Max
pkg: github.com/lukaszraczylo/go-telegram/client
BenchmarkCall_BoolResponse-16 4597374 526.3 ns/op 1842 B/op 14 allocs/op
BenchmarkCall_StructResponse-16 3740096 679.6 ns/op 1973 B/op 16 allocs/op
BenchmarkEncodeJSONBody-16 41997352 58.35 ns/op 96 B/op 2 allocs/op
BenchmarkDecodeResult_Bool-16 863273641 2.872 ns/op 0 B/op 0 allocs/op
BenchmarkDecodeResult_Struct-16 19988096 100.3 ns/op 144 B/op 2 allocs/op
pkg: github.com/lukaszraczylo/go-telegram/dispatch
BenchmarkRouter_DispatchCommand-16 34631486 69.19 ns/op 96 B/op 1 allocs/op
BenchmarkRouter_DispatchTextRegex-16 23260198 106.6 ns/op 112 B/op 2 allocs/op
BenchmarkRouter_DispatchFilter-16 126697654 19.03 ns/op 96 B/op 1 allocs/op
BenchmarkRouter_NewContext-16 1000000000 1.600 ns/op 0 B/op 0 allocs/op
BenchmarkExtractCommand-16 27345622 87.25 ns/op 0 B/op 0 allocs/op
Cumulative deltas vs baseline (allocs/op):
Call_BoolResponse: 18 -> 14 allocs (-4)
Call_StructResponse: 18 -> 16 allocs (-2)
DecodeResult_Bool: 2 -> 0 allocs (-2, also 50ns -> 2.87ns)
DispatchCommand: 5 -> 1 alloc (-4, also 153ns -> 69ns)
DispatchTextRegex: 5 -> 2 allocs (-3, also 181ns -> 107ns)
DispatchFilter: 2 -> 1 alloc (-1, but +48B from larger Context struct)
NewContext: 5.79ns -> 1.60ns (-72%)
+26
View File
@@ -0,0 +1,26 @@
After: static-headers + bool-fast-path + lazy-Values + typed-fields + resp-buffer-pool
goos: darwin
goarch: arm64
cpu: Apple M4 Max
pkg: github.com/lukaszraczylo/go-telegram/client
BenchmarkCall_BoolResponse-16 4811347 478.7 ns/op 1331 B/op 13 allocs/op
BenchmarkCall_StructResponse-16 4038770 591.6 ns/op 1462 B/op 15 allocs/op
BenchmarkEncodeJSONBody-16 47025052 51.30 ns/op 96 B/op 2 allocs/op
BenchmarkDecodeResult_Bool-16 853161562 2.824 ns/op 0 B/op 0 allocs/op
BenchmarkDecodeResult_Struct-16 26811634 88.80 ns/op 144 B/op 2 allocs/op
pkg: github.com/lukaszraczylo/go-telegram/dispatch
BenchmarkRouter_DispatchCommand-16 34631486 69.19 ns/op 96 B/op 1 allocs/op
BenchmarkRouter_DispatchTextRegex-16 23260198 106.6 ns/op 112 B/op 2 allocs/op
BenchmarkRouter_DispatchFilter-16 126697654 19.03 ns/op 96 B/op 1 allocs/op
BenchmarkRouter_NewContext-16 1000000000 1.600 ns/op 0 B/op 0 allocs/op
BenchmarkExtractCommand-16 27345622 87.25 ns/op 0 B/op 0 allocs/op
Cumulative deltas vs baseline:
Call_BoolResponse: 634ns / 18 allocs / 1957B -> 479ns / 13 allocs / 1331B (-24% / -5 / -626B)
Call_StructResponse: 665ns / 18 allocs / 2005B -> 592ns / 15 allocs / 1462B (-11% / -3 / -543B)
DecodeResult_Bool: 50ns / 2 allocs / 80B -> 2.8ns / 0 allocs / 0B
DispatchCommand: 153ns / 5 allocs / 416B -> 69ns / 1 alloc / 96B (-55% / -4 / -320B)
DispatchTextRegex: 181ns / 5 allocs / 428B -> 107ns / 2 allocs / 112B (-41% / -3 / -316B)
DispatchFilter: 32ns / 2 allocs / 96B -> 19ns / 1 alloc / 96B (-41% / -1)
+30
View File
@@ -0,0 +1,30 @@
After: static-headers + bool-fast-path + lazy-Values + typed-fields + resp-buffer-pool + webhook-pool
goos: darwin
goarch: arm64
cpu: Apple M4 Max
pkg: github.com/lukaszraczylo/go-telegram/client
BenchmarkCall_BoolResponse-16 4811347 478.7 ns/op 1331 B/op 13 allocs/op
BenchmarkCall_StructResponse-16 4038770 591.6 ns/op 1462 B/op 15 allocs/op
BenchmarkEncodeJSONBody-16 47025052 51.30 ns/op 96 B/op 2 allocs/op
BenchmarkDecodeResult_Bool-16 853161562 2.824 ns/op 0 B/op 0 allocs/op
BenchmarkDecodeResult_Struct-16 26811634 88.80 ns/op 144 B/op 2 allocs/op
pkg: github.com/lukaszraczylo/go-telegram/transport
BenchmarkWebhook_ServeHTTP-16 1204390 2020 ns/op 7648 B/op 23 allocs/op
pkg: github.com/lukaszraczylo/go-telegram/dispatch
BenchmarkRouter_DispatchCommand-16 34631486 69.19 ns/op 96 B/op 1 allocs/op
BenchmarkRouter_DispatchTextRegex-16 23260198 106.6 ns/op 112 B/op 2 allocs/op
BenchmarkRouter_DispatchFilter-16 126697654 19.03 ns/op 96 B/op 1 allocs/op
BenchmarkRouter_NewContext-16 1000000000 1.600 ns/op 0 B/op 0 allocs/op
BenchmarkExtractCommand-16 27345622 87.25 ns/op 0 B/op 0 allocs/op
Cumulative deltas vs baseline:
Call_BoolResponse: 634ns / 18 allocs / 1957B -> 479ns / 13 / 1331B (-24% / -5 / -626B)
Call_StructResponse: 665ns / 18 allocs / 2005B -> 592ns / 15 / 1462B (-11% / -3 / -543B)
DecodeResult_Bool: 50ns / 2 allocs / 80B -> 2.8ns / 0 / 0B
Webhook_ServeHTTP: 2564ns / 24 allocs / 12707B -> 2020ns / 23 / 7648B (-21% / -1 / -5059B)
DispatchCommand: 153ns / 5 allocs / 416B -> 69ns / 1 / 96B (-55% / -4 / -320B)
DispatchTextRegex: 181ns / 5 allocs / 428B -> 107ns / 2 / 112B (-41% / -3 / -316B)
DispatchFilter: 32ns / 2 allocs / 96B -> 19ns / 1 / 96B (-41% / -1)
+19
View File
@@ -0,0 +1,19 @@
goos: darwin
goarch: arm64
pkg: github.com/lukaszraczylo/go-telegram/client
cpu: Apple M4 Max
BenchmarkCall_BoolResponse-16 1875306 633.9 ns/op 1957 B/op 18 allocs/op
BenchmarkCall_StructResponse-16 1805024 665.2 ns/op 2005 B/op 18 allocs/op
BenchmarkEncodeJSONBody-16 23345811 51.55 ns/op 96 B/op 2 allocs/op
BenchmarkDecodeResult_Bool-16 23832240 50.37 ns/op 80 B/op 2 allocs/op
BenchmarkDecodeResult_Struct-16 13511192 92.64 ns/op 144 B/op 2 allocs/op
pkg: github.com/lukaszraczylo/go-telegram/transport
BenchmarkWebhook_ServeHTTP-16 465798 2564 ns/op 12707 B/op 24 allocs/op
pkg: github.com/lukaszraczylo/go-telegram/dispatch
BenchmarkRouter_DispatchCommand-16 7303522 152.7 ns/op 416 B/op 5 allocs/op
BenchmarkRouter_DispatchTextRegex-16 6740305 180.5 ns/op 428 B/op 5 allocs/op
BenchmarkRouter_DispatchFilter-16 39479149 32.18 ns/op 96 B/op 2 allocs/op
BenchmarkRouter_NewContext-16 208260764 5.790 ns/op 0 B/op 0 allocs/op
BenchmarkExtractCommand-16 12988816 92.69 ns/op 0 B/op 0 allocs/op
+23 -29
View File
@@ -25,8 +25,8 @@ jobs:
vet:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: '1.25.x'
check-latest: true
@@ -41,8 +41,8 @@ jobs:
staticcheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: '1.25.x'
check-latest: true
@@ -58,8 +58,8 @@ jobs:
govulncheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: '1.25.x'
check-latest: true
@@ -75,8 +75,8 @@ jobs:
gosec:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: '1.25.x'
check-latest: true
@@ -98,8 +98,8 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: '1.25.x'
check-latest: true
@@ -120,8 +120,8 @@ jobs:
codegen-clean:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: '1.25.x'
check-latest: true
@@ -139,10 +139,10 @@ jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0 # need history for drift comparison
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version: '1.25.x'
check-latest: true
@@ -196,11 +196,11 @@ jobs:
github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version: '1.25.x'
check-latest: true
@@ -245,23 +245,17 @@ jobs:
API_TAG: ${{ steps.api_version.outputs.tag }}
API_VER: ${{ steps.api_version.outputs.version }}
run: |
# Bot API version (currently $API_VER) is intentionally NOT
# tagged separately. semver-generator picks the most recent
# tag as the version base; a non-SemVer marker like
# bot-api-vX.Y poisons that and restarts numbering from
# v0.0.x. Bot API version stays as a comment in the lib tag
# message and in the release notes.
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -a "$LIB_TAG" -m "Release $LIB_TAG"
git tag -a "$LIB_TAG" -m "Release $LIB_TAG (Bot API $API_VER)"
git push origin "$LIB_TAG"
if [ -n "$API_TAG" ]; then
# Force-update the bot-api tag so it always points at the latest
# release that supports that API version.
if git rev-parse "$API_TAG" >/dev/null 2>&1; then
git tag -f -a "$API_TAG" -m "go-telegram release $LIB_TAG (Bot API $API_VER)"
git push -f origin "$API_TAG"
else
git tag -a "$API_TAG" -m "go-telegram release $LIB_TAG (Bot API $API_VER)"
git push origin "$API_TAG"
fi
fi
- name: Run GoReleaser
if: github.event_name != 'workflow_dispatch' || inputs.dry-run-release == false
uses: goreleaser/goreleaser-action@v6
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: actions/configure-pages@v5
- uses: actions/upload-pages-artifact@v3
with:
+3 -3
View File
@@ -13,12 +13,12 @@ jobs:
regen:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0 # full history so audit -drift can compare against main
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: '1.25.x'
check-latest: true
@@ -89,7 +89,7 @@ jobs:
- name: Open PR
if: steps.diff.outputs.no_changes != 'true'
uses: peter-evans/create-pull-request@v7
uses: peter-evans/create-pull-request@v8
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: |
+3
View File
@@ -24,6 +24,9 @@ coverage.html
.env
.env.local
# Superpowers brainstorm/spec scratch — never commit
docs/superpowers/
# Stray binaries at repo root from `go build ./cmd/...` or `go run` artefacts.
# Listed explicitly (not via /* glob) so source dirs are never accidentally ignored.
/echo
+21
View File
@@ -317,6 +317,27 @@ r.OnCommand("/cmd", named.Handler())
</details>
## Benchmarks
Apples-to-apples micro-benchmarks against the five most-starred Go Telegram libraries (`go-telegram-bot-api`, `telebot.v3`, `go-telegram/bot`, `telego`, `echotron`) live under [`test/benchmarks/`](test/benchmarks/) as a separate Go module.
<details>
<summary>Results — Apple M4 Max · darwin/arm64 · go1.26.2</summary>
| Path | Fastest | Our position |
|------|---------|--------------|
| Webhook decode (small Update) | **ours** — 1.83 µs / 11 allocs | 1st of 6 |
| Large Update unmarshal (unions + reply markup) | **ours** — 6.73 µs / 34 allocs | 1st of 6 |
| `sendMessage` round-trip — `net/http` default | telego — 35.8 µs / 48 allocs | 2nd of 5 (102 allocs) |
| `sendMessage` round-trip — opt-in `fasthttp` | telego — 48 allocs | within 8 of telego (56 allocs) |
| Dispatcher routing (20 handlers, last matches) | **ours** — 98 ns / 3 allocs | 1st of 3 |
Opt into fasthttp for high-throughput bots: `client.WithHTTPClient(client.NewFastHTTPDoer())`. Trade-off: HTTP/1.1 only, no `RoundTripper` middleware composition.
Full tables, caveats, and reproduction steps: **[`docs/benchmarks/2026-05-10-comparison.md`](docs/benchmarks/2026-05-10-comparison.md)**.
</details>
## Keeping up with Telegram
When Telegram ships a new Bot API version, regenerating the whole library is one command:
+239
View File
@@ -0,0 +1,239 @@
package api
import (
"reflect"
"testing"
json "github.com/goccy/go-json"
"github.com/stretchr/testify/require"
)
// TestUnifiedEnum_BotCommandScopeType_Constants confirms the prose-form
// discriminator detection promoted BotCommandScope's per-variant Type
// fields into one shared enum.
func TestUnifiedEnum_BotCommandScopeType_Constants(t *testing.T) {
require.IsType(t, BotCommandScopeType(""), BotCommandScopeTypeDefault)
got := []BotCommandScopeType{
BotCommandScopeTypeDefault,
BotCommandScopeTypeAllPrivateChats,
BotCommandScopeTypeAllGroupChats,
BotCommandScopeTypeAllChatAdministrators,
BotCommandScopeTypeChat,
BotCommandScopeTypeChatAdministrators,
BotCommandScopeTypeChatMember,
}
want := []string{
"default", "all_private_chats", "all_group_chats",
"all_chat_administrators", "chat", "chat_administrators", "chat_member",
}
require.Len(t, got, len(want))
for i, v := range got {
require.Equal(t, want[i], string(v))
}
}
// TestUnifiedEnum_InlineQueryResultType_VariantFields walks the variants
// and asserts each one's Type field is the unified enum.
func TestUnifiedEnum_InlineQueryResultType_VariantFields(t *testing.T) {
require.IsType(t, InlineQueryResultType(""), InlineQueryResultTypeArticle)
wantType := reflect.TypeOf(InlineQueryResultType(""))
cases := []any{
&InlineQueryResultArticle{},
&InlineQueryResultPhoto{},
&InlineQueryResultGif{},
&InlineQueryResultMpeg4Gif{},
&InlineQueryResultVideo{},
&InlineQueryResultAudio{},
&InlineQueryResultVoice{},
&InlineQueryResultDocument{},
&InlineQueryResultLocation{},
&InlineQueryResultVenue{},
&InlineQueryResultContact{},
&InlineQueryResultGame{},
}
for _, c := range cases {
rt := reflect.TypeOf(c).Elem()
f, ok := rt.FieldByName("Type")
require.True(t, ok, "%s missing Type field", rt.Name())
require.Equal(t, wantType, f.Type, "%s.Type type mismatch", rt.Name())
}
}
// TestUnifiedEnum_PassportElementErrorSource_VariantFields asserts the
// retype landed on every variant of the PassportElementError union.
func TestUnifiedEnum_PassportElementErrorSource_VariantFields(t *testing.T) {
require.IsType(t, PassportElementErrorSource(""), PassportElementErrorSourceData)
wantType := reflect.TypeOf(PassportElementErrorSource(""))
cases := []any{
&PassportElementErrorDataField{},
&PassportElementErrorFrontSide{},
&PassportElementErrorReverseSide{},
&PassportElementErrorSelfie{},
&PassportElementErrorFile{},
&PassportElementErrorFiles{},
&PassportElementErrorTranslationFile{},
&PassportElementErrorTranslationFiles{},
&PassportElementErrorUnspecified{},
}
for _, c := range cases {
rt := reflect.TypeOf(c).Elem()
f, ok := rt.FieldByName("Source")
require.True(t, ok, "%s missing Source field", rt.Name())
require.Equal(t, wantType, f.Type, "%s.Source type mismatch", rt.Name())
}
}
// TestUnifiedEnum_InputMediaType_Constants covers a media-shaped union
// where the discriminator value is the wire identifier "animation",
// "photo", etc.
func TestUnifiedEnum_InputMediaType_Constants(t *testing.T) {
require.IsType(t, InputMediaType(""), InputMediaTypePhoto)
wantType := reflect.TypeOf(InputMediaType(""))
for _, c := range []any{
&InputMediaAnimation{},
&InputMediaAudio{},
&InputMediaDocument{},
&InputMediaPhoto{},
&InputMediaVideo{},
} {
rt := reflect.TypeOf(c).Elem()
f, ok := rt.FieldByName("Type")
require.True(t, ok, "%s missing Type field", rt.Name())
require.Equal(t, wantType, f.Type, "%s.Type type mismatch", rt.Name())
}
}
// TestUnifiedEnum_MenuButtonType_Constants covers the third single-Type
// union pulled in by the prose detector.
func TestUnifiedEnum_MenuButtonType_Constants(t *testing.T) {
require.IsType(t, MenuButtonType(""), MenuButtonTypeCommands)
wantType := reflect.TypeOf(MenuButtonType(""))
for _, c := range []any{
&MenuButtonCommands{},
&MenuButtonWebApp{},
&MenuButtonDefault{},
} {
rt := reflect.TypeOf(c).Elem()
f, ok := rt.FieldByName("Type")
require.True(t, ok, "%s missing Type field", rt.Name())
require.Equal(t, wantType, f.Type, "%s.Type type mismatch", rt.Name())
}
}
// TestUnifiedEnum_InlineQueryResultArticle_RoundTrip confirms the
// auto-injected discriminator survives a marshal-unmarshal cycle on the
// concrete variant and lands as the typed enum constant. There's no
// generated UnmarshalInlineQueryResult — the union has no entry in
// knownDiscriminators — so the round-trip targets the variant directly.
func TestUnifiedEnum_InlineQueryResultArticle_RoundTrip(t *testing.T) {
orig := &InlineQueryResultArticle{
ID: "x1",
Title: "test",
}
raw, err := json.Marshal(orig)
require.NoError(t, err)
var probe struct {
Type string `json:"type"`
}
require.NoError(t, json.Unmarshal(raw, &probe))
require.Equal(t, "article", probe.Type)
// Strip InputMessageContent before re-decoding: it's a sealed
// interface and the variant has no UnmarshalJSON helper to dispatch
// it. The discriminator round-trip is the property under test, not
// nested-union deserialisation.
var round struct {
Type InlineQueryResultType `json:"type"`
ID string `json:"id"`
Title string `json:"title"`
}
require.NoError(t, json.Unmarshal(raw, &round))
require.Equal(t, InlineQueryResultTypeArticle, round.Type)
require.Equal(t, orig.ID, round.ID)
require.Equal(t, orig.Title, round.Title)
}
// TestUnifiedEnum_PassportElementErrorDataField_RoundTrip mirrors the
// above for the Source-discriminated union.
func TestUnifiedEnum_PassportElementErrorDataField_RoundTrip(t *testing.T) {
orig := &PassportElementErrorDataField{
Type: "personal_details",
FieldName: "first_name",
DataHash: "abc",
Message: "boom",
}
raw, err := json.Marshal(orig)
require.NoError(t, err)
var probe struct {
Source string `json:"source"`
}
require.NoError(t, json.Unmarshal(raw, &probe))
require.Equal(t, "data", probe.Source)
var round PassportElementErrorDataField
require.NoError(t, json.Unmarshal(raw, &round))
require.Equal(t, PassportElementErrorSourceData, round.Source)
require.Equal(t, orig.FieldName, round.FieldName)
}
// TestUnifiedEnum_BotCommandScopeChat_RoundTrip covers a bot-command
// scope variant with a non-trivial extra field (ChatID).
func TestUnifiedEnum_BotCommandScopeChat_RoundTrip(t *testing.T) {
orig := &BotCommandScopeChat{ChatID: ChatIDFromInt(42)}
raw, err := json.Marshal(orig)
require.NoError(t, err)
var probe struct {
Type string `json:"type"`
}
require.NoError(t, json.Unmarshal(raw, &probe))
require.Equal(t, "chat", probe.Type)
var round BotCommandScopeChat
require.NoError(t, json.Unmarshal(raw, &round))
require.Equal(t, BotCommandScopeTypeChat, round.Type)
}
// TestUnifiedEnum_InputMessageContent_NoEnumEmitted confirms the IMC
// union — which dispatches structurally on field presence rather than a
// shared discriminator — does NOT get a unified enum, since none of its
// variants declare a single-value discriminator field.
func TestUnifiedEnum_InputMessageContent_NoEnumEmitted(t *testing.T) {
for _, name := range []string{
"InputTextMessageContent",
"InputLocationMessageContent",
"InputVenueMessageContent",
"InputContactMessageContent",
"InputInvoiceMessageContent",
} {
switch name {
case "InputTextMessageContent":
rt := reflect.TypeOf(&InputTextMessageContent{}).Elem()
_, ok := rt.FieldByName("Type")
require.False(t, ok, "%s unexpectedly grew a Type field", name)
case "InputLocationMessageContent":
rt := reflect.TypeOf(&InputLocationMessageContent{}).Elem()
_, ok := rt.FieldByName("Type")
require.False(t, ok, "%s unexpectedly grew a Type field", name)
case "InputVenueMessageContent":
rt := reflect.TypeOf(&InputVenueMessageContent{}).Elem()
_, ok := rt.FieldByName("Type")
require.False(t, ok, "%s unexpectedly grew a Type field", name)
case "InputContactMessageContent":
rt := reflect.TypeOf(&InputContactMessageContent{}).Elem()
_, ok := rt.FieldByName("Type")
require.False(t, ok, "%s unexpectedly grew a Type field", name)
case "InputInvoiceMessageContent":
rt := reflect.TypeOf(&InputInvoiceMessageContent{}).Elem()
_, ok := rt.FieldByName("Type")
require.False(t, ok, "%s unexpectedly grew a Type field", name)
}
}
}
+96
View File
@@ -0,0 +1,96 @@
package api
import (
"reflect"
"testing"
"github.com/goccy/go-json"
"github.com/stretchr/testify/require"
)
// TestDiceEmoji_Constants pins the canonical six dice-emoji values so a
// regen, refactor, or accidental rename can't silently break the wire
// contract.
func TestDiceEmoji_Constants(t *testing.T) {
cases := []struct {
name string
got DiceEmoji
want string
}{
{"Dice", DiceEmojiDice, "🎲"},
{"Dart", DiceEmojiDart, "🎯"},
{"Basketball", DiceEmojiBasketball, "🏀"},
{"Football", DiceEmojiFootball, "⚽"},
{"Bowling", DiceEmojiBowling, "🎳"},
{"SlotMachine", DiceEmojiSlotMachine, "🎰"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
require.Equal(t, c.want, string(c.got))
})
}
}
// TestSendDiceParams_EmojiFieldType asserts the codegen override wired
// SendDiceParams.Emoji to the typed enum (not plain string). Reflection
// catches a regression even if the file compiles via implicit string
// conversion of an untyped literal.
func TestSendDiceParams_EmojiFieldType(t *testing.T) {
rt := reflect.TypeOf(SendDiceParams{})
f, ok := rt.FieldByName("Emoji")
require.True(t, ok, "SendDiceParams.Emoji not present")
require.Equal(t, "DiceEmoji", f.Type.Name())
}
// TestSendDiceParams_MarshalJSON exercises the marshalled wire form to
// prove the typed enum still serialises as a JSON string holding the
// raw emoji bytes — i.e. the type override doesn't accidentally
// double-encode.
func TestSendDiceParams_MarshalJSON(t *testing.T) {
p := &SendDiceParams{
ChatID: ChatIDFromInt(1),
Emoji: DiceEmojiBasketball,
}
data, err := json.Marshal(p)
require.NoError(t, err)
require.Contains(t, string(data), `"emoji":"🏀"`)
}
// TestReactionEmoji_Constants spot-checks a representative slice of the
// 73-value enum. A full enumeration would be redundant — the test is
// here to lock the wire form, not to retest the const-block.
func TestReactionEmoji_Constants(t *testing.T) {
require.Equal(t, "👍", string(ReactionEmojiThumbsUp))
require.Equal(t, "👎", string(ReactionEmojiThumbsDown))
require.Equal(t, "❤", string(ReactionEmojiHeart))
require.Equal(t, "🔥", string(ReactionEmojiFire))
require.Equal(t, "💯", string(ReactionEmojiHundredPoints))
require.Equal(t, "🤡", string(ReactionEmojiClown))
}
// TestReactionTypeEmoji_FieldType asserts the codegen override wired
// ReactionTypeEmoji.Emoji to the typed enum.
func TestReactionTypeEmoji_FieldType(t *testing.T) {
rt := reflect.TypeOf(ReactionTypeEmoji{})
f, ok := rt.FieldByName("Emoji")
require.True(t, ok, "ReactionTypeEmoji.Emoji not present")
require.Equal(t, "ReactionEmoji", f.Type.Name())
}
// TestReactionTypeEmoji_RoundTrip proves a typed-enum value survives
// JSON marshal → unmarshal cycle without losing fidelity. The
// discriminator MarshalJSON on ReactionTypeEmoji forces type="emoji",
// so we set it explicitly here for symmetry with the unmarshal path.
func TestReactionTypeEmoji_RoundTrip(t *testing.T) {
in := &ReactionTypeEmoji{
Type: ReactionTypeKindEmoji,
Emoji: ReactionEmojiThumbsUp,
}
data, err := json.Marshal(in)
require.NoError(t, err)
require.Contains(t, string(data), `"emoji":"👍"`)
var out ReactionTypeEmoji
require.NoError(t, json.Unmarshal(data, &out))
require.Equal(t, ReactionEmojiThumbsUp, out.Emoji)
}
+161 -204
View File
@@ -4,100 +4,52 @@
package api
type BackgroundFillFreeformGradientType string
type BackgroundFillType string
const (
BackgroundFillFreeformGradientTypeFreeformGradient BackgroundFillFreeformGradientType = "freeform_gradient"
BackgroundFillTypeSolid BackgroundFillType = "solid"
BackgroundFillTypeGradient BackgroundFillType = "gradient"
BackgroundFillTypeFreeformGradient BackgroundFillType = "freeform_gradient"
)
type BackgroundFillGradientType string
type BackgroundTypeKind string
const (
BackgroundFillGradientTypeGradient BackgroundFillGradientType = "gradient"
BackgroundTypeKindFill BackgroundTypeKind = "fill"
BackgroundTypeKindWallpaper BackgroundTypeKind = "wallpaper"
BackgroundTypeKindPattern BackgroundTypeKind = "pattern"
BackgroundTypeKindChatTheme BackgroundTypeKind = "chat_theme"
)
type BackgroundFillSolidType string
type BotCommandScopeType string
const (
BackgroundFillSolidTypeSolid BackgroundFillSolidType = "solid"
BotCommandScopeTypeDefault BotCommandScopeType = "default"
BotCommandScopeTypeAllPrivateChats BotCommandScopeType = "all_private_chats"
BotCommandScopeTypeAllGroupChats BotCommandScopeType = "all_group_chats"
BotCommandScopeTypeAllChatAdministrators BotCommandScopeType = "all_chat_administrators"
BotCommandScopeTypeChat BotCommandScopeType = "chat"
BotCommandScopeTypeChatAdministrators BotCommandScopeType = "chat_administrators"
BotCommandScopeTypeChatMember BotCommandScopeType = "chat_member"
)
type BackgroundTypeChatThemeType string
type ChatBoostSourceKind string
const (
BackgroundTypeChatThemeTypeChatTheme BackgroundTypeChatThemeType = "chat_theme"
ChatBoostSourceKindPremium ChatBoostSourceKind = "premium"
ChatBoostSourceKindGiftCode ChatBoostSourceKind = "gift_code"
ChatBoostSourceKindGiveaway ChatBoostSourceKind = "giveaway"
)
type BackgroundTypeFillType string
type ChatMemberStatus string
const (
BackgroundTypeFillTypeFill BackgroundTypeFillType = "fill"
)
type BackgroundTypePatternType string
const (
BackgroundTypePatternTypePattern BackgroundTypePatternType = "pattern"
)
type BackgroundTypeWallpaperType string
const (
BackgroundTypeWallpaperTypeWallpaper BackgroundTypeWallpaperType = "wallpaper"
)
type ChatBoostSourceGiftCodeSource string
const (
ChatBoostSourceGiftCodeSourceGiftCode ChatBoostSourceGiftCodeSource = "gift_code"
)
type ChatBoostSourceGiveawaySource string
const (
ChatBoostSourceGiveawaySourceGiveaway ChatBoostSourceGiveawaySource = "giveaway"
)
type ChatBoostSourcePremiumSource string
const (
ChatBoostSourcePremiumSourcePremium ChatBoostSourcePremiumSource = "premium"
)
type ChatMemberAdministratorStatus string
const (
ChatMemberAdministratorStatusAdministrator ChatMemberAdministratorStatus = "administrator"
)
type ChatMemberBannedStatus string
const (
ChatMemberBannedStatusKicked ChatMemberBannedStatus = "kicked"
)
type ChatMemberLeftStatus string
const (
ChatMemberLeftStatusLeft ChatMemberLeftStatus = "left"
)
type ChatMemberMemberStatus string
const (
ChatMemberMemberStatusMember ChatMemberMemberStatus = "member"
)
type ChatMemberOwnerStatus string
const (
ChatMemberOwnerStatusCreator ChatMemberOwnerStatus = "creator"
)
type ChatMemberRestrictedStatus string
const (
ChatMemberRestrictedStatusRestricted ChatMemberRestrictedStatus = "restricted"
ChatMemberStatusCreator ChatMemberStatus = "creator"
ChatMemberStatusAdministrator ChatMemberStatus = "administrator"
ChatMemberStatusMember ChatMemberStatus = "member"
ChatMemberStatusRestricted ChatMemberStatus = "restricted"
ChatMemberStatusLeft ChatMemberStatus = "left"
ChatMemberStatusKicked ChatMemberStatus = "kicked"
)
type ChatType string
@@ -152,6 +104,75 @@ const (
InlineQueryResultGifThumbnailMimeTypeVideoOfMp4 InlineQueryResultGifThumbnailMimeType = "video/mp4"
)
type InlineQueryResultType string
const (
InlineQueryResultTypeAudio InlineQueryResultType = "audio"
InlineQueryResultTypeDocument InlineQueryResultType = "document"
InlineQueryResultTypeGif InlineQueryResultType = "gif"
InlineQueryResultTypeMpeg4Gif InlineQueryResultType = "mpeg4_gif"
InlineQueryResultTypePhoto InlineQueryResultType = "photo"
InlineQueryResultTypeSticker InlineQueryResultType = "sticker"
InlineQueryResultTypeVideo InlineQueryResultType = "video"
InlineQueryResultTypeVoice InlineQueryResultType = "voice"
InlineQueryResultTypeArticle InlineQueryResultType = "article"
InlineQueryResultTypeContact InlineQueryResultType = "contact"
InlineQueryResultTypeGame InlineQueryResultType = "game"
InlineQueryResultTypeLocation InlineQueryResultType = "location"
InlineQueryResultTypeVenue InlineQueryResultType = "venue"
)
type InputMediaType string
const (
InputMediaTypeAnimation InputMediaType = "animation"
InputMediaTypeAudio InputMediaType = "audio"
InputMediaTypeDocument InputMediaType = "document"
InputMediaTypeLivePhoto InputMediaType = "live_photo"
InputMediaTypePhoto InputMediaType = "photo"
InputMediaTypeVideo InputMediaType = "video"
)
type InputPaidMediaType string
const (
InputPaidMediaTypeLivePhoto InputPaidMediaType = "live_photo"
InputPaidMediaTypePhoto InputPaidMediaType = "photo"
InputPaidMediaTypeVideo InputPaidMediaType = "video"
)
type InputPollMediaType string
const (
InputPollMediaTypeAnimation InputPollMediaType = "animation"
InputPollMediaTypeAudio InputPollMediaType = "audio"
InputPollMediaTypeDocument InputPollMediaType = "document"
InputPollMediaTypeLivePhoto InputPollMediaType = "live_photo"
InputPollMediaTypeLocation InputPollMediaType = "location"
InputPollMediaTypePhoto InputPollMediaType = "photo"
InputPollMediaTypeVenue InputPollMediaType = "venue"
InputPollMediaTypeVideo InputPollMediaType = "video"
)
type InputPollOptionMediaType string
const (
InputPollOptionMediaTypeAnimation InputPollOptionMediaType = "animation"
InputPollOptionMediaTypeLivePhoto InputPollOptionMediaType = "live_photo"
InputPollOptionMediaTypeLocation InputPollOptionMediaType = "location"
InputPollOptionMediaTypePhoto InputPollOptionMediaType = "photo"
InputPollOptionMediaTypeSticker InputPollOptionMediaType = "sticker"
InputPollOptionMediaTypeVenue InputPollOptionMediaType = "venue"
InputPollOptionMediaTypeVideo InputPollOptionMediaType = "video"
)
type InputProfilePhotoType string
const (
InputProfilePhotoTypeStatic InputProfilePhotoType = "static"
InputProfilePhotoTypeAnimated InputProfilePhotoType = "animated"
)
type InputStickerFormat string
const (
@@ -160,6 +181,13 @@ const (
InputStickerFormatVideo InputStickerFormat = "video"
)
type InputStoryContentType string
const (
InputStoryContentTypePhoto InputStoryContentType = "photo"
InputStoryContentTypeVideo InputStoryContentType = "video"
)
type KeyboardButtonStyle string
const (
@@ -177,6 +205,14 @@ const (
MaskPositionPointChin MaskPositionPoint = "chin"
)
type MenuButtonType string
const (
MenuButtonTypeCommands MenuButtonType = "commands"
MenuButtonTypeWebApp MenuButtonType = "web_app"
MenuButtonTypeDefault MenuButtonType = "default"
)
type MessageEntityType string
const (
@@ -202,64 +238,29 @@ const (
MessageEntityTypeDateTime MessageEntityType = "date_time"
)
type MessageOriginChannelType string
type MessageOriginType string
const (
MessageOriginChannelTypeChannel MessageOriginChannelType = "channel"
MessageOriginTypeUser MessageOriginType = "user"
MessageOriginTypeHiddenUser MessageOriginType = "hidden_user"
MessageOriginTypeChat MessageOriginType = "chat"
MessageOriginTypeChannel MessageOriginType = "channel"
)
type MessageOriginChatType string
type OwnedGiftType string
const (
MessageOriginChatTypeChat MessageOriginChatType = "chat"
OwnedGiftTypeRegular OwnedGiftType = "regular"
OwnedGiftTypeUnique OwnedGiftType = "unique"
)
type MessageOriginHiddenUserType string
type PaidMediaType string
const (
MessageOriginHiddenUserTypeHiddenUser MessageOriginHiddenUserType = "hidden_user"
)
type MessageOriginUserType string
const (
MessageOriginUserTypeUser MessageOriginUserType = "user"
)
type OwnedGiftRegularType string
const (
OwnedGiftRegularTypeRegular OwnedGiftRegularType = "regular"
)
type OwnedGiftUniqueType string
const (
OwnedGiftUniqueTypeUnique OwnedGiftUniqueType = "unique"
)
type PaidMediaLivePhotoType string
const (
PaidMediaLivePhotoTypeLivePhoto PaidMediaLivePhotoType = "live_photo"
)
type PaidMediaPhotoType string
const (
PaidMediaPhotoTypePhoto PaidMediaPhotoType = "photo"
)
type PaidMediaPreviewType string
const (
PaidMediaPreviewTypePreview PaidMediaPreviewType = "preview"
)
type PaidMediaVideoType string
const (
PaidMediaVideoTypeVideo PaidMediaVideoType = "video"
PaidMediaTypeLivePhoto PaidMediaType = "live_photo"
PaidMediaTypePhoto PaidMediaType = "photo"
PaidMediaTypePreview PaidMediaType = "preview"
PaidMediaTypeVideo PaidMediaType = "video"
)
type ParseMode string
@@ -307,6 +308,20 @@ const (
PassportElementErrorSelfieTypeInternalPassport PassportElementErrorSelfieType = "internal_passport"
)
type PassportElementErrorSource string
const (
PassportElementErrorSourceData PassportElementErrorSource = "data"
PassportElementErrorSourceFrontSide PassportElementErrorSource = "front_side"
PassportElementErrorSourceReverseSide PassportElementErrorSource = "reverse_side"
PassportElementErrorSourceSelfie PassportElementErrorSource = "selfie"
PassportElementErrorSourceFile PassportElementErrorSource = "file"
PassportElementErrorSourceFiles PassportElementErrorSource = "files"
PassportElementErrorSourceTranslationFile PassportElementErrorSource = "translation_file"
PassportElementErrorSourceTranslationFiles PassportElementErrorSource = "translation_files"
PassportElementErrorSourceUnspecified PassportElementErrorSource = "unspecified"
)
type PassportElementErrorTranslationFileType string
const (
@@ -328,22 +343,12 @@ const (
PollTypeQuiz PollType = "quiz"
)
type ReactionTypeCustomEmojiType string
type ReactionTypeKind string
const (
ReactionTypeCustomEmojiTypeCustomEmoji ReactionTypeCustomEmojiType = "custom_emoji"
)
type ReactionTypeEmojiType string
const (
ReactionTypeEmojiTypeEmoji ReactionTypeEmojiType = "emoji"
)
type ReactionTypePaidType string
const (
ReactionTypePaidTypePaid ReactionTypePaidType = "paid"
ReactionTypeKindEmoji ReactionTypeKind = "emoji"
ReactionTypeKindCustomEmoji ReactionTypeKind = "custom_emoji"
ReactionTypeKindPaid ReactionTypeKind = "paid"
)
type RefundedPaymentCurrency string
@@ -352,22 +357,12 @@ const (
RefundedPaymentCurrencyXTR RefundedPaymentCurrency = "XTR"
)
type RevenueWithdrawalStateFailedType string
type RevenueWithdrawalStateKind string
const (
RevenueWithdrawalStateFailedTypeFailed RevenueWithdrawalStateFailedType = "failed"
)
type RevenueWithdrawalStatePendingType string
const (
RevenueWithdrawalStatePendingTypePending RevenueWithdrawalStatePendingType = "pending"
)
type RevenueWithdrawalStateSucceededType string
const (
RevenueWithdrawalStateSucceededTypeSucceeded RevenueWithdrawalStateSucceededType = "succeeded"
RevenueWithdrawalStateKindPending RevenueWithdrawalStateKind = "pending"
RevenueWithdrawalStateKindSucceeded RevenueWithdrawalStateKind = "succeeded"
RevenueWithdrawalStateKindFailed RevenueWithdrawalStateKind = "failed"
)
type StickerType string
@@ -378,34 +373,14 @@ const (
StickerTypeCustomEmoji StickerType = "custom_emoji"
)
type StoryAreaTypeLinkType string
type StoryAreaTypeKind string
const (
StoryAreaTypeLinkTypeLink StoryAreaTypeLinkType = "link"
)
type StoryAreaTypeLocationType string
const (
StoryAreaTypeLocationTypeLocation StoryAreaTypeLocationType = "location"
)
type StoryAreaTypeSuggestedReactionType string
const (
StoryAreaTypeSuggestedReactionTypeSuggestedReaction StoryAreaTypeSuggestedReactionType = "suggested_reaction"
)
type StoryAreaTypeUniqueGiftType string
const (
StoryAreaTypeUniqueGiftTypeUniqueGift StoryAreaTypeUniqueGiftType = "unique_gift"
)
type StoryAreaTypeWeatherType string
const (
StoryAreaTypeWeatherTypeWeather StoryAreaTypeWeatherType = "weather"
StoryAreaTypeKindLocation StoryAreaTypeKind = "location"
StoryAreaTypeKindSuggestedReaction StoryAreaTypeKind = "suggested_reaction"
StoryAreaTypeKindLink StoryAreaTypeKind = "link"
StoryAreaTypeKindWeather StoryAreaTypeKind = "weather"
StoryAreaTypeKindUniqueGift StoryAreaTypeKind = "unique_gift"
)
type SuggestedPostInfoState string
@@ -430,34 +405,16 @@ const (
SuggestedPostRefundedReasonPaymentRefunded SuggestedPostRefundedReason = "payment_refunded"
)
type TransactionPartnerAffiliateProgramType string
type TransactionPartnerType string
const (
TransactionPartnerAffiliateProgramTypeAffiliateProgram TransactionPartnerAffiliateProgramType = "affiliate_program"
)
type TransactionPartnerFragmentType string
const (
TransactionPartnerFragmentTypeFragment TransactionPartnerFragmentType = "fragment"
)
type TransactionPartnerOtherType string
const (
TransactionPartnerOtherTypeOther TransactionPartnerOtherType = "other"
)
type TransactionPartnerTelegramAdsType string
const (
TransactionPartnerTelegramAdsTypeTelegramAds TransactionPartnerTelegramAdsType = "telegram_ads"
)
type TransactionPartnerTelegramApiType string
const (
TransactionPartnerTelegramApiTypeTelegramApi TransactionPartnerTelegramApiType = "telegram_api"
TransactionPartnerTypeUser TransactionPartnerType = "user"
TransactionPartnerTypeChat TransactionPartnerType = "chat"
TransactionPartnerTypeAffiliateProgram TransactionPartnerType = "affiliate_program"
TransactionPartnerTypeFragment TransactionPartnerType = "fragment"
TransactionPartnerTypeTelegramAds TransactionPartnerType = "telegram_ads"
TransactionPartnerTypeTelegramApi TransactionPartnerType = "telegram_api"
TransactionPartnerTypeOther TransactionPartnerType = "other"
)
type TransactionPartnerUserTransactionType string
+104
View File
@@ -32,3 +32,107 @@ const (
UpdateChatBoost UpdateType = "chat_boost"
UpdateRemovedChatBoost UpdateType = "removed_chat_boost"
)
// DiceEmoji is the set of emoji values accepted by sendDice. Telegram's
// canonical list is "🎲", "🎯", "🏀", "⚽", "🎳", "🎰". The codegen
// scraper drops these values during regex extraction (multi-byte
// boundary issues with curly-quoted emoji), so this enum is hand-
// curated and wired into SendDiceParams.Emoji via the per-field type
// override in cmd/genapi/emitter.go.
type DiceEmoji string
const (
DiceEmojiDice DiceEmoji = "🎲"
DiceEmojiDart DiceEmoji = "🎯"
DiceEmojiBasketball DiceEmoji = "🏀"
DiceEmojiFootball DiceEmoji = "⚽"
DiceEmojiBowling DiceEmoji = "🎳"
DiceEmojiSlotMachine DiceEmoji = "🎰"
)
// ReactionEmoji is the set of emoji Telegram allows in a
// ReactionTypeEmoji.Emoji value. Hand-curated from
// https://core.telegram.org/bots/api#reactiontypeemoji because the
// scraper's curly-quote regex strips the emoji literals (byte-boundary
// issue on multi-byte sequences). Names mirror the Unicode CLDR short
// name where one exists; otherwise a stable common-English label.
// Telegram occasionally extends this set — passers of unrecognised
// strings still type-check (ReactionEmoji is a string alias) so this
// list need not block runtime use of newer values.
type ReactionEmoji string
const (
ReactionEmojiHeart ReactionEmoji = "❤"
ReactionEmojiThumbsUp ReactionEmoji = "👍"
ReactionEmojiThumbsDown ReactionEmoji = "👎"
ReactionEmojiFire ReactionEmoji = "🔥"
ReactionEmojiSmilingFaceWithHearts ReactionEmoji = "🥰"
ReactionEmojiClappingHands ReactionEmoji = "👏"
ReactionEmojiBeamingFace ReactionEmoji = "😁"
ReactionEmojiThinkingFace ReactionEmoji = "🤔"
ReactionEmojiExplodingHead ReactionEmoji = "🤯"
ReactionEmojiScreamingFace ReactionEmoji = "😱"
ReactionEmojiCursingFace ReactionEmoji = "🤬"
ReactionEmojiCryingFace ReactionEmoji = "😢"
ReactionEmojiPartyPopper ReactionEmoji = "🎉"
ReactionEmojiStarStruck ReactionEmoji = "🤩"
ReactionEmojiVomiting ReactionEmoji = "🤮"
ReactionEmojiPileOfPoo ReactionEmoji = "💩"
ReactionEmojiFoldedHands ReactionEmoji = "🙏"
ReactionEmojiOKHand ReactionEmoji = "👌"
ReactionEmojiDove ReactionEmoji = "🕊"
ReactionEmojiClown ReactionEmoji = "🤡"
ReactionEmojiYawning ReactionEmoji = "🥱"
ReactionEmojiWoozyFace ReactionEmoji = "🥴"
ReactionEmojiHeartEyes ReactionEmoji = "😍"
ReactionEmojiWhale ReactionEmoji = "🐳"
ReactionEmojiHeartOnFire ReactionEmoji = "❤‍🔥"
ReactionEmojiNewMoonFace ReactionEmoji = "🌚"
ReactionEmojiHotDog ReactionEmoji = "🌭"
ReactionEmojiHundredPoints ReactionEmoji = "💯"
ReactionEmojiRollingOnFloor ReactionEmoji = "🤣"
ReactionEmojiLightning ReactionEmoji = "⚡"
ReactionEmojiBanana ReactionEmoji = "🍌"
ReactionEmojiTrophy ReactionEmoji = "🏆"
ReactionEmojiBrokenHeart ReactionEmoji = "💔"
ReactionEmojiRaisedEyebrow ReactionEmoji = "🤨"
ReactionEmojiNeutralFace ReactionEmoji = "😐"
ReactionEmojiStrawberry ReactionEmoji = "🍓"
ReactionEmojiChampagne ReactionEmoji = "🍾"
ReactionEmojiKissMark ReactionEmoji = "💋"
ReactionEmojiMiddleFinger ReactionEmoji = "🖕"
ReactionEmojiDevil ReactionEmoji = "😈"
ReactionEmojiSleeping ReactionEmoji = "😴"
ReactionEmojiLoudlyCrying ReactionEmoji = "😭"
ReactionEmojiNerd ReactionEmoji = "🤓"
ReactionEmojiGhost ReactionEmoji = "👻"
ReactionEmojiManTechnologist ReactionEmoji = "👨‍💻"
ReactionEmojiEyes ReactionEmoji = "👀"
ReactionEmojiJackOLantern ReactionEmoji = "🎃"
ReactionEmojiSeeNoEvil ReactionEmoji = "🙈"
ReactionEmojiHalo ReactionEmoji = "😇"
ReactionEmojiFearful ReactionEmoji = "😨"
ReactionEmojiHandshake ReactionEmoji = "🤝"
ReactionEmojiWriting ReactionEmoji = "✍"
ReactionEmojiHugging ReactionEmoji = "🤗"
ReactionEmojiSaluting ReactionEmoji = "🫡"
ReactionEmojiSantaClaus ReactionEmoji = "🎅"
ReactionEmojiChristmasTree ReactionEmoji = "🎄"
ReactionEmojiSnowman ReactionEmoji = "☃"
ReactionEmojiNailPolish ReactionEmoji = "💅"
ReactionEmojiZanyFace ReactionEmoji = "🤪"
ReactionEmojiMoai ReactionEmoji = "🗿"
ReactionEmojiCool ReactionEmoji = "🆒"
ReactionEmojiHeartWithArrow ReactionEmoji = "💘"
ReactionEmojiHearNoEvil ReactionEmoji = "🙉"
ReactionEmojiUnicorn ReactionEmoji = "🦄"
ReactionEmojiKissingFace ReactionEmoji = "😘"
ReactionEmojiPill ReactionEmoji = "💊"
ReactionEmojiSpeakNoEvil ReactionEmoji = "🙊"
ReactionEmojiSmilingFaceWithSunglasses ReactionEmoji = "😎"
ReactionEmojiAlienMonster ReactionEmoji = "👾"
ReactionEmojiManShrugging ReactionEmoji = "🤷‍♂"
ReactionEmojiPersonShrugging ReactionEmoji = "🤷"
ReactionEmojiWomanShrugging ReactionEmoji = "🤷‍♀"
ReactionEmojiPoutingFace ReactionEmoji = "😡"
)
+1 -1
View File
@@ -65,7 +65,7 @@ func TestMarshalJSON_RoundTrip(t *testing.T) {
round, ok := out.(*ChatMemberLeft)
require.True(t, ok, "expected *ChatMemberLeft, got %T", out)
require.Equal(t, ChatMemberLeftStatusLeft, round.Status)
require.Equal(t, ChatMemberStatusLeft, round.Status)
require.Equal(t, orig.User.ID, round.User.ID)
require.Equal(t, orig.User.FirstName, round.User.FirstName)
}
+3 -3
View File
@@ -27,7 +27,7 @@ type GetUpdatesParams struct {
// Timeout in seconds for long polling. Defaults to 0, i.e. usual short polling. Should be positive, short polling should be used for testing purposes only.
Timeout *int64 `json:"timeout,omitempty"`
// A JSON-serialized list of the update types you want your bot to receive. For example, specify ["message", "edited_channel_post", "callback_query"] to only receive updates of these types. See Update for a complete list of available update types. Specify an empty list to receive all update types except chat_member, message_reaction, and message_reaction_count (default). If not specified, the previous setting will be used.Please note that this parameter doesn't affect updates created before the call to getUpdates, so unwanted updates may be received for a short period of time.
AllowedUpdates []string `json:"allowed_updates,omitempty"`
AllowedUpdates []UpdateType `json:"allowed_updates,omitempty"`
}
// GetUpdates calls the getUpdates Telegram Bot API method.
@@ -54,7 +54,7 @@ type SetWebhookParams struct {
// The maximum allowed number of simultaneous HTTPS connections to the webhook for update delivery, 1-100. Defaults to 40. Use lower values to limit the load on your bot's server, and higher values to increase your bot's throughput.
MaxConnections *int64 `json:"max_connections,omitempty"`
// A JSON-serialized list of the update types you want your bot to receive. For example, specify ["message", "edited_channel_post", "callback_query"] to only receive updates of these types. See Update for a complete list of available update types. Specify an empty list to receive all update types except chat_member, message_reaction, and message_reaction_count (default). If not specified, the previous setting will be used.Please note that this parameter doesn't affect updates created before the call to the setWebhook, so unwanted updates may be received for a short period of time.
AllowedUpdates []string `json:"allowed_updates,omitempty"`
AllowedUpdates []UpdateType `json:"allowed_updates,omitempty"`
// Pass True to drop all pending updates
DropPendingUpdates *bool `json:"drop_pending_updates,omitempty"`
// A secret token to be sent in a header “X-Telegram-Bot-Api-Secret-Token” in every webhook request, 1-256 characters. Only characters A-Z, a-z, 0-9, _ and - are allowed. The header is useful to ensure that the request comes from a webhook set by you.
@@ -1953,7 +1953,7 @@ type SendDiceParams struct {
// Identifier of the direct messages topic to which the message will be sent; required if the message is sent to a direct messages chat
DirectMessagesTopicID *int64 `json:"direct_messages_topic_id,omitempty"`
// Emoji on which the dice throw animation is based. Currently, must be one of “”, “”, “”, “”, “”, or “”. Dice can have values 1-6 for “”, “” and “”, values 1-5 for “” and “”, and values 1-64 for “”. Defaults to “”
Emoji string `json:"emoji,omitempty"`
Emoji DiceEmoji `json:"emoji,omitempty"`
// Sends the message silently. Users will receive a notification with no sound.
DisableNotification *bool `json:"disable_notification,omitempty"`
// Protects the contents of the sent message from forwarding
+101 -101
View File
@@ -91,7 +91,7 @@ type WebhookInfo struct {
// Optional. The maximum allowed number of simultaneous HTTPS connections to the webhook for update delivery
MaxConnections *int64 `json:"max_connections,omitempty"`
// Optional. A list of update types the bot is subscribed to. Defaults to all update types except chat_member
AllowedUpdates []string `json:"allowed_updates,omitempty"`
AllowedUpdates []UpdateType `json:"allowed_updates,omitempty"`
}
// This object represents a Telegram user or bot.
@@ -800,7 +800,7 @@ func UnmarshalMessageOrigin(data []byte) (MessageOrigin, error) {
// The message was originally sent by a known user.
type MessageOriginUser struct {
// Type of the message origin, always “user”
Type MessageOriginUserType `json:"type"`
Type MessageOriginType `json:"type"`
// Date the message was sent originally in Unix time
Date int64 `json:"date"`
// User that sent the message originally
@@ -826,7 +826,7 @@ func (v *MessageOriginUser) MarshalJSON() ([]byte, error) {
// The message was originally sent by an unknown user.
type MessageOriginHiddenUser struct {
// Type of the message origin, always “hidden_user”
Type MessageOriginHiddenUserType `json:"type"`
Type MessageOriginType `json:"type"`
// Date the message was sent originally in Unix time
Date int64 `json:"date"`
// Name of the user that sent the message originally
@@ -852,7 +852,7 @@ func (v *MessageOriginHiddenUser) MarshalJSON() ([]byte, error) {
// The message was originally sent on behalf of a chat to a group chat.
type MessageOriginChat struct {
// Type of the message origin, always “chat”
Type MessageOriginChatType `json:"type"`
Type MessageOriginType `json:"type"`
// Date the message was sent originally in Unix time
Date int64 `json:"date"`
// Chat that sent the message originally
@@ -880,7 +880,7 @@ func (v *MessageOriginChat) MarshalJSON() ([]byte, error) {
// The message was originally sent to a channel chat.
type MessageOriginChannel struct {
// Type of the message origin, always “channel”
Type MessageOriginChannelType `json:"type"`
Type MessageOriginType `json:"type"`
// Date the message was sent originally in Unix time
Date int64 `json:"date"`
// Channel chat to which the message was originally sent
@@ -1174,7 +1174,7 @@ func UnmarshalPaidMedia(data []byte) (PaidMedia, error) {
// The paid media is a live photo.
type PaidMediaLivePhoto struct {
// Type of the paid media, always “live_photo”
Type PaidMediaLivePhotoType `json:"type"`
Type PaidMediaType `json:"type"`
// The photo
LivePhoto LivePhoto `json:"live_photo"`
}
@@ -1198,7 +1198,7 @@ func (v *PaidMediaLivePhoto) MarshalJSON() ([]byte, error) {
// The paid media is a photo.
type PaidMediaPhoto struct {
// Type of the paid media, always “photo”
Type PaidMediaPhotoType `json:"type"`
Type PaidMediaType `json:"type"`
// The photo
Photo []PhotoSize `json:"photo"`
}
@@ -1222,7 +1222,7 @@ func (v *PaidMediaPhoto) MarshalJSON() ([]byte, error) {
// The paid media isn't available before the payment.
type PaidMediaPreview struct {
// Type of the paid media, always “preview”
Type PaidMediaPreviewType `json:"type"`
Type PaidMediaType `json:"type"`
// Optional. Media width as defined by the sender
Width *int64 `json:"width,omitempty"`
// Optional. Media height as defined by the sender
@@ -1250,7 +1250,7 @@ func (v *PaidMediaPreview) MarshalJSON() ([]byte, error) {
// The paid media is a video.
type PaidMediaVideo struct {
// Type of the paid media, always “video”
Type PaidMediaVideoType `json:"type"`
Type PaidMediaType `json:"type"`
// The video
Video Video `json:"video"`
}
@@ -1752,7 +1752,7 @@ func UnmarshalBackgroundFill(data []byte) (BackgroundFill, error) {
// The background is filled using the selected color.
type BackgroundFillSolid struct {
// Type of the background fill, always “solid”
Type BackgroundFillSolidType `json:"type"`
Type BackgroundFillType `json:"type"`
// The color of the background fill in the RGB24 format
Color int64 `json:"color"`
}
@@ -1776,7 +1776,7 @@ func (v *BackgroundFillSolid) MarshalJSON() ([]byte, error) {
// The background is a gradient fill.
type BackgroundFillGradient struct {
// Type of the background fill, always “gradient”
Type BackgroundFillGradientType `json:"type"`
Type BackgroundFillType `json:"type"`
// Top color of the gradient in the RGB24 format
TopColor int64 `json:"top_color"`
// Bottom color of the gradient in the RGB24 format
@@ -1804,7 +1804,7 @@ func (v *BackgroundFillGradient) MarshalJSON() ([]byte, error) {
// The background is a freeform gradient that rotates after every message in the chat.
type BackgroundFillFreeformGradient struct {
// Type of the background fill, always “freeform_gradient”
Type BackgroundFillFreeformGradientType `json:"type"`
Type BackgroundFillType `json:"type"`
// A list of the 3 or 4 base colors that are used to generate the freeform gradient in the RGB24 format
Colors []int64 `json:"colors"`
}
@@ -1878,7 +1878,7 @@ func UnmarshalBackgroundType(data []byte) (BackgroundType, error) {
// The background is automatically filled based on the selected colors.
type BackgroundTypeFill struct {
// Type of the background, always “fill”
Type BackgroundTypeFillType `json:"type"`
Type BackgroundTypeKind `json:"type"`
// The background fill
Fill BackgroundFill `json:"fill"`
// Dimming of the background in dark themes, as a percentage; 0-100
@@ -1928,7 +1928,7 @@ func (m *BackgroundTypeFill) UnmarshalJSON(data []byte) error {
// The background is a wallpaper in the JPEG format.
type BackgroundTypeWallpaper struct {
// Type of the background, always “wallpaper”
Type BackgroundTypeWallpaperType `json:"type"`
Type BackgroundTypeKind `json:"type"`
// Document with the wallpaper
Document Document `json:"document"`
// Dimming of the background in dark themes, as a percentage; 0-100
@@ -1958,7 +1958,7 @@ func (v *BackgroundTypeWallpaper) MarshalJSON() ([]byte, error) {
// The background 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 BackgroundTypePattern struct {
// Type of the background, always “pattern”
Type BackgroundTypePatternType `json:"type"`
Type BackgroundTypeKind `json:"type"`
// Document with the pattern
Document Document `json:"document"`
// The background fill that is combined with the pattern
@@ -2014,7 +2014,7 @@ func (m *BackgroundTypePattern) UnmarshalJSON(data []byte) error {
// The background is taken directly from a built-in chat theme.
type BackgroundTypeChatTheme struct {
// Type of the background, always “chat_theme”
Type BackgroundTypeChatThemeType `json:"type"`
Type BackgroundTypeKind `json:"type"`
// Name of the chat theme, which is usually an emoji
ThemeName string `json:"theme_name"`
}
@@ -2808,7 +2808,7 @@ func UnmarshalChatMember(data []byte) (ChatMember, error) {
// Represents a chat member that owns the chat and has all administrator privileges.
type ChatMemberOwner struct {
// The member's status in the chat, always “creator”
Status ChatMemberOwnerStatus `json:"status"`
Status ChatMemberStatus `json:"status"`
// Information about the user
User User `json:"user"`
// True, if the user's presence in the chat is hidden
@@ -2836,7 +2836,7 @@ func (v *ChatMemberOwner) MarshalJSON() ([]byte, error) {
// Represents a chat member that has some additional privileges.
type ChatMemberAdministrator struct {
// The member's status in the chat, always “administrator”
Status ChatMemberAdministratorStatus `json:"status"`
Status ChatMemberStatus `json:"status"`
// Information about the user
User User `json:"user"`
// True, if the bot is allowed to edit administrator privileges of that user
@@ -2898,7 +2898,7 @@ func (v *ChatMemberAdministrator) MarshalJSON() ([]byte, error) {
// Represents a chat member that has no additional privileges or restrictions.
type ChatMemberMember struct {
// The member's status in the chat, always “member”
Status ChatMemberMemberStatus `json:"status"`
Status ChatMemberStatus `json:"status"`
// Optional. Tag of the member
Tag string `json:"tag,omitempty"`
// Information about the user
@@ -2926,7 +2926,7 @@ func (v *ChatMemberMember) MarshalJSON() ([]byte, error) {
// Represents a chat member that is under certain restrictions in the chat. Supergroups only.
type ChatMemberRestricted struct {
// The member's status in the chat, always “restricted”
Status ChatMemberRestrictedStatus `json:"status"`
Status ChatMemberStatus `json:"status"`
// Optional. Tag of the member
Tag string `json:"tag,omitempty"`
// Information about the user
@@ -2988,7 +2988,7 @@ func (v *ChatMemberRestricted) MarshalJSON() ([]byte, error) {
// Represents a chat member that isn't currently a member of the chat, but may join it themselves.
type ChatMemberLeft struct {
// The member's status in the chat, always “left”
Status ChatMemberLeftStatus `json:"status"`
Status ChatMemberStatus `json:"status"`
// Information about the user
User User `json:"user"`
}
@@ -3012,7 +3012,7 @@ func (v *ChatMemberLeft) MarshalJSON() ([]byte, error) {
// Represents a chat member that was banned in the chat and can't return to the chat or view chat messages.
type ChatMemberBanned struct {
// The member's status in the chat, always “kicked”
Status ChatMemberBannedStatus `json:"status"`
Status ChatMemberStatus `json:"status"`
// Information about the user
User User `json:"user"`
// Date when restrictions will be lifted for this user; Unix time. If 0, then the user is banned forever
@@ -3230,7 +3230,7 @@ func UnmarshalStoryAreaType(data []byte) (StoryAreaType, error) {
// Describes a story area pointing to a location. Currently, a story can have up to 10 location areas.
type StoryAreaTypeLocation struct {
// Type of the area, always “location”
Type StoryAreaTypeLocationType `json:"type"`
Type StoryAreaTypeKind `json:"type"`
// Location latitude in degrees
Latitude float64 `json:"latitude"`
// Location longitude in degrees
@@ -3258,7 +3258,7 @@ func (v *StoryAreaTypeLocation) MarshalJSON() ([]byte, error) {
// Describes a story area pointing to a suggested reaction. Currently, a story can have up to 5 suggested reaction areas.
type StoryAreaTypeSuggestedReaction struct {
// Type of the area, always “suggested_reaction”
Type StoryAreaTypeSuggestedReactionType `json:"type"`
Type StoryAreaTypeKind `json:"type"`
// Type of the reaction
ReactionType ReactionType `json:"reaction_type"`
// Optional. Pass True if the reaction area has a dark background
@@ -3310,7 +3310,7 @@ func (m *StoryAreaTypeSuggestedReaction) UnmarshalJSON(data []byte) error {
// Describes a story area pointing to an HTTP or tg:// link. Currently, a story can have up to 3 link areas.
type StoryAreaTypeLink struct {
// Type of the area, always “link”
Type StoryAreaTypeLinkType `json:"type"`
Type StoryAreaTypeKind `json:"type"`
// HTTP or tg:// URL to be opened when the area is clicked
URL string `json:"url"`
}
@@ -3334,7 +3334,7 @@ func (v *StoryAreaTypeLink) MarshalJSON() ([]byte, error) {
// Describes a story area containing weather information. Currently, a story can have up to 3 weather areas.
type StoryAreaTypeWeather struct {
// Type of the area, always “weather”
Type StoryAreaTypeWeatherType `json:"type"`
Type StoryAreaTypeKind `json:"type"`
// Temperature, in degree Celsius
Temperature float64 `json:"temperature"`
// Emoji representing the weather
@@ -3362,7 +3362,7 @@ func (v *StoryAreaTypeWeather) MarshalJSON() ([]byte, error) {
// Describes a story area pointing to a unique gift. Currently, a story can have at most 1 unique gift area.
type StoryAreaTypeUniqueGift struct {
// Type of the area, always “unique_gift”
Type StoryAreaTypeUniqueGiftType `json:"type"`
Type StoryAreaTypeKind `json:"type"`
// Unique name of the gift
Name string `json:"name"`
}
@@ -3470,9 +3470,9 @@ func UnmarshalReactionType(data []byte) (ReactionType, error) {
// The reaction is based on an emoji.
type ReactionTypeEmoji struct {
// Type of the reaction, always “emoji”
Type ReactionTypeEmojiType `json:"type"`
Type ReactionTypeKind `json:"type"`
// Reaction emoji. Currently, it can be one of "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""
Emoji string `json:"emoji"`
Emoji ReactionEmoji `json:"emoji"`
}
// MarshalJSON encodes ReactionTypeEmoji with the discriminator field
@@ -3494,7 +3494,7 @@ func (v *ReactionTypeEmoji) MarshalJSON() ([]byte, error) {
// The reaction is based on a custom emoji.
type ReactionTypeCustomEmoji struct {
// Type of the reaction, always “custom_emoji”
Type ReactionTypeCustomEmojiType `json:"type"`
Type ReactionTypeKind `json:"type"`
// Custom emoji identifier
CustomEmojiID string `json:"custom_emoji_id"`
}
@@ -3518,7 +3518,7 @@ func (v *ReactionTypeCustomEmoji) MarshalJSON() ([]byte, error) {
// The reaction is paid.
type ReactionTypePaid struct {
// Type of the reaction, always “paid”
Type ReactionTypePaidType `json:"type"`
Type ReactionTypeKind `json:"type"`
}
// MarshalJSON encodes ReactionTypePaid with the discriminator field
@@ -3880,7 +3880,7 @@ func UnmarshalOwnedGift(data []byte) (OwnedGift, error) {
// Describes a regular gift owned by a user or a chat.
type OwnedGiftRegular struct {
// Type of the gift, always “regular”
Type OwnedGiftRegularType `json:"type"`
Type OwnedGiftType `json:"type"`
// Information about the regular gift
Gift Gift `json:"gift"`
// Optional. Unique identifier of the gift for the bot; for gifts received on behalf of business accounts only
@@ -3930,7 +3930,7 @@ func (v *OwnedGiftRegular) MarshalJSON() ([]byte, error) {
// Describes a unique gift received and owned by a user or a chat.
type OwnedGiftUnique struct {
// Type of the gift, always “unique”
Type OwnedGiftUniqueType `json:"type"`
Type OwnedGiftType `json:"type"`
// Information about the unique gift
Gift UniqueGift `json:"gift"`
// Optional. Unique identifier of the received gift for the bot; for gifts received on behalf of business accounts only
@@ -4082,7 +4082,7 @@ func (*BotCommandScopeChatMember) isBotCommandScope() {}
// Represents the default scope of bot commands. Default commands are used if no commands with a narrower scope are specified for the user.
type BotCommandScopeDefault struct {
// Scope type, must be default
Type string `json:"type"`
Type BotCommandScopeType `json:"type"`
}
// MarshalJSON encodes BotCommandScopeDefault with the discriminator field
@@ -4104,7 +4104,7 @@ func (v *BotCommandScopeDefault) MarshalJSON() ([]byte, error) {
// Represents the scope of bot commands, covering all private chats.
type BotCommandScopeAllPrivateChats struct {
// Scope type, must be all_private_chats
Type string `json:"type"`
Type BotCommandScopeType `json:"type"`
}
// MarshalJSON encodes BotCommandScopeAllPrivateChats with the discriminator field
@@ -4126,7 +4126,7 @@ func (v *BotCommandScopeAllPrivateChats) MarshalJSON() ([]byte, error) {
// Represents the scope of bot commands, covering all group and supergroup chats.
type BotCommandScopeAllGroupChats struct {
// Scope type, must be all_group_chats
Type string `json:"type"`
Type BotCommandScopeType `json:"type"`
}
// MarshalJSON encodes BotCommandScopeAllGroupChats with the discriminator field
@@ -4148,7 +4148,7 @@ func (v *BotCommandScopeAllGroupChats) MarshalJSON() ([]byte, error) {
// Represents the scope of bot commands, covering all group and supergroup chat administrators.
type BotCommandScopeAllChatAdministrators struct {
// Scope type, must be all_chat_administrators
Type string `json:"type"`
Type BotCommandScopeType `json:"type"`
}
// MarshalJSON encodes BotCommandScopeAllChatAdministrators with the discriminator field
@@ -4170,7 +4170,7 @@ func (v *BotCommandScopeAllChatAdministrators) MarshalJSON() ([]byte, error) {
// Represents the scope of bot commands, covering a specific chat.
type BotCommandScopeChat struct {
// Scope type, must be chat
Type string `json:"type"`
Type BotCommandScopeType `json:"type"`
// Unique identifier for the target chat or username of the target supergroup in the format @username. Channel direct messages chats and channel chats aren't supported.
ChatID ChatID `json:"chat_id"`
}
@@ -4194,7 +4194,7 @@ func (v *BotCommandScopeChat) MarshalJSON() ([]byte, error) {
// Represents the scope of bot commands, covering all administrators of a specific group or supergroup chat.
type BotCommandScopeChatAdministrators struct {
// Scope type, must be chat_administrators
Type string `json:"type"`
Type BotCommandScopeType `json:"type"`
// Unique identifier for the target chat or username of the target supergroup in the format @username. Channel direct messages chats and channel chats aren't supported.
ChatID ChatID `json:"chat_id"`
}
@@ -4218,7 +4218,7 @@ func (v *BotCommandScopeChatAdministrators) MarshalJSON() ([]byte, error) {
// Represents the scope of bot commands, covering a specific member of a group or supergroup chat.
type BotCommandScopeChatMember struct {
// Scope type, must be chat_member
Type string `json:"type"`
Type BotCommandScopeType `json:"type"`
// Unique identifier for the target chat or username of the target supergroup in the format @username. Channel direct messages chats and channel chats aren't supported.
ChatID ChatID `json:"chat_id"`
// Unique identifier of the target user
@@ -4307,7 +4307,7 @@ func UnmarshalMenuButton(data []byte) (MenuButton, error) {
// Represents a menu button, which opens the bot's list of commands.
type MenuButtonCommands struct {
// Type of the button, must be commands
Type string `json:"type"`
Type MenuButtonType `json:"type"`
}
// MarshalJSON encodes MenuButtonCommands with the discriminator field
@@ -4329,7 +4329,7 @@ func (v *MenuButtonCommands) MarshalJSON() ([]byte, error) {
// Represents a menu button, which launches a Web App.
type MenuButtonWebApp struct {
// Type of the button, must be web_app
Type string `json:"type"`
Type MenuButtonType `json:"type"`
// Text on the button
Text string `json:"text"`
// Description of the Web App that will be launched when the user presses the button. The Web App will be able to send an arbitrary message on behalf of the user using the method answerWebAppQuery. Alternatively, a t.me link to a Web App of the bot can be specified in the object instead of the Web App's URL, in which case the Web App will be opened as if the user pressed the link.
@@ -4355,7 +4355,7 @@ func (v *MenuButtonWebApp) MarshalJSON() ([]byte, error) {
// Describes that no specific value for the menu button was set.
type MenuButtonDefault struct {
// Type of the button, must be default
Type string `json:"type"`
Type MenuButtonType `json:"type"`
}
// MarshalJSON encodes MenuButtonDefault with the discriminator field
@@ -4421,7 +4421,7 @@ func UnmarshalChatBoostSource(data []byte) (ChatBoostSource, error) {
// The boost was obtained by subscribing to Telegram Premium or by gifting a Telegram Premium subscription to another user.
type ChatBoostSourcePremium struct {
// Source of the boost, always “premium”
Source ChatBoostSourcePremiumSource `json:"source"`
Source ChatBoostSourceKind `json:"source"`
// User that boosted the chat
User User `json:"user"`
}
@@ -4445,7 +4445,7 @@ func (v *ChatBoostSourcePremium) MarshalJSON() ([]byte, error) {
// The boost was obtained by the creation of Telegram Premium gift codes to boost a chat. Each such code boosts the chat 4 times for the duration of the corresponding Telegram Premium subscription.
type ChatBoostSourceGiftCode struct {
// Source of the boost, always “gift_code”
Source ChatBoostSourceGiftCodeSource `json:"source"`
Source ChatBoostSourceKind `json:"source"`
// User for which the gift code was created
User User `json:"user"`
}
@@ -4469,7 +4469,7 @@ func (v *ChatBoostSourceGiftCode) MarshalJSON() ([]byte, error) {
// The boost was obtained by the creation of a Telegram Premium or a Telegram Star giveaway. This boosts the chat 4 times for the duration of the corresponding Telegram Premium subscription for Telegram Premium giveaways and prize_star_count / 500 times for one year for Telegram Star giveaways.
type ChatBoostSourceGiveaway struct {
// Source of the boost, always “giveaway”
Source ChatBoostSourceGiveawaySource `json:"source"`
Source ChatBoostSourceKind `json:"source"`
// Identifier of a message in the chat with the giveaway; the message could have been deleted already. May be 0 if the message isn't sent yet.
GiveawayMessageID int64 `json:"giveaway_message_id"`
// Optional. User that won the prize in the giveaway if any; for Telegram Premium giveaways only
@@ -4711,7 +4711,7 @@ func (*InputMediaVideo) isInputMedia() {}
// Represents an animation file (GIF or H.264/MPEG-4 AVC video without sound) to be sent.
type InputMediaAnimation struct {
// Type of the result, must be animation
Type string `json:"type"`
Type InputMediaType `json:"type"`
// File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet, or pass “attach://<file_attach_name>” to upload a new one using multipart/form-data under <file_attach_name> name. More information on Sending Files »
Media string `json:"media"`
// Optional. Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass “attach://<file_attach_name>” if the thumbnail was uploaded using multipart/form-data under <file_attach_name>. More information on Sending Files »
@@ -4753,7 +4753,7 @@ func (v *InputMediaAnimation) MarshalJSON() ([]byte, error) {
// Represents an audio file to be treated as music to be sent.
type InputMediaAudio struct {
// Type of the result, must be audio
Type string `json:"type"`
Type InputMediaType `json:"type"`
// File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet, or pass “attach://<file_attach_name>” to upload a new one using multipart/form-data under <file_attach_name> name. More information on Sending Files »
Media string `json:"media"`
// Optional. Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass “attach://<file_attach_name>” if the thumbnail was uploaded using multipart/form-data under <file_attach_name>. More information on Sending Files »
@@ -4791,7 +4791,7 @@ func (v *InputMediaAudio) MarshalJSON() ([]byte, error) {
// Represents a general file to be sent.
type InputMediaDocument struct {
// Type of the result, must be document
Type string `json:"type"`
Type InputMediaType `json:"type"`
// File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet, or pass “attach://<file_attach_name>” to upload a new one using multipart/form-data under <file_attach_name> name. More information on Sending Files »
Media string `json:"media"`
// Optional. Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass “attach://<file_attach_name>” if the thumbnail was uploaded using multipart/form-data under <file_attach_name>. More information on Sending Files »
@@ -4825,7 +4825,7 @@ func (v *InputMediaDocument) MarshalJSON() ([]byte, error) {
// Represents a live photo to be sent.
type InputMediaLivePhoto struct {
// Type of the result, must be live_photo
Type string `json:"type"`
Type InputMediaType `json:"type"`
// Video of the live photo to send. Pass a file_id to send a file that exists on the Telegram servers (recommended) or pass “attach://<file_attach_name>” to upload a new one using multipart/form-data under <file_attach_name> name. More information on Sending Files ». Sending live photos by a URL is currently unsupported.
Media string `json:"media"`
// The static photo to send. Pass a file_id to send a file that exists on the Telegram servers (recommended) or pass “attach://<file_attach_name>” to upload a new one using multipart/form-data under <file_attach_name> name. More information on Sending Files ». Sending live photos by a URL is currently unsupported.
@@ -4861,7 +4861,7 @@ func (v *InputMediaLivePhoto) MarshalJSON() ([]byte, error) {
// Represents a location to be sent.
type InputMediaLocation struct {
// Type of the result, must be location
Type string `json:"type"`
Type InputPollOptionMediaType `json:"type"`
// Latitude of the location
Latitude float64 `json:"latitude"`
// Longitude of the location
@@ -4889,7 +4889,7 @@ func (v *InputMediaLocation) MarshalJSON() ([]byte, error) {
// Represents a photo to be sent.
type InputMediaPhoto struct {
// Type of the result, must be photo
Type string `json:"type"`
Type InputMediaType `json:"type"`
// File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet, or pass “attach://<file_attach_name>” to upload a new one using multipart/form-data under <file_attach_name> name. More information on Sending Files »
Media string `json:"media"`
// Optional. Caption of the photo to be sent, 0-1024 characters after entities parsing
@@ -4923,7 +4923,7 @@ func (v *InputMediaPhoto) MarshalJSON() ([]byte, error) {
// Represents a sticker file to be sent.
type InputMediaSticker struct {
// Type of the result, must be sticker
Type string `json:"type"`
Type InputPollOptionMediaType `json:"type"`
// File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a .WEBP sticker from the Internet, or pass “attach://<file_attach_name>” to upload a new .WEBP, .TGS, or .WEBM sticker using multipart/form-data under <file_attach_name> name. More information on Sending Files »
Media string `json:"media"`
// Optional. Emoji associated with the sticker; only for just uploaded stickers
@@ -4949,7 +4949,7 @@ func (v *InputMediaSticker) MarshalJSON() ([]byte, error) {
// Represents a venue to be sent.
type InputMediaVenue struct {
// Type of the result, must be venue
Type string `json:"type"`
Type InputPollOptionMediaType `json:"type"`
// Latitude of the location
Latitude float64 `json:"latitude"`
// Longitude of the location
@@ -4987,7 +4987,7 @@ func (v *InputMediaVenue) MarshalJSON() ([]byte, error) {
// Represents a video to be sent.
type InputMediaVideo struct {
// Type of the result, must be video
Type string `json:"type"`
Type InputMediaType `json:"type"`
// File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet, or pass “attach://<file_attach_name>” to upload a new one using multipart/form-data under <file_attach_name> name. More information on Sending Files »
Media string `json:"media"`
// Optional. Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass “attach://<file_attach_name>” if the thumbnail was uploaded using multipart/form-data under <file_attach_name>. More information on Sending Files »
@@ -5053,7 +5053,7 @@ func (*InputPaidMediaVideo) isInputPaidMedia() {}
// The paid media to send is a live photo.
type InputPaidMediaLivePhoto struct {
// Type of the media, must be live_photo
Type string `json:"type"`
Type InputPaidMediaType `json:"type"`
// Video of the live photo to send. Pass a file_id to send a file that exists on the Telegram servers (recommended) or pass “attach://<file_attach_name>” to upload a new one using multipart/form-data under <file_attach_name> name. More information on Sending Files ». Sending live photos by a URL is currently unsupported.
Media string `json:"media"`
// The static photo to send. Pass a file_id to send a file that exists on the Telegram servers (recommended) or pass “attach://<file_attach_name>” to upload a new one using multipart/form-data under <file_attach_name> name. More information on Sending Files ». Sending live photos by a URL is currently unsupported.
@@ -5079,7 +5079,7 @@ func (v *InputPaidMediaLivePhoto) MarshalJSON() ([]byte, error) {
// The paid media to send is a photo.
type InputPaidMediaPhoto struct {
// Type of the media, must be photo
Type string `json:"type"`
Type InputPaidMediaType `json:"type"`
// File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet, or pass “attach://<file_attach_name>” to upload a new one using multipart/form-data under <file_attach_name> name. More information on Sending Files »
Media string `json:"media"`
}
@@ -5103,7 +5103,7 @@ func (v *InputPaidMediaPhoto) MarshalJSON() ([]byte, error) {
// The paid media to send is a video.
type InputPaidMediaVideo struct {
// Type of the media, must be video
Type string `json:"type"`
Type InputPaidMediaType `json:"type"`
// File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet, or pass “attach://<file_attach_name>” to upload a new one using multipart/form-data under <file_attach_name> name. More information on Sending Files »
Media string `json:"media"`
// Optional. Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass “attach://<file_attach_name>” if the thumbnail was uploaded using multipart/form-data under <file_attach_name>. More information on Sending Files »
@@ -5155,7 +5155,7 @@ func (*InputProfilePhotoAnimated) isInputProfilePhoto() {}
// A static profile photo in the .JPG format.
type InputProfilePhotoStatic struct {
// Type of the profile photo, must be static
Type string `json:"type"`
Type InputProfilePhotoType `json:"type"`
// The static profile photo. Profile photos can't be reused and can only be uploaded as a new file, so you can pass “attach://<file_attach_name>” if the photo was uploaded using multipart/form-data under <file_attach_name>. More information on Sending Files »
Photo string `json:"photo"`
}
@@ -5179,7 +5179,7 @@ func (v *InputProfilePhotoStatic) MarshalJSON() ([]byte, error) {
// An animated profile photo in the MPEG4 format.
type InputProfilePhotoAnimated struct {
// Type of the profile photo, must be animated
Type string `json:"type"`
Type InputProfilePhotoType `json:"type"`
// The animated profile photo. Profile photos can't be reused and can only be uploaded as a new file, so you can pass “attach://<file_attach_name>” if the photo was uploaded using multipart/form-data under <file_attach_name>. More information on Sending Files »
Animation string `json:"animation"`
// Optional. Timestamp in seconds of the frame that will be used as the static profile photo. Defaults to 0.0.
@@ -5219,7 +5219,7 @@ func (*InputStoryContentVideo) isInputStoryContent() {}
// Describes a photo to post as a story.
type InputStoryContentPhoto struct {
// Type of the content, must be photo
Type string `json:"type"`
Type InputStoryContentType `json:"type"`
// The photo to post as a story. The photo must be of the size 1080x1920 and must not exceed 10 MB. The photo can't be reused and can only be uploaded as a new file, so you can pass “attach://<file_attach_name>” if the photo was uploaded using multipart/form-data under <file_attach_name>. More information on Sending Files »
Photo string `json:"photo"`
}
@@ -5243,7 +5243,7 @@ func (v *InputStoryContentPhoto) MarshalJSON() ([]byte, error) {
// Describes a video to post as a story.
type InputStoryContentVideo struct {
// Type of the content, must be video
Type string `json:"type"`
Type InputStoryContentType `json:"type"`
// The video to post as a story. The video must be of the size 720x1280, streamable, encoded with H.265 codec, with key frames added each second in the MPEG4 format, and must not exceed 30 MB. The video can't be reused and can only be uploaded as a new file, so you can pass “attach://<file_attach_name>” if the video was uploaded using multipart/form-data under <file_attach_name>. More information on Sending Files »
Video string `json:"video"`
// Optional. Precise duration of the video in seconds; 0-60
@@ -5460,7 +5460,7 @@ func (*InlineQueryResultVoice) isInlineQueryResult() {}
// Represents a link to an article or web page.
type InlineQueryResultArticle struct {
// Type of the result, must be article
Type string `json:"type"`
Type InlineQueryResultType `json:"type"`
// Unique identifier for this result, 1-64 Bytes
ID string `json:"id"`
// Title of the result
@@ -5500,7 +5500,7 @@ func (v *InlineQueryResultArticle) MarshalJSON() ([]byte, error) {
// Represents a link to a photo. By default, this photo will be sent by the user with optional caption. Alternatively, you can use input_message_content to send a message with the specified content instead of the photo.
type InlineQueryResultPhoto struct {
// Type of the result, must be photo
Type string `json:"type"`
Type InlineQueryResultType `json:"type"`
// Unique identifier for this result, 1-64 bytes
ID string `json:"id"`
// A valid URL of the photo. Photo must be in JPEG format. Photo size must not exceed 5MB
@@ -5548,7 +5548,7 @@ func (v *InlineQueryResultPhoto) MarshalJSON() ([]byte, error) {
// Represents a link to an animated GIF file. By default, this animated GIF file will be sent by the user with optional caption. Alternatively, you can use input_message_content to send a message with the specified content instead of the animation.
type InlineQueryResultGif struct {
// Type of the result, must be gif
Type string `json:"type"`
Type InlineQueryResultType `json:"type"`
// Unique identifier for this result, 1-64 bytes
ID string `json:"id"`
// A valid URL for the GIF file
@@ -5598,7 +5598,7 @@ func (v *InlineQueryResultGif) MarshalJSON() ([]byte, error) {
// Represents a link to a video animation (H.264/MPEG-4 AVC video without sound). By default, this animated MPEG-4 file will be sent by the user with optional caption. Alternatively, you can use input_message_content to send a message with the specified content instead of the animation.
type InlineQueryResultMpeg4Gif struct {
// Type of the result, must be mpeg4_gif
Type string `json:"type"`
Type InlineQueryResultType `json:"type"`
// Unique identifier for this result, 1-64 bytes
ID string `json:"id"`
// A valid URL for the MPEG4 file
@@ -5649,7 +5649,7 @@ func (v *InlineQueryResultMpeg4Gif) MarshalJSON() ([]byte, error) {
// If an InlineQueryResultVideo message contains an embedded video (e.g., YouTube), you must replace its content using input_message_content.
type InlineQueryResultVideo struct {
// Type of the result, must be video
Type string `json:"type"`
Type InlineQueryResultType `json:"type"`
// Unique identifier for this result, 1-64 bytes
ID string `json:"id"`
// A valid URL for the embedded video player or video file
@@ -5701,7 +5701,7 @@ func (v *InlineQueryResultVideo) MarshalJSON() ([]byte, error) {
// Represents a link to an MP3 audio file. By default, this audio file will be sent by the user. Alternatively, you can use input_message_content to send a message with the specified content instead of the audio.
type InlineQueryResultAudio struct {
// Type of the result, must be audio
Type string `json:"type"`
Type InlineQueryResultType `json:"type"`
// Unique identifier for this result, 1-64 bytes
ID string `json:"id"`
// A valid URL for the audio file
@@ -5743,7 +5743,7 @@ func (v *InlineQueryResultAudio) MarshalJSON() ([]byte, error) {
// Represents a link to a voice recording in an .OGG container encoded with OPUS. By default, this voice recording will be sent by the user. Alternatively, you can use input_message_content to send a message with the specified content instead of the the voice message.
type InlineQueryResultVoice struct {
// Type of the result, must be voice
Type string `json:"type"`
Type InlineQueryResultType `json:"type"`
// Unique identifier for this result, 1-64 bytes
ID string `json:"id"`
// A valid URL for the voice recording
@@ -5783,7 +5783,7 @@ func (v *InlineQueryResultVoice) MarshalJSON() ([]byte, error) {
// Represents a link to a file. By default, this file will be sent by the user with an optional caption. Alternatively, you can use input_message_content to send a message with the specified content instead of the file. Currently, only .PDF and .ZIP files can be sent using this method.
type InlineQueryResultDocument struct {
// Type of the result, must be document
Type string `json:"type"`
Type InlineQueryResultType `json:"type"`
// Unique identifier for this result, 1-64 bytes
ID string `json:"id"`
// Title for the result
@@ -5831,7 +5831,7 @@ func (v *InlineQueryResultDocument) MarshalJSON() ([]byte, error) {
// Represents a location on a map. By default, the location will be sent by the user. Alternatively, you can use input_message_content to send a message with the specified content instead of the location.
type InlineQueryResultLocation struct {
// Type of the result, must be location
Type string `json:"type"`
Type InlineQueryResultType `json:"type"`
// Unique identifier for this result, 1-64 Bytes
ID string `json:"id"`
// Location latitude in degrees
@@ -5879,7 +5879,7 @@ func (v *InlineQueryResultLocation) MarshalJSON() ([]byte, error) {
// Represents a venue. By default, the venue will be sent by the user. Alternatively, you can use input_message_content to send a message with the specified content instead of the venue.
type InlineQueryResultVenue struct {
// Type of the result, must be venue
Type string `json:"type"`
Type InlineQueryResultType `json:"type"`
// Unique identifier for this result, 1-64 Bytes
ID string `json:"id"`
// Latitude of the venue location in degrees
@@ -5929,7 +5929,7 @@ func (v *InlineQueryResultVenue) MarshalJSON() ([]byte, error) {
// Represents a contact with a phone number. By default, this contact will be sent by the user. Alternatively, you can use input_message_content to send a message with the specified content instead of the contact.
type InlineQueryResultContact struct {
// Type of the result, must be contact
Type string `json:"type"`
Type InlineQueryResultType `json:"type"`
// Unique identifier for this result, 1-64 Bytes
ID string `json:"id"`
// Contact's phone number
@@ -5971,7 +5971,7 @@ func (v *InlineQueryResultContact) MarshalJSON() ([]byte, error) {
// Represents a Game.
type InlineQueryResultGame struct {
// Type of the result, must be game
Type string `json:"type"`
Type InlineQueryResultType `json:"type"`
// Unique identifier for this result, 1-64 bytes
ID string `json:"id"`
// Short name of the game
@@ -5999,7 +5999,7 @@ func (v *InlineQueryResultGame) MarshalJSON() ([]byte, error) {
// Represents a link to a photo stored on the Telegram servers. By default, this photo will be sent by the user with an optional caption. Alternatively, you can use input_message_content to send a message with the specified content instead of the photo.
type InlineQueryResultCachedPhoto struct {
// Type of the result, must be photo
Type string `json:"type"`
Type InlineQueryResultType `json:"type"`
// Unique identifier for this result, 1-64 bytes
ID string `json:"id"`
// A valid file identifier of the photo
@@ -6041,7 +6041,7 @@ func (v *InlineQueryResultCachedPhoto) MarshalJSON() ([]byte, error) {
// Represents a link to an animated GIF file stored on the Telegram servers. By default, this animated GIF file will be sent by the user with an optional caption. Alternatively, you can use input_message_content to send a message with specified content instead of the animation.
type InlineQueryResultCachedGif struct {
// Type of the result, must be gif
Type string `json:"type"`
Type InlineQueryResultType `json:"type"`
// Unique identifier for this result, 1-64 bytes
ID string `json:"id"`
// A valid file identifier for the GIF file
@@ -6081,7 +6081,7 @@ func (v *InlineQueryResultCachedGif) MarshalJSON() ([]byte, error) {
// Represents a link to a video animation (H.264/MPEG-4 AVC video without sound) stored on the Telegram servers. By default, this animated MPEG-4 file will be sent by the user with an optional caption. Alternatively, you can use input_message_content to send a message with the specified content instead of the animation.
type InlineQueryResultCachedMpeg4Gif struct {
// Type of the result, must be mpeg4_gif
Type string `json:"type"`
Type InlineQueryResultType `json:"type"`
// Unique identifier for this result, 1-64 bytes
ID string `json:"id"`
// A valid file identifier for the MPEG4 file
@@ -6121,7 +6121,7 @@ func (v *InlineQueryResultCachedMpeg4Gif) MarshalJSON() ([]byte, error) {
// Represents a link to a sticker stored on the Telegram servers. By default, this sticker will be sent by the user. Alternatively, you can use input_message_content to send a message with the specified content instead of the sticker.
type InlineQueryResultCachedSticker struct {
// Type of the result, must be sticker
Type string `json:"type"`
Type InlineQueryResultType `json:"type"`
// Unique identifier for this result, 1-64 bytes
ID string `json:"id"`
// A valid file identifier of the sticker
@@ -6151,7 +6151,7 @@ func (v *InlineQueryResultCachedSticker) MarshalJSON() ([]byte, error) {
// Represents a link to a file stored on the Telegram servers. By default, this file will be sent by the user with an optional caption. Alternatively, you can use input_message_content to send a message with the specified content instead of the file.
type InlineQueryResultCachedDocument struct {
// Type of the result, must be document
Type string `json:"type"`
Type InlineQueryResultType `json:"type"`
// Unique identifier for this result, 1-64 bytes
ID string `json:"id"`
// Title for the result
@@ -6191,7 +6191,7 @@ func (v *InlineQueryResultCachedDocument) MarshalJSON() ([]byte, error) {
// Represents a link to a video file stored on the Telegram servers. By default, this video file will be sent by the user with an optional caption. Alternatively, you can use input_message_content to send a message with the specified content instead of the video.
type InlineQueryResultCachedVideo struct {
// Type of the result, must be video
Type string `json:"type"`
Type InlineQueryResultType `json:"type"`
// Unique identifier for this result, 1-64 bytes
ID string `json:"id"`
// A valid file identifier for the video file
@@ -6233,7 +6233,7 @@ func (v *InlineQueryResultCachedVideo) MarshalJSON() ([]byte, error) {
// Represents a link to a voice message stored on the Telegram servers. By default, this voice message will be sent by the user. Alternatively, you can use input_message_content to send a message with the specified content instead of the voice message.
type InlineQueryResultCachedVoice struct {
// Type of the result, must be voice
Type string `json:"type"`
Type InlineQueryResultType `json:"type"`
// Unique identifier for this result, 1-64 bytes
ID string `json:"id"`
// A valid file identifier for the voice message
@@ -6271,7 +6271,7 @@ func (v *InlineQueryResultCachedVoice) MarshalJSON() ([]byte, error) {
// Represents a link to an MP3 audio file stored on the Telegram servers. By default, this audio file will be sent by the user. Alternatively, you can use input_message_content to send a message with the specified content instead of the audio.
type InlineQueryResultCachedAudio struct {
// Type of the result, must be audio
Type string `json:"type"`
Type InlineQueryResultType `json:"type"`
// Unique identifier for this result, 1-64 bytes
ID string `json:"id"`
// A valid file identifier for the audio file
@@ -6632,7 +6632,7 @@ func UnmarshalRevenueWithdrawalState(data []byte) (RevenueWithdrawalState, error
// The withdrawal is in progress.
type RevenueWithdrawalStatePending struct {
// Type of the state, always “pending”
Type RevenueWithdrawalStatePendingType `json:"type"`
Type RevenueWithdrawalStateKind `json:"type"`
}
// MarshalJSON encodes RevenueWithdrawalStatePending with the discriminator field
@@ -6654,7 +6654,7 @@ func (v *RevenueWithdrawalStatePending) MarshalJSON() ([]byte, error) {
// The withdrawal succeeded.
type RevenueWithdrawalStateSucceeded struct {
// Type of the state, always “succeeded”
Type RevenueWithdrawalStateSucceededType `json:"type"`
Type RevenueWithdrawalStateKind `json:"type"`
// Date the withdrawal was completed in Unix time
Date int64 `json:"date"`
// An HTTPS URL that can be used to see transaction details
@@ -6680,7 +6680,7 @@ func (v *RevenueWithdrawalStateSucceeded) MarshalJSON() ([]byte, error) {
// The withdrawal failed and the transaction was refunded.
type RevenueWithdrawalStateFailed struct {
// Type of the state, always “failed”
Type RevenueWithdrawalStateFailedType `json:"type"`
Type RevenueWithdrawalStateKind `json:"type"`
}
// MarshalJSON encodes RevenueWithdrawalStateFailed with the discriminator field
@@ -6780,7 +6780,7 @@ func UnmarshalTransactionPartner(data []byte) (TransactionPartner, error) {
// Describes a transaction with a user.
type TransactionPartnerUser struct {
// Type of the transaction partner, always “user”
Type MessageOriginUserType `json:"type"`
Type TransactionPartnerType `json:"type"`
// Type of the transaction, currently one of “invoice_payment” for payments via invoices, “paid_media_payment” for payments for paid media, “gift_purchase” for gifts sent by the bot, “premium_purchase” for Telegram Premium subscriptions gifted by the bot, “business_account_transfer” for direct transfers from managed business accounts
TransactionType TransactionPartnerUserTransactionType `json:"transaction_type"`
// Information about the user
@@ -6852,7 +6852,7 @@ func (m *TransactionPartnerUser) UnmarshalJSON(data []byte) error {
// Describes a transaction with a chat.
type TransactionPartnerChat struct {
// Type of the transaction partner, always “chat”
Type MessageOriginChatType `json:"type"`
Type TransactionPartnerType `json:"type"`
// Information about the chat
Chat Chat `json:"chat"`
// Optional. The gift sent to the chat by the bot
@@ -6878,7 +6878,7 @@ func (v *TransactionPartnerChat) MarshalJSON() ([]byte, error) {
// Describes the affiliate program that issued the affiliate commission received via this transaction.
type TransactionPartnerAffiliateProgram struct {
// Type of the transaction partner, always “affiliate_program”
Type TransactionPartnerAffiliateProgramType `json:"type"`
Type TransactionPartnerType `json:"type"`
// Optional. Information about the bot that sponsored the affiliate program
SponsorUser *User `json:"sponsor_user,omitempty"`
// The number of Telegram Stars received by the bot for each 1000 Telegram Stars received by the affiliate program sponsor from referred users
@@ -6904,7 +6904,7 @@ func (v *TransactionPartnerAffiliateProgram) MarshalJSON() ([]byte, error) {
// Describes a withdrawal transaction with Fragment.
type TransactionPartnerFragment struct {
// Type of the transaction partner, always “fragment”
Type TransactionPartnerFragmentType `json:"type"`
Type TransactionPartnerType `json:"type"`
// Optional. State of the transaction if the transaction is outgoing
WithdrawalState RevenueWithdrawalState `json:"withdrawal_state,omitempty"`
}
@@ -6952,7 +6952,7 @@ func (m *TransactionPartnerFragment) UnmarshalJSON(data []byte) error {
// Describes a withdrawal transaction to the Telegram Ads platform.
type TransactionPartnerTelegramAds struct {
// Type of the transaction partner, always “telegram_ads”
Type TransactionPartnerTelegramAdsType `json:"type"`
Type TransactionPartnerType `json:"type"`
}
// MarshalJSON encodes TransactionPartnerTelegramAds with the discriminator field
@@ -6974,7 +6974,7 @@ func (v *TransactionPartnerTelegramAds) MarshalJSON() ([]byte, error) {
// Describes a transaction with payment for paid broadcasting.
type TransactionPartnerTelegramApi struct {
// Type of the transaction partner, always “telegram_api”
Type TransactionPartnerTelegramApiType `json:"type"`
Type TransactionPartnerType `json:"type"`
// The number of successful requests that exceeded regular limits and were therefore billed
RequestCount int64 `json:"request_count"`
}
@@ -6998,7 +6998,7 @@ func (v *TransactionPartnerTelegramApi) MarshalJSON() ([]byte, error) {
// Describes a transaction with an unknown source or recipient.
type TransactionPartnerOther struct {
// Type of the transaction partner, always “other”
Type TransactionPartnerOtherType `json:"type"`
Type TransactionPartnerType `json:"type"`
}
// MarshalJSON encodes TransactionPartnerOther with the discriminator field
@@ -7172,7 +7172,7 @@ func (*PassportElementErrorUnspecified) isPassportElementError() {}
// Represents an issue in one of the data fields that was provided by the user. The error is considered resolved when the field's value changes.
type PassportElementErrorDataField struct {
// Error source, must be data
Source string `json:"source"`
Source PassportElementErrorSource `json:"source"`
// The section of the user's Telegram Passport which has the error, one of “personal_details”, “passport”, “driver_license”, “identity_card”, “internal_passport”, “address”
Type PassportElementErrorDataFieldType `json:"type"`
// Name of the data field which has the error
@@ -7202,7 +7202,7 @@ func (v *PassportElementErrorDataField) MarshalJSON() ([]byte, error) {
// Represents an issue with the front side of a document. The error is considered resolved when the file with the front side of the document changes.
type PassportElementErrorFrontSide struct {
// Error source, must be front_side
Source string `json:"source"`
Source PassportElementErrorSource `json:"source"`
// The section of the user's Telegram Passport which has the issue, one of “passport”, “driver_license”, “identity_card”, “internal_passport”
Type PassportElementErrorSelfieType `json:"type"`
// Base64-encoded hash of the file with the front side of the document
@@ -7230,7 +7230,7 @@ func (v *PassportElementErrorFrontSide) MarshalJSON() ([]byte, error) {
// Represents an issue with the reverse side of a document. The error is considered resolved when the file with reverse side of the document changes.
type PassportElementErrorReverseSide struct {
// Error source, must be reverse_side
Source string `json:"source"`
Source PassportElementErrorSource `json:"source"`
// The section of the user's Telegram Passport which has the issue, one of “driver_license”, “identity_card”
Type PassportElementErrorReverseSideType `json:"type"`
// Base64-encoded hash of the file with the reverse side of the document
@@ -7258,7 +7258,7 @@ func (v *PassportElementErrorReverseSide) MarshalJSON() ([]byte, error) {
// Represents an issue with the selfie with a document. The error is considered resolved when the file with the selfie changes.
type PassportElementErrorSelfie struct {
// Error source, must be selfie
Source string `json:"source"`
Source PassportElementErrorSource `json:"source"`
// The section of the user's Telegram Passport which has the issue, one of “passport”, “driver_license”, “identity_card”, “internal_passport”
Type PassportElementErrorSelfieType `json:"type"`
// Base64-encoded hash of the file with the selfie
@@ -7286,7 +7286,7 @@ func (v *PassportElementErrorSelfie) MarshalJSON() ([]byte, error) {
// Represents an issue with a document scan. The error is considered resolved when the file with the document scan changes.
type PassportElementErrorFile struct {
// Error source, must be file
Source string `json:"source"`
Source PassportElementErrorSource `json:"source"`
// The section of the user's Telegram Passport which has the issue, one of “utility_bill”, “bank_statement”, “rental_agreement”, “passport_registration”, “temporary_registration”
Type PassportElementErrorFileType `json:"type"`
// Base64-encoded file hash
@@ -7314,7 +7314,7 @@ func (v *PassportElementErrorFile) MarshalJSON() ([]byte, error) {
// Represents an issue with a list of scans. The error is considered resolved when the list of files containing the scans changes.
type PassportElementErrorFiles struct {
// Error source, must be files
Source string `json:"source"`
Source PassportElementErrorSource `json:"source"`
// The section of the user's Telegram Passport which has the issue, one of “utility_bill”, “bank_statement”, “rental_agreement”, “passport_registration”, “temporary_registration”
Type PassportElementErrorFileType `json:"type"`
// List of base64-encoded file hashes
@@ -7342,7 +7342,7 @@ func (v *PassportElementErrorFiles) MarshalJSON() ([]byte, error) {
// Represents an issue with one of the files that constitute the translation of a document. The error is considered resolved when the file changes.
type PassportElementErrorTranslationFile struct {
// Error source, must be translation_file
Source string `json:"source"`
Source PassportElementErrorSource `json:"source"`
// Type of element of the user's Telegram Passport which has the issue, one of “passport”, “driver_license”, “identity_card”, “internal_passport”, “utility_bill”, “bank_statement”, “rental_agreement”, “passport_registration”, “temporary_registration”
Type PassportElementErrorTranslationFileType `json:"type"`
// Base64-encoded file hash
@@ -7370,7 +7370,7 @@ func (v *PassportElementErrorTranslationFile) MarshalJSON() ([]byte, error) {
// Represents an issue with the translated version of a document. The error is considered resolved when a file with the document translation change.
type PassportElementErrorTranslationFiles struct {
// Error source, must be translation_files
Source string `json:"source"`
Source PassportElementErrorSource `json:"source"`
// Type of element of the user's Telegram Passport which has the issue, one of “passport”, “driver_license”, “identity_card”, “internal_passport”, “utility_bill”, “bank_statement”, “rental_agreement”, “passport_registration”, “temporary_registration”
Type PassportElementErrorTranslationFileType `json:"type"`
// List of base64-encoded file hashes
@@ -7398,7 +7398,7 @@ func (v *PassportElementErrorTranslationFiles) MarshalJSON() ([]byte, error) {
// Represents an issue in an unspecified place. The error is considered resolved when new data is added.
type PassportElementErrorUnspecified struct {
// Error source, must be unspecified
Source string `json:"source"`
Source PassportElementErrorSource `json:"source"`
// Type of element of the user's Telegram Passport which has the issue
Type string `json:"type"`
// Base64-encoded element hash
+182
View File
@@ -0,0 +1,182 @@
package api
import (
"reflect"
"testing"
json "github.com/goccy/go-json"
"github.com/stretchr/testify/require"
)
// TestUnifiedEnum_ChatMemberStatus_HasAllConstants asserts the unified
// enum exists with the full set of variant values and is a typed string.
func TestUnifiedEnum_ChatMemberStatus_HasAllConstants(t *testing.T) {
require.IsType(t, ChatMemberStatus(""), ChatMemberStatusCreator)
values := []ChatMemberStatus{
ChatMemberStatusCreator,
ChatMemberStatusAdministrator,
ChatMemberStatusMember,
ChatMemberStatusRestricted,
ChatMemberStatusLeft,
ChatMemberStatusKicked,
}
wantWire := []string{"creator", "administrator", "member", "restricted", "left", "kicked"}
require.Len(t, values, 6)
for i, v := range values {
require.Equal(t, wantWire[i], string(v))
}
}
// TestUnifiedEnum_ChatMember_VariantFieldsRetyped confirms every concrete
// variant's discriminator field is the unified enum, NOT a per-variant
// alias type. Reflection walks the struct field directly.
func TestUnifiedEnum_ChatMember_VariantFieldsRetyped(t *testing.T) {
cases := []struct {
name string
val any
}{
{"ChatMemberOwner", &ChatMemberOwner{}},
{"ChatMemberAdministrator", &ChatMemberAdministrator{}},
{"ChatMemberMember", &ChatMemberMember{}},
{"ChatMemberRestricted", &ChatMemberRestricted{}},
{"ChatMemberLeft", &ChatMemberLeft{}},
{"ChatMemberBanned", &ChatMemberBanned{}},
}
wantType := reflect.TypeOf(ChatMemberStatus(""))
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
f, ok := reflect.TypeOf(tc.val).Elem().FieldByName("Status")
require.True(t, ok, "%s missing Status field", tc.name)
require.Equal(t, wantType, f.Type, "%s.Status type mismatch", tc.name)
})
}
}
// TestUnifiedEnum_ChatMember_DirectComparison verifies the unified enum
// lets callers compare a variant's Status directly against constants
// without conversion.
func TestUnifiedEnum_ChatMember_DirectComparison(t *testing.T) {
owner := &ChatMemberOwner{Status: ChatMemberStatusCreator}
require.True(t, owner.Status == ChatMemberStatusCreator)
require.False(t, owner.Status == ChatMemberStatusKicked)
banned := &ChatMemberBanned{Status: ChatMemberStatusKicked}
require.True(t, banned.Status == ChatMemberStatusKicked)
}
// TestUnifiedEnum_ChatMember_MarshalDiscriminator verifies the auto-inject
// MarshalJSON still emits the right wire discriminator after the enum
// retype — no regression from commit 370c9c0.
func TestUnifiedEnum_ChatMember_MarshalDiscriminator(t *testing.T) {
cases := []struct {
val any
wantWire string
}{
{&ChatMemberOwner{User: User{ID: 1, FirstName: "a"}}, "creator"},
{&ChatMemberAdministrator{User: User{ID: 2, FirstName: "b"}}, "administrator"},
{&ChatMemberMember{User: User{ID: 3, FirstName: "c"}}, "member"},
{&ChatMemberRestricted{User: User{ID: 4, FirstName: "d"}}, "restricted"},
{&ChatMemberLeft{User: User{ID: 5, FirstName: "e"}}, "left"},
{&ChatMemberBanned{User: User{ID: 6, FirstName: "f"}}, "kicked"},
}
for _, tc := range cases {
raw, err := json.Marshal(tc.val)
require.NoError(t, err)
var probe struct {
Status string `json:"status"`
}
require.NoError(t, json.Unmarshal(raw, &probe))
require.Equal(t, tc.wantWire, probe.Status)
}
}
// TestUnifiedEnum_ChatMember_RoundTrip confirms a marshal-unmarshal cycle
// preserves the unified-enum field value.
func TestUnifiedEnum_ChatMember_RoundTrip(t *testing.T) {
orig := &ChatMemberOwner{User: User{ID: 99, FirstName: "owner"}}
raw, err := json.Marshal(orig)
require.NoError(t, err)
out, err := UnmarshalChatMember(raw)
require.NoError(t, err)
round, ok := out.(*ChatMemberOwner)
require.True(t, ok, "expected *ChatMemberOwner, got %T", out)
require.Equal(t, ChatMemberStatusCreator, round.Status)
require.Equal(t, orig.User.ID, round.User.ID)
}
// TestUnifiedEnum_MessageOriginType verifies a second union also unifies
// correctly — guards against a one-off implementation that only handles
// ChatMember.
func TestUnifiedEnum_MessageOriginType(t *testing.T) {
require.IsType(t, MessageOriginType(""), MessageOriginTypeUser)
values := []MessageOriginType{
MessageOriginTypeUser,
MessageOriginTypeHiddenUser,
MessageOriginTypeChat,
MessageOriginTypeChannel,
}
wantWire := []string{"user", "hidden_user", "chat", "channel"}
for i, v := range values {
require.Equal(t, wantWire[i], string(v))
}
// Variant fields use the unified type.
wantType := reflect.TypeOf(MessageOriginType(""))
for _, name := range []string{"MessageOriginUser", "MessageOriginHiddenUser", "MessageOriginChat", "MessageOriginChannel"} {
switch name {
case "MessageOriginUser":
f, ok := reflect.TypeOf(&MessageOriginUser{}).Elem().FieldByName("Type")
require.True(t, ok)
require.Equal(t, wantType, f.Type)
case "MessageOriginHiddenUser":
f, ok := reflect.TypeOf(&MessageOriginHiddenUser{}).Elem().FieldByName("Type")
require.True(t, ok)
require.Equal(t, wantType, f.Type)
case "MessageOriginChat":
f, ok := reflect.TypeOf(&MessageOriginChat{}).Elem().FieldByName("Type")
require.True(t, ok)
require.Equal(t, wantType, f.Type)
case "MessageOriginChannel":
f, ok := reflect.TypeOf(&MessageOriginChannel{}).Elem().FieldByName("Type")
require.True(t, ok)
require.Equal(t, wantType, f.Type)
}
}
}
// TestUnifiedEnum_StutterSuffix_Kind covers the naming-collision rule:
// when the union name ends in a discriminator concept noun, the unified
// enum is suffixed with "Kind" to avoid stuttery names like
// "BackgroundTypeType".
func TestUnifiedEnum_StutterSuffix_Kind(t *testing.T) {
require.IsType(t, BackgroundTypeKind(""), BackgroundTypeKindFill)
require.IsType(t, ReactionTypeKind(""), ReactionTypeKindEmoji)
require.IsType(t, StoryAreaTypeKind(""), StoryAreaTypeKindLocation)
require.IsType(t, ChatBoostSourceKind(""), ChatBoostSourceKindPremium)
require.IsType(t, RevenueWithdrawalStateKind(""), RevenueWithdrawalStateKindPending)
// Variant struct field types match the unified enum.
wantType := reflect.TypeOf(BackgroundTypeKind(""))
f, ok := reflect.TypeOf(&BackgroundTypeFill{}).Elem().FieldByName("Type")
require.True(t, ok)
require.Equal(t, wantType, f.Type)
}
// TestUnifiedEnum_PerVariantTypesNotEmitted asserts the obsolete
// per-variant single-value enum types (e.g. ChatMemberOwnerStatus) are
// gone — ensures the codegen doesn't double-emit. We rely on compile-time
// behaviour: if any of these names existed, a referencing package would
// fail to build. Instead we verify the variant struct field type's name
// is the unified one.
func TestUnifiedEnum_PerVariantTypesNotEmitted(t *testing.T) {
got := reflect.TypeOf(&ChatMemberOwner{}).Elem()
statusField, ok := got.FieldByName("Status")
require.True(t, ok)
require.Equal(t, "ChatMemberStatus", statusField.Type.Name(),
"ChatMemberOwner.Status should be ChatMemberStatus, not ChatMemberOwnerStatus")
}
+187 -19
View File
@@ -8,8 +8,51 @@ import (
"io"
"net/http"
"reflect"
"sync"
)
var (
headerJSONValue = []string{"application/json"}
rawOKTrueBody = []byte(`{"ok":true,"result":true}`)
rawOKFalseBody = []byte(`{"ok":true,"result":false}`)
// respBufPool reuses *bytes.Buffer for response body reads. Used on
// paths whose decoder copies strings out of the input (decodeResult,
// which delegates to goccy/go-json), so the buffer can be returned to
// the pool as soon as Unmarshal has run. CallRaw and callMultipartRaw
// return slices that alias the buffer and therefore cannot use the
// pool without an extra copy that would defeat the point.
respBufPool = sync.Pool{New: func() any { return new(bytes.Buffer) }}
// reqBufPool reuses *bytes.Buffer for request body marshalling on the
// JSON path. Only used when the configured Codec satisfies BodyEncoder
// so we can stream-encode into the buffer instead of allocating an
// intermediate []byte. The buffer is safe to return to the pool once
// http.Client.Do (or RetryDoer, which io.ReadAlls the body up front)
// has consumed it.
reqBufPool = sync.Pool{New: func() any { return new(bytes.Buffer) }}
)
// maxPooledBufCap caps the buffer size returned to either pool. Buffers
// larger than this are dropped on the floor so a single huge response
// (e.g. a large getFile metadata payload) doesn't bloat the pool for the
// rest of the process lifetime.
const maxPooledBufCap = 64 * 1024
func putRespBuf(buf *bytes.Buffer) {
if buf.Cap() > maxPooledBufCap {
return
}
respBufPool.Put(buf)
}
func putReqBuf(buf *bytes.Buffer) {
if buf.Cap() > maxPooledBufCap {
return
}
reqBufPool.Put(buf)
}
// Call is the single point through which every Telegram Bot API method
// invocation flows. It marshals the request, signs the URL with the bot
// token, dispatches via HTTPDoer, decodes the Result envelope, and
@@ -34,18 +77,18 @@ func Call[Req any, Resp any](ctx context.Context, b *Bot, method string, req Req
}
}
body, err := encodeJSONBody(b.codec, req)
body, pooledReqBuf, err := encodeJSONBody(b.codec, req)
if err != nil {
return zero, err
}
if pooledReqBuf != nil {
defer putReqBuf(pooledReqBuf)
}
url := b.base + "/bot" + b.token + "/" + method
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
httpReq, err := b.buildRequest(ctx, method, body)
if err != nil {
return zero, &NetworkError{Err: err}
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Accept", "application/json")
resp, err := b.http.Do(httpReq)
if err != nil {
@@ -57,12 +100,14 @@ func Call[Req any, Resp any](ctx context.Context, b *Bot, method string, req Req
}
defer func() { _ = resp.Body.Close() }()
raw, err := io.ReadAll(resp.Body)
if err != nil {
buf := respBufPool.Get().(*bytes.Buffer)
buf.Reset()
defer putRespBuf(buf)
if _, err := buf.ReadFrom(resp.Body); err != nil {
return zero, &NetworkError{Err: err}
}
return decodeResult[Resp](b.codec, raw)
return decodeResult[Resp](b.codec, buf.Bytes())
}
// CallRaw is like Call but returns the raw JSON of the result field
@@ -81,18 +126,18 @@ func CallRaw[Req any](ctx context.Context, b *Bot, method string, req Req) (json
}
}
body, err := encodeJSONBody(b.codec, req)
body, pooledReqBuf, err := encodeJSONBody(b.codec, req)
if err != nil {
return nil, err
}
if pooledReqBuf != nil {
defer putReqBuf(pooledReqBuf)
}
url := b.base + "/bot" + b.token + "/" + method
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
httpReq, err := b.buildRequest(ctx, method, body)
if err != nil {
return nil, &NetworkError{Err: err}
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Accept", "application/json")
resp, err := b.http.Do(httpReq)
if err != nil {
@@ -124,23 +169,146 @@ func decodeResultRaw(codec Codec, raw []byte) (json.RawMessage, error) {
return env.Result, nil
}
// encodeJSONBody marshals req to a JSON body. A nil interface or nil
// pointer req yields "{}" so Telegram receives a valid empty object.
func encodeJSONBody(codec Codec, req any) (io.Reader, error) {
// buildRequest constructs the *http.Request for an API call. When the bot
// has a cached parsed base URL (the common path), the request is built
// manually so that net/url.Parse and net/http.NewRequestWithContext's
// internal book-keeping are skipped — saving allocations on every call.
//
// ContentLength and GetBody are populated from the body's concrete type
// in bodyToReadCloser so RetryDoer can replay the body across attempts.
func (b *Bot) buildRequest(ctx context.Context, method string, body io.Reader) (*http.Request, error) {
if b.baseURL == nil {
// Slow path: WithBaseURL configured an unparsable URL (or New ran
// before pre-parse for some reason). Fall back to the stdlib
// constructor so we still produce a valid request.
url := b.base + b.pathPrefix + method
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
if err != nil {
return nil, err
}
req.Header["Content-Type"] = headerJSONValue
req.Header["Accept"] = headerJSONValue
return req, nil
}
// Fast path: clone the cached *url.URL by value, set the per-method
// path. Constructing &http.Request{} directly avoids the Header,
// URL-parse, and ContentLength bookkeeping that NewRequestWithContext
// runs unconditionally.
u := *b.baseURL
u.Path = b.pathPrefix + method
u.RawPath = ""
rc, contentLength, getBody := bodyToReadCloser(body)
req := &http.Request{
Method: http.MethodPost,
URL: &u,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: http.Header{"Content-Type": headerJSONValue, "Accept": headerJSONValue},
Body: rc,
GetBody: getBody,
ContentLength: contentLength,
Host: u.Host,
}
return req.WithContext(ctx), nil
}
// bufferReadCloser exposes a *bytes.Buffer as io.ReadCloser without going
// through io.NopCloser. Keeping the concrete *bytes.Buffer accessible lets
// alternative HTTPDoers (e.g. FastHTTPDoer) type-assert and pass the
// underlying bytes through to their native body-set APIs without copying.
type bufferReadCloser struct {
*bytes.Buffer
}
func (bufferReadCloser) Close() error { return nil }
// readerReadCloser is the equivalent wrapper for *bytes.Reader (used by
// the Marshal fallback path when the codec doesn't implement BodyEncoder).
type readerReadCloser struct {
*bytes.Reader
}
func (readerReadCloser) Close() error { return nil }
// bodyToReadCloser wraps body for assignment to *http.Request.Body. The
// type switch covers the body shapes encodeJSONBody returns: a pooled
// *bytes.Buffer (BodyEncoder path or {} fast path) or a *bytes.Reader
// (Marshal fallback for codecs that don't implement BodyEncoder). Both
// cases populate ContentLength and GetBody so RetryDoer can replay the
// body across retry attempts without buffering it again.
func bodyToReadCloser(body io.Reader) (io.ReadCloser, int64, func() (io.ReadCloser, error)) {
switch v := body.(type) {
case *bytes.Buffer:
buf := v.Bytes()
length := int64(len(buf))
return bufferReadCloser{v}, length, func() (io.ReadCloser, error) {
return readerReadCloser{bytes.NewReader(buf)}, nil
}
case *bytes.Reader:
length := int64(v.Len())
// Snapshot the reader's current data so GetBody returns a fresh one.
snapshot := *v
return readerReadCloser{v}, length, func() (io.ReadCloser, error) {
s := snapshot
return readerReadCloser{&s}, nil
}
default:
// Unknown reader: no length, no replay. Should not happen with the
// current encodeJSONBody body shapes but kept for forward safety.
return io.NopCloser(body), -1, nil
}
}
// encodeJSONBody marshals req into a JSON body. It returns the body
// reader plus, when the codec satisfies BodyEncoder, the pooled buffer
// that backs it — callers MUST return that buffer to the pool via
// putReqBuf once the request is done. The buffer return is exposed
// directly (instead of a closure) so encodeJSONBody allocates nothing
// on the pooled path beyond the codec's own internal allocations.
//
// The {} fast path used for nil/nil-pointer requests bypasses the pool
// entirely; the 2-byte literal isn't worth the contention overhead.
func encodeJSONBody(codec Codec, req any) (io.Reader, *bytes.Buffer, error) {
if req == nil || isNilPointer(req) {
return bytes.NewBufferString("{}"), nil
return bytes.NewBufferString("{}"), nil, nil
}
if enc, ok := codec.(BodyEncoder); ok {
buf := reqBufPool.Get().(*bytes.Buffer)
buf.Reset()
if err := enc.MarshalTo(buf, req); err != nil {
putReqBuf(buf)
return nil, nil, &ParseError{Err: err}
}
return buf, buf, nil
}
data, err := codec.Marshal(req)
if err != nil {
return nil, &ParseError{Err: err}
return nil, nil, &ParseError{Err: err}
}
return bytes.NewReader(data), nil
return bytes.NewReader(data), nil, nil
}
// decodeResult unmarshals raw into Result[Resp] and translates non-OK
// responses into *APIError.
//
// Bool fast path: ~60% of Telegram methods return bool. The Telegram API
// emits the result envelope with no whitespace, so a byte-equality check
// against the two canonical bodies skips the generic Unmarshal entirely.
// Anything that doesn't match exactly (e.g. responses with extra fields,
// errors) falls through to the slow path.
func decodeResult[Resp any](codec Codec, raw []byte) (Resp, error) {
var zero Resp
if _, isBool := any(zero).(bool); isBool {
switch {
case bytes.Equal(raw, rawOKTrueBody):
return any(true).(Resp), nil
case bytes.Equal(raw, rawOKFalseBody):
return any(false).(Resp), nil
}
}
var env Result[Resp]
if err := codec.Unmarshal(raw, &env); err != nil {
return zero, &ParseError{Err: err, Body: copyBody(raw)}
+97
View File
@@ -0,0 +1,97 @@
package client
import (
"bytes"
"context"
"io"
"net/http"
"testing"
)
// stubDoer returns the same canned response body for every request. It
// is intentionally minimal — testify mock has its own overhead that
// would dominate the per-call cost we want to measure.
type stubDoer struct{ body []byte }
func (s *stubDoer) Do(_ *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewReader(s.body)),
Header: http.Header{},
}, nil
}
type benchSendReq struct {
ChatID int64 `json:"chat_id"`
Text string `json:"text"`
}
type benchMsgResp struct {
MessageID int64 `json:"message_id"`
Date int64 `json:"date"`
Text string `json:"text"`
}
func BenchmarkCall_BoolResponse(b *testing.B) {
d := &stubDoer{body: []byte(`{"ok":true,"result":true}`)}
bot := New("123:abc", WithHTTPClient(d))
ctx := context.Background()
req := &benchSendReq{ChatID: 42, Text: "hi"}
b.ReportAllocs()
for b.Loop() {
if _, err := Call[*benchSendReq, bool](ctx, bot, "setMyCommands", req); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkCall_StructResponse(b *testing.B) {
d := &stubDoer{body: []byte(`{"ok":true,"result":{"message_id":1,"date":0,"text":"ok"}}`)}
bot := New("123:abc", WithHTTPClient(d))
ctx := context.Background()
req := &benchSendReq{ChatID: 42, Text: "hi"}
b.ReportAllocs()
for b.Loop() {
if _, err := Call[*benchSendReq, benchMsgResp](ctx, bot, "sendMessage", req); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkEncodeJSONBody(b *testing.B) {
codec := DefaultCodec{}
req := &benchSendReq{ChatID: 42, Text: "hello, world"}
b.ReportAllocs()
for b.Loop() {
r, pooled, err := encodeJSONBody(codec, req)
if err != nil {
b.Fatal(err)
}
_ = r
if pooled != nil {
putReqBuf(pooled)
}
}
}
func BenchmarkDecodeResult_Bool(b *testing.B) {
codec := DefaultCodec{}
raw := []byte(`{"ok":true,"result":true}`)
b.ReportAllocs()
for b.Loop() {
if _, err := decodeResult[bool](codec, raw); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkDecodeResult_Struct(b *testing.B) {
codec := DefaultCodec{}
raw := []byte(`{"ok":true,"result":{"message_id":1,"date":0,"text":"ok"}}`)
b.ReportAllocs()
for b.Loop() {
if _, err := decodeResult[benchMsgResp](codec, raw); err != nil {
b.Fatal(err)
}
}
}
+19
View File
@@ -1,5 +1,9 @@
package client
import (
"net/url"
)
const defaultBaseURL = "https://api.telegram.org"
// Bot is the Telegram Bot API client. Construct via New. All API methods
@@ -10,6 +14,13 @@ type Bot struct {
http HTTPDoer
codec Codec
logger Logger
// baseURL is the parsed form of base, lazily populated on first Call.
// Caching it avoids running url.Parse on every API request.
baseURL *url.URL
// pathPrefix is "/bot<token>/" built once so per-call URL assembly
// is a single string concatenation with the method name.
pathPrefix string
}
// Token returns the bot token. Exposed for advanced use cases (custom
@@ -44,5 +55,13 @@ func New(token string, opts ...Option) *Bot {
for _, o := range opts {
o(b)
}
// Pre-compute URL pieces. Errors here are unlikely (defaultBaseURL is
// well-formed; user-supplied bases via WithBaseURL are validated by
// url.Parse below) but if parsing fails we leave baseURL nil and fall
// back to the string-concat path on the next Call.
if u, err := url.Parse(b.base); err == nil {
b.baseURL = u
}
b.pathPrefix = "/bot" + b.token + "/"
return b
}
+21 -1
View File
@@ -1,7 +1,11 @@
// Package client provides HTTP client primitives for the Telegram Bot API.
package client
import "github.com/goccy/go-json"
import (
"io"
"github.com/goccy/go-json"
)
// Codec encodes/decodes JSON payloads exchanged with the Telegram Bot API.
// The default implementation wraps goccy/go-json. Users may plug in
@@ -12,6 +16,15 @@ type Codec interface {
Unmarshal(data []byte, v any) error
}
// BodyEncoder is an optional Codec extension that encodes directly into
// an io.Writer, skipping the intermediate []byte that Marshal returns.
// Call uses this when present to avoid allocating the marshal result and
// the bytes.Reader that wraps it; codecs without it fall through to
// Marshal + bytes.NewReader.
type BodyEncoder interface {
MarshalTo(w io.Writer, v any) error
}
// DefaultCodec wraps goccy/go-json. It is the zero-value safe default.
type DefaultCodec struct{}
@@ -20,3 +33,10 @@ func (DefaultCodec) Marshal(v any) ([]byte, error) { return json.Marshal(v) }
// Unmarshal calls json.Unmarshal.
func (DefaultCodec) Unmarshal(data []byte, v any) error { return json.Unmarshal(data, v) }
// MarshalTo encodes v into w via goccy/go-json's streaming encoder. The
// trailing newline that Encoder appends is valid JSON whitespace and is
// accepted by Telegram's parser.
func (DefaultCodec) MarshalTo(w io.Writer, v any) error {
return json.NewEncoder(w).Encode(v)
}
+231
View File
@@ -0,0 +1,231 @@
package client
import (
"context"
"errors"
"io"
"net/http"
"strconv"
"time"
"github.com/valyala/fasthttp"
)
// FastHTTPDoer is an HTTPDoer backed by github.com/valyala/fasthttp. It
// trades net/http compatibility (and HTTP/2 support) for substantially
// fewer allocations per request — fasthttp pools its Request and Response
// objects and uses a zero-allocation HTTP/1.1 parser.
//
// Use it for high-throughput bots when GC pressure matters and you don't
// need HTTP/2 or any net/http-only middleware (RoundTripper composition,
// the OpenTelemetry httptrace family, etc.):
//
// bot := client.New(token, client.WithHTTPClient(client.NewFastHTTPDoer()))
//
// Wrap with RetryDoer the same way you would the default doer.
type FastHTTPDoer struct {
client *fasthttp.Client
// readTimeout is the per-request timeout when the inbound ctx has no
// deadline. Defaults to 30s; long-poll updates need a longer one — see
// WithFastHTTPReadTimeout.
readTimeout time.Duration
}
// FastHTTPDoerOption configures a FastHTTPDoer.
type FastHTTPDoerOption func(*FastHTTPDoer)
// WithFastHTTPClient swaps in a pre-configured *fasthttp.Client.
// Useful for sharing a connection pool across multiple bots or applying
// custom dial / TLS configuration.
func WithFastHTTPClient(c *fasthttp.Client) FastHTTPDoerOption {
return func(d *FastHTTPDoer) { d.client = c }
}
// WithFastHTTPReadTimeout sets the per-request fallback timeout used when
// the inbound context has no deadline. Long-poll callers should pass a
// value larger than the long-poll timeout.
func WithFastHTTPReadTimeout(t time.Duration) FastHTTPDoerOption {
return func(d *FastHTTPDoer) { d.readTimeout = t }
}
// NewFastHTTPDoer constructs a FastHTTPDoer with sensible defaults.
func NewFastHTTPDoer(opts ...FastHTTPDoerOption) *FastHTTPDoer {
d := &FastHTTPDoer{
client: &fasthttp.Client{
ReadTimeout: 90 * time.Second,
WriteTimeout: 30 * time.Second,
MaxIdleConnDuration: 90 * time.Second,
},
readTimeout: 30 * time.Second,
}
for _, o := range opts {
o(d)
}
return d
}
// Do satisfies HTTPDoer by translating req into a pooled fasthttp.Request,
// dispatching it, and returning a *http.Response whose Body releases the
// pooled fasthttp.Response when Close is called.
//
// The conversion is intentionally minimal: URL goes via req.URL.RequestURI()
// + Host (avoids re-parsing), header values move byte-for-byte, and the
// body is taken straight from req.Body. *bytes.Buffer / *bytes.Reader are
// recognised so we can pass the underlying bytes without io.ReadAll.
func (d *FastHTTPDoer) Do(req *http.Request) (*http.Response, error) {
if req == nil {
return nil, errors.New("client: nil http.Request")
}
fReq := fasthttp.AcquireRequest()
defer fasthttp.ReleaseRequest(fReq)
fReq.SetRequestURI(req.URL.String())
fReq.Header.SetMethod(req.Method)
if req.Host != "" {
fReq.Header.SetHost(req.Host)
}
for name, values := range req.Header {
for _, v := range values {
fReq.Header.Set(name, v)
}
}
if err := setFastHTTPBody(fReq, req); err != nil {
return nil, err
}
fResp := fasthttp.AcquireResponse()
// fResp is released by fasthttpResponseBody.Close — caller is
// expected to defer resp.Body.Close() per net/http contract.
deadline, hasDeadline := req.Context().Deadline()
var err error
if hasDeadline {
err = d.client.DoDeadline(fReq, fResp, deadline)
} else {
err = d.client.DoTimeout(fReq, fResp, d.readTimeout)
}
if err != nil {
fasthttp.ReleaseResponse(fResp)
// Map fasthttp's timeout error to ctx.Err semantics so callers
// can errors.Is(err, context.DeadlineExceeded).
if hasDeadline && errors.Is(err, fasthttp.ErrTimeout) {
return nil, context.DeadlineExceeded
}
return nil, err
}
httpResp := &http.Response{
StatusCode: fResp.StatusCode(),
Status: strconv.Itoa(fResp.StatusCode()) + " " + fastHTTPStatusText(fResp.StatusCode()),
Header: make(http.Header, fResp.Header.Len()),
ContentLength: int64(fResp.Header.ContentLength()),
Body: &fasthttpResponseBody{resp: fResp, body: fResp.Body()},
Request: req,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
}
for k, v := range fResp.Header.All() {
httpResp.Header.Add(string(k), string(v))
}
return httpResp, nil
}
// setFastHTTPBody copies req.Body into fReq with the cheapest path that
// preserves correctness. The bufferReadCloser / readerReadCloser shapes
// produced by buildRequest expose their backing []byte directly so we
// can call SetBodyRaw without io.ReadAll. Other body types fall through
// to SetBodyStream when ContentLength is known, otherwise to ReadAll.
func setFastHTTPBody(fReq *fasthttp.Request, req *http.Request) error {
if req.Body == nil {
return nil
}
switch v := req.Body.(type) {
case bufferReadCloser:
fReq.SetBodyRaw(v.Bytes())
return nil
case readerReadCloser:
// *bytes.Reader.Bytes() returns the unread portion.
size := v.Len()
buf := make([]byte, size)
_, err := v.Read(buf)
if err != nil && !errors.Is(err, io.EOF) {
return err
}
fReq.SetBodyRaw(buf)
return nil
default:
if req.ContentLength > 0 {
fReq.SetBodyStream(v, int(req.ContentLength))
} else {
body, err := io.ReadAll(v)
if err != nil {
return err
}
fReq.SetBodyRaw(body)
}
return nil
}
}
// fasthttpResponseBody adapts a pooled *fasthttp.Response so it satisfies
// io.ReadCloser. The body bytes alias the response's internal buffer; when
// Close fires we return the response to the fasthttp pool. Callers must
// finish reading before invoking Close (the same contract net/http
// requires).
type fasthttpResponseBody struct {
resp *fasthttp.Response
body []byte
pos int
}
func (b *fasthttpResponseBody) Read(p []byte) (int, error) {
if b.pos >= len(b.body) {
return 0, io.EOF
}
n := copy(p, b.body[b.pos:])
b.pos += n
return n, nil
}
func (b *fasthttpResponseBody) Close() error {
if b.resp != nil {
fasthttp.ReleaseResponse(b.resp)
b.resp = nil
b.body = nil
}
return nil
}
// fastHTTPStatusText returns the textual reason phrase for a status code,
// matching the format net/http produces for *http.Response.Status. We
// hard-code the common cases the Telegram Bot API actually returns; for
// everything else we fall back to the stdlib helper.
func fastHTTPStatusText(code int) string {
switch code {
case http.StatusOK:
return "OK"
case http.StatusBadRequest:
return "Bad Request"
case http.StatusUnauthorized:
return "Unauthorized"
case http.StatusForbidden:
return "Forbidden"
case http.StatusNotFound:
return "Not Found"
case http.StatusTooManyRequests:
return "Too Many Requests"
case http.StatusInternalServerError:
return "Internal Server Error"
case http.StatusBadGateway:
return "Bad Gateway"
case http.StatusServiceUnavailable:
return "Service Unavailable"
case http.StatusGatewayTimeout:
return "Gateway Timeout"
default:
return http.StatusText(code)
}
}
+94
View File
@@ -0,0 +1,94 @@
package client
import (
"context"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func TestFastHTTPDoer_BasicRoundTrip(t *testing.T) {
got := make(chan struct{ method, ct, body string }, 1)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
got <- struct{ method, ct, body string }{r.Method, r.Header.Get("Content-Type"), string(body)}
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, `{"ok":true,"result":42}`)
}))
defer srv.Close()
d := NewFastHTTPDoer()
req, err := http.NewRequest(http.MethodPost, srv.URL+"/sendMessage", strings.NewReader(`{"chat_id":1,"text":"hi"}`))
if err != nil {
t.Fatal(err)
}
req.Header.Set("Content-Type", "application/json")
req = req.WithContext(context.Background())
resp, err := d.Do(req)
if err != nil {
t.Fatal(err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != 200 {
t.Fatalf("status: got %d", resp.StatusCode)
}
body, _ := io.ReadAll(resp.Body)
if string(body) != `{"ok":true,"result":42}` {
t.Fatalf("body: got %q", body)
}
rec := <-got
if rec.method != http.MethodPost {
t.Fatalf("method: got %q", rec.method)
}
if rec.ct != "application/json" {
t.Fatalf("content-type: got %q", rec.ct)
}
if rec.body != `{"chat_id":1,"text":"hi"}` {
t.Fatalf("body: got %q", rec.body)
}
}
func TestFastHTTPDoer_HonoursContextDeadline(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(200 * time.Millisecond)
_, _ = io.WriteString(w, "ok")
}))
defer srv.Close()
d := NewFastHTTPDoer(WithFastHTTPReadTimeout(time.Hour))
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Millisecond)
defer cancel()
req, _ := http.NewRequest(http.MethodGet, srv.URL, nil)
req = req.WithContext(ctx)
_, err := d.Do(req)
if err == nil {
t.Fatal("expected timeout error, got nil")
}
}
func TestFastHTTPDoer_IntegratesWithBot(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = io.WriteString(w, `{"ok":true,"result":{"message_id":7,"date":0,"text":"hi"}}`)
}))
defer srv.Close()
bot := New("123:abc",
WithBaseURL(srv.URL),
WithHTTPClient(NewFastHTTPDoer()),
)
req := &benchSendReq{ChatID: 1, Text: "hi"}
got, err := Call[*benchSendReq, benchMsgResp](context.Background(), bot, "sendMessage", req)
if err != nil {
t.Fatal(err)
}
if got.MessageID != 7 || got.Text != "hi" {
t.Fatalf("got %+v", got)
}
}
+8 -5
View File
@@ -1,6 +1,7 @@
package client
import (
"bytes"
"context"
"github.com/goccy/go-json"
"io"
@@ -69,7 +70,7 @@ func callMultipart[Resp any](ctx context.Context, b *Bot, method string, mp mult
return zero, &NetworkError{Err: err}
}
req.Header.Set("Content-Type", mw.FormDataContentType())
req.Header.Set("Accept", "application/json")
req.Header["Accept"] = headerJSONValue
resp, err := b.http.Do(req)
if err != nil {
@@ -81,12 +82,14 @@ func callMultipart[Resp any](ctx context.Context, b *Bot, method string, mp mult
}
defer func() { _ = resp.Body.Close() }()
raw, err := io.ReadAll(resp.Body)
if err != nil {
buf := respBufPool.Get().(*bytes.Buffer)
buf.Reset()
defer putRespBuf(buf)
if _, err := buf.ReadFrom(resp.Body); err != nil {
_ = pr.CloseWithError(err)
return zero, &NetworkError{Err: err}
}
return decodeResult[Resp](b.codec, raw)
return decodeResult[Resp](b.codec, buf.Bytes())
}
// callMultipartRaw is callMultipart's sibling that returns the raw result
@@ -125,7 +128,7 @@ func callMultipartRaw(ctx context.Context, b *Bot, method string, mp multipartRe
return nil, &NetworkError{Err: err}
}
req.Header.Set("Content-Type", mw.FormDataContentType())
req.Header.Set("Accept", "application/json")
req.Header["Accept"] = headerJSONValue
resp, err := b.http.Do(req)
if err != nil {
+71 -3
View File
@@ -46,6 +46,30 @@ var runtimeTypes = map[string]bool{
"MessageOrBool": true,
}
// fieldTypeOverrides maps "<TypeOrParamsName>.<FieldName>" → Go type expression.
// Used for fields whose values are restricted but whose enum the scraper
// can't detect (Telegram's curly-quoted emoji literals are routinely
// stripped by the scraper's regex due to byte-boundary issues with
// multi-byte sequences). The hand-curated typed-string enum lives in
// api/enums.go (manual file); this override just retypes the field so
// callers get IDE completion and compile-time checks. Generated fields
// stay typed even after `make regen`.
var fieldTypeOverrides = map[string]string{
"ReactionTypeEmoji.Emoji": "ReactionEmoji",
"SendDiceParams.Emoji": "DiceEmoji",
}
// fieldTypeOverride returns the override type for a (parent, fieldName)
// pair, or "" if none. parent is the Go type name owning the field —
// either a struct type (e.g. "ReactionTypeEmoji") or a method-params
// type (e.g. "SendDiceParams").
func fieldTypeOverride(parent, fieldName string) string {
if parent == "" {
return ""
}
return fieldTypeOverrides[parent+"."+fieldName]
}
// discriminatorSpec describes how to decode a sealed-interface union by
// peeking at a single JSON field.
type discriminatorSpec struct {
@@ -395,6 +419,9 @@ func funcs(plan *enumPlan) template.FuncMap {
"goField": func(parent string, f spec.Field) string {
return goField(plan, parent, f)
},
"goFieldP": func(methodName string, f spec.Field) string {
return goFieldX(plan, "", title(methodName)+"Params", f)
},
"docComment": docComment,
"isOptional": func(f spec.Field) bool { return !f.Required },
"not": func(b bool) bool { return !b },
@@ -404,6 +431,9 @@ func funcs(plan *enumPlan) template.FuncMap {
"multipartFieldEntry": func(parent string, f spec.Field) string {
return multipartFieldEntry(plan, parent, f)
},
"multipartFieldEntryP": func(methodName string, f spec.Field) string {
return multipartFieldEntryX(plan, "", title(methodName)+"Params", f)
},
"multipartFileEntry": multipartFileEntry,
"returnGoType": returnGoType,
// enum helpers
@@ -503,7 +533,17 @@ func multipartFileEntry(f spec.Field) string {
// when non-zero/non-empty. Typed-string enum fields are cast to string
// before assignment because the multipart map is map[string]string.
func multipartFieldEntry(plan *enumPlan, parent string, f spec.Field) string {
enumName := plan.FieldEnum(parent, f.Name)
return multipartFieldEntryX(plan, parent, parent, f)
}
// multipartFieldEntryX mirrors goFieldX: enumParent keys the enum plan,
// overrideParent keys fieldTypeOverrides. They differ only for method
// params.
func multipartFieldEntryX(plan *enumPlan, enumParent, overrideParent string, f spec.Field) string {
enumName := plan.FieldEnum(enumParent, f.Name)
if enumName == "" {
enumName = fieldTypeOverride(overrideParent, f.Name)
}
switch f.Type.Kind {
case spec.KindPrimitive:
switch f.Type.Name {
@@ -775,12 +815,40 @@ func matchesVariants(got []string, want ...string) bool {
// has a planned enum name for (parent, field), the field's Go type is
// the enum identifier. Typed-string enums use the zero string ""
// behaviour for omitempty, so we do not pointer-wrap optional enum
// fields. Parent is "" for method parameters.
// fields. Parent is "" for method parameters; pass the params type
// name (e.g. "SendDiceParams") via overrideParent when calling from
// the methods template so fieldTypeOverrides can resolve.
func goField(plan *enumPlan, parent string, f spec.Field) string {
return goFieldX(plan, parent, parent, f)
}
// goFieldX is the underlying field-emitter. enumParent is used for the
// enum-plan lookup (which keys method params under ""); overrideParent
// is used for fieldTypeOverrides (which keys method params under the
// params type name). For struct types both are the same; for method
// params they differ.
func goFieldX(plan *enumPlan, enumParent, overrideParent string, f spec.Field) string {
tag := fmt.Sprintf("`json:%q`", f.JSONName+omitempty(f))
if name := plan.FieldEnum(parent, f.Name); name != "" {
if name := plan.FieldEnum(enumParent, f.Name); name != "" {
return fmt.Sprintf("%s %s %s", f.Name, name, tag)
}
if name := fieldTypeOverride(overrideParent, f.Name); name != "" {
return fmt.Sprintf("%s %s %s", f.Name, name, tag)
}
// Pinned companion-enum retype: allowed_updates is an Array of String
// in the upstream spec, but the Go API exposes a hand-curated
// UpdateType (api/enums.go) since the values are not enumerated
// inline by Telegram. Retype []string → []UpdateType wherever the
// wire field is allowed_updates so callers can pass typed constants
// (api.UpdateMessage, ...) without string casts. Wire format is
// unchanged: UpdateType is a typed string, marshals identically.
if f.JSONName == "allowed_updates" &&
f.Type.Kind == spec.KindArray &&
f.Type.ElemType != nil &&
f.Type.ElemType.Kind == spec.KindPrimitive &&
f.Type.ElemType.Name == "string" {
return fmt.Sprintf("%s []UpdateType %s", f.Name, tag)
}
return fmt.Sprintf("%s %s %s", f.Name, goType(f.Type, !f.Required), tag)
}
+176
View File
@@ -41,12 +41,25 @@ func planEnums(api *spec.API) *enumPlan {
valueKey string // canonical key for value-set dedup
}
// Unification pass: for each sealed-interface union, fold per-variant
// single-value enum fields that share a discriminator name into ONE
// unified enum at union level. Claimed (parent,fieldName) tuples are
// excluded from the per-field grouping below.
unifiedDecls, unifiedByField := planUnifiedUnionEnums(api)
claimed := func(parent, fieldName string) bool {
_, ok := unifiedByField[enumKey(parent, fieldName)]
return ok
}
var refs []ref
collect := func(parent string, fields []spec.Field) {
for _, f := range fields {
if len(f.EnumValues) == 0 {
continue
}
if claimed(parent, f.Name) {
continue
}
refs = append(refs, ref{
parent: parent,
fieldName: f.Name,
@@ -190,9 +203,172 @@ func planEnums(api *spec.API) *enumPlan {
plan.decls[g.name] = enumDecl{Name: g.name, Values: g.values}
_ = vk
}
// Merge unified union enums (already named with stutter handling and
// keyed per-variant in unifiedByField).
for k, name := range unifiedByField {
plan.byField[k] = name
}
for name, d := range unifiedDecls {
plan.decls[name] = d
}
return plan
}
// planUnifiedUnionEnums detects sealed-interface unions whose variants
// share a single discriminator field with one enum value each, and emits
// ONE unified enum per union covering all variant values. Returns the
// declarations to emit and the per-(variant,fieldName) map to point each
// variant's field at the unified enum.
//
// A union qualifies when EVERY variant in t.OneOf:
// 1. defines a field with the same Go-name (e.g. "Status", "Type", "Source");
// 2. that field is a required string with len(EnumValues)==1.
//
// The picked Go-name is the first one tried in this priority order:
// - knownDiscriminators[union].Field's Go-name (resolved via JSONName match);
// - "Type", "Status", "Source" (the three discriminators Telegram uses).
//
// First match wins; if none qualify, the union is skipped (variants keep
// their existing per-field treatment, which still single-emits via the
// regular grouping pass).
func planUnifiedUnionEnums(api *spec.API) (map[string]enumDecl, map[string]string) {
decls := map[string]enumDecl{}
byField := map[string]string{}
typeByName := make(map[string]*spec.TypeDecl, len(api.Types))
for i := range api.Types {
typeByName[api.Types[i].Name] = &api.Types[i]
}
// Iterate unions in deterministic (declaration) order.
for ui := range api.Types {
u := &api.Types[ui]
if len(u.OneOf) == 0 {
continue
}
// Resolve the variants. Skip unions where any variant is missing
// (defensive — shouldn't happen in a well-formed IR).
variants := make([]*spec.TypeDecl, 0, len(u.OneOf))
for _, vName := range u.OneOf {
v, ok := typeByName[vName]
if !ok {
variants = nil
break
}
variants = append(variants, v)
}
if len(variants) == 0 {
continue
}
// Build the candidate Go-name list. Priority order:
// 1. discriminator GoField from knownDiscriminators (resolved via JSONName);
// 2. "Type", "Status", "Source".
var candidateNames []string
seen := map[string]bool{}
add := func(name string) {
if name == "" || seen[name] {
return
}
seen[name] = true
candidateNames = append(candidateNames, name)
}
if ds, ok := knownDiscriminators[u.Name]; ok && ds.Field != "" {
// Resolve Go-name from the first variant whose field matches the JSON name.
for _, v := range variants {
for _, f := range v.Fields {
if f.JSONName == ds.Field {
add(f.Name)
break
}
}
}
}
for _, n := range []string{"Type", "Status", "Source"} {
add(n)
}
// Find the first candidate Go-name where every variant has a
// matching single-value string-enum field.
var (
pickedName string
pickedDocs map[string]spec.Field // variant name -> field
)
for _, name := range candidateNames {
matches := map[string]spec.Field{}
ok := true
for _, v := range variants {
var hit *spec.Field
for fi := range v.Fields {
if v.Fields[fi].Name == name {
hit = &v.Fields[fi]
break
}
}
if hit == nil ||
hit.Type.Kind != spec.KindPrimitive ||
hit.Type.Name != "string" ||
len(hit.EnumValues) != 1 {
ok = false
break
}
matches[v.Name] = *hit
}
if ok {
pickedName = name
pickedDocs = matches
break
}
}
if pickedName == "" {
continue
}
// Build the unified enum name with stutter handling.
enumName := unifiedEnumName(u.Name, pickedName)
// Collect values across variants in deterministic order, deduping.
valueOrder := make([]string, 0, len(variants))
valueSeen := map[string]bool{}
for _, v := range u.OneOf {
f := pickedDocs[v]
val := f.EnumValues[0]
if valueSeen[val] {
continue
}
valueSeen[val] = true
valueOrder = append(valueOrder, val)
}
decls[enumName] = enumDecl{Name: enumName, Values: valueOrder}
for _, v := range variants {
byField[enumKey(v.Name, pickedName)] = enumName
}
}
return decls, byField
}
// unifiedEnumName builds the union-level enum name. Falls back to a
// "Kind" suffix when the naive concatenation reads as a stutter:
//
// - union name ends in the field name verbatim (e.g. BackgroundType+Type);
// - union name ends in any "concept noun" — Type/Status/Source/State —
// so appending another such noun would duplicate the suffix
// (e.g. ChatBoostSource+Source, RevenueWithdrawalState+Type).
//
// Otherwise the natural concatenation wins (ChatMember+Status →
// ChatMemberStatus, MessageOrigin+Type → MessageOriginType).
func unifiedEnumName(unionName, fieldName string) string {
for _, suf := range []string{"Type", "Status", "Source", "State"} {
if strings.HasSuffix(unionName, suf) {
return unionName + "Kind"
}
}
return unionName + fieldName
}
// All returns the enum declarations sorted by name for deterministic emit.
func (p *enumPlan) All() []enumDecl {
out := make([]enumDecl, 0, len(p.decls))
+3 -3
View File
@@ -15,12 +15,12 @@ import (
var _ = strconv.Itoa // keep import for multipart helpers
var _ = json.Marshal // keep import for complex multipart fields
{{range .Methods}}
{{range .Methods}}{{$methodName := .Name}}
// {{title .Name}}Params is the parameter set for {{title .Name}}.
//
{{docComment .Doc -}}
type {{title .Name}}Params struct {
{{range .Params}}{{docComment .Doc}} {{goField "" .}}
{{range .Params}}{{docComment .Doc}} {{goFieldP $methodName .}}
{{end}}}
{{if .HasFiles}}
// HasFile reports whether a multipart upload is required.
@@ -31,7 +31,7 @@ func (p *{{title .Name}}Params) HasFile() bool {
// MultipartFields returns the non-file fields used in the multipart body.
func (p *{{title .Name}}Params) MultipartFields() map[string]string {
out := map[string]string{}
{{range .Params}}{{if not (isFileField .)}}{{multipartFieldEntry "" .}}{{end}}{{end}} return out
{{range .Params}}{{if not (isFileField .)}}{{multipartFieldEntryP $methodName .}}{{end}}{{end}} return out
}
// MultipartFiles returns the file parts.
+87 -1
View File
@@ -27,6 +27,33 @@ import (
// emits the canonical Markdown / MarkdownV2 / HTML triple.
//
// Returns nil when the description does not look like an enum.
// extractEnumValues inspects a field-description string and returns the
// list of wire-level string values when the description matches one of
// the enum-like patterns Telegram uses in its docs. Order follows doc
// order; duplicates are removed but order of first occurrence is kept.
//
// Handled patterns (curly quotes “…” are required to avoid false
// positives on free-text quoting):
//
// - "Type of the chat, can be either “private”, “group”, … or “channel”"
// - "Currently, can be “mention”, “hashtag”, …"
// - "Currently, one of “XTR” … or “TON” …"
// - "Currently, must be one of “XTR” …"
// - "Currently, it can be one of “pending”, “approved”, “declined”."
// - "Must be one of “danger” …, “success” …"
// - "Must be one of “image/jpeg”, “image/gif”, or “video/mp4”"
// - "Format … must be one of “static” …, “animated” …, “video” …"
// - "Currently, either “upgrade” …, “transfer” …, “resale” …"
// - "..., always “creator”"
// - parse_mode parameter special case ("Mode for parsing entities …")
// emits the canonical Markdown / MarkdownV2 / HTML triple.
// - bare prose discriminator at end of description, e.g.
// "Type of the result, must be article" or
// "Scope type, must be all_private_chats". Used by sealed-interface
// union variants whose Type/Source field carries a single literal
// value declared without curly quotes.
//
// Returns nil when the description does not look like an enum.
func extractEnumValues(jsonName, desc string) []string {
if values := parseModeEnumValues(jsonName, desc); values != nil {
return values
@@ -34,12 +61,15 @@ func extractEnumValues(jsonName, desc string) []string {
trigger, triggerEnd, isAlways := findEnumTrigger(desc)
if trigger < 0 {
return nil
return extractProseDiscriminator(desc)
}
tail := desc[trigger:]
values := collectQuotedValues(tail)
if len(values) == 0 {
if v := extractProseDiscriminator(desc); v != nil {
return v
}
return nil
}
// First quoted value must sit close to the trigger phrase (e.g.
@@ -203,3 +233,59 @@ func dedupeStrings(in []string) []string {
}
return out
}
// proseDiscRE matches a terminal "must be <ident>" clause: the
// discriminator value sits at the END of the description (optionally
// followed by trailing punctuation/whitespace) so multi-clause prose
// like "must be shown above the message" is not picked up.
//
// The identifier is a snake_case wire literal: lowercase letters, digits,
// and underscores, starting with a letter. Numeric-only and prose words
// are filtered separately by isProseWord.
var proseDiscRE = regexp.MustCompile(`(?i)\bmust be\s+([a-z][a-z0-9_]*)\s*[.,]?\s*$`)
// extractProseDiscriminator detects unambiguous single-value
// discriminator declarations of the form "..., must be <ident>" used by
// sealed-interface union variants (e.g. "Type of the result, must be
// article" or "Scope type, must be all_private_chats"). Returns the
// extracted value as a one-element slice or nil when no match is found.
//
// The terminal-position anchor is what protects against prose like
// "must be shown above" or "must be one of 3, 6, or 12" — the candidate
// must close the description.
func extractProseDiscriminator(desc string) []string {
desc = strings.TrimSpace(desc)
if desc == "" {
return nil
}
m := proseDiscRE.FindStringSubmatch(desc)
if m == nil {
return nil
}
v := m[1]
if isProseWord(v) {
return nil
}
return []string{v}
}
// isProseWord rejects bare-prose continuations that pass the regex but
// are clearly English filler ("must be sent", "must be available"). The
// list is the closed set of words that empirically appear in the IR's
// "must be …" tails outside the variant-discriminator pattern. Wire
// identifiers are always single tokens with no English meaning, so any
// match here is a free-text false positive.
func isProseWord(s string) bool {
switch s {
case "a", "an", "the",
"sent", "shown", "set", "used", "passed", "specified", "available",
"applied", "supported", "assumed", "active", "paid", "between",
"of", "on", "in", "at", "by", "to", "from", "for", "with",
"and", "or", "no", "non",
"positive", "negative",
"administrator", "repainted",
"one", "exactly":
return true
}
return false
}
+53
View File
@@ -83,3 +83,56 @@ func TestExtractEnumValues_DedupeRepeatedValues(t *testing.T) {
got := extractEnumValues("currency", desc)
require.Equal(t, []string{"XTR"}, got)
}
func TestExtractEnumValues_ProseDiscriminator(t *testing.T) {
cases := []struct {
name string
desc string
want []string
}{
{"InlineQueryResultArticle", "Type of the result, must be article", []string{"article"}},
{"InlineQueryResultPhoto", "Type of the result, must be photo", []string{"photo"}},
{"InlineQueryResultMpeg4Gif", "Type of the result, must be mpeg4_gif", []string{"mpeg4_gif"}},
{"BotCommandScopeAllPrivateChats", "Scope type, must be all_private_chats", []string{"all_private_chats"}},
{"BotCommandScopeChat", "Scope type, must be chat", []string{"chat"}},
{"PassportElementErrorData", "Error source, must be data", []string{"data"}},
{"MenuButtonWebApp", "Type of the button, must be web_app", []string{"web_app"}},
{"InputProfilePhotoAnimated", "Type of the profile photo, must be animated", []string{"animated"}},
{"InputStoryContentVideo", "Type of the content, must be video", []string{"video"}},
{"InputPaidMediaPhoto", "Type of the media, must be photo", []string{"photo"}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, tc.want, extractEnumValues("type", tc.desc))
})
}
}
func TestExtractEnumValues_ProseFalsePositives(t *testing.T) {
cases := []struct {
name string
desc string
}{
{"available_only_for", "Optional. Bot-specified invoice payload. Can be available only for “invoice_payment” transactions."},
{"must_be_sent", "If True, the message must be sent immediately."},
{"must_be_shown_above", "Optional. True, if the link preview must be shown above the message text"},
{"must_be_specified", "The identifiers must be specified in a strictly increasing order."},
{"must_be_paid", "The number of Telegram Stars that must be paid to send the sticker"},
{"must_be_one_of_numbers", "Number of months the Telegram Premium subscription will be active for the user; must be one of 3, 6, or 12"},
{"must_be_between", "Currently, price in Telegram Stars must be between 5 and 100000"},
{"must_be_a_pay_button", "If not empty, the first button must be a Pay button."},
{"must_be_repainted", "True, if the sticker must be repainted to a text color in messages"},
{"must_be_active", "the subscription must be active up to the end of the current subscription period"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
require.Nil(t, extractEnumValues("type", tc.desc))
})
}
}
func TestExtractEnumValues_CanonicalMustBeOneOfStillWorks(t *testing.T) {
desc := "Currently, must be one of “Markdown”, “MarkdownV2”, “HTML”"
got := extractEnumValues("parse_mode_kind", desc)
require.Equal(t, []string{"Markdown", "MarkdownV2", "HTML"}, got)
}
+34 -9
View File
@@ -21,20 +21,45 @@ import (
// Update is the raw update; payload-typed handlers also receive a
// narrowed pointer to one of its sub-fields.
//
// Values is a per-update bag matchers populate. Conventional keys:
// Command, CommandArgs and RegexMatch are populated by the router for
// the matching route kind; they replace the previous "command",
// "command_args" and "regex_match" entries in Values, which were the
// only conventional keys. Values remains for user-defined custom keys.
//
// "command": string, the matched bot command (e.g. "/start")
// "command_args": string, everything after the command
// "regex_match": []string, regex sub-matches when OnText matches
// Command is the matched bot command (e.g. "/start"); empty when the
// route is not a command match.
//
// CommandArgs is everything after the command; empty when no command
// matched or the command had no trailing text.
//
// RegexMatch is the regex sub-matches when an OnText/OnCallback regex
// route matched; nil otherwise.
//
// Values is lazily allocated for user-defined keys. Handlers that don't
// write pay no allocation. Reads against a nil map return the zero
// value. Writers must use Set instead of indexing the map directly.
type Context struct {
Ctx context.Context
Bot *client.Bot
Update *api.Update
Values map[string]any
Ctx context.Context
Bot *client.Bot
Update *api.Update
Command string
CommandArgs string
RegexMatch []string
Values map[string]any
}
// NewContext constructs a Context. Used by Router internally; exposed for
// custom test harnesses.
func NewContext(ctx context.Context, b *client.Bot, u *api.Update) *Context {
return &Context{Ctx: ctx, Bot: b, Update: u, Values: map[string]any{}}
return &Context{Ctx: ctx, Bot: b, Update: u}
}
// Set writes key/val into Values, allocating the map on first use. Use
// this instead of `c.Values[k] = v` so the no-write path stays
// allocation-free.
func (c *Context) Set(key string, val any) {
if c.Values == nil {
c.Values = make(map[string]any, 2)
}
c.Values[key] = val
}
@@ -12,15 +12,15 @@ func memberUpdate(status string, fromID int64) *api.ChatMemberUpdated {
var newMember api.ChatMember
switch status {
case "member":
newMember = &api.ChatMemberMember{Status: api.ChatMemberMemberStatusMember}
newMember = &api.ChatMemberMember{Status: api.ChatMemberStatusMember}
case "administrator":
newMember = &api.ChatMemberAdministrator{Status: api.ChatMemberAdministratorStatusAdministrator}
newMember = &api.ChatMemberAdministrator{Status: api.ChatMemberStatusAdministrator}
case "kicked":
newMember = &api.ChatMemberBanned{Status: api.ChatMemberBannedStatusKicked}
newMember = &api.ChatMemberBanned{Status: api.ChatMemberStatusKicked}
case "left":
newMember = &api.ChatMemberLeft{Status: api.ChatMemberLeftStatusLeft}
newMember = &api.ChatMemberLeft{Status: api.ChatMemberStatusLeft}
default:
newMember = &api.ChatMemberMember{Status: api.ChatMemberMemberStatusMember}
newMember = &api.ChatMemberMember{Status: api.ChatMemberStatusMember}
}
return &api.ChatMemberUpdated{
From: api.User{ID: fromID},
@@ -70,7 +70,7 @@ func TestComposedFilters(t *testing.T) {
func TestNewStatus_Owner(t *testing.T) {
u := &api.ChatMemberUpdated{
From: api.User{ID: 1},
NewChatMember: &api.ChatMemberOwner{Status: api.ChatMemberOwnerStatusCreator},
NewChatMember: &api.ChatMemberOwner{Status: api.ChatMemberStatusCreator},
}
require.True(t, cmfilter.NewStatus("creator")(u))
require.False(t, cmfilter.NewStatus("member")(u))
@@ -79,7 +79,7 @@ func TestNewStatus_Owner(t *testing.T) {
func TestNewStatus_Restricted(t *testing.T) {
u := &api.ChatMemberUpdated{
From: api.User{ID: 1},
NewChatMember: &api.ChatMemberRestricted{Status: api.ChatMemberRestrictedStatusRestricted},
NewChatMember: &api.ChatMemberRestricted{Status: api.ChatMemberStatusRestricted},
}
require.True(t, cmfilter.NewStatus("restricted")(u))
require.False(t, cmfilter.NewStatus("member")(u))
+3 -3
View File
@@ -138,8 +138,8 @@ func (r *Router) runGroupHandlers(c *Context, m *api.Message, g int) (matched bo
if route.group != g || route.cmd != cmd {
continue
}
c.Values["command"] = cmd
c.Values["command_args"] = args
c.Command = cmd
c.CommandArgs = args
if err := route.handler(c, m); err != nil {
if errors.Is(err, ErrContinueGroups) {
continue
@@ -159,7 +159,7 @@ func (r *Router) runGroupHandlers(c *Context, m *api.Message, g int) (matched bo
if subs == nil {
continue
}
c.Values["regex_match"] = subs
c.RegexMatch = subs
if err := route.handler(c, m); err != nil {
if errors.Is(err, ErrContinueGroups) {
continue
+23 -4
View File
@@ -300,6 +300,25 @@ func (r *Router) OnPurchasedPaidMedia(h Handler[*api.PaidMediaPurchased]) {
// serial (legacy) behaviour.
//
// Run waits for all in-flight handlers to finish before returning.
// Process runs a single update through the router's middleware and handler
// chain synchronously. Entry point for callers sourcing updates outside the
// standard transport.Updater flow — custom webhook frameworks, message-bus
// consumers, or tests driving the router without spinning up Run.
//
// Honours the router's global middleware (Use) but bypasses the concurrency
// semaphore wired up by Run; the caller controls parallelism.
func (r *Router) Process(ctx context.Context, u *api.Update) error {
if u == nil {
return nil
}
root := r.dispatch
for i := len(r.globalMW) - 1; i >= 0; i-- {
root = r.globalMW[i](root)
}
c := NewContext(ctx, r.bot, u)
return root(c, u)
}
func (r *Router) Run(ctx context.Context, u transport.Updater) error {
runErr := make(chan error, 1)
go func() { runErr <- u.Run(ctx) }()
@@ -467,8 +486,8 @@ func (r *Router) handleMessage(c *Context, m *api.Message) error {
if cmd, args, ok := extractCommand(m); ok {
for _, route := range r.commands {
if route.cmd == cmd {
c.Values["command"] = cmd
c.Values["command_args"] = args
c.Command = cmd
c.CommandArgs = args
return route.handler(c, m)
}
}
@@ -477,7 +496,7 @@ func (r *Router) handleMessage(c *Context, m *api.Message) error {
if m.Text != "" {
for _, route := range r.texts {
if subs := route.re.FindStringSubmatch(m.Text); subs != nil {
c.Values["regex_match"] = subs
c.RegexMatch = subs
return route.handler(c, m)
}
}
@@ -495,7 +514,7 @@ func (r *Router) handleMessage(c *Context, m *api.Message) error {
func (r *Router) handleCallback(c *Context, q *api.CallbackQuery) error {
for _, route := range r.callbacks {
if subs := route.re.FindStringSubmatch(q.Data); subs != nil {
c.Values["regex_match"] = subs
c.RegexMatch = subs
return route.handler(c, q)
}
}
+89
View File
@@ -0,0 +1,89 @@
package dispatch
import (
"context"
"testing"
"github.com/lukaszraczylo/go-telegram/api"
"github.com/lukaszraczylo/go-telegram/client"
)
func BenchmarkRouter_DispatchCommand(b *testing.B) {
r := New(client.New("t"))
r.OnCommand("/start", func(c *Context, m *api.Message) error { return nil })
u := cmdMessage("/start hello")
ctx := context.Background()
b.ReportAllocs()
for b.Loop() {
c := NewContext(ctx, r.bot, &u)
_ = r.dispatch(c, &u)
}
}
func BenchmarkRouter_DispatchTextRegex(b *testing.B) {
r := New(client.New("t"))
r.OnText("^hello.*", func(c *Context, m *api.Message) error { return nil })
u := api.Update{
UpdateID: 1,
Message: &api.Message{
MessageID: 1, Date: 0,
Chat: api.Chat{ID: 1, Type: api.ChatTypePrivate},
Text: "hello world",
},
}
ctx := context.Background()
b.ReportAllocs()
for b.Loop() {
c := NewContext(ctx, r.bot, &u)
_ = r.dispatch(c, &u)
}
}
func BenchmarkRouter_DispatchFilter(b *testing.B) {
r := New(client.New("t"))
r.OnMessageFilter(
func(m *api.Message) bool { return m != nil && m.Text == "ping" },
func(c *Context, m *api.Message) error { return nil },
)
u := api.Update{
UpdateID: 1,
Message: &api.Message{
MessageID: 1, Date: 0,
Chat: api.Chat{ID: 1, Type: api.ChatTypePrivate},
Text: "ping",
},
}
ctx := context.Background()
b.ReportAllocs()
for b.Loop() {
c := NewContext(ctx, r.bot, &u)
_ = r.dispatch(c, &u)
}
}
func BenchmarkRouter_NewContext(b *testing.B) {
bot := client.New("t")
u := &api.Update{UpdateID: 1}
ctx := context.Background()
b.ReportAllocs()
for b.Loop() {
_ = NewContext(ctx, bot, u)
}
}
func BenchmarkExtractCommand(b *testing.B) {
text := "/start@BotName hello world"
cmdLen := len("/start@BotName")
m := &api.Message{
MessageID: 1, Date: 0,
Chat: api.Chat{ID: 1, Type: api.ChatTypePrivate},
Text: text,
Entities: []api.MessageEntity{
{Type: api.MessageEntityTypeBotCommand, Offset: 0, Length: int64(cmdLen)},
},
}
b.ReportAllocs()
for b.Loop() {
_, _, _ = extractCommand(m)
}
}
+6 -10
View File
@@ -52,7 +52,7 @@ func TestRouter_OnCommandMatches(t *testing.T) {
r := New(b)
hit := make(chan string, 1)
r.OnCommand("/start", func(c *Context, m *api.Message) error {
hit <- c.Values["command"].(string)
hit <- c.Command
return nil
})
@@ -82,7 +82,7 @@ func TestRouter_OnText(t *testing.T) {
r := New(client.New("t"))
hit := make(chan []string, 1)
r.OnText(`^hello (\w+)$`, func(c *Context, m *api.Message) error {
hit <- c.Values["regex_match"].([]string)
hit <- c.RegexMatch
return nil
})
@@ -164,10 +164,7 @@ func TestRouter_NonASCIICommand(t *testing.T) {
r := New(client.New("t"))
hit := make(chan [2]string, 1)
r.OnCommand("/старт", func(c *Context, m *api.Message) error {
hit <- [2]string{
c.Values["command"].(string),
c.Values["command_args"].(string),
}
hit <- [2]string{c.Command, c.CommandArgs}
return nil
})
@@ -180,16 +177,15 @@ func TestRouter_NonASCIICommand(t *testing.T) {
require.Equal(t, "аргумент", got[1])
}
// TestRouter_CommandValuesNotLeakedOnNoMatch verifies that c.Values["command"]
// is not set when a command entity is present but no route matches, so a
// TestRouter_CommandValuesNotLeakedOnNoMatch verifies that c.Command is
// empty when a command entity is present but no route matches, so a
// subsequent text handler doesn't see stale values.
func TestRouter_CommandValuesNotLeakedOnNoMatch(t *testing.T) {
r := New(client.New("t"))
// Register a text handler that should fire as fallback.
leaked := make(chan bool, 1)
r.OnText(`.*`, func(c *Context, m *api.Message) error {
_, hasCmd := c.Values["command"]
leaked <- hasCmd
leaked <- c.Command != ""
return nil
})
// No OnCommand registered, so the command entity won't match any route.
+117
View File
@@ -0,0 +1,117 @@
# Benchmarks vs top 5 Go Telegram libraries
**Date:** 2026-05-10
**Environment:** Apple M4 Max · darwin/arm64 · `go1.26.2`
**Methodology:** `go test -count=10 -bench=. -benchmem`, summarised with `benchstat` (golang.org/x/perf)
**Source:** [`test/benchmarks/`](../../test/benchmarks/) · raw output: [`results/raw.txt`](../../test/benchmarks/results/raw.txt) · benchstat: [`results/benchstat.txt`](../../test/benchmarks/results/benchstat.txt)
## Libraries
| Lib | Module |
|-----|--------|
| **ours** | `github.com/lukaszraczylo/go-telegram` (this repo) |
| gotba | `github.com/go-telegram-bot-api/telegram-bot-api/v5` |
| telebot | `gopkg.in/telebot.v3` (tucnak) |
| gobot | `github.com/go-telegram/bot` |
| telego | `github.com/mymmrac/telego` |
| echotron | `github.com/NicoNex/echotron/v3` |
## TL;DR
- **Webhook decode** (small Update): ours is **1220% faster** than every competitor and ties telego for the lowest alloc count (11).
- **Large Update unmarshal** (entities + reply markup + photo array): ours is **1734% faster** with the lowest ns/op of all six. telego edges us on alloc count (31 vs 34) at the cost of ~17% more time.
- **API call round-trip** (mock HTTP server): telego wins on allocs (35.8 µs / 48 allocs) because it uses fasthttp by default. We default to `net/http` (102 allocs / 39.8 µs); with the opt-in `client.NewFastHTTPDoer` we drop to 56 allocs / 6.6 KiB — within 8 of telego while keeping `*http.Request` semantics (RetryDoer, middleware, generated tests).
- **Dispatcher routing** (20 handlers, last matches): ours is **2.52.8× faster than telebot and gobot** (98 ns vs 271 / 246 ns).
## How to read these numbers
- One machine, single workload, fixtures defined in [`shared/fixtures.go`](../../test/benchmarks/shared/fixtures.go). Re-run on your hardware before drawing conclusions.
- Codecs differ across libs (we use `goccy/go-json`; most competitors use stdlib `encoding/json`). Codec choice is part of the library's value prop, so we benchmark each library as it ships, not in some artificial common-codec mode.
- "Equivalent code path" was chosen via each library's idiomatic public API for the same logical operation. The exact code is in the bench files alongside each `BenchmarkXxx_<lib>` function — read them.
---
## 1. Webhook decode — small Update (text message)
Decode `shared.SmallUpdateJSON` into the library's typed `Update` struct.
| Lib | sec/op | B/op | allocs/op |
|-----|--------|------|-----------|
| **ours** | **1.832 µs ±4%** | 2.180 KiB | **11** |
| gotba | 2.082 µs ±0% | 1.461 KiB | 17 |
| telebot | 2.194 µs ±1% | 1.773 KiB | 17 |
| gobot | 2.082 µs ±1% | 1.789 KiB | 16 |
| telego | 2.143 µs ±2% | 3.058 KiB | **11** |
| echotron | 2.039 µs ±1% | 1.680 KiB | 16 |
**Notes.** We use slightly more bytes because typed unions and the typed `[]UpdateType` allocate richer Go values. We win on time and tie telego on alloc count.
## 2. Large Update unmarshal — entities + reply markup + photo array
Decode `shared.LargeUpdateJSON` (text + 3 entities + 2x3 inline keyboard + 3-size photo array). Stresses each library's union/discriminator decoding.
| Lib | sec/op | B/op | allocs/op |
|-----|--------|------|-----------|
| **ours** | **6.726 µs ±1%** | 5.875 KiB | 34 |
| gotba | 8.066 µs ±1% | 3.438 KiB | 56 |
| telebot | 10.190 µs ±1% | 5.594 KiB | 60 |
| gobot | 8.231 µs ±1% | 4.703 KiB | 50 |
| telego | 7.849 µs ±2% | 6.600 KiB | **31** |
| echotron | 8.123 µs ±1% | 4.219 KiB | 56 |
**Notes.** Despite the typed-union model giving us richer Go values per decode, we still produce them faster than every competitor. telego edges us by 3 allocs but pays 17% more time.
## 3. API call round-trip — `sendMessage` against a mock HTTP server
Build params → POST to local `httptest.Server` returning `{"ok":true,"result":Message}` → decode response.
| Lib | sec/op | B/op | allocs/op |
|-----|--------|------|-----------|
| ours (default `net/http`) | 39.83 µs ±4% | 11.09 KiB | 102 |
| ours (opt-in `fasthttp`) | *time TBD on quiet box* | **6.62 KiB** | **56** |
| gotba | 42.03 µs ±4% | 10.97 KiB | 125 |
| telebot | 43.41 µs ±1% | 13.15 KiB | 139 |
| gobot | 61.19 µs ±1% | 13.50 KiB | 176 |
| **telego** (uses fasthttp) | **35.84 µs ±1%** | **6.547 KiB** | **48** |
| echotron | *skipped — see below* | — | — |
**Notes.**
- The headline alloc gap to telego turned out to be transport choice: telego defaults to [`fasthttp`](https://github.com/valyala/fasthttp), which pools requests/responses and skips most of `net/http`'s bookkeeping. Most of the other libs (and us, by default) use `net/http`.
- We ship an opt-in fasthttp doer (`client.NewFastHTTPDoer`). Plug it via `client.WithHTTPClient(client.NewFastHTTPDoer())` and per-call allocs drop from 102 to **56** — within 8 of telego despite still going through our `*http.Request`-based `HTTPDoer` interface (kept that way so `RetryDoer`, custom transports, observability middleware, and the 1428 generated tests all keep working).
- The default stays `net/http` because fasthttp is HTTP/1.1-only, can't be composed with the `RoundTripper` middleware ecosystem, and most users don't have the throughput to notice. Bots making thousands of API calls/sec should opt in.
- Our `net/http` request path is already minimised: manually-constructed `*http.Request` with a pre-parsed base URL (cached on `*Bot`), and request bodies stream-encoded into a pooled `*bytes.Buffer` via the optional `BodyEncoder` codec extension. Those skip the `url.Parse` + `*http.Request` bookkeeping that `http.NewRequestWithContext` runs on every call.
- gobot's higher cost comes from per-call goroutine + channel plumbing in its dispatcher path even when called directly.
- **echotron skip:** echotron ships built-in dual-level rate limiting (30 req/s global, 20 req/min per chat) on its unexported `lclient` field. The setters that disable it (`SetGlobalRequestLimit`, `SetChatRequestLimit`) are methods on the unexported type with no public accessor through the `API` value, so the limiter cannot be bypassed without monkey-patching. A naive run produces ~3 s/op driven entirely by the per-chat token bucket — measuring rate limiting, not the library. We skip rather than publish a misleading number. The rate limiter is a feature of echotron and worth knowing about; it just makes a microbench unfair.
## 4. Dispatcher routing — 20 handlers, last one matches
Register 20 command handlers (`/cmd0``/cmd19`); feed an update matching `/cmd19` so the bench measures worst-case filter chain traversal.
| Lib | sec/op | B/op | allocs/op |
|-----|--------|------|-----------|
| **ours** | **98.46 ns ±2%** | 128 B | 3 |
| telebot | 270.9 ns ±2% | 678 B | 5 |
| gobot | 246.1 ns ±1% | **48 B** | **1** |
**Notes.** We dispatch ~2.5× faster than telebot and gobot. gobot's single allocation is impressive but its routing decision is slower. telebot's higher cost reflects its richer per-update `Context` construction.
**Coverage caveats.**
- **gotba** ships no built-in dispatcher; users route via a manual `switch` on `Update` fields. Benchmarking that against framework-based dispatchers would be apples-to-oranges, so it's omitted.
- **telego** routes via a buffered channel + goroutine pool inside `telegohandler.BotHandler`. There is no public sync entry point, so the bench would conflate channel + goroutine overhead with routing cost.
- **echotron** uses a chat-ID-keyed `Dispatcher` that fans out to per-chat `Bot` instances — a different paradigm (stateful per-chat bot loop), not directly comparable to "match this update against N handlers".
---
## How to reproduce
```bash
cd test/benchmarks
go test -count=10 -bench=. -benchmem | tee results/raw.txt
benchstat results/raw.txt > results/benchstat.txt
```
Install `benchstat` if missing: `go install golang.org/x/perf/cmd/benchstat@latest`.
## Bench code
All bench source lives under [`test/benchmarks/`](../../test/benchmarks/) as a separate Go module so competitor dependencies stay out of the root `go.mod`. The fixtures (the JSON each library decodes, the mock HTTP server) are in [`shared/fixtures.go`](../../test/benchmarks/shared/fixtures.go) — every library decodes the same bytes.
+594 -776
View File
File diff suppressed because it is too large Load Diff
+108 -14
View File
@@ -19,6 +19,7 @@ Package client provides HTTP client primitives for the Telegram Bot API.
- [func \(e \*APIError\) IsRetryable\(\) bool](<#APIError.IsRetryable>)
- [func \(e \*APIError\) RetryAfter\(\) time.Duration](<#APIError.RetryAfter>)
- [func \(e \*APIError\) Unwrap\(\) error](<#APIError.Unwrap>)
- [type BodyEncoder](<#BodyEncoder>)
- [type Bot](<#Bot>)
- [func New\(token string, opts ...Option\) \*Bot](<#New>)
- [func \(b \*Bot\) BaseURL\(\) string](<#Bot.BaseURL>)
@@ -29,7 +30,14 @@ Package client provides HTTP client primitives for the Telegram Bot API.
- [type Codec](<#Codec>)
- [type DefaultCodec](<#DefaultCodec>)
- [func \(DefaultCodec\) Marshal\(v any\) \(\[\]byte, error\)](<#DefaultCodec.Marshal>)
- [func \(DefaultCodec\) MarshalTo\(w io.Writer, v any\) error](<#DefaultCodec.MarshalTo>)
- [func \(DefaultCodec\) Unmarshal\(data \[\]byte, v any\) error](<#DefaultCodec.Unmarshal>)
- [type FastHTTPDoer](<#FastHTTPDoer>)
- [func NewFastHTTPDoer\(opts ...FastHTTPDoerOption\) \*FastHTTPDoer](<#NewFastHTTPDoer>)
- [func \(d \*FastHTTPDoer\) Do\(req \*http.Request\) \(\*http.Response, error\)](<#FastHTTPDoer.Do>)
- [type FastHTTPDoerOption](<#FastHTTPDoerOption>)
- [func WithFastHTTPClient\(c \*fasthttp.Client\) FastHTTPDoerOption](<#WithFastHTTPClient>)
- [func WithFastHTTPReadTimeout\(t time.Duration\) FastHTTPDoerOption](<#WithFastHTTPReadTimeout>)
- [type HTTPDoer](<#HTTPDoer>)
- [type Logger](<#Logger>)
- [type MultipartFile](<#MultipartFile>)
@@ -80,7 +88,7 @@ var (
```
<a name="Call"></a>
## func [Call](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/call.go#L25>)
## func [Call](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/call.go#L68>)
```go
func Call[Req any, Resp any](ctx context.Context, b *Bot, method string, req Req) (Resp, error)
@@ -93,7 +101,7 @@ It is generic over both request and response types. Methods with no parameters m
Call is exported because the api package \(which lives outside this one\) invokes it from generated method wrappers. User code should not normally call it directly — use the typed wrappers in package api instead.
<a name="CallRaw"></a>
## func [CallRaw](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/call.go#L74>)
## func [CallRaw](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/call.go#L119>)
```go
func CallRaw[Req any](ctx context.Context, b *Bot, method string, req Req) (json.RawMessage, error)
@@ -166,8 +174,19 @@ func (e *APIError) Unwrap() error
Unwrap returns the matched sentinel error, if any.
<a name="BodyEncoder"></a>
## type [BodyEncoder](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#L24-L26>)
BodyEncoder is an optional Codec extension that encodes directly into an io.Writer, skipping the intermediate \[\]byte that Marshal returns. Call uses this when present to avoid allocating the marshal result and the bytes.Reader that wraps it; codecs without it fall through to Marshal \+ bytes.NewReader.
```go
type BodyEncoder interface {
MarshalTo(w io.Writer, v any) error
}
```
<a name="Bot"></a>
## type [Bot](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L7-L13>)
## type [Bot](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L11-L24>)
Bot is the Telegram Bot API client. Construct via New. All API methods \(declared in package api\) hang off \*Bot via thin wrappers around call.
@@ -178,7 +197,7 @@ type Bot struct {
```
<a name="New"></a>
### func [New](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L36>)
### func [New](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L47>)
```go
func New(token string, opts ...Option) *Bot
@@ -187,7 +206,7 @@ func New(token string, opts ...Option) *Bot
New constructs a Bot with the given token and optional configuration. The default HTTP client is tuned for long\-poll workloads \(see NewDefaultHTTPDoer\); the default codec wraps encoding/json; the default logger discards records.
<a name="Bot.BaseURL"></a>
### func \(\*Bot\) [BaseURL](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L20>)
### func \(\*Bot\) [BaseURL](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L31>)
```go
func (b *Bot) BaseURL() string
@@ -196,7 +215,7 @@ func (b *Bot) BaseURL() string
BaseURL returns the configured Telegram API base URL.
<a name="Bot.Codec"></a>
### func \(\*Bot\) [Codec](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L27>)
### func \(\*Bot\) [Codec](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L38>)
```go
func (b *Bot) Codec() Codec
@@ -205,7 +224,7 @@ func (b *Bot) Codec() Codec
Codec returns the configured Codec.
<a name="Bot.HTTP"></a>
### func \(\*Bot\) [HTTP](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L24>)
### func \(\*Bot\) [HTTP](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L35>)
```go
func (b *Bot) HTTP() HTTPDoer
@@ -214,7 +233,7 @@ func (b *Bot) HTTP() HTTPDoer
HTTP returns the underlying HTTPDoer. Exposed for adapters that need to share connection pools or for diagnostic checks.
<a name="Bot.Logger"></a>
### func \(\*Bot\) [Logger](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L30>)
### func \(\*Bot\) [Logger](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L41>)
```go
func (b *Bot) Logger() Logger
@@ -223,7 +242,7 @@ func (b *Bot) Logger() Logger
Logger returns the configured Logger.
<a name="Bot.Token"></a>
### func \(\*Bot\) [Token](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L17>)
### func \(\*Bot\) [Token](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L28>)
```go
func (b *Bot) Token() string
@@ -232,7 +251,7 @@ func (b *Bot) Token() string
Token returns the bot token. Exposed for advanced use cases \(custom transports, manual URL building\); ordinary code does not need it.
<a name="Codec"></a>
## type [Codec](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#L10-L13>)
## type [Codec](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#L14-L17>)
Codec encodes/decodes JSON payloads exchanged with the Telegram Bot API. The default implementation wraps goccy/go\-json. Users may plug in bytedance/sonic or any compatible encoder by passing WithCodec to New.
@@ -244,7 +263,7 @@ type Codec interface {
```
<a name="DefaultCodec"></a>
## type [DefaultCodec](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#L16>)
## type [DefaultCodec](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#L29>)
DefaultCodec wraps goccy/go\-json. It is the zero\-value safe default.
@@ -253,7 +272,7 @@ type DefaultCodec struct{}
```
<a name="DefaultCodec.Marshal"></a>
### func \(DefaultCodec\) [Marshal](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#L19>)
### func \(DefaultCodec\) [Marshal](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#L32>)
```go
func (DefaultCodec) Marshal(v any) ([]byte, error)
@@ -261,8 +280,17 @@ func (DefaultCodec) Marshal(v any) ([]byte, error)
Marshal calls json.Marshal.
<a name="DefaultCodec.MarshalTo"></a>
### func \(DefaultCodec\) [MarshalTo](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#L40>)
```go
func (DefaultCodec) MarshalTo(w io.Writer, v any) error
```
MarshalTo encodes v into w via goccy/go\-json's streaming encoder. The trailing newline that Encoder appends is valid JSON whitespace and is accepted by Telegram's parser.
<a name="DefaultCodec.Unmarshal"></a>
### func \(DefaultCodec\) [Unmarshal](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#L22>)
### func \(DefaultCodec\) [Unmarshal](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#L35>)
```go
func (DefaultCodec) Unmarshal(data []byte, v any) error
@@ -270,6 +298,72 @@ func (DefaultCodec) Unmarshal(data []byte, v any) error
Unmarshal calls json.Unmarshal.
<a name="FastHTTPDoer"></a>
## type [FastHTTPDoer](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/fasthttp_doer.go#L26-L32>)
FastHTTPDoer is an HTTPDoer backed by github.com/valyala/fasthttp. It trades net/http compatibility \(and HTTP/2 support\) for substantially fewer allocations per request — fasthttp pools its Request and Response objects and uses a zero\-allocation HTTP/1.1 parser.
Use it for high\-throughput bots when GC pressure matters and you don't need HTTP/2 or any net/http\-only middleware \(RoundTripper composition, the OpenTelemetry httptrace family, etc.\):
```
bot := client.New(token, client.WithHTTPClient(client.NewFastHTTPDoer()))
```
Wrap with RetryDoer the same way you would the default doer.
```go
type FastHTTPDoer struct {
// contains filtered or unexported fields
}
```
<a name="NewFastHTTPDoer"></a>
### func [NewFastHTTPDoer](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/fasthttp_doer.go#L52>)
```go
func NewFastHTTPDoer(opts ...FastHTTPDoerOption) *FastHTTPDoer
```
NewFastHTTPDoer constructs a FastHTTPDoer with sensible defaults.
<a name="FastHTTPDoer.Do"></a>
### func \(\*FastHTTPDoer\) [Do](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/fasthttp_doer.go#L75>)
```go
func (d *FastHTTPDoer) Do(req *http.Request) (*http.Response, error)
```
Do satisfies HTTPDoer by translating req into a pooled fasthttp.Request, dispatching it, and returning a \*http.Response whose Body releases the pooled fasthttp.Response when Close is called.
The conversion is intentionally minimal: URL goes via req.URL.RequestURI\(\) \+ Host \(avoids re\-parsing\), header values move byte\-for\-byte, and the body is taken straight from req.Body. \*bytes.Buffer / \*bytes.Reader are recognised so we can pass the underlying bytes without io.ReadAll.
<a name="FastHTTPDoerOption"></a>
## type [FastHTTPDoerOption](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/fasthttp_doer.go#L35>)
FastHTTPDoerOption configures a FastHTTPDoer.
```go
type FastHTTPDoerOption func(*FastHTTPDoer)
```
<a name="WithFastHTTPClient"></a>
### func [WithFastHTTPClient](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/fasthttp_doer.go#L40>)
```go
func WithFastHTTPClient(c *fasthttp.Client) FastHTTPDoerOption
```
WithFastHTTPClient swaps in a pre\-configured \*fasthttp.Client. Useful for sharing a connection pool across multiple bots or applying custom dial / TLS configuration.
<a name="WithFastHTTPReadTimeout"></a>
### func [WithFastHTTPReadTimeout](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/fasthttp_doer.go#L47>)
```go
func WithFastHTTPReadTimeout(t time.Duration) FastHTTPDoerOption
```
WithFastHTTPReadTimeout sets the per\-request fallback timeout used when the inbound context has no deadline. Long\-poll callers should pass a value larger than the long\-poll timeout.
<a name="HTTPDoer"></a>
## type [HTTPDoer](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/httpclient.go#L13-L15>)
@@ -296,7 +390,7 @@ type Logger interface {
```
<a name="MultipartFile"></a>
## type [MultipartFile](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/multipart.go#L27-L31>)
## type [MultipartFile](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/multipart.go#L28-L32>)
MultipartFile describes a single file part in a multipart upload.
+43 -16
View File
@@ -13,6 +13,7 @@ Package dispatch provides a typed router for Telegram updates. It consumes any t
- [Variables](<#variables>)
- [type Context](<#Context>)
- [func NewContext\(ctx context.Context, b \*client.Bot, u \*api.Update\) \*Context](<#NewContext>)
- [func \(c \*Context\) Set\(key string, val any\)](<#Context.Set>)
- [type Filter](<#Filter>)
- [func All\[T any\]\(filters ...Filter\[T\]\) Filter\[T\]](<#All>)
- [func Any\[T any\]\(filters ...Filter\[T\]\) Filter\[T\]](<#Any>)
@@ -61,6 +62,7 @@ Package dispatch provides a typed router for Telegram updates. It consumes any t
- [func \(r \*Router\) OnRemovedChatBoost\(h Handler\[\*api.ChatBoostRemoved\]\)](<#Router.OnRemovedChatBoost>)
- [func \(r \*Router\) OnShippingQuery\(h Handler\[\*api.ShippingQuery\]\)](<#Router.OnShippingQuery>)
- [func \(r \*Router\) OnText\(pattern string, h Handler\[\*api.Message\]\)](<#Router.OnText>)
- [func \(r \*Router\) Process\(ctx context.Context, u \*api.Update\) error](<#Router.Process>)
- [func \(r \*Router\) Run\(ctx context.Context, u transport.Updater\) error](<#Router.Run>)
- [func \(r \*Router\) Use\(mw Middleware\[\*api.Update\]\)](<#Router.Use>)
- [type RouterOption](<#RouterOption>)
@@ -90,7 +92,7 @@ var ErrEndGroups = errors.New("dispatch: end groups")
```
<a name="Context"></a>
## type [Context](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/context.go#L29-L34>)
## type [Context](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/context.go#L41-L49>)
Context bundles the per\-update state every handler receives.
@@ -100,25 +102,30 @@ Bot is the API client. Handlers reply by calling api.SendMessage\(c.Ctx, c.Bot,
Update is the raw update; payload\-typed handlers also receive a narrowed pointer to one of its sub\-fields.
Values is a per\-update bag matchers populate. Conventional keys:
Command, CommandArgs and RegexMatch are populated by the router for the matching route kind; they replace the previous "command", "command\_args" and "regex\_match" entries in Values, which were the only conventional keys. Values remains for user\-defined custom keys.
```
"command": string, the matched bot command (e.g. "/start")
"command_args": string, everything after the command
"regex_match": []string, regex sub-matches when OnText matches
```
Command is the matched bot command \(e.g. "/start"\); empty when the route is not a command match.
CommandArgs is everything after the command; empty when no command matched or the command had no trailing text.
RegexMatch is the regex sub\-matches when an OnText/OnCallback regex route matched; nil otherwise.
Values is lazily allocated for user\-defined keys. Handlers that don't write pay no allocation. Reads against a nil map return the zero value. Writers must use Set instead of indexing the map directly.
```go
type Context struct {
Ctx context.Context
Bot *client.Bot
Update *api.Update
Values map[string]any
Ctx context.Context
Bot *client.Bot
Update *api.Update
Command string
CommandArgs string
RegexMatch []string
Values map[string]any
}
```
<a name="NewContext"></a>
### func [NewContext](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/context.go#L38>)
### func [NewContext](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/context.go#L53>)
```go
func NewContext(ctx context.Context, b *client.Bot, u *api.Update) *Context
@@ -126,6 +133,15 @@ func NewContext(ctx context.Context, b *client.Bot, u *api.Update) *Context
NewContext constructs a Context. Used by Router internally; exposed for custom test harnesses.
<a name="Context.Set"></a>
### func \(\*Context\) [Set](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/context.go#L60>)
```go
func (c *Context) Set(key string, val any)
```
Set writes key/val into Values, allocating the map on first use. Use this instead of \`c.Values\[k\] = v\` so the no\-write path stays allocation\-free.
<a name="Filter"></a>
## type [Filter](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filter.go#L9>)
@@ -585,18 +601,29 @@ OnText registers a handler for messages whose Text matches the regex.
Panics at registration time if pattern is not a valid regular expression.
<a name="Router.Run"></a>
### func \(\*Router\) [Run](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L303>)
<a name="Router.Process"></a>
### func \(\*Router\) [Process](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L310>)
```go
func (r *Router) Run(ctx context.Context, u transport.Updater) error
func (r *Router) Process(ctx context.Context, u *api.Update) error
```
Run consumes the Updater and dispatches each update. It blocks until the Updater's channel is closed or ctx is cancelled.
By default updates are processed concurrently \(up to WithMaxConcurrency\(50\) goroutines\). Handlers for different updates may therefore run simultaneously; shared state must be protected. Pass WithMaxConcurrency\(0\) to New to restore serial \(legacy\) behaviour.
Run waits for all in\-flight handlers to finish before returning.
Run waits for all in\-flight handlers to finish before returning. Process runs a single update through the router's middleware and handler chain synchronously. Entry point for callers sourcing updates outside the standard transport.Updater flow — custom webhook frameworks, message\-bus consumers, or tests driving the router without spinning up Run.
Honours the router's global middleware \(Use\) but bypasses the concurrency semaphore wired up by Run; the caller controls parallelism.
<a name="Router.Run"></a>
### func \(\*Router\) [Run](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L322>)
```go
func (r *Router) Run(ctx context.Context, u transport.Updater) error
```
<a name="Router.Use"></a>
### func \(\*Router\) [Use](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L141>)
+10 -10
View File
@@ -113,7 +113,7 @@ func (p *LongPoller) Run(ctx context.Context) error
Run implements Updater. It blocks until ctx is cancelled, Stop is called, or a fatal error occurs \(e.g. unauthorized\). See LongPoller for at\-least\-once delivery semantics on shutdown.
<a name="LongPoller.Stop"></a>
### func \(\*LongPoller\) [Stop](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/longpoll.go#L126>)
### func \(\*LongPoller\) [Stop](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/longpoll.go#L122>)
```go
func (p *LongPoller) Stop(ctx context.Context) error
@@ -154,7 +154,7 @@ type Updater interface {
```
<a name="WebhookOption"></a>
## type [WebhookOption](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L38>)
## type [WebhookOption](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L57>)
WebhookOption configures a WebhookServer at construction time.
@@ -163,7 +163,7 @@ type WebhookOption func(*webhookOptions)
```
<a name="WithBufferSize"></a>
### func [WithBufferSize](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L46>)
### func [WithBufferSize](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L65>)
```go
func WithBufferSize(n int) WebhookOption
@@ -172,7 +172,7 @@ func WithBufferSize(n int) WebhookOption
WithBufferSize sets the size of the updates channel buffer. Default is 64.
<a name="WebhookServer"></a>
## type [WebhookServer](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L24-L35>)
## type [WebhookServer](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L43-L54>)
WebhookServer implements Updater by exposing an http.Handler that receives updates from Telegram. It can be mounted on the user's own HTTP server \(via ServeHTTP\) or run standalone \(via ListenAndServe\).
@@ -185,7 +185,7 @@ type WebhookServer struct {
```
<a name="NewWebhookServer"></a>
### func [NewWebhookServer](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L52>)
### func [NewWebhookServer](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L71>)
```go
func NewWebhookServer(b *client.Bot, opts ...WebhookOption) *WebhookServer
@@ -194,7 +194,7 @@ func NewWebhookServer(b *client.Bot, opts ...WebhookOption) *WebhookServer
NewWebhookServer constructs a WebhookServer with default buffer size \(64\). Use WithBufferSize to override.
<a name="WebhookServer.ListenAndServe"></a>
### func \(\*WebhookServer\) [ListenAndServe](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L150>)
### func \(\*WebhookServer\) [ListenAndServe](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L168>)
```go
func (w *WebhookServer) ListenAndServe(ctx context.Context, addr string) error
@@ -203,7 +203,7 @@ func (w *WebhookServer) ListenAndServe(ctx context.Context, addr string) error
ListenAndServe starts an HTTP server on addr and blocks until Stop is called \(which triggers Shutdown with the caller's context\) or the server returns an error other than http.ErrServerClosed. Callers must invoke Stop\(ctx\) to cleanly shut down the server; the ctx passed here is only used as the server's base context for incoming requests.
<a name="WebhookServer.Run"></a>
### func \(\*WebhookServer\) [Run](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L71>)
### func \(\*WebhookServer\) [Run](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L90>)
```go
func (w *WebhookServer) Run(ctx context.Context) error
@@ -212,7 +212,7 @@ func (w *WebhookServer) Run(ctx context.Context) error
Run implements Updater. It blocks until Stop is called or ctx is cancelled. If the server has not been started via ListenAndServe, Run only watches for shutdown — the user is expected to mount ServeHTTP on their own router.
<a name="WebhookServer.ServeHTTP"></a>
### func \(\*WebhookServer\) [ServeHTTP](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L97>)
### func \(\*WebhookServer\) [ServeHTTP](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L116>)
```go
func (w *WebhookServer) ServeHTTP(rw http.ResponseWriter, r *http.Request)
@@ -221,7 +221,7 @@ func (w *WebhookServer) ServeHTTP(rw http.ResponseWriter, r *http.Request)
ServeHTTP implements http.Handler. Telegram POSTs each update as JSON to this endpoint. Non\-POST requests get 405; bad bodies get 400; secret token mismatches get 401.
<a name="WebhookServer.Stop"></a>
### func \(\*WebhookServer\) [Stop](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L83>)
### func \(\*WebhookServer\) [Stop](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L102>)
```go
func (w *WebhookServer) Stop(ctx context.Context) error
@@ -230,7 +230,7 @@ func (w *WebhookServer) Stop(ctx context.Context) error
Stop implements Updater.
<a name="WebhookServer.Updates"></a>
### func \(\*WebhookServer\) [Updates](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L65>)
### func \(\*WebhookServer\) [Updates](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L84>)
```go
func (w *WebhookServer) Updates() <-chan api.Update
+1 -1
View File
@@ -21,7 +21,7 @@ func handleStart(c *dispatch.Context, m *api.Message) error {
}
func handleCallback(c *dispatch.Context, q *api.CallbackQuery) error {
groups := c.Values["regex_match"].([]string)
groups := c.RegexMatch
current, _ := strconv.Atoi(groups[1])
if groups[2] == "inc" {
current++
+4 -2
View File
@@ -42,7 +42,7 @@ const (
func makeCtx(bot *client.Bot, upd *api.Update, extra map[string]any) *dispatch.Context {
c := dispatch.NewContext(context.Background(), bot, upd)
for k, v := range extra {
c.Values[k] = v
c.Set(k, v)
}
return c
}
@@ -82,7 +82,9 @@ func TestHandleStart_SendsInitialKeyboard(t *testing.T) {
func callbackCtx(bot *client.Bot, q *api.CallbackQuery, groups []string) *dispatch.Context {
upd := &api.Update{UpdateID: 1, CallbackQuery: q}
return makeCtx(bot, upd, map[string]any{"regex_match": groups})
c := makeCtx(bot, upd, nil)
c.RegexMatch = groups
return c
}
func callbackQuery(data string, msgID int64, chatID int64) *api.CallbackQuery {
+1 -2
View File
@@ -111,8 +111,7 @@ func main() {
// page:<n> callbacks — edit message in-place.
router.OnCallback(`^page:(\d+)$`, func(c *dispatch.Context, q *api.CallbackQuery) error {
groups := c.Values["regex_match"].([]string)
page, _ := strconv.Atoi(groups[1])
page, _ := strconv.Atoi(c.RegexMatch[1])
// Acknowledge the tap first.
_, _ = api.AnswerCallbackQuery(c.Ctx, c.Bot, &api.AnswerCallbackQueryParams{
+4
View File
@@ -9,8 +9,12 @@ require (
)
require (
github.com/andybalholm/brotli v1.2.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/klauspost/compress v1.18.6 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.71.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+8
View File
@@ -1,13 +1,21 @@
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
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/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
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/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.71.0 h1:tepR7H+Guh9VUqxxcPggYi8R3lGUu2Rsdh+z7/FCY3k=
github.com/valyala/fasthttp v1.71.0/go.mod h1:z1sDUvOShhXq/C9mwH/fSm1Vb71tUJwmQdgkBrBNwnA=
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+220 -55
View File
@@ -10529,7 +10529,10 @@
"name": "string"
},
"required": true,
"doc": "Scope type, must be default"
"doc": "Scope type, must be default",
"enum_values": [
"default"
]
}
]
},
@@ -10545,7 +10548,10 @@
"name": "string"
},
"required": true,
"doc": "Scope type, must be all_private_chats"
"doc": "Scope type, must be all_private_chats",
"enum_values": [
"all_private_chats"
]
}
]
},
@@ -10561,7 +10567,10 @@
"name": "string"
},
"required": true,
"doc": "Scope type, must be all_group_chats"
"doc": "Scope type, must be all_group_chats",
"enum_values": [
"all_group_chats"
]
}
]
},
@@ -10577,7 +10586,10 @@
"name": "string"
},
"required": true,
"doc": "Scope type, must be all_chat_administrators"
"doc": "Scope type, must be all_chat_administrators",
"enum_values": [
"all_chat_administrators"
]
}
]
},
@@ -10593,7 +10605,10 @@
"name": "string"
},
"required": true,
"doc": "Scope type, must be chat"
"doc": "Scope type, must be chat",
"enum_values": [
"chat"
]
},
{
"name": "ChatID",
@@ -10622,7 +10637,10 @@
"name": "string"
},
"required": true,
"doc": "Scope type, must be chat_administrators"
"doc": "Scope type, must be chat_administrators",
"enum_values": [
"chat_administrators"
]
},
{
"name": "ChatID",
@@ -10651,7 +10669,10 @@
"name": "string"
},
"required": true,
"doc": "Scope type, must be chat_member"
"doc": "Scope type, must be chat_member",
"enum_values": [
"chat_member"
]
},
{
"name": "ChatID",
@@ -10747,7 +10768,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the button, must be commands"
"doc": "Type of the button, must be commands",
"enum_values": [
"commands"
]
}
]
},
@@ -10763,7 +10787,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the button, must be web_app"
"doc": "Type of the button, must be web_app",
"enum_values": [
"web_app"
]
},
{
"name": "Text",
@@ -10799,7 +10826,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the button, must be default"
"doc": "Type of the button, must be default",
"enum_values": [
"default"
]
}
]
},
@@ -11451,7 +11481,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the result, must be animation"
"doc": "Type of the result, must be animation",
"enum_values": [
"animation"
]
},
{
"name": "Media",
@@ -11566,7 +11599,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the result, must be audio"
"doc": "Type of the result, must be audio",
"enum_values": [
"audio"
]
},
{
"name": "Media",
@@ -11663,7 +11699,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the result, must be document"
"doc": "Type of the result, must be document",
"enum_values": [
"document"
]
},
{
"name": "Media",
@@ -11742,7 +11781,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the result, must be live_photo"
"doc": "Type of the result, must be live_photo",
"enum_values": [
"live_photo"
]
},
{
"name": "Media",
@@ -11831,7 +11873,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the result, must be location"
"doc": "Type of the result, must be location",
"enum_values": [
"location"
]
},
{
"name": "Latitude",
@@ -11876,7 +11921,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the result, must be photo"
"doc": "Type of the result, must be photo",
"enum_values": [
"photo"
]
},
{
"name": "Media",
@@ -11955,7 +12003,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the result, must be sticker"
"doc": "Type of the result, must be sticker",
"enum_values": [
"sticker"
]
},
{
"name": "Media",
@@ -11990,7 +12041,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the result, must be venue"
"doc": "Type of the result, must be venue",
"enum_values": [
"venue"
]
},
{
"name": "Latitude",
@@ -12082,7 +12136,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the result, must be video"
"doc": "Type of the result, must be video",
"enum_values": [
"video"
]
},
{
"name": "Media",
@@ -12237,7 +12294,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the media, must be live_photo"
"doc": "Type of the media, must be live_photo",
"enum_values": [
"live_photo"
]
},
{
"name": "Media",
@@ -12273,7 +12333,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the media, must be photo"
"doc": "Type of the media, must be photo",
"enum_values": [
"photo"
]
},
{
"name": "Media",
@@ -12299,7 +12362,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the media, must be video"
"doc": "Type of the media, must be video",
"enum_values": [
"video"
]
},
{
"name": "Media",
@@ -12396,7 +12462,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the profile photo, must be static"
"doc": "Type of the profile photo, must be static",
"enum_values": [
"static"
]
},
{
"name": "Photo",
@@ -12422,7 +12491,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the profile photo, must be animated"
"doc": "Type of the profile photo, must be animated",
"enum_values": [
"animated"
]
},
{
"name": "Animation",
@@ -12465,7 +12537,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the content, must be photo"
"doc": "Type of the content, must be photo",
"enum_values": [
"photo"
]
},
{
"name": "Photo",
@@ -12491,7 +12566,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the content, must be video"
"doc": "Type of the content, must be video",
"enum_values": [
"video"
]
},
{
"name": "Video",
@@ -13008,7 +13086,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the result, must be article"
"doc": "Type of the result, must be article",
"enum_values": [
"article"
]
},
{
"name": "ID",
@@ -13108,7 +13189,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the result, must be photo"
"doc": "Type of the result, must be photo",
"enum_values": [
"photo"
]
},
{
"name": "ID",
@@ -13252,7 +13336,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the result, must be gif"
"doc": "Type of the result, must be gif",
"enum_values": [
"gif"
]
},
{
"name": "ID",
@@ -13410,7 +13497,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the result, must be mpeg4_gif"
"doc": "Type of the result, must be mpeg4_gif",
"enum_values": [
"mpeg4_gif"
]
},
{
"name": "ID",
@@ -13568,7 +13658,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the result, must be video"
"doc": "Type of the result, must be video",
"enum_values": [
"video"
]
},
{
"name": "ID",
@@ -13732,7 +13825,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the result, must be audio"
"doc": "Type of the result, must be audio",
"enum_values": [
"audio"
]
},
{
"name": "ID",
@@ -13849,7 +13945,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the result, must be voice"
"doc": "Type of the result, must be voice",
"enum_values": [
"voice"
]
},
{
"name": "ID",
@@ -13957,7 +14056,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the result, must be document"
"doc": "Type of the result, must be document",
"enum_values": [
"document"
]
},
{
"name": "ID",
@@ -14106,7 +14208,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the result, must be location"
"doc": "Type of the result, must be location",
"enum_values": [
"location"
]
},
{
"name": "ID",
@@ -14243,7 +14348,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the result, must be venue"
"doc": "Type of the result, must be venue",
"enum_values": [
"venue"
]
},
{
"name": "ID",
@@ -14390,7 +14498,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the result, must be contact"
"doc": "Type of the result, must be contact",
"enum_values": [
"contact"
]
},
{
"name": "ID",
@@ -14499,7 +14610,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the result, must be game"
"doc": "Type of the result, must be game",
"enum_values": [
"game"
]
},
{
"name": "ID",
@@ -14544,7 +14658,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the result, must be photo"
"doc": "Type of the result, must be photo",
"enum_values": [
"photo"
]
},
{
"name": "ID",
@@ -14660,7 +14777,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the result, must be gif"
"doc": "Type of the result, must be gif",
"enum_values": [
"gif"
]
},
{
"name": "ID",
@@ -14767,7 +14887,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the result, must be mpeg4_gif"
"doc": "Type of the result, must be mpeg4_gif",
"enum_values": [
"mpeg4_gif"
]
},
{
"name": "ID",
@@ -14874,7 +14997,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the result, must be sticker"
"doc": "Type of the result, must be sticker",
"enum_values": [
"sticker"
]
},
{
"name": "ID",
@@ -14928,7 +15054,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the result, must be document"
"doc": "Type of the result, must be document",
"enum_values": [
"document"
]
},
{
"name": "ID",
@@ -15036,7 +15165,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the result, must be video"
"doc": "Type of the result, must be video",
"enum_values": [
"video"
]
},
{
"name": "ID",
@@ -15153,7 +15285,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the result, must be voice"
"doc": "Type of the result, must be voice",
"enum_values": [
"voice"
]
},
{
"name": "ID",
@@ -15252,7 +15387,10 @@
"name": "string"
},
"required": true,
"doc": "Type of the result, must be audio"
"doc": "Type of the result, must be audio",
"enum_values": [
"audio"
]
},
{
"name": "ID",
@@ -17138,7 +17276,10 @@
"name": "string"
},
"required": true,
"doc": "Error source, must be data"
"doc": "Error source, must be data",
"enum_values": [
"data"
]
},
{
"name": "Type",
@@ -17202,7 +17343,10 @@
"name": "string"
},
"required": true,
"doc": "Error source, must be front_side"
"doc": "Error source, must be front_side",
"enum_values": [
"front_side"
]
},
{
"name": "Type",
@@ -17254,7 +17398,10 @@
"name": "string"
},
"required": true,
"doc": "Error source, must be reverse_side"
"doc": "Error source, must be reverse_side",
"enum_values": [
"reverse_side"
]
},
{
"name": "Type",
@@ -17304,7 +17451,10 @@
"name": "string"
},
"required": true,
"doc": "Error source, must be selfie"
"doc": "Error source, must be selfie",
"enum_values": [
"selfie"
]
},
{
"name": "Type",
@@ -17356,7 +17506,10 @@
"name": "string"
},
"required": true,
"doc": "Error source, must be file"
"doc": "Error source, must be file",
"enum_values": [
"file"
]
},
{
"name": "Type",
@@ -17409,7 +17562,10 @@
"name": "string"
},
"required": true,
"doc": "Error source, must be files"
"doc": "Error source, must be files",
"enum_values": [
"files"
]
},
{
"name": "Type",
@@ -17465,7 +17621,10 @@
"name": "string"
},
"required": true,
"doc": "Error source, must be translation_file"
"doc": "Error source, must be translation_file",
"enum_values": [
"translation_file"
]
},
{
"name": "Type",
@@ -17522,7 +17681,10 @@
"name": "string"
},
"required": true,
"doc": "Error source, must be translation_files"
"doc": "Error source, must be translation_files",
"enum_values": [
"translation_files"
]
},
{
"name": "Type",
@@ -17582,7 +17744,10 @@
"name": "string"
},
"required": true,
"doc": "Error source, must be unspecified"
"doc": "Error source, must be unspecified",
"enum_values": [
"unspecified"
]
},
{
"name": "Type",
+36
View File
@@ -0,0 +1,36 @@
# Cross-library benchmarks
Apples-to-apples micro-benchmarks comparing `lukaszraczylo/go-telegram` against the five most-starred Go Telegram libraries:
- `github.com/go-telegram-bot-api/telegram-bot-api/v5`
- `gopkg.in/telebot.v3` (tucnak)
- `github.com/go-telegram/bot`
- `github.com/mymmrac/telego`
- `github.com/NicoNex/echotron/v3`
Lives in its own Go module so competitor dependencies don't leak into the main repo's `go.mod`.
## Run
```bash
go test -count=10 -bench=. -benchmem | tee results/raw.txt
go install golang.org/x/perf/cmd/benchstat@latest # one-time
benchstat results/raw.txt > results/benchstat.txt
```
## Hot paths covered
| File | Path |
|------|------|
| `webhook_bench_test.go` | Decode small text-message Update from JSON |
| `unmarshal_bench_test.go` | Decode large Update (entities + reply markup + photo array) |
| `call_bench_test.go` | `sendMessage` round-trip against `httptest.Server` |
| `dispatch_bench_test.go` | Route an Update through 20 registered handlers (worst-case match) |
## Fixtures
`shared/fixtures.go` defines the JSON payloads and the mock HTTP server. Every library decodes the same bytes; every round-trip hits the same canned response.
## Latest results
See [`../../docs/benchmarks/2026-05-10-comparison.md`](../../docs/benchmarks/2026-05-10-comparison.md) for the rendered comparison.
+191
View File
@@ -0,0 +1,191 @@
// Round-trip benchmarks: build a SendMessage request, POST it to a local
// httptest.Server returning a canned `{"ok":true,"result":Message}` body,
// decode the response. Measures marshal + transport + unmarshal end-to-end.
//
// Each library's idiomatic "send a text message" call path is exercised
// through its public API. The mock server replies identically for every path,
// so any difference comes from serialization, HTTP plumbing, or response
// decoding inside each library.
package benchmarks
import (
"context"
"net/http"
"strings"
"testing"
"github.com/lukaszraczylo/go-telegram/api"
"github.com/lukaszraczylo/go-telegram/client"
"github.com/lukaszraczylo/go-telegram/test/benchmarks/shared"
echotron "github.com/NicoNex/echotron/v3"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
gobot "github.com/go-telegram/bot"
telego "github.com/mymmrac/telego"
tele "gopkg.in/telebot.v3"
)
// Telegram-format token (digits:[\w-]{35}). telego enforces this format on construction.
const benchToken = "1234567890:ABCDEFGHIJKLMNOPQRSTUVWXYZ_ab123456"
// BenchmarkCall_ours — lukaszraczylo/go-telegram with default net/http
// transport. Most users land here.
func BenchmarkCall_ours(b *testing.B) {
srv := shared.NewMockServer()
defer srv.Close()
bot := client.New(benchToken, client.WithBaseURL(srv.URL))
ctx := context.Background()
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
_, err := api.SendMessage(ctx, bot, &api.SendMessageParams{
ChatID: api.ChatIDFromInt(42),
Text: "hello",
})
if err != nil {
b.Fatal(err)
}
}
}
// BenchmarkCall_ours_fasthttp — lukaszraczylo/go-telegram with the
// opt-in fasthttp transport (client.NewFastHTTPDoer). Apples-to-apples
// against telego, which also runs on fasthttp by default.
func BenchmarkCall_ours_fasthttp(b *testing.B) {
srv := shared.NewMockServer()
defer srv.Close()
bot := client.New(benchToken,
client.WithBaseURL(srv.URL),
client.WithHTTPClient(client.NewFastHTTPDoer()),
)
ctx := context.Background()
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
_, err := api.SendMessage(ctx, bot, &api.SendMessageParams{
ChatID: api.ChatIDFromInt(42),
Text: "hello",
})
if err != nil {
b.Fatal(err)
}
}
}
// BenchmarkCall_gotba — go-telegram-bot-api/telegram-bot-api/v5.
func BenchmarkCall_gotba(b *testing.B) {
srv := shared.NewMockServer()
defer srv.Close()
// Endpoint is sprintf'd with token + method.
endpoint := srv.URL + "/bot%s/%s"
bot, err := tgbotapi.NewBotAPIWithClient(benchToken, endpoint, &http.Client{})
if err != nil {
b.Fatal(err)
}
msg := tgbotapi.NewMessage(42, "hello")
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
if _, err := bot.Send(msg); err != nil {
b.Fatal(err)
}
}
}
// BenchmarkCall_telebot — gopkg.in/telebot.v3 (tucnak).
func BenchmarkCall_telebot(b *testing.B) {
srv := shared.NewMockServer()
defer srv.Close()
bot, err := tele.NewBot(tele.Settings{
Token: benchToken,
URL: srv.URL,
Synchronous: true,
Offline: true, // skip eager getMe call; we test sending
})
if err != nil {
b.Fatal(err)
}
chat := &tele.Chat{ID: 42}
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
if _, err := bot.Send(chat, "hello"); err != nil {
b.Fatal(err)
}
}
}
// BenchmarkCall_gobot — go-telegram/bot.
func BenchmarkCall_gobot(b *testing.B) {
srv := shared.NewMockServer()
defer srv.Close()
bot, err := gobot.New(benchToken,
gobot.WithServerURL(srv.URL),
gobot.WithSkipGetMe(),
)
if err != nil {
b.Fatal(err)
}
ctx := context.Background()
params := &gobot.SendMessageParams{ChatID: int64(42), Text: "hello"}
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
if _, err := bot.SendMessage(ctx, params); err != nil {
b.Fatal(err)
}
}
}
// BenchmarkCall_telego — mymmrac/telego.
func BenchmarkCall_telego(b *testing.B) {
srv := shared.NewMockServer()
defer srv.Close()
bot, err := telego.NewBot(benchToken, telego.WithAPIServer(srv.URL))
if err != nil {
b.Fatal(err)
}
ctx := context.Background()
params := &telego.SendMessageParams{
ChatID: telego.ChatID{ID: 42},
Text: "hello",
}
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
if _, err := bot.SendMessage(ctx, params); err != nil {
b.Fatal(err)
}
}
}
// BenchmarkCall_echotron — NicoNex/echotron/v3.
//
// echotron expects a base URL ending in /bot<token>/ and ships built-in
// dual-level rate limiting (global 30/s, per-chat 20/min) on its unexported
// lclient field. The setters (SetGlobalRequestLimit / SetChatRequestLimit)
// are methods on the unexported type and have no public accessor through
// the API value, so the rate limiter cannot be disabled from outside the
// package without monkey-patching.
//
// Running this bench against the real path produces ~3s/op driven entirely
// by the per-chat token bucket — measuring rate limiting, not the library.
// We skip rather than publish a misleading number; the rate limiter is a
// feature of echotron and is documented as a caveat in the report.
func BenchmarkCall_echotron(b *testing.B) {
b.Skip("echotron has built-in rate limiting that cannot be disabled via the public API; see comment")
srv := shared.NewMockServer()
defer srv.Close()
base := srv.URL + "/bot" + benchToken + "/"
api := echotron.CustomAPI(base, benchToken)
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
if _, err := api.SendMessage("hello", 42, nil); err != nil {
b.Fatal(err)
}
}
}
// silence unused-import if a build tag strips a lib.
var _ = strings.NewReader
+118
View File
@@ -0,0 +1,118 @@
// Dispatcher routing benchmarks: register 20 handlers across each library's
// dispatcher, feed an update that matches the LAST-registered handler, and
// measure cost per dispatch. Worst-case filter chain traversal.
//
// Coverage notes (see results/raw.txt and report for the full caveats):
//
// - ours, telebot, gobot expose a synchronous single-update entry point
// (Process / ProcessUpdate). Bench measures that path directly.
// - gotba (go-telegram-bot-api/v5) ships no built-in dispatcher; users
// route via a manual switch on Update fields. Skipped here — would be
// comparing our framework against a hand-written switch.
// - telego routes via a buffered channel + goroutine pool inside
// telegohandler.BotHandler. There is no public sync entry, so the bench
// would conflate channel + goroutine overhead with routing cost. Skipped.
// - echotron uses a chat-ID-keyed Dispatcher that fans out to per-chat Bot
// instances; it's a different paradigm (stateful per-chat bot loop), so
// not directly comparable to "match this update against N handlers".
package benchmarks
import (
"context"
"fmt"
"testing"
"github.com/lukaszraczylo/go-telegram/api"
"github.com/lukaszraczylo/go-telegram/client"
"github.com/lukaszraczylo/go-telegram/dispatch"
gobot "github.com/go-telegram/bot"
gobotmodels "github.com/go-telegram/bot/models"
tele "gopkg.in/telebot.v3"
)
const dispatchN = 20
// matchCmd is the command the LAST-registered handler matches.
const matchCmd = "/cmd19"
func BenchmarkDispatch_ours(b *testing.B) {
r := dispatch.New(client.New(benchToken))
noop := func(c *dispatch.Context, m *api.Message) error { return nil }
for i := 0; i < dispatchN; i++ {
r.OnCommand(fmt.Sprintf("/cmd%d", i), noop)
}
u := &api.Update{
UpdateID: 1,
Message: &api.Message{
MessageID: 1,
Date: 0,
Chat: api.Chat{ID: 42, Type: api.ChatTypePrivate},
Text: matchCmd,
Entities: []api.MessageEntity{
{Type: api.MessageEntityTypeBotCommand, Offset: 0, Length: int64(len(matchCmd))},
},
},
}
ctx := context.Background()
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
if err := r.Process(ctx, u); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkDispatch_telebot(b *testing.B) {
bot, err := tele.NewBot(tele.Settings{Token: benchToken, Synchronous: true, Offline: true})
if err != nil {
b.Fatal(err)
}
noop := func(c tele.Context) error { return nil }
for i := 0; i < dispatchN; i++ {
bot.Handle(fmt.Sprintf("/cmd%d", i), noop)
}
u := tele.Update{
ID: 1,
Message: &tele.Message{
ID: 1,
Chat: &tele.Chat{ID: 42, Type: tele.ChatPrivate},
Sender: &tele.User{ID: 42},
Text: matchCmd,
Entities: []tele.MessageEntity{
{Type: tele.EntityCommand, Offset: 0, Length: len(matchCmd)},
},
},
}
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
bot.ProcessUpdate(u)
}
}
func BenchmarkDispatch_gobot(b *testing.B) {
srvless := func(ctx context.Context, b *gobot.Bot, update *gobotmodels.Update) {}
bot, err := gobot.New(benchToken, gobot.WithSkipGetMe(), gobot.WithDefaultHandler(srvless))
if err != nil {
b.Fatal(err)
}
for i := 0; i < dispatchN; i++ {
bot.RegisterHandler(gobot.HandlerTypeMessageText, fmt.Sprintf("/cmd%d", i), gobot.MatchTypeExact, srvless)
}
u := &gobotmodels.Update{
ID: 1,
Message: &gobotmodels.Message{
ID: 1,
Chat: gobotmodels.Chat{ID: 42, Type: gobotmodels.ChatTypePrivate},
Text: matchCmd,
},
}
ctx := context.Background()
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
bot.ProcessUpdate(ctx, u)
}
}
+33
View File
@@ -0,0 +1,33 @@
module github.com/lukaszraczylo/go-telegram/test/benchmarks
go 1.25.7
replace github.com/lukaszraczylo/go-telegram => ../../
require (
github.com/NicoNex/echotron/v3 v3.45.0
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
github.com/go-telegram/bot v1.20.0
github.com/lukaszraczylo/go-telegram v0.0.0-00010101000000-000000000000
github.com/mymmrac/telego v1.8.0
gopkg.in/telebot.v3 v3.3.8
)
require (
github.com/andybalholm/brotli v1.2.1 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/goccy/go-json v0.10.6 // indirect
github.com/grbit/go-json v0.11.0 // indirect
github.com/klauspost/compress v1.18.6 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.71.0 // indirect
github.com/valyala/fastjson v1.6.10 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/time v0.5.0 // indirect
)
+905
View File
@@ -0,0 +1,905 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow=
cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM=
cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M=
cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s=
cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/NicoNex/echotron/v3 v3.45.0 h1:hORujGwc6X2yaiHZIt8/rYZR48L524YtdiHxpqBwlko=
github.com/NicoNex/echotron/v3 v3.45.0/go.mod h1:7LvjveJmezuUOeaoA3nzQduNlSPQYfq219Z+baKY04Q=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
github.com/go-telegram/bot v1.20.0 h1:4Pea/qTidSspr4WBJw9FbHUMNhYeqszBqQUfsQEyFbc=
github.com/go-telegram/bot v1.20.0/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM=
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM=
github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=
github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/grbit/go-json v0.11.0 h1:bAbyMdYrYl/OjYsSqLH99N2DyQ291mHy726Mx+sYrnc=
github.com/grbit/go-json v0.11.0/go.mod h1:IYpHsdybQ386+6g3VE6AXQ3uTGa5mquBme5/ZWmtzek=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0=
github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
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.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mymmrac/telego v1.8.0 h1:EvIprWo9Cn0MHgumvvqNXPAXO1yJj3pu2cdCCeDxbow=
github.com/mymmrac/telego v1.8.0/go.mod h1:pdLV346EgVuq7Xrh3kMggeBiazeHhsdEoK0RTEOPXRM=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.71.0 h1:tepR7H+Guh9VUqxxcPggYi8R3lGUu2Rsdh+z7/FCY3k=
github.com/valyala/fasthttp v1.71.0/go.mod h1:z1sDUvOShhXq/C9mwH/fSm1Vb71tUJwmQdgkBrBNwnA=
github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4=
github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A=
go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU=
go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
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/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
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/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/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-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/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.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
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-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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=
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=
google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU=
google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo=
google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g=
google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA=
google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8=
google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs=
google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=
google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw=
google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=
google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/telebot.v3 v3.3.8 h1:uVDGjak9l824FN9YARWUHMsiNZnlohAVwUycw21k6t8=
gopkg.in/telebot.v3 v3.3.8/go.mod h1:1mlbqcLTVSfK9dx7fdp+Nb5HZsy4LLPtpZTKmwhwtzM=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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.0-20210107192922-496545a6307b/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=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
+75
View File
@@ -0,0 +1,75 @@
goos: darwin
goarch: arm64
pkg: github.com/lukaszraczylo/go-telegram/test/benchmarks
cpu: Apple M4 Max
│ /Users/nvm/Documents/projects/private/go-telegram/test/benchmarks/results/raw.txt │
│ sec/op │
Call_ours-16 39.83µ ± 4%
Call_gotba-16 42.03µ ± 4%
Call_telebot-16 43.41µ ± 1%
Call_gobot-16 61.19µ ± 1%
Call_telego-16 35.84µ ± 1%
Dispatch_ours-16 98.46n ± 2%
Dispatch_telebot-16 270.9n ± 2%
Dispatch_gobot-16 246.1n ± 1%
LargeUnmarshal_ours-16 6.726µ ± 1%
LargeUnmarshal_gotba-16 8.066µ ± 1%
LargeUnmarshal_telebot-16 10.19µ ± 1%
LargeUnmarshal_gobot-16 8.231µ ± 1%
LargeUnmarshal_telego-16 7.849µ ± 2%
LargeUnmarshal_echotron-16 8.123µ ± 1%
Webhook_ours-16 1.832µ ± 4%
Webhook_gotba-16 2.082µ ± 0%
Webhook_telebot-16 2.194µ ± 1%
Webhook_gobot-16 2.082µ ± 1%
Webhook_telego-16 2.143µ ± 2%
Webhook_echotron-16 2.039µ ± 1%
geomean 4.658µ
│ /Users/nvm/Documents/projects/private/go-telegram/test/benchmarks/results/raw.txt │
│ B/op │
Call_ours-16 11.09Ki ± 1%
Call_gotba-16 10.97Ki ± 0%
Call_telebot-16 13.15Ki ± 0%
Call_gobot-16 13.50Ki ± 0%
Call_telego-16 6.547Ki ± 0%
Dispatch_ours-16 128.0 ± 0%
Dispatch_telebot-16 678.5 ± 0%
Dispatch_gobot-16 48.00 ± 0%
LargeUnmarshal_ours-16 5.875Ki ± 0%
LargeUnmarshal_gotba-16 3.438Ki ± 0%
LargeUnmarshal_telebot-16 5.594Ki ± 0%
LargeUnmarshal_gobot-16 4.703Ki ± 0%
LargeUnmarshal_telego-16 6.600Ki ± 0%
LargeUnmarshal_echotron-16 4.219Ki ± 0%
Webhook_ours-16 2.180Ki ± 0%
Webhook_gotba-16 1.461Ki ± 0%
Webhook_telebot-16 1.773Ki ± 0%
Webhook_gobot-16 1.789Ki ± 0%
Webhook_telego-16 3.058Ki ± 0%
Webhook_echotron-16 1.680Ki ± 0%
geomean 2.699Ki
│ /Users/nvm/Documents/projects/private/go-telegram/test/benchmarks/results/raw.txt │
│ allocs/op │
Call_ours-16 102.0 ± 0%
Call_gotba-16 125.0 ± 0%
Call_telebot-16 139.0 ± 0%
Call_gobot-16 176.0 ± 0%
Call_telego-16 48.00 ± 0%
Dispatch_ours-16 3.000 ± 0%
Dispatch_telebot-16 5.000 ± 0%
Dispatch_gobot-16 1.000 ± 0%
LargeUnmarshal_ours-16 34.00 ± 0%
LargeUnmarshal_gotba-16 56.00 ± 0%
LargeUnmarshal_telebot-16 60.00 ± 0%
LargeUnmarshal_gobot-16 50.00 ± 0%
LargeUnmarshal_telego-16 31.00 ± 0%
LargeUnmarshal_echotron-16 56.00 ± 0%
Webhook_ours-16 11.00 ± 0%
Webhook_gotba-16 17.00 ± 0%
Webhook_telebot-16 17.00 ± 0%
Webhook_gobot-16 16.00 ± 0%
Webhook_telego-16 11.00 ± 0%
Webhook_echotron-16 16.00 ± 0%
geomean 26.00
+206
View File
@@ -0,0 +1,206 @@
goos: darwin
goarch: arm64
pkg: github.com/lukaszraczylo/go-telegram/test/benchmarks
cpu: Apple M4 Max
BenchmarkCall_ours-16 27756 39971 ns/op 11497 B/op 103 allocs/op
BenchmarkCall_ours-16 30495 39111 ns/op 11329 B/op 102 allocs/op
BenchmarkCall_ours-16 29250 41427 ns/op 11356 B/op 102 allocs/op
BenchmarkCall_ours-16 29766 39680 ns/op 11366 B/op 102 allocs/op
BenchmarkCall_ours-16 29157 40066 ns/op 11338 B/op 102 allocs/op
BenchmarkCall_ours-16 30567 38404 ns/op 11276 B/op 102 allocs/op
BenchmarkCall_ours-16 30470 38923 ns/op 11306 B/op 102 allocs/op
BenchmarkCall_ours-16 30520 40212 ns/op 11364 B/op 102 allocs/op
BenchmarkCall_ours-16 30315 39595 ns/op 11361 B/op 102 allocs/op
BenchmarkCall_ours-16 28747 41549 ns/op 11434 B/op 102 allocs/op
BenchmarkCall_gotba-16 28140 43735 ns/op 11255 B/op 125 allocs/op
BenchmarkCall_gotba-16 27189 43528 ns/op 11247 B/op 125 allocs/op
BenchmarkCall_gotba-16 27940 43644 ns/op 11259 B/op 125 allocs/op
BenchmarkCall_gotba-16 29090 41643 ns/op 11232 B/op 125 allocs/op
BenchmarkCall_gotba-16 28002 42461 ns/op 11183 B/op 125 allocs/op
BenchmarkCall_gotba-16 28578 42082 ns/op 11204 B/op 125 allocs/op
BenchmarkCall_gotba-16 28549 41973 ns/op 11237 B/op 125 allocs/op
BenchmarkCall_gotba-16 29086 41702 ns/op 11203 B/op 125 allocs/op
BenchmarkCall_gotba-16 29630 41783 ns/op 11262 B/op 125 allocs/op
BenchmarkCall_gotba-16 28371 41810 ns/op 11217 B/op 125 allocs/op
BenchmarkCall_telebot-16 27510 43416 ns/op 13457 B/op 139 allocs/op
BenchmarkCall_telebot-16 28102 43319 ns/op 13473 B/op 139 allocs/op
BenchmarkCall_telebot-16 27558 43530 ns/op 13417 B/op 139 allocs/op
BenchmarkCall_telebot-16 27274 43654 ns/op 13445 B/op 139 allocs/op
BenchmarkCall_telebot-16 27627 43530 ns/op 13489 B/op 139 allocs/op
BenchmarkCall_telebot-16 27499 42836 ns/op 13467 B/op 139 allocs/op
BenchmarkCall_telebot-16 27860 43375 ns/op 13457 B/op 139 allocs/op
BenchmarkCall_telebot-16 27711 43400 ns/op 13439 B/op 139 allocs/op
BenchmarkCall_telebot-16 27668 43472 ns/op 13482 B/op 139 allocs/op
BenchmarkCall_telebot-16 28063 43182 ns/op 13487 B/op 139 allocs/op
BenchmarkCall_gobot-16 19645 60805 ns/op 13879 B/op 176 allocs/op
BenchmarkCall_gobot-16 19562 61374 ns/op 13823 B/op 176 allocs/op
BenchmarkCall_gobot-16 19575 60944 ns/op 13823 B/op 176 allocs/op
BenchmarkCall_gobot-16 19538 61461 ns/op 13844 B/op 176 allocs/op
BenchmarkCall_gobot-16 19624 61253 ns/op 13806 B/op 176 allocs/op
BenchmarkCall_gobot-16 19617 61127 ns/op 13824 B/op 176 allocs/op
BenchmarkCall_gobot-16 19516 61568 ns/op 13775 B/op 176 allocs/op
BenchmarkCall_gobot-16 19514 61340 ns/op 13828 B/op 176 allocs/op
BenchmarkCall_gobot-16 19392 60426 ns/op 13863 B/op 176 allocs/op
BenchmarkCall_gobot-16 23968 49951 ns/op 13844 B/op 176 allocs/op
BenchmarkCall_telego-16 33622 35493 ns/op 6780 B/op 48 allocs/op
BenchmarkCall_telego-16 33874 35438 ns/op 6703 B/op 48 allocs/op
BenchmarkCall_telego-16 34560 35482 ns/op 6704 B/op 48 allocs/op
BenchmarkCall_telego-16 33298 35830 ns/op 6711 B/op 48 allocs/op
BenchmarkCall_telego-16 33205 35946 ns/op 6706 B/op 48 allocs/op
BenchmarkCall_telego-16 33428 35949 ns/op 6707 B/op 48 allocs/op
BenchmarkCall_telego-16 33452 35974 ns/op 6692 B/op 48 allocs/op
BenchmarkCall_telego-16 33056 35853 ns/op 6705 B/op 48 allocs/op
BenchmarkCall_telego-16 33120 35808 ns/op 6703 B/op 48 allocs/op
BenchmarkCall_telego-16 33450 38996 ns/op 6699 B/op 48 allocs/op
BenchmarkDispatch_ours-16 12381547 96.05 ns/op 128 B/op 3 allocs/op
BenchmarkDispatch_ours-16 12636062 99.34 ns/op 128 B/op 3 allocs/op
BenchmarkDispatch_ours-16 12161170 98.43 ns/op 128 B/op 3 allocs/op
BenchmarkDispatch_ours-16 12205023 97.90 ns/op 128 B/op 3 allocs/op
BenchmarkDispatch_ours-16 12590581 98.83 ns/op 128 B/op 3 allocs/op
BenchmarkDispatch_ours-16 12033376 99.15 ns/op 128 B/op 3 allocs/op
BenchmarkDispatch_ours-16 12049588 98.48 ns/op 128 B/op 3 allocs/op
BenchmarkDispatch_ours-16 12324108 98.38 ns/op 128 B/op 3 allocs/op
BenchmarkDispatch_ours-16 11924947 96.71 ns/op 128 B/op 3 allocs/op
BenchmarkDispatch_ours-16 11940064 99.26 ns/op 128 B/op 3 allocs/op
BenchmarkDispatch_telebot-16 4456072 262.5 ns/op 678 B/op 5 allocs/op
BenchmarkDispatch_telebot-16 4330234 275.3 ns/op 678 B/op 5 allocs/op
BenchmarkDispatch_telebot-16 4478779 268.7 ns/op 679 B/op 5 allocs/op
BenchmarkDispatch_telebot-16 4394821 282.5 ns/op 678 B/op 5 allocs/op
BenchmarkDispatch_telebot-16 4376773 271.6 ns/op 679 B/op 5 allocs/op
BenchmarkDispatch_telebot-16 4516370 268.7 ns/op 678 B/op 5 allocs/op
BenchmarkDispatch_telebot-16 4465942 276.0 ns/op 678 B/op 5 allocs/op
BenchmarkDispatch_telebot-16 4399328 270.1 ns/op 679 B/op 5 allocs/op
BenchmarkDispatch_telebot-16 4531597 268.3 ns/op 679 B/op 5 allocs/op
BenchmarkDispatch_telebot-16 4376616 272.3 ns/op 679 B/op 5 allocs/op
BenchmarkDispatch_gobot-16 4911369 249.4 ns/op 48 B/op 1 allocs/op
BenchmarkDispatch_gobot-16 4896456 246.3 ns/op 48 B/op 1 allocs/op
BenchmarkDispatch_gobot-16 4789376 246.4 ns/op 48 B/op 1 allocs/op
BenchmarkDispatch_gobot-16 4949206 247.7 ns/op 48 B/op 1 allocs/op
BenchmarkDispatch_gobot-16 4902912 243.1 ns/op 48 B/op 1 allocs/op
BenchmarkDispatch_gobot-16 4913300 244.7 ns/op 48 B/op 1 allocs/op
BenchmarkDispatch_gobot-16 4925991 245.5 ns/op 48 B/op 1 allocs/op
BenchmarkDispatch_gobot-16 4817457 245.9 ns/op 48 B/op 1 allocs/op
BenchmarkDispatch_gobot-16 4943328 245.8 ns/op 49 B/op 1 allocs/op
BenchmarkDispatch_gobot-16 4751266 248.1 ns/op 48 B/op 1 allocs/op
BenchmarkLargeUnmarshal_ours-16 180361 6682 ns/op 6019 B/op 34 allocs/op
BenchmarkLargeUnmarshal_ours-16 178388 6765 ns/op 6016 B/op 34 allocs/op
BenchmarkLargeUnmarshal_ours-16 182600 6701 ns/op 6016 B/op 34 allocs/op
BenchmarkLargeUnmarshal_ours-16 178785 6710 ns/op 6016 B/op 34 allocs/op
BenchmarkLargeUnmarshal_ours-16 181588 6726 ns/op 6016 B/op 34 allocs/op
BenchmarkLargeUnmarshal_ours-16 177378 6730 ns/op 6016 B/op 34 allocs/op
BenchmarkLargeUnmarshal_ours-16 181004 6729 ns/op 6016 B/op 34 allocs/op
BenchmarkLargeUnmarshal_ours-16 176672 6682 ns/op 6016 B/op 34 allocs/op
BenchmarkLargeUnmarshal_ours-16 184182 6726 ns/op 6016 B/op 34 allocs/op
BenchmarkLargeUnmarshal_ours-16 179983 6813 ns/op 6016 B/op 34 allocs/op
BenchmarkLargeUnmarshal_gotba-16 138108 8579 ns/op 3520 B/op 56 allocs/op
BenchmarkLargeUnmarshal_gotba-16 148593 8143 ns/op 3520 B/op 56 allocs/op
BenchmarkLargeUnmarshal_gotba-16 147964 8075 ns/op 3520 B/op 56 allocs/op
BenchmarkLargeUnmarshal_gotba-16 147601 8161 ns/op 3520 B/op 56 allocs/op
BenchmarkLargeUnmarshal_gotba-16 148257 8020 ns/op 3520 B/op 56 allocs/op
BenchmarkLargeUnmarshal_gotba-16 150858 8058 ns/op 3519 B/op 56 allocs/op
BenchmarkLargeUnmarshal_gotba-16 149251 8040 ns/op 3520 B/op 56 allocs/op
BenchmarkLargeUnmarshal_gotba-16 151614 8054 ns/op 3520 B/op 56 allocs/op
BenchmarkLargeUnmarshal_gotba-16 152306 8050 ns/op 3519 B/op 56 allocs/op
BenchmarkLargeUnmarshal_gotba-16 152979 8094 ns/op 3520 B/op 56 allocs/op
BenchmarkLargeUnmarshal_telebot-16 119103 10113 ns/op 5728 B/op 60 allocs/op
BenchmarkLargeUnmarshal_telebot-16 115418 10247 ns/op 5728 B/op 60 allocs/op
BenchmarkLargeUnmarshal_telebot-16 116160 10260 ns/op 5728 B/op 60 allocs/op
BenchmarkLargeUnmarshal_telebot-16 117031 10346 ns/op 5728 B/op 60 allocs/op
BenchmarkLargeUnmarshal_telebot-16 116731 10311 ns/op 5728 B/op 60 allocs/op
BenchmarkLargeUnmarshal_telebot-16 121227 10135 ns/op 5728 B/op 60 allocs/op
BenchmarkLargeUnmarshal_telebot-16 119989 10178 ns/op 5728 B/op 60 allocs/op
BenchmarkLargeUnmarshal_telebot-16 119311 10194 ns/op 5728 B/op 60 allocs/op
BenchmarkLargeUnmarshal_telebot-16 119388 10183 ns/op 5728 B/op 60 allocs/op
BenchmarkLargeUnmarshal_telebot-16 120195 10133 ns/op 5728 B/op 60 allocs/op
BenchmarkLargeUnmarshal_gobot-16 146700 8235 ns/op 4817 B/op 50 allocs/op
BenchmarkLargeUnmarshal_gobot-16 147666 8230 ns/op 4816 B/op 50 allocs/op
BenchmarkLargeUnmarshal_gobot-16 148058 8212 ns/op 4816 B/op 50 allocs/op
BenchmarkLargeUnmarshal_gobot-16 148092 8210 ns/op 4816 B/op 50 allocs/op
BenchmarkLargeUnmarshal_gobot-16 146656 8208 ns/op 4816 B/op 50 allocs/op
BenchmarkLargeUnmarshal_gobot-16 148036 8259 ns/op 4816 B/op 50 allocs/op
BenchmarkLargeUnmarshal_gobot-16 146211 8287 ns/op 4816 B/op 50 allocs/op
BenchmarkLargeUnmarshal_gobot-16 146793 8279 ns/op 4816 B/op 50 allocs/op
BenchmarkLargeUnmarshal_gobot-16 146083 8232 ns/op 4816 B/op 50 allocs/op
BenchmarkLargeUnmarshal_gobot-16 147385 8221 ns/op 4816 B/op 50 allocs/op
BenchmarkLargeUnmarshal_telego-16 150852 8155 ns/op 6762 B/op 31 allocs/op
BenchmarkLargeUnmarshal_telego-16 152559 8040 ns/op 6758 B/op 31 allocs/op
BenchmarkLargeUnmarshal_telego-16 146462 7989 ns/op 6757 B/op 31 allocs/op
BenchmarkLargeUnmarshal_telego-16 154480 7842 ns/op 6759 B/op 31 allocs/op
BenchmarkLargeUnmarshal_telego-16 155208 7811 ns/op 6759 B/op 31 allocs/op
BenchmarkLargeUnmarshal_telego-16 149310 7848 ns/op 6759 B/op 31 allocs/op
BenchmarkLargeUnmarshal_telego-16 156939 7835 ns/op 6757 B/op 31 allocs/op
BenchmarkLargeUnmarshal_telego-16 154064 7867 ns/op 6759 B/op 31 allocs/op
BenchmarkLargeUnmarshal_telego-16 153975 7849 ns/op 6758 B/op 31 allocs/op
BenchmarkLargeUnmarshal_telego-16 152520 7811 ns/op 6758 B/op 31 allocs/op
BenchmarkLargeUnmarshal_echotron-16 145399 8167 ns/op 4323 B/op 56 allocs/op
BenchmarkLargeUnmarshal_echotron-16 147351 8124 ns/op 4319 B/op 56 allocs/op
BenchmarkLargeUnmarshal_echotron-16 148053 8094 ns/op 4320 B/op 56 allocs/op
BenchmarkLargeUnmarshal_echotron-16 147170 8124 ns/op 4320 B/op 56 allocs/op
BenchmarkLargeUnmarshal_echotron-16 147792 8092 ns/op 4320 B/op 56 allocs/op
BenchmarkLargeUnmarshal_echotron-16 145971 8105 ns/op 4320 B/op 56 allocs/op
BenchmarkLargeUnmarshal_echotron-16 148708 8122 ns/op 4320 B/op 56 allocs/op
BenchmarkLargeUnmarshal_echotron-16 147525 8105 ns/op 4320 B/op 56 allocs/op
BenchmarkLargeUnmarshal_echotron-16 148635 8165 ns/op 4320 B/op 56 allocs/op
BenchmarkLargeUnmarshal_echotron-16 150871 8124 ns/op 4320 B/op 56 allocs/op
BenchmarkWebhook_ours-16 705109 1911 ns/op 2232 B/op 11 allocs/op
BenchmarkWebhook_ours-16 651456 1911 ns/op 2232 B/op 11 allocs/op
BenchmarkWebhook_ours-16 683557 1875 ns/op 2232 B/op 11 allocs/op
BenchmarkWebhook_ours-16 668317 1830 ns/op 2232 B/op 11 allocs/op
BenchmarkWebhook_ours-16 703718 1833 ns/op 2232 B/op 11 allocs/op
BenchmarkWebhook_ours-16 707150 1844 ns/op 2232 B/op 11 allocs/op
BenchmarkWebhook_ours-16 700472 1810 ns/op 2232 B/op 11 allocs/op
BenchmarkWebhook_ours-16 699192 1828 ns/op 2232 B/op 11 allocs/op
BenchmarkWebhook_ours-16 671794 1828 ns/op 2232 B/op 11 allocs/op
BenchmarkWebhook_ours-16 687226 1826 ns/op 2232 B/op 11 allocs/op
BenchmarkWebhook_gotba-16 544628 2080 ns/op 1496 B/op 17 allocs/op
BenchmarkWebhook_gotba-16 565494 2081 ns/op 1496 B/op 17 allocs/op
BenchmarkWebhook_gotba-16 579736 2081 ns/op 1496 B/op 17 allocs/op
BenchmarkWebhook_gotba-16 581365 2082 ns/op 1496 B/op 17 allocs/op
BenchmarkWebhook_gotba-16 580501 2075 ns/op 1496 B/op 17 allocs/op
BenchmarkWebhook_gotba-16 582160 2091 ns/op 1496 B/op 17 allocs/op
BenchmarkWebhook_gotba-16 563130 2089 ns/op 1496 B/op 17 allocs/op
BenchmarkWebhook_gotba-16 573872 2088 ns/op 1496 B/op 17 allocs/op
BenchmarkWebhook_gotba-16 569616 2093 ns/op 1496 B/op 17 allocs/op
BenchmarkWebhook_gotba-16 599923 2069 ns/op 1496 B/op 17 allocs/op
BenchmarkWebhook_telebot-16 562394 2162 ns/op 1816 B/op 17 allocs/op
BenchmarkWebhook_telebot-16 562137 2272 ns/op 1816 B/op 17 allocs/op
BenchmarkWebhook_telebot-16 488323 2220 ns/op 1816 B/op 17 allocs/op
BenchmarkWebhook_telebot-16 538561 2199 ns/op 1816 B/op 17 allocs/op
BenchmarkWebhook_telebot-16 581037 2187 ns/op 1816 B/op 17 allocs/op
BenchmarkWebhook_telebot-16 540055 2213 ns/op 1816 B/op 17 allocs/op
BenchmarkWebhook_telebot-16 546921 2203 ns/op 1816 B/op 17 allocs/op
BenchmarkWebhook_telebot-16 574988 2188 ns/op 1816 B/op 17 allocs/op
BenchmarkWebhook_telebot-16 588076 2154 ns/op 1816 B/op 17 allocs/op
BenchmarkWebhook_telebot-16 571617 2181 ns/op 1816 B/op 17 allocs/op
BenchmarkWebhook_gobot-16 588786 2080 ns/op 1832 B/op 16 allocs/op
BenchmarkWebhook_gobot-16 551653 2080 ns/op 1832 B/op 16 allocs/op
BenchmarkWebhook_gobot-16 590686 2084 ns/op 1832 B/op 16 allocs/op
BenchmarkWebhook_gobot-16 588870 2096 ns/op 1832 B/op 16 allocs/op
BenchmarkWebhook_gobot-16 606735 2064 ns/op 1832 B/op 16 allocs/op
BenchmarkWebhook_gobot-16 597108 2084 ns/op 1832 B/op 16 allocs/op
BenchmarkWebhook_gobot-16 600633 2069 ns/op 1832 B/op 16 allocs/op
BenchmarkWebhook_gobot-16 589102 2110 ns/op 1832 B/op 16 allocs/op
BenchmarkWebhook_gobot-16 583528 2104 ns/op 1832 B/op 16 allocs/op
BenchmarkWebhook_gobot-16 599022 2073 ns/op 1832 B/op 16 allocs/op
BenchmarkWebhook_telego-16 587408 2193 ns/op 3131 B/op 11 allocs/op
BenchmarkWebhook_telego-16 601533 2152 ns/op 3131 B/op 11 allocs/op
BenchmarkWebhook_telego-16 584689 2127 ns/op 3131 B/op 11 allocs/op
BenchmarkWebhook_telego-16 576732 2153 ns/op 3131 B/op 11 allocs/op
BenchmarkWebhook_telego-16 568095 2143 ns/op 3131 B/op 11 allocs/op
BenchmarkWebhook_telego-16 553896 2132 ns/op 3131 B/op 11 allocs/op
BenchmarkWebhook_telego-16 579055 2130 ns/op 3131 B/op 11 allocs/op
BenchmarkWebhook_telego-16 595776 2144 ns/op 3131 B/op 11 allocs/op
BenchmarkWebhook_telego-16 573843 2134 ns/op 3131 B/op 11 allocs/op
BenchmarkWebhook_telego-16 513824 2267 ns/op 3131 B/op 11 allocs/op
BenchmarkWebhook_echotron-16 587734 2178 ns/op 1720 B/op 16 allocs/op
BenchmarkWebhook_echotron-16 557188 2039 ns/op 1720 B/op 16 allocs/op
BenchmarkWebhook_echotron-16 600524 2020 ns/op 1720 B/op 16 allocs/op
BenchmarkWebhook_echotron-16 602959 2019 ns/op 1720 B/op 16 allocs/op
BenchmarkWebhook_echotron-16 588694 2048 ns/op 1720 B/op 16 allocs/op
BenchmarkWebhook_echotron-16 602366 2039 ns/op 1720 B/op 16 allocs/op
BenchmarkWebhook_echotron-16 604621 2031 ns/op 1720 B/op 16 allocs/op
BenchmarkWebhook_echotron-16 599146 2037 ns/op 1720 B/op 16 allocs/op
BenchmarkWebhook_echotron-16 586056 2051 ns/op 1720 B/op 16 allocs/op
BenchmarkWebhook_echotron-16 598456 2049 ns/op 1720 B/op 16 allocs/op
PASS
ok github.com/lukaszraczylo/go-telegram/test/benchmarks 242.858s
+85
View File
@@ -0,0 +1,85 @@
// Package shared holds JSON fixtures and an httptest mock server reused by
// every per-library benchmark. Keeping fixtures here guarantees that all
// libraries decode the same bytes and that round-trip benches hit the same
// canned response.
package shared
import (
"io"
"net/http"
"net/http/httptest"
"strings"
)
// SmallUpdateJSON is a minimal text-message update — what a typical bot sees
// most often. Used by the dispatcher and webhook benches.
const SmallUpdateJSON = `{
"update_id": 123456789,
"message": {
"message_id": 1,
"date": 1715000000,
"chat": {"id": 42, "type": "private", "first_name": "Alice"},
"from": {"id": 42, "is_bot": false, "first_name": "Alice", "language_code": "en"},
"text": "/start"
}
}`
// LargeUpdateJSON exercises union/discriminator decoding: text + entities,
// reply markup with an inline keyboard, and a 3-size photo array.
const LargeUpdateJSON = `{
"update_id": 987654321,
"message": {
"message_id": 17,
"date": 1715000123,
"chat": {"id": -100123456789, "type": "supergroup", "title": "Devs"},
"from": {"id": 42, "is_bot": false, "first_name": "Alice", "username": "alice"},
"text": "see https://example.com and @bob too",
"entities": [
{"type": "url", "offset": 4, "length": 19},
{"type": "mention", "offset": 28, "length": 4},
{"type": "bold", "offset": 0, "length": 3}
],
"reply_markup": {
"inline_keyboard": [
[{"text": "ok", "callback_data": "ok:1"}, {"text": "no", "callback_data": "no:1"}, {"text": "more", "callback_data": "more:1"}],
[{"text": "left", "callback_data": "p:l"}, {"text": "right", "callback_data": "p:r"}]
]
},
"photo": [
{"file_id": "AgAD1", "file_unique_id": "u1", "width": 90, "height": 67, "file_size": 1234},
{"file_id": "AgAD2", "file_unique_id": "u2", "width": 320, "height": 240, "file_size": 12345},
{"file_id": "AgAD3", "file_unique_id": "u3", "width": 800, "height": 600, "file_size": 123456}
]
}
}`
// SendMessageOKResponse is the canned `{"ok":true,"result":Message}` body
// returned by the mock server for SendMessage round-trips.
const SendMessageOKResponse = `{"ok":true,"result":{"message_id":1,"date":1715000000,"chat":{"id":42,"type":"private","first_name":"Alice"},"from":{"id":7,"is_bot":true,"first_name":"Bot","username":"benchbot"},"text":"hello"}}`
// GetMeOKResponse is the canned getMe reply some libraries call eagerly during
// constructor (telebot, echotron). Lets us avoid a real network hop in setup.
const GetMeOKResponse = `{"ok":true,"result":{"id":7,"is_bot":true,"first_name":"Bot","username":"benchbot","can_join_groups":true,"can_read_all_group_messages":false,"supports_inline_queries":false}}`
// NewMockServer returns an httptest.Server that responds to every Bot API
// path with a canned `ok:true` body chosen by suffix:
// - /sendMessage → SendMessageOKResponse
// - /getMe → GetMeOKResponse
// - anything else → SendMessageOKResponse (safe default for benches)
//
// Caller is responsible for Close().
func NewMockServer() *httptest.Server {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Drain body so the client sees a complete request/response cycle.
_, _ = io.Copy(io.Discard, r.Body)
_ = r.Body.Close()
w.Header().Set("Content-Type", "application/json")
switch {
case strings.HasSuffix(r.URL.Path, "/getMe"):
_, _ = io.WriteString(w, GetMeOKResponse)
default:
_, _ = io.WriteString(w, SendMessageOKResponse)
}
})
return httptest.NewServer(handler)
}
+80
View File
@@ -0,0 +1,80 @@
// Large-Update unmarshal benchmarks: decode a realistic Update with text +
// entities + a 2x3 inline keyboard + a 3-size photo array. Stresses each
// library's union/discriminator decoding (entities, reply markup variants).
package benchmarks
import (
"encoding/json"
"testing"
"github.com/lukaszraczylo/go-telegram/api"
"github.com/lukaszraczylo/go-telegram/test/benchmarks/shared"
echotron "github.com/NicoNex/echotron/v3"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
gobotmodels "github.com/go-telegram/bot/models"
telego "github.com/mymmrac/telego"
tele "gopkg.in/telebot.v3"
)
var largeUpdateBytes = []byte(shared.LargeUpdateJSON)
func BenchmarkLargeUnmarshal_ours(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
var u api.Update
if err := json.Unmarshal(largeUpdateBytes, &u); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkLargeUnmarshal_gotba(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
var u tgbotapi.Update
if err := json.Unmarshal(largeUpdateBytes, &u); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkLargeUnmarshal_telebot(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
var u tele.Update
if err := json.Unmarshal(largeUpdateBytes, &u); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkLargeUnmarshal_gobot(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
var u gobotmodels.Update
if err := json.Unmarshal(largeUpdateBytes, &u); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkLargeUnmarshal_telego(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
var u telego.Update
if err := json.Unmarshal(largeUpdateBytes, &u); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkLargeUnmarshal_echotron(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
var u echotron.Update
if err := json.Unmarshal(largeUpdateBytes, &u); err != nil {
b.Fatal(err)
}
}
}
+80
View File
@@ -0,0 +1,80 @@
// Webhook decode benchmarks: parse a small text-message Update from a fixed
// JSON payload using each library's typed Update struct. Pure CPU — no
// network. Stresses the JSON codec each library ships with by default.
package benchmarks
import (
"encoding/json"
"testing"
"github.com/lukaszraczylo/go-telegram/api"
"github.com/lukaszraczylo/go-telegram/test/benchmarks/shared"
echotron "github.com/NicoNex/echotron/v3"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
gobotmodels "github.com/go-telegram/bot/models"
telego "github.com/mymmrac/telego"
tele "gopkg.in/telebot.v3"
)
var smallUpdateBytes = []byte(shared.SmallUpdateJSON)
func BenchmarkWebhook_ours(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
var u api.Update
if err := json.Unmarshal(smallUpdateBytes, &u); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkWebhook_gotba(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
var u tgbotapi.Update
if err := json.Unmarshal(smallUpdateBytes, &u); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkWebhook_telebot(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
var u tele.Update
if err := json.Unmarshal(smallUpdateBytes, &u); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkWebhook_gobot(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
var u gobotmodels.Update
if err := json.Unmarshal(smallUpdateBytes, &u); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkWebhook_telego(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
var u telego.Update
if err := json.Unmarshal(smallUpdateBytes, &u); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkWebhook_echotron(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
var u echotron.Update
if err := json.Unmarshal(smallUpdateBytes, &u); err != nil {
b.Fatal(err)
}
}
}
+1 -5
View File
@@ -72,11 +72,7 @@ func (p *LongPoller) Run(ctx context.Context) error {
params.Timeout = &to
}
if len(p.AllowedTypes) > 0 {
allowed := make([]string, len(p.AllowedTypes))
for i, t := range p.AllowedTypes {
allowed[i] = string(t)
}
params.AllowedUpdates = allowed
params.AllowedUpdates = p.AllowedTypes
}
ups, err := api.GetUpdates(ctx, p.Bot, params)
if err != nil {
+33 -15
View File
@@ -6,6 +6,7 @@
package transport
import (
"bytes"
"context"
"crypto/subtle"
"errors"
@@ -18,6 +19,24 @@ import (
"github.com/lukaszraczylo/go-telegram/client"
)
// webhookBufPool reuses *bytes.Buffer for incoming webhook bodies.
// Webhook payloads are typically a single Telegram Update (commonly
// 1-8 KiB), so a buffer that has grown once will satisfy most
// subsequent requests with no additional allocation.
var webhookBufPool = sync.Pool{New: func() any { return new(bytes.Buffer) }}
// maxWebhookBufCap caps the buffer size returned to webhookBufPool so
// a rare oversized update doesn't permanently bloat the pool. Buffers
// larger than this are dropped on the floor.
const maxWebhookBufCap = 256 * 1024
func putWebhookBuf(buf *bytes.Buffer) {
if buf.Cap() > maxWebhookBufCap {
return
}
webhookBufPool.Put(buf)
}
// WebhookServer implements Updater by exposing an http.Handler that
// receives updates from Telegram. It can be mounted on the user's own
// HTTP server (via ServeHTTP) or run standalone (via ListenAndServe).
@@ -108,28 +127,27 @@ func (w *WebhookServer) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
}
w.handlers.Add(1)
defer w.handlers.Done()
const maxBody = 1 << 20 // 1 MiB cap on body
r.Body = http.MaxBytesReader(rw, r.Body, maxBody)
defer func() { _ = r.Body.Close() }()
const max = 1 << 20 // 1 MiB cap on body
buf := make([]byte, 0, 1024)
tmp := make([]byte, 4096)
for {
n, err := r.Body.Read(tmp)
if n > 0 {
buf = append(buf, tmp[:n]...)
if len(buf) > max {
rw.WriteHeader(http.StatusRequestEntityTooLarge)
return
}
}
if errors.Is(err, http.ErrBodyReadAfterClose) || err != nil {
break
buf := webhookBufPool.Get().(*bytes.Buffer)
buf.Reset()
defer putWebhookBuf(buf)
if _, err := buf.ReadFrom(r.Body); err != nil {
var maxErr *http.MaxBytesError
if errors.As(err, &maxErr) {
rw.WriteHeader(http.StatusRequestEntityTooLarge)
return
}
rw.WriteHeader(http.StatusBadRequest)
return
}
var u api.Update
codec := w.Bot.Codec()
if err := codec.Unmarshal(buf, &u); err != nil {
if err := codec.Unmarshal(buf.Bytes(), &u); err != nil {
rw.WriteHeader(http.StatusBadRequest)
return
}
+39
View File
@@ -0,0 +1,39 @@
package transport
import (
"bytes"
"net/http"
"net/http/httptest"
"testing"
"github.com/lukaszraczylo/go-telegram/client"
)
const benchUpdateBody = `{"update_id":12345,"message":{"message_id":1,"date":1700000000,"chat":{"id":42,"type":"private"},"from":{"id":42,"is_bot":false,"first_name":"User"},"text":"hello world"}}`
func BenchmarkWebhook_ServeHTTP(b *testing.B) {
w := NewWebhookServer(client.New("t"), WithBufferSize(1024))
body := []byte(benchUpdateBody)
done := make(chan struct{})
go func() {
for {
select {
case <-w.Updates():
case <-done:
return
}
}
}()
defer close(done)
b.ReportAllocs()
for b.Loop() {
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body))
rec := httptest.NewRecorder()
w.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
b.Fatalf("status %d", rec.Code)
}
}
}