Compare commits

..
45 Commits
Author SHA1 Message Date
github-actions[bot]andGitHub 2276ce81ed chore(deps): update dependencies
Automated dependency update. All tests passed.
2026-10-10 03:26:21 +00:00
github-actions[bot]andGitHub ae45e17f43 chore(deps): update module github.com/valyala/fasthttp to v1.75.0
Automated dependency update. All tests passed.
2026-10-08 03:27:17 +00:00
github-actions[bot]andGitHub 8cb03ef0e9 chore(deps): update module github.com/molecule-man/go-brrr to v1.2.0
Automated dependency update. All tests passed.
2026-10-06 03:24:29 +00:00
lukaszraczylo d535029beb fix(ci): read regen Go version from go.mod 2026-10-05 10:31:05 +01:00
github-actions[bot]andGitHub 23cca1df8d chore(deps): update dependencies
Automated dependency update. All tests passed.
2026-10-04 04:23:05 +00:00
lukaszraczylo 131bbca0ed ci(deps): update dependencies with Renovate [skip ci] 2026-10-03 18:44:32 +01:00
ba6f76f59f Update go.mod and go.sum (#20)
Co-authored-by: lukaszraczylo <[email protected]>
2026-10-03 03:29:26 +00:00
lukaszraczyloandGitHub 93ebf41a3c ci: use the shared pull request, release and autoupdate workflows (#19)
* ci(workflows): migrate to shared go-pr and go-release workflows

Replace the generic vet, staticcheck, govulncheck, gosec, test and release jobs with the shared go-pr and go-release workflows.
Keep the codegen-clean and audit jobs in ci.yml. Add an examples build job and the ci-success aggregator there.
Add autoupdate.yaml from the shared go-autoupdate workflow.
The release job now runs only on Go source changes and on manual dispatch.
The dry-run input and the Bot API note in the tag message no longer exist.

* ci(release): gate release on repo checks and export Bot API version

Move the codegen-clean, audit and examples jobs into the reusable checks.yml workflow. The ci.yml and release.yml workflows both call it.
The release job now needs the checks job. A failed or cancelled check blocks the release.
Add workflow-prepare.sh. It appends BOT_API_VERSION to GITHUB_ENV so the GoReleaser header shows the Bot API version.
The script does nothing when GITHUB_ENV is unset.

* ci(regen): name release.yml in the merge step comments

The release now runs from release.yml. The comments still named ci.yml as the workflow that cuts the release.
2026-10-02 04:42:00 +01:00
lukaszraczylo 53afd9890f ci: pin govulncheck to v1.7.0
govulncheck@latest resolves to v1.8.0, which declares go 1.26.0 in its
module file. Every job in this workflow pins go-version 1.25.x, so the
install step fails before any scanning runs, which fails ci-success and
skips the release job.

v1.7.0 declares go 1.25.0 and installs on the pinned toolchain. This also
matches the convention used for staticcheck, which is already pinned to
an exact version. Pinning the binary does not freeze the vulnerability
database, which govulncheck fetches at run time.

Claude-Session: https://claude.ai/code/session_01Fu2wXqGUrfVBi5aLafKk8U
2026-09-15 11:34:33 +01:00
lukaszraczylo d666cda3b1 feat(genapi): derive discriminators and decode RichText alternates
The generator gated all union decoder emission behind knownDiscriminators, a
hand-curated map of 13 entries. Any sealed union absent from that map received
no decoder, so decoding must fail. Bot API v10 added RichBlock and RichText,
which were absent. Roughly 8.6 percent of Telegram updates failed with HTTP 400.

The generator now derives discriminators from the specification instead of a
fixed list. A gate check fails the build when a union resolves no discriminator.
Eleven unions gain decoders that never had one: BotCommandScope, InputMedia,
InputPaidMedia, InputPollMedia, InputPollOptionMedia, InputProfilePhoto,
InputRichBlock, InputStoryContent, PassportElementError, RichBlock and RichText.

RichText can also appear as a bare string or an array of RichText. The scraper
now extracts these alternate shapes into the intermediate representation. The
emitter adds RichTextPlain and RichTextSequence. The decoder dispatches on the
first JSON byte before it reads any discriminator.

Unions without alternates must regenerate byte for byte. The golden test proves
this.

Claude-Session: https://claude.ai/code/session_01Fu2wXqGUrfVBi5aLafKk8U
2026-09-15 10:59:48 +01:00
lukaszraczylo fa84f08b25 ci(regen): stop the weekly no-op update PR
The Telegram page ends with a "page generated in NN.NNms" comment that
changes on every fetch. Change detection saw it, so the run dispatched after
the codegen fix opened and merged a PR whose only content was that comment.
With the merge step in place, that becomes a pointless release every week.

Strip the comment on capture, and gate change detection on the generated
surface instead of testdata/html/, because the snapshot file name and the
latest.html symlink change every run by design. A fresh fetch now matches the
committed snapshot byte for byte while the API is unchanged.
2026-09-15 10:55:38 +01:00
54790f3e5b chore(api): regenerate from Telegram Bot API v10.3 (#18)
Co-authored-by: lukaszraczylo <[email protected]>
2026-09-14 06:40:10 +00:00
ab5f2c5700 chore(api): regenerate from Telegram Bot API v10.3 (#17)
Co-authored-by: lukaszraczylo <[email protected]>
2026-09-07 06:39:21 +00:00
926d51881d chore(api): regenerate from Telegram Bot API v10.3 (#16)
Co-authored-by: lukaszraczylo <[email protected]>
2026-08-31 06:39:17 +00:00
4511c823a3 chore(api): regenerate from Telegram Bot API v10.2 (#15)
Co-authored-by: lukaszraczylo <[email protected]>
2026-08-24 06:21:30 +00:00
7c5aca3bf8 chore(api): regenerate from Telegram Bot API v10.2 (#14)
Co-authored-by: lukaszraczylo <[email protected]>
2026-08-17 06:19:14 +00:00
4e6382ceeb chore(api): regenerate from Telegram Bot API v10.2 (#13)
Co-authored-by: lukaszraczylo <[email protected]>
2026-08-10 07:52:19 +00:00
b6881461fd chore(api): regenerate from Telegram Bot API v10.2 (#12)
Co-authored-by: lukaszraczylo <[email protected]>
2026-08-03 09:57:22 +00:00
6612c24dfc chore(api): regenerate from Telegram Bot API v10.2 (#11)
Co-authored-by: lukaszraczylo <[email protected]>
2026-07-27 10:02:19 +00:00
3620f8ce1e chore(api): regenerate from Telegram Bot API v10.2 (#10)
Co-authored-by: lukaszraczylo <[email protected]>
2026-07-21 21:56:30 +00:00
lukaszraczylo 7eebdd1f40 ci(regen): merge the weekly regeneration PR automatically
The weekly job already gates the update. `make regen` fails on any audit
fallback and the test step runs the full race suite, so a PR only opens when
both pass. Merge it from the same job.

A PR opened with GITHUB_TOKEN does not trigger ci.yml, so GitHub auto-merge
would wait for checks that never start. The push to main triggers ci.yml,
which cuts the release.
2026-07-21 22:52:28 +01:00
lukaszraczylo 9c363f470a chore(api): regenerate from Telegram Bot API v10.2
Snapshot taken 2026-07-21. Notable shape change: ReplyParameters.MessageID
becomes optional (*int64) because Bot API 10.2 allows a reply to target
ephemeral_message_id instead. This breaks callers that set MessageID by value,
so examples/echo passes the address now.

New surface includes the ephemeral message methods, sendRichMessage with
InputRichMessageMedia, and the Community service messages.
2026-07-21 22:52:20 +01:00
lukaszraczylo 5aabbe6151 fix(codegen): parse recapitalised Array-of return sentence
Telegram rewrote the forwardMessages and copyMessages return sentence from
"an array of MessageId" to "an Array of MessageId". The pattern that handles
the "... of the sent messages is returned" shape matched lowercase only, so
both methods fell back to bool and the weekly regen job failed at the audit
step.

Add approved_any_locations to overrides.json so a union that renders as `any`
by design can be approved as data instead of a code change. Approve
InputRichMessageMedia.Media, a five-variant InputMedia union added in Bot
API 10.2 that has no declared parent type.

Make the drift test compare HEAD against itself. It compared the working tree
against HEAD, so it failed whenever a regen run added a method.
2026-07-21 22:52:06 +01:00
lukaszraczylo cd05e6a426 fixup! fix(genapi): scope method-param enums per method, dedupe case-colliding enum consts 2026-07-21 22:35:53 +01:00
lukaszraczylo 140ea13bde fix(genapi): scope method-param enums per method, dedupe case-colliding enum consts
Bot API update added RichBlockListItem.type values (a/A/i/I/1) that
Pascal-case to identical const idents, and answerGuestQuery's non-enum
'result' param picked up answerChatJoinRequestQuery's Result enum via
the shared parent="" enum-plan key.

- key method params as method:<name> in the enum plan byField map
- enumDecl.ConstName resolves case collisions with Lower/Upper prefix
- regenerate api/ from 2026-06-11 snapshot
2026-06-12 00:03:45 +01:00
0731f10907 chore(api): regenerate from Telegram Bot API v10.0 (#7)
Co-authored-by: lukaszraczylo <[email protected]>
2026-06-01 13:15:45 +01:00
lukaszraczylo 6685414374 fix(ci): regen docs + bump x/net to v0.55.0
- regen docs/reference/client.md after the Version docstring rewrite
  in the previous SendForModule commit (gomarkdoc output drifted).
- bump golang.org/x/net 0.54.0 -> 0.55.0 for the govulncheck fixes
  (GO-2026-5027/5028/5029/5030 — html.Parse XSS / DoS chain reachable
  from cmd/scrape).
2026-05-22 23:44:24 +01:00
lukaszraczylo 404411b20c feat(telemetry): switch to SendForModule, drop hand-bumped Version
Replaces the hand-bumped client.Version="0.7.11" with
telemetry.SendForModule which reads the actual module version from
runtime/debug.BuildInfo.Deps at the consumer's build time. This
means future releases (v0.7.12, v0.8.0, …) will be reported by the
running bot WITHOUT requiring a manual edit of client/version.go on
every tag — consumers just `go get -u` and rebuild.

client.Version is preserved as a fallback for replace directives /
detached `go run`, and remains overridable via -ldflags.

Bumps oss-telemetry to v0.2.1.
2026-05-22 23:35:22 +01:00
lukaszraczylo 8d85e61da5 docs: collapse telemetry section to link upstream docs
Replaces the duplicated opt-out table with a link to the canonical
oss-telemetry README section. Keeps the project-specific env var name
and the local source pointer; removes drift risk for the rest.
2026-05-21 04:03:55 +01:00
lukaszraczylo 609c4ce649 feat: anonymous adoption telemetry on first client.New
Sends one fire-and-forget POST per process the first time a consumer
constructs a Bot via client.New. Helps track real-world adoption and
version spread of the library. No identifiers, no API contents.

Implementation:
- new client/version.go: exported Version var (currently 0.7.11)
- new client/telemetry.go: sync.Once gate + telemetry.Send call
- client.go New(): single fireTelemetryOnce() line at function entry
- client/telemetry_test.go: TestMain disables outgoing pings during the
  library's own test suite

README §Telemetry documents the payload, the opt-out env vars, and links
to the upstream oss-telemetry source so consumers can audit what is sent.

Opt out via any of:
  DO_NOT_TRACK=1
  OSS_TELEMETRY_DISABLED=1
  GO_TELEGRAM_DISABLE_TELEMETRY=1
  osstelemetry.Disable() before the first client.New
2026-05-21 03:59:07 +01:00
lukaszraczyloandGitHub d39be13822 chore(ci): bump remaining Node 20 actions (#6)
actions/cache v4 -> v5 (Node 24 runtime; no API change)
actions/upload-artifact v4 -> v7 (ESM + Node 24; existing name/path usage unaffected)

Clears the last Node 20 deprecation warnings surfaced during the
release run in d6ecbde.
2026-05-20 23:39:16 +01:00
lukaszraczyloandGitHub d6ecbdea48 chore(ci): bump goreleaser-action v6 -> v7 (#5)
Node 20 deprecation. v7 release notes call out only node24+ESM
internal changes; inputs/outputs unchanged from v6.
2026-05-20 23:30:34 +01:00
lukaszraczyloandGitHub 99c906c3c5 fix(make): SCRAPE_INPUT tracks latest.html symlink (#4)
regen-from-fixture (used by ci.yml codegen-clean gate) hardcoded
testdata/html/snapshot_2026-05-08.html. After regen.yml committed a
newer snapshot via PR, the pinned path drifted from committed api/
output and codegen-clean failed on main.

Resolving SCRAPE_INPUT via `readlink testdata/html/latest.html` lets
each regen automatically advance the fixture pointer without a manual
Makefile bump, while preserving the override (`SCRAPE_INPUT=path \
make regen-from-fixture`) for ad-hoc replays of historical snapshots.
2026-05-20 23:23:02 +01:00
f5250197b7 chore(api): regenerate from Telegram Bot API v10.0 (#3)
Co-authored-by: lukaszraczylo <[email protected]>
2026-05-20 23:15:44 +01:00
lukaszraczyloandGitHub 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
96 changed files with 292407 additions and 2177 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
+20
View File
@@ -0,0 +1,20 @@
name: Update dependencies
on:
workflow_dispatch:
schedule:
- cron: "0 3 * * *"
permissions:
actions: write
contents: write
issues: write
pull-requests: write
jobs:
update:
uses: lukaszraczylo/shared-actions/.github/workflows/renovate-autoupdate.yaml@main
with:
go-version: ">=1.25"
node: false
release-workflow: release.yml
+61
View File
@@ -0,0 +1,61 @@
name: checks
# Repo-specific gates. Called by ci.yml (PR/push) and release.yml (release gate).
on:
workflow_call:
permissions:
contents: read
jobs:
examples:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: '>=1.25'
check-latest: true
- name: Build all examples
run: go build ./examples/...
codegen-clean:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: '>=1.25'
check-latest: true
- name: Regenerate against pinned snapshot
run: make regen-from-fixture
- name: Assert clean diff
run: git diff --exit-code internal/spec/api.json api/ docs/reference/
audit:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0 # need history for drift comparison
- uses: actions/setup-go@v6
with:
go-version: '>=1.25'
check-latest: true
- name: Audit fallbacks
run: make audit
- name: Audit drift vs base
# On PRs: compare against the merge base (origin/<base>).
# On push to main: compare against the parent commit.
# Drift is informational; doesn't fail CI.
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE="origin/${{ github.base_ref }}"
else
BASE="HEAD~1"
fi
echo "Drift base: $BASE"
go run ./cmd/audit -ir internal/spec/api.json -drift -against "$BASE" || true
+10 -238
View File
@@ -1,20 +1,16 @@
name: ci
# Repo-specific checks run through checks.yml. Generic vet, staticcheck,
# govulncheck, gosec and tests run in pr.yaml (shared go-pr). Releases run in
# release.yml (shared go-release), gated by the same checks.yml.
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
inputs:
dry-run-release:
description: "Compute release version, do not tag or release"
type: boolean
default: false
permissions:
contents: write
pull-requests: read
packages: write
contents: read
# Cancel any in-flight CI runs for the same branch when a new push lands.
concurrency:
@@ -22,157 +18,15 @@ concurrency:
cancel-in-progress: true
jobs:
vet:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.25.x'
check-latest: true
- uses: actions/cache@v4
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-1.25-${{ hashFiles('**/go.sum') }}
- run: go vet ./...
checks:
uses: ./.github/workflows/checks.yml
staticcheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.25.x'
check-latest: true
- uses: actions/cache@v4
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-1.25-${{ hashFiles('**/go.sum') }}
- run: go install honnef.co/go/tools/cmd/[email protected]
- run: staticcheck ./...
govulncheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.25.x'
check-latest: true
- uses: actions/cache@v4
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-1.25-${{ hashFiles('**/go.sum') }}
- run: go install golang.org/x/vuln/cmd/govulncheck@latest
- run: govulncheck ./...
gosec:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.25.x'
check-latest: true
- uses: actions/cache@v4
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-1.25-${{ hashFiles('**/go.sum') }}
- run: go install github.com/securego/gosec/v2/cmd/gosec@latest
# G404: math/rand/v2 jitter in transport/backoff.go — intentional (not crypto)
# G304: os.ReadFile from CLI flag variable — intentional (tool, not server)
# G306: 0o644 on generated doc artifacts in cmd/scrape — intentional
# G204: git subprocess in cmd/audit uses CLI flag path — intentional (operator tool)
# G706: log.Printf with values from Telegram/env in examples — illustrative,
# library users are expected to sanitise before logging in production
- run: gosec -quiet -exclude=G404,G304,G306,G204,G706 -exclude-dir=testdata ./...
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.25.x'
check-latest: true
- uses: actions/cache@v4
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-1.25-${{ hashFiles('**/go.sum') }}
- run: go test -race -coverprofile=coverage.out ./...
- name: Build all examples
run: go build ./examples/...
- uses: actions/upload-artifact@v4
with:
name: coverage
path: coverage.out
codegen-clean:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.25.x'
check-latest: true
- uses: actions/cache@v4
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-1.25-${{ hashFiles('**/go.sum') }}
- name: Regenerate against pinned snapshot
run: make regen-from-fixture
- name: Assert clean diff
run: git diff --exit-code internal/spec/api.json api/ docs/reference/
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # need history for drift comparison
- uses: actions/setup-go@v5
with:
go-version: '1.25.x'
check-latest: true
- uses: actions/cache@v4
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-1.25-${{ hashFiles('**/go.sum') }}
- name: Audit fallbacks
run: make audit
- name: Audit drift vs base
# On PRs: compare against the merge base (origin/<base>).
# On push to main: compare against the parent commit.
# Drift is informational; doesn't fail CI.
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE="origin/${{ github.base_ref }}"
else
BASE="HEAD~1"
fi
echo "Drift base: $BASE"
go run ./cmd/audit -ir internal/spec/api.json -drift -against "$BASE" || true
# Aggregate gate — depends on every check above. Used as a single
# required status check in branch protection AND as a dependency for the
# release job below.
# Aggregate gate for the repo-local checks. Keeps the `ci-success` name
# so an existing branch-protection rule keeps resolving.
ci-success:
runs-on: ubuntu-latest
needs: [vet, staticcheck, govulncheck, gosec, test, codegen-clean, audit]
timeout-minutes: 5
needs: [checks]
if: always()
steps:
- name: All checks passed
@@ -184,85 +38,3 @@ jobs:
echo "Failed/cancelled jobs:"
echo '${{ toJSON(needs) }}'
exit 1
# Auto-release fires on every clean push to main (and on manual
# workflow_dispatch for testing). Computes next SemVer from commit
# history via lukaszraczylo/semver-generator, dual-tags
# (v<X.Y.Z> + bot-api-v<A.B>), runs GoReleaser.
release:
needs: ci-success
if: |
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version: '1.25.x'
check-latest: true
# Action interface from
# https://github.com/lukaszraczylo/semver-generator/blob/main/action.yml
# Inputs: repository_local: true (use already-cloned repo)
# existing: true (respect existing tags as base)
# Output: semantic_version (bare version string, no "v" prefix)
- name: Compute next SemVer
id: semver
uses: lukaszraczylo/semver-generator@v1
with:
repository_local: true
existing: true
config_file: .semver.yaml
- name: Read Bot API version
id: api_version
run: |
VERSION=$(python3 -c 'import json; print(json.load(open("internal/spec/api.json"))["version"])')
if [ -z "$VERSION" ] || [ "$VERSION" = "null" ]; then
echo "tag=" >> "$GITHUB_OUTPUT"
else
echo "tag=bot-api-v${VERSION}" >> "$GITHUB_OUTPUT"
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Dry-run summary
if: github.event_name == 'workflow_dispatch' && inputs.dry-run-release == true
run: |
echo "Would release: v${{ steps.semver.outputs.semantic_version }}"
if [ -n "${{ steps.api_version.outputs.tag }}" ]; then
echo "Would also tag: ${{ steps.api_version.outputs.tag }} (Bot API ${{ steps.api_version.outputs.version }})"
fi
echo "Skipping tag + release (dry-run)."
- name: Tag library + bot-api versions
if: github.event_name != 'workflow_dispatch' || inputs.dry-run-release == false
env:
LIB_TAG: v${{ steps.semver.outputs.semantic_version }}
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 (Bot API $API_VER)"
git push origin "$LIB_TAG"
- name: Run GoReleaser
if: github.event_name != 'workflow_dispatch' || inputs.dry-run-release == false
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: '~> v2'
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BOT_API_VERSION: ${{ steps.api_version.outputs.version }}
+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:
+22
View File
@@ -0,0 +1,22 @@
name: Pull Request
on:
pull_request:
branches:
- main
push:
branches:
- "**"
- "!main"
permissions:
contents: write
actions: write
pull-requests: write
security-events: write
jobs:
pr-checks:
uses: lukaszraczylo/shared-actions/.github/workflows/go-pr.yaml@main
with:
go-version: ">=1.25"
+29 -7
View File
@@ -13,14 +13,14 @@ 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'
go-version-file: go.mod
check-latest: true
- name: Capture latest snapshot
@@ -29,7 +29,12 @@ jobs:
DATE=$(date +%Y-%m-%d)
DEST="testdata/html/snapshot_${DATE}.html"
curl -fsSL --user-agent "go-telegram codegen scraper" \
https://core.telegram.org/bots/api > "$DEST"
https://core.telegram.org/bots/api -o "$DEST.tmp"
# The page ends with a "<!-- page generated in NN.NNms -->" comment
# that changes on every fetch. Drop it, so an unchanged API produces
# a byte-identical snapshot instead of a no-op update PR.
sed '/<!-- page generated in .*ms -->/d' "$DEST.tmp" > "$DEST"
rm "$DEST.tmp"
ln -sf "snapshot_${DATE}.html" testdata/html/latest.html
echo "date=$DATE" >> $GITHUB_OUTPUT
echo "dest=$DEST" >> $GITHUB_OUTPUT
@@ -76,7 +81,10 @@ jobs:
id: diff
run: |
git status --porcelain
if git diff --quiet internal/spec/api.json api/ testdata/html/; then
# Gate on the generated surface only. A fresh snapshot file and the
# latest.html symlink flip every run, so including testdata/html/
# here would open a PR on a week when Telegram changed nothing.
if git diff --quiet internal/spec/api.json api/ docs/reference/; then
echo "no_changes=true" >> $GITHUB_OUTPUT
fi
@@ -88,8 +96,9 @@ jobs:
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Open PR
id: 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: |
@@ -115,4 +124,17 @@ jobs:
Inspect the IR diff (`internal/spec/api.json`) for added/changed/removed methods.
CI must pass before merge. Auto-merge: enable when satisfied with the diff.
This PR merges itself once every step above passes. The gates are
`make regen` (which fails on any audit fallback) and `go test -race ./...`,
both run in this job before the PR opens.
# A PR opened with GITHUB_TOKEN does not trigger `ci.yml` or `pr.yaml`, so waiting for
# PR checks would wait forever. The gates already ran in this job: `make
# regen` fails on any audit fallback, and the test step runs the full race
# suite. Merging here therefore means "merged because the checks passed".
# `release.yml` then runs on the push to main and cuts the release.
- name: Merge regen PR
if: steps.diff.outputs.no_changes != 'true' && steps.pr.outputs.pull-request-number != ''
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh pr merge "${{ steps.pr.outputs.pull-request-number }}" --squash --delete-branch
+28
View File
@@ -0,0 +1,28 @@
name: Release
on:
push:
branches:
- main
paths:
- "**.go"
- "go.mod"
- "go.sum"
workflow_dispatch:
permissions:
contents: write
packages: write
id-token: write
jobs:
checks:
uses: ./.github/workflows/checks.yml
release:
needs: checks
uses: lukaszraczylo/shared-actions/.github/workflows/go-release.yaml@main
with:
go-version: ">=1.25"
semver-config: ".semver.yaml"
secrets: inherit
+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
+4 -1
View File
@@ -35,7 +35,10 @@ lint: vet
integration:
$(GO) test -tags=integration -v ./test/integration/...
SCRAPE_INPUT ?= testdata/html/snapshot_2026-05-08.html
# Resolve via testdata/html/latest.html symlink so the pinned-fixture target
# auto-tracks whatever snapshot regen.yml last committed. Override with
# SCRAPE_INPUT=path/to/snapshot.html for ad-hoc replays.
SCRAPE_INPUT ?= testdata/html/$(shell readlink testdata/html/latest.html)
SCRAPE_OUTPUT ?= internal/spec/api.json
snapshot:
+35
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:
@@ -350,6 +371,20 @@ bot := client.New("token", client.WithHTTPClient(fakeDoer{
The library's own generated test suite (`api/methods_gen_test.go`) covers 176 methods × 8 scenarios each: Success, APIError, NetworkError, ParseError, ContextCanceled, MissingRequiredFields, Forbidden, ServerError.
## Telemetry
On the **first call to `client.New`** in a process, this library sends a
single anonymous adoption ping — project name, version, timestamp; no
identifiers, no message contents, no API call metadata. Fire-and-forget
with a 2-second timeout; cannot block `New` or panic.
Local source: [`client/telemetry.go`](client/telemetry.go). Upstream
implementation, exact wire format, and full opt-out documentation:
**[oss-telemetry — Disabling telemetry](https://github.com/lukaszraczylo/oss-telemetry#disabling-telemetry)**.
Quick opt-out: set any of `DO_NOT_TRACK=1`, `OSS_TELEMETRY_DISABLED=1`,
or `GO_TELEGRAM_DISABLE_TELEMETRY=1`.
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md).
+147
View File
@@ -33,6 +33,14 @@ const (
BotCommandScopeTypeChatMember BotCommandScopeType = "chat_member"
)
type BotSubscriptionUpdatedState string
const (
BotSubscriptionUpdatedStateCanceled BotSubscriptionUpdatedState = "canceled"
BotSubscriptionUpdatedStateActive BotSubscriptionUpdatedState = "active"
BotSubscriptionUpdatedStateFailed BotSubscriptionUpdatedState = "failed"
)
type ChatBoostSourceKind string
const (
@@ -133,6 +141,12 @@ const (
InputMediaTypeVideo InputMediaType = "video"
)
type InputMediaVoiceNoteType string
const (
InputMediaVoiceNoteTypeVoiceNote InputMediaVoiceNoteType = "voice_note"
)
type InputPaidMediaType string
const (
@@ -158,6 +172,7 @@ type InputPollOptionMediaType string
const (
InputPollOptionMediaTypeAnimation InputPollOptionMediaType = "animation"
InputPollOptionMediaTypeLink InputPollOptionMediaType = "link"
InputPollOptionMediaTypeLivePhoto InputPollOptionMediaType = "live_photo"
InputPollOptionMediaTypeLocation InputPollOptionMediaType = "location"
InputPollOptionMediaTypePhoto InputPollOptionMediaType = "photo"
@@ -173,6 +188,35 @@ const (
InputProfilePhotoTypeAnimated InputProfilePhotoType = "animated"
)
type InputRichBlockType string
const (
InputRichBlockTypeParagraph InputRichBlockType = "paragraph"
InputRichBlockTypeHeading InputRichBlockType = "heading"
InputRichBlockTypePre InputRichBlockType = "pre"
InputRichBlockTypeFooter InputRichBlockType = "footer"
InputRichBlockTypeDivider InputRichBlockType = "divider"
InputRichBlockTypeMathematicalExpression InputRichBlockType = "mathematical_expression"
InputRichBlockTypeAnchor InputRichBlockType = "anchor"
InputRichBlockTypeList InputRichBlockType = "list"
InputRichBlockTypeBlockquote InputRichBlockType = "blockquote"
InputRichBlockTypeExpandableBlockquote InputRichBlockType = "expandable_blockquote"
InputRichBlockTypePullquote InputRichBlockType = "pullquote"
InputRichBlockTypeCollage InputRichBlockType = "collage"
InputRichBlockTypeSlideshow InputRichBlockType = "slideshow"
InputRichBlockTypeTable InputRichBlockType = "table"
InputRichBlockTypeDetails InputRichBlockType = "details"
InputRichBlockTypeMap InputRichBlockType = "map"
InputRichBlockTypeButtons InputRichBlockType = "buttons"
InputRichBlockTypeAnimation InputRichBlockType = "animation"
InputRichBlockTypeAudio InputRichBlockType = "audio"
InputRichBlockTypeDocument InputRichBlockType = "document"
InputRichBlockTypePhoto InputRichBlockType = "photo"
InputRichBlockTypeVideo InputRichBlockType = "video"
InputRichBlockTypeVoiceNote InputRichBlockType = "voice_note"
InputRichBlockTypeThinking InputRichBlockType = "thinking"
)
type InputStickerFormat string
const (
@@ -357,6 +401,14 @@ const (
RefundedPaymentCurrencyXTR RefundedPaymentCurrency = "XTR"
)
type Result string
const (
ResultApprove Result = "approve"
ResultDecline Result = "decline"
ResultQueue Result = "queue"
)
type RevenueWithdrawalStateKind string
const (
@@ -365,6 +417,101 @@ const (
RevenueWithdrawalStateKindFailed RevenueWithdrawalStateKind = "failed"
)
type RichBlockButtonsAlign string
const (
RichBlockButtonsAlignLeft RichBlockButtonsAlign = "left"
RichBlockButtonsAlignCenter RichBlockButtonsAlign = "center"
RichBlockButtonsAlignRight RichBlockButtonsAlign = "right"
)
type RichBlockListItemType string
const (
RichBlockListItemTypeLowerA RichBlockListItemType = "a"
RichBlockListItemTypeUpperA RichBlockListItemType = "A"
RichBlockListItemTypeLowerI RichBlockListItemType = "i"
RichBlockListItemTypeUpperI RichBlockListItemType = "I"
RichBlockListItemType1 RichBlockListItemType = "1"
)
type RichBlockTableCellValign string
const (
RichBlockTableCellValignTop RichBlockTableCellValign = "top"
RichBlockTableCellValignMiddle RichBlockTableCellValign = "middle"
RichBlockTableCellValignBottom RichBlockTableCellValign = "bottom"
)
type RichBlockType string
const (
RichBlockTypeParagraph RichBlockType = "paragraph"
RichBlockTypeHeading RichBlockType = "heading"
RichBlockTypePre RichBlockType = "pre"
RichBlockTypeFooter RichBlockType = "footer"
RichBlockTypeDivider RichBlockType = "divider"
RichBlockTypeMathematicalExpression RichBlockType = "mathematical_expression"
RichBlockTypeAnchor RichBlockType = "anchor"
RichBlockTypeList RichBlockType = "list"
RichBlockTypeBlockquote RichBlockType = "blockquote"
RichBlockTypeExpandableBlockquote RichBlockType = "expandable_blockquote"
RichBlockTypePullquote RichBlockType = "pullquote"
RichBlockTypeCollage RichBlockType = "collage"
RichBlockTypeSlideshow RichBlockType = "slideshow"
RichBlockTypeTable RichBlockType = "table"
RichBlockTypeDetails RichBlockType = "details"
RichBlockTypeMap RichBlockType = "map"
RichBlockTypeButtons RichBlockType = "buttons"
RichBlockTypeAnimation RichBlockType = "animation"
RichBlockTypeAudio RichBlockType = "audio"
RichBlockTypeDocument RichBlockType = "document"
RichBlockTypePhoto RichBlockType = "photo"
RichBlockTypeVideo RichBlockType = "video"
RichBlockTypeVoiceNote RichBlockType = "voice_note"
RichBlockTypeThinking RichBlockType = "thinking"
)
type RichMessageButtonStyle string
const (
RichMessageButtonStyleDanger RichMessageButtonStyle = "danger"
RichMessageButtonStyleSuccess RichMessageButtonStyle = "success"
RichMessageButtonStylePrimary RichMessageButtonStyle = "primary"
RichMessageButtonStyleLink RichMessageButtonStyle = "link"
)
type RichTextType string
const (
RichTextTypeBold RichTextType = "bold"
RichTextTypeItalic RichTextType = "italic"
RichTextTypeUnderline RichTextType = "underline"
RichTextTypeStrikethrough RichTextType = "strikethrough"
RichTextTypeSpoiler RichTextType = "spoiler"
RichTextTypeDateTime RichTextType = "date_time"
RichTextTypeTextMention RichTextType = "text_mention"
RichTextTypeSubscript RichTextType = "subscript"
RichTextTypeSuperscript RichTextType = "superscript"
RichTextTypeMarked RichTextType = "marked"
RichTextTypeCode RichTextType = "code"
RichTextTypeCustomEmoji RichTextType = "custom_emoji"
RichTextTypeMathematicalExpression RichTextType = "mathematical_expression"
RichTextTypeURL RichTextType = "url"
RichTextTypeEmailAddress RichTextType = "email_address"
RichTextTypePhoneNumber RichTextType = "phone_number"
RichTextTypeBankCardNumber RichTextType = "bank_card_number"
RichTextTypeMention RichTextType = "mention"
RichTextTypeHashtag RichTextType = "hashtag"
RichTextTypeCashtag RichTextType = "cashtag"
RichTextTypeBotCommand RichTextType = "bot_command"
RichTextTypeButton RichTextType = "button"
RichTextTypeAnchor RichTextType = "anchor"
RichTextTypeAnchorLink RichTextType = "anchor_link"
RichTextTypeReference RichTextType = "reference"
RichTextTypeReferenceLink RichTextType = "reference_link"
)
type StickerType string
const (
+452 -124
View File
File diff suppressed because it is too large Load Diff
+1352 -21
View File
File diff suppressed because it is too large Load Diff
+210
View File
@@ -0,0 +1,210 @@
package api
import (
"testing"
json "github.com/goccy/go-json"
"github.com/stretchr/testify/require"
)
// TestUpdate_RichMessage_Decodes is a regression test for the Bot API v10
// rich_message decode failure: RichBlock (21 variants) and RichText (25
// variants) had no knownDiscriminators entry, so genapi never emitted
// UnmarshalRichBlock / UnmarshalRichText and any update whose message
// carried rich_message failed to decode — in production this rejected
// roughly 8.6% of Telegram updates with HTTP 400. Uses goccy/go-json, the
// codec production runs; plain encoding/json failed the same way.
func TestUpdate_RichMessage_Decodes(t *testing.T) {
body := `{"update_id":1,"message":{"message_id":7,"date":1789000000,"chat":{"id":-100123,"type":"supergroup"},` +
`"rich_message":{"blocks":[{"type":"paragraph","text":{"type":"bold","text":{"type":"anchor","name":"hi"}}}]}}}`
var u Update
require.NoError(t, json.Unmarshal([]byte(body), &u))
require.NotNil(t, u.Message)
require.NotNil(t, u.Message.RichMessage)
require.Len(t, u.Message.RichMessage.Blocks, 1)
para, ok := u.Message.RichMessage.Blocks[0].(*RichBlockParagraph)
require.True(t, ok, "blocks[0] must dispatch to *RichBlockParagraph, got %T", u.Message.RichMessage.Blocks[0])
bold, ok := para.Text.(*RichTextBold)
require.True(t, ok, "paragraph.text must dispatch to *RichTextBold, got %T", para.Text)
anchor, ok := bold.Text.(*RichTextAnchor)
require.True(t, ok, "bold.text must dispatch to *RichTextAnchor, got %T", bold.Text)
require.Equal(t, "hi", anchor.Name)
}
// TestUnmarshalRichBlock_UnknownType confirms an unrecognised discriminator
// value surfaces a clear error instead of silently decoding a zero-valued
// interface.
func TestUnmarshalRichBlock_UnknownType(t *testing.T) {
_, err := UnmarshalRichBlock([]byte(`{"type":"not_a_real_block"}`))
require.Error(t, err)
require.ErrorContains(t, err, "unknown type")
}
// TestUpdate_RichMessage_UnknownBlockType confirms the unknown-type error
// from UnmarshalRichBlock propagates all the way up through
// RichMessage.UnmarshalJSON to the top-level Update decode.
func TestUpdate_RichMessage_UnknownBlockType(t *testing.T) {
body := `{"update_id":1,"message":{"message_id":7,"date":1789000000,"chat":{"id":-100123,"type":"supergroup"},` +
`"rich_message":{"blocks":[{"type":"not_a_real_block"}]}}}`
var u Update
err := json.Unmarshal([]byte(body), &u)
require.Error(t, err)
require.ErrorContains(t, err, "unknown type")
}
// TestUpdate_RichMessage_PlainStringText is a regression test for the Bot
// API v10 RichText decode gap this task fixes. Telegram declares RichText
// as decodable from "either a String for plain text, an Array of
// RichText, or any of the following [object] types" — but the IR (and
// therefore UnmarshalRichText) only ever recorded the object variants, so
// a paragraph whose text is a bare JSON string failed with "invalid
// character '\"' looking for beginning of value". A plain-text paragraph
// is the most common real rich_message payload, so this was the
// production-facing half of the bug.
func TestUpdate_RichMessage_PlainStringText(t *testing.T) {
body := `{"update_id":1,"message":{"message_id":7,"date":1789000000,"chat":{"id":-100123,"type":"supergroup"},` +
`"rich_message":{"blocks":[{"type":"paragraph","text":"hello world"}]}}}`
var u Update
require.NoError(t, json.Unmarshal([]byte(body), &u))
require.NotNil(t, u.Message)
require.NotNil(t, u.Message.RichMessage)
require.Len(t, u.Message.RichMessage.Blocks, 1)
para, ok := u.Message.RichMessage.Blocks[0].(*RichBlockParagraph)
require.True(t, ok, "blocks[0] must dispatch to *RichBlockParagraph, got %T", u.Message.RichMessage.Blocks[0])
plain, ok := para.Text.(*RichTextPlain)
require.True(t, ok, "paragraph.text must dispatch to *RichTextPlain for a bare JSON string, got %T", para.Text)
require.Equal(t, "hello world", plain.Text)
}
// TestUpdate_RichMessage_ArrayText confirms Telegram's second declared
// RichText alternate — a bare JSON array of RichText — decodes into
// RichTextSequence, with each element dispatched recursively through
// UnmarshalRichText (an object variant in this case).
func TestUpdate_RichMessage_ArrayText(t *testing.T) {
body := `{"update_id":1,"message":{"message_id":7,"date":1789000000,"chat":{"id":-100123,"type":"supergroup"},` +
`"rich_message":{"blocks":[{"type":"paragraph","text":[` +
`{"type":"bold","text":"x"},{"type":"italic","text":"y"}` +
`]}]}}}`
var u Update
require.NoError(t, json.Unmarshal([]byte(body), &u))
para, ok := u.Message.RichMessage.Blocks[0].(*RichBlockParagraph)
require.True(t, ok)
seq, ok := para.Text.(*RichTextSequence)
require.True(t, ok, "paragraph.text must dispatch to *RichTextSequence for a JSON array, got %T", para.Text)
require.Len(t, seq.Items, 2)
bold, ok := seq.Items[0].(*RichTextBold)
require.True(t, ok, "sequence[0] must be *RichTextBold, got %T", seq.Items[0])
boldText, ok := bold.Text.(*RichTextPlain)
require.True(t, ok)
require.Equal(t, "x", boldText.Text)
italic, ok := seq.Items[1].(*RichTextItalic)
require.True(t, ok, "sequence[1] must be *RichTextItalic, got %T", seq.Items[1])
italicText, ok := italic.Text.(*RichTextPlain)
require.True(t, ok)
require.Equal(t, "y", italicText.Text)
}
// TestUpdate_RichMessage_ArrayText_MixedElements confirms an array whose
// elements mix the bare-string shape and the object shape dispatches
// each element independently — the array alternate and the string
// alternate compose, they aren't mutually exclusive per element.
func TestUpdate_RichMessage_ArrayText_MixedElements(t *testing.T) {
body := `{"update_id":1,"message":{"message_id":7,"date":1789000000,"chat":{"id":-100123,"type":"supergroup"},` +
`"rich_message":{"blocks":[{"type":"paragraph","text":[` +
`"plain segment",{"type":"bold","text":"bold segment"}` +
`]}]}}}`
var u Update
require.NoError(t, json.Unmarshal([]byte(body), &u))
para, ok := u.Message.RichMessage.Blocks[0].(*RichBlockParagraph)
require.True(t, ok)
seq, ok := para.Text.(*RichTextSequence)
require.True(t, ok)
require.Len(t, seq.Items, 2)
plain, ok := seq.Items[0].(*RichTextPlain)
require.True(t, ok, "sequence[0] must be *RichTextPlain, got %T", seq.Items[0])
require.Equal(t, "plain segment", plain.Text)
bold, ok := seq.Items[1].(*RichTextBold)
require.True(t, ok, "sequence[1] must be *RichTextBold, got %T", seq.Items[1])
boldText, ok := bold.Text.(*RichTextPlain)
require.True(t, ok)
require.Equal(t, "bold segment", boldText.Text)
}
// TestRichTextPlain_MarshalJSON_RoundTrip confirms the plain-text
// alternate marshals back to a bare JSON string (not an object), and
// that string decodes straight back through UnmarshalRichText.
func TestRichTextPlain_MarshalJSON_RoundTrip(t *testing.T) {
v := &RichTextPlain{Text: "hello"}
b, err := json.Marshal(v)
require.NoError(t, err)
require.JSONEq(t, `"hello"`, string(b))
decoded, err := UnmarshalRichText(b)
require.NoError(t, err)
got, ok := decoded.(*RichTextPlain)
require.True(t, ok)
require.Equal(t, "hello", got.Text)
}
// TestRichTextSequence_MarshalJSON_RoundTrip confirms the array
// alternate marshals back to a bare JSON array (not an object wrapping
// one), preserving each element's own wire shape, and round-trips
// through UnmarshalRichText.
func TestRichTextSequence_MarshalJSON_RoundTrip(t *testing.T) {
v := &RichTextSequence{
Items: []RichText{
&RichTextPlain{Text: "a"},
&RichTextBold{Text: &RichTextPlain{Text: "b"}},
},
}
b, err := json.Marshal(v)
require.NoError(t, err)
require.JSONEq(t, `["a",{"type":"bold","text":"b"}]`, string(b))
decoded, err := UnmarshalRichText(b)
require.NoError(t, err)
seq, ok := decoded.(*RichTextSequence)
require.True(t, ok)
require.Len(t, seq.Items, 2)
p0, ok := seq.Items[0].(*RichTextPlain)
require.True(t, ok)
require.Equal(t, "a", p0.Text)
b1, ok := seq.Items[1].(*RichTextBold)
require.True(t, ok)
p1, ok := b1.Text.(*RichTextPlain)
require.True(t, ok)
require.Equal(t, "b", p1.Text)
}
// TestUnmarshalRichText_UnrecognisedShape confirms a JSON value that is
// neither a string, an array, nor an object surfaces a clear error
// instead of silently producing a zero-valued interface.
func TestUnmarshalRichText_UnrecognisedShape(t *testing.T) {
_, err := UnmarshalRichText([]byte(`42`))
require.Error(t, err)
require.ErrorContains(t, err, "unrecognised JSON value")
}
+4630 -133
View File
File diff suppressed because it is too large Load Diff
+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)
}
}
}
+20
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
@@ -34,6 +45,7 @@ func (b *Bot) Logger() Logger { return b.logger }
// NewDefaultHTTPDoer); the default codec wraps encoding/json; the default
// logger discards records.
func New(token string, opts ...Option) *Bot {
fireTelemetryOnce()
b := &Bot{
token: token,
base: defaultBaseURL,
@@ -44,5 +56,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 {
+33
View File
@@ -0,0 +1,33 @@
package client
import (
"sync"
telemetry "github.com/lukaszraczylo/oss-telemetry"
)
// telemetryOnce guards the single anonymous "library used" ping that is sent
// on the first call to New. Long-running bots typically construct one Bot;
// short-lived programs or test suites may construct many, but the Once gate
// keeps the fire-and-forget call from amplifying into per-construction pings.
var telemetryOnce sync.Once
// fireTelemetryOnce dispatches a fire-and-forget anonymous adoption ping.
//
// The call is failproof by contract of oss-telemetry: it never blocks New,
// never panics, never returns errors, and silently no-ops if disabled or
// if the network is unavailable.
//
// Opt-out is honored via any of these environment variables (case-insensitive
// truthy values "1", "true", "yes", "on"):
//
// - DO_NOT_TRACK
// - OSS_TELEMETRY_DISABLED
// - GO_TELEGRAM_DISABLE_TELEMETRY
//
// See README §Telemetry for the full disclosure.
func fireTelemetryOnce() {
telemetryOnce.Do(func() {
telemetry.SendForModule("go-telegram", "github.com/lukaszraczylo/go-telegram", Version)
})
}
+54
View File
@@ -0,0 +1,54 @@
package client
import (
"os"
"sync"
"testing"
telemetry "github.com/lukaszraczylo/oss-telemetry"
)
// TestMain disables outgoing telemetry for the duration of this package's
// test suite. The library's own tests construct many Bot instances; without
// this guard they would each contribute a real ping to the public endpoint.
// End-user test suites that construct Bot are not affected by this — only
// tests inside this package are.
func TestMain(m *testing.M) {
telemetry.Disable()
os.Exit(m.Run())
}
// TestFireTelemetryOnce_OnlyFiresOnce verifies the sync.Once gate. Even if
// New is called repeatedly, the underlying telemetry.Send is invoked at most
// once per process. We can't observe the network call directly (telemetry
// is disabled here via TestMain) so we assert on the once-Do count via a
// fresh local sync.Once paralleling the production one.
func TestFireTelemetryOnce_OnlyFiresOnce(t *testing.T) {
// Reset the package-level Once so this test starts from a clean state.
telemetryOnce = sync.Once{}
t.Cleanup(func() { telemetryOnce = sync.Once{} })
calls := 0
probe := func() { telemetryOnce.Do(func() { calls++ }) }
for i := 0; i < 50; i++ {
probe()
}
if calls != 1 {
t.Fatalf("expected exactly 1 Once execution, got %d", calls)
}
}
// TestNew_DoesNotPanicUnderRepeatedConstruction is a smoke test that
// telemetry wiring does not affect New's existing contract. New must never
// panic, regardless of telemetry state.
func TestNew_DoesNotPanicUnderRepeatedConstruction(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Fatalf("New panicked: %v", r)
}
}()
for i := 0; i < 20; i++ {
_ = New("test-token-" + string(rune('A'+i)))
}
}
+11
View File
@@ -0,0 +1,11 @@
package client
// Version is a fallback version string used only when Go's build info is
// unavailable (replace directives, detached `go run`) or has been overridden
// via linker flags. The authoritative version forwarded to telemetry is
// resolved at runtime by [telemetry.SendForModule] from the build info of
// whatever binary linked this library, so this constant does NOT need to be
// bumped on every release. Exposed as a var (not const) for ldflag override:
//
// go build -ldflags="-X github.com/lukaszraczylo/go-telegram/client.Version=1.2.3"
var Version = "0.0.0-fallback"
+11 -5
View File
@@ -125,12 +125,18 @@ func TestAuditDrift_InvalidRefReturnsError(t *testing.T) {
}
func TestAuditDrift_SameRefNoDrift(t *testing.T) {
// Compare HEAD's IR against itself, not against the working tree: during a
// regen run the working tree legitimately carries new methods, and drift
// against HEAD is the expected outcome rather than a failure.
irPath := "../../internal/spec/api.json"
cur, err := loadIR(irPath)
out, err := exec.Command("git", "show", "HEAD:"+irPath).Output()
if err != nil {
t.Skip("api.json not available, skipping drift test")
t.Skip("api.json not available at HEAD, skipping drift test")
}
changes, err := auditDrift(irPath, "HEAD", cur)
var head spec.API
require.NoError(t, json.Unmarshal(out, &head))
changes, err := auditDrift(irPath, "HEAD", &head)
require.NoError(t, err)
require.Empty(t, changes)
}
@@ -166,7 +172,7 @@ func TestAuditAny_FlagsUnknownMethodReturn(t *testing.T) {
},
},
}
out := auditAny(api)
out := auditAny(api, &spec.Overrides{})
require.Len(t, out, 1)
require.Contains(t, out[0], "any return: weirdMethod")
}
@@ -182,7 +188,7 @@ func TestAuditAny_FlagsUnknownMethodParam(t *testing.T) {
},
},
}
out := auditAny(api)
out := auditAny(api, &spec.Overrides{})
require.Len(t, out, 1)
require.Contains(t, out[0], "any param: weirdMethod.Thing")
}
+7 -6
View File
@@ -54,7 +54,7 @@ func main() {
var problems []string
problems = append(problems, auditBool(api, overrides)...)
problems = append(problems, auditAny(api)...)
problems = append(problems, auditAny(api, overrides)...)
driftFound := false
if *checkDrift {
@@ -140,8 +140,9 @@ func looksGenuinelyBool(doc string) bool {
// auditAny scans the IR for any KindOneOf TypeRef that would render as
// `any` in generated code (not matched by ChatID/InputFile-or-string/known
// union heuristics). Reports each occurrence with location.
func auditAny(api *spec.API) []string {
// union heuristics). Reports each occurrence with location, except the
// locations approved in overrides.
func auditAny(api *spec.API, ov *spec.Overrides) []string {
var out []string
isKnownUnion := func(variants []string) bool {
if hasVariants(variants, "int64", "string") {
@@ -166,17 +167,17 @@ func auditAny(api *spec.API) []string {
}
for _, t := range api.Types {
for _, f := range t.Fields {
if isAny(f.Type) {
if isAny(f.Type) && !ov.IsAnyApproved(t.Name+"."+f.Name) {
out = append(out, fmt.Sprintf("any field: %s.%s (variants=%v)", t.Name, f.Name, f.Type.Variants))
}
}
}
for _, m := range api.Methods {
if isAny(m.Returns) {
if isAny(m.Returns) && !ov.IsAnyApproved(m.Name) {
out = append(out, fmt.Sprintf("any return: %s (variants=%v)", m.Name, m.Returns.Variants))
}
for _, p := range m.Params {
if isAny(p.Type) {
if isAny(p.Type) && !ov.IsAnyApproved(m.Name+"."+p.Name) {
out = append(out, fmt.Sprintf("any param: %s.%s (variants=%v)", m.Name, p.Name, p.Type.Variants))
}
}
+31 -5
View File
@@ -66,11 +66,37 @@ func TestAuditAny_FlagsUnrecognisedOneOf(t *testing.T) {
},
},
}
out := auditAny(api)
out := auditAny(api, &spec.Overrides{})
require.Len(t, out, 1)
require.Contains(t, out[0], "any field: Foo.Bar")
}
func TestAuditAny_SkipsApprovedLocations(t *testing.T) {
api := &spec.API{
Types: []spec.TypeDecl{
{
Name: "Foo",
Fields: []spec.Field{
{Name: "Bar", JSONName: "bar", Type: spec.TypeRef{Kind: spec.KindOneOf, Variants: []string{"A", "B"}}},
},
},
},
Methods: []spec.MethodDecl{
{
Name: "doThing",
Returns: spec.TypeRef{Kind: spec.KindOneOf, Variants: []string{"A", "B"}},
Params: []spec.Field{
{Name: "Baz", JSONName: "baz", Type: spec.TypeRef{Kind: spec.KindOneOf, Variants: []string{"A", "B"}}},
},
},
},
}
require.Len(t, auditAny(api, &spec.Overrides{}), 3)
ov := &spec.Overrides{ApprovedAnyLocations: []string{"Foo.Bar", "doThing", "doThing.Baz"}}
require.Empty(t, auditAny(api, ov))
}
func TestAuditAny_SkipsChatIDShape(t *testing.T) {
api := &spec.API{
Types: []spec.TypeDecl{
@@ -82,7 +108,7 @@ func TestAuditAny_SkipsChatIDShape(t *testing.T) {
},
},
}
require.Empty(t, auditAny(api))
require.Empty(t, auditAny(api, &spec.Overrides{}))
}
func TestAuditAny_SkipsKnownUnion(t *testing.T) {
@@ -97,7 +123,7 @@ func TestAuditAny_SkipsKnownUnion(t *testing.T) {
},
},
}
require.Empty(t, auditAny(api))
require.Empty(t, auditAny(api, &spec.Overrides{}))
}
func TestAuditAny_SkipsReplyMarkupShape(t *testing.T) {
@@ -111,7 +137,7 @@ func TestAuditAny_SkipsReplyMarkupShape(t *testing.T) {
},
},
}
require.Empty(t, auditAny(api))
require.Empty(t, auditAny(api, &spec.Overrides{}))
}
func TestAuditAny_SkipsInputFileShape(t *testing.T) {
@@ -125,7 +151,7 @@ func TestAuditAny_SkipsInputFileShape(t *testing.T) {
},
},
}
require.Empty(t, auditAny(api))
require.Empty(t, auditAny(api, &spec.Overrides{}))
}
// ---- diffSignatures ------------------------------------------------------
+144
View File
@@ -0,0 +1,144 @@
package main
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/lukaszraczylo/go-telegram/internal/spec"
"github.com/stretchr/testify/require"
)
// TestUnionAlternates_NoAlternates confirms a union with no Alternates
// (the common case — every union except RichText) produces no synthetic
// variants.
func TestUnionAlternates_NoAlternates(t *testing.T) {
td := spec.TypeDecl{Name: "ChatMember", OneOf: []string{"ChatMemberOwner"}}
require.Nil(t, unionAlternates(td))
}
// TestUnionAlternates_StringAndArray confirms both alternate shapes map
// to the expected synthetic variant names, in declaration order.
func TestUnionAlternates_StringAndArray(t *testing.T) {
td := spec.TypeDecl{
Name: "RichText",
OneOf: []string{"RichTextBold"},
Alternates: []spec.Alternate{
{Shape: spec.AlternateString},
{Shape: spec.AlternateArray},
},
}
got := unionAlternates(td)
require.Equal(t, []alternateVariant{
{Name: "RichTextPlain", IsString: true},
{Name: "RichTextSequence", IsArray: true},
}, got)
}
// TestEmitTypes_UnionWithAlternates_EmitsShapeDispatcher builds a minimal
// synthetic API with one discriminated union carrying Alternates (mirrors
// RichText's shape without depending on the full real spec) and confirms
// genapi emits: the two synthetic variant structs, their marker methods
// and MarshalJSON, and a shape-dispatching Unmarshal<Union> that branches
// on the first non-whitespace byte before falling back to the existing
// discriminator switch.
func TestEmitTypes_UnionWithAlternates_EmitsShapeDispatcher(t *testing.T) {
api := &spec.API{
Types: []spec.TypeDecl{
{
Name: "Widget",
OneOf: []string{"WidgetBold"},
Alternates: []spec.Alternate{
{Shape: spec.AlternateString},
{Shape: spec.AlternateArray},
},
},
{
Name: "WidgetBold",
Fields: []spec.Field{
{Name: "Type", JSONName: "type", Required: true,
Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "string"}, EnumValues: []string{"bold"}},
},
},
},
}
tmp := t.TempDir()
e := newEmitter(api, tmp)
require.NoError(t, e.emitTypes())
got, err := os.ReadFile(filepath.Join(tmp, "types.gen.go"))
require.NoError(t, err)
src := string(got)
// Synthetic variants exist and implement the sealed interface.
require.Contains(t, src, "type WidgetPlain struct")
require.Contains(t, src, "Text string")
require.Contains(t, src, "func (*WidgetPlain) isWidget() {}")
require.Contains(t, src, "type WidgetSequence struct")
require.Contains(t, src, "Items []Widget")
require.Contains(t, src, "func (*WidgetSequence) isWidget() {}")
// Round-trip MarshalJSON for both.
require.Contains(t, src, "func (v *WidgetPlain) MarshalJSON() ([]byte, error) {")
require.Contains(t, src, "return json.Marshal(v.Text)")
require.Contains(t, src, "func (v *WidgetSequence) MarshalJSON() ([]byte, error) {")
require.Contains(t, src, "return json.Marshal(v.Items)")
// Shape-dispatching UnmarshalWidget: trims, branches on '"', '[', '{'.
require.Contains(t, src, "func UnmarshalWidget(data []byte) (Widget, error) {")
require.Contains(t, src, `trimmed := bytes.TrimLeft(data, " \t\r\n")`)
require.Contains(t, src, "case '\"':")
require.Contains(t, src, "return &WidgetPlain{Text: s}, nil")
require.Contains(t, src, "case '[':")
require.Contains(t, src, "return &WidgetSequence{Items: items}, nil")
require.Contains(t, src, "case '{':")
// The pre-existing discriminator switch is preserved beneath the
// shape dispatch, unrecognised discriminator still a clear error.
require.Contains(t, src, `fmt.Errorf("Widget: unknown type %q", probe.V)`)
// The struct-holding-the-union case inherits shape dispatch for free:
// WidgetBold has no union-typed field itself, so nothing further to
// assert there beyond successful compilation-shaped source (covered
// by TestEmit_Types_FixtureGolden's gofmt round trip for the real
// fixture, and by the api-package tests for full decode behaviour).
require.False(t, strings.Contains(src, "WidgetPlainSequence"), "sanity: no accidental name collision")
}
// TestEmitTypes_UnionWithoutAlternates_Unaffected confirms a discriminated
// union with no Alternates emits the original (pre-alternates) shape:
// no synthetic variants, no shape dispatch, and the same "decodes a X
// from JSON by inspecting" doc comment wording as before this feature.
func TestEmitTypes_UnionWithoutAlternates_Unaffected(t *testing.T) {
api := &spec.API{
Types: []spec.TypeDecl{
{Name: "Widget", OneOf: []string{"WidgetBold"}},
{
Name: "WidgetBold",
Fields: []spec.Field{
{Name: "Type", JSONName: "type", Required: true,
Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "string"}, EnumValues: []string{"bold"}},
},
},
},
}
tmp := t.TempDir()
e := newEmitter(api, tmp)
require.NoError(t, e.emitTypes())
got, err := os.ReadFile(filepath.Join(tmp, "types.gen.go"))
require.NoError(t, err)
src := string(got)
require.NotContains(t, src, "WidgetPlain")
require.NotContains(t, src, "WidgetSequence")
// The package-level `var _ = bytes.TrimLeft` import guard is always
// present (it keeps the "bytes" import valid even when no union in
// this API has alternates); what must NOT appear is the per-union
// shape-dispatch call site.
require.NotContains(t, src, "trimmed := bytes.TrimLeft(data,")
require.Contains(t, src, "// UnmarshalWidget decodes a Widget from JSON by inspecting the")
require.Contains(t, src, `// "type" field and dispatching to the correct concrete type.`)
}
+148
View File
@@ -0,0 +1,148 @@
package main
import (
"testing"
"github.com/lukaszraczylo/go-telegram/internal/spec"
"github.com/stretchr/testify/require"
)
// TestDeriveDiscriminators_RichBlockAndRichText_FromRealSpec is a
// regression test for the Bot API v10 rich_message decode failure:
// RichBlock and RichText had no knownDiscriminators entry, so genapi
// never emitted UnmarshalRichBlock / UnmarshalRichText and any update
// whose message carried rich_message failed to decode. Derivation must
// pick "type" for both and cover every variant the spec declares.
//
// The expected variant counts are read from the spec rather than pinned
// as literals. Telegram adds variants in most releases - v10.3 took
// RichBlock from 21 to 24 and RichText from 25 to 26 - so a frozen
// number turns a routine regeneration into a spurious failure while
// testing nothing the coverage assertion does not already cover.
func TestDeriveDiscriminators_RichBlockAndRichText_FromRealSpec(t *testing.T) {
api, err := loadAPI("../../internal/spec/api.json")
require.NoError(t, err)
// declaredVariants returns how many concrete variants the spec lists
// for a union, so the assertions below track the spec instead of a
// hardcoded count.
declaredVariants := func(union string) int {
for i := range api.Types {
if api.Types[i].Name == union {
return len(api.Types[i].OneOf)
}
}
t.Fatalf("union %q not found in spec", union)
return 0
}
derived, _ := deriveDiscriminators(api)
rb, ok := derived["RichBlock"]
require.True(t, ok, "RichBlock must derive a discriminator")
require.Equal(t, "type", rb.Field)
require.Len(t, rb.Variants, declaredVariants("RichBlock"),
"every RichBlock variant declared in the spec must be covered")
require.Equal(t, "RichBlockParagraph", rb.Variants["paragraph"])
require.Equal(t, "RichBlockTable", rb.Variants["table"])
require.Equal(t, "RichBlockThinking", rb.Variants["thinking"])
rt, ok := derived["RichText"]
require.True(t, ok, "RichText must derive a discriminator")
require.Equal(t, "type", rt.Field)
require.Len(t, rt.Variants, declaredVariants("RichText"),
"every RichText variant declared in the spec must be covered")
require.Equal(t, "RichTextBold", rt.Variants["bold"])
require.Equal(t, "RichTextAnchorLink", rt.Variants["anchor_link"])
require.Equal(t, "RichTextReferenceLink", rt.Variants["reference_link"])
}
// TestMergeDiscriminators_HandCuratedOverridesDerived confirms a
// knownDiscriminators entry always wins the merge even when derivation
// would also have produced a (different) answer for the same union —
// hand-curation is the escape hatch for a union the generic rule gets
// wrong.
func TestMergeDiscriminators_HandCuratedOverridesDerived(t *testing.T) {
api := &spec.API{
Types: []spec.TypeDecl{
{Name: "Widget", OneOf: []string{"WidgetA", "WidgetB"}},
{Name: "WidgetA", Fields: []spec.Field{
{Name: "Type", JSONName: "type", Required: true,
Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "string"}, EnumValues: []string{"a"}},
}},
{Name: "WidgetB", Fields: []spec.Field{
{Name: "Type", JSONName: "type", Required: true,
Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "string"}, EnumValues: []string{"b"}},
}},
},
}
derived, ungated := deriveDiscriminators(api)
require.Empty(t, ungated)
require.Equal(t, discriminatorSpec{
Field: "type",
Variants: map[string]string{"a": "WidgetA", "b": "WidgetB"},
}, derived["Widget"])
// Inject a hand-curated override that disagrees with the derived
// spec, merge, and confirm the override wins outright.
override := discriminatorSpec{Field: "kind", Variants: map[string]string{"custom": "WidgetA"}}
knownDiscriminators["Widget"] = override
t.Cleanup(func() { delete(knownDiscriminators, "Widget") })
merged := mergeDiscriminators(derived)
require.Equal(t, override, merged["Widget"])
}
// TestCheckDiscriminatorGate_FailsOnUngatedInboundUnion confirms the
// build gate fails generation when a union Telegram can actually send
// has no derivable discriminator: the variants share no common
// Type/Status/Source field at all.
func TestCheckDiscriminatorGate_FailsOnUngatedInboundUnion(t *testing.T) {
api := &spec.API{
Types: []spec.TypeDecl{
{Name: "Widget", OneOf: []string{"WidgetA", "WidgetB"}},
{Name: "WidgetA", Fields: []spec.Field{
{Name: "Kind", JSONName: "kind", Required: true, Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "string"}},
}},
{Name: "WidgetB", Fields: []spec.Field{
{Name: "Flavor", JSONName: "flavor", Required: true, Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "string"}},
}},
// Container is a plain (non-Input) type with a field of the
// undiscriminated union — this is what makes Widget reachable
// from real inbound Telegram data.
{Name: "Container", Fields: []spec.Field{
{Name: "W", JSONName: "w", Type: spec.TypeRef{Kind: spec.KindNamed, Name: "Widget"}},
}},
},
}
err := checkDiscriminatorGate(api)
require.Error(t, err)
require.ErrorContains(t, err, "Widget")
require.ErrorContains(t, err, "WidgetA")
require.ErrorContains(t, err, "WidgetB")
}
// TestCheckDiscriminatorGate_SilentForInputOnlyUnion confirms the same
// undiscriminated union does NOT fail the build when its only reference
// is a field on an Input*-prefixed type — those are always constructed
// and marshalled by the caller, never decoded from a Telegram response.
func TestCheckDiscriminatorGate_SilentForInputOnlyUnion(t *testing.T) {
api := &spec.API{
Types: []spec.TypeDecl{
{Name: "Widget", OneOf: []string{"WidgetA", "WidgetB"}},
{Name: "WidgetA", Fields: []spec.Field{
{Name: "Kind", JSONName: "kind", Required: true, Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "string"}},
}},
{Name: "WidgetB", Fields: []spec.Field{
{Name: "Flavor", JSONName: "flavor", Required: true, Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "string"}},
}},
{Name: "InputContainer", Fields: []spec.Field{
{Name: "W", JSONName: "w", Type: spec.TypeRef{Kind: spec.KindNamed, Name: "Widget"}},
}},
},
}
require.NoError(t, checkDiscriminatorGate(api))
}
+367 -46
View File
@@ -3,6 +3,7 @@ package main
import (
"bytes"
_ "embed"
"errors"
"fmt"
"github.com/goccy/go-json"
"go/format"
@@ -10,6 +11,7 @@ import (
"path/filepath"
"regexp"
"sort"
"strings"
"text/template"
"github.com/lukaszraczylo/go-telegram/internal/spec"
@@ -77,9 +79,18 @@ type discriminatorSpec struct {
Variants map[string]string // discriminator value → concrete Go type name
}
// knownDiscriminators maps parent union name → discriminator spec.
// Used by the template helpers hasDiscriminator / discriminatorField /
// discriminatorMap to emit UnmarshalXxx helpers.
// knownDiscriminators is the hand-curated OVERRIDE table for sealed-union
// discriminators. deriveDiscriminators computes the same kind of map
// automatically from the IR for every union that has one (see
// mergeDiscriminators); an entry here always wins the merge regardless of
// what derivation would have produced. Use this table only for unions the
// generic rule can't handle — e.g. MaybeInaccessibleMessage's integer
// discriminator, which needs bespoke decode logic no discriminatorSpec
// can express (Field is "" and Variants is nil; the template emits a
// hand-coded Unmarshal via isMaybeInaccessibleMessage instead). Every
// other entry here exists only because it predates derivation — removing
// one and re-running `go test -run TestEmit -update ./cmd/genapi/...`
// should reproduce it byte-for-byte from deriveDiscriminators.
var knownDiscriminators = map[string]discriminatorSpec{
"ChatMember": {
Field: "status",
@@ -205,15 +216,24 @@ type emitter struct {
// template to emit per-variant MarshalJSON that hardcodes the
// discriminator so callers don't have to set it by hand.
variantDiscs map[string]variantDiscriminator
// discs is the merged union-level discriminator table: derivation
// output with knownDiscriminators layered on top as overrides (see
// mergeDiscriminators). Every codegen path that decides whether a
// union auto-decodes — and how — consults this instead of the
// package-level knownDiscriminators map, so a union qualifies the
// moment deriveDiscriminators can resolve it, hand-curated or not.
discs map[string]discriminatorSpec
}
func newEmitter(api *spec.API, outDir string) *emitter {
knownInterfaceTypes = buildUnionTypeSet(api)
derived, _ := deriveDiscriminators(api)
return &emitter{
api: api,
outDir: outDir,
enums: planEnums(api),
variantDiscs: variantDiscriminators(api),
discs: mergeDiscriminators(derived),
}
}
@@ -349,6 +369,262 @@ func parseDiscriminatorDoc(doc string) string {
return ""
}
// discriminatorFieldCandidates returns the Go field names to try, in
// priority order, when looking for a discriminator shared by every
// variant of a union: the hand-curated field from knownDiscriminators
// (resolved to its Go name via the first variant that declares it) when
// the union already has an override entry, then "Type", "Status",
// "Source" — the three names Telegram's docs consistently use for this
// purpose. Shared by planUnifiedUnionEnums (enum naming, enums.go) and
// deriveDiscriminators (decode dispatch, below) so the two never disagree
// on where to look.
func discriminatorFieldCandidates(unionName string, variants []*spec.TypeDecl) []string {
var candidates []string
seen := map[string]bool{}
add := func(name string) {
if name == "" || seen[name] {
return
}
seen[name] = true
candidates = append(candidates, name)
}
if ds, ok := knownDiscriminators[unionName]; ok && ds.Field != "" {
for _, v := range variants {
for _, f := range v.Fields {
if f.JSONName == ds.Field {
add(f.Name)
break
}
}
}
}
add("Type")
add("Status")
add("Source")
return candidates
}
// fieldByGoName returns the field named name on fields, or nil if none matches.
func fieldByGoName(fields []spec.Field, name string) *spec.Field {
for i := range fields {
if fields[i].Name == name {
return &fields[i]
}
}
return nil
}
// discriminatorValue extracts the wire-level value a variant field
// contributes to a discriminator: a single-entry EnumValues list (the
// scraper's detected value), or a doc-parsed "must be X" / "always “X”"
// literal via parseDiscriminatorDoc (the same extraction
// extractVariantDiscriminator uses for the first-field case). Returns ""
// when the field isn't a required string or carries no resolvable value.
func discriminatorValue(f spec.Field) string {
if !f.Required || f.Type.Kind != spec.KindPrimitive || f.Type.Name != "string" {
return ""
}
if len(f.EnumValues) == 1 {
return f.EnumValues[0]
}
return parseDiscriminatorDoc(f.Doc)
}
// deriveDiscriminators inspects every union TypeDecl (len(OneOf) > 0) in
// api and tries to pick a discriminator JSON field automatically, so the
// hand-maintained knownDiscriminators map no longer has to name every
// union that qualifies. A union qualifies when, for some candidate field
// (see discriminatorFieldCandidates), EVERY variant declares that field
// and discriminatorValue resolves a non-empty wire value for it, and
// those values are unique across variants.
//
// Returns the derived map plus the names of unions that did not qualify,
// in declaration order. Callers merge the derived map with
// knownDiscriminators (mergeDiscriminators) and decide what to do about
// the leftovers — rescue via a hand-curated override, or fail the build
// if the union is reachable from real Telegram data (checkDiscriminatorGate).
func deriveDiscriminators(api *spec.API) (map[string]discriminatorSpec, []string) {
typeByName := make(map[string]*spec.TypeDecl, len(api.Types))
for i := range api.Types {
typeByName[api.Types[i].Name] = &api.Types[i]
}
derived := make(map[string]discriminatorSpec, 32)
var ungated []string
for i := range api.Types {
u := &api.Types[i]
if len(u.OneOf) == 0 {
continue
}
variants := make([]*spec.TypeDecl, 0, len(u.OneOf))
resolved := true
for _, vName := range u.OneOf {
v, found := typeByName[vName]
if !found {
resolved = false
break
}
variants = append(variants, v)
}
if !resolved {
ungated = append(ungated, u.Name)
continue
}
ds, qualified := deriveUnionDiscriminator(u.Name, variants)
if !qualified {
ungated = append(ungated, u.Name)
continue
}
derived[u.Name] = ds
}
return derived, ungated
}
// deriveUnionDiscriminator tries each candidate Go field name in
// priority order and returns the first one where every variant declares
// that field with a resolvable, unique wire value.
func deriveUnionDiscriminator(unionName string, variants []*spec.TypeDecl) (discriminatorSpec, bool) {
for _, goName := range discriminatorFieldCandidates(unionName, variants) {
jsonField := ""
values := make(map[string]string, len(variants)) // wire value -> variant Go type name
qualifies := true
for _, v := range variants {
f := fieldByGoName(v.Fields, goName)
if f == nil {
qualifies = false
break
}
value := discriminatorValue(*f)
if value == "" {
qualifies = false
break
}
if jsonField == "" {
jsonField = f.JSONName
} else if f.JSONName != jsonField {
qualifies = false
break
}
if _, dup := values[value]; dup {
qualifies = false
break
}
values[value] = v.Name
}
if qualifies {
return discriminatorSpec{Field: jsonField, Variants: values}, true
}
}
return discriminatorSpec{}, false
}
// mergeDiscriminators layers the hand-curated knownDiscriminators table
// on top of derived: every entry deriveDiscriminators produces is used
// as-is unless knownDiscriminators also names that union, in which case
// the hand-curated entry wins outright (the whole discriminatorSpec, not
// a per-key merge).
func mergeDiscriminators(derived map[string]discriminatorSpec) map[string]discriminatorSpec {
merged := make(map[string]discriminatorSpec, len(derived)+len(knownDiscriminators))
for name, ds := range derived {
merged[name] = ds
}
for name, ds := range knownDiscriminators {
merged[name] = ds
}
return merged
}
// isInboundReachable reports whether Telegram can actually send data
// typed as the named union to the bot: as a field on some type that
// isn't itself send-only, or as a method's return value. "Input*"-named
// unions are excluded outright regardless of where they're referenced —
// by Telegram's own naming convention in this API, every such union is
// something the bot constructs and marshals, never something it decodes.
func isInboundReachable(api *spec.API, unionName string) bool {
if strings.HasPrefix(unionName, "Input") {
return false
}
for _, m := range api.Methods {
if typeRefIsUnion(m.Returns, api, unionName) {
return true
}
}
for _, t := range api.Types {
if strings.HasPrefix(t.Name, "Input") {
continue // send-only struct; its fields are never decoded either.
}
for _, f := range t.Fields {
if typeRefIsUnion(f.Type, api, unionName) {
return true
}
}
}
return false
}
// typeRefIsUnion reports whether tr resolves to the named union: directly,
// as an array element, or via an inline oneOf variant-set match against
// unionName's own OneOf list.
func typeRefIsUnion(tr spec.TypeRef, api *spec.API, unionName string) bool {
switch tr.Kind {
case spec.KindNamed:
return tr.Name == unionName
case spec.KindArray:
if tr.ElemType != nil {
return typeRefIsUnion(*tr.ElemType, api, unionName)
}
case spec.KindOneOf:
for i := range api.Types {
if api.Types[i].Name == unionName && matchesVariants(tr.Variants, api.Types[i].OneOf...) {
return true
}
}
}
return false
}
// checkDiscriminatorGate fails codegen when a union Telegram can actually
// send to the bot has no usable discriminator — neither derived nor
// hand-curated. Such a union decodes to a bare interface field that no
// JSON codec can ever fill (the RichBlock/RichText Bot API v10
// regression this generalisation fixes: both had 20+ variants and no
// knownDiscriminators entry, so genapi silently emitted no UnmarshalXxx
// helper at all). Send-only "Input*" unions are exempt — see
// isInboundReachable.
func checkDiscriminatorGate(api *spec.API) error {
_, ungated := deriveDiscriminators(api)
typeByName := make(map[string]*spec.TypeDecl, len(api.Types))
for i := range api.Types {
typeByName[api.Types[i].Name] = &api.Types[i]
}
var offending []string
for _, name := range ungated {
if _, overridden := knownDiscriminators[name]; overridden {
continue // hand-curated rescue, e.g. MaybeInaccessibleMessage.
}
if isInboundReachable(api, name) {
offending = append(offending, name)
}
}
if len(offending) == 0 {
return nil
}
var b strings.Builder
fmt.Fprintf(&b, "genapi: %d inbound union(s) have no derivable or hand-curated discriminator "+
"and would decode as an unfillable interface field:", len(offending))
for _, name := range offending {
fmt.Fprintf(&b, "\n - %s (variants: %s): no single field is present, required, and holds a unique non-empty wire value on every variant",
name, strings.Join(typeByName[name].OneOf, ", "))
}
return errors.New(b.String())
}
// knownInterfaceTypes is the full set of sealed-interface union type names
// (both auto-decoded ones in knownDiscriminators and marker-only ones from
// types with OneOf). Populated at emitter construction. goType and
@@ -359,7 +635,7 @@ var knownInterfaceTypes = map[string]bool{}
// emitTypes renders types.gen.go.
func (e *emitter) emitTypes() error {
t, err := template.New("types").Funcs(funcsWithDiscs(e.enums, e.variantDiscs)).Parse(typesTmpl)
t, err := template.New("types").Funcs(funcsWithDiscs(e.enums, e.variantDiscs, e.discs)).Parse(typesTmpl)
if err != nil {
return fmt.Errorf("parse types.tmpl: %w", err)
}
@@ -396,31 +672,35 @@ func loadAPI(path string) (*spec.API, error) {
}
// funcsWithDiscs returns the shared FuncMap with the variant
// discriminator helpers bound to discs. types.tmpl uses
// discriminator helpers bound to variantDiscs. types.tmpl uses
// variantDiscFor/variantHasDisc to emit per-variant MarshalJSON that
// hardcodes the wire discriminator value.
func funcsWithDiscs(plan *enumPlan, discs map[string]variantDiscriminator) template.FuncMap {
fm := funcs(plan)
// hardcodes the wire discriminator value. unionDiscs is the merged
// union-level table (see mergeDiscriminators), threaded into funcs.
func funcsWithDiscs(plan *enumPlan, variantDiscs map[string]variantDiscriminator, unionDiscs map[string]discriminatorSpec) template.FuncMap {
fm := funcs(plan, unionDiscs)
fm["variantHasDisc"] = func(name string) bool {
_, ok := discs[name]
_, ok := variantDiscs[name]
return ok
}
fm["variantDiscField"] = func(name string) string { return discs[name].JSONField }
fm["variantDiscGoField"] = func(name string) string { return discs[name].GoField }
fm["variantDiscValue"] = func(name string) string { return discs[name].Value }
fm["variantDiscField"] = func(name string) string { return variantDiscs[name].JSONField }
fm["variantDiscGoField"] = func(name string) string { return variantDiscs[name].GoField }
fm["variantDiscValue"] = func(name string) string { return variantDiscs[name].Value }
return fm
}
// funcs is the FuncMap shared across templates. plan is the resolved
// enum plan; pass nil only in unit tests that don't exercise enums.
func funcs(plan *enumPlan) template.FuncMap {
// enum plan; pass nil only in unit tests that don't exercise enums. discs
// is the merged union-level discriminator table (see
// mergeDiscriminators); pass nil (or knownDiscriminators directly) in
// unit tests that only exercise the hand-curated unions.
func funcs(plan *enumPlan, discs map[string]discriminatorSpec) template.FuncMap {
return template.FuncMap{
"goType": goType,
"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)
return goFieldX(plan, methodEnumParent(methodName), title(methodName)+"Params", f)
},
"docComment": docComment,
"isOptional": func(f spec.Field) bool { return !f.Required },
@@ -432,7 +712,7 @@ func funcs(plan *enumPlan) template.FuncMap {
return multipartFieldEntry(plan, parent, f)
},
"multipartFieldEntryP": func(methodName string, f spec.Field) string {
return multipartFieldEntryX(plan, "", title(methodName)+"Params", f)
return multipartFieldEntryX(plan, methodEnumParent(methodName), title(methodName)+"Params", f)
},
"multipartFileEntry": multipartFileEntry,
"returnGoType": returnGoType,
@@ -444,20 +724,22 @@ func funcs(plan *enumPlan) template.FuncMap {
return plan.All()
},
"enumConstName": constName,
// discriminator helpers for types.tmpl
"hasDiscriminator": func(name string) bool { s, ok := knownDiscriminators[name]; return ok && len(s.Variants) > 0 },
// discriminator helpers for types.tmpl — all consult the merged
// table (discs), not the package-level knownDiscriminators, so a
// derived-only union gates exactly like a hand-curated one.
"hasDiscriminator": func(name string) bool { s, ok := discs[name]; return ok && len(s.Variants) > 0 },
"isSealedUnionReturn": func(tr spec.TypeRef) bool {
if tr.Kind != spec.KindNamed {
return false
}
s, ok := knownDiscriminators[tr.Name]
s, ok := discs[tr.Name]
return ok && len(s.Variants) > 0
},
"isSealedUnionArrayReturn": func(tr spec.TypeRef) bool {
if tr.Kind != spec.KindArray || tr.ElemType == nil || tr.ElemType.Kind != spec.KindNamed {
return false
}
s, ok := knownDiscriminators[tr.ElemType.Name]
s, ok := discs[tr.ElemType.Name]
return ok && len(s.Variants) > 0
},
"sealedUnionElemName": func(tr spec.TypeRef) string {
@@ -467,12 +749,13 @@ func funcs(plan *enumPlan) template.FuncMap {
return ""
},
"isMaybeInaccessibleMessage": func(name string) bool { return name == "MaybeInaccessibleMessage" },
"discriminatorField": func(name string) string { return knownDiscriminators[name].Field },
"discriminatorMap": func(name string) map[string]string { return knownDiscriminators[name].Variants },
"discriminatorField": func(name string) string { return discs[name].Field },
"discriminatorMap": func(name string) map[string]string { return discs[name].Variants },
// union-field helpers for per-struct UnmarshalJSON emission
"unionFields": unionFieldsOf,
"isArrayUnion": func(tr spec.TypeRef) bool { return hasUnionElem(tr) },
"unionTypeName": func(tr spec.TypeRef) string { name, _ := unionTypeFor(tr); return name },
"unionFields": func(t spec.TypeDecl) []unionField { return unionFieldsOf(t, discs) },
"isArrayUnion": func(tr spec.TypeRef) bool { return hasUnionElem(tr, discs) },
"unionTypeName": func(tr spec.TypeRef) string { name, _ := unionTypeFor(tr, discs); return name },
"unionAlternates": unionAlternates,
}
}
@@ -645,7 +928,7 @@ func returnGoElem(tr spec.TypeRef) string {
// emitMethods renders methods.gen.go.
func (e *emitter) emitMethods() error {
t, err := template.New("methods").Funcs(funcs(e.enums)).Parse(methodsTmpl)
t, err := template.New("methods").Funcs(funcs(e.enums, e.discs)).Parse(methodsTmpl)
if err != nil {
return fmt.Errorf("parse methods.tmpl: %w", err)
}
@@ -662,7 +945,7 @@ func (e *emitter) emitMethods() error {
// emitEnums renders enums.gen.go.
func (e *emitter) emitEnums() error {
t, err := template.New("enums").Funcs(funcs(e.enums)).Parse(enumsTmpl)
t, err := template.New("enums").Funcs(funcs(e.enums, e.discs)).Parse(enumsTmpl)
if err != nil {
return fmt.Errorf("parse enums.tmpl: %w", err)
}
@@ -735,11 +1018,12 @@ type unionField struct {
}
// unionFieldsOf returns the subset of t.Fields whose type is a known
// discriminated union (directly or as array element).
func unionFieldsOf(t spec.TypeDecl) []unionField {
// discriminated union (directly or as array element), per discs — the
// merged union-level discriminator table (see mergeDiscriminators).
func unionFieldsOf(t spec.TypeDecl, discs map[string]discriminatorSpec) []unionField {
var out []unionField
for _, f := range t.Fields {
if u, ok := unionTypeFor(f.Type); ok {
if u, ok := unionTypeFor(f.Type, discs); ok {
out = append(out, unionField{Field: f, UnionName: u})
}
}
@@ -747,19 +1031,20 @@ func unionFieldsOf(t spec.TypeDecl) []unionField {
}
// unionTypeFor inspects a TypeRef and reports whether it (or its array
// element) is a known discriminated union. Returns the union name and true.
func unionTypeFor(tr spec.TypeRef) (string, bool) {
// element) is a known discriminated union in discs. Returns the union
// name and true.
func unionTypeFor(tr spec.TypeRef, discs map[string]discriminatorSpec) (string, bool) {
switch tr.Kind {
case spec.KindNamed:
if _, ok := knownDiscriminators[tr.Name]; ok {
if _, ok := discs[tr.Name]; ok {
return tr.Name, true
}
case spec.KindArray:
if tr.ElemType != nil {
return unionTypeFor(*tr.ElemType)
return unionTypeFor(*tr.ElemType, discs)
}
case spec.KindOneOf:
if u := unionNameByVariants(tr.Variants); u != "" {
if u := unionNameByVariants(tr.Variants, discs); u != "" {
return u, true
}
}
@@ -768,8 +1053,8 @@ func unionTypeFor(tr spec.TypeRef) (string, bool) {
// unionNameByVariants finds the parent union whose variant type names exactly
// match the given variant set (order-insensitive).
func unionNameByVariants(variants []string) string {
for parentName, ds := range knownDiscriminators {
func unionNameByVariants(variants []string, discs map[string]discriminatorSpec) string {
for parentName, ds := range discs {
wanted := make([]string, 0, len(ds.Variants))
for _, vt := range ds.Variants {
wanted = append(wanted, vt)
@@ -782,14 +1067,50 @@ func unionNameByVariants(variants []string) string {
}
// hasUnionElem reports whether tr is an array whose element type is a known union.
func hasUnionElem(tr spec.TypeRef) bool {
func hasUnionElem(tr spec.TypeRef, discs map[string]discriminatorSpec) bool {
if tr.Kind != spec.KindArray || tr.ElemType == nil {
return false
}
_, ok := unionTypeFor(*tr.ElemType)
_, ok := unionTypeFor(*tr.ElemType, discs)
return ok
}
// alternateVariant describes one synthetic sealed-interface variant the
// generator emits for a union's non-object wire shape (see
// spec.Alternate): a bare-JSON-string variant, or a variant holding a
// JSON array of the union itself. Exactly one of IsString / IsArray is
// set. types.tmpl emits the struct, marker method, and MarshalJSON for
// each, and Unmarshal<Union> dispatches to it by inspecting the first
// non-whitespace byte of the wire value.
type alternateVariant struct {
Name string // synthetic Go type name, e.g. "RichTextPlain"
IsString bool
IsArray bool
}
// unionAlternates maps a union TypeDecl's spec.Alternate entries (see
// cmd/scrape's extractAlternates) to the synthetic Go variant
// descriptors types.tmpl emits alongside the regular OneOf variants:
// "<Union>Plain" for the bare-string shape, "<Union>Sequence" for the
// array-of-<Union> shape. Returns nil for a union with no alternates,
// which is every union except RichText as of Bot API v10 — so this is a
// no-op for the rest of the type list.
func unionAlternates(t spec.TypeDecl) []alternateVariant {
if len(t.Alternates) == 0 {
return nil
}
out := make([]alternateVariant, 0, len(t.Alternates))
for _, a := range t.Alternates {
switch a.Shape {
case spec.AlternateString:
out = append(out, alternateVariant{Name: t.Name + "Plain", IsString: true})
case spec.AlternateArray:
out = append(out, alternateVariant{Name: t.Name + "Sequence", IsArray: true})
}
}
return out
}
// matchesVariants reports whether got equals want as a set (order-insensitive).
func matchesVariants(got []string, want ...string) bool {
if len(got) != len(want) {
@@ -915,15 +1236,15 @@ func buildUnionTypeSet(api *spec.API) map[string]bool {
// used in generated test param literals. plan supplies typed-enum names
// so a method-param sentinel for a ParseMode field becomes a typed
// constant rather than a magic string.
func makeSentinelValue(unionTypes map[string]bool, plan *enumPlan) func(spec.Field) string {
return func(f spec.Field) string {
return sentinelForField(f, unionTypes, plan)
func makeSentinelValue(unionTypes map[string]bool, plan *enumPlan) func(string, spec.Field) string {
return func(methodName string, f spec.Field) string {
return sentinelForField(methodName, f, unionTypes, plan)
}
}
func sentinelForField(f spec.Field, unionTypes map[string]bool, plan *enumPlan) string {
if name := plan.FieldEnum("", f.Name); name != "" && len(f.EnumValues) > 0 {
return constName(name, f.EnumValues[0])
func sentinelForField(methodName string, f spec.Field, unionTypes map[string]bool, plan *enumPlan) string {
if name := plan.FieldEnum(methodEnumParent(methodName), f.Name); name != "" && len(f.EnumValues) > 0 {
return plan.ConstFor(name, f.EnumValues[0])
}
tr := f.Type
switch tr.Kind {
@@ -1023,7 +1344,7 @@ func (e *emitter) emitTests() error {
unionTypes := buildUnionTypeSet(e.api)
// Add test-specific helpers to the shared func map.
fm := funcs(e.enums)
fm := funcs(e.enums, e.discs)
fm["sentinelValue"] = makeSentinelValue(unionTypes, e.enums)
fm["successResp"] = successResp
+77 -34
View File
@@ -26,15 +26,21 @@ type enumPlan struct {
}
// enumKey identifies a single Field occurrence so the emitter can look
// up the enum name later. Parent is "" for method params (the method
// doesn't share a Go type with the field).
// up the enum name later. Parent is the type name for struct fields and
// methodEnumParent(name) for method params, so two methods sharing a
// param name never alias each other's enum (or non-enum) fields.
func enumKey(parent, fieldName string) string { return parent + "::" + fieldName }
// methodEnumParent is the enum-plan key parent for a method's params.
// The "method:" prefix keeps it disjoint from type names.
func methodEnumParent(methodName string) string { return "method:" + methodName }
// planEnums walks the IR, decides on enum names, deduplicates, and
// returns an enumPlan. All scraper-marked enum fields are covered.
func planEnums(api *spec.API) *enumPlan {
type ref struct {
parent string
parent string // naming parent ("" for method params)
keyParent string // byField key parent (methodEnumParent(...) for method params)
fieldName string
jsonName string
values []string
@@ -52,7 +58,7 @@ func planEnums(api *spec.API) *enumPlan {
}
var refs []ref
collect := func(parent string, fields []spec.Field) {
collect := func(parent, keyParent string, fields []spec.Field) {
for _, f := range fields {
if len(f.EnumValues) == 0 {
continue
@@ -62,6 +68,7 @@ func planEnums(api *spec.API) *enumPlan {
}
refs = append(refs, ref{
parent: parent,
keyParent: keyParent,
fieldName: f.Name,
jsonName: f.JSONName,
values: f.EnumValues,
@@ -70,13 +77,15 @@ func planEnums(api *spec.API) *enumPlan {
}
}
for _, t := range api.Types {
collect(t.Name, t.Fields)
collect(t.Name, t.Name, t.Fields)
}
for _, m := range api.Methods {
// Method params have no shared Go parent type, so we pass "" as
// the parent. The default-name heuristic still produces the
// right answer for ParseMode-style enums.
collect("", m.Params)
// Method params have no shared Go parent type, so the naming
// parent is "" (the default-name heuristic still produces the
// right answer for ParseMode-style enums), but the byField key
// is method-scoped so a same-named non-enum param on another
// method can never pick up this enum.
collect("", methodEnumParent(m.Name), m.Params)
}
// candidate name per ref (before collision resolution)
@@ -196,7 +205,7 @@ func planEnums(api *spec.API) *enumPlan {
}
for i, r := range refs {
name := groups[r.valueKey].name
plan.byField[enumKey(r.parent, r.fieldName)] = name
plan.byField[enumKey(r.keyParent, r.fieldName)] = name
_ = i
}
for vk, g := range groups {
@@ -262,32 +271,12 @@ func planUnifiedUnionEnums(api *spec.API) (map[string]enumDecl, map[string]strin
continue
}
// Build the candidate Go-name list. Priority order:
// Build the candidate Go-name list (shared with deriveDiscriminators
// in emitter.go so enum naming and decode dispatch never disagree
// on where to look). 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)
}
candidateNames := discriminatorFieldCandidates(u.Name, variants)
// Find the first candidate Go-name where every variant has a
// matching single-value string-enum field.
@@ -389,6 +378,18 @@ func (p *enumPlan) FieldEnum(parent, fieldName string) string {
return p.byField[enumKey(parent, fieldName)]
}
// ConstFor returns the collision-resolved constant identifier for value
// within the named enum declaration. Falls back to the plain constName
// when the declaration is unknown (unit tests with partial plans).
func (p *enumPlan) ConstFor(enumName, value string) string {
if p != nil {
if d, ok := p.decls[enumName]; ok {
return d.ConstName(value)
}
}
return constName(enumName, value)
}
// defaultEnumName picks an initial Go enum name for a field. parse_mode
// fields collapse to the canonical "ParseMode"; otherwise the name is
// parent + PascalCase(jsonName).
@@ -406,6 +407,48 @@ func constName(enumName, value string) string {
return enumName + valuePascal(value)
}
// ConstName returns the constant identifier for value within this enum,
// resolving case-collisions between values that Pascal-case to the same
// identifier (e.g. RichBlockListItem.type values "a" and "A"). Colliding
// values get a Lower/Upper prefix on the value part based on the case of
// the value's first letter; any residual collision gets a numeric suffix.
func (e enumDecl) ConstName(value string) string {
counts := map[string]int{}
for _, v := range e.Values {
counts[constName(e.Name, v)]++
}
plain := constName(e.Name, value)
if counts[plain] <= 1 {
return plain
}
cased := e.Name + casePrefix(value) + valuePascal(value)
// Residual collision (same value text repeated, or Lower/Upper still
// ambiguous): append the value's 1-based position for determinism.
casedCounts := map[string]int{}
pos := 0
for i, v := range e.Values {
if counts[constName(e.Name, v)] > 1 {
casedCounts[e.Name+casePrefix(v)+valuePascal(v)]++
}
if v == value {
pos = i + 1
}
}
if casedCounts[cased] > 1 {
return cased + itoa(pos)
}
return cased
}
// casePrefix distinguishes case-colliding enum values: "Lower" when the
// value starts with a lowercase letter, "Upper" otherwise.
func casePrefix(v string) string {
if v != "" && v[0] >= 'a' && v[0] <= 'z' {
return "Lower"
}
return "Upper"
}
func valuePascal(v string) string {
// "image/jpeg" → "ImageOfJpeg"
parts := strings.Split(v, "/")
+1 -1
View File
@@ -8,6 +8,6 @@ package api
type {{$e.Name}} string
const (
{{range $v := $e.Values}} {{enumConstName $e.Name $v}} {{$e.Name}} = {{printf "%q" $v}}
{{range $v := $e.Values}} {{$e.ConstName $v}} {{$e.Name}} = {{printf "%q" $v}}
{{end}})
{{end}}
+56
View File
@@ -0,0 +1,56 @@
package main
import (
"testing"
"github.com/lukaszraczylo/go-telegram/internal/spec"
"github.com/stretchr/testify/require"
)
// Regression: Bot API 9.x RichBlockListItem.type enumerates "a"/"A"/"i"/"I"/"1",
// which Pascal-case to the same identifier. ConstName must disambiguate.
func TestEnumDecl_ConstName_CaseCollision(t *testing.T) {
d := enumDecl{Name: "RichBlockListItemType", Values: []string{"a", "A", "i", "I", "1"}}
got := map[string]bool{}
for _, v := range d.Values {
n := d.ConstName(v)
require.False(t, got[n], "duplicate const ident %q for value %q", n, v)
got[n] = true
}
require.Equal(t, "RichBlockListItemTypeLowerA", d.ConstName("a"))
require.Equal(t, "RichBlockListItemTypeUpperA", d.ConstName("A"))
// Non-colliding values keep the plain name.
require.Equal(t, "RichBlockListItemType1", d.ConstName("1"))
}
// Regression: answerChatJoinRequestQuery has an enum param named "result";
// answerGuestQuery has a NON-enum param also named "result". The enum plan
// must scope method params per method so the enum never leaks onto the
// other method's field.
func TestPlanEnums_MethodParamsScopedPerMethod(t *testing.T) {
api := &spec.API{
Methods: []spec.MethodDecl{
{
Name: "answerChatJoinRequestQuery",
Params: []spec.Field{{
Name: "Result", JSONName: "result", Required: true,
Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "string"},
EnumValues: []string{"approve", "decline", "queue"},
}},
},
{
Name: "answerGuestQuery",
Params: []spec.Field{{
Name: "Result", JSONName: "result", Required: true,
Type: spec.TypeRef{Kind: spec.KindNamed, Name: "InlineQueryResult"},
}},
},
},
}
plan := planEnums(api)
require.Equal(t, "Result",
plan.FieldEnum(methodEnumParent("answerChatJoinRequestQuery"), "Result"))
require.Empty(t,
plan.FieldEnum(methodEnumParent("answerGuestQuery"), "Result"),
"non-enum param must not inherit another method's enum")
}
+16 -16
View File
@@ -280,7 +280,7 @@ func TestMultipartFieldEntry_Named_Optional(t *testing.T) {
func TestUnionTypeFor_DirectNamed(t *testing.T) {
tr := spec.TypeRef{Kind: spec.KindNamed, Name: "ChatMember"}
name, ok := unionTypeFor(tr)
name, ok := unionTypeFor(tr, knownDiscriminators)
require.True(t, ok)
require.Equal(t, "ChatMember", name)
}
@@ -288,26 +288,26 @@ func TestUnionTypeFor_DirectNamed(t *testing.T) {
func TestUnionTypeFor_Array(t *testing.T) {
elem := spec.TypeRef{Kind: spec.KindNamed, Name: "ChatMember"}
tr := spec.TypeRef{Kind: spec.KindArray, ElemType: &elem}
name, ok := unionTypeFor(tr)
name, ok := unionTypeFor(tr, knownDiscriminators)
require.True(t, ok)
require.Equal(t, "ChatMember", name)
}
func TestUnionTypeFor_ArrayNilElem(t *testing.T) {
tr := spec.TypeRef{Kind: spec.KindArray}
_, ok := unionTypeFor(tr)
_, ok := unionTypeFor(tr, knownDiscriminators)
require.False(t, ok)
}
func TestUnionTypeFor_NotUnion(t *testing.T) {
tr := spec.TypeRef{Kind: spec.KindNamed, Name: "Message"}
_, ok := unionTypeFor(tr)
_, ok := unionTypeFor(tr, knownDiscriminators)
require.False(t, ok)
}
func TestUnionTypeFor_Unknown(t *testing.T) {
tr := spec.TypeRef{Kind: spec.Kind(99)}
_, ok := unionTypeFor(tr)
_, ok := unionTypeFor(tr, knownDiscriminators)
require.False(t, ok)
}
@@ -321,12 +321,12 @@ func TestUnionNameByVariants_ChatMember(t *testing.T) {
"ChatMemberOwner", "ChatMemberAdministrator", "ChatMemberMember",
"ChatMemberRestricted", "ChatMemberLeft", "ChatMemberBanned",
}
name := unionNameByVariants(variants)
name := unionNameByVariants(variants, knownDiscriminators)
require.Equal(t, "ChatMember", name)
}
func TestUnionNameByVariants_Unknown(t *testing.T) {
name := unionNameByVariants([]string{"X", "Y", "Z"})
name := unionNameByVariants([]string{"X", "Y", "Z"}, knownDiscriminators)
require.Equal(t, "", name)
}
@@ -336,24 +336,24 @@ func TestUnionNameByVariants_Unknown(t *testing.T) {
func TestHasUnionElem_NonArray(t *testing.T) {
tr := spec.TypeRef{Kind: spec.KindNamed, Name: "ChatMember"}
require.False(t, hasUnionElem(tr))
require.False(t, hasUnionElem(tr, knownDiscriminators))
}
func TestHasUnionElem_ArrayNilElem(t *testing.T) {
tr := spec.TypeRef{Kind: spec.KindArray}
require.False(t, hasUnionElem(tr))
require.False(t, hasUnionElem(tr, knownDiscriminators))
}
func TestHasUnionElem_ArrayUnionElem(t *testing.T) {
elem := spec.TypeRef{Kind: spec.KindNamed, Name: "ChatMember"}
tr := spec.TypeRef{Kind: spec.KindArray, ElemType: &elem}
require.True(t, hasUnionElem(tr))
require.True(t, hasUnionElem(tr, knownDiscriminators))
}
func TestHasUnionElem_ArrayNonUnionElem(t *testing.T) {
elem := spec.TypeRef{Kind: spec.KindNamed, Name: "Message"}
tr := spec.TypeRef{Kind: spec.KindArray, ElemType: &elem}
require.False(t, hasUnionElem(tr))
require.False(t, hasUnionElem(tr, knownDiscriminators))
}
// ---------------------------------------------------------------------------
@@ -369,7 +369,7 @@ func TestUnionFieldsOf_WithUnionField(t *testing.T) {
{Name: "Date", JSONName: "date", Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "int64"}},
},
}
uf := unionFieldsOf(td)
uf := unionFieldsOf(td, knownDiscriminators)
require.Len(t, uf, 2)
require.Equal(t, "ChatMember", uf[0].UnionName)
}
@@ -574,7 +574,7 @@ func TestSentinelForField(t *testing.T) {
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got := sentinelForField(c.field, unionTypes, nil)
got := sentinelForField("testMethod", c.field, unionTypes, nil)
require.Contains(t, got, c.contains, "sentinelForField for %q", c.name)
})
}
@@ -621,14 +621,14 @@ func TestUnionTypeFor_OneOfVariants(t *testing.T) {
"ChatMemberRestricted", "ChatMemberLeft", "ChatMemberBanned",
},
}
name, ok := unionTypeFor(tr)
name, ok := unionTypeFor(tr, knownDiscriminators)
require.True(t, ok)
require.Equal(t, "ChatMember", name)
}
func TestUnionTypeFor_OneOfNoMatch(t *testing.T) {
tr := spec.TypeRef{Kind: spec.KindOneOf, Variants: []string{"Foo", "Bar"}}
_, ok := unionTypeFor(tr)
_, ok := unionTypeFor(tr, knownDiscriminators)
require.False(t, ok)
}
@@ -637,7 +637,7 @@ func TestUnionTypeFor_OneOfNoMatch(t *testing.T) {
// ---------------------------------------------------------------------------
func TestFuncs_HasExpectedKeys(t *testing.T) {
fm := funcs(nil)
fm := funcs(nil, nil)
require.NotNil(t, fm)
for _, key := range []string{"goType", "docComment", "returnGoType", "unionFields"} {
require.NotNil(t, fm[key], "funcs() missing key %q", key)
+3
View File
@@ -29,6 +29,9 @@ func run(input, outdir string) error {
if err != nil {
return fmt.Errorf("load api: %w", err)
}
if err := checkDiscriminatorGate(api); err != nil {
return err
}
if err := os.MkdirAll(outdir, 0o750); err != nil {
return err
}
+7 -7
View File
@@ -48,7 +48,7 @@ func Test_{{$mName}}_Success(t *testing.T) {
{{- if .Params}}
params := &{{$mName}}Params{
{{- range .Params}}{{if .Required}}
{{.Name}}: {{sentinelValue .}},{{end}}
{{.Name}}: {{sentinelValue $m.Name .}},{{end}}
{{- end}}
}
_, err := {{$mName}}(context.Background(), bot, params)
@@ -67,7 +67,7 @@ func Test_{{$mName}}_APIError(t *testing.T) {
{{- if .Params}}
params := &{{$mName}}Params{
{{- range .Params}}{{if .Required}}
{{.Name}}: {{sentinelValue .}},{{end}}
{{.Name}}: {{sentinelValue $m.Name .}},{{end}}
{{- end}}
}
_, err := {{$mName}}(context.Background(), bot, params)
@@ -89,7 +89,7 @@ func Test_{{$mName}}_NetworkError(t *testing.T) {
{{- if .Params}}
params := &{{$mName}}Params{
{{- range .Params}}{{if .Required}}
{{.Name}}: {{sentinelValue .}},{{end}}
{{.Name}}: {{sentinelValue $m.Name .}},{{end}}
{{- end}}
}
_, err := {{$mName}}(context.Background(), bot, params)
@@ -109,7 +109,7 @@ func Test_{{$mName}}_ParseError(t *testing.T) {
{{- if .Params}}
params := &{{$mName}}Params{
{{- range .Params}}{{if .Required}}
{{.Name}}: {{sentinelValue .}},{{end}}
{{.Name}}: {{sentinelValue $m.Name .}},{{end}}
{{- end}}
}
_, err := {{$mName}}(context.Background(), bot, params)
@@ -132,7 +132,7 @@ func Test_{{$mName}}_ContextCanceled(t *testing.T) {
{{- if .Params}}
params := &{{$mName}}Params{
{{- range .Params}}{{if .Required}}
{{.Name}}: {{sentinelValue .}},{{end}}
{{.Name}}: {{sentinelValue $m.Name .}},{{end}}
{{- end}}
}
_, err := {{$mName}}(ctx, bot, params)
@@ -177,7 +177,7 @@ func Test_{{$mName}}_Forbidden(t *testing.T) {
{{- if .Params}}
params := &{{$mName}}Params{
{{- range .Params}}{{if .Required}}
{{.Name}}: {{sentinelValue .}},{{end}}
{{.Name}}: {{sentinelValue $m.Name .}},{{end}}
{{- end}}
}
_, err := {{$mName}}(context.Background(), bot, params)
@@ -203,7 +203,7 @@ func Test_{{$mName}}_ServerError(t *testing.T) {
{{- if .Params}}
params := &{{$mName}}Params{
{{- range .Params}}{{if .Required}}
{{.Name}}: {{sentinelValue .}},{{end}}
{{.Name}}: {{sentinelValue $m.Name .}},{{end}}
{{- end}}
}
_, err := {{$mName}}(context.Background(), bot, params)
+65 -6
View File
@@ -7,14 +7,16 @@
package api
import (
"bytes"
"github.com/goccy/go-json"
"fmt"
"io"
)
var _ = io.Discard // keep import even if no fields use io
var _ = json.Marshal // keep import for UnmarshalXxx helpers
var _ = fmt.Errorf // keep import for UnmarshalXxx helpers
var _ = io.Discard // keep import even if no fields use io
var _ = json.Marshal // keep import for UnmarshalXxx helpers
var _ = fmt.Errorf // keep import for UnmarshalXxx helpers
var _ = bytes.TrimLeft // keep import for shape-dispatching UnmarshalXxx helpers (unions with alternates)
{{range .Types}}
{{- $td := . -}}
@@ -22,6 +24,7 @@ var _ = fmt.Errorf // keep import for UnmarshalXxx helpers
// {{.Name}} is a union type. The following concrete variants implement
// it:
{{range .OneOf}}// - {{.}}
{{end}}{{range unionAlternates .}}// - {{.Name}} ({{if .IsString}}bare JSON string{{else}}JSON array of {{$td.Name}}{{end}})
{{end}}//
{{docComment .Doc -}}
type {{.Name}} interface{ is{{.Name}}() }
@@ -30,11 +33,67 @@ type {{.Name}} interface{ is{{.Name}}() }
// is{{$td.Name}} is the marker method that makes {{.}} implement {{$td.Name}}.
func (*{{.}}) is{{$td.Name}}() {}
{{end}}
{{range unionAlternates .}}
// {{.Name}} implements {{$td.Name}} for the {{if .IsString}}bare JSON string{{else}}JSON array of {{$td.Name}}{{end}} wire
// shape Telegram documents for this union, alongside its object variants.
type {{.Name}} struct {
{{if .IsString}} Text string
{{else}} Items []{{$td.Name}}
{{end}}}
// is{{$td.Name}} is the marker method that makes {{.Name}} implement {{$td.Name}}.
func (*{{.Name}}) is{{$td.Name}}() {}
{{if .IsString}}// MarshalJSON encodes {{.Name}} back to its bare JSON string wire shape.
func (v *{{.Name}}) MarshalJSON() ([]byte, error) {
return json.Marshal(v.Text)
}
{{else}}// MarshalJSON encodes {{.Name}} back to its bare JSON array wire shape.
func (v *{{.Name}}) MarshalJSON() ([]byte, error) {
return json.Marshal(v.Items)
}
{{end}}
{{end}}
{{if hasDiscriminator .Name}}
// Unmarshal{{.Name}} decodes a {{.Name}} from JSON by inspecting the
{{$alts := unionAlternates .}}{{if $alts}}// Unmarshal{{.Name}} decodes a {{.Name}} from JSON. Telegram documents
// this type as either a bare JSON string, a JSON array of {{.Name}}, or
// an object dispatched by its "{{discriminatorField .Name}}" field;
// leading whitespace is trimmed and the first remaining byte selects
// the shape.
{{else}}// Unmarshal{{.Name}} decodes a {{.Name}} from JSON by inspecting the
// "{{discriminatorField .Name}}" field and dispatching to the correct concrete type.
func Unmarshal{{.Name}}(data []byte) ({{.Name}}, error) {
var probe struct {
{{end}}func Unmarshal{{.Name}}(data []byte) ({{.Name}}, error) {
{{if $alts}} trimmed := bytes.TrimLeft(data, " \t\r\n")
if len(trimmed) == 0 {
return nil, fmt.Errorf("{{.Name}}: empty JSON value")
}
switch trimmed[0] {
{{range $alts}}{{if .IsString}} case '"':
var s string
if err := json.Unmarshal(data, &s); err != nil {
return nil, fmt.Errorf("{{$td.Name}}: decoding string: %w", err)
}
return &{{.Name}}{Text: s}, nil
{{else}} case '[':
var raws []json.RawMessage
if err := json.Unmarshal(data, &raws); err != nil {
return nil, fmt.Errorf("{{$td.Name}}: decoding array: %w", err)
}
items := make([]{{$td.Name}}, 0, len(raws))
for i, r := range raws {
v, err := Unmarshal{{$td.Name}}(r)
if err != nil {
return nil, fmt.Errorf("{{$td.Name}}: decoding array[%d]: %w", i, err)
}
items = append(items, v)
}
return &{{.Name}}{Items: items}, nil
{{end}}{{end}} case '{':
// fall through to the discriminator switch below.
default:
return nil, fmt.Errorf("{{.Name}}: unrecognised JSON value starting with %q", trimmed[0])
}
{{end}} var probe struct {
V string `json:"{{discriminatorField .Name}}"`
}
if err := json.Unmarshal(data, &probe); err != nil {
+50
View File
@@ -0,0 +1,50 @@
package main
import (
"regexp"
"github.com/lukaszraczylo/go-telegram/internal/spec"
)
// altStringRE matches the scalar-string alternate clause Telegram uses
// for union types that can also decode as plain text on the wire, e.g.
// "a String for plain text". The trailing "for ..." clause is optional
// and, when present, is not captured — only the shape matters. "an"
// (rather than "a") is tolerated so a future rephrasing ("an String")
// doesn't silently stop matching, and the word boundary keeps "Strings"
// or "SubString" from misfiring.
var altStringRE = regexp.MustCompile(`(?i)\ban?\s+String\b`)
// altArrayRE builds the pattern that matches the self-referential array
// alternate clause Telegram uses for recursive union types, e.g. "an
// Array of RichText". Matching is anchored to the union's own name so a
// doc mentioning "an Array of" some other type is never mistaken for a
// shape alternate.
func altArrayRE(unionName string) *regexp.Regexp {
return regexp.MustCompile(`(?i)\ban?\s+Array\s+of\s+` + regexp.QuoteMeta(unionName) + `\b`)
}
// extractAlternates detects Telegram's "can be either a String for
// plain text, an Array of <Union>, or any of the following types:"
// phrasing on a union type's doc string and returns the non-object
// shapes it declares, in a fixed (string, then array) order regardless
// of how the sentence orders its clauses.
//
// Conservative by design: callers invoke this only for types that
// already have OneOf populated (see typeFromSection in scrape.go), and
// it only recognises the two shapes Telegram's docs currently use for
// this ("a String" and "an Array of <the union's own name>") — anything
// else in the doc is ignored, so free-text mentioning "String" or
// "Array" elsewhere in a union's description can't produce a false
// positive for a type this pattern wasn't written for. As of Bot API
// v10, RichText is the only union whose doc matches either pattern.
func extractAlternates(unionName, doc string) []spec.Alternate {
var alts []spec.Alternate
if altStringRE.MatchString(doc) {
alts = append(alts, spec.Alternate{Shape: spec.AlternateString})
}
if altArrayRE(unionName).MatchString(doc) {
alts = append(alts, spec.Alternate{Shape: spec.AlternateArray})
}
return alts
}
+99
View File
@@ -0,0 +1,99 @@
package main
import (
"os"
"testing"
"github.com/stretchr/testify/require"
"github.com/lukaszraczylo/go-telegram/internal/spec"
)
func TestExtractAlternates_RichTextDoc(t *testing.T) {
// Exact doc string as scraped from the live Bot API v10 docs page
// (internal/spec/api.json / testdata/html/latest.html).
doc := "This object represents a rich formatted text. Currently, it can be either a String for plain text, " +
"an Array of RichText, or any of the following types:"
got := extractAlternates("RichText", doc)
require.Equal(t, []spec.Alternate{
{Shape: spec.AlternateString},
{Shape: spec.AlternateArray},
}, got)
}
func TestExtractAlternates_OrderIndependent(t *testing.T) {
// Same two clauses, reversed, plus an extra clause in between —
// order and surrounding prose must not matter.
doc := "Currently, it can be either an Array of RichText, something else entirely, a String for plain text, " +
"or any of the following types:"
got := extractAlternates("RichText", doc)
require.Equal(t, []spec.Alternate{
{Shape: spec.AlternateString},
{Shape: spec.AlternateArray},
}, got)
}
func TestExtractAlternates_ObjectOnlyUnion_NoAlternates(t *testing.T) {
// RichBlock's doc is object-only: no String/Array alternate clause.
doc := "This object represents a block in a rich formatted message. Currently, it can be any of the following types:"
got := extractAlternates("RichBlock", doc)
require.Nil(t, got)
}
func TestExtractAlternates_ArrayOfDifferentType_NotSelfReferential(t *testing.T) {
// "an Array of X" only counts when X is the union's own name — an
// array of some other type mentioned in the doc must not match.
doc := "This object can be either an Array of PhotoSize or any of the following types:"
got := extractAlternates("RichText", doc)
require.Nil(t, got)
}
func TestExtractAlternates_StringOnly(t *testing.T) {
doc := "This object can be either a String or any of the following types:"
got := extractAlternates("Widget", doc)
require.Equal(t, []spec.Alternate{{Shape: spec.AlternateString}}, got)
}
func TestExtractAlternates_PlainTypeDoc_NoFalsePositive(t *testing.T) {
// Ordinary field/type prose mentioning neither shape must not match.
doc := "This object represents a chat photo."
got := extractAlternates("ChatPhoto", doc)
require.Nil(t, got)
}
// TestTypeFromSection_RichText_RealSnapshot grounds the extraction in the
// real pinned docs snapshot (not just a literal fixture string): scraping
// testdata/html/latest.html end-to-end must produce a RichText TypeDecl
// with both alternates, matching the same live text that broke decoding
// of plain-text and array-shaped rich_message payloads.
func TestTypeFromSection_RichText_RealSnapshot(t *testing.T) {
htmlBytes, err := os.ReadFile("../../testdata/html/latest.html")
require.NoError(t, err)
api, err := scrape(htmlBytes)
require.NoError(t, err)
var richText *spec.TypeDecl
for i := range api.Types {
if api.Types[i].Name == "RichText" {
richText = &api.Types[i]
break
}
}
require.NotNil(t, richText, "RichText type must be present in the real docs snapshot")
require.Equal(t, []spec.Alternate{
{Shape: spec.AlternateString},
{Shape: spec.AlternateArray},
}, richText.Alternates)
// RichBlock is the control: object-only union, no alternates.
var richBlock *spec.TypeDecl
for i := range api.Types {
if api.Types[i].Name == "RichBlock" {
richBlock = &api.Types[i]
break
}
}
require.NotNil(t, richBlock)
require.Nil(t, richBlock.Alternates)
}
+1 -1
View File
@@ -42,7 +42,7 @@ func extractReturn(desc string) spec.TypeRef {
return spec.TypeRef{Kind: spec.KindArray, ElemType: &elem}
}},
// "an array of X of the sent messages is returned" (ForwardMessages/CopyMessages shape).
{regexp.MustCompile(`(?:[Oo]n success[,.]?\s+)?an? array of ([A-Z][A-Za-z0-9]+)(?:\s+of [^.]+?)?\s+(?:objects\s+)?(?:is|are) returned`), func(m []string) spec.TypeRef {
{regexp.MustCompile(`(?:[Oo]n success[,.]?\s+)?an? [Aa]rray of ([A-Z][A-Za-z0-9]+)(?:\s+of [^.]+?)?\s+(?:objects\s+)?(?:is|are) returned`), func(m []string) spec.TypeRef {
elem := primitiveOrNamed(m[1])
return spec.TypeRef{Kind: spec.KindArray, ElemType: &elem}
}},
+2
View File
@@ -29,6 +29,8 @@ func TestExtractReturn(t *testing.T) {
{"On success, an array of Message objects that were sent is returned.", spec.TypeRef{Kind: spec.KindArray, ElemType: &spec.TypeRef{Kind: spec.KindNamed, Name: "Message"}}},
// ForwardMessages/CopyMessages shape: "an array of X of the sent messages is returned".
{"On success, an array of MessageId of the sent messages is returned.", spec.TypeRef{Kind: spec.KindArray, ElemType: &spec.TypeRef{Kind: spec.KindNamed, Name: "MessageId"}}},
// Bot API 10.1 recapitalised the same sentence to "an Array of X".
{"On success, an Array of MessageId of the sent messages is returned.", spec.TypeRef{Kind: spec.KindArray, ElemType: &spec.TypeRef{Kind: spec.KindNamed, Name: "MessageId"}}},
// "Returns X on success" (no article) — OwnedGifts, StarAmount, Story, MenuButton, etc.
{"Returns the gifts received and owned by a managed business account. Returns OwnedGifts on success.", spec.TypeRef{Kind: spec.KindNamed, Name: "OwnedGifts"}},
{"Returns StarAmount on success.", spec.TypeRef{Kind: spec.KindNamed, Name: "StarAmount"}},
+4
View File
@@ -38,6 +38,10 @@ func typeFromSection(s section) spec.TypeDecl {
} else if len(s.Lists) > 0 {
// Union: extract variant names from <li><a>...</a></li>.
td.OneOf = extractListLinks(s.Lists[0])
// Some unions (e.g. RichText) also declare non-object wire shapes
// ("a String for plain text", "an Array of RichText") in the same
// sentence as the object-variant list; extract those too.
td.Alternates = extractAlternates(td.Name, td.Doc)
}
return td
}
+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
}
+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 **12–20% faster** than every competitor and ties telego for the lowest alloc count (11).
- **Large Update unmarshal** (entities + reply markup + photo array): ours is **17–34% 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.5–2.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.
+4950 -1093
View File
File diff suppressed because it is too large Load Diff
+118 -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>)
@@ -79,8 +87,18 @@ var (
)
```
<a name="Version"></a>Version is a fallback version string used only when Go's build info is unavailable \(replace directives, detached \`go run\`\) or has been overridden via linker flags. The authoritative version forwarded to telemetry is resolved at runtime by \[telemetry.SendForModule\] from the build info of whatever binary linked this library, so this constant does NOT need to be bumped on every release. Exposed as a var \(not const\) for ldflag override:
```
go build -ldflags="-X github.com/lukaszraczylo/go-telegram/client.Version=1.2.3"
```
```go
var Version = "0.0.0-fallback"
```
<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 +111,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 +184,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 +207,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 +216,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 +225,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 +234,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 +243,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 +252,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 +261,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 +273,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 +282,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 +290,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 +308,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 +400,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>)
+9 -9
View File
@@ -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 -1
View File
@@ -26,7 +26,7 @@ func handleEcho(c *dispatch.Context, m *api.Message) error {
_, err := api.SendMessage(c.Ctx, c.Bot, &api.SendMessageParams{
ChatID: api.ChatIDFromInt(m.Chat.ID),
Text: m.Text,
ReplyParameters: &api.ReplyParameters{MessageID: m.MessageID},
ReplyParameters: &api.ReplyParameters{MessageID: &m.MessageID},
})
return err
}
+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{
+11 -8
View File
@@ -1,16 +1,19 @@
module github.com/lukaszraczylo/go-telegram
go 1.25.0
go 1.26.0
require (
github.com/goccy/go-json v0.10.6
github.com/stretchr/testify v1.9.0
golang.org/x/net v0.54.0
github.com/goccy/go-json v0.11.2
github.com/lukaszraczylo/oss-telemetry v0.2.5
github.com/stretchr/testify v1.12.1
github.com/valyala/fasthttp v1.75.0
golang.org/x/net v0.59.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
github.com/klauspost/compress v1.20.1 // indirect
github.com/molecule-man/go-brrr v1.2.0 // indirect
github.com/stretchr/objx v0.5.3 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
)
+22 -16
View File
@@ -1,16 +1,22 @@
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/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=
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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
github.com/goccy/go-json v0.11.2 h1:jdZv93Tt4ioR8yW1CoNsvSxrcZlCXAUU1aZXN7gpXUA=
github.com/goccy/go-json v0.11.2/go.mod h1:3NdmfEkZlB7YI5UFw/qdFKq8XN1aiWR0YyRPWZNQltY=
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/lukaszraczylo/oss-telemetry v0.2.5 h1:W6Qgwp7IY4yBRO361MmnEAmM59motdQpbAFAx8v6fJM=
github.com/lukaszraczylo/oss-telemetry v0.2.5/go.mod h1:+Cn78qZo8rc3T9eZt0v3oICYRdd75wORtSidc8lNjDQ=
github.com/molecule-man/go-brrr v1.2.0 h1:dOJU45BC3Gc2WXoxAW0rgCAgVuu2gruAOGncfieA0Jw=
github.com/molecule-man/go-brrr v1.2.0/go.mod h1:7ybW6/7gA3oKY45jOfVNjSJDtrr6ea4tzbsTkjmQDC4=
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
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.75.0 h1:W3xrvNJHG60j2ZCjxpP8kkB+oPG8kh9/g1n4+3xUwNA=
github.com/valyala/fasthttp v1.75.0/go.mod h1:JiVkPRZU124PuPsreV/s9dZRgG2lRK7wNEikdQ0Rk4s=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
+4968 -293
View File
File diff suppressed because it is too large Load Diff
+25
View File
@@ -24,6 +24,31 @@ type TypeDecl struct {
// OneOf, when non-empty, indicates this type is a union and lists the concrete variant type names.
// Variants are emitted as concrete structs implementing a sealed interface.
OneOf []string `json:"one_of,omitempty"`
// Alternates lists non-object wire shapes this union may also decode
// as, in addition to the object variants in OneOf. Only populated for
// unions whose doc declares them (Telegram's "can be either a String
// for plain text, an Array of X, or any of the following types:"
// phrasing) — as of Bot API v10 that's RichText alone, so every other
// type's serialised IR is unaffected by this field's existence.
Alternates []Alternate `json:"alternates,omitempty"`
}
// AlternateShape enumerates the non-object wire shapes a union's
// Alternates field records.
type AlternateShape string
const (
// AlternateString: the union may decode from a bare JSON string.
AlternateString AlternateShape = "string"
// AlternateArray: the union may decode from a JSON array whose
// elements are themselves the same union type.
AlternateArray AlternateShape = "array"
)
// Alternate records one non-object shape a union type may appear as on
// the wire, beyond its OneOf object variants.
type Alternate struct {
Shape AlternateShape `json:"shape"`
}
// MethodDecl describes a Telegram API method.
+20
View File
@@ -22,6 +22,12 @@ type Overrides struct {
// ApprovedBoolMethods lists methods whose returns are genuinely bool.
// The audit tool ignores these.
ApprovedBoolMethods []string `json:"approved_bool_methods,omitempty"`
// ApprovedAnyLocations lists IR locations whose union renders as `any` by
// design, keyed "<TypeName>.<FieldName>" for struct fields,
// "<methodName>.<ParamName>" for params and "<methodName>" for returns.
// The audit tool ignores these.
ApprovedAnyLocations []string `json:"approved_any_locations,omitempty"`
}
// LoadOverrides reads and parses overrides.json. Returns an empty Overrides
@@ -73,3 +79,17 @@ func (o *Overrides) IsBoolApproved(methodName string) bool {
}
return false
}
// IsAnyApproved reports whether location is on the approved `any` list.
// See ApprovedAnyLocations for the key format.
func (o *Overrides) IsAnyApproved(location string) bool {
if o == nil {
return false
}
for _, n := range o.ApprovedAnyLocations {
if n == location {
return true
}
}
return false
}
+3
View File
@@ -1,6 +1,9 @@
{
"method_returns": {},
"field_types": {},
"approved_any_locations": [
"InputRichMessageMedia.Media"
],
"approved_bool_methods": [
"setWebhook",
"deleteWebhook",
+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)
}
}
+34
View File
@@ -0,0 +1,34 @@
module github.com/lukaszraczylo/go-telegram/test/benchmarks
go 1.26.7
replace github.com/lukaszraczylo/go-telegram => ../../
require (
github.com/NicoNex/echotron/v3 v3.46.0
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
github.com/go-telegram/bot v1.27.0
github.com/lukaszraczylo/go-telegram v0.0.0-00010101000000-000000000000
github.com/mymmrac/telego v1.12.1
gopkg.in/telebot.v3 v3.3.8
)
require (
github.com/bytedance/gopkg v0.1.4 // indirect
github.com/bytedance/sonic v1.15.4 // indirect
github.com/bytedance/sonic/loader v0.5.2 // indirect
github.com/cloudwego/base64x v0.1.7 // indirect
github.com/goccy/go-json v0.11.2 // indirect
github.com/grbit/go-json v0.11.0 // indirect
github.com/klauspost/compress v1.20.1 // indirect
github.com/klauspost/cpuid/v2 v2.4.0 // indirect
github.com/lukaszraczylo/oss-telemetry v0.2.5 // indirect
github.com/molecule-man/go-brrr v1.2.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.75.0 // indirect
github.com/valyala/fastjson v1.6.10 // indirect
golang.org/x/arch v0.32.0 // indirect
golang.org/x/sys v0.49.0 // indirect
golang.org/x/time v0.16.0 // indirect
)
+908
View File
@@ -0,0 +1,908 @@
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.46.0 h1:IHPhEDU3pbxa3uLnxRz5e247SYEVzIbQS014qvuWFOU=
github.com/NicoNex/echotron/v3 v3.46.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/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.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
github.com/bytedance/sonic v1.15.4 h1:FgtV/4aBHpla9AxuMpuuzVUpa/Cf3izufkxNmnEzdI8=
github.com/bytedance/sonic v1.15.4/go.mod h1:8e51yTPdY8M6t+vvGL1c2Y1xL9i+frEeIAQAEl75NUc=
github.com/bytedance/sonic/loader v0.5.2 h1:0QtP1gevc1OZ6/H8Lb9BRZiCXd1Ftjd3OKuj1T1lBIo=
github.com/bytedance/sonic/loader v0.5.2/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.7 h1:NppS+Fgzg5ovhn4NkUXaDT3x9jldgH5ToMCqzBSi2zI=
github.com/cloudwego/base64x v0.1.7/go.mod h1:Cu1PV9zfrSf7ET2tIbWbbEy7jO7HHJ13q4X2SQ8aWYg=
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.27.0 h1:PG9yBDzM8O26oUerE9SZwWlHon4RpH8WcNslZ7CEZmE=
github.com/go-telegram/bot v1.27.0/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM=
github.com/goccy/go-json v0.11.2 h1:jdZv93Tt4ioR8yW1CoNsvSxrcZlCXAUU1aZXN7gpXUA=
github.com/goccy/go-json v0.11.2/go.mod h1:3NdmfEkZlB7YI5UFw/qdFKq8XN1aiWR0YyRPWZNQltY=
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.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
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/lukaszraczylo/oss-telemetry v0.2.5 h1:W6Qgwp7IY4yBRO361MmnEAmM59motdQpbAFAx8v6fJM=
github.com/lukaszraczylo/oss-telemetry v0.2.5/go.mod h1:+Cn78qZo8rc3T9eZt0v3oICYRdd75wORtSidc8lNjDQ=
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/molecule-man/go-brrr v1.2.0 h1:dOJU45BC3Gc2WXoxAW0rgCAgVuu2gruAOGncfieA0Jw=
github.com/molecule-man/go-brrr v1.2.0/go.mod h1:7ybW6/7gA3oKY45jOfVNjSJDtrr6ea4tzbsTkjmQDC4=
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.12.1 h1:yx1T5pPSNsU3BjLR7jnfY0D4dtL9caH58Y9e8uzjR88=
github.com/mymmrac/telego v1.12.1/go.mod h1:K4z3Z3Qr6AA8yEjSry3JGScu506NlLl1O4Gqascmop4=
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/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/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
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.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
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.75.0 h1:W3xrvNJHG60j2ZCjxpP8kkB+oPG8kh9/g1n4+3xUwNA=
github.com/valyala/fasthttp v1.75.0/go.mod h1:JiVkPRZU124PuPsreV/s9dZRgG2lRK7wNEikdQ0Rk4s=
github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4=
github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE=
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=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/arch v0.32.0 h1:NjGf8/xGf5JgAJ4dw4NtME5R1L7xatagqAg95T3LgII=
golang.org/x/arch v0.32.0/go.mod h1:KcJSod3cqT2dKcjBxqTyGfbumNikqU9p5tHJinPJnuY=
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.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
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.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
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/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
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/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)
}
}
}
+5 -3
View File
@@ -7,14 +7,16 @@
package api
import (
"bytes"
"fmt"
"github.com/goccy/go-json"
"io"
)
var _ = io.Discard // keep import even if no fields use io
var _ = json.Marshal // keep import for UnmarshalXxx helpers
var _ = fmt.Errorf // keep import for UnmarshalXxx helpers
var _ = io.Discard // keep import even if no fields use io
var _ = json.Marshal // keep import for UnmarshalXxx helpers
var _ = fmt.Errorf // keep import for UnmarshalXxx helpers
var _ = bytes.TrimLeft // keep import for shape-dispatching UnmarshalXxx helpers (unions with alternates)
// This object represents a Telegram user or bot.
type User struct {
+1 -1
View File
@@ -1 +1 @@
snapshot_2026-05-08.html
snapshot_2026-09-14.html
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+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)
}
}
}
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# Exports BOT_API_VERSION for the .goreleaser.yaml release header.
set -euo pipefail
[ -n "${GITHUB_ENV:-}" ] || exit 0
ver=$(python3 -c 'import json; print(json.load(open("internal/spec/api.json"))["version"])')
if [ -z "$ver" ] || [ "$ver" = "null" ]; then
exit 0
fi
echo "BOT_API_VERSION=${ver}" >> "$GITHUB_ENV"