mirror of
https://github.com/lukaszraczylo/go-telegram.git
synced 2026-06-06 22:49:32 +00:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7eb3398396 | |||
| 26b98a5372 | |||
| a416bda5f3 | |||
| 0ee539e991 | |||
| da27421521 | |||
| 728b28b0c5 | |||
| 9cfe193e2e | |||
| 79c0617867 | |||
| 60eb0a89b5 | |||
| fecef22f48 | |||
| 5523ed2b06 | |||
| 62c76e7e4e | |||
| 6f9b29ea0c | |||
| bd80af240d | |||
| af180b75c5 | |||
| 29bf575cfd | |||
| 370c9c0802 | |||
| 6ab80c27e1 | |||
| 13ea7097e1 | |||
| f899cc2663 |
@@ -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
|
||||
@@ -245,23 +245,17 @@ jobs:
|
||||
API_TAG: ${{ steps.api_version.outputs.tag }}
|
||||
API_VER: ${{ steps.api_version.outputs.version }}
|
||||
run: |
|
||||
# Bot API version (currently $API_VER) is intentionally NOT
|
||||
# tagged separately. semver-generator picks the most recent
|
||||
# tag as the version base; a non-SemVer marker like
|
||||
# bot-api-vX.Y poisons that and restarts numbering from
|
||||
# v0.0.x. Bot API version stays as a comment in the lib tag
|
||||
# message and in the release notes.
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git tag -a "$LIB_TAG" -m "Release $LIB_TAG"
|
||||
git tag -a "$LIB_TAG" -m "Release $LIB_TAG (Bot API $API_VER)"
|
||||
git push origin "$LIB_TAG"
|
||||
|
||||
if [ -n "$API_TAG" ]; then
|
||||
# Force-update the bot-api tag so it always points at the latest
|
||||
# release that supports that API version.
|
||||
if git rev-parse "$API_TAG" >/dev/null 2>&1; then
|
||||
git tag -f -a "$API_TAG" -m "go-telegram release $LIB_TAG (Bot API $API_VER)"
|
||||
git push -f origin "$API_TAG"
|
||||
else
|
||||
git tag -a "$API_TAG" -m "go-telegram release $LIB_TAG (Bot API $API_VER)"
|
||||
git push origin "$API_TAG"
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Run GoReleaser
|
||||
if: github.event_name != 'workflow_dispatch' || inputs.dry-run-release == false
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
|
||||
@@ -82,7 +82,11 @@ DOC_PACKAGES := \
|
||||
|
||||
docs:
|
||||
@which gomarkdoc > /dev/null || (echo "installing gomarkdoc..." && $(GO) install github.com/princjef/gomarkdoc/cmd/gomarkdoc@v1.1.0)
|
||||
gomarkdoc -o 'docs/reference/{{.Dir}}.md' $(DOC_PACKAGES)
|
||||
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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
+161
-204
@@ -4,100 +4,52 @@
|
||||
|
||||
package api
|
||||
|
||||
type BackgroundFillFreeformGradientType string
|
||||
type BackgroundFillType string
|
||||
|
||||
const (
|
||||
BackgroundFillFreeformGradientTypeFreeformGradient BackgroundFillFreeformGradientType = "freeform_gradient"
|
||||
BackgroundFillTypeSolid BackgroundFillType = "solid"
|
||||
BackgroundFillTypeGradient BackgroundFillType = "gradient"
|
||||
BackgroundFillTypeFreeformGradient BackgroundFillType = "freeform_gradient"
|
||||
)
|
||||
|
||||
type BackgroundFillGradientType string
|
||||
type BackgroundTypeKind string
|
||||
|
||||
const (
|
||||
BackgroundFillGradientTypeGradient BackgroundFillGradientType = "gradient"
|
||||
BackgroundTypeKindFill BackgroundTypeKind = "fill"
|
||||
BackgroundTypeKindWallpaper BackgroundTypeKind = "wallpaper"
|
||||
BackgroundTypeKindPattern BackgroundTypeKind = "pattern"
|
||||
BackgroundTypeKindChatTheme BackgroundTypeKind = "chat_theme"
|
||||
)
|
||||
|
||||
type BackgroundFillSolidType string
|
||||
type BotCommandScopeType string
|
||||
|
||||
const (
|
||||
BackgroundFillSolidTypeSolid BackgroundFillSolidType = "solid"
|
||||
BotCommandScopeTypeDefault BotCommandScopeType = "default"
|
||||
BotCommandScopeTypeAllPrivateChats BotCommandScopeType = "all_private_chats"
|
||||
BotCommandScopeTypeAllGroupChats BotCommandScopeType = "all_group_chats"
|
||||
BotCommandScopeTypeAllChatAdministrators BotCommandScopeType = "all_chat_administrators"
|
||||
BotCommandScopeTypeChat BotCommandScopeType = "chat"
|
||||
BotCommandScopeTypeChatAdministrators BotCommandScopeType = "chat_administrators"
|
||||
BotCommandScopeTypeChatMember BotCommandScopeType = "chat_member"
|
||||
)
|
||||
|
||||
type BackgroundTypeChatThemeType string
|
||||
type ChatBoostSourceKind string
|
||||
|
||||
const (
|
||||
BackgroundTypeChatThemeTypeChatTheme BackgroundTypeChatThemeType = "chat_theme"
|
||||
ChatBoostSourceKindPremium ChatBoostSourceKind = "premium"
|
||||
ChatBoostSourceKindGiftCode ChatBoostSourceKind = "gift_code"
|
||||
ChatBoostSourceKindGiveaway ChatBoostSourceKind = "giveaway"
|
||||
)
|
||||
|
||||
type BackgroundTypeFillType string
|
||||
type ChatMemberStatus string
|
||||
|
||||
const (
|
||||
BackgroundTypeFillTypeFill BackgroundTypeFillType = "fill"
|
||||
)
|
||||
|
||||
type BackgroundTypePatternType string
|
||||
|
||||
const (
|
||||
BackgroundTypePatternTypePattern BackgroundTypePatternType = "pattern"
|
||||
)
|
||||
|
||||
type BackgroundTypeWallpaperType string
|
||||
|
||||
const (
|
||||
BackgroundTypeWallpaperTypeWallpaper BackgroundTypeWallpaperType = "wallpaper"
|
||||
)
|
||||
|
||||
type ChatBoostSourceGiftCodeSource string
|
||||
|
||||
const (
|
||||
ChatBoostSourceGiftCodeSourceGiftCode ChatBoostSourceGiftCodeSource = "gift_code"
|
||||
)
|
||||
|
||||
type ChatBoostSourceGiveawaySource string
|
||||
|
||||
const (
|
||||
ChatBoostSourceGiveawaySourceGiveaway ChatBoostSourceGiveawaySource = "giveaway"
|
||||
)
|
||||
|
||||
type ChatBoostSourcePremiumSource string
|
||||
|
||||
const (
|
||||
ChatBoostSourcePremiumSourcePremium ChatBoostSourcePremiumSource = "premium"
|
||||
)
|
||||
|
||||
type ChatMemberAdministratorStatus string
|
||||
|
||||
const (
|
||||
ChatMemberAdministratorStatusAdministrator ChatMemberAdministratorStatus = "administrator"
|
||||
)
|
||||
|
||||
type ChatMemberBannedStatus string
|
||||
|
||||
const (
|
||||
ChatMemberBannedStatusKicked ChatMemberBannedStatus = "kicked"
|
||||
)
|
||||
|
||||
type ChatMemberLeftStatus string
|
||||
|
||||
const (
|
||||
ChatMemberLeftStatusLeft ChatMemberLeftStatus = "left"
|
||||
)
|
||||
|
||||
type ChatMemberMemberStatus string
|
||||
|
||||
const (
|
||||
ChatMemberMemberStatusMember ChatMemberMemberStatus = "member"
|
||||
)
|
||||
|
||||
type ChatMemberOwnerStatus string
|
||||
|
||||
const (
|
||||
ChatMemberOwnerStatusCreator ChatMemberOwnerStatus = "creator"
|
||||
)
|
||||
|
||||
type ChatMemberRestrictedStatus string
|
||||
|
||||
const (
|
||||
ChatMemberRestrictedStatusRestricted ChatMemberRestrictedStatus = "restricted"
|
||||
ChatMemberStatusCreator ChatMemberStatus = "creator"
|
||||
ChatMemberStatusAdministrator ChatMemberStatus = "administrator"
|
||||
ChatMemberStatusMember ChatMemberStatus = "member"
|
||||
ChatMemberStatusRestricted ChatMemberStatus = "restricted"
|
||||
ChatMemberStatusLeft ChatMemberStatus = "left"
|
||||
ChatMemberStatusKicked ChatMemberStatus = "kicked"
|
||||
)
|
||||
|
||||
type ChatType string
|
||||
@@ -152,6 +104,75 @@ const (
|
||||
InlineQueryResultGifThumbnailMimeTypeVideoOfMp4 InlineQueryResultGifThumbnailMimeType = "video/mp4"
|
||||
)
|
||||
|
||||
type InlineQueryResultType string
|
||||
|
||||
const (
|
||||
InlineQueryResultTypeAudio InlineQueryResultType = "audio"
|
||||
InlineQueryResultTypeDocument InlineQueryResultType = "document"
|
||||
InlineQueryResultTypeGif InlineQueryResultType = "gif"
|
||||
InlineQueryResultTypeMpeg4Gif InlineQueryResultType = "mpeg4_gif"
|
||||
InlineQueryResultTypePhoto InlineQueryResultType = "photo"
|
||||
InlineQueryResultTypeSticker InlineQueryResultType = "sticker"
|
||||
InlineQueryResultTypeVideo InlineQueryResultType = "video"
|
||||
InlineQueryResultTypeVoice InlineQueryResultType = "voice"
|
||||
InlineQueryResultTypeArticle InlineQueryResultType = "article"
|
||||
InlineQueryResultTypeContact InlineQueryResultType = "contact"
|
||||
InlineQueryResultTypeGame InlineQueryResultType = "game"
|
||||
InlineQueryResultTypeLocation InlineQueryResultType = "location"
|
||||
InlineQueryResultTypeVenue InlineQueryResultType = "venue"
|
||||
)
|
||||
|
||||
type InputMediaType string
|
||||
|
||||
const (
|
||||
InputMediaTypeAnimation InputMediaType = "animation"
|
||||
InputMediaTypeAudio InputMediaType = "audio"
|
||||
InputMediaTypeDocument InputMediaType = "document"
|
||||
InputMediaTypeLivePhoto InputMediaType = "live_photo"
|
||||
InputMediaTypePhoto InputMediaType = "photo"
|
||||
InputMediaTypeVideo InputMediaType = "video"
|
||||
)
|
||||
|
||||
type InputPaidMediaType string
|
||||
|
||||
const (
|
||||
InputPaidMediaTypeLivePhoto InputPaidMediaType = "live_photo"
|
||||
InputPaidMediaTypePhoto InputPaidMediaType = "photo"
|
||||
InputPaidMediaTypeVideo InputPaidMediaType = "video"
|
||||
)
|
||||
|
||||
type InputPollMediaType string
|
||||
|
||||
const (
|
||||
InputPollMediaTypeAnimation InputPollMediaType = "animation"
|
||||
InputPollMediaTypeAudio InputPollMediaType = "audio"
|
||||
InputPollMediaTypeDocument InputPollMediaType = "document"
|
||||
InputPollMediaTypeLivePhoto InputPollMediaType = "live_photo"
|
||||
InputPollMediaTypeLocation InputPollMediaType = "location"
|
||||
InputPollMediaTypePhoto InputPollMediaType = "photo"
|
||||
InputPollMediaTypeVenue InputPollMediaType = "venue"
|
||||
InputPollMediaTypeVideo InputPollMediaType = "video"
|
||||
)
|
||||
|
||||
type InputPollOptionMediaType string
|
||||
|
||||
const (
|
||||
InputPollOptionMediaTypeAnimation InputPollOptionMediaType = "animation"
|
||||
InputPollOptionMediaTypeLivePhoto InputPollOptionMediaType = "live_photo"
|
||||
InputPollOptionMediaTypeLocation InputPollOptionMediaType = "location"
|
||||
InputPollOptionMediaTypePhoto InputPollOptionMediaType = "photo"
|
||||
InputPollOptionMediaTypeSticker InputPollOptionMediaType = "sticker"
|
||||
InputPollOptionMediaTypeVenue InputPollOptionMediaType = "venue"
|
||||
InputPollOptionMediaTypeVideo InputPollOptionMediaType = "video"
|
||||
)
|
||||
|
||||
type InputProfilePhotoType string
|
||||
|
||||
const (
|
||||
InputProfilePhotoTypeStatic InputProfilePhotoType = "static"
|
||||
InputProfilePhotoTypeAnimated InputProfilePhotoType = "animated"
|
||||
)
|
||||
|
||||
type InputStickerFormat string
|
||||
|
||||
const (
|
||||
@@ -160,6 +181,13 @@ const (
|
||||
InputStickerFormatVideo InputStickerFormat = "video"
|
||||
)
|
||||
|
||||
type InputStoryContentType string
|
||||
|
||||
const (
|
||||
InputStoryContentTypePhoto InputStoryContentType = "photo"
|
||||
InputStoryContentTypeVideo InputStoryContentType = "video"
|
||||
)
|
||||
|
||||
type KeyboardButtonStyle string
|
||||
|
||||
const (
|
||||
@@ -177,6 +205,14 @@ const (
|
||||
MaskPositionPointChin MaskPositionPoint = "chin"
|
||||
)
|
||||
|
||||
type MenuButtonType string
|
||||
|
||||
const (
|
||||
MenuButtonTypeCommands MenuButtonType = "commands"
|
||||
MenuButtonTypeWebApp MenuButtonType = "web_app"
|
||||
MenuButtonTypeDefault MenuButtonType = "default"
|
||||
)
|
||||
|
||||
type MessageEntityType string
|
||||
|
||||
const (
|
||||
@@ -202,64 +238,29 @@ const (
|
||||
MessageEntityTypeDateTime MessageEntityType = "date_time"
|
||||
)
|
||||
|
||||
type MessageOriginChannelType string
|
||||
type MessageOriginType string
|
||||
|
||||
const (
|
||||
MessageOriginChannelTypeChannel MessageOriginChannelType = "channel"
|
||||
MessageOriginTypeUser MessageOriginType = "user"
|
||||
MessageOriginTypeHiddenUser MessageOriginType = "hidden_user"
|
||||
MessageOriginTypeChat MessageOriginType = "chat"
|
||||
MessageOriginTypeChannel MessageOriginType = "channel"
|
||||
)
|
||||
|
||||
type MessageOriginChatType string
|
||||
type OwnedGiftType string
|
||||
|
||||
const (
|
||||
MessageOriginChatTypeChat MessageOriginChatType = "chat"
|
||||
OwnedGiftTypeRegular OwnedGiftType = "regular"
|
||||
OwnedGiftTypeUnique OwnedGiftType = "unique"
|
||||
)
|
||||
|
||||
type MessageOriginHiddenUserType string
|
||||
type PaidMediaType string
|
||||
|
||||
const (
|
||||
MessageOriginHiddenUserTypeHiddenUser MessageOriginHiddenUserType = "hidden_user"
|
||||
)
|
||||
|
||||
type MessageOriginUserType string
|
||||
|
||||
const (
|
||||
MessageOriginUserTypeUser MessageOriginUserType = "user"
|
||||
)
|
||||
|
||||
type OwnedGiftRegularType string
|
||||
|
||||
const (
|
||||
OwnedGiftRegularTypeRegular OwnedGiftRegularType = "regular"
|
||||
)
|
||||
|
||||
type OwnedGiftUniqueType string
|
||||
|
||||
const (
|
||||
OwnedGiftUniqueTypeUnique OwnedGiftUniqueType = "unique"
|
||||
)
|
||||
|
||||
type PaidMediaLivePhotoType string
|
||||
|
||||
const (
|
||||
PaidMediaLivePhotoTypeLivePhoto PaidMediaLivePhotoType = "live_photo"
|
||||
)
|
||||
|
||||
type PaidMediaPhotoType string
|
||||
|
||||
const (
|
||||
PaidMediaPhotoTypePhoto PaidMediaPhotoType = "photo"
|
||||
)
|
||||
|
||||
type PaidMediaPreviewType string
|
||||
|
||||
const (
|
||||
PaidMediaPreviewTypePreview PaidMediaPreviewType = "preview"
|
||||
)
|
||||
|
||||
type PaidMediaVideoType string
|
||||
|
||||
const (
|
||||
PaidMediaVideoTypeVideo PaidMediaVideoType = "video"
|
||||
PaidMediaTypeLivePhoto PaidMediaType = "live_photo"
|
||||
PaidMediaTypePhoto PaidMediaType = "photo"
|
||||
PaidMediaTypePreview PaidMediaType = "preview"
|
||||
PaidMediaTypeVideo PaidMediaType = "video"
|
||||
)
|
||||
|
||||
type ParseMode string
|
||||
@@ -307,6 +308,20 @@ const (
|
||||
PassportElementErrorSelfieTypeInternalPassport PassportElementErrorSelfieType = "internal_passport"
|
||||
)
|
||||
|
||||
type PassportElementErrorSource string
|
||||
|
||||
const (
|
||||
PassportElementErrorSourceData PassportElementErrorSource = "data"
|
||||
PassportElementErrorSourceFrontSide PassportElementErrorSource = "front_side"
|
||||
PassportElementErrorSourceReverseSide PassportElementErrorSource = "reverse_side"
|
||||
PassportElementErrorSourceSelfie PassportElementErrorSource = "selfie"
|
||||
PassportElementErrorSourceFile PassportElementErrorSource = "file"
|
||||
PassportElementErrorSourceFiles PassportElementErrorSource = "files"
|
||||
PassportElementErrorSourceTranslationFile PassportElementErrorSource = "translation_file"
|
||||
PassportElementErrorSourceTranslationFiles PassportElementErrorSource = "translation_files"
|
||||
PassportElementErrorSourceUnspecified PassportElementErrorSource = "unspecified"
|
||||
)
|
||||
|
||||
type PassportElementErrorTranslationFileType string
|
||||
|
||||
const (
|
||||
@@ -328,22 +343,12 @@ const (
|
||||
PollTypeQuiz PollType = "quiz"
|
||||
)
|
||||
|
||||
type ReactionTypeCustomEmojiType string
|
||||
type ReactionTypeKind string
|
||||
|
||||
const (
|
||||
ReactionTypeCustomEmojiTypeCustomEmoji ReactionTypeCustomEmojiType = "custom_emoji"
|
||||
)
|
||||
|
||||
type ReactionTypeEmojiType string
|
||||
|
||||
const (
|
||||
ReactionTypeEmojiTypeEmoji ReactionTypeEmojiType = "emoji"
|
||||
)
|
||||
|
||||
type ReactionTypePaidType string
|
||||
|
||||
const (
|
||||
ReactionTypePaidTypePaid ReactionTypePaidType = "paid"
|
||||
ReactionTypeKindEmoji ReactionTypeKind = "emoji"
|
||||
ReactionTypeKindCustomEmoji ReactionTypeKind = "custom_emoji"
|
||||
ReactionTypeKindPaid ReactionTypeKind = "paid"
|
||||
)
|
||||
|
||||
type RefundedPaymentCurrency string
|
||||
@@ -352,22 +357,12 @@ const (
|
||||
RefundedPaymentCurrencyXTR RefundedPaymentCurrency = "XTR"
|
||||
)
|
||||
|
||||
type RevenueWithdrawalStateFailedType string
|
||||
type RevenueWithdrawalStateKind string
|
||||
|
||||
const (
|
||||
RevenueWithdrawalStateFailedTypeFailed RevenueWithdrawalStateFailedType = "failed"
|
||||
)
|
||||
|
||||
type RevenueWithdrawalStatePendingType string
|
||||
|
||||
const (
|
||||
RevenueWithdrawalStatePendingTypePending RevenueWithdrawalStatePendingType = "pending"
|
||||
)
|
||||
|
||||
type RevenueWithdrawalStateSucceededType string
|
||||
|
||||
const (
|
||||
RevenueWithdrawalStateSucceededTypeSucceeded RevenueWithdrawalStateSucceededType = "succeeded"
|
||||
RevenueWithdrawalStateKindPending RevenueWithdrawalStateKind = "pending"
|
||||
RevenueWithdrawalStateKindSucceeded RevenueWithdrawalStateKind = "succeeded"
|
||||
RevenueWithdrawalStateKindFailed RevenueWithdrawalStateKind = "failed"
|
||||
)
|
||||
|
||||
type StickerType string
|
||||
@@ -378,34 +373,14 @@ const (
|
||||
StickerTypeCustomEmoji StickerType = "custom_emoji"
|
||||
)
|
||||
|
||||
type StoryAreaTypeLinkType string
|
||||
type StoryAreaTypeKind string
|
||||
|
||||
const (
|
||||
StoryAreaTypeLinkTypeLink StoryAreaTypeLinkType = "link"
|
||||
)
|
||||
|
||||
type StoryAreaTypeLocationType string
|
||||
|
||||
const (
|
||||
StoryAreaTypeLocationTypeLocation StoryAreaTypeLocationType = "location"
|
||||
)
|
||||
|
||||
type StoryAreaTypeSuggestedReactionType string
|
||||
|
||||
const (
|
||||
StoryAreaTypeSuggestedReactionTypeSuggestedReaction StoryAreaTypeSuggestedReactionType = "suggested_reaction"
|
||||
)
|
||||
|
||||
type StoryAreaTypeUniqueGiftType string
|
||||
|
||||
const (
|
||||
StoryAreaTypeUniqueGiftTypeUniqueGift StoryAreaTypeUniqueGiftType = "unique_gift"
|
||||
)
|
||||
|
||||
type StoryAreaTypeWeatherType string
|
||||
|
||||
const (
|
||||
StoryAreaTypeWeatherTypeWeather StoryAreaTypeWeatherType = "weather"
|
||||
StoryAreaTypeKindLocation StoryAreaTypeKind = "location"
|
||||
StoryAreaTypeKindSuggestedReaction StoryAreaTypeKind = "suggested_reaction"
|
||||
StoryAreaTypeKindLink StoryAreaTypeKind = "link"
|
||||
StoryAreaTypeKindWeather StoryAreaTypeKind = "weather"
|
||||
StoryAreaTypeKindUniqueGift StoryAreaTypeKind = "unique_gift"
|
||||
)
|
||||
|
||||
type SuggestedPostInfoState string
|
||||
@@ -430,34 +405,16 @@ const (
|
||||
SuggestedPostRefundedReasonPaymentRefunded SuggestedPostRefundedReason = "payment_refunded"
|
||||
)
|
||||
|
||||
type TransactionPartnerAffiliateProgramType string
|
||||
type TransactionPartnerType string
|
||||
|
||||
const (
|
||||
TransactionPartnerAffiliateProgramTypeAffiliateProgram TransactionPartnerAffiliateProgramType = "affiliate_program"
|
||||
)
|
||||
|
||||
type TransactionPartnerFragmentType string
|
||||
|
||||
const (
|
||||
TransactionPartnerFragmentTypeFragment TransactionPartnerFragmentType = "fragment"
|
||||
)
|
||||
|
||||
type TransactionPartnerOtherType string
|
||||
|
||||
const (
|
||||
TransactionPartnerOtherTypeOther TransactionPartnerOtherType = "other"
|
||||
)
|
||||
|
||||
type TransactionPartnerTelegramAdsType string
|
||||
|
||||
const (
|
||||
TransactionPartnerTelegramAdsTypeTelegramAds TransactionPartnerTelegramAdsType = "telegram_ads"
|
||||
)
|
||||
|
||||
type TransactionPartnerTelegramApiType string
|
||||
|
||||
const (
|
||||
TransactionPartnerTelegramApiTypeTelegramApi TransactionPartnerTelegramApiType = "telegram_api"
|
||||
TransactionPartnerTypeUser TransactionPartnerType = "user"
|
||||
TransactionPartnerTypeChat TransactionPartnerType = "chat"
|
||||
TransactionPartnerTypeAffiliateProgram TransactionPartnerType = "affiliate_program"
|
||||
TransactionPartnerTypeFragment TransactionPartnerType = "fragment"
|
||||
TransactionPartnerTypeTelegramAds TransactionPartnerType = "telegram_ads"
|
||||
TransactionPartnerTypeTelegramApi TransactionPartnerType = "telegram_api"
|
||||
TransactionPartnerTypeOther TransactionPartnerType = "other"
|
||||
)
|
||||
|
||||
type TransactionPartnerUserTransactionType string
|
||||
|
||||
+104
@@ -32,3 +32,107 @@ const (
|
||||
UpdateChatBoost UpdateType = "chat_boost"
|
||||
UpdateRemovedChatBoost UpdateType = "removed_chat_boost"
|
||||
)
|
||||
|
||||
// DiceEmoji is the set of emoji values accepted by sendDice. Telegram's
|
||||
// canonical list is "🎲", "🎯", "🏀", "⚽", "🎳", "🎰". The codegen
|
||||
// scraper drops these values during regex extraction (multi-byte
|
||||
// boundary issues with curly-quoted emoji), so this enum is hand-
|
||||
// curated and wired into SendDiceParams.Emoji via the per-field type
|
||||
// override in cmd/genapi/emitter.go.
|
||||
type DiceEmoji string
|
||||
|
||||
const (
|
||||
DiceEmojiDice DiceEmoji = "🎲"
|
||||
DiceEmojiDart DiceEmoji = "🎯"
|
||||
DiceEmojiBasketball DiceEmoji = "🏀"
|
||||
DiceEmojiFootball DiceEmoji = "⚽"
|
||||
DiceEmojiBowling DiceEmoji = "🎳"
|
||||
DiceEmojiSlotMachine DiceEmoji = "🎰"
|
||||
)
|
||||
|
||||
// ReactionEmoji is the set of emoji Telegram allows in a
|
||||
// ReactionTypeEmoji.Emoji value. Hand-curated from
|
||||
// https://core.telegram.org/bots/api#reactiontypeemoji because the
|
||||
// scraper's curly-quote regex strips the emoji literals (byte-boundary
|
||||
// issue on multi-byte sequences). Names mirror the Unicode CLDR short
|
||||
// name where one exists; otherwise a stable common-English label.
|
||||
// Telegram occasionally extends this set — passers of unrecognised
|
||||
// strings still type-check (ReactionEmoji is a string alias) so this
|
||||
// list need not block runtime use of newer values.
|
||||
type ReactionEmoji string
|
||||
|
||||
const (
|
||||
ReactionEmojiHeart ReactionEmoji = "❤"
|
||||
ReactionEmojiThumbsUp ReactionEmoji = "👍"
|
||||
ReactionEmojiThumbsDown ReactionEmoji = "👎"
|
||||
ReactionEmojiFire ReactionEmoji = "🔥"
|
||||
ReactionEmojiSmilingFaceWithHearts ReactionEmoji = "🥰"
|
||||
ReactionEmojiClappingHands ReactionEmoji = "👏"
|
||||
ReactionEmojiBeamingFace ReactionEmoji = "😁"
|
||||
ReactionEmojiThinkingFace ReactionEmoji = "🤔"
|
||||
ReactionEmojiExplodingHead ReactionEmoji = "🤯"
|
||||
ReactionEmojiScreamingFace ReactionEmoji = "😱"
|
||||
ReactionEmojiCursingFace ReactionEmoji = "🤬"
|
||||
ReactionEmojiCryingFace ReactionEmoji = "😢"
|
||||
ReactionEmojiPartyPopper ReactionEmoji = "🎉"
|
||||
ReactionEmojiStarStruck ReactionEmoji = "🤩"
|
||||
ReactionEmojiVomiting ReactionEmoji = "🤮"
|
||||
ReactionEmojiPileOfPoo ReactionEmoji = "💩"
|
||||
ReactionEmojiFoldedHands ReactionEmoji = "🙏"
|
||||
ReactionEmojiOKHand ReactionEmoji = "👌"
|
||||
ReactionEmojiDove ReactionEmoji = "🕊"
|
||||
ReactionEmojiClown ReactionEmoji = "🤡"
|
||||
ReactionEmojiYawning ReactionEmoji = "🥱"
|
||||
ReactionEmojiWoozyFace ReactionEmoji = "🥴"
|
||||
ReactionEmojiHeartEyes ReactionEmoji = "😍"
|
||||
ReactionEmojiWhale ReactionEmoji = "🐳"
|
||||
ReactionEmojiHeartOnFire ReactionEmoji = "❤🔥"
|
||||
ReactionEmojiNewMoonFace ReactionEmoji = "🌚"
|
||||
ReactionEmojiHotDog ReactionEmoji = "🌭"
|
||||
ReactionEmojiHundredPoints ReactionEmoji = "💯"
|
||||
ReactionEmojiRollingOnFloor ReactionEmoji = "🤣"
|
||||
ReactionEmojiLightning ReactionEmoji = "⚡"
|
||||
ReactionEmojiBanana ReactionEmoji = "🍌"
|
||||
ReactionEmojiTrophy ReactionEmoji = "🏆"
|
||||
ReactionEmojiBrokenHeart ReactionEmoji = "💔"
|
||||
ReactionEmojiRaisedEyebrow ReactionEmoji = "🤨"
|
||||
ReactionEmojiNeutralFace ReactionEmoji = "😐"
|
||||
ReactionEmojiStrawberry ReactionEmoji = "🍓"
|
||||
ReactionEmojiChampagne ReactionEmoji = "🍾"
|
||||
ReactionEmojiKissMark ReactionEmoji = "💋"
|
||||
ReactionEmojiMiddleFinger ReactionEmoji = "🖕"
|
||||
ReactionEmojiDevil ReactionEmoji = "😈"
|
||||
ReactionEmojiSleeping ReactionEmoji = "😴"
|
||||
ReactionEmojiLoudlyCrying ReactionEmoji = "😭"
|
||||
ReactionEmojiNerd ReactionEmoji = "🤓"
|
||||
ReactionEmojiGhost ReactionEmoji = "👻"
|
||||
ReactionEmojiManTechnologist ReactionEmoji = "👨💻"
|
||||
ReactionEmojiEyes ReactionEmoji = "👀"
|
||||
ReactionEmojiJackOLantern ReactionEmoji = "🎃"
|
||||
ReactionEmojiSeeNoEvil ReactionEmoji = "🙈"
|
||||
ReactionEmojiHalo ReactionEmoji = "😇"
|
||||
ReactionEmojiFearful ReactionEmoji = "😨"
|
||||
ReactionEmojiHandshake ReactionEmoji = "🤝"
|
||||
ReactionEmojiWriting ReactionEmoji = "✍"
|
||||
ReactionEmojiHugging ReactionEmoji = "🤗"
|
||||
ReactionEmojiSaluting ReactionEmoji = "🫡"
|
||||
ReactionEmojiSantaClaus ReactionEmoji = "🎅"
|
||||
ReactionEmojiChristmasTree ReactionEmoji = "🎄"
|
||||
ReactionEmojiSnowman ReactionEmoji = "☃"
|
||||
ReactionEmojiNailPolish ReactionEmoji = "💅"
|
||||
ReactionEmojiZanyFace ReactionEmoji = "🤪"
|
||||
ReactionEmojiMoai ReactionEmoji = "🗿"
|
||||
ReactionEmojiCool ReactionEmoji = "🆒"
|
||||
ReactionEmojiHeartWithArrow ReactionEmoji = "💘"
|
||||
ReactionEmojiHearNoEvil ReactionEmoji = "🙉"
|
||||
ReactionEmojiUnicorn ReactionEmoji = "🦄"
|
||||
ReactionEmojiKissingFace ReactionEmoji = "😘"
|
||||
ReactionEmojiPill ReactionEmoji = "💊"
|
||||
ReactionEmojiSpeakNoEvil ReactionEmoji = "🙊"
|
||||
ReactionEmojiSmilingFaceWithSunglasses ReactionEmoji = "😎"
|
||||
ReactionEmojiAlienMonster ReactionEmoji = "👾"
|
||||
ReactionEmojiManShrugging ReactionEmoji = "🤷♂"
|
||||
ReactionEmojiPersonShrugging ReactionEmoji = "🤷"
|
||||
ReactionEmojiWomanShrugging ReactionEmoji = "🤷♀"
|
||||
ReactionEmojiPoutingFace ReactionEmoji = "😡"
|
||||
)
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
+25
-9
@@ -27,7 +27,7 @@ type GetUpdatesParams struct {
|
||||
// Timeout in seconds for long polling. Defaults to 0, i.e. usual short polling. Should be positive, short polling should be used for testing purposes only.
|
||||
Timeout *int64 `json:"timeout,omitempty"`
|
||||
// A JSON-serialized list of the update types you want your bot to receive. For example, specify ["message", "edited_channel_post", "callback_query"] to only receive updates of these types. See Update for a complete list of available update types. Specify an empty list to receive all update types except chat_member, message_reaction, and message_reaction_count (default). If not specified, the previous setting will be used.Please note that this parameter doesn't affect updates created before the call to getUpdates, so unwanted updates may be received for a short period of time.
|
||||
AllowedUpdates []string `json:"allowed_updates,omitempty"`
|
||||
AllowedUpdates []UpdateType `json:"allowed_updates,omitempty"`
|
||||
}
|
||||
|
||||
// GetUpdates calls the getUpdates Telegram Bot API method.
|
||||
@@ -54,7 +54,7 @@ type SetWebhookParams struct {
|
||||
// The maximum allowed number of simultaneous HTTPS connections to the webhook for update delivery, 1-100. Defaults to 40. Use lower values to limit the load on your bot's server, and higher values to increase your bot's throughput.
|
||||
MaxConnections *int64 `json:"max_connections,omitempty"`
|
||||
// A JSON-serialized list of the update types you want your bot to receive. For example, specify ["message", "edited_channel_post", "callback_query"] to only receive updates of these types. See Update for a complete list of available update types. Specify an empty list to receive all update types except chat_member, message_reaction, and message_reaction_count (default). If not specified, the previous setting will be used.Please note that this parameter doesn't affect updates created before the call to the setWebhook, so unwanted updates may be received for a short period of time.
|
||||
AllowedUpdates []string `json:"allowed_updates,omitempty"`
|
||||
AllowedUpdates []UpdateType `json:"allowed_updates,omitempty"`
|
||||
// Pass True to drop all pending updates
|
||||
DropPendingUpdates *bool `json:"drop_pending_updates,omitempty"`
|
||||
// A secret token to be sent in a header “X-Telegram-Bot-Api-Secret-Token” in every webhook request, 1-256 characters. Only characters A-Z, a-z, 0-9, _ and - are allowed. The header is useful to ensure that the request comes from a webhook set by you.
|
||||
@@ -1875,7 +1875,7 @@ type SendPollParams struct {
|
||||
// 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.
|
||||
@@ -1889,7 +1889,7 @@ type SendPollParams struct {
|
||||
// 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
|
||||
@@ -1953,7 +1953,7 @@ type SendDiceParams struct {
|
||||
// Identifier of the direct messages topic to which the message will be sent; required if the message is sent to a direct messages chat
|
||||
DirectMessagesTopicID *int64 `json:"direct_messages_topic_id,omitempty"`
|
||||
// Emoji on which the dice throw animation is based. Currently, must be one of “”, “”, “”, “”, “”, or “”. Dice can have values 1-6 for “”, “” and “”, values 1-5 for “” and “”, and values 1-64 for “”. Defaults to “”
|
||||
Emoji string `json:"emoji,omitempty"`
|
||||
Emoji DiceEmoji `json:"emoji,omitempty"`
|
||||
// Sends the message silently. Users will receive a notification with no sound.
|
||||
DisableNotification *bool `json:"disable_notification,omitempty"`
|
||||
// Protects the contents of the sent message from forwarding
|
||||
@@ -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.
|
||||
@@ -3124,7 +3140,7 @@ 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"`
|
||||
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,7 +3157,7 @@ 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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
+1704
-120
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")
|
||||
}
|
||||
+51
-7
@@ -8,8 +8,36 @@ 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) }}
|
||||
)
|
||||
|
||||
// maxPooledBufCap caps the buffer size returned to respBufPool. 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)
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -44,8 +72,8 @@ func Call[Req any, Resp any](ctx context.Context, b *Bot, method string, req Req
|
||||
if err != nil {
|
||||
return zero, &NetworkError{Err: err}
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Accept", "application/json")
|
||||
httpReq.Header["Content-Type"] = headerJSONValue
|
||||
httpReq.Header["Accept"] = headerJSONValue
|
||||
|
||||
resp, err := b.http.Do(httpReq)
|
||||
if err != nil {
|
||||
@@ -57,12 +85,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
|
||||
@@ -91,8 +121,8 @@ func CallRaw[Req any](ctx context.Context, b *Bot, method string, req Req) (json
|
||||
if err != nil {
|
||||
return nil, &NetworkError{Err: err}
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Accept", "application/json")
|
||||
httpReq.Header["Content-Type"] = headerJSONValue
|
||||
httpReq.Header["Accept"] = headerJSONValue
|
||||
|
||||
resp, err := b.http.Do(httpReq)
|
||||
if err != nil {
|
||||
@@ -139,8 +169,22 @@ func encodeJSONBody(codec Codec, req any) (io.Reader, error) {
|
||||
|
||||
// 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,94 @@
|
||||
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, err := encodeJSONBody(codec, req)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
_ = r
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+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 {
|
||||
|
||||
+265
-6
@@ -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 {
|
||||
@@ -165,15 +199,167 @@ 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, enums: planEnums(api)}
|
||||
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(e.enums)).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)
|
||||
}
|
||||
@@ -209,6 +395,22 @@ func loadAPI(path string) (*spec.API, error) {
|
||||
return &api, nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -217,6 +419,9 @@ func funcs(plan *enumPlan) template.FuncMap {
|
||||
"goField": func(parent string, f spec.Field) string {
|
||||
return goField(plan, parent, f)
|
||||
},
|
||||
"goFieldP": func(methodName string, f spec.Field) string {
|
||||
return goFieldX(plan, "", title(methodName)+"Params", f)
|
||||
},
|
||||
"docComment": docComment,
|
||||
"isOptional": func(f spec.Field) bool { return !f.Required },
|
||||
"not": func(b bool) bool { return !b },
|
||||
@@ -226,6 +431,9 @@ func funcs(plan *enumPlan) template.FuncMap {
|
||||
"multipartFieldEntry": func(parent string, f spec.Field) string {
|
||||
return multipartFieldEntry(plan, parent, f)
|
||||
},
|
||||
"multipartFieldEntryP": func(methodName string, f spec.Field) string {
|
||||
return multipartFieldEntryX(plan, "", title(methodName)+"Params", f)
|
||||
},
|
||||
"multipartFileEntry": multipartFileEntry,
|
||||
"returnGoType": returnGoType,
|
||||
// enum helpers
|
||||
@@ -245,6 +453,19 @@ func funcs(plan *enumPlan) 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 },
|
||||
@@ -312,7 +533,17 @@ func multipartFileEntry(f spec.Field) string {
|
||||
// when non-zero/non-empty. Typed-string enum fields are cast to string
|
||||
// before assignment because the multipart map is map[string]string.
|
||||
func multipartFieldEntry(plan *enumPlan, parent string, f spec.Field) string {
|
||||
enumName := plan.FieldEnum(parent, f.Name)
|
||||
return multipartFieldEntryX(plan, parent, parent, f)
|
||||
}
|
||||
|
||||
// multipartFieldEntryX mirrors goFieldX: enumParent keys the enum plan,
|
||||
// overrideParent keys fieldTypeOverrides. They differ only for method
|
||||
// params.
|
||||
func multipartFieldEntryX(plan *enumPlan, enumParent, overrideParent string, f spec.Field) string {
|
||||
enumName := plan.FieldEnum(enumParent, f.Name)
|
||||
if enumName == "" {
|
||||
enumName = fieldTypeOverride(overrideParent, f.Name)
|
||||
}
|
||||
switch f.Type.Kind {
|
||||
case spec.KindPrimitive:
|
||||
switch f.Type.Name {
|
||||
@@ -464,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
|
||||
}
|
||||
@@ -584,12 +815,40 @@ func matchesVariants(got []string, want ...string) bool {
|
||||
// has a planned enum name for (parent, field), the field's Go type is
|
||||
// the enum identifier. Typed-string enums use the zero string ""
|
||||
// behaviour for omitempty, so we do not pointer-wrap optional enum
|
||||
// fields. Parent is "" for method parameters.
|
||||
// fields. Parent is "" for method parameters; pass the params type
|
||||
// name (e.g. "SendDiceParams") via overrideParent when calling from
|
||||
// the methods template so fieldTypeOverrides can resolve.
|
||||
func goField(plan *enumPlan, parent string, f spec.Field) string {
|
||||
return goFieldX(plan, parent, parent, f)
|
||||
}
|
||||
|
||||
// goFieldX is the underlying field-emitter. enumParent is used for the
|
||||
// enum-plan lookup (which keys method params under ""); overrideParent
|
||||
// is used for fieldTypeOverrides (which keys method params under the
|
||||
// params type name). For struct types both are the same; for method
|
||||
// params they differ.
|
||||
func goFieldX(plan *enumPlan, enumParent, overrideParent string, f spec.Field) string {
|
||||
tag := fmt.Sprintf("`json:%q`", f.JSONName+omitempty(f))
|
||||
if name := plan.FieldEnum(parent, f.Name); name != "" {
|
||||
if name := plan.FieldEnum(enumParent, f.Name); name != "" {
|
||||
return fmt.Sprintf("%s %s %s", f.Name, name, tag)
|
||||
}
|
||||
if name := fieldTypeOverride(overrideParent, f.Name); name != "" {
|
||||
return fmt.Sprintf("%s %s %s", f.Name, name, tag)
|
||||
}
|
||||
// Pinned companion-enum retype: allowed_updates is an Array of String
|
||||
// in the upstream spec, but the Go API exposes a hand-curated
|
||||
// UpdateType (api/enums.go) since the values are not enumerated
|
||||
// inline by Telegram. Retype []string → []UpdateType wherever the
|
||||
// wire field is allowed_updates so callers can pass typed constants
|
||||
// (api.UpdateMessage, ...) without string casts. Wire format is
|
||||
// unchanged: UpdateType is a typed string, marshals identically.
|
||||
if f.JSONName == "allowed_updates" &&
|
||||
f.Type.Kind == spec.KindArray &&
|
||||
f.Type.ElemType != nil &&
|
||||
f.Type.ElemType.Kind == spec.KindPrimitive &&
|
||||
f.Type.ElemType.Name == "string" {
|
||||
return fmt.Sprintf("%s []UpdateType %s", f.Name, tag)
|
||||
}
|
||||
return fmt.Sprintf("%s %s %s", f.Name, goType(f.Type, !f.Required), tag)
|
||||
}
|
||||
|
||||
|
||||
@@ -41,12 +41,25 @@ func planEnums(api *spec.API) *enumPlan {
|
||||
valueKey string // canonical key for value-set dedup
|
||||
}
|
||||
|
||||
// Unification pass: for each sealed-interface union, fold per-variant
|
||||
// single-value enum fields that share a discriminator name into ONE
|
||||
// unified enum at union level. Claimed (parent,fieldName) tuples are
|
||||
// excluded from the per-field grouping below.
|
||||
unifiedDecls, unifiedByField := planUnifiedUnionEnums(api)
|
||||
claimed := func(parent, fieldName string) bool {
|
||||
_, ok := unifiedByField[enumKey(parent, fieldName)]
|
||||
return ok
|
||||
}
|
||||
|
||||
var refs []ref
|
||||
collect := func(parent string, fields []spec.Field) {
|
||||
for _, f := range fields {
|
||||
if len(f.EnumValues) == 0 {
|
||||
continue
|
||||
}
|
||||
if claimed(parent, f.Name) {
|
||||
continue
|
||||
}
|
||||
refs = append(refs, ref{
|
||||
parent: parent,
|
||||
fieldName: f.Name,
|
||||
@@ -190,9 +203,172 @@ func planEnums(api *spec.API) *enumPlan {
|
||||
plan.decls[g.name] = enumDecl{Name: g.name, Values: g.values}
|
||||
_ = vk
|
||||
}
|
||||
// Merge unified union enums (already named with stutter handling and
|
||||
// keyed per-variant in unifiedByField).
|
||||
for k, name := range unifiedByField {
|
||||
plan.byField[k] = name
|
||||
}
|
||||
for name, d := range unifiedDecls {
|
||||
plan.decls[name] = d
|
||||
}
|
||||
return plan
|
||||
}
|
||||
|
||||
// planUnifiedUnionEnums detects sealed-interface unions whose variants
|
||||
// share a single discriminator field with one enum value each, and emits
|
||||
// ONE unified enum per union covering all variant values. Returns the
|
||||
// declarations to emit and the per-(variant,fieldName) map to point each
|
||||
// variant's field at the unified enum.
|
||||
//
|
||||
// A union qualifies when EVERY variant in t.OneOf:
|
||||
// 1. defines a field with the same Go-name (e.g. "Status", "Type", "Source");
|
||||
// 2. that field is a required string with len(EnumValues)==1.
|
||||
//
|
||||
// The picked Go-name is the first one tried in this priority order:
|
||||
// - knownDiscriminators[union].Field's Go-name (resolved via JSONName match);
|
||||
// - "Type", "Status", "Source" (the three discriminators Telegram uses).
|
||||
//
|
||||
// First match wins; if none qualify, the union is skipped (variants keep
|
||||
// their existing per-field treatment, which still single-emits via the
|
||||
// regular grouping pass).
|
||||
func planUnifiedUnionEnums(api *spec.API) (map[string]enumDecl, map[string]string) {
|
||||
decls := map[string]enumDecl{}
|
||||
byField := map[string]string{}
|
||||
|
||||
typeByName := make(map[string]*spec.TypeDecl, len(api.Types))
|
||||
for i := range api.Types {
|
||||
typeByName[api.Types[i].Name] = &api.Types[i]
|
||||
}
|
||||
|
||||
// Iterate unions in deterministic (declaration) order.
|
||||
for ui := range api.Types {
|
||||
u := &api.Types[ui]
|
||||
if len(u.OneOf) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Resolve the variants. Skip unions where any variant is missing
|
||||
// (defensive — shouldn't happen in a well-formed IR).
|
||||
variants := make([]*spec.TypeDecl, 0, len(u.OneOf))
|
||||
for _, vName := range u.OneOf {
|
||||
v, ok := typeByName[vName]
|
||||
if !ok {
|
||||
variants = nil
|
||||
break
|
||||
}
|
||||
variants = append(variants, v)
|
||||
}
|
||||
if len(variants) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Build the candidate Go-name list. Priority order:
|
||||
// 1. discriminator GoField from knownDiscriminators (resolved via JSONName);
|
||||
// 2. "Type", "Status", "Source".
|
||||
var candidateNames []string
|
||||
seen := map[string]bool{}
|
||||
add := func(name string) {
|
||||
if name == "" || seen[name] {
|
||||
return
|
||||
}
|
||||
seen[name] = true
|
||||
candidateNames = append(candidateNames, name)
|
||||
}
|
||||
if ds, ok := knownDiscriminators[u.Name]; ok && ds.Field != "" {
|
||||
// Resolve Go-name from the first variant whose field matches the JSON name.
|
||||
for _, v := range variants {
|
||||
for _, f := range v.Fields {
|
||||
if f.JSONName == ds.Field {
|
||||
add(f.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, n := range []string{"Type", "Status", "Source"} {
|
||||
add(n)
|
||||
}
|
||||
|
||||
// Find the first candidate Go-name where every variant has a
|
||||
// matching single-value string-enum field.
|
||||
var (
|
||||
pickedName string
|
||||
pickedDocs map[string]spec.Field // variant name -> field
|
||||
)
|
||||
for _, name := range candidateNames {
|
||||
matches := map[string]spec.Field{}
|
||||
ok := true
|
||||
for _, v := range variants {
|
||||
var hit *spec.Field
|
||||
for fi := range v.Fields {
|
||||
if v.Fields[fi].Name == name {
|
||||
hit = &v.Fields[fi]
|
||||
break
|
||||
}
|
||||
}
|
||||
if hit == nil ||
|
||||
hit.Type.Kind != spec.KindPrimitive ||
|
||||
hit.Type.Name != "string" ||
|
||||
len(hit.EnumValues) != 1 {
|
||||
ok = false
|
||||
break
|
||||
}
|
||||
matches[v.Name] = *hit
|
||||
}
|
||||
if ok {
|
||||
pickedName = name
|
||||
pickedDocs = matches
|
||||
break
|
||||
}
|
||||
}
|
||||
if pickedName == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Build the unified enum name with stutter handling.
|
||||
enumName := unifiedEnumName(u.Name, pickedName)
|
||||
|
||||
// Collect values across variants in deterministic order, deduping.
|
||||
valueOrder := make([]string, 0, len(variants))
|
||||
valueSeen := map[string]bool{}
|
||||
for _, v := range u.OneOf {
|
||||
f := pickedDocs[v]
|
||||
val := f.EnumValues[0]
|
||||
if valueSeen[val] {
|
||||
continue
|
||||
}
|
||||
valueSeen[val] = true
|
||||
valueOrder = append(valueOrder, val)
|
||||
}
|
||||
|
||||
decls[enumName] = enumDecl{Name: enumName, Values: valueOrder}
|
||||
for _, v := range variants {
|
||||
byField[enumKey(v.Name, pickedName)] = enumName
|
||||
}
|
||||
}
|
||||
|
||||
return decls, byField
|
||||
}
|
||||
|
||||
// unifiedEnumName builds the union-level enum name. Falls back to a
|
||||
// "Kind" suffix when the naive concatenation reads as a stutter:
|
||||
//
|
||||
// - union name ends in the field name verbatim (e.g. BackgroundType+Type);
|
||||
// - union name ends in any "concept noun" — Type/Status/Source/State —
|
||||
// so appending another such noun would duplicate the suffix
|
||||
// (e.g. ChatBoostSource+Source, RevenueWithdrawalState+Type).
|
||||
//
|
||||
// Otherwise the natural concatenation wins (ChatMember+Status →
|
||||
// ChatMemberStatus, MessageOrigin+Type → MessageOriginType).
|
||||
func unifiedEnumName(unionName, fieldName string) string {
|
||||
for _, suf := range []string{"Type", "Status", "Source", "State"} {
|
||||
if strings.HasSuffix(unionName, suf) {
|
||||
return unionName + "Kind"
|
||||
}
|
||||
}
|
||||
return unionName + fieldName
|
||||
}
|
||||
|
||||
// All returns the enum declarations sorted by name for deterministic emit.
|
||||
func (p *enumPlan) All() []enumDecl {
|
||||
out := make([]enumDecl, 0, len(p.decls))
|
||||
|
||||
+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 -}}
|
||||
|
||||
@@ -84,6 +84,23 @@ func UnmarshalMaybeInaccessibleMessage(data []byte) (MaybeInaccessibleMessage, e
|
||||
type {{.Name}} struct {
|
||||
{{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.
|
||||
|
||||
+87
-1
@@ -27,6 +27,33 @@ import (
|
||||
// emits the canonical Markdown / MarkdownV2 / HTML triple.
|
||||
//
|
||||
// Returns nil when the description does not look like an enum.
|
||||
// extractEnumValues inspects a field-description string and returns the
|
||||
// list of wire-level string values when the description matches one of
|
||||
// the enum-like patterns Telegram uses in its docs. Order follows doc
|
||||
// order; duplicates are removed but order of first occurrence is kept.
|
||||
//
|
||||
// Handled patterns (curly quotes “…” are required to avoid false
|
||||
// positives on free-text quoting):
|
||||
//
|
||||
// - "Type of the chat, can be either “private”, “group”, … or “channel”"
|
||||
// - "Currently, can be “mention”, “hashtag”, …"
|
||||
// - "Currently, one of “XTR” … or “TON” …"
|
||||
// - "Currently, must be one of “XTR” …"
|
||||
// - "Currently, it can be one of “pending”, “approved”, “declined”."
|
||||
// - "Must be one of “danger” …, “success” …"
|
||||
// - "Must be one of “image/jpeg”, “image/gif”, or “video/mp4”"
|
||||
// - "Format … must be one of “static” …, “animated” …, “video” …"
|
||||
// - "Currently, either “upgrade” …, “transfer” …, “resale” …"
|
||||
// - "..., always “creator”"
|
||||
// - parse_mode parameter special case ("Mode for parsing entities …")
|
||||
// emits the canonical Markdown / MarkdownV2 / HTML triple.
|
||||
// - bare prose discriminator at end of description, e.g.
|
||||
// "Type of the result, must be article" or
|
||||
// "Scope type, must be all_private_chats". Used by sealed-interface
|
||||
// union variants whose Type/Source field carries a single literal
|
||||
// value declared without curly quotes.
|
||||
//
|
||||
// Returns nil when the description does not look like an enum.
|
||||
func extractEnumValues(jsonName, desc string) []string {
|
||||
if values := parseModeEnumValues(jsonName, desc); values != nil {
|
||||
return values
|
||||
@@ -34,12 +61,15 @@ func extractEnumValues(jsonName, desc string) []string {
|
||||
|
||||
trigger, triggerEnd, isAlways := findEnumTrigger(desc)
|
||||
if trigger < 0 {
|
||||
return nil
|
||||
return extractProseDiscriminator(desc)
|
||||
}
|
||||
tail := desc[trigger:]
|
||||
|
||||
values := collectQuotedValues(tail)
|
||||
if len(values) == 0 {
|
||||
if v := extractProseDiscriminator(desc); v != nil {
|
||||
return v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// First quoted value must sit close to the trigger phrase (e.g.
|
||||
@@ -203,3 +233,59 @@ func dedupeStrings(in []string) []string {
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// proseDiscRE matches a terminal "must be <ident>" clause: the
|
||||
// discriminator value sits at the END of the description (optionally
|
||||
// followed by trailing punctuation/whitespace) so multi-clause prose
|
||||
// like "must be shown above the message" is not picked up.
|
||||
//
|
||||
// The identifier is a snake_case wire literal: lowercase letters, digits,
|
||||
// and underscores, starting with a letter. Numeric-only and prose words
|
||||
// are filtered separately by isProseWord.
|
||||
var proseDiscRE = regexp.MustCompile(`(?i)\bmust be\s+([a-z][a-z0-9_]*)\s*[.,]?\s*$`)
|
||||
|
||||
// extractProseDiscriminator detects unambiguous single-value
|
||||
// discriminator declarations of the form "..., must be <ident>" used by
|
||||
// sealed-interface union variants (e.g. "Type of the result, must be
|
||||
// article" or "Scope type, must be all_private_chats"). Returns the
|
||||
// extracted value as a one-element slice or nil when no match is found.
|
||||
//
|
||||
// The terminal-position anchor is what protects against prose like
|
||||
// "must be shown above" or "must be one of 3, 6, or 12" — the candidate
|
||||
// must close the description.
|
||||
func extractProseDiscriminator(desc string) []string {
|
||||
desc = strings.TrimSpace(desc)
|
||||
if desc == "" {
|
||||
return nil
|
||||
}
|
||||
m := proseDiscRE.FindStringSubmatch(desc)
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
v := m[1]
|
||||
if isProseWord(v) {
|
||||
return nil
|
||||
}
|
||||
return []string{v}
|
||||
}
|
||||
|
||||
// isProseWord rejects bare-prose continuations that pass the regex but
|
||||
// are clearly English filler ("must be sent", "must be available"). The
|
||||
// list is the closed set of words that empirically appear in the IR's
|
||||
// "must be …" tails outside the variant-discriminator pattern. Wire
|
||||
// identifiers are always single tokens with no English meaning, so any
|
||||
// match here is a free-text false positive.
|
||||
func isProseWord(s string) bool {
|
||||
switch s {
|
||||
case "a", "an", "the",
|
||||
"sent", "shown", "set", "used", "passed", "specified", "available",
|
||||
"applied", "supported", "assumed", "active", "paid", "between",
|
||||
"of", "on", "in", "at", "by", "to", "from", "for", "with",
|
||||
"and", "or", "no", "non",
|
||||
"positive", "negative",
|
||||
"administrator", "repainted",
|
||||
"one", "exactly":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -83,3 +83,56 @@ func TestExtractEnumValues_DedupeRepeatedValues(t *testing.T) {
|
||||
got := extractEnumValues("currency", desc)
|
||||
require.Equal(t, []string{"XTR"}, got)
|
||||
}
|
||||
|
||||
func TestExtractEnumValues_ProseDiscriminator(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
desc string
|
||||
want []string
|
||||
}{
|
||||
{"InlineQueryResultArticle", "Type of the result, must be article", []string{"article"}},
|
||||
{"InlineQueryResultPhoto", "Type of the result, must be photo", []string{"photo"}},
|
||||
{"InlineQueryResultMpeg4Gif", "Type of the result, must be mpeg4_gif", []string{"mpeg4_gif"}},
|
||||
{"BotCommandScopeAllPrivateChats", "Scope type, must be all_private_chats", []string{"all_private_chats"}},
|
||||
{"BotCommandScopeChat", "Scope type, must be chat", []string{"chat"}},
|
||||
{"PassportElementErrorData", "Error source, must be data", []string{"data"}},
|
||||
{"MenuButtonWebApp", "Type of the button, must be web_app", []string{"web_app"}},
|
||||
{"InputProfilePhotoAnimated", "Type of the profile photo, must be animated", []string{"animated"}},
|
||||
{"InputStoryContentVideo", "Type of the content, must be video", []string{"video"}},
|
||||
{"InputPaidMediaPhoto", "Type of the media, must be photo", []string{"photo"}},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require.Equal(t, tc.want, extractEnumValues("type", tc.desc))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractEnumValues_ProseFalsePositives(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
desc string
|
||||
}{
|
||||
{"available_only_for", "Optional. Bot-specified invoice payload. Can be available only for “invoice_payment” transactions."},
|
||||
{"must_be_sent", "If True, the message must be sent immediately."},
|
||||
{"must_be_shown_above", "Optional. True, if the link preview must be shown above the message text"},
|
||||
{"must_be_specified", "The identifiers must be specified in a strictly increasing order."},
|
||||
{"must_be_paid", "The number of Telegram Stars that must be paid to send the sticker"},
|
||||
{"must_be_one_of_numbers", "Number of months the Telegram Premium subscription will be active for the user; must be one of 3, 6, or 12"},
|
||||
{"must_be_between", "Currently, price in Telegram Stars must be between 5 and 100000"},
|
||||
{"must_be_a_pay_button", "If not empty, the first button must be a Pay button."},
|
||||
{"must_be_repainted", "True, if the sticker must be repainted to a text color in messages"},
|
||||
{"must_be_active", "the subscription must be active up to the end of the current subscription period"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require.Nil(t, extractEnumValues("type", tc.desc))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractEnumValues_CanonicalMustBeOneOfStillWorks(t *testing.T) {
|
||||
desc := "Currently, must be one of “Markdown”, “MarkdownV2”, “HTML”"
|
||||
got := extractEnumValues("parse_mode_kind", desc)
|
||||
require.Equal(t, []string{"Markdown", "MarkdownV2", "HTML"}, got)
|
||||
}
|
||||
|
||||
+34
-9
@@ -21,20 +21,45 @@ import (
|
||||
// Update is the raw update; payload-typed handlers also receive a
|
||||
// narrowed pointer to one of its sub-fields.
|
||||
//
|
||||
// Values is a per-update bag matchers populate. Conventional keys:
|
||||
// Command, CommandArgs and RegexMatch are populated by the router for
|
||||
// the matching route kind; they replace the previous "command",
|
||||
// "command_args" and "regex_match" entries in Values, which were the
|
||||
// only conventional keys. Values remains for user-defined custom keys.
|
||||
//
|
||||
// "command": string, the matched bot command (e.g. "/start")
|
||||
// "command_args": string, everything after the command
|
||||
// "regex_match": []string, regex sub-matches when OnText matches
|
||||
// Command is the matched bot command (e.g. "/start"); empty when the
|
||||
// route is not a command match.
|
||||
//
|
||||
// CommandArgs is everything after the command; empty when no command
|
||||
// matched or the command had no trailing text.
|
||||
//
|
||||
// RegexMatch is the regex sub-matches when an OnText/OnCallback regex
|
||||
// route matched; nil otherwise.
|
||||
//
|
||||
// Values is lazily allocated for user-defined keys. Handlers that don't
|
||||
// write pay no allocation. Reads against a nil map return the zero
|
||||
// value. Writers must use Set instead of indexing the map directly.
|
||||
type Context struct {
|
||||
Ctx context.Context
|
||||
Bot *client.Bot
|
||||
Update *api.Update
|
||||
Values map[string]any
|
||||
Ctx context.Context
|
||||
Bot *client.Bot
|
||||
Update *api.Update
|
||||
Command string
|
||||
CommandArgs string
|
||||
RegexMatch []string
|
||||
Values map[string]any
|
||||
}
|
||||
|
||||
// NewContext constructs a Context. Used by Router internally; exposed for
|
||||
// custom test harnesses.
|
||||
func NewContext(ctx context.Context, b *client.Bot, u *api.Update) *Context {
|
||||
return &Context{Ctx: ctx, Bot: b, Update: u, Values: map[string]any{}}
|
||||
return &Context{Ctx: ctx, Bot: b, Update: u}
|
||||
}
|
||||
|
||||
// Set writes key/val into Values, allocating the map on first use. Use
|
||||
// this instead of `c.Values[k] = v` so the no-write path stays
|
||||
// allocation-free.
|
||||
func (c *Context) Set(key string, val any) {
|
||||
if c.Values == nil {
|
||||
c.Values = make(map[string]any, 2)
|
||||
}
|
||||
c.Values[key] = val
|
||||
}
|
||||
|
||||
@@ -12,15 +12,15 @@ func memberUpdate(status string, fromID int64) *api.ChatMemberUpdated {
|
||||
var newMember api.ChatMember
|
||||
switch status {
|
||||
case "member":
|
||||
newMember = &api.ChatMemberMember{Status: api.ChatMemberMemberStatusMember}
|
||||
newMember = &api.ChatMemberMember{Status: api.ChatMemberStatusMember}
|
||||
case "administrator":
|
||||
newMember = &api.ChatMemberAdministrator{Status: api.ChatMemberAdministratorStatusAdministrator}
|
||||
newMember = &api.ChatMemberAdministrator{Status: api.ChatMemberStatusAdministrator}
|
||||
case "kicked":
|
||||
newMember = &api.ChatMemberBanned{Status: api.ChatMemberBannedStatusKicked}
|
||||
newMember = &api.ChatMemberBanned{Status: api.ChatMemberStatusKicked}
|
||||
case "left":
|
||||
newMember = &api.ChatMemberLeft{Status: api.ChatMemberLeftStatusLeft}
|
||||
newMember = &api.ChatMemberLeft{Status: api.ChatMemberStatusLeft}
|
||||
default:
|
||||
newMember = &api.ChatMemberMember{Status: api.ChatMemberMemberStatusMember}
|
||||
newMember = &api.ChatMemberMember{Status: api.ChatMemberStatusMember}
|
||||
}
|
||||
return &api.ChatMemberUpdated{
|
||||
From: api.User{ID: fromID},
|
||||
@@ -70,7 +70,7 @@ func TestComposedFilters(t *testing.T) {
|
||||
func TestNewStatus_Owner(t *testing.T) {
|
||||
u := &api.ChatMemberUpdated{
|
||||
From: api.User{ID: 1},
|
||||
NewChatMember: &api.ChatMemberOwner{Status: api.ChatMemberOwnerStatusCreator},
|
||||
NewChatMember: &api.ChatMemberOwner{Status: api.ChatMemberStatusCreator},
|
||||
}
|
||||
require.True(t, cmfilter.NewStatus("creator")(u))
|
||||
require.False(t, cmfilter.NewStatus("member")(u))
|
||||
@@ -79,7 +79,7 @@ func TestNewStatus_Owner(t *testing.T) {
|
||||
func TestNewStatus_Restricted(t *testing.T) {
|
||||
u := &api.ChatMemberUpdated{
|
||||
From: api.User{ID: 1},
|
||||
NewChatMember: &api.ChatMemberRestricted{Status: api.ChatMemberRestrictedStatusRestricted},
|
||||
NewChatMember: &api.ChatMemberRestricted{Status: api.ChatMemberStatusRestricted},
|
||||
}
|
||||
require.True(t, cmfilter.NewStatus("restricted")(u))
|
||||
require.False(t, cmfilter.NewStatus("member")(u))
|
||||
|
||||
@@ -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))
|
||||
|
||||
+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
|
||||
|
||||
+4
-4
@@ -467,8 +467,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 +477,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 +495,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package dispatch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/lukaszraczylo/go-telegram/api"
|
||||
"github.com/lukaszraczylo/go-telegram/client"
|
||||
)
|
||||
|
||||
func BenchmarkRouter_DispatchCommand(b *testing.B) {
|
||||
r := New(client.New("t"))
|
||||
r.OnCommand("/start", func(c *Context, m *api.Message) error { return nil })
|
||||
u := cmdMessage("/start hello")
|
||||
ctx := context.Background()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
c := NewContext(ctx, r.bot, &u)
|
||||
_ = r.dispatch(c, &u)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRouter_DispatchTextRegex(b *testing.B) {
|
||||
r := New(client.New("t"))
|
||||
r.OnText("^hello.*", func(c *Context, m *api.Message) error { return nil })
|
||||
u := api.Update{
|
||||
UpdateID: 1,
|
||||
Message: &api.Message{
|
||||
MessageID: 1, Date: 0,
|
||||
Chat: api.Chat{ID: 1, Type: api.ChatTypePrivate},
|
||||
Text: "hello world",
|
||||
},
|
||||
}
|
||||
ctx := context.Background()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
c := NewContext(ctx, r.bot, &u)
|
||||
_ = r.dispatch(c, &u)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRouter_DispatchFilter(b *testing.B) {
|
||||
r := New(client.New("t"))
|
||||
r.OnMessageFilter(
|
||||
func(m *api.Message) bool { return m != nil && m.Text == "ping" },
|
||||
func(c *Context, m *api.Message) error { return nil },
|
||||
)
|
||||
u := api.Update{
|
||||
UpdateID: 1,
|
||||
Message: &api.Message{
|
||||
MessageID: 1, Date: 0,
|
||||
Chat: api.Chat{ID: 1, Type: api.ChatTypePrivate},
|
||||
Text: "ping",
|
||||
},
|
||||
}
|
||||
ctx := context.Background()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
c := NewContext(ctx, r.bot, &u)
|
||||
_ = r.dispatch(c, &u)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRouter_NewContext(b *testing.B) {
|
||||
bot := client.New("t")
|
||||
u := &api.Update{UpdateID: 1}
|
||||
ctx := context.Background()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
_ = NewContext(ctx, bot, u)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkExtractCommand(b *testing.B) {
|
||||
text := "/start@BotName hello world"
|
||||
cmdLen := len("/start@BotName")
|
||||
m := &api.Message{
|
||||
MessageID: 1, Date: 0,
|
||||
Chat: api.Chat{ID: 1, Type: api.ChatTypePrivate},
|
||||
Text: text,
|
||||
Entities: []api.MessageEntity{
|
||||
{Type: api.MessageEntityTypeBotCommand, Offset: 0, Length: int64(cmdLen)},
|
||||
},
|
||||
}
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
_, _, _ = extractCommand(m)
|
||||
}
|
||||
}
|
||||
+6
-10
@@ -52,7 +52,7 @@ func TestRouter_OnCommandMatches(t *testing.T) {
|
||||
r := New(b)
|
||||
hit := make(chan string, 1)
|
||||
r.OnCommand("/start", func(c *Context, m *api.Message) error {
|
||||
hit <- c.Values["command"].(string)
|
||||
hit <- c.Command
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -82,7 +82,7 @@ func TestRouter_OnText(t *testing.T) {
|
||||
r := New(client.New("t"))
|
||||
hit := make(chan []string, 1)
|
||||
r.OnText(`^hello (\w+)$`, func(c *Context, m *api.Message) error {
|
||||
hit <- c.Values["regex_match"].([]string)
|
||||
hit <- c.RegexMatch
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -164,10 +164,7 @@ func TestRouter_NonASCIICommand(t *testing.T) {
|
||||
r := New(client.New("t"))
|
||||
hit := make(chan [2]string, 1)
|
||||
r.OnCommand("/старт", func(c *Context, m *api.Message) error {
|
||||
hit <- [2]string{
|
||||
c.Values["command"].(string),
|
||||
c.Values["command_args"].(string),
|
||||
}
|
||||
hit <- [2]string{c.Command, c.CommandArgs}
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -180,16 +177,15 @@ func TestRouter_NonASCIICommand(t *testing.T) {
|
||||
require.Equal(t, "аргумент", got[1])
|
||||
}
|
||||
|
||||
// TestRouter_CommandValuesNotLeakedOnNoMatch verifies that c.Values["command"]
|
||||
// is not set when a command entity is present but no route matches, so a
|
||||
// TestRouter_CommandValuesNotLeakedOnNoMatch verifies that c.Command is
|
||||
// empty when a command entity is present but no route matches, so a
|
||||
// subsequent text handler doesn't see stale values.
|
||||
func TestRouter_CommandValuesNotLeakedOnNoMatch(t *testing.T) {
|
||||
r := New(client.New("t"))
|
||||
// Register a text handler that should fire as fallback.
|
||||
leaked := make(chan bool, 1)
|
||||
r.OnText(`.*`, func(c *Context, m *api.Message) error {
|
||||
_, hasCmd := c.Values["command"]
|
||||
leaked <- hasCmd
|
||||
leaked <- c.Command != ""
|
||||
return nil
|
||||
})
|
||||
// No OnCommand registered, so the command entity won't match any route.
|
||||
|
||||
+2219
-1411
File diff suppressed because it is too large
Load Diff
+49
-49
@@ -80,7 +80,7 @@ var (
|
||||
```
|
||||
|
||||
<a name="Call"></a>
|
||||
## func Call
|
||||
## func [Call](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/call.go#L53>)
|
||||
|
||||
```go
|
||||
func Call[Req any, Resp any](ctx context.Context, b *Bot, method string, req Req) (Resp, error)
|
||||
@@ -93,7 +93,7 @@ It is generic over both request and response types. Methods with no parameters m
|
||||
Call is exported because the api package \(which lives outside this one\) invokes it from generated method wrappers. User code should not normally call it directly — use the typed wrappers in package api instead.
|
||||
|
||||
<a name="CallRaw"></a>
|
||||
## func CallRaw
|
||||
## func [CallRaw](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/call.go#L104>)
|
||||
|
||||
```go
|
||||
func CallRaw[Req any](ctx context.Context, b *Bot, method string, req Req) (json.RawMessage, error)
|
||||
@@ -104,7 +104,7 @@ CallRaw is like Call but returns the raw JSON of the result field instead of dec
|
||||
CallRaw still translates non\-OK responses into \*APIError just like Call.
|
||||
|
||||
<a name="NewDefaultHTTPDoer"></a>
|
||||
## func NewDefaultHTTPDoer
|
||||
## func [NewDefaultHTTPDoer](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/httpclient.go#L22>)
|
||||
|
||||
```go
|
||||
func NewDefaultHTTPDoer() *http.Client
|
||||
@@ -117,7 +117,7 @@ NewDefaultHTTPDoer returns an \*http.Client with sensible defaults for Telegram
|
||||
- HTTP/2 enabled \(default in net/http\).
|
||||
|
||||
<a name="APIError"></a>
|
||||
## type APIError
|
||||
## 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.
|
||||
|
||||
@@ -131,7 +131,7 @@ type APIError struct {
|
||||
```
|
||||
|
||||
<a name="APIError.Error"></a>
|
||||
### func \(\*APIError\) Error
|
||||
### func \(\*APIError\) [Error](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/errors.go#L24>)
|
||||
|
||||
```go
|
||||
func (e *APIError) Error() string
|
||||
@@ -140,7 +140,7 @@ func (e *APIError) Error() string
|
||||
Error implements error.
|
||||
|
||||
<a name="APIError.IsRetryable"></a>
|
||||
### func \(\*APIError\) IsRetryable
|
||||
### func \(\*APIError\) [IsRetryable](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/errors.go#L32>)
|
||||
|
||||
```go
|
||||
func (e *APIError) IsRetryable() bool
|
||||
@@ -149,7 +149,7 @@ func (e *APIError) IsRetryable() bool
|
||||
IsRetryable returns true for transient HTTP statuses \(429, 5xx\).
|
||||
|
||||
<a name="APIError.RetryAfter"></a>
|
||||
### func \(\*APIError\) RetryAfter
|
||||
### func \(\*APIError\) [RetryAfter](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/errors.go#L38>)
|
||||
|
||||
```go
|
||||
func (e *APIError) RetryAfter() time.Duration
|
||||
@@ -158,7 +158,7 @@ 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
|
||||
### func \(\*APIError\) [Unwrap](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/errors.go#L29>)
|
||||
|
||||
```go
|
||||
func (e *APIError) Unwrap() error
|
||||
@@ -167,7 +167,7 @@ func (e *APIError) Unwrap() error
|
||||
Unwrap returns the matched sentinel error, if any.
|
||||
|
||||
<a name="Bot"></a>
|
||||
## type Bot
|
||||
## type [Bot](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L7-L13>)
|
||||
|
||||
Bot is the Telegram Bot API client. Construct via New. All API methods \(declared in package api\) hang off \*Bot via thin wrappers around call.
|
||||
|
||||
@@ -178,7 +178,7 @@ type Bot struct {
|
||||
```
|
||||
|
||||
<a name="New"></a>
|
||||
### func New
|
||||
### func [New](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L36>)
|
||||
|
||||
```go
|
||||
func New(token string, opts ...Option) *Bot
|
||||
@@ -187,7 +187,7 @@ func New(token string, opts ...Option) *Bot
|
||||
New constructs a Bot with the given token and optional configuration. The default HTTP client is tuned for long\-poll workloads \(see NewDefaultHTTPDoer\); the default codec wraps encoding/json; the default logger discards records.
|
||||
|
||||
<a name="Bot.BaseURL"></a>
|
||||
### func \(\*Bot\) BaseURL
|
||||
### func \(\*Bot\) [BaseURL](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L20>)
|
||||
|
||||
```go
|
||||
func (b *Bot) BaseURL() string
|
||||
@@ -196,7 +196,7 @@ func (b *Bot) BaseURL() string
|
||||
BaseURL returns the configured Telegram API base URL.
|
||||
|
||||
<a name="Bot.Codec"></a>
|
||||
### func \(\*Bot\) Codec
|
||||
### func \(\*Bot\) [Codec](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L27>)
|
||||
|
||||
```go
|
||||
func (b *Bot) Codec() Codec
|
||||
@@ -205,7 +205,7 @@ func (b *Bot) Codec() Codec
|
||||
Codec returns the configured Codec.
|
||||
|
||||
<a name="Bot.HTTP"></a>
|
||||
### func \(\*Bot\) HTTP
|
||||
### func \(\*Bot\) [HTTP](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L24>)
|
||||
|
||||
```go
|
||||
func (b *Bot) HTTP() HTTPDoer
|
||||
@@ -214,7 +214,7 @@ func (b *Bot) HTTP() HTTPDoer
|
||||
HTTP returns the underlying HTTPDoer. Exposed for adapters that need to share connection pools or for diagnostic checks.
|
||||
|
||||
<a name="Bot.Logger"></a>
|
||||
### func \(\*Bot\) Logger
|
||||
### func \(\*Bot\) [Logger](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L30>)
|
||||
|
||||
```go
|
||||
func (b *Bot) Logger() Logger
|
||||
@@ -223,7 +223,7 @@ func (b *Bot) Logger() Logger
|
||||
Logger returns the configured Logger.
|
||||
|
||||
<a name="Bot.Token"></a>
|
||||
### func \(\*Bot\) Token
|
||||
### func \(\*Bot\) [Token](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L17>)
|
||||
|
||||
```go
|
||||
func (b *Bot) Token() string
|
||||
@@ -232,7 +232,7 @@ func (b *Bot) Token() string
|
||||
Token returns the bot token. Exposed for advanced use cases \(custom transports, manual URL building\); ordinary code does not need it.
|
||||
|
||||
<a name="Codec"></a>
|
||||
## type Codec
|
||||
## type [Codec](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#L10-L13>)
|
||||
|
||||
Codec encodes/decodes JSON payloads exchanged with the Telegram Bot API. The default implementation wraps goccy/go\-json. Users may plug in bytedance/sonic or any compatible encoder by passing WithCodec to New.
|
||||
|
||||
@@ -244,7 +244,7 @@ type Codec interface {
|
||||
```
|
||||
|
||||
<a name="DefaultCodec"></a>
|
||||
## type DefaultCodec
|
||||
## type [DefaultCodec](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#L16>)
|
||||
|
||||
DefaultCodec wraps goccy/go\-json. It is the zero\-value safe default.
|
||||
|
||||
@@ -253,7 +253,7 @@ type DefaultCodec struct{}
|
||||
```
|
||||
|
||||
<a name="DefaultCodec.Marshal"></a>
|
||||
### func \(DefaultCodec\) Marshal
|
||||
### func \(DefaultCodec\) [Marshal](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#L19>)
|
||||
|
||||
```go
|
||||
func (DefaultCodec) Marshal(v any) ([]byte, error)
|
||||
@@ -262,7 +262,7 @@ func (DefaultCodec) Marshal(v any) ([]byte, error)
|
||||
Marshal calls json.Marshal.
|
||||
|
||||
<a name="DefaultCodec.Unmarshal"></a>
|
||||
### func \(DefaultCodec\) Unmarshal
|
||||
### func \(DefaultCodec\) [Unmarshal](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#L22>)
|
||||
|
||||
```go
|
||||
func (DefaultCodec) Unmarshal(data []byte, v any) error
|
||||
@@ -271,7 +271,7 @@ func (DefaultCodec) Unmarshal(data []byte, v any) error
|
||||
Unmarshal calls json.Unmarshal.
|
||||
|
||||
<a name="HTTPDoer"></a>
|
||||
## type HTTPDoer
|
||||
## 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.
|
||||
|
||||
@@ -282,7 +282,7 @@ type HTTPDoer interface {
|
||||
```
|
||||
|
||||
<a name="Logger"></a>
|
||||
## type Logger
|
||||
## 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.
|
||||
|
||||
@@ -296,7 +296,7 @@ type Logger interface {
|
||||
```
|
||||
|
||||
<a name="MultipartFile"></a>
|
||||
## type MultipartFile
|
||||
## 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.
|
||||
|
||||
@@ -309,7 +309,7 @@ type MultipartFile struct {
|
||||
```
|
||||
|
||||
<a name="NetworkError"></a>
|
||||
## type NetworkError
|
||||
## 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\).
|
||||
|
||||
@@ -318,7 +318,7 @@ type NetworkError struct{ Err error }
|
||||
```
|
||||
|
||||
<a name="NetworkError.Error"></a>
|
||||
### func \(\*NetworkError\) Error
|
||||
### func \(\*NetworkError\) [Error](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/errors.go#L49>)
|
||||
|
||||
```go
|
||||
func (e *NetworkError) Error() string
|
||||
@@ -327,7 +327,7 @@ func (e *NetworkError) Error() string
|
||||
|
||||
|
||||
<a name="NetworkError.Unwrap"></a>
|
||||
### func \(\*NetworkError\) Unwrap
|
||||
### func \(\*NetworkError\) [Unwrap](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/errors.go#L51>)
|
||||
|
||||
```go
|
||||
func (e *NetworkError) Unwrap() error
|
||||
@@ -336,7 +336,7 @@ func (e *NetworkError) Unwrap() error
|
||||
|
||||
|
||||
<a name="NoopLogger"></a>
|
||||
## type NoopLogger
|
||||
## 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.
|
||||
|
||||
@@ -345,7 +345,7 @@ type NoopLogger struct{}
|
||||
```
|
||||
|
||||
<a name="NoopLogger.Debug"></a>
|
||||
### func \(NoopLogger\) Debug
|
||||
### func \(NoopLogger\) [Debug](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/logger.go#L16>)
|
||||
|
||||
```go
|
||||
func (NoopLogger) Debug(string, ...any)
|
||||
@@ -354,7 +354,7 @@ func (NoopLogger) Debug(string, ...any)
|
||||
|
||||
|
||||
<a name="NoopLogger.Error"></a>
|
||||
### func \(NoopLogger\) Error
|
||||
### func \(NoopLogger\) [Error](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/logger.go#L19>)
|
||||
|
||||
```go
|
||||
func (NoopLogger) Error(string, ...any)
|
||||
@@ -363,7 +363,7 @@ func (NoopLogger) Error(string, ...any)
|
||||
|
||||
|
||||
<a name="NoopLogger.Info"></a>
|
||||
### func \(NoopLogger\) Info
|
||||
### func \(NoopLogger\) [Info](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/logger.go#L17>)
|
||||
|
||||
```go
|
||||
func (NoopLogger) Info(string, ...any)
|
||||
@@ -372,7 +372,7 @@ func (NoopLogger) Info(string, ...any)
|
||||
|
||||
|
||||
<a name="NoopLogger.Warn"></a>
|
||||
### func \(NoopLogger\) Warn
|
||||
### func \(NoopLogger\) [Warn](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/logger.go#L18>)
|
||||
|
||||
```go
|
||||
func (NoopLogger) Warn(string, ...any)
|
||||
@@ -381,7 +381,7 @@ func (NoopLogger) Warn(string, ...any)
|
||||
|
||||
|
||||
<a name="Option"></a>
|
||||
## type Option
|
||||
## 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.
|
||||
|
||||
@@ -390,7 +390,7 @@ type Option func(*Bot)
|
||||
```
|
||||
|
||||
<a name="WithBaseURL"></a>
|
||||
### func WithBaseURL
|
||||
### func [WithBaseURL](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/options.go#L18>)
|
||||
|
||||
```go
|
||||
func WithBaseURL(url string) Option
|
||||
@@ -399,7 +399,7 @@ 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
|
||||
### func [WithCodec](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/options.go#L14>)
|
||||
|
||||
```go
|
||||
func WithCodec(c Codec) Option
|
||||
@@ -408,7 +408,7 @@ 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
|
||||
### func [WithHTTPClient](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/options.go#L10>)
|
||||
|
||||
```go
|
||||
func WithHTTPClient(c HTTPDoer) Option
|
||||
@@ -417,7 +417,7 @@ 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
|
||||
### func [WithLogger](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/options.go#L22>)
|
||||
|
||||
```go
|
||||
func WithLogger(l Logger) Option
|
||||
@@ -426,7 +426,7 @@ func WithLogger(l Logger) Option
|
||||
WithLogger sets the logger used for diagnostic events. Passing nil silently disables logging.
|
||||
|
||||
<a name="ParseError"></a>
|
||||
## type ParseError
|
||||
## 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.
|
||||
|
||||
@@ -438,7 +438,7 @@ type ParseError struct {
|
||||
```
|
||||
|
||||
<a name="ParseError.Error"></a>
|
||||
### func \(\*ParseError\) Error
|
||||
### func \(\*ParseError\) [Error](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/errors.go#L60>)
|
||||
|
||||
```go
|
||||
func (e *ParseError) Error() string
|
||||
@@ -447,7 +447,7 @@ func (e *ParseError) Error() string
|
||||
|
||||
|
||||
<a name="ParseError.Unwrap"></a>
|
||||
### func \(\*ParseError\) Unwrap
|
||||
### func \(\*ParseError\) [Unwrap](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/errors.go#L68>)
|
||||
|
||||
```go
|
||||
func (e *ParseError) Unwrap() error
|
||||
@@ -456,7 +456,7 @@ func (e *ParseError) Unwrap() error
|
||||
|
||||
|
||||
<a name="ResponseParameters"></a>
|
||||
## type ResponseParameters
|
||||
## 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.
|
||||
|
||||
@@ -470,7 +470,7 @@ type ResponseParameters struct {
|
||||
```
|
||||
|
||||
<a name="Result"></a>
|
||||
## type Result
|
||||
## 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.
|
||||
|
||||
@@ -487,7 +487,7 @@ type Result[T any] struct {
|
||||
```
|
||||
|
||||
<a name="RetryDoer"></a>
|
||||
## type RetryDoer
|
||||
## 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.
|
||||
|
||||
@@ -505,7 +505,7 @@ type RetryDoer struct {
|
||||
```
|
||||
|
||||
<a name="NewRetryDoer"></a>
|
||||
### func NewRetryDoer
|
||||
### func [NewRetryDoer](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/retry.go#L63>)
|
||||
|
||||
```go
|
||||
func NewRetryDoer(inner HTTPDoer, opts ...RetryOption) *RetryDoer
|
||||
@@ -514,7 +514,7 @@ func NewRetryDoer(inner HTTPDoer, opts ...RetryOption) *RetryDoer
|
||||
NewRetryDoer wraps inner with retry behaviour.
|
||||
|
||||
<a name="RetryDoer.Do"></a>
|
||||
### func \(\*RetryDoer\) Do
|
||||
### 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)
|
||||
@@ -523,7 +523,7 @@ 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
|
||||
## type [RetryOption](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/retry.go#L33>)
|
||||
|
||||
RetryOption configures a RetryDoer.
|
||||
|
||||
@@ -532,7 +532,7 @@ type RetryOption func(*RetryDoer)
|
||||
```
|
||||
|
||||
<a name="WithBackoffFactor"></a>
|
||||
### func WithBackoffFactor
|
||||
### func [WithBackoffFactor](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/retry.go#L52>)
|
||||
|
||||
```go
|
||||
func WithBackoffFactor(f float64) RetryOption
|
||||
@@ -541,7 +541,7 @@ func WithBackoffFactor(f float64) RetryOption
|
||||
WithBackoffFactor sets the exponential growth factor. Default 2.0.
|
||||
|
||||
<a name="WithBaseBackoff"></a>
|
||||
### func WithBaseBackoff
|
||||
### func [WithBaseBackoff](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/retry.go#L42>)
|
||||
|
||||
```go
|
||||
func WithBaseBackoff(d time.Duration) RetryOption
|
||||
@@ -550,7 +550,7 @@ func WithBaseBackoff(d time.Duration) RetryOption
|
||||
WithBaseBackoff sets the initial backoff duration. Default 500ms.
|
||||
|
||||
<a name="WithJitter"></a>
|
||||
### func WithJitter
|
||||
### func [WithJitter](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/retry.go#L58>)
|
||||
|
||||
```go
|
||||
func WithJitter(j float64) RetryOption
|
||||
@@ -559,7 +559,7 @@ 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
|
||||
### func [WithMaxAttempts](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/retry.go#L37>)
|
||||
|
||||
```go
|
||||
func WithMaxAttempts(n int) RetryOption
|
||||
@@ -568,7 +568,7 @@ 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
|
||||
### func [WithMaxBackoff](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/retry.go#L47>)
|
||||
|
||||
```go
|
||||
func WithMaxBackoff(d time.Duration) RetryOption
|
||||
|
||||
+83
-68
@@ -13,6 +13,7 @@ Package dispatch provides a typed router for Telegram updates. It consumes any t
|
||||
- [Variables](<#variables>)
|
||||
- [type Context](<#Context>)
|
||||
- [func NewContext\(ctx context.Context, b \*client.Bot, u \*api.Update\) \*Context](<#NewContext>)
|
||||
- [func \(c \*Context\) Set\(key string, val any\)](<#Context.Set>)
|
||||
- [type Filter](<#Filter>)
|
||||
- [func All\[T any\]\(filters ...Filter\[T\]\) Filter\[T\]](<#All>)
|
||||
- [func Any\[T any\]\(filters ...Filter\[T\]\) Filter\[T\]](<#Any>)
|
||||
@@ -90,7 +91,7 @@ var ErrEndGroups = errors.New("dispatch: end groups")
|
||||
```
|
||||
|
||||
<a name="Context"></a>
|
||||
## type Context
|
||||
## type [Context](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/context.go#L41-L49>)
|
||||
|
||||
Context bundles the per\-update state every handler receives.
|
||||
|
||||
@@ -100,25 +101,30 @@ Bot is the API client. Handlers reply by calling api.SendMessage\(c.Ctx, c.Bot,
|
||||
|
||||
Update is the raw update; payload\-typed handlers also receive a narrowed pointer to one of its sub\-fields.
|
||||
|
||||
Values is a per\-update bag matchers populate. Conventional keys:
|
||||
Command, CommandArgs and RegexMatch are populated by the router for the matching route kind; they replace the previous "command", "command\_args" and "regex\_match" entries in Values, which were the only conventional keys. Values remains for user\-defined custom keys.
|
||||
|
||||
```
|
||||
"command": string, the matched bot command (e.g. "/start")
|
||||
"command_args": string, everything after the command
|
||||
"regex_match": []string, regex sub-matches when OnText matches
|
||||
```
|
||||
Command is the matched bot command \(e.g. "/start"\); empty when the route is not a command match.
|
||||
|
||||
CommandArgs is everything after the command; empty when no command matched or the command had no trailing text.
|
||||
|
||||
RegexMatch is the regex sub\-matches when an OnText/OnCallback regex route matched; nil otherwise.
|
||||
|
||||
Values is lazily allocated for user\-defined keys. Handlers that don't write pay no allocation. Reads against a nil map return the zero value. Writers must use Set instead of indexing the map directly.
|
||||
|
||||
```go
|
||||
type Context struct {
|
||||
Ctx context.Context
|
||||
Bot *client.Bot
|
||||
Update *api.Update
|
||||
Values map[string]any
|
||||
Ctx context.Context
|
||||
Bot *client.Bot
|
||||
Update *api.Update
|
||||
Command string
|
||||
CommandArgs string
|
||||
RegexMatch []string
|
||||
Values map[string]any
|
||||
}
|
||||
```
|
||||
|
||||
<a name="NewContext"></a>
|
||||
### func NewContext
|
||||
### func [NewContext](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/context.go#L53>)
|
||||
|
||||
```go
|
||||
func NewContext(ctx context.Context, b *client.Bot, u *api.Update) *Context
|
||||
@@ -126,8 +132,17 @@ 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
|
||||
## 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.
|
||||
|
||||
@@ -142,7 +157,7 @@ type Filter[T any] func(payload T) bool
|
||||
```
|
||||
|
||||
<a name="All"></a>
|
||||
### func All
|
||||
### func [All](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filter.go#L48>)
|
||||
|
||||
```go
|
||||
func All[T any](filters ...Filter[T]) Filter[T]
|
||||
@@ -151,7 +166,7 @@ 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
|
||||
### func [Any](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filter.go#L61>)
|
||||
|
||||
```go
|
||||
func Any[T any](filters ...Filter[T]) Filter[T]
|
||||
@@ -160,7 +175,7 @@ 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
|
||||
### 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]
|
||||
@@ -169,7 +184,7 @@ 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
|
||||
### func \(Filter\[T\]\) [Not](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filter.go#L42>)
|
||||
|
||||
```go
|
||||
func (f Filter[T]) Not() Filter[T]
|
||||
@@ -178,7 +193,7 @@ func (f Filter[T]) Not() Filter[T]
|
||||
Not returns a Filter that inverts f.
|
||||
|
||||
<a name="Filter[T].Or"></a>
|
||||
### func \(Filter\[T\]\) Or
|
||||
### 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]
|
||||
@@ -187,7 +202,7 @@ 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
|
||||
## 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.
|
||||
|
||||
@@ -196,7 +211,7 @@ type Handler[T any] func(ctx *Context, payload T) error
|
||||
```
|
||||
|
||||
<a name="Middleware"></a>
|
||||
## type Middleware
|
||||
## 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\)\)\).
|
||||
|
||||
@@ -205,7 +220,7 @@ type Middleware[T any] func(Handler[T]) Handler[T]
|
||||
```
|
||||
|
||||
<a name="Chain"></a>
|
||||
### func Chain
|
||||
### func [Chain](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/handler.go#L14>)
|
||||
|
||||
```go
|
||||
func Chain[T any](mws ...Middleware[T]) Middleware[T]
|
||||
@@ -214,7 +229,7 @@ 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
|
||||
### func [Recovery](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/middleware.go#L13>)
|
||||
|
||||
```go
|
||||
func Recovery() Middleware[*api.Update]
|
||||
@@ -223,7 +238,7 @@ 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
|
||||
## 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.
|
||||
|
||||
@@ -236,7 +251,7 @@ type NamedHandlers[T any] struct {
|
||||
```
|
||||
|
||||
<a name="NewNamedHandlers"></a>
|
||||
### func NewNamedHandlers
|
||||
### func [NewNamedHandlers](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/named.go#L22>)
|
||||
|
||||
```go
|
||||
func NewNamedHandlers[T any]() *NamedHandlers[T]
|
||||
@@ -245,7 +260,7 @@ func NewNamedHandlers[T any]() *NamedHandlers[T]
|
||||
NewNamedHandlers returns a new, empty NamedHandlers\[T\].
|
||||
|
||||
<a name="NamedHandlers[T].Handler"></a>
|
||||
### func \(\*NamedHandlers\[T\]\) Handler
|
||||
### func \(\*NamedHandlers\[T\]\) [Handler](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/named.go#L81>)
|
||||
|
||||
```go
|
||||
func (n *NamedHandlers[T]) Handler() Handler[T]
|
||||
@@ -263,7 +278,7 @@ 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
|
||||
### 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
|
||||
@@ -272,7 +287,7 @@ func (n *NamedHandlers[T]) Has(name string) bool
|
||||
Has reports whether name is registered.
|
||||
|
||||
<a name="NamedHandlers[T].Names"></a>
|
||||
### func \(\*NamedHandlers\[T\]\) Names
|
||||
### func \(\*NamedHandlers\[T\]\) [Names](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/named.go#L63>)
|
||||
|
||||
```go
|
||||
func (n *NamedHandlers[T]) Names() []string
|
||||
@@ -281,7 +296,7 @@ func (n *NamedHandlers[T]) Names() []string
|
||||
Names returns the registered names in registration order.
|
||||
|
||||
<a name="NamedHandlers[T].Remove"></a>
|
||||
### func \(\*NamedHandlers\[T\]\) Remove
|
||||
### 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
|
||||
@@ -290,7 +305,7 @@ 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
|
||||
### 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])
|
||||
@@ -299,7 +314,7 @@ 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
|
||||
## type [Router](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L19-L64>)
|
||||
|
||||
Router dispatches updates from any Updater to typed handlers.
|
||||
|
||||
@@ -312,7 +327,7 @@ type Router struct {
|
||||
```
|
||||
|
||||
<a name="New"></a>
|
||||
### func New
|
||||
### func [New](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L128>)
|
||||
|
||||
```go
|
||||
func New(b *client.Bot, opts ...RouterOption) *Router
|
||||
@@ -321,7 +336,7 @@ 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
|
||||
### func \(\*Router\) [Group](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/groups.go#L40>)
|
||||
|
||||
```go
|
||||
func (r *Router) Group(group int) *RouterScope
|
||||
@@ -330,7 +345,7 @@ 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
|
||||
### func \(\*Router\) [OnBusinessConnection](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L285>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnBusinessConnection(h Handler[*api.BusinessConnection])
|
||||
@@ -339,7 +354,7 @@ func (r *Router) OnBusinessConnection(h Handler[*api.BusinessConnection])
|
||||
OnBusinessConnection registers a handler for business connection updates.
|
||||
|
||||
<a name="Router.OnCallback"></a>
|
||||
### func \(\*Router\) OnCallback
|
||||
### 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])
|
||||
@@ -350,7 +365,7 @@ 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
|
||||
### 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])
|
||||
@@ -359,7 +374,7 @@ func (r *Router) OnCallbackFilter(f Filter[*api.CallbackQuery], h Handler[*api.C
|
||||
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
|
||||
### func \(\*Router\) [OnChannelPost](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L177>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnChannelPost(h Handler[*api.Message])
|
||||
@@ -368,7 +383,7 @@ func (r *Router) OnChannelPost(h Handler[*api.Message])
|
||||
OnChannelPost registers a handler for channel post updates.
|
||||
|
||||
<a name="Router.OnChatBoost"></a>
|
||||
### func \(\*Router\) OnChatBoost
|
||||
### func \(\*Router\) [OnChatBoost](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L275>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnChatBoost(h Handler[*api.ChatBoostUpdated])
|
||||
@@ -377,7 +392,7 @@ func (r *Router) OnChatBoost(h Handler[*api.ChatBoostUpdated])
|
||||
OnChatBoost registers a handler for chat boost updates.
|
||||
|
||||
<a name="Router.OnChatJoinRequest"></a>
|
||||
### func \(\*Router\) OnChatJoinRequest
|
||||
### func \(\*Router\) [OnChatJoinRequest](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L225>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnChatJoinRequest(h Handler[*api.ChatJoinRequest])
|
||||
@@ -386,7 +401,7 @@ func (r *Router) OnChatJoinRequest(h Handler[*api.ChatJoinRequest])
|
||||
OnChatJoinRequest registers a handler for chat join requests.
|
||||
|
||||
<a name="Router.OnChatJoinRequestFilter"></a>
|
||||
### func \(\*Router\) OnChatJoinRequestFilter
|
||||
### 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])
|
||||
@@ -395,7 +410,7 @@ func (r *Router) OnChatJoinRequestFilter(f Filter[*api.ChatJoinRequest], h Handl
|
||||
OnChatJoinRequestFilter registers a filtered handler for chat join requests.
|
||||
|
||||
<a name="Router.OnChatMember"></a>
|
||||
### func \(\*Router\) OnChatMember
|
||||
### func \(\*Router\) [OnChatMember](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L215>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnChatMember(h Handler[*api.ChatMemberUpdated])
|
||||
@@ -404,7 +419,7 @@ 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
|
||||
### 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])
|
||||
@@ -413,7 +428,7 @@ func (r *Router) OnChatMemberFilter(f Filter[*api.ChatMemberUpdated], h Handler[
|
||||
OnChatMemberFilter registers a filtered handler for chat member status changes.
|
||||
|
||||
<a name="Router.OnChosenInlineResult"></a>
|
||||
### func \(\*Router\) OnChosenInlineResult
|
||||
### func \(\*Router\) [OnChosenInlineResult](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L260>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnChosenInlineResult(h Handler[*api.ChosenInlineResult])
|
||||
@@ -422,7 +437,7 @@ func (r *Router) OnChosenInlineResult(h Handler[*api.ChosenInlineResult])
|
||||
OnChosenInlineResult registers a handler for chosen inline results.
|
||||
|
||||
<a name="Router.OnCommand"></a>
|
||||
### func \(\*Router\) OnCommand
|
||||
### 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])
|
||||
@@ -431,7 +446,7 @@ 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
|
||||
### func \(\*Router\) [OnEditedChannelPost](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L182>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnEditedChannelPost(h Handler[*api.Message])
|
||||
@@ -440,7 +455,7 @@ 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
|
||||
### func \(\*Router\) [OnEditedMessage](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L172>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnEditedMessage(h Handler[*api.Message])
|
||||
@@ -449,7 +464,7 @@ func (r *Router) OnEditedMessage(h Handler[*api.Message])
|
||||
OnEditedMessage registers a handler for edited message updates.
|
||||
|
||||
<a name="Router.OnInlineQuery"></a>
|
||||
### func \(\*Router\) OnInlineQuery
|
||||
### func \(\*Router\) [OnInlineQuery](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L167>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnInlineQuery(h Handler[*api.InlineQuery])
|
||||
@@ -458,7 +473,7 @@ 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
|
||||
### 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])
|
||||
@@ -467,7 +482,7 @@ func (r *Router) OnInlineQueryFilter(f Filter[*api.InlineQuery], h Handler[*api.
|
||||
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
|
||||
### 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])
|
||||
@@ -476,7 +491,7 @@ 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
|
||||
### func \(\*Router\) [OnMessageReaction](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L265>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnMessageReaction(h Handler[*api.MessageReactionUpdated])
|
||||
@@ -485,7 +500,7 @@ func (r *Router) OnMessageReaction(h Handler[*api.MessageReactionUpdated])
|
||||
OnMessageReaction registers a handler for message reaction updates.
|
||||
|
||||
<a name="Router.OnMessageReactionCount"></a>
|
||||
### func \(\*Router\) OnMessageReactionCount
|
||||
### func \(\*Router\) [OnMessageReactionCount](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L270>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnMessageReactionCount(h Handler[*api.MessageReactionCountUpdated])
|
||||
@@ -494,7 +509,7 @@ func (r *Router) OnMessageReactionCount(h Handler[*api.MessageReactionCountUpdat
|
||||
OnMessageReactionCount registers a handler for anonymous message reaction count updates.
|
||||
|
||||
<a name="Router.OnMyChatMember"></a>
|
||||
### func \(\*Router\) OnMyChatMember
|
||||
### func \(\*Router\) [OnMyChatMember](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L205>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnMyChatMember(h Handler[*api.ChatMemberUpdated])
|
||||
@@ -503,7 +518,7 @@ 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
|
||||
### 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])
|
||||
@@ -512,7 +527,7 @@ func (r *Router) OnMyChatMemberFilter(f Filter[*api.ChatMemberUpdated], h Handle
|
||||
OnMyChatMemberFilter registers a filtered handler for bot's own chat member status changes.
|
||||
|
||||
<a name="Router.OnPoll"></a>
|
||||
### func \(\*Router\) OnPoll
|
||||
### func \(\*Router\) [OnPoll](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L250>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnPoll(h Handler[*api.Poll])
|
||||
@@ -521,7 +536,7 @@ func (r *Router) OnPoll(h Handler[*api.Poll])
|
||||
OnPoll registers a handler for poll state updates.
|
||||
|
||||
<a name="Router.OnPollAnswer"></a>
|
||||
### func \(\*Router\) OnPollAnswer
|
||||
### func \(\*Router\) [OnPollAnswer](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L255>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnPollAnswer(h Handler[*api.PollAnswer])
|
||||
@@ -530,7 +545,7 @@ func (r *Router) OnPollAnswer(h Handler[*api.PollAnswer])
|
||||
OnPollAnswer registers a handler for poll answer updates.
|
||||
|
||||
<a name="Router.OnPreCheckoutQuery"></a>
|
||||
### func \(\*Router\) OnPreCheckoutQuery
|
||||
### func \(\*Router\) [OnPreCheckoutQuery](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L235>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnPreCheckoutQuery(h Handler[*api.PreCheckoutQuery])
|
||||
@@ -539,7 +554,7 @@ func (r *Router) OnPreCheckoutQuery(h Handler[*api.PreCheckoutQuery])
|
||||
OnPreCheckoutQuery registers a handler for pre\-checkout queries.
|
||||
|
||||
<a name="Router.OnPreCheckoutQueryFilter"></a>
|
||||
### func \(\*Router\) OnPreCheckoutQueryFilter
|
||||
### 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])
|
||||
@@ -548,7 +563,7 @@ func (r *Router) OnPreCheckoutQueryFilter(f Filter[*api.PreCheckoutQuery], h Han
|
||||
OnPreCheckoutQueryFilter registers a filtered handler for pre\-checkout queries.
|
||||
|
||||
<a name="Router.OnPurchasedPaidMedia"></a>
|
||||
### func \(\*Router\) OnPurchasedPaidMedia
|
||||
### func \(\*Router\) [OnPurchasedPaidMedia](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L290>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnPurchasedPaidMedia(h Handler[*api.PaidMediaPurchased])
|
||||
@@ -557,7 +572,7 @@ 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
|
||||
### func \(\*Router\) [OnRemovedChatBoost](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L280>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnRemovedChatBoost(h Handler[*api.ChatBoostRemoved])
|
||||
@@ -566,7 +581,7 @@ 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
|
||||
### func \(\*Router\) [OnShippingQuery](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L245>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnShippingQuery(h Handler[*api.ShippingQuery])
|
||||
@@ -575,7 +590,7 @@ func (r *Router) OnShippingQuery(h Handler[*api.ShippingQuery])
|
||||
OnShippingQuery registers a handler for shipping queries.
|
||||
|
||||
<a name="Router.OnText"></a>
|
||||
### func \(\*Router\) OnText
|
||||
### 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])
|
||||
@@ -586,7 +601,7 @@ OnText registers a handler for messages whose Text matches the regex.
|
||||
Panics at registration time if pattern is not a valid regular expression.
|
||||
|
||||
<a name="Router.Run"></a>
|
||||
### func \(\*Router\) Run
|
||||
### func \(\*Router\) [Run](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L303>)
|
||||
|
||||
```go
|
||||
func (r *Router) Run(ctx context.Context, u transport.Updater) error
|
||||
@@ -599,7 +614,7 @@ By default updates are processed concurrently \(up to WithMaxConcurrency\(50\) g
|
||||
Run waits for all in\-flight handlers to finish before returning.
|
||||
|
||||
<a name="Router.Use"></a>
|
||||
### func \(\*Router\) Use
|
||||
### func \(\*Router\) [Use](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L141>)
|
||||
|
||||
```go
|
||||
func (r *Router) Use(mw Middleware[*api.Update])
|
||||
@@ -608,7 +623,7 @@ func (r *Router) Use(mw Middleware[*api.Update])
|
||||
Use registers a global middleware applied to every Update dispatch.
|
||||
|
||||
<a name="RouterOption"></a>
|
||||
## type RouterOption
|
||||
## type [RouterOption](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L97>)
|
||||
|
||||
RouterOption configures a Router at construction time.
|
||||
|
||||
@@ -617,7 +632,7 @@ type RouterOption func(*Router)
|
||||
```
|
||||
|
||||
<a name="WithMaxConcurrency"></a>
|
||||
### func WithMaxConcurrency
|
||||
### func [WithMaxConcurrency](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L106>)
|
||||
|
||||
```go
|
||||
func WithMaxConcurrency(n int) RouterOption
|
||||
@@ -628,7 +643,7 @@ WithMaxConcurrency sets the maximum number of updates processed in parallel. Def
|
||||
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
|
||||
## 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.
|
||||
|
||||
@@ -639,7 +654,7 @@ type RouterScope struct {
|
||||
```
|
||||
|
||||
<a name="RouterScope.OnCommand"></a>
|
||||
### func \(\*RouterScope\) OnCommand
|
||||
### 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])
|
||||
@@ -648,7 +663,7 @@ 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
|
||||
### 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])
|
||||
@@ -657,7 +672,7 @@ func (s *RouterScope) OnMessageFilter(f Filter[*api.Message], h Handler[*api.Mes
|
||||
OnMessageFilter registers a filter\-based message handler in this group.
|
||||
|
||||
<a name="RouterScope.OnText"></a>
|
||||
### func \(\*RouterScope\) OnText
|
||||
### 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])
|
||||
|
||||
@@ -36,7 +36,7 @@ var ErrKeyNotFound = errors.New("conversation: key not found")
|
||||
```
|
||||
|
||||
<a name="End"></a>
|
||||
## func End
|
||||
## func [End](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/conversation/handler.go#L34>)
|
||||
|
||||
```go
|
||||
func End() error
|
||||
@@ -45,7 +45,7 @@ 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
|
||||
## func [Next](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/conversation/handler.go#L28>)
|
||||
|
||||
```go
|
||||
func Next(s State) error
|
||||
@@ -54,7 +54,7 @@ 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
|
||||
## 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\).
|
||||
|
||||
@@ -87,7 +87,7 @@ type Conversation struct {
|
||||
```
|
||||
|
||||
<a name="Conversation.Dispatch"></a>
|
||||
### func \(\*Conversation\) Dispatch
|
||||
### 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]
|
||||
@@ -98,7 +98,7 @@ Dispatch is a global middleware\-shaped Handler that consumes updates and routes
|
||||
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
|
||||
## 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:
|
||||
|
||||
@@ -112,7 +112,7 @@ type Handler func(ctx *dispatch.Context, u *api.Update) error
|
||||
```
|
||||
|
||||
<a name="KeyStrategy"></a>
|
||||
## type KeyStrategy
|
||||
## 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.
|
||||
|
||||
@@ -158,7 +158,7 @@ var KeyByUserAndChat KeyStrategy = func(u *api.Update) string {
|
||||
```
|
||||
|
||||
<a name="MemoryStorage"></a>
|
||||
## type MemoryStorage
|
||||
## 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.
|
||||
|
||||
@@ -169,7 +169,7 @@ type MemoryStorage struct {
|
||||
```
|
||||
|
||||
<a name="NewMemoryStorage"></a>
|
||||
### func NewMemoryStorage
|
||||
### func [NewMemoryStorage](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/conversation/in_memory.go#L17>)
|
||||
|
||||
```go
|
||||
func NewMemoryStorage() *MemoryStorage
|
||||
@@ -178,7 +178,7 @@ func NewMemoryStorage() *MemoryStorage
|
||||
NewMemoryStorage constructs an empty in\-memory storage.
|
||||
|
||||
<a name="MemoryStorage.Delete"></a>
|
||||
### func \(\*MemoryStorage\) Delete
|
||||
### 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
|
||||
@@ -187,7 +187,7 @@ func (s *MemoryStorage) Delete(_ context.Context, key string) error
|
||||
|
||||
|
||||
<a name="MemoryStorage.Get"></a>
|
||||
### func \(\*MemoryStorage\) Get
|
||||
### 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)
|
||||
@@ -196,7 +196,7 @@ func (s *MemoryStorage) Get(_ context.Context, key string) (State, error)
|
||||
|
||||
|
||||
<a name="MemoryStorage.Set"></a>
|
||||
### func \(\*MemoryStorage\) Set
|
||||
### 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
|
||||
@@ -205,7 +205,7 @@ func (s *MemoryStorage) Set(_ context.Context, key string, state State) error
|
||||
|
||||
|
||||
<a name="State"></a>
|
||||
## type State
|
||||
## 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.
|
||||
|
||||
@@ -214,7 +214,7 @@ type State string
|
||||
```
|
||||
|
||||
<a name="Step"></a>
|
||||
## type Step
|
||||
## 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.
|
||||
|
||||
@@ -226,7 +226,7 @@ type Step struct {
|
||||
```
|
||||
|
||||
<a name="Storage"></a>
|
||||
## type Storage
|
||||
## 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.
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ Package callback provides Filter helpers for \*api.CallbackQuery payloads.
|
||||
|
||||
|
||||
<a name="Data"></a>
|
||||
## func Data
|
||||
## 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]
|
||||
@@ -26,7 +26,7 @@ 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
|
||||
## 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]
|
||||
@@ -35,7 +35,7 @@ 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
|
||||
## 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]
|
||||
@@ -44,7 +44,7 @@ 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
|
||||
## 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]
|
||||
|
||||
@@ -15,7 +15,7 @@ Package chatjoinrequest provides Filter helpers for \*api.ChatJoinRequest payloa
|
||||
|
||||
|
||||
<a name="FromUser"></a>
|
||||
## func FromUser
|
||||
## 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]
|
||||
@@ -24,7 +24,7 @@ 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
|
||||
## 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]
|
||||
|
||||
@@ -15,7 +15,7 @@ Package chatmember provides Filter helpers for \*api.ChatMemberUpdated payloads.
|
||||
|
||||
|
||||
<a name="FromUser"></a>
|
||||
## func FromUser
|
||||
## 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]
|
||||
@@ -24,7 +24,7 @@ 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
|
||||
## 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]
|
||||
|
||||
@@ -16,7 +16,7 @@ Package inline provides Filter helpers for \*api.InlineQuery payloads.
|
||||
|
||||
|
||||
<a name="Query"></a>
|
||||
## func Query
|
||||
## 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]
|
||||
@@ -25,7 +25,7 @@ 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
|
||||
## 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]
|
||||
@@ -34,7 +34,7 @@ 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
|
||||
## 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]
|
||||
|
||||
@@ -27,7 +27,7 @@ Package message provides Filter helpers for \*api.Message payloads.
|
||||
|
||||
|
||||
<a name="AnyCommand"></a>
|
||||
## func AnyCommand
|
||||
## func [AnyCommand](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/message/message.go#L69>)
|
||||
|
||||
```go
|
||||
func AnyCommand() dispatch.Filter[*api.Message]
|
||||
@@ -36,7 +36,7 @@ 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
|
||||
## 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]
|
||||
@@ -45,7 +45,7 @@ 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
|
||||
## 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]
|
||||
@@ -54,7 +54,7 @@ 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
|
||||
## 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]
|
||||
@@ -63,7 +63,7 @@ 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
|
||||
## func [HasDocument](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/message/message.go#L101>)
|
||||
|
||||
```go
|
||||
func HasDocument() dispatch.Filter[*api.Message]
|
||||
@@ -72,7 +72,7 @@ func HasDocument() dispatch.Filter[*api.Message]
|
||||
HasDocument returns a Filter that matches messages with a Document attachment.
|
||||
|
||||
<a name="HasEntity"></a>
|
||||
## func HasEntity
|
||||
## 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]
|
||||
@@ -81,7 +81,7 @@ 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
|
||||
## func [HasPhoto](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/message/message.go#L94>)
|
||||
|
||||
```go
|
||||
func HasPhoto() dispatch.Filter[*api.Message]
|
||||
@@ -90,7 +90,7 @@ func HasPhoto() dispatch.Filter[*api.Message]
|
||||
HasPhoto returns a Filter that matches messages with a Photo attachment.
|
||||
|
||||
<a name="InChat"></a>
|
||||
## func InChat
|
||||
## 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]
|
||||
@@ -99,7 +99,7 @@ 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
|
||||
## func [IsForward](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/message/message.go#L87>)
|
||||
|
||||
```go
|
||||
func IsForward() dispatch.Filter[*api.Message]
|
||||
@@ -108,7 +108,7 @@ func IsForward() dispatch.Filter[*api.Message]
|
||||
IsForward returns a Filter that matches messages that have ForwardOrigin set.
|
||||
|
||||
<a name="IsReply"></a>
|
||||
## func IsReply
|
||||
## func [IsReply](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/message/message.go#L80>)
|
||||
|
||||
```go
|
||||
func IsReply() dispatch.Filter[*api.Message]
|
||||
@@ -117,7 +117,7 @@ func IsReply() dispatch.Filter[*api.Message]
|
||||
IsReply returns a Filter that matches messages that have ReplyToMessage set.
|
||||
|
||||
<a name="Text"></a>
|
||||
## func Text
|
||||
## 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]
|
||||
@@ -126,7 +126,7 @@ 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
|
||||
## 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]
|
||||
@@ -135,7 +135,7 @@ 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
|
||||
## 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]
|
||||
@@ -144,7 +144,7 @@ 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
|
||||
## 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]
|
||||
|
||||
@@ -15,7 +15,7 @@ Package precheckoutquery provides Filter helpers for \*api.PreCheckoutQuery payl
|
||||
|
||||
|
||||
<a name="Currency"></a>
|
||||
## func Currency
|
||||
## 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]
|
||||
@@ -24,7 +24,7 @@ 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
|
||||
## 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]
|
||||
|
||||
+19
-19
@@ -34,7 +34,7 @@ All implementations satisfy the Updater interface so user code can swap one for
|
||||
|
||||
|
||||
<a name="BackoffStrategy"></a>
|
||||
## type BackoffStrategy
|
||||
## 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.
|
||||
|
||||
@@ -45,7 +45,7 @@ type BackoffStrategy interface {
|
||||
```
|
||||
|
||||
<a name="ExponentialBackoff"></a>
|
||||
## type ExponentialBackoff
|
||||
## 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.
|
||||
|
||||
@@ -59,7 +59,7 @@ type ExponentialBackoff struct {
|
||||
```
|
||||
|
||||
<a name="DefaultBackoff"></a>
|
||||
### func DefaultBackoff
|
||||
### func [DefaultBackoff](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/backoff.go#L26>)
|
||||
|
||||
```go
|
||||
func DefaultBackoff() *ExponentialBackoff
|
||||
@@ -68,7 +68,7 @@ func DefaultBackoff() *ExponentialBackoff
|
||||
DefaultBackoff returns an ExponentialBackoff with library defaults.
|
||||
|
||||
<a name="ExponentialBackoff.NextDelay"></a>
|
||||
### func \(\*ExponentialBackoff\) NextDelay
|
||||
### func \(\*ExponentialBackoff\) [NextDelay](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/backoff.go#L36>)
|
||||
|
||||
```go
|
||||
func (b *ExponentialBackoff) NextDelay(attempt int) time.Duration
|
||||
@@ -77,7 +77,7 @@ func (b *ExponentialBackoff) NextDelay(attempt int) time.Duration
|
||||
NextDelay implements BackoffStrategy.
|
||||
|
||||
<a name="LongPoller"></a>
|
||||
## type LongPoller
|
||||
## 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\).
|
||||
|
||||
@@ -95,7 +95,7 @@ type LongPoller struct {
|
||||
```
|
||||
|
||||
<a name="NewLongPoller"></a>
|
||||
### func NewLongPoller
|
||||
### func [NewLongPoller](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/longpoll.go#L34>)
|
||||
|
||||
```go
|
||||
func NewLongPoller(b *client.Bot) *LongPoller
|
||||
@@ -104,7 +104,7 @@ func NewLongPoller(b *client.Bot) *LongPoller
|
||||
NewLongPoller constructs a LongPoller with sensible defaults.
|
||||
|
||||
<a name="LongPoller.Run"></a>
|
||||
### func \(\*LongPoller\) Run
|
||||
### func \(\*LongPoller\) [Run](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/longpoll.go#L51>)
|
||||
|
||||
```go
|
||||
func (p *LongPoller) Run(ctx context.Context) error
|
||||
@@ -113,7 +113,7 @@ func (p *LongPoller) Run(ctx context.Context) error
|
||||
Run implements Updater. It blocks until ctx is cancelled, Stop is called, or a fatal error occurs \(e.g. unauthorized\). See LongPoller for at\-least\-once delivery semantics on shutdown.
|
||||
|
||||
<a name="LongPoller.Stop"></a>
|
||||
### func \(\*LongPoller\) Stop
|
||||
### func \(\*LongPoller\) [Stop](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/longpoll.go#L122>)
|
||||
|
||||
```go
|
||||
func (p *LongPoller) Stop(ctx context.Context) error
|
||||
@@ -122,7 +122,7 @@ func (p *LongPoller) Stop(ctx context.Context) error
|
||||
Stop implements Updater.
|
||||
|
||||
<a name="LongPoller.Updates"></a>
|
||||
### func \(\*LongPoller\) Updates
|
||||
### func \(\*LongPoller\) [Updates](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/longpoll.go#L46>)
|
||||
|
||||
```go
|
||||
func (p *LongPoller) Updates() <-chan api.Update
|
||||
@@ -131,7 +131,7 @@ func (p *LongPoller) Updates() <-chan api.Update
|
||||
Updates implements Updater.
|
||||
|
||||
<a name="Updater"></a>
|
||||
## type Updater
|
||||
## type [Updater](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/updater.go#L13-L23>)
|
||||
|
||||
Updater is the abstraction over update sources. Implementations must:
|
||||
|
||||
@@ -154,7 +154,7 @@ type Updater interface {
|
||||
```
|
||||
|
||||
<a name="WebhookOption"></a>
|
||||
## type WebhookOption
|
||||
## type [WebhookOption](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L57>)
|
||||
|
||||
WebhookOption configures a WebhookServer at construction time.
|
||||
|
||||
@@ -163,7 +163,7 @@ type WebhookOption func(*webhookOptions)
|
||||
```
|
||||
|
||||
<a name="WithBufferSize"></a>
|
||||
### func WithBufferSize
|
||||
### func [WithBufferSize](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L65>)
|
||||
|
||||
```go
|
||||
func WithBufferSize(n int) WebhookOption
|
||||
@@ -172,7 +172,7 @@ func WithBufferSize(n int) WebhookOption
|
||||
WithBufferSize sets the size of the updates channel buffer. Default is 64.
|
||||
|
||||
<a name="WebhookServer"></a>
|
||||
## type WebhookServer
|
||||
## type [WebhookServer](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L43-L54>)
|
||||
|
||||
WebhookServer implements Updater by exposing an http.Handler that receives updates from Telegram. It can be mounted on the user's own HTTP server \(via ServeHTTP\) or run standalone \(via ListenAndServe\).
|
||||
|
||||
@@ -185,7 +185,7 @@ type WebhookServer struct {
|
||||
```
|
||||
|
||||
<a name="NewWebhookServer"></a>
|
||||
### func NewWebhookServer
|
||||
### func [NewWebhookServer](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L71>)
|
||||
|
||||
```go
|
||||
func NewWebhookServer(b *client.Bot, opts ...WebhookOption) *WebhookServer
|
||||
@@ -194,7 +194,7 @@ func NewWebhookServer(b *client.Bot, opts ...WebhookOption) *WebhookServer
|
||||
NewWebhookServer constructs a WebhookServer with default buffer size \(64\). Use WithBufferSize to override.
|
||||
|
||||
<a name="WebhookServer.ListenAndServe"></a>
|
||||
### func \(\*WebhookServer\) ListenAndServe
|
||||
### func \(\*WebhookServer\) [ListenAndServe](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L168>)
|
||||
|
||||
```go
|
||||
func (w *WebhookServer) ListenAndServe(ctx context.Context, addr string) error
|
||||
@@ -203,7 +203,7 @@ func (w *WebhookServer) ListenAndServe(ctx context.Context, addr string) error
|
||||
ListenAndServe starts an HTTP server on addr and blocks until Stop is called \(which triggers Shutdown with the caller's context\) or the server returns an error other than http.ErrServerClosed. Callers must invoke Stop\(ctx\) to cleanly shut down the server; the ctx passed here is only used as the server's base context for incoming requests.
|
||||
|
||||
<a name="WebhookServer.Run"></a>
|
||||
### func \(\*WebhookServer\) Run
|
||||
### func \(\*WebhookServer\) [Run](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L90>)
|
||||
|
||||
```go
|
||||
func (w *WebhookServer) Run(ctx context.Context) error
|
||||
@@ -212,7 +212,7 @@ func (w *WebhookServer) Run(ctx context.Context) error
|
||||
Run implements Updater. It blocks until Stop is called or ctx is cancelled. If the server has not been started via ListenAndServe, Run only watches for shutdown — the user is expected to mount ServeHTTP on their own router.
|
||||
|
||||
<a name="WebhookServer.ServeHTTP"></a>
|
||||
### func \(\*WebhookServer\) ServeHTTP
|
||||
### func \(\*WebhookServer\) [ServeHTTP](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L116>)
|
||||
|
||||
```go
|
||||
func (w *WebhookServer) ServeHTTP(rw http.ResponseWriter, r *http.Request)
|
||||
@@ -221,7 +221,7 @@ func (w *WebhookServer) ServeHTTP(rw http.ResponseWriter, r *http.Request)
|
||||
ServeHTTP implements http.Handler. Telegram POSTs each update as JSON to this endpoint. Non\-POST requests get 405; bad bodies get 400; secret token mismatches get 401.
|
||||
|
||||
<a name="WebhookServer.Stop"></a>
|
||||
### func \(\*WebhookServer\) Stop
|
||||
### func \(\*WebhookServer\) [Stop](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L102>)
|
||||
|
||||
```go
|
||||
func (w *WebhookServer) Stop(ctx context.Context) error
|
||||
@@ -230,7 +230,7 @@ func (w *WebhookServer) Stop(ctx context.Context) error
|
||||
Stop implements Updater.
|
||||
|
||||
<a name="WebhookServer.Updates"></a>
|
||||
### func \(\*WebhookServer\) Updates
|
||||
### func \(\*WebhookServer\) [Updates](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L84>)
|
||||
|
||||
```go
|
||||
func (w *WebhookServer) Updates() <-chan api.Update
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -82,7 +82,9 @@ func TestHandleStart_SendsInitialKeyboard(t *testing.T) {
|
||||
|
||||
func callbackCtx(bot *client.Bot, q *api.CallbackQuery, groups []string) *dispatch.Context {
|
||||
upd := &api.Update{UpdateID: 1, CallbackQuery: q}
|
||||
return makeCtx(bot, upd, map[string]any{"regex_match": groups})
|
||||
c := makeCtx(bot, upd, nil)
|
||||
c.RegexMatch = groups
|
||||
return c
|
||||
}
|
||||
|
||||
func callbackQuery(data string, msgID int64, chatID int64) *api.CallbackQuery {
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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{
|
||||
|
||||
+220
-55
@@ -10529,7 +10529,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Scope type, must be default"
|
||||
"doc": "Scope type, must be default",
|
||||
"enum_values": [
|
||||
"default"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -10545,7 +10548,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Scope type, must be all_private_chats"
|
||||
"doc": "Scope type, must be all_private_chats",
|
||||
"enum_values": [
|
||||
"all_private_chats"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -10561,7 +10567,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Scope type, must be all_group_chats"
|
||||
"doc": "Scope type, must be all_group_chats",
|
||||
"enum_values": [
|
||||
"all_group_chats"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -10577,7 +10586,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Scope type, must be all_chat_administrators"
|
||||
"doc": "Scope type, must be all_chat_administrators",
|
||||
"enum_values": [
|
||||
"all_chat_administrators"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -10593,7 +10605,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Scope type, must be chat"
|
||||
"doc": "Scope type, must be chat",
|
||||
"enum_values": [
|
||||
"chat"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ChatID",
|
||||
@@ -10622,7 +10637,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Scope type, must be chat_administrators"
|
||||
"doc": "Scope type, must be chat_administrators",
|
||||
"enum_values": [
|
||||
"chat_administrators"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ChatID",
|
||||
@@ -10651,7 +10669,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Scope type, must be chat_member"
|
||||
"doc": "Scope type, must be chat_member",
|
||||
"enum_values": [
|
||||
"chat_member"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ChatID",
|
||||
@@ -10747,7 +10768,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the button, must be commands"
|
||||
"doc": "Type of the button, must be commands",
|
||||
"enum_values": [
|
||||
"commands"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -10763,7 +10787,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the button, must be web_app"
|
||||
"doc": "Type of the button, must be web_app",
|
||||
"enum_values": [
|
||||
"web_app"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Text",
|
||||
@@ -10799,7 +10826,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the button, must be default"
|
||||
"doc": "Type of the button, must be default",
|
||||
"enum_values": [
|
||||
"default"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -11451,7 +11481,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the result, must be animation"
|
||||
"doc": "Type of the result, must be animation",
|
||||
"enum_values": [
|
||||
"animation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Media",
|
||||
@@ -11566,7 +11599,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the result, must be audio"
|
||||
"doc": "Type of the result, must be audio",
|
||||
"enum_values": [
|
||||
"audio"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Media",
|
||||
@@ -11663,7 +11699,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the result, must be document"
|
||||
"doc": "Type of the result, must be document",
|
||||
"enum_values": [
|
||||
"document"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Media",
|
||||
@@ -11742,7 +11781,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the result, must be live_photo"
|
||||
"doc": "Type of the result, must be live_photo",
|
||||
"enum_values": [
|
||||
"live_photo"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Media",
|
||||
@@ -11831,7 +11873,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the result, must be location"
|
||||
"doc": "Type of the result, must be location",
|
||||
"enum_values": [
|
||||
"location"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Latitude",
|
||||
@@ -11876,7 +11921,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the result, must be photo"
|
||||
"doc": "Type of the result, must be photo",
|
||||
"enum_values": [
|
||||
"photo"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Media",
|
||||
@@ -11955,7 +12003,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the result, must be sticker"
|
||||
"doc": "Type of the result, must be sticker",
|
||||
"enum_values": [
|
||||
"sticker"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Media",
|
||||
@@ -11990,7 +12041,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the result, must be venue"
|
||||
"doc": "Type of the result, must be venue",
|
||||
"enum_values": [
|
||||
"venue"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Latitude",
|
||||
@@ -12082,7 +12136,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the result, must be video"
|
||||
"doc": "Type of the result, must be video",
|
||||
"enum_values": [
|
||||
"video"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Media",
|
||||
@@ -12237,7 +12294,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the media, must be live_photo"
|
||||
"doc": "Type of the media, must be live_photo",
|
||||
"enum_values": [
|
||||
"live_photo"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Media",
|
||||
@@ -12273,7 +12333,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the media, must be photo"
|
||||
"doc": "Type of the media, must be photo",
|
||||
"enum_values": [
|
||||
"photo"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Media",
|
||||
@@ -12299,7 +12362,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the media, must be video"
|
||||
"doc": "Type of the media, must be video",
|
||||
"enum_values": [
|
||||
"video"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Media",
|
||||
@@ -12396,7 +12462,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the profile photo, must be static"
|
||||
"doc": "Type of the profile photo, must be static",
|
||||
"enum_values": [
|
||||
"static"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Photo",
|
||||
@@ -12422,7 +12491,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the profile photo, must be animated"
|
||||
"doc": "Type of the profile photo, must be animated",
|
||||
"enum_values": [
|
||||
"animated"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Animation",
|
||||
@@ -12465,7 +12537,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the content, must be photo"
|
||||
"doc": "Type of the content, must be photo",
|
||||
"enum_values": [
|
||||
"photo"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Photo",
|
||||
@@ -12491,7 +12566,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the content, must be video"
|
||||
"doc": "Type of the content, must be video",
|
||||
"enum_values": [
|
||||
"video"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Video",
|
||||
@@ -13008,7 +13086,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the result, must be article"
|
||||
"doc": "Type of the result, must be article",
|
||||
"enum_values": [
|
||||
"article"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ID",
|
||||
@@ -13108,7 +13189,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the result, must be photo"
|
||||
"doc": "Type of the result, must be photo",
|
||||
"enum_values": [
|
||||
"photo"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ID",
|
||||
@@ -13252,7 +13336,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the result, must be gif"
|
||||
"doc": "Type of the result, must be gif",
|
||||
"enum_values": [
|
||||
"gif"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ID",
|
||||
@@ -13410,7 +13497,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the result, must be mpeg4_gif"
|
||||
"doc": "Type of the result, must be mpeg4_gif",
|
||||
"enum_values": [
|
||||
"mpeg4_gif"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ID",
|
||||
@@ -13568,7 +13658,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the result, must be video"
|
||||
"doc": "Type of the result, must be video",
|
||||
"enum_values": [
|
||||
"video"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ID",
|
||||
@@ -13732,7 +13825,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the result, must be audio"
|
||||
"doc": "Type of the result, must be audio",
|
||||
"enum_values": [
|
||||
"audio"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ID",
|
||||
@@ -13849,7 +13945,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the result, must be voice"
|
||||
"doc": "Type of the result, must be voice",
|
||||
"enum_values": [
|
||||
"voice"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ID",
|
||||
@@ -13957,7 +14056,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the result, must be document"
|
||||
"doc": "Type of the result, must be document",
|
||||
"enum_values": [
|
||||
"document"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ID",
|
||||
@@ -14106,7 +14208,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the result, must be location"
|
||||
"doc": "Type of the result, must be location",
|
||||
"enum_values": [
|
||||
"location"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ID",
|
||||
@@ -14243,7 +14348,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the result, must be venue"
|
||||
"doc": "Type of the result, must be venue",
|
||||
"enum_values": [
|
||||
"venue"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ID",
|
||||
@@ -14390,7 +14498,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the result, must be contact"
|
||||
"doc": "Type of the result, must be contact",
|
||||
"enum_values": [
|
||||
"contact"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ID",
|
||||
@@ -14499,7 +14610,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the result, must be game"
|
||||
"doc": "Type of the result, must be game",
|
||||
"enum_values": [
|
||||
"game"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ID",
|
||||
@@ -14544,7 +14658,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the result, must be photo"
|
||||
"doc": "Type of the result, must be photo",
|
||||
"enum_values": [
|
||||
"photo"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ID",
|
||||
@@ -14660,7 +14777,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the result, must be gif"
|
||||
"doc": "Type of the result, must be gif",
|
||||
"enum_values": [
|
||||
"gif"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ID",
|
||||
@@ -14767,7 +14887,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the result, must be mpeg4_gif"
|
||||
"doc": "Type of the result, must be mpeg4_gif",
|
||||
"enum_values": [
|
||||
"mpeg4_gif"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ID",
|
||||
@@ -14874,7 +14997,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the result, must be sticker"
|
||||
"doc": "Type of the result, must be sticker",
|
||||
"enum_values": [
|
||||
"sticker"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ID",
|
||||
@@ -14928,7 +15054,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the result, must be document"
|
||||
"doc": "Type of the result, must be document",
|
||||
"enum_values": [
|
||||
"document"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ID",
|
||||
@@ -15036,7 +15165,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the result, must be video"
|
||||
"doc": "Type of the result, must be video",
|
||||
"enum_values": [
|
||||
"video"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ID",
|
||||
@@ -15153,7 +15285,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the result, must be voice"
|
||||
"doc": "Type of the result, must be voice",
|
||||
"enum_values": [
|
||||
"voice"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ID",
|
||||
@@ -15252,7 +15387,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Type of the result, must be audio"
|
||||
"doc": "Type of the result, must be audio",
|
||||
"enum_values": [
|
||||
"audio"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ID",
|
||||
@@ -17138,7 +17276,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Error source, must be data"
|
||||
"doc": "Error source, must be data",
|
||||
"enum_values": [
|
||||
"data"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Type",
|
||||
@@ -17202,7 +17343,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Error source, must be front_side"
|
||||
"doc": "Error source, must be front_side",
|
||||
"enum_values": [
|
||||
"front_side"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Type",
|
||||
@@ -17254,7 +17398,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Error source, must be reverse_side"
|
||||
"doc": "Error source, must be reverse_side",
|
||||
"enum_values": [
|
||||
"reverse_side"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Type",
|
||||
@@ -17304,7 +17451,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Error source, must be selfie"
|
||||
"doc": "Error source, must be selfie",
|
||||
"enum_values": [
|
||||
"selfie"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Type",
|
||||
@@ -17356,7 +17506,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Error source, must be file"
|
||||
"doc": "Error source, must be file",
|
||||
"enum_values": [
|
||||
"file"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Type",
|
||||
@@ -17409,7 +17562,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Error source, must be files"
|
||||
"doc": "Error source, must be files",
|
||||
"enum_values": [
|
||||
"files"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Type",
|
||||
@@ -17465,7 +17621,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Error source, must be translation_file"
|
||||
"doc": "Error source, must be translation_file",
|
||||
"enum_values": [
|
||||
"translation_file"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Type",
|
||||
@@ -17522,7 +17681,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Error source, must be translation_files"
|
||||
"doc": "Error source, must be translation_files",
|
||||
"enum_values": [
|
||||
"translation_files"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Type",
|
||||
@@ -17582,7 +17744,10 @@
|
||||
"name": "string"
|
||||
},
|
||||
"required": true,
|
||||
"doc": "Error source, must be unspecified"
|
||||
"doc": "Error source, must be unspecified",
|
||||
"enum_values": [
|
||||
"unspecified"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Type",
|
||||
|
||||
@@ -72,11 +72,7 @@ func (p *LongPoller) Run(ctx context.Context) error {
|
||||
params.Timeout = &to
|
||||
}
|
||||
if len(p.AllowedTypes) > 0 {
|
||||
allowed := make([]string, len(p.AllowedTypes))
|
||||
for i, t := range p.AllowedTypes {
|
||||
allowed[i] = string(t)
|
||||
}
|
||||
params.AllowedUpdates = allowed
|
||||
params.AllowedUpdates = p.AllowedTypes
|
||||
}
|
||||
ups, err := api.GetUpdates(ctx, p.Bot, params)
|
||||
if err != nil {
|
||||
|
||||
+33
-15
@@ -6,6 +6,7 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"errors"
|
||||
@@ -18,6 +19,24 @@ import (
|
||||
"github.com/lukaszraczylo/go-telegram/client"
|
||||
)
|
||||
|
||||
// webhookBufPool reuses *bytes.Buffer for incoming webhook bodies.
|
||||
// Webhook payloads are typically a single Telegram Update (commonly
|
||||
// 1-8 KiB), so a buffer that has grown once will satisfy most
|
||||
// subsequent requests with no additional allocation.
|
||||
var webhookBufPool = sync.Pool{New: func() any { return new(bytes.Buffer) }}
|
||||
|
||||
// maxWebhookBufCap caps the buffer size returned to webhookBufPool so
|
||||
// a rare oversized update doesn't permanently bloat the pool. Buffers
|
||||
// larger than this are dropped on the floor.
|
||||
const maxWebhookBufCap = 256 * 1024
|
||||
|
||||
func putWebhookBuf(buf *bytes.Buffer) {
|
||||
if buf.Cap() > maxWebhookBufCap {
|
||||
return
|
||||
}
|
||||
webhookBufPool.Put(buf)
|
||||
}
|
||||
|
||||
// WebhookServer implements Updater by exposing an http.Handler that
|
||||
// receives updates from Telegram. It can be mounted on the user's own
|
||||
// HTTP server (via ServeHTTP) or run standalone (via ListenAndServe).
|
||||
@@ -108,28 +127,27 @@ func (w *WebhookServer) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
w.handlers.Add(1)
|
||||
defer w.handlers.Done()
|
||||
|
||||
const maxBody = 1 << 20 // 1 MiB cap on body
|
||||
r.Body = http.MaxBytesReader(rw, r.Body, maxBody)
|
||||
defer func() { _ = r.Body.Close() }()
|
||||
|
||||
const max = 1 << 20 // 1 MiB cap on body
|
||||
buf := make([]byte, 0, 1024)
|
||||
tmp := make([]byte, 4096)
|
||||
for {
|
||||
n, err := r.Body.Read(tmp)
|
||||
if n > 0 {
|
||||
buf = append(buf, tmp[:n]...)
|
||||
if len(buf) > max {
|
||||
rw.WriteHeader(http.StatusRequestEntityTooLarge)
|
||||
return
|
||||
}
|
||||
}
|
||||
if errors.Is(err, http.ErrBodyReadAfterClose) || err != nil {
|
||||
break
|
||||
buf := webhookBufPool.Get().(*bytes.Buffer)
|
||||
buf.Reset()
|
||||
defer putWebhookBuf(buf)
|
||||
if _, err := buf.ReadFrom(r.Body); err != nil {
|
||||
var maxErr *http.MaxBytesError
|
||||
if errors.As(err, &maxErr) {
|
||||
rw.WriteHeader(http.StatusRequestEntityTooLarge)
|
||||
return
|
||||
}
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var u api.Update
|
||||
codec := w.Bot.Codec()
|
||||
if err := codec.Unmarshal(buf, &u); err != nil {
|
||||
if err := codec.Unmarshal(buf.Bytes(), &u); err != nil {
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/lukaszraczylo/go-telegram/client"
|
||||
)
|
||||
|
||||
const benchUpdateBody = `{"update_id":12345,"message":{"message_id":1,"date":1700000000,"chat":{"id":42,"type":"private"},"from":{"id":42,"is_bot":false,"first_name":"User"},"text":"hello world"}}`
|
||||
|
||||
func BenchmarkWebhook_ServeHTTP(b *testing.B) {
|
||||
w := NewWebhookServer(client.New("t"), WithBufferSize(1024))
|
||||
body := []byte(benchUpdateBody)
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-w.Updates():
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
defer close(done)
|
||||
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body))
|
||||
rec := httptest.NewRecorder()
|
||||
w.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
b.Fatalf("status %d", rec.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user