mirror of
https://github.com/lukaszraczylo/go-telegram.git
synced 2026-06-06 22:49:32 +00:00
Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0731f10907 | |||
| 6685414374 | |||
| 404411b20c | |||
| 8d85e61da5 | |||
| 609c4ce649 | |||
| d39be13822 | |||
| d6ecbdea48 | |||
| 99c906c3c5 | |||
| f5250197b7 | |||
| bbbeb8461b | |||
| bfb7e9875e | |||
| 75c7ce3119 | |||
| 607c3e8ddd | |||
| c9a062ea04 | |||
| 7eb3398396 | |||
| 26b98a5372 | |||
| a416bda5f3 | |||
| 0ee539e991 | |||
| da27421521 | |||
| 728b28b0c5 | |||
| 9cfe193e2e | |||
| 79c0617867 | |||
| 60eb0a89b5 | |||
| fecef22f48 | |||
| 5523ed2b06 | |||
| 62c76e7e4e | |||
| 6f9b29ea0c | |||
| bd80af240d | |||
| af180b75c5 | |||
| 29bf575cfd | |||
| 370c9c0802 | |||
| 6ab80c27e1 | |||
| 13ea7097e1 | |||
| f899cc2663 | |||
| 5a27b53f30 | |||
| 1ff26006f7 | |||
| f3fbef64ca | |||
| e4614d800f | |||
| 3c04d7b0b1 | |||
| 1da759ba8a | |||
| 1088b7f4d7 | |||
| 35058dd70b | |||
| 750a7f51f6 | |||
| b491829267 | |||
| ac7cae8fa7 |
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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%)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
+33
-39
@@ -25,12 +25,12 @@ jobs:
|
||||
vet:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.25.x'
|
||||
check-latest: true
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
@@ -41,12 +41,12 @@ jobs:
|
||||
staticcheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.25.x'
|
||||
check-latest: true
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
@@ -58,12 +58,12 @@ jobs:
|
||||
govulncheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.25.x'
|
||||
check-latest: true
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
@@ -75,12 +75,12 @@ jobs:
|
||||
gosec:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.25.x'
|
||||
check-latest: true
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
@@ -98,12 +98,12 @@ jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.25.x'
|
||||
check-latest: true
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
@@ -112,7 +112,7 @@ jobs:
|
||||
- run: go test -race -coverprofile=coverage.out ./...
|
||||
- name: Build all examples
|
||||
run: go build ./examples/...
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: coverage
|
||||
path: coverage.out
|
||||
@@ -120,12 +120,12 @@ jobs:
|
||||
codegen-clean:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.25.x'
|
||||
check-latest: true
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
@@ -134,19 +134,19 @@ jobs:
|
||||
- name: Regenerate against pinned snapshot
|
||||
run: make regen-from-fixture
|
||||
- name: Assert clean diff
|
||||
run: git diff --exit-code internal/spec/api.json api/
|
||||
run: git diff --exit-code internal/spec/api.json api/ docs/reference/
|
||||
|
||||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0 # need history for drift comparison
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.25.x'
|
||||
check-latest: true
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
@@ -196,11 +196,11 @@ jobs:
|
||||
github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.25.x'
|
||||
check-latest: true
|
||||
@@ -245,26 +245,20 @@ jobs:
|
||||
API_TAG: ${{ steps.api_version.outputs.tag }}
|
||||
API_VER: ${{ steps.api_version.outputs.version }}
|
||||
run: |
|
||||
# Bot API version (currently $API_VER) is intentionally NOT
|
||||
# tagged separately. semver-generator picks the most recent
|
||||
# tag as the version base; a non-SemVer marker like
|
||||
# bot-api-vX.Y poisons that and restarts numbering from
|
||||
# v0.0.x. Bot API version stays as a comment in the lib tag
|
||||
# message and in the release notes.
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git tag -a "$LIB_TAG" -m "Release $LIB_TAG"
|
||||
git tag -a "$LIB_TAG" -m "Release $LIB_TAG (Bot API $API_VER)"
|
||||
git push origin "$LIB_TAG"
|
||||
|
||||
if [ -n "$API_TAG" ]; then
|
||||
# Force-update the bot-api tag so it always points at the latest
|
||||
# release that supports that API version.
|
||||
if git rev-parse "$API_TAG" >/dev/null 2>&1; then
|
||||
git tag -f -a "$API_TAG" -m "go-telegram release $LIB_TAG (Bot API $API_VER)"
|
||||
git push -f origin "$API_TAG"
|
||||
else
|
||||
git tag -a "$API_TAG" -m "go-telegram release $LIB_TAG (Bot API $API_VER)"
|
||||
git push origin "$API_TAG"
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Run GoReleaser
|
||||
if: github.event_name != 'workflow_dispatch' || inputs.dry-run-release == false
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
uses: goreleaser/goreleaser-action@v7
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: '~> v2'
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
name: pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- '.github/workflows/pages.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/configure-pages@v5
|
||||
- uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: docs/
|
||||
- id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
@@ -13,12 +13,12 @@ jobs:
|
||||
regen:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0 # full history so audit -drift can compare against main
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.25.x'
|
||||
check-latest: true
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
|
||||
- name: Open PR
|
||||
if: steps.diff.outputs.no_changes != 'true'
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@v8
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: |
|
||||
|
||||
@@ -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
|
||||
|
||||
+10
-6
@@ -19,11 +19,12 @@ blacklist:
|
||||
- "Merge remote-tracking branch"
|
||||
- "go mod tidy"
|
||||
|
||||
# Strip the auto-generated bot-api-vX.Y tag prefix when scanning existing
|
||||
# tags — those are markers that point at library releases, not version
|
||||
# sources themselves.
|
||||
tag_prefixes:
|
||||
- "bot-api-"
|
||||
# NOTE: do not configure tag_prefixes here. The action's behaviour with
|
||||
# that option present is to FILTER tags down to those matching the
|
||||
# listed prefixes — meaning v0.x.y / v1.x.y tags get ignored and version
|
||||
# numbering restarts from zero. The bot-api-vX.Y tag is created by a
|
||||
# separate workflow step and does not need to participate in version
|
||||
# selection.
|
||||
|
||||
wording:
|
||||
patch:
|
||||
@@ -39,5 +40,8 @@ wording:
|
||||
minor:
|
||||
- "feat"
|
||||
major:
|
||||
- "breaking"
|
||||
# Match only the canonical Conventional Commits trailer. The bare
|
||||
# word "breaking" is too greedy under semver-generator's fuzzy match —
|
||||
# it triggers on substrings like "breaking-value drift" inside a
|
||||
# commit body and wrongly produces a major bump.
|
||||
- "BREAKING CHANGE"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.PHONY: test test-race lint vet integration regen snapshot regen-from-fixture test-update-golden clean clean-generated audit audit-drift help
|
||||
.PHONY: test test-race lint vet integration regen snapshot regen-from-fixture test-update-golden clean clean-generated audit audit-drift docs docs-check help
|
||||
|
||||
GO ?= go
|
||||
|
||||
@@ -14,6 +14,8 @@ help:
|
||||
@echo " test-update-golden - refresh golden test fixtures (Plan 2)"
|
||||
@echo " audit - report any-typed/bool fallbacks in current IR"
|
||||
@echo " audit-drift - audit + compare against HEAD's IR for signature changes"
|
||||
@echo " docs - regenerate markdown reference docs into docs/reference/"
|
||||
@echo " docs-check - assert docs/reference/ is up to date (CI gate)"
|
||||
@echo " clean-generated - delete generated api/*.gen.go and internal/spec/api.json"
|
||||
@echo " clean - clean-generated + transient artefacts (binaries, coverage)"
|
||||
|
||||
@@ -33,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:
|
||||
@@ -44,12 +49,14 @@ regen: clean-generated
|
||||
$(GO) run ./cmd/audit -ir $(SCRAPE_OUTPUT)
|
||||
$(GO) run ./cmd/genapi -input $(SCRAPE_OUTPUT) -outdir api
|
||||
$(GO) test ./api/...
|
||||
$(MAKE) docs
|
||||
|
||||
regen-from-fixture: clean-generated
|
||||
$(GO) run ./cmd/scrape -input $(SCRAPE_INPUT) -output $(SCRAPE_OUTPUT)
|
||||
$(GO) run ./cmd/audit -ir $(SCRAPE_OUTPUT)
|
||||
$(GO) run ./cmd/genapi -input $(SCRAPE_OUTPUT) -outdir api
|
||||
$(GO) test ./api/...
|
||||
$(MAKE) docs
|
||||
|
||||
audit:
|
||||
$(GO) run ./cmd/audit -ir $(SCRAPE_OUTPUT)
|
||||
@@ -61,6 +68,32 @@ test-update-golden:
|
||||
$(GO) test -run TestEmit -update ./cmd/genapi/...
|
||||
$(GO) test -run TestScrape -update ./cmd/scrape/...
|
||||
|
||||
# Regenerate godoc-style markdown reference docs into docs/reference/.
|
||||
# Auto-installs gomarkdoc on first run.
|
||||
DOC_PACKAGES := \
|
||||
./client \
|
||||
./transport \
|
||||
./dispatch \
|
||||
./dispatch/conversation \
|
||||
./dispatch/filters/message \
|
||||
./dispatch/filters/callback \
|
||||
./dispatch/filters/inline \
|
||||
./dispatch/filters/chatmember \
|
||||
./dispatch/filters/chatjoinrequest \
|
||||
./dispatch/filters/precheckoutquery \
|
||||
./api
|
||||
|
||||
docs:
|
||||
@which gomarkdoc > /dev/null || (echo "installing gomarkdoc..." && $(GO) install github.com/princjef/gomarkdoc/cmd/gomarkdoc@v1.1.0)
|
||||
gomarkdoc \
|
||||
--repository.url=https://github.com/lukaszraczylo/go-telegram \
|
||||
--repository.default-branch=main \
|
||||
--repository.path=/ \
|
||||
-o 'docs/reference/{{.Dir}}.md' $(DOC_PACKAGES)
|
||||
|
||||
docs-check: docs
|
||||
@git diff --exit-code docs/reference/ || (echo "docs/reference/ is stale — run 'make docs' and commit" && exit 1)
|
||||
|
||||
# clean-generated removes ONLY codegen output. Source code (cmd/scrape,
|
||||
# cmd/genapi, runtime helpers) is untouched. Run before regen to avoid
|
||||
# orphan files lingering when the IR shrinks (renamed/removed methods).
|
||||
|
||||
@@ -1,61 +1,71 @@
|
||||
# go-telegram
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="docs/logo-dark.svg">
|
||||
<img alt="go-telegram" src="docs/logo-light.svg" width="320">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
> A fully-generated, strongly-typed Go client for the Telegram Bot API — no `any`, no guessing.
|
||||
<p align="center">
|
||||
<strong>Build Telegram bots in Go that just work.</strong><br>
|
||||
Type-safe. Batteries included. Always up to date with the latest Bot API.
|
||||
</p>
|
||||
|
||||
[](https://github.com/lukaszraczylo/go-telegram/actions/workflows/ci.yml)
|
||||
[](https://pkg.go.dev/github.com/lukaszraczylo/go-telegram)
|
||||
[](go.mod)
|
||||
[](LICENSE)
|
||||
<p align="center">
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram/actions/workflows/ci.yml"><img src="https://github.com/lukaszraczylo/go-telegram/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
||||
<a href="https://pkg.go.dev/github.com/lukaszraczylo/go-telegram"><img src="https://pkg.go.dev/badge/github.com/lukaszraczylo/go-telegram.svg" alt="Go Reference"></a>
|
||||
<a href="go.mod"><img src="https://img.shields.io/github/go-mod/go-version/lukaszraczylo/go-telegram" alt="Go Version"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License: MIT"></a>
|
||||
</p>
|
||||
|
||||
> Bot API **v10.0** · 176 methods · 301 types · 1428 auto-generated tests
|
||||
<p align="center">
|
||||
Bot API <strong>v10.0</strong> · 176 methods · 301 types · 1428 auto-generated tests
|
||||
</p>
|
||||
|
||||
Most Telegram bot libraries expose Telegram's "Integer or String" fields as `interface{}` or `any`. Every union type in go-telegram is a real Go type with compile-time safety and auto-decoding. The entire API surface is code-generated from a committed HTML snapshot of the live Telegram docs — regenerating picks up new Bot API versions in one command, with a self-verifying pipeline that catches regressions before they ship.
|
||||
<p align="center">
|
||||
<a href="https://go-telegram.raczylo.com/">Website</a> ·
|
||||
<a href="docs/reference/">API Reference</a> ·
|
||||
<a href="examples/">Examples</a> ·
|
||||
<a href="https://pkg.go.dev/github.com/lukaszraczylo/go-telegram">pkg.go.dev</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## Hello, Telegram 👋
|
||||
|
||||
```go
|
||||
bot := client.New(os.Getenv("TELEGRAM_BOT_TOKEN"),
|
||||
client.WithHTTPClient(client.NewRetryDoer(client.NewDefaultHTTPDoer())),
|
||||
)
|
||||
|
||||
bot := client.New(os.Getenv("TELEGRAM_BOT_TOKEN"))
|
||||
router := dispatch.New(bot)
|
||||
|
||||
router.OnCommand("/start", func(c *dispatch.Context, m *api.Message) error {
|
||||
_, err := api.SendMessage(c.Ctx, c.Bot, &api.SendMessageParams{
|
||||
ChatID: api.ChatIDFromInt(m.Chat.ID),
|
||||
Text: "Hello! Send me anything to echo.",
|
||||
})
|
||||
return err
|
||||
})
|
||||
router.OnText(`.+`, func(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},
|
||||
Text: "Hi " + m.From.FirstName + "! 👋",
|
||||
})
|
||||
return err
|
||||
})
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
router.Run(ctx, transport.NewLongPoller(bot))
|
||||
```
|
||||
|
||||
## Why go-telegram
|
||||
That's a working bot. No magic strings, no `any`, no guessing what fields exist — your editor autocompletes everything.
|
||||
|
||||
| Feature | What it means for you |
|
||||
|---|---|
|
||||
| **Typed unions** | `ChatID`, `MessageOrBool`, `InputFile`, and 13 discriminated-union interfaces give you `switch v.(type)` instead of runtime panics |
|
||||
| **Full Bot API v10.0** | 176 methods and 301 types — all generated, none hand-written, nothing missing |
|
||||
| **Self-verifying codegen** | `make snapshot && make regen` regenerates everything and runs 1428 tests; any regression fails the pipeline |
|
||||
| **Pluggable transport + codec** | `HTTPDoer` and `Codec` are one-method interfaces — swap in fasthttp, sonic, or your test fake without forking |
|
||||
| **Retry middleware** | `RetryDoer` honours Telegram's `retry_after`, backs off on 5xx, replays request bodies |
|
||||
| **Composable dispatcher** | Per-update goroutine pool (default 50), filter combinators (`And`/`Or`/`Not`), conversation state machines, named handlers |
|
||||
## Why you'll like it
|
||||
|
||||
## Quickstart
|
||||
- 🎯 **No `any`, anywhere.** Telegram's "Integer or String" and "one of N types" unions are real Go types you can `switch` on.
|
||||
- 🔋 **Batteries included.** Long-poll, webhooks, retries on rate limits, conversation state machines, filters, handler groups — out of the box.
|
||||
- 🔄 **Always current.** The whole API is generated from Telegram's live docs. New Bot API release? `make regen` and you're done.
|
||||
- 🪶 **Pluggable everything.** Swap the HTTP client, JSON codec, or storage backend with a one-method interface. No forks.
|
||||
- 🧪 **Already tested.** 1428 generated tests cover every method × every failure mode (success, API errors, network failures, parse errors, timeouts, missing fields, forbidden, server errors).
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
go get github.com/lukaszraczylo/go-telegram
|
||||
```
|
||||
|
||||
Full echo bot — long-poll, graceful shutdown, retry on 429:
|
||||
## A complete echo bot
|
||||
|
||||
Long-poll, graceful shutdown, retries on Telegram's `429 retry_after`:
|
||||
|
||||
```go
|
||||
package main
|
||||
@@ -131,7 +141,30 @@ Run any example: `TELEGRAM_BOT_TOKEN=xxx go run ./examples/<name>`
|
||||
| | [`polls`](examples/polls) | `sendPoll` and answer tally |
|
||||
| | [`payments`](examples/payments) | Invoice → pre-checkout → success |
|
||||
|
||||
## Concepts
|
||||
## Optional fields
|
||||
|
||||
Telegram marks many fields as optional. For optional **scalars** (int, bool, float) we use pointers so you can explicitly send `false` or `0` when the wire format needs to override a chat default. The `api.Ptr` helper keeps that ergonomic:
|
||||
|
||||
```go
|
||||
api.SendMessage(ctx, bot, &api.SendMessageParams{
|
||||
ChatID: api.ChatIDFromInt(chatID),
|
||||
Text: "hi",
|
||||
DisableNotification: api.Ptr(true), // type inferred
|
||||
})
|
||||
|
||||
api.GetUserProfilePhotos(ctx, bot, &api.GetUserProfilePhotosParams{
|
||||
UserID: userID,
|
||||
Limit: api.Ptr[int64](5), // explicit type for untyped literals
|
||||
})
|
||||
```
|
||||
|
||||
Optional structs and slices are already nullable in Go — no helper needed.
|
||||
|
||||
## Reference docs
|
||||
|
||||
Full API reference is auto-generated from source comments and lives in [`docs/reference/`](docs/reference/README.md) — browse package by package on GitHub, or read it rendered at [go-telegram.raczylo.com](https://go-telegram.raczylo.com/) and [pkg.go.dev](https://pkg.go.dev/github.com/lukaszraczylo/go-telegram).
|
||||
|
||||
## How it works
|
||||
|
||||
<details>
|
||||
<summary>Bot client and pluggable transport</summary>
|
||||
@@ -284,25 +317,39 @@ r.OnCommand("/cmd", named.Handler())
|
||||
|
||||
</details>
|
||||
|
||||
## Codegen pipeline
|
||||
## Benchmarks
|
||||
|
||||
The full API surface in `api/*.gen.go` is generated from a committed HTML snapshot of `core.telegram.org/bots/api`:
|
||||
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:
|
||||
|
||||
```bash
|
||||
make snapshot # fetch and commit latest HTML from core.telegram.org
|
||||
make regen # scrape → audit → emit Go code → run generated tests
|
||||
go test -race ./...
|
||||
make snapshot # grab the latest HTML from core.telegram.org
|
||||
make regen # scrape → audit → emit Go → run tests → regenerate docs
|
||||
```
|
||||
|
||||
`make regen` is self-verifying. The audit tool (`cmd/audit`) checks:
|
||||
The audit tool checks for `any`-typed escapes, surprise `bool` returns, and signature drift. CI runs it on every PR, and a weekly workflow opens an auto-PR with regenerated code so a new Bot API version never sits longer than a week.
|
||||
|
||||
- `any`-typed fields or returns that escaped the union machinery
|
||||
- Methods returning `bool` not on the approved list (`internal/spec/overrides.json`)
|
||||
- Signature drift vs HEAD's IR (added/removed/changed return types)
|
||||
|
||||
Exit codes: 0 clean · 1 fallback · 2 drift · 3 invalid. CI runs the audit on every PR. A weekly `regen.yml` workflow opens a PR with regenerated code and the audit summary in the body.
|
||||
|
||||
To track a new Bot API release: run `make snapshot && make regen`, review the audit output, update `internal/spec/overrides.json` for any newly unparseable methods, and submit a PR.
|
||||
If something in Telegram's docs trips up the scraper, add an override to `internal/spec/overrides.json`. The audit will tell you what to put there.
|
||||
|
||||
## Testing
|
||||
|
||||
@@ -324,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).
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
json "github.com/goccy/go-json"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestUnifiedEnum_BotCommandScopeType_Constants confirms the prose-form
|
||||
// discriminator detection promoted BotCommandScope's per-variant Type
|
||||
// fields into one shared enum.
|
||||
func TestUnifiedEnum_BotCommandScopeType_Constants(t *testing.T) {
|
||||
require.IsType(t, BotCommandScopeType(""), BotCommandScopeTypeDefault)
|
||||
|
||||
got := []BotCommandScopeType{
|
||||
BotCommandScopeTypeDefault,
|
||||
BotCommandScopeTypeAllPrivateChats,
|
||||
BotCommandScopeTypeAllGroupChats,
|
||||
BotCommandScopeTypeAllChatAdministrators,
|
||||
BotCommandScopeTypeChat,
|
||||
BotCommandScopeTypeChatAdministrators,
|
||||
BotCommandScopeTypeChatMember,
|
||||
}
|
||||
want := []string{
|
||||
"default", "all_private_chats", "all_group_chats",
|
||||
"all_chat_administrators", "chat", "chat_administrators", "chat_member",
|
||||
}
|
||||
require.Len(t, got, len(want))
|
||||
for i, v := range got {
|
||||
require.Equal(t, want[i], string(v))
|
||||
}
|
||||
}
|
||||
|
||||
// TestUnifiedEnum_InlineQueryResultType_VariantFields walks the variants
|
||||
// and asserts each one's Type field is the unified enum.
|
||||
func TestUnifiedEnum_InlineQueryResultType_VariantFields(t *testing.T) {
|
||||
require.IsType(t, InlineQueryResultType(""), InlineQueryResultTypeArticle)
|
||||
|
||||
wantType := reflect.TypeOf(InlineQueryResultType(""))
|
||||
cases := []any{
|
||||
&InlineQueryResultArticle{},
|
||||
&InlineQueryResultPhoto{},
|
||||
&InlineQueryResultGif{},
|
||||
&InlineQueryResultMpeg4Gif{},
|
||||
&InlineQueryResultVideo{},
|
||||
&InlineQueryResultAudio{},
|
||||
&InlineQueryResultVoice{},
|
||||
&InlineQueryResultDocument{},
|
||||
&InlineQueryResultLocation{},
|
||||
&InlineQueryResultVenue{},
|
||||
&InlineQueryResultContact{},
|
||||
&InlineQueryResultGame{},
|
||||
}
|
||||
for _, c := range cases {
|
||||
rt := reflect.TypeOf(c).Elem()
|
||||
f, ok := rt.FieldByName("Type")
|
||||
require.True(t, ok, "%s missing Type field", rt.Name())
|
||||
require.Equal(t, wantType, f.Type, "%s.Type type mismatch", rt.Name())
|
||||
}
|
||||
}
|
||||
|
||||
// TestUnifiedEnum_PassportElementErrorSource_VariantFields asserts the
|
||||
// retype landed on every variant of the PassportElementError union.
|
||||
func TestUnifiedEnum_PassportElementErrorSource_VariantFields(t *testing.T) {
|
||||
require.IsType(t, PassportElementErrorSource(""), PassportElementErrorSourceData)
|
||||
|
||||
wantType := reflect.TypeOf(PassportElementErrorSource(""))
|
||||
cases := []any{
|
||||
&PassportElementErrorDataField{},
|
||||
&PassportElementErrorFrontSide{},
|
||||
&PassportElementErrorReverseSide{},
|
||||
&PassportElementErrorSelfie{},
|
||||
&PassportElementErrorFile{},
|
||||
&PassportElementErrorFiles{},
|
||||
&PassportElementErrorTranslationFile{},
|
||||
&PassportElementErrorTranslationFiles{},
|
||||
&PassportElementErrorUnspecified{},
|
||||
}
|
||||
for _, c := range cases {
|
||||
rt := reflect.TypeOf(c).Elem()
|
||||
f, ok := rt.FieldByName("Source")
|
||||
require.True(t, ok, "%s missing Source field", rt.Name())
|
||||
require.Equal(t, wantType, f.Type, "%s.Source type mismatch", rt.Name())
|
||||
}
|
||||
}
|
||||
|
||||
// TestUnifiedEnum_InputMediaType_Constants covers a media-shaped union
|
||||
// where the discriminator value is the wire identifier "animation",
|
||||
// "photo", etc.
|
||||
func TestUnifiedEnum_InputMediaType_Constants(t *testing.T) {
|
||||
require.IsType(t, InputMediaType(""), InputMediaTypePhoto)
|
||||
|
||||
wantType := reflect.TypeOf(InputMediaType(""))
|
||||
for _, c := range []any{
|
||||
&InputMediaAnimation{},
|
||||
&InputMediaAudio{},
|
||||
&InputMediaDocument{},
|
||||
&InputMediaPhoto{},
|
||||
&InputMediaVideo{},
|
||||
} {
|
||||
rt := reflect.TypeOf(c).Elem()
|
||||
f, ok := rt.FieldByName("Type")
|
||||
require.True(t, ok, "%s missing Type field", rt.Name())
|
||||
require.Equal(t, wantType, f.Type, "%s.Type type mismatch", rt.Name())
|
||||
}
|
||||
}
|
||||
|
||||
// TestUnifiedEnum_MenuButtonType_Constants covers the third single-Type
|
||||
// union pulled in by the prose detector.
|
||||
func TestUnifiedEnum_MenuButtonType_Constants(t *testing.T) {
|
||||
require.IsType(t, MenuButtonType(""), MenuButtonTypeCommands)
|
||||
|
||||
wantType := reflect.TypeOf(MenuButtonType(""))
|
||||
for _, c := range []any{
|
||||
&MenuButtonCommands{},
|
||||
&MenuButtonWebApp{},
|
||||
&MenuButtonDefault{},
|
||||
} {
|
||||
rt := reflect.TypeOf(c).Elem()
|
||||
f, ok := rt.FieldByName("Type")
|
||||
require.True(t, ok, "%s missing Type field", rt.Name())
|
||||
require.Equal(t, wantType, f.Type, "%s.Type type mismatch", rt.Name())
|
||||
}
|
||||
}
|
||||
|
||||
// TestUnifiedEnum_InlineQueryResultArticle_RoundTrip confirms the
|
||||
// auto-injected discriminator survives a marshal-unmarshal cycle on the
|
||||
// concrete variant and lands as the typed enum constant. There's no
|
||||
// generated UnmarshalInlineQueryResult — the union has no entry in
|
||||
// knownDiscriminators — so the round-trip targets the variant directly.
|
||||
func TestUnifiedEnum_InlineQueryResultArticle_RoundTrip(t *testing.T) {
|
||||
orig := &InlineQueryResultArticle{
|
||||
ID: "x1",
|
||||
Title: "test",
|
||||
}
|
||||
raw, err := json.Marshal(orig)
|
||||
require.NoError(t, err)
|
||||
|
||||
var probe struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(raw, &probe))
|
||||
require.Equal(t, "article", probe.Type)
|
||||
|
||||
// Strip InputMessageContent before re-decoding: it's a sealed
|
||||
// interface and the variant has no UnmarshalJSON helper to dispatch
|
||||
// it. The discriminator round-trip is the property under test, not
|
||||
// nested-union deserialisation.
|
||||
var round struct {
|
||||
Type InlineQueryResultType `json:"type"`
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(raw, &round))
|
||||
require.Equal(t, InlineQueryResultTypeArticle, round.Type)
|
||||
require.Equal(t, orig.ID, round.ID)
|
||||
require.Equal(t, orig.Title, round.Title)
|
||||
}
|
||||
|
||||
// TestUnifiedEnum_PassportElementErrorDataField_RoundTrip mirrors the
|
||||
// above for the Source-discriminated union.
|
||||
func TestUnifiedEnum_PassportElementErrorDataField_RoundTrip(t *testing.T) {
|
||||
orig := &PassportElementErrorDataField{
|
||||
Type: "personal_details",
|
||||
FieldName: "first_name",
|
||||
DataHash: "abc",
|
||||
Message: "boom",
|
||||
}
|
||||
raw, err := json.Marshal(orig)
|
||||
require.NoError(t, err)
|
||||
|
||||
var probe struct {
|
||||
Source string `json:"source"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(raw, &probe))
|
||||
require.Equal(t, "data", probe.Source)
|
||||
|
||||
var round PassportElementErrorDataField
|
||||
require.NoError(t, json.Unmarshal(raw, &round))
|
||||
require.Equal(t, PassportElementErrorSourceData, round.Source)
|
||||
require.Equal(t, orig.FieldName, round.FieldName)
|
||||
}
|
||||
|
||||
// TestUnifiedEnum_BotCommandScopeChat_RoundTrip covers a bot-command
|
||||
// scope variant with a non-trivial extra field (ChatID).
|
||||
func TestUnifiedEnum_BotCommandScopeChat_RoundTrip(t *testing.T) {
|
||||
orig := &BotCommandScopeChat{ChatID: ChatIDFromInt(42)}
|
||||
raw, err := json.Marshal(orig)
|
||||
require.NoError(t, err)
|
||||
|
||||
var probe struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(raw, &probe))
|
||||
require.Equal(t, "chat", probe.Type)
|
||||
|
||||
var round BotCommandScopeChat
|
||||
require.NoError(t, json.Unmarshal(raw, &round))
|
||||
require.Equal(t, BotCommandScopeTypeChat, round.Type)
|
||||
}
|
||||
|
||||
// TestUnifiedEnum_InputMessageContent_NoEnumEmitted confirms the IMC
|
||||
// union — which dispatches structurally on field presence rather than a
|
||||
// shared discriminator — does NOT get a unified enum, since none of its
|
||||
// variants declare a single-value discriminator field.
|
||||
func TestUnifiedEnum_InputMessageContent_NoEnumEmitted(t *testing.T) {
|
||||
for _, name := range []string{
|
||||
"InputTextMessageContent",
|
||||
"InputLocationMessageContent",
|
||||
"InputVenueMessageContent",
|
||||
"InputContactMessageContent",
|
||||
"InputInvoiceMessageContent",
|
||||
} {
|
||||
switch name {
|
||||
case "InputTextMessageContent":
|
||||
rt := reflect.TypeOf(&InputTextMessageContent{}).Elem()
|
||||
_, ok := rt.FieldByName("Type")
|
||||
require.False(t, ok, "%s unexpectedly grew a Type field", name)
|
||||
case "InputLocationMessageContent":
|
||||
rt := reflect.TypeOf(&InputLocationMessageContent{}).Elem()
|
||||
_, ok := rt.FieldByName("Type")
|
||||
require.False(t, ok, "%s unexpectedly grew a Type field", name)
|
||||
case "InputVenueMessageContent":
|
||||
rt := reflect.TypeOf(&InputVenueMessageContent{}).Elem()
|
||||
_, ok := rt.FieldByName("Type")
|
||||
require.False(t, ok, "%s unexpectedly grew a Type field", name)
|
||||
case "InputContactMessageContent":
|
||||
rt := reflect.TypeOf(&InputContactMessageContent{}).Elem()
|
||||
_, ok := rt.FieldByName("Type")
|
||||
require.False(t, ok, "%s unexpectedly grew a Type field", name)
|
||||
case "InputInvoiceMessageContent":
|
||||
rt := reflect.TypeOf(&InputInvoiceMessageContent{}).Elem()
|
||||
_, ok := rt.FieldByName("Type")
|
||||
require.False(t, ok, "%s unexpectedly grew a Type field", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestDiceEmoji_Constants pins the canonical six dice-emoji values so a
|
||||
// regen, refactor, or accidental rename can't silently break the wire
|
||||
// contract.
|
||||
func TestDiceEmoji_Constants(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
got DiceEmoji
|
||||
want string
|
||||
}{
|
||||
{"Dice", DiceEmojiDice, "🎲"},
|
||||
{"Dart", DiceEmojiDart, "🎯"},
|
||||
{"Basketball", DiceEmojiBasketball, "🏀"},
|
||||
{"Football", DiceEmojiFootball, "⚽"},
|
||||
{"Bowling", DiceEmojiBowling, "🎳"},
|
||||
{"SlotMachine", DiceEmojiSlotMachine, "🎰"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
require.Equal(t, c.want, string(c.got))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSendDiceParams_EmojiFieldType asserts the codegen override wired
|
||||
// SendDiceParams.Emoji to the typed enum (not plain string). Reflection
|
||||
// catches a regression even if the file compiles via implicit string
|
||||
// conversion of an untyped literal.
|
||||
func TestSendDiceParams_EmojiFieldType(t *testing.T) {
|
||||
rt := reflect.TypeOf(SendDiceParams{})
|
||||
f, ok := rt.FieldByName("Emoji")
|
||||
require.True(t, ok, "SendDiceParams.Emoji not present")
|
||||
require.Equal(t, "DiceEmoji", f.Type.Name())
|
||||
}
|
||||
|
||||
// TestSendDiceParams_MarshalJSON exercises the marshalled wire form to
|
||||
// prove the typed enum still serialises as a JSON string holding the
|
||||
// raw emoji bytes — i.e. the type override doesn't accidentally
|
||||
// double-encode.
|
||||
func TestSendDiceParams_MarshalJSON(t *testing.T) {
|
||||
p := &SendDiceParams{
|
||||
ChatID: ChatIDFromInt(1),
|
||||
Emoji: DiceEmojiBasketball,
|
||||
}
|
||||
data, err := json.Marshal(p)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, string(data), `"emoji":"🏀"`)
|
||||
}
|
||||
|
||||
// TestReactionEmoji_Constants spot-checks a representative slice of the
|
||||
// 73-value enum. A full enumeration would be redundant — the test is
|
||||
// here to lock the wire form, not to retest the const-block.
|
||||
func TestReactionEmoji_Constants(t *testing.T) {
|
||||
require.Equal(t, "👍", string(ReactionEmojiThumbsUp))
|
||||
require.Equal(t, "👎", string(ReactionEmojiThumbsDown))
|
||||
require.Equal(t, "❤", string(ReactionEmojiHeart))
|
||||
require.Equal(t, "🔥", string(ReactionEmojiFire))
|
||||
require.Equal(t, "💯", string(ReactionEmojiHundredPoints))
|
||||
require.Equal(t, "🤡", string(ReactionEmojiClown))
|
||||
}
|
||||
|
||||
// TestReactionTypeEmoji_FieldType asserts the codegen override wired
|
||||
// ReactionTypeEmoji.Emoji to the typed enum.
|
||||
func TestReactionTypeEmoji_FieldType(t *testing.T) {
|
||||
rt := reflect.TypeOf(ReactionTypeEmoji{})
|
||||
f, ok := rt.FieldByName("Emoji")
|
||||
require.True(t, ok, "ReactionTypeEmoji.Emoji not present")
|
||||
require.Equal(t, "ReactionEmoji", f.Type.Name())
|
||||
}
|
||||
|
||||
// TestReactionTypeEmoji_RoundTrip proves a typed-enum value survives
|
||||
// JSON marshal → unmarshal cycle without losing fidelity. The
|
||||
// discriminator MarshalJSON on ReactionTypeEmoji forces type="emoji",
|
||||
// so we set it explicitly here for symmetry with the unmarshal path.
|
||||
func TestReactionTypeEmoji_RoundTrip(t *testing.T) {
|
||||
in := &ReactionTypeEmoji{
|
||||
Type: ReactionTypeKindEmoji,
|
||||
Emoji: ReactionEmojiThumbsUp,
|
||||
}
|
||||
data, err := json.Marshal(in)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, string(data), `"emoji":"👍"`)
|
||||
|
||||
var out ReactionTypeEmoji
|
||||
require.NoError(t, json.Unmarshal(data, &out))
|
||||
require.Equal(t, ReactionEmojiThumbsUp, out.Emoji)
|
||||
}
|
||||
+420
-33
@@ -4,16 +4,54 @@
|
||||
|
||||
package api
|
||||
|
||||
// ParseMode controls how Telegram interprets formatting in message text.
|
||||
type ParseMode string
|
||||
type BackgroundFillType string
|
||||
|
||||
const (
|
||||
ParseModeMarkdown ParseMode = "Markdown" // legacy
|
||||
ParseModeMarkdownV2 ParseMode = "MarkdownV2"
|
||||
ParseModeHTML ParseMode = "HTML"
|
||||
BackgroundFillTypeSolid BackgroundFillType = "solid"
|
||||
BackgroundFillTypeGradient BackgroundFillType = "gradient"
|
||||
BackgroundFillTypeFreeformGradient BackgroundFillType = "freeform_gradient"
|
||||
)
|
||||
|
||||
type BackgroundTypeKind string
|
||||
|
||||
const (
|
||||
BackgroundTypeKindFill BackgroundTypeKind = "fill"
|
||||
BackgroundTypeKindWallpaper BackgroundTypeKind = "wallpaper"
|
||||
BackgroundTypeKindPattern BackgroundTypeKind = "pattern"
|
||||
BackgroundTypeKindChatTheme BackgroundTypeKind = "chat_theme"
|
||||
)
|
||||
|
||||
type BotCommandScopeType string
|
||||
|
||||
const (
|
||||
BotCommandScopeTypeDefault BotCommandScopeType = "default"
|
||||
BotCommandScopeTypeAllPrivateChats BotCommandScopeType = "all_private_chats"
|
||||
BotCommandScopeTypeAllGroupChats BotCommandScopeType = "all_group_chats"
|
||||
BotCommandScopeTypeAllChatAdministrators BotCommandScopeType = "all_chat_administrators"
|
||||
BotCommandScopeTypeChat BotCommandScopeType = "chat"
|
||||
BotCommandScopeTypeChatAdministrators BotCommandScopeType = "chat_administrators"
|
||||
BotCommandScopeTypeChatMember BotCommandScopeType = "chat_member"
|
||||
)
|
||||
|
||||
type ChatBoostSourceKind string
|
||||
|
||||
const (
|
||||
ChatBoostSourceKindPremium ChatBoostSourceKind = "premium"
|
||||
ChatBoostSourceKindGiftCode ChatBoostSourceKind = "gift_code"
|
||||
ChatBoostSourceKindGiveaway ChatBoostSourceKind = "giveaway"
|
||||
)
|
||||
|
||||
type ChatMemberStatus string
|
||||
|
||||
const (
|
||||
ChatMemberStatusCreator ChatMemberStatus = "creator"
|
||||
ChatMemberStatusAdministrator ChatMemberStatus = "administrator"
|
||||
ChatMemberStatusMember ChatMemberStatus = "member"
|
||||
ChatMemberStatusRestricted ChatMemberStatus = "restricted"
|
||||
ChatMemberStatusLeft ChatMemberStatus = "left"
|
||||
ChatMemberStatusKicked ChatMemberStatus = "kicked"
|
||||
)
|
||||
|
||||
// ChatType is the type of a Telegram chat.
|
||||
type ChatType string
|
||||
|
||||
const (
|
||||
@@ -23,38 +61,387 @@ const (
|
||||
ChatTypeChannel ChatType = "channel"
|
||||
)
|
||||
|
||||
// UpdateType identifies an Update payload variant. Used by allowed_updates
|
||||
// in getUpdates / setWebhook.
|
||||
type UpdateType string
|
||||
type EncryptedPassportElementType string
|
||||
|
||||
const (
|
||||
UpdateMessage UpdateType = "message"
|
||||
UpdateEditedMessage UpdateType = "edited_message"
|
||||
UpdateChannelPost UpdateType = "channel_post"
|
||||
UpdateEditedChannelPost UpdateType = "edited_channel_post"
|
||||
UpdateCallbackQuery UpdateType = "callback_query"
|
||||
UpdateInlineQuery UpdateType = "inline_query"
|
||||
EncryptedPassportElementTypePersonalDetails EncryptedPassportElementType = "personal_details"
|
||||
EncryptedPassportElementTypePassport EncryptedPassportElementType = "passport"
|
||||
EncryptedPassportElementTypeDriverLicense EncryptedPassportElementType = "driver_license"
|
||||
EncryptedPassportElementTypeIdentityCard EncryptedPassportElementType = "identity_card"
|
||||
EncryptedPassportElementTypeInternalPassport EncryptedPassportElementType = "internal_passport"
|
||||
EncryptedPassportElementTypeAddress EncryptedPassportElementType = "address"
|
||||
EncryptedPassportElementTypeUtilityBill EncryptedPassportElementType = "utility_bill"
|
||||
EncryptedPassportElementTypeBankStatement EncryptedPassportElementType = "bank_statement"
|
||||
EncryptedPassportElementTypeRentalAgreement EncryptedPassportElementType = "rental_agreement"
|
||||
EncryptedPassportElementTypePassportRegistration EncryptedPassportElementType = "passport_registration"
|
||||
EncryptedPassportElementTypeTemporaryRegistration EncryptedPassportElementType = "temporary_registration"
|
||||
EncryptedPassportElementTypePhoneNumber EncryptedPassportElementType = "phone_number"
|
||||
EncryptedPassportElementTypeEmail EncryptedPassportElementType = "email"
|
||||
)
|
||||
|
||||
type InlineQueryChatType string
|
||||
|
||||
const (
|
||||
InlineQueryChatTypeSender InlineQueryChatType = "sender"
|
||||
InlineQueryChatTypePrivate InlineQueryChatType = "private"
|
||||
InlineQueryChatTypeGroup InlineQueryChatType = "group"
|
||||
InlineQueryChatTypeSupergroup InlineQueryChatType = "supergroup"
|
||||
InlineQueryChatTypeChannel InlineQueryChatType = "channel"
|
||||
)
|
||||
|
||||
type InlineQueryResultDocumentMimeType string
|
||||
|
||||
const (
|
||||
InlineQueryResultDocumentMimeTypeApplicationOfPdf InlineQueryResultDocumentMimeType = "application/pdf"
|
||||
InlineQueryResultDocumentMimeTypeApplicationOfZip InlineQueryResultDocumentMimeType = "application/zip"
|
||||
)
|
||||
|
||||
type InlineQueryResultGifThumbnailMimeType string
|
||||
|
||||
const (
|
||||
InlineQueryResultGifThumbnailMimeTypeImageOfJpeg InlineQueryResultGifThumbnailMimeType = "image/jpeg"
|
||||
InlineQueryResultGifThumbnailMimeTypeImageOfGif InlineQueryResultGifThumbnailMimeType = "image/gif"
|
||||
InlineQueryResultGifThumbnailMimeTypeVideoOfMp4 InlineQueryResultGifThumbnailMimeType = "video/mp4"
|
||||
)
|
||||
|
||||
type InlineQueryResultType string
|
||||
|
||||
const (
|
||||
InlineQueryResultTypeAudio InlineQueryResultType = "audio"
|
||||
InlineQueryResultTypeDocument InlineQueryResultType = "document"
|
||||
InlineQueryResultTypeGif InlineQueryResultType = "gif"
|
||||
InlineQueryResultTypeMpeg4Gif InlineQueryResultType = "mpeg4_gif"
|
||||
InlineQueryResultTypePhoto InlineQueryResultType = "photo"
|
||||
InlineQueryResultTypeSticker InlineQueryResultType = "sticker"
|
||||
InlineQueryResultTypeVideo InlineQueryResultType = "video"
|
||||
InlineQueryResultTypeVoice InlineQueryResultType = "voice"
|
||||
InlineQueryResultTypeArticle InlineQueryResultType = "article"
|
||||
InlineQueryResultTypeContact InlineQueryResultType = "contact"
|
||||
InlineQueryResultTypeGame InlineQueryResultType = "game"
|
||||
InlineQueryResultTypeLocation InlineQueryResultType = "location"
|
||||
InlineQueryResultTypeVenue InlineQueryResultType = "venue"
|
||||
)
|
||||
|
||||
type InputMediaType string
|
||||
|
||||
const (
|
||||
InputMediaTypeAnimation InputMediaType = "animation"
|
||||
InputMediaTypeAudio InputMediaType = "audio"
|
||||
InputMediaTypeDocument InputMediaType = "document"
|
||||
InputMediaTypeLivePhoto InputMediaType = "live_photo"
|
||||
InputMediaTypePhoto InputMediaType = "photo"
|
||||
InputMediaTypeVideo InputMediaType = "video"
|
||||
)
|
||||
|
||||
type InputPaidMediaType string
|
||||
|
||||
const (
|
||||
InputPaidMediaTypeLivePhoto InputPaidMediaType = "live_photo"
|
||||
InputPaidMediaTypePhoto InputPaidMediaType = "photo"
|
||||
InputPaidMediaTypeVideo InputPaidMediaType = "video"
|
||||
)
|
||||
|
||||
type InputPollMediaType string
|
||||
|
||||
const (
|
||||
InputPollMediaTypeAnimation InputPollMediaType = "animation"
|
||||
InputPollMediaTypeAudio InputPollMediaType = "audio"
|
||||
InputPollMediaTypeDocument InputPollMediaType = "document"
|
||||
InputPollMediaTypeLivePhoto InputPollMediaType = "live_photo"
|
||||
InputPollMediaTypeLocation InputPollMediaType = "location"
|
||||
InputPollMediaTypePhoto InputPollMediaType = "photo"
|
||||
InputPollMediaTypeVenue InputPollMediaType = "venue"
|
||||
InputPollMediaTypeVideo InputPollMediaType = "video"
|
||||
)
|
||||
|
||||
type InputPollOptionMediaType string
|
||||
|
||||
const (
|
||||
InputPollOptionMediaTypeAnimation InputPollOptionMediaType = "animation"
|
||||
InputPollOptionMediaTypeLivePhoto InputPollOptionMediaType = "live_photo"
|
||||
InputPollOptionMediaTypeLocation InputPollOptionMediaType = "location"
|
||||
InputPollOptionMediaTypePhoto InputPollOptionMediaType = "photo"
|
||||
InputPollOptionMediaTypeSticker InputPollOptionMediaType = "sticker"
|
||||
InputPollOptionMediaTypeVenue InputPollOptionMediaType = "venue"
|
||||
InputPollOptionMediaTypeVideo InputPollOptionMediaType = "video"
|
||||
)
|
||||
|
||||
type InputProfilePhotoType string
|
||||
|
||||
const (
|
||||
InputProfilePhotoTypeStatic InputProfilePhotoType = "static"
|
||||
InputProfilePhotoTypeAnimated InputProfilePhotoType = "animated"
|
||||
)
|
||||
|
||||
type InputStickerFormat string
|
||||
|
||||
const (
|
||||
InputStickerFormatStatic InputStickerFormat = "static"
|
||||
InputStickerFormatAnimated InputStickerFormat = "animated"
|
||||
InputStickerFormatVideo InputStickerFormat = "video"
|
||||
)
|
||||
|
||||
type InputStoryContentType string
|
||||
|
||||
const (
|
||||
InputStoryContentTypePhoto InputStoryContentType = "photo"
|
||||
InputStoryContentTypeVideo InputStoryContentType = "video"
|
||||
)
|
||||
|
||||
type KeyboardButtonStyle string
|
||||
|
||||
const (
|
||||
KeyboardButtonStyleDanger KeyboardButtonStyle = "danger"
|
||||
KeyboardButtonStyleSuccess KeyboardButtonStyle = "success"
|
||||
KeyboardButtonStylePrimary KeyboardButtonStyle = "primary"
|
||||
)
|
||||
|
||||
type MaskPositionPoint string
|
||||
|
||||
const (
|
||||
MaskPositionPointForehead MaskPositionPoint = "forehead"
|
||||
MaskPositionPointEyes MaskPositionPoint = "eyes"
|
||||
MaskPositionPointMouth MaskPositionPoint = "mouth"
|
||||
MaskPositionPointChin MaskPositionPoint = "chin"
|
||||
)
|
||||
|
||||
type MenuButtonType string
|
||||
|
||||
const (
|
||||
MenuButtonTypeCommands MenuButtonType = "commands"
|
||||
MenuButtonTypeWebApp MenuButtonType = "web_app"
|
||||
MenuButtonTypeDefault MenuButtonType = "default"
|
||||
)
|
||||
|
||||
// MessageEntityType is the kind of an entity (mention, hashtag, command, ...).
|
||||
type MessageEntityType string
|
||||
|
||||
const (
|
||||
EntityMention MessageEntityType = "mention"
|
||||
EntityHashtag MessageEntityType = "hashtag"
|
||||
EntityCashtag MessageEntityType = "cashtag"
|
||||
EntityBotCommand MessageEntityType = "bot_command"
|
||||
EntityURL MessageEntityType = "url"
|
||||
EntityEmail MessageEntityType = "email"
|
||||
EntityPhoneNumber MessageEntityType = "phone_number"
|
||||
EntityBold MessageEntityType = "bold"
|
||||
EntityItalic MessageEntityType = "italic"
|
||||
EntityUnderline MessageEntityType = "underline"
|
||||
EntityStrike MessageEntityType = "strikethrough"
|
||||
EntitySpoiler MessageEntityType = "spoiler"
|
||||
EntityCode MessageEntityType = "code"
|
||||
EntityPre MessageEntityType = "pre"
|
||||
EntityTextLink MessageEntityType = "text_link"
|
||||
EntityTextMention MessageEntityType = "text_mention"
|
||||
EntityCustomEmoji MessageEntityType = "custom_emoji"
|
||||
MessageEntityTypeMention MessageEntityType = "mention"
|
||||
MessageEntityTypeHashtag MessageEntityType = "hashtag"
|
||||
MessageEntityTypeCashtag MessageEntityType = "cashtag"
|
||||
MessageEntityTypeBotCommand MessageEntityType = "bot_command"
|
||||
MessageEntityTypeURL MessageEntityType = "url"
|
||||
MessageEntityTypeEmail MessageEntityType = "email"
|
||||
MessageEntityTypePhoneNumber MessageEntityType = "phone_number"
|
||||
MessageEntityTypeBold MessageEntityType = "bold"
|
||||
MessageEntityTypeItalic MessageEntityType = "italic"
|
||||
MessageEntityTypeUnderline MessageEntityType = "underline"
|
||||
MessageEntityTypeStrikethrough MessageEntityType = "strikethrough"
|
||||
MessageEntityTypeSpoiler MessageEntityType = "spoiler"
|
||||
MessageEntityTypeBlockquote MessageEntityType = "blockquote"
|
||||
MessageEntityTypeExpandableBlockquote MessageEntityType = "expandable_blockquote"
|
||||
MessageEntityTypeCode MessageEntityType = "code"
|
||||
MessageEntityTypePre MessageEntityType = "pre"
|
||||
MessageEntityTypeTextLink MessageEntityType = "text_link"
|
||||
MessageEntityTypeTextMention MessageEntityType = "text_mention"
|
||||
MessageEntityTypeCustomEmoji MessageEntityType = "custom_emoji"
|
||||
MessageEntityTypeDateTime MessageEntityType = "date_time"
|
||||
)
|
||||
|
||||
type MessageOriginType string
|
||||
|
||||
const (
|
||||
MessageOriginTypeUser MessageOriginType = "user"
|
||||
MessageOriginTypeHiddenUser MessageOriginType = "hidden_user"
|
||||
MessageOriginTypeChat MessageOriginType = "chat"
|
||||
MessageOriginTypeChannel MessageOriginType = "channel"
|
||||
)
|
||||
|
||||
type OwnedGiftType string
|
||||
|
||||
const (
|
||||
OwnedGiftTypeRegular OwnedGiftType = "regular"
|
||||
OwnedGiftTypeUnique OwnedGiftType = "unique"
|
||||
)
|
||||
|
||||
type PaidMediaType string
|
||||
|
||||
const (
|
||||
PaidMediaTypeLivePhoto PaidMediaType = "live_photo"
|
||||
PaidMediaTypePhoto PaidMediaType = "photo"
|
||||
PaidMediaTypePreview PaidMediaType = "preview"
|
||||
PaidMediaTypeVideo PaidMediaType = "video"
|
||||
)
|
||||
|
||||
type ParseMode string
|
||||
|
||||
const (
|
||||
ParseModeMarkdown ParseMode = "Markdown"
|
||||
ParseModeMarkdownV2 ParseMode = "MarkdownV2"
|
||||
ParseModeHTML ParseMode = "HTML"
|
||||
)
|
||||
|
||||
type PassportElementErrorDataFieldType string
|
||||
|
||||
const (
|
||||
PassportElementErrorDataFieldTypePersonalDetails PassportElementErrorDataFieldType = "personal_details"
|
||||
PassportElementErrorDataFieldTypePassport PassportElementErrorDataFieldType = "passport"
|
||||
PassportElementErrorDataFieldTypeDriverLicense PassportElementErrorDataFieldType = "driver_license"
|
||||
PassportElementErrorDataFieldTypeIdentityCard PassportElementErrorDataFieldType = "identity_card"
|
||||
PassportElementErrorDataFieldTypeInternalPassport PassportElementErrorDataFieldType = "internal_passport"
|
||||
PassportElementErrorDataFieldTypeAddress PassportElementErrorDataFieldType = "address"
|
||||
)
|
||||
|
||||
type PassportElementErrorFileType string
|
||||
|
||||
const (
|
||||
PassportElementErrorFileTypeUtilityBill PassportElementErrorFileType = "utility_bill"
|
||||
PassportElementErrorFileTypeBankStatement PassportElementErrorFileType = "bank_statement"
|
||||
PassportElementErrorFileTypeRentalAgreement PassportElementErrorFileType = "rental_agreement"
|
||||
PassportElementErrorFileTypePassportRegistration PassportElementErrorFileType = "passport_registration"
|
||||
PassportElementErrorFileTypeTemporaryRegistration PassportElementErrorFileType = "temporary_registration"
|
||||
)
|
||||
|
||||
type PassportElementErrorReverseSideType string
|
||||
|
||||
const (
|
||||
PassportElementErrorReverseSideTypeDriverLicense PassportElementErrorReverseSideType = "driver_license"
|
||||
PassportElementErrorReverseSideTypeIdentityCard PassportElementErrorReverseSideType = "identity_card"
|
||||
)
|
||||
|
||||
type PassportElementErrorSelfieType string
|
||||
|
||||
const (
|
||||
PassportElementErrorSelfieTypePassport PassportElementErrorSelfieType = "passport"
|
||||
PassportElementErrorSelfieTypeDriverLicense PassportElementErrorSelfieType = "driver_license"
|
||||
PassportElementErrorSelfieTypeIdentityCard PassportElementErrorSelfieType = "identity_card"
|
||||
PassportElementErrorSelfieTypeInternalPassport PassportElementErrorSelfieType = "internal_passport"
|
||||
)
|
||||
|
||||
type PassportElementErrorSource string
|
||||
|
||||
const (
|
||||
PassportElementErrorSourceData PassportElementErrorSource = "data"
|
||||
PassportElementErrorSourceFrontSide PassportElementErrorSource = "front_side"
|
||||
PassportElementErrorSourceReverseSide PassportElementErrorSource = "reverse_side"
|
||||
PassportElementErrorSourceSelfie PassportElementErrorSource = "selfie"
|
||||
PassportElementErrorSourceFile PassportElementErrorSource = "file"
|
||||
PassportElementErrorSourceFiles PassportElementErrorSource = "files"
|
||||
PassportElementErrorSourceTranslationFile PassportElementErrorSource = "translation_file"
|
||||
PassportElementErrorSourceTranslationFiles PassportElementErrorSource = "translation_files"
|
||||
PassportElementErrorSourceUnspecified PassportElementErrorSource = "unspecified"
|
||||
)
|
||||
|
||||
type PassportElementErrorTranslationFileType string
|
||||
|
||||
const (
|
||||
PassportElementErrorTranslationFileTypePassport PassportElementErrorTranslationFileType = "passport"
|
||||
PassportElementErrorTranslationFileTypeDriverLicense PassportElementErrorTranslationFileType = "driver_license"
|
||||
PassportElementErrorTranslationFileTypeIdentityCard PassportElementErrorTranslationFileType = "identity_card"
|
||||
PassportElementErrorTranslationFileTypeInternalPassport PassportElementErrorTranslationFileType = "internal_passport"
|
||||
PassportElementErrorTranslationFileTypeUtilityBill PassportElementErrorTranslationFileType = "utility_bill"
|
||||
PassportElementErrorTranslationFileTypeBankStatement PassportElementErrorTranslationFileType = "bank_statement"
|
||||
PassportElementErrorTranslationFileTypeRentalAgreement PassportElementErrorTranslationFileType = "rental_agreement"
|
||||
PassportElementErrorTranslationFileTypePassportRegistration PassportElementErrorTranslationFileType = "passport_registration"
|
||||
PassportElementErrorTranslationFileTypeTemporaryRegistration PassportElementErrorTranslationFileType = "temporary_registration"
|
||||
)
|
||||
|
||||
type PollType string
|
||||
|
||||
const (
|
||||
PollTypeRegular PollType = "regular"
|
||||
PollTypeQuiz PollType = "quiz"
|
||||
)
|
||||
|
||||
type ReactionTypeKind string
|
||||
|
||||
const (
|
||||
ReactionTypeKindEmoji ReactionTypeKind = "emoji"
|
||||
ReactionTypeKindCustomEmoji ReactionTypeKind = "custom_emoji"
|
||||
ReactionTypeKindPaid ReactionTypeKind = "paid"
|
||||
)
|
||||
|
||||
type RefundedPaymentCurrency string
|
||||
|
||||
const (
|
||||
RefundedPaymentCurrencyXTR RefundedPaymentCurrency = "XTR"
|
||||
)
|
||||
|
||||
type RevenueWithdrawalStateKind string
|
||||
|
||||
const (
|
||||
RevenueWithdrawalStateKindPending RevenueWithdrawalStateKind = "pending"
|
||||
RevenueWithdrawalStateKindSucceeded RevenueWithdrawalStateKind = "succeeded"
|
||||
RevenueWithdrawalStateKindFailed RevenueWithdrawalStateKind = "failed"
|
||||
)
|
||||
|
||||
type StickerType string
|
||||
|
||||
const (
|
||||
StickerTypeRegular StickerType = "regular"
|
||||
StickerTypeMask StickerType = "mask"
|
||||
StickerTypeCustomEmoji StickerType = "custom_emoji"
|
||||
)
|
||||
|
||||
type StoryAreaTypeKind string
|
||||
|
||||
const (
|
||||
StoryAreaTypeKindLocation StoryAreaTypeKind = "location"
|
||||
StoryAreaTypeKindSuggestedReaction StoryAreaTypeKind = "suggested_reaction"
|
||||
StoryAreaTypeKindLink StoryAreaTypeKind = "link"
|
||||
StoryAreaTypeKindWeather StoryAreaTypeKind = "weather"
|
||||
StoryAreaTypeKindUniqueGift StoryAreaTypeKind = "unique_gift"
|
||||
)
|
||||
|
||||
type SuggestedPostInfoState string
|
||||
|
||||
const (
|
||||
SuggestedPostInfoStatePending SuggestedPostInfoState = "pending"
|
||||
SuggestedPostInfoStateApproved SuggestedPostInfoState = "approved"
|
||||
SuggestedPostInfoStateDeclined SuggestedPostInfoState = "declined"
|
||||
)
|
||||
|
||||
type SuggestedPostPaidCurrency string
|
||||
|
||||
const (
|
||||
SuggestedPostPaidCurrencyXTR SuggestedPostPaidCurrency = "XTR"
|
||||
SuggestedPostPaidCurrencyTON SuggestedPostPaidCurrency = "TON"
|
||||
)
|
||||
|
||||
type SuggestedPostRefundedReason string
|
||||
|
||||
const (
|
||||
SuggestedPostRefundedReasonPostDeleted SuggestedPostRefundedReason = "post_deleted"
|
||||
SuggestedPostRefundedReasonPaymentRefunded SuggestedPostRefundedReason = "payment_refunded"
|
||||
)
|
||||
|
||||
type TransactionPartnerType string
|
||||
|
||||
const (
|
||||
TransactionPartnerTypeUser TransactionPartnerType = "user"
|
||||
TransactionPartnerTypeChat TransactionPartnerType = "chat"
|
||||
TransactionPartnerTypeAffiliateProgram TransactionPartnerType = "affiliate_program"
|
||||
TransactionPartnerTypeFragment TransactionPartnerType = "fragment"
|
||||
TransactionPartnerTypeTelegramAds TransactionPartnerType = "telegram_ads"
|
||||
TransactionPartnerTypeTelegramApi TransactionPartnerType = "telegram_api"
|
||||
TransactionPartnerTypeOther TransactionPartnerType = "other"
|
||||
)
|
||||
|
||||
type TransactionPartnerUserTransactionType string
|
||||
|
||||
const (
|
||||
TransactionPartnerUserTransactionTypeInvoicePayment TransactionPartnerUserTransactionType = "invoice_payment"
|
||||
TransactionPartnerUserTransactionTypePaidMediaPayment TransactionPartnerUserTransactionType = "paid_media_payment"
|
||||
TransactionPartnerUserTransactionTypeGiftPurchase TransactionPartnerUserTransactionType = "gift_purchase"
|
||||
TransactionPartnerUserTransactionTypePremiumPurchase TransactionPartnerUserTransactionType = "premium_purchase"
|
||||
TransactionPartnerUserTransactionTypeBusinessAccountTransfer TransactionPartnerUserTransactionType = "business_account_transfer"
|
||||
)
|
||||
|
||||
type UniqueGiftInfoOrigin string
|
||||
|
||||
const (
|
||||
UniqueGiftInfoOriginUpgrade UniqueGiftInfoOrigin = "upgrade"
|
||||
UniqueGiftInfoOriginTransfer UniqueGiftInfoOrigin = "transfer"
|
||||
UniqueGiftInfoOriginResale UniqueGiftInfoOrigin = "resale"
|
||||
UniqueGiftInfoOriginGiftedUpgrade UniqueGiftInfoOrigin = "gifted_upgrade"
|
||||
UniqueGiftInfoOriginOffer UniqueGiftInfoOrigin = "offer"
|
||||
)
|
||||
|
||||
type UniqueGiftModelRarity string
|
||||
|
||||
const (
|
||||
UniqueGiftModelRarityUncommon UniqueGiftModelRarity = "uncommon"
|
||||
UniqueGiftModelRarityRare UniqueGiftModelRarity = "rare"
|
||||
UniqueGiftModelRarityEpic UniqueGiftModelRarity = "epic"
|
||||
UniqueGiftModelRarityLegendary UniqueGiftModelRarity = "legendary"
|
||||
)
|
||||
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
package api
|
||||
|
||||
// UpdateType identifies an Update payload variant. Used by allowed_updates
|
||||
// in getUpdates / setWebhook. The Telegram docs do not enumerate these
|
||||
// values inline (they are derived from the optional fields of Update),
|
||||
// so the codegen pipeline cannot synthesise this enum and it lives here
|
||||
// as a hand-curated companion to the generated enums.gen.go.
|
||||
type UpdateType string
|
||||
|
||||
const (
|
||||
UpdateMessage UpdateType = "message"
|
||||
UpdateEditedMessage UpdateType = "edited_message"
|
||||
UpdateChannelPost UpdateType = "channel_post"
|
||||
UpdateEditedChannelPost UpdateType = "edited_channel_post"
|
||||
UpdateBusinessConnection UpdateType = "business_connection"
|
||||
UpdateBusinessMessage UpdateType = "business_message"
|
||||
UpdateEditedBusinessMessage UpdateType = "edited_business_message"
|
||||
UpdateDeletedBusinessMessages UpdateType = "deleted_business_messages"
|
||||
UpdateMessageReaction UpdateType = "message_reaction"
|
||||
UpdateMessageReactionCount UpdateType = "message_reaction_count"
|
||||
UpdateInlineQuery UpdateType = "inline_query"
|
||||
UpdateChosenInlineResult UpdateType = "chosen_inline_result"
|
||||
UpdateCallbackQuery UpdateType = "callback_query"
|
||||
UpdateShippingQuery UpdateType = "shipping_query"
|
||||
UpdatePreCheckoutQuery UpdateType = "pre_checkout_query"
|
||||
UpdatePurchasedPaidMedia UpdateType = "purchased_paid_media"
|
||||
UpdatePoll UpdateType = "poll"
|
||||
UpdatePollAnswer UpdateType = "poll_answer"
|
||||
UpdateMyChatMember UpdateType = "my_chat_member"
|
||||
UpdateChatMember UpdateType = "chat_member"
|
||||
UpdateChatJoinRequest UpdateType = "chat_join_request"
|
||||
UpdateChatBoost UpdateType = "chat_boost"
|
||||
UpdateRemovedChatBoost UpdateType = "removed_chat_boost"
|
||||
)
|
||||
|
||||
// DiceEmoji is the set of emoji values accepted by sendDice. Telegram's
|
||||
// canonical list is "🎲", "🎯", "🏀", "⚽", "🎳", "🎰". The codegen
|
||||
// scraper drops these values during regex extraction (multi-byte
|
||||
// boundary issues with curly-quoted emoji), so this enum is hand-
|
||||
// curated and wired into SendDiceParams.Emoji via the per-field type
|
||||
// override in cmd/genapi/emitter.go.
|
||||
type DiceEmoji string
|
||||
|
||||
const (
|
||||
DiceEmojiDice DiceEmoji = "🎲"
|
||||
DiceEmojiDart DiceEmoji = "🎯"
|
||||
DiceEmojiBasketball DiceEmoji = "🏀"
|
||||
DiceEmojiFootball DiceEmoji = "⚽"
|
||||
DiceEmojiBowling DiceEmoji = "🎳"
|
||||
DiceEmojiSlotMachine DiceEmoji = "🎰"
|
||||
)
|
||||
|
||||
// ReactionEmoji is the set of emoji Telegram allows in a
|
||||
// ReactionTypeEmoji.Emoji value. Hand-curated from
|
||||
// https://core.telegram.org/bots/api#reactiontypeemoji because the
|
||||
// scraper's curly-quote regex strips the emoji literals (byte-boundary
|
||||
// issue on multi-byte sequences). Names mirror the Unicode CLDR short
|
||||
// name where one exists; otherwise a stable common-English label.
|
||||
// Telegram occasionally extends this set — passers of unrecognised
|
||||
// strings still type-check (ReactionEmoji is a string alias) so this
|
||||
// list need not block runtime use of newer values.
|
||||
type ReactionEmoji string
|
||||
|
||||
const (
|
||||
ReactionEmojiHeart ReactionEmoji = "❤"
|
||||
ReactionEmojiThumbsUp ReactionEmoji = "👍"
|
||||
ReactionEmojiThumbsDown ReactionEmoji = "👎"
|
||||
ReactionEmojiFire ReactionEmoji = "🔥"
|
||||
ReactionEmojiSmilingFaceWithHearts ReactionEmoji = "🥰"
|
||||
ReactionEmojiClappingHands ReactionEmoji = "👏"
|
||||
ReactionEmojiBeamingFace ReactionEmoji = "😁"
|
||||
ReactionEmojiThinkingFace ReactionEmoji = "🤔"
|
||||
ReactionEmojiExplodingHead ReactionEmoji = "🤯"
|
||||
ReactionEmojiScreamingFace ReactionEmoji = "😱"
|
||||
ReactionEmojiCursingFace ReactionEmoji = "🤬"
|
||||
ReactionEmojiCryingFace ReactionEmoji = "😢"
|
||||
ReactionEmojiPartyPopper ReactionEmoji = "🎉"
|
||||
ReactionEmojiStarStruck ReactionEmoji = "🤩"
|
||||
ReactionEmojiVomiting ReactionEmoji = "🤮"
|
||||
ReactionEmojiPileOfPoo ReactionEmoji = "💩"
|
||||
ReactionEmojiFoldedHands ReactionEmoji = "🙏"
|
||||
ReactionEmojiOKHand ReactionEmoji = "👌"
|
||||
ReactionEmojiDove ReactionEmoji = "🕊"
|
||||
ReactionEmojiClown ReactionEmoji = "🤡"
|
||||
ReactionEmojiYawning ReactionEmoji = "🥱"
|
||||
ReactionEmojiWoozyFace ReactionEmoji = "🥴"
|
||||
ReactionEmojiHeartEyes ReactionEmoji = "😍"
|
||||
ReactionEmojiWhale ReactionEmoji = "🐳"
|
||||
ReactionEmojiHeartOnFire ReactionEmoji = "❤🔥"
|
||||
ReactionEmojiNewMoonFace ReactionEmoji = "🌚"
|
||||
ReactionEmojiHotDog ReactionEmoji = "🌭"
|
||||
ReactionEmojiHundredPoints ReactionEmoji = "💯"
|
||||
ReactionEmojiRollingOnFloor ReactionEmoji = "🤣"
|
||||
ReactionEmojiLightning ReactionEmoji = "⚡"
|
||||
ReactionEmojiBanana ReactionEmoji = "🍌"
|
||||
ReactionEmojiTrophy ReactionEmoji = "🏆"
|
||||
ReactionEmojiBrokenHeart ReactionEmoji = "💔"
|
||||
ReactionEmojiRaisedEyebrow ReactionEmoji = "🤨"
|
||||
ReactionEmojiNeutralFace ReactionEmoji = "😐"
|
||||
ReactionEmojiStrawberry ReactionEmoji = "🍓"
|
||||
ReactionEmojiChampagne ReactionEmoji = "🍾"
|
||||
ReactionEmojiKissMark ReactionEmoji = "💋"
|
||||
ReactionEmojiMiddleFinger ReactionEmoji = "🖕"
|
||||
ReactionEmojiDevil ReactionEmoji = "😈"
|
||||
ReactionEmojiSleeping ReactionEmoji = "😴"
|
||||
ReactionEmojiLoudlyCrying ReactionEmoji = "😭"
|
||||
ReactionEmojiNerd ReactionEmoji = "🤓"
|
||||
ReactionEmojiGhost ReactionEmoji = "👻"
|
||||
ReactionEmojiManTechnologist ReactionEmoji = "👨💻"
|
||||
ReactionEmojiEyes ReactionEmoji = "👀"
|
||||
ReactionEmojiJackOLantern ReactionEmoji = "🎃"
|
||||
ReactionEmojiSeeNoEvil ReactionEmoji = "🙈"
|
||||
ReactionEmojiHalo ReactionEmoji = "😇"
|
||||
ReactionEmojiFearful ReactionEmoji = "😨"
|
||||
ReactionEmojiHandshake ReactionEmoji = "🤝"
|
||||
ReactionEmojiWriting ReactionEmoji = "✍"
|
||||
ReactionEmojiHugging ReactionEmoji = "🤗"
|
||||
ReactionEmojiSaluting ReactionEmoji = "🫡"
|
||||
ReactionEmojiSantaClaus ReactionEmoji = "🎅"
|
||||
ReactionEmojiChristmasTree ReactionEmoji = "🎄"
|
||||
ReactionEmojiSnowman ReactionEmoji = "☃"
|
||||
ReactionEmojiNailPolish ReactionEmoji = "💅"
|
||||
ReactionEmojiZanyFace ReactionEmoji = "🤪"
|
||||
ReactionEmojiMoai ReactionEmoji = "🗿"
|
||||
ReactionEmojiCool ReactionEmoji = "🆒"
|
||||
ReactionEmojiHeartWithArrow ReactionEmoji = "💘"
|
||||
ReactionEmojiHearNoEvil ReactionEmoji = "🙉"
|
||||
ReactionEmojiUnicorn ReactionEmoji = "🦄"
|
||||
ReactionEmojiKissingFace ReactionEmoji = "😘"
|
||||
ReactionEmojiPill ReactionEmoji = "💊"
|
||||
ReactionEmojiSpeakNoEvil ReactionEmoji = "🙊"
|
||||
ReactionEmojiSmilingFaceWithSunglasses ReactionEmoji = "😎"
|
||||
ReactionEmojiAlienMonster ReactionEmoji = "👾"
|
||||
ReactionEmojiManShrugging ReactionEmoji = "🤷♂"
|
||||
ReactionEmojiPersonShrugging ReactionEmoji = "🤷"
|
||||
ReactionEmojiWomanShrugging ReactionEmoji = "🤷♀"
|
||||
ReactionEmojiPoutingFace ReactionEmoji = "😡"
|
||||
)
|
||||
@@ -0,0 +1,55 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/lukaszraczylo/go-telegram/client"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestGetChatAdministrators_DecodesUnionSlice is a regression test for the
|
||||
// bug where GetChatAdministrators was emitted with the generic client.Call
|
||||
// against []ChatMember — encoding/json cannot unmarshal a slice of an
|
||||
// interface, so the call always failed at the parse step.
|
||||
//
|
||||
// The fix makes the codegen emit CallRaw + per-element UnmarshalChatMember
|
||||
// for any method returning []<sealed-interface union>.
|
||||
func TestGetChatAdministrators_DecodesUnionSlice(t *testing.T) {
|
||||
body := `{"ok":true,"result":[
|
||||
{"status":"creator","user":{"id":1,"is_bot":false,"first_name":"Owner"},"is_anonymous":false},
|
||||
{"status":"administrator","user":{"id":2,"is_bot":false,"first_name":"Admin"},"can_be_edited":false,"is_anonymous":false,"can_manage_chat":true,"can_delete_messages":true,"can_manage_video_chats":false,"can_restrict_members":true,"can_promote_members":false,"can_change_info":true,"can_invite_users":true,"can_post_stories":false,"can_edit_stories":false,"can_delete_stories":false}
|
||||
]}`
|
||||
|
||||
m := &mockDoer{}
|
||||
m.On("Do", mock.Anything).Return(newJSONResp(200, body), nil)
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
|
||||
admins, err := GetChatAdministrators(context.Background(), bot,
|
||||
&GetChatAdministratorsParams{ChatID: ChatIDFromInt(-100123)})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, admins, 2)
|
||||
|
||||
owner, ok := admins[0].(*ChatMemberOwner)
|
||||
require.True(t, ok, "first element must dispatch to ChatMemberOwner, got %T", admins[0])
|
||||
require.Equal(t, int64(1), owner.User.ID)
|
||||
|
||||
admin, ok := admins[1].(*ChatMemberAdministrator)
|
||||
require.True(t, ok, "second element must dispatch to ChatMemberAdministrator, got %T", admins[1])
|
||||
require.True(t, admin.CanManageChat)
|
||||
require.False(t, admin.CanPromoteMembers)
|
||||
}
|
||||
|
||||
// TestGetChatAdministrators_EmptyArray covers the zero-admin edge case
|
||||
// (a basic group with no admins, or the bot itself filtered out).
|
||||
func TestGetChatAdministrators_EmptyArray(t *testing.T) {
|
||||
m := &mockDoer{}
|
||||
m.On("Do", mock.Anything).Return(newJSONResp(200, `{"ok":true,"result":[]}`), nil)
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
|
||||
admins, err := GetChatAdministrators(context.Background(), bot,
|
||||
&GetChatAdministratorsParams{ChatID: ChatIDFromInt(-100123)})
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, admins)
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
json "github.com/goccy/go-json"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestMarshalJSON_TypeDiscriminator_AutoInjected verifies the generated
|
||||
// MarshalJSON hardcodes the wire discriminator for a Type-keyed variant
|
||||
// even when the caller leaves the field zero.
|
||||
func TestMarshalJSON_TypeDiscriminator_AutoInjected(t *testing.T) {
|
||||
scope := &BotCommandScopeAllPrivateChats{}
|
||||
got, err := json.Marshal(scope)
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, `{"type":"all_private_chats"}`, string(got))
|
||||
}
|
||||
|
||||
// TestMarshalJSON_SourceDiscriminator_AutoInjected verifies the same
|
||||
// for variants that use a non-Type discriminator field. PassportElement
|
||||
// errors key on "source" instead.
|
||||
func TestMarshalJSON_SourceDiscriminator_AutoInjected(t *testing.T) {
|
||||
err := &PassportElementErrorDataField{
|
||||
Type: PassportElementErrorDataFieldTypePersonalDetails,
|
||||
FieldName: "first_name",
|
||||
DataHash: "abc123",
|
||||
Message: "bad data",
|
||||
}
|
||||
got, mErr := json.Marshal(err)
|
||||
require.NoError(t, mErr)
|
||||
require.JSONEq(t,
|
||||
`{"source":"data","type":"personal_details","field_name":"first_name","data_hash":"abc123","message":"bad data"}`,
|
||||
string(got),
|
||||
)
|
||||
}
|
||||
|
||||
// TestMarshalJSON_UserSuppliedDiscriminator_Overridden documents the
|
||||
// safety guarantee: a typo or stale value the caller pastes into the
|
||||
// struct literal is silently overridden by the generated MarshalJSON.
|
||||
// This is what saves callers from Telegram's "silent reject" failure
|
||||
// mode when a discriminator is wrong.
|
||||
func TestMarshalJSON_UserSuppliedDiscriminator_Overridden(t *testing.T) {
|
||||
scope := &BotCommandScopeAllPrivateChats{Type: "wrong"}
|
||||
got, err := json.Marshal(scope)
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, `{"type":"all_private_chats"}`, string(got))
|
||||
}
|
||||
|
||||
// TestMarshalJSON_RoundTrip confirms a marshal-then-unmarshal cycle
|
||||
// preserves user-supplied fields. Discriminator field is set on the
|
||||
// way out, read back on the way in — no data loss.
|
||||
//
|
||||
// Uses ChatMember (one of the auto-decode unions) so the round-trip
|
||||
// can route through the generated UnmarshalChatMember dispatcher.
|
||||
func TestMarshalJSON_RoundTrip(t *testing.T) {
|
||||
orig := &ChatMemberLeft{
|
||||
User: User{ID: 42, IsBot: false, FirstName: "alice"},
|
||||
}
|
||||
raw, err := json.Marshal(orig)
|
||||
require.NoError(t, err)
|
||||
|
||||
out, err := UnmarshalChatMember(raw)
|
||||
require.NoError(t, err)
|
||||
|
||||
round, ok := out.(*ChatMemberLeft)
|
||||
require.True(t, ok, "expected *ChatMemberLeft, got %T", out)
|
||||
require.Equal(t, ChatMemberStatusLeft, round.Status)
|
||||
require.Equal(t, orig.User.ID, round.User.ID)
|
||||
require.Equal(t, orig.User.FirstName, round.User.FirstName)
|
||||
}
|
||||
|
||||
// TestMarshalJSON_InputMessageContent_NoDiscriminator confirms that
|
||||
// variants of InputMessageContent (the structurally-dispatched union
|
||||
// Telegram identifies by field presence, not by a "type" field) do
|
||||
// NOT get an injected discriminator. Their fields ride out as-is.
|
||||
func TestMarshalJSON_InputMessageContent_NoDiscriminator(t *testing.T) {
|
||||
content := &InputTextMessageContent{
|
||||
MessageText: "hello world",
|
||||
}
|
||||
got, err := json.Marshal(content)
|
||||
require.NoError(t, err)
|
||||
// No "type" field should appear; just message_text.
|
||||
require.JSONEq(t, `{"message_text":"hello world"}`, string(got))
|
||||
}
|
||||
|
||||
// TestMarshalJSON_NonDiscriminatorMembers_RidealongUnchanged verifies
|
||||
// the alias-embedding pattern: every non-discriminator field on the
|
||||
// variant marshals through the *alias and keeps its own json tag and
|
||||
// omitempty behaviour. Caption + ParseMode here exercise both
|
||||
// required-string-with-discriminator and optional-with-omitempty.
|
||||
func TestMarshalJSON_NonDiscriminatorMembers_RidealongUnchanged(t *testing.T) {
|
||||
media := &InputMediaPhoto{
|
||||
Media: "https://example.com/photo.jpg",
|
||||
Caption: "look",
|
||||
}
|
||||
got, err := json.Marshal(media)
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t,
|
||||
`{"type":"photo","media":"https://example.com/photo.jpg","caption":"look"}`,
|
||||
string(got),
|
||||
)
|
||||
}
|
||||
+134
-118
@@ -27,7 +27,7 @@ type GetUpdatesParams struct {
|
||||
// Timeout in seconds for long polling. Defaults to 0, i.e. usual short polling. Should be positive, short polling should be used for testing purposes only.
|
||||
Timeout *int64 `json:"timeout,omitempty"`
|
||||
// A JSON-serialized list of the update types you want your bot to receive. For example, specify ["message", "edited_channel_post", "callback_query"] to only receive updates of these types. See Update for a complete list of available update types. Specify an empty list to receive all update types except chat_member, message_reaction, and message_reaction_count (default). If not specified, the previous setting will be used.Please note that this parameter doesn't affect updates created before the call to getUpdates, so unwanted updates may be received for a short period of time.
|
||||
AllowedUpdates []string `json:"allowed_updates,omitempty"`
|
||||
AllowedUpdates []UpdateType `json:"allowed_updates,omitempty"`
|
||||
}
|
||||
|
||||
// GetUpdates calls the getUpdates Telegram Bot API method.
|
||||
@@ -45,7 +45,7 @@ func GetUpdates(ctx context.Context, b *client.Bot, p *GetUpdatesParams) ([]Upda
|
||||
// Notes1. You will not be able to receive updates using getUpdates for as long as an outgoing webhook is set up.2. To use a self-signed certificate, you need to upload your public key certificate using certificate parameter. Please upload as InputFile, sending a String will not work.3. Ports currently supported for webhooks: 443, 80, 88, 8443.
|
||||
// If you're having any trouble setting up webhooks, please check out this amazing guide to webhooks.
|
||||
type SetWebhookParams struct {
|
||||
// HTTPS URL to send updates to. Use an empty string to remove webhook integration
|
||||
// HTTPS URL to send updates to. Use an empty string to remove webhook integration.
|
||||
URL string `json:"url"`
|
||||
// Upload your public key certificate so that the root certificate in use can be checked. See our self-signed guide for details.
|
||||
Certificate *InputFile `json:"certificate,omitempty"`
|
||||
@@ -54,7 +54,7 @@ type SetWebhookParams struct {
|
||||
// The maximum allowed number of simultaneous HTTPS connections to the webhook for update delivery, 1-100. Defaults to 40. Use lower values to limit the load on your bot's server, and higher values to increase your bot's throughput.
|
||||
MaxConnections *int64 `json:"max_connections,omitempty"`
|
||||
// A JSON-serialized list of the update types you want your bot to receive. For example, specify ["message", "edited_channel_post", "callback_query"] to only receive updates of these types. See Update for a complete list of available update types. Specify an empty list to receive all update types except chat_member, message_reaction, and message_reaction_count (default). If not specified, the previous setting will be used.Please note that this parameter doesn't affect updates created before the call to the setWebhook, so unwanted updates may be received for a short period of time.
|
||||
AllowedUpdates []string `json:"allowed_updates,omitempty"`
|
||||
AllowedUpdates []UpdateType `json:"allowed_updates,omitempty"`
|
||||
// Pass True to drop all pending updates
|
||||
DropPendingUpdates *bool `json:"drop_pending_updates,omitempty"`
|
||||
// A secret token to be sent in a header “X-Telegram-Bot-Api-Secret-Token” in every webhook request, 1-256 characters. Only characters A-Z, a-z, 0-9, _ and - are allowed. The header is useful to ensure that the request comes from a webhook set by you.
|
||||
@@ -198,7 +198,7 @@ type SendMessageParams struct {
|
||||
// Text of the message to be sent, 1-4096 characters after entities parsing
|
||||
Text string `json:"text"`
|
||||
// Mode for parsing entities in the message text. See formatting options for more details.
|
||||
ParseMode string `json:"parse_mode,omitempty"`
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in message text, which can be specified instead of parse_mode
|
||||
Entities []MessageEntity `json:"entities,omitempty"`
|
||||
// Link preview generation options for the message
|
||||
@@ -215,7 +215,7 @@ type SendMessageParams struct {
|
||||
SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
|
||||
// Description of the message to reply to
|
||||
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user.
|
||||
ReplyMarkup any `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -302,10 +302,10 @@ type CopyMessageParams struct {
|
||||
MessageID int64 `json:"message_id"`
|
||||
// New start timestamp for the copied video in the message
|
||||
VideoStartTimestamp *int64 `json:"video_start_timestamp,omitempty"`
|
||||
// New caption for media, 0-1024 characters after entities parsing. If not specified, the original caption is kept
|
||||
// New caption for media, 0-1024 characters after entities parsing. If not specified, the original caption is kept.
|
||||
Caption string `json:"caption,omitempty"`
|
||||
// Mode for parsing entities in the new caption. See formatting options for more details.
|
||||
ParseMode string `json:"parse_mode,omitempty"`
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the new caption, which can be specified instead of parse_mode
|
||||
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
|
||||
// Pass True, if the caption must be shown above the message media. Ignored if a new caption isn't specified.
|
||||
@@ -322,7 +322,7 @@ type CopyMessageParams struct {
|
||||
SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
|
||||
// Description of the message to reply to
|
||||
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user.
|
||||
ReplyMarkup any `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -379,7 +379,7 @@ type SendPhotoParams struct {
|
||||
// Photo caption (may also be used when resending photos by file_id), 0-1024 characters after entities parsing
|
||||
Caption string `json:"caption,omitempty"`
|
||||
// Mode for parsing entities in the photo caption. See formatting options for more details.
|
||||
ParseMode string `json:"parse_mode,omitempty"`
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the caption, which can be specified instead of parse_mode
|
||||
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
|
||||
// Pass True, if the caption must be shown above the message media
|
||||
@@ -398,7 +398,7 @@ type SendPhotoParams struct {
|
||||
SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
|
||||
// Description of the message to reply to
|
||||
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user.
|
||||
ReplyMarkup any `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -427,7 +427,7 @@ func (p *SendPhotoParams) MultipartFields() map[string]string {
|
||||
out["caption"] = p.Caption
|
||||
}
|
||||
if p.ParseMode != "" {
|
||||
out["parse_mode"] = p.ParseMode
|
||||
out["parse_mode"] = string(p.ParseMode)
|
||||
}
|
||||
if p.CaptionEntities != nil {
|
||||
if b, _ := json.Marshal(p.CaptionEntities); len(b) > 0 {
|
||||
@@ -509,7 +509,7 @@ type SendLivePhotoParams struct {
|
||||
// Video caption (may also be used when resending videos by file_id), 0-1024 characters after entities parsing
|
||||
Caption string `json:"caption,omitempty"`
|
||||
// Mode for parsing entities in the video caption. See formatting options for more details.
|
||||
ParseMode string `json:"parse_mode,omitempty"`
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the caption, which can be specified instead of parse_mode
|
||||
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
|
||||
// Pass True, if the caption must be shown above the message media
|
||||
@@ -560,7 +560,7 @@ func (p *SendLivePhotoParams) MultipartFields() map[string]string {
|
||||
out["caption"] = p.Caption
|
||||
}
|
||||
if p.ParseMode != "" {
|
||||
out["parse_mode"] = p.ParseMode
|
||||
out["parse_mode"] = string(p.ParseMode)
|
||||
}
|
||||
if p.CaptionEntities != nil {
|
||||
if b, _ := json.Marshal(p.CaptionEntities); len(b) > 0 {
|
||||
@@ -648,7 +648,7 @@ type SendAudioParams struct {
|
||||
// Audio caption, 0-1024 characters after entities parsing
|
||||
Caption string `json:"caption,omitempty"`
|
||||
// Mode for parsing entities in the audio caption. See formatting options for more details.
|
||||
ParseMode string `json:"parse_mode,omitempty"`
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the caption, which can be specified instead of parse_mode
|
||||
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
|
||||
// Duration of the audio in seconds
|
||||
@@ -671,7 +671,7 @@ type SendAudioParams struct {
|
||||
SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
|
||||
// Description of the message to reply to
|
||||
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user.
|
||||
ReplyMarkup any `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -703,7 +703,7 @@ func (p *SendAudioParams) MultipartFields() map[string]string {
|
||||
out["caption"] = p.Caption
|
||||
}
|
||||
if p.ParseMode != "" {
|
||||
out["parse_mode"] = p.ParseMode
|
||||
out["parse_mode"] = string(p.ParseMode)
|
||||
}
|
||||
if p.CaptionEntities != nil {
|
||||
if b, _ := json.Marshal(p.CaptionEntities); len(b) > 0 {
|
||||
@@ -796,7 +796,7 @@ type SendDocumentParams struct {
|
||||
// Document caption (may also be used when resending documents by file_id), 0-1024 characters after entities parsing
|
||||
Caption string `json:"caption,omitempty"`
|
||||
// Mode for parsing entities in the document caption. See formatting options for more details.
|
||||
ParseMode string `json:"parse_mode,omitempty"`
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the caption, which can be specified instead of parse_mode
|
||||
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
|
||||
// Disables automatic server-side content type detection for files uploaded using multipart/form-data
|
||||
@@ -813,7 +813,7 @@ type SendDocumentParams struct {
|
||||
SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
|
||||
// Description of the message to reply to
|
||||
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user.
|
||||
ReplyMarkup any `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -845,7 +845,7 @@ func (p *SendDocumentParams) MultipartFields() map[string]string {
|
||||
out["caption"] = p.Caption
|
||||
}
|
||||
if p.ParseMode != "" {
|
||||
out["parse_mode"] = p.ParseMode
|
||||
out["parse_mode"] = string(p.ParseMode)
|
||||
}
|
||||
if p.CaptionEntities != nil {
|
||||
if b, _ := json.Marshal(p.CaptionEntities); len(b) > 0 {
|
||||
@@ -941,7 +941,7 @@ type SendVideoParams struct {
|
||||
// Video caption (may also be used when resending videos by file_id), 0-1024 characters after entities parsing
|
||||
Caption string `json:"caption,omitempty"`
|
||||
// Mode for parsing entities in the video caption. See formatting options for more details.
|
||||
ParseMode string `json:"parse_mode,omitempty"`
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the caption, which can be specified instead of parse_mode
|
||||
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
|
||||
// Pass True, if the caption must be shown above the message media
|
||||
@@ -962,7 +962,7 @@ type SendVideoParams struct {
|
||||
SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
|
||||
// Description of the message to reply to
|
||||
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user.
|
||||
ReplyMarkup any `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -1009,7 +1009,7 @@ func (p *SendVideoParams) MultipartFields() map[string]string {
|
||||
out["caption"] = p.Caption
|
||||
}
|
||||
if p.ParseMode != "" {
|
||||
out["parse_mode"] = p.ParseMode
|
||||
out["parse_mode"] = string(p.ParseMode)
|
||||
}
|
||||
if p.CaptionEntities != nil {
|
||||
if b, _ := json.Marshal(p.CaptionEntities); len(b) > 0 {
|
||||
@@ -1114,7 +1114,7 @@ type SendAnimationParams struct {
|
||||
// Animation caption (may also be used when resending animation by file_id), 0-1024 characters after entities parsing
|
||||
Caption string `json:"caption,omitempty"`
|
||||
// Mode for parsing entities in the animation caption. See formatting options for more details.
|
||||
ParseMode string `json:"parse_mode,omitempty"`
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the caption, which can be specified instead of parse_mode
|
||||
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
|
||||
// Pass True, if the caption must be shown above the message media
|
||||
@@ -1133,7 +1133,7 @@ type SendAnimationParams struct {
|
||||
SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
|
||||
// Description of the message to reply to
|
||||
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user.
|
||||
ReplyMarkup any `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -1174,7 +1174,7 @@ func (p *SendAnimationParams) MultipartFields() map[string]string {
|
||||
out["caption"] = p.Caption
|
||||
}
|
||||
if p.ParseMode != "" {
|
||||
out["parse_mode"] = p.ParseMode
|
||||
out["parse_mode"] = string(p.ParseMode)
|
||||
}
|
||||
if p.CaptionEntities != nil {
|
||||
if b, _ := json.Marshal(p.CaptionEntities); len(b) > 0 {
|
||||
@@ -1261,7 +1261,7 @@ type SendVoiceParams struct {
|
||||
// Voice message caption, 0-1024 characters after entities parsing
|
||||
Caption string `json:"caption,omitempty"`
|
||||
// Mode for parsing entities in the voice message caption. See formatting options for more details.
|
||||
ParseMode string `json:"parse_mode,omitempty"`
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the caption, which can be specified instead of parse_mode
|
||||
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
|
||||
// Duration of the voice message in seconds
|
||||
@@ -1278,7 +1278,7 @@ type SendVoiceParams struct {
|
||||
SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
|
||||
// Description of the message to reply to
|
||||
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user.
|
||||
ReplyMarkup any `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -1307,7 +1307,7 @@ func (p *SendVoiceParams) MultipartFields() map[string]string {
|
||||
out["caption"] = p.Caption
|
||||
}
|
||||
if p.ParseMode != "" {
|
||||
out["parse_mode"] = p.ParseMode
|
||||
out["parse_mode"] = string(p.ParseMode)
|
||||
}
|
||||
if p.CaptionEntities != nil {
|
||||
if b, _ := json.Marshal(p.CaptionEntities); len(b) > 0 {
|
||||
@@ -1379,7 +1379,7 @@ type SendVideoNoteParams struct {
|
||||
MessageThreadID *int64 `json:"message_thread_id,omitempty"`
|
||||
// Identifier of the direct messages topic to which the message will be sent; required if the message is sent to a direct messages chat
|
||||
DirectMessagesTopicID *int64 `json:"direct_messages_topic_id,omitempty"`
|
||||
// Video note to send. Pass a file_id as String to send a video note that exists on the Telegram servers (recommended) or upload a new video using multipart/form-data. More information on Sending Files ». Sending video notes by a URL is currently unsupported
|
||||
// Video note to send. Pass a file_id as String to send a video note that exists on the Telegram servers (recommended) or upload a new video using multipart/form-data. More information on Sending Files ». Sending video notes by a URL is currently unsupported.
|
||||
VideoNote *InputFile `json:"video_note"`
|
||||
// Duration of sent video in seconds
|
||||
Duration *int64 `json:"duration,omitempty"`
|
||||
@@ -1399,7 +1399,7 @@ type SendVideoNoteParams struct {
|
||||
SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
|
||||
// Description of the message to reply to
|
||||
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user.
|
||||
ReplyMarkup any `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -1511,7 +1511,7 @@ type SendPaidMediaParams struct {
|
||||
// Media caption, 0-1024 characters after entities parsing
|
||||
Caption string `json:"caption,omitempty"`
|
||||
// Mode for parsing entities in the media caption. See formatting options for more details.
|
||||
ParseMode string `json:"parse_mode,omitempty"`
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the caption, which can be specified instead of parse_mode
|
||||
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
|
||||
// Pass True, if the caption must be shown above the message media
|
||||
@@ -1526,7 +1526,7 @@ type SendPaidMediaParams struct {
|
||||
SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
|
||||
// Description of the message to reply to
|
||||
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user.
|
||||
ReplyMarkup any `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -1559,7 +1559,7 @@ func (p *SendPaidMediaParams) MultipartFields() map[string]string {
|
||||
out["caption"] = p.Caption
|
||||
}
|
||||
if p.ParseMode != "" {
|
||||
out["parse_mode"] = p.ParseMode
|
||||
out["parse_mode"] = string(p.ParseMode)
|
||||
}
|
||||
if p.CaptionEntities != nil {
|
||||
if b, _ := json.Marshal(p.CaptionEntities); len(b) > 0 {
|
||||
@@ -1707,7 +1707,7 @@ type SendLocationParams struct {
|
||||
Longitude float64 `json:"longitude"`
|
||||
// The radius of uncertainty for the location, measured in meters; 0-1500
|
||||
HorizontalAccuracy *float64 `json:"horizontal_accuracy,omitempty"`
|
||||
// Period in seconds during which the location will be updated (see Live Locations, should be between 60 and 86400, or 0x7FFFFFFF for live locations that can be edited indefinitely.
|
||||
// Period in seconds during which the location will be updated (see Live Locations, should be between 60 and 86400, or 0x7FFFFFFF for live locations that can be edited indefinitely
|
||||
LivePeriod *int64 `json:"live_period,omitempty"`
|
||||
// For live locations, a direction in which the user is moving, in degrees. Must be between 1 and 360 if specified.
|
||||
Heading *int64 `json:"heading,omitempty"`
|
||||
@@ -1725,7 +1725,7 @@ type SendLocationParams struct {
|
||||
SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
|
||||
// Description of the message to reply to
|
||||
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user.
|
||||
ReplyMarkup any `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -1776,7 +1776,7 @@ type SendVenueParams struct {
|
||||
SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
|
||||
// Description of the message to reply to
|
||||
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user.
|
||||
ReplyMarkup any `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -1819,7 +1819,7 @@ type SendContactParams struct {
|
||||
SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
|
||||
// Description of the message to reply to
|
||||
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user.
|
||||
ReplyMarkup any `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -1842,9 +1842,9 @@ type SendPollParams struct {
|
||||
MessageThreadID *int64 `json:"message_thread_id,omitempty"`
|
||||
// Poll question, 1-300 characters
|
||||
Question string `json:"question"`
|
||||
// Mode for parsing entities in the question. See formatting options for more details. Currently, only custom emoji entities are allowed
|
||||
QuestionParseMode string `json:"question_parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the poll question. It can be specified instead of question_parse_mode
|
||||
// Mode for parsing entities in the question. See formatting options for more details. Currently, only custom emoji entities are allowed.
|
||||
QuestionParseMode ParseMode `json:"question_parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the poll question. It can be specified instead of question_parse_mode.
|
||||
QuestionEntities []MessageEntity `json:"question_entities,omitempty"`
|
||||
// A JSON-serialized list of 1-12 answer options
|
||||
Options []InputPollOption `json:"options"`
|
||||
@@ -1864,18 +1864,18 @@ type SendPollParams struct {
|
||||
HideResultsUntilCloses *bool `json:"hide_results_until_closes,omitempty"`
|
||||
// Pass True, if voting is limited to users who have been members of the chat where the poll is being sent for more than 24 hours; for channel chats only
|
||||
MembersOnly *bool `json:"members_only,omitempty"`
|
||||
// A JSON-serialized list of 0-12 two-letter ISO 3166-1 alpha-2 country codes indicating the countries from which users can vote in the poll; for channel chats only. If omitted or empty, then users from any country can participate in the poll.
|
||||
// A JSON-serialized list of 0-12 two-letter ISO 3166-1 alpha-2 country codes indicating the countries from which users can vote in the poll; for channel chats only. Use “FT” as a country code to allow users with anonymous numbers to vote. If omitted or empty, then users from any country can participate in the poll.
|
||||
CountryCodes []string `json:"country_codes,omitempty"`
|
||||
// A JSON-serialized list of monotonically increasing 0-based identifiers of the correct answer options, required for polls in quiz mode
|
||||
CorrectOptionIds []int64 `json:"correct_option_ids,omitempty"`
|
||||
// Text that is shown when a user chooses an incorrect answer or taps on the lamp icon in a quiz-style poll, 0-200 characters with at most 2 line feeds after entities parsing
|
||||
Explanation string `json:"explanation,omitempty"`
|
||||
// Mode for parsing entities in the explanation. See formatting options for more details.
|
||||
ExplanationParseMode string `json:"explanation_parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the poll explanation. It can be specified instead of explanation_parse_mode
|
||||
ExplanationParseMode ParseMode `json:"explanation_parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the poll explanation. It can be specified instead of explanation_parse_mode.
|
||||
ExplanationEntities []MessageEntity `json:"explanation_entities,omitempty"`
|
||||
// Media added to the quiz explanation
|
||||
ExplanationMedia *InputPollMedia `json:"explanation_media,omitempty"`
|
||||
ExplanationMedia InputPollMedia `json:"explanation_media,omitempty"`
|
||||
// Amount of time in seconds the poll will be active after creation, 5-2628000. Can't be used together with close_date.
|
||||
OpenPeriod *int64 `json:"open_period,omitempty"`
|
||||
// Point in time (Unix timestamp) when the poll will be automatically closed. Must be at least 5 and no more than 2628000 seconds in the future. Can't be used together with open_period.
|
||||
@@ -1885,11 +1885,11 @@ type SendPollParams struct {
|
||||
// Description of the poll to be sent, 0-1024 characters after entities parsing
|
||||
Description string `json:"description,omitempty"`
|
||||
// Mode for parsing entities in the poll description. See formatting options for more details.
|
||||
DescriptionParseMode string `json:"description_parse_mode,omitempty"`
|
||||
DescriptionParseMode ParseMode `json:"description_parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the poll description, which can be specified instead of description_parse_mode
|
||||
DescriptionEntities []MessageEntity `json:"description_entities,omitempty"`
|
||||
// Media added to the poll description
|
||||
Media *InputPollMedia `json:"media,omitempty"`
|
||||
Media InputPollMedia `json:"media,omitempty"`
|
||||
// Sends the message silently. Users will receive a notification with no sound.
|
||||
DisableNotification *bool `json:"disable_notification,omitempty"`
|
||||
// Protects the contents of the sent message from forwarding and saving
|
||||
@@ -1900,7 +1900,7 @@ type SendPollParams struct {
|
||||
MessageEffectID string `json:"message_effect_id,omitempty"`
|
||||
// Description of the message to reply to
|
||||
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user.
|
||||
ReplyMarkup any `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -1952,8 +1952,8 @@ type SendDiceParams struct {
|
||||
MessageThreadID *int64 `json:"message_thread_id,omitempty"`
|
||||
// Identifier of the direct messages topic to which the message will be sent; required if the message is sent to a direct messages chat
|
||||
DirectMessagesTopicID *int64 `json:"direct_messages_topic_id,omitempty"`
|
||||
// Emoji on which the dice throw animation is based. Currently, must be one of “”, “”, “”, “”, “”, or “”. Dice can have values 1-6 for “”, “” and “”, values 1-5 for “” and “”, and values 1-64 for “”. Defaults to “”
|
||||
Emoji string `json:"emoji,omitempty"`
|
||||
// Emoji on which the dice throw animation is based. Currently, must be one of “”, “”, “”, “”, “”, or “”. Dice can have values 1-6 for “”, “” and “”, values 1-5 for “” and “”, and values 1-64 for “”. Defaults to “”.
|
||||
Emoji DiceEmoji `json:"emoji,omitempty"`
|
||||
// Sends the message silently. Users will receive a notification with no sound.
|
||||
DisableNotification *bool `json:"disable_notification,omitempty"`
|
||||
// Protects the contents of the sent message from forwarding
|
||||
@@ -1966,7 +1966,7 @@ type SendDiceParams struct {
|
||||
SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
|
||||
// Description of the message to reply to
|
||||
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user.
|
||||
ReplyMarkup any `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -1990,7 +1990,7 @@ type SendMessageDraftParams struct {
|
||||
// Text of the message to be sent, 0-4096 characters after entities parsing. Pass an empty text to show a “Thinking…” placeholder.
|
||||
Text string `json:"text,omitempty"`
|
||||
// Mode for parsing entities in the message text. See formatting options for more details.
|
||||
ParseMode string `json:"parse_mode,omitempty"`
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in message text, which can be specified instead of parse_mode
|
||||
Entities []MessageEntity `json:"entities,omitempty"`
|
||||
}
|
||||
@@ -2010,7 +2010,7 @@ func SendMessageDraft(ctx context.Context, b *client.Bot, p *SendMessageDraftPar
|
||||
type SendChatActionParams struct {
|
||||
// Unique identifier of the business connection on behalf of which the action will be sent
|
||||
BusinessConnectionID string `json:"business_connection_id,omitempty"`
|
||||
// Unique identifier for the target chat or username of the target bot, supergroup or channel in the format @username. Channel chats and channel direct messages chats aren't supported.
|
||||
// Unique identifier for the target chat or username of the target bot or supergroup in the format @username. Channel chats and channel direct messages chats aren't supported.
|
||||
ChatID ChatID `json:"chat_id"`
|
||||
// Unique identifier for the target message thread or topic of a forum; for supergroups and private chats of bots with forum topic mode enabled only
|
||||
MessageThreadID *int64 `json:"message_thread_id,omitempty"`
|
||||
@@ -2174,7 +2174,7 @@ type RestrictChatMemberParams struct {
|
||||
Permissions ChatPermissions `json:"permissions"`
|
||||
// Pass True if chat permissions are set independently. Otherwise, the can_send_other_messages and can_add_web_page_previews permissions will imply the can_send_messages, can_send_audios, can_send_documents, can_send_photos, can_send_videos, can_send_video_notes, and can_send_voice_notes permissions; the can_send_polls permission will imply the can_send_messages permission.
|
||||
UseIndependentChatPermissions *bool `json:"use_independent_chat_permissions,omitempty"`
|
||||
// Date when restrictions will be lifted for the user; Unix time. If user is restricted for more than 366 days or less than 30 seconds from the current time, they are considered to be restricted forever
|
||||
// Date when restrictions will be lifted for the user; Unix time. If user is restricted for more than 366 days or less than 30 seconds from the current time, they are considered to be restricted forever.
|
||||
UntilDate *int64 `json:"until_date,omitempty"`
|
||||
}
|
||||
|
||||
@@ -2201,7 +2201,7 @@ type PromoteChatMemberParams struct {
|
||||
CanDeleteMessages *bool `json:"can_delete_messages,omitempty"`
|
||||
// Pass True if the administrator can manage video chats
|
||||
CanManageVideoChats *bool `json:"can_manage_video_chats,omitempty"`
|
||||
// Pass True if the administrator can restrict, ban or unban chat members, or access supergroup statistics. For backward compatibility, defaults to True for promotions of channel administrators
|
||||
// Pass True if the administrator can restrict, ban or unban chat members, or access supergroup statistics. For backward compatibility, defaults to True for promotions of channel administrators.
|
||||
CanRestrictMembers *bool `json:"can_restrict_members,omitempty"`
|
||||
// Pass True if the administrator can add new administrators with a subset of their own privileges or demote administrators that they have promoted, directly or indirectly (promoted by administrators that were appointed by him)
|
||||
CanPromoteMembers *bool `json:"can_promote_members,omitempty"`
|
||||
@@ -2356,7 +2356,7 @@ type CreateChatInviteLinkParams struct {
|
||||
ExpireDate *int64 `json:"expire_date,omitempty"`
|
||||
// The maximum number of users that can be members of the chat simultaneously after joining the chat via this invite link; 1-99999
|
||||
MemberLimit *int64 `json:"member_limit,omitempty"`
|
||||
// True, if users joining the chat via the link need to be approved by chat administrators. If True, member_limit can't be specified
|
||||
// True, if users joining the chat via the link need to be approved by chat administrators. If True, member_limit can't be specified.
|
||||
CreatesJoinRequest *bool `json:"creates_join_request,omitempty"`
|
||||
}
|
||||
|
||||
@@ -2381,7 +2381,7 @@ type EditChatInviteLinkParams struct {
|
||||
ExpireDate *int64 `json:"expire_date,omitempty"`
|
||||
// The maximum number of users that can be members of the chat simultaneously after joining the chat via this invite link; 1-99999
|
||||
MemberLimit *int64 `json:"member_limit,omitempty"`
|
||||
// True, if users joining the chat via the link need to be approved by chat administrators. If True, member_limit can't be specified
|
||||
// True, if users joining the chat via the link need to be approved by chat administrators. If True, member_limit can't be specified.
|
||||
CreatesJoinRequest *bool `json:"creates_join_request,omitempty"`
|
||||
}
|
||||
|
||||
@@ -2676,7 +2676,23 @@ type GetChatAdministratorsParams struct {
|
||||
//
|
||||
// Use this method to get a list of administrators in a chat. Returns an Array of ChatMember objects.
|
||||
func GetChatAdministrators(ctx context.Context, b *client.Bot, p *GetChatAdministratorsParams) ([]ChatMember, error) {
|
||||
return client.Call[*GetChatAdministratorsParams, []ChatMember](ctx, b, "getChatAdministrators", p)
|
||||
raw, err := client.CallRaw[*GetChatAdministratorsParams](ctx, b, "getChatAdministrators", p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var elems []json.RawMessage
|
||||
if err := json.Unmarshal(raw, &elems); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]ChatMember, 0, len(elems))
|
||||
for _, e := range elems {
|
||||
v, err := UnmarshalChatMember(e)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, v)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// GetChatMemberCountParams is the parameter set for GetChatMemberCount.
|
||||
@@ -2785,7 +2801,7 @@ type CreateForumTopicParams struct {
|
||||
ChatID ChatID `json:"chat_id"`
|
||||
// Topic name, 1-128 characters
|
||||
Name string `json:"name"`
|
||||
// Color of the topic icon in RGB format. Currently, must be one of 7322096 (0x6FB9F0), 16766590 (0xFFD67E), 13338331 (0xCB86DB), 9367192 (0x8EEE98), 16749490 (0xFF93B2), or 16478047 (0xFB6F5F)
|
||||
// Color of the topic icon in RGB format. Currently, must be one of 7322096 (0x6FB9F0), 16766590 (0xFFD67E), 13338331 (0xCB86DB), 9367192 (0x8EEE98), 16749490 (0xFF93B2), or 16478047 (0xFB6F5F).
|
||||
IconColor *int64 `json:"icon_color,omitempty"`
|
||||
// Unique identifier of the custom emoji shown as the topic icon. Use getForumTopicIconStickers to get all allowed custom emoji identifiers.
|
||||
IconCustomEmojiID string `json:"icon_custom_emoji_id,omitempty"`
|
||||
@@ -2806,9 +2822,9 @@ type EditForumTopicParams struct {
|
||||
ChatID ChatID `json:"chat_id"`
|
||||
// Unique identifier for the target message thread of the forum topic
|
||||
MessageThreadID int64 `json:"message_thread_id"`
|
||||
// New topic name, 0-128 characters. If not specified or empty, the current name of the topic will be kept
|
||||
// New topic name, 0-128 characters. If not specified or empty, the current name of the topic will be kept.
|
||||
Name string `json:"name,omitempty"`
|
||||
// New unique identifier of the custom emoji shown as the topic icon. Use getForumTopicIconStickers to get all allowed custom emoji identifiers. Pass an empty string to remove the icon. If not specified, the current icon will be kept
|
||||
// New unique identifier of the custom emoji shown as the topic icon. Use getForumTopicIconStickers to get all allowed custom emoji identifiers. Pass an empty string to remove the icon. If not specified, the current icon will be kept.
|
||||
IconCustomEmojiID string `json:"icon_custom_emoji_id,omitempty"`
|
||||
}
|
||||
|
||||
@@ -2986,7 +3002,7 @@ func UnpinAllGeneralForumTopicMessages(ctx context.Context, b *client.Bot, p *Un
|
||||
type AnswerCallbackQueryParams struct {
|
||||
// Unique identifier for the query to be answered
|
||||
CallbackQueryID string `json:"callback_query_id"`
|
||||
// Text of the notification. If not specified, nothing will be shown to the user, 0-200 characters
|
||||
// Text of the notification. If not specified, nothing will be shown to the user, 0-200 characters.
|
||||
Text string `json:"text,omitempty"`
|
||||
// If True, an alert will be shown by the client instead of a notification at the top of the chat screen. Defaults to false.
|
||||
ShowAlert *bool `json:"show_alert,omitempty"`
|
||||
@@ -3124,8 +3140,8 @@ type SetMyCommandsParams struct {
|
||||
// A JSON-serialized list of bot commands to be set as the list of the bot's commands. At most 100 commands can be specified.
|
||||
Commands []BotCommand `json:"commands"`
|
||||
// A JSON-serialized object, describing scope of users for which the commands are relevant. Defaults to BotCommandScopeDefault.
|
||||
Scope *BotCommandScope `json:"scope,omitempty"`
|
||||
// A two-letter ISO 639-1 language code. If empty, commands will be applied to all users from the given scope, for whose language there are no dedicated commands
|
||||
Scope BotCommandScope `json:"scope,omitempty"`
|
||||
// A two-letter ISO 639-1 language code. If empty, commands will be applied to all users from the given scope, for whose language there are no dedicated commands.
|
||||
LanguageCode string `json:"language_code,omitempty"`
|
||||
}
|
||||
|
||||
@@ -3141,8 +3157,8 @@ func SetMyCommands(ctx context.Context, b *client.Bot, p *SetMyCommandsParams) (
|
||||
// Use this method to delete the list of the bot's commands for the given scope and user language. After deletion, higher level commands will be shown to affected users. Returns True on success.
|
||||
type DeleteMyCommandsParams struct {
|
||||
// A JSON-serialized object, describing scope of users for which the commands are relevant. Defaults to BotCommandScopeDefault.
|
||||
Scope *BotCommandScope `json:"scope,omitempty"`
|
||||
// A two-letter ISO 639-1 language code. If empty, commands will be applied to all users from the given scope, for whose language there are no dedicated commands
|
||||
Scope BotCommandScope `json:"scope,omitempty"`
|
||||
// A two-letter ISO 639-1 language code. If empty, commands will be applied to all users from the given scope, for whose language there are no dedicated commands.
|
||||
LanguageCode string `json:"language_code,omitempty"`
|
||||
}
|
||||
|
||||
@@ -3158,7 +3174,7 @@ func DeleteMyCommands(ctx context.Context, b *client.Bot, p *DeleteMyCommandsPar
|
||||
// Use this method to get the current list of the bot's commands for the given scope and user language. Returns an Array of BotCommand objects. If commands aren't set, an empty list is returned.
|
||||
type GetMyCommandsParams struct {
|
||||
// A JSON-serialized object, describing scope of users. Defaults to BotCommandScopeDefault.
|
||||
Scope *BotCommandScope `json:"scope,omitempty"`
|
||||
Scope BotCommandScope `json:"scope,omitempty"`
|
||||
// A two-letter ISO 639-1 language code or an empty string
|
||||
LanguageCode string `json:"language_code,omitempty"`
|
||||
}
|
||||
@@ -3298,9 +3314,9 @@ func RemoveMyProfilePhoto(ctx context.Context, b *client.Bot, p *RemoveMyProfile
|
||||
//
|
||||
// Use this method to change the bot's menu button in a private chat, or the default menu button. Returns True on success.
|
||||
type SetChatMenuButtonParams struct {
|
||||
// Unique identifier for the target private chat. If not specified, default bot's menu button will be changed
|
||||
// Unique identifier for the target private chat. If not specified, the bot's default menu button will be changed.
|
||||
ChatID *int64 `json:"chat_id,omitempty"`
|
||||
// A JSON-serialized object for the bot's new menu button. Defaults to MenuButtonDefault
|
||||
// A JSON-serialized object for the bot's new menu button. Defaults to MenuButtonDefault.
|
||||
MenuButton MenuButton `json:"menu_button,omitempty"`
|
||||
}
|
||||
|
||||
@@ -3315,7 +3331,7 @@ func SetChatMenuButton(ctx context.Context, b *client.Bot, p *SetChatMenuButtonP
|
||||
//
|
||||
// Use this method to get the current value of the bot's menu button in a private chat, or the default menu button. Returns MenuButton on success.
|
||||
type GetChatMenuButtonParams struct {
|
||||
// Unique identifier for the target private chat. If not specified, default bot's menu button will be returned
|
||||
// Unique identifier for the target private chat. If not specified, the bot's default menu button will be returned.
|
||||
ChatID *int64 `json:"chat_id,omitempty"`
|
||||
}
|
||||
|
||||
@@ -3390,7 +3406,7 @@ type SendGiftParams struct {
|
||||
// Text that will be shown along with the gift; 0-128 characters
|
||||
Text string `json:"text,omitempty"`
|
||||
// Mode for parsing entities in the text. See formatting options for more details. Entities other than “bold”, “italic”, “underline”, “strikethrough”, “spoiler”, “custom_emoji”, and “date_time” are ignored.
|
||||
TextParseMode string `json:"text_parse_mode,omitempty"`
|
||||
TextParseMode ParseMode `json:"text_parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the gift text. It can be specified instead of text_parse_mode. Entities other than “bold”, “italic”, “underline”, “strikethrough”, “spoiler”, “custom_emoji”, and “date_time” are ignored.
|
||||
TextEntities []MessageEntity `json:"text_entities,omitempty"`
|
||||
}
|
||||
@@ -3415,7 +3431,7 @@ type GiftPremiumSubscriptionParams struct {
|
||||
// Text that will be shown along with the service message about the subscription; 0-128 characters
|
||||
Text string `json:"text,omitempty"`
|
||||
// Mode for parsing entities in the text. See formatting options for more details. Entities other than “bold”, “italic”, “underline”, “strikethrough”, “spoiler”, “custom_emoji”, and “date_time” are ignored.
|
||||
TextParseMode string `json:"text_parse_mode,omitempty"`
|
||||
TextParseMode ParseMode `json:"text_parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the gift text. It can be specified instead of text_parse_mode. Entities other than “bold”, “italic”, “underline”, “strikethrough”, “spoiler”, “custom_emoji”, and “date_time” are ignored.
|
||||
TextEntities []MessageEntity `json:"text_entities,omitempty"`
|
||||
}
|
||||
@@ -3516,7 +3532,7 @@ func ReadBusinessMessage(ctx context.Context, b *client.Bot, p *ReadBusinessMess
|
||||
type DeleteBusinessMessagesParams struct {
|
||||
// Unique identifier of the business connection on behalf of which to delete the messages
|
||||
BusinessConnectionID string `json:"business_connection_id"`
|
||||
// A JSON-serialized list of 1-100 identifiers of messages to delete. All messages must be from the same chat. See deleteMessage for limitations on which messages can be deleted
|
||||
// A JSON-serialized list of 1-100 identifiers of messages to delete. All messages must be from the same chat. See deleteMessage for limitations on which messages can be deleted.
|
||||
MessageIds []int64 `json:"message_ids"`
|
||||
}
|
||||
|
||||
@@ -3691,7 +3707,7 @@ type GetBusinessAccountGiftsParams struct {
|
||||
SortByPrice *bool `json:"sort_by_price,omitempty"`
|
||||
// Offset of the first entry to return as received from the previous request; use empty string to get the first chunk of results
|
||||
Offset string `json:"offset,omitempty"`
|
||||
// The maximum number of gifts to be returned; 1-100. Defaults to 100
|
||||
// The maximum number of gifts to be returned; 1-100. Defaults to 100.
|
||||
Limit *int64 `json:"limit,omitempty"`
|
||||
}
|
||||
|
||||
@@ -3722,7 +3738,7 @@ type GetUserGiftsParams struct {
|
||||
SortByPrice *bool `json:"sort_by_price,omitempty"`
|
||||
// Offset of the first entry to return as received from the previous request; use an empty string to get the first chunk of results
|
||||
Offset string `json:"offset,omitempty"`
|
||||
// The maximum number of gifts to be returned; 1-100. Defaults to 100
|
||||
// The maximum number of gifts to be returned; 1-100. Defaults to 100.
|
||||
Limit *int64 `json:"limit,omitempty"`
|
||||
}
|
||||
|
||||
@@ -3757,7 +3773,7 @@ type GetChatGiftsParams struct {
|
||||
SortByPrice *bool `json:"sort_by_price,omitempty"`
|
||||
// Offset of the first entry to return as received from the previous request; use an empty string to get the first chunk of results
|
||||
Offset string `json:"offset,omitempty"`
|
||||
// The maximum number of gifts to be returned; 1-100. Defaults to 100
|
||||
// The maximum number of gifts to be returned; 1-100. Defaults to 100.
|
||||
Limit *int64 `json:"limit,omitempty"`
|
||||
}
|
||||
|
||||
@@ -3840,7 +3856,7 @@ type PostStoryParams struct {
|
||||
// Caption of the story, 0-2048 characters after entities parsing
|
||||
Caption string `json:"caption,omitempty"`
|
||||
// Mode for parsing entities in the story caption. See formatting options for more details.
|
||||
ParseMode string `json:"parse_mode,omitempty"`
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the caption, which can be specified instead of parse_mode
|
||||
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
|
||||
// A JSON-serialized list of clickable areas to be shown on the story
|
||||
@@ -3896,7 +3912,7 @@ type EditStoryParams struct {
|
||||
// Caption of the story, 0-2048 characters after entities parsing
|
||||
Caption string `json:"caption,omitempty"`
|
||||
// Mode for parsing entities in the story caption. See formatting options for more details.
|
||||
ParseMode string `json:"parse_mode,omitempty"`
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the caption, which can be specified instead of parse_mode
|
||||
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
|
||||
// A JSON-serialized list of clickable areas to be shown on the story
|
||||
@@ -3975,7 +3991,7 @@ func SavePreparedInlineMessage(ctx context.Context, b *client.Bot, p *SavePrepar
|
||||
type SavePreparedKeyboardButtonParams struct {
|
||||
// Unique identifier of the target user that can use the button
|
||||
UserID int64 `json:"user_id"`
|
||||
// A JSON-serialized object describing the button to be saved. The button must be of the type request_users, request_chat, or request_managed_bot
|
||||
// A JSON-serialized object describing the button to be saved. The button must be of the type request_users, request_chat, or request_managed_bot.
|
||||
Button KeyboardButton `json:"button"`
|
||||
}
|
||||
|
||||
@@ -3994,19 +4010,19 @@ type EditMessageTextParams struct {
|
||||
BusinessConnectionID string `json:"business_connection_id,omitempty"`
|
||||
// Required if inline_message_id is not specified. Unique identifier for the target chat or username of the target bot, supergroup or channel in the format @username.
|
||||
ChatID *ChatID `json:"chat_id,omitempty"`
|
||||
// Required if inline_message_id is not specified. Identifier of the message to edit
|
||||
// Required if inline_message_id is not specified. Identifier of the message to edit.
|
||||
MessageID *int64 `json:"message_id,omitempty"`
|
||||
// Required if chat_id and message_id are not specified. Identifier of the inline message
|
||||
// Required if chat_id and message_id are not specified. Identifier of the inline message.
|
||||
InlineMessageID string `json:"inline_message_id,omitempty"`
|
||||
// New text of the message, 1-4096 characters after entities parsing
|
||||
Text string `json:"text"`
|
||||
// Mode for parsing entities in the message text. See formatting options for more details.
|
||||
ParseMode string `json:"parse_mode,omitempty"`
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in message text, which can be specified instead of parse_mode
|
||||
Entities []MessageEntity `json:"entities,omitempty"`
|
||||
// Link preview generation options for the message
|
||||
LinkPreviewOptions *LinkPreviewOptions `json:"link_preview_options,omitempty"`
|
||||
// A JSON-serialized object for an inline keyboard.
|
||||
// A JSON-serialized object for an inline keyboard
|
||||
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4025,19 +4041,19 @@ type EditMessageCaptionParams struct {
|
||||
BusinessConnectionID string `json:"business_connection_id,omitempty"`
|
||||
// Required if inline_message_id is not specified. Unique identifier for the target chat or username of the target bot, supergroup or channel in the format @username.
|
||||
ChatID *ChatID `json:"chat_id,omitempty"`
|
||||
// Required if inline_message_id is not specified. Identifier of the message to edit
|
||||
// Required if inline_message_id is not specified. Identifier of the message to edit.
|
||||
MessageID *int64 `json:"message_id,omitempty"`
|
||||
// Required if chat_id and message_id are not specified. Identifier of the inline message
|
||||
// Required if chat_id and message_id are not specified. Identifier of the inline message.
|
||||
InlineMessageID string `json:"inline_message_id,omitempty"`
|
||||
// New caption of the message, 0-1024 characters after entities parsing
|
||||
Caption string `json:"caption,omitempty"`
|
||||
// Mode for parsing entities in the message caption. See formatting options for more details.
|
||||
ParseMode string `json:"parse_mode,omitempty"`
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the caption, which can be specified instead of parse_mode
|
||||
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
|
||||
// Pass True, if the caption must be shown above the message media. Supported only for animation, photo and video messages.
|
||||
ShowCaptionAboveMedia *bool `json:"show_caption_above_media,omitempty"`
|
||||
// A JSON-serialized object for an inline keyboard.
|
||||
// A JSON-serialized object for an inline keyboard
|
||||
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4056,13 +4072,13 @@ type EditMessageMediaParams struct {
|
||||
BusinessConnectionID string `json:"business_connection_id,omitempty"`
|
||||
// Required if inline_message_id is not specified. Unique identifier for the target chat or username of the target bot, supergroup or channel in the format @username.
|
||||
ChatID *ChatID `json:"chat_id,omitempty"`
|
||||
// Required if inline_message_id is not specified. Identifier of the message to edit
|
||||
// Required if inline_message_id is not specified. Identifier of the message to edit.
|
||||
MessageID *int64 `json:"message_id,omitempty"`
|
||||
// Required if chat_id and message_id are not specified. Identifier of the inline message
|
||||
// Required if chat_id and message_id are not specified. Identifier of the inline message.
|
||||
InlineMessageID string `json:"inline_message_id,omitempty"`
|
||||
// A JSON-serialized object for a new media content of the message
|
||||
Media InputMedia `json:"media"`
|
||||
// A JSON-serialized object for a new inline keyboard.
|
||||
// A JSON-serialized object for a new inline keyboard
|
||||
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4118,15 +4134,15 @@ type EditMessageLiveLocationParams struct {
|
||||
BusinessConnectionID string `json:"business_connection_id,omitempty"`
|
||||
// Required if inline_message_id is not specified. Unique identifier for the target chat or username of the target bot, supergroup or channel in the format @username.
|
||||
ChatID *ChatID `json:"chat_id,omitempty"`
|
||||
// Required if inline_message_id is not specified. Identifier of the message to edit
|
||||
// Required if inline_message_id is not specified. Identifier of the message to edit.
|
||||
MessageID *int64 `json:"message_id,omitempty"`
|
||||
// Required if chat_id and message_id are not specified. Identifier of the inline message
|
||||
// Required if chat_id and message_id are not specified. Identifier of the inline message.
|
||||
InlineMessageID string `json:"inline_message_id,omitempty"`
|
||||
// Latitude of new location
|
||||
Latitude float64 `json:"latitude"`
|
||||
// Longitude of new location
|
||||
Longitude float64 `json:"longitude"`
|
||||
// New period in seconds during which the location can be updated, starting from the message send date. If 0x7FFFFFFF is specified, then the location can be updated forever. Otherwise, the new value must not exceed the current live_period by more than a day, and the live location expiration date must remain within the next 90 days. If not specified, then live_period remains unchanged
|
||||
// New period in seconds during which the location can be updated, starting from the message send date. If 0x7FFFFFFF is specified, then the location can be updated forever. Otherwise, the new value must not exceed the current live_period by more than a day, and the live location expiration date must remain within the next 90 days. If not specified, then live_period remains unchanged.
|
||||
LivePeriod *int64 `json:"live_period,omitempty"`
|
||||
// The radius of uncertainty for the location, measured in meters; 0-1500
|
||||
HorizontalAccuracy *float64 `json:"horizontal_accuracy,omitempty"`
|
||||
@@ -4134,7 +4150,7 @@ type EditMessageLiveLocationParams struct {
|
||||
Heading *int64 `json:"heading,omitempty"`
|
||||
// The maximum distance for proximity alerts about approaching another chat member, in meters. Must be between 1 and 100000 if specified.
|
||||
ProximityAlertRadius *int64 `json:"proximity_alert_radius,omitempty"`
|
||||
// A JSON-serialized object for a new inline keyboard.
|
||||
// A JSON-serialized object for a new inline keyboard
|
||||
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4153,11 +4169,11 @@ type StopMessageLiveLocationParams struct {
|
||||
BusinessConnectionID string `json:"business_connection_id,omitempty"`
|
||||
// Required if inline_message_id is not specified. Unique identifier for the target chat or username of the target bot, supergroup or channel in the format @username.
|
||||
ChatID *ChatID `json:"chat_id,omitempty"`
|
||||
// Required if inline_message_id is not specified. Identifier of the message with live location to stop
|
||||
// Required if inline_message_id is not specified. Identifier of the message with live location to stop.
|
||||
MessageID *int64 `json:"message_id,omitempty"`
|
||||
// Required if chat_id and message_id are not specified. Identifier of the inline message
|
||||
// Required if chat_id and message_id are not specified. Identifier of the inline message.
|
||||
InlineMessageID string `json:"inline_message_id,omitempty"`
|
||||
// A JSON-serialized object for a new inline keyboard.
|
||||
// A JSON-serialized object for a new inline keyboard
|
||||
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4199,11 +4215,11 @@ type EditMessageReplyMarkupParams struct {
|
||||
BusinessConnectionID string `json:"business_connection_id,omitempty"`
|
||||
// Required if inline_message_id is not specified. Unique identifier for the target chat or username of the target bot, supergroup or channel in the format @username.
|
||||
ChatID *ChatID `json:"chat_id,omitempty"`
|
||||
// Required if inline_message_id is not specified. Identifier of the message to edit
|
||||
// Required if inline_message_id is not specified. Identifier of the message to edit.
|
||||
MessageID *int64 `json:"message_id,omitempty"`
|
||||
// Required if chat_id and message_id are not specified. Identifier of the inline message
|
||||
// Required if chat_id and message_id are not specified. Identifier of the inline message.
|
||||
InlineMessageID string `json:"inline_message_id,omitempty"`
|
||||
// A JSON-serialized object for an inline keyboard.
|
||||
// A JSON-serialized object for an inline keyboard
|
||||
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4224,7 +4240,7 @@ type StopPollParams struct {
|
||||
ChatID ChatID `json:"chat_id"`
|
||||
// Identifier of the original message with the poll
|
||||
MessageID int64 `json:"message_id"`
|
||||
// A JSON-serialized object for a new message inline keyboard.
|
||||
// A JSON-serialized object for a new message inline keyboard
|
||||
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4243,7 +4259,7 @@ type ApproveSuggestedPostParams struct {
|
||||
ChatID int64 `json:"chat_id"`
|
||||
// Identifier of a suggested post message to approve
|
||||
MessageID int64 `json:"message_id"`
|
||||
// Point in time (Unix timestamp) when the post is expected to be published; omit if the date has already been specified when the suggested post was created. If specified, then the date must be not more than 2678400 seconds (30 days) in the future
|
||||
// Point in time (Unix timestamp) when the post is expected to be published; omit if the date has already been specified when the suggested post was created. If specified, then the date must be not more than 2678400 seconds (30 days) in the future.
|
||||
SendDate *int64 `json:"send_date,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4296,7 +4312,7 @@ func DeleteMessage(ctx context.Context, b *client.Bot, p *DeleteMessageParams) (
|
||||
type DeleteMessagesParams struct {
|
||||
// Unique identifier for the target chat or username of the target bot, supergroup or channel in the format @username
|
||||
ChatID ChatID `json:"chat_id"`
|
||||
// A JSON-serialized list of 1-100 identifiers of messages to delete. See deleteMessage for limitations on which messages can be deleted
|
||||
// A JSON-serialized list of 1-100 identifiers of messages to delete. See deleteMessage for limitations on which messages can be deleted.
|
||||
MessageIds []int64 `json:"message_ids"`
|
||||
}
|
||||
|
||||
@@ -4311,7 +4327,7 @@ func DeleteMessages(ctx context.Context, b *client.Bot, p *DeleteMessagesParams)
|
||||
//
|
||||
// Use this method to remove a reaction from a message in a group or a supergroup chat. The bot must have the 'can_delete_messages' administrator right in the chat. Returns True on success.
|
||||
type DeleteMessageReactionParams struct {
|
||||
// Unique identifier for the target chat or username of the target supergroup (in the format @username)
|
||||
// Unique identifier for the target chat or username of the target supergroup in the format @username
|
||||
ChatID ChatID `json:"chat_id"`
|
||||
// Identifier of the target message
|
||||
MessageID int64 `json:"message_id"`
|
||||
@@ -4332,7 +4348,7 @@ func DeleteMessageReaction(ctx context.Context, b *client.Bot, p *DeleteMessageR
|
||||
//
|
||||
// Use this method to remove up to 10000 recent reactions in a group or a supergroup chat added by a given user or chat. The bot must have the 'can_delete_messages' administrator right in the chat. Returns True on success.
|
||||
type DeleteAllMessageReactionsParams struct {
|
||||
// Unique identifier for the target chat or username of the target supergroup (in the format @username)
|
||||
// Unique identifier for the target chat or username of the target supergroup in the format @username
|
||||
ChatID ChatID `json:"chat_id"`
|
||||
// Identifier of the user whose reactions will be removed, if the reactions were added by a user
|
||||
UserID *int64 `json:"user_id,omitempty"`
|
||||
@@ -4375,7 +4391,7 @@ type SendStickerParams struct {
|
||||
SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
|
||||
// Description of the message to reply to
|
||||
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user.
|
||||
ReplyMarkup any `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4492,7 +4508,7 @@ type UploadStickerFileParams struct {
|
||||
// A file with the sticker in .WEBP, .PNG, .TGS, or .WEBM format. See https://core.telegram.org/stickers for technical requirements. More information on Sending Files »
|
||||
Sticker *InputFile `json:"sticker"`
|
||||
// Format of the sticker, must be one of “static”, “animated”, “video”
|
||||
StickerFormat string `json:"sticker_format"`
|
||||
StickerFormat InputStickerFormat `json:"sticker_format"`
|
||||
}
|
||||
|
||||
// HasFile reports whether a multipart upload is required.
|
||||
@@ -4507,7 +4523,7 @@ func (p *UploadStickerFileParams) HasFile() bool {
|
||||
func (p *UploadStickerFileParams) MultipartFields() map[string]string {
|
||||
out := map[string]string{}
|
||||
out["user_id"] = strconv.FormatInt(p.UserID, 10)
|
||||
out["sticker_format"] = p.StickerFormat
|
||||
out["sticker_format"] = string(p.StickerFormat)
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -4707,7 +4723,7 @@ type SetStickerSetThumbnailParams struct {
|
||||
// A .WEBP or .PNG image with the thumbnail, must be up to 128 kilobytes in size and have a width and height of exactly 100px, or a .TGS animation with a thumbnail up to 32 kilobytes in size (see https://core.telegram.org/stickers#animation-requirements for animated sticker technical requirements), or a .WEBM video with the thumbnail up to 32 kilobytes in size; see https://core.telegram.org/stickers#video-requirements for video sticker technical requirements. Pass a file_id as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. More information on Sending Files ». Animated and video sticker set thumbnails can't be uploaded via HTTP URL. If omitted, then the thumbnail is dropped and the first sticker is used as the thumbnail.
|
||||
Thumbnail *InputFile `json:"thumbnail,omitempty"`
|
||||
// Format of the thumbnail, must be one of “static” for a .WEBP or .PNG image, “animated” for a .TGS animation, or “video” for a .WEBM video
|
||||
Format string `json:"format"`
|
||||
Format InputStickerFormat `json:"format"`
|
||||
}
|
||||
|
||||
// HasFile reports whether a multipart upload is required.
|
||||
@@ -4723,7 +4739,7 @@ func (p *SetStickerSetThumbnailParams) MultipartFields() map[string]string {
|
||||
out := map[string]string{}
|
||||
out["name"] = p.Name
|
||||
out["user_id"] = strconv.FormatInt(p.UserID, 10)
|
||||
out["format"] = p.Format
|
||||
out["format"] = string(p.Format)
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -4753,7 +4769,7 @@ func SetStickerSetThumbnail(ctx context.Context, b *client.Bot, p *SetStickerSet
|
||||
type SetCustomEmojiStickerSetThumbnailParams struct {
|
||||
// Sticker set name
|
||||
Name string `json:"name"`
|
||||
// Custom emoji identifier of a sticker from the sticker set; pass an empty string to drop the thumbnail and use the first sticker as the thumbnail.
|
||||
// Custom emoji identifier of a sticker from the sticker set; pass an empty string to drop the thumbnail and use the first sticker as the thumbnail
|
||||
CustomEmojiID string `json:"custom_emoji_id,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4830,7 +4846,7 @@ type SendInvoiceParams struct {
|
||||
MaxTipAmount *int64 `json:"max_tip_amount,omitempty"`
|
||||
// A JSON-serialized array of suggested amounts of tips in the smallest units of the currency (integer, not float/double). At most 4 suggested tip amounts can be specified. The suggested tip amounts must be positive, passed in a strictly increased order and must not exceed max_tip_amount.
|
||||
SuggestedTipAmounts []int64 `json:"suggested_tip_amounts,omitempty"`
|
||||
// Unique deep-linking parameter. If left empty, forwarded copies of the sent message will have a Pay button, allowing multiple users to pay directly from the forwarded message, using the same invoice. If non-empty, forwarded copies of the sent message will have a URL button with a deep link to the bot (instead of a Pay button), with the value used as the start parameter
|
||||
// Unique deep-linking parameter. If left empty, forwarded copies of the sent message will have a Pay button, allowing multiple users to pay directly from the forwarded message, using the same invoice. If non-empty, forwarded copies of the sent message will have a URL button with a deep link to the bot (instead of a Pay button), with the value used as the start parameter.
|
||||
StartParameter string `json:"start_parameter,omitempty"`
|
||||
// JSON-serialized data about the invoice, which will be shared with the payment provider. A detailed description of required fields should be provided by the payment provider.
|
||||
ProviderData string `json:"provider_data,omitempty"`
|
||||
@@ -5102,15 +5118,15 @@ type SetGameScoreParams struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
// New score, must be non-negative
|
||||
Score int64 `json:"score"`
|
||||
// Pass True if the high score is allowed to decrease. This can be useful when fixing mistakes or banning cheaters
|
||||
// Pass True if the high score is allowed to decrease. This can be useful when fixing mistakes or banning cheaters.
|
||||
Force *bool `json:"force,omitempty"`
|
||||
// Pass True if the game message should not be automatically edited to include the current scoreboard
|
||||
DisableEditMessage *bool `json:"disable_edit_message,omitempty"`
|
||||
// Required if inline_message_id is not specified. Unique identifier for the target chat
|
||||
// Required if inline_message_id is not specified. Unique identifier for the target chat.
|
||||
ChatID *int64 `json:"chat_id,omitempty"`
|
||||
// Required if inline_message_id is not specified. Identifier of the sent message
|
||||
// Required if inline_message_id is not specified. Identifier of the sent message.
|
||||
MessageID *int64 `json:"message_id,omitempty"`
|
||||
// Required if chat_id and message_id are not specified. Identifier of the inline message
|
||||
// Required if chat_id and message_id are not specified. Identifier of the inline message.
|
||||
InlineMessageID string `json:"inline_message_id,omitempty"`
|
||||
}
|
||||
|
||||
@@ -5128,11 +5144,11 @@ func SetGameScore(ctx context.Context, b *client.Bot, p *SetGameScoreParams) (*M
|
||||
type GetGameHighScoresParams struct {
|
||||
// Target user id
|
||||
UserID int64 `json:"user_id"`
|
||||
// Required if inline_message_id is not specified. Unique identifier for the target chat
|
||||
// Required if inline_message_id is not specified. Unique identifier for the target chat.
|
||||
ChatID *int64 `json:"chat_id,omitempty"`
|
||||
// Required if inline_message_id is not specified. Identifier of the sent message
|
||||
// Required if inline_message_id is not specified. Identifier of the sent message.
|
||||
MessageID *int64 `json:"message_id,omitempty"`
|
||||
// Required if chat_id and message_id are not specified. Identifier of the inline message
|
||||
// Required if chat_id and message_id are not specified. Identifier of the inline message.
|
||||
InlineMessageID string `json:"inline_message_id,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
+14
-14
@@ -20954,7 +20954,7 @@ func Test_UploadStickerFile_Success(t *testing.T) {
|
||||
params := &UploadStickerFileParams{
|
||||
UserID: 42,
|
||||
Sticker: &InputFile{PathOrID: "file_id_test"},
|
||||
StickerFormat: "test_value",
|
||||
StickerFormat: InputStickerFormatStatic,
|
||||
}
|
||||
_, err := UploadStickerFile(context.Background(), bot, params)
|
||||
require.NoError(t, err)
|
||||
@@ -20969,7 +20969,7 @@ func Test_UploadStickerFile_APIError(t *testing.T) {
|
||||
params := &UploadStickerFileParams{
|
||||
UserID: 42,
|
||||
Sticker: &InputFile{PathOrID: "file_id_test"},
|
||||
StickerFormat: "test_value",
|
||||
StickerFormat: InputStickerFormatStatic,
|
||||
}
|
||||
_, err := UploadStickerFile(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
@@ -20987,7 +20987,7 @@ func Test_UploadStickerFile_NetworkError(t *testing.T) {
|
||||
params := &UploadStickerFileParams{
|
||||
UserID: 42,
|
||||
Sticker: &InputFile{PathOrID: "file_id_test"},
|
||||
StickerFormat: "test_value",
|
||||
StickerFormat: InputStickerFormatStatic,
|
||||
}
|
||||
_, err := UploadStickerFile(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
@@ -21003,7 +21003,7 @@ func Test_UploadStickerFile_ParseError(t *testing.T) {
|
||||
params := &UploadStickerFileParams{
|
||||
UserID: 42,
|
||||
Sticker: &InputFile{PathOrID: "file_id_test"},
|
||||
StickerFormat: "test_value",
|
||||
StickerFormat: InputStickerFormatStatic,
|
||||
}
|
||||
_, err := UploadStickerFile(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
@@ -21022,7 +21022,7 @@ func Test_UploadStickerFile_ContextCanceled(t *testing.T) {
|
||||
params := &UploadStickerFileParams{
|
||||
UserID: 42,
|
||||
Sticker: &InputFile{PathOrID: "file_id_test"},
|
||||
StickerFormat: "test_value",
|
||||
StickerFormat: InputStickerFormatStatic,
|
||||
}
|
||||
_, err := UploadStickerFile(ctx, bot, params)
|
||||
require.Error(t, err)
|
||||
@@ -21063,7 +21063,7 @@ func Test_UploadStickerFile_Forbidden(t *testing.T) {
|
||||
params := &UploadStickerFileParams{
|
||||
UserID: 42,
|
||||
Sticker: &InputFile{PathOrID: "file_id_test"},
|
||||
StickerFormat: "test_value",
|
||||
StickerFormat: InputStickerFormatStatic,
|
||||
}
|
||||
_, err := UploadStickerFile(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
@@ -21085,7 +21085,7 @@ func Test_UploadStickerFile_ServerError(t *testing.T) {
|
||||
params := &UploadStickerFileParams{
|
||||
UserID: 42,
|
||||
Sticker: &InputFile{PathOrID: "file_id_test"},
|
||||
StickerFormat: "test_value",
|
||||
StickerFormat: InputStickerFormatStatic,
|
||||
}
|
||||
_, err := UploadStickerFile(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
@@ -22415,7 +22415,7 @@ func Test_SetStickerSetThumbnail_Success(t *testing.T) {
|
||||
params := &SetStickerSetThumbnailParams{
|
||||
Name: "test_value",
|
||||
UserID: 42,
|
||||
Format: "test_value",
|
||||
Format: InputStickerFormatStatic,
|
||||
}
|
||||
_, err := SetStickerSetThumbnail(context.Background(), bot, params)
|
||||
require.NoError(t, err)
|
||||
@@ -22430,7 +22430,7 @@ func Test_SetStickerSetThumbnail_APIError(t *testing.T) {
|
||||
params := &SetStickerSetThumbnailParams{
|
||||
Name: "test_value",
|
||||
UserID: 42,
|
||||
Format: "test_value",
|
||||
Format: InputStickerFormatStatic,
|
||||
}
|
||||
_, err := SetStickerSetThumbnail(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
@@ -22448,7 +22448,7 @@ func Test_SetStickerSetThumbnail_NetworkError(t *testing.T) {
|
||||
params := &SetStickerSetThumbnailParams{
|
||||
Name: "test_value",
|
||||
UserID: 42,
|
||||
Format: "test_value",
|
||||
Format: InputStickerFormatStatic,
|
||||
}
|
||||
_, err := SetStickerSetThumbnail(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
@@ -22464,7 +22464,7 @@ func Test_SetStickerSetThumbnail_ParseError(t *testing.T) {
|
||||
params := &SetStickerSetThumbnailParams{
|
||||
Name: "test_value",
|
||||
UserID: 42,
|
||||
Format: "test_value",
|
||||
Format: InputStickerFormatStatic,
|
||||
}
|
||||
_, err := SetStickerSetThumbnail(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
@@ -22483,7 +22483,7 @@ func Test_SetStickerSetThumbnail_ContextCanceled(t *testing.T) {
|
||||
params := &SetStickerSetThumbnailParams{
|
||||
Name: "test_value",
|
||||
UserID: 42,
|
||||
Format: "test_value",
|
||||
Format: InputStickerFormatStatic,
|
||||
}
|
||||
_, err := SetStickerSetThumbnail(ctx, bot, params)
|
||||
require.Error(t, err)
|
||||
@@ -22524,7 +22524,7 @@ func Test_SetStickerSetThumbnail_Forbidden(t *testing.T) {
|
||||
params := &SetStickerSetThumbnailParams{
|
||||
Name: "test_value",
|
||||
UserID: 42,
|
||||
Format: "test_value",
|
||||
Format: InputStickerFormatStatic,
|
||||
}
|
||||
_, err := SetStickerSetThumbnail(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
@@ -22546,7 +22546,7 @@ func Test_SetStickerSetThumbnail_ServerError(t *testing.T) {
|
||||
params := &SetStickerSetThumbnailParams{
|
||||
Name: "test_value",
|
||||
UserID: 42,
|
||||
Format: "test_value",
|
||||
Format: InputStickerFormatStatic,
|
||||
}
|
||||
_, err := SetStickerSetThumbnail(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package api
|
||||
|
||||
// Ptr returns a pointer to v. Useful for optional scalar fields where
|
||||
// the wire format must distinguish absent (nil) from an explicit zero
|
||||
// value (e.g. DisableNotification: api.Ptr(false) to override the
|
||||
// chat default).
|
||||
//
|
||||
// For untyped literals, supply the type parameter explicitly:
|
||||
//
|
||||
// Limit: api.Ptr[int64](5)
|
||||
//
|
||||
// For already-typed values, type inference handles it:
|
||||
//
|
||||
// var n int64 = 5
|
||||
// Limit: api.Ptr(n)
|
||||
func Ptr[T any](v T) *T { return &v }
|
||||
@@ -0,0 +1,28 @@
|
||||
package api_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/lukaszraczylo/go-telegram/api"
|
||||
)
|
||||
|
||||
func TestPtr(t *testing.T) {
|
||||
if got := api.Ptr[int64](5); got == nil || *got != 5 {
|
||||
t.Fatalf("Ptr[int64](5) = %v, want *5", got)
|
||||
}
|
||||
if got := api.Ptr(false); got == nil || *got != false {
|
||||
t.Fatalf("Ptr(false) = %v, want *false", got)
|
||||
}
|
||||
if got := api.Ptr("hello"); got == nil || *got != "hello" {
|
||||
t.Fatalf("Ptr(\"hello\") = %v, want *\"hello\"", got)
|
||||
}
|
||||
|
||||
n := int64(42)
|
||||
got := api.Ptr(n)
|
||||
if got == nil || *got != 42 {
|
||||
t.Fatalf("Ptr(n) = %v, want *42", got)
|
||||
}
|
||||
if got == &n {
|
||||
t.Fatalf("Ptr should copy, not alias caller's variable")
|
||||
}
|
||||
}
|
||||
+1824
-240
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,182 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
json "github.com/goccy/go-json"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestUnifiedEnum_ChatMemberStatus_HasAllConstants asserts the unified
|
||||
// enum exists with the full set of variant values and is a typed string.
|
||||
func TestUnifiedEnum_ChatMemberStatus_HasAllConstants(t *testing.T) {
|
||||
require.IsType(t, ChatMemberStatus(""), ChatMemberStatusCreator)
|
||||
|
||||
values := []ChatMemberStatus{
|
||||
ChatMemberStatusCreator,
|
||||
ChatMemberStatusAdministrator,
|
||||
ChatMemberStatusMember,
|
||||
ChatMemberStatusRestricted,
|
||||
ChatMemberStatusLeft,
|
||||
ChatMemberStatusKicked,
|
||||
}
|
||||
wantWire := []string{"creator", "administrator", "member", "restricted", "left", "kicked"}
|
||||
require.Len(t, values, 6)
|
||||
for i, v := range values {
|
||||
require.Equal(t, wantWire[i], string(v))
|
||||
}
|
||||
}
|
||||
|
||||
// TestUnifiedEnum_ChatMember_VariantFieldsRetyped confirms every concrete
|
||||
// variant's discriminator field is the unified enum, NOT a per-variant
|
||||
// alias type. Reflection walks the struct field directly.
|
||||
func TestUnifiedEnum_ChatMember_VariantFieldsRetyped(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
val any
|
||||
}{
|
||||
{"ChatMemberOwner", &ChatMemberOwner{}},
|
||||
{"ChatMemberAdministrator", &ChatMemberAdministrator{}},
|
||||
{"ChatMemberMember", &ChatMemberMember{}},
|
||||
{"ChatMemberRestricted", &ChatMemberRestricted{}},
|
||||
{"ChatMemberLeft", &ChatMemberLeft{}},
|
||||
{"ChatMemberBanned", &ChatMemberBanned{}},
|
||||
}
|
||||
wantType := reflect.TypeOf(ChatMemberStatus(""))
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
f, ok := reflect.TypeOf(tc.val).Elem().FieldByName("Status")
|
||||
require.True(t, ok, "%s missing Status field", tc.name)
|
||||
require.Equal(t, wantType, f.Type, "%s.Status type mismatch", tc.name)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestUnifiedEnum_ChatMember_DirectComparison verifies the unified enum
|
||||
// lets callers compare a variant's Status directly against constants
|
||||
// without conversion.
|
||||
func TestUnifiedEnum_ChatMember_DirectComparison(t *testing.T) {
|
||||
owner := &ChatMemberOwner{Status: ChatMemberStatusCreator}
|
||||
require.True(t, owner.Status == ChatMemberStatusCreator)
|
||||
require.False(t, owner.Status == ChatMemberStatusKicked)
|
||||
|
||||
banned := &ChatMemberBanned{Status: ChatMemberStatusKicked}
|
||||
require.True(t, banned.Status == ChatMemberStatusKicked)
|
||||
}
|
||||
|
||||
// TestUnifiedEnum_ChatMember_MarshalDiscriminator verifies the auto-inject
|
||||
// MarshalJSON still emits the right wire discriminator after the enum
|
||||
// retype — no regression from commit 370c9c0.
|
||||
func TestUnifiedEnum_ChatMember_MarshalDiscriminator(t *testing.T) {
|
||||
cases := []struct {
|
||||
val any
|
||||
wantWire string
|
||||
}{
|
||||
{&ChatMemberOwner{User: User{ID: 1, FirstName: "a"}}, "creator"},
|
||||
{&ChatMemberAdministrator{User: User{ID: 2, FirstName: "b"}}, "administrator"},
|
||||
{&ChatMemberMember{User: User{ID: 3, FirstName: "c"}}, "member"},
|
||||
{&ChatMemberRestricted{User: User{ID: 4, FirstName: "d"}}, "restricted"},
|
||||
{&ChatMemberLeft{User: User{ID: 5, FirstName: "e"}}, "left"},
|
||||
{&ChatMemberBanned{User: User{ID: 6, FirstName: "f"}}, "kicked"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
raw, err := json.Marshal(tc.val)
|
||||
require.NoError(t, err)
|
||||
|
||||
var probe struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(raw, &probe))
|
||||
require.Equal(t, tc.wantWire, probe.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// TestUnifiedEnum_ChatMember_RoundTrip confirms a marshal-unmarshal cycle
|
||||
// preserves the unified-enum field value.
|
||||
func TestUnifiedEnum_ChatMember_RoundTrip(t *testing.T) {
|
||||
orig := &ChatMemberOwner{User: User{ID: 99, FirstName: "owner"}}
|
||||
raw, err := json.Marshal(orig)
|
||||
require.NoError(t, err)
|
||||
|
||||
out, err := UnmarshalChatMember(raw)
|
||||
require.NoError(t, err)
|
||||
|
||||
round, ok := out.(*ChatMemberOwner)
|
||||
require.True(t, ok, "expected *ChatMemberOwner, got %T", out)
|
||||
require.Equal(t, ChatMemberStatusCreator, round.Status)
|
||||
require.Equal(t, orig.User.ID, round.User.ID)
|
||||
}
|
||||
|
||||
// TestUnifiedEnum_MessageOriginType verifies a second union also unifies
|
||||
// correctly — guards against a one-off implementation that only handles
|
||||
// ChatMember.
|
||||
func TestUnifiedEnum_MessageOriginType(t *testing.T) {
|
||||
require.IsType(t, MessageOriginType(""), MessageOriginTypeUser)
|
||||
|
||||
values := []MessageOriginType{
|
||||
MessageOriginTypeUser,
|
||||
MessageOriginTypeHiddenUser,
|
||||
MessageOriginTypeChat,
|
||||
MessageOriginTypeChannel,
|
||||
}
|
||||
wantWire := []string{"user", "hidden_user", "chat", "channel"}
|
||||
for i, v := range values {
|
||||
require.Equal(t, wantWire[i], string(v))
|
||||
}
|
||||
|
||||
// Variant fields use the unified type.
|
||||
wantType := reflect.TypeOf(MessageOriginType(""))
|
||||
for _, name := range []string{"MessageOriginUser", "MessageOriginHiddenUser", "MessageOriginChat", "MessageOriginChannel"} {
|
||||
switch name {
|
||||
case "MessageOriginUser":
|
||||
f, ok := reflect.TypeOf(&MessageOriginUser{}).Elem().FieldByName("Type")
|
||||
require.True(t, ok)
|
||||
require.Equal(t, wantType, f.Type)
|
||||
case "MessageOriginHiddenUser":
|
||||
f, ok := reflect.TypeOf(&MessageOriginHiddenUser{}).Elem().FieldByName("Type")
|
||||
require.True(t, ok)
|
||||
require.Equal(t, wantType, f.Type)
|
||||
case "MessageOriginChat":
|
||||
f, ok := reflect.TypeOf(&MessageOriginChat{}).Elem().FieldByName("Type")
|
||||
require.True(t, ok)
|
||||
require.Equal(t, wantType, f.Type)
|
||||
case "MessageOriginChannel":
|
||||
f, ok := reflect.TypeOf(&MessageOriginChannel{}).Elem().FieldByName("Type")
|
||||
require.True(t, ok)
|
||||
require.Equal(t, wantType, f.Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestUnifiedEnum_StutterSuffix_Kind covers the naming-collision rule:
|
||||
// when the union name ends in a discriminator concept noun, the unified
|
||||
// enum is suffixed with "Kind" to avoid stuttery names like
|
||||
// "BackgroundTypeType".
|
||||
func TestUnifiedEnum_StutterSuffix_Kind(t *testing.T) {
|
||||
require.IsType(t, BackgroundTypeKind(""), BackgroundTypeKindFill)
|
||||
require.IsType(t, ReactionTypeKind(""), ReactionTypeKindEmoji)
|
||||
require.IsType(t, StoryAreaTypeKind(""), StoryAreaTypeKindLocation)
|
||||
require.IsType(t, ChatBoostSourceKind(""), ChatBoostSourceKindPremium)
|
||||
require.IsType(t, RevenueWithdrawalStateKind(""), RevenueWithdrawalStateKindPending)
|
||||
|
||||
// Variant struct field types match the unified enum.
|
||||
wantType := reflect.TypeOf(BackgroundTypeKind(""))
|
||||
f, ok := reflect.TypeOf(&BackgroundTypeFill{}).Elem().FieldByName("Type")
|
||||
require.True(t, ok)
|
||||
require.Equal(t, wantType, f.Type)
|
||||
}
|
||||
|
||||
// TestUnifiedEnum_PerVariantTypesNotEmitted asserts the obsolete
|
||||
// per-variant single-value enum types (e.g. ChatMemberOwnerStatus) are
|
||||
// gone — ensures the codegen doesn't double-emit. We rely on compile-time
|
||||
// behaviour: if any of these names existed, a referencing package would
|
||||
// fail to build. Instead we verify the variant struct field type's name
|
||||
// is the unified one.
|
||||
func TestUnifiedEnum_PerVariantTypesNotEmitted(t *testing.T) {
|
||||
got := reflect.TypeOf(&ChatMemberOwner{}).Elem()
|
||||
statusField, ok := got.FieldByName("Status")
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "ChatMemberStatus", statusField.Type.Name(),
|
||||
"ChatMemberOwner.Status should be ChatMemberStatus, not ChatMemberOwnerStatus")
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/lukaszraczylo/go-telegram/client"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestSetMyCommands_BotCommandScope_NoPointerToInterface is a regression
|
||||
// test for the bug where sealed-interface union types without an
|
||||
// auto-decode discriminator (BotCommandScope, InputMedia, etc.) were
|
||||
// emitted as `*<Union>` (pointer-to-interface) when used as optional
|
||||
// fields. Pointer-to-interface is a Go anti-pattern: the interface is
|
||||
// already nil-able, and callers were forced to write
|
||||
// `Scope: &someConcreteScope` instead of `Scope: someConcreteScope`.
|
||||
//
|
||||
// This test confirms the field is now bare-interface-typed: a concrete
|
||||
// variant assigns directly, and a nil scope omits the field via
|
||||
// omitempty.
|
||||
func TestSetMyCommands_BotCommandScope_NoPointerToInterface(t *testing.T) {
|
||||
var captured string
|
||||
m := &mockDoer{}
|
||||
m.On("Do", mock.MatchedBy(func(r *http.Request) bool {
|
||||
if r.Body != nil {
|
||||
b, _ := io.ReadAll(r.Body)
|
||||
captured = string(b)
|
||||
}
|
||||
return true
|
||||
})).Return(&http.Response{
|
||||
StatusCode: 200,
|
||||
Body: io.NopCloser(strings.NewReader(`{"ok":true,"result":true}`)),
|
||||
Header: http.Header{"Content-Type": []string{"application/json"}},
|
||||
}, nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
|
||||
// Direct assignment of a concrete variant — only possible when Scope
|
||||
// is `BotCommandScope` (interface), not `*BotCommandScope`.
|
||||
ok, err := SetMyCommands(context.Background(), bot, &SetMyCommandsParams{
|
||||
Commands: []BotCommand{{Command: "start", Description: "begin"}},
|
||||
Scope: &BotCommandScopeAllPrivateChats{},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.True(t, ok)
|
||||
require.Contains(t, captured, `"scope":{"type":"all_private_chats"}`)
|
||||
}
|
||||
|
||||
// TestSetMyCommands_NilScope_OmitsField confirms omitempty works on the
|
||||
// bare-interface field when the caller doesn't supply a scope.
|
||||
func TestSetMyCommands_NilScope_OmitsField(t *testing.T) {
|
||||
var captured string
|
||||
m := &mockDoer{}
|
||||
m.On("Do", mock.MatchedBy(func(r *http.Request) bool {
|
||||
if r.Body != nil {
|
||||
b, _ := io.ReadAll(r.Body)
|
||||
captured = string(b)
|
||||
}
|
||||
return true
|
||||
})).Return(&http.Response{
|
||||
StatusCode: 200,
|
||||
Body: io.NopCloser(strings.NewReader(`{"ok":true,"result":true}`)),
|
||||
Header: http.Header{"Content-Type": []string{"application/json"}},
|
||||
}, nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
_, err := SetMyCommands(context.Background(), bot, &SetMyCommandsParams{
|
||||
Commands: []BotCommand{{Command: "start", Description: "begin"}},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotContains(t, captured, `"scope"`, "nil scope must be omitted from JSON")
|
||||
}
|
||||
+187
-19
@@ -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)}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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)))
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
+321
-27
@@ -8,12 +8,22 @@ import (
|
||||
"go/format"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"text/template"
|
||||
|
||||
"github.com/lukaszraczylo/go-telegram/internal/spec"
|
||||
)
|
||||
|
||||
// Discriminator-value extractors. The curly form ("always “X”") is
|
||||
// authoritative because Telegram quotes wire literals with curly quotes
|
||||
// throughout the docs; the bare form ("must be X") is the looser
|
||||
// non-quoted variant used for BotCommandScope, InputMedia, etc.
|
||||
var (
|
||||
discCurlyRE = regexp.MustCompile(`(?:must be|always)\s+“([^”]+)”`)
|
||||
discBareRE = regexp.MustCompile(`must be\s+([A-Za-z0-9_]+)(?:[\s.,]|$)`)
|
||||
)
|
||||
|
||||
//go:embed types.tmpl
|
||||
var typesTmpl string
|
||||
|
||||
@@ -36,6 +46,30 @@ var runtimeTypes = map[string]bool{
|
||||
"MessageOrBool": true,
|
||||
}
|
||||
|
||||
// fieldTypeOverrides maps "<TypeOrParamsName>.<FieldName>" → Go type expression.
|
||||
// Used for fields whose values are restricted but whose enum the scraper
|
||||
// can't detect (Telegram's curly-quoted emoji literals are routinely
|
||||
// stripped by the scraper's regex due to byte-boundary issues with
|
||||
// multi-byte sequences). The hand-curated typed-string enum lives in
|
||||
// api/enums.go (manual file); this override just retypes the field so
|
||||
// callers get IDE completion and compile-time checks. Generated fields
|
||||
// stay typed even after `make regen`.
|
||||
var fieldTypeOverrides = map[string]string{
|
||||
"ReactionTypeEmoji.Emoji": "ReactionEmoji",
|
||||
"SendDiceParams.Emoji": "DiceEmoji",
|
||||
}
|
||||
|
||||
// fieldTypeOverride returns the override type for a (parent, fieldName)
|
||||
// pair, or "" if none. parent is the Go type name owning the field —
|
||||
// either a struct type (e.g. "ReactionTypeEmoji") or a method-params
|
||||
// type (e.g. "SendDiceParams").
|
||||
func fieldTypeOverride(parent, fieldName string) string {
|
||||
if parent == "" {
|
||||
return ""
|
||||
}
|
||||
return fieldTypeOverrides[parent+"."+fieldName]
|
||||
}
|
||||
|
||||
// discriminatorSpec describes how to decode a sealed-interface union by
|
||||
// peeking at a single JSON field.
|
||||
type discriminatorSpec struct {
|
||||
@@ -164,15 +198,168 @@ var knownDiscriminators = map[string]discriminatorSpec{
|
||||
type emitter struct {
|
||||
api *spec.API
|
||||
outDir string
|
||||
enums *enumPlan
|
||||
// variantDiscs maps a concrete variant type name (e.g.
|
||||
// "BotCommandScopeAllPrivateChats") to its discriminator wire-field
|
||||
// + value. Populated once at construction; consulted by the types
|
||||
// template to emit per-variant MarshalJSON that hardcodes the
|
||||
// discriminator so callers don't have to set it by hand.
|
||||
variantDiscs map[string]variantDiscriminator
|
||||
}
|
||||
|
||||
func newEmitter(api *spec.API, outDir string) *emitter {
|
||||
return &emitter{api: api, outDir: outDir}
|
||||
knownInterfaceTypes = buildUnionTypeSet(api)
|
||||
return &emitter{
|
||||
api: api,
|
||||
outDir: outDir,
|
||||
enums: planEnums(api),
|
||||
variantDiscs: variantDiscriminators(api),
|
||||
}
|
||||
}
|
||||
|
||||
// variantDiscriminator describes the JSON field+value that identifies a
|
||||
// concrete variant of a sealed-interface union on the wire.
|
||||
type variantDiscriminator struct {
|
||||
JSONField string // wire field name, e.g. "type" or "source"
|
||||
GoField string // Go struct field name, e.g. "Type" or "Source"
|
||||
Value string // the wire value, e.g. "all_private_chats"
|
||||
}
|
||||
|
||||
// variantDiscriminators returns variantTypeName → discriminator for every
|
||||
// concrete struct that participates in a sealed-interface union and has
|
||||
// a string-typed first field whose doc fixes its value (the canonical
|
||||
// "must be X" / "always “X”" patterns Telegram uses).
|
||||
//
|
||||
// Resolution order:
|
||||
//
|
||||
// 1. knownDiscriminators reverse-lookup (the 13 auto-decode unions).
|
||||
// This guarantees parity with UnmarshalXxx dispatch for the unions
|
||||
// that round-trip through the library.
|
||||
// 2. Doc-string analysis of the variant's first field, for marker-only
|
||||
// unions (BotCommandScope, InputMedia, etc.) where the IR has no
|
||||
// explicit discriminator metadata.
|
||||
//
|
||||
// Variants whose first field has no discriminator hint (Message,
|
||||
// InaccessibleMessage, the InputMessageContent family) are omitted —
|
||||
// the caller writes the dispatching fields directly and Telegram
|
||||
// identifies them structurally.
|
||||
func variantDiscriminators(api *spec.API) map[string]variantDiscriminator {
|
||||
out := make(map[string]variantDiscriminator, 128)
|
||||
|
||||
// Pass 1: reverse-lookup from knownDiscriminators.
|
||||
for _, ds := range knownDiscriminators {
|
||||
if ds.Field == "" {
|
||||
continue
|
||||
}
|
||||
for value, variant := range ds.Variants {
|
||||
out[variant] = variantDiscriminator{
|
||||
JSONField: ds.Field,
|
||||
Value: value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build the set of every variant type referenced by any OneOf so we
|
||||
// can scan only those (avoids matching free-text "must be" prose in
|
||||
// non-variant types like Message).
|
||||
variantSet := make(map[string]bool, 128)
|
||||
for _, t := range api.Types {
|
||||
for _, v := range t.OneOf {
|
||||
variantSet[v] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2: doc-parse for variants without a known discriminator.
|
||||
for _, t := range api.Types {
|
||||
if !variantSet[t.Name] {
|
||||
continue
|
||||
}
|
||||
if _, ok := out[t.Name]; ok {
|
||||
// Pass-1 already provided the wire value; we still need
|
||||
// the Go field name (mirrors the JSON field but with
|
||||
// proper case). Resolve from t.Fields by JSONName match.
|
||||
disc := out[t.Name]
|
||||
for _, f := range t.Fields {
|
||||
if f.JSONName == disc.JSONField {
|
||||
disc.GoField = f.Name
|
||||
out[t.Name] = disc
|
||||
break
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
disc, ok := extractVariantDiscriminator(t)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
out[t.Name] = disc
|
||||
}
|
||||
|
||||
// Drop entries we couldn't resolve a Go field for (defensive — every
|
||||
// pass-1 hit should have matched, but better to skip than emit
|
||||
// broken code referencing an unknown field name).
|
||||
for name, d := range out {
|
||||
if d.GoField == "" {
|
||||
delete(out, name)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// extractVariantDiscriminator inspects the first field of a variant
|
||||
// struct and returns its discriminator if the field is a required
|
||||
// string whose doc nails the value via "must be X" or "always “X”".
|
||||
// Returns (zero, false) when no clear discriminator is present.
|
||||
func extractVariantDiscriminator(t spec.TypeDecl) (variantDiscriminator, bool) {
|
||||
if len(t.Fields) == 0 {
|
||||
return variantDiscriminator{}, false
|
||||
}
|
||||
f := t.Fields[0]
|
||||
if !f.Required || f.Type.Kind != spec.KindPrimitive || f.Type.Name != "string" {
|
||||
return variantDiscriminator{}, false
|
||||
}
|
||||
value := parseDiscriminatorDoc(f.Doc)
|
||||
if value == "" {
|
||||
return variantDiscriminator{}, false
|
||||
}
|
||||
return variantDiscriminator{
|
||||
JSONField: f.JSONName,
|
||||
GoField: f.Name,
|
||||
Value: value,
|
||||
}, true
|
||||
}
|
||||
|
||||
// parseDiscriminatorDoc extracts the wire-level discriminator value
|
||||
// from a field doc string. Handles both Telegram phrasings:
|
||||
//
|
||||
// - "Scope type, must be all_private_chats" (bare token)
|
||||
// - "Type of the message origin, always “user”" (curly-quoted)
|
||||
//
|
||||
// Returns "" when no discriminator is present.
|
||||
func parseDiscriminatorDoc(doc string) string {
|
||||
// Curly-quoted form takes priority: "must be “X”" or "always “X”".
|
||||
if m := discCurlyRE.FindStringSubmatch(doc); len(m) == 2 {
|
||||
return m[1]
|
||||
}
|
||||
// Bare-token form: "must be <ident>" terminated by end-of-string,
|
||||
// punctuation, or whitespace.
|
||||
if m := discBareRE.FindStringSubmatch(doc); len(m) == 2 {
|
||||
return m[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// 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
|
||||
// unionTypeFor consult this so optional fields of any union type stay
|
||||
// bare interface, never *Interface (which is meaningless in Go and trips
|
||||
// users at every call site).
|
||||
var knownInterfaceTypes = map[string]bool{}
|
||||
|
||||
// emitTypes renders types.gen.go.
|
||||
func (e *emitter) emitTypes() error {
|
||||
t, err := template.New("types").Funcs(funcs()).Parse(typesTmpl)
|
||||
t, err := template.New("types").Funcs(funcsWithDiscs(e.enums, e.variantDiscs)).Parse(typesTmpl)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse types.tmpl: %w", err)
|
||||
}
|
||||
@@ -208,20 +395,55 @@ func loadAPI(path string) (*spec.API, error) {
|
||||
return &api, nil
|
||||
}
|
||||
|
||||
// funcs is the FuncMap shared across templates.
|
||||
func funcs() template.FuncMap {
|
||||
// funcsWithDiscs returns the shared FuncMap with the variant
|
||||
// discriminator helpers bound to discs. 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)
|
||||
fm["variantHasDisc"] = func(name string) bool {
|
||||
_, ok := discs[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 }
|
||||
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 {
|
||||
return template.FuncMap{
|
||||
"goType": goType,
|
||||
"goField": goField,
|
||||
"docComment": docComment,
|
||||
"isOptional": func(f spec.Field) bool { return !f.Required },
|
||||
"not": func(b bool) bool { return !b },
|
||||
"title": title,
|
||||
"isFileField": isFileField,
|
||||
"fileCheck": fileCheck,
|
||||
"multipartFieldEntry": multipartFieldEntry,
|
||||
"multipartFileEntry": multipartFileEntry,
|
||||
"returnGoType": returnGoType,
|
||||
"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)
|
||||
},
|
||||
"docComment": docComment,
|
||||
"isOptional": func(f spec.Field) bool { return !f.Required },
|
||||
"not": func(b bool) bool { return !b },
|
||||
"title": title,
|
||||
"isFileField": isFileField,
|
||||
"fileCheck": fileCheck,
|
||||
"multipartFieldEntry": func(parent string, f spec.Field) string {
|
||||
return multipartFieldEntry(plan, parent, f)
|
||||
},
|
||||
"multipartFieldEntryP": func(methodName string, f spec.Field) string {
|
||||
return multipartFieldEntryX(plan, "", title(methodName)+"Params", f)
|
||||
},
|
||||
"multipartFileEntry": multipartFileEntry,
|
||||
"returnGoType": returnGoType,
|
||||
// enum helpers
|
||||
"enums": func() []enumDecl {
|
||||
if plan == nil {
|
||||
return nil
|
||||
}
|
||||
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 },
|
||||
"isSealedUnionReturn": func(tr spec.TypeRef) bool {
|
||||
@@ -231,6 +453,19 @@ func funcs() template.FuncMap {
|
||||
s, ok := knownDiscriminators[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]
|
||||
return ok && len(s.Variants) > 0
|
||||
},
|
||||
"sealedUnionElemName": func(tr spec.TypeRef) string {
|
||||
if tr.Kind == spec.KindArray && tr.ElemType != nil {
|
||||
return tr.ElemType.Name
|
||||
}
|
||||
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 },
|
||||
@@ -295,8 +530,20 @@ func multipartFileEntry(f spec.Field) string {
|
||||
|
||||
// multipartFieldEntry generates the line that adds f to the multipart map.
|
||||
// Required scalar fields go in unconditionally; optional ones go in only
|
||||
// when non-zero/non-empty.
|
||||
func multipartFieldEntry(f spec.Field) string {
|
||||
// when non-zero/non-empty. Typed-string enum fields are cast to string
|
||||
// before assignment because the multipart map is map[string]string.
|
||||
func multipartFieldEntry(plan *enumPlan, parent string, f spec.Field) string {
|
||||
return multipartFieldEntryX(plan, parent, parent, f)
|
||||
}
|
||||
|
||||
// multipartFieldEntryX mirrors goFieldX: enumParent keys the enum plan,
|
||||
// overrideParent keys fieldTypeOverrides. They differ only for method
|
||||
// params.
|
||||
func multipartFieldEntryX(plan *enumPlan, enumParent, overrideParent string, f spec.Field) string {
|
||||
enumName := plan.FieldEnum(enumParent, f.Name)
|
||||
if enumName == "" {
|
||||
enumName = fieldTypeOverride(overrideParent, f.Name)
|
||||
}
|
||||
switch f.Type.Kind {
|
||||
case spec.KindPrimitive:
|
||||
switch f.Type.Name {
|
||||
@@ -306,6 +553,12 @@ func multipartFieldEntry(f spec.Field) string {
|
||||
}
|
||||
return fmt.Sprintf("\tif p.%s != nil { out[%q] = strconv.FormatInt(*p.%s, 10) }\n", f.Name, f.JSONName, f.Name)
|
||||
case "string":
|
||||
if enumName != "" {
|
||||
if f.Required {
|
||||
return fmt.Sprintf("\tout[%q] = string(p.%s)\n", f.JSONName, f.Name)
|
||||
}
|
||||
return fmt.Sprintf("\tif p.%s != \"\" { out[%q] = string(p.%s) }\n", f.Name, f.JSONName, f.Name)
|
||||
}
|
||||
if f.Required {
|
||||
return fmt.Sprintf("\tout[%q] = p.%s\n", f.JSONName, f.Name)
|
||||
}
|
||||
@@ -392,7 +645,7 @@ func returnGoElem(tr spec.TypeRef) string {
|
||||
|
||||
// emitMethods renders methods.gen.go.
|
||||
func (e *emitter) emitMethods() error {
|
||||
t, err := template.New("methods").Funcs(funcs()).Parse(methodsTmpl)
|
||||
t, err := template.New("methods").Funcs(funcs(e.enums)).Parse(methodsTmpl)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse methods.tmpl: %w", err)
|
||||
}
|
||||
@@ -409,7 +662,7 @@ func (e *emitter) emitMethods() error {
|
||||
|
||||
// emitEnums renders enums.gen.go.
|
||||
func (e *emitter) emitEnums() error {
|
||||
t, err := template.New("enums").Funcs(funcs()).Parse(enumsTmpl)
|
||||
t, err := template.New("enums").Funcs(funcs(e.enums)).Parse(enumsTmpl)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse enums.tmpl: %w", err)
|
||||
}
|
||||
@@ -442,7 +695,7 @@ func goType(tr spec.TypeRef, optional bool) string {
|
||||
// multipart helpers (fileCheck, multipartFileEntry) call
|
||||
// f.IsLocalUpload() and dereference Reader, both of which
|
||||
// expect a pointer receiver.
|
||||
if _, isUnion := knownDiscriminators[tr.Name]; isUnion {
|
||||
if knownInterfaceTypes[tr.Name] {
|
||||
// Interface type — never add *.
|
||||
return tr.Name
|
||||
}
|
||||
@@ -558,8 +811,44 @@ func matchesVariants(got []string, want ...string) bool {
|
||||
}
|
||||
|
||||
// goField returns the Go struct-field declaration for a Field.
|
||||
func goField(f spec.Field) string {
|
||||
// When the field carries scraper-detected enum values and the emitter
|
||||
// has a planned enum name for (parent, field), the field's Go type is
|
||||
// the enum identifier. Typed-string enums use the zero string ""
|
||||
// behaviour for omitempty, so we do not pointer-wrap optional enum
|
||||
// fields. Parent is "" for method parameters; pass the params type
|
||||
// name (e.g. "SendDiceParams") via overrideParent when calling from
|
||||
// the methods template so fieldTypeOverrides can resolve.
|
||||
func goField(plan *enumPlan, parent string, f spec.Field) string {
|
||||
return goFieldX(plan, parent, parent, f)
|
||||
}
|
||||
|
||||
// goFieldX is the underlying field-emitter. enumParent is used for the
|
||||
// enum-plan lookup (which keys method params under ""); overrideParent
|
||||
// is used for fieldTypeOverrides (which keys method params under the
|
||||
// params type name). For struct types both are the same; for method
|
||||
// params they differ.
|
||||
func goFieldX(plan *enumPlan, enumParent, overrideParent string, f spec.Field) string {
|
||||
tag := fmt.Sprintf("`json:%q`", f.JSONName+omitempty(f))
|
||||
if name := plan.FieldEnum(enumParent, f.Name); name != "" {
|
||||
return fmt.Sprintf("%s %s %s", f.Name, name, tag)
|
||||
}
|
||||
if name := fieldTypeOverride(overrideParent, f.Name); name != "" {
|
||||
return fmt.Sprintf("%s %s %s", f.Name, name, tag)
|
||||
}
|
||||
// Pinned companion-enum retype: allowed_updates is an Array of String
|
||||
// in the upstream spec, but the Go API exposes a hand-curated
|
||||
// UpdateType (api/enums.go) since the values are not enumerated
|
||||
// inline by Telegram. Retype []string → []UpdateType wherever the
|
||||
// wire field is allowed_updates so callers can pass typed constants
|
||||
// (api.UpdateMessage, ...) without string casts. Wire format is
|
||||
// unchanged: UpdateType is a typed string, marshals identically.
|
||||
if f.JSONName == "allowed_updates" &&
|
||||
f.Type.Kind == spec.KindArray &&
|
||||
f.Type.ElemType != nil &&
|
||||
f.Type.ElemType.Kind == spec.KindPrimitive &&
|
||||
f.Type.ElemType.Name == "string" {
|
||||
return fmt.Sprintf("%s []UpdateType %s", f.Name, tag)
|
||||
}
|
||||
return fmt.Sprintf("%s %s %s", f.Name, goType(f.Type, !f.Required), tag)
|
||||
}
|
||||
|
||||
@@ -623,14 +912,19 @@ func buildUnionTypeSet(api *spec.API) map[string]bool {
|
||||
|
||||
// makeSentinelValue returns a sentinelValue func that uses the given union type set.
|
||||
// It returns a minimal valid Go expression for a spec.Field's type,
|
||||
// used in generated test param literals.
|
||||
func makeSentinelValue(unionTypes map[string]bool) func(spec.Field) string {
|
||||
// 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)
|
||||
return sentinelForField(f, unionTypes, plan)
|
||||
}
|
||||
}
|
||||
|
||||
func sentinelForField(f spec.Field, unionTypes map[string]bool) string {
|
||||
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])
|
||||
}
|
||||
tr := f.Type
|
||||
switch tr.Kind {
|
||||
case spec.KindPrimitive:
|
||||
@@ -729,8 +1023,8 @@ func (e *emitter) emitTests() error {
|
||||
unionTypes := buildUnionTypeSet(e.api)
|
||||
|
||||
// Add test-specific helpers to the shared func map.
|
||||
fm := funcs()
|
||||
fm["sentinelValue"] = makeSentinelValue(unionTypes)
|
||||
fm := funcs(e.enums)
|
||||
fm["sentinelValue"] = makeSentinelValue(unionTypes, e.enums)
|
||||
fm["successResp"] = successResp
|
||||
|
||||
t, err := template.New("tests").Funcs(fm).Parse(testsTmpl)
|
||||
|
||||
@@ -0,0 +1,483 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/lukaszraczylo/go-telegram/internal/spec"
|
||||
)
|
||||
|
||||
// enumDecl is one generated enum: a Go type alias of string plus a set
|
||||
// of named constants. Values keep doc order; constant identifiers are
|
||||
// derived from values via constName.
|
||||
type enumDecl struct {
|
||||
Name string
|
||||
Values []string
|
||||
}
|
||||
|
||||
// enumPlan is the deduplicated, name-resolved set of enums emitted from
|
||||
// an API IR. Lookup returns the enum name for a given field reference;
|
||||
// All returns the deterministic-ordered list of declarations to emit.
|
||||
type enumPlan struct {
|
||||
// fieldKey -> enum name. The fieldKey is a string built by enumKey.
|
||||
byField map[string]string
|
||||
// enum name -> declaration.
|
||||
decls map[string]enumDecl
|
||||
}
|
||||
|
||||
// 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).
|
||||
func enumKey(parent, fieldName string) string { return parent + "::" + fieldName }
|
||||
|
||||
// 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
|
||||
fieldName string
|
||||
jsonName string
|
||||
values []string
|
||||
valueKey string // canonical key for value-set dedup
|
||||
}
|
||||
|
||||
// Unification pass: for each sealed-interface union, fold per-variant
|
||||
// single-value enum fields that share a discriminator name into ONE
|
||||
// unified enum at union level. Claimed (parent,fieldName) tuples are
|
||||
// excluded from the per-field grouping below.
|
||||
unifiedDecls, unifiedByField := planUnifiedUnionEnums(api)
|
||||
claimed := func(parent, fieldName string) bool {
|
||||
_, ok := unifiedByField[enumKey(parent, fieldName)]
|
||||
return ok
|
||||
}
|
||||
|
||||
var refs []ref
|
||||
collect := func(parent string, fields []spec.Field) {
|
||||
for _, f := range fields {
|
||||
if len(f.EnumValues) == 0 {
|
||||
continue
|
||||
}
|
||||
if claimed(parent, f.Name) {
|
||||
continue
|
||||
}
|
||||
refs = append(refs, ref{
|
||||
parent: parent,
|
||||
fieldName: f.Name,
|
||||
jsonName: f.JSONName,
|
||||
values: f.EnumValues,
|
||||
valueKey: valueKey(f.EnumValues),
|
||||
})
|
||||
}
|
||||
}
|
||||
for _, t := range api.Types {
|
||||
collect(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)
|
||||
}
|
||||
|
||||
// candidate name per ref (before collision resolution)
|
||||
candidate := make([]string, len(refs))
|
||||
for i, r := range refs {
|
||||
candidate[i] = defaultEnumName(r.parent, r.jsonName, r.fieldName)
|
||||
}
|
||||
|
||||
// Group by valueKey to coalesce identical value-sets across fields.
|
||||
// Choose canonical name: prefer the most common candidate; tie-break
|
||||
// by shortest name; final tie-break alphabetical.
|
||||
type groupInfo struct {
|
||||
values []string
|
||||
name string
|
||||
first int
|
||||
}
|
||||
groups := map[string]*groupInfo{}
|
||||
for i, r := range refs {
|
||||
g, ok := groups[r.valueKey]
|
||||
if !ok {
|
||||
groups[r.valueKey] = &groupInfo{values: r.values, first: i}
|
||||
g = groups[r.valueKey]
|
||||
}
|
||||
_ = g
|
||||
}
|
||||
// Rank candidate names per group.
|
||||
for vk := range groups {
|
||||
counts := map[string]int{}
|
||||
hasParent := map[string]bool{}
|
||||
var names []string
|
||||
for i, r := range refs {
|
||||
if r.valueKey != vk {
|
||||
continue
|
||||
}
|
||||
n := candidate[i]
|
||||
if _, ok := counts[n]; !ok {
|
||||
names = append(names, n)
|
||||
}
|
||||
counts[n]++
|
||||
if r.parent != "" {
|
||||
hasParent[n] = true
|
||||
}
|
||||
}
|
||||
// Pick the canonical name for this group:
|
||||
// 1. highest occurrence count wins;
|
||||
// 2. names that originated from a parent type win over plain
|
||||
// method-param candidates (avoids "Format"-style
|
||||
// monosyllables);
|
||||
// 3. shortest name wins;
|
||||
// 4. alphabetical for full determinism.
|
||||
sort.SliceStable(names, func(a, b int) bool {
|
||||
if counts[names[a]] != counts[names[b]] {
|
||||
return counts[names[a]] > counts[names[b]]
|
||||
}
|
||||
if hasParent[names[a]] != hasParent[names[b]] {
|
||||
return hasParent[names[a]]
|
||||
}
|
||||
if len(names[a]) != len(names[b]) {
|
||||
return len(names[a]) < len(names[b])
|
||||
}
|
||||
return names[a] < names[b]
|
||||
})
|
||||
groups[vk].name = names[0]
|
||||
}
|
||||
|
||||
// Collision pass: two groups must not share the same enum name.
|
||||
// When that happens, suffix the loser(s) with their parent type
|
||||
// name so the result is unique. Iterate in deterministic order
|
||||
// (groups sorted by valueKey).
|
||||
used := map[string]string{} // name -> valueKey owner
|
||||
var keys []string
|
||||
for vk := range groups {
|
||||
keys = append(keys, vk)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, vk := range keys {
|
||||
g := groups[vk]
|
||||
if _, taken := used[g.name]; !taken {
|
||||
used[g.name] = vk
|
||||
continue
|
||||
}
|
||||
// Find a unique name by prepending a parent prefix from one of
|
||||
// the contributing refs (the lowest-index ref in this group).
|
||||
for i, r := range refs {
|
||||
if r.valueKey != vk {
|
||||
continue
|
||||
}
|
||||
if r.parent == "" {
|
||||
continue
|
||||
}
|
||||
cand := r.parent + goNamePart(r.jsonName)
|
||||
if _, taken := used[cand]; !taken {
|
||||
g.name = cand
|
||||
used[cand] = vk
|
||||
goto next
|
||||
}
|
||||
_ = i
|
||||
}
|
||||
// Fallback: append a numeric disambiguator. Should not happen
|
||||
// in practice for the Telegram docs but keeps the algorithm
|
||||
// total.
|
||||
for n := 2; ; n++ {
|
||||
cand := groups[vk].name + itoa(n)
|
||||
if _, taken := used[cand]; !taken {
|
||||
g.name = cand
|
||||
used[cand] = vk
|
||||
break
|
||||
}
|
||||
}
|
||||
next:
|
||||
}
|
||||
|
||||
// Build the plan.
|
||||
plan := &enumPlan{
|
||||
byField: map[string]string{},
|
||||
decls: map[string]enumDecl{},
|
||||
}
|
||||
for i, r := range refs {
|
||||
name := groups[r.valueKey].name
|
||||
plan.byField[enumKey(r.parent, r.fieldName)] = name
|
||||
_ = i
|
||||
}
|
||||
for vk, g := range groups {
|
||||
plan.decls[g.name] = enumDecl{Name: g.name, Values: g.values}
|
||||
_ = vk
|
||||
}
|
||||
// Merge unified union enums (already named with stutter handling and
|
||||
// keyed per-variant in unifiedByField).
|
||||
for k, name := range unifiedByField {
|
||||
plan.byField[k] = name
|
||||
}
|
||||
for name, d := range unifiedDecls {
|
||||
plan.decls[name] = d
|
||||
}
|
||||
return plan
|
||||
}
|
||||
|
||||
// planUnifiedUnionEnums detects sealed-interface unions whose variants
|
||||
// share a single discriminator field with one enum value each, and emits
|
||||
// ONE unified enum per union covering all variant values. Returns the
|
||||
// declarations to emit and the per-(variant,fieldName) map to point each
|
||||
// variant's field at the unified enum.
|
||||
//
|
||||
// A union qualifies when EVERY variant in t.OneOf:
|
||||
// 1. defines a field with the same Go-name (e.g. "Status", "Type", "Source");
|
||||
// 2. that field is a required string with len(EnumValues)==1.
|
||||
//
|
||||
// The picked Go-name is the first one tried in this priority order:
|
||||
// - knownDiscriminators[union].Field's Go-name (resolved via JSONName match);
|
||||
// - "Type", "Status", "Source" (the three discriminators Telegram uses).
|
||||
//
|
||||
// First match wins; if none qualify, the union is skipped (variants keep
|
||||
// their existing per-field treatment, which still single-emits via the
|
||||
// regular grouping pass).
|
||||
func planUnifiedUnionEnums(api *spec.API) (map[string]enumDecl, map[string]string) {
|
||||
decls := map[string]enumDecl{}
|
||||
byField := map[string]string{}
|
||||
|
||||
typeByName := make(map[string]*spec.TypeDecl, len(api.Types))
|
||||
for i := range api.Types {
|
||||
typeByName[api.Types[i].Name] = &api.Types[i]
|
||||
}
|
||||
|
||||
// Iterate unions in deterministic (declaration) order.
|
||||
for ui := range api.Types {
|
||||
u := &api.Types[ui]
|
||||
if len(u.OneOf) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Resolve the variants. Skip unions where any variant is missing
|
||||
// (defensive — shouldn't happen in a well-formed IR).
|
||||
variants := make([]*spec.TypeDecl, 0, len(u.OneOf))
|
||||
for _, vName := range u.OneOf {
|
||||
v, ok := typeByName[vName]
|
||||
if !ok {
|
||||
variants = nil
|
||||
break
|
||||
}
|
||||
variants = append(variants, v)
|
||||
}
|
||||
if len(variants) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Build the candidate Go-name list. Priority order:
|
||||
// 1. discriminator GoField from knownDiscriminators (resolved via JSONName);
|
||||
// 2. "Type", "Status", "Source".
|
||||
var candidateNames []string
|
||||
seen := map[string]bool{}
|
||||
add := func(name string) {
|
||||
if name == "" || seen[name] {
|
||||
return
|
||||
}
|
||||
seen[name] = true
|
||||
candidateNames = append(candidateNames, name)
|
||||
}
|
||||
if ds, ok := knownDiscriminators[u.Name]; ok && ds.Field != "" {
|
||||
// Resolve Go-name from the first variant whose field matches the JSON name.
|
||||
for _, v := range variants {
|
||||
for _, f := range v.Fields {
|
||||
if f.JSONName == ds.Field {
|
||||
add(f.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, n := range []string{"Type", "Status", "Source"} {
|
||||
add(n)
|
||||
}
|
||||
|
||||
// Find the first candidate Go-name where every variant has a
|
||||
// matching single-value string-enum field.
|
||||
var (
|
||||
pickedName string
|
||||
pickedDocs map[string]spec.Field // variant name -> field
|
||||
)
|
||||
for _, name := range candidateNames {
|
||||
matches := map[string]spec.Field{}
|
||||
ok := true
|
||||
for _, v := range variants {
|
||||
var hit *spec.Field
|
||||
for fi := range v.Fields {
|
||||
if v.Fields[fi].Name == name {
|
||||
hit = &v.Fields[fi]
|
||||
break
|
||||
}
|
||||
}
|
||||
if hit == nil ||
|
||||
hit.Type.Kind != spec.KindPrimitive ||
|
||||
hit.Type.Name != "string" ||
|
||||
len(hit.EnumValues) != 1 {
|
||||
ok = false
|
||||
break
|
||||
}
|
||||
matches[v.Name] = *hit
|
||||
}
|
||||
if ok {
|
||||
pickedName = name
|
||||
pickedDocs = matches
|
||||
break
|
||||
}
|
||||
}
|
||||
if pickedName == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Build the unified enum name with stutter handling.
|
||||
enumName := unifiedEnumName(u.Name, pickedName)
|
||||
|
||||
// Collect values across variants in deterministic order, deduping.
|
||||
valueOrder := make([]string, 0, len(variants))
|
||||
valueSeen := map[string]bool{}
|
||||
for _, v := range u.OneOf {
|
||||
f := pickedDocs[v]
|
||||
val := f.EnumValues[0]
|
||||
if valueSeen[val] {
|
||||
continue
|
||||
}
|
||||
valueSeen[val] = true
|
||||
valueOrder = append(valueOrder, val)
|
||||
}
|
||||
|
||||
decls[enumName] = enumDecl{Name: enumName, Values: valueOrder}
|
||||
for _, v := range variants {
|
||||
byField[enumKey(v.Name, pickedName)] = enumName
|
||||
}
|
||||
}
|
||||
|
||||
return decls, byField
|
||||
}
|
||||
|
||||
// unifiedEnumName builds the union-level enum name. Falls back to a
|
||||
// "Kind" suffix when the naive concatenation reads as a stutter:
|
||||
//
|
||||
// - union name ends in the field name verbatim (e.g. BackgroundType+Type);
|
||||
// - union name ends in any "concept noun" — Type/Status/Source/State —
|
||||
// so appending another such noun would duplicate the suffix
|
||||
// (e.g. ChatBoostSource+Source, RevenueWithdrawalState+Type).
|
||||
//
|
||||
// Otherwise the natural concatenation wins (ChatMember+Status →
|
||||
// ChatMemberStatus, MessageOrigin+Type → MessageOriginType).
|
||||
func unifiedEnumName(unionName, fieldName string) string {
|
||||
for _, suf := range []string{"Type", "Status", "Source", "State"} {
|
||||
if strings.HasSuffix(unionName, suf) {
|
||||
return unionName + "Kind"
|
||||
}
|
||||
}
|
||||
return unionName + fieldName
|
||||
}
|
||||
|
||||
// All returns the enum declarations sorted by name for deterministic emit.
|
||||
func (p *enumPlan) All() []enumDecl {
|
||||
out := make([]enumDecl, 0, len(p.decls))
|
||||
for _, d := range p.decls {
|
||||
out = append(out, d)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
||||
return out
|
||||
}
|
||||
|
||||
// FieldEnum returns the enum name for a field on a given parent type
|
||||
// (use parent="" for method parameters), or "" if the field is not an
|
||||
// enum.
|
||||
func (p *enumPlan) FieldEnum(parent, fieldName string) string {
|
||||
if p == nil {
|
||||
return ""
|
||||
}
|
||||
return p.byField[enumKey(parent, fieldName)]
|
||||
}
|
||||
|
||||
// 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).
|
||||
func defaultEnumName(parent, jsonName, fieldName string) string {
|
||||
if strings.HasSuffix(jsonName, "parse_mode") {
|
||||
return "ParseMode"
|
||||
}
|
||||
return parent + goNamePart(jsonName)
|
||||
}
|
||||
|
||||
// constName builds a Go constant identifier "<EnumName><PascalValue>"
|
||||
// from a wire value. Slashes (mime types) become "Of" so
|
||||
// "image/jpeg" → "ImageOfJpeg".
|
||||
func constName(enumName, value string) string {
|
||||
return enumName + valuePascal(value)
|
||||
}
|
||||
|
||||
func valuePascal(v string) string {
|
||||
// "image/jpeg" → "ImageOfJpeg"
|
||||
parts := strings.Split(v, "/")
|
||||
for i, p := range parts {
|
||||
parts[i] = goNamePart(p)
|
||||
}
|
||||
return strings.Join(parts, "Of")
|
||||
}
|
||||
|
||||
// goNamePart converts a snake_case (or already-PascalCase) token to
|
||||
// PascalCase, mirroring scrape.goName behaviour without the acronym
|
||||
// special-cases (which apply to wire identifiers, not enum values).
|
||||
func goNamePart(s string) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
parts := strings.Split(s, "_")
|
||||
var b strings.Builder
|
||||
for _, p := range parts {
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
// Acronyms used in Telegram wire names. Keeping in sync with
|
||||
// scrape/table.go avoids divergent capitalisation between
|
||||
// fieldName and constName.
|
||||
switch p {
|
||||
case "id":
|
||||
b.WriteString("ID")
|
||||
continue
|
||||
case "url":
|
||||
b.WriteString("URL")
|
||||
continue
|
||||
case "ip":
|
||||
b.WriteString("IP")
|
||||
continue
|
||||
case "https":
|
||||
b.WriteString("HTTPS")
|
||||
continue
|
||||
case "json":
|
||||
b.WriteString("JSON")
|
||||
continue
|
||||
case "html":
|
||||
b.WriteString("HTML")
|
||||
continue
|
||||
}
|
||||
if c := p[0]; c >= 'a' && c <= 'z' {
|
||||
b.WriteByte(c - 'a' + 'A')
|
||||
b.WriteString(p[1:])
|
||||
} else {
|
||||
b.WriteString(p)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func valueKey(values []string) string {
|
||||
cp := make([]string, len(values))
|
||||
copy(cp, values)
|
||||
sort.Strings(cp)
|
||||
return strings.Join(cp, "\x00")
|
||||
}
|
||||
|
||||
func itoa(n int) string {
|
||||
if n == 0 {
|
||||
return "0"
|
||||
}
|
||||
var buf [20]byte
|
||||
i := len(buf)
|
||||
for n > 0 {
|
||||
i--
|
||||
buf[i] = byte('0' + n%10)
|
||||
n /= 10
|
||||
}
|
||||
return string(buf[i:])
|
||||
}
|
||||
+5
-52
@@ -4,57 +4,10 @@
|
||||
|
||||
package api
|
||||
|
||||
// ParseMode controls how Telegram interprets formatting in message text.
|
||||
type ParseMode string
|
||||
{{range $e := enums}}
|
||||
type {{$e.Name}} string
|
||||
|
||||
const (
|
||||
ParseModeMarkdown ParseMode = "Markdown" // legacy
|
||||
ParseModeMarkdownV2 ParseMode = "MarkdownV2"
|
||||
ParseModeHTML ParseMode = "HTML"
|
||||
)
|
||||
|
||||
// ChatType is the type of a Telegram chat.
|
||||
type ChatType string
|
||||
|
||||
const (
|
||||
ChatTypePrivate ChatType = "private"
|
||||
ChatTypeGroup ChatType = "group"
|
||||
ChatTypeSupergroup ChatType = "supergroup"
|
||||
ChatTypeChannel ChatType = "channel"
|
||||
)
|
||||
|
||||
// UpdateType identifies an Update payload variant. Used by allowed_updates
|
||||
// in getUpdates / setWebhook.
|
||||
type UpdateType string
|
||||
|
||||
const (
|
||||
UpdateMessage UpdateType = "message"
|
||||
UpdateEditedMessage UpdateType = "edited_message"
|
||||
UpdateChannelPost UpdateType = "channel_post"
|
||||
UpdateEditedChannelPost UpdateType = "edited_channel_post"
|
||||
UpdateCallbackQuery UpdateType = "callback_query"
|
||||
UpdateInlineQuery UpdateType = "inline_query"
|
||||
)
|
||||
|
||||
// MessageEntityType is the kind of an entity (mention, hashtag, command, ...).
|
||||
type MessageEntityType string
|
||||
|
||||
const (
|
||||
EntityMention MessageEntityType = "mention"
|
||||
EntityHashtag MessageEntityType = "hashtag"
|
||||
EntityCashtag MessageEntityType = "cashtag"
|
||||
EntityBotCommand MessageEntityType = "bot_command"
|
||||
EntityURL MessageEntityType = "url"
|
||||
EntityEmail MessageEntityType = "email"
|
||||
EntityPhoneNumber MessageEntityType = "phone_number"
|
||||
EntityBold MessageEntityType = "bold"
|
||||
EntityItalic MessageEntityType = "italic"
|
||||
EntityUnderline MessageEntityType = "underline"
|
||||
EntityStrike MessageEntityType = "strikethrough"
|
||||
EntitySpoiler MessageEntityType = "spoiler"
|
||||
EntityCode MessageEntityType = "code"
|
||||
EntityPre MessageEntityType = "pre"
|
||||
EntityTextLink MessageEntityType = "text_link"
|
||||
EntityTextMention MessageEntityType = "text_mention"
|
||||
EntityCustomEmoji MessageEntityType = "custom_emoji"
|
||||
)
|
||||
{{range $v := $e.Values}} {{enumConstName $e.Name $v}} {{$e.Name}} = {{printf "%q" $v}}
|
||||
{{end}})
|
||||
{{end}}
|
||||
|
||||
+17
-17
@@ -175,101 +175,101 @@ func makeFieldVariants(name, jname string, kind spec.Kind, variants []string, re
|
||||
|
||||
func TestMultipartFieldEntry_Int64Required(t *testing.T) {
|
||||
f := makeField("ChatID", "chat_id", "int64", spec.KindPrimitive, true)
|
||||
got := multipartFieldEntry(f)
|
||||
got := multipartFieldEntry(nil, "", f)
|
||||
require.Contains(t, got, `FormatInt`)
|
||||
require.NotContains(t, got, "if p.")
|
||||
}
|
||||
|
||||
func TestMultipartFieldEntry_Int64Optional(t *testing.T) {
|
||||
f := makeField("MessageThreadID", "message_thread_id", "int64", spec.KindPrimitive, false)
|
||||
got := multipartFieldEntry(f)
|
||||
got := multipartFieldEntry(nil, "", f)
|
||||
require.Contains(t, got, `FormatInt`)
|
||||
require.Contains(t, got, "if p.")
|
||||
}
|
||||
|
||||
func TestMultipartFieldEntry_StringRequired(t *testing.T) {
|
||||
f := makeField("Text", "text", "string", spec.KindPrimitive, true)
|
||||
got := multipartFieldEntry(f)
|
||||
got := multipartFieldEntry(nil, "", f)
|
||||
require.Contains(t, got, `out["text"]`)
|
||||
require.NotContains(t, got, "if p.Text")
|
||||
}
|
||||
|
||||
func TestMultipartFieldEntry_StringOptional(t *testing.T) {
|
||||
f := makeField("ParseMode", "parse_mode", "string", spec.KindPrimitive, false)
|
||||
got := multipartFieldEntry(f)
|
||||
got := multipartFieldEntry(nil, "", f)
|
||||
require.Contains(t, got, `if p.ParseMode`)
|
||||
}
|
||||
|
||||
func TestMultipartFieldEntry_BoolRequired(t *testing.T) {
|
||||
f := makeField("DisableNotification", "disable_notification", "bool", spec.KindPrimitive, true)
|
||||
got := multipartFieldEntry(f)
|
||||
got := multipartFieldEntry(nil, "", f)
|
||||
require.Contains(t, got, `FormatBool`)
|
||||
require.NotContains(t, got, "if p.")
|
||||
}
|
||||
|
||||
func TestMultipartFieldEntry_BoolOptional(t *testing.T) {
|
||||
f := makeField("Protected", "protect_content", "bool", spec.KindPrimitive, false)
|
||||
got := multipartFieldEntry(f)
|
||||
got := multipartFieldEntry(nil, "", f)
|
||||
require.Contains(t, got, `FormatBool`)
|
||||
require.Contains(t, got, "if p.")
|
||||
}
|
||||
|
||||
func TestMultipartFieldEntry_Float64Required(t *testing.T) {
|
||||
f := makeField("Latitude", "latitude", "float64", spec.KindPrimitive, true)
|
||||
got := multipartFieldEntry(f)
|
||||
got := multipartFieldEntry(nil, "", f)
|
||||
require.Contains(t, got, `FormatFloat`)
|
||||
require.NotContains(t, got, "if p.")
|
||||
}
|
||||
|
||||
func TestMultipartFieldEntry_Float64Optional(t *testing.T) {
|
||||
f := makeField("Longitude", "longitude", "float64", spec.KindPrimitive, false)
|
||||
got := multipartFieldEntry(f)
|
||||
got := multipartFieldEntry(nil, "", f)
|
||||
require.Contains(t, got, `FormatFloat`)
|
||||
require.Contains(t, got, "if p.")
|
||||
}
|
||||
|
||||
func TestMultipartFieldEntry_OneOf_ChatIDRequired(t *testing.T) {
|
||||
f := makeFieldVariants("ChatID", "chat_id", spec.KindOneOf, []string{"int64", "string"}, true)
|
||||
got := multipartFieldEntry(f)
|
||||
got := multipartFieldEntry(nil, "", f)
|
||||
require.Contains(t, got, `.String()`)
|
||||
require.NotContains(t, got, "IsZero")
|
||||
}
|
||||
|
||||
func TestMultipartFieldEntry_OneOf_ChatIDOptional(t *testing.T) {
|
||||
f := makeFieldVariants("ChatID", "chat_id", spec.KindOneOf, []string{"int64", "string"}, false)
|
||||
got := multipartFieldEntry(f)
|
||||
got := multipartFieldEntry(nil, "", f)
|
||||
require.Contains(t, got, `IsZero`)
|
||||
}
|
||||
|
||||
func TestMultipartFieldEntry_OneOf_InputFileOrString(t *testing.T) {
|
||||
f := makeFieldVariants("Photo", "photo", spec.KindOneOf, []string{"InputFile", "string"}, false)
|
||||
got := multipartFieldEntry(f)
|
||||
got := multipartFieldEntry(nil, "", f)
|
||||
require.Contains(t, got, `PathOrID`)
|
||||
}
|
||||
|
||||
func TestMultipartFieldEntry_OneOf_SealedRequired(t *testing.T) {
|
||||
f := makeFieldVariants("Markup", "reply_markup", spec.KindOneOf, []string{"A", "B"}, true)
|
||||
got := multipartFieldEntry(f)
|
||||
got := multipartFieldEntry(nil, "", f)
|
||||
require.Contains(t, got, `json.Marshal`)
|
||||
}
|
||||
|
||||
func TestMultipartFieldEntry_OneOf_SealedOptional(t *testing.T) {
|
||||
f := makeFieldVariants("Markup", "reply_markup", spec.KindOneOf, []string{"A", "B"}, false)
|
||||
got := multipartFieldEntry(f)
|
||||
got := multipartFieldEntry(nil, "", f)
|
||||
require.Contains(t, got, `json.Marshal`)
|
||||
require.Contains(t, got, "if p.Markup")
|
||||
}
|
||||
|
||||
func TestMultipartFieldEntry_Named_Required(t *testing.T) {
|
||||
f := makeField("Entities", "entities", "MessageEntity", spec.KindNamed, true)
|
||||
got := multipartFieldEntry(f)
|
||||
got := multipartFieldEntry(nil, "", f)
|
||||
require.Contains(t, got, `json.Marshal`)
|
||||
require.NotContains(t, got, "if p.")
|
||||
}
|
||||
|
||||
func TestMultipartFieldEntry_Named_Optional(t *testing.T) {
|
||||
f := makeField("Entities", "entities", "MessageEntity", spec.KindNamed, false)
|
||||
got := multipartFieldEntry(f)
|
||||
got := multipartFieldEntry(nil, "", f)
|
||||
require.Contains(t, got, `json.Marshal`)
|
||||
require.Contains(t, got, "if p.")
|
||||
}
|
||||
@@ -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)
|
||||
got := sentinelForField(c.field, unionTypes, nil)
|
||||
require.Contains(t, got, c.contains, "sentinelForField for %q", c.name)
|
||||
})
|
||||
}
|
||||
@@ -637,7 +637,7 @@ func TestUnionTypeFor_OneOfNoMatch(t *testing.T) {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestFuncs_HasExpectedKeys(t *testing.T) {
|
||||
fm := funcs()
|
||||
fm := funcs(nil)
|
||||
require.NotNil(t, fm)
|
||||
for _, key := range []string{"goType", "docComment", "returnGoType", "unionFields"} {
|
||||
require.NotNil(t, fm[key], "funcs() missing key %q", key)
|
||||
|
||||
+21
-3
@@ -15,12 +15,12 @@ import (
|
||||
var _ = strconv.Itoa // keep import for multipart helpers
|
||||
var _ = json.Marshal // keep import for complex multipart fields
|
||||
|
||||
{{range .Methods}}
|
||||
{{range .Methods}}{{$methodName := .Name}}
|
||||
// {{title .Name}}Params is the parameter set for {{title .Name}}.
|
||||
//
|
||||
{{docComment .Doc -}}
|
||||
type {{title .Name}}Params struct {
|
||||
{{range .Params}}{{docComment .Doc}} {{goField .}}
|
||||
{{range .Params}}{{docComment .Doc}} {{goFieldP $methodName .}}
|
||||
{{end}}}
|
||||
{{if .HasFiles}}
|
||||
// HasFile reports whether a multipart upload is required.
|
||||
@@ -31,7 +31,7 @@ func (p *{{title .Name}}Params) HasFile() bool {
|
||||
// MultipartFields returns the non-file fields used in the multipart body.
|
||||
func (p *{{title .Name}}Params) MultipartFields() map[string]string {
|
||||
out := map[string]string{}
|
||||
{{range .Params}}{{if not (isFileField .)}}{{multipartFieldEntry .}}{{end}}{{end}} return out
|
||||
{{range .Params}}{{if not (isFileField .)}}{{multipartFieldEntryP $methodName .}}{{end}}{{end}} return out
|
||||
}
|
||||
|
||||
// MultipartFiles returns the file parts.
|
||||
@@ -51,6 +51,24 @@ func {{title .Name}}(ctx context.Context, b *client.Bot, p *{{title .Name}}Param
|
||||
return nil, err
|
||||
}
|
||||
return Unmarshal{{.Returns.Name}}(raw)
|
||||
{{else if isSealedUnionArrayReturn .Returns -}}
|
||||
raw, err := client.CallRaw[*{{title .Name}}Params](ctx, b, "{{.Name}}", p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var elems []json.RawMessage
|
||||
if err := json.Unmarshal(raw, &elems); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]{{sealedUnionElemName .Returns}}, 0, len(elems))
|
||||
for _, e := range elems {
|
||||
v, err := Unmarshal{{sealedUnionElemName .Returns}}(e)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, v)
|
||||
}
|
||||
return out, nil
|
||||
{{else -}}
|
||||
return client.Call[*{{title .Name}}Params, {{returnGoType .Returns}}](ctx, b, "{{.Name}}", p)
|
||||
{{end -}}
|
||||
|
||||
+18
-1
@@ -82,8 +82,25 @@ func UnmarshalMaybeInaccessibleMessage(data []byte) (MaybeInaccessibleMessage, e
|
||||
{{else}}
|
||||
{{docComment .Doc -}}
|
||||
type {{.Name}} struct {
|
||||
{{range .Fields}}{{docComment .Doc}}{{goField .}}
|
||||
{{range .Fields}}{{docComment .Doc}}{{goField $td.Name .}}
|
||||
{{end}}}
|
||||
{{if variantHasDisc .Name}}
|
||||
// MarshalJSON encodes {{.Name}} with the discriminator field
|
||||
// "{{variantDiscField .Name}}" forced to {{printf "%q" (variantDiscValue .Name)}}.
|
||||
// The hardcoded value frees callers from setting {{variantDiscGoField .Name}} by hand —
|
||||
// any user-supplied value on the struct literal is overridden so a typo
|
||||
// can't slip through to Telegram.
|
||||
func (v *{{.Name}}) MarshalJSON() ([]byte, error) {
|
||||
type alias {{.Name}}
|
||||
return json.Marshal(&struct {
|
||||
{{variantDiscGoField .Name}} string `json:"{{variantDiscField .Name}}"`
|
||||
*alias
|
||||
}{
|
||||
{{variantDiscGoField .Name}}: {{printf "%q" (variantDiscValue .Name)}},
|
||||
alias: (*alias)(v),
|
||||
})
|
||||
}
|
||||
{{end}}
|
||||
{{$unionFields := unionFields .}}{{if $unionFields}}
|
||||
// UnmarshalJSON decodes {{.Name}} by dispatching union-typed fields
|
||||
// ({{range $i, $u := $unionFields}}{{if $i}}, {{end}}{{$u.Field.Name}}{{end}}) through their concrete UnmarshalXxx helpers.
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// extractEnumValues inspects a field-description string and returns the
|
||||
// list of wire-level string values when the description matches one of
|
||||
// the enum-like patterns Telegram uses in its docs. Order follows doc
|
||||
// order; duplicates are removed but order of first occurrence is kept.
|
||||
//
|
||||
// Handled patterns (curly quotes “…” are required to avoid false
|
||||
// positives on free-text quoting):
|
||||
//
|
||||
// - "Type of the chat, can be either “private”, “group”, … or “channel”"
|
||||
// - "Currently, can be “mention”, “hashtag”, …"
|
||||
// - "Currently, one of “XTR” … or “TON” …"
|
||||
// - "Currently, must be one of “XTR” …"
|
||||
// - "Currently, it can be one of “pending”, “approved”, “declined”."
|
||||
// - "Must be one of “danger” …, “success” …"
|
||||
// - "Must be one of “image/jpeg”, “image/gif”, or “video/mp4”"
|
||||
// - "Format … must be one of “static” …, “animated” …, “video” …"
|
||||
// - "Currently, either “upgrade” …, “transfer” …, “resale” …"
|
||||
// - "..., always “creator”"
|
||||
// - parse_mode parameter special case ("Mode for parsing entities …")
|
||||
// emits the canonical Markdown / MarkdownV2 / HTML triple.
|
||||
//
|
||||
// Returns nil when the description does not look like an enum.
|
||||
// extractEnumValues inspects a field-description string and returns the
|
||||
// list of wire-level string values when the description matches one of
|
||||
// the enum-like patterns Telegram uses in its docs. Order follows doc
|
||||
// order; duplicates are removed but order of first occurrence is kept.
|
||||
//
|
||||
// Handled patterns (curly quotes “…” are required to avoid false
|
||||
// positives on free-text quoting):
|
||||
//
|
||||
// - "Type of the chat, can be either “private”, “group”, … or “channel”"
|
||||
// - "Currently, can be “mention”, “hashtag”, …"
|
||||
// - "Currently, one of “XTR” … or “TON” …"
|
||||
// - "Currently, must be one of “XTR” …"
|
||||
// - "Currently, it can be one of “pending”, “approved”, “declined”."
|
||||
// - "Must be one of “danger” …, “success” …"
|
||||
// - "Must be one of “image/jpeg”, “image/gif”, or “video/mp4”"
|
||||
// - "Format … must be one of “static” …, “animated” …, “video” …"
|
||||
// - "Currently, either “upgrade” …, “transfer” …, “resale” …"
|
||||
// - "..., always “creator”"
|
||||
// - parse_mode parameter special case ("Mode for parsing entities …")
|
||||
// emits the canonical Markdown / MarkdownV2 / HTML triple.
|
||||
// - bare prose discriminator at end of description, e.g.
|
||||
// "Type of the result, must be article" or
|
||||
// "Scope type, must be all_private_chats". Used by sealed-interface
|
||||
// union variants whose Type/Source field carries a single literal
|
||||
// value declared without curly quotes.
|
||||
//
|
||||
// Returns nil when the description does not look like an enum.
|
||||
func extractEnumValues(jsonName, desc string) []string {
|
||||
if values := parseModeEnumValues(jsonName, desc); values != nil {
|
||||
return values
|
||||
}
|
||||
|
||||
trigger, triggerEnd, isAlways := findEnumTrigger(desc)
|
||||
if trigger < 0 {
|
||||
return extractProseDiscriminator(desc)
|
||||
}
|
||||
tail := desc[trigger:]
|
||||
|
||||
values := collectQuotedValues(tail)
|
||||
if len(values) == 0 {
|
||||
if v := extractProseDiscriminator(desc); v != nil {
|
||||
return v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// First quoted value must sit close to the trigger phrase (e.g.
|
||||
// "can be “private”…"). Phrasings like "can be available only for
|
||||
// “invoice_payment”…" introduce a referenced value, not an enum,
|
||||
// and the gap between trigger end and first quote rules them out.
|
||||
firstQuote := strings.Index(desc[triggerEnd:], "“")
|
||||
if firstQuote < 0 {
|
||||
return nil
|
||||
}
|
||||
gap := desc[triggerEnd : triggerEnd+firstQuote]
|
||||
// Allow "always " as a permitted bridge (e.g. "Currently, always
|
||||
// “XTR”") and promote the match to single-value form.
|
||||
if strings.Contains(strings.ToLower(gap), "always ") {
|
||||
isAlways = true
|
||||
} else if firstQuote > 8 {
|
||||
return nil
|
||||
}
|
||||
// Single-value matches are only credible after "always". Multi-
|
||||
// value matches are credible after any trigger; the trigger phrase
|
||||
// already constrained the context.
|
||||
if !isAlways && len(values) < 2 {
|
||||
return nil
|
||||
}
|
||||
for _, v := range values {
|
||||
if !looksLikeEnumValue(v) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return dedupeStrings(values)
|
||||
}
|
||||
|
||||
// parseMode parameters do not list values inline — Telegram links to a
|
||||
// separate "formatting options" section. We hardcode the canonical set
|
||||
// here so callers get a typed ParseMode without writing magic strings.
|
||||
func parseModeEnumValues(jsonName, desc string) []string {
|
||||
if !strings.HasSuffix(jsonName, "parse_mode") {
|
||||
return nil
|
||||
}
|
||||
if !strings.Contains(desc, "Mode for parsing entities") {
|
||||
return nil
|
||||
}
|
||||
return []string{"Markdown", "MarkdownV2", "HTML"}
|
||||
}
|
||||
|
||||
// enumTriggers are anchor phrases that introduce a list of valid wire
|
||||
// values. Order matches longest-prefix priority; the matcher uses the
|
||||
// earliest match in the description.
|
||||
var enumTriggers = []string{
|
||||
"can be either ",
|
||||
"can be one of ",
|
||||
"can be ",
|
||||
"must be one of ",
|
||||
"must be ",
|
||||
"currently one of ",
|
||||
"currently, one of ",
|
||||
"currently, either ",
|
||||
"currently, must be one of ",
|
||||
"currently, can be ",
|
||||
"currently, it can be one of ",
|
||||
"currently, ",
|
||||
"one of ",
|
||||
"either ",
|
||||
"always ",
|
||||
}
|
||||
|
||||
// findEnumTrigger returns the byte offset where the first enum trigger
|
||||
// phrase begins, the offset just past the phrase, and whether the
|
||||
// trigger is the single-value "always" form. Returns (-1, -1, false)
|
||||
// when no trigger matches. Matching is case-insensitive so "Currently"
|
||||
// and "currently" both fire.
|
||||
func findEnumTrigger(desc string) (int, int, bool) {
|
||||
lower := strings.ToLower(desc)
|
||||
bestStart := -1
|
||||
bestEnd := -1
|
||||
bestAlways := false
|
||||
for _, t := range enumTriggers {
|
||||
i := strings.Index(lower, t)
|
||||
if i < 0 {
|
||||
continue
|
||||
}
|
||||
if bestStart != -1 && i >= bestStart {
|
||||
// Earlier-trigger wins outright; on a tie, the longer trigger
|
||||
// (which we visit first) already populated bestEnd.
|
||||
continue
|
||||
}
|
||||
bestStart = i
|
||||
bestEnd = i + len(t)
|
||||
bestAlways = t == "always "
|
||||
}
|
||||
return bestStart, bestEnd, bestAlways
|
||||
}
|
||||
|
||||
// quotedRE matches a curly-quoted token: “value”.
|
||||
var quotedRE = regexp.MustCompile(`“([^”]*)”`)
|
||||
|
||||
// collectQuotedValues returns the contents of every “…” pair in s in
|
||||
// order. Multi-line is fine; the docs use single-paragraph cells.
|
||||
func collectQuotedValues(s string) []string {
|
||||
matches := quotedRE.FindAllStringSubmatch(s, -1)
|
||||
out := make([]string, 0, len(matches))
|
||||
for _, m := range matches {
|
||||
out = append(out, m[1])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// looksLikeEnumValue returns true for short identifiers that fit the
|
||||
// shape of a Telegram wire enum. This rules out values like
|
||||
// "attach://…", "h264", arbitrary URLs, and stylised punctuation.
|
||||
//
|
||||
// Permitted shapes:
|
||||
//
|
||||
// a-z0-9_ (e.g. "private", "bot_command")
|
||||
// A-Z0-9_ (e.g. "XTR", "TON", "MarkdownV2")
|
||||
// mixed case incl. "/" once (e.g. "image/jpeg", "video/mp4")
|
||||
func looksLikeEnumValue(v string) bool {
|
||||
if v == "" || len(v) > 64 {
|
||||
return false
|
||||
}
|
||||
if strings.Contains(v, "://") || strings.Contains(v, " ") {
|
||||
return false
|
||||
}
|
||||
// "image/jpeg"-style mime types: at most one slash, both halves alnum.
|
||||
if i := strings.Index(v, "/"); i >= 0 {
|
||||
if strings.Count(v, "/") > 1 {
|
||||
return false
|
||||
}
|
||||
left, right := v[:i], v[i+1:]
|
||||
return isIdent(left) && isIdent(right)
|
||||
}
|
||||
return isIdent(v)
|
||||
}
|
||||
|
||||
func isIdent(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
for _, r := range s {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z':
|
||||
case r >= 'A' && r <= 'Z':
|
||||
case r >= '0' && r <= '9':
|
||||
case r == '_' || r == '-' || r == '.':
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func dedupeStrings(in []string) []string {
|
||||
seen := make(map[string]struct{}, len(in))
|
||||
out := make([]string, 0, len(in))
|
||||
for _, s := range in {
|
||||
if _, ok := seen[s]; ok {
|
||||
continue
|
||||
}
|
||||
seen[s] = struct{}{}
|
||||
out = append(out, s)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// proseDiscRE matches a terminal "must be <ident>" clause: the
|
||||
// discriminator value sits at the END of the description (optionally
|
||||
// followed by trailing punctuation/whitespace) so multi-clause prose
|
||||
// like "must be shown above the message" is not picked up.
|
||||
//
|
||||
// The identifier is a snake_case wire literal: lowercase letters, digits,
|
||||
// and underscores, starting with a letter. Numeric-only and prose words
|
||||
// are filtered separately by isProseWord.
|
||||
var proseDiscRE = regexp.MustCompile(`(?i)\bmust be\s+([a-z][a-z0-9_]*)\s*[.,]?\s*$`)
|
||||
|
||||
// extractProseDiscriminator detects unambiguous single-value
|
||||
// discriminator declarations of the form "..., must be <ident>" used by
|
||||
// sealed-interface union variants (e.g. "Type of the result, must be
|
||||
// article" or "Scope type, must be all_private_chats"). Returns the
|
||||
// extracted value as a one-element slice or nil when no match is found.
|
||||
//
|
||||
// The terminal-position anchor is what protects against prose like
|
||||
// "must be shown above" or "must be one of 3, 6, or 12" — the candidate
|
||||
// must close the description.
|
||||
func extractProseDiscriminator(desc string) []string {
|
||||
desc = strings.TrimSpace(desc)
|
||||
if desc == "" {
|
||||
return nil
|
||||
}
|
||||
m := proseDiscRE.FindStringSubmatch(desc)
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
v := m[1]
|
||||
if isProseWord(v) {
|
||||
return nil
|
||||
}
|
||||
return []string{v}
|
||||
}
|
||||
|
||||
// isProseWord rejects bare-prose continuations that pass the regex but
|
||||
// are clearly English filler ("must be sent", "must be available"). The
|
||||
// list is the closed set of words that empirically appear in the IR's
|
||||
// "must be …" tails outside the variant-discriminator pattern. Wire
|
||||
// identifiers are always single tokens with no English meaning, so any
|
||||
// match here is a free-text false positive.
|
||||
func isProseWord(s string) bool {
|
||||
switch s {
|
||||
case "a", "an", "the",
|
||||
"sent", "shown", "set", "used", "passed", "specified", "available",
|
||||
"applied", "supported", "assumed", "active", "paid", "between",
|
||||
"of", "on", "in", "at", "by", "to", "from", "for", "with",
|
||||
"and", "or", "no", "non",
|
||||
"positive", "negative",
|
||||
"administrator", "repainted",
|
||||
"one", "exactly":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestExtractEnumValues_CanBeEither(t *testing.T) {
|
||||
desc := "Type of the chat, can be either “private”, “group”, “supergroup” or “channel”"
|
||||
got := extractEnumValues("type", desc)
|
||||
require.Equal(t, []string{"private", "group", "supergroup", "channel"}, got)
|
||||
}
|
||||
|
||||
func TestExtractEnumValues_CurrentlyCanBe(t *testing.T) {
|
||||
desc := "Type of the entity. Currently, can be “mention” (@username), “hashtag” (#hashtag), or “code” (monowidth string)"
|
||||
got := extractEnumValues("type", desc)
|
||||
require.Equal(t, []string{"mention", "hashtag", "code"}, got)
|
||||
}
|
||||
|
||||
func TestExtractEnumValues_AlwaysSingle(t *testing.T) {
|
||||
desc := "Type of the message origin, always “user”"
|
||||
got := extractEnumValues("type", desc)
|
||||
require.Equal(t, []string{"user"}, got)
|
||||
}
|
||||
|
||||
func TestExtractEnumValues_MustBeOneOfMime(t *testing.T) {
|
||||
desc := "Optional. MIME type of the thumbnail, must be one of “image/jpeg”, “image/gif”, or “video/mp4”. Defaults to “image/jpeg”"
|
||||
got := extractEnumValues("thumbnail_mime_type", desc)
|
||||
require.Equal(t, []string{"image/jpeg", "image/gif", "video/mp4"}, got)
|
||||
}
|
||||
|
||||
func TestExtractEnumValues_ParseModeSpecial(t *testing.T) {
|
||||
desc := "Optional. Mode for parsing entities in the message text. See formatting options for more details."
|
||||
got := extractEnumValues("parse_mode", desc)
|
||||
require.Equal(t, []string{"Markdown", "MarkdownV2", "HTML"}, got)
|
||||
}
|
||||
|
||||
func TestExtractEnumValues_QuestionParseMode(t *testing.T) {
|
||||
desc := "Mode for parsing entities in the question. See formatting options for more details."
|
||||
got := extractEnumValues("question_parse_mode", desc)
|
||||
require.Equal(t, []string{"Markdown", "MarkdownV2", "HTML"}, got)
|
||||
}
|
||||
|
||||
func TestExtractEnumValues_FalsePositiveReferencedValue(t *testing.T) {
|
||||
// "Can be available only for "X"" is NOT an enum: the quote is a
|
||||
// reference to a transaction-type value, not an introduced list.
|
||||
desc := "Optional. Bot-specified invoice payload. Can be available only for “invoice_payment” transactions."
|
||||
got := extractEnumValues("invoice_payload", desc)
|
||||
require.Nil(t, got)
|
||||
}
|
||||
|
||||
func TestExtractEnumValues_FalsePositiveSingleQuotedNonEnum(t *testing.T) {
|
||||
// "can be ignored" with a quoted reference value later — not an enum.
|
||||
desc := "Optional. Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side."
|
||||
got := extractEnumValues("thumbnail", desc)
|
||||
require.Nil(t, got)
|
||||
}
|
||||
|
||||
func TestExtractEnumValues_RefundedPaymentCurrentlyAlways(t *testing.T) {
|
||||
desc := "Three-letter ISO 4217 currency code, or “XTR” for payments in Telegram Stars. Currently, always “XTR”"
|
||||
got := extractEnumValues("currency", desc)
|
||||
require.Equal(t, []string{"XTR"}, got)
|
||||
}
|
||||
|
||||
func TestExtractEnumValues_RejectURLValues(t *testing.T) {
|
||||
// "attach://<file_attach_name>" must never be promoted to an enum value.
|
||||
desc := "Pass “attach://<file_attach_name>” to upload a new one"
|
||||
got := extractEnumValues("media", desc)
|
||||
require.Nil(t, got)
|
||||
}
|
||||
|
||||
func TestExtractEnumValues_StringTypeOnly(t *testing.T) {
|
||||
// (Sanity — table.go gates on string type, but the function itself
|
||||
// should still respond consistently.)
|
||||
desc := "ABC, can be “a”, “b”"
|
||||
got := extractEnumValues("x", desc)
|
||||
require.Equal(t, []string{"a", "b"}, got)
|
||||
}
|
||||
|
||||
func TestExtractEnumValues_DedupeRepeatedValues(t *testing.T) {
|
||||
desc := "Currently, one of “XTR” for Telegram Stars or “XTR” again"
|
||||
got := extractEnumValues("currency", desc)
|
||||
require.Equal(t, []string{"XTR"}, got)
|
||||
}
|
||||
|
||||
func TestExtractEnumValues_ProseDiscriminator(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
desc string
|
||||
want []string
|
||||
}{
|
||||
{"InlineQueryResultArticle", "Type of the result, must be article", []string{"article"}},
|
||||
{"InlineQueryResultPhoto", "Type of the result, must be photo", []string{"photo"}},
|
||||
{"InlineQueryResultMpeg4Gif", "Type of the result, must be mpeg4_gif", []string{"mpeg4_gif"}},
|
||||
{"BotCommandScopeAllPrivateChats", "Scope type, must be all_private_chats", []string{"all_private_chats"}},
|
||||
{"BotCommandScopeChat", "Scope type, must be chat", []string{"chat"}},
|
||||
{"PassportElementErrorData", "Error source, must be data", []string{"data"}},
|
||||
{"MenuButtonWebApp", "Type of the button, must be web_app", []string{"web_app"}},
|
||||
{"InputProfilePhotoAnimated", "Type of the profile photo, must be animated", []string{"animated"}},
|
||||
{"InputStoryContentVideo", "Type of the content, must be video", []string{"video"}},
|
||||
{"InputPaidMediaPhoto", "Type of the media, must be photo", []string{"photo"}},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require.Equal(t, tc.want, extractEnumValues("type", tc.desc))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractEnumValues_ProseFalsePositives(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
desc string
|
||||
}{
|
||||
{"available_only_for", "Optional. Bot-specified invoice payload. Can be available only for “invoice_payment” transactions."},
|
||||
{"must_be_sent", "If True, the message must be sent immediately."},
|
||||
{"must_be_shown_above", "Optional. True, if the link preview must be shown above the message text"},
|
||||
{"must_be_specified", "The identifiers must be specified in a strictly increasing order."},
|
||||
{"must_be_paid", "The number of Telegram Stars that must be paid to send the sticker"},
|
||||
{"must_be_one_of_numbers", "Number of months the Telegram Premium subscription will be active for the user; must be one of 3, 6, or 12"},
|
||||
{"must_be_between", "Currently, price in Telegram Stars must be between 5 and 100000"},
|
||||
{"must_be_a_pay_button", "If not empty, the first button must be a Pay button."},
|
||||
{"must_be_repainted", "True, if the sticker must be repainted to a text color in messages"},
|
||||
{"must_be_active", "the subscription must be active up to the end of the current subscription period"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require.Nil(t, extractEnumValues("type", tc.desc))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractEnumValues_CanonicalMustBeOneOfStillWorks(t *testing.T) {
|
||||
desc := "Currently, must be one of “Markdown”, “MarkdownV2”, “HTML”"
|
||||
got := extractEnumValues("parse_mode_kind", desc)
|
||||
require.Equal(t, []string{"Markdown", "MarkdownV2", "HTML"}, got)
|
||||
}
|
||||
+22
-10
@@ -30,12 +30,18 @@ func parseFieldsTable(t *html.Node) []spec.Field {
|
||||
desc := strings.TrimSpace(textOf(cells[2]))
|
||||
|
||||
required := !strings.HasPrefix(desc, "Optional.")
|
||||
tref := parseTypeRef(typeText)
|
||||
var enumVals []string
|
||||
if tref.Kind == spec.KindPrimitive && tref.Name == "string" {
|
||||
enumVals = extractEnumValues(jname, desc)
|
||||
}
|
||||
fields = append(fields, spec.Field{
|
||||
Name: goName(jname),
|
||||
JSONName: jname,
|
||||
Type: parseTypeRef(typeText),
|
||||
Required: required,
|
||||
Doc: desc,
|
||||
Name: goName(jname),
|
||||
JSONName: jname,
|
||||
Type: tref,
|
||||
Required: required,
|
||||
Doc: desc,
|
||||
EnumValues: enumVals,
|
||||
})
|
||||
}
|
||||
return fields
|
||||
@@ -59,12 +65,18 @@ func parseParamsTable(t *html.Node) []spec.Field {
|
||||
req := strings.EqualFold(strings.TrimSpace(textOf(cells[2])), "Yes")
|
||||
desc := strings.TrimSpace(textOf(cells[3]))
|
||||
|
||||
tref := parseTypeRef(typeText)
|
||||
var enumVals []string
|
||||
if tref.Kind == spec.KindPrimitive && tref.Name == "string" {
|
||||
enumVals = extractEnumValues(jname, desc)
|
||||
}
|
||||
params = append(params, spec.Field{
|
||||
Name: goName(jname),
|
||||
JSONName: jname,
|
||||
Type: parseTypeRef(typeText),
|
||||
Required: req,
|
||||
Doc: desc,
|
||||
Name: goName(jname),
|
||||
JSONName: jname,
|
||||
Type: tref,
|
||||
Required: req,
|
||||
Doc: desc,
|
||||
EnumValues: enumVals,
|
||||
})
|
||||
}
|
||||
return params
|
||||
|
||||
+34
-9
@@ -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
|
||||
}
|
||||
|
||||
@@ -15,17 +15,17 @@ func NewStatus(s string) dispatch.Filter[*api.ChatMemberUpdated] {
|
||||
}
|
||||
switch m := u.NewChatMember.(type) {
|
||||
case *api.ChatMemberOwner:
|
||||
return m.Status == s
|
||||
return string(m.Status) == s
|
||||
case *api.ChatMemberAdministrator:
|
||||
return m.Status == s
|
||||
return string(m.Status) == s
|
||||
case *api.ChatMemberMember:
|
||||
return m.Status == s
|
||||
return string(m.Status) == s
|
||||
case *api.ChatMemberRestricted:
|
||||
return m.Status == s
|
||||
return string(m.Status) == s
|
||||
case *api.ChatMemberLeft:
|
||||
return m.Status == s
|
||||
return string(m.Status) == s
|
||||
case *api.ChatMemberBanned:
|
||||
return m.Status == s
|
||||
return string(m.Status) == s
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -12,15 +12,15 @@ func memberUpdate(status string, fromID int64) *api.ChatMemberUpdated {
|
||||
var newMember api.ChatMember
|
||||
switch status {
|
||||
case "member":
|
||||
newMember = &api.ChatMemberMember{Status: status}
|
||||
newMember = &api.ChatMemberMember{Status: api.ChatMemberStatusMember}
|
||||
case "administrator":
|
||||
newMember = &api.ChatMemberAdministrator{Status: status}
|
||||
newMember = &api.ChatMemberAdministrator{Status: api.ChatMemberStatusAdministrator}
|
||||
case "kicked":
|
||||
newMember = &api.ChatMemberBanned{Status: status}
|
||||
newMember = &api.ChatMemberBanned{Status: api.ChatMemberStatusKicked}
|
||||
case "left":
|
||||
newMember = &api.ChatMemberLeft{Status: status}
|
||||
newMember = &api.ChatMemberLeft{Status: api.ChatMemberStatusLeft}
|
||||
default:
|
||||
newMember = &api.ChatMemberMember{Status: status}
|
||||
newMember = &api.ChatMemberMember{Status: api.ChatMemberStatusMember}
|
||||
}
|
||||
return &api.ChatMemberUpdated{
|
||||
From: api.User{ID: fromID},
|
||||
@@ -70,7 +70,7 @@ func TestComposedFilters(t *testing.T) {
|
||||
func TestNewStatus_Owner(t *testing.T) {
|
||||
u := &api.ChatMemberUpdated{
|
||||
From: api.User{ID: 1},
|
||||
NewChatMember: &api.ChatMemberOwner{Status: "creator"},
|
||||
NewChatMember: &api.ChatMemberOwner{Status: api.ChatMemberStatusCreator},
|
||||
}
|
||||
require.True(t, cmfilter.NewStatus("creator")(u))
|
||||
require.False(t, cmfilter.NewStatus("member")(u))
|
||||
@@ -79,7 +79,7 @@ func TestNewStatus_Owner(t *testing.T) {
|
||||
func TestNewStatus_Restricted(t *testing.T) {
|
||||
u := &api.ChatMemberUpdated{
|
||||
From: api.User{ID: 1},
|
||||
NewChatMember: &api.ChatMemberRestricted{Status: "restricted"},
|
||||
NewChatMember: &api.ChatMemberRestricted{Status: api.ChatMemberStatusRestricted},
|
||||
}
|
||||
require.True(t, cmfilter.NewStatus("restricted")(u))
|
||||
require.False(t, cmfilter.NewStatus("member")(u))
|
||||
|
||||
@@ -48,7 +48,7 @@ func Command(name string) dispatch.Filter[*api.Message] {
|
||||
return false
|
||||
}
|
||||
first := m.Entities[0]
|
||||
if first.Type != string(api.EntityBotCommand) || first.Offset != 0 {
|
||||
if first.Type != api.MessageEntityTypeBotCommand || first.Offset != 0 {
|
||||
return false
|
||||
}
|
||||
end := int(first.Length)
|
||||
@@ -72,7 +72,7 @@ func AnyCommand() dispatch.Filter[*api.Message] {
|
||||
return false
|
||||
}
|
||||
first := m.Entities[0]
|
||||
return first.Type == string(api.EntityBotCommand) && first.Offset == 0
|
||||
return first.Type == api.MessageEntityTypeBotCommand && first.Offset == 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,8 +105,8 @@ func HasDocument() dispatch.Filter[*api.Message] {
|
||||
}
|
||||
|
||||
// HasEntity returns a Filter that matches messages whose Entities contain at
|
||||
// least one entity of type t (e.g. string(api.EntityBotCommand)).
|
||||
func HasEntity(t string) dispatch.Filter[*api.Message] {
|
||||
// least one entity of type t (e.g. api.MessageEntityTypeBotCommand).
|
||||
func HasEntity(t api.MessageEntityType) dispatch.Filter[*api.Message] {
|
||||
return func(m *api.Message) bool {
|
||||
if m == nil {
|
||||
return false
|
||||
@@ -123,7 +123,7 @@ func HasEntity(t string) dispatch.Filter[*api.Message] {
|
||||
// ChatType returns a Filter that matches messages whose Chat.Type equals t.
|
||||
func ChatType(t api.ChatType) dispatch.Filter[*api.Message] {
|
||||
return func(m *api.Message) bool {
|
||||
return m != nil && m.Chat.Type == string(t)
|
||||
return m != nil && m.Chat.Type == t
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
func msg(text string) *api.Message {
|
||||
return &api.Message{
|
||||
MessageID: 1,
|
||||
Chat: api.Chat{ID: 1, Type: string(api.ChatTypePrivate)},
|
||||
Chat: api.Chat{ID: 1, Type: api.ChatTypePrivate},
|
||||
Text: text,
|
||||
}
|
||||
}
|
||||
@@ -20,10 +20,10 @@ func cmdMsg(cmd string) *api.Message {
|
||||
text := cmd
|
||||
return &api.Message{
|
||||
MessageID: 1,
|
||||
Chat: api.Chat{ID: 1, Type: string(api.ChatTypePrivate)},
|
||||
Chat: api.Chat{ID: 1, Type: api.ChatTypePrivate},
|
||||
Text: text,
|
||||
Entities: []api.MessageEntity{
|
||||
{Type: string(api.EntityBotCommand), Offset: 0, Length: int64(len([]rune(text)))},
|
||||
{Type: api.MessageEntityTypeBotCommand, Offset: 0, Length: int64(len([]rune(text)))},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,7 @@ func TestCommand(t *testing.T) {
|
||||
t.Run("strips BotName suffix", func(t *testing.T) {
|
||||
m := &api.Message{
|
||||
Text: "/start@MyBot",
|
||||
Entities: []api.MessageEntity{{Type: string(api.EntityBotCommand), Offset: 0, Length: 12}},
|
||||
Entities: []api.MessageEntity{{Type: api.MessageEntityTypeBotCommand, Offset: 0, Length: 12}},
|
||||
}
|
||||
f := msgfilter.Command("/start")
|
||||
require.True(t, f(m))
|
||||
@@ -109,7 +109,7 @@ func TestIsForward(t *testing.T) {
|
||||
// ForwardOrigin is a MessageOrigin interface; set via a concrete type.
|
||||
f := msgfilter.IsForward()
|
||||
m := msg("fwd")
|
||||
m.ForwardOrigin = &api.MessageOriginUser{Type: "user"}
|
||||
m.ForwardOrigin = &api.MessageOriginUser{}
|
||||
require.True(t, f(m))
|
||||
require.False(t, f(msg("no fwd")))
|
||||
require.False(t, f(nil))
|
||||
@@ -134,9 +134,9 @@ func TestHasDocument(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestHasEntity(t *testing.T) {
|
||||
f := msgfilter.HasEntity(string(api.EntityURL))
|
||||
f := msgfilter.HasEntity(api.MessageEntityTypeURL)
|
||||
m := msg("check https://example.com")
|
||||
m.Entities = []api.MessageEntity{{Type: string(api.EntityURL), Offset: 6, Length: 19}}
|
||||
m.Entities = []api.MessageEntity{{Type: api.MessageEntityTypeURL, Offset: 6, Length: 19}}
|
||||
require.True(t, f(m))
|
||||
require.False(t, f(msg("plain")))
|
||||
require.False(t, f(nil))
|
||||
@@ -148,7 +148,7 @@ func TestChatType(t *testing.T) {
|
||||
require.True(t, f(private))
|
||||
|
||||
group := msg("hi")
|
||||
group.Chat.Type = string(api.ChatTypeGroup)
|
||||
group.Chat.Type = api.ChatTypeGroup
|
||||
require.False(t, f(group))
|
||||
require.False(t, f(nil))
|
||||
}
|
||||
@@ -183,6 +183,6 @@ func TestComposedMessageFilters(t *testing.T) {
|
||||
require.True(t, f(m))
|
||||
|
||||
m2 := msg("say hello")
|
||||
m2.Chat.Type = string(api.ChatTypeGroup)
|
||||
m2.Chat.Type = api.ChatTypeGroup
|
||||
require.False(t, f(m2))
|
||||
}
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
@@ -17,7 +17,7 @@ func msgUpdate(id int64, text string) api.Update {
|
||||
UpdateID: id,
|
||||
Message: &api.Message{
|
||||
MessageID: id,
|
||||
Chat: api.Chat{ID: 1, Type: string(api.ChatTypePrivate)},
|
||||
Chat: api.Chat{ID: 1, Type: api.ChatTypePrivate},
|
||||
Text: text,
|
||||
},
|
||||
}
|
||||
@@ -29,10 +29,10 @@ func cmdUpdate(id int64, cmd string) api.Update {
|
||||
UpdateID: id,
|
||||
Message: &api.Message{
|
||||
MessageID: id,
|
||||
Chat: api.Chat{ID: 1, Type: string(api.ChatTypePrivate)},
|
||||
Chat: api.Chat{ID: 1, Type: api.ChatTypePrivate},
|
||||
Text: cmd,
|
||||
Entities: []api.MessageEntity{
|
||||
{Type: string(api.EntityBotCommand), Offset: 0, Length: int64(len(cmd))},
|
||||
{Type: api.MessageEntityTypeBotCommand, Offset: 0, Length: int64(len(cmd))},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
+24
-5
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -516,7 +535,7 @@ func extractCommand(m *api.Message) (cmd, args string, ok bool) {
|
||||
return "", "", false
|
||||
}
|
||||
first := m.Entities[0]
|
||||
if first.Type != string(api.EntityBotCommand) || first.Offset != 0 {
|
||||
if first.Type != api.MessageEntityTypeBotCommand || first.Offset != 0 {
|
||||
return "", "", false
|
||||
}
|
||||
cmd, sliceOk := utf16Slice(m.Text, int(first.Offset), int(first.Length))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
+21
-25
@@ -31,9 +31,9 @@ func cmdMessage(text string) api.Update {
|
||||
return api.Update{
|
||||
UpdateID: 1,
|
||||
Message: &api.Message{
|
||||
MessageID: 1, Date: 0, Chat: api.Chat{ID: 1, Type: string(api.ChatTypePrivate)},
|
||||
MessageID: 1, Date: 0, Chat: api.Chat{ID: 1, Type: api.ChatTypePrivate},
|
||||
Text: text,
|
||||
Entities: []api.MessageEntity{{Type: string(api.EntityBotCommand), Offset: 0, Length: int64(indexEnd(text))}},
|
||||
Entities: []api.MessageEntity{{Type: api.MessageEntityTypeBotCommand, Offset: 0, Length: int64(indexEnd(text))}},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -153,10 +153,10 @@ func TestRouter_NonASCIICommand(t *testing.T) {
|
||||
UpdateID: 1,
|
||||
Message: &api.Message{
|
||||
MessageID: 1,
|
||||
Chat: api.Chat{ID: 1, Type: string(api.ChatTypePrivate)},
|
||||
Chat: api.Chat{ID: 1, Type: api.ChatTypePrivate},
|
||||
Text: text,
|
||||
Entities: []api.MessageEntity{
|
||||
{Type: string(api.EntityBotCommand), Offset: 0, Length: cmdU16Len},
|
||||
{Type: api.MessageEntityTypeBotCommand, Offset: 0, Length: cmdU16Len},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,23 +177,22 @@ 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.
|
||||
u := api.Update{UpdateID: 1, Message: &api.Message{
|
||||
MessageID: 1, Chat: api.Chat{ID: 1, Type: "private"},
|
||||
Text: "/unknown",
|
||||
Entities: []api.MessageEntity{{Type: string(api.EntityBotCommand), Offset: 0, Length: 8}},
|
||||
Entities: []api.MessageEntity{{Type: api.MessageEntityTypeBotCommand, Offset: 0, Length: 8}},
|
||||
}}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
|
||||
@@ -247,7 +243,7 @@ func TestRouter_OnChannelPost(t *testing.T) {
|
||||
})
|
||||
|
||||
u := api.Update{UpdateID: 1, ChannelPost: &api.Message{
|
||||
MessageID: 99, Chat: api.Chat{ID: -100, Type: string(api.ChatTypeChannel)},
|
||||
MessageID: 99, Chat: api.Chat{ID: -100, Type: api.ChatTypeChannel},
|
||||
}}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
|
||||
defer cancel()
|
||||
@@ -393,7 +389,7 @@ func TestRouter_OnInlineQueryFilter_Matches(t *testing.T) {
|
||||
func TestRouter_FilterChain_Composition(t *testing.T) {
|
||||
// Filter: private chat AND text contains "hello"
|
||||
privateChat := Filter[*api.Message](func(m *api.Message) bool {
|
||||
return m != nil && m.Chat.Type == string(api.ChatTypePrivate)
|
||||
return m != nil && m.Chat.Type == api.ChatTypePrivate
|
||||
})
|
||||
hasHello := Filter[*api.Message](func(m *api.Message) bool {
|
||||
return m != nil && len(m.Text) > 0 && containsStr(m.Text, "hello")
|
||||
@@ -405,10 +401,10 @@ func TestRouter_FilterChain_Composition(t *testing.T) {
|
||||
r.OnMessageFilter(combined, func(c *Context, m *api.Message) error { hit <- m.Text; return nil })
|
||||
|
||||
match := api.Update{UpdateID: 1, Message: &api.Message{
|
||||
MessageID: 1, Chat: api.Chat{ID: 1, Type: string(api.ChatTypePrivate)}, Text: "say hello",
|
||||
MessageID: 1, Chat: api.Chat{ID: 1, Type: api.ChatTypePrivate}, Text: "say hello",
|
||||
}}
|
||||
noMatch := api.Update{UpdateID: 2, Message: &api.Message{
|
||||
MessageID: 2, Chat: api.Chat{ID: 2, Type: string(api.ChatTypeGroup)}, Text: "say hello",
|
||||
MessageID: 2, Chat: api.Chat{ID: 2, Type: api.ChatTypeGroup}, Text: "say hello",
|
||||
}}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
|
||||
@@ -462,7 +458,7 @@ func TestRouter_ConcurrentDispatch_AllHandlersFire(t *testing.T) {
|
||||
for i := range ups {
|
||||
ups[i] = api.Update{UpdateID: int64(i + 1), Message: &api.Message{
|
||||
MessageID: int64(i + 1),
|
||||
Chat: api.Chat{ID: 1, Type: string(api.ChatTypePrivate)},
|
||||
Chat: api.Chat{ID: 1, Type: api.ChatTypePrivate},
|
||||
Text: "hi",
|
||||
}}
|
||||
}
|
||||
@@ -493,7 +489,7 @@ func TestRouter_ConcurrentDispatch_SemaphoreBoundsConcurrency(t *testing.T) {
|
||||
for i := range ups {
|
||||
ups[i] = api.Update{UpdateID: int64(i + 1), Message: &api.Message{
|
||||
MessageID: int64(i + 1),
|
||||
Chat: api.Chat{ID: 1, Type: string(api.ChatTypePrivate)},
|
||||
Chat: api.Chat{ID: 1, Type: api.ChatTypePrivate},
|
||||
Text: "hi",
|
||||
}}
|
||||
}
|
||||
@@ -555,7 +551,7 @@ func TestRouter_ConcurrentDispatch_WaitsForInFlight(t *testing.T) {
|
||||
)
|
||||
|
||||
u := api.Update{UpdateID: 1, Message: &api.Message{
|
||||
MessageID: 1, Chat: api.Chat{ID: 1, Type: string(api.ChatTypePrivate)}, Text: "hi",
|
||||
MessageID: 1, Chat: api.Chat{ID: 1, Type: api.ChatTypePrivate}, Text: "hi",
|
||||
}}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
@@ -594,7 +590,7 @@ func TestRouter_SerialMode_NoRace(t *testing.T) {
|
||||
for i := range ups {
|
||||
ups[i] = api.Update{UpdateID: int64(i + 1), Message: &api.Message{
|
||||
MessageID: int64(i + 1),
|
||||
Chat: api.Chat{ID: 1, Type: string(api.ChatTypePrivate)},
|
||||
Chat: api.Chat{ID: 1, Type: api.ChatTypePrivate},
|
||||
Text: "hi",
|
||||
}}
|
||||
}
|
||||
@@ -908,7 +904,7 @@ func TestRouter_ContextCancel_UnblocksWaitingAcquire(t *testing.T) {
|
||||
for i := range limit {
|
||||
lu.Send(api.Update{UpdateID: int64(i + 1), Message: &api.Message{
|
||||
MessageID: int64(i + 1),
|
||||
Chat: api.Chat{ID: 1, Type: string(api.ChatTypePrivate)},
|
||||
Chat: api.Chat{ID: 1, Type: api.ChatTypePrivate},
|
||||
Text: "hi",
|
||||
}})
|
||||
}
|
||||
@@ -919,7 +915,7 @@ func TestRouter_ContextCancel_UnblocksWaitingAcquire(t *testing.T) {
|
||||
// Send one more update — Run will block trying to acquire the full semaphore.
|
||||
lu.Send(api.Update{UpdateID: int64(limit + 1), Message: &api.Message{
|
||||
MessageID: int64(limit + 1),
|
||||
Chat: api.Chat{ID: 1, Type: string(api.ChatTypePrivate)},
|
||||
Chat: api.Chat{ID: 1, Type: api.ChatTypePrivate},
|
||||
Text: "extra",
|
||||
}})
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
go-telegram.raczylo.com
|
||||
@@ -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.
|
||||
+962
@@ -0,0 +1,962 @@
|
||||
<!doctype html>
|
||||
<html lang="en" class="scroll-smooth">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>go-telegram — Strongly-typed Go client for the Telegram Bot API</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="A fully-generated, strongly-typed Go client for the Telegram Bot API. 176 methods, 1408 generated tests, zero any in the public surface."
|
||||
/>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class'
|
||||
}
|
||||
</script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||
/>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<style>
|
||||
body { font-family: "Inter", sans-serif; }
|
||||
code, pre { font-family: "JetBrains Mono", monospace; }
|
||||
.theme-transition {
|
||||
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
.animate-fade-in-up { animation: fadeInUp 0.6s ease-out; }
|
||||
.animate-float { animation: float 3s ease-in-out infinite; }
|
||||
.glass {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
.dark .glass {
|
||||
background: rgba(17, 24, 39, 0.7);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.gradient-text {
|
||||
background: linear-gradient(135deg, #29B6F6 0%, #0288D1 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
.dark .gradient-text {
|
||||
background: linear-gradient(135deg, #4FC3F7 0%, #81D4FA 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
.shadow-modern { box-shadow: 0 10px 40px -10px rgba(0, 0, 0, 0.1); }
|
||||
.dark .shadow-modern { box-shadow: 0 10px 40px -10px rgba(0, 0, 0, 0.4); }
|
||||
html { scroll-behavior: smooth; }
|
||||
/* Syntax highlight tokens */
|
||||
.tok-kw { color: #c792ea; }
|
||||
.tok-fn { color: #82aaff; }
|
||||
.tok-str { color: #c3e88d; }
|
||||
.tok-cmt { color: #546e7a; font-style: italic; }
|
||||
.tok-pkg { color: #ffcb6b; }
|
||||
.tok-num { color: #f78c6c; }
|
||||
.tok-type { color: #4FC3F7; }
|
||||
</style>
|
||||
<script>
|
||||
if (localStorage.theme === "dark" || (!("theme" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches)) {
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 theme-transition">
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="fixed w-full glass shadow-modern z-50 theme-transition">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6">
|
||||
<div class="flex justify-between h-16 items-center">
|
||||
<a href="#" class="flex items-center hover:opacity-80 transition-opacity duration-300">
|
||||
<img src="logo-light.svg" alt="go-telegram logo" class="h-10 w-auto dark:hidden" />
|
||||
<img src="logo-dark.svg" alt="go-telegram logo" class="h-10 w-auto hidden dark:block" />
|
||||
</a>
|
||||
<div class="hidden md:flex space-x-6">
|
||||
<a href="#features" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 font-medium">Features</a>
|
||||
<a href="#comparison" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 font-medium">Comparison</a>
|
||||
<a href="#install" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 font-medium">Install</a>
|
||||
<a href="#usage" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 font-medium">Usage</a>
|
||||
<a href="#examples" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 font-medium">Examples</a>
|
||||
<a href="#advanced" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 font-medium">Advanced</a>
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram/tree/main/docs/reference" target="_blank" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 font-medium">Reference</a>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<button id="theme-toggle" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 p-2 min-w-[44px] min-h-[44px] flex items-center justify-center" aria-label="Toggle theme">
|
||||
<i class="fas fa-moon dark:hidden text-xl"></i>
|
||||
<i class="fas fa-sun hidden dark:inline text-xl"></i>
|
||||
</button>
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram" target="_blank" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 p-2 min-w-[44px] min-h-[44px] flex items-center justify-center" aria-label="View on GitHub">
|
||||
<i class="fab fa-github text-xl"></i>
|
||||
</a>
|
||||
<button id="mobile-menu-toggle" class="md:hidden text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 p-2 min-w-[44px] min-h-[44px] flex items-center justify-center" aria-label="Toggle menu">
|
||||
<i class="fas fa-bars text-xl" id="menu-open-icon"></i>
|
||||
<i class="fas fa-times text-xl hidden" id="menu-close-icon"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mobile-menu" class="hidden md:hidden border-t border-gray-200 dark:border-gray-700">
|
||||
<div class="px-4 py-3 space-y-1 bg-white dark:bg-gray-800">
|
||||
<a href="#features" class="block px-3 py-3 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded font-medium">Features</a>
|
||||
<a href="#comparison" class="block px-3 py-3 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded font-medium">Comparison</a>
|
||||
<a href="#install" class="block px-3 py-3 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded font-medium">Install</a>
|
||||
<a href="#usage" class="block px-3 py-3 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded font-medium">Usage</a>
|
||||
<a href="#examples" class="block px-3 py-3 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded font-medium">Examples</a>
|
||||
<a href="#advanced" class="block px-3 py-3 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded font-medium">Advanced</a>
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram/tree/main/docs/reference" target="_blank" class="block px-3 py-3 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded font-medium">Reference</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="relative pt-24 sm:pt-32 pb-12 sm:pb-20 overflow-hidden">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-sky-50 via-cyan-50 to-blue-50 dark:from-gray-900 dark:via-sky-900/20 dark:to-cyan-900/20 theme-transition"></div>
|
||||
<div class="absolute top-0 -left-4 w-72 h-72 bg-cyan-300 dark:bg-cyan-500 rounded-full mix-blend-multiply dark:mix-blend-soft-light filter blur-xl opacity-20 animate-float"></div>
|
||||
<div class="absolute top-0 -right-4 w-72 h-72 bg-sky-300 dark:bg-sky-500 rounded-full mix-blend-multiply dark:mix-blend-soft-light filter blur-xl opacity-20 animate-float" style="animation-delay: 1s;"></div>
|
||||
<div class="absolute -bottom-8 left-20 w-72 h-72 bg-blue-300 dark:bg-blue-500 rounded-full mix-blend-multiply dark:mix-blend-soft-light filter blur-xl opacity-20 animate-float" style="animation-delay: 2s;"></div>
|
||||
|
||||
<div class="relative max-w-6xl mx-auto px-4 sm:px-6">
|
||||
<div class="text-center">
|
||||
<div class="mb-6 sm:mb-8 flex justify-center animate-fade-in-up">
|
||||
<img src="logo-light.svg" alt="go-telegram logo" class="h-20 sm:h-24 md:h-32 w-auto dark:hidden" />
|
||||
<img src="logo-dark.svg" alt="go-telegram logo" class="h-20 sm:h-24 md:h-32 w-auto hidden dark:block" />
|
||||
</div>
|
||||
<h1 class="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold text-gray-900 dark:text-gray-100 mb-4 sm:mb-6 leading-tight animate-fade-in-up" style="animation-delay: 0.1s;">
|
||||
A fully-generated,<br /><span class="gradient-text">strongly-typed</span> Go client<br />for the Telegram Bot API
|
||||
</h1>
|
||||
<p class="text-base sm:text-lg md:text-xl text-gray-600 dark:text-gray-300 mb-8 sm:mb-10 max-w-2xl mx-auto leading-relaxed px-4 animate-fade-in-up" style="animation-delay: 0.2s;">
|
||||
176 methods. 1408 generated tests. Zero <code class="bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded text-sm">any</code> in the public surface.
|
||||
</p>
|
||||
<div class="flex flex-col sm:flex-row gap-3 sm:gap-4 justify-center mb-8 sm:mb-10 px-4 animate-fade-in-up" style="animation-delay: 0.3s;">
|
||||
<a href="#install" class="group relative bg-gradient-to-r from-sky-500 to-cyan-600 hover:from-sky-600 hover:to-cyan-700 text-white px-8 py-3 rounded-lg font-medium transition-all duration-300 min-h-[48px] flex items-center justify-center shadow-lg hover:shadow-xl hover:scale-105">
|
||||
<i class="fas fa-rocket mr-2"></i><span class="relative z-10">Get started</span>
|
||||
</a>
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram" target="_blank" class="group glass hover:shadow-lg text-gray-900 dark:text-gray-100 px-8 py-3 rounded-lg font-medium transition-all duration-300 min-h-[48px] flex items-center justify-center hover:scale-105">
|
||||
<i class="fab fa-github mr-2"></i>View on GitHub
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Stats row -->
|
||||
<div class="flex flex-wrap justify-center gap-x-3 gap-y-1 text-sm font-mono text-gray-500 dark:text-gray-400 mb-12 sm:mb-16 px-4 animate-fade-in-up" style="animation-delay: 0.35s;">
|
||||
<span>176 methods</span>
|
||||
<span class="text-gray-300 dark:text-gray-600">·</span>
|
||||
<span>301 types</span>
|
||||
<span class="text-gray-300 dark:text-gray-600">·</span>
|
||||
<span>1,408 tests</span>
|
||||
<span class="text-gray-300 dark:text-gray-600">·</span>
|
||||
<span>MIT licensed</span>
|
||||
</div>
|
||||
|
||||
<!-- Code preview card -->
|
||||
<div class="mt-4 max-w-2xl mx-auto px-4 animate-fade-in-up animate-float" style="animation-delay: 0.4s; animation-duration: 4s;">
|
||||
<div class="relative group">
|
||||
<div class="absolute -inset-1 bg-gradient-to-r from-sky-500 to-cyan-600 rounded-xl blur opacity-25 group-hover:opacity-50 transition duration-500"></div>
|
||||
<div class="relative bg-gradient-to-br from-gray-900 to-gray-800 rounded-xl p-5 text-left border border-gray-700">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span class="w-3 h-3 rounded-full bg-red-500"></span>
|
||||
<span class="w-3 h-3 rounded-full bg-yellow-500"></span>
|
||||
<span class="w-3 h-3 rounded-full bg-green-500"></span>
|
||||
<span class="ml-2 text-xs text-gray-500 font-mono">echo_bot.go</span>
|
||||
</div>
|
||||
<pre class="text-sm text-gray-100 overflow-x-auto"><code class="font-mono"><span class="tok-kw">package</span> <span class="tok-pkg">main</span>
|
||||
|
||||
<span class="tok-kw">import</span> (
|
||||
<span class="tok-str">"context"</span>
|
||||
<span class="tok-str">"log"</span>
|
||||
<span class="tok-str">"os"</span>
|
||||
|
||||
<span class="tok-str">"github.com/lukaszraczylo/go-telegram/api"</span>
|
||||
<span class="tok-str">"github.com/lukaszraczylo/go-telegram/client"</span>
|
||||
<span class="tok-str">"github.com/lukaszraczylo/go-telegram/dispatch"</span>
|
||||
)
|
||||
|
||||
<span class="tok-kw">func</span> <span class="tok-fn">main</span>() {
|
||||
bot, _ := <span class="tok-pkg">client</span>.<span class="tok-fn">NewRetryDoer</span>(os.<span class="tok-fn">Getenv</span>(<span class="tok-str">"BOT_TOKEN"</span>), <span class="tok-kw">nil</span>)
|
||||
d := <span class="tok-pkg">dispatch</span>.<span class="tok-fn">New</span>(bot)
|
||||
|
||||
d.<span class="tok-fn">OnMessage</span>(<span class="tok-kw">func</span>(ctx <span class="tok-type">context.Context</span>, msg *<span class="tok-type">api.Message</span>) {
|
||||
bot.<span class="tok-fn">SendMessage</span>(ctx, &<span class="tok-type">api.SendMessageParams</span>{
|
||||
ChatID: <span class="tok-pkg">api</span>.<span class="tok-fn">ChatIDFromInt</span>(msg.Chat.ID),
|
||||
Text: msg.Text,
|
||||
})
|
||||
})
|
||||
|
||||
<span class="tok-kw">if</span> err := d.<span class="tok-fn">Run</span>(<span class="tok-pkg">context</span>.<span class="tok-fn">Background</span>()); err != <span class="tok-kw">nil</span> {
|
||||
<span class="tok-pkg">log</span>.<span class="tok-fn">Fatal</span>(err)
|
||||
}
|
||||
}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Features Section -->
|
||||
<section id="features" class="py-12 sm:py-16 md:py-20 bg-white dark:bg-gray-900 theme-transition">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6">
|
||||
<div class="text-center mb-8 sm:mb-12">
|
||||
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-3 sm:mb-4">Features</h2>
|
||||
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-300 px-4">Built for correctness, composability, and production reliability</p>
|
||||
</div>
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div class="glass p-5 rounded-xl group hover:shadow-lg transition-all duration-300">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-sky-500 to-sky-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-code-branch text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1">Generated from the live docs</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">IR + emitter pipeline runs weekly; a PR opens automatically for any Telegram-side change.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="glass p-5 rounded-xl group hover:shadow-lg transition-all duration-300">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-emerald-500 to-emerald-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-shield-halved text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1">No <code class="text-xs bg-emerald-100 dark:bg-emerald-900 px-1 rounded">any</code> in the public API</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Typed unions — <code class="text-xs">ChatID</code>, <code class="text-xs">MessageOrBool</code> — sealed interfaces with marker methods and auto-decode.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="glass p-5 rounded-xl group hover:shadow-lg transition-all duration-300">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-purple-500 to-purple-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-plug text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1">Pluggable transport + codec</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Drop in fasthttp, sonic, or goccy/go-json with a one-line swap.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="glass p-5 rounded-xl group hover:shadow-lg transition-all duration-300">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-amber-500 to-amber-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-bolt text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1">Production-ready out of the box</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Retry middleware honouring <code class="text-xs">retry_after</code>, panic recovery, structured errors with sentinels.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="glass p-5 rounded-xl group hover:shadow-lg transition-all duration-300">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-rose-500 to-rose-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-route text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1">Typed dispatcher</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Generic <code class="text-xs">Handler[T]</code>, composable filters, conversation handler with pluggable storage, per-update goroutine pool.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="glass p-5 rounded-xl group hover:shadow-lg transition-all duration-300">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-teal-500 to-teal-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-circle-check text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1">Self-verifying codegen</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Every regen runs scrape → audit → emit → 1408 generated tests. Nothing ships without passing.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Comparison Section -->
|
||||
<section id="comparison" class="py-12 sm:py-16 md:py-20 bg-gray-50 dark:bg-gray-800 theme-transition">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6">
|
||||
<div class="text-center mb-8 sm:mb-12">
|
||||
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-3 sm:mb-4">Comparison</h2>
|
||||
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-300 px-4">How go-telegram compares to other Go Telegram bot libraries</p>
|
||||
</div>
|
||||
|
||||
<!-- Mobile cards -->
|
||||
<div class="block md:hidden space-y-4 mb-8">
|
||||
<div class="glass rounded-xl p-5 shadow-modern">
|
||||
<div class="flex items-center gap-3 mb-4 pb-3 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-sky-500 to-cyan-600 flex items-center justify-center">
|
||||
<i class="fas fa-paper-plane text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-gray-900 dark:text-gray-100">go-telegram</h4>
|
||||
<p class="text-xs text-gray-500">HTML scraper · this library</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-2 text-sm">
|
||||
<div class="flex items-center gap-2"><span class="text-green-500 font-bold">✓</span><span class="text-gray-600 dark:text-gray-400">Generated from spec (HTML)</span></div>
|
||||
<div class="flex items-center gap-2"><span class="text-green-500 font-bold">✓</span><span class="text-gray-600 dark:text-gray-400">Typed unions, no any</span></div>
|
||||
<div class="flex items-center gap-2"><span class="text-green-500 font-bold">✓</span><span class="text-gray-600 dark:text-gray-400">1,408 auto-generated tests</span></div>
|
||||
<div class="flex items-center gap-2"><span class="text-green-500 font-bold">✓</span><span class="text-gray-600 dark:text-gray-400">Conversation handler</span></div>
|
||||
<div class="flex items-center gap-2"><span class="text-green-500 font-bold">✓</span><span class="text-gray-600 dark:text-gray-400">Retry middleware (retry_after)</span></div>
|
||||
<div class="flex items-center gap-2"><span class="text-green-500 font-bold">✓</span><span class="text-gray-600 dark:text-gray-400">Pluggable JSON codec</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="glass rounded-xl p-5 shadow-modern">
|
||||
<div class="flex items-center gap-3 mb-4 pb-3 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-indigo-600 to-indigo-700 flex items-center justify-center">
|
||||
<span class="text-white font-bold text-xs">gtb</span>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-gray-900 dark:text-gray-100">gotgbot/v2</h4>
|
||||
<p class="text-xs text-gray-500">JSON spec · popular library</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-2 text-sm">
|
||||
<div class="flex items-center gap-2"><span class="text-green-500 font-bold">✓</span><span class="text-gray-600 dark:text-gray-400">Generated from spec (JSON)</span></div>
|
||||
<div class="flex items-center gap-2"><span class="text-yellow-500 font-bold">△</span><span class="text-gray-600 dark:text-gray-400">Partial typed unions</span></div>
|
||||
<div class="flex items-center gap-2"><span class="text-red-500 font-bold">✗</span><span class="text-gray-400">Auto-generated tests</span></div>
|
||||
<div class="flex items-center gap-2"><span class="text-green-500 font-bold">✓</span><span class="text-gray-600 dark:text-gray-400">Conversation handler</span></div>
|
||||
<div class="flex items-center gap-2"><span class="text-yellow-500 font-bold">△</span><span class="text-gray-600 dark:text-gray-400">User-implemented retry</span></div>
|
||||
<div class="flex items-center gap-2"><span class="text-red-500 font-bold">✗</span><span class="text-gray-400">Pluggable JSON codec</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="glass rounded-xl p-5 shadow-modern">
|
||||
<div class="flex items-center gap-3 mb-4 pb-3 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-gray-600 to-gray-700 flex items-center justify-center">
|
||||
<span class="text-white font-bold text-xs">tba</span>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-gray-900 dark:text-gray-100">telegram-bot-api/v5</h4>
|
||||
<p class="text-xs text-gray-500">Hand-coded</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-2 text-sm">
|
||||
<div class="flex items-center gap-2"><span class="text-red-500 font-bold">✗</span><span class="text-gray-400">Generated from spec</span></div>
|
||||
<div class="flex items-center gap-2"><span class="text-red-500 font-bold">✗</span><span class="text-gray-400">Typed unions</span></div>
|
||||
<div class="flex items-center gap-2"><span class="text-red-500 font-bold">✗</span><span class="text-gray-400">Auto-generated tests</span></div>
|
||||
<div class="flex items-center gap-2"><span class="text-red-500 font-bold">✗</span><span class="text-gray-400">Conversation handler</span></div>
|
||||
<div class="flex items-center gap-2"><span class="text-red-500 font-bold">✗</span><span class="text-gray-400">Retry middleware</span></div>
|
||||
<div class="flex items-center gap-2"><span class="text-red-500 font-bold">✗</span><span class="text-gray-400">Pluggable JSON codec</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Desktop table -->
|
||||
<div class="hidden md:block glass rounded-xl overflow-hidden shadow-modern">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gradient-to-r from-sky-500 to-cyan-600 text-white">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left font-semibold">Feature</th>
|
||||
<th class="px-4 py-3 text-center font-semibold">go-telegram</th>
|
||||
<th class="px-4 py-3 text-center font-semibold"><a href="https://github.com/PaulSonOfLars/gotgbot" class="hover:underline" target="_blank">gotgbot/v2</a></th>
|
||||
<th class="px-4 py-3 text-center font-semibold"><a href="https://github.com/go-telegram-bot-api/telegram-bot-api" class="hover:underline" target="_blank">telegram-bot-api/v5</a></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/50">
|
||||
<td class="px-4 py-3 text-gray-700 dark:text-gray-300 font-medium">Generated from spec</td>
|
||||
<td class="px-4 py-3 text-center"><span class="text-green-500 font-bold">✓</span> <span class="text-xs text-gray-500">HTML scraper</span></td>
|
||||
<td class="px-4 py-3 text-center"><span class="text-green-500 font-bold">✓</span> <span class="text-xs text-gray-500">JSON spec</span></td>
|
||||
<td class="px-4 py-3 text-center"><span class="text-red-500 font-bold">✗</span> <span class="text-xs text-gray-500">hand-coded</span></td>
|
||||
</tr>
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/50">
|
||||
<td class="px-4 py-3 text-gray-700 dark:text-gray-300 font-medium">Typed unions (no <code class="text-xs">any</code>)</td>
|
||||
<td class="px-4 py-3 text-center"><span class="text-green-500 font-bold">✓</span> <span class="text-xs text-gray-500">ChatID, MessageOrBool, sealed interfaces</span></td>
|
||||
<td class="px-4 py-3 text-center"><span class="text-yellow-500 font-bold">△</span> <span class="text-xs text-gray-500">partial</span></td>
|
||||
<td class="px-4 py-3 text-center"><span class="text-red-500 font-bold">✗</span></td>
|
||||
</tr>
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/50">
|
||||
<td class="px-4 py-3 text-gray-700 dark:text-gray-300 font-medium">Auto-generated tests</td>
|
||||
<td class="px-4 py-3 text-center"><span class="text-green-500 font-bold">✓</span> <span class="text-xs text-gray-500">1,408 (8 scenarios/method)</span></td>
|
||||
<td class="px-4 py-3 text-center"><span class="text-red-500 font-bold">✗</span></td>
|
||||
<td class="px-4 py-3 text-center"><span class="text-red-500 font-bold">✗</span></td>
|
||||
</tr>
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/50">
|
||||
<td class="px-4 py-3 text-gray-700 dark:text-gray-300 font-medium">Conversation handler</td>
|
||||
<td class="px-4 py-3 text-center"><span class="text-green-500 font-bold">✓</span> <span class="text-xs text-gray-500">pluggable storage</span></td>
|
||||
<td class="px-4 py-3 text-center"><span class="text-green-500 font-bold">✓</span></td>
|
||||
<td class="px-4 py-3 text-center"><span class="text-red-500 font-bold">✗</span></td>
|
||||
</tr>
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/50">
|
||||
<td class="px-4 py-3 text-gray-700 dark:text-gray-300 font-medium">Retry middleware</td>
|
||||
<td class="px-4 py-3 text-center"><span class="text-green-500 font-bold">✓</span> <span class="text-xs text-gray-500">honours retry_after</span></td>
|
||||
<td class="px-4 py-3 text-center"><span class="text-yellow-500 font-bold">△</span> <span class="text-xs text-gray-500">user-implemented</span></td>
|
||||
<td class="px-4 py-3 text-center"><span class="text-red-500 font-bold">✗</span></td>
|
||||
</tr>
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/50">
|
||||
<td class="px-4 py-3 text-gray-700 dark:text-gray-300 font-medium">Pluggable JSON codec</td>
|
||||
<td class="px-4 py-3 text-center"><span class="text-green-500 font-bold">✓</span></td>
|
||||
<td class="px-4 py-3 text-center"><span class="text-red-500 font-bold">✗</span></td>
|
||||
<td class="px-4 py-3 text-center"><span class="text-red-500 font-bold">✗</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Installation Section -->
|
||||
<section id="install" class="py-12 sm:py-16 md:py-20 bg-white dark:bg-gray-900 theme-transition">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6">
|
||||
<div class="text-center mb-10 sm:mb-12">
|
||||
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-3 sm:mb-4">Installation</h2>
|
||||
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-300 px-4">One command. No CGO, no system deps.</p>
|
||||
</div>
|
||||
<div class="space-y-4 sm:space-y-6">
|
||||
<div class="glass p-6 sm:p-8 rounded-xl shadow-modern hover:shadow-xl transition-all duration-300">
|
||||
<div class="flex items-center mb-4">
|
||||
<i class="fas fa-cubes text-sky-500 dark:text-sky-400 text-2xl mr-3"></i>
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-gray-100">go get</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 text-sm">Requires Go 1.21+</p>
|
||||
</div>
|
||||
</div>
|
||||
<div onclick="copyToClipboard('go get github.com/lukaszraczylo/go-telegram', this)" class="relative bg-gradient-to-br from-gray-900 to-gray-800 dark:from-gray-950 dark:to-black text-gray-100 p-4 rounded-lg text-sm cursor-pointer group overflow-x-auto border border-gray-700 hover:border-sky-500 transition-all duration-300">
|
||||
<code class="block whitespace-nowrap font-mono">go get github.com/lukaszraczylo/go-telegram</code>
|
||||
<div class="absolute top-3 right-3"><i class="fas fa-copy text-gray-500 group-hover:text-sky-400 transition-colors duration-300"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="glass p-6 sm:p-8 rounded-xl shadow-modern hover:shadow-xl transition-all duration-300">
|
||||
<div class="flex items-center mb-4">
|
||||
<i class="fas fa-book text-cyan-500 dark:text-cyan-400 text-2xl mr-3"></i>
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-gray-100">pkg.go.dev</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 text-sm">Full API reference</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="https://pkg.go.dev/github.com/lukaszraczylo/go-telegram" target="_blank" class="block text-center bg-gradient-to-r from-cyan-600 to-sky-700 hover:from-cyan-700 hover:to-sky-800 text-white px-4 py-3 rounded-lg text-sm font-medium shadow-lg hover:shadow-xl transition-all duration-300 hover:scale-105">
|
||||
<i class="fas fa-external-link-alt mr-2"></i>View on pkg.go.dev
|
||||
</a>
|
||||
</div>
|
||||
<div class="glass p-6 sm:p-8 rounded-xl shadow-modern hover:shadow-xl transition-all duration-300">
|
||||
<div class="flex items-center mb-4">
|
||||
<i class="fas fa-book-open text-emerald-500 dark:text-emerald-400 text-2xl mr-3"></i>
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-gray-100">Markdown reference</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 text-sm">Auto-generated, browse on GitHub</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram/tree/main/docs/reference" target="_blank" class="block text-center bg-gradient-to-r from-emerald-600 to-teal-700 hover:from-emerald-700 hover:to-teal-800 text-white px-4 py-3 rounded-lg text-sm font-medium shadow-lg hover:shadow-xl transition-all duration-300 hover:scale-105">
|
||||
<i class="fas fa-external-link-alt mr-2"></i>Browse reference docs
|
||||
</a>
|
||||
</div>
|
||||
<div class="glass p-6 sm:p-8 rounded-xl shadow-modern hover:shadow-xl transition-all duration-300">
|
||||
<div class="flex items-center mb-4">
|
||||
<i class="fas fa-file-alt text-blue-500 dark:text-blue-400 text-2xl mr-3"></i>
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-gray-100">Telegram Bot API reference</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 text-sm">Upstream spec this library tracks</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="https://core.telegram.org/bots/api" target="_blank" class="block text-center bg-gradient-to-r from-blue-500 to-blue-700 hover:from-blue-600 hover:to-blue-800 text-white px-4 py-3 rounded-lg text-sm font-medium shadow-lg hover:shadow-xl transition-all duration-300 hover:scale-105">
|
||||
<i class="fas fa-external-link-alt mr-2"></i>core.telegram.org/bots/api
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Usage Section -->
|
||||
<section id="usage" class="py-12 sm:py-16 md:py-20 bg-gray-50 dark:bg-gray-800 theme-transition">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6">
|
||||
<div class="text-center mb-10 sm:mb-12">
|
||||
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-3 sm:mb-4">Usage</h2>
|
||||
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-300 px-4">Echo bot — the minimal working example</p>
|
||||
</div>
|
||||
|
||||
<div class="relative group mb-10">
|
||||
<div class="absolute -inset-1 bg-gradient-to-r from-sky-500 to-cyan-600 rounded-xl blur opacity-25 group-hover:opacity-50 transition duration-300"></div>
|
||||
<div class="relative bg-gradient-to-br from-gray-900 to-gray-800 rounded-xl p-4 sm:p-6 overflow-x-auto border border-gray-700">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-file-code text-sky-400 mr-2"></i>
|
||||
<span class="text-sky-400 text-sm font-mono font-semibold">echo_bot.go</span>
|
||||
</div>
|
||||
<button onclick="copyToClipboard(document.getElementById('echo-bot-code').textContent, this)" class="text-gray-400 hover:text-sky-400 transition-colors duration-300" aria-label="Copy code">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
<pre class="text-xs sm:text-sm text-gray-100 overflow-x-auto"><code id="echo-bot-code" class="font-mono">package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/lukaszraczylo/go-telegram/api"
|
||||
"github.com/lukaszraczylo/go-telegram/client"
|
||||
"github.com/lukaszraczylo/go-telegram/dispatch"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// NewRetryDoer wraps the default transport with retry middleware
|
||||
// that honours Telegram's retry_after field automatically.
|
||||
bot, err := client.NewRetryDoer(os.Getenv("BOT_TOKEN"), nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
d := dispatch.New(bot)
|
||||
|
||||
// Handler[T] is generic — the type parameter is the concrete update type.
|
||||
d.OnMessage(func(ctx context.Context, msg *api.Message) {
|
||||
_, err := bot.SendMessage(ctx, &api.SendMessageParams{
|
||||
// ChatIDFromInt returns a typed ChatID — no interface{} here.
|
||||
ChatID: api.ChatIDFromInt(msg.Chat.ID),
|
||||
Text: msg.Text,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("send error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
if err := d.Run(context.Background()); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Codegen pipeline -->
|
||||
<div id="codegen" class="glass p-6 sm:p-8 rounded-xl shadow-modern">
|
||||
<h3 class="text-lg sm:text-xl font-semibold text-gray-900 dark:text-gray-100 mb-2"><i class="fas fa-cogs text-sky-500 mr-2"></i>Codegen pipeline</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-6">The emitter scrapes the live Telegram Bot API docs (HTML), builds an intermediate representation (<code class="text-xs">api.json</code>), then generates Go code and tests. An audit step validates every method signature against the IR before emission.</p>
|
||||
|
||||
<!-- Flow diagram -->
|
||||
<div class="flex flex-wrap items-center justify-center gap-2 sm:gap-3 mb-6 text-sm font-mono">
|
||||
<div class="px-3 py-2 bg-sky-100 dark:bg-sky-900/40 text-sky-800 dark:text-sky-300 rounded-lg border border-sky-200 dark:border-sky-700 flex items-center gap-2">
|
||||
<i class="fas fa-globe text-sky-500"></i> HTML docs
|
||||
</div>
|
||||
<span class="text-gray-400 font-sans">→</span>
|
||||
<div class="px-3 py-2 bg-purple-100 dark:bg-purple-900/40 text-purple-800 dark:text-purple-300 rounded-lg border border-purple-200 dark:border-purple-700 flex items-center gap-2">
|
||||
<i class="fas fa-database text-purple-500"></i> IR (api.json)
|
||||
</div>
|
||||
<span class="text-gray-400 font-sans">→</span>
|
||||
<div class="px-3 py-2 bg-amber-100 dark:bg-amber-900/40 text-amber-800 dark:text-amber-300 rounded-lg border border-amber-200 dark:border-amber-700 flex items-center gap-2">
|
||||
<i class="fas fa-search text-amber-500"></i> audit
|
||||
</div>
|
||||
<span class="text-gray-400 font-sans">→</span>
|
||||
<div class="px-3 py-2 bg-emerald-100 dark:bg-emerald-900/40 text-emerald-800 dark:text-emerald-300 rounded-lg border border-emerald-200 dark:border-emerald-700 flex items-center gap-2">
|
||||
<i class="fas fa-code text-emerald-500"></i> Go code + tests
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div onclick="copyToClipboard('make snapshot && make regen', this)" class="relative bg-gradient-to-br from-gray-900 to-gray-800 text-gray-100 p-4 rounded-lg text-sm cursor-pointer group overflow-x-auto border border-gray-700 hover:border-sky-500 transition-all duration-300">
|
||||
<code class="block whitespace-nowrap font-mono">make snapshot && make regen</code>
|
||||
<div class="absolute top-3 right-3"><i class="fas fa-copy text-gray-500 group-hover:text-sky-400 transition-colors duration-300"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Examples Section -->
|
||||
<section id="examples" class="py-12 sm:py-16 md:py-20 bg-white dark:bg-gray-900 theme-transition">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6">
|
||||
<div class="text-center mb-8 sm:mb-12">
|
||||
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-3 sm:mb-4">Examples</h2>
|
||||
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-300 px-4">14 runnable bots covering the most common patterns</p>
|
||||
</div>
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<!-- Echo -->
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram/tree/main/examples/echo" target="_blank" class="glass p-4 rounded-xl group hover:shadow-lg transition-all duration-300 flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-sky-500 to-sky-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-reply text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1 group-hover:text-sky-600 dark:group-hover:text-sky-400">echo</h3>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Minimal echo bot — get up and running in 30 lines.</p>
|
||||
</div>
|
||||
</a>
|
||||
<!-- Callback -->
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram/tree/main/examples/callback" target="_blank" class="glass p-4 rounded-xl group hover:shadow-lg transition-all duration-300 flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-purple-500 to-purple-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-hand-pointer text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1 group-hover:text-sky-600 dark:group-hover:text-sky-400">callback</h3>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Inline keyboard buttons and callback query handling.</p>
|
||||
</div>
|
||||
</a>
|
||||
<!-- Inline -->
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram/tree/main/examples/inline" target="_blank" class="glass p-4 rounded-xl group hover:shadow-lg transition-all duration-300 flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-cyan-500 to-cyan-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-search text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1 group-hover:text-sky-600 dark:group-hover:text-sky-400">inline</h3>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Inline mode queries and result sets.</p>
|
||||
</div>
|
||||
</a>
|
||||
<!-- Conversation -->
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram/tree/main/examples/conversation" target="_blank" class="glass p-4 rounded-xl group hover:shadow-lg transition-all duration-300 flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-emerald-500 to-emerald-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-comments text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1 group-hover:text-sky-600 dark:group-hover:text-sky-400">conversation</h3>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Multi-step conversation flows with pluggable state storage.</p>
|
||||
</div>
|
||||
</a>
|
||||
<!-- Stateful -->
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram/tree/main/examples/stateful" target="_blank" class="glass p-4 rounded-xl group hover:shadow-lg transition-all duration-300 flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-amber-500 to-amber-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-memory text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1 group-hover:text-sky-600 dark:group-hover:text-sky-400">stateful</h3>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Per-user state machine pattern.</p>
|
||||
</div>
|
||||
</a>
|
||||
<!-- Admin -->
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram/tree/main/examples/admin" target="_blank" class="glass p-4 rounded-xl group hover:shadow-lg transition-all duration-300 flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-rose-500 to-rose-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-user-shield text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1 group-hover:text-sky-600 dark:group-hover:text-sky-400">admin</h3>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Admin commands — ban, kick, restrict members.</p>
|
||||
</div>
|
||||
</a>
|
||||
<!-- Middleware -->
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram/tree/main/examples/middleware" target="_blank" class="glass p-4 rounded-xl group hover:shadow-lg transition-all duration-300 flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-indigo-500 to-indigo-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-layer-group text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1 group-hover:text-sky-600 dark:group-hover:text-sky-400">middleware</h3>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Composable middleware chain — logging, auth, rate limiting.</p>
|
||||
</div>
|
||||
</a>
|
||||
<!-- Files -->
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram/tree/main/examples/files" target="_blank" class="glass p-4 rounded-xl group hover:shadow-lg transition-all duration-300 flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-teal-500 to-teal-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-paperclip text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1 group-hover:text-sky-600 dark:group-hover:text-sky-400">files</h3>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Upload and download photos, documents, audio.</p>
|
||||
</div>
|
||||
</a>
|
||||
<!-- Webhook -->
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram/tree/main/examples/webhook" target="_blank" class="glass p-4 rounded-xl group hover:shadow-lg transition-all duration-300 flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-lime-500 to-lime-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-network-wired text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1 group-hover:text-sky-600 dark:group-hover:text-sky-400">webhook</h3>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Webhook server instead of long-polling.</p>
|
||||
</div>
|
||||
</a>
|
||||
<!-- Polls -->
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram/tree/main/examples/polls" target="_blank" class="glass p-4 rounded-xl group hover:shadow-lg transition-all duration-300 flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-fuchsia-500 to-fuchsia-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-poll text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1 group-hover:text-sky-600 dark:group-hover:text-sky-400">polls</h3>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Send polls and handle poll-answer updates.</p>
|
||||
</div>
|
||||
</a>
|
||||
<!-- Payments -->
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram/tree/main/examples/payments" target="_blank" class="glass p-4 rounded-xl group hover:shadow-lg transition-all duration-300 flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-yellow-500 to-yellow-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-credit-card text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1 group-hover:text-sky-600 dark:group-hover:text-sky-400">payments</h3>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Telegram Payments — invoices, pre-checkout, successful payment.</p>
|
||||
</div>
|
||||
</a>
|
||||
<!-- Pagination -->
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram/tree/main/examples/pagination" target="_blank" class="glass p-4 rounded-xl group hover:shadow-lg transition-all duration-300 flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-slate-500 to-slate-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-chevron-right text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1 group-hover:text-sky-600 dark:group-hover:text-sky-400">pagination</h3>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Paginated inline keyboards for long result sets.</p>
|
||||
</div>
|
||||
</a>
|
||||
<!-- Welcome -->
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram/tree/main/examples/welcome" target="_blank" class="glass p-4 rounded-xl group hover:shadow-lg transition-all duration-300 flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-green-500 to-green-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-door-open text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1 group-hover:text-sky-600 dark:group-hover:text-sky-400">welcome</h3>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Greet new members joining a group or channel.</p>
|
||||
</div>
|
||||
</a>
|
||||
<!-- Moderation -->
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram/tree/main/examples/moderation" target="_blank" class="glass p-4 rounded-xl group hover:shadow-lg transition-all duration-300 flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-red-500 to-red-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-gavel text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1 group-hover:text-sky-600 dark:group-hover:text-sky-400">moderation</h3>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Automated content moderation with filter chains.</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Advanced Section -->
|
||||
<section id="advanced" class="py-12 sm:py-16 md:py-20 bg-gray-50 dark:bg-gray-800 theme-transition">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6">
|
||||
<div class="text-center mb-10 sm:mb-12">
|
||||
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-3 sm:mb-4">Advanced</h2>
|
||||
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-300 px-4">Power-user patterns — expand to read</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<!-- Conversation flows -->
|
||||
<details class="glass rounded-xl overflow-hidden group">
|
||||
<summary class="flex items-center justify-between p-5 cursor-pointer list-none select-none">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-9 h-9 rounded-lg bg-gradient-to-br from-emerald-500 to-emerald-600 flex items-center justify-center flex-shrink-0">
|
||||
<i class="fas fa-comments text-white text-sm"></i>
|
||||
</div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100">Conversation flows</h3>
|
||||
</div>
|
||||
<i class="fas fa-chevron-down text-gray-400 transition-transform duration-300 group-open:rotate-180"></i>
|
||||
</summary>
|
||||
<div class="px-5 pb-5 border-t border-gray-200 dark:border-gray-700">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-4 mb-4">
|
||||
The conversation handler chains multiple message steps and stores state between them. Any storage backend implementing the
|
||||
<code class="text-xs bg-gray-100 dark:bg-gray-800 px-1 rounded">StateStorage</code> interface works — in-memory, Redis, Postgres.
|
||||
</p>
|
||||
<div class="bg-gradient-to-br from-gray-900 to-gray-800 rounded-xl p-4 text-sm text-gray-100 overflow-x-auto border border-gray-700">
|
||||
<pre class="font-mono"><code>conv := conversation.New(storage)
|
||||
|
||||
conv.AddState("ask_name", func(ctx context.Context, msg *api.Message) (string, error) {
|
||||
bot.SendMessage(ctx, &api.SendMessageParams{
|
||||
ChatID: api.ChatIDFromInt(msg.Chat.ID),
|
||||
Text: "What's your name?",
|
||||
})
|
||||
return "ask_age", nil
|
||||
})
|
||||
|
||||
conv.AddState("ask_age", func(ctx context.Context, msg *api.Message) (string, error) {
|
||||
name := conv.GetData(ctx, "name")
|
||||
// ... handle age input
|
||||
return conversation.Done, nil
|
||||
})
|
||||
|
||||
d.OnMessage(conv.Handler("start"))</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Custom filters -->
|
||||
<details class="glass rounded-xl overflow-hidden group">
|
||||
<summary class="flex items-center justify-between p-5 cursor-pointer list-none select-none">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-9 h-9 rounded-lg bg-gradient-to-br from-purple-500 to-purple-600 flex items-center justify-center flex-shrink-0">
|
||||
<i class="fas fa-filter text-white text-sm"></i>
|
||||
</div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100">Custom filters</h3>
|
||||
</div>
|
||||
<i class="fas fa-chevron-down text-gray-400 transition-transform duration-300 group-open:rotate-180"></i>
|
||||
</summary>
|
||||
<div class="px-5 pb-5 border-t border-gray-200 dark:border-gray-700">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-4 mb-4">
|
||||
Filters compose with <code class="text-xs bg-gray-100 dark:bg-gray-800 px-1 rounded">And</code>, <code class="text-xs bg-gray-100 dark:bg-gray-800 px-1 rounded">Or</code>, <code class="text-xs bg-gray-100 dark:bg-gray-800 px-1 rounded">Not</code>.
|
||||
A filter is just a function <code class="text-xs bg-gray-100 dark:bg-gray-800 px-1 rounded">func(*api.Update) bool</code>.
|
||||
</p>
|
||||
<div class="bg-gradient-to-br from-gray-900 to-gray-800 rounded-xl p-4 text-sm text-gray-100 overflow-x-auto border border-gray-700">
|
||||
<pre class="font-mono"><code>// Only handle private messages from admins
|
||||
adminOnly := filters.And(
|
||||
filters.IsPrivate,
|
||||
filters.UserIDIn(adminIDs...),
|
||||
)
|
||||
|
||||
d.OnMessage(func(ctx context.Context, msg *api.Message) {
|
||||
// handler body
|
||||
}, adminOnly)
|
||||
|
||||
// Custom filter — any function works
|
||||
isLong := func(u *api.Update) bool {
|
||||
return u.Message != nil && len(u.Message.Text) > 200
|
||||
}
|
||||
d.OnMessage(handleLongMsg, isLong)</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Custom HTTP/JSON -->
|
||||
<details class="glass rounded-xl overflow-hidden group">
|
||||
<summary class="flex items-center justify-between p-5 cursor-pointer list-none select-none">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-9 h-9 rounded-lg bg-gradient-to-br from-amber-500 to-amber-600 flex items-center justify-center flex-shrink-0">
|
||||
<i class="fas fa-plug text-white text-sm"></i>
|
||||
</div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100">Custom HTTP / JSON codec</h3>
|
||||
</div>
|
||||
<i class="fas fa-chevron-down text-gray-400 transition-transform duration-300 group-open:rotate-180"></i>
|
||||
</summary>
|
||||
<div class="px-5 pb-5 border-t border-gray-200 dark:border-gray-700">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-4 mb-4">
|
||||
Pass a <code class="text-xs bg-gray-100 dark:bg-gray-800 px-1 rounded">transport.Options</code> to swap the HTTP client or JSON codec.
|
||||
Useful for squeezing throughput on high-volume bots.
|
||||
</p>
|
||||
<div class="bg-gradient-to-br from-gray-900 to-gray-800 rounded-xl p-4 text-sm text-gray-100 overflow-x-auto border border-gray-700">
|
||||
<pre class="font-mono"><code>import (
|
||||
"github.com/lukaszraczylo/go-telegram/client"
|
||||
"github.com/lukaszraczylo/go-telegram/transport"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
opts := &client.Options{
|
||||
Transport: transport.NewFasthttpTransport(nil),
|
||||
Codec: transport.JSONCodec(jsoniter.ConfigCompatibleWithStandardLibrary),
|
||||
}
|
||||
|
||||
bot, err := client.NewRetryDoer(token, opts)</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="bg-gray-900 dark:bg-black text-gray-400 py-8 sm:py-10 theme-transition">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6">
|
||||
<div class="grid grid-cols-2 sm:grid-cols-2 md:grid-cols-4 gap-6 sm:gap-8">
|
||||
<div class="col-span-2 sm:col-span-1">
|
||||
<img src="logo-dark.svg" alt="go-telegram logo" class="h-10 sm:h-12 w-auto mb-3 sm:mb-4" />
|
||||
<p class="text-xs sm:text-sm">Fully-generated, strongly-typed Go client for the Telegram Bot API.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-white font-semibold mb-3 sm:mb-4 text-sm sm:text-base">Links</h3>
|
||||
<ul class="space-y-1.5 sm:space-y-2 text-xs sm:text-sm">
|
||||
<li><a href="https://github.com/lukaszraczylo/go-telegram" target="_blank" class="hover:text-white transition"><i class="fab fa-github mr-2"></i>GitHub</a></li>
|
||||
<li><a href="https://github.com/lukaszraczylo/go-telegram/issues" target="_blank" class="hover:text-white transition"><i class="fas fa-bug mr-2"></i>Issues</a></li>
|
||||
<li><a href="https://github.com/lukaszraczylo/go-telegram/releases" target="_blank" class="hover:text-white transition"><i class="fas fa-tag mr-2"></i>Releases</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-white font-semibold mb-3 sm:mb-4 text-sm sm:text-base">Docs</h3>
|
||||
<ul class="space-y-1.5 sm:space-y-2 text-xs sm:text-sm">
|
||||
<li><a href="https://pkg.go.dev/github.com/lukaszraczylo/go-telegram" target="_blank" class="hover:text-white transition"><i class="fas fa-book mr-1"></i> pkg.go.dev</a></li>
|
||||
<li><a href="https://core.telegram.org/bots/api" target="_blank" class="hover:text-white transition">Telegram Bot API</a></li>
|
||||
<li><a href="https://github.com/lukaszraczylo/go-telegram#readme" target="_blank" class="hover:text-white transition">README</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-span-2 sm:col-span-1">
|
||||
<h3 class="text-white font-semibold mb-3 sm:mb-4 text-sm sm:text-base">Sibling projects</h3>
|
||||
<ul class="space-y-1.5 sm:space-y-2 text-xs sm:text-sm">
|
||||
<li><a href="https://kportal.raczylo.com" target="_blank" class="hover:text-white transition"><i class="fas fa-dharmachakra mr-2"></i>kportal</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 sm:mt-8 pt-6 sm:pt-8 border-t border-gray-800 text-center text-xs sm:text-sm">
|
||||
<p>Made by <a href="https://github.com/lukaszraczylo" class="text-sky-400 hover:text-sky-300 transition">Lukasz Raczylo</a></p>
|
||||
<p class="mt-1.5 sm:mt-2">MIT License</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Mobile menu toggle
|
||||
const mobileMenuToggle = document.getElementById("mobile-menu-toggle");
|
||||
const mobileMenu = document.getElementById("mobile-menu");
|
||||
const menuOpenIcon = document.getElementById("menu-open-icon");
|
||||
const menuCloseIcon = document.getElementById("menu-close-icon");
|
||||
|
||||
mobileMenuToggle.addEventListener("click", () => {
|
||||
mobileMenu.classList.toggle("hidden");
|
||||
menuOpenIcon.classList.toggle("hidden");
|
||||
menuCloseIcon.classList.toggle("hidden");
|
||||
});
|
||||
|
||||
const mobileMenuLinks = mobileMenu.querySelectorAll("a");
|
||||
mobileMenuLinks.forEach(link => {
|
||||
link.addEventListener("click", () => {
|
||||
mobileMenu.classList.add("hidden");
|
||||
menuOpenIcon.classList.remove("hidden");
|
||||
menuCloseIcon.classList.add("hidden");
|
||||
});
|
||||
});
|
||||
|
||||
// Theme toggle
|
||||
const themeToggle = document.getElementById("theme-toggle");
|
||||
themeToggle.addEventListener("click", () => {
|
||||
if (document.documentElement.classList.contains("dark")) {
|
||||
document.documentElement.classList.remove("dark");
|
||||
localStorage.theme = "light";
|
||||
} else {
|
||||
document.documentElement.classList.add("dark");
|
||||
localStorage.theme = "dark";
|
||||
}
|
||||
});
|
||||
|
||||
// Copy to clipboard
|
||||
function copyToClipboard(text, button) {
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(() => showCopySuccess(button)).catch(() => fallbackCopy(text, button));
|
||||
} else {
|
||||
fallbackCopy(text, button);
|
||||
}
|
||||
}
|
||||
|
||||
function fallbackCopy(text, button) {
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = text;
|
||||
textarea.style.position = "fixed";
|
||||
textarea.style.opacity = "0";
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
try {
|
||||
document.execCommand("copy") ? showCopySuccess(button) : showCopyError(button);
|
||||
} catch (err) {
|
||||
showCopyError(button);
|
||||
}
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
|
||||
function showCopySuccess(button) {
|
||||
const original = button.innerHTML;
|
||||
button.innerHTML = '<i class="fas fa-check text-green-500"></i>';
|
||||
setTimeout(() => button.innerHTML = original, 2000);
|
||||
}
|
||||
|
||||
function showCopyError(button) {
|
||||
const original = button.innerHTML;
|
||||
button.innerHTML = '<i class="fas fa-times text-red-500"></i>';
|
||||
setTimeout(() => button.innerHTML = original, 2000);
|
||||
}
|
||||
|
||||
// Smooth scrolling
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener("click", function(e) {
|
||||
e.preventDefault();
|
||||
const target = document.querySelector(this.getAttribute("href"));
|
||||
if (target) target.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 56" fill="none">
|
||||
<!-- Paper plane glyph in Telegram blue gradient -->
|
||||
<defs>
|
||||
<linearGradient id="planeGradD" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#29B6F6"/>
|
||||
<stop offset="100%" stop-color="#4FC3F7"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- Plane body -->
|
||||
<polygon points="4,28 44,12 36,44" fill="url(#planeGradD)" opacity="0.15"/>
|
||||
<polygon points="4,28 44,12 36,44" stroke="url(#planeGradD)" stroke-width="1.5" fill="none"/>
|
||||
<!-- Plane wings / send arrow -->
|
||||
<path d="M4 28 L44 12 L30 30 Z" fill="url(#planeGradD)"/>
|
||||
<path d="M30 30 L36 44 L24 34 Z" fill="url(#planeGradD)" opacity="0.7"/>
|
||||
<!-- Fold line -->
|
||||
<line x1="30" y1="30" x2="24" y2="34" stroke="white" stroke-width="1" opacity="0.8"/>
|
||||
<!-- Wordmark -->
|
||||
<text x="56" y="35" font-family="Inter, sans-serif" font-weight="700" font-size="20" fill="#e5e7eb" letter-spacing="-0.5">go-telegram</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 977 B |
@@ -0,0 +1,19 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 56" fill="none">
|
||||
<!-- Paper plane glyph in Telegram blue gradient -->
|
||||
<defs>
|
||||
<linearGradient id="planeGradL" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#29B6F6"/>
|
||||
<stop offset="100%" stop-color="#0288D1"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- Plane body -->
|
||||
<polygon points="4,28 44,12 36,44" fill="url(#planeGradL)" opacity="0.15"/>
|
||||
<polygon points="4,28 44,12 36,44" stroke="url(#planeGradL)" stroke-width="1.5" fill="none"/>
|
||||
<!-- Plane wings / send arrow -->
|
||||
<path d="M4 28 L44 12 L30 30 Z" fill="url(#planeGradL)"/>
|
||||
<path d="M30 30 L36 44 L24 34 Z" fill="url(#planeGradL)" opacity="0.7"/>
|
||||
<!-- Fold line -->
|
||||
<line x1="30" y1="30" x2="24" y2="34" stroke="white" stroke-width="1" opacity="0.8"/>
|
||||
<!-- Wordmark -->
|
||||
<text x="56" y="35" font-family="Inter, sans-serif" font-weight="700" font-size="20" fill="#1f2937" letter-spacing="-0.5">go-telegram</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 977 B |
@@ -0,0 +1,25 @@
|
||||
# API Reference
|
||||
|
||||
Auto-generated from Go source comments by [gomarkdoc](https://github.com/princjef/gomarkdoc). Do not edit by hand — run `make docs` to regenerate.
|
||||
|
||||
## Packages
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`api`](api.md) | Telegram Bot API types and method wrappers — 176 methods, 301 types, fully generated |
|
||||
| [`client`](client.md) | Bot client, codec, HTTP doer, retry middleware |
|
||||
| [`transport`](transport.md) | Long-poll and webhook transports |
|
||||
| [`dispatch`](dispatch.md) | Update router, filters, handler groups, named handlers |
|
||||
| [`dispatch/conversation`](dispatch/conversation.md) | Multi-step conversation state machines |
|
||||
| [`dispatch/filters/message`](dispatch/filters/message.md) | Message filters — `Command`, `Text`, `IsReply`, etc. |
|
||||
| [`dispatch/filters/callback`](dispatch/filters/callback.md) | Callback query filters |
|
||||
| [`dispatch/filters/inline`](dispatch/filters/inline.md) | Inline query filters |
|
||||
| [`dispatch/filters/chatmember`](dispatch/filters/chatmember.md) | Chat member update filters |
|
||||
| [`dispatch/filters/chatjoinrequest`](dispatch/filters/chatjoinrequest.md) | Join request filters |
|
||||
| [`dispatch/filters/precheckoutquery`](dispatch/filters/precheckoutquery.md) | Pre-checkout filters for payments |
|
||||
|
||||
## Also see
|
||||
|
||||
- [Project home](../index.html) — landing page with examples and overview
|
||||
- [GitHub repository](https://github.com/lukaszraczylo/go-telegram)
|
||||
- [pkg.go.dev](https://pkg.go.dev/github.com/lukaszraczylo/go-telegram) — official Go package documentation
|
||||
+16100
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,683 @@
|
||||
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
|
||||
|
||||
# client
|
||||
|
||||
```go
|
||||
import "github.com/lukaszraczylo/go-telegram/client"
|
||||
```
|
||||
|
||||
Package client provides HTTP client primitives for the Telegram Bot API.
|
||||
|
||||
## Index
|
||||
|
||||
- [Variables](<#variables>)
|
||||
- [func Call\[Req any, Resp any\]\(ctx context.Context, b \*Bot, method string, req Req\) \(Resp, error\)](<#Call>)
|
||||
- [func CallRaw\[Req any\]\(ctx context.Context, b \*Bot, method string, req Req\) \(json.RawMessage, error\)](<#CallRaw>)
|
||||
- [func NewDefaultHTTPDoer\(\) \*http.Client](<#NewDefaultHTTPDoer>)
|
||||
- [type APIError](<#APIError>)
|
||||
- [func \(e \*APIError\) Error\(\) string](<#APIError.Error>)
|
||||
- [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>)
|
||||
- [func \(b \*Bot\) Codec\(\) Codec](<#Bot.Codec>)
|
||||
- [func \(b \*Bot\) HTTP\(\) HTTPDoer](<#Bot.HTTP>)
|
||||
- [func \(b \*Bot\) Logger\(\) Logger](<#Bot.Logger>)
|
||||
- [func \(b \*Bot\) Token\(\) string](<#Bot.Token>)
|
||||
- [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>)
|
||||
- [type NetworkError](<#NetworkError>)
|
||||
- [func \(e \*NetworkError\) Error\(\) string](<#NetworkError.Error>)
|
||||
- [func \(e \*NetworkError\) Unwrap\(\) error](<#NetworkError.Unwrap>)
|
||||
- [type NoopLogger](<#NoopLogger>)
|
||||
- [func \(NoopLogger\) Debug\(string, ...any\)](<#NoopLogger.Debug>)
|
||||
- [func \(NoopLogger\) Error\(string, ...any\)](<#NoopLogger.Error>)
|
||||
- [func \(NoopLogger\) Info\(string, ...any\)](<#NoopLogger.Info>)
|
||||
- [func \(NoopLogger\) Warn\(string, ...any\)](<#NoopLogger.Warn>)
|
||||
- [type Option](<#Option>)
|
||||
- [func WithBaseURL\(url string\) Option](<#WithBaseURL>)
|
||||
- [func WithCodec\(c Codec\) Option](<#WithCodec>)
|
||||
- [func WithHTTPClient\(c HTTPDoer\) Option](<#WithHTTPClient>)
|
||||
- [func WithLogger\(l Logger\) Option](<#WithLogger>)
|
||||
- [type ParseError](<#ParseError>)
|
||||
- [func \(e \*ParseError\) Error\(\) string](<#ParseError.Error>)
|
||||
- [func \(e \*ParseError\) Unwrap\(\) error](<#ParseError.Unwrap>)
|
||||
- [type ResponseParameters](<#ResponseParameters>)
|
||||
- [type Result](<#Result>)
|
||||
- [type RetryDoer](<#RetryDoer>)
|
||||
- [func NewRetryDoer\(inner HTTPDoer, opts ...RetryOption\) \*RetryDoer](<#NewRetryDoer>)
|
||||
- [func \(d \*RetryDoer\) Do\(req \*http.Request\) \(\*http.Response, error\)](<#RetryDoer.Do>)
|
||||
- [type RetryOption](<#RetryOption>)
|
||||
- [func WithBackoffFactor\(f float64\) RetryOption](<#WithBackoffFactor>)
|
||||
- [func WithBaseBackoff\(d time.Duration\) RetryOption](<#WithBaseBackoff>)
|
||||
- [func WithJitter\(j float64\) RetryOption](<#WithJitter>)
|
||||
- [func WithMaxAttempts\(n int\) RetryOption](<#WithMaxAttempts>)
|
||||
- [func WithMaxBackoff\(d time.Duration\) RetryOption](<#WithMaxBackoff>)
|
||||
|
||||
|
||||
## Variables
|
||||
|
||||
<a name="ErrUnauthorized"></a>Sentinel errors returned via APIError.Unwrap when the description matches. Compare with errors.Is.
|
||||
|
||||
```go
|
||||
var (
|
||||
ErrUnauthorized = errors.New("telegram: unauthorized")
|
||||
ErrChatNotFound = errors.New("telegram: chat not found")
|
||||
ErrMessageNotModified = errors.New("telegram: message is not modified")
|
||||
ErrTooManyRequests = errors.New("telegram: too many requests")
|
||||
ErrBadRequest = errors.New("telegram: bad request")
|
||||
ErrForbidden = errors.New("telegram: forbidden")
|
||||
ErrUserNotFound = errors.New("telegram: user not found")
|
||||
ErrMessageNotFound = errors.New("telegram: message not found")
|
||||
)
|
||||
```
|
||||
|
||||
<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#L68>)
|
||||
|
||||
```go
|
||||
func Call[Req any, Resp any](ctx context.Context, b *Bot, method string, req Req) (Resp, error)
|
||||
```
|
||||
|
||||
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 translates non\-OK responses into typed errors.
|
||||
|
||||
It is generic over both request and response types. Methods with no parameters may pass a nil Req; the helper sends "\{\}" in that case so Telegram receives a syntactically valid empty object.
|
||||
|
||||
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#L119>)
|
||||
|
||||
```go
|
||||
func CallRaw[Req any](ctx context.Context, b *Bot, method string, req Req) (json.RawMessage, error)
|
||||
```
|
||||
|
||||
CallRaw is like Call but returns the raw JSON of the result field instead of decoding it into a typed value. Generated method wrappers for sealed\-interface return types \(ChatMember, MenuButton, etc.\) use this helper, then dispatch through the union's UnmarshalXxx function.
|
||||
|
||||
CallRaw still translates non\-OK responses into \*APIError just like Call.
|
||||
|
||||
<a name="NewDefaultHTTPDoer"></a>
|
||||
## func [NewDefaultHTTPDoer](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/httpclient.go#L22>)
|
||||
|
||||
```go
|
||||
func NewDefaultHTTPDoer() *http.Client
|
||||
```
|
||||
|
||||
NewDefaultHTTPDoer returns an \*http.Client with sensible defaults for Telegram Bot API usage:
|
||||
|
||||
- 60s overall timeout \(longer than typical long\-poll Timeout=30s\).
|
||||
- Connection pooling sized for a small number of long\-lived hosts.
|
||||
- HTTP/2 enabled \(default in net/http\).
|
||||
|
||||
<a name="APIError"></a>
|
||||
## type [APIError](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/errors.go#L13-L21>)
|
||||
|
||||
APIError represents a non\-OK Telegram Bot API response. It satisfies error and unwraps to a sentinel \(ErrUnauthorized, etc.\) where the description matches a known prefix, enabling errors.Is checks.
|
||||
|
||||
```go
|
||||
type APIError struct {
|
||||
Code int
|
||||
Description string
|
||||
Parameters *ResponseParameters
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
```
|
||||
|
||||
<a name="APIError.Error"></a>
|
||||
### func \(\*APIError\) [Error](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/errors.go#L24>)
|
||||
|
||||
```go
|
||||
func (e *APIError) Error() string
|
||||
```
|
||||
|
||||
Error implements error.
|
||||
|
||||
<a name="APIError.IsRetryable"></a>
|
||||
### func \(\*APIError\) [IsRetryable](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/errors.go#L32>)
|
||||
|
||||
```go
|
||||
func (e *APIError) IsRetryable() bool
|
||||
```
|
||||
|
||||
IsRetryable returns true for transient HTTP statuses \(429, 5xx\).
|
||||
|
||||
<a name="APIError.RetryAfter"></a>
|
||||
### func \(\*APIError\) [RetryAfter](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/errors.go#L38>)
|
||||
|
||||
```go
|
||||
func (e *APIError) RetryAfter() time.Duration
|
||||
```
|
||||
|
||||
RetryAfter returns the recommended back\-off duration. It honours the Telegram\-supplied retry\_after parameter; if absent, returns 0.
|
||||
|
||||
<a name="APIError.Unwrap"></a>
|
||||
### func \(\*APIError\) [Unwrap](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/errors.go#L29>)
|
||||
|
||||
```go
|
||||
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#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.
|
||||
|
||||
```go
|
||||
type Bot struct {
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
```
|
||||
|
||||
<a name="New"></a>
|
||||
### func [New](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L47>)
|
||||
|
||||
```go
|
||||
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#L31>)
|
||||
|
||||
```go
|
||||
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#L38>)
|
||||
|
||||
```go
|
||||
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#L35>)
|
||||
|
||||
```go
|
||||
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#L41>)
|
||||
|
||||
```go
|
||||
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#L28>)
|
||||
|
||||
```go
|
||||
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#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.
|
||||
|
||||
```go
|
||||
type Codec interface {
|
||||
Marshal(v any) ([]byte, error)
|
||||
Unmarshal(data []byte, v any) error
|
||||
}
|
||||
```
|
||||
|
||||
<a name="DefaultCodec"></a>
|
||||
## 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.
|
||||
|
||||
```go
|
||||
type DefaultCodec struct{}
|
||||
```
|
||||
|
||||
<a name="DefaultCodec.Marshal"></a>
|
||||
### func \(DefaultCodec\) [Marshal](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#L32>)
|
||||
|
||||
```go
|
||||
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#L35>)
|
||||
|
||||
```go
|
||||
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>)
|
||||
|
||||
HTTPDoer abstracts the HTTP transport. The default is a net/http client tuned for Telegram's long\-poll usage. Users may plug in valyala/fasthttp \(via an adapter\), or any custom retry/circuit\-breaker client by passing WithHTTPClient to New.
|
||||
|
||||
```go
|
||||
type HTTPDoer interface {
|
||||
Do(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
```
|
||||
|
||||
<a name="Logger"></a>
|
||||
## type [Logger](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/logger.go#L6-L11>)
|
||||
|
||||
Logger is a slog\-shaped logging interface. Users pass any compatible implementation via WithLogger. The default is NoopLogger, which discards everything.
|
||||
|
||||
```go
|
||||
type Logger interface {
|
||||
Debug(msg string, attrs ...any)
|
||||
Info(msg string, attrs ...any)
|
||||
Warn(msg string, attrs ...any)
|
||||
Error(msg string, attrs ...any)
|
||||
}
|
||||
```
|
||||
|
||||
<a name="MultipartFile"></a>
|
||||
## 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.
|
||||
|
||||
```go
|
||||
type MultipartFile struct {
|
||||
FieldName string
|
||||
Filename string
|
||||
Reader io.Reader
|
||||
}
|
||||
```
|
||||
|
||||
<a name="NetworkError"></a>
|
||||
## type [NetworkError](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/errors.go#L47>)
|
||||
|
||||
NetworkError wraps a transport\-level failure \(DNS, TCP, TLS, timeout short of an HTTP response\).
|
||||
|
||||
```go
|
||||
type NetworkError struct{ Err error }
|
||||
```
|
||||
|
||||
<a name="NetworkError.Error"></a>
|
||||
### func \(\*NetworkError\) [Error](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/errors.go#L49>)
|
||||
|
||||
```go
|
||||
func (e *NetworkError) Error() string
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="NetworkError.Unwrap"></a>
|
||||
### func \(\*NetworkError\) [Unwrap](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/errors.go#L51>)
|
||||
|
||||
```go
|
||||
func (e *NetworkError) Unwrap() error
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="NoopLogger"></a>
|
||||
## type [NoopLogger](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/logger.go#L14>)
|
||||
|
||||
NoopLogger discards all log records. It is the zero\-value safe default.
|
||||
|
||||
```go
|
||||
type NoopLogger struct{}
|
||||
```
|
||||
|
||||
<a name="NoopLogger.Debug"></a>
|
||||
### func \(NoopLogger\) [Debug](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/logger.go#L16>)
|
||||
|
||||
```go
|
||||
func (NoopLogger) Debug(string, ...any)
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="NoopLogger.Error"></a>
|
||||
### func \(NoopLogger\) [Error](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/logger.go#L19>)
|
||||
|
||||
```go
|
||||
func (NoopLogger) Error(string, ...any)
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="NoopLogger.Info"></a>
|
||||
### func \(NoopLogger\) [Info](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/logger.go#L17>)
|
||||
|
||||
```go
|
||||
func (NoopLogger) Info(string, ...any)
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="NoopLogger.Warn"></a>
|
||||
### func \(NoopLogger\) [Warn](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/logger.go#L18>)
|
||||
|
||||
```go
|
||||
func (NoopLogger) Warn(string, ...any)
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="Option"></a>
|
||||
## type [Option](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/options.go#L5>)
|
||||
|
||||
Option configures a Bot at construction time. Per\-call configuration is expressed via typed parameter structs \(e.g. SendMessageParams\), not options.
|
||||
|
||||
```go
|
||||
type Option func(*Bot)
|
||||
```
|
||||
|
||||
<a name="WithBaseURL"></a>
|
||||
### func [WithBaseURL](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/options.go#L18>)
|
||||
|
||||
```go
|
||||
func WithBaseURL(url string) Option
|
||||
```
|
||||
|
||||
WithBaseURL overrides the API base URL. Useful for testing against a local httptest.Server, or for self\-hosted Bot API servers.
|
||||
|
||||
<a name="WithCodec"></a>
|
||||
### func [WithCodec](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/options.go#L14>)
|
||||
|
||||
```go
|
||||
func WithCodec(c Codec) Option
|
||||
```
|
||||
|
||||
WithCodec overrides the JSON codec. Pass goccy/go\-json, sonic, or any type implementing Codec to swap out encoding/json.
|
||||
|
||||
<a name="WithHTTPClient"></a>
|
||||
### func [WithHTTPClient](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/options.go#L10>)
|
||||
|
||||
```go
|
||||
func WithHTTPClient(c HTTPDoer) Option
|
||||
```
|
||||
|
||||
WithHTTPClient overrides the HTTP transport. Pass any HTTPDoer implementation \(e.g. an \*http.Client wrapping a custom RoundTripper, or a fasthttp adapter\).
|
||||
|
||||
<a name="WithLogger"></a>
|
||||
### func [WithLogger](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/options.go#L22>)
|
||||
|
||||
```go
|
||||
func WithLogger(l Logger) Option
|
||||
```
|
||||
|
||||
WithLogger sets the logger used for diagnostic events. Passing nil silently disables logging.
|
||||
|
||||
<a name="ParseError"></a>
|
||||
## type [ParseError](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/errors.go#L55-L58>)
|
||||
|
||||
ParseError wraps a JSON decode failure on a response body. Body is retained \(truncated to 4 KiB\); Error\(\) displays up to 256 bytes for diagnostics.
|
||||
|
||||
```go
|
||||
type ParseError struct {
|
||||
Err error
|
||||
Body []byte
|
||||
}
|
||||
```
|
||||
|
||||
<a name="ParseError.Error"></a>
|
||||
### func \(\*ParseError\) [Error](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/errors.go#L60>)
|
||||
|
||||
```go
|
||||
func (e *ParseError) Error() string
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="ParseError.Unwrap"></a>
|
||||
### func \(\*ParseError\) [Unwrap](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/errors.go#L68>)
|
||||
|
||||
```go
|
||||
func (e *ParseError) Unwrap() error
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="ResponseParameters"></a>
|
||||
## type [ResponseParameters](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/result.go#L24-L27>)
|
||||
|
||||
ResponseParameters is the optional metadata Telegram includes on certain failures. The most common is RetryAfter \(seconds\) on 429 responses.
|
||||
|
||||
This type is duplicated in package api for users; keeping a copy here avoids an import cycle \(api imports client, not vice versa\).
|
||||
|
||||
```go
|
||||
type ResponseParameters struct {
|
||||
MigrateToChatID int64 `json:"migrate_to_chat_id,omitempty"`
|
||||
RetryAfter int `json:"retry_after,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
<a name="Result"></a>
|
||||
## type [Result](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/result.go#L11-L17>)
|
||||
|
||||
Result is the universal Telegram API response envelope. Every successful response is shaped \{"ok":true,"result":T,...\}; failure responses set ok to false and populate ErrorCode / Description / Parameters.
|
||||
|
||||
Result is generic over T so generated method wrappers can decode the strongly\-typed payload directly. Users do not normally construct or inspect Result values; method wrappers unwrap them and return either the typed payload or a \*APIError.
|
||||
|
||||
```go
|
||||
type Result[T any] struct {
|
||||
OK bool `json:"ok"`
|
||||
Result T `json:"result,omitempty"`
|
||||
ErrorCode int `json:"error_code,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Parameters *ResponseParameters `json:"parameters,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
<a name="RetryDoer"></a>
|
||||
## type [RetryDoer](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/retry.go#L23-L30>)
|
||||
|
||||
RetryDoer is an HTTPDoer that retries transient failures \(429, 5xx, and network errors\) with exponential backoff. It honours the retry\_after value Telegram supplies on rate\-limit responses.
|
||||
|
||||
Wrap any HTTPDoer to add retry behaviour:
|
||||
|
||||
```
|
||||
bot := client.New(token, client.WithHTTPClient(
|
||||
client.NewRetryDoer(client.NewDefaultHTTPDoer())))
|
||||
```
|
||||
|
||||
```go
|
||||
type RetryDoer struct {
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
```
|
||||
|
||||
<a name="NewRetryDoer"></a>
|
||||
### func [NewRetryDoer](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/retry.go#L63>)
|
||||
|
||||
```go
|
||||
func NewRetryDoer(inner HTTPDoer, opts ...RetryOption) *RetryDoer
|
||||
```
|
||||
|
||||
NewRetryDoer wraps inner with retry behaviour.
|
||||
|
||||
<a name="RetryDoer.Do"></a>
|
||||
### func \(\*RetryDoer\) [Do](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/retry.go#L80>)
|
||||
|
||||
```go
|
||||
func (d *RetryDoer) Do(req *http.Request) (*http.Response, error)
|
||||
```
|
||||
|
||||
Do dispatches via the inner HTTPDoer and retries on transient failures. The request body is buffered on first attempt so it can be replayed.
|
||||
|
||||
<a name="RetryOption"></a>
|
||||
## type [RetryOption](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/retry.go#L33>)
|
||||
|
||||
RetryOption configures a RetryDoer.
|
||||
|
||||
```go
|
||||
type RetryOption func(*RetryDoer)
|
||||
```
|
||||
|
||||
<a name="WithBackoffFactor"></a>
|
||||
### func [WithBackoffFactor](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/retry.go#L52>)
|
||||
|
||||
```go
|
||||
func WithBackoffFactor(f float64) RetryOption
|
||||
```
|
||||
|
||||
WithBackoffFactor sets the exponential growth factor. Default 2.0.
|
||||
|
||||
<a name="WithBaseBackoff"></a>
|
||||
### func [WithBaseBackoff](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/retry.go#L42>)
|
||||
|
||||
```go
|
||||
func WithBaseBackoff(d time.Duration) RetryOption
|
||||
```
|
||||
|
||||
WithBaseBackoff sets the initial backoff duration. Default 500ms.
|
||||
|
||||
<a name="WithJitter"></a>
|
||||
### func [WithJitter](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/retry.go#L58>)
|
||||
|
||||
```go
|
||||
func WithJitter(j float64) RetryOption
|
||||
```
|
||||
|
||||
WithJitter sets the jitter fraction \(0..1\) applied to each backoff. Default 0.2.
|
||||
|
||||
<a name="WithMaxAttempts"></a>
|
||||
### func [WithMaxAttempts](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/retry.go#L37>)
|
||||
|
||||
```go
|
||||
func WithMaxAttempts(n int) RetryOption
|
||||
```
|
||||
|
||||
WithMaxAttempts sets the maximum number of attempts \(including the initial one\). Default 4 \(one initial \+ three retries\).
|
||||
|
||||
<a name="WithMaxBackoff"></a>
|
||||
### func [WithMaxBackoff](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/retry.go#L47>)
|
||||
|
||||
```go
|
||||
func WithMaxBackoff(d time.Duration) RetryOption
|
||||
```
|
||||
|
||||
WithMaxBackoff caps the backoff at max. Default 30s.
|
||||
|
||||
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)
|
||||
@@ -0,0 +1,695 @@
|
||||
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
|
||||
|
||||
# dispatch
|
||||
|
||||
```go
|
||||
import "github.com/lukaszraczylo/go-telegram/dispatch"
|
||||
```
|
||||
|
||||
Package dispatch provides a typed router for Telegram updates. It consumes any transport.Updater and dispatches updates to handlers registered by command, regex, or update\-payload kind.
|
||||
|
||||
## Index
|
||||
|
||||
- [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>)
|
||||
- [func \(f Filter\[T\]\) And\(others ...Filter\[T\]\) Filter\[T\]](<#Filter[T].And>)
|
||||
- [func \(f Filter\[T\]\) Not\(\) Filter\[T\]](<#Filter[T].Not>)
|
||||
- [func \(f Filter\[T\]\) Or\(others ...Filter\[T\]\) Filter\[T\]](<#Filter[T].Or>)
|
||||
- [type Handler](<#Handler>)
|
||||
- [type Middleware](<#Middleware>)
|
||||
- [func Chain\[T any\]\(mws ...Middleware\[T\]\) Middleware\[T\]](<#Chain>)
|
||||
- [func Recovery\(\) Middleware\[\*api.Update\]](<#Recovery>)
|
||||
- [type NamedHandlers](<#NamedHandlers>)
|
||||
- [func NewNamedHandlers\[T any\]\(\) \*NamedHandlers\[T\]](<#NewNamedHandlers>)
|
||||
- [func \(n \*NamedHandlers\[T\]\) Handler\(\) Handler\[T\]](<#NamedHandlers[T].Handler>)
|
||||
- [func \(n \*NamedHandlers\[T\]\) Has\(name string\) bool](<#NamedHandlers[T].Has>)
|
||||
- [func \(n \*NamedHandlers\[T\]\) Names\(\) \[\]string](<#NamedHandlers[T].Names>)
|
||||
- [func \(n \*NamedHandlers\[T\]\) Remove\(name string\) bool](<#NamedHandlers[T].Remove>)
|
||||
- [func \(n \*NamedHandlers\[T\]\) Set\(name string, h Handler\[T\]\)](<#NamedHandlers[T].Set>)
|
||||
- [type Router](<#Router>)
|
||||
- [func New\(b \*client.Bot, opts ...RouterOption\) \*Router](<#New>)
|
||||
- [func \(r \*Router\) Group\(group int\) \*RouterScope](<#Router.Group>)
|
||||
- [func \(r \*Router\) OnBusinessConnection\(h Handler\[\*api.BusinessConnection\]\)](<#Router.OnBusinessConnection>)
|
||||
- [func \(r \*Router\) OnCallback\(pattern string, h Handler\[\*api.CallbackQuery\]\)](<#Router.OnCallback>)
|
||||
- [func \(r \*Router\) OnCallbackFilter\(f Filter\[\*api.CallbackQuery\], h Handler\[\*api.CallbackQuery\]\)](<#Router.OnCallbackFilter>)
|
||||
- [func \(r \*Router\) OnChannelPost\(h Handler\[\*api.Message\]\)](<#Router.OnChannelPost>)
|
||||
- [func \(r \*Router\) OnChatBoost\(h Handler\[\*api.ChatBoostUpdated\]\)](<#Router.OnChatBoost>)
|
||||
- [func \(r \*Router\) OnChatJoinRequest\(h Handler\[\*api.ChatJoinRequest\]\)](<#Router.OnChatJoinRequest>)
|
||||
- [func \(r \*Router\) OnChatJoinRequestFilter\(f Filter\[\*api.ChatJoinRequest\], h Handler\[\*api.ChatJoinRequest\]\)](<#Router.OnChatJoinRequestFilter>)
|
||||
- [func \(r \*Router\) OnChatMember\(h Handler\[\*api.ChatMemberUpdated\]\)](<#Router.OnChatMember>)
|
||||
- [func \(r \*Router\) OnChatMemberFilter\(f Filter\[\*api.ChatMemberUpdated\], h Handler\[\*api.ChatMemberUpdated\]\)](<#Router.OnChatMemberFilter>)
|
||||
- [func \(r \*Router\) OnChosenInlineResult\(h Handler\[\*api.ChosenInlineResult\]\)](<#Router.OnChosenInlineResult>)
|
||||
- [func \(r \*Router\) OnCommand\(cmd string, h Handler\[\*api.Message\]\)](<#Router.OnCommand>)
|
||||
- [func \(r \*Router\) OnEditedChannelPost\(h Handler\[\*api.Message\]\)](<#Router.OnEditedChannelPost>)
|
||||
- [func \(r \*Router\) OnEditedMessage\(h Handler\[\*api.Message\]\)](<#Router.OnEditedMessage>)
|
||||
- [func \(r \*Router\) OnInlineQuery\(h Handler\[\*api.InlineQuery\]\)](<#Router.OnInlineQuery>)
|
||||
- [func \(r \*Router\) OnInlineQueryFilter\(f Filter\[\*api.InlineQuery\], h Handler\[\*api.InlineQuery\]\)](<#Router.OnInlineQueryFilter>)
|
||||
- [func \(r \*Router\) OnMessageFilter\(f Filter\[\*api.Message\], h Handler\[\*api.Message\]\)](<#Router.OnMessageFilter>)
|
||||
- [func \(r \*Router\) OnMessageReaction\(h Handler\[\*api.MessageReactionUpdated\]\)](<#Router.OnMessageReaction>)
|
||||
- [func \(r \*Router\) OnMessageReactionCount\(h Handler\[\*api.MessageReactionCountUpdated\]\)](<#Router.OnMessageReactionCount>)
|
||||
- [func \(r \*Router\) OnMyChatMember\(h Handler\[\*api.ChatMemberUpdated\]\)](<#Router.OnMyChatMember>)
|
||||
- [func \(r \*Router\) OnMyChatMemberFilter\(f Filter\[\*api.ChatMemberUpdated\], h Handler\[\*api.ChatMemberUpdated\]\)](<#Router.OnMyChatMemberFilter>)
|
||||
- [func \(r \*Router\) OnPoll\(h Handler\[\*api.Poll\]\)](<#Router.OnPoll>)
|
||||
- [func \(r \*Router\) OnPollAnswer\(h Handler\[\*api.PollAnswer\]\)](<#Router.OnPollAnswer>)
|
||||
- [func \(r \*Router\) OnPreCheckoutQuery\(h Handler\[\*api.PreCheckoutQuery\]\)](<#Router.OnPreCheckoutQuery>)
|
||||
- [func \(r \*Router\) OnPreCheckoutQueryFilter\(f Filter\[\*api.PreCheckoutQuery\], h Handler\[\*api.PreCheckoutQuery\]\)](<#Router.OnPreCheckoutQueryFilter>)
|
||||
- [func \(r \*Router\) OnPurchasedPaidMedia\(h Handler\[\*api.PaidMediaPurchased\]\)](<#Router.OnPurchasedPaidMedia>)
|
||||
- [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>)
|
||||
- [func WithMaxConcurrency\(n int\) RouterOption](<#WithMaxConcurrency>)
|
||||
- [type RouterScope](<#RouterScope>)
|
||||
- [func \(s \*RouterScope\) OnCommand\(cmd string, h Handler\[\*api.Message\]\)](<#RouterScope.OnCommand>)
|
||||
- [func \(s \*RouterScope\) OnMessageFilter\(f Filter\[\*api.Message\], h Handler\[\*api.Message\]\)](<#RouterScope.OnMessageFilter>)
|
||||
- [func \(s \*RouterScope\) OnText\(pattern string, h Handler\[\*api.Message\]\)](<#RouterScope.OnText>)
|
||||
|
||||
|
||||
## Variables
|
||||
|
||||
<a name="ErrContinueGroups"></a>ErrContinueGroups signals that this group's handler should be treated as not\-matching when returned by a handler: dispatch moves on to the next handler in the same group, then to subsequent groups.
|
||||
|
||||
Without ErrContinueGroups, a non\-error return from a matched handler stops dispatch \(default first\-match\-wins semantics\).
|
||||
|
||||
```go
|
||||
var ErrContinueGroups = errors.New("dispatch: continue groups")
|
||||
```
|
||||
|
||||
<a name="ErrEndGroups"></a>ErrEndGroups stops dispatch from running any further handlers in any group for this update when returned by a handler. Use it to indicate the update has been definitively handled.
|
||||
|
||||
errors.Is\(err, ErrEndGroups\) is the canonical check, though dispatch itself recognises it by exact identity.
|
||||
|
||||
```go
|
||||
var ErrEndGroups = errors.New("dispatch: end groups")
|
||||
```
|
||||
|
||||
<a name="Context"></a>
|
||||
## type [Context](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/context.go#L41-L49>)
|
||||
|
||||
Context bundles the per\-update state every handler receives.
|
||||
|
||||
Ctx is the request context propagated from Router.Run; cancelling the run cancels every handler.
|
||||
|
||||
Bot is the API client. Handlers reply by calling api.SendMessage\(c.Ctx, c.Bot, ...\) etc.
|
||||
|
||||
Update is the raw update; payload\-typed handlers also receive a narrowed pointer to one of its sub\-fields.
|
||||
|
||||
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 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
|
||||
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#L53>)
|
||||
|
||||
```go
|
||||
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>)
|
||||
|
||||
Filter is a predicate over a typed payload \(e.g. \*api.Message\). Filters compose via And/Or/Not for multi\-condition matching.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
f := message.HasPhoto().And(message.InChat(-100123456789))
|
||||
```
|
||||
|
||||
```go
|
||||
type Filter[T any] func(payload T) bool
|
||||
```
|
||||
|
||||
<a name="All"></a>
|
||||
### func [All](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filter.go#L48>)
|
||||
|
||||
```go
|
||||
func All[T any](filters ...Filter[T]) Filter[T]
|
||||
```
|
||||
|
||||
All combines filters with AND. Returns a Filter that matches when all match. Returns a filter that always matches when filters is empty.
|
||||
|
||||
<a name="Any"></a>
|
||||
### func [Any](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filter.go#L61>)
|
||||
|
||||
```go
|
||||
func Any[T any](filters ...Filter[T]) Filter[T]
|
||||
```
|
||||
|
||||
Any combines filters with OR. Returns a Filter that matches when at least one matches. Returns a filter that never matches when filters is empty.
|
||||
|
||||
<a name="Filter[T].And"></a>
|
||||
### func \(Filter\[T\]\) [And](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filter.go#L12>)
|
||||
|
||||
```go
|
||||
func (f Filter[T]) And(others ...Filter[T]) Filter[T]
|
||||
```
|
||||
|
||||
And returns a Filter that matches iff f and every one of others matches.
|
||||
|
||||
<a name="Filter[T].Not"></a>
|
||||
### func \(Filter\[T\]\) [Not](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filter.go#L42>)
|
||||
|
||||
```go
|
||||
func (f Filter[T]) Not() Filter[T]
|
||||
```
|
||||
|
||||
Not returns a Filter that inverts f.
|
||||
|
||||
<a name="Filter[T].Or"></a>
|
||||
### func \(Filter\[T\]\) [Or](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filter.go#L27>)
|
||||
|
||||
```go
|
||||
func (f Filter[T]) Or(others ...Filter[T]) Filter[T]
|
||||
```
|
||||
|
||||
Or returns a Filter that matches iff f matches OR any of others matches.
|
||||
|
||||
<a name="Handler"></a>
|
||||
## type [Handler](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/handler.go#L6>)
|
||||
|
||||
Handler is a generic handler over update payload type T. T is typically \*api.Message, \*api.CallbackQuery, \*api.InlineQuery, or \*api.Update for global middleware.
|
||||
|
||||
```go
|
||||
type Handler[T any] func(ctx *Context, payload T) error
|
||||
```
|
||||
|
||||
<a name="Middleware"></a>
|
||||
## type [Middleware](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/handler.go#L11>)
|
||||
|
||||
Middleware wraps a Handler\[T\] with cross\-cutting behaviour \(logging, recovery, auth\). Middleware composition is left\-to\-right: Use\(a,b,c\) runs as a\(b\(c\(handler\)\)\).
|
||||
|
||||
```go
|
||||
type Middleware[T any] func(Handler[T]) Handler[T]
|
||||
```
|
||||
|
||||
<a name="Chain"></a>
|
||||
### func [Chain](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/handler.go#L14>)
|
||||
|
||||
```go
|
||||
func Chain[T any](mws ...Middleware[T]) Middleware[T]
|
||||
```
|
||||
|
||||
Chain composes a slice of middleware into a single Middleware\[T\].
|
||||
|
||||
<a name="Recovery"></a>
|
||||
### func [Recovery](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/middleware.go#L13>)
|
||||
|
||||
```go
|
||||
func Recovery() Middleware[*api.Update]
|
||||
```
|
||||
|
||||
Recovery returns middleware that recovers from panics in downstream handlers, converting them into a returned error and logging via the bot's configured logger. Registered automatically by NewRouter.
|
||||
|
||||
<a name="NamedHandlers"></a>
|
||||
## type [NamedHandlers](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/named.go#L15-L19>)
|
||||
|
||||
NamedHandlers manages handlers by string name, allowing runtime registration, replacement, and removal. This complements the Router's registration methods: each registration via Named\*\(\) also gets a name for later lookup.
|
||||
|
||||
Use case: a plugin system that loads/unloads command handlers without restarting the bot.
|
||||
|
||||
```go
|
||||
type NamedHandlers[T any] struct {
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
```
|
||||
|
||||
<a name="NewNamedHandlers"></a>
|
||||
### func [NewNamedHandlers](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/named.go#L22>)
|
||||
|
||||
```go
|
||||
func NewNamedHandlers[T any]() *NamedHandlers[T]
|
||||
```
|
||||
|
||||
NewNamedHandlers returns a new, empty NamedHandlers\[T\].
|
||||
|
||||
<a name="NamedHandlers[T].Handler"></a>
|
||||
### func \(\*NamedHandlers\[T\]\) [Handler](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/named.go#L81>)
|
||||
|
||||
```go
|
||||
func (n *NamedHandlers[T]) Handler() Handler[T]
|
||||
```
|
||||
|
||||
Handler returns a single Handler\[T\] that runs each registered handler in registration order, first non\-nil error stops the chain. Use this to wire NamedHandlers into a Router.OnXxx call:
|
||||
|
||||
```
|
||||
names := dispatch.NewNamedHandlers[*api.Message]()
|
||||
names.Set("logger", loggingHandler)
|
||||
names.Set("audit", auditHandler)
|
||||
router.OnCommand("/admin", names.Handler())
|
||||
```
|
||||
|
||||
Subsequent Set/Remove calls take effect on the next dispatch.
|
||||
|
||||
<a name="NamedHandlers[T].Has"></a>
|
||||
### func \(\*NamedHandlers\[T\]\) [Has](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/named.go#L55>)
|
||||
|
||||
```go
|
||||
func (n *NamedHandlers[T]) Has(name string) bool
|
||||
```
|
||||
|
||||
Has reports whether name is registered.
|
||||
|
||||
<a name="NamedHandlers[T].Names"></a>
|
||||
### func \(\*NamedHandlers\[T\]\) [Names](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/named.go#L63>)
|
||||
|
||||
```go
|
||||
func (n *NamedHandlers[T]) Names() []string
|
||||
```
|
||||
|
||||
Names returns the registered names in registration order.
|
||||
|
||||
<a name="NamedHandlers[T].Remove"></a>
|
||||
### func \(\*NamedHandlers\[T\]\) [Remove](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/named.go#L38>)
|
||||
|
||||
```go
|
||||
func (n *NamedHandlers[T]) Remove(name string) bool
|
||||
```
|
||||
|
||||
Remove unregisters the handler under name. Returns true if it existed.
|
||||
|
||||
<a name="NamedHandlers[T].Set"></a>
|
||||
### func \(\*NamedHandlers\[T\]\) [Set](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/named.go#L28>)
|
||||
|
||||
```go
|
||||
func (n *NamedHandlers[T]) Set(name string, h Handler[T])
|
||||
```
|
||||
|
||||
Set registers or replaces the handler under name. If name is new, it is appended to the end of the registration order.
|
||||
|
||||
<a name="Router"></a>
|
||||
## type [Router](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L19-L64>)
|
||||
|
||||
Router dispatches updates from any Updater to typed handlers.
|
||||
|
||||
Matchers run in registration order; first match wins. A panic\-recovery middleware is attached automatically and runs around every dispatch.
|
||||
|
||||
```go
|
||||
type Router struct {
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
```
|
||||
|
||||
<a name="New"></a>
|
||||
### func [New](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L128>)
|
||||
|
||||
```go
|
||||
func New(b *client.Bot, opts ...RouterOption) *Router
|
||||
```
|
||||
|
||||
New constructs a Router. Recovery middleware is added by default; users can disable it by passing WithoutRecovery \(not implemented here, but the hook is in place via Use\).
|
||||
|
||||
<a name="Router.Group"></a>
|
||||
### func \(\*Router\) [Group](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/groups.go#L40>)
|
||||
|
||||
```go
|
||||
func (r *Router) Group(group int) *RouterScope
|
||||
```
|
||||
|
||||
Group returns a RouterScope that registers handlers in the given group. Group 0 \(the default\) runs first, then group 1, etc. Within a group, handlers run in registration order; the first non\-skipped match terminates dispatch unless the handler returns ErrContinueGroups.
|
||||
|
||||
<a name="Router.OnBusinessConnection"></a>
|
||||
### func \(\*Router\) [OnBusinessConnection](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L285>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnBusinessConnection(h Handler[*api.BusinessConnection])
|
||||
```
|
||||
|
||||
OnBusinessConnection registers a handler for business connection updates.
|
||||
|
||||
<a name="Router.OnCallback"></a>
|
||||
### func \(\*Router\) [OnCallback](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L161>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnCallback(pattern string, h Handler[*api.CallbackQuery])
|
||||
```
|
||||
|
||||
OnCallback registers a handler for callback queries whose Data matches the regex.
|
||||
|
||||
Panics at registration time if pattern is not a valid regular expression.
|
||||
|
||||
<a name="Router.OnCallbackFilter"></a>
|
||||
### func \(\*Router\) [OnCallbackFilter](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L194>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnCallbackFilter(f Filter[*api.CallbackQuery], h Handler[*api.CallbackQuery])
|
||||
```
|
||||
|
||||
OnCallbackFilter registers a typed callback\-query handler gated by filter f. Filter routes are checked after pattern\-based OnCallback routes; first match wins.
|
||||
|
||||
<a name="Router.OnChannelPost"></a>
|
||||
### func \(\*Router\) [OnChannelPost](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L177>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnChannelPost(h Handler[*api.Message])
|
||||
```
|
||||
|
||||
OnChannelPost registers a handler for channel post updates.
|
||||
|
||||
<a name="Router.OnChatBoost"></a>
|
||||
### func \(\*Router\) [OnChatBoost](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L275>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnChatBoost(h Handler[*api.ChatBoostUpdated])
|
||||
```
|
||||
|
||||
OnChatBoost registers a handler for chat boost updates.
|
||||
|
||||
<a name="Router.OnChatJoinRequest"></a>
|
||||
### func \(\*Router\) [OnChatJoinRequest](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L225>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnChatJoinRequest(h Handler[*api.ChatJoinRequest])
|
||||
```
|
||||
|
||||
OnChatJoinRequest registers a handler for chat join requests.
|
||||
|
||||
<a name="Router.OnChatJoinRequestFilter"></a>
|
||||
### func \(\*Router\) [OnChatJoinRequestFilter](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L230>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnChatJoinRequestFilter(f Filter[*api.ChatJoinRequest], h Handler[*api.ChatJoinRequest])
|
||||
```
|
||||
|
||||
OnChatJoinRequestFilter registers a filtered handler for chat join requests.
|
||||
|
||||
<a name="Router.OnChatMember"></a>
|
||||
### func \(\*Router\) [OnChatMember](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L215>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnChatMember(h Handler[*api.ChatMemberUpdated])
|
||||
```
|
||||
|
||||
OnChatMember registers a handler for chat member status changes.
|
||||
|
||||
<a name="Router.OnChatMemberFilter"></a>
|
||||
### func \(\*Router\) [OnChatMemberFilter](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L220>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnChatMemberFilter(f Filter[*api.ChatMemberUpdated], h Handler[*api.ChatMemberUpdated])
|
||||
```
|
||||
|
||||
OnChatMemberFilter registers a filtered handler for chat member status changes.
|
||||
|
||||
<a name="Router.OnChosenInlineResult"></a>
|
||||
### func \(\*Router\) [OnChosenInlineResult](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L260>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnChosenInlineResult(h Handler[*api.ChosenInlineResult])
|
||||
```
|
||||
|
||||
OnChosenInlineResult registers a handler for chosen inline results.
|
||||
|
||||
<a name="Router.OnCommand"></a>
|
||||
### func \(\*Router\) [OnCommand](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L146>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnCommand(cmd string, h Handler[*api.Message])
|
||||
```
|
||||
|
||||
OnCommand registers a handler for a slash command. The command string includes the leading slash \(e.g. "/start"\). Matching strips an optional "@BotName" suffix.
|
||||
|
||||
<a name="Router.OnEditedChannelPost"></a>
|
||||
### func \(\*Router\) [OnEditedChannelPost](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L182>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnEditedChannelPost(h Handler[*api.Message])
|
||||
```
|
||||
|
||||
OnEditedChannelPost registers a handler for edited channel post updates.
|
||||
|
||||
<a name="Router.OnEditedMessage"></a>
|
||||
### func \(\*Router\) [OnEditedMessage](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L172>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnEditedMessage(h Handler[*api.Message])
|
||||
```
|
||||
|
||||
OnEditedMessage registers a handler for edited message updates.
|
||||
|
||||
<a name="Router.OnInlineQuery"></a>
|
||||
### func \(\*Router\) [OnInlineQuery](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L167>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnInlineQuery(h Handler[*api.InlineQuery])
|
||||
```
|
||||
|
||||
OnInlineQuery registers a handler for inline queries \(one matcher only; inline queries are not partitioned by content here\).
|
||||
|
||||
<a name="Router.OnInlineQueryFilter"></a>
|
||||
### func \(\*Router\) [OnInlineQueryFilter](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L200>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnInlineQueryFilter(f Filter[*api.InlineQuery], h Handler[*api.InlineQuery])
|
||||
```
|
||||
|
||||
OnInlineQueryFilter registers an inline\-query handler gated by filter f. Filter routes are checked after bare OnInlineQuery handlers; first match wins.
|
||||
|
||||
<a name="Router.OnMessageFilter"></a>
|
||||
### func \(\*Router\) [OnMessageFilter](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L188>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnMessageFilter(f Filter[*api.Message], h Handler[*api.Message])
|
||||
```
|
||||
|
||||
OnMessageFilter registers a typed message handler gated by filter f. Filter routes are checked after command and text routes; first match wins.
|
||||
|
||||
<a name="Router.OnMessageReaction"></a>
|
||||
### func \(\*Router\) [OnMessageReaction](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L265>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnMessageReaction(h Handler[*api.MessageReactionUpdated])
|
||||
```
|
||||
|
||||
OnMessageReaction registers a handler for message reaction updates.
|
||||
|
||||
<a name="Router.OnMessageReactionCount"></a>
|
||||
### func \(\*Router\) [OnMessageReactionCount](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L270>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnMessageReactionCount(h Handler[*api.MessageReactionCountUpdated])
|
||||
```
|
||||
|
||||
OnMessageReactionCount registers a handler for anonymous message reaction count updates.
|
||||
|
||||
<a name="Router.OnMyChatMember"></a>
|
||||
### func \(\*Router\) [OnMyChatMember](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L205>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnMyChatMember(h Handler[*api.ChatMemberUpdated])
|
||||
```
|
||||
|
||||
OnMyChatMember registers a handler for bot's own chat member status changes.
|
||||
|
||||
<a name="Router.OnMyChatMemberFilter"></a>
|
||||
### func \(\*Router\) [OnMyChatMemberFilter](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L210>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnMyChatMemberFilter(f Filter[*api.ChatMemberUpdated], h Handler[*api.ChatMemberUpdated])
|
||||
```
|
||||
|
||||
OnMyChatMemberFilter registers a filtered handler for bot's own chat member status changes.
|
||||
|
||||
<a name="Router.OnPoll"></a>
|
||||
### func \(\*Router\) [OnPoll](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L250>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnPoll(h Handler[*api.Poll])
|
||||
```
|
||||
|
||||
OnPoll registers a handler for poll state updates.
|
||||
|
||||
<a name="Router.OnPollAnswer"></a>
|
||||
### func \(\*Router\) [OnPollAnswer](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L255>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnPollAnswer(h Handler[*api.PollAnswer])
|
||||
```
|
||||
|
||||
OnPollAnswer registers a handler for poll answer updates.
|
||||
|
||||
<a name="Router.OnPreCheckoutQuery"></a>
|
||||
### func \(\*Router\) [OnPreCheckoutQuery](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L235>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnPreCheckoutQuery(h Handler[*api.PreCheckoutQuery])
|
||||
```
|
||||
|
||||
OnPreCheckoutQuery registers a handler for pre\-checkout queries.
|
||||
|
||||
<a name="Router.OnPreCheckoutQueryFilter"></a>
|
||||
### func \(\*Router\) [OnPreCheckoutQueryFilter](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L240>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnPreCheckoutQueryFilter(f Filter[*api.PreCheckoutQuery], h Handler[*api.PreCheckoutQuery])
|
||||
```
|
||||
|
||||
OnPreCheckoutQueryFilter registers a filtered handler for pre\-checkout queries.
|
||||
|
||||
<a name="Router.OnPurchasedPaidMedia"></a>
|
||||
### func \(\*Router\) [OnPurchasedPaidMedia](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L290>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnPurchasedPaidMedia(h Handler[*api.PaidMediaPurchased])
|
||||
```
|
||||
|
||||
OnPurchasedPaidMedia registers a handler for purchased paid media updates.
|
||||
|
||||
<a name="Router.OnRemovedChatBoost"></a>
|
||||
### func \(\*Router\) [OnRemovedChatBoost](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L280>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnRemovedChatBoost(h Handler[*api.ChatBoostRemoved])
|
||||
```
|
||||
|
||||
OnRemovedChatBoost registers a handler for removed chat boost updates.
|
||||
|
||||
<a name="Router.OnShippingQuery"></a>
|
||||
### func \(\*Router\) [OnShippingQuery](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L245>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnShippingQuery(h Handler[*api.ShippingQuery])
|
||||
```
|
||||
|
||||
OnShippingQuery registers a handler for shipping queries.
|
||||
|
||||
<a name="Router.OnText"></a>
|
||||
### func \(\*Router\) [OnText](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L153>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnText(pattern string, h Handler[*api.Message])
|
||||
```
|
||||
|
||||
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.Process"></a>
|
||||
### func \(\*Router\) [Process](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L310>)
|
||||
|
||||
```go
|
||||
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. 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>)
|
||||
|
||||
```go
|
||||
func (r *Router) Use(mw Middleware[*api.Update])
|
||||
```
|
||||
|
||||
Use registers a global middleware applied to every Update dispatch.
|
||||
|
||||
<a name="RouterOption"></a>
|
||||
## type [RouterOption](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L97>)
|
||||
|
||||
RouterOption configures a Router at construction time.
|
||||
|
||||
```go
|
||||
type RouterOption func(*Router)
|
||||
```
|
||||
|
||||
<a name="WithMaxConcurrency"></a>
|
||||
### func [WithMaxConcurrency](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L106>)
|
||||
|
||||
```go
|
||||
func WithMaxConcurrency(n int) RouterOption
|
||||
```
|
||||
|
||||
WithMaxConcurrency sets the maximum number of updates processed in parallel. Default is 50. Pass 0 to dispatch serially \(one update at a time, in the calling goroutine — the legacy behaviour before v1.1.0\).
|
||||
|
||||
Note: concurrent dispatch means handlers for different updates may run simultaneously. Handlers that mutate shared state must be safe for concurrent access.
|
||||
|
||||
<a name="RouterScope"></a>
|
||||
## type [RouterScope](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/groups.go#L31-L34>)
|
||||
|
||||
RouterScope registers handlers into a specific priority group on its parent Router. Group 0 runs first, then group 1, etc. Within a group, handlers run in registration order; the first non\-skipped match terminates dispatch unless the handler returns ErrContinueGroups.
|
||||
|
||||
```go
|
||||
type RouterScope struct {
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
```
|
||||
|
||||
<a name="RouterScope.OnCommand"></a>
|
||||
### func \(\*RouterScope\) [OnCommand](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/groups.go#L45>)
|
||||
|
||||
```go
|
||||
func (s *RouterScope) OnCommand(cmd string, h Handler[*api.Message])
|
||||
```
|
||||
|
||||
OnCommand registers a command handler in this group.
|
||||
|
||||
<a name="RouterScope.OnMessageFilter"></a>
|
||||
### func \(\*RouterScope\) [OnMessageFilter](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/groups.go#L60>)
|
||||
|
||||
```go
|
||||
func (s *RouterScope) OnMessageFilter(f Filter[*api.Message], h Handler[*api.Message])
|
||||
```
|
||||
|
||||
OnMessageFilter registers a filter\-based message handler in this group.
|
||||
|
||||
<a name="RouterScope.OnText"></a>
|
||||
### func \(\*RouterScope\) [OnText](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/groups.go#L53>)
|
||||
|
||||
```go
|
||||
func (s *RouterScope) OnText(pattern string, h Handler[*api.Message])
|
||||
```
|
||||
|
||||
OnText registers a regex text handler in this group. Panics at registration time if pattern is not a valid regular expression.
|
||||
|
||||
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)
|
||||
@@ -0,0 +1,243 @@
|
||||
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
|
||||
|
||||
# conversation
|
||||
|
||||
```go
|
||||
import "github.com/lukaszraczylo/go-telegram/dispatch/conversation"
|
||||
```
|
||||
|
||||
Package conversation implements a stateful conversation handler for the go\-telegram dispatch router. It provides a state\-machine abstraction over multi\-step Telegram bot interactions, with pluggable storage and flexible key strategies.
|
||||
|
||||
## Index
|
||||
|
||||
- [Variables](<#variables>)
|
||||
- [func End\(\) error](<#End>)
|
||||
- [func Next\(s State\) error](<#Next>)
|
||||
- [type Conversation](<#Conversation>)
|
||||
- [func \(c \*Conversation\) Dispatch\(next dispatch.Handler\[\*api.Update\]\) dispatch.Handler\[\*api.Update\]](<#Conversation.Dispatch>)
|
||||
- [type Handler](<#Handler>)
|
||||
- [type KeyStrategy](<#KeyStrategy>)
|
||||
- [type MemoryStorage](<#MemoryStorage>)
|
||||
- [func NewMemoryStorage\(\) \*MemoryStorage](<#NewMemoryStorage>)
|
||||
- [func \(s \*MemoryStorage\) Delete\(\_ context.Context, key string\) error](<#MemoryStorage.Delete>)
|
||||
- [func \(s \*MemoryStorage\) Get\(\_ context.Context, key string\) \(State, error\)](<#MemoryStorage.Get>)
|
||||
- [func \(s \*MemoryStorage\) Set\(\_ context.Context, key string, state State\) error](<#MemoryStorage.Set>)
|
||||
- [type State](<#State>)
|
||||
- [type Step](<#Step>)
|
||||
- [type Storage](<#Storage>)
|
||||
|
||||
|
||||
## Variables
|
||||
|
||||
<a name="ErrKeyNotFound"></a>ErrKeyNotFound is returned by Storage.Get when no conversation is active for the given key.
|
||||
|
||||
```go
|
||||
var ErrKeyNotFound = errors.New("conversation: key not found")
|
||||
```
|
||||
|
||||
<a name="End"></a>
|
||||
## func [End](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/conversation/handler.go#L34>)
|
||||
|
||||
```go
|
||||
func End() error
|
||||
```
|
||||
|
||||
End signals the conversation has finished and state should be cleared. Conversation handlers return End\(\) to terminate.
|
||||
|
||||
<a name="Next"></a>
|
||||
## func [Next](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/conversation/handler.go#L28>)
|
||||
|
||||
```go
|
||||
func Next(s State) error
|
||||
```
|
||||
|
||||
Next signals the conversation should advance to the given state. Conversation handlers return Next\("state\_name"\) to transition.
|
||||
|
||||
<a name="Conversation"></a>
|
||||
## type [Conversation](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/conversation/handler.go#L55-L79>)
|
||||
|
||||
Conversation is a stateful handler with entry, per\-state, exit and fallback steps. A conversation is keyed by KeyStrategy \(default KeyByUserAndChat\) and persisted by Storage \(default in\-memory\).
|
||||
|
||||
```go
|
||||
type Conversation struct {
|
||||
// EntryPoints starts a new conversation when a matching filter fires
|
||||
// and no conversation is already active for the key.
|
||||
EntryPoints []Step
|
||||
|
||||
// States maps each state to the steps that handle it.
|
||||
States map[State][]Step
|
||||
|
||||
// Exits, if any match, end the active conversation early. Useful for
|
||||
// /cancel-style commands.
|
||||
Exits []Step
|
||||
|
||||
// Fallbacks run when no state step matches the current update.
|
||||
Fallbacks []Step
|
||||
|
||||
// Storage persists conversation state. Defaults to NewMemoryStorage.
|
||||
Storage Storage
|
||||
|
||||
// KeyStrategy derives the persistence key. Defaults to KeyByUserAndChat.
|
||||
KeyStrategy KeyStrategy
|
||||
|
||||
// AllowReEntry, when true, lets entry-point steps fire even while a
|
||||
// conversation is already active for the key (effectively restarting it).
|
||||
AllowReEntry bool
|
||||
}
|
||||
```
|
||||
|
||||
<a name="Conversation.Dispatch"></a>
|
||||
### func \(\*Conversation\) [Dispatch](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/conversation/handler.go#L87>)
|
||||
|
||||
```go
|
||||
func (c *Conversation) Dispatch(next dispatch.Handler[*api.Update]) dispatch.Handler[*api.Update]
|
||||
```
|
||||
|
||||
Dispatch is a global middleware\-shaped Handler that consumes updates and routes them through the conversation graph. Register via router.Use\(conv.Dispatch\).
|
||||
|
||||
If the conversation claims an update, downstream handlers are skipped. If the conversation does not claim it, downstream handlers run as normal.
|
||||
|
||||
<a name="Handler"></a>
|
||||
## type [Handler](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/conversation/handler.go#L44>)
|
||||
|
||||
Handler defines a step in the conversation. Receives the dispatch context and the raw update. Returns:
|
||||
|
||||
- nil to stay in the current state
|
||||
- Next\("state"\) to transition to a different state
|
||||
- End\(\) to end the conversation
|
||||
- any other non\-nil error to surface to the dispatcher \(state unchanged\)
|
||||
|
||||
```go
|
||||
type Handler func(ctx *dispatch.Context, u *api.Update) error
|
||||
```
|
||||
|
||||
<a name="KeyStrategy"></a>
|
||||
## type [KeyStrategy](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/conversation/key.go#L16>)
|
||||
|
||||
KeyStrategy derives a persistence key from an update. Strategies determine how conversation scope works — per\-user, per\-chat, or per\-user\-and\-chat. Implementations must return a stable string for the same logical scope across updates.
|
||||
|
||||
Returns the empty string if the update doesn't have enough context to derive a key \(in which case the conversation handler skips it\).
|
||||
|
||||
```go
|
||||
type KeyStrategy func(u *api.Update) string
|
||||
```
|
||||
|
||||
<a name="KeyByChat"></a>KeyByChat derives a key from the chat ID. Useful for group flows where any user in the chat can drive the conversation.
|
||||
|
||||
```go
|
||||
var KeyByChat KeyStrategy = func(u *api.Update) string {
|
||||
if cid := chatID(u); cid != 0 {
|
||||
return fmt.Sprintf("c:%d", cid)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
```
|
||||
|
||||
<a name="KeyByUser"></a>KeyByUser derives a key from the sending user's ID. Useful for DM conversations and any flow that should follow the user across chats.
|
||||
|
||||
```go
|
||||
var KeyByUser KeyStrategy = func(u *api.Update) string {
|
||||
if uid := userID(u); uid != 0 {
|
||||
return fmt.Sprintf("u:%d", uid)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
```
|
||||
|
||||
<a name="KeyByUserAndChat"></a>KeyByUserAndChat derives a key from both user and chat IDs. The most common strategy: each user has their own conversation per chat.
|
||||
|
||||
```go
|
||||
var KeyByUserAndChat KeyStrategy = func(u *api.Update) string {
|
||||
uid := userID(u)
|
||||
cid := chatID(u)
|
||||
if uid == 0 || cid == 0 {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("uc:%d:%d", cid, uid)
|
||||
}
|
||||
```
|
||||
|
||||
<a name="MemoryStorage"></a>
|
||||
## type [MemoryStorage](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/conversation/in_memory.go#L11-L14>)
|
||||
|
||||
MemoryStorage is the default in\-process Storage. It is safe for concurrent use. Conversation state is lost on process restart; use a custom Storage backed by a database for persistent flows.
|
||||
|
||||
```go
|
||||
type MemoryStorage struct {
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
```
|
||||
|
||||
<a name="NewMemoryStorage"></a>
|
||||
### func [NewMemoryStorage](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/conversation/in_memory.go#L17>)
|
||||
|
||||
```go
|
||||
func NewMemoryStorage() *MemoryStorage
|
||||
```
|
||||
|
||||
NewMemoryStorage constructs an empty in\-memory storage.
|
||||
|
||||
<a name="MemoryStorage.Delete"></a>
|
||||
### func \(\*MemoryStorage\) [Delete](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/conversation/in_memory.go#L38>)
|
||||
|
||||
```go
|
||||
func (s *MemoryStorage) Delete(_ context.Context, key string) error
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="MemoryStorage.Get"></a>
|
||||
### func \(\*MemoryStorage\) [Get](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/conversation/in_memory.go#L21>)
|
||||
|
||||
```go
|
||||
func (s *MemoryStorage) Get(_ context.Context, key string) (State, error)
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="MemoryStorage.Set"></a>
|
||||
### func \(\*MemoryStorage\) [Set](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/conversation/in_memory.go#L31>)
|
||||
|
||||
```go
|
||||
func (s *MemoryStorage) Set(_ context.Context, key string, state State) error
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="State"></a>
|
||||
## type [State](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/conversation/state.go#L9>)
|
||||
|
||||
State is a label identifying a node in the conversation graph. The empty string is the implicit "no active conversation" state.
|
||||
|
||||
```go
|
||||
type State string
|
||||
```
|
||||
|
||||
<a name="Step"></a>
|
||||
## type [Step](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/conversation/handler.go#L47-L50>)
|
||||
|
||||
Step pairs a filter with a handler for one conversation step.
|
||||
|
||||
```go
|
||||
type Step struct {
|
||||
Filter dispatch.Filter[*api.Update]
|
||||
Handler Handler
|
||||
}
|
||||
```
|
||||
|
||||
<a name="Storage"></a>
|
||||
## type [Storage](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/conversation/storage.go#L16-L20>)
|
||||
|
||||
Storage persists per\-user \(or per\-chat, per\-message — depending on the KeyStrategy in use\) conversation state across update deliveries.
|
||||
|
||||
Implementations must be safe for concurrent use.
|
||||
|
||||
```go
|
||||
type Storage interface {
|
||||
Get(ctx context.Context, key string) (State, error)
|
||||
Set(ctx context.Context, key string, state State) error
|
||||
Delete(ctx context.Context, key string) error
|
||||
}
|
||||
```
|
||||
|
||||
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)
|
||||
@@ -0,0 +1,55 @@
|
||||
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
|
||||
|
||||
# callback
|
||||
|
||||
```go
|
||||
import "github.com/lukaszraczylo/go-telegram/dispatch/filters/callback"
|
||||
```
|
||||
|
||||
Package callback provides Filter helpers for \*api.CallbackQuery payloads.
|
||||
|
||||
## Index
|
||||
|
||||
- [func Data\(pattern string\) dispatch.Filter\[\*api.CallbackQuery\]](<#Data>)
|
||||
- [func DataEquals\(s string\) dispatch.Filter\[\*api.CallbackQuery\]](<#DataEquals>)
|
||||
- [func DataPrefix\(prefix string\) dispatch.Filter\[\*api.CallbackQuery\]](<#DataPrefix>)
|
||||
- [func FromUser\(userID int64\) dispatch.Filter\[\*api.CallbackQuery\]](<#FromUser>)
|
||||
|
||||
|
||||
<a name="Data"></a>
|
||||
## func [Data](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/callback/callback.go#L14>)
|
||||
|
||||
```go
|
||||
func Data(pattern string) dispatch.Filter[*api.CallbackQuery]
|
||||
```
|
||||
|
||||
Data returns a Filter that matches callback queries whose Data matches pattern \(regex\). Panics at registration time on an invalid pattern.
|
||||
|
||||
<a name="DataEquals"></a>
|
||||
## func [DataEquals](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/callback/callback.go#L23>)
|
||||
|
||||
```go
|
||||
func DataEquals(s string) dispatch.Filter[*api.CallbackQuery]
|
||||
```
|
||||
|
||||
DataEquals returns a Filter that matches callback queries whose Data equals s exactly.
|
||||
|
||||
<a name="DataPrefix"></a>
|
||||
## func [DataPrefix](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/callback/callback.go#L31>)
|
||||
|
||||
```go
|
||||
func DataPrefix(prefix string) dispatch.Filter[*api.CallbackQuery]
|
||||
```
|
||||
|
||||
DataPrefix returns a Filter that matches callback queries whose Data starts with prefix.
|
||||
|
||||
<a name="FromUser"></a>
|
||||
## func [FromUser](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/callback/callback.go#L39>)
|
||||
|
||||
```go
|
||||
func FromUser(userID int64) dispatch.Filter[*api.CallbackQuery]
|
||||
```
|
||||
|
||||
FromUser returns a Filter that matches callback queries whose From.ID equals userID.
|
||||
|
||||
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)
|
||||
@@ -0,0 +1,35 @@
|
||||
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
|
||||
|
||||
# chatjoinrequest
|
||||
|
||||
```go
|
||||
import "github.com/lukaszraczylo/go-telegram/dispatch/filters/chatjoinrequest"
|
||||
```
|
||||
|
||||
Package chatjoinrequest provides Filter helpers for \*api.ChatJoinRequest payloads.
|
||||
|
||||
## Index
|
||||
|
||||
- [func FromUser\(uid int64\) dispatch.Filter\[\*api.ChatJoinRequest\]](<#FromUser>)
|
||||
- [func InChat\(cid int64\) dispatch.Filter\[\*api.ChatJoinRequest\]](<#InChat>)
|
||||
|
||||
|
||||
<a name="FromUser"></a>
|
||||
## func [FromUser](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/chatjoinrequest/chatjoinrequest.go#L11>)
|
||||
|
||||
```go
|
||||
func FromUser(uid int64) dispatch.Filter[*api.ChatJoinRequest]
|
||||
```
|
||||
|
||||
FromUser returns a Filter that matches join requests where the requesting user's ID equals uid.
|
||||
|
||||
<a name="InChat"></a>
|
||||
## func [InChat](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/chatjoinrequest/chatjoinrequest.go#L19>)
|
||||
|
||||
```go
|
||||
func InChat(cid int64) dispatch.Filter[*api.ChatJoinRequest]
|
||||
```
|
||||
|
||||
InChat returns a Filter that matches join requests directed at the chat with the given chat ID.
|
||||
|
||||
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)
|
||||
@@ -0,0 +1,35 @@
|
||||
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
|
||||
|
||||
# chatmember
|
||||
|
||||
```go
|
||||
import "github.com/lukaszraczylo/go-telegram/dispatch/filters/chatmember"
|
||||
```
|
||||
|
||||
Package chatmember provides Filter helpers for \*api.ChatMemberUpdated payloads.
|
||||
|
||||
## Index
|
||||
|
||||
- [func FromUser\(uid int64\) dispatch.Filter\[\*api.ChatMemberUpdated\]](<#FromUser>)
|
||||
- [func NewStatus\(s string\) dispatch.Filter\[\*api.ChatMemberUpdated\]](<#NewStatus>)
|
||||
|
||||
|
||||
<a name="FromUser"></a>
|
||||
## func [FromUser](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/chatmember/chatmember.go#L37>)
|
||||
|
||||
```go
|
||||
func FromUser(uid int64) dispatch.Filter[*api.ChatMemberUpdated]
|
||||
```
|
||||
|
||||
FromUser returns a Filter that matches updates where the acting user \(From.ID\) equals uid.
|
||||
|
||||
<a name="NewStatus"></a>
|
||||
## func [NewStatus](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/chatmember/chatmember.go#L11>)
|
||||
|
||||
```go
|
||||
func NewStatus(s string) dispatch.Filter[*api.ChatMemberUpdated]
|
||||
```
|
||||
|
||||
NewStatus returns a Filter that matches updates where the new chat member status equals s \(e.g. "member", "administrator", "kicked", "left"\).
|
||||
|
||||
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)
|
||||
@@ -0,0 +1,45 @@
|
||||
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
|
||||
|
||||
# inline
|
||||
|
||||
```go
|
||||
import "github.com/lukaszraczylo/go-telegram/dispatch/filters/inline"
|
||||
```
|
||||
|
||||
Package inline provides Filter helpers for \*api.InlineQuery payloads.
|
||||
|
||||
## Index
|
||||
|
||||
- [func Query\(pattern string\) dispatch.Filter\[\*api.InlineQuery\]](<#Query>)
|
||||
- [func QueryEquals\(s string\) dispatch.Filter\[\*api.InlineQuery\]](<#QueryEquals>)
|
||||
- [func QueryPrefix\(prefix string\) dispatch.Filter\[\*api.InlineQuery\]](<#QueryPrefix>)
|
||||
|
||||
|
||||
<a name="Query"></a>
|
||||
## func [Query](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/inline/inline.go#L14>)
|
||||
|
||||
```go
|
||||
func Query(pattern string) dispatch.Filter[*api.InlineQuery]
|
||||
```
|
||||
|
||||
Query returns a Filter that matches inline queries whose Query field matches pattern \(regex\). Panics at registration time on an invalid pattern.
|
||||
|
||||
<a name="QueryEquals"></a>
|
||||
## func [QueryEquals](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/inline/inline.go#L23>)
|
||||
|
||||
```go
|
||||
func QueryEquals(s string) dispatch.Filter[*api.InlineQuery]
|
||||
```
|
||||
|
||||
QueryEquals returns a Filter that matches inline queries whose Query equals s exactly.
|
||||
|
||||
<a name="QueryPrefix"></a>
|
||||
## func [QueryPrefix](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/inline/inline.go#L31>)
|
||||
|
||||
```go
|
||||
func QueryPrefix(prefix string) dispatch.Filter[*api.InlineQuery]
|
||||
```
|
||||
|
||||
QueryPrefix returns a Filter that matches inline queries whose Query starts with prefix.
|
||||
|
||||
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)
|
||||
@@ -0,0 +1,155 @@
|
||||
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
|
||||
|
||||
# message
|
||||
|
||||
```go
|
||||
import "github.com/lukaszraczylo/go-telegram/dispatch/filters/message"
|
||||
```
|
||||
|
||||
Package message provides Filter helpers for \*api.Message payloads.
|
||||
|
||||
## Index
|
||||
|
||||
- [func AnyCommand\(\) dispatch.Filter\[\*api.Message\]](<#AnyCommand>)
|
||||
- [func ChatType\(t api.ChatType\) dispatch.Filter\[\*api.Message\]](<#ChatType>)
|
||||
- [func Command\(name string\) dispatch.Filter\[\*api.Message\]](<#Command>)
|
||||
- [func FromUser\(userID int64\) dispatch.Filter\[\*api.Message\]](<#FromUser>)
|
||||
- [func HasDocument\(\) dispatch.Filter\[\*api.Message\]](<#HasDocument>)
|
||||
- [func HasEntity\(t api.MessageEntityType\) dispatch.Filter\[\*api.Message\]](<#HasEntity>)
|
||||
- [func HasPhoto\(\) dispatch.Filter\[\*api.Message\]](<#HasPhoto>)
|
||||
- [func InChat\(chatID int64\) dispatch.Filter\[\*api.Message\]](<#InChat>)
|
||||
- [func IsForward\(\) dispatch.Filter\[\*api.Message\]](<#IsForward>)
|
||||
- [func IsReply\(\) dispatch.Filter\[\*api.Message\]](<#IsReply>)
|
||||
- [func Text\(pattern string\) dispatch.Filter\[\*api.Message\]](<#Text>)
|
||||
- [func TextContains\(sub string\) dispatch.Filter\[\*api.Message\]](<#TextContains>)
|
||||
- [func TextEquals\(s string\) dispatch.Filter\[\*api.Message\]](<#TextEquals>)
|
||||
- [func TextPrefix\(prefix string\) dispatch.Filter\[\*api.Message\]](<#TextPrefix>)
|
||||
|
||||
|
||||
<a name="AnyCommand"></a>
|
||||
## func [AnyCommand](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/message/message.go#L69>)
|
||||
|
||||
```go
|
||||
func AnyCommand() dispatch.Filter[*api.Message]
|
||||
```
|
||||
|
||||
AnyCommand returns a Filter that matches any message starting with a bot\_command entity at offset 0.
|
||||
|
||||
<a name="ChatType"></a>
|
||||
## func [ChatType](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/message/message.go#L124>)
|
||||
|
||||
```go
|
||||
func ChatType(t api.ChatType) dispatch.Filter[*api.Message]
|
||||
```
|
||||
|
||||
ChatType returns a Filter that matches messages whose Chat.Type equals t.
|
||||
|
||||
<a name="Command"></a>
|
||||
## func [Command](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/message/message.go#L44>)
|
||||
|
||||
```go
|
||||
func Command(name string) dispatch.Filter[*api.Message]
|
||||
```
|
||||
|
||||
Command returns a Filter that matches messages whose first entity is a bot\_command equal to "/\<name\>" \(with or without "@BotName" suffix\).
|
||||
|
||||
<a name="FromUser"></a>
|
||||
## func [FromUser](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/message/message.go#L131>)
|
||||
|
||||
```go
|
||||
func FromUser(userID int64) dispatch.Filter[*api.Message]
|
||||
```
|
||||
|
||||
FromUser returns a Filter that matches messages whose From.ID equals userID.
|
||||
|
||||
<a name="HasDocument"></a>
|
||||
## func [HasDocument](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/message/message.go#L101>)
|
||||
|
||||
```go
|
||||
func HasDocument() dispatch.Filter[*api.Message]
|
||||
```
|
||||
|
||||
HasDocument returns a Filter that matches messages with a Document attachment.
|
||||
|
||||
<a name="HasEntity"></a>
|
||||
## func [HasEntity](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/message/message.go#L109>)
|
||||
|
||||
```go
|
||||
func HasEntity(t api.MessageEntityType) dispatch.Filter[*api.Message]
|
||||
```
|
||||
|
||||
HasEntity returns a Filter that matches messages whose Entities contain at least one entity of type t \(e.g. api.MessageEntityTypeBotCommand\).
|
||||
|
||||
<a name="HasPhoto"></a>
|
||||
## func [HasPhoto](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/message/message.go#L94>)
|
||||
|
||||
```go
|
||||
func HasPhoto() dispatch.Filter[*api.Message]
|
||||
```
|
||||
|
||||
HasPhoto returns a Filter that matches messages with a Photo attachment.
|
||||
|
||||
<a name="InChat"></a>
|
||||
## func [InChat](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/message/message.go#L138>)
|
||||
|
||||
```go
|
||||
func InChat(chatID int64) dispatch.Filter[*api.Message]
|
||||
```
|
||||
|
||||
InChat returns a Filter that matches messages whose Chat.ID equals chatID.
|
||||
|
||||
<a name="IsForward"></a>
|
||||
## func [IsForward](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/message/message.go#L87>)
|
||||
|
||||
```go
|
||||
func IsForward() dispatch.Filter[*api.Message]
|
||||
```
|
||||
|
||||
IsForward returns a Filter that matches messages that have ForwardOrigin set.
|
||||
|
||||
<a name="IsReply"></a>
|
||||
## func [IsReply](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/message/message.go#L80>)
|
||||
|
||||
```go
|
||||
func IsReply() dispatch.Filter[*api.Message]
|
||||
```
|
||||
|
||||
IsReply returns a Filter that matches messages that have ReplyToMessage set.
|
||||
|
||||
<a name="Text"></a>
|
||||
## func [Text](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/message/message.go#L14>)
|
||||
|
||||
```go
|
||||
func Text(pattern string) dispatch.Filter[*api.Message]
|
||||
```
|
||||
|
||||
Text returns a Filter that matches messages whose Text matches pattern \(regex\). Panics at registration time on an invalid pattern.
|
||||
|
||||
<a name="TextContains"></a>
|
||||
## func [TextContains](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/message/message.go#L36>)
|
||||
|
||||
```go
|
||||
func TextContains(sub string) dispatch.Filter[*api.Message]
|
||||
```
|
||||
|
||||
TextContains returns a Filter that matches messages whose Text contains sub.
|
||||
|
||||
<a name="TextEquals"></a>
|
||||
## func [TextEquals](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/message/message.go#L22>)
|
||||
|
||||
```go
|
||||
func TextEquals(s string) dispatch.Filter[*api.Message]
|
||||
```
|
||||
|
||||
TextEquals returns a Filter that matches messages whose Text equals s exactly.
|
||||
|
||||
<a name="TextPrefix"></a>
|
||||
## func [TextPrefix](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/message/message.go#L29>)
|
||||
|
||||
```go
|
||||
func TextPrefix(prefix string) dispatch.Filter[*api.Message]
|
||||
```
|
||||
|
||||
TextPrefix returns a Filter that matches messages whose Text starts with prefix.
|
||||
|
||||
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)
|
||||
@@ -0,0 +1,35 @@
|
||||
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
|
||||
|
||||
# precheckoutquery
|
||||
|
||||
```go
|
||||
import "github.com/lukaszraczylo/go-telegram/dispatch/filters/precheckoutquery"
|
||||
```
|
||||
|
||||
Package precheckoutquery provides Filter helpers for \*api.PreCheckoutQuery payloads.
|
||||
|
||||
## Index
|
||||
|
||||
- [func Currency\(c string\) dispatch.Filter\[\*api.PreCheckoutQuery\]](<#Currency>)
|
||||
- [func FromUser\(uid int64\) dispatch.Filter\[\*api.PreCheckoutQuery\]](<#FromUser>)
|
||||
|
||||
|
||||
<a name="Currency"></a>
|
||||
## func [Currency](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/precheckoutquery/precheckoutquery.go#L11>)
|
||||
|
||||
```go
|
||||
func Currency(c string) dispatch.Filter[*api.PreCheckoutQuery]
|
||||
```
|
||||
|
||||
Currency returns a Filter that matches pre\-checkout queries with the given ISO 4217 currency code \(e.g. "USD", "EUR", "XTR"\).
|
||||
|
||||
<a name="FromUser"></a>
|
||||
## func [FromUser](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/precheckoutquery/precheckoutquery.go#L19>)
|
||||
|
||||
```go
|
||||
func FromUser(uid int64) dispatch.Filter[*api.PreCheckoutQuery]
|
||||
```
|
||||
|
||||
FromUser returns a Filter that matches pre\-checkout queries sent by the user with the given ID.
|
||||
|
||||
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)
|
||||
@@ -0,0 +1,241 @@
|
||||
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
|
||||
|
||||
# transport
|
||||
|
||||
```go
|
||||
import "github.com/lukaszraczylo/go-telegram/transport"
|
||||
```
|
||||
|
||||
Package transport provides update delivery mechanisms \(long\-poll and webhook\) that feed updates into the dispatch package's Router.
|
||||
|
||||
All implementations satisfy the Updater interface so user code can swap one for the other without touching handler logic.
|
||||
|
||||
## Index
|
||||
|
||||
- [type BackoffStrategy](<#BackoffStrategy>)
|
||||
- [type ExponentialBackoff](<#ExponentialBackoff>)
|
||||
- [func DefaultBackoff\(\) \*ExponentialBackoff](<#DefaultBackoff>)
|
||||
- [func \(b \*ExponentialBackoff\) NextDelay\(attempt int\) time.Duration](<#ExponentialBackoff.NextDelay>)
|
||||
- [type LongPoller](<#LongPoller>)
|
||||
- [func NewLongPoller\(b \*client.Bot\) \*LongPoller](<#NewLongPoller>)
|
||||
- [func \(p \*LongPoller\) Run\(ctx context.Context\) error](<#LongPoller.Run>)
|
||||
- [func \(p \*LongPoller\) Stop\(ctx context.Context\) error](<#LongPoller.Stop>)
|
||||
- [func \(p \*LongPoller\) Updates\(\) \<\-chan api.Update](<#LongPoller.Updates>)
|
||||
- [type Updater](<#Updater>)
|
||||
- [type WebhookOption](<#WebhookOption>)
|
||||
- [func WithBufferSize\(n int\) WebhookOption](<#WithBufferSize>)
|
||||
- [type WebhookServer](<#WebhookServer>)
|
||||
- [func NewWebhookServer\(b \*client.Bot, opts ...WebhookOption\) \*WebhookServer](<#NewWebhookServer>)
|
||||
- [func \(w \*WebhookServer\) ListenAndServe\(ctx context.Context, addr string\) error](<#WebhookServer.ListenAndServe>)
|
||||
- [func \(w \*WebhookServer\) Run\(ctx context.Context\) error](<#WebhookServer.Run>)
|
||||
- [func \(w \*WebhookServer\) ServeHTTP\(rw http.ResponseWriter, r \*http.Request\)](<#WebhookServer.ServeHTTP>)
|
||||
- [func \(w \*WebhookServer\) Stop\(ctx context.Context\) error](<#WebhookServer.Stop>)
|
||||
- [func \(w \*WebhookServer\) Updates\(\) \<\-chan api.Update](<#WebhookServer.Updates>)
|
||||
|
||||
|
||||
<a name="BackoffStrategy"></a>
|
||||
## type [BackoffStrategy](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/backoff.go#L12-L14>)
|
||||
|
||||
BackoffStrategy returns the duration to wait before the next attempt after \`attempt\` consecutive failures \(1\-based\). Implementations must be safe to call from a single goroutine.
|
||||
|
||||
```go
|
||||
type BackoffStrategy interface {
|
||||
NextDelay(attempt int) time.Duration
|
||||
}
|
||||
```
|
||||
|
||||
<a name="ExponentialBackoff"></a>
|
||||
## type [ExponentialBackoff](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/backoff.go#L18-L23>)
|
||||
|
||||
ExponentialBackoff implements capped exponential back\-off with jitter. Defaults: Base=500ms, Max=30s, Factor=2.0, Jitter=0.2.
|
||||
|
||||
```go
|
||||
type ExponentialBackoff struct {
|
||||
Base time.Duration
|
||||
Max time.Duration
|
||||
Factor float64
|
||||
Jitter float64 // 0..1; fraction of computed delay added/subtracted at random
|
||||
}
|
||||
```
|
||||
|
||||
<a name="DefaultBackoff"></a>
|
||||
### func [DefaultBackoff](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/backoff.go#L26>)
|
||||
|
||||
```go
|
||||
func DefaultBackoff() *ExponentialBackoff
|
||||
```
|
||||
|
||||
DefaultBackoff returns an ExponentialBackoff with library defaults.
|
||||
|
||||
<a name="ExponentialBackoff.NextDelay"></a>
|
||||
### func \(\*ExponentialBackoff\) [NextDelay](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/backoff.go#L36>)
|
||||
|
||||
```go
|
||||
func (b *ExponentialBackoff) NextDelay(attempt int) time.Duration
|
||||
```
|
||||
|
||||
NextDelay implements BackoffStrategy.
|
||||
|
||||
<a name="LongPoller"></a>
|
||||
## type [LongPoller](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/longpoll.go#L21-L31>)
|
||||
|
||||
LongPoller pulls updates via Bot.GetUpdates in a loop, advancing the offset cursor after each batch. It applies BackoffStrategy on transient errors \(network failures, 5xx, 429\).
|
||||
|
||||
At\-least\-once semantics on shutdown: when ctx is cancelled or Stop is called mid\-batch, any updates already fetched but not yet dispatched are dropped without advancing the offset. On the next restart those updates will be re\-delivered by Telegram.
|
||||
|
||||
```go
|
||||
type LongPoller struct {
|
||||
Bot *client.Bot
|
||||
Timeout int // seconds, default 30
|
||||
Limit int // 1..100, default 100
|
||||
AllowedTypes []api.UpdateType
|
||||
Backoff BackoffStrategy
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
```
|
||||
|
||||
<a name="NewLongPoller"></a>
|
||||
### func [NewLongPoller](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/longpoll.go#L34>)
|
||||
|
||||
```go
|
||||
func NewLongPoller(b *client.Bot) *LongPoller
|
||||
```
|
||||
|
||||
NewLongPoller constructs a LongPoller with sensible defaults.
|
||||
|
||||
<a name="LongPoller.Run"></a>
|
||||
### func \(\*LongPoller\) [Run](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/longpoll.go#L51>)
|
||||
|
||||
```go
|
||||
func (p *LongPoller) Run(ctx context.Context) error
|
||||
```
|
||||
|
||||
Run implements Updater. It blocks until ctx is cancelled, Stop is called, or a fatal error occurs \(e.g. unauthorized\). See LongPoller for at\-least\-once delivery semantics on shutdown.
|
||||
|
||||
<a name="LongPoller.Stop"></a>
|
||||
### func \(\*LongPoller\) [Stop](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/longpoll.go#L122>)
|
||||
|
||||
```go
|
||||
func (p *LongPoller) Stop(ctx context.Context) error
|
||||
```
|
||||
|
||||
Stop implements Updater.
|
||||
|
||||
<a name="LongPoller.Updates"></a>
|
||||
### func \(\*LongPoller\) [Updates](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/longpoll.go#L46>)
|
||||
|
||||
```go
|
||||
func (p *LongPoller) Updates() <-chan api.Update
|
||||
```
|
||||
|
||||
Updates implements Updater.
|
||||
|
||||
<a name="Updater"></a>
|
||||
## type [Updater](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/updater.go#L13-L23>)
|
||||
|
||||
Updater is the abstraction over update sources. Implementations must:
|
||||
|
||||
- return a channel from Updates\(\) that receives every Update they read.
|
||||
- close the channel after Run returns.
|
||||
- honour ctx cancellation in Run.
|
||||
|
||||
```go
|
||||
type Updater interface {
|
||||
// Updates returns the channel updates flow into. Multiple readers
|
||||
// is implementation-defined; users should treat it as single-reader.
|
||||
Updates() <-chan api.Update
|
||||
// Run blocks until ctx is cancelled or a fatal error occurs. It is
|
||||
// the user's responsibility to call Run in a goroutine if needed.
|
||||
Run(ctx context.Context) error
|
||||
// Stop signals Run to exit and waits for the channel to drain.
|
||||
// Implementations must be idempotent.
|
||||
Stop(ctx context.Context) error
|
||||
}
|
||||
```
|
||||
|
||||
<a name="WebhookOption"></a>
|
||||
## type [WebhookOption](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L57>)
|
||||
|
||||
WebhookOption configures a WebhookServer at construction time.
|
||||
|
||||
```go
|
||||
type WebhookOption func(*webhookOptions)
|
||||
```
|
||||
|
||||
<a name="WithBufferSize"></a>
|
||||
### func [WithBufferSize](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L65>)
|
||||
|
||||
```go
|
||||
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#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\).
|
||||
|
||||
```go
|
||||
type WebhookServer struct {
|
||||
Bot *client.Bot
|
||||
SecretToken string // verify X-Telegram-Bot-Api-Secret-Token; empty disables
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
```
|
||||
|
||||
<a name="NewWebhookServer"></a>
|
||||
### func [NewWebhookServer](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L71>)
|
||||
|
||||
```go
|
||||
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#L168>)
|
||||
|
||||
```go
|
||||
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#L90>)
|
||||
|
||||
```go
|
||||
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#L116>)
|
||||
|
||||
```go
|
||||
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#L102>)
|
||||
|
||||
```go
|
||||
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#L84>)
|
||||
|
||||
```go
|
||||
func (w *WebhookServer) Updates() <-chan api.Update
|
||||
```
|
||||
|
||||
Updates implements Updater.
|
||||
|
||||
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,499 +0,0 @@
|
||||
# go-telegram — Design Spec
|
||||
|
||||
- **Date:** 2026-05-08
|
||||
- **Module:** `github.com/lukaszraczylo/go-telegram`
|
||||
- **License:** MIT
|
||||
- **Status:** Approved for planning
|
||||
- **Author:** Lukasz Raczylo (with Claude assistance)
|
||||
|
||||
## 1. Purpose & scope
|
||||
|
||||
A Go library for the Telegram Bot API, primarily a portfolio piece showcasing:
|
||||
|
||||
- Codegen-driven full API coverage (parsed from `https://core.telegram.org/bots/api`)
|
||||
- Pragmatic Go generics
|
||||
- Pluggable HTTP transport and JSON codec for resource-conscious deployments
|
||||
- Long-poll and webhook update delivery behind a unified interface
|
||||
- A typed dispatcher/router for handlers, commands, and callbacks
|
||||
- Comprehensive testify-based unit tests with golden fixtures for codegen
|
||||
|
||||
Out of scope for v1: the daily auto-regen GitHub Action (deferred — see §11).
|
||||
|
||||
## 2. Non-goals
|
||||
|
||||
- Competing on completeness/maturity with `mymmrac/telego` or `go-telegram-bot-api/telegram-bot-api`. We optimise for clarity and design.
|
||||
- Bot-framework features beyond the dispatcher (no plugin marketplace, no FSM, no scenes).
|
||||
- A documentation website. `pkg.go.dev` + README is sufficient.
|
||||
|
||||
## 3. Requirements
|
||||
|
||||
### Functional
|
||||
|
||||
1. Cover all Telegram Bot API methods and types via codegen.
|
||||
2. Support long-poll and webhook update delivery, both implementing one `Updater` interface.
|
||||
3. Allow the user to swap the HTTP client (e.g. `valyala/fasthttp`) and JSON codec (e.g. `goccy/go-json`).
|
||||
4. Provide a typed dispatcher with command, text-regex, callback, and inline-query matching plus generic middleware.
|
||||
5. Provide unit tests using `stretchr/testify` covering happy paths and explicit edge cases.
|
||||
6. Provide an optional integration test suite gated by build tag and env vars.
|
||||
|
||||
### Non-functional
|
||||
|
||||
- Lean dependency footprint (stdlib + `golang.org/x/net/html` + testify).
|
||||
- Deterministic, reproducible codegen (`go test ./... -run TestGen` is hermetic).
|
||||
- Generated files committed to the repo so consumers do not need to run codegen.
|
||||
- Doc comments on every exported symbol; generated types carry Telegram's verbatim prose.
|
||||
|
||||
## 4. Architecture
|
||||
|
||||
Two-stage codegen with a JSON intermediate representation:
|
||||
|
||||
```
|
||||
HTML page cmd/scrape api.json (IR) cmd/genapi api/*.gen.go
|
||||
```
|
||||
|
||||
The IR is committed; PRs from a future regen workflow show the diff against the previous IR, providing a readable changelog of Telegram-side changes.
|
||||
|
||||
### 4.1 Repository layout
|
||||
|
||||
```
|
||||
go-telegram/
|
||||
├── api/ GENERATED types + method wrappers (typed param structs)
|
||||
│ ├── types.gen.go
|
||||
│ ├── methods.gen.go
|
||||
│ └── enums.gen.go
|
||||
├── client/ HAND Bot client, request building, error handling
|
||||
│ ├── client.go
|
||||
│ ├── codec.go Codec interface + encoding/json default
|
||||
│ ├── httpclient.go HTTPDoer interface + net/http default
|
||||
│ ├── errors.go Typed APIError, NetworkError, ParseError
|
||||
│ └── result.go generic Result[T] decode
|
||||
├── transport/ HAND Updater abstraction
|
||||
│ ├── updater.go Updater interface
|
||||
│ ├── longpoll.go LongPoller
|
||||
│ └── webhook.go WebhookServer
|
||||
├── dispatch/ HAND Handler router
|
||||
│ ├── router.go Router, OnCommand/OnCallback/OnText
|
||||
│ ├── middleware.go Generic Middleware[T]
|
||||
│ └── context.go Per-update context
|
||||
├── internal/
|
||||
│ └── spec/ Shared IR types
|
||||
│ ├── ir.go Types describing parsed Telegram API
|
||||
│ └── api.json Committed golden IR (regenerated by scraper)
|
||||
├── cmd/
|
||||
│ ├── scrape/ HTML → api.json
|
||||
│ └── genapi/ api.json → api/*.gen.go
|
||||
├── examples/
|
||||
│ ├── echo/ Long-poll echo bot
|
||||
│ └── webhook/ Webhook bot with command router
|
||||
├── testdata/
|
||||
│ ├── html/ Golden HTML snapshots for scraper
|
||||
│ ├── golden/ Expected api.json + emitted Go for codegen tests
|
||||
│ └── responses/ Canned Telegram JSON responses
|
||||
├── .github/workflows/
|
||||
│ └── ci.yml lint + test + codegen-clean check
|
||||
├── Makefile regen, test, lint targets
|
||||
├── go.mod module github.com/lukaszraczylo/go-telegram
|
||||
├── LICENSE MIT
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 5. Core types and client (`client/`)
|
||||
|
||||
### 5.1 Pluggability interfaces
|
||||
|
||||
```go
|
||||
// Codec is the JSON encoder/decoder. Default impl wraps encoding/json.
|
||||
type Codec interface {
|
||||
Marshal(v any) ([]byte, error)
|
||||
Unmarshal(data []byte, v any) error
|
||||
}
|
||||
|
||||
// HTTPDoer is the HTTP transport. Default is *http.Client.
|
||||
type HTTPDoer interface {
|
||||
Do(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
// Logger is a slog-shaped interface; nil-safe default writes nowhere.
|
||||
type Logger interface {
|
||||
Debug(msg string, attrs ...any)
|
||||
Info(msg string, attrs ...any)
|
||||
Warn(msg string, attrs ...any)
|
||||
Error(msg string, attrs ...any)
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 Bot client
|
||||
|
||||
```go
|
||||
type Bot struct {
|
||||
token string
|
||||
base string // https://api.telegram.org
|
||||
http HTTPDoer
|
||||
codec Codec
|
||||
logger Logger
|
||||
}
|
||||
|
||||
type Option func(*Bot)
|
||||
func WithHTTPClient(c HTTPDoer) Option
|
||||
func WithCodec(c Codec) Option
|
||||
func WithBaseURL(url string) Option
|
||||
func WithLogger(l Logger) Option
|
||||
|
||||
func New(token string, opts ...Option) *Bot
|
||||
```
|
||||
|
||||
Constructor-level functional options only; per-call params are typed structs (codegen-friendly).
|
||||
|
||||
### 5.3 Result envelope and call helper
|
||||
|
||||
```go
|
||||
type Result[T any] struct {
|
||||
OK bool `json:"ok"`
|
||||
Result T `json:"result,omitempty"`
|
||||
ErrorCode int `json:"error_code,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Parameters *ResponseParameters `json:"parameters,omitempty"`
|
||||
}
|
||||
|
||||
// Single point for marshalling, URL signing, decoding, error mapping.
|
||||
// Used by every generated method wrapper.
|
||||
func call[Req, Resp any](ctx context.Context, b *Bot, method string, req Req) (Resp, error)
|
||||
```
|
||||
|
||||
Generated wrappers stay thin:
|
||||
|
||||
```go
|
||||
func (b *Bot) SendMessage(ctx context.Context, p *SendMessageParams) (*Message, error) {
|
||||
return call[*SendMessageParams, *Message](ctx, b, "sendMessage", p)
|
||||
}
|
||||
```
|
||||
|
||||
### 5.4 Errors
|
||||
|
||||
```go
|
||||
type APIError struct {
|
||||
Code int
|
||||
Description string
|
||||
Parameters *ResponseParameters // retry_after, migrate_to_chat_id
|
||||
}
|
||||
func (e *APIError) Error() string
|
||||
func (e *APIError) IsRetryable() bool // 429, 5xx
|
||||
func (e *APIError) RetryAfter() time.Duration
|
||||
|
||||
type NetworkError struct{ Err error }
|
||||
type ParseError struct{ Err error; Body []byte }
|
||||
```
|
||||
|
||||
Sentinel errors via `errors.Is`: `ErrUnauthorized`, `ErrChatNotFound`, `ErrMessageNotModified`, `ErrTooManyRequests`. Mapped from `error_code` plus description-prefix matching.
|
||||
|
||||
## 6. Codegen pipeline
|
||||
|
||||
### 6.1 Stage 1 — `cmd/scrape/`
|
||||
|
||||
- Input: live URL `https://core.telegram.org/bots/api` (default) or a local HTML fixture (`-input` flag).
|
||||
- Parser: `golang.org/x/net/html` (no goquery).
|
||||
- Walk strategy: traverse `<h4>` headings sequentially. Lowercase first letter → method. Uppercase → type.
|
||||
- Following `<p>` until next heading → description.
|
||||
- Following `<table>` → fields/params (columns: Field|Type|Required|Description for types; Parameter|Type|Required|Description for methods).
|
||||
- Return type extracted by regex on description: `Returns *X* on success`, `Returns an Array of X`, `Returns True on success`.
|
||||
- Italic markers in the type column denote optional, array depth, and union-member candidates.
|
||||
- "Recent changes" section parsed for current API version.
|
||||
|
||||
### 6.2 Intermediate representation (`internal/spec/ir.go`)
|
||||
|
||||
```go
|
||||
type API struct {
|
||||
Version string
|
||||
Types []TypeDecl
|
||||
Methods []MethodDecl
|
||||
}
|
||||
type TypeDecl struct {
|
||||
Name string
|
||||
Doc string
|
||||
Fields []Field
|
||||
OneOf []string // unions (InputMedia, ChatMember, …)
|
||||
}
|
||||
type MethodDecl struct {
|
||||
Name string // sendMessage
|
||||
Doc string
|
||||
Params []Field
|
||||
Returns TypeRef
|
||||
HasFiles bool // forces multipart
|
||||
}
|
||||
type Field struct {
|
||||
Name string
|
||||
JSONName string
|
||||
Type TypeRef
|
||||
Required bool
|
||||
Doc string
|
||||
}
|
||||
type Kind int
|
||||
const (
|
||||
KindPrimitive Kind = iota
|
||||
KindNamed
|
||||
KindArray
|
||||
KindOneOf
|
||||
)
|
||||
type TypeRef struct {
|
||||
Kind Kind
|
||||
Name string
|
||||
ElemType *TypeRef
|
||||
Variants []string
|
||||
}
|
||||
```
|
||||
|
||||
`internal/spec/api.json` is committed. Marshalling is stable (sorted fields, deterministic JSON output) so diffs read as a Telegram changelog.
|
||||
|
||||
### 6.3 Stage 2 — `cmd/genapi/`
|
||||
|
||||
- Reads `api.json`.
|
||||
- Emits Go via `text/template`, finalised with `go/format`.
|
||||
- Templates:
|
||||
- `types.tmpl` → struct per `TypeDecl`. Optional fields are pointers (or `omitempty` for slices/maps). Doc comments verbatim from API.
|
||||
- `enums.tmpl` → string consts for known enumerations (parse modes, chat types, etc., extracted from doc prose).
|
||||
- `oneof.tmpl` → union types as `interface { isFooBar() }` plus concrete impls and a `UnmarshalJSON` that switches on a discriminator field (typically `type` or `source`).
|
||||
- `methods.tmpl` → param struct + thin `Bot.<MethodName>` wrapper using `call[…]`.
|
||||
- `multipart.tmpl` → for methods with `HasFiles`, custom request builder using `mime/multipart`.
|
||||
- Header on every emitted file: `// Code generated by cmd/genapi. DO NOT EDIT.` + `//go:build !ignore_autogenerated`.
|
||||
|
||||
### 6.4 Makefile contract
|
||||
|
||||
The Makefile owns the codegen entry points; tools and CI call `make`, never raw `go run`:
|
||||
|
||||
| Target | What it does |
|
||||
|---|---|
|
||||
| `make snapshot` | `curl -fsSL https://core.telegram.org/bots/api > testdata/html/snapshot_<date>.html` and update a `latest.html` symlink. |
|
||||
| `make regen` | Run scraper against `testdata/html/latest.html`, then run emitter. Writes `internal/spec/api.json` and `api/*.gen.go`. |
|
||||
| `make regen-from-fixture` | Same as `make regen` but pinned to `testdata/html/snapshot_2026-05-08.html` for deterministic CI checks. |
|
||||
| `make test` | `go test -race ./...` |
|
||||
| `make test-update-golden` | `go test -run TestGen -update ./...` to refresh golden fixtures. |
|
||||
| `make lint` | `go vet` + `staticcheck`. |
|
||||
| `make integration` | `go test -tags=integration ./test/integration/...` (requires env). |
|
||||
|
||||
## 7. Transport (`transport/`)
|
||||
|
||||
```go
|
||||
type Updater interface {
|
||||
Updates() <-chan api.Update
|
||||
Run(ctx context.Context) error
|
||||
Stop(ctx context.Context) error
|
||||
}
|
||||
```
|
||||
|
||||
### 7.1 LongPoller
|
||||
|
||||
```go
|
||||
type LongPoller struct {
|
||||
Bot *client.Bot
|
||||
Timeout int // seconds, default 30
|
||||
Limit int // 1..100, default 100
|
||||
AllowedTypes []api.UpdateType
|
||||
Backoff BackoffStrategy
|
||||
}
|
||||
```
|
||||
|
||||
Calls `getUpdates` in a loop, tracks `offset`, applies exponential backoff on transient errors via `BackoffStrategy`.
|
||||
|
||||
### 7.2 WebhookServer
|
||||
|
||||
```go
|
||||
type WebhookServer struct {
|
||||
Bot *client.Bot
|
||||
SecretToken string // verify X-Telegram-Bot-Api-Secret-Token
|
||||
BufferSize int
|
||||
}
|
||||
func (w *WebhookServer) ServeHTTP(rw http.ResponseWriter, r *http.Request)
|
||||
func (w *WebhookServer) ListenAndServe(ctx context.Context, addr string) error
|
||||
```
|
||||
|
||||
`ServeHTTP` lets users mount on their own router. `ListenAndServe` is a convenience for standalone use.
|
||||
|
||||
## 8. Dispatcher (`dispatch/`)
|
||||
|
||||
```go
|
||||
type Context struct {
|
||||
Ctx context.Context
|
||||
Bot *client.Bot
|
||||
Update *api.Update
|
||||
Values map[string]any // matched groups, command args
|
||||
}
|
||||
|
||||
type Handler[T any] func(ctx *Context, payload T) error
|
||||
type Middleware[T any] func(Handler[T]) Handler[T]
|
||||
|
||||
type Router struct{ /* … */ }
|
||||
|
||||
func New(bot *client.Bot) *Router
|
||||
|
||||
func (r *Router) OnCommand(cmd string, h Handler[*api.Message])
|
||||
func (r *Router) OnText(pattern string, h Handler[*api.Message])
|
||||
func (r *Router) OnCallback(pattern string, h Handler[*api.CallbackQuery])
|
||||
func (r *Router) OnInlineQuery(h Handler[*api.InlineQuery])
|
||||
func (r *Router) OnEditedMessage(h Handler[*api.Message])
|
||||
func (r *Router) Use(mw Middleware[*api.Update])
|
||||
|
||||
func (r *Router) Run(ctx context.Context, u transport.Updater) error
|
||||
```
|
||||
|
||||
Matchers run in registration order; first match wins. A panic-recovery middleware is registered automatically. Generic `Handler[T]` keeps payloads precisely typed in user code.
|
||||
|
||||
## 9. Testing strategy
|
||||
|
||||
### 9.1 Unit tests (every package, fast, no network)
|
||||
|
||||
- `testify/require` for assertions, `testify/mock` on `client.HTTPDoer`.
|
||||
- Edge cases explicitly covered:
|
||||
- API error decode for `429` with `retry_after` → `*APIError.IsRetryable()=true`, `RetryAfter()=N`.
|
||||
- Network error from transport → wrapped `*NetworkError`.
|
||||
- Malformed JSON → `*ParseError`.
|
||||
- Void-result methods (`setWebhook` returning `bool`).
|
||||
- Optional pointer fields nil round-trip.
|
||||
- OneOf union types unmarshal via discriminator.
|
||||
- Multipart upload path for methods with `InputFile`.
|
||||
- Context cancellation mid-call returns `ctx.Err()`.
|
||||
- Long-poller backoff after transient error.
|
||||
- Webhook secret-token mismatch → 401.
|
||||
- Webhook handles oversized body, malformed JSON, wrong content-type.
|
||||
- Router: command match with/without bot mention (`/start@MyBot`), regex matchers, panic recovery, middleware ordering, no-match update.
|
||||
|
||||
### 9.2 Codegen golden tests
|
||||
|
||||
```
|
||||
testdata/html/
|
||||
├── snapshot_2026-05-08.html full Bots API page snapshot
|
||||
└── small_fixture.html hand-crafted minimal page (1 type, 1 method)
|
||||
testdata/golden/
|
||||
├── api.json expected IR from snapshot
|
||||
└── *.gen.go expected emitted Go
|
||||
```
|
||||
|
||||
- `cmd/scrape` test: parse fixture → compare to golden `api.json`.
|
||||
- `cmd/genapi` test: read golden `api.json` → compare emitted Go to golden `*.gen.go`.
|
||||
- `-update` flag (custom in `internal/testutil`) regenerates goldens deliberately.
|
||||
|
||||
### 9.3 Optional integration suite
|
||||
|
||||
- Build tag `//go:build integration`.
|
||||
- Skipped by default `go test ./...`.
|
||||
- Activated by `go test -tags=integration ./test/integration/...`.
|
||||
- Requires `TELEGRAM_BOT_TOKEN` (and `TELEGRAM_TEST_CHAT_ID` where applicable).
|
||||
- Covers `getMe`, `sendMessage`, `setWebhook`/`deleteWebhook`, `getUpdates` loop with short timeout.
|
||||
- Not part of default CI to avoid flakes.
|
||||
|
||||
## 10. CI
|
||||
|
||||
`.github/workflows/ci.yml` (every push and PR):
|
||||
|
||||
- `actions/setup-go` matrix: 1.23, 1.24
|
||||
- `go vet ./...`
|
||||
- `staticcheck ./...`
|
||||
- `go test -race -coverprofile=coverage.out ./...`
|
||||
- Codegen-clean check: `make regen-from-fixture` + `git diff --exit-code` to assert generated files match the committed IR for the snapshot fixture (deterministic).
|
||||
- Upload coverage artifact.
|
||||
|
||||
## 11. Handling API changes & test maintenance
|
||||
|
||||
Telegram ships changes to the Bot API roughly every 1–3 months: new methods, new types, added optional fields, occasional removals or renames, occasional union-variant additions. The design must absorb these with minimum manual work.
|
||||
|
||||
### 11.1 Test-suite invariants under change
|
||||
|
||||
Tests are layered so that the cost of an API change is bounded.
|
||||
|
||||
| Test layer | Affected by API change? | Cost |
|
||||
|---|---|---|
|
||||
| `client/` unit tests (call helper, error mapping, codec, multipart builder) | No — they target `call[Req,Resp]`, not specific methods. | Zero. |
|
||||
| `transport/` unit tests (long-poll loop, webhook server) | No — they target update plumbing, not payload fields. | Zero. |
|
||||
| `dispatch/` unit tests (matchers, middleware, router) | No — generic over `*api.Update`. | Zero. |
|
||||
| Codegen golden tests (`cmd/scrape`, `cmd/genapi`) | Yes — golden `api.json` and `*.gen.go` will diff. | Refresh goldens deliberately (`go test -run TestGen -update`). |
|
||||
| Codegen "shape" smoke tests | Only if a code-generation pattern changes. | One test per pattern, not per method. |
|
||||
| Examples (`examples/echo`, `examples/webhook`) | Only if they reference a removed/renamed symbol. | Hand-fix; rare. |
|
||||
| Integration suite (build tag `integration`) | Only for the ~5 methods it touches. | Hand-fix on removal/rename; rare. |
|
||||
|
||||
The deliberate invariant: **we do not write a test per generated method.** All ~100+ generated wrappers funnel through `call[Req,Resp]` and the multipart builder; coverage of those two paths covers them all. Each generated method is then sanity-checked by being type-checked at compile time against the IR.
|
||||
|
||||
### 11.2 Shape smoke tests
|
||||
|
||||
In `api/api_test.go`, one test per code-generation pattern, hitting a representative method through a mocked `HTTPDoer`:
|
||||
|
||||
- **Simple** — `getMe` (no params, scalar response).
|
||||
- **Typed-struct param** — `sendMessage` (struct in, object out).
|
||||
- **Optional fields** — `sendMessage` with only required fields set; verify omitted fields do not appear in the request body.
|
||||
- **Array result** — `getUpdates` (array of `Update`).
|
||||
- **Bool result** — `setWebhook`.
|
||||
- **Multipart upload** — `sendDocument` with an `InputFile` (verify content-type, boundary, field names).
|
||||
- **OneOf union response** — `getChatMember` (returns `ChatMember` union).
|
||||
- **OneOf union request** — `sendMediaGroup` (accepts `[]InputMedia` union).
|
||||
|
||||
If new code-generation patterns appear (Telegram introducing a new shape we have not seen), one new shape test is added — not one per affected method.
|
||||
|
||||
### 11.3 Categories of change and how each is absorbed
|
||||
|
||||
| Change | Pipeline effect | Test effect |
|
||||
|---|---|---|
|
||||
| New type | Appears in `api.json`, emitted into `types.gen.go`. | Golden diff only. Refresh. |
|
||||
| New optional field | Same. | Golden diff only. |
|
||||
| New required field | Same. Breaking for users who construct that struct literally. | Golden diff only; example code may need update. |
|
||||
| Removed type/method/field | Disappears from emitted Go. Breaking for users referring to it. | Golden diff. Integration test or example may break — fix or skip. |
|
||||
| Renamed field | Old name disappears, new appears. | Same as above; no automatic rename (we treat as remove + add). |
|
||||
| New method | Wrapper generated; no new test required (shape tests cover the call path). | Golden diff. |
|
||||
| Return type changed for an existing method | Wrapper signature changes. Breaking. | Golden diff; integration test for that method may break. |
|
||||
| New OneOf variant | `UnmarshalJSON` switch grows a case. | Golden diff. If a brand-new variant style appears, may require scraper work and a new shape test. |
|
||||
| Telegram doc layout change | Scraper may misparse. | Scraper unit test against the new HTML fixture should be added before regenerating. |
|
||||
|
||||
### 11.4 The change procedure
|
||||
|
||||
When the scraper output changes:
|
||||
|
||||
1. Run `make regen-from-fixture` against the current `testdata/html/snapshot_*.html` (deterministic check) — confirm zero unrelated diffs.
|
||||
2. Capture a fresh HTML snapshot: `make snapshot` (writes `testdata/html/snapshot_<date>.html`).
|
||||
3. Run `make regen` against the new snapshot → IR diff appears in `internal/spec/api.json`.
|
||||
4. Review the IR diff as a Telegram changelog. This is the human read-through; it is the entire point of having an IR.
|
||||
5. Run `go test ./...` → expect golden codegen diffs.
|
||||
6. Refresh goldens: `go test -run TestGen -update`.
|
||||
7. Re-run `go test ./...` → green.
|
||||
8. If shape tests reveal a new code-generation pattern (e.g. a never-before-seen union shape), extend templates and add a shape test before refreshing goldens.
|
||||
9. If `examples/` or integration tests reference a removed symbol, fix them.
|
||||
10. Commit with a clear message: `chore(api): regenerate from Telegram Bot API vX.Y` plus a bullet list extracted from the IR diff (added/changed/removed types and methods).
|
||||
|
||||
This is the same procedure the future auto-regen workflow will run; doing it by hand first ensures the workflow has nothing surprising to do.
|
||||
|
||||
### 11.5 Versioning policy
|
||||
|
||||
- Library SemVer is decoupled from Telegram's API version.
|
||||
- Telegram-side additions → minor bump.
|
||||
- Telegram-side removals or signature changes → major bump (we do not preserve removed symbols as deprecated stubs; the breaking change ships).
|
||||
- Bug fixes in hand-written code → patch bump.
|
||||
- Each release records the Telegram API version it was generated against in the release notes and in a `// Generated from Bot API vX.Y` constant in `api/version.gen.go`.
|
||||
|
||||
## 12. Future work (deferred)
|
||||
|
||||
- **Auto-regen workflow** — daily cron + `workflow_dispatch` that runs `cmd/scrape` against the live URL, regenerates code, opens a PR with a diff summary, and auto-merges on green CI. Implementation sketch retained in design discussion; not part of v1 acceptance.
|
||||
- **Release workflow** — tag-triggered `goreleaser` pipeline producing GH Releases. SemVer for the library; Telegram's API version recorded in release notes.
|
||||
- **Additional codecs/HTTP adapters** as separate sub-packages or contrib modules so users can opt in without bringing transitive deps into the core.
|
||||
|
||||
## 13. Dependency policy
|
||||
|
||||
Production:
|
||||
- Go standard library
|
||||
- `golang.org/x/net/html` (scraper only)
|
||||
|
||||
Test-only:
|
||||
- `github.com/stretchr/testify`
|
||||
|
||||
Explicit non-deps: `goquery`, `cobra`, third-party logging, third-party HTTP clients, third-party JSON codecs (these are user-supplied via `HTTPDoer` and `Codec`).
|
||||
|
||||
## 14. Acceptance criteria
|
||||
|
||||
v1 is done when:
|
||||
|
||||
1. `go test ./...` passes on a clean checkout with no env vars.
|
||||
2. `make regen` produces zero diff against the committed `api.json` and `api/*.gen.go` when run against the committed HTML fixture.
|
||||
3. `examples/echo` and `examples/webhook` build and the echo example runs end-to-end against a real bot when `TELEGRAM_BOT_TOKEN` is set.
|
||||
4. `go vet`, `staticcheck`, and `go test -race` are clean.
|
||||
5. Every exported symbol in hand-written packages has a doc comment.
|
||||
6. README covers Why, Install, Quick Start, Custom HTTP/JSON, Webhooks, Dispatcher, Updating, Contributing.
|
||||
7. The integration test suite (`-tags=integration`) runs cleanly when env is provided.
|
||||
|
||||
## 15. Open questions
|
||||
|
||||
None at sign-off. (Auto-regen behaviour intentionally deferred.)
|
||||
@@ -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++
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -68,7 +68,7 @@ func TestHandleStart_SendsInitialKeyboard(t *testing.T) {
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
msg := &api.Message{
|
||||
MessageID: 1,
|
||||
Chat: api.Chat{ID: 42, Type: string(api.ChatTypePrivate)},
|
||||
Chat: api.Chat{ID: 42, Type: api.ChatTypePrivate},
|
||||
From: &api.User{ID: 7, FirstName: "Alice"},
|
||||
Text: "/start",
|
||||
}
|
||||
@@ -82,13 +82,15 @@ 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 {
|
||||
msg := &api.Message{
|
||||
MessageID: msgID,
|
||||
Chat: api.Chat{ID: chatID, Type: string(api.ChatTypePrivate)},
|
||||
Chat: api.Chat{ID: chatID, Type: api.ChatTypePrivate},
|
||||
}
|
||||
return &api.CallbackQuery{
|
||||
ID: "cb1",
|
||||
|
||||
@@ -46,7 +46,7 @@ func msgUpd(userID, chatID int64, text string) api.Update {
|
||||
}
|
||||
}
|
||||
entities = append(entities, api.MessageEntity{
|
||||
Type: string(api.EntityBotCommand),
|
||||
Type: api.MessageEntityTypeBotCommand,
|
||||
Offset: 0,
|
||||
Length: int64(end),
|
||||
})
|
||||
@@ -56,7 +56,7 @@ func msgUpd(userID, chatID int64, text string) api.Update {
|
||||
Message: &api.Message{
|
||||
MessageID: 1,
|
||||
From: &api.User{ID: userID},
|
||||
Chat: api.Chat{ID: chatID, Type: string(api.ChatTypePrivate)},
|
||||
Chat: api.Chat{ID: chatID, Type: api.ChatTypePrivate},
|
||||
Text: text,
|
||||
Entities: entities,
|
||||
},
|
||||
|
||||
@@ -55,7 +55,7 @@ func TestHandleStart_GreetsUser(t *testing.T) {
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
msg := &api.Message{
|
||||
MessageID: 1,
|
||||
Chat: api.Chat{ID: 42, Type: string(api.ChatTypePrivate)},
|
||||
Chat: api.Chat{ID: 42, Type: api.ChatTypePrivate},
|
||||
From: &api.User{ID: 7, FirstName: "Alice"},
|
||||
Text: "/start",
|
||||
}
|
||||
@@ -82,7 +82,7 @@ func TestHandleEcho_RepliesWithSameText(t *testing.T) {
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
msg := &api.Message{
|
||||
MessageID: 5,
|
||||
Chat: api.Chat{ID: 42, Type: string(api.ChatTypePrivate)},
|
||||
Chat: api.Chat{ID: 42, Type: api.ChatTypePrivate},
|
||||
From: &api.User{ID: 7, FirstName: "Alice"},
|
||||
Text: "hello echo",
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ func main() {
|
||||
// Echo the query as article results.
|
||||
results := []api.InlineQueryResult{
|
||||
&api.InlineQueryResultArticle{
|
||||
Type: "article",
|
||||
ID: "echo",
|
||||
Title: "Echo: " + q.Query,
|
||||
InputMessageContent: &api.InputTextMessageContent{
|
||||
@@ -43,7 +42,6 @@ func main() {
|
||||
},
|
||||
},
|
||||
&api.InlineQueryResultArticle{
|
||||
Type: "article",
|
||||
ID: "upper",
|
||||
Title: "UPPER: " + strings.ToUpper(q.Query),
|
||||
InputMessageContent: &api.InputTextMessageContent{
|
||||
|
||||
+11
-14
@@ -96,11 +96,10 @@ func kickHandler(c *dispatch.Context, m *api.Message) error {
|
||||
}); err != nil {
|
||||
return handleAdminErr(c, m.Chat.ID, err)
|
||||
}
|
||||
truVal := true
|
||||
if _, err := api.UnbanChatMember(c.Ctx, c.Bot, &api.UnbanChatMemberParams{
|
||||
ChatID: api.ChatIDFromInt(m.Chat.ID),
|
||||
UserID: target,
|
||||
OnlyIfBanned: &truVal,
|
||||
OnlyIfBanned: api.Ptr(true),
|
||||
}); err != nil {
|
||||
log.Printf("unban after kick: %v", err)
|
||||
}
|
||||
@@ -130,23 +129,21 @@ func muteHandler(c *dispatch.Context, m *api.Message) error {
|
||||
reply(c, m.Chat.ID, "Reply to a user's message with /mute to silence them for 1 hour.")
|
||||
return nil
|
||||
}
|
||||
until := time.Now().Add(time.Hour).Unix()
|
||||
falseVal := false
|
||||
if _, err := api.RestrictChatMember(c.Ctx, c.Bot, &api.RestrictChatMemberParams{
|
||||
ChatID: api.ChatIDFromInt(m.Chat.ID),
|
||||
UserID: target,
|
||||
Permissions: api.ChatPermissions{
|
||||
CanSendMessages: &falseVal,
|
||||
CanSendAudios: &falseVal,
|
||||
CanSendDocuments: &falseVal,
|
||||
CanSendPhotos: &falseVal,
|
||||
CanSendVideos: &falseVal,
|
||||
CanSendVideoNotes: &falseVal,
|
||||
CanSendVoiceNotes: &falseVal,
|
||||
CanSendPolls: &falseVal,
|
||||
CanSendOtherMessages: &falseVal,
|
||||
CanSendMessages: api.Ptr(false),
|
||||
CanSendAudios: api.Ptr(false),
|
||||
CanSendDocuments: api.Ptr(false),
|
||||
CanSendPhotos: api.Ptr(false),
|
||||
CanSendVideos: api.Ptr(false),
|
||||
CanSendVideoNotes: api.Ptr(false),
|
||||
CanSendVoiceNotes: api.Ptr(false),
|
||||
CanSendPolls: api.Ptr(false),
|
||||
CanSendOtherMessages: api.Ptr(false),
|
||||
},
|
||||
UntilDate: &until,
|
||||
UntilDate: api.Ptr(time.Now().Add(time.Hour).Unix()),
|
||||
}); err != nil {
|
||||
return handleAdminErr(c, m.Chat.ID, err)
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -94,12 +94,11 @@ func main() {
|
||||
})
|
||||
return nil
|
||||
}
|
||||
isAnon := false
|
||||
msg, err := api.SendPoll(c.Ctx, c.Bot, &api.SendPollParams{
|
||||
ChatID: api.ChatIDFromInt(m.Chat.ID),
|
||||
Question: question,
|
||||
Options: pollOptions,
|
||||
IsAnonymous: &isAnon,
|
||||
IsAnonymous: api.Ptr(false),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -49,7 +49,7 @@ func TestHandlePing_RepliesWithPong(t *testing.T) {
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
msg := &api.Message{
|
||||
MessageID: 1,
|
||||
Chat: api.Chat{ID: 42, Type: string(api.ChatTypePrivate)},
|
||||
Chat: api.Chat{ID: 42, Type: api.ChatTypePrivate},
|
||||
From: &api.User{ID: 7, FirstName: "Alice"},
|
||||
Text: "/ping",
|
||||
}
|
||||
|
||||
@@ -4,13 +4,18 @@ go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/goccy/go-json v0.10.6
|
||||
github.com/lukaszraczylo/oss-telemetry v0.2.1
|
||||
github.com/stretchr/testify v1.9.0
|
||||
golang.org/x/net v0.54.0
|
||||
github.com/valyala/fasthttp v1.71.0
|
||||
golang.org/x/net v0.55.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.2.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/klauspost/compress v1.18.6 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
|
||||
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/lukaszraczylo/oss-telemetry v0.2.1 h1:6ULyfzXplpdmIY/i01OPM1jeod9+L1RAhI0jtbVnJI0=
|
||||
github.com/lukaszraczylo/oss-telemetry v0.2.1/go.mod h1:+Cn78qZo8rc3T9eZt0v3oICYRdd75wORtSidc8lNjDQ=
|
||||
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=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.71.0 h1:tepR7H+Guh9VUqxxcPggYi8R3lGUu2Rsdh+z7/FCY3k=
|
||||
github.com/valyala/fasthttp v1.71.0/go.mod h1:z1sDUvOShhXq/C9mwH/fSm1Vb71tUJwmQdgkBrBNwnA=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
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=
|
||||
|
||||
+1049
-308
File diff suppressed because it is too large
Load Diff
@@ -45,6 +45,11 @@ type Field struct {
|
||||
Type TypeRef `json:"type"`
|
||||
Required bool `json:"required,omitempty"`
|
||||
Doc string `json:"doc,omitempty"`
|
||||
// EnumValues, when non-empty, lists the wire-level string values the
|
||||
// scraper detected for an enum-like description ("can be A, B or C",
|
||||
// "always X", parse_mode special-case). Order is doc order, deduped.
|
||||
// Emitted as a typed Go enum that replaces the field's string type.
|
||||
EnumValues []string `json:"enum_values,omitempty"`
|
||||
}
|
||||
|
||||
// Kind enumerates TypeRef shapes.
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
module github.com/lukaszraczylo/go-telegram/test/benchmarks
|
||||
|
||||
go 1.25.7
|
||||
|
||||
replace github.com/lukaszraczylo/go-telegram => ../../
|
||||
|
||||
require (
|
||||
github.com/NicoNex/echotron/v3 v3.45.0
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
|
||||
github.com/go-telegram/bot v1.20.0
|
||||
github.com/lukaszraczylo/go-telegram v0.0.0-00010101000000-000000000000
|
||||
github.com/mymmrac/telego v1.8.0
|
||||
gopkg.in/telebot.v3 v3.3.8
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.2.1 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.15.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/goccy/go-json v0.10.6 // indirect
|
||||
github.com/grbit/go-json v0.11.0 // indirect
|
||||
github.com/klauspost/compress v1.18.6 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.71.0 // indirect
|
||||
github.com/valyala/fastjson v1.6.10 // indirect
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,905 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
|
||||
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
|
||||
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
|
||||
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
|
||||
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
|
||||
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
|
||||
cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
|
||||
cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
|
||||
cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
|
||||
cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
|
||||
cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
|
||||
cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
|
||||
cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
|
||||
cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
|
||||
cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow=
|
||||
cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM=
|
||||
cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M=
|
||||
cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s=
|
||||
cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||
github.com/NicoNex/echotron/v3 v3.45.0 h1:hORujGwc6X2yaiHZIt8/rYZR48L524YtdiHxpqBwlko=
|
||||
github.com/NicoNex/echotron/v3 v3.45.0/go.mod h1:7LvjveJmezuUOeaoA3nzQduNlSPQYfq219Z+baKY04Q=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
|
||||
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
|
||||
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
|
||||
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
|
||||
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
|
||||
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
|
||||
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
|
||||
github.com/go-telegram/bot v1.20.0 h1:4Pea/qTidSspr4WBJw9FbHUMNhYeqszBqQUfsQEyFbc=
|
||||
github.com/go-telegram/bot v1.20.0/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM=
|
||||
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
|
||||
github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
|
||||
github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM=
|
||||
github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=
|
||||
github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c=
|
||||
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
||||
github.com/grbit/go-json v0.11.0 h1:bAbyMdYrYl/OjYsSqLH99N2DyQ291mHy726Mx+sYrnc=
|
||||
github.com/grbit/go-json v0.11.0/go.mod h1:IYpHsdybQ386+6g3VE6AXQ3uTGa5mquBme5/ZWmtzek=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0=
|
||||
github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||
github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
|
||||
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
|
||||
github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
|
||||
github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
|
||||
github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
||||
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
||||
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
|
||||
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mymmrac/telego v1.8.0 h1:EvIprWo9Cn0MHgumvvqNXPAXO1yJj3pu2cdCCeDxbow=
|
||||
github.com/mymmrac/telego v1.8.0/go.mod h1:pdLV346EgVuq7Xrh3kMggeBiazeHhsdEoK0RTEOPXRM=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
|
||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo=
|
||||
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.71.0 h1:tepR7H+Guh9VUqxxcPggYi8R3lGUu2Rsdh+z7/FCY3k=
|
||||
github.com/valyala/fasthttp v1.71.0/go.mod h1:z1sDUvOShhXq/C9mwH/fSm1Vb71tUJwmQdgkBrBNwnA=
|
||||
github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4=
|
||||
github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
|
||||
go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU=
|
||||
go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
|
||||
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
|
||||
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
|
||||
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
|
||||
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
|
||||
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
|
||||
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
|
||||
google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
|
||||
google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
|
||||
google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
|
||||
google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
|
||||
google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
|
||||
google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
|
||||
google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
|
||||
google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=
|
||||
google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU=
|
||||
google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
|
||||
google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo=
|
||||
google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g=
|
||||
google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA=
|
||||
google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8=
|
||||
google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs=
|
||||
google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=
|
||||
google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw=
|
||||
google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
|
||||
google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
|
||||
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
|
||||
google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
|
||||
google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
|
||||
google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
|
||||
google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
|
||||
google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
|
||||
google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
|
||||
google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
|
||||
google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
|
||||
google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
|
||||
google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
|
||||
google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
|
||||
google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
|
||||
google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
|
||||
google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
|
||||
google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
|
||||
google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
|
||||
google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
|
||||
google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=
|
||||
google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
|
||||
google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
|
||||
google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
|
||||
google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
|
||||
google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
|
||||
google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
|
||||
google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
|
||||
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||
google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||
google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
|
||||
google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
|
||||
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||
google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||
google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
|
||||
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
|
||||
google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
|
||||
google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/telebot.v3 v3.3.8 h1:uVDGjak9l824FN9YARWUHMsiNZnlohAVwUycw21k6t8=
|
||||
gopkg.in/telebot.v3 v3.3.8/go.mod h1:1mlbqcLTVSfK9dx7fdp+Nb5HZsy4LLPtpZTKmwhwtzM=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user