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 | |
|---|---|---|---|
| 5523ed2b06 | |||
| 6f9b29ea0c | |||
| bd80af240d | |||
| af180b75c5 | |||
| 29bf575cfd | |||
| 370c9c0802 | |||
| 6ab80c27e1 | |||
| 13ea7097e1 | |||
| f899cc2663 | |||
| 5a27b53f30 | |||
| 1ff26006f7 | |||
| f3fbef64ca | |||
| e4614d800f | |||
| 3c04d7b0b1 | |||
| 1da759ba8a | |||
| 1088b7f4d7 | |||
| 35058dd70b | |||
| 750a7f51f6 | |||
| b491829267 | |||
| ac7cae8fa7 |
@@ -134,7 +134,7 @@ jobs:
|
||||
- name: Regenerate against pinned snapshot
|
||||
run: make regen-from-fixture
|
||||
- name: Assert clean diff
|
||||
run: git diff --exit-code internal/spec/api.json api/
|
||||
run: git diff --exit-code internal/spec/api.json api/ docs/reference/
|
||||
|
||||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
name: pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- '.github/workflows/pages.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/configure-pages@v5
|
||||
- uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: docs/
|
||||
- id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
+10
-6
@@ -19,11 +19,12 @@ blacklist:
|
||||
- "Merge remote-tracking branch"
|
||||
- "go mod tidy"
|
||||
|
||||
# Strip the auto-generated bot-api-vX.Y tag prefix when scanning existing
|
||||
# tags — those are markers that point at library releases, not version
|
||||
# sources themselves.
|
||||
tag_prefixes:
|
||||
- "bot-api-"
|
||||
# NOTE: do not configure tag_prefixes here. The action's behaviour with
|
||||
# that option present is to FILTER tags down to those matching the
|
||||
# listed prefixes — meaning v0.x.y / v1.x.y tags get ignored and version
|
||||
# numbering restarts from zero. The bot-api-vX.Y tag is created by a
|
||||
# separate workflow step and does not need to participate in version
|
||||
# selection.
|
||||
|
||||
wording:
|
||||
patch:
|
||||
@@ -39,5 +40,8 @@ wording:
|
||||
minor:
|
||||
- "feat"
|
||||
major:
|
||||
- "breaking"
|
||||
# Match only the canonical Conventional Commits trailer. The bare
|
||||
# word "breaking" is too greedy under semver-generator's fuzzy match —
|
||||
# it triggers on substrings like "breaking-value drift" inside a
|
||||
# commit body and wrongly produces a major bump.
|
||||
- "BREAKING CHANGE"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.PHONY: test test-race lint vet integration regen snapshot regen-from-fixture test-update-golden clean clean-generated audit audit-drift help
|
||||
.PHONY: test test-race lint vet integration regen snapshot regen-from-fixture test-update-golden clean clean-generated audit audit-drift docs docs-check help
|
||||
|
||||
GO ?= go
|
||||
|
||||
@@ -14,6 +14,8 @@ help:
|
||||
@echo " test-update-golden - refresh golden test fixtures (Plan 2)"
|
||||
@echo " audit - report any-typed/bool fallbacks in current IR"
|
||||
@echo " audit-drift - audit + compare against HEAD's IR for signature changes"
|
||||
@echo " docs - regenerate markdown reference docs into docs/reference/"
|
||||
@echo " docs-check - assert docs/reference/ is up to date (CI gate)"
|
||||
@echo " clean-generated - delete generated api/*.gen.go and internal/spec/api.json"
|
||||
@echo " clean - clean-generated + transient artefacts (binaries, coverage)"
|
||||
|
||||
@@ -44,12 +46,14 @@ regen: clean-generated
|
||||
$(GO) run ./cmd/audit -ir $(SCRAPE_OUTPUT)
|
||||
$(GO) run ./cmd/genapi -input $(SCRAPE_OUTPUT) -outdir api
|
||||
$(GO) test ./api/...
|
||||
$(MAKE) docs
|
||||
|
||||
regen-from-fixture: clean-generated
|
||||
$(GO) run ./cmd/scrape -input $(SCRAPE_INPUT) -output $(SCRAPE_OUTPUT)
|
||||
$(GO) run ./cmd/audit -ir $(SCRAPE_OUTPUT)
|
||||
$(GO) run ./cmd/genapi -input $(SCRAPE_OUTPUT) -outdir api
|
||||
$(GO) test ./api/...
|
||||
$(MAKE) docs
|
||||
|
||||
audit:
|
||||
$(GO) run ./cmd/audit -ir $(SCRAPE_OUTPUT)
|
||||
@@ -61,6 +65,32 @@ test-update-golden:
|
||||
$(GO) test -run TestEmit -update ./cmd/genapi/...
|
||||
$(GO) test -run TestScrape -update ./cmd/scrape/...
|
||||
|
||||
# Regenerate godoc-style markdown reference docs into docs/reference/.
|
||||
# Auto-installs gomarkdoc on first run.
|
||||
DOC_PACKAGES := \
|
||||
./client \
|
||||
./transport \
|
||||
./dispatch \
|
||||
./dispatch/conversation \
|
||||
./dispatch/filters/message \
|
||||
./dispatch/filters/callback \
|
||||
./dispatch/filters/inline \
|
||||
./dispatch/filters/chatmember \
|
||||
./dispatch/filters/chatjoinrequest \
|
||||
./dispatch/filters/precheckoutquery \
|
||||
./api
|
||||
|
||||
docs:
|
||||
@which gomarkdoc > /dev/null || (echo "installing gomarkdoc..." && $(GO) install github.com/princjef/gomarkdoc/cmd/gomarkdoc@v1.1.0)
|
||||
gomarkdoc \
|
||||
--repository.url=https://github.com/lukaszraczylo/go-telegram \
|
||||
--repository.default-branch=main \
|
||||
--repository.path=/ \
|
||||
-o 'docs/reference/{{.Dir}}.md' $(DOC_PACKAGES)
|
||||
|
||||
docs-check: docs
|
||||
@git diff --exit-code docs/reference/ || (echo "docs/reference/ is stale — run 'make docs' and commit" && exit 1)
|
||||
|
||||
# clean-generated removes ONLY codegen output. Source code (cmd/scrape,
|
||||
# cmd/genapi, runtime helpers) is untouched. Run before regen to avoid
|
||||
# orphan files lingering when the IR shrinks (renamed/removed methods).
|
||||
|
||||
@@ -1,61 +1,71 @@
|
||||
# go-telegram
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="docs/logo-dark.svg">
|
||||
<img alt="go-telegram" src="docs/logo-light.svg" width="320">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
> A fully-generated, strongly-typed Go client for the Telegram Bot API — no `any`, no guessing.
|
||||
<p align="center">
|
||||
<strong>Build Telegram bots in Go that just work.</strong><br>
|
||||
Type-safe. Batteries included. Always up to date with the latest Bot API.
|
||||
</p>
|
||||
|
||||
[](https://github.com/lukaszraczylo/go-telegram/actions/workflows/ci.yml)
|
||||
[](https://pkg.go.dev/github.com/lukaszraczylo/go-telegram)
|
||||
[](go.mod)
|
||||
[](LICENSE)
|
||||
<p align="center">
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram/actions/workflows/ci.yml"><img src="https://github.com/lukaszraczylo/go-telegram/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
||||
<a href="https://pkg.go.dev/github.com/lukaszraczylo/go-telegram"><img src="https://pkg.go.dev/badge/github.com/lukaszraczylo/go-telegram.svg" alt="Go Reference"></a>
|
||||
<a href="go.mod"><img src="https://img.shields.io/github/go-mod/go-version/lukaszraczylo/go-telegram" alt="Go Version"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License: MIT"></a>
|
||||
</p>
|
||||
|
||||
> Bot API **v10.0** · 176 methods · 301 types · 1428 auto-generated tests
|
||||
<p align="center">
|
||||
Bot API <strong>v10.0</strong> · 176 methods · 301 types · 1428 auto-generated tests
|
||||
</p>
|
||||
|
||||
Most Telegram bot libraries expose Telegram's "Integer or String" fields as `interface{}` or `any`. Every union type in go-telegram is a real Go type with compile-time safety and auto-decoding. The entire API surface is code-generated from a committed HTML snapshot of the live Telegram docs — regenerating picks up new Bot API versions in one command, with a self-verifying pipeline that catches regressions before they ship.
|
||||
<p align="center">
|
||||
<a href="https://go-telegram.raczylo.com/">Website</a> ·
|
||||
<a href="docs/reference/">API Reference</a> ·
|
||||
<a href="examples/">Examples</a> ·
|
||||
<a href="https://pkg.go.dev/github.com/lukaszraczylo/go-telegram">pkg.go.dev</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## Hello, Telegram 👋
|
||||
|
||||
```go
|
||||
bot := client.New(os.Getenv("TELEGRAM_BOT_TOKEN"),
|
||||
client.WithHTTPClient(client.NewRetryDoer(client.NewDefaultHTTPDoer())),
|
||||
)
|
||||
|
||||
bot := client.New(os.Getenv("TELEGRAM_BOT_TOKEN"))
|
||||
router := dispatch.New(bot)
|
||||
|
||||
router.OnCommand("/start", func(c *dispatch.Context, m *api.Message) error {
|
||||
_, err := api.SendMessage(c.Ctx, c.Bot, &api.SendMessageParams{
|
||||
ChatID: api.ChatIDFromInt(m.Chat.ID),
|
||||
Text: "Hello! Send me anything to echo.",
|
||||
})
|
||||
return err
|
||||
})
|
||||
router.OnText(`.+`, func(c *dispatch.Context, m *api.Message) error {
|
||||
_, err := api.SendMessage(c.Ctx, c.Bot, &api.SendMessageParams{
|
||||
ChatID: api.ChatIDFromInt(m.Chat.ID),
|
||||
Text: m.Text,
|
||||
ReplyParameters: &api.ReplyParameters{MessageID: m.MessageID},
|
||||
Text: "Hi " + m.From.FirstName + "! 👋",
|
||||
})
|
||||
return err
|
||||
})
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
router.Run(ctx, transport.NewLongPoller(bot))
|
||||
```
|
||||
|
||||
## Why go-telegram
|
||||
That's a working bot. No magic strings, no `any`, no guessing what fields exist — your editor autocompletes everything.
|
||||
|
||||
| Feature | What it means for you |
|
||||
|---|---|
|
||||
| **Typed unions** | `ChatID`, `MessageOrBool`, `InputFile`, and 13 discriminated-union interfaces give you `switch v.(type)` instead of runtime panics |
|
||||
| **Full Bot API v10.0** | 176 methods and 301 types — all generated, none hand-written, nothing missing |
|
||||
| **Self-verifying codegen** | `make snapshot && make regen` regenerates everything and runs 1428 tests; any regression fails the pipeline |
|
||||
| **Pluggable transport + codec** | `HTTPDoer` and `Codec` are one-method interfaces — swap in fasthttp, sonic, or your test fake without forking |
|
||||
| **Retry middleware** | `RetryDoer` honours Telegram's `retry_after`, backs off on 5xx, replays request bodies |
|
||||
| **Composable dispatcher** | Per-update goroutine pool (default 50), filter combinators (`And`/`Or`/`Not`), conversation state machines, named handlers |
|
||||
## Why you'll like it
|
||||
|
||||
## Quickstart
|
||||
- 🎯 **No `any`, anywhere.** Telegram's "Integer or String" and "one of N types" unions are real Go types you can `switch` on.
|
||||
- 🔋 **Batteries included.** Long-poll, webhooks, retries on rate limits, conversation state machines, filters, handler groups — out of the box.
|
||||
- 🔄 **Always current.** The whole API is generated from Telegram's live docs. New Bot API release? `make regen` and you're done.
|
||||
- 🪶 **Pluggable everything.** Swap the HTTP client, JSON codec, or storage backend with a one-method interface. No forks.
|
||||
- 🧪 **Already tested.** 1428 generated tests cover every method × every failure mode (success, API errors, network failures, parse errors, timeouts, missing fields, forbidden, server errors).
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
go get github.com/lukaszraczylo/go-telegram
|
||||
```
|
||||
|
||||
Full echo bot — long-poll, graceful shutdown, retry on 429:
|
||||
## A complete echo bot
|
||||
|
||||
Long-poll, graceful shutdown, retries on Telegram's `429 retry_after`:
|
||||
|
||||
```go
|
||||
package main
|
||||
@@ -131,7 +141,30 @@ Run any example: `TELEGRAM_BOT_TOKEN=xxx go run ./examples/<name>`
|
||||
| | [`polls`](examples/polls) | `sendPoll` and answer tally |
|
||||
| | [`payments`](examples/payments) | Invoice → pre-checkout → success |
|
||||
|
||||
## Concepts
|
||||
## Optional fields
|
||||
|
||||
Telegram marks many fields as optional. For optional **scalars** (int, bool, float) we use pointers so you can explicitly send `false` or `0` when the wire format needs to override a chat default. The `api.Ptr` helper keeps that ergonomic:
|
||||
|
||||
```go
|
||||
api.SendMessage(ctx, bot, &api.SendMessageParams{
|
||||
ChatID: api.ChatIDFromInt(chatID),
|
||||
Text: "hi",
|
||||
DisableNotification: api.Ptr(true), // type inferred
|
||||
})
|
||||
|
||||
api.GetUserProfilePhotos(ctx, bot, &api.GetUserProfilePhotosParams{
|
||||
UserID: userID,
|
||||
Limit: api.Ptr[int64](5), // explicit type for untyped literals
|
||||
})
|
||||
```
|
||||
|
||||
Optional structs and slices are already nullable in Go — no helper needed.
|
||||
|
||||
## Reference docs
|
||||
|
||||
Full API reference is auto-generated from source comments and lives in [`docs/reference/`](docs/reference/README.md) — browse package by package on GitHub, or read it rendered at [go-telegram.raczylo.com](https://go-telegram.raczylo.com/) and [pkg.go.dev](https://pkg.go.dev/github.com/lukaszraczylo/go-telegram).
|
||||
|
||||
## How it works
|
||||
|
||||
<details>
|
||||
<summary>Bot client and pluggable transport</summary>
|
||||
@@ -284,25 +317,18 @@ r.OnCommand("/cmd", named.Handler())
|
||||
|
||||
</details>
|
||||
|
||||
## Codegen pipeline
|
||||
## Keeping up with Telegram
|
||||
|
||||
The full API surface in `api/*.gen.go` is generated from a committed HTML snapshot of `core.telegram.org/bots/api`:
|
||||
When Telegram ships a new Bot API version, regenerating the whole library is one command:
|
||||
|
||||
```bash
|
||||
make snapshot # fetch and commit latest HTML from core.telegram.org
|
||||
make regen # scrape → audit → emit Go code → run generated tests
|
||||
go test -race ./...
|
||||
make snapshot # grab the latest HTML from core.telegram.org
|
||||
make regen # scrape → audit → emit Go → run tests → regenerate docs
|
||||
```
|
||||
|
||||
`make regen` is self-verifying. The audit tool (`cmd/audit`) checks:
|
||||
The audit tool checks for `any`-typed escapes, surprise `bool` returns, and signature drift. CI runs it on every PR, and a weekly workflow opens an auto-PR with regenerated code so a new Bot API version never sits longer than a week.
|
||||
|
||||
- `any`-typed fields or returns that escaped the union machinery
|
||||
- Methods returning `bool` not on the approved list (`internal/spec/overrides.json`)
|
||||
- Signature drift vs HEAD's IR (added/removed/changed return types)
|
||||
|
||||
Exit codes: 0 clean · 1 fallback · 2 drift · 3 invalid. CI runs the audit on every PR. A weekly `regen.yml` workflow opens a PR with regenerated code and the audit summary in the body.
|
||||
|
||||
To track a new Bot API release: run `make snapshot && make regen`, review the audit output, update `internal/spec/overrides.json` for any newly unparseable methods, and submit a PR.
|
||||
If something in Telegram's docs trips up the scraper, add an override to `internal/spec/overrides.json`. The audit will tell you what to put there.
|
||||
|
||||
## Testing
|
||||
|
||||
|
||||
+310
-33
@@ -4,16 +4,42 @@
|
||||
|
||||
package api
|
||||
|
||||
// ParseMode controls how Telegram interprets formatting in message text.
|
||||
type ParseMode string
|
||||
type BackgroundFillType string
|
||||
|
||||
const (
|
||||
ParseModeMarkdown ParseMode = "Markdown" // legacy
|
||||
ParseModeMarkdownV2 ParseMode = "MarkdownV2"
|
||||
ParseModeHTML ParseMode = "HTML"
|
||||
BackgroundFillTypeSolid BackgroundFillType = "solid"
|
||||
BackgroundFillTypeGradient BackgroundFillType = "gradient"
|
||||
BackgroundFillTypeFreeformGradient BackgroundFillType = "freeform_gradient"
|
||||
)
|
||||
|
||||
type BackgroundTypeKind string
|
||||
|
||||
const (
|
||||
BackgroundTypeKindFill BackgroundTypeKind = "fill"
|
||||
BackgroundTypeKindWallpaper BackgroundTypeKind = "wallpaper"
|
||||
BackgroundTypeKindPattern BackgroundTypeKind = "pattern"
|
||||
BackgroundTypeKindChatTheme BackgroundTypeKind = "chat_theme"
|
||||
)
|
||||
|
||||
type ChatBoostSourceKind string
|
||||
|
||||
const (
|
||||
ChatBoostSourceKindPremium ChatBoostSourceKind = "premium"
|
||||
ChatBoostSourceKindGiftCode ChatBoostSourceKind = "gift_code"
|
||||
ChatBoostSourceKindGiveaway ChatBoostSourceKind = "giveaway"
|
||||
)
|
||||
|
||||
type ChatMemberStatus string
|
||||
|
||||
const (
|
||||
ChatMemberStatusCreator ChatMemberStatus = "creator"
|
||||
ChatMemberStatusAdministrator ChatMemberStatus = "administrator"
|
||||
ChatMemberStatusMember ChatMemberStatus = "member"
|
||||
ChatMemberStatusRestricted ChatMemberStatus = "restricted"
|
||||
ChatMemberStatusLeft ChatMemberStatus = "left"
|
||||
ChatMemberStatusKicked ChatMemberStatus = "kicked"
|
||||
)
|
||||
|
||||
// ChatType is the type of a Telegram chat.
|
||||
type ChatType string
|
||||
|
||||
const (
|
||||
@@ -23,38 +49,289 @@ const (
|
||||
ChatTypeChannel ChatType = "channel"
|
||||
)
|
||||
|
||||
// UpdateType identifies an Update payload variant. Used by allowed_updates
|
||||
// in getUpdates / setWebhook.
|
||||
type UpdateType string
|
||||
type EncryptedPassportElementType string
|
||||
|
||||
const (
|
||||
UpdateMessage UpdateType = "message"
|
||||
UpdateEditedMessage UpdateType = "edited_message"
|
||||
UpdateChannelPost UpdateType = "channel_post"
|
||||
UpdateEditedChannelPost UpdateType = "edited_channel_post"
|
||||
UpdateCallbackQuery UpdateType = "callback_query"
|
||||
UpdateInlineQuery UpdateType = "inline_query"
|
||||
EncryptedPassportElementTypePersonalDetails EncryptedPassportElementType = "personal_details"
|
||||
EncryptedPassportElementTypePassport EncryptedPassportElementType = "passport"
|
||||
EncryptedPassportElementTypeDriverLicense EncryptedPassportElementType = "driver_license"
|
||||
EncryptedPassportElementTypeIdentityCard EncryptedPassportElementType = "identity_card"
|
||||
EncryptedPassportElementTypeInternalPassport EncryptedPassportElementType = "internal_passport"
|
||||
EncryptedPassportElementTypeAddress EncryptedPassportElementType = "address"
|
||||
EncryptedPassportElementTypeUtilityBill EncryptedPassportElementType = "utility_bill"
|
||||
EncryptedPassportElementTypeBankStatement EncryptedPassportElementType = "bank_statement"
|
||||
EncryptedPassportElementTypeRentalAgreement EncryptedPassportElementType = "rental_agreement"
|
||||
EncryptedPassportElementTypePassportRegistration EncryptedPassportElementType = "passport_registration"
|
||||
EncryptedPassportElementTypeTemporaryRegistration EncryptedPassportElementType = "temporary_registration"
|
||||
EncryptedPassportElementTypePhoneNumber EncryptedPassportElementType = "phone_number"
|
||||
EncryptedPassportElementTypeEmail EncryptedPassportElementType = "email"
|
||||
)
|
||||
|
||||
type InlineQueryChatType string
|
||||
|
||||
const (
|
||||
InlineQueryChatTypeSender InlineQueryChatType = "sender"
|
||||
InlineQueryChatTypePrivate InlineQueryChatType = "private"
|
||||
InlineQueryChatTypeGroup InlineQueryChatType = "group"
|
||||
InlineQueryChatTypeSupergroup InlineQueryChatType = "supergroup"
|
||||
InlineQueryChatTypeChannel InlineQueryChatType = "channel"
|
||||
)
|
||||
|
||||
type InlineQueryResultDocumentMimeType string
|
||||
|
||||
const (
|
||||
InlineQueryResultDocumentMimeTypeApplicationOfPdf InlineQueryResultDocumentMimeType = "application/pdf"
|
||||
InlineQueryResultDocumentMimeTypeApplicationOfZip InlineQueryResultDocumentMimeType = "application/zip"
|
||||
)
|
||||
|
||||
type InlineQueryResultGifThumbnailMimeType string
|
||||
|
||||
const (
|
||||
InlineQueryResultGifThumbnailMimeTypeImageOfJpeg InlineQueryResultGifThumbnailMimeType = "image/jpeg"
|
||||
InlineQueryResultGifThumbnailMimeTypeImageOfGif InlineQueryResultGifThumbnailMimeType = "image/gif"
|
||||
InlineQueryResultGifThumbnailMimeTypeVideoOfMp4 InlineQueryResultGifThumbnailMimeType = "video/mp4"
|
||||
)
|
||||
|
||||
type InputStickerFormat string
|
||||
|
||||
const (
|
||||
InputStickerFormatStatic InputStickerFormat = "static"
|
||||
InputStickerFormatAnimated InputStickerFormat = "animated"
|
||||
InputStickerFormatVideo InputStickerFormat = "video"
|
||||
)
|
||||
|
||||
type KeyboardButtonStyle string
|
||||
|
||||
const (
|
||||
KeyboardButtonStyleDanger KeyboardButtonStyle = "danger"
|
||||
KeyboardButtonStyleSuccess KeyboardButtonStyle = "success"
|
||||
KeyboardButtonStylePrimary KeyboardButtonStyle = "primary"
|
||||
)
|
||||
|
||||
type MaskPositionPoint string
|
||||
|
||||
const (
|
||||
MaskPositionPointForehead MaskPositionPoint = "forehead"
|
||||
MaskPositionPointEyes MaskPositionPoint = "eyes"
|
||||
MaskPositionPointMouth MaskPositionPoint = "mouth"
|
||||
MaskPositionPointChin MaskPositionPoint = "chin"
|
||||
)
|
||||
|
||||
// MessageEntityType is the kind of an entity (mention, hashtag, command, ...).
|
||||
type MessageEntityType string
|
||||
|
||||
const (
|
||||
EntityMention MessageEntityType = "mention"
|
||||
EntityHashtag MessageEntityType = "hashtag"
|
||||
EntityCashtag MessageEntityType = "cashtag"
|
||||
EntityBotCommand MessageEntityType = "bot_command"
|
||||
EntityURL MessageEntityType = "url"
|
||||
EntityEmail MessageEntityType = "email"
|
||||
EntityPhoneNumber MessageEntityType = "phone_number"
|
||||
EntityBold MessageEntityType = "bold"
|
||||
EntityItalic MessageEntityType = "italic"
|
||||
EntityUnderline MessageEntityType = "underline"
|
||||
EntityStrike MessageEntityType = "strikethrough"
|
||||
EntitySpoiler MessageEntityType = "spoiler"
|
||||
EntityCode MessageEntityType = "code"
|
||||
EntityPre MessageEntityType = "pre"
|
||||
EntityTextLink MessageEntityType = "text_link"
|
||||
EntityTextMention MessageEntityType = "text_mention"
|
||||
EntityCustomEmoji MessageEntityType = "custom_emoji"
|
||||
MessageEntityTypeMention MessageEntityType = "mention"
|
||||
MessageEntityTypeHashtag MessageEntityType = "hashtag"
|
||||
MessageEntityTypeCashtag MessageEntityType = "cashtag"
|
||||
MessageEntityTypeBotCommand MessageEntityType = "bot_command"
|
||||
MessageEntityTypeURL MessageEntityType = "url"
|
||||
MessageEntityTypeEmail MessageEntityType = "email"
|
||||
MessageEntityTypePhoneNumber MessageEntityType = "phone_number"
|
||||
MessageEntityTypeBold MessageEntityType = "bold"
|
||||
MessageEntityTypeItalic MessageEntityType = "italic"
|
||||
MessageEntityTypeUnderline MessageEntityType = "underline"
|
||||
MessageEntityTypeStrikethrough MessageEntityType = "strikethrough"
|
||||
MessageEntityTypeSpoiler MessageEntityType = "spoiler"
|
||||
MessageEntityTypeBlockquote MessageEntityType = "blockquote"
|
||||
MessageEntityTypeExpandableBlockquote MessageEntityType = "expandable_blockquote"
|
||||
MessageEntityTypeCode MessageEntityType = "code"
|
||||
MessageEntityTypePre MessageEntityType = "pre"
|
||||
MessageEntityTypeTextLink MessageEntityType = "text_link"
|
||||
MessageEntityTypeTextMention MessageEntityType = "text_mention"
|
||||
MessageEntityTypeCustomEmoji MessageEntityType = "custom_emoji"
|
||||
MessageEntityTypeDateTime MessageEntityType = "date_time"
|
||||
)
|
||||
|
||||
type MessageOriginType string
|
||||
|
||||
const (
|
||||
MessageOriginTypeUser MessageOriginType = "user"
|
||||
MessageOriginTypeHiddenUser MessageOriginType = "hidden_user"
|
||||
MessageOriginTypeChat MessageOriginType = "chat"
|
||||
MessageOriginTypeChannel MessageOriginType = "channel"
|
||||
)
|
||||
|
||||
type OwnedGiftType string
|
||||
|
||||
const (
|
||||
OwnedGiftTypeRegular OwnedGiftType = "regular"
|
||||
OwnedGiftTypeUnique OwnedGiftType = "unique"
|
||||
)
|
||||
|
||||
type PaidMediaType string
|
||||
|
||||
const (
|
||||
PaidMediaTypeLivePhoto PaidMediaType = "live_photo"
|
||||
PaidMediaTypePhoto PaidMediaType = "photo"
|
||||
PaidMediaTypePreview PaidMediaType = "preview"
|
||||
PaidMediaTypeVideo PaidMediaType = "video"
|
||||
)
|
||||
|
||||
type ParseMode string
|
||||
|
||||
const (
|
||||
ParseModeMarkdown ParseMode = "Markdown"
|
||||
ParseModeMarkdownV2 ParseMode = "MarkdownV2"
|
||||
ParseModeHTML ParseMode = "HTML"
|
||||
)
|
||||
|
||||
type PassportElementErrorDataFieldType string
|
||||
|
||||
const (
|
||||
PassportElementErrorDataFieldTypePersonalDetails PassportElementErrorDataFieldType = "personal_details"
|
||||
PassportElementErrorDataFieldTypePassport PassportElementErrorDataFieldType = "passport"
|
||||
PassportElementErrorDataFieldTypeDriverLicense PassportElementErrorDataFieldType = "driver_license"
|
||||
PassportElementErrorDataFieldTypeIdentityCard PassportElementErrorDataFieldType = "identity_card"
|
||||
PassportElementErrorDataFieldTypeInternalPassport PassportElementErrorDataFieldType = "internal_passport"
|
||||
PassportElementErrorDataFieldTypeAddress PassportElementErrorDataFieldType = "address"
|
||||
)
|
||||
|
||||
type PassportElementErrorFileType string
|
||||
|
||||
const (
|
||||
PassportElementErrorFileTypeUtilityBill PassportElementErrorFileType = "utility_bill"
|
||||
PassportElementErrorFileTypeBankStatement PassportElementErrorFileType = "bank_statement"
|
||||
PassportElementErrorFileTypeRentalAgreement PassportElementErrorFileType = "rental_agreement"
|
||||
PassportElementErrorFileTypePassportRegistration PassportElementErrorFileType = "passport_registration"
|
||||
PassportElementErrorFileTypeTemporaryRegistration PassportElementErrorFileType = "temporary_registration"
|
||||
)
|
||||
|
||||
type PassportElementErrorReverseSideType string
|
||||
|
||||
const (
|
||||
PassportElementErrorReverseSideTypeDriverLicense PassportElementErrorReverseSideType = "driver_license"
|
||||
PassportElementErrorReverseSideTypeIdentityCard PassportElementErrorReverseSideType = "identity_card"
|
||||
)
|
||||
|
||||
type PassportElementErrorSelfieType string
|
||||
|
||||
const (
|
||||
PassportElementErrorSelfieTypePassport PassportElementErrorSelfieType = "passport"
|
||||
PassportElementErrorSelfieTypeDriverLicense PassportElementErrorSelfieType = "driver_license"
|
||||
PassportElementErrorSelfieTypeIdentityCard PassportElementErrorSelfieType = "identity_card"
|
||||
PassportElementErrorSelfieTypeInternalPassport PassportElementErrorSelfieType = "internal_passport"
|
||||
)
|
||||
|
||||
type PassportElementErrorTranslationFileType string
|
||||
|
||||
const (
|
||||
PassportElementErrorTranslationFileTypePassport PassportElementErrorTranslationFileType = "passport"
|
||||
PassportElementErrorTranslationFileTypeDriverLicense PassportElementErrorTranslationFileType = "driver_license"
|
||||
PassportElementErrorTranslationFileTypeIdentityCard PassportElementErrorTranslationFileType = "identity_card"
|
||||
PassportElementErrorTranslationFileTypeInternalPassport PassportElementErrorTranslationFileType = "internal_passport"
|
||||
PassportElementErrorTranslationFileTypeUtilityBill PassportElementErrorTranslationFileType = "utility_bill"
|
||||
PassportElementErrorTranslationFileTypeBankStatement PassportElementErrorTranslationFileType = "bank_statement"
|
||||
PassportElementErrorTranslationFileTypeRentalAgreement PassportElementErrorTranslationFileType = "rental_agreement"
|
||||
PassportElementErrorTranslationFileTypePassportRegistration PassportElementErrorTranslationFileType = "passport_registration"
|
||||
PassportElementErrorTranslationFileTypeTemporaryRegistration PassportElementErrorTranslationFileType = "temporary_registration"
|
||||
)
|
||||
|
||||
type PollType string
|
||||
|
||||
const (
|
||||
PollTypeRegular PollType = "regular"
|
||||
PollTypeQuiz PollType = "quiz"
|
||||
)
|
||||
|
||||
type ReactionTypeKind string
|
||||
|
||||
const (
|
||||
ReactionTypeKindEmoji ReactionTypeKind = "emoji"
|
||||
ReactionTypeKindCustomEmoji ReactionTypeKind = "custom_emoji"
|
||||
ReactionTypeKindPaid ReactionTypeKind = "paid"
|
||||
)
|
||||
|
||||
type RefundedPaymentCurrency string
|
||||
|
||||
const (
|
||||
RefundedPaymentCurrencyXTR RefundedPaymentCurrency = "XTR"
|
||||
)
|
||||
|
||||
type RevenueWithdrawalStateKind string
|
||||
|
||||
const (
|
||||
RevenueWithdrawalStateKindPending RevenueWithdrawalStateKind = "pending"
|
||||
RevenueWithdrawalStateKindSucceeded RevenueWithdrawalStateKind = "succeeded"
|
||||
RevenueWithdrawalStateKindFailed RevenueWithdrawalStateKind = "failed"
|
||||
)
|
||||
|
||||
type StickerType string
|
||||
|
||||
const (
|
||||
StickerTypeRegular StickerType = "regular"
|
||||
StickerTypeMask StickerType = "mask"
|
||||
StickerTypeCustomEmoji StickerType = "custom_emoji"
|
||||
)
|
||||
|
||||
type StoryAreaTypeKind string
|
||||
|
||||
const (
|
||||
StoryAreaTypeKindLocation StoryAreaTypeKind = "location"
|
||||
StoryAreaTypeKindSuggestedReaction StoryAreaTypeKind = "suggested_reaction"
|
||||
StoryAreaTypeKindLink StoryAreaTypeKind = "link"
|
||||
StoryAreaTypeKindWeather StoryAreaTypeKind = "weather"
|
||||
StoryAreaTypeKindUniqueGift StoryAreaTypeKind = "unique_gift"
|
||||
)
|
||||
|
||||
type SuggestedPostInfoState string
|
||||
|
||||
const (
|
||||
SuggestedPostInfoStatePending SuggestedPostInfoState = "pending"
|
||||
SuggestedPostInfoStateApproved SuggestedPostInfoState = "approved"
|
||||
SuggestedPostInfoStateDeclined SuggestedPostInfoState = "declined"
|
||||
)
|
||||
|
||||
type SuggestedPostPaidCurrency string
|
||||
|
||||
const (
|
||||
SuggestedPostPaidCurrencyXTR SuggestedPostPaidCurrency = "XTR"
|
||||
SuggestedPostPaidCurrencyTON SuggestedPostPaidCurrency = "TON"
|
||||
)
|
||||
|
||||
type SuggestedPostRefundedReason string
|
||||
|
||||
const (
|
||||
SuggestedPostRefundedReasonPostDeleted SuggestedPostRefundedReason = "post_deleted"
|
||||
SuggestedPostRefundedReasonPaymentRefunded SuggestedPostRefundedReason = "payment_refunded"
|
||||
)
|
||||
|
||||
type TransactionPartnerType string
|
||||
|
||||
const (
|
||||
TransactionPartnerTypeUser TransactionPartnerType = "user"
|
||||
TransactionPartnerTypeChat TransactionPartnerType = "chat"
|
||||
TransactionPartnerTypeAffiliateProgram TransactionPartnerType = "affiliate_program"
|
||||
TransactionPartnerTypeFragment TransactionPartnerType = "fragment"
|
||||
TransactionPartnerTypeTelegramAds TransactionPartnerType = "telegram_ads"
|
||||
TransactionPartnerTypeTelegramApi TransactionPartnerType = "telegram_api"
|
||||
TransactionPartnerTypeOther TransactionPartnerType = "other"
|
||||
)
|
||||
|
||||
type TransactionPartnerUserTransactionType string
|
||||
|
||||
const (
|
||||
TransactionPartnerUserTransactionTypeInvoicePayment TransactionPartnerUserTransactionType = "invoice_payment"
|
||||
TransactionPartnerUserTransactionTypePaidMediaPayment TransactionPartnerUserTransactionType = "paid_media_payment"
|
||||
TransactionPartnerUserTransactionTypeGiftPurchase TransactionPartnerUserTransactionType = "gift_purchase"
|
||||
TransactionPartnerUserTransactionTypePremiumPurchase TransactionPartnerUserTransactionType = "premium_purchase"
|
||||
TransactionPartnerUserTransactionTypeBusinessAccountTransfer TransactionPartnerUserTransactionType = "business_account_transfer"
|
||||
)
|
||||
|
||||
type UniqueGiftInfoOrigin string
|
||||
|
||||
const (
|
||||
UniqueGiftInfoOriginUpgrade UniqueGiftInfoOrigin = "upgrade"
|
||||
UniqueGiftInfoOriginTransfer UniqueGiftInfoOrigin = "transfer"
|
||||
UniqueGiftInfoOriginResale UniqueGiftInfoOrigin = "resale"
|
||||
UniqueGiftInfoOriginGiftedUpgrade UniqueGiftInfoOrigin = "gifted_upgrade"
|
||||
UniqueGiftInfoOriginOffer UniqueGiftInfoOrigin = "offer"
|
||||
)
|
||||
|
||||
type UniqueGiftModelRarity string
|
||||
|
||||
const (
|
||||
UniqueGiftModelRarityUncommon UniqueGiftModelRarity = "uncommon"
|
||||
UniqueGiftModelRarityRare UniqueGiftModelRarity = "rare"
|
||||
UniqueGiftModelRarityEpic UniqueGiftModelRarity = "epic"
|
||||
UniqueGiftModelRarityLegendary UniqueGiftModelRarity = "legendary"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package api
|
||||
|
||||
// UpdateType identifies an Update payload variant. Used by allowed_updates
|
||||
// in getUpdates / setWebhook. The Telegram docs do not enumerate these
|
||||
// values inline (they are derived from the optional fields of Update),
|
||||
// so the codegen pipeline cannot synthesise this enum and it lives here
|
||||
// as a hand-curated companion to the generated enums.gen.go.
|
||||
type UpdateType string
|
||||
|
||||
const (
|
||||
UpdateMessage UpdateType = "message"
|
||||
UpdateEditedMessage UpdateType = "edited_message"
|
||||
UpdateChannelPost UpdateType = "channel_post"
|
||||
UpdateEditedChannelPost UpdateType = "edited_channel_post"
|
||||
UpdateBusinessConnection UpdateType = "business_connection"
|
||||
UpdateBusinessMessage UpdateType = "business_message"
|
||||
UpdateEditedBusinessMessage UpdateType = "edited_business_message"
|
||||
UpdateDeletedBusinessMessages UpdateType = "deleted_business_messages"
|
||||
UpdateMessageReaction UpdateType = "message_reaction"
|
||||
UpdateMessageReactionCount UpdateType = "message_reaction_count"
|
||||
UpdateInlineQuery UpdateType = "inline_query"
|
||||
UpdateChosenInlineResult UpdateType = "chosen_inline_result"
|
||||
UpdateCallbackQuery UpdateType = "callback_query"
|
||||
UpdateShippingQuery UpdateType = "shipping_query"
|
||||
UpdatePreCheckoutQuery UpdateType = "pre_checkout_query"
|
||||
UpdatePurchasedPaidMedia UpdateType = "purchased_paid_media"
|
||||
UpdatePoll UpdateType = "poll"
|
||||
UpdatePollAnswer UpdateType = "poll_answer"
|
||||
UpdateMyChatMember UpdateType = "my_chat_member"
|
||||
UpdateChatMember UpdateType = "chat_member"
|
||||
UpdateChatJoinRequest UpdateType = "chat_join_request"
|
||||
UpdateChatBoost UpdateType = "chat_boost"
|
||||
UpdateRemovedChatBoost UpdateType = "removed_chat_boost"
|
||||
)
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
+54
-38
@@ -198,7 +198,7 @@ type SendMessageParams struct {
|
||||
// Text of the message to be sent, 1-4096 characters after entities parsing
|
||||
Text string `json:"text"`
|
||||
// Mode for parsing entities in the message text. See formatting options for more details.
|
||||
ParseMode string `json:"parse_mode,omitempty"`
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in message text, which can be specified instead of parse_mode
|
||||
Entities []MessageEntity `json:"entities,omitempty"`
|
||||
// Link preview generation options for the message
|
||||
@@ -305,7 +305,7 @@ type CopyMessageParams struct {
|
||||
// New caption for media, 0-1024 characters after entities parsing. If not specified, the original caption is kept
|
||||
Caption string `json:"caption,omitempty"`
|
||||
// Mode for parsing entities in the new caption. See formatting options for more details.
|
||||
ParseMode string `json:"parse_mode,omitempty"`
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the new caption, which can be specified instead of parse_mode
|
||||
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
|
||||
// Pass True, if the caption must be shown above the message media. Ignored if a new caption isn't specified.
|
||||
@@ -379,7 +379,7 @@ type SendPhotoParams struct {
|
||||
// Photo caption (may also be used when resending photos by file_id), 0-1024 characters after entities parsing
|
||||
Caption string `json:"caption,omitempty"`
|
||||
// Mode for parsing entities in the photo caption. See formatting options for more details.
|
||||
ParseMode string `json:"parse_mode,omitempty"`
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the caption, which can be specified instead of parse_mode
|
||||
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
|
||||
// Pass True, if the caption must be shown above the message media
|
||||
@@ -427,7 +427,7 @@ func (p *SendPhotoParams) MultipartFields() map[string]string {
|
||||
out["caption"] = p.Caption
|
||||
}
|
||||
if p.ParseMode != "" {
|
||||
out["parse_mode"] = p.ParseMode
|
||||
out["parse_mode"] = string(p.ParseMode)
|
||||
}
|
||||
if p.CaptionEntities != nil {
|
||||
if b, _ := json.Marshal(p.CaptionEntities); len(b) > 0 {
|
||||
@@ -509,7 +509,7 @@ type SendLivePhotoParams struct {
|
||||
// Video caption (may also be used when resending videos by file_id), 0-1024 characters after entities parsing
|
||||
Caption string `json:"caption,omitempty"`
|
||||
// Mode for parsing entities in the video caption. See formatting options for more details.
|
||||
ParseMode string `json:"parse_mode,omitempty"`
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the caption, which can be specified instead of parse_mode
|
||||
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
|
||||
// Pass True, if the caption must be shown above the message media
|
||||
@@ -560,7 +560,7 @@ func (p *SendLivePhotoParams) MultipartFields() map[string]string {
|
||||
out["caption"] = p.Caption
|
||||
}
|
||||
if p.ParseMode != "" {
|
||||
out["parse_mode"] = p.ParseMode
|
||||
out["parse_mode"] = string(p.ParseMode)
|
||||
}
|
||||
if p.CaptionEntities != nil {
|
||||
if b, _ := json.Marshal(p.CaptionEntities); len(b) > 0 {
|
||||
@@ -648,7 +648,7 @@ type SendAudioParams struct {
|
||||
// Audio caption, 0-1024 characters after entities parsing
|
||||
Caption string `json:"caption,omitempty"`
|
||||
// Mode for parsing entities in the audio caption. See formatting options for more details.
|
||||
ParseMode string `json:"parse_mode,omitempty"`
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the caption, which can be specified instead of parse_mode
|
||||
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
|
||||
// Duration of the audio in seconds
|
||||
@@ -703,7 +703,7 @@ func (p *SendAudioParams) MultipartFields() map[string]string {
|
||||
out["caption"] = p.Caption
|
||||
}
|
||||
if p.ParseMode != "" {
|
||||
out["parse_mode"] = p.ParseMode
|
||||
out["parse_mode"] = string(p.ParseMode)
|
||||
}
|
||||
if p.CaptionEntities != nil {
|
||||
if b, _ := json.Marshal(p.CaptionEntities); len(b) > 0 {
|
||||
@@ -796,7 +796,7 @@ type SendDocumentParams struct {
|
||||
// Document caption (may also be used when resending documents by file_id), 0-1024 characters after entities parsing
|
||||
Caption string `json:"caption,omitempty"`
|
||||
// Mode for parsing entities in the document caption. See formatting options for more details.
|
||||
ParseMode string `json:"parse_mode,omitempty"`
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the caption, which can be specified instead of parse_mode
|
||||
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
|
||||
// Disables automatic server-side content type detection for files uploaded using multipart/form-data
|
||||
@@ -845,7 +845,7 @@ func (p *SendDocumentParams) MultipartFields() map[string]string {
|
||||
out["caption"] = p.Caption
|
||||
}
|
||||
if p.ParseMode != "" {
|
||||
out["parse_mode"] = p.ParseMode
|
||||
out["parse_mode"] = string(p.ParseMode)
|
||||
}
|
||||
if p.CaptionEntities != nil {
|
||||
if b, _ := json.Marshal(p.CaptionEntities); len(b) > 0 {
|
||||
@@ -941,7 +941,7 @@ type SendVideoParams struct {
|
||||
// Video caption (may also be used when resending videos by file_id), 0-1024 characters after entities parsing
|
||||
Caption string `json:"caption,omitempty"`
|
||||
// Mode for parsing entities in the video caption. See formatting options for more details.
|
||||
ParseMode string `json:"parse_mode,omitempty"`
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the caption, which can be specified instead of parse_mode
|
||||
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
|
||||
// Pass True, if the caption must be shown above the message media
|
||||
@@ -1009,7 +1009,7 @@ func (p *SendVideoParams) MultipartFields() map[string]string {
|
||||
out["caption"] = p.Caption
|
||||
}
|
||||
if p.ParseMode != "" {
|
||||
out["parse_mode"] = p.ParseMode
|
||||
out["parse_mode"] = string(p.ParseMode)
|
||||
}
|
||||
if p.CaptionEntities != nil {
|
||||
if b, _ := json.Marshal(p.CaptionEntities); len(b) > 0 {
|
||||
@@ -1114,7 +1114,7 @@ type SendAnimationParams struct {
|
||||
// Animation caption (may also be used when resending animation by file_id), 0-1024 characters after entities parsing
|
||||
Caption string `json:"caption,omitempty"`
|
||||
// Mode for parsing entities in the animation caption. See formatting options for more details.
|
||||
ParseMode string `json:"parse_mode,omitempty"`
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the caption, which can be specified instead of parse_mode
|
||||
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
|
||||
// Pass True, if the caption must be shown above the message media
|
||||
@@ -1174,7 +1174,7 @@ func (p *SendAnimationParams) MultipartFields() map[string]string {
|
||||
out["caption"] = p.Caption
|
||||
}
|
||||
if p.ParseMode != "" {
|
||||
out["parse_mode"] = p.ParseMode
|
||||
out["parse_mode"] = string(p.ParseMode)
|
||||
}
|
||||
if p.CaptionEntities != nil {
|
||||
if b, _ := json.Marshal(p.CaptionEntities); len(b) > 0 {
|
||||
@@ -1261,7 +1261,7 @@ type SendVoiceParams struct {
|
||||
// Voice message caption, 0-1024 characters after entities parsing
|
||||
Caption string `json:"caption,omitempty"`
|
||||
// Mode for parsing entities in the voice message caption. See formatting options for more details.
|
||||
ParseMode string `json:"parse_mode,omitempty"`
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the caption, which can be specified instead of parse_mode
|
||||
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
|
||||
// Duration of the voice message in seconds
|
||||
@@ -1307,7 +1307,7 @@ func (p *SendVoiceParams) MultipartFields() map[string]string {
|
||||
out["caption"] = p.Caption
|
||||
}
|
||||
if p.ParseMode != "" {
|
||||
out["parse_mode"] = p.ParseMode
|
||||
out["parse_mode"] = string(p.ParseMode)
|
||||
}
|
||||
if p.CaptionEntities != nil {
|
||||
if b, _ := json.Marshal(p.CaptionEntities); len(b) > 0 {
|
||||
@@ -1511,7 +1511,7 @@ type SendPaidMediaParams struct {
|
||||
// Media caption, 0-1024 characters after entities parsing
|
||||
Caption string `json:"caption,omitempty"`
|
||||
// Mode for parsing entities in the media caption. See formatting options for more details.
|
||||
ParseMode string `json:"parse_mode,omitempty"`
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the caption, which can be specified instead of parse_mode
|
||||
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
|
||||
// Pass True, if the caption must be shown above the message media
|
||||
@@ -1559,7 +1559,7 @@ func (p *SendPaidMediaParams) MultipartFields() map[string]string {
|
||||
out["caption"] = p.Caption
|
||||
}
|
||||
if p.ParseMode != "" {
|
||||
out["parse_mode"] = p.ParseMode
|
||||
out["parse_mode"] = string(p.ParseMode)
|
||||
}
|
||||
if p.CaptionEntities != nil {
|
||||
if b, _ := json.Marshal(p.CaptionEntities); len(b) > 0 {
|
||||
@@ -1843,7 +1843,7 @@ type SendPollParams struct {
|
||||
// Poll question, 1-300 characters
|
||||
Question string `json:"question"`
|
||||
// Mode for parsing entities in the question. See formatting options for more details. Currently, only custom emoji entities are allowed
|
||||
QuestionParseMode string `json:"question_parse_mode,omitempty"`
|
||||
QuestionParseMode ParseMode `json:"question_parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the poll question. It can be specified instead of question_parse_mode
|
||||
QuestionEntities []MessageEntity `json:"question_entities,omitempty"`
|
||||
// A JSON-serialized list of 1-12 answer options
|
||||
@@ -1871,11 +1871,11 @@ type SendPollParams struct {
|
||||
// Text that is shown when a user chooses an incorrect answer or taps on the lamp icon in a quiz-style poll, 0-200 characters with at most 2 line feeds after entities parsing
|
||||
Explanation string `json:"explanation,omitempty"`
|
||||
// Mode for parsing entities in the explanation. See formatting options for more details.
|
||||
ExplanationParseMode string `json:"explanation_parse_mode,omitempty"`
|
||||
ExplanationParseMode ParseMode `json:"explanation_parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the poll explanation. It can be specified instead of explanation_parse_mode
|
||||
ExplanationEntities []MessageEntity `json:"explanation_entities,omitempty"`
|
||||
// Media added to the quiz explanation
|
||||
ExplanationMedia *InputPollMedia `json:"explanation_media,omitempty"`
|
||||
ExplanationMedia InputPollMedia `json:"explanation_media,omitempty"`
|
||||
// Amount of time in seconds the poll will be active after creation, 5-2628000. Can't be used together with close_date.
|
||||
OpenPeriod *int64 `json:"open_period,omitempty"`
|
||||
// Point in time (Unix timestamp) when the poll will be automatically closed. Must be at least 5 and no more than 2628000 seconds in the future. Can't be used together with open_period.
|
||||
@@ -1885,11 +1885,11 @@ type SendPollParams struct {
|
||||
// Description of the poll to be sent, 0-1024 characters after entities parsing
|
||||
Description string `json:"description,omitempty"`
|
||||
// Mode for parsing entities in the poll description. See formatting options for more details.
|
||||
DescriptionParseMode string `json:"description_parse_mode,omitempty"`
|
||||
DescriptionParseMode ParseMode `json:"description_parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the poll description, which can be specified instead of description_parse_mode
|
||||
DescriptionEntities []MessageEntity `json:"description_entities,omitempty"`
|
||||
// Media added to the poll description
|
||||
Media *InputPollMedia `json:"media,omitempty"`
|
||||
Media InputPollMedia `json:"media,omitempty"`
|
||||
// Sends the message silently. Users will receive a notification with no sound.
|
||||
DisableNotification *bool `json:"disable_notification,omitempty"`
|
||||
// Protects the contents of the sent message from forwarding and saving
|
||||
@@ -1990,7 +1990,7 @@ type SendMessageDraftParams struct {
|
||||
// Text of the message to be sent, 0-4096 characters after entities parsing. Pass an empty text to show a “Thinking…” placeholder.
|
||||
Text string `json:"text,omitempty"`
|
||||
// Mode for parsing entities in the message text. See formatting options for more details.
|
||||
ParseMode string `json:"parse_mode,omitempty"`
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in message text, which can be specified instead of parse_mode
|
||||
Entities []MessageEntity `json:"entities,omitempty"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -3390,7 +3406,7 @@ type SendGiftParams struct {
|
||||
// Text that will be shown along with the gift; 0-128 characters
|
||||
Text string `json:"text,omitempty"`
|
||||
// Mode for parsing entities in the text. See formatting options for more details. Entities other than “bold”, “italic”, “underline”, “strikethrough”, “spoiler”, “custom_emoji”, and “date_time” are ignored.
|
||||
TextParseMode string `json:"text_parse_mode,omitempty"`
|
||||
TextParseMode ParseMode `json:"text_parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the gift text. It can be specified instead of text_parse_mode. Entities other than “bold”, “italic”, “underline”, “strikethrough”, “spoiler”, “custom_emoji”, and “date_time” are ignored.
|
||||
TextEntities []MessageEntity `json:"text_entities,omitempty"`
|
||||
}
|
||||
@@ -3415,7 +3431,7 @@ type GiftPremiumSubscriptionParams struct {
|
||||
// Text that will be shown along with the service message about the subscription; 0-128 characters
|
||||
Text string `json:"text,omitempty"`
|
||||
// Mode for parsing entities in the text. See formatting options for more details. Entities other than “bold”, “italic”, “underline”, “strikethrough”, “spoiler”, “custom_emoji”, and “date_time” are ignored.
|
||||
TextParseMode string `json:"text_parse_mode,omitempty"`
|
||||
TextParseMode ParseMode `json:"text_parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the gift text. It can be specified instead of text_parse_mode. Entities other than “bold”, “italic”, “underline”, “strikethrough”, “spoiler”, “custom_emoji”, and “date_time” are ignored.
|
||||
TextEntities []MessageEntity `json:"text_entities,omitempty"`
|
||||
}
|
||||
@@ -3840,7 +3856,7 @@ type PostStoryParams struct {
|
||||
// Caption of the story, 0-2048 characters after entities parsing
|
||||
Caption string `json:"caption,omitempty"`
|
||||
// Mode for parsing entities in the story caption. See formatting options for more details.
|
||||
ParseMode string `json:"parse_mode,omitempty"`
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the caption, which can be specified instead of parse_mode
|
||||
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
|
||||
// A JSON-serialized list of clickable areas to be shown on the story
|
||||
@@ -3896,7 +3912,7 @@ type EditStoryParams struct {
|
||||
// Caption of the story, 0-2048 characters after entities parsing
|
||||
Caption string `json:"caption,omitempty"`
|
||||
// Mode for parsing entities in the story caption. See formatting options for more details.
|
||||
ParseMode string `json:"parse_mode,omitempty"`
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the caption, which can be specified instead of parse_mode
|
||||
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
|
||||
// A JSON-serialized list of clickable areas to be shown on the story
|
||||
@@ -4001,7 +4017,7 @@ type EditMessageTextParams struct {
|
||||
// New text of the message, 1-4096 characters after entities parsing
|
||||
Text string `json:"text"`
|
||||
// Mode for parsing entities in the message text. See formatting options for more details.
|
||||
ParseMode string `json:"parse_mode,omitempty"`
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in message text, which can be specified instead of parse_mode
|
||||
Entities []MessageEntity `json:"entities,omitempty"`
|
||||
// Link preview generation options for the message
|
||||
@@ -4032,7 +4048,7 @@ type EditMessageCaptionParams struct {
|
||||
// New caption of the message, 0-1024 characters after entities parsing
|
||||
Caption string `json:"caption,omitempty"`
|
||||
// Mode for parsing entities in the message caption. See formatting options for more details.
|
||||
ParseMode string `json:"parse_mode,omitempty"`
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the caption, which can be specified instead of parse_mode
|
||||
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
|
||||
// Pass True, if the caption must be shown above the message media. Supported only for animation, photo and video messages.
|
||||
@@ -4492,7 +4508,7 @@ type UploadStickerFileParams struct {
|
||||
// A file with the sticker in .WEBP, .PNG, .TGS, or .WEBM format. See https://core.telegram.org/stickers for technical requirements. More information on Sending Files »
|
||||
Sticker *InputFile `json:"sticker"`
|
||||
// Format of the sticker, must be one of “static”, “animated”, “video”
|
||||
StickerFormat string `json:"sticker_format"`
|
||||
StickerFormat InputStickerFormat `json:"sticker_format"`
|
||||
}
|
||||
|
||||
// HasFile reports whether a multipart upload is required.
|
||||
@@ -4507,7 +4523,7 @@ func (p *UploadStickerFileParams) HasFile() bool {
|
||||
func (p *UploadStickerFileParams) MultipartFields() map[string]string {
|
||||
out := map[string]string{}
|
||||
out["user_id"] = strconv.FormatInt(p.UserID, 10)
|
||||
out["sticker_format"] = p.StickerFormat
|
||||
out["sticker_format"] = string(p.StickerFormat)
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -4707,7 +4723,7 @@ type SetStickerSetThumbnailParams struct {
|
||||
// A .WEBP or .PNG image with the thumbnail, must be up to 128 kilobytes in size and have a width and height of exactly 100px, or a .TGS animation with a thumbnail up to 32 kilobytes in size (see https://core.telegram.org/stickers#animation-requirements for animated sticker technical requirements), or a .WEBM video with the thumbnail up to 32 kilobytes in size; see https://core.telegram.org/stickers#video-requirements for video sticker technical requirements. Pass a file_id as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. More information on Sending Files ». Animated and video sticker set thumbnails can't be uploaded via HTTP URL. If omitted, then the thumbnail is dropped and the first sticker is used as the thumbnail.
|
||||
Thumbnail *InputFile `json:"thumbnail,omitempty"`
|
||||
// Format of the thumbnail, must be one of “static” for a .WEBP or .PNG image, “animated” for a .TGS animation, or “video” for a .WEBM video
|
||||
Format string `json:"format"`
|
||||
Format InputStickerFormat `json:"format"`
|
||||
}
|
||||
|
||||
// HasFile reports whether a multipart upload is required.
|
||||
@@ -4723,7 +4739,7 @@ func (p *SetStickerSetThumbnailParams) MultipartFields() map[string]string {
|
||||
out := map[string]string{}
|
||||
out["name"] = p.Name
|
||||
out["user_id"] = strconv.FormatInt(p.UserID, 10)
|
||||
out["format"] = p.Format
|
||||
out["format"] = string(p.Format)
|
||||
return out
|
||||
}
|
||||
|
||||
|
||||
+14
-14
@@ -20954,7 +20954,7 @@ func Test_UploadStickerFile_Success(t *testing.T) {
|
||||
params := &UploadStickerFileParams{
|
||||
UserID: 42,
|
||||
Sticker: &InputFile{PathOrID: "file_id_test"},
|
||||
StickerFormat: "test_value",
|
||||
StickerFormat: InputStickerFormatStatic,
|
||||
}
|
||||
_, err := UploadStickerFile(context.Background(), bot, params)
|
||||
require.NoError(t, err)
|
||||
@@ -20969,7 +20969,7 @@ func Test_UploadStickerFile_APIError(t *testing.T) {
|
||||
params := &UploadStickerFileParams{
|
||||
UserID: 42,
|
||||
Sticker: &InputFile{PathOrID: "file_id_test"},
|
||||
StickerFormat: "test_value",
|
||||
StickerFormat: InputStickerFormatStatic,
|
||||
}
|
||||
_, err := UploadStickerFile(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
@@ -20987,7 +20987,7 @@ func Test_UploadStickerFile_NetworkError(t *testing.T) {
|
||||
params := &UploadStickerFileParams{
|
||||
UserID: 42,
|
||||
Sticker: &InputFile{PathOrID: "file_id_test"},
|
||||
StickerFormat: "test_value",
|
||||
StickerFormat: InputStickerFormatStatic,
|
||||
}
|
||||
_, err := UploadStickerFile(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
@@ -21003,7 +21003,7 @@ func Test_UploadStickerFile_ParseError(t *testing.T) {
|
||||
params := &UploadStickerFileParams{
|
||||
UserID: 42,
|
||||
Sticker: &InputFile{PathOrID: "file_id_test"},
|
||||
StickerFormat: "test_value",
|
||||
StickerFormat: InputStickerFormatStatic,
|
||||
}
|
||||
_, err := UploadStickerFile(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
@@ -21022,7 +21022,7 @@ func Test_UploadStickerFile_ContextCanceled(t *testing.T) {
|
||||
params := &UploadStickerFileParams{
|
||||
UserID: 42,
|
||||
Sticker: &InputFile{PathOrID: "file_id_test"},
|
||||
StickerFormat: "test_value",
|
||||
StickerFormat: InputStickerFormatStatic,
|
||||
}
|
||||
_, err := UploadStickerFile(ctx, bot, params)
|
||||
require.Error(t, err)
|
||||
@@ -21063,7 +21063,7 @@ func Test_UploadStickerFile_Forbidden(t *testing.T) {
|
||||
params := &UploadStickerFileParams{
|
||||
UserID: 42,
|
||||
Sticker: &InputFile{PathOrID: "file_id_test"},
|
||||
StickerFormat: "test_value",
|
||||
StickerFormat: InputStickerFormatStatic,
|
||||
}
|
||||
_, err := UploadStickerFile(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
@@ -21085,7 +21085,7 @@ func Test_UploadStickerFile_ServerError(t *testing.T) {
|
||||
params := &UploadStickerFileParams{
|
||||
UserID: 42,
|
||||
Sticker: &InputFile{PathOrID: "file_id_test"},
|
||||
StickerFormat: "test_value",
|
||||
StickerFormat: InputStickerFormatStatic,
|
||||
}
|
||||
_, err := UploadStickerFile(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
@@ -22415,7 +22415,7 @@ func Test_SetStickerSetThumbnail_Success(t *testing.T) {
|
||||
params := &SetStickerSetThumbnailParams{
|
||||
Name: "test_value",
|
||||
UserID: 42,
|
||||
Format: "test_value",
|
||||
Format: InputStickerFormatStatic,
|
||||
}
|
||||
_, err := SetStickerSetThumbnail(context.Background(), bot, params)
|
||||
require.NoError(t, err)
|
||||
@@ -22430,7 +22430,7 @@ func Test_SetStickerSetThumbnail_APIError(t *testing.T) {
|
||||
params := &SetStickerSetThumbnailParams{
|
||||
Name: "test_value",
|
||||
UserID: 42,
|
||||
Format: "test_value",
|
||||
Format: InputStickerFormatStatic,
|
||||
}
|
||||
_, err := SetStickerSetThumbnail(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
@@ -22448,7 +22448,7 @@ func Test_SetStickerSetThumbnail_NetworkError(t *testing.T) {
|
||||
params := &SetStickerSetThumbnailParams{
|
||||
Name: "test_value",
|
||||
UserID: 42,
|
||||
Format: "test_value",
|
||||
Format: InputStickerFormatStatic,
|
||||
}
|
||||
_, err := SetStickerSetThumbnail(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
@@ -22464,7 +22464,7 @@ func Test_SetStickerSetThumbnail_ParseError(t *testing.T) {
|
||||
params := &SetStickerSetThumbnailParams{
|
||||
Name: "test_value",
|
||||
UserID: 42,
|
||||
Format: "test_value",
|
||||
Format: InputStickerFormatStatic,
|
||||
}
|
||||
_, err := SetStickerSetThumbnail(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
@@ -22483,7 +22483,7 @@ func Test_SetStickerSetThumbnail_ContextCanceled(t *testing.T) {
|
||||
params := &SetStickerSetThumbnailParams{
|
||||
Name: "test_value",
|
||||
UserID: 42,
|
||||
Format: "test_value",
|
||||
Format: InputStickerFormatStatic,
|
||||
}
|
||||
_, err := SetStickerSetThumbnail(ctx, bot, params)
|
||||
require.Error(t, err)
|
||||
@@ -22524,7 +22524,7 @@ func Test_SetStickerSetThumbnail_Forbidden(t *testing.T) {
|
||||
params := &SetStickerSetThumbnailParams{
|
||||
Name: "test_value",
|
||||
UserID: 42,
|
||||
Format: "test_value",
|
||||
Format: InputStickerFormatStatic,
|
||||
}
|
||||
_, err := SetStickerSetThumbnail(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
@@ -22546,7 +22546,7 @@ func Test_SetStickerSetThumbnail_ServerError(t *testing.T) {
|
||||
params := &SetStickerSetThumbnailParams{
|
||||
Name: "test_value",
|
||||
UserID: 42,
|
||||
Format: "test_value",
|
||||
Format: InputStickerFormatStatic,
|
||||
}
|
||||
_, err := SetStickerSetThumbnail(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package api
|
||||
|
||||
// Ptr returns a pointer to v. Useful for optional scalar fields where
|
||||
// the wire format must distinguish absent (nil) from an explicit zero
|
||||
// value (e.g. DisableNotification: api.Ptr(false) to override the
|
||||
// chat default).
|
||||
//
|
||||
// For untyped literals, supply the type parameter explicitly:
|
||||
//
|
||||
// Limit: api.Ptr[int64](5)
|
||||
//
|
||||
// For already-typed values, type inference handles it:
|
||||
//
|
||||
// var n int64 = 5
|
||||
// Limit: api.Ptr(n)
|
||||
func Ptr[T any](v T) *T { return &v }
|
||||
@@ -0,0 +1,28 @@
|
||||
package api_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/lukaszraczylo/go-telegram/api"
|
||||
)
|
||||
|
||||
func TestPtr(t *testing.T) {
|
||||
if got := api.Ptr[int64](5); got == nil || *got != 5 {
|
||||
t.Fatalf("Ptr[int64](5) = %v, want *5", got)
|
||||
}
|
||||
if got := api.Ptr(false); got == nil || *got != false {
|
||||
t.Fatalf("Ptr(false) = %v, want *false", got)
|
||||
}
|
||||
if got := api.Ptr("hello"); got == nil || *got != "hello" {
|
||||
t.Fatalf("Ptr(\"hello\") = %v, want *\"hello\"", got)
|
||||
}
|
||||
|
||||
n := int64(42)
|
||||
got := api.Ptr(n)
|
||||
if got == nil || *got != 42 {
|
||||
t.Fatalf("Ptr(n) = %v, want *42", got)
|
||||
}
|
||||
if got == &n {
|
||||
t.Fatalf("Ptr should copy, not alias caller's variable")
|
||||
}
|
||||
}
|
||||
+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")
|
||||
}
|
||||
+253
-27
@@ -8,12 +8,22 @@ import (
|
||||
"go/format"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"text/template"
|
||||
|
||||
"github.com/lukaszraczylo/go-telegram/internal/spec"
|
||||
)
|
||||
|
||||
// Discriminator-value extractors. The curly form ("always “X”") is
|
||||
// authoritative because Telegram quotes wire literals with curly quotes
|
||||
// throughout the docs; the bare form ("must be X") is the looser
|
||||
// non-quoted variant used for BotCommandScope, InputMedia, etc.
|
||||
var (
|
||||
discCurlyRE = regexp.MustCompile(`(?:must be|always)\s+“([^”]+)”`)
|
||||
discBareRE = regexp.MustCompile(`must be\s+([A-Za-z0-9_]+)(?:[\s.,]|$)`)
|
||||
)
|
||||
|
||||
//go:embed types.tmpl
|
||||
var typesTmpl string
|
||||
|
||||
@@ -164,15 +174,168 @@ var knownDiscriminators = map[string]discriminatorSpec{
|
||||
type emitter struct {
|
||||
api *spec.API
|
||||
outDir string
|
||||
enums *enumPlan
|
||||
// variantDiscs maps a concrete variant type name (e.g.
|
||||
// "BotCommandScopeAllPrivateChats") to its discriminator wire-field
|
||||
// + value. Populated once at construction; consulted by the types
|
||||
// template to emit per-variant MarshalJSON that hardcodes the
|
||||
// discriminator so callers don't have to set it by hand.
|
||||
variantDiscs map[string]variantDiscriminator
|
||||
}
|
||||
|
||||
func newEmitter(api *spec.API, outDir string) *emitter {
|
||||
return &emitter{api: api, outDir: outDir}
|
||||
knownInterfaceTypes = buildUnionTypeSet(api)
|
||||
return &emitter{
|
||||
api: api,
|
||||
outDir: outDir,
|
||||
enums: planEnums(api),
|
||||
variantDiscs: variantDiscriminators(api),
|
||||
}
|
||||
}
|
||||
|
||||
// variantDiscriminator describes the JSON field+value that identifies a
|
||||
// concrete variant of a sealed-interface union on the wire.
|
||||
type variantDiscriminator struct {
|
||||
JSONField string // wire field name, e.g. "type" or "source"
|
||||
GoField string // Go struct field name, e.g. "Type" or "Source"
|
||||
Value string // the wire value, e.g. "all_private_chats"
|
||||
}
|
||||
|
||||
// variantDiscriminators returns variantTypeName → discriminator for every
|
||||
// concrete struct that participates in a sealed-interface union and has
|
||||
// a string-typed first field whose doc fixes its value (the canonical
|
||||
// "must be X" / "always “X”" patterns Telegram uses).
|
||||
//
|
||||
// Resolution order:
|
||||
//
|
||||
// 1. knownDiscriminators reverse-lookup (the 13 auto-decode unions).
|
||||
// This guarantees parity with UnmarshalXxx dispatch for the unions
|
||||
// that round-trip through the library.
|
||||
// 2. Doc-string analysis of the variant's first field, for marker-only
|
||||
// unions (BotCommandScope, InputMedia, etc.) where the IR has no
|
||||
// explicit discriminator metadata.
|
||||
//
|
||||
// Variants whose first field has no discriminator hint (Message,
|
||||
// InaccessibleMessage, the InputMessageContent family) are omitted —
|
||||
// the caller writes the dispatching fields directly and Telegram
|
||||
// identifies them structurally.
|
||||
func variantDiscriminators(api *spec.API) map[string]variantDiscriminator {
|
||||
out := make(map[string]variantDiscriminator, 128)
|
||||
|
||||
// Pass 1: reverse-lookup from knownDiscriminators.
|
||||
for _, ds := range knownDiscriminators {
|
||||
if ds.Field == "" {
|
||||
continue
|
||||
}
|
||||
for value, variant := range ds.Variants {
|
||||
out[variant] = variantDiscriminator{
|
||||
JSONField: ds.Field,
|
||||
Value: value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build the set of every variant type referenced by any OneOf so we
|
||||
// can scan only those (avoids matching free-text "must be" prose in
|
||||
// non-variant types like Message).
|
||||
variantSet := make(map[string]bool, 128)
|
||||
for _, t := range api.Types {
|
||||
for _, v := range t.OneOf {
|
||||
variantSet[v] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2: doc-parse for variants without a known discriminator.
|
||||
for _, t := range api.Types {
|
||||
if !variantSet[t.Name] {
|
||||
continue
|
||||
}
|
||||
if _, ok := out[t.Name]; ok {
|
||||
// Pass-1 already provided the wire value; we still need
|
||||
// the Go field name (mirrors the JSON field but with
|
||||
// proper case). Resolve from t.Fields by JSONName match.
|
||||
disc := out[t.Name]
|
||||
for _, f := range t.Fields {
|
||||
if f.JSONName == disc.JSONField {
|
||||
disc.GoField = f.Name
|
||||
out[t.Name] = disc
|
||||
break
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
disc, ok := extractVariantDiscriminator(t)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
out[t.Name] = disc
|
||||
}
|
||||
|
||||
// Drop entries we couldn't resolve a Go field for (defensive — every
|
||||
// pass-1 hit should have matched, but better to skip than emit
|
||||
// broken code referencing an unknown field name).
|
||||
for name, d := range out {
|
||||
if d.GoField == "" {
|
||||
delete(out, name)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// extractVariantDiscriminator inspects the first field of a variant
|
||||
// struct and returns its discriminator if the field is a required
|
||||
// string whose doc nails the value via "must be X" or "always “X”".
|
||||
// Returns (zero, false) when no clear discriminator is present.
|
||||
func extractVariantDiscriminator(t spec.TypeDecl) (variantDiscriminator, bool) {
|
||||
if len(t.Fields) == 0 {
|
||||
return variantDiscriminator{}, false
|
||||
}
|
||||
f := t.Fields[0]
|
||||
if !f.Required || f.Type.Kind != spec.KindPrimitive || f.Type.Name != "string" {
|
||||
return variantDiscriminator{}, false
|
||||
}
|
||||
value := parseDiscriminatorDoc(f.Doc)
|
||||
if value == "" {
|
||||
return variantDiscriminator{}, false
|
||||
}
|
||||
return variantDiscriminator{
|
||||
JSONField: f.JSONName,
|
||||
GoField: f.Name,
|
||||
Value: value,
|
||||
}, true
|
||||
}
|
||||
|
||||
// parseDiscriminatorDoc extracts the wire-level discriminator value
|
||||
// from a field doc string. Handles both Telegram phrasings:
|
||||
//
|
||||
// - "Scope type, must be all_private_chats" (bare token)
|
||||
// - "Type of the message origin, always “user”" (curly-quoted)
|
||||
//
|
||||
// Returns "" when no discriminator is present.
|
||||
func parseDiscriminatorDoc(doc string) string {
|
||||
// Curly-quoted form takes priority: "must be “X”" or "always “X”".
|
||||
if m := discCurlyRE.FindStringSubmatch(doc); len(m) == 2 {
|
||||
return m[1]
|
||||
}
|
||||
// Bare-token form: "must be <ident>" terminated by end-of-string,
|
||||
// punctuation, or whitespace.
|
||||
if m := discBareRE.FindStringSubmatch(doc); len(m) == 2 {
|
||||
return m[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// knownInterfaceTypes is the full set of sealed-interface union type names
|
||||
// (both auto-decoded ones in knownDiscriminators and marker-only ones from
|
||||
// types with OneOf). Populated at emitter construction. goType and
|
||||
// unionTypeFor consult this so optional fields of any union type stay
|
||||
// bare interface, never *Interface (which is meaningless in Go and trips
|
||||
// users at every call site).
|
||||
var knownInterfaceTypes = map[string]bool{}
|
||||
|
||||
// emitTypes renders types.gen.go.
|
||||
func (e *emitter) emitTypes() error {
|
||||
t, err := template.New("types").Funcs(funcs()).Parse(typesTmpl)
|
||||
t, err := template.New("types").Funcs(funcsWithDiscs(e.enums, e.variantDiscs)).Parse(typesTmpl)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse types.tmpl: %w", err)
|
||||
}
|
||||
@@ -208,20 +371,49 @@ func loadAPI(path string) (*spec.API, error) {
|
||||
return &api, nil
|
||||
}
|
||||
|
||||
// funcs is the FuncMap shared across templates.
|
||||
func funcs() template.FuncMap {
|
||||
// funcsWithDiscs returns the shared FuncMap with the variant
|
||||
// discriminator helpers bound to discs. types.tmpl uses
|
||||
// variantDiscFor/variantHasDisc to emit per-variant MarshalJSON that
|
||||
// hardcodes the wire discriminator value.
|
||||
func funcsWithDiscs(plan *enumPlan, discs map[string]variantDiscriminator) template.FuncMap {
|
||||
fm := funcs(plan)
|
||||
fm["variantHasDisc"] = func(name string) bool {
|
||||
_, ok := discs[name]
|
||||
return ok
|
||||
}
|
||||
fm["variantDiscField"] = func(name string) string { return discs[name].JSONField }
|
||||
fm["variantDiscGoField"] = func(name string) string { return discs[name].GoField }
|
||||
fm["variantDiscValue"] = func(name string) string { return discs[name].Value }
|
||||
return fm
|
||||
}
|
||||
|
||||
// funcs is the FuncMap shared across templates. plan is the resolved
|
||||
// enum plan; pass nil only in unit tests that don't exercise enums.
|
||||
func funcs(plan *enumPlan) template.FuncMap {
|
||||
return template.FuncMap{
|
||||
"goType": goType,
|
||||
"goField": goField,
|
||||
"docComment": docComment,
|
||||
"isOptional": func(f spec.Field) bool { return !f.Required },
|
||||
"not": func(b bool) bool { return !b },
|
||||
"title": title,
|
||||
"isFileField": isFileField,
|
||||
"fileCheck": fileCheck,
|
||||
"multipartFieldEntry": multipartFieldEntry,
|
||||
"multipartFileEntry": multipartFileEntry,
|
||||
"returnGoType": returnGoType,
|
||||
"goType": goType,
|
||||
"goField": func(parent string, f spec.Field) string {
|
||||
return goField(plan, parent, f)
|
||||
},
|
||||
"docComment": docComment,
|
||||
"isOptional": func(f spec.Field) bool { return !f.Required },
|
||||
"not": func(b bool) bool { return !b },
|
||||
"title": title,
|
||||
"isFileField": isFileField,
|
||||
"fileCheck": fileCheck,
|
||||
"multipartFieldEntry": func(parent string, f spec.Field) string {
|
||||
return multipartFieldEntry(plan, parent, f)
|
||||
},
|
||||
"multipartFileEntry": multipartFileEntry,
|
||||
"returnGoType": returnGoType,
|
||||
// enum helpers
|
||||
"enums": func() []enumDecl {
|
||||
if plan == nil {
|
||||
return nil
|
||||
}
|
||||
return plan.All()
|
||||
},
|
||||
"enumConstName": constName,
|
||||
// discriminator helpers for types.tmpl
|
||||
"hasDiscriminator": func(name string) bool { s, ok := knownDiscriminators[name]; return ok && len(s.Variants) > 0 },
|
||||
"isSealedUnionReturn": func(tr spec.TypeRef) bool {
|
||||
@@ -231,6 +423,19 @@ func funcs() template.FuncMap {
|
||||
s, ok := knownDiscriminators[tr.Name]
|
||||
return ok && len(s.Variants) > 0
|
||||
},
|
||||
"isSealedUnionArrayReturn": func(tr spec.TypeRef) bool {
|
||||
if tr.Kind != spec.KindArray || tr.ElemType == nil || tr.ElemType.Kind != spec.KindNamed {
|
||||
return false
|
||||
}
|
||||
s, ok := knownDiscriminators[tr.ElemType.Name]
|
||||
return ok && len(s.Variants) > 0
|
||||
},
|
||||
"sealedUnionElemName": func(tr spec.TypeRef) string {
|
||||
if tr.Kind == spec.KindArray && tr.ElemType != nil {
|
||||
return tr.ElemType.Name
|
||||
}
|
||||
return ""
|
||||
},
|
||||
"isMaybeInaccessibleMessage": func(name string) bool { return name == "MaybeInaccessibleMessage" },
|
||||
"discriminatorField": func(name string) string { return knownDiscriminators[name].Field },
|
||||
"discriminatorMap": func(name string) map[string]string { return knownDiscriminators[name].Variants },
|
||||
@@ -295,8 +500,10 @@ func multipartFileEntry(f spec.Field) string {
|
||||
|
||||
// multipartFieldEntry generates the line that adds f to the multipart map.
|
||||
// Required scalar fields go in unconditionally; optional ones go in only
|
||||
// when non-zero/non-empty.
|
||||
func multipartFieldEntry(f spec.Field) string {
|
||||
// when non-zero/non-empty. Typed-string enum fields are cast to string
|
||||
// before assignment because the multipart map is map[string]string.
|
||||
func multipartFieldEntry(plan *enumPlan, parent string, f spec.Field) string {
|
||||
enumName := plan.FieldEnum(parent, f.Name)
|
||||
switch f.Type.Kind {
|
||||
case spec.KindPrimitive:
|
||||
switch f.Type.Name {
|
||||
@@ -306,6 +513,12 @@ func multipartFieldEntry(f spec.Field) string {
|
||||
}
|
||||
return fmt.Sprintf("\tif p.%s != nil { out[%q] = strconv.FormatInt(*p.%s, 10) }\n", f.Name, f.JSONName, f.Name)
|
||||
case "string":
|
||||
if enumName != "" {
|
||||
if f.Required {
|
||||
return fmt.Sprintf("\tout[%q] = string(p.%s)\n", f.JSONName, f.Name)
|
||||
}
|
||||
return fmt.Sprintf("\tif p.%s != \"\" { out[%q] = string(p.%s) }\n", f.Name, f.JSONName, f.Name)
|
||||
}
|
||||
if f.Required {
|
||||
return fmt.Sprintf("\tout[%q] = p.%s\n", f.JSONName, f.Name)
|
||||
}
|
||||
@@ -392,7 +605,7 @@ func returnGoElem(tr spec.TypeRef) string {
|
||||
|
||||
// emitMethods renders methods.gen.go.
|
||||
func (e *emitter) emitMethods() error {
|
||||
t, err := template.New("methods").Funcs(funcs()).Parse(methodsTmpl)
|
||||
t, err := template.New("methods").Funcs(funcs(e.enums)).Parse(methodsTmpl)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse methods.tmpl: %w", err)
|
||||
}
|
||||
@@ -409,7 +622,7 @@ func (e *emitter) emitMethods() error {
|
||||
|
||||
// emitEnums renders enums.gen.go.
|
||||
func (e *emitter) emitEnums() error {
|
||||
t, err := template.New("enums").Funcs(funcs()).Parse(enumsTmpl)
|
||||
t, err := template.New("enums").Funcs(funcs(e.enums)).Parse(enumsTmpl)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse enums.tmpl: %w", err)
|
||||
}
|
||||
@@ -442,7 +655,7 @@ func goType(tr spec.TypeRef, optional bool) string {
|
||||
// multipart helpers (fileCheck, multipartFileEntry) call
|
||||
// f.IsLocalUpload() and dereference Reader, both of which
|
||||
// expect a pointer receiver.
|
||||
if _, isUnion := knownDiscriminators[tr.Name]; isUnion {
|
||||
if knownInterfaceTypes[tr.Name] {
|
||||
// Interface type — never add *.
|
||||
return tr.Name
|
||||
}
|
||||
@@ -558,8 +771,16 @@ func matchesVariants(got []string, want ...string) bool {
|
||||
}
|
||||
|
||||
// goField returns the Go struct-field declaration for a Field.
|
||||
func goField(f spec.Field) string {
|
||||
// When the field carries scraper-detected enum values and the emitter
|
||||
// has a planned enum name for (parent, field), the field's Go type is
|
||||
// the enum identifier. Typed-string enums use the zero string ""
|
||||
// behaviour for omitempty, so we do not pointer-wrap optional enum
|
||||
// fields. Parent is "" for method parameters.
|
||||
func goField(plan *enumPlan, parent string, f spec.Field) string {
|
||||
tag := fmt.Sprintf("`json:%q`", f.JSONName+omitempty(f))
|
||||
if name := plan.FieldEnum(parent, f.Name); name != "" {
|
||||
return fmt.Sprintf("%s %s %s", f.Name, name, tag)
|
||||
}
|
||||
return fmt.Sprintf("%s %s %s", f.Name, goType(f.Type, !f.Required), tag)
|
||||
}
|
||||
|
||||
@@ -623,14 +844,19 @@ func buildUnionTypeSet(api *spec.API) map[string]bool {
|
||||
|
||||
// makeSentinelValue returns a sentinelValue func that uses the given union type set.
|
||||
// It returns a minimal valid Go expression for a spec.Field's type,
|
||||
// used in generated test param literals.
|
||||
func makeSentinelValue(unionTypes map[string]bool) func(spec.Field) string {
|
||||
// used in generated test param literals. plan supplies typed-enum names
|
||||
// so a method-param sentinel for a ParseMode field becomes a typed
|
||||
// constant rather than a magic string.
|
||||
func makeSentinelValue(unionTypes map[string]bool, plan *enumPlan) func(spec.Field) string {
|
||||
return func(f spec.Field) string {
|
||||
return sentinelForField(f, unionTypes)
|
||||
return sentinelForField(f, unionTypes, plan)
|
||||
}
|
||||
}
|
||||
|
||||
func sentinelForField(f spec.Field, unionTypes map[string]bool) string {
|
||||
func sentinelForField(f spec.Field, unionTypes map[string]bool, plan *enumPlan) string {
|
||||
if name := plan.FieldEnum("", f.Name); name != "" && len(f.EnumValues) > 0 {
|
||||
return constName(name, f.EnumValues[0])
|
||||
}
|
||||
tr := f.Type
|
||||
switch tr.Kind {
|
||||
case spec.KindPrimitive:
|
||||
@@ -729,8 +955,8 @@ func (e *emitter) emitTests() error {
|
||||
unionTypes := buildUnionTypeSet(e.api)
|
||||
|
||||
// Add test-specific helpers to the shared func map.
|
||||
fm := funcs()
|
||||
fm["sentinelValue"] = makeSentinelValue(unionTypes)
|
||||
fm := funcs(e.enums)
|
||||
fm["sentinelValue"] = makeSentinelValue(unionTypes, e.enums)
|
||||
fm["successResp"] = successResp
|
||||
|
||||
t, err := template.New("tests").Funcs(fm).Parse(testsTmpl)
|
||||
|
||||
@@ -0,0 +1,483 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/lukaszraczylo/go-telegram/internal/spec"
|
||||
)
|
||||
|
||||
// enumDecl is one generated enum: a Go type alias of string plus a set
|
||||
// of named constants. Values keep doc order; constant identifiers are
|
||||
// derived from values via constName.
|
||||
type enumDecl struct {
|
||||
Name string
|
||||
Values []string
|
||||
}
|
||||
|
||||
// enumPlan is the deduplicated, name-resolved set of enums emitted from
|
||||
// an API IR. Lookup returns the enum name for a given field reference;
|
||||
// All returns the deterministic-ordered list of declarations to emit.
|
||||
type enumPlan struct {
|
||||
// fieldKey -> enum name. The fieldKey is a string built by enumKey.
|
||||
byField map[string]string
|
||||
// enum name -> declaration.
|
||||
decls map[string]enumDecl
|
||||
}
|
||||
|
||||
// enumKey identifies a single Field occurrence so the emitter can look
|
||||
// up the enum name later. Parent is "" for method params (the method
|
||||
// doesn't share a Go type with the field).
|
||||
func enumKey(parent, fieldName string) string { return parent + "::" + fieldName }
|
||||
|
||||
// planEnums walks the IR, decides on enum names, deduplicates, and
|
||||
// returns an enumPlan. All scraper-marked enum fields are covered.
|
||||
func planEnums(api *spec.API) *enumPlan {
|
||||
type ref struct {
|
||||
parent string
|
||||
fieldName string
|
||||
jsonName string
|
||||
values []string
|
||||
valueKey string // canonical key for value-set dedup
|
||||
}
|
||||
|
||||
// Unification pass: for each sealed-interface union, fold per-variant
|
||||
// single-value enum fields that share a discriminator name into ONE
|
||||
// unified enum at union level. Claimed (parent,fieldName) tuples are
|
||||
// excluded from the per-field grouping below.
|
||||
unifiedDecls, unifiedByField := planUnifiedUnionEnums(api)
|
||||
claimed := func(parent, fieldName string) bool {
|
||||
_, ok := unifiedByField[enumKey(parent, fieldName)]
|
||||
return ok
|
||||
}
|
||||
|
||||
var refs []ref
|
||||
collect := func(parent string, fields []spec.Field) {
|
||||
for _, f := range fields {
|
||||
if len(f.EnumValues) == 0 {
|
||||
continue
|
||||
}
|
||||
if claimed(parent, f.Name) {
|
||||
continue
|
||||
}
|
||||
refs = append(refs, ref{
|
||||
parent: parent,
|
||||
fieldName: f.Name,
|
||||
jsonName: f.JSONName,
|
||||
values: f.EnumValues,
|
||||
valueKey: valueKey(f.EnumValues),
|
||||
})
|
||||
}
|
||||
}
|
||||
for _, t := range api.Types {
|
||||
collect(t.Name, t.Fields)
|
||||
}
|
||||
for _, m := range api.Methods {
|
||||
// Method params have no shared Go parent type, so we pass "" as
|
||||
// the parent. The default-name heuristic still produces the
|
||||
// right answer for ParseMode-style enums.
|
||||
collect("", m.Params)
|
||||
}
|
||||
|
||||
// candidate name per ref (before collision resolution)
|
||||
candidate := make([]string, len(refs))
|
||||
for i, r := range refs {
|
||||
candidate[i] = defaultEnumName(r.parent, r.jsonName, r.fieldName)
|
||||
}
|
||||
|
||||
// Group by valueKey to coalesce identical value-sets across fields.
|
||||
// Choose canonical name: prefer the most common candidate; tie-break
|
||||
// by shortest name; final tie-break alphabetical.
|
||||
type groupInfo struct {
|
||||
values []string
|
||||
name string
|
||||
first int
|
||||
}
|
||||
groups := map[string]*groupInfo{}
|
||||
for i, r := range refs {
|
||||
g, ok := groups[r.valueKey]
|
||||
if !ok {
|
||||
groups[r.valueKey] = &groupInfo{values: r.values, first: i}
|
||||
g = groups[r.valueKey]
|
||||
}
|
||||
_ = g
|
||||
}
|
||||
// Rank candidate names per group.
|
||||
for vk := range groups {
|
||||
counts := map[string]int{}
|
||||
hasParent := map[string]bool{}
|
||||
var names []string
|
||||
for i, r := range refs {
|
||||
if r.valueKey != vk {
|
||||
continue
|
||||
}
|
||||
n := candidate[i]
|
||||
if _, ok := counts[n]; !ok {
|
||||
names = append(names, n)
|
||||
}
|
||||
counts[n]++
|
||||
if r.parent != "" {
|
||||
hasParent[n] = true
|
||||
}
|
||||
}
|
||||
// Pick the canonical name for this group:
|
||||
// 1. highest occurrence count wins;
|
||||
// 2. names that originated from a parent type win over plain
|
||||
// method-param candidates (avoids "Format"-style
|
||||
// monosyllables);
|
||||
// 3. shortest name wins;
|
||||
// 4. alphabetical for full determinism.
|
||||
sort.SliceStable(names, func(a, b int) bool {
|
||||
if counts[names[a]] != counts[names[b]] {
|
||||
return counts[names[a]] > counts[names[b]]
|
||||
}
|
||||
if hasParent[names[a]] != hasParent[names[b]] {
|
||||
return hasParent[names[a]]
|
||||
}
|
||||
if len(names[a]) != len(names[b]) {
|
||||
return len(names[a]) < len(names[b])
|
||||
}
|
||||
return names[a] < names[b]
|
||||
})
|
||||
groups[vk].name = names[0]
|
||||
}
|
||||
|
||||
// Collision pass: two groups must not share the same enum name.
|
||||
// When that happens, suffix the loser(s) with their parent type
|
||||
// name so the result is unique. Iterate in deterministic order
|
||||
// (groups sorted by valueKey).
|
||||
used := map[string]string{} // name -> valueKey owner
|
||||
var keys []string
|
||||
for vk := range groups {
|
||||
keys = append(keys, vk)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, vk := range keys {
|
||||
g := groups[vk]
|
||||
if _, taken := used[g.name]; !taken {
|
||||
used[g.name] = vk
|
||||
continue
|
||||
}
|
||||
// Find a unique name by prepending a parent prefix from one of
|
||||
// the contributing refs (the lowest-index ref in this group).
|
||||
for i, r := range refs {
|
||||
if r.valueKey != vk {
|
||||
continue
|
||||
}
|
||||
if r.parent == "" {
|
||||
continue
|
||||
}
|
||||
cand := r.parent + goNamePart(r.jsonName)
|
||||
if _, taken := used[cand]; !taken {
|
||||
g.name = cand
|
||||
used[cand] = vk
|
||||
goto next
|
||||
}
|
||||
_ = i
|
||||
}
|
||||
// Fallback: append a numeric disambiguator. Should not happen
|
||||
// in practice for the Telegram docs but keeps the algorithm
|
||||
// total.
|
||||
for n := 2; ; n++ {
|
||||
cand := groups[vk].name + itoa(n)
|
||||
if _, taken := used[cand]; !taken {
|
||||
g.name = cand
|
||||
used[cand] = vk
|
||||
break
|
||||
}
|
||||
}
|
||||
next:
|
||||
}
|
||||
|
||||
// Build the plan.
|
||||
plan := &enumPlan{
|
||||
byField: map[string]string{},
|
||||
decls: map[string]enumDecl{},
|
||||
}
|
||||
for i, r := range refs {
|
||||
name := groups[r.valueKey].name
|
||||
plan.byField[enumKey(r.parent, r.fieldName)] = name
|
||||
_ = i
|
||||
}
|
||||
for vk, g := range groups {
|
||||
plan.decls[g.name] = enumDecl{Name: g.name, Values: g.values}
|
||||
_ = vk
|
||||
}
|
||||
// Merge unified union enums (already named with stutter handling and
|
||||
// keyed per-variant in unifiedByField).
|
||||
for k, name := range unifiedByField {
|
||||
plan.byField[k] = name
|
||||
}
|
||||
for name, d := range unifiedDecls {
|
||||
plan.decls[name] = d
|
||||
}
|
||||
return plan
|
||||
}
|
||||
|
||||
// planUnifiedUnionEnums detects sealed-interface unions whose variants
|
||||
// share a single discriminator field with one enum value each, and emits
|
||||
// ONE unified enum per union covering all variant values. Returns the
|
||||
// declarations to emit and the per-(variant,fieldName) map to point each
|
||||
// variant's field at the unified enum.
|
||||
//
|
||||
// A union qualifies when EVERY variant in t.OneOf:
|
||||
// 1. defines a field with the same Go-name (e.g. "Status", "Type", "Source");
|
||||
// 2. that field is a required string with len(EnumValues)==1.
|
||||
//
|
||||
// The picked Go-name is the first one tried in this priority order:
|
||||
// - knownDiscriminators[union].Field's Go-name (resolved via JSONName match);
|
||||
// - "Type", "Status", "Source" (the three discriminators Telegram uses).
|
||||
//
|
||||
// First match wins; if none qualify, the union is skipped (variants keep
|
||||
// their existing per-field treatment, which still single-emits via the
|
||||
// regular grouping pass).
|
||||
func planUnifiedUnionEnums(api *spec.API) (map[string]enumDecl, map[string]string) {
|
||||
decls := map[string]enumDecl{}
|
||||
byField := map[string]string{}
|
||||
|
||||
typeByName := make(map[string]*spec.TypeDecl, len(api.Types))
|
||||
for i := range api.Types {
|
||||
typeByName[api.Types[i].Name] = &api.Types[i]
|
||||
}
|
||||
|
||||
// Iterate unions in deterministic (declaration) order.
|
||||
for ui := range api.Types {
|
||||
u := &api.Types[ui]
|
||||
if len(u.OneOf) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Resolve the variants. Skip unions where any variant is missing
|
||||
// (defensive — shouldn't happen in a well-formed IR).
|
||||
variants := make([]*spec.TypeDecl, 0, len(u.OneOf))
|
||||
for _, vName := range u.OneOf {
|
||||
v, ok := typeByName[vName]
|
||||
if !ok {
|
||||
variants = nil
|
||||
break
|
||||
}
|
||||
variants = append(variants, v)
|
||||
}
|
||||
if len(variants) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Build the candidate Go-name list. Priority order:
|
||||
// 1. discriminator GoField from knownDiscriminators (resolved via JSONName);
|
||||
// 2. "Type", "Status", "Source".
|
||||
var candidateNames []string
|
||||
seen := map[string]bool{}
|
||||
add := func(name string) {
|
||||
if name == "" || seen[name] {
|
||||
return
|
||||
}
|
||||
seen[name] = true
|
||||
candidateNames = append(candidateNames, name)
|
||||
}
|
||||
if ds, ok := knownDiscriminators[u.Name]; ok && ds.Field != "" {
|
||||
// Resolve Go-name from the first variant whose field matches the JSON name.
|
||||
for _, v := range variants {
|
||||
for _, f := range v.Fields {
|
||||
if f.JSONName == ds.Field {
|
||||
add(f.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, n := range []string{"Type", "Status", "Source"} {
|
||||
add(n)
|
||||
}
|
||||
|
||||
// Find the first candidate Go-name where every variant has a
|
||||
// matching single-value string-enum field.
|
||||
var (
|
||||
pickedName string
|
||||
pickedDocs map[string]spec.Field // variant name -> field
|
||||
)
|
||||
for _, name := range candidateNames {
|
||||
matches := map[string]spec.Field{}
|
||||
ok := true
|
||||
for _, v := range variants {
|
||||
var hit *spec.Field
|
||||
for fi := range v.Fields {
|
||||
if v.Fields[fi].Name == name {
|
||||
hit = &v.Fields[fi]
|
||||
break
|
||||
}
|
||||
}
|
||||
if hit == nil ||
|
||||
hit.Type.Kind != spec.KindPrimitive ||
|
||||
hit.Type.Name != "string" ||
|
||||
len(hit.EnumValues) != 1 {
|
||||
ok = false
|
||||
break
|
||||
}
|
||||
matches[v.Name] = *hit
|
||||
}
|
||||
if ok {
|
||||
pickedName = name
|
||||
pickedDocs = matches
|
||||
break
|
||||
}
|
||||
}
|
||||
if pickedName == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Build the unified enum name with stutter handling.
|
||||
enumName := unifiedEnumName(u.Name, pickedName)
|
||||
|
||||
// Collect values across variants in deterministic order, deduping.
|
||||
valueOrder := make([]string, 0, len(variants))
|
||||
valueSeen := map[string]bool{}
|
||||
for _, v := range u.OneOf {
|
||||
f := pickedDocs[v]
|
||||
val := f.EnumValues[0]
|
||||
if valueSeen[val] {
|
||||
continue
|
||||
}
|
||||
valueSeen[val] = true
|
||||
valueOrder = append(valueOrder, val)
|
||||
}
|
||||
|
||||
decls[enumName] = enumDecl{Name: enumName, Values: valueOrder}
|
||||
for _, v := range variants {
|
||||
byField[enumKey(v.Name, pickedName)] = enumName
|
||||
}
|
||||
}
|
||||
|
||||
return decls, byField
|
||||
}
|
||||
|
||||
// unifiedEnumName builds the union-level enum name. Falls back to a
|
||||
// "Kind" suffix when the naive concatenation reads as a stutter:
|
||||
//
|
||||
// - union name ends in the field name verbatim (e.g. BackgroundType+Type);
|
||||
// - union name ends in any "concept noun" — Type/Status/Source/State —
|
||||
// so appending another such noun would duplicate the suffix
|
||||
// (e.g. ChatBoostSource+Source, RevenueWithdrawalState+Type).
|
||||
//
|
||||
// Otherwise the natural concatenation wins (ChatMember+Status →
|
||||
// ChatMemberStatus, MessageOrigin+Type → MessageOriginType).
|
||||
func unifiedEnumName(unionName, fieldName string) string {
|
||||
for _, suf := range []string{"Type", "Status", "Source", "State"} {
|
||||
if strings.HasSuffix(unionName, suf) {
|
||||
return unionName + "Kind"
|
||||
}
|
||||
}
|
||||
return unionName + fieldName
|
||||
}
|
||||
|
||||
// All returns the enum declarations sorted by name for deterministic emit.
|
||||
func (p *enumPlan) All() []enumDecl {
|
||||
out := make([]enumDecl, 0, len(p.decls))
|
||||
for _, d := range p.decls {
|
||||
out = append(out, d)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
||||
return out
|
||||
}
|
||||
|
||||
// FieldEnum returns the enum name for a field on a given parent type
|
||||
// (use parent="" for method parameters), or "" if the field is not an
|
||||
// enum.
|
||||
func (p *enumPlan) FieldEnum(parent, fieldName string) string {
|
||||
if p == nil {
|
||||
return ""
|
||||
}
|
||||
return p.byField[enumKey(parent, fieldName)]
|
||||
}
|
||||
|
||||
// defaultEnumName picks an initial Go enum name for a field. parse_mode
|
||||
// fields collapse to the canonical "ParseMode"; otherwise the name is
|
||||
// parent + PascalCase(jsonName).
|
||||
func defaultEnumName(parent, jsonName, fieldName string) string {
|
||||
if strings.HasSuffix(jsonName, "parse_mode") {
|
||||
return "ParseMode"
|
||||
}
|
||||
return parent + goNamePart(jsonName)
|
||||
}
|
||||
|
||||
// constName builds a Go constant identifier "<EnumName><PascalValue>"
|
||||
// from a wire value. Slashes (mime types) become "Of" so
|
||||
// "image/jpeg" → "ImageOfJpeg".
|
||||
func constName(enumName, value string) string {
|
||||
return enumName + valuePascal(value)
|
||||
}
|
||||
|
||||
func valuePascal(v string) string {
|
||||
// "image/jpeg" → "ImageOfJpeg"
|
||||
parts := strings.Split(v, "/")
|
||||
for i, p := range parts {
|
||||
parts[i] = goNamePart(p)
|
||||
}
|
||||
return strings.Join(parts, "Of")
|
||||
}
|
||||
|
||||
// goNamePart converts a snake_case (or already-PascalCase) token to
|
||||
// PascalCase, mirroring scrape.goName behaviour without the acronym
|
||||
// special-cases (which apply to wire identifiers, not enum values).
|
||||
func goNamePart(s string) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
parts := strings.Split(s, "_")
|
||||
var b strings.Builder
|
||||
for _, p := range parts {
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
// Acronyms used in Telegram wire names. Keeping in sync with
|
||||
// scrape/table.go avoids divergent capitalisation between
|
||||
// fieldName and constName.
|
||||
switch p {
|
||||
case "id":
|
||||
b.WriteString("ID")
|
||||
continue
|
||||
case "url":
|
||||
b.WriteString("URL")
|
||||
continue
|
||||
case "ip":
|
||||
b.WriteString("IP")
|
||||
continue
|
||||
case "https":
|
||||
b.WriteString("HTTPS")
|
||||
continue
|
||||
case "json":
|
||||
b.WriteString("JSON")
|
||||
continue
|
||||
case "html":
|
||||
b.WriteString("HTML")
|
||||
continue
|
||||
}
|
||||
if c := p[0]; c >= 'a' && c <= 'z' {
|
||||
b.WriteByte(c - 'a' + 'A')
|
||||
b.WriteString(p[1:])
|
||||
} else {
|
||||
b.WriteString(p)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func valueKey(values []string) string {
|
||||
cp := make([]string, len(values))
|
||||
copy(cp, values)
|
||||
sort.Strings(cp)
|
||||
return strings.Join(cp, "\x00")
|
||||
}
|
||||
|
||||
func itoa(n int) string {
|
||||
if n == 0 {
|
||||
return "0"
|
||||
}
|
||||
var buf [20]byte
|
||||
i := len(buf)
|
||||
for n > 0 {
|
||||
i--
|
||||
buf[i] = byte('0' + n%10)
|
||||
n /= 10
|
||||
}
|
||||
return string(buf[i:])
|
||||
}
|
||||
+5
-52
@@ -4,57 +4,10 @@
|
||||
|
||||
package api
|
||||
|
||||
// ParseMode controls how Telegram interprets formatting in message text.
|
||||
type ParseMode string
|
||||
{{range $e := enums}}
|
||||
type {{$e.Name}} string
|
||||
|
||||
const (
|
||||
ParseModeMarkdown ParseMode = "Markdown" // legacy
|
||||
ParseModeMarkdownV2 ParseMode = "MarkdownV2"
|
||||
ParseModeHTML ParseMode = "HTML"
|
||||
)
|
||||
|
||||
// ChatType is the type of a Telegram chat.
|
||||
type ChatType string
|
||||
|
||||
const (
|
||||
ChatTypePrivate ChatType = "private"
|
||||
ChatTypeGroup ChatType = "group"
|
||||
ChatTypeSupergroup ChatType = "supergroup"
|
||||
ChatTypeChannel ChatType = "channel"
|
||||
)
|
||||
|
||||
// UpdateType identifies an Update payload variant. Used by allowed_updates
|
||||
// in getUpdates / setWebhook.
|
||||
type UpdateType string
|
||||
|
||||
const (
|
||||
UpdateMessage UpdateType = "message"
|
||||
UpdateEditedMessage UpdateType = "edited_message"
|
||||
UpdateChannelPost UpdateType = "channel_post"
|
||||
UpdateEditedChannelPost UpdateType = "edited_channel_post"
|
||||
UpdateCallbackQuery UpdateType = "callback_query"
|
||||
UpdateInlineQuery UpdateType = "inline_query"
|
||||
)
|
||||
|
||||
// MessageEntityType is the kind of an entity (mention, hashtag, command, ...).
|
||||
type MessageEntityType string
|
||||
|
||||
const (
|
||||
EntityMention MessageEntityType = "mention"
|
||||
EntityHashtag MessageEntityType = "hashtag"
|
||||
EntityCashtag MessageEntityType = "cashtag"
|
||||
EntityBotCommand MessageEntityType = "bot_command"
|
||||
EntityURL MessageEntityType = "url"
|
||||
EntityEmail MessageEntityType = "email"
|
||||
EntityPhoneNumber MessageEntityType = "phone_number"
|
||||
EntityBold MessageEntityType = "bold"
|
||||
EntityItalic MessageEntityType = "italic"
|
||||
EntityUnderline MessageEntityType = "underline"
|
||||
EntityStrike MessageEntityType = "strikethrough"
|
||||
EntitySpoiler MessageEntityType = "spoiler"
|
||||
EntityCode MessageEntityType = "code"
|
||||
EntityPre MessageEntityType = "pre"
|
||||
EntityTextLink MessageEntityType = "text_link"
|
||||
EntityTextMention MessageEntityType = "text_mention"
|
||||
EntityCustomEmoji MessageEntityType = "custom_emoji"
|
||||
)
|
||||
{{range $v := $e.Values}} {{enumConstName $e.Name $v}} {{$e.Name}} = {{printf "%q" $v}}
|
||||
{{end}})
|
||||
{{end}}
|
||||
|
||||
+17
-17
@@ -175,101 +175,101 @@ func makeFieldVariants(name, jname string, kind spec.Kind, variants []string, re
|
||||
|
||||
func TestMultipartFieldEntry_Int64Required(t *testing.T) {
|
||||
f := makeField("ChatID", "chat_id", "int64", spec.KindPrimitive, true)
|
||||
got := multipartFieldEntry(f)
|
||||
got := multipartFieldEntry(nil, "", f)
|
||||
require.Contains(t, got, `FormatInt`)
|
||||
require.NotContains(t, got, "if p.")
|
||||
}
|
||||
|
||||
func TestMultipartFieldEntry_Int64Optional(t *testing.T) {
|
||||
f := makeField("MessageThreadID", "message_thread_id", "int64", spec.KindPrimitive, false)
|
||||
got := multipartFieldEntry(f)
|
||||
got := multipartFieldEntry(nil, "", f)
|
||||
require.Contains(t, got, `FormatInt`)
|
||||
require.Contains(t, got, "if p.")
|
||||
}
|
||||
|
||||
func TestMultipartFieldEntry_StringRequired(t *testing.T) {
|
||||
f := makeField("Text", "text", "string", spec.KindPrimitive, true)
|
||||
got := multipartFieldEntry(f)
|
||||
got := multipartFieldEntry(nil, "", f)
|
||||
require.Contains(t, got, `out["text"]`)
|
||||
require.NotContains(t, got, "if p.Text")
|
||||
}
|
||||
|
||||
func TestMultipartFieldEntry_StringOptional(t *testing.T) {
|
||||
f := makeField("ParseMode", "parse_mode", "string", spec.KindPrimitive, false)
|
||||
got := multipartFieldEntry(f)
|
||||
got := multipartFieldEntry(nil, "", f)
|
||||
require.Contains(t, got, `if p.ParseMode`)
|
||||
}
|
||||
|
||||
func TestMultipartFieldEntry_BoolRequired(t *testing.T) {
|
||||
f := makeField("DisableNotification", "disable_notification", "bool", spec.KindPrimitive, true)
|
||||
got := multipartFieldEntry(f)
|
||||
got := multipartFieldEntry(nil, "", f)
|
||||
require.Contains(t, got, `FormatBool`)
|
||||
require.NotContains(t, got, "if p.")
|
||||
}
|
||||
|
||||
func TestMultipartFieldEntry_BoolOptional(t *testing.T) {
|
||||
f := makeField("Protected", "protect_content", "bool", spec.KindPrimitive, false)
|
||||
got := multipartFieldEntry(f)
|
||||
got := multipartFieldEntry(nil, "", f)
|
||||
require.Contains(t, got, `FormatBool`)
|
||||
require.Contains(t, got, "if p.")
|
||||
}
|
||||
|
||||
func TestMultipartFieldEntry_Float64Required(t *testing.T) {
|
||||
f := makeField("Latitude", "latitude", "float64", spec.KindPrimitive, true)
|
||||
got := multipartFieldEntry(f)
|
||||
got := multipartFieldEntry(nil, "", f)
|
||||
require.Contains(t, got, `FormatFloat`)
|
||||
require.NotContains(t, got, "if p.")
|
||||
}
|
||||
|
||||
func TestMultipartFieldEntry_Float64Optional(t *testing.T) {
|
||||
f := makeField("Longitude", "longitude", "float64", spec.KindPrimitive, false)
|
||||
got := multipartFieldEntry(f)
|
||||
got := multipartFieldEntry(nil, "", f)
|
||||
require.Contains(t, got, `FormatFloat`)
|
||||
require.Contains(t, got, "if p.")
|
||||
}
|
||||
|
||||
func TestMultipartFieldEntry_OneOf_ChatIDRequired(t *testing.T) {
|
||||
f := makeFieldVariants("ChatID", "chat_id", spec.KindOneOf, []string{"int64", "string"}, true)
|
||||
got := multipartFieldEntry(f)
|
||||
got := multipartFieldEntry(nil, "", f)
|
||||
require.Contains(t, got, `.String()`)
|
||||
require.NotContains(t, got, "IsZero")
|
||||
}
|
||||
|
||||
func TestMultipartFieldEntry_OneOf_ChatIDOptional(t *testing.T) {
|
||||
f := makeFieldVariants("ChatID", "chat_id", spec.KindOneOf, []string{"int64", "string"}, false)
|
||||
got := multipartFieldEntry(f)
|
||||
got := multipartFieldEntry(nil, "", f)
|
||||
require.Contains(t, got, `IsZero`)
|
||||
}
|
||||
|
||||
func TestMultipartFieldEntry_OneOf_InputFileOrString(t *testing.T) {
|
||||
f := makeFieldVariants("Photo", "photo", spec.KindOneOf, []string{"InputFile", "string"}, false)
|
||||
got := multipartFieldEntry(f)
|
||||
got := multipartFieldEntry(nil, "", f)
|
||||
require.Contains(t, got, `PathOrID`)
|
||||
}
|
||||
|
||||
func TestMultipartFieldEntry_OneOf_SealedRequired(t *testing.T) {
|
||||
f := makeFieldVariants("Markup", "reply_markup", spec.KindOneOf, []string{"A", "B"}, true)
|
||||
got := multipartFieldEntry(f)
|
||||
got := multipartFieldEntry(nil, "", f)
|
||||
require.Contains(t, got, `json.Marshal`)
|
||||
}
|
||||
|
||||
func TestMultipartFieldEntry_OneOf_SealedOptional(t *testing.T) {
|
||||
f := makeFieldVariants("Markup", "reply_markup", spec.KindOneOf, []string{"A", "B"}, false)
|
||||
got := multipartFieldEntry(f)
|
||||
got := multipartFieldEntry(nil, "", f)
|
||||
require.Contains(t, got, `json.Marshal`)
|
||||
require.Contains(t, got, "if p.Markup")
|
||||
}
|
||||
|
||||
func TestMultipartFieldEntry_Named_Required(t *testing.T) {
|
||||
f := makeField("Entities", "entities", "MessageEntity", spec.KindNamed, true)
|
||||
got := multipartFieldEntry(f)
|
||||
got := multipartFieldEntry(nil, "", f)
|
||||
require.Contains(t, got, `json.Marshal`)
|
||||
require.NotContains(t, got, "if p.")
|
||||
}
|
||||
|
||||
func TestMultipartFieldEntry_Named_Optional(t *testing.T) {
|
||||
f := makeField("Entities", "entities", "MessageEntity", spec.KindNamed, false)
|
||||
got := multipartFieldEntry(f)
|
||||
got := multipartFieldEntry(nil, "", f)
|
||||
require.Contains(t, got, `json.Marshal`)
|
||||
require.Contains(t, got, "if p.")
|
||||
}
|
||||
@@ -574,7 +574,7 @@ func TestSentinelForField(t *testing.T) {
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
got := sentinelForField(c.field, unionTypes)
|
||||
got := sentinelForField(c.field, unionTypes, nil)
|
||||
require.Contains(t, got, c.contains, "sentinelForField for %q", c.name)
|
||||
})
|
||||
}
|
||||
@@ -637,7 +637,7 @@ func TestUnionTypeFor_OneOfNoMatch(t *testing.T) {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestFuncs_HasExpectedKeys(t *testing.T) {
|
||||
fm := funcs()
|
||||
fm := funcs(nil)
|
||||
require.NotNil(t, fm)
|
||||
for _, key := range []string{"goType", "docComment", "returnGoType", "unionFields"} {
|
||||
require.NotNil(t, fm[key], "funcs() missing key %q", key)
|
||||
|
||||
+20
-2
@@ -20,7 +20,7 @@ var _ = json.Marshal // keep import for complex multipart fields
|
||||
//
|
||||
{{docComment .Doc -}}
|
||||
type {{title .Name}}Params struct {
|
||||
{{range .Params}}{{docComment .Doc}} {{goField .}}
|
||||
{{range .Params}}{{docComment .Doc}} {{goField "" .}}
|
||||
{{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 .)}}{{multipartFieldEntry "" .}}{{end}}{{end}} return out
|
||||
}
|
||||
|
||||
// MultipartFiles returns the file parts.
|
||||
@@ -51,6 +51,24 @@ func {{title .Name}}(ctx context.Context, b *client.Bot, p *{{title .Name}}Param
|
||||
return nil, err
|
||||
}
|
||||
return Unmarshal{{.Returns.Name}}(raw)
|
||||
{{else if isSealedUnionArrayReturn .Returns -}}
|
||||
raw, err := client.CallRaw[*{{title .Name}}Params](ctx, b, "{{.Name}}", p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var elems []json.RawMessage
|
||||
if err := json.Unmarshal(raw, &elems); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]{{sealedUnionElemName .Returns}}, 0, len(elems))
|
||||
for _, e := range elems {
|
||||
v, err := Unmarshal{{sealedUnionElemName .Returns}}(e)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, v)
|
||||
}
|
||||
return out, nil
|
||||
{{else -}}
|
||||
return client.Call[*{{title .Name}}Params, {{returnGoType .Returns}}](ctx, b, "{{.Name}}", p)
|
||||
{{end -}}
|
||||
|
||||
+18
-1
@@ -82,8 +82,25 @@ func UnmarshalMaybeInaccessibleMessage(data []byte) (MaybeInaccessibleMessage, e
|
||||
{{else}}
|
||||
{{docComment .Doc -}}
|
||||
type {{.Name}} struct {
|
||||
{{range .Fields}}{{docComment .Doc}}{{goField .}}
|
||||
{{range .Fields}}{{docComment .Doc}}{{goField $td.Name .}}
|
||||
{{end}}}
|
||||
{{if variantHasDisc .Name}}
|
||||
// MarshalJSON encodes {{.Name}} with the discriminator field
|
||||
// "{{variantDiscField .Name}}" forced to {{printf "%q" (variantDiscValue .Name)}}.
|
||||
// The hardcoded value frees callers from setting {{variantDiscGoField .Name}} by hand —
|
||||
// any user-supplied value on the struct literal is overridden so a typo
|
||||
// can't slip through to Telegram.
|
||||
func (v *{{.Name}}) MarshalJSON() ([]byte, error) {
|
||||
type alias {{.Name}}
|
||||
return json.Marshal(&struct {
|
||||
{{variantDiscGoField .Name}} string `json:"{{variantDiscField .Name}}"`
|
||||
*alias
|
||||
}{
|
||||
{{variantDiscGoField .Name}}: {{printf "%q" (variantDiscValue .Name)}},
|
||||
alias: (*alias)(v),
|
||||
})
|
||||
}
|
||||
{{end}}
|
||||
{{$unionFields := unionFields .}}{{if $unionFields}}
|
||||
// UnmarshalJSON decodes {{.Name}} by dispatching union-typed fields
|
||||
// ({{range $i, $u := $unionFields}}{{if $i}}, {{end}}{{$u.Field.Name}}{{end}}) through their concrete UnmarshalXxx helpers.
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// extractEnumValues inspects a field-description string and returns the
|
||||
// list of wire-level string values when the description matches one of
|
||||
// the enum-like patterns Telegram uses in its docs. Order follows doc
|
||||
// order; duplicates are removed but order of first occurrence is kept.
|
||||
//
|
||||
// Handled patterns (curly quotes “…” are required to avoid false
|
||||
// positives on free-text quoting):
|
||||
//
|
||||
// - "Type of the chat, can be either “private”, “group”, … or “channel”"
|
||||
// - "Currently, can be “mention”, “hashtag”, …"
|
||||
// - "Currently, one of “XTR” … or “TON” …"
|
||||
// - "Currently, must be one of “XTR” …"
|
||||
// - "Currently, it can be one of “pending”, “approved”, “declined”."
|
||||
// - "Must be one of “danger” …, “success” …"
|
||||
// - "Must be one of “image/jpeg”, “image/gif”, or “video/mp4”"
|
||||
// - "Format … must be one of “static” …, “animated” …, “video” …"
|
||||
// - "Currently, either “upgrade” …, “transfer” …, “resale” …"
|
||||
// - "..., always “creator”"
|
||||
// - parse_mode parameter special case ("Mode for parsing entities …")
|
||||
// emits the canonical Markdown / MarkdownV2 / HTML triple.
|
||||
//
|
||||
// Returns nil when the description does not look like an enum.
|
||||
func extractEnumValues(jsonName, desc string) []string {
|
||||
if values := parseModeEnumValues(jsonName, desc); values != nil {
|
||||
return values
|
||||
}
|
||||
|
||||
trigger, triggerEnd, isAlways := findEnumTrigger(desc)
|
||||
if trigger < 0 {
|
||||
return nil
|
||||
}
|
||||
tail := desc[trigger:]
|
||||
|
||||
values := collectQuotedValues(tail)
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
// First quoted value must sit close to the trigger phrase (e.g.
|
||||
// "can be “private”…"). Phrasings like "can be available only for
|
||||
// “invoice_payment”…" introduce a referenced value, not an enum,
|
||||
// and the gap between trigger end and first quote rules them out.
|
||||
firstQuote := strings.Index(desc[triggerEnd:], "“")
|
||||
if firstQuote < 0 {
|
||||
return nil
|
||||
}
|
||||
gap := desc[triggerEnd : triggerEnd+firstQuote]
|
||||
// Allow "always " as a permitted bridge (e.g. "Currently, always
|
||||
// “XTR”") and promote the match to single-value form.
|
||||
if strings.Contains(strings.ToLower(gap), "always ") {
|
||||
isAlways = true
|
||||
} else if firstQuote > 8 {
|
||||
return nil
|
||||
}
|
||||
// Single-value matches are only credible after "always". Multi-
|
||||
// value matches are credible after any trigger; the trigger phrase
|
||||
// already constrained the context.
|
||||
if !isAlways && len(values) < 2 {
|
||||
return nil
|
||||
}
|
||||
for _, v := range values {
|
||||
if !looksLikeEnumValue(v) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return dedupeStrings(values)
|
||||
}
|
||||
|
||||
// parseMode parameters do not list values inline — Telegram links to a
|
||||
// separate "formatting options" section. We hardcode the canonical set
|
||||
// here so callers get a typed ParseMode without writing magic strings.
|
||||
func parseModeEnumValues(jsonName, desc string) []string {
|
||||
if !strings.HasSuffix(jsonName, "parse_mode") {
|
||||
return nil
|
||||
}
|
||||
if !strings.Contains(desc, "Mode for parsing entities") {
|
||||
return nil
|
||||
}
|
||||
return []string{"Markdown", "MarkdownV2", "HTML"}
|
||||
}
|
||||
|
||||
// enumTriggers are anchor phrases that introduce a list of valid wire
|
||||
// values. Order matches longest-prefix priority; the matcher uses the
|
||||
// earliest match in the description.
|
||||
var enumTriggers = []string{
|
||||
"can be either ",
|
||||
"can be one of ",
|
||||
"can be ",
|
||||
"must be one of ",
|
||||
"must be ",
|
||||
"currently one of ",
|
||||
"currently, one of ",
|
||||
"currently, either ",
|
||||
"currently, must be one of ",
|
||||
"currently, can be ",
|
||||
"currently, it can be one of ",
|
||||
"currently, ",
|
||||
"one of ",
|
||||
"either ",
|
||||
"always ",
|
||||
}
|
||||
|
||||
// findEnumTrigger returns the byte offset where the first enum trigger
|
||||
// phrase begins, the offset just past the phrase, and whether the
|
||||
// trigger is the single-value "always" form. Returns (-1, -1, false)
|
||||
// when no trigger matches. Matching is case-insensitive so "Currently"
|
||||
// and "currently" both fire.
|
||||
func findEnumTrigger(desc string) (int, int, bool) {
|
||||
lower := strings.ToLower(desc)
|
||||
bestStart := -1
|
||||
bestEnd := -1
|
||||
bestAlways := false
|
||||
for _, t := range enumTriggers {
|
||||
i := strings.Index(lower, t)
|
||||
if i < 0 {
|
||||
continue
|
||||
}
|
||||
if bestStart != -1 && i >= bestStart {
|
||||
// Earlier-trigger wins outright; on a tie, the longer trigger
|
||||
// (which we visit first) already populated bestEnd.
|
||||
continue
|
||||
}
|
||||
bestStart = i
|
||||
bestEnd = i + len(t)
|
||||
bestAlways = t == "always "
|
||||
}
|
||||
return bestStart, bestEnd, bestAlways
|
||||
}
|
||||
|
||||
// quotedRE matches a curly-quoted token: “value”.
|
||||
var quotedRE = regexp.MustCompile(`“([^”]*)”`)
|
||||
|
||||
// collectQuotedValues returns the contents of every “…” pair in s in
|
||||
// order. Multi-line is fine; the docs use single-paragraph cells.
|
||||
func collectQuotedValues(s string) []string {
|
||||
matches := quotedRE.FindAllStringSubmatch(s, -1)
|
||||
out := make([]string, 0, len(matches))
|
||||
for _, m := range matches {
|
||||
out = append(out, m[1])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// looksLikeEnumValue returns true for short identifiers that fit the
|
||||
// shape of a Telegram wire enum. This rules out values like
|
||||
// "attach://…", "h264", arbitrary URLs, and stylised punctuation.
|
||||
//
|
||||
// Permitted shapes:
|
||||
//
|
||||
// a-z0-9_ (e.g. "private", "bot_command")
|
||||
// A-Z0-9_ (e.g. "XTR", "TON", "MarkdownV2")
|
||||
// mixed case incl. "/" once (e.g. "image/jpeg", "video/mp4")
|
||||
func looksLikeEnumValue(v string) bool {
|
||||
if v == "" || len(v) > 64 {
|
||||
return false
|
||||
}
|
||||
if strings.Contains(v, "://") || strings.Contains(v, " ") {
|
||||
return false
|
||||
}
|
||||
// "image/jpeg"-style mime types: at most one slash, both halves alnum.
|
||||
if i := strings.Index(v, "/"); i >= 0 {
|
||||
if strings.Count(v, "/") > 1 {
|
||||
return false
|
||||
}
|
||||
left, right := v[:i], v[i+1:]
|
||||
return isIdent(left) && isIdent(right)
|
||||
}
|
||||
return isIdent(v)
|
||||
}
|
||||
|
||||
func isIdent(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
for _, r := range s {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z':
|
||||
case r >= 'A' && r <= 'Z':
|
||||
case r >= '0' && r <= '9':
|
||||
case r == '_' || r == '-' || r == '.':
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func dedupeStrings(in []string) []string {
|
||||
seen := make(map[string]struct{}, len(in))
|
||||
out := make([]string, 0, len(in))
|
||||
for _, s := range in {
|
||||
if _, ok := seen[s]; ok {
|
||||
continue
|
||||
}
|
||||
seen[s] = struct{}{}
|
||||
out = append(out, s)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestExtractEnumValues_CanBeEither(t *testing.T) {
|
||||
desc := "Type of the chat, can be either “private”, “group”, “supergroup” or “channel”"
|
||||
got := extractEnumValues("type", desc)
|
||||
require.Equal(t, []string{"private", "group", "supergroup", "channel"}, got)
|
||||
}
|
||||
|
||||
func TestExtractEnumValues_CurrentlyCanBe(t *testing.T) {
|
||||
desc := "Type of the entity. Currently, can be “mention” (@username), “hashtag” (#hashtag), or “code” (monowidth string)"
|
||||
got := extractEnumValues("type", desc)
|
||||
require.Equal(t, []string{"mention", "hashtag", "code"}, got)
|
||||
}
|
||||
|
||||
func TestExtractEnumValues_AlwaysSingle(t *testing.T) {
|
||||
desc := "Type of the message origin, always “user”"
|
||||
got := extractEnumValues("type", desc)
|
||||
require.Equal(t, []string{"user"}, got)
|
||||
}
|
||||
|
||||
func TestExtractEnumValues_MustBeOneOfMime(t *testing.T) {
|
||||
desc := "Optional. MIME type of the thumbnail, must be one of “image/jpeg”, “image/gif”, or “video/mp4”. Defaults to “image/jpeg”"
|
||||
got := extractEnumValues("thumbnail_mime_type", desc)
|
||||
require.Equal(t, []string{"image/jpeg", "image/gif", "video/mp4"}, got)
|
||||
}
|
||||
|
||||
func TestExtractEnumValues_ParseModeSpecial(t *testing.T) {
|
||||
desc := "Optional. Mode for parsing entities in the message text. See formatting options for more details."
|
||||
got := extractEnumValues("parse_mode", desc)
|
||||
require.Equal(t, []string{"Markdown", "MarkdownV2", "HTML"}, got)
|
||||
}
|
||||
|
||||
func TestExtractEnumValues_QuestionParseMode(t *testing.T) {
|
||||
desc := "Mode for parsing entities in the question. See formatting options for more details."
|
||||
got := extractEnumValues("question_parse_mode", desc)
|
||||
require.Equal(t, []string{"Markdown", "MarkdownV2", "HTML"}, got)
|
||||
}
|
||||
|
||||
func TestExtractEnumValues_FalsePositiveReferencedValue(t *testing.T) {
|
||||
// "Can be available only for "X"" is NOT an enum: the quote is a
|
||||
// reference to a transaction-type value, not an introduced list.
|
||||
desc := "Optional. Bot-specified invoice payload. Can be available only for “invoice_payment” transactions."
|
||||
got := extractEnumValues("invoice_payload", desc)
|
||||
require.Nil(t, got)
|
||||
}
|
||||
|
||||
func TestExtractEnumValues_FalsePositiveSingleQuotedNonEnum(t *testing.T) {
|
||||
// "can be ignored" with a quoted reference value later — not an enum.
|
||||
desc := "Optional. Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side."
|
||||
got := extractEnumValues("thumbnail", desc)
|
||||
require.Nil(t, got)
|
||||
}
|
||||
|
||||
func TestExtractEnumValues_RefundedPaymentCurrentlyAlways(t *testing.T) {
|
||||
desc := "Three-letter ISO 4217 currency code, or “XTR” for payments in Telegram Stars. Currently, always “XTR”"
|
||||
got := extractEnumValues("currency", desc)
|
||||
require.Equal(t, []string{"XTR"}, got)
|
||||
}
|
||||
|
||||
func TestExtractEnumValues_RejectURLValues(t *testing.T) {
|
||||
// "attach://<file_attach_name>" must never be promoted to an enum value.
|
||||
desc := "Pass “attach://<file_attach_name>” to upload a new one"
|
||||
got := extractEnumValues("media", desc)
|
||||
require.Nil(t, got)
|
||||
}
|
||||
|
||||
func TestExtractEnumValues_StringTypeOnly(t *testing.T) {
|
||||
// (Sanity — table.go gates on string type, but the function itself
|
||||
// should still respond consistently.)
|
||||
desc := "ABC, can be “a”, “b”"
|
||||
got := extractEnumValues("x", desc)
|
||||
require.Equal(t, []string{"a", "b"}, got)
|
||||
}
|
||||
|
||||
func TestExtractEnumValues_DedupeRepeatedValues(t *testing.T) {
|
||||
desc := "Currently, one of “XTR” for Telegram Stars or “XTR” again"
|
||||
got := extractEnumValues("currency", desc)
|
||||
require.Equal(t, []string{"XTR"}, got)
|
||||
}
|
||||
+22
-10
@@ -30,12 +30,18 @@ func parseFieldsTable(t *html.Node) []spec.Field {
|
||||
desc := strings.TrimSpace(textOf(cells[2]))
|
||||
|
||||
required := !strings.HasPrefix(desc, "Optional.")
|
||||
tref := parseTypeRef(typeText)
|
||||
var enumVals []string
|
||||
if tref.Kind == spec.KindPrimitive && tref.Name == "string" {
|
||||
enumVals = extractEnumValues(jname, desc)
|
||||
}
|
||||
fields = append(fields, spec.Field{
|
||||
Name: goName(jname),
|
||||
JSONName: jname,
|
||||
Type: parseTypeRef(typeText),
|
||||
Required: required,
|
||||
Doc: desc,
|
||||
Name: goName(jname),
|
||||
JSONName: jname,
|
||||
Type: tref,
|
||||
Required: required,
|
||||
Doc: desc,
|
||||
EnumValues: enumVals,
|
||||
})
|
||||
}
|
||||
return fields
|
||||
@@ -59,12 +65,18 @@ func parseParamsTable(t *html.Node) []spec.Field {
|
||||
req := strings.EqualFold(strings.TrimSpace(textOf(cells[2])), "Yes")
|
||||
desc := strings.TrimSpace(textOf(cells[3]))
|
||||
|
||||
tref := parseTypeRef(typeText)
|
||||
var enumVals []string
|
||||
if tref.Kind == spec.KindPrimitive && tref.Name == "string" {
|
||||
enumVals = extractEnumValues(jname, desc)
|
||||
}
|
||||
params = append(params, spec.Field{
|
||||
Name: goName(jname),
|
||||
JSONName: jname,
|
||||
Type: parseTypeRef(typeText),
|
||||
Required: req,
|
||||
Doc: desc,
|
||||
Name: goName(jname),
|
||||
JSONName: jname,
|
||||
Type: tref,
|
||||
Required: req,
|
||||
Doc: desc,
|
||||
EnumValues: enumVals,
|
||||
})
|
||||
}
|
||||
return params
|
||||
|
||||
@@ -15,17 +15,17 @@ func NewStatus(s string) dispatch.Filter[*api.ChatMemberUpdated] {
|
||||
}
|
||||
switch m := u.NewChatMember.(type) {
|
||||
case *api.ChatMemberOwner:
|
||||
return m.Status == s
|
||||
return string(m.Status) == s
|
||||
case *api.ChatMemberAdministrator:
|
||||
return m.Status == s
|
||||
return string(m.Status) == s
|
||||
case *api.ChatMemberMember:
|
||||
return m.Status == s
|
||||
return string(m.Status) == s
|
||||
case *api.ChatMemberRestricted:
|
||||
return m.Status == s
|
||||
return string(m.Status) == s
|
||||
case *api.ChatMemberLeft:
|
||||
return m.Status == s
|
||||
return string(m.Status) == s
|
||||
case *api.ChatMemberBanned:
|
||||
return m.Status == s
|
||||
return string(m.Status) == s
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -12,15 +12,15 @@ func memberUpdate(status string, fromID int64) *api.ChatMemberUpdated {
|
||||
var newMember api.ChatMember
|
||||
switch status {
|
||||
case "member":
|
||||
newMember = &api.ChatMemberMember{Status: status}
|
||||
newMember = &api.ChatMemberMember{Status: api.ChatMemberStatusMember}
|
||||
case "administrator":
|
||||
newMember = &api.ChatMemberAdministrator{Status: status}
|
||||
newMember = &api.ChatMemberAdministrator{Status: api.ChatMemberStatusAdministrator}
|
||||
case "kicked":
|
||||
newMember = &api.ChatMemberBanned{Status: status}
|
||||
newMember = &api.ChatMemberBanned{Status: api.ChatMemberStatusKicked}
|
||||
case "left":
|
||||
newMember = &api.ChatMemberLeft{Status: status}
|
||||
newMember = &api.ChatMemberLeft{Status: api.ChatMemberStatusLeft}
|
||||
default:
|
||||
newMember = &api.ChatMemberMember{Status: status}
|
||||
newMember = &api.ChatMemberMember{Status: api.ChatMemberStatusMember}
|
||||
}
|
||||
return &api.ChatMemberUpdated{
|
||||
From: api.User{ID: fromID},
|
||||
@@ -70,7 +70,7 @@ func TestComposedFilters(t *testing.T) {
|
||||
func TestNewStatus_Owner(t *testing.T) {
|
||||
u := &api.ChatMemberUpdated{
|
||||
From: api.User{ID: 1},
|
||||
NewChatMember: &api.ChatMemberOwner{Status: "creator"},
|
||||
NewChatMember: &api.ChatMemberOwner{Status: api.ChatMemberStatusCreator},
|
||||
}
|
||||
require.True(t, cmfilter.NewStatus("creator")(u))
|
||||
require.False(t, cmfilter.NewStatus("member")(u))
|
||||
@@ -79,7 +79,7 @@ func TestNewStatus_Owner(t *testing.T) {
|
||||
func TestNewStatus_Restricted(t *testing.T) {
|
||||
u := &api.ChatMemberUpdated{
|
||||
From: api.User{ID: 1},
|
||||
NewChatMember: &api.ChatMemberRestricted{Status: "restricted"},
|
||||
NewChatMember: &api.ChatMemberRestricted{Status: api.ChatMemberStatusRestricted},
|
||||
}
|
||||
require.True(t, cmfilter.NewStatus("restricted")(u))
|
||||
require.False(t, cmfilter.NewStatus("member")(u))
|
||||
|
||||
@@ -48,7 +48,7 @@ func Command(name string) dispatch.Filter[*api.Message] {
|
||||
return false
|
||||
}
|
||||
first := m.Entities[0]
|
||||
if first.Type != string(api.EntityBotCommand) || first.Offset != 0 {
|
||||
if first.Type != api.MessageEntityTypeBotCommand || first.Offset != 0 {
|
||||
return false
|
||||
}
|
||||
end := int(first.Length)
|
||||
@@ -72,7 +72,7 @@ func AnyCommand() dispatch.Filter[*api.Message] {
|
||||
return false
|
||||
}
|
||||
first := m.Entities[0]
|
||||
return first.Type == string(api.EntityBotCommand) && first.Offset == 0
|
||||
return first.Type == api.MessageEntityTypeBotCommand && first.Offset == 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,8 +105,8 @@ func HasDocument() dispatch.Filter[*api.Message] {
|
||||
}
|
||||
|
||||
// HasEntity returns a Filter that matches messages whose Entities contain at
|
||||
// least one entity of type t (e.g. string(api.EntityBotCommand)).
|
||||
func HasEntity(t string) dispatch.Filter[*api.Message] {
|
||||
// least one entity of type t (e.g. api.MessageEntityTypeBotCommand).
|
||||
func HasEntity(t api.MessageEntityType) dispatch.Filter[*api.Message] {
|
||||
return func(m *api.Message) bool {
|
||||
if m == nil {
|
||||
return false
|
||||
@@ -123,7 +123,7 @@ func HasEntity(t string) dispatch.Filter[*api.Message] {
|
||||
// ChatType returns a Filter that matches messages whose Chat.Type equals t.
|
||||
func ChatType(t api.ChatType) dispatch.Filter[*api.Message] {
|
||||
return func(m *api.Message) bool {
|
||||
return m != nil && m.Chat.Type == string(t)
|
||||
return m != nil && m.Chat.Type == t
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
func msg(text string) *api.Message {
|
||||
return &api.Message{
|
||||
MessageID: 1,
|
||||
Chat: api.Chat{ID: 1, Type: string(api.ChatTypePrivate)},
|
||||
Chat: api.Chat{ID: 1, Type: api.ChatTypePrivate},
|
||||
Text: text,
|
||||
}
|
||||
}
|
||||
@@ -20,10 +20,10 @@ func cmdMsg(cmd string) *api.Message {
|
||||
text := cmd
|
||||
return &api.Message{
|
||||
MessageID: 1,
|
||||
Chat: api.Chat{ID: 1, Type: string(api.ChatTypePrivate)},
|
||||
Chat: api.Chat{ID: 1, Type: api.ChatTypePrivate},
|
||||
Text: text,
|
||||
Entities: []api.MessageEntity{
|
||||
{Type: string(api.EntityBotCommand), Offset: 0, Length: int64(len([]rune(text)))},
|
||||
{Type: api.MessageEntityTypeBotCommand, Offset: 0, Length: int64(len([]rune(text)))},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,7 @@ func TestCommand(t *testing.T) {
|
||||
t.Run("strips BotName suffix", func(t *testing.T) {
|
||||
m := &api.Message{
|
||||
Text: "/start@MyBot",
|
||||
Entities: []api.MessageEntity{{Type: string(api.EntityBotCommand), Offset: 0, Length: 12}},
|
||||
Entities: []api.MessageEntity{{Type: api.MessageEntityTypeBotCommand, Offset: 0, Length: 12}},
|
||||
}
|
||||
f := msgfilter.Command("/start")
|
||||
require.True(t, f(m))
|
||||
@@ -109,7 +109,7 @@ func TestIsForward(t *testing.T) {
|
||||
// ForwardOrigin is a MessageOrigin interface; set via a concrete type.
|
||||
f := msgfilter.IsForward()
|
||||
m := msg("fwd")
|
||||
m.ForwardOrigin = &api.MessageOriginUser{Type: "user"}
|
||||
m.ForwardOrigin = &api.MessageOriginUser{}
|
||||
require.True(t, f(m))
|
||||
require.False(t, f(msg("no fwd")))
|
||||
require.False(t, f(nil))
|
||||
@@ -134,9 +134,9 @@ func TestHasDocument(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestHasEntity(t *testing.T) {
|
||||
f := msgfilter.HasEntity(string(api.EntityURL))
|
||||
f := msgfilter.HasEntity(api.MessageEntityTypeURL)
|
||||
m := msg("check https://example.com")
|
||||
m.Entities = []api.MessageEntity{{Type: string(api.EntityURL), Offset: 6, Length: 19}}
|
||||
m.Entities = []api.MessageEntity{{Type: api.MessageEntityTypeURL, Offset: 6, Length: 19}}
|
||||
require.True(t, f(m))
|
||||
require.False(t, f(msg("plain")))
|
||||
require.False(t, f(nil))
|
||||
@@ -148,7 +148,7 @@ func TestChatType(t *testing.T) {
|
||||
require.True(t, f(private))
|
||||
|
||||
group := msg("hi")
|
||||
group.Chat.Type = string(api.ChatTypeGroup)
|
||||
group.Chat.Type = api.ChatTypeGroup
|
||||
require.False(t, f(group))
|
||||
require.False(t, f(nil))
|
||||
}
|
||||
@@ -183,6 +183,6 @@ func TestComposedMessageFilters(t *testing.T) {
|
||||
require.True(t, f(m))
|
||||
|
||||
m2 := msg("say hello")
|
||||
m2.Chat.Type = string(api.ChatTypeGroup)
|
||||
m2.Chat.Type = api.ChatTypeGroup
|
||||
require.False(t, f(m2))
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ func msgUpdate(id int64, text string) api.Update {
|
||||
UpdateID: id,
|
||||
Message: &api.Message{
|
||||
MessageID: id,
|
||||
Chat: api.Chat{ID: 1, Type: string(api.ChatTypePrivate)},
|
||||
Chat: api.Chat{ID: 1, Type: api.ChatTypePrivate},
|
||||
Text: text,
|
||||
},
|
||||
}
|
||||
@@ -29,10 +29,10 @@ func cmdUpdate(id int64, cmd string) api.Update {
|
||||
UpdateID: id,
|
||||
Message: &api.Message{
|
||||
MessageID: id,
|
||||
Chat: api.Chat{ID: 1, Type: string(api.ChatTypePrivate)},
|
||||
Chat: api.Chat{ID: 1, Type: api.ChatTypePrivate},
|
||||
Text: cmd,
|
||||
Entities: []api.MessageEntity{
|
||||
{Type: string(api.EntityBotCommand), Offset: 0, Length: int64(len(cmd))},
|
||||
{Type: api.MessageEntityTypeBotCommand, Offset: 0, Length: int64(len(cmd))},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
+1
-1
@@ -516,7 +516,7 @@ func extractCommand(m *api.Message) (cmd, args string, ok bool) {
|
||||
return "", "", false
|
||||
}
|
||||
first := m.Entities[0]
|
||||
if first.Type != string(api.EntityBotCommand) || first.Offset != 0 {
|
||||
if first.Type != api.MessageEntityTypeBotCommand || first.Offset != 0 {
|
||||
return "", "", false
|
||||
}
|
||||
cmd, sliceOk := utf16Slice(m.Text, int(first.Offset), int(first.Length))
|
||||
|
||||
+15
-15
@@ -31,9 +31,9 @@ func cmdMessage(text string) api.Update {
|
||||
return api.Update{
|
||||
UpdateID: 1,
|
||||
Message: &api.Message{
|
||||
MessageID: 1, Date: 0, Chat: api.Chat{ID: 1, Type: string(api.ChatTypePrivate)},
|
||||
MessageID: 1, Date: 0, Chat: api.Chat{ID: 1, Type: api.ChatTypePrivate},
|
||||
Text: text,
|
||||
Entities: []api.MessageEntity{{Type: string(api.EntityBotCommand), Offset: 0, Length: int64(indexEnd(text))}},
|
||||
Entities: []api.MessageEntity{{Type: api.MessageEntityTypeBotCommand, Offset: 0, Length: int64(indexEnd(text))}},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -153,10 +153,10 @@ func TestRouter_NonASCIICommand(t *testing.T) {
|
||||
UpdateID: 1,
|
||||
Message: &api.Message{
|
||||
MessageID: 1,
|
||||
Chat: api.Chat{ID: 1, Type: string(api.ChatTypePrivate)},
|
||||
Chat: api.Chat{ID: 1, Type: api.ChatTypePrivate},
|
||||
Text: text,
|
||||
Entities: []api.MessageEntity{
|
||||
{Type: string(api.EntityBotCommand), Offset: 0, Length: cmdU16Len},
|
||||
{Type: api.MessageEntityTypeBotCommand, Offset: 0, Length: cmdU16Len},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -196,7 +196,7 @@ func TestRouter_CommandValuesNotLeakedOnNoMatch(t *testing.T) {
|
||||
u := api.Update{UpdateID: 1, Message: &api.Message{
|
||||
MessageID: 1, Chat: api.Chat{ID: 1, Type: "private"},
|
||||
Text: "/unknown",
|
||||
Entities: []api.MessageEntity{{Type: string(api.EntityBotCommand), Offset: 0, Length: 8}},
|
||||
Entities: []api.MessageEntity{{Type: api.MessageEntityTypeBotCommand, Offset: 0, Length: 8}},
|
||||
}}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
|
||||
@@ -247,7 +247,7 @@ func TestRouter_OnChannelPost(t *testing.T) {
|
||||
})
|
||||
|
||||
u := api.Update{UpdateID: 1, ChannelPost: &api.Message{
|
||||
MessageID: 99, Chat: api.Chat{ID: -100, Type: string(api.ChatTypeChannel)},
|
||||
MessageID: 99, Chat: api.Chat{ID: -100, Type: api.ChatTypeChannel},
|
||||
}}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
|
||||
defer cancel()
|
||||
@@ -393,7 +393,7 @@ func TestRouter_OnInlineQueryFilter_Matches(t *testing.T) {
|
||||
func TestRouter_FilterChain_Composition(t *testing.T) {
|
||||
// Filter: private chat AND text contains "hello"
|
||||
privateChat := Filter[*api.Message](func(m *api.Message) bool {
|
||||
return m != nil && m.Chat.Type == string(api.ChatTypePrivate)
|
||||
return m != nil && m.Chat.Type == api.ChatTypePrivate
|
||||
})
|
||||
hasHello := Filter[*api.Message](func(m *api.Message) bool {
|
||||
return m != nil && len(m.Text) > 0 && containsStr(m.Text, "hello")
|
||||
@@ -405,10 +405,10 @@ func TestRouter_FilterChain_Composition(t *testing.T) {
|
||||
r.OnMessageFilter(combined, func(c *Context, m *api.Message) error { hit <- m.Text; return nil })
|
||||
|
||||
match := api.Update{UpdateID: 1, Message: &api.Message{
|
||||
MessageID: 1, Chat: api.Chat{ID: 1, Type: string(api.ChatTypePrivate)}, Text: "say hello",
|
||||
MessageID: 1, Chat: api.Chat{ID: 1, Type: api.ChatTypePrivate}, Text: "say hello",
|
||||
}}
|
||||
noMatch := api.Update{UpdateID: 2, Message: &api.Message{
|
||||
MessageID: 2, Chat: api.Chat{ID: 2, Type: string(api.ChatTypeGroup)}, Text: "say hello",
|
||||
MessageID: 2, Chat: api.Chat{ID: 2, Type: api.ChatTypeGroup}, Text: "say hello",
|
||||
}}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
|
||||
@@ -462,7 +462,7 @@ func TestRouter_ConcurrentDispatch_AllHandlersFire(t *testing.T) {
|
||||
for i := range ups {
|
||||
ups[i] = api.Update{UpdateID: int64(i + 1), Message: &api.Message{
|
||||
MessageID: int64(i + 1),
|
||||
Chat: api.Chat{ID: 1, Type: string(api.ChatTypePrivate)},
|
||||
Chat: api.Chat{ID: 1, Type: api.ChatTypePrivate},
|
||||
Text: "hi",
|
||||
}}
|
||||
}
|
||||
@@ -493,7 +493,7 @@ func TestRouter_ConcurrentDispatch_SemaphoreBoundsConcurrency(t *testing.T) {
|
||||
for i := range ups {
|
||||
ups[i] = api.Update{UpdateID: int64(i + 1), Message: &api.Message{
|
||||
MessageID: int64(i + 1),
|
||||
Chat: api.Chat{ID: 1, Type: string(api.ChatTypePrivate)},
|
||||
Chat: api.Chat{ID: 1, Type: api.ChatTypePrivate},
|
||||
Text: "hi",
|
||||
}}
|
||||
}
|
||||
@@ -555,7 +555,7 @@ func TestRouter_ConcurrentDispatch_WaitsForInFlight(t *testing.T) {
|
||||
)
|
||||
|
||||
u := api.Update{UpdateID: 1, Message: &api.Message{
|
||||
MessageID: 1, Chat: api.Chat{ID: 1, Type: string(api.ChatTypePrivate)}, Text: "hi",
|
||||
MessageID: 1, Chat: api.Chat{ID: 1, Type: api.ChatTypePrivate}, Text: "hi",
|
||||
}}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
@@ -594,7 +594,7 @@ func TestRouter_SerialMode_NoRace(t *testing.T) {
|
||||
for i := range ups {
|
||||
ups[i] = api.Update{UpdateID: int64(i + 1), Message: &api.Message{
|
||||
MessageID: int64(i + 1),
|
||||
Chat: api.Chat{ID: 1, Type: string(api.ChatTypePrivate)},
|
||||
Chat: api.Chat{ID: 1, Type: api.ChatTypePrivate},
|
||||
Text: "hi",
|
||||
}}
|
||||
}
|
||||
@@ -908,7 +908,7 @@ func TestRouter_ContextCancel_UnblocksWaitingAcquire(t *testing.T) {
|
||||
for i := range limit {
|
||||
lu.Send(api.Update{UpdateID: int64(i + 1), Message: &api.Message{
|
||||
MessageID: int64(i + 1),
|
||||
Chat: api.Chat{ID: 1, Type: string(api.ChatTypePrivate)},
|
||||
Chat: api.Chat{ID: 1, Type: api.ChatTypePrivate},
|
||||
Text: "hi",
|
||||
}})
|
||||
}
|
||||
@@ -919,7 +919,7 @@ func TestRouter_ContextCancel_UnblocksWaitingAcquire(t *testing.T) {
|
||||
// Send one more update — Run will block trying to acquire the full semaphore.
|
||||
lu.Send(api.Update{UpdateID: int64(limit + 1), Message: &api.Message{
|
||||
MessageID: int64(limit + 1),
|
||||
Chat: api.Chat{ID: 1, Type: string(api.ChatTypePrivate)},
|
||||
Chat: api.Chat{ID: 1, Type: api.ChatTypePrivate},
|
||||
Text: "extra",
|
||||
}})
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
go-telegram.raczylo.com
|
||||
+962
@@ -0,0 +1,962 @@
|
||||
<!doctype html>
|
||||
<html lang="en" class="scroll-smooth">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>go-telegram — Strongly-typed Go client for the Telegram Bot API</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="A fully-generated, strongly-typed Go client for the Telegram Bot API. 176 methods, 1408 generated tests, zero any in the public surface."
|
||||
/>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class'
|
||||
}
|
||||
</script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||
/>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<style>
|
||||
body { font-family: "Inter", sans-serif; }
|
||||
code, pre { font-family: "JetBrains Mono", monospace; }
|
||||
.theme-transition {
|
||||
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
.animate-fade-in-up { animation: fadeInUp 0.6s ease-out; }
|
||||
.animate-float { animation: float 3s ease-in-out infinite; }
|
||||
.glass {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
.dark .glass {
|
||||
background: rgba(17, 24, 39, 0.7);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.gradient-text {
|
||||
background: linear-gradient(135deg, #29B6F6 0%, #0288D1 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
.dark .gradient-text {
|
||||
background: linear-gradient(135deg, #4FC3F7 0%, #81D4FA 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
.shadow-modern { box-shadow: 0 10px 40px -10px rgba(0, 0, 0, 0.1); }
|
||||
.dark .shadow-modern { box-shadow: 0 10px 40px -10px rgba(0, 0, 0, 0.4); }
|
||||
html { scroll-behavior: smooth; }
|
||||
/* Syntax highlight tokens */
|
||||
.tok-kw { color: #c792ea; }
|
||||
.tok-fn { color: #82aaff; }
|
||||
.tok-str { color: #c3e88d; }
|
||||
.tok-cmt { color: #546e7a; font-style: italic; }
|
||||
.tok-pkg { color: #ffcb6b; }
|
||||
.tok-num { color: #f78c6c; }
|
||||
.tok-type { color: #4FC3F7; }
|
||||
</style>
|
||||
<script>
|
||||
if (localStorage.theme === "dark" || (!("theme" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches)) {
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 theme-transition">
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="fixed w-full glass shadow-modern z-50 theme-transition">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6">
|
||||
<div class="flex justify-between h-16 items-center">
|
||||
<a href="#" class="flex items-center hover:opacity-80 transition-opacity duration-300">
|
||||
<img src="logo-light.svg" alt="go-telegram logo" class="h-10 w-auto dark:hidden" />
|
||||
<img src="logo-dark.svg" alt="go-telegram logo" class="h-10 w-auto hidden dark:block" />
|
||||
</a>
|
||||
<div class="hidden md:flex space-x-6">
|
||||
<a href="#features" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 font-medium">Features</a>
|
||||
<a href="#comparison" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 font-medium">Comparison</a>
|
||||
<a href="#install" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 font-medium">Install</a>
|
||||
<a href="#usage" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 font-medium">Usage</a>
|
||||
<a href="#examples" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 font-medium">Examples</a>
|
||||
<a href="#advanced" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 font-medium">Advanced</a>
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram/tree/main/docs/reference" target="_blank" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 font-medium">Reference</a>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<button id="theme-toggle" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 p-2 min-w-[44px] min-h-[44px] flex items-center justify-center" aria-label="Toggle theme">
|
||||
<i class="fas fa-moon dark:hidden text-xl"></i>
|
||||
<i class="fas fa-sun hidden dark:inline text-xl"></i>
|
||||
</button>
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram" target="_blank" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 p-2 min-w-[44px] min-h-[44px] flex items-center justify-center" aria-label="View on GitHub">
|
||||
<i class="fab fa-github text-xl"></i>
|
||||
</a>
|
||||
<button id="mobile-menu-toggle" class="md:hidden text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 p-2 min-w-[44px] min-h-[44px] flex items-center justify-center" aria-label="Toggle menu">
|
||||
<i class="fas fa-bars text-xl" id="menu-open-icon"></i>
|
||||
<i class="fas fa-times text-xl hidden" id="menu-close-icon"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mobile-menu" class="hidden md:hidden border-t border-gray-200 dark:border-gray-700">
|
||||
<div class="px-4 py-3 space-y-1 bg-white dark:bg-gray-800">
|
||||
<a href="#features" class="block px-3 py-3 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded font-medium">Features</a>
|
||||
<a href="#comparison" class="block px-3 py-3 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded font-medium">Comparison</a>
|
||||
<a href="#install" class="block px-3 py-3 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded font-medium">Install</a>
|
||||
<a href="#usage" class="block px-3 py-3 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded font-medium">Usage</a>
|
||||
<a href="#examples" class="block px-3 py-3 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded font-medium">Examples</a>
|
||||
<a href="#advanced" class="block px-3 py-3 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded font-medium">Advanced</a>
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram/tree/main/docs/reference" target="_blank" class="block px-3 py-3 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded font-medium">Reference</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="relative pt-24 sm:pt-32 pb-12 sm:pb-20 overflow-hidden">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-sky-50 via-cyan-50 to-blue-50 dark:from-gray-900 dark:via-sky-900/20 dark:to-cyan-900/20 theme-transition"></div>
|
||||
<div class="absolute top-0 -left-4 w-72 h-72 bg-cyan-300 dark:bg-cyan-500 rounded-full mix-blend-multiply dark:mix-blend-soft-light filter blur-xl opacity-20 animate-float"></div>
|
||||
<div class="absolute top-0 -right-4 w-72 h-72 bg-sky-300 dark:bg-sky-500 rounded-full mix-blend-multiply dark:mix-blend-soft-light filter blur-xl opacity-20 animate-float" style="animation-delay: 1s;"></div>
|
||||
<div class="absolute -bottom-8 left-20 w-72 h-72 bg-blue-300 dark:bg-blue-500 rounded-full mix-blend-multiply dark:mix-blend-soft-light filter blur-xl opacity-20 animate-float" style="animation-delay: 2s;"></div>
|
||||
|
||||
<div class="relative max-w-6xl mx-auto px-4 sm:px-6">
|
||||
<div class="text-center">
|
||||
<div class="mb-6 sm:mb-8 flex justify-center animate-fade-in-up">
|
||||
<img src="logo-light.svg" alt="go-telegram logo" class="h-20 sm:h-24 md:h-32 w-auto dark:hidden" />
|
||||
<img src="logo-dark.svg" alt="go-telegram logo" class="h-20 sm:h-24 md:h-32 w-auto hidden dark:block" />
|
||||
</div>
|
||||
<h1 class="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold text-gray-900 dark:text-gray-100 mb-4 sm:mb-6 leading-tight animate-fade-in-up" style="animation-delay: 0.1s;">
|
||||
A fully-generated,<br /><span class="gradient-text">strongly-typed</span> Go client<br />for the Telegram Bot API
|
||||
</h1>
|
||||
<p class="text-base sm:text-lg md:text-xl text-gray-600 dark:text-gray-300 mb-8 sm:mb-10 max-w-2xl mx-auto leading-relaxed px-4 animate-fade-in-up" style="animation-delay: 0.2s;">
|
||||
176 methods. 1408 generated tests. Zero <code class="bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded text-sm">any</code> in the public surface.
|
||||
</p>
|
||||
<div class="flex flex-col sm:flex-row gap-3 sm:gap-4 justify-center mb-8 sm:mb-10 px-4 animate-fade-in-up" style="animation-delay: 0.3s;">
|
||||
<a href="#install" class="group relative bg-gradient-to-r from-sky-500 to-cyan-600 hover:from-sky-600 hover:to-cyan-700 text-white px-8 py-3 rounded-lg font-medium transition-all duration-300 min-h-[48px] flex items-center justify-center shadow-lg hover:shadow-xl hover:scale-105">
|
||||
<i class="fas fa-rocket mr-2"></i><span class="relative z-10">Get started</span>
|
||||
</a>
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram" target="_blank" class="group glass hover:shadow-lg text-gray-900 dark:text-gray-100 px-8 py-3 rounded-lg font-medium transition-all duration-300 min-h-[48px] flex items-center justify-center hover:scale-105">
|
||||
<i class="fab fa-github mr-2"></i>View on GitHub
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Stats row -->
|
||||
<div class="flex flex-wrap justify-center gap-x-3 gap-y-1 text-sm font-mono text-gray-500 dark:text-gray-400 mb-12 sm:mb-16 px-4 animate-fade-in-up" style="animation-delay: 0.35s;">
|
||||
<span>176 methods</span>
|
||||
<span class="text-gray-300 dark:text-gray-600">·</span>
|
||||
<span>301 types</span>
|
||||
<span class="text-gray-300 dark:text-gray-600">·</span>
|
||||
<span>1,408 tests</span>
|
||||
<span class="text-gray-300 dark:text-gray-600">·</span>
|
||||
<span>MIT licensed</span>
|
||||
</div>
|
||||
|
||||
<!-- Code preview card -->
|
||||
<div class="mt-4 max-w-2xl mx-auto px-4 animate-fade-in-up animate-float" style="animation-delay: 0.4s; animation-duration: 4s;">
|
||||
<div class="relative group">
|
||||
<div class="absolute -inset-1 bg-gradient-to-r from-sky-500 to-cyan-600 rounded-xl blur opacity-25 group-hover:opacity-50 transition duration-500"></div>
|
||||
<div class="relative bg-gradient-to-br from-gray-900 to-gray-800 rounded-xl p-5 text-left border border-gray-700">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span class="w-3 h-3 rounded-full bg-red-500"></span>
|
||||
<span class="w-3 h-3 rounded-full bg-yellow-500"></span>
|
||||
<span class="w-3 h-3 rounded-full bg-green-500"></span>
|
||||
<span class="ml-2 text-xs text-gray-500 font-mono">echo_bot.go</span>
|
||||
</div>
|
||||
<pre class="text-sm text-gray-100 overflow-x-auto"><code class="font-mono"><span class="tok-kw">package</span> <span class="tok-pkg">main</span>
|
||||
|
||||
<span class="tok-kw">import</span> (
|
||||
<span class="tok-str">"context"</span>
|
||||
<span class="tok-str">"log"</span>
|
||||
<span class="tok-str">"os"</span>
|
||||
|
||||
<span class="tok-str">"github.com/lukaszraczylo/go-telegram/api"</span>
|
||||
<span class="tok-str">"github.com/lukaszraczylo/go-telegram/client"</span>
|
||||
<span class="tok-str">"github.com/lukaszraczylo/go-telegram/dispatch"</span>
|
||||
)
|
||||
|
||||
<span class="tok-kw">func</span> <span class="tok-fn">main</span>() {
|
||||
bot, _ := <span class="tok-pkg">client</span>.<span class="tok-fn">NewRetryDoer</span>(os.<span class="tok-fn">Getenv</span>(<span class="tok-str">"BOT_TOKEN"</span>), <span class="tok-kw">nil</span>)
|
||||
d := <span class="tok-pkg">dispatch</span>.<span class="tok-fn">New</span>(bot)
|
||||
|
||||
d.<span class="tok-fn">OnMessage</span>(<span class="tok-kw">func</span>(ctx <span class="tok-type">context.Context</span>, msg *<span class="tok-type">api.Message</span>) {
|
||||
bot.<span class="tok-fn">SendMessage</span>(ctx, &<span class="tok-type">api.SendMessageParams</span>{
|
||||
ChatID: <span class="tok-pkg">api</span>.<span class="tok-fn">ChatIDFromInt</span>(msg.Chat.ID),
|
||||
Text: msg.Text,
|
||||
})
|
||||
})
|
||||
|
||||
<span class="tok-kw">if</span> err := d.<span class="tok-fn">Run</span>(<span class="tok-pkg">context</span>.<span class="tok-fn">Background</span>()); err != <span class="tok-kw">nil</span> {
|
||||
<span class="tok-pkg">log</span>.<span class="tok-fn">Fatal</span>(err)
|
||||
}
|
||||
}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Features Section -->
|
||||
<section id="features" class="py-12 sm:py-16 md:py-20 bg-white dark:bg-gray-900 theme-transition">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6">
|
||||
<div class="text-center mb-8 sm:mb-12">
|
||||
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-3 sm:mb-4">Features</h2>
|
||||
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-300 px-4">Built for correctness, composability, and production reliability</p>
|
||||
</div>
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div class="glass p-5 rounded-xl group hover:shadow-lg transition-all duration-300">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-sky-500 to-sky-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-code-branch text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1">Generated from the live docs</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">IR + emitter pipeline runs weekly; a PR opens automatically for any Telegram-side change.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="glass p-5 rounded-xl group hover:shadow-lg transition-all duration-300">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-emerald-500 to-emerald-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-shield-halved text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1">No <code class="text-xs bg-emerald-100 dark:bg-emerald-900 px-1 rounded">any</code> in the public API</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Typed unions — <code class="text-xs">ChatID</code>, <code class="text-xs">MessageOrBool</code> — sealed interfaces with marker methods and auto-decode.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="glass p-5 rounded-xl group hover:shadow-lg transition-all duration-300">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-purple-500 to-purple-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-plug text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1">Pluggable transport + codec</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Drop in fasthttp, sonic, or goccy/go-json with a one-line swap.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="glass p-5 rounded-xl group hover:shadow-lg transition-all duration-300">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-amber-500 to-amber-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-bolt text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1">Production-ready out of the box</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Retry middleware honouring <code class="text-xs">retry_after</code>, panic recovery, structured errors with sentinels.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="glass p-5 rounded-xl group hover:shadow-lg transition-all duration-300">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-rose-500 to-rose-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-route text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1">Typed dispatcher</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Generic <code class="text-xs">Handler[T]</code>, composable filters, conversation handler with pluggable storage, per-update goroutine pool.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="glass p-5 rounded-xl group hover:shadow-lg transition-all duration-300">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-teal-500 to-teal-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-circle-check text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1">Self-verifying codegen</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Every regen runs scrape → audit → emit → 1408 generated tests. Nothing ships without passing.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Comparison Section -->
|
||||
<section id="comparison" class="py-12 sm:py-16 md:py-20 bg-gray-50 dark:bg-gray-800 theme-transition">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6">
|
||||
<div class="text-center mb-8 sm:mb-12">
|
||||
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-3 sm:mb-4">Comparison</h2>
|
||||
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-300 px-4">How go-telegram compares to other Go Telegram bot libraries</p>
|
||||
</div>
|
||||
|
||||
<!-- Mobile cards -->
|
||||
<div class="block md:hidden space-y-4 mb-8">
|
||||
<div class="glass rounded-xl p-5 shadow-modern">
|
||||
<div class="flex items-center gap-3 mb-4 pb-3 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-sky-500 to-cyan-600 flex items-center justify-center">
|
||||
<i class="fas fa-paper-plane text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-gray-900 dark:text-gray-100">go-telegram</h4>
|
||||
<p class="text-xs text-gray-500">HTML scraper · this library</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-2 text-sm">
|
||||
<div class="flex items-center gap-2"><span class="text-green-500 font-bold">✓</span><span class="text-gray-600 dark:text-gray-400">Generated from spec (HTML)</span></div>
|
||||
<div class="flex items-center gap-2"><span class="text-green-500 font-bold">✓</span><span class="text-gray-600 dark:text-gray-400">Typed unions, no any</span></div>
|
||||
<div class="flex items-center gap-2"><span class="text-green-500 font-bold">✓</span><span class="text-gray-600 dark:text-gray-400">1,408 auto-generated tests</span></div>
|
||||
<div class="flex items-center gap-2"><span class="text-green-500 font-bold">✓</span><span class="text-gray-600 dark:text-gray-400">Conversation handler</span></div>
|
||||
<div class="flex items-center gap-2"><span class="text-green-500 font-bold">✓</span><span class="text-gray-600 dark:text-gray-400">Retry middleware (retry_after)</span></div>
|
||||
<div class="flex items-center gap-2"><span class="text-green-500 font-bold">✓</span><span class="text-gray-600 dark:text-gray-400">Pluggable JSON codec</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="glass rounded-xl p-5 shadow-modern">
|
||||
<div class="flex items-center gap-3 mb-4 pb-3 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-indigo-600 to-indigo-700 flex items-center justify-center">
|
||||
<span class="text-white font-bold text-xs">gtb</span>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-gray-900 dark:text-gray-100">gotgbot/v2</h4>
|
||||
<p class="text-xs text-gray-500">JSON spec · popular library</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-2 text-sm">
|
||||
<div class="flex items-center gap-2"><span class="text-green-500 font-bold">✓</span><span class="text-gray-600 dark:text-gray-400">Generated from spec (JSON)</span></div>
|
||||
<div class="flex items-center gap-2"><span class="text-yellow-500 font-bold">△</span><span class="text-gray-600 dark:text-gray-400">Partial typed unions</span></div>
|
||||
<div class="flex items-center gap-2"><span class="text-red-500 font-bold">✗</span><span class="text-gray-400">Auto-generated tests</span></div>
|
||||
<div class="flex items-center gap-2"><span class="text-green-500 font-bold">✓</span><span class="text-gray-600 dark:text-gray-400">Conversation handler</span></div>
|
||||
<div class="flex items-center gap-2"><span class="text-yellow-500 font-bold">△</span><span class="text-gray-600 dark:text-gray-400">User-implemented retry</span></div>
|
||||
<div class="flex items-center gap-2"><span class="text-red-500 font-bold">✗</span><span class="text-gray-400">Pluggable JSON codec</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="glass rounded-xl p-5 shadow-modern">
|
||||
<div class="flex items-center gap-3 mb-4 pb-3 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-gray-600 to-gray-700 flex items-center justify-center">
|
||||
<span class="text-white font-bold text-xs">tba</span>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-gray-900 dark:text-gray-100">telegram-bot-api/v5</h4>
|
||||
<p class="text-xs text-gray-500">Hand-coded</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-2 text-sm">
|
||||
<div class="flex items-center gap-2"><span class="text-red-500 font-bold">✗</span><span class="text-gray-400">Generated from spec</span></div>
|
||||
<div class="flex items-center gap-2"><span class="text-red-500 font-bold">✗</span><span class="text-gray-400">Typed unions</span></div>
|
||||
<div class="flex items-center gap-2"><span class="text-red-500 font-bold">✗</span><span class="text-gray-400">Auto-generated tests</span></div>
|
||||
<div class="flex items-center gap-2"><span class="text-red-500 font-bold">✗</span><span class="text-gray-400">Conversation handler</span></div>
|
||||
<div class="flex items-center gap-2"><span class="text-red-500 font-bold">✗</span><span class="text-gray-400">Retry middleware</span></div>
|
||||
<div class="flex items-center gap-2"><span class="text-red-500 font-bold">✗</span><span class="text-gray-400">Pluggable JSON codec</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Desktop table -->
|
||||
<div class="hidden md:block glass rounded-xl overflow-hidden shadow-modern">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gradient-to-r from-sky-500 to-cyan-600 text-white">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left font-semibold">Feature</th>
|
||||
<th class="px-4 py-3 text-center font-semibold">go-telegram</th>
|
||||
<th class="px-4 py-3 text-center font-semibold"><a href="https://github.com/PaulSonOfLars/gotgbot" class="hover:underline" target="_blank">gotgbot/v2</a></th>
|
||||
<th class="px-4 py-3 text-center font-semibold"><a href="https://github.com/go-telegram-bot-api/telegram-bot-api" class="hover:underline" target="_blank">telegram-bot-api/v5</a></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/50">
|
||||
<td class="px-4 py-3 text-gray-700 dark:text-gray-300 font-medium">Generated from spec</td>
|
||||
<td class="px-4 py-3 text-center"><span class="text-green-500 font-bold">✓</span> <span class="text-xs text-gray-500">HTML scraper</span></td>
|
||||
<td class="px-4 py-3 text-center"><span class="text-green-500 font-bold">✓</span> <span class="text-xs text-gray-500">JSON spec</span></td>
|
||||
<td class="px-4 py-3 text-center"><span class="text-red-500 font-bold">✗</span> <span class="text-xs text-gray-500">hand-coded</span></td>
|
||||
</tr>
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/50">
|
||||
<td class="px-4 py-3 text-gray-700 dark:text-gray-300 font-medium">Typed unions (no <code class="text-xs">any</code>)</td>
|
||||
<td class="px-4 py-3 text-center"><span class="text-green-500 font-bold">✓</span> <span class="text-xs text-gray-500">ChatID, MessageOrBool, sealed interfaces</span></td>
|
||||
<td class="px-4 py-3 text-center"><span class="text-yellow-500 font-bold">△</span> <span class="text-xs text-gray-500">partial</span></td>
|
||||
<td class="px-4 py-3 text-center"><span class="text-red-500 font-bold">✗</span></td>
|
||||
</tr>
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/50">
|
||||
<td class="px-4 py-3 text-gray-700 dark:text-gray-300 font-medium">Auto-generated tests</td>
|
||||
<td class="px-4 py-3 text-center"><span class="text-green-500 font-bold">✓</span> <span class="text-xs text-gray-500">1,408 (8 scenarios/method)</span></td>
|
||||
<td class="px-4 py-3 text-center"><span class="text-red-500 font-bold">✗</span></td>
|
||||
<td class="px-4 py-3 text-center"><span class="text-red-500 font-bold">✗</span></td>
|
||||
</tr>
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/50">
|
||||
<td class="px-4 py-3 text-gray-700 dark:text-gray-300 font-medium">Conversation handler</td>
|
||||
<td class="px-4 py-3 text-center"><span class="text-green-500 font-bold">✓</span> <span class="text-xs text-gray-500">pluggable storage</span></td>
|
||||
<td class="px-4 py-3 text-center"><span class="text-green-500 font-bold">✓</span></td>
|
||||
<td class="px-4 py-3 text-center"><span class="text-red-500 font-bold">✗</span></td>
|
||||
</tr>
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/50">
|
||||
<td class="px-4 py-3 text-gray-700 dark:text-gray-300 font-medium">Retry middleware</td>
|
||||
<td class="px-4 py-3 text-center"><span class="text-green-500 font-bold">✓</span> <span class="text-xs text-gray-500">honours retry_after</span></td>
|
||||
<td class="px-4 py-3 text-center"><span class="text-yellow-500 font-bold">△</span> <span class="text-xs text-gray-500">user-implemented</span></td>
|
||||
<td class="px-4 py-3 text-center"><span class="text-red-500 font-bold">✗</span></td>
|
||||
</tr>
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/50">
|
||||
<td class="px-4 py-3 text-gray-700 dark:text-gray-300 font-medium">Pluggable JSON codec</td>
|
||||
<td class="px-4 py-3 text-center"><span class="text-green-500 font-bold">✓</span></td>
|
||||
<td class="px-4 py-3 text-center"><span class="text-red-500 font-bold">✗</span></td>
|
||||
<td class="px-4 py-3 text-center"><span class="text-red-500 font-bold">✗</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Installation Section -->
|
||||
<section id="install" class="py-12 sm:py-16 md:py-20 bg-white dark:bg-gray-900 theme-transition">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6">
|
||||
<div class="text-center mb-10 sm:mb-12">
|
||||
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-3 sm:mb-4">Installation</h2>
|
||||
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-300 px-4">One command. No CGO, no system deps.</p>
|
||||
</div>
|
||||
<div class="space-y-4 sm:space-y-6">
|
||||
<div class="glass p-6 sm:p-8 rounded-xl shadow-modern hover:shadow-xl transition-all duration-300">
|
||||
<div class="flex items-center mb-4">
|
||||
<i class="fas fa-cubes text-sky-500 dark:text-sky-400 text-2xl mr-3"></i>
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-gray-100">go get</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 text-sm">Requires Go 1.21+</p>
|
||||
</div>
|
||||
</div>
|
||||
<div onclick="copyToClipboard('go get github.com/lukaszraczylo/go-telegram', this)" class="relative bg-gradient-to-br from-gray-900 to-gray-800 dark:from-gray-950 dark:to-black text-gray-100 p-4 rounded-lg text-sm cursor-pointer group overflow-x-auto border border-gray-700 hover:border-sky-500 transition-all duration-300">
|
||||
<code class="block whitespace-nowrap font-mono">go get github.com/lukaszraczylo/go-telegram</code>
|
||||
<div class="absolute top-3 right-3"><i class="fas fa-copy text-gray-500 group-hover:text-sky-400 transition-colors duration-300"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="glass p-6 sm:p-8 rounded-xl shadow-modern hover:shadow-xl transition-all duration-300">
|
||||
<div class="flex items-center mb-4">
|
||||
<i class="fas fa-book text-cyan-500 dark:text-cyan-400 text-2xl mr-3"></i>
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-gray-100">pkg.go.dev</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 text-sm">Full API reference</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="https://pkg.go.dev/github.com/lukaszraczylo/go-telegram" target="_blank" class="block text-center bg-gradient-to-r from-cyan-600 to-sky-700 hover:from-cyan-700 hover:to-sky-800 text-white px-4 py-3 rounded-lg text-sm font-medium shadow-lg hover:shadow-xl transition-all duration-300 hover:scale-105">
|
||||
<i class="fas fa-external-link-alt mr-2"></i>View on pkg.go.dev
|
||||
</a>
|
||||
</div>
|
||||
<div class="glass p-6 sm:p-8 rounded-xl shadow-modern hover:shadow-xl transition-all duration-300">
|
||||
<div class="flex items-center mb-4">
|
||||
<i class="fas fa-book-open text-emerald-500 dark:text-emerald-400 text-2xl mr-3"></i>
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-gray-100">Markdown reference</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 text-sm">Auto-generated, browse on GitHub</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram/tree/main/docs/reference" target="_blank" class="block text-center bg-gradient-to-r from-emerald-600 to-teal-700 hover:from-emerald-700 hover:to-teal-800 text-white px-4 py-3 rounded-lg text-sm font-medium shadow-lg hover:shadow-xl transition-all duration-300 hover:scale-105">
|
||||
<i class="fas fa-external-link-alt mr-2"></i>Browse reference docs
|
||||
</a>
|
||||
</div>
|
||||
<div class="glass p-6 sm:p-8 rounded-xl shadow-modern hover:shadow-xl transition-all duration-300">
|
||||
<div class="flex items-center mb-4">
|
||||
<i class="fas fa-file-alt text-blue-500 dark:text-blue-400 text-2xl mr-3"></i>
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-gray-100">Telegram Bot API reference</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 text-sm">Upstream spec this library tracks</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="https://core.telegram.org/bots/api" target="_blank" class="block text-center bg-gradient-to-r from-blue-500 to-blue-700 hover:from-blue-600 hover:to-blue-800 text-white px-4 py-3 rounded-lg text-sm font-medium shadow-lg hover:shadow-xl transition-all duration-300 hover:scale-105">
|
||||
<i class="fas fa-external-link-alt mr-2"></i>core.telegram.org/bots/api
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Usage Section -->
|
||||
<section id="usage" class="py-12 sm:py-16 md:py-20 bg-gray-50 dark:bg-gray-800 theme-transition">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6">
|
||||
<div class="text-center mb-10 sm:mb-12">
|
||||
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-3 sm:mb-4">Usage</h2>
|
||||
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-300 px-4">Echo bot — the minimal working example</p>
|
||||
</div>
|
||||
|
||||
<div class="relative group mb-10">
|
||||
<div class="absolute -inset-1 bg-gradient-to-r from-sky-500 to-cyan-600 rounded-xl blur opacity-25 group-hover:opacity-50 transition duration-300"></div>
|
||||
<div class="relative bg-gradient-to-br from-gray-900 to-gray-800 rounded-xl p-4 sm:p-6 overflow-x-auto border border-gray-700">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-file-code text-sky-400 mr-2"></i>
|
||||
<span class="text-sky-400 text-sm font-mono font-semibold">echo_bot.go</span>
|
||||
</div>
|
||||
<button onclick="copyToClipboard(document.getElementById('echo-bot-code').textContent, this)" class="text-gray-400 hover:text-sky-400 transition-colors duration-300" aria-label="Copy code">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
<pre class="text-xs sm:text-sm text-gray-100 overflow-x-auto"><code id="echo-bot-code" class="font-mono">package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/lukaszraczylo/go-telegram/api"
|
||||
"github.com/lukaszraczylo/go-telegram/client"
|
||||
"github.com/lukaszraczylo/go-telegram/dispatch"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// NewRetryDoer wraps the default transport with retry middleware
|
||||
// that honours Telegram's retry_after field automatically.
|
||||
bot, err := client.NewRetryDoer(os.Getenv("BOT_TOKEN"), nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
d := dispatch.New(bot)
|
||||
|
||||
// Handler[T] is generic — the type parameter is the concrete update type.
|
||||
d.OnMessage(func(ctx context.Context, msg *api.Message) {
|
||||
_, err := bot.SendMessage(ctx, &api.SendMessageParams{
|
||||
// ChatIDFromInt returns a typed ChatID — no interface{} here.
|
||||
ChatID: api.ChatIDFromInt(msg.Chat.ID),
|
||||
Text: msg.Text,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("send error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
if err := d.Run(context.Background()); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Codegen pipeline -->
|
||||
<div id="codegen" class="glass p-6 sm:p-8 rounded-xl shadow-modern">
|
||||
<h3 class="text-lg sm:text-xl font-semibold text-gray-900 dark:text-gray-100 mb-2"><i class="fas fa-cogs text-sky-500 mr-2"></i>Codegen pipeline</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-6">The emitter scrapes the live Telegram Bot API docs (HTML), builds an intermediate representation (<code class="text-xs">api.json</code>), then generates Go code and tests. An audit step validates every method signature against the IR before emission.</p>
|
||||
|
||||
<!-- Flow diagram -->
|
||||
<div class="flex flex-wrap items-center justify-center gap-2 sm:gap-3 mb-6 text-sm font-mono">
|
||||
<div class="px-3 py-2 bg-sky-100 dark:bg-sky-900/40 text-sky-800 dark:text-sky-300 rounded-lg border border-sky-200 dark:border-sky-700 flex items-center gap-2">
|
||||
<i class="fas fa-globe text-sky-500"></i> HTML docs
|
||||
</div>
|
||||
<span class="text-gray-400 font-sans">→</span>
|
||||
<div class="px-3 py-2 bg-purple-100 dark:bg-purple-900/40 text-purple-800 dark:text-purple-300 rounded-lg border border-purple-200 dark:border-purple-700 flex items-center gap-2">
|
||||
<i class="fas fa-database text-purple-500"></i> IR (api.json)
|
||||
</div>
|
||||
<span class="text-gray-400 font-sans">→</span>
|
||||
<div class="px-3 py-2 bg-amber-100 dark:bg-amber-900/40 text-amber-800 dark:text-amber-300 rounded-lg border border-amber-200 dark:border-amber-700 flex items-center gap-2">
|
||||
<i class="fas fa-search text-amber-500"></i> audit
|
||||
</div>
|
||||
<span class="text-gray-400 font-sans">→</span>
|
||||
<div class="px-3 py-2 bg-emerald-100 dark:bg-emerald-900/40 text-emerald-800 dark:text-emerald-300 rounded-lg border border-emerald-200 dark:border-emerald-700 flex items-center gap-2">
|
||||
<i class="fas fa-code text-emerald-500"></i> Go code + tests
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div onclick="copyToClipboard('make snapshot && make regen', this)" class="relative bg-gradient-to-br from-gray-900 to-gray-800 text-gray-100 p-4 rounded-lg text-sm cursor-pointer group overflow-x-auto border border-gray-700 hover:border-sky-500 transition-all duration-300">
|
||||
<code class="block whitespace-nowrap font-mono">make snapshot && make regen</code>
|
||||
<div class="absolute top-3 right-3"><i class="fas fa-copy text-gray-500 group-hover:text-sky-400 transition-colors duration-300"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Examples Section -->
|
||||
<section id="examples" class="py-12 sm:py-16 md:py-20 bg-white dark:bg-gray-900 theme-transition">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6">
|
||||
<div class="text-center mb-8 sm:mb-12">
|
||||
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-3 sm:mb-4">Examples</h2>
|
||||
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-300 px-4">14 runnable bots covering the most common patterns</p>
|
||||
</div>
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<!-- Echo -->
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram/tree/main/examples/echo" target="_blank" class="glass p-4 rounded-xl group hover:shadow-lg transition-all duration-300 flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-sky-500 to-sky-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-reply text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1 group-hover:text-sky-600 dark:group-hover:text-sky-400">echo</h3>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Minimal echo bot — get up and running in 30 lines.</p>
|
||||
</div>
|
||||
</a>
|
||||
<!-- Callback -->
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram/tree/main/examples/callback" target="_blank" class="glass p-4 rounded-xl group hover:shadow-lg transition-all duration-300 flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-purple-500 to-purple-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-hand-pointer text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1 group-hover:text-sky-600 dark:group-hover:text-sky-400">callback</h3>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Inline keyboard buttons and callback query handling.</p>
|
||||
</div>
|
||||
</a>
|
||||
<!-- Inline -->
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram/tree/main/examples/inline" target="_blank" class="glass p-4 rounded-xl group hover:shadow-lg transition-all duration-300 flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-cyan-500 to-cyan-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-search text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1 group-hover:text-sky-600 dark:group-hover:text-sky-400">inline</h3>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Inline mode queries and result sets.</p>
|
||||
</div>
|
||||
</a>
|
||||
<!-- Conversation -->
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram/tree/main/examples/conversation" target="_blank" class="glass p-4 rounded-xl group hover:shadow-lg transition-all duration-300 flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-emerald-500 to-emerald-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-comments text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1 group-hover:text-sky-600 dark:group-hover:text-sky-400">conversation</h3>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Multi-step conversation flows with pluggable state storage.</p>
|
||||
</div>
|
||||
</a>
|
||||
<!-- Stateful -->
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram/tree/main/examples/stateful" target="_blank" class="glass p-4 rounded-xl group hover:shadow-lg transition-all duration-300 flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-amber-500 to-amber-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-memory text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1 group-hover:text-sky-600 dark:group-hover:text-sky-400">stateful</h3>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Per-user state machine pattern.</p>
|
||||
</div>
|
||||
</a>
|
||||
<!-- Admin -->
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram/tree/main/examples/admin" target="_blank" class="glass p-4 rounded-xl group hover:shadow-lg transition-all duration-300 flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-rose-500 to-rose-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-user-shield text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1 group-hover:text-sky-600 dark:group-hover:text-sky-400">admin</h3>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Admin commands — ban, kick, restrict members.</p>
|
||||
</div>
|
||||
</a>
|
||||
<!-- Middleware -->
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram/tree/main/examples/middleware" target="_blank" class="glass p-4 rounded-xl group hover:shadow-lg transition-all duration-300 flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-indigo-500 to-indigo-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-layer-group text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1 group-hover:text-sky-600 dark:group-hover:text-sky-400">middleware</h3>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Composable middleware chain — logging, auth, rate limiting.</p>
|
||||
</div>
|
||||
</a>
|
||||
<!-- Files -->
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram/tree/main/examples/files" target="_blank" class="glass p-4 rounded-xl group hover:shadow-lg transition-all duration-300 flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-teal-500 to-teal-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-paperclip text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1 group-hover:text-sky-600 dark:group-hover:text-sky-400">files</h3>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Upload and download photos, documents, audio.</p>
|
||||
</div>
|
||||
</a>
|
||||
<!-- Webhook -->
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram/tree/main/examples/webhook" target="_blank" class="glass p-4 rounded-xl group hover:shadow-lg transition-all duration-300 flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-lime-500 to-lime-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-network-wired text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1 group-hover:text-sky-600 dark:group-hover:text-sky-400">webhook</h3>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Webhook server instead of long-polling.</p>
|
||||
</div>
|
||||
</a>
|
||||
<!-- Polls -->
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram/tree/main/examples/polls" target="_blank" class="glass p-4 rounded-xl group hover:shadow-lg transition-all duration-300 flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-fuchsia-500 to-fuchsia-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-poll text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1 group-hover:text-sky-600 dark:group-hover:text-sky-400">polls</h3>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Send polls and handle poll-answer updates.</p>
|
||||
</div>
|
||||
</a>
|
||||
<!-- Payments -->
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram/tree/main/examples/payments" target="_blank" class="glass p-4 rounded-xl group hover:shadow-lg transition-all duration-300 flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-yellow-500 to-yellow-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-credit-card text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1 group-hover:text-sky-600 dark:group-hover:text-sky-400">payments</h3>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Telegram Payments — invoices, pre-checkout, successful payment.</p>
|
||||
</div>
|
||||
</a>
|
||||
<!-- Pagination -->
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram/tree/main/examples/pagination" target="_blank" class="glass p-4 rounded-xl group hover:shadow-lg transition-all duration-300 flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-slate-500 to-slate-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-chevron-right text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1 group-hover:text-sky-600 dark:group-hover:text-sky-400">pagination</h3>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Paginated inline keyboards for long result sets.</p>
|
||||
</div>
|
||||
</a>
|
||||
<!-- Welcome -->
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram/tree/main/examples/welcome" target="_blank" class="glass p-4 rounded-xl group hover:shadow-lg transition-all duration-300 flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-green-500 to-green-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-door-open text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1 group-hover:text-sky-600 dark:group-hover:text-sky-400">welcome</h3>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Greet new members joining a group or channel.</p>
|
||||
</div>
|
||||
</a>
|
||||
<!-- Moderation -->
|
||||
<a href="https://github.com/lukaszraczylo/go-telegram/tree/main/examples/moderation" target="_blank" class="glass p-4 rounded-xl group hover:shadow-lg transition-all duration-300 flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-red-500 to-red-600 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-gavel text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-1 group-hover:text-sky-600 dark:group-hover:text-sky-400">moderation</h3>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Automated content moderation with filter chains.</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Advanced Section -->
|
||||
<section id="advanced" class="py-12 sm:py-16 md:py-20 bg-gray-50 dark:bg-gray-800 theme-transition">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6">
|
||||
<div class="text-center mb-10 sm:mb-12">
|
||||
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-3 sm:mb-4">Advanced</h2>
|
||||
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-300 px-4">Power-user patterns — expand to read</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<!-- Conversation flows -->
|
||||
<details class="glass rounded-xl overflow-hidden group">
|
||||
<summary class="flex items-center justify-between p-5 cursor-pointer list-none select-none">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-9 h-9 rounded-lg bg-gradient-to-br from-emerald-500 to-emerald-600 flex items-center justify-center flex-shrink-0">
|
||||
<i class="fas fa-comments text-white text-sm"></i>
|
||||
</div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100">Conversation flows</h3>
|
||||
</div>
|
||||
<i class="fas fa-chevron-down text-gray-400 transition-transform duration-300 group-open:rotate-180"></i>
|
||||
</summary>
|
||||
<div class="px-5 pb-5 border-t border-gray-200 dark:border-gray-700">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-4 mb-4">
|
||||
The conversation handler chains multiple message steps and stores state between them. Any storage backend implementing the
|
||||
<code class="text-xs bg-gray-100 dark:bg-gray-800 px-1 rounded">StateStorage</code> interface works — in-memory, Redis, Postgres.
|
||||
</p>
|
||||
<div class="bg-gradient-to-br from-gray-900 to-gray-800 rounded-xl p-4 text-sm text-gray-100 overflow-x-auto border border-gray-700">
|
||||
<pre class="font-mono"><code>conv := conversation.New(storage)
|
||||
|
||||
conv.AddState("ask_name", func(ctx context.Context, msg *api.Message) (string, error) {
|
||||
bot.SendMessage(ctx, &api.SendMessageParams{
|
||||
ChatID: api.ChatIDFromInt(msg.Chat.ID),
|
||||
Text: "What's your name?",
|
||||
})
|
||||
return "ask_age", nil
|
||||
})
|
||||
|
||||
conv.AddState("ask_age", func(ctx context.Context, msg *api.Message) (string, error) {
|
||||
name := conv.GetData(ctx, "name")
|
||||
// ... handle age input
|
||||
return conversation.Done, nil
|
||||
})
|
||||
|
||||
d.OnMessage(conv.Handler("start"))</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Custom filters -->
|
||||
<details class="glass rounded-xl overflow-hidden group">
|
||||
<summary class="flex items-center justify-between p-5 cursor-pointer list-none select-none">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-9 h-9 rounded-lg bg-gradient-to-br from-purple-500 to-purple-600 flex items-center justify-center flex-shrink-0">
|
||||
<i class="fas fa-filter text-white text-sm"></i>
|
||||
</div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100">Custom filters</h3>
|
||||
</div>
|
||||
<i class="fas fa-chevron-down text-gray-400 transition-transform duration-300 group-open:rotate-180"></i>
|
||||
</summary>
|
||||
<div class="px-5 pb-5 border-t border-gray-200 dark:border-gray-700">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-4 mb-4">
|
||||
Filters compose with <code class="text-xs bg-gray-100 dark:bg-gray-800 px-1 rounded">And</code>, <code class="text-xs bg-gray-100 dark:bg-gray-800 px-1 rounded">Or</code>, <code class="text-xs bg-gray-100 dark:bg-gray-800 px-1 rounded">Not</code>.
|
||||
A filter is just a function <code class="text-xs bg-gray-100 dark:bg-gray-800 px-1 rounded">func(*api.Update) bool</code>.
|
||||
</p>
|
||||
<div class="bg-gradient-to-br from-gray-900 to-gray-800 rounded-xl p-4 text-sm text-gray-100 overflow-x-auto border border-gray-700">
|
||||
<pre class="font-mono"><code>// Only handle private messages from admins
|
||||
adminOnly := filters.And(
|
||||
filters.IsPrivate,
|
||||
filters.UserIDIn(adminIDs...),
|
||||
)
|
||||
|
||||
d.OnMessage(func(ctx context.Context, msg *api.Message) {
|
||||
// handler body
|
||||
}, adminOnly)
|
||||
|
||||
// Custom filter — any function works
|
||||
isLong := func(u *api.Update) bool {
|
||||
return u.Message != nil && len(u.Message.Text) > 200
|
||||
}
|
||||
d.OnMessage(handleLongMsg, isLong)</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Custom HTTP/JSON -->
|
||||
<details class="glass rounded-xl overflow-hidden group">
|
||||
<summary class="flex items-center justify-between p-5 cursor-pointer list-none select-none">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-9 h-9 rounded-lg bg-gradient-to-br from-amber-500 to-amber-600 flex items-center justify-center flex-shrink-0">
|
||||
<i class="fas fa-plug text-white text-sm"></i>
|
||||
</div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-gray-100">Custom HTTP / JSON codec</h3>
|
||||
</div>
|
||||
<i class="fas fa-chevron-down text-gray-400 transition-transform duration-300 group-open:rotate-180"></i>
|
||||
</summary>
|
||||
<div class="px-5 pb-5 border-t border-gray-200 dark:border-gray-700">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-4 mb-4">
|
||||
Pass a <code class="text-xs bg-gray-100 dark:bg-gray-800 px-1 rounded">transport.Options</code> to swap the HTTP client or JSON codec.
|
||||
Useful for squeezing throughput on high-volume bots.
|
||||
</p>
|
||||
<div class="bg-gradient-to-br from-gray-900 to-gray-800 rounded-xl p-4 text-sm text-gray-100 overflow-x-auto border border-gray-700">
|
||||
<pre class="font-mono"><code>import (
|
||||
"github.com/lukaszraczylo/go-telegram/client"
|
||||
"github.com/lukaszraczylo/go-telegram/transport"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
opts := &client.Options{
|
||||
Transport: transport.NewFasthttpTransport(nil),
|
||||
Codec: transport.JSONCodec(jsoniter.ConfigCompatibleWithStandardLibrary),
|
||||
}
|
||||
|
||||
bot, err := client.NewRetryDoer(token, opts)</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="bg-gray-900 dark:bg-black text-gray-400 py-8 sm:py-10 theme-transition">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6">
|
||||
<div class="grid grid-cols-2 sm:grid-cols-2 md:grid-cols-4 gap-6 sm:gap-8">
|
||||
<div class="col-span-2 sm:col-span-1">
|
||||
<img src="logo-dark.svg" alt="go-telegram logo" class="h-10 sm:h-12 w-auto mb-3 sm:mb-4" />
|
||||
<p class="text-xs sm:text-sm">Fully-generated, strongly-typed Go client for the Telegram Bot API.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-white font-semibold mb-3 sm:mb-4 text-sm sm:text-base">Links</h3>
|
||||
<ul class="space-y-1.5 sm:space-y-2 text-xs sm:text-sm">
|
||||
<li><a href="https://github.com/lukaszraczylo/go-telegram" target="_blank" class="hover:text-white transition"><i class="fab fa-github mr-2"></i>GitHub</a></li>
|
||||
<li><a href="https://github.com/lukaszraczylo/go-telegram/issues" target="_blank" class="hover:text-white transition"><i class="fas fa-bug mr-2"></i>Issues</a></li>
|
||||
<li><a href="https://github.com/lukaszraczylo/go-telegram/releases" target="_blank" class="hover:text-white transition"><i class="fas fa-tag mr-2"></i>Releases</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-white font-semibold mb-3 sm:mb-4 text-sm sm:text-base">Docs</h3>
|
||||
<ul class="space-y-1.5 sm:space-y-2 text-xs sm:text-sm">
|
||||
<li><a href="https://pkg.go.dev/github.com/lukaszraczylo/go-telegram" target="_blank" class="hover:text-white transition"><i class="fas fa-book mr-1"></i> pkg.go.dev</a></li>
|
||||
<li><a href="https://core.telegram.org/bots/api" target="_blank" class="hover:text-white transition">Telegram Bot API</a></li>
|
||||
<li><a href="https://github.com/lukaszraczylo/go-telegram#readme" target="_blank" class="hover:text-white transition">README</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-span-2 sm:col-span-1">
|
||||
<h3 class="text-white font-semibold mb-3 sm:mb-4 text-sm sm:text-base">Sibling projects</h3>
|
||||
<ul class="space-y-1.5 sm:space-y-2 text-xs sm:text-sm">
|
||||
<li><a href="https://kportal.raczylo.com" target="_blank" class="hover:text-white transition"><i class="fas fa-dharmachakra mr-2"></i>kportal</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 sm:mt-8 pt-6 sm:pt-8 border-t border-gray-800 text-center text-xs sm:text-sm">
|
||||
<p>Made by <a href="https://github.com/lukaszraczylo" class="text-sky-400 hover:text-sky-300 transition">Lukasz Raczylo</a></p>
|
||||
<p class="mt-1.5 sm:mt-2">MIT License</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Mobile menu toggle
|
||||
const mobileMenuToggle = document.getElementById("mobile-menu-toggle");
|
||||
const mobileMenu = document.getElementById("mobile-menu");
|
||||
const menuOpenIcon = document.getElementById("menu-open-icon");
|
||||
const menuCloseIcon = document.getElementById("menu-close-icon");
|
||||
|
||||
mobileMenuToggle.addEventListener("click", () => {
|
||||
mobileMenu.classList.toggle("hidden");
|
||||
menuOpenIcon.classList.toggle("hidden");
|
||||
menuCloseIcon.classList.toggle("hidden");
|
||||
});
|
||||
|
||||
const mobileMenuLinks = mobileMenu.querySelectorAll("a");
|
||||
mobileMenuLinks.forEach(link => {
|
||||
link.addEventListener("click", () => {
|
||||
mobileMenu.classList.add("hidden");
|
||||
menuOpenIcon.classList.remove("hidden");
|
||||
menuCloseIcon.classList.add("hidden");
|
||||
});
|
||||
});
|
||||
|
||||
// Theme toggle
|
||||
const themeToggle = document.getElementById("theme-toggle");
|
||||
themeToggle.addEventListener("click", () => {
|
||||
if (document.documentElement.classList.contains("dark")) {
|
||||
document.documentElement.classList.remove("dark");
|
||||
localStorage.theme = "light";
|
||||
} else {
|
||||
document.documentElement.classList.add("dark");
|
||||
localStorage.theme = "dark";
|
||||
}
|
||||
});
|
||||
|
||||
// Copy to clipboard
|
||||
function copyToClipboard(text, button) {
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(() => showCopySuccess(button)).catch(() => fallbackCopy(text, button));
|
||||
} else {
|
||||
fallbackCopy(text, button);
|
||||
}
|
||||
}
|
||||
|
||||
function fallbackCopy(text, button) {
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = text;
|
||||
textarea.style.position = "fixed";
|
||||
textarea.style.opacity = "0";
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
try {
|
||||
document.execCommand("copy") ? showCopySuccess(button) : showCopyError(button);
|
||||
} catch (err) {
|
||||
showCopyError(button);
|
||||
}
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
|
||||
function showCopySuccess(button) {
|
||||
const original = button.innerHTML;
|
||||
button.innerHTML = '<i class="fas fa-check text-green-500"></i>';
|
||||
setTimeout(() => button.innerHTML = original, 2000);
|
||||
}
|
||||
|
||||
function showCopyError(button) {
|
||||
const original = button.innerHTML;
|
||||
button.innerHTML = '<i class="fas fa-times text-red-500"></i>';
|
||||
setTimeout(() => button.innerHTML = original, 2000);
|
||||
}
|
||||
|
||||
// Smooth scrolling
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener("click", function(e) {
|
||||
e.preventDefault();
|
||||
const target = document.querySelector(this.getAttribute("href"));
|
||||
if (target) target.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 56" fill="none">
|
||||
<!-- Paper plane glyph in Telegram blue gradient -->
|
||||
<defs>
|
||||
<linearGradient id="planeGradD" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#29B6F6"/>
|
||||
<stop offset="100%" stop-color="#4FC3F7"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- Plane body -->
|
||||
<polygon points="4,28 44,12 36,44" fill="url(#planeGradD)" opacity="0.15"/>
|
||||
<polygon points="4,28 44,12 36,44" stroke="url(#planeGradD)" stroke-width="1.5" fill="none"/>
|
||||
<!-- Plane wings / send arrow -->
|
||||
<path d="M4 28 L44 12 L30 30 Z" fill="url(#planeGradD)"/>
|
||||
<path d="M30 30 L36 44 L24 34 Z" fill="url(#planeGradD)" opacity="0.7"/>
|
||||
<!-- Fold line -->
|
||||
<line x1="30" y1="30" x2="24" y2="34" stroke="white" stroke-width="1" opacity="0.8"/>
|
||||
<!-- Wordmark -->
|
||||
<text x="56" y="35" font-family="Inter, sans-serif" font-weight="700" font-size="20" fill="#e5e7eb" letter-spacing="-0.5">go-telegram</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 977 B |
@@ -0,0 +1,19 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 56" fill="none">
|
||||
<!-- Paper plane glyph in Telegram blue gradient -->
|
||||
<defs>
|
||||
<linearGradient id="planeGradL" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#29B6F6"/>
|
||||
<stop offset="100%" stop-color="#0288D1"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- Plane body -->
|
||||
<polygon points="4,28 44,12 36,44" fill="url(#planeGradL)" opacity="0.15"/>
|
||||
<polygon points="4,28 44,12 36,44" stroke="url(#planeGradL)" stroke-width="1.5" fill="none"/>
|
||||
<!-- Plane wings / send arrow -->
|
||||
<path d="M4 28 L44 12 L30 30 Z" fill="url(#planeGradL)"/>
|
||||
<path d="M30 30 L36 44 L24 34 Z" fill="url(#planeGradL)" opacity="0.7"/>
|
||||
<!-- Fold line -->
|
||||
<line x1="30" y1="30" x2="24" y2="34" stroke="white" stroke-width="1" opacity="0.8"/>
|
||||
<!-- Wordmark -->
|
||||
<text x="56" y="35" font-family="Inter, sans-serif" font-weight="700" font-size="20" fill="#1f2937" letter-spacing="-0.5">go-telegram</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 977 B |
@@ -0,0 +1,25 @@
|
||||
# API Reference
|
||||
|
||||
Auto-generated from Go source comments by [gomarkdoc](https://github.com/princjef/gomarkdoc). Do not edit by hand — run `make docs` to regenerate.
|
||||
|
||||
## Packages
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`api`](api.md) | Telegram Bot API types and method wrappers — 176 methods, 301 types, fully generated |
|
||||
| [`client`](client.md) | Bot client, codec, HTTP doer, retry middleware |
|
||||
| [`transport`](transport.md) | Long-poll and webhook transports |
|
||||
| [`dispatch`](dispatch.md) | Update router, filters, handler groups, named handlers |
|
||||
| [`dispatch/conversation`](dispatch/conversation.md) | Multi-step conversation state machines |
|
||||
| [`dispatch/filters/message`](dispatch/filters/message.md) | Message filters — `Command`, `Text`, `IsReply`, etc. |
|
||||
| [`dispatch/filters/callback`](dispatch/filters/callback.md) | Callback query filters |
|
||||
| [`dispatch/filters/inline`](dispatch/filters/inline.md) | Inline query filters |
|
||||
| [`dispatch/filters/chatmember`](dispatch/filters/chatmember.md) | Chat member update filters |
|
||||
| [`dispatch/filters/chatjoinrequest`](dispatch/filters/chatjoinrequest.md) | Join request filters |
|
||||
| [`dispatch/filters/precheckoutquery`](dispatch/filters/precheckoutquery.md) | Pre-checkout filters for payments |
|
||||
|
||||
## Also see
|
||||
|
||||
- [Project home](../index.html) — landing page with examples and overview
|
||||
- [GitHub repository](https://github.com/lukaszraczylo/go-telegram)
|
||||
- [pkg.go.dev](https://pkg.go.dev/github.com/lukaszraczylo/go-telegram) — official Go package documentation
|
||||
+15757
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,579 @@
|
||||
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
|
||||
|
||||
# client
|
||||
|
||||
```go
|
||||
import "github.com/lukaszraczylo/go-telegram/client"
|
||||
```
|
||||
|
||||
Package client provides HTTP client primitives for the Telegram Bot API.
|
||||
|
||||
## Index
|
||||
|
||||
- [Variables](<#variables>)
|
||||
- [func Call\[Req any, Resp any\]\(ctx context.Context, b \*Bot, method string, req Req\) \(Resp, error\)](<#Call>)
|
||||
- [func CallRaw\[Req any\]\(ctx context.Context, b \*Bot, method string, req Req\) \(json.RawMessage, error\)](<#CallRaw>)
|
||||
- [func NewDefaultHTTPDoer\(\) \*http.Client](<#NewDefaultHTTPDoer>)
|
||||
- [type APIError](<#APIError>)
|
||||
- [func \(e \*APIError\) Error\(\) string](<#APIError.Error>)
|
||||
- [func \(e \*APIError\) IsRetryable\(\) bool](<#APIError.IsRetryable>)
|
||||
- [func \(e \*APIError\) RetryAfter\(\) time.Duration](<#APIError.RetryAfter>)
|
||||
- [func \(e \*APIError\) Unwrap\(\) error](<#APIError.Unwrap>)
|
||||
- [type Bot](<#Bot>)
|
||||
- [func New\(token string, opts ...Option\) \*Bot](<#New>)
|
||||
- [func \(b \*Bot\) BaseURL\(\) string](<#Bot.BaseURL>)
|
||||
- [func \(b \*Bot\) Codec\(\) Codec](<#Bot.Codec>)
|
||||
- [func \(b \*Bot\) HTTP\(\) HTTPDoer](<#Bot.HTTP>)
|
||||
- [func \(b \*Bot\) Logger\(\) Logger](<#Bot.Logger>)
|
||||
- [func \(b \*Bot\) Token\(\) string](<#Bot.Token>)
|
||||
- [type Codec](<#Codec>)
|
||||
- [type DefaultCodec](<#DefaultCodec>)
|
||||
- [func \(DefaultCodec\) Marshal\(v any\) \(\[\]byte, error\)](<#DefaultCodec.Marshal>)
|
||||
- [func \(DefaultCodec\) Unmarshal\(data \[\]byte, v any\) error](<#DefaultCodec.Unmarshal>)
|
||||
- [type HTTPDoer](<#HTTPDoer>)
|
||||
- [type Logger](<#Logger>)
|
||||
- [type MultipartFile](<#MultipartFile>)
|
||||
- [type NetworkError](<#NetworkError>)
|
||||
- [func \(e \*NetworkError\) Error\(\) string](<#NetworkError.Error>)
|
||||
- [func \(e \*NetworkError\) Unwrap\(\) error](<#NetworkError.Unwrap>)
|
||||
- [type NoopLogger](<#NoopLogger>)
|
||||
- [func \(NoopLogger\) Debug\(string, ...any\)](<#NoopLogger.Debug>)
|
||||
- [func \(NoopLogger\) Error\(string, ...any\)](<#NoopLogger.Error>)
|
||||
- [func \(NoopLogger\) Info\(string, ...any\)](<#NoopLogger.Info>)
|
||||
- [func \(NoopLogger\) Warn\(string, ...any\)](<#NoopLogger.Warn>)
|
||||
- [type Option](<#Option>)
|
||||
- [func WithBaseURL\(url string\) Option](<#WithBaseURL>)
|
||||
- [func WithCodec\(c Codec\) Option](<#WithCodec>)
|
||||
- [func WithHTTPClient\(c HTTPDoer\) Option](<#WithHTTPClient>)
|
||||
- [func WithLogger\(l Logger\) Option](<#WithLogger>)
|
||||
- [type ParseError](<#ParseError>)
|
||||
- [func \(e \*ParseError\) Error\(\) string](<#ParseError.Error>)
|
||||
- [func \(e \*ParseError\) Unwrap\(\) error](<#ParseError.Unwrap>)
|
||||
- [type ResponseParameters](<#ResponseParameters>)
|
||||
- [type Result](<#Result>)
|
||||
- [type RetryDoer](<#RetryDoer>)
|
||||
- [func NewRetryDoer\(inner HTTPDoer, opts ...RetryOption\) \*RetryDoer](<#NewRetryDoer>)
|
||||
- [func \(d \*RetryDoer\) Do\(req \*http.Request\) \(\*http.Response, error\)](<#RetryDoer.Do>)
|
||||
- [type RetryOption](<#RetryOption>)
|
||||
- [func WithBackoffFactor\(f float64\) RetryOption](<#WithBackoffFactor>)
|
||||
- [func WithBaseBackoff\(d time.Duration\) RetryOption](<#WithBaseBackoff>)
|
||||
- [func WithJitter\(j float64\) RetryOption](<#WithJitter>)
|
||||
- [func WithMaxAttempts\(n int\) RetryOption](<#WithMaxAttempts>)
|
||||
- [func WithMaxBackoff\(d time.Duration\) RetryOption](<#WithMaxBackoff>)
|
||||
|
||||
|
||||
## Variables
|
||||
|
||||
<a name="ErrUnauthorized"></a>Sentinel errors returned via APIError.Unwrap when the description matches. Compare with errors.Is.
|
||||
|
||||
```go
|
||||
var (
|
||||
ErrUnauthorized = errors.New("telegram: unauthorized")
|
||||
ErrChatNotFound = errors.New("telegram: chat not found")
|
||||
ErrMessageNotModified = errors.New("telegram: message is not modified")
|
||||
ErrTooManyRequests = errors.New("telegram: too many requests")
|
||||
ErrBadRequest = errors.New("telegram: bad request")
|
||||
ErrForbidden = errors.New("telegram: forbidden")
|
||||
ErrUserNotFound = errors.New("telegram: user not found")
|
||||
ErrMessageNotFound = errors.New("telegram: message not found")
|
||||
)
|
||||
```
|
||||
|
||||
<a name="Call"></a>
|
||||
## func [Call](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/call.go#L25>)
|
||||
|
||||
```go
|
||||
func Call[Req any, Resp any](ctx context.Context, b *Bot, method string, req Req) (Resp, error)
|
||||
```
|
||||
|
||||
Call is the single point through which every Telegram Bot API method invocation flows. It marshals the request, signs the URL with the bot token, dispatches via HTTPDoer, decodes the Result envelope, and translates non\-OK responses into typed errors.
|
||||
|
||||
It is generic over both request and response types. Methods with no parameters may pass a nil Req; the helper sends "\{\}" in that case so Telegram receives a syntactically valid empty object.
|
||||
|
||||
Call is exported because the api package \(which lives outside this one\) invokes it from generated method wrappers. User code should not normally call it directly — use the typed wrappers in package api instead.
|
||||
|
||||
<a name="CallRaw"></a>
|
||||
## func [CallRaw](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/call.go#L74>)
|
||||
|
||||
```go
|
||||
func CallRaw[Req any](ctx context.Context, b *Bot, method string, req Req) (json.RawMessage, error)
|
||||
```
|
||||
|
||||
CallRaw is like Call but returns the raw JSON of the result field instead of decoding it into a typed value. Generated method wrappers for sealed\-interface return types \(ChatMember, MenuButton, etc.\) use this helper, then dispatch through the union's UnmarshalXxx function.
|
||||
|
||||
CallRaw still translates non\-OK responses into \*APIError just like Call.
|
||||
|
||||
<a name="NewDefaultHTTPDoer"></a>
|
||||
## func [NewDefaultHTTPDoer](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/httpclient.go#L22>)
|
||||
|
||||
```go
|
||||
func NewDefaultHTTPDoer() *http.Client
|
||||
```
|
||||
|
||||
NewDefaultHTTPDoer returns an \*http.Client with sensible defaults for Telegram Bot API usage:
|
||||
|
||||
- 60s overall timeout \(longer than typical long\-poll Timeout=30s\).
|
||||
- Connection pooling sized for a small number of long\-lived hosts.
|
||||
- HTTP/2 enabled \(default in net/http\).
|
||||
|
||||
<a name="APIError"></a>
|
||||
## type [APIError](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/errors.go#L13-L21>)
|
||||
|
||||
APIError represents a non\-OK Telegram Bot API response. It satisfies error and unwraps to a sentinel \(ErrUnauthorized, etc.\) where the description matches a known prefix, enabling errors.Is checks.
|
||||
|
||||
```go
|
||||
type APIError struct {
|
||||
Code int
|
||||
Description string
|
||||
Parameters *ResponseParameters
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
```
|
||||
|
||||
<a name="APIError.Error"></a>
|
||||
### func \(\*APIError\) [Error](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/errors.go#L24>)
|
||||
|
||||
```go
|
||||
func (e *APIError) Error() string
|
||||
```
|
||||
|
||||
Error implements error.
|
||||
|
||||
<a name="APIError.IsRetryable"></a>
|
||||
### func \(\*APIError\) [IsRetryable](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/errors.go#L32>)
|
||||
|
||||
```go
|
||||
func (e *APIError) IsRetryable() bool
|
||||
```
|
||||
|
||||
IsRetryable returns true for transient HTTP statuses \(429, 5xx\).
|
||||
|
||||
<a name="APIError.RetryAfter"></a>
|
||||
### func \(\*APIError\) [RetryAfter](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/errors.go#L38>)
|
||||
|
||||
```go
|
||||
func (e *APIError) RetryAfter() time.Duration
|
||||
```
|
||||
|
||||
RetryAfter returns the recommended back\-off duration. It honours the Telegram\-supplied retry\_after parameter; if absent, returns 0.
|
||||
|
||||
<a name="APIError.Unwrap"></a>
|
||||
### func \(\*APIError\) [Unwrap](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/errors.go#L29>)
|
||||
|
||||
```go
|
||||
func (e *APIError) Unwrap() error
|
||||
```
|
||||
|
||||
Unwrap returns the matched sentinel error, if any.
|
||||
|
||||
<a name="Bot"></a>
|
||||
## 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.
|
||||
|
||||
```go
|
||||
type Bot struct {
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
```
|
||||
|
||||
<a name="New"></a>
|
||||
### func [New](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L36>)
|
||||
|
||||
```go
|
||||
func New(token string, opts ...Option) *Bot
|
||||
```
|
||||
|
||||
New constructs a Bot with the given token and optional configuration. The default HTTP client is tuned for long\-poll workloads \(see NewDefaultHTTPDoer\); the default codec wraps encoding/json; the default logger discards records.
|
||||
|
||||
<a name="Bot.BaseURL"></a>
|
||||
### func \(\*Bot\) [BaseURL](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L20>)
|
||||
|
||||
```go
|
||||
func (b *Bot) BaseURL() string
|
||||
```
|
||||
|
||||
BaseURL returns the configured Telegram API base URL.
|
||||
|
||||
<a name="Bot.Codec"></a>
|
||||
### func \(\*Bot\) [Codec](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L27>)
|
||||
|
||||
```go
|
||||
func (b *Bot) Codec() Codec
|
||||
```
|
||||
|
||||
Codec returns the configured Codec.
|
||||
|
||||
<a name="Bot.HTTP"></a>
|
||||
### func \(\*Bot\) [HTTP](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L24>)
|
||||
|
||||
```go
|
||||
func (b *Bot) HTTP() HTTPDoer
|
||||
```
|
||||
|
||||
HTTP returns the underlying HTTPDoer. Exposed for adapters that need to share connection pools or for diagnostic checks.
|
||||
|
||||
<a name="Bot.Logger"></a>
|
||||
### func \(\*Bot\) [Logger](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L30>)
|
||||
|
||||
```go
|
||||
func (b *Bot) Logger() Logger
|
||||
```
|
||||
|
||||
Logger returns the configured Logger.
|
||||
|
||||
<a name="Bot.Token"></a>
|
||||
### func \(\*Bot\) [Token](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L17>)
|
||||
|
||||
```go
|
||||
func (b *Bot) Token() string
|
||||
```
|
||||
|
||||
Token returns the bot token. Exposed for advanced use cases \(custom transports, manual URL building\); ordinary code does not need it.
|
||||
|
||||
<a name="Codec"></a>
|
||||
## type [Codec](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#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.
|
||||
|
||||
```go
|
||||
type Codec interface {
|
||||
Marshal(v any) ([]byte, error)
|
||||
Unmarshal(data []byte, v any) error
|
||||
}
|
||||
```
|
||||
|
||||
<a name="DefaultCodec"></a>
|
||||
## type [DefaultCodec](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#L16>)
|
||||
|
||||
DefaultCodec wraps goccy/go\-json. It is the zero\-value safe default.
|
||||
|
||||
```go
|
||||
type DefaultCodec struct{}
|
||||
```
|
||||
|
||||
<a name="DefaultCodec.Marshal"></a>
|
||||
### func \(DefaultCodec\) [Marshal](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#L19>)
|
||||
|
||||
```go
|
||||
func (DefaultCodec) Marshal(v any) ([]byte, error)
|
||||
```
|
||||
|
||||
Marshal calls json.Marshal.
|
||||
|
||||
<a name="DefaultCodec.Unmarshal"></a>
|
||||
### func \(DefaultCodec\) [Unmarshal](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#L22>)
|
||||
|
||||
```go
|
||||
func (DefaultCodec) Unmarshal(data []byte, v any) error
|
||||
```
|
||||
|
||||
Unmarshal calls json.Unmarshal.
|
||||
|
||||
<a name="HTTPDoer"></a>
|
||||
## type [HTTPDoer](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/httpclient.go#L13-L15>)
|
||||
|
||||
HTTPDoer abstracts the HTTP transport. The default is a net/http client tuned for Telegram's long\-poll usage. Users may plug in valyala/fasthttp \(via an adapter\), or any custom retry/circuit\-breaker client by passing WithHTTPClient to New.
|
||||
|
||||
```go
|
||||
type HTTPDoer interface {
|
||||
Do(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
```
|
||||
|
||||
<a name="Logger"></a>
|
||||
## type [Logger](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/logger.go#L6-L11>)
|
||||
|
||||
Logger is a slog\-shaped logging interface. Users pass any compatible implementation via WithLogger. The default is NoopLogger, which discards everything.
|
||||
|
||||
```go
|
||||
type Logger interface {
|
||||
Debug(msg string, attrs ...any)
|
||||
Info(msg string, attrs ...any)
|
||||
Warn(msg string, attrs ...any)
|
||||
Error(msg string, attrs ...any)
|
||||
}
|
||||
```
|
||||
|
||||
<a name="MultipartFile"></a>
|
||||
## type [MultipartFile](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/multipart.go#L27-L31>)
|
||||
|
||||
MultipartFile describes a single file part in a multipart upload.
|
||||
|
||||
```go
|
||||
type MultipartFile struct {
|
||||
FieldName string
|
||||
Filename string
|
||||
Reader io.Reader
|
||||
}
|
||||
```
|
||||
|
||||
<a name="NetworkError"></a>
|
||||
## type [NetworkError](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/errors.go#L47>)
|
||||
|
||||
NetworkError wraps a transport\-level failure \(DNS, TCP, TLS, timeout short of an HTTP response\).
|
||||
|
||||
```go
|
||||
type NetworkError struct{ Err error }
|
||||
```
|
||||
|
||||
<a name="NetworkError.Error"></a>
|
||||
### func \(\*NetworkError\) [Error](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/errors.go#L49>)
|
||||
|
||||
```go
|
||||
func (e *NetworkError) Error() string
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="NetworkError.Unwrap"></a>
|
||||
### func \(\*NetworkError\) [Unwrap](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/errors.go#L51>)
|
||||
|
||||
```go
|
||||
func (e *NetworkError) Unwrap() error
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="NoopLogger"></a>
|
||||
## type [NoopLogger](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/logger.go#L14>)
|
||||
|
||||
NoopLogger discards all log records. It is the zero\-value safe default.
|
||||
|
||||
```go
|
||||
type NoopLogger struct{}
|
||||
```
|
||||
|
||||
<a name="NoopLogger.Debug"></a>
|
||||
### func \(NoopLogger\) [Debug](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/logger.go#L16>)
|
||||
|
||||
```go
|
||||
func (NoopLogger) Debug(string, ...any)
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="NoopLogger.Error"></a>
|
||||
### func \(NoopLogger\) [Error](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/logger.go#L19>)
|
||||
|
||||
```go
|
||||
func (NoopLogger) Error(string, ...any)
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="NoopLogger.Info"></a>
|
||||
### func \(NoopLogger\) [Info](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/logger.go#L17>)
|
||||
|
||||
```go
|
||||
func (NoopLogger) Info(string, ...any)
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="NoopLogger.Warn"></a>
|
||||
### func \(NoopLogger\) [Warn](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/logger.go#L18>)
|
||||
|
||||
```go
|
||||
func (NoopLogger) Warn(string, ...any)
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="Option"></a>
|
||||
## type [Option](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/options.go#L5>)
|
||||
|
||||
Option configures a Bot at construction time. Per\-call configuration is expressed via typed parameter structs \(e.g. SendMessageParams\), not options.
|
||||
|
||||
```go
|
||||
type Option func(*Bot)
|
||||
```
|
||||
|
||||
<a name="WithBaseURL"></a>
|
||||
### func [WithBaseURL](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/options.go#L18>)
|
||||
|
||||
```go
|
||||
func WithBaseURL(url string) Option
|
||||
```
|
||||
|
||||
WithBaseURL overrides the API base URL. Useful for testing against a local httptest.Server, or for self\-hosted Bot API servers.
|
||||
|
||||
<a name="WithCodec"></a>
|
||||
### func [WithCodec](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/options.go#L14>)
|
||||
|
||||
```go
|
||||
func WithCodec(c Codec) Option
|
||||
```
|
||||
|
||||
WithCodec overrides the JSON codec. Pass goccy/go\-json, sonic, or any type implementing Codec to swap out encoding/json.
|
||||
|
||||
<a name="WithHTTPClient"></a>
|
||||
### func [WithHTTPClient](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/options.go#L10>)
|
||||
|
||||
```go
|
||||
func WithHTTPClient(c HTTPDoer) Option
|
||||
```
|
||||
|
||||
WithHTTPClient overrides the HTTP transport. Pass any HTTPDoer implementation \(e.g. an \*http.Client wrapping a custom RoundTripper, or a fasthttp adapter\).
|
||||
|
||||
<a name="WithLogger"></a>
|
||||
### func [WithLogger](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/options.go#L22>)
|
||||
|
||||
```go
|
||||
func WithLogger(l Logger) Option
|
||||
```
|
||||
|
||||
WithLogger sets the logger used for diagnostic events. Passing nil silently disables logging.
|
||||
|
||||
<a name="ParseError"></a>
|
||||
## type [ParseError](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/errors.go#L55-L58>)
|
||||
|
||||
ParseError wraps a JSON decode failure on a response body. Body is retained \(truncated to 4 KiB\); Error\(\) displays up to 256 bytes for diagnostics.
|
||||
|
||||
```go
|
||||
type ParseError struct {
|
||||
Err error
|
||||
Body []byte
|
||||
}
|
||||
```
|
||||
|
||||
<a name="ParseError.Error"></a>
|
||||
### func \(\*ParseError\) [Error](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/errors.go#L60>)
|
||||
|
||||
```go
|
||||
func (e *ParseError) Error() string
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="ParseError.Unwrap"></a>
|
||||
### func \(\*ParseError\) [Unwrap](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/errors.go#L68>)
|
||||
|
||||
```go
|
||||
func (e *ParseError) Unwrap() error
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="ResponseParameters"></a>
|
||||
## type [ResponseParameters](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/result.go#L24-L27>)
|
||||
|
||||
ResponseParameters is the optional metadata Telegram includes on certain failures. The most common is RetryAfter \(seconds\) on 429 responses.
|
||||
|
||||
This type is duplicated in package api for users; keeping a copy here avoids an import cycle \(api imports client, not vice versa\).
|
||||
|
||||
```go
|
||||
type ResponseParameters struct {
|
||||
MigrateToChatID int64 `json:"migrate_to_chat_id,omitempty"`
|
||||
RetryAfter int `json:"retry_after,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
<a name="Result"></a>
|
||||
## type [Result](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/result.go#L11-L17>)
|
||||
|
||||
Result is the universal Telegram API response envelope. Every successful response is shaped \{"ok":true,"result":T,...\}; failure responses set ok to false and populate ErrorCode / Description / Parameters.
|
||||
|
||||
Result is generic over T so generated method wrappers can decode the strongly\-typed payload directly. Users do not normally construct or inspect Result values; method wrappers unwrap them and return either the typed payload or a \*APIError.
|
||||
|
||||
```go
|
||||
type Result[T any] struct {
|
||||
OK bool `json:"ok"`
|
||||
Result T `json:"result,omitempty"`
|
||||
ErrorCode int `json:"error_code,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Parameters *ResponseParameters `json:"parameters,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
<a name="RetryDoer"></a>
|
||||
## type [RetryDoer](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/retry.go#L23-L30>)
|
||||
|
||||
RetryDoer is an HTTPDoer that retries transient failures \(429, 5xx, and network errors\) with exponential backoff. It honours the retry\_after value Telegram supplies on rate\-limit responses.
|
||||
|
||||
Wrap any HTTPDoer to add retry behaviour:
|
||||
|
||||
```
|
||||
bot := client.New(token, client.WithHTTPClient(
|
||||
client.NewRetryDoer(client.NewDefaultHTTPDoer())))
|
||||
```
|
||||
|
||||
```go
|
||||
type RetryDoer struct {
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
```
|
||||
|
||||
<a name="NewRetryDoer"></a>
|
||||
### func [NewRetryDoer](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/retry.go#L63>)
|
||||
|
||||
```go
|
||||
func NewRetryDoer(inner HTTPDoer, opts ...RetryOption) *RetryDoer
|
||||
```
|
||||
|
||||
NewRetryDoer wraps inner with retry behaviour.
|
||||
|
||||
<a name="RetryDoer.Do"></a>
|
||||
### func \(\*RetryDoer\) [Do](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/retry.go#L80>)
|
||||
|
||||
```go
|
||||
func (d *RetryDoer) Do(req *http.Request) (*http.Response, error)
|
||||
```
|
||||
|
||||
Do dispatches via the inner HTTPDoer and retries on transient failures. The request body is buffered on first attempt so it can be replayed.
|
||||
|
||||
<a name="RetryOption"></a>
|
||||
## type [RetryOption](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/retry.go#L33>)
|
||||
|
||||
RetryOption configures a RetryDoer.
|
||||
|
||||
```go
|
||||
type RetryOption func(*RetryDoer)
|
||||
```
|
||||
|
||||
<a name="WithBackoffFactor"></a>
|
||||
### func [WithBackoffFactor](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/retry.go#L52>)
|
||||
|
||||
```go
|
||||
func WithBackoffFactor(f float64) RetryOption
|
||||
```
|
||||
|
||||
WithBackoffFactor sets the exponential growth factor. Default 2.0.
|
||||
|
||||
<a name="WithBaseBackoff"></a>
|
||||
### func [WithBaseBackoff](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/retry.go#L42>)
|
||||
|
||||
```go
|
||||
func WithBaseBackoff(d time.Duration) RetryOption
|
||||
```
|
||||
|
||||
WithBaseBackoff sets the initial backoff duration. Default 500ms.
|
||||
|
||||
<a name="WithJitter"></a>
|
||||
### func [WithJitter](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/retry.go#L58>)
|
||||
|
||||
```go
|
||||
func WithJitter(j float64) RetryOption
|
||||
```
|
||||
|
||||
WithJitter sets the jitter fraction \(0..1\) applied to each backoff. Default 0.2.
|
||||
|
||||
<a name="WithMaxAttempts"></a>
|
||||
### func [WithMaxAttempts](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/retry.go#L37>)
|
||||
|
||||
```go
|
||||
func WithMaxAttempts(n int) RetryOption
|
||||
```
|
||||
|
||||
WithMaxAttempts sets the maximum number of attempts \(including the initial one\). Default 4 \(one initial \+ three retries\).
|
||||
|
||||
<a name="WithMaxBackoff"></a>
|
||||
### func [WithMaxBackoff](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/retry.go#L47>)
|
||||
|
||||
```go
|
||||
func WithMaxBackoff(d time.Duration) RetryOption
|
||||
```
|
||||
|
||||
WithMaxBackoff caps the backoff at max. Default 30s.
|
||||
|
||||
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)
|
||||
@@ -0,0 +1,668 @@
|
||||
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
|
||||
|
||||
# dispatch
|
||||
|
||||
```go
|
||||
import "github.com/lukaszraczylo/go-telegram/dispatch"
|
||||
```
|
||||
|
||||
Package dispatch provides a typed router for Telegram updates. It consumes any transport.Updater and dispatches updates to handlers registered by command, regex, or update\-payload kind.
|
||||
|
||||
## Index
|
||||
|
||||
- [Variables](<#variables>)
|
||||
- [type Context](<#Context>)
|
||||
- [func NewContext\(ctx context.Context, b \*client.Bot, u \*api.Update\) \*Context](<#NewContext>)
|
||||
- [type Filter](<#Filter>)
|
||||
- [func All\[T any\]\(filters ...Filter\[T\]\) Filter\[T\]](<#All>)
|
||||
- [func Any\[T any\]\(filters ...Filter\[T\]\) Filter\[T\]](<#Any>)
|
||||
- [func \(f Filter\[T\]\) And\(others ...Filter\[T\]\) Filter\[T\]](<#Filter[T].And>)
|
||||
- [func \(f Filter\[T\]\) Not\(\) Filter\[T\]](<#Filter[T].Not>)
|
||||
- [func \(f Filter\[T\]\) Or\(others ...Filter\[T\]\) Filter\[T\]](<#Filter[T].Or>)
|
||||
- [type Handler](<#Handler>)
|
||||
- [type Middleware](<#Middleware>)
|
||||
- [func Chain\[T any\]\(mws ...Middleware\[T\]\) Middleware\[T\]](<#Chain>)
|
||||
- [func Recovery\(\) Middleware\[\*api.Update\]](<#Recovery>)
|
||||
- [type NamedHandlers](<#NamedHandlers>)
|
||||
- [func NewNamedHandlers\[T any\]\(\) \*NamedHandlers\[T\]](<#NewNamedHandlers>)
|
||||
- [func \(n \*NamedHandlers\[T\]\) Handler\(\) Handler\[T\]](<#NamedHandlers[T].Handler>)
|
||||
- [func \(n \*NamedHandlers\[T\]\) Has\(name string\) bool](<#NamedHandlers[T].Has>)
|
||||
- [func \(n \*NamedHandlers\[T\]\) Names\(\) \[\]string](<#NamedHandlers[T].Names>)
|
||||
- [func \(n \*NamedHandlers\[T\]\) Remove\(name string\) bool](<#NamedHandlers[T].Remove>)
|
||||
- [func \(n \*NamedHandlers\[T\]\) Set\(name string, h Handler\[T\]\)](<#NamedHandlers[T].Set>)
|
||||
- [type Router](<#Router>)
|
||||
- [func New\(b \*client.Bot, opts ...RouterOption\) \*Router](<#New>)
|
||||
- [func \(r \*Router\) Group\(group int\) \*RouterScope](<#Router.Group>)
|
||||
- [func \(r \*Router\) OnBusinessConnection\(h Handler\[\*api.BusinessConnection\]\)](<#Router.OnBusinessConnection>)
|
||||
- [func \(r \*Router\) OnCallback\(pattern string, h Handler\[\*api.CallbackQuery\]\)](<#Router.OnCallback>)
|
||||
- [func \(r \*Router\) OnCallbackFilter\(f Filter\[\*api.CallbackQuery\], h Handler\[\*api.CallbackQuery\]\)](<#Router.OnCallbackFilter>)
|
||||
- [func \(r \*Router\) OnChannelPost\(h Handler\[\*api.Message\]\)](<#Router.OnChannelPost>)
|
||||
- [func \(r \*Router\) OnChatBoost\(h Handler\[\*api.ChatBoostUpdated\]\)](<#Router.OnChatBoost>)
|
||||
- [func \(r \*Router\) OnChatJoinRequest\(h Handler\[\*api.ChatJoinRequest\]\)](<#Router.OnChatJoinRequest>)
|
||||
- [func \(r \*Router\) OnChatJoinRequestFilter\(f Filter\[\*api.ChatJoinRequest\], h Handler\[\*api.ChatJoinRequest\]\)](<#Router.OnChatJoinRequestFilter>)
|
||||
- [func \(r \*Router\) OnChatMember\(h Handler\[\*api.ChatMemberUpdated\]\)](<#Router.OnChatMember>)
|
||||
- [func \(r \*Router\) OnChatMemberFilter\(f Filter\[\*api.ChatMemberUpdated\], h Handler\[\*api.ChatMemberUpdated\]\)](<#Router.OnChatMemberFilter>)
|
||||
- [func \(r \*Router\) OnChosenInlineResult\(h Handler\[\*api.ChosenInlineResult\]\)](<#Router.OnChosenInlineResult>)
|
||||
- [func \(r \*Router\) OnCommand\(cmd string, h Handler\[\*api.Message\]\)](<#Router.OnCommand>)
|
||||
- [func \(r \*Router\) OnEditedChannelPost\(h Handler\[\*api.Message\]\)](<#Router.OnEditedChannelPost>)
|
||||
- [func \(r \*Router\) OnEditedMessage\(h Handler\[\*api.Message\]\)](<#Router.OnEditedMessage>)
|
||||
- [func \(r \*Router\) OnInlineQuery\(h Handler\[\*api.InlineQuery\]\)](<#Router.OnInlineQuery>)
|
||||
- [func \(r \*Router\) OnInlineQueryFilter\(f Filter\[\*api.InlineQuery\], h Handler\[\*api.InlineQuery\]\)](<#Router.OnInlineQueryFilter>)
|
||||
- [func \(r \*Router\) OnMessageFilter\(f Filter\[\*api.Message\], h Handler\[\*api.Message\]\)](<#Router.OnMessageFilter>)
|
||||
- [func \(r \*Router\) OnMessageReaction\(h Handler\[\*api.MessageReactionUpdated\]\)](<#Router.OnMessageReaction>)
|
||||
- [func \(r \*Router\) OnMessageReactionCount\(h Handler\[\*api.MessageReactionCountUpdated\]\)](<#Router.OnMessageReactionCount>)
|
||||
- [func \(r \*Router\) OnMyChatMember\(h Handler\[\*api.ChatMemberUpdated\]\)](<#Router.OnMyChatMember>)
|
||||
- [func \(r \*Router\) OnMyChatMemberFilter\(f Filter\[\*api.ChatMemberUpdated\], h Handler\[\*api.ChatMemberUpdated\]\)](<#Router.OnMyChatMemberFilter>)
|
||||
- [func \(r \*Router\) OnPoll\(h Handler\[\*api.Poll\]\)](<#Router.OnPoll>)
|
||||
- [func \(r \*Router\) OnPollAnswer\(h Handler\[\*api.PollAnswer\]\)](<#Router.OnPollAnswer>)
|
||||
- [func \(r \*Router\) OnPreCheckoutQuery\(h Handler\[\*api.PreCheckoutQuery\]\)](<#Router.OnPreCheckoutQuery>)
|
||||
- [func \(r \*Router\) OnPreCheckoutQueryFilter\(f Filter\[\*api.PreCheckoutQuery\], h Handler\[\*api.PreCheckoutQuery\]\)](<#Router.OnPreCheckoutQueryFilter>)
|
||||
- [func \(r \*Router\) OnPurchasedPaidMedia\(h Handler\[\*api.PaidMediaPurchased\]\)](<#Router.OnPurchasedPaidMedia>)
|
||||
- [func \(r \*Router\) OnRemovedChatBoost\(h Handler\[\*api.ChatBoostRemoved\]\)](<#Router.OnRemovedChatBoost>)
|
||||
- [func \(r \*Router\) OnShippingQuery\(h Handler\[\*api.ShippingQuery\]\)](<#Router.OnShippingQuery>)
|
||||
- [func \(r \*Router\) OnText\(pattern string, h Handler\[\*api.Message\]\)](<#Router.OnText>)
|
||||
- [func \(r \*Router\) Run\(ctx context.Context, u transport.Updater\) error](<#Router.Run>)
|
||||
- [func \(r \*Router\) Use\(mw Middleware\[\*api.Update\]\)](<#Router.Use>)
|
||||
- [type RouterOption](<#RouterOption>)
|
||||
- [func WithMaxConcurrency\(n int\) RouterOption](<#WithMaxConcurrency>)
|
||||
- [type RouterScope](<#RouterScope>)
|
||||
- [func \(s \*RouterScope\) OnCommand\(cmd string, h Handler\[\*api.Message\]\)](<#RouterScope.OnCommand>)
|
||||
- [func \(s \*RouterScope\) OnMessageFilter\(f Filter\[\*api.Message\], h Handler\[\*api.Message\]\)](<#RouterScope.OnMessageFilter>)
|
||||
- [func \(s \*RouterScope\) OnText\(pattern string, h Handler\[\*api.Message\]\)](<#RouterScope.OnText>)
|
||||
|
||||
|
||||
## Variables
|
||||
|
||||
<a name="ErrContinueGroups"></a>ErrContinueGroups signals that this group's handler should be treated as not\-matching when returned by a handler: dispatch moves on to the next handler in the same group, then to subsequent groups.
|
||||
|
||||
Without ErrContinueGroups, a non\-error return from a matched handler stops dispatch \(default first\-match\-wins semantics\).
|
||||
|
||||
```go
|
||||
var ErrContinueGroups = errors.New("dispatch: continue groups")
|
||||
```
|
||||
|
||||
<a name="ErrEndGroups"></a>ErrEndGroups stops dispatch from running any further handlers in any group for this update when returned by a handler. Use it to indicate the update has been definitively handled.
|
||||
|
||||
errors.Is\(err, ErrEndGroups\) is the canonical check, though dispatch itself recognises it by exact identity.
|
||||
|
||||
```go
|
||||
var ErrEndGroups = errors.New("dispatch: end groups")
|
||||
```
|
||||
|
||||
<a name="Context"></a>
|
||||
## type [Context](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/context.go#L29-L34>)
|
||||
|
||||
Context bundles the per\-update state every handler receives.
|
||||
|
||||
Ctx is the request context propagated from Router.Run; cancelling the run cancels every handler.
|
||||
|
||||
Bot is the API client. Handlers reply by calling api.SendMessage\(c.Ctx, c.Bot, ...\) etc.
|
||||
|
||||
Update is the raw update; payload\-typed handlers also receive a narrowed pointer to one of its sub\-fields.
|
||||
|
||||
Values is a per\-update bag matchers populate. Conventional 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
|
||||
```
|
||||
|
||||
```go
|
||||
type Context struct {
|
||||
Ctx context.Context
|
||||
Bot *client.Bot
|
||||
Update *api.Update
|
||||
Values map[string]any
|
||||
}
|
||||
```
|
||||
|
||||
<a name="NewContext"></a>
|
||||
### func [NewContext](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/context.go#L38>)
|
||||
|
||||
```go
|
||||
func NewContext(ctx context.Context, b *client.Bot, u *api.Update) *Context
|
||||
```
|
||||
|
||||
NewContext constructs a Context. Used by Router internally; exposed for custom test harnesses.
|
||||
|
||||
<a name="Filter"></a>
|
||||
## type [Filter](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filter.go#L9>)
|
||||
|
||||
Filter is a predicate over a typed payload \(e.g. \*api.Message\). Filters compose via And/Or/Not for multi\-condition matching.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
f := message.HasPhoto().And(message.InChat(-100123456789))
|
||||
```
|
||||
|
||||
```go
|
||||
type Filter[T any] func(payload T) bool
|
||||
```
|
||||
|
||||
<a name="All"></a>
|
||||
### func [All](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filter.go#L48>)
|
||||
|
||||
```go
|
||||
func All[T any](filters ...Filter[T]) Filter[T]
|
||||
```
|
||||
|
||||
All combines filters with AND. Returns a Filter that matches when all match. Returns a filter that always matches when filters is empty.
|
||||
|
||||
<a name="Any"></a>
|
||||
### func [Any](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filter.go#L61>)
|
||||
|
||||
```go
|
||||
func Any[T any](filters ...Filter[T]) Filter[T]
|
||||
```
|
||||
|
||||
Any combines filters with OR. Returns a Filter that matches when at least one matches. Returns a filter that never matches when filters is empty.
|
||||
|
||||
<a name="Filter[T].And"></a>
|
||||
### func \(Filter\[T\]\) [And](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filter.go#L12>)
|
||||
|
||||
```go
|
||||
func (f Filter[T]) And(others ...Filter[T]) Filter[T]
|
||||
```
|
||||
|
||||
And returns a Filter that matches iff f and every one of others matches.
|
||||
|
||||
<a name="Filter[T].Not"></a>
|
||||
### func \(Filter\[T\]\) [Not](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filter.go#L42>)
|
||||
|
||||
```go
|
||||
func (f Filter[T]) Not() Filter[T]
|
||||
```
|
||||
|
||||
Not returns a Filter that inverts f.
|
||||
|
||||
<a name="Filter[T].Or"></a>
|
||||
### func \(Filter\[T\]\) [Or](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filter.go#L27>)
|
||||
|
||||
```go
|
||||
func (f Filter[T]) Or(others ...Filter[T]) Filter[T]
|
||||
```
|
||||
|
||||
Or returns a Filter that matches iff f matches OR any of others matches.
|
||||
|
||||
<a name="Handler"></a>
|
||||
## type [Handler](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/handler.go#L6>)
|
||||
|
||||
Handler is a generic handler over update payload type T. T is typically \*api.Message, \*api.CallbackQuery, \*api.InlineQuery, or \*api.Update for global middleware.
|
||||
|
||||
```go
|
||||
type Handler[T any] func(ctx *Context, payload T) error
|
||||
```
|
||||
|
||||
<a name="Middleware"></a>
|
||||
## type [Middleware](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/handler.go#L11>)
|
||||
|
||||
Middleware wraps a Handler\[T\] with cross\-cutting behaviour \(logging, recovery, auth\). Middleware composition is left\-to\-right: Use\(a,b,c\) runs as a\(b\(c\(handler\)\)\).
|
||||
|
||||
```go
|
||||
type Middleware[T any] func(Handler[T]) Handler[T]
|
||||
```
|
||||
|
||||
<a name="Chain"></a>
|
||||
### func [Chain](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/handler.go#L14>)
|
||||
|
||||
```go
|
||||
func Chain[T any](mws ...Middleware[T]) Middleware[T]
|
||||
```
|
||||
|
||||
Chain composes a slice of middleware into a single Middleware\[T\].
|
||||
|
||||
<a name="Recovery"></a>
|
||||
### func [Recovery](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/middleware.go#L13>)
|
||||
|
||||
```go
|
||||
func Recovery() Middleware[*api.Update]
|
||||
```
|
||||
|
||||
Recovery returns middleware that recovers from panics in downstream handlers, converting them into a returned error and logging via the bot's configured logger. Registered automatically by NewRouter.
|
||||
|
||||
<a name="NamedHandlers"></a>
|
||||
## type [NamedHandlers](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/named.go#L15-L19>)
|
||||
|
||||
NamedHandlers manages handlers by string name, allowing runtime registration, replacement, and removal. This complements the Router's registration methods: each registration via Named\*\(\) also gets a name for later lookup.
|
||||
|
||||
Use case: a plugin system that loads/unloads command handlers without restarting the bot.
|
||||
|
||||
```go
|
||||
type NamedHandlers[T any] struct {
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
```
|
||||
|
||||
<a name="NewNamedHandlers"></a>
|
||||
### func [NewNamedHandlers](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/named.go#L22>)
|
||||
|
||||
```go
|
||||
func NewNamedHandlers[T any]() *NamedHandlers[T]
|
||||
```
|
||||
|
||||
NewNamedHandlers returns a new, empty NamedHandlers\[T\].
|
||||
|
||||
<a name="NamedHandlers[T].Handler"></a>
|
||||
### func \(\*NamedHandlers\[T\]\) [Handler](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/named.go#L81>)
|
||||
|
||||
```go
|
||||
func (n *NamedHandlers[T]) Handler() Handler[T]
|
||||
```
|
||||
|
||||
Handler returns a single Handler\[T\] that runs each registered handler in registration order, first non\-nil error stops the chain. Use this to wire NamedHandlers into a Router.OnXxx call:
|
||||
|
||||
```
|
||||
names := dispatch.NewNamedHandlers[*api.Message]()
|
||||
names.Set("logger", loggingHandler)
|
||||
names.Set("audit", auditHandler)
|
||||
router.OnCommand("/admin", names.Handler())
|
||||
```
|
||||
|
||||
Subsequent Set/Remove calls take effect on the next dispatch.
|
||||
|
||||
<a name="NamedHandlers[T].Has"></a>
|
||||
### func \(\*NamedHandlers\[T\]\) [Has](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/named.go#L55>)
|
||||
|
||||
```go
|
||||
func (n *NamedHandlers[T]) Has(name string) bool
|
||||
```
|
||||
|
||||
Has reports whether name is registered.
|
||||
|
||||
<a name="NamedHandlers[T].Names"></a>
|
||||
### func \(\*NamedHandlers\[T\]\) [Names](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/named.go#L63>)
|
||||
|
||||
```go
|
||||
func (n *NamedHandlers[T]) Names() []string
|
||||
```
|
||||
|
||||
Names returns the registered names in registration order.
|
||||
|
||||
<a name="NamedHandlers[T].Remove"></a>
|
||||
### func \(\*NamedHandlers\[T\]\) [Remove](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/named.go#L38>)
|
||||
|
||||
```go
|
||||
func (n *NamedHandlers[T]) Remove(name string) bool
|
||||
```
|
||||
|
||||
Remove unregisters the handler under name. Returns true if it existed.
|
||||
|
||||
<a name="NamedHandlers[T].Set"></a>
|
||||
### func \(\*NamedHandlers\[T\]\) [Set](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/named.go#L28>)
|
||||
|
||||
```go
|
||||
func (n *NamedHandlers[T]) Set(name string, h Handler[T])
|
||||
```
|
||||
|
||||
Set registers or replaces the handler under name. If name is new, it is appended to the end of the registration order.
|
||||
|
||||
<a name="Router"></a>
|
||||
## type [Router](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L19-L64>)
|
||||
|
||||
Router dispatches updates from any Updater to typed handlers.
|
||||
|
||||
Matchers run in registration order; first match wins. A panic\-recovery middleware is attached automatically and runs around every dispatch.
|
||||
|
||||
```go
|
||||
type Router struct {
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
```
|
||||
|
||||
<a name="New"></a>
|
||||
### func [New](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L128>)
|
||||
|
||||
```go
|
||||
func New(b *client.Bot, opts ...RouterOption) *Router
|
||||
```
|
||||
|
||||
New constructs a Router. Recovery middleware is added by default; users can disable it by passing WithoutRecovery \(not implemented here, but the hook is in place via Use\).
|
||||
|
||||
<a name="Router.Group"></a>
|
||||
### func \(\*Router\) [Group](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/groups.go#L40>)
|
||||
|
||||
```go
|
||||
func (r *Router) Group(group int) *RouterScope
|
||||
```
|
||||
|
||||
Group returns a RouterScope that registers handlers in the given group. Group 0 \(the default\) runs first, then group 1, etc. Within a group, handlers run in registration order; the first non\-skipped match terminates dispatch unless the handler returns ErrContinueGroups.
|
||||
|
||||
<a name="Router.OnBusinessConnection"></a>
|
||||
### func \(\*Router\) [OnBusinessConnection](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L285>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnBusinessConnection(h Handler[*api.BusinessConnection])
|
||||
```
|
||||
|
||||
OnBusinessConnection registers a handler for business connection updates.
|
||||
|
||||
<a name="Router.OnCallback"></a>
|
||||
### func \(\*Router\) [OnCallback](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L161>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnCallback(pattern string, h Handler[*api.CallbackQuery])
|
||||
```
|
||||
|
||||
OnCallback registers a handler for callback queries whose Data matches the regex.
|
||||
|
||||
Panics at registration time if pattern is not a valid regular expression.
|
||||
|
||||
<a name="Router.OnCallbackFilter"></a>
|
||||
### func \(\*Router\) [OnCallbackFilter](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L194>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnCallbackFilter(f Filter[*api.CallbackQuery], h Handler[*api.CallbackQuery])
|
||||
```
|
||||
|
||||
OnCallbackFilter registers a typed callback\-query handler gated by filter f. Filter routes are checked after pattern\-based OnCallback routes; first match wins.
|
||||
|
||||
<a name="Router.OnChannelPost"></a>
|
||||
### func \(\*Router\) [OnChannelPost](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L177>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnChannelPost(h Handler[*api.Message])
|
||||
```
|
||||
|
||||
OnChannelPost registers a handler for channel post updates.
|
||||
|
||||
<a name="Router.OnChatBoost"></a>
|
||||
### func \(\*Router\) [OnChatBoost](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L275>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnChatBoost(h Handler[*api.ChatBoostUpdated])
|
||||
```
|
||||
|
||||
OnChatBoost registers a handler for chat boost updates.
|
||||
|
||||
<a name="Router.OnChatJoinRequest"></a>
|
||||
### func \(\*Router\) [OnChatJoinRequest](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L225>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnChatJoinRequest(h Handler[*api.ChatJoinRequest])
|
||||
```
|
||||
|
||||
OnChatJoinRequest registers a handler for chat join requests.
|
||||
|
||||
<a name="Router.OnChatJoinRequestFilter"></a>
|
||||
### func \(\*Router\) [OnChatJoinRequestFilter](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L230>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnChatJoinRequestFilter(f Filter[*api.ChatJoinRequest], h Handler[*api.ChatJoinRequest])
|
||||
```
|
||||
|
||||
OnChatJoinRequestFilter registers a filtered handler for chat join requests.
|
||||
|
||||
<a name="Router.OnChatMember"></a>
|
||||
### func \(\*Router\) [OnChatMember](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L215>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnChatMember(h Handler[*api.ChatMemberUpdated])
|
||||
```
|
||||
|
||||
OnChatMember registers a handler for chat member status changes.
|
||||
|
||||
<a name="Router.OnChatMemberFilter"></a>
|
||||
### func \(\*Router\) [OnChatMemberFilter](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L220>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnChatMemberFilter(f Filter[*api.ChatMemberUpdated], h Handler[*api.ChatMemberUpdated])
|
||||
```
|
||||
|
||||
OnChatMemberFilter registers a filtered handler for chat member status changes.
|
||||
|
||||
<a name="Router.OnChosenInlineResult"></a>
|
||||
### func \(\*Router\) [OnChosenInlineResult](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L260>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnChosenInlineResult(h Handler[*api.ChosenInlineResult])
|
||||
```
|
||||
|
||||
OnChosenInlineResult registers a handler for chosen inline results.
|
||||
|
||||
<a name="Router.OnCommand"></a>
|
||||
### func \(\*Router\) [OnCommand](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L146>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnCommand(cmd string, h Handler[*api.Message])
|
||||
```
|
||||
|
||||
OnCommand registers a handler for a slash command. The command string includes the leading slash \(e.g. "/start"\). Matching strips an optional "@BotName" suffix.
|
||||
|
||||
<a name="Router.OnEditedChannelPost"></a>
|
||||
### func \(\*Router\) [OnEditedChannelPost](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L182>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnEditedChannelPost(h Handler[*api.Message])
|
||||
```
|
||||
|
||||
OnEditedChannelPost registers a handler for edited channel post updates.
|
||||
|
||||
<a name="Router.OnEditedMessage"></a>
|
||||
### func \(\*Router\) [OnEditedMessage](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L172>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnEditedMessage(h Handler[*api.Message])
|
||||
```
|
||||
|
||||
OnEditedMessage registers a handler for edited message updates.
|
||||
|
||||
<a name="Router.OnInlineQuery"></a>
|
||||
### func \(\*Router\) [OnInlineQuery](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L167>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnInlineQuery(h Handler[*api.InlineQuery])
|
||||
```
|
||||
|
||||
OnInlineQuery registers a handler for inline queries \(one matcher only; inline queries are not partitioned by content here\).
|
||||
|
||||
<a name="Router.OnInlineQueryFilter"></a>
|
||||
### func \(\*Router\) [OnInlineQueryFilter](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L200>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnInlineQueryFilter(f Filter[*api.InlineQuery], h Handler[*api.InlineQuery])
|
||||
```
|
||||
|
||||
OnInlineQueryFilter registers an inline\-query handler gated by filter f. Filter routes are checked after bare OnInlineQuery handlers; first match wins.
|
||||
|
||||
<a name="Router.OnMessageFilter"></a>
|
||||
### func \(\*Router\) [OnMessageFilter](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L188>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnMessageFilter(f Filter[*api.Message], h Handler[*api.Message])
|
||||
```
|
||||
|
||||
OnMessageFilter registers a typed message handler gated by filter f. Filter routes are checked after command and text routes; first match wins.
|
||||
|
||||
<a name="Router.OnMessageReaction"></a>
|
||||
### func \(\*Router\) [OnMessageReaction](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L265>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnMessageReaction(h Handler[*api.MessageReactionUpdated])
|
||||
```
|
||||
|
||||
OnMessageReaction registers a handler for message reaction updates.
|
||||
|
||||
<a name="Router.OnMessageReactionCount"></a>
|
||||
### func \(\*Router\) [OnMessageReactionCount](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L270>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnMessageReactionCount(h Handler[*api.MessageReactionCountUpdated])
|
||||
```
|
||||
|
||||
OnMessageReactionCount registers a handler for anonymous message reaction count updates.
|
||||
|
||||
<a name="Router.OnMyChatMember"></a>
|
||||
### func \(\*Router\) [OnMyChatMember](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L205>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnMyChatMember(h Handler[*api.ChatMemberUpdated])
|
||||
```
|
||||
|
||||
OnMyChatMember registers a handler for bot's own chat member status changes.
|
||||
|
||||
<a name="Router.OnMyChatMemberFilter"></a>
|
||||
### func \(\*Router\) [OnMyChatMemberFilter](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L210>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnMyChatMemberFilter(f Filter[*api.ChatMemberUpdated], h Handler[*api.ChatMemberUpdated])
|
||||
```
|
||||
|
||||
OnMyChatMemberFilter registers a filtered handler for bot's own chat member status changes.
|
||||
|
||||
<a name="Router.OnPoll"></a>
|
||||
### func \(\*Router\) [OnPoll](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L250>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnPoll(h Handler[*api.Poll])
|
||||
```
|
||||
|
||||
OnPoll registers a handler for poll state updates.
|
||||
|
||||
<a name="Router.OnPollAnswer"></a>
|
||||
### func \(\*Router\) [OnPollAnswer](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L255>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnPollAnswer(h Handler[*api.PollAnswer])
|
||||
```
|
||||
|
||||
OnPollAnswer registers a handler for poll answer updates.
|
||||
|
||||
<a name="Router.OnPreCheckoutQuery"></a>
|
||||
### func \(\*Router\) [OnPreCheckoutQuery](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L235>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnPreCheckoutQuery(h Handler[*api.PreCheckoutQuery])
|
||||
```
|
||||
|
||||
OnPreCheckoutQuery registers a handler for pre\-checkout queries.
|
||||
|
||||
<a name="Router.OnPreCheckoutQueryFilter"></a>
|
||||
### func \(\*Router\) [OnPreCheckoutQueryFilter](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L240>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnPreCheckoutQueryFilter(f Filter[*api.PreCheckoutQuery], h Handler[*api.PreCheckoutQuery])
|
||||
```
|
||||
|
||||
OnPreCheckoutQueryFilter registers a filtered handler for pre\-checkout queries.
|
||||
|
||||
<a name="Router.OnPurchasedPaidMedia"></a>
|
||||
### func \(\*Router\) [OnPurchasedPaidMedia](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L290>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnPurchasedPaidMedia(h Handler[*api.PaidMediaPurchased])
|
||||
```
|
||||
|
||||
OnPurchasedPaidMedia registers a handler for purchased paid media updates.
|
||||
|
||||
<a name="Router.OnRemovedChatBoost"></a>
|
||||
### func \(\*Router\) [OnRemovedChatBoost](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L280>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnRemovedChatBoost(h Handler[*api.ChatBoostRemoved])
|
||||
```
|
||||
|
||||
OnRemovedChatBoost registers a handler for removed chat boost updates.
|
||||
|
||||
<a name="Router.OnShippingQuery"></a>
|
||||
### func \(\*Router\) [OnShippingQuery](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L245>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnShippingQuery(h Handler[*api.ShippingQuery])
|
||||
```
|
||||
|
||||
OnShippingQuery registers a handler for shipping queries.
|
||||
|
||||
<a name="Router.OnText"></a>
|
||||
### func \(\*Router\) [OnText](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L153>)
|
||||
|
||||
```go
|
||||
func (r *Router) OnText(pattern string, h Handler[*api.Message])
|
||||
```
|
||||
|
||||
OnText registers a handler for messages whose Text matches the regex.
|
||||
|
||||
Panics at registration time if pattern is not a valid regular expression.
|
||||
|
||||
<a name="Router.Run"></a>
|
||||
### 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
|
||||
```
|
||||
|
||||
Run consumes the Updater and dispatches each update. It blocks until the Updater's channel is closed or ctx is cancelled.
|
||||
|
||||
By default updates are processed concurrently \(up to WithMaxConcurrency\(50\) goroutines\). Handlers for different updates may therefore run simultaneously; shared state must be protected. Pass WithMaxConcurrency\(0\) to New to restore serial \(legacy\) behaviour.
|
||||
|
||||
Run waits for all in\-flight handlers to finish before returning.
|
||||
|
||||
<a name="Router.Use"></a>
|
||||
### func \(\*Router\) [Use](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L141>)
|
||||
|
||||
```go
|
||||
func (r *Router) Use(mw Middleware[*api.Update])
|
||||
```
|
||||
|
||||
Use registers a global middleware applied to every Update dispatch.
|
||||
|
||||
<a name="RouterOption"></a>
|
||||
## type [RouterOption](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L97>)
|
||||
|
||||
RouterOption configures a Router at construction time.
|
||||
|
||||
```go
|
||||
type RouterOption func(*Router)
|
||||
```
|
||||
|
||||
<a name="WithMaxConcurrency"></a>
|
||||
### func [WithMaxConcurrency](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/router.go#L106>)
|
||||
|
||||
```go
|
||||
func WithMaxConcurrency(n int) RouterOption
|
||||
```
|
||||
|
||||
WithMaxConcurrency sets the maximum number of updates processed in parallel. Default is 50. Pass 0 to dispatch serially \(one update at a time, in the calling goroutine — the legacy behaviour before v1.1.0\).
|
||||
|
||||
Note: concurrent dispatch means handlers for different updates may run simultaneously. Handlers that mutate shared state must be safe for concurrent access.
|
||||
|
||||
<a name="RouterScope"></a>
|
||||
## type [RouterScope](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/groups.go#L31-L34>)
|
||||
|
||||
RouterScope registers handlers into a specific priority group on its parent Router. Group 0 runs first, then group 1, etc. Within a group, handlers run in registration order; the first non\-skipped match terminates dispatch unless the handler returns ErrContinueGroups.
|
||||
|
||||
```go
|
||||
type RouterScope struct {
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
```
|
||||
|
||||
<a name="RouterScope.OnCommand"></a>
|
||||
### func \(\*RouterScope\) [OnCommand](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/groups.go#L45>)
|
||||
|
||||
```go
|
||||
func (s *RouterScope) OnCommand(cmd string, h Handler[*api.Message])
|
||||
```
|
||||
|
||||
OnCommand registers a command handler in this group.
|
||||
|
||||
<a name="RouterScope.OnMessageFilter"></a>
|
||||
### func \(\*RouterScope\) [OnMessageFilter](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/groups.go#L60>)
|
||||
|
||||
```go
|
||||
func (s *RouterScope) OnMessageFilter(f Filter[*api.Message], h Handler[*api.Message])
|
||||
```
|
||||
|
||||
OnMessageFilter registers a filter\-based message handler in this group.
|
||||
|
||||
<a name="RouterScope.OnText"></a>
|
||||
### func \(\*RouterScope\) [OnText](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/groups.go#L53>)
|
||||
|
||||
```go
|
||||
func (s *RouterScope) OnText(pattern string, h Handler[*api.Message])
|
||||
```
|
||||
|
||||
OnText registers a regex text handler in this group. Panics at registration time if pattern is not a valid regular expression.
|
||||
|
||||
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)
|
||||
@@ -0,0 +1,243 @@
|
||||
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
|
||||
|
||||
# conversation
|
||||
|
||||
```go
|
||||
import "github.com/lukaszraczylo/go-telegram/dispatch/conversation"
|
||||
```
|
||||
|
||||
Package conversation implements a stateful conversation handler for the go\-telegram dispatch router. It provides a state\-machine abstraction over multi\-step Telegram bot interactions, with pluggable storage and flexible key strategies.
|
||||
|
||||
## Index
|
||||
|
||||
- [Variables](<#variables>)
|
||||
- [func End\(\) error](<#End>)
|
||||
- [func Next\(s State\) error](<#Next>)
|
||||
- [type Conversation](<#Conversation>)
|
||||
- [func \(c \*Conversation\) Dispatch\(next dispatch.Handler\[\*api.Update\]\) dispatch.Handler\[\*api.Update\]](<#Conversation.Dispatch>)
|
||||
- [type Handler](<#Handler>)
|
||||
- [type KeyStrategy](<#KeyStrategy>)
|
||||
- [type MemoryStorage](<#MemoryStorage>)
|
||||
- [func NewMemoryStorage\(\) \*MemoryStorage](<#NewMemoryStorage>)
|
||||
- [func \(s \*MemoryStorage\) Delete\(\_ context.Context, key string\) error](<#MemoryStorage.Delete>)
|
||||
- [func \(s \*MemoryStorage\) Get\(\_ context.Context, key string\) \(State, error\)](<#MemoryStorage.Get>)
|
||||
- [func \(s \*MemoryStorage\) Set\(\_ context.Context, key string, state State\) error](<#MemoryStorage.Set>)
|
||||
- [type State](<#State>)
|
||||
- [type Step](<#Step>)
|
||||
- [type Storage](<#Storage>)
|
||||
|
||||
|
||||
## Variables
|
||||
|
||||
<a name="ErrKeyNotFound"></a>ErrKeyNotFound is returned by Storage.Get when no conversation is active for the given key.
|
||||
|
||||
```go
|
||||
var ErrKeyNotFound = errors.New("conversation: key not found")
|
||||
```
|
||||
|
||||
<a name="End"></a>
|
||||
## func [End](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/conversation/handler.go#L34>)
|
||||
|
||||
```go
|
||||
func End() error
|
||||
```
|
||||
|
||||
End signals the conversation has finished and state should be cleared. Conversation handlers return End\(\) to terminate.
|
||||
|
||||
<a name="Next"></a>
|
||||
## func [Next](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/conversation/handler.go#L28>)
|
||||
|
||||
```go
|
||||
func Next(s State) error
|
||||
```
|
||||
|
||||
Next signals the conversation should advance to the given state. Conversation handlers return Next\("state\_name"\) to transition.
|
||||
|
||||
<a name="Conversation"></a>
|
||||
## type [Conversation](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/conversation/handler.go#L55-L79>)
|
||||
|
||||
Conversation is a stateful handler with entry, per\-state, exit and fallback steps. A conversation is keyed by KeyStrategy \(default KeyByUserAndChat\) and persisted by Storage \(default in\-memory\).
|
||||
|
||||
```go
|
||||
type Conversation struct {
|
||||
// EntryPoints starts a new conversation when a matching filter fires
|
||||
// and no conversation is already active for the key.
|
||||
EntryPoints []Step
|
||||
|
||||
// States maps each state to the steps that handle it.
|
||||
States map[State][]Step
|
||||
|
||||
// Exits, if any match, end the active conversation early. Useful for
|
||||
// /cancel-style commands.
|
||||
Exits []Step
|
||||
|
||||
// Fallbacks run when no state step matches the current update.
|
||||
Fallbacks []Step
|
||||
|
||||
// Storage persists conversation state. Defaults to NewMemoryStorage.
|
||||
Storage Storage
|
||||
|
||||
// KeyStrategy derives the persistence key. Defaults to KeyByUserAndChat.
|
||||
KeyStrategy KeyStrategy
|
||||
|
||||
// AllowReEntry, when true, lets entry-point steps fire even while a
|
||||
// conversation is already active for the key (effectively restarting it).
|
||||
AllowReEntry bool
|
||||
}
|
||||
```
|
||||
|
||||
<a name="Conversation.Dispatch"></a>
|
||||
### func \(\*Conversation\) [Dispatch](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/conversation/handler.go#L87>)
|
||||
|
||||
```go
|
||||
func (c *Conversation) Dispatch(next dispatch.Handler[*api.Update]) dispatch.Handler[*api.Update]
|
||||
```
|
||||
|
||||
Dispatch is a global middleware\-shaped Handler that consumes updates and routes them through the conversation graph. Register via router.Use\(conv.Dispatch\).
|
||||
|
||||
If the conversation claims an update, downstream handlers are skipped. If the conversation does not claim it, downstream handlers run as normal.
|
||||
|
||||
<a name="Handler"></a>
|
||||
## type [Handler](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/conversation/handler.go#L44>)
|
||||
|
||||
Handler defines a step in the conversation. Receives the dispatch context and the raw update. Returns:
|
||||
|
||||
- nil to stay in the current state
|
||||
- Next\("state"\) to transition to a different state
|
||||
- End\(\) to end the conversation
|
||||
- any other non\-nil error to surface to the dispatcher \(state unchanged\)
|
||||
|
||||
```go
|
||||
type Handler func(ctx *dispatch.Context, u *api.Update) error
|
||||
```
|
||||
|
||||
<a name="KeyStrategy"></a>
|
||||
## type [KeyStrategy](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/conversation/key.go#L16>)
|
||||
|
||||
KeyStrategy derives a persistence key from an update. Strategies determine how conversation scope works — per\-user, per\-chat, or per\-user\-and\-chat. Implementations must return a stable string for the same logical scope across updates.
|
||||
|
||||
Returns the empty string if the update doesn't have enough context to derive a key \(in which case the conversation handler skips it\).
|
||||
|
||||
```go
|
||||
type KeyStrategy func(u *api.Update) string
|
||||
```
|
||||
|
||||
<a name="KeyByChat"></a>KeyByChat derives a key from the chat ID. Useful for group flows where any user in the chat can drive the conversation.
|
||||
|
||||
```go
|
||||
var KeyByChat KeyStrategy = func(u *api.Update) string {
|
||||
if cid := chatID(u); cid != 0 {
|
||||
return fmt.Sprintf("c:%d", cid)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
```
|
||||
|
||||
<a name="KeyByUser"></a>KeyByUser derives a key from the sending user's ID. Useful for DM conversations and any flow that should follow the user across chats.
|
||||
|
||||
```go
|
||||
var KeyByUser KeyStrategy = func(u *api.Update) string {
|
||||
if uid := userID(u); uid != 0 {
|
||||
return fmt.Sprintf("u:%d", uid)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
```
|
||||
|
||||
<a name="KeyByUserAndChat"></a>KeyByUserAndChat derives a key from both user and chat IDs. The most common strategy: each user has their own conversation per chat.
|
||||
|
||||
```go
|
||||
var KeyByUserAndChat KeyStrategy = func(u *api.Update) string {
|
||||
uid := userID(u)
|
||||
cid := chatID(u)
|
||||
if uid == 0 || cid == 0 {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("uc:%d:%d", cid, uid)
|
||||
}
|
||||
```
|
||||
|
||||
<a name="MemoryStorage"></a>
|
||||
## type [MemoryStorage](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/conversation/in_memory.go#L11-L14>)
|
||||
|
||||
MemoryStorage is the default in\-process Storage. It is safe for concurrent use. Conversation state is lost on process restart; use a custom Storage backed by a database for persistent flows.
|
||||
|
||||
```go
|
||||
type MemoryStorage struct {
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
```
|
||||
|
||||
<a name="NewMemoryStorage"></a>
|
||||
### func [NewMemoryStorage](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/conversation/in_memory.go#L17>)
|
||||
|
||||
```go
|
||||
func NewMemoryStorage() *MemoryStorage
|
||||
```
|
||||
|
||||
NewMemoryStorage constructs an empty in\-memory storage.
|
||||
|
||||
<a name="MemoryStorage.Delete"></a>
|
||||
### func \(\*MemoryStorage\) [Delete](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/conversation/in_memory.go#L38>)
|
||||
|
||||
```go
|
||||
func (s *MemoryStorage) Delete(_ context.Context, key string) error
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="MemoryStorage.Get"></a>
|
||||
### func \(\*MemoryStorage\) [Get](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/conversation/in_memory.go#L21>)
|
||||
|
||||
```go
|
||||
func (s *MemoryStorage) Get(_ context.Context, key string) (State, error)
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="MemoryStorage.Set"></a>
|
||||
### func \(\*MemoryStorage\) [Set](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/conversation/in_memory.go#L31>)
|
||||
|
||||
```go
|
||||
func (s *MemoryStorage) Set(_ context.Context, key string, state State) error
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="State"></a>
|
||||
## type [State](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/conversation/state.go#L9>)
|
||||
|
||||
State is a label identifying a node in the conversation graph. The empty string is the implicit "no active conversation" state.
|
||||
|
||||
```go
|
||||
type State string
|
||||
```
|
||||
|
||||
<a name="Step"></a>
|
||||
## type [Step](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/conversation/handler.go#L47-L50>)
|
||||
|
||||
Step pairs a filter with a handler for one conversation step.
|
||||
|
||||
```go
|
||||
type Step struct {
|
||||
Filter dispatch.Filter[*api.Update]
|
||||
Handler Handler
|
||||
}
|
||||
```
|
||||
|
||||
<a name="Storage"></a>
|
||||
## type [Storage](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/conversation/storage.go#L16-L20>)
|
||||
|
||||
Storage persists per\-user \(or per\-chat, per\-message — depending on the KeyStrategy in use\) conversation state across update deliveries.
|
||||
|
||||
Implementations must be safe for concurrent use.
|
||||
|
||||
```go
|
||||
type Storage interface {
|
||||
Get(ctx context.Context, key string) (State, error)
|
||||
Set(ctx context.Context, key string, state State) error
|
||||
Delete(ctx context.Context, key string) error
|
||||
}
|
||||
```
|
||||
|
||||
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)
|
||||
@@ -0,0 +1,55 @@
|
||||
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
|
||||
|
||||
# callback
|
||||
|
||||
```go
|
||||
import "github.com/lukaszraczylo/go-telegram/dispatch/filters/callback"
|
||||
```
|
||||
|
||||
Package callback provides Filter helpers for \*api.CallbackQuery payloads.
|
||||
|
||||
## Index
|
||||
|
||||
- [func Data\(pattern string\) dispatch.Filter\[\*api.CallbackQuery\]](<#Data>)
|
||||
- [func DataEquals\(s string\) dispatch.Filter\[\*api.CallbackQuery\]](<#DataEquals>)
|
||||
- [func DataPrefix\(prefix string\) dispatch.Filter\[\*api.CallbackQuery\]](<#DataPrefix>)
|
||||
- [func FromUser\(userID int64\) dispatch.Filter\[\*api.CallbackQuery\]](<#FromUser>)
|
||||
|
||||
|
||||
<a name="Data"></a>
|
||||
## func [Data](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/callback/callback.go#L14>)
|
||||
|
||||
```go
|
||||
func Data(pattern string) dispatch.Filter[*api.CallbackQuery]
|
||||
```
|
||||
|
||||
Data returns a Filter that matches callback queries whose Data matches pattern \(regex\). Panics at registration time on an invalid pattern.
|
||||
|
||||
<a name="DataEquals"></a>
|
||||
## func [DataEquals](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/callback/callback.go#L23>)
|
||||
|
||||
```go
|
||||
func DataEquals(s string) dispatch.Filter[*api.CallbackQuery]
|
||||
```
|
||||
|
||||
DataEquals returns a Filter that matches callback queries whose Data equals s exactly.
|
||||
|
||||
<a name="DataPrefix"></a>
|
||||
## func [DataPrefix](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/callback/callback.go#L31>)
|
||||
|
||||
```go
|
||||
func DataPrefix(prefix string) dispatch.Filter[*api.CallbackQuery]
|
||||
```
|
||||
|
||||
DataPrefix returns a Filter that matches callback queries whose Data starts with prefix.
|
||||
|
||||
<a name="FromUser"></a>
|
||||
## func [FromUser](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/callback/callback.go#L39>)
|
||||
|
||||
```go
|
||||
func FromUser(userID int64) dispatch.Filter[*api.CallbackQuery]
|
||||
```
|
||||
|
||||
FromUser returns a Filter that matches callback queries whose From.ID equals userID.
|
||||
|
||||
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)
|
||||
@@ -0,0 +1,35 @@
|
||||
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
|
||||
|
||||
# chatjoinrequest
|
||||
|
||||
```go
|
||||
import "github.com/lukaszraczylo/go-telegram/dispatch/filters/chatjoinrequest"
|
||||
```
|
||||
|
||||
Package chatjoinrequest provides Filter helpers for \*api.ChatJoinRequest payloads.
|
||||
|
||||
## Index
|
||||
|
||||
- [func FromUser\(uid int64\) dispatch.Filter\[\*api.ChatJoinRequest\]](<#FromUser>)
|
||||
- [func InChat\(cid int64\) dispatch.Filter\[\*api.ChatJoinRequest\]](<#InChat>)
|
||||
|
||||
|
||||
<a name="FromUser"></a>
|
||||
## func [FromUser](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/chatjoinrequest/chatjoinrequest.go#L11>)
|
||||
|
||||
```go
|
||||
func FromUser(uid int64) dispatch.Filter[*api.ChatJoinRequest]
|
||||
```
|
||||
|
||||
FromUser returns a Filter that matches join requests where the requesting user's ID equals uid.
|
||||
|
||||
<a name="InChat"></a>
|
||||
## func [InChat](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/chatjoinrequest/chatjoinrequest.go#L19>)
|
||||
|
||||
```go
|
||||
func InChat(cid int64) dispatch.Filter[*api.ChatJoinRequest]
|
||||
```
|
||||
|
||||
InChat returns a Filter that matches join requests directed at the chat with the given chat ID.
|
||||
|
||||
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)
|
||||
@@ -0,0 +1,35 @@
|
||||
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
|
||||
|
||||
# chatmember
|
||||
|
||||
```go
|
||||
import "github.com/lukaszraczylo/go-telegram/dispatch/filters/chatmember"
|
||||
```
|
||||
|
||||
Package chatmember provides Filter helpers for \*api.ChatMemberUpdated payloads.
|
||||
|
||||
## Index
|
||||
|
||||
- [func FromUser\(uid int64\) dispatch.Filter\[\*api.ChatMemberUpdated\]](<#FromUser>)
|
||||
- [func NewStatus\(s string\) dispatch.Filter\[\*api.ChatMemberUpdated\]](<#NewStatus>)
|
||||
|
||||
|
||||
<a name="FromUser"></a>
|
||||
## func [FromUser](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/chatmember/chatmember.go#L37>)
|
||||
|
||||
```go
|
||||
func FromUser(uid int64) dispatch.Filter[*api.ChatMemberUpdated]
|
||||
```
|
||||
|
||||
FromUser returns a Filter that matches updates where the acting user \(From.ID\) equals uid.
|
||||
|
||||
<a name="NewStatus"></a>
|
||||
## func [NewStatus](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/chatmember/chatmember.go#L11>)
|
||||
|
||||
```go
|
||||
func NewStatus(s string) dispatch.Filter[*api.ChatMemberUpdated]
|
||||
```
|
||||
|
||||
NewStatus returns a Filter that matches updates where the new chat member status equals s \(e.g. "member", "administrator", "kicked", "left"\).
|
||||
|
||||
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)
|
||||
@@ -0,0 +1,45 @@
|
||||
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
|
||||
|
||||
# inline
|
||||
|
||||
```go
|
||||
import "github.com/lukaszraczylo/go-telegram/dispatch/filters/inline"
|
||||
```
|
||||
|
||||
Package inline provides Filter helpers for \*api.InlineQuery payloads.
|
||||
|
||||
## Index
|
||||
|
||||
- [func Query\(pattern string\) dispatch.Filter\[\*api.InlineQuery\]](<#Query>)
|
||||
- [func QueryEquals\(s string\) dispatch.Filter\[\*api.InlineQuery\]](<#QueryEquals>)
|
||||
- [func QueryPrefix\(prefix string\) dispatch.Filter\[\*api.InlineQuery\]](<#QueryPrefix>)
|
||||
|
||||
|
||||
<a name="Query"></a>
|
||||
## func [Query](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/inline/inline.go#L14>)
|
||||
|
||||
```go
|
||||
func Query(pattern string) dispatch.Filter[*api.InlineQuery]
|
||||
```
|
||||
|
||||
Query returns a Filter that matches inline queries whose Query field matches pattern \(regex\). Panics at registration time on an invalid pattern.
|
||||
|
||||
<a name="QueryEquals"></a>
|
||||
## func [QueryEquals](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/inline/inline.go#L23>)
|
||||
|
||||
```go
|
||||
func QueryEquals(s string) dispatch.Filter[*api.InlineQuery]
|
||||
```
|
||||
|
||||
QueryEquals returns a Filter that matches inline queries whose Query equals s exactly.
|
||||
|
||||
<a name="QueryPrefix"></a>
|
||||
## func [QueryPrefix](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/inline/inline.go#L31>)
|
||||
|
||||
```go
|
||||
func QueryPrefix(prefix string) dispatch.Filter[*api.InlineQuery]
|
||||
```
|
||||
|
||||
QueryPrefix returns a Filter that matches inline queries whose Query starts with prefix.
|
||||
|
||||
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)
|
||||
@@ -0,0 +1,155 @@
|
||||
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
|
||||
|
||||
# message
|
||||
|
||||
```go
|
||||
import "github.com/lukaszraczylo/go-telegram/dispatch/filters/message"
|
||||
```
|
||||
|
||||
Package message provides Filter helpers for \*api.Message payloads.
|
||||
|
||||
## Index
|
||||
|
||||
- [func AnyCommand\(\) dispatch.Filter\[\*api.Message\]](<#AnyCommand>)
|
||||
- [func ChatType\(t api.ChatType\) dispatch.Filter\[\*api.Message\]](<#ChatType>)
|
||||
- [func Command\(name string\) dispatch.Filter\[\*api.Message\]](<#Command>)
|
||||
- [func FromUser\(userID int64\) dispatch.Filter\[\*api.Message\]](<#FromUser>)
|
||||
- [func HasDocument\(\) dispatch.Filter\[\*api.Message\]](<#HasDocument>)
|
||||
- [func HasEntity\(t api.MessageEntityType\) dispatch.Filter\[\*api.Message\]](<#HasEntity>)
|
||||
- [func HasPhoto\(\) dispatch.Filter\[\*api.Message\]](<#HasPhoto>)
|
||||
- [func InChat\(chatID int64\) dispatch.Filter\[\*api.Message\]](<#InChat>)
|
||||
- [func IsForward\(\) dispatch.Filter\[\*api.Message\]](<#IsForward>)
|
||||
- [func IsReply\(\) dispatch.Filter\[\*api.Message\]](<#IsReply>)
|
||||
- [func Text\(pattern string\) dispatch.Filter\[\*api.Message\]](<#Text>)
|
||||
- [func TextContains\(sub string\) dispatch.Filter\[\*api.Message\]](<#TextContains>)
|
||||
- [func TextEquals\(s string\) dispatch.Filter\[\*api.Message\]](<#TextEquals>)
|
||||
- [func TextPrefix\(prefix string\) dispatch.Filter\[\*api.Message\]](<#TextPrefix>)
|
||||
|
||||
|
||||
<a name="AnyCommand"></a>
|
||||
## func [AnyCommand](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/message/message.go#L69>)
|
||||
|
||||
```go
|
||||
func AnyCommand() dispatch.Filter[*api.Message]
|
||||
```
|
||||
|
||||
AnyCommand returns a Filter that matches any message starting with a bot\_command entity at offset 0.
|
||||
|
||||
<a name="ChatType"></a>
|
||||
## func [ChatType](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/message/message.go#L124>)
|
||||
|
||||
```go
|
||||
func ChatType(t api.ChatType) dispatch.Filter[*api.Message]
|
||||
```
|
||||
|
||||
ChatType returns a Filter that matches messages whose Chat.Type equals t.
|
||||
|
||||
<a name="Command"></a>
|
||||
## func [Command](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/message/message.go#L44>)
|
||||
|
||||
```go
|
||||
func Command(name string) dispatch.Filter[*api.Message]
|
||||
```
|
||||
|
||||
Command returns a Filter that matches messages whose first entity is a bot\_command equal to "/\<name\>" \(with or without "@BotName" suffix\).
|
||||
|
||||
<a name="FromUser"></a>
|
||||
## func [FromUser](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/message/message.go#L131>)
|
||||
|
||||
```go
|
||||
func FromUser(userID int64) dispatch.Filter[*api.Message]
|
||||
```
|
||||
|
||||
FromUser returns a Filter that matches messages whose From.ID equals userID.
|
||||
|
||||
<a name="HasDocument"></a>
|
||||
## func [HasDocument](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/message/message.go#L101>)
|
||||
|
||||
```go
|
||||
func HasDocument() dispatch.Filter[*api.Message]
|
||||
```
|
||||
|
||||
HasDocument returns a Filter that matches messages with a Document attachment.
|
||||
|
||||
<a name="HasEntity"></a>
|
||||
## func [HasEntity](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/message/message.go#L109>)
|
||||
|
||||
```go
|
||||
func HasEntity(t api.MessageEntityType) dispatch.Filter[*api.Message]
|
||||
```
|
||||
|
||||
HasEntity returns a Filter that matches messages whose Entities contain at least one entity of type t \(e.g. api.MessageEntityTypeBotCommand\).
|
||||
|
||||
<a name="HasPhoto"></a>
|
||||
## func [HasPhoto](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/message/message.go#L94>)
|
||||
|
||||
```go
|
||||
func HasPhoto() dispatch.Filter[*api.Message]
|
||||
```
|
||||
|
||||
HasPhoto returns a Filter that matches messages with a Photo attachment.
|
||||
|
||||
<a name="InChat"></a>
|
||||
## func [InChat](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/message/message.go#L138>)
|
||||
|
||||
```go
|
||||
func InChat(chatID int64) dispatch.Filter[*api.Message]
|
||||
```
|
||||
|
||||
InChat returns a Filter that matches messages whose Chat.ID equals chatID.
|
||||
|
||||
<a name="IsForward"></a>
|
||||
## func [IsForward](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/message/message.go#L87>)
|
||||
|
||||
```go
|
||||
func IsForward() dispatch.Filter[*api.Message]
|
||||
```
|
||||
|
||||
IsForward returns a Filter that matches messages that have ForwardOrigin set.
|
||||
|
||||
<a name="IsReply"></a>
|
||||
## func [IsReply](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/message/message.go#L80>)
|
||||
|
||||
```go
|
||||
func IsReply() dispatch.Filter[*api.Message]
|
||||
```
|
||||
|
||||
IsReply returns a Filter that matches messages that have ReplyToMessage set.
|
||||
|
||||
<a name="Text"></a>
|
||||
## func [Text](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/message/message.go#L14>)
|
||||
|
||||
```go
|
||||
func Text(pattern string) dispatch.Filter[*api.Message]
|
||||
```
|
||||
|
||||
Text returns a Filter that matches messages whose Text matches pattern \(regex\). Panics at registration time on an invalid pattern.
|
||||
|
||||
<a name="TextContains"></a>
|
||||
## func [TextContains](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/message/message.go#L36>)
|
||||
|
||||
```go
|
||||
func TextContains(sub string) dispatch.Filter[*api.Message]
|
||||
```
|
||||
|
||||
TextContains returns a Filter that matches messages whose Text contains sub.
|
||||
|
||||
<a name="TextEquals"></a>
|
||||
## func [TextEquals](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/message/message.go#L22>)
|
||||
|
||||
```go
|
||||
func TextEquals(s string) dispatch.Filter[*api.Message]
|
||||
```
|
||||
|
||||
TextEquals returns a Filter that matches messages whose Text equals s exactly.
|
||||
|
||||
<a name="TextPrefix"></a>
|
||||
## func [TextPrefix](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/message/message.go#L29>)
|
||||
|
||||
```go
|
||||
func TextPrefix(prefix string) dispatch.Filter[*api.Message]
|
||||
```
|
||||
|
||||
TextPrefix returns a Filter that matches messages whose Text starts with prefix.
|
||||
|
||||
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)
|
||||
@@ -0,0 +1,35 @@
|
||||
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
|
||||
|
||||
# precheckoutquery
|
||||
|
||||
```go
|
||||
import "github.com/lukaszraczylo/go-telegram/dispatch/filters/precheckoutquery"
|
||||
```
|
||||
|
||||
Package precheckoutquery provides Filter helpers for \*api.PreCheckoutQuery payloads.
|
||||
|
||||
## Index
|
||||
|
||||
- [func Currency\(c string\) dispatch.Filter\[\*api.PreCheckoutQuery\]](<#Currency>)
|
||||
- [func FromUser\(uid int64\) dispatch.Filter\[\*api.PreCheckoutQuery\]](<#FromUser>)
|
||||
|
||||
|
||||
<a name="Currency"></a>
|
||||
## func [Currency](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/precheckoutquery/precheckoutquery.go#L11>)
|
||||
|
||||
```go
|
||||
func Currency(c string) dispatch.Filter[*api.PreCheckoutQuery]
|
||||
```
|
||||
|
||||
Currency returns a Filter that matches pre\-checkout queries with the given ISO 4217 currency code \(e.g. "USD", "EUR", "XTR"\).
|
||||
|
||||
<a name="FromUser"></a>
|
||||
## func [FromUser](<https://github.com/lukaszraczylo/go-telegram/blob/main/dispatch/filters/precheckoutquery/precheckoutquery.go#L19>)
|
||||
|
||||
```go
|
||||
func FromUser(uid int64) dispatch.Filter[*api.PreCheckoutQuery]
|
||||
```
|
||||
|
||||
FromUser returns a Filter that matches pre\-checkout queries sent by the user with the given ID.
|
||||
|
||||
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)
|
||||
@@ -0,0 +1,241 @@
|
||||
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
|
||||
|
||||
# transport
|
||||
|
||||
```go
|
||||
import "github.com/lukaszraczylo/go-telegram/transport"
|
||||
```
|
||||
|
||||
Package transport provides update delivery mechanisms \(long\-poll and webhook\) that feed updates into the dispatch package's Router.
|
||||
|
||||
All implementations satisfy the Updater interface so user code can swap one for the other without touching handler logic.
|
||||
|
||||
## Index
|
||||
|
||||
- [type BackoffStrategy](<#BackoffStrategy>)
|
||||
- [type ExponentialBackoff](<#ExponentialBackoff>)
|
||||
- [func DefaultBackoff\(\) \*ExponentialBackoff](<#DefaultBackoff>)
|
||||
- [func \(b \*ExponentialBackoff\) NextDelay\(attempt int\) time.Duration](<#ExponentialBackoff.NextDelay>)
|
||||
- [type LongPoller](<#LongPoller>)
|
||||
- [func NewLongPoller\(b \*client.Bot\) \*LongPoller](<#NewLongPoller>)
|
||||
- [func \(p \*LongPoller\) Run\(ctx context.Context\) error](<#LongPoller.Run>)
|
||||
- [func \(p \*LongPoller\) Stop\(ctx context.Context\) error](<#LongPoller.Stop>)
|
||||
- [func \(p \*LongPoller\) Updates\(\) \<\-chan api.Update](<#LongPoller.Updates>)
|
||||
- [type Updater](<#Updater>)
|
||||
- [type WebhookOption](<#WebhookOption>)
|
||||
- [func WithBufferSize\(n int\) WebhookOption](<#WithBufferSize>)
|
||||
- [type WebhookServer](<#WebhookServer>)
|
||||
- [func NewWebhookServer\(b \*client.Bot, opts ...WebhookOption\) \*WebhookServer](<#NewWebhookServer>)
|
||||
- [func \(w \*WebhookServer\) ListenAndServe\(ctx context.Context, addr string\) error](<#WebhookServer.ListenAndServe>)
|
||||
- [func \(w \*WebhookServer\) Run\(ctx context.Context\) error](<#WebhookServer.Run>)
|
||||
- [func \(w \*WebhookServer\) ServeHTTP\(rw http.ResponseWriter, r \*http.Request\)](<#WebhookServer.ServeHTTP>)
|
||||
- [func \(w \*WebhookServer\) Stop\(ctx context.Context\) error](<#WebhookServer.Stop>)
|
||||
- [func \(w \*WebhookServer\) Updates\(\) \<\-chan api.Update](<#WebhookServer.Updates>)
|
||||
|
||||
|
||||
<a name="BackoffStrategy"></a>
|
||||
## type [BackoffStrategy](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/backoff.go#L12-L14>)
|
||||
|
||||
BackoffStrategy returns the duration to wait before the next attempt after \`attempt\` consecutive failures \(1\-based\). Implementations must be safe to call from a single goroutine.
|
||||
|
||||
```go
|
||||
type BackoffStrategy interface {
|
||||
NextDelay(attempt int) time.Duration
|
||||
}
|
||||
```
|
||||
|
||||
<a name="ExponentialBackoff"></a>
|
||||
## type [ExponentialBackoff](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/backoff.go#L18-L23>)
|
||||
|
||||
ExponentialBackoff implements capped exponential back\-off with jitter. Defaults: Base=500ms, Max=30s, Factor=2.0, Jitter=0.2.
|
||||
|
||||
```go
|
||||
type ExponentialBackoff struct {
|
||||
Base time.Duration
|
||||
Max time.Duration
|
||||
Factor float64
|
||||
Jitter float64 // 0..1; fraction of computed delay added/subtracted at random
|
||||
}
|
||||
```
|
||||
|
||||
<a name="DefaultBackoff"></a>
|
||||
### func [DefaultBackoff](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/backoff.go#L26>)
|
||||
|
||||
```go
|
||||
func DefaultBackoff() *ExponentialBackoff
|
||||
```
|
||||
|
||||
DefaultBackoff returns an ExponentialBackoff with library defaults.
|
||||
|
||||
<a name="ExponentialBackoff.NextDelay"></a>
|
||||
### func \(\*ExponentialBackoff\) [NextDelay](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/backoff.go#L36>)
|
||||
|
||||
```go
|
||||
func (b *ExponentialBackoff) NextDelay(attempt int) time.Duration
|
||||
```
|
||||
|
||||
NextDelay implements BackoffStrategy.
|
||||
|
||||
<a name="LongPoller"></a>
|
||||
## type [LongPoller](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/longpoll.go#L21-L31>)
|
||||
|
||||
LongPoller pulls updates via Bot.GetUpdates in a loop, advancing the offset cursor after each batch. It applies BackoffStrategy on transient errors \(network failures, 5xx, 429\).
|
||||
|
||||
At\-least\-once semantics on shutdown: when ctx is cancelled or Stop is called mid\-batch, any updates already fetched but not yet dispatched are dropped without advancing the offset. On the next restart those updates will be re\-delivered by Telegram.
|
||||
|
||||
```go
|
||||
type LongPoller struct {
|
||||
Bot *client.Bot
|
||||
Timeout int // seconds, default 30
|
||||
Limit int // 1..100, default 100
|
||||
AllowedTypes []api.UpdateType
|
||||
Backoff BackoffStrategy
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
```
|
||||
|
||||
<a name="NewLongPoller"></a>
|
||||
### func [NewLongPoller](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/longpoll.go#L34>)
|
||||
|
||||
```go
|
||||
func NewLongPoller(b *client.Bot) *LongPoller
|
||||
```
|
||||
|
||||
NewLongPoller constructs a LongPoller with sensible defaults.
|
||||
|
||||
<a name="LongPoller.Run"></a>
|
||||
### func \(\*LongPoller\) [Run](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/longpoll.go#L51>)
|
||||
|
||||
```go
|
||||
func (p *LongPoller) Run(ctx context.Context) error
|
||||
```
|
||||
|
||||
Run implements Updater. It blocks until ctx is cancelled, Stop is called, or a fatal error occurs \(e.g. unauthorized\). See LongPoller for at\-least\-once delivery semantics on shutdown.
|
||||
|
||||
<a name="LongPoller.Stop"></a>
|
||||
### func \(\*LongPoller\) [Stop](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/longpoll.go#L126>)
|
||||
|
||||
```go
|
||||
func (p *LongPoller) Stop(ctx context.Context) error
|
||||
```
|
||||
|
||||
Stop implements Updater.
|
||||
|
||||
<a name="LongPoller.Updates"></a>
|
||||
### func \(\*LongPoller\) [Updates](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/longpoll.go#L46>)
|
||||
|
||||
```go
|
||||
func (p *LongPoller) Updates() <-chan api.Update
|
||||
```
|
||||
|
||||
Updates implements Updater.
|
||||
|
||||
<a name="Updater"></a>
|
||||
## type [Updater](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/updater.go#L13-L23>)
|
||||
|
||||
Updater is the abstraction over update sources. Implementations must:
|
||||
|
||||
- return a channel from Updates\(\) that receives every Update they read.
|
||||
- close the channel after Run returns.
|
||||
- honour ctx cancellation in Run.
|
||||
|
||||
```go
|
||||
type Updater interface {
|
||||
// Updates returns the channel updates flow into. Multiple readers
|
||||
// is implementation-defined; users should treat it as single-reader.
|
||||
Updates() <-chan api.Update
|
||||
// Run blocks until ctx is cancelled or a fatal error occurs. It is
|
||||
// the user's responsibility to call Run in a goroutine if needed.
|
||||
Run(ctx context.Context) error
|
||||
// Stop signals Run to exit and waits for the channel to drain.
|
||||
// Implementations must be idempotent.
|
||||
Stop(ctx context.Context) error
|
||||
}
|
||||
```
|
||||
|
||||
<a name="WebhookOption"></a>
|
||||
## type [WebhookOption](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L38>)
|
||||
|
||||
WebhookOption configures a WebhookServer at construction time.
|
||||
|
||||
```go
|
||||
type WebhookOption func(*webhookOptions)
|
||||
```
|
||||
|
||||
<a name="WithBufferSize"></a>
|
||||
### func [WithBufferSize](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L46>)
|
||||
|
||||
```go
|
||||
func WithBufferSize(n int) WebhookOption
|
||||
```
|
||||
|
||||
WithBufferSize sets the size of the updates channel buffer. Default is 64.
|
||||
|
||||
<a name="WebhookServer"></a>
|
||||
## type [WebhookServer](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L24-L35>)
|
||||
|
||||
WebhookServer implements Updater by exposing an http.Handler that receives updates from Telegram. It can be mounted on the user's own HTTP server \(via ServeHTTP\) or run standalone \(via ListenAndServe\).
|
||||
|
||||
```go
|
||||
type WebhookServer struct {
|
||||
Bot *client.Bot
|
||||
SecretToken string // verify X-Telegram-Bot-Api-Secret-Token; empty disables
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
```
|
||||
|
||||
<a name="NewWebhookServer"></a>
|
||||
### func [NewWebhookServer](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L52>)
|
||||
|
||||
```go
|
||||
func NewWebhookServer(b *client.Bot, opts ...WebhookOption) *WebhookServer
|
||||
```
|
||||
|
||||
NewWebhookServer constructs a WebhookServer with default buffer size \(64\). Use WithBufferSize to override.
|
||||
|
||||
<a name="WebhookServer.ListenAndServe"></a>
|
||||
### func \(\*WebhookServer\) [ListenAndServe](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L150>)
|
||||
|
||||
```go
|
||||
func (w *WebhookServer) ListenAndServe(ctx context.Context, addr string) error
|
||||
```
|
||||
|
||||
ListenAndServe starts an HTTP server on addr and blocks until Stop is called \(which triggers Shutdown with the caller's context\) or the server returns an error other than http.ErrServerClosed. Callers must invoke Stop\(ctx\) to cleanly shut down the server; the ctx passed here is only used as the server's base context for incoming requests.
|
||||
|
||||
<a name="WebhookServer.Run"></a>
|
||||
### func \(\*WebhookServer\) [Run](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L71>)
|
||||
|
||||
```go
|
||||
func (w *WebhookServer) Run(ctx context.Context) error
|
||||
```
|
||||
|
||||
Run implements Updater. It blocks until Stop is called or ctx is cancelled. If the server has not been started via ListenAndServe, Run only watches for shutdown — the user is expected to mount ServeHTTP on their own router.
|
||||
|
||||
<a name="WebhookServer.ServeHTTP"></a>
|
||||
### func \(\*WebhookServer\) [ServeHTTP](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L97>)
|
||||
|
||||
```go
|
||||
func (w *WebhookServer) ServeHTTP(rw http.ResponseWriter, r *http.Request)
|
||||
```
|
||||
|
||||
ServeHTTP implements http.Handler. Telegram POSTs each update as JSON to this endpoint. Non\-POST requests get 405; bad bodies get 400; secret token mismatches get 401.
|
||||
|
||||
<a name="WebhookServer.Stop"></a>
|
||||
### func \(\*WebhookServer\) [Stop](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L83>)
|
||||
|
||||
```go
|
||||
func (w *WebhookServer) Stop(ctx context.Context) error
|
||||
```
|
||||
|
||||
Stop implements Updater.
|
||||
|
||||
<a name="WebhookServer.Updates"></a>
|
||||
### func \(\*WebhookServer\) [Updates](<https://github.com/lukaszraczylo/go-telegram/blob/main/transport/webhook.go#L65>)
|
||||
|
||||
```go
|
||||
func (w *WebhookServer) Updates() <-chan api.Update
|
||||
```
|
||||
|
||||
Updates implements Updater.
|
||||
|
||||
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,499 +0,0 @@
|
||||
# go-telegram — Design Spec
|
||||
|
||||
- **Date:** 2026-05-08
|
||||
- **Module:** `github.com/lukaszraczylo/go-telegram`
|
||||
- **License:** MIT
|
||||
- **Status:** Approved for planning
|
||||
- **Author:** Lukasz Raczylo (with Claude assistance)
|
||||
|
||||
## 1. Purpose & scope
|
||||
|
||||
A Go library for the Telegram Bot API, primarily a portfolio piece showcasing:
|
||||
|
||||
- Codegen-driven full API coverage (parsed from `https://core.telegram.org/bots/api`)
|
||||
- Pragmatic Go generics
|
||||
- Pluggable HTTP transport and JSON codec for resource-conscious deployments
|
||||
- Long-poll and webhook update delivery behind a unified interface
|
||||
- A typed dispatcher/router for handlers, commands, and callbacks
|
||||
- Comprehensive testify-based unit tests with golden fixtures for codegen
|
||||
|
||||
Out of scope for v1: the daily auto-regen GitHub Action (deferred — see §11).
|
||||
|
||||
## 2. Non-goals
|
||||
|
||||
- Competing on completeness/maturity with `mymmrac/telego` or `go-telegram-bot-api/telegram-bot-api`. We optimise for clarity and design.
|
||||
- Bot-framework features beyond the dispatcher (no plugin marketplace, no FSM, no scenes).
|
||||
- A documentation website. `pkg.go.dev` + README is sufficient.
|
||||
|
||||
## 3. Requirements
|
||||
|
||||
### Functional
|
||||
|
||||
1. Cover all Telegram Bot API methods and types via codegen.
|
||||
2. Support long-poll and webhook update delivery, both implementing one `Updater` interface.
|
||||
3. Allow the user to swap the HTTP client (e.g. `valyala/fasthttp`) and JSON codec (e.g. `goccy/go-json`).
|
||||
4. Provide a typed dispatcher with command, text-regex, callback, and inline-query matching plus generic middleware.
|
||||
5. Provide unit tests using `stretchr/testify` covering happy paths and explicit edge cases.
|
||||
6. Provide an optional integration test suite gated by build tag and env vars.
|
||||
|
||||
### Non-functional
|
||||
|
||||
- Lean dependency footprint (stdlib + `golang.org/x/net/html` + testify).
|
||||
- Deterministic, reproducible codegen (`go test ./... -run TestGen` is hermetic).
|
||||
- Generated files committed to the repo so consumers do not need to run codegen.
|
||||
- Doc comments on every exported symbol; generated types carry Telegram's verbatim prose.
|
||||
|
||||
## 4. Architecture
|
||||
|
||||
Two-stage codegen with a JSON intermediate representation:
|
||||
|
||||
```
|
||||
HTML page cmd/scrape api.json (IR) cmd/genapi api/*.gen.go
|
||||
```
|
||||
|
||||
The IR is committed; PRs from a future regen workflow show the diff against the previous IR, providing a readable changelog of Telegram-side changes.
|
||||
|
||||
### 4.1 Repository layout
|
||||
|
||||
```
|
||||
go-telegram/
|
||||
├── api/ GENERATED types + method wrappers (typed param structs)
|
||||
│ ├── types.gen.go
|
||||
│ ├── methods.gen.go
|
||||
│ └── enums.gen.go
|
||||
├── client/ HAND Bot client, request building, error handling
|
||||
│ ├── client.go
|
||||
│ ├── codec.go Codec interface + encoding/json default
|
||||
│ ├── httpclient.go HTTPDoer interface + net/http default
|
||||
│ ├── errors.go Typed APIError, NetworkError, ParseError
|
||||
│ └── result.go generic Result[T] decode
|
||||
├── transport/ HAND Updater abstraction
|
||||
│ ├── updater.go Updater interface
|
||||
│ ├── longpoll.go LongPoller
|
||||
│ └── webhook.go WebhookServer
|
||||
├── dispatch/ HAND Handler router
|
||||
│ ├── router.go Router, OnCommand/OnCallback/OnText
|
||||
│ ├── middleware.go Generic Middleware[T]
|
||||
│ └── context.go Per-update context
|
||||
├── internal/
|
||||
│ └── spec/ Shared IR types
|
||||
│ ├── ir.go Types describing parsed Telegram API
|
||||
│ └── api.json Committed golden IR (regenerated by scraper)
|
||||
├── cmd/
|
||||
│ ├── scrape/ HTML → api.json
|
||||
│ └── genapi/ api.json → api/*.gen.go
|
||||
├── examples/
|
||||
│ ├── echo/ Long-poll echo bot
|
||||
│ └── webhook/ Webhook bot with command router
|
||||
├── testdata/
|
||||
│ ├── html/ Golden HTML snapshots for scraper
|
||||
│ ├── golden/ Expected api.json + emitted Go for codegen tests
|
||||
│ └── responses/ Canned Telegram JSON responses
|
||||
├── .github/workflows/
|
||||
│ └── ci.yml lint + test + codegen-clean check
|
||||
├── Makefile regen, test, lint targets
|
||||
├── go.mod module github.com/lukaszraczylo/go-telegram
|
||||
├── LICENSE MIT
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 5. Core types and client (`client/`)
|
||||
|
||||
### 5.1 Pluggability interfaces
|
||||
|
||||
```go
|
||||
// Codec is the JSON encoder/decoder. Default impl wraps encoding/json.
|
||||
type Codec interface {
|
||||
Marshal(v any) ([]byte, error)
|
||||
Unmarshal(data []byte, v any) error
|
||||
}
|
||||
|
||||
// HTTPDoer is the HTTP transport. Default is *http.Client.
|
||||
type HTTPDoer interface {
|
||||
Do(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
// Logger is a slog-shaped interface; nil-safe default writes nowhere.
|
||||
type Logger interface {
|
||||
Debug(msg string, attrs ...any)
|
||||
Info(msg string, attrs ...any)
|
||||
Warn(msg string, attrs ...any)
|
||||
Error(msg string, attrs ...any)
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 Bot client
|
||||
|
||||
```go
|
||||
type Bot struct {
|
||||
token string
|
||||
base string // https://api.telegram.org
|
||||
http HTTPDoer
|
||||
codec Codec
|
||||
logger Logger
|
||||
}
|
||||
|
||||
type Option func(*Bot)
|
||||
func WithHTTPClient(c HTTPDoer) Option
|
||||
func WithCodec(c Codec) Option
|
||||
func WithBaseURL(url string) Option
|
||||
func WithLogger(l Logger) Option
|
||||
|
||||
func New(token string, opts ...Option) *Bot
|
||||
```
|
||||
|
||||
Constructor-level functional options only; per-call params are typed structs (codegen-friendly).
|
||||
|
||||
### 5.3 Result envelope and call helper
|
||||
|
||||
```go
|
||||
type Result[T any] struct {
|
||||
OK bool `json:"ok"`
|
||||
Result T `json:"result,omitempty"`
|
||||
ErrorCode int `json:"error_code,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Parameters *ResponseParameters `json:"parameters,omitempty"`
|
||||
}
|
||||
|
||||
// Single point for marshalling, URL signing, decoding, error mapping.
|
||||
// Used by every generated method wrapper.
|
||||
func call[Req, Resp any](ctx context.Context, b *Bot, method string, req Req) (Resp, error)
|
||||
```
|
||||
|
||||
Generated wrappers stay thin:
|
||||
|
||||
```go
|
||||
func (b *Bot) SendMessage(ctx context.Context, p *SendMessageParams) (*Message, error) {
|
||||
return call[*SendMessageParams, *Message](ctx, b, "sendMessage", p)
|
||||
}
|
||||
```
|
||||
|
||||
### 5.4 Errors
|
||||
|
||||
```go
|
||||
type APIError struct {
|
||||
Code int
|
||||
Description string
|
||||
Parameters *ResponseParameters // retry_after, migrate_to_chat_id
|
||||
}
|
||||
func (e *APIError) Error() string
|
||||
func (e *APIError) IsRetryable() bool // 429, 5xx
|
||||
func (e *APIError) RetryAfter() time.Duration
|
||||
|
||||
type NetworkError struct{ Err error }
|
||||
type ParseError struct{ Err error; Body []byte }
|
||||
```
|
||||
|
||||
Sentinel errors via `errors.Is`: `ErrUnauthorized`, `ErrChatNotFound`, `ErrMessageNotModified`, `ErrTooManyRequests`. Mapped from `error_code` plus description-prefix matching.
|
||||
|
||||
## 6. Codegen pipeline
|
||||
|
||||
### 6.1 Stage 1 — `cmd/scrape/`
|
||||
|
||||
- Input: live URL `https://core.telegram.org/bots/api` (default) or a local HTML fixture (`-input` flag).
|
||||
- Parser: `golang.org/x/net/html` (no goquery).
|
||||
- Walk strategy: traverse `<h4>` headings sequentially. Lowercase first letter → method. Uppercase → type.
|
||||
- Following `<p>` until next heading → description.
|
||||
- Following `<table>` → fields/params (columns: Field|Type|Required|Description for types; Parameter|Type|Required|Description for methods).
|
||||
- Return type extracted by regex on description: `Returns *X* on success`, `Returns an Array of X`, `Returns True on success`.
|
||||
- Italic markers in the type column denote optional, array depth, and union-member candidates.
|
||||
- "Recent changes" section parsed for current API version.
|
||||
|
||||
### 6.2 Intermediate representation (`internal/spec/ir.go`)
|
||||
|
||||
```go
|
||||
type API struct {
|
||||
Version string
|
||||
Types []TypeDecl
|
||||
Methods []MethodDecl
|
||||
}
|
||||
type TypeDecl struct {
|
||||
Name string
|
||||
Doc string
|
||||
Fields []Field
|
||||
OneOf []string // unions (InputMedia, ChatMember, …)
|
||||
}
|
||||
type MethodDecl struct {
|
||||
Name string // sendMessage
|
||||
Doc string
|
||||
Params []Field
|
||||
Returns TypeRef
|
||||
HasFiles bool // forces multipart
|
||||
}
|
||||
type Field struct {
|
||||
Name string
|
||||
JSONName string
|
||||
Type TypeRef
|
||||
Required bool
|
||||
Doc string
|
||||
}
|
||||
type Kind int
|
||||
const (
|
||||
KindPrimitive Kind = iota
|
||||
KindNamed
|
||||
KindArray
|
||||
KindOneOf
|
||||
)
|
||||
type TypeRef struct {
|
||||
Kind Kind
|
||||
Name string
|
||||
ElemType *TypeRef
|
||||
Variants []string
|
||||
}
|
||||
```
|
||||
|
||||
`internal/spec/api.json` is committed. Marshalling is stable (sorted fields, deterministic JSON output) so diffs read as a Telegram changelog.
|
||||
|
||||
### 6.3 Stage 2 — `cmd/genapi/`
|
||||
|
||||
- Reads `api.json`.
|
||||
- Emits Go via `text/template`, finalised with `go/format`.
|
||||
- Templates:
|
||||
- `types.tmpl` → struct per `TypeDecl`. Optional fields are pointers (or `omitempty` for slices/maps). Doc comments verbatim from API.
|
||||
- `enums.tmpl` → string consts for known enumerations (parse modes, chat types, etc., extracted from doc prose).
|
||||
- `oneof.tmpl` → union types as `interface { isFooBar() }` plus concrete impls and a `UnmarshalJSON` that switches on a discriminator field (typically `type` or `source`).
|
||||
- `methods.tmpl` → param struct + thin `Bot.<MethodName>` wrapper using `call[…]`.
|
||||
- `multipart.tmpl` → for methods with `HasFiles`, custom request builder using `mime/multipart`.
|
||||
- Header on every emitted file: `// Code generated by cmd/genapi. DO NOT EDIT.` + `//go:build !ignore_autogenerated`.
|
||||
|
||||
### 6.4 Makefile contract
|
||||
|
||||
The Makefile owns the codegen entry points; tools and CI call `make`, never raw `go run`:
|
||||
|
||||
| Target | What it does |
|
||||
|---|---|
|
||||
| `make snapshot` | `curl -fsSL https://core.telegram.org/bots/api > testdata/html/snapshot_<date>.html` and update a `latest.html` symlink. |
|
||||
| `make regen` | Run scraper against `testdata/html/latest.html`, then run emitter. Writes `internal/spec/api.json` and `api/*.gen.go`. |
|
||||
| `make regen-from-fixture` | Same as `make regen` but pinned to `testdata/html/snapshot_2026-05-08.html` for deterministic CI checks. |
|
||||
| `make test` | `go test -race ./...` |
|
||||
| `make test-update-golden` | `go test -run TestGen -update ./...` to refresh golden fixtures. |
|
||||
| `make lint` | `go vet` + `staticcheck`. |
|
||||
| `make integration` | `go test -tags=integration ./test/integration/...` (requires env). |
|
||||
|
||||
## 7. Transport (`transport/`)
|
||||
|
||||
```go
|
||||
type Updater interface {
|
||||
Updates() <-chan api.Update
|
||||
Run(ctx context.Context) error
|
||||
Stop(ctx context.Context) error
|
||||
}
|
||||
```
|
||||
|
||||
### 7.1 LongPoller
|
||||
|
||||
```go
|
||||
type LongPoller struct {
|
||||
Bot *client.Bot
|
||||
Timeout int // seconds, default 30
|
||||
Limit int // 1..100, default 100
|
||||
AllowedTypes []api.UpdateType
|
||||
Backoff BackoffStrategy
|
||||
}
|
||||
```
|
||||
|
||||
Calls `getUpdates` in a loop, tracks `offset`, applies exponential backoff on transient errors via `BackoffStrategy`.
|
||||
|
||||
### 7.2 WebhookServer
|
||||
|
||||
```go
|
||||
type WebhookServer struct {
|
||||
Bot *client.Bot
|
||||
SecretToken string // verify X-Telegram-Bot-Api-Secret-Token
|
||||
BufferSize int
|
||||
}
|
||||
func (w *WebhookServer) ServeHTTP(rw http.ResponseWriter, r *http.Request)
|
||||
func (w *WebhookServer) ListenAndServe(ctx context.Context, addr string) error
|
||||
```
|
||||
|
||||
`ServeHTTP` lets users mount on their own router. `ListenAndServe` is a convenience for standalone use.
|
||||
|
||||
## 8. Dispatcher (`dispatch/`)
|
||||
|
||||
```go
|
||||
type Context struct {
|
||||
Ctx context.Context
|
||||
Bot *client.Bot
|
||||
Update *api.Update
|
||||
Values map[string]any // matched groups, command args
|
||||
}
|
||||
|
||||
type Handler[T any] func(ctx *Context, payload T) error
|
||||
type Middleware[T any] func(Handler[T]) Handler[T]
|
||||
|
||||
type Router struct{ /* … */ }
|
||||
|
||||
func New(bot *client.Bot) *Router
|
||||
|
||||
func (r *Router) OnCommand(cmd string, h Handler[*api.Message])
|
||||
func (r *Router) OnText(pattern string, h Handler[*api.Message])
|
||||
func (r *Router) OnCallback(pattern string, h Handler[*api.CallbackQuery])
|
||||
func (r *Router) OnInlineQuery(h Handler[*api.InlineQuery])
|
||||
func (r *Router) OnEditedMessage(h Handler[*api.Message])
|
||||
func (r *Router) Use(mw Middleware[*api.Update])
|
||||
|
||||
func (r *Router) Run(ctx context.Context, u transport.Updater) error
|
||||
```
|
||||
|
||||
Matchers run in registration order; first match wins. A panic-recovery middleware is registered automatically. Generic `Handler[T]` keeps payloads precisely typed in user code.
|
||||
|
||||
## 9. Testing strategy
|
||||
|
||||
### 9.1 Unit tests (every package, fast, no network)
|
||||
|
||||
- `testify/require` for assertions, `testify/mock` on `client.HTTPDoer`.
|
||||
- Edge cases explicitly covered:
|
||||
- API error decode for `429` with `retry_after` → `*APIError.IsRetryable()=true`, `RetryAfter()=N`.
|
||||
- Network error from transport → wrapped `*NetworkError`.
|
||||
- Malformed JSON → `*ParseError`.
|
||||
- Void-result methods (`setWebhook` returning `bool`).
|
||||
- Optional pointer fields nil round-trip.
|
||||
- OneOf union types unmarshal via discriminator.
|
||||
- Multipart upload path for methods with `InputFile`.
|
||||
- Context cancellation mid-call returns `ctx.Err()`.
|
||||
- Long-poller backoff after transient error.
|
||||
- Webhook secret-token mismatch → 401.
|
||||
- Webhook handles oversized body, malformed JSON, wrong content-type.
|
||||
- Router: command match with/without bot mention (`/start@MyBot`), regex matchers, panic recovery, middleware ordering, no-match update.
|
||||
|
||||
### 9.2 Codegen golden tests
|
||||
|
||||
```
|
||||
testdata/html/
|
||||
├── snapshot_2026-05-08.html full Bots API page snapshot
|
||||
└── small_fixture.html hand-crafted minimal page (1 type, 1 method)
|
||||
testdata/golden/
|
||||
├── api.json expected IR from snapshot
|
||||
└── *.gen.go expected emitted Go
|
||||
```
|
||||
|
||||
- `cmd/scrape` test: parse fixture → compare to golden `api.json`.
|
||||
- `cmd/genapi` test: read golden `api.json` → compare emitted Go to golden `*.gen.go`.
|
||||
- `-update` flag (custom in `internal/testutil`) regenerates goldens deliberately.
|
||||
|
||||
### 9.3 Optional integration suite
|
||||
|
||||
- Build tag `//go:build integration`.
|
||||
- Skipped by default `go test ./...`.
|
||||
- Activated by `go test -tags=integration ./test/integration/...`.
|
||||
- Requires `TELEGRAM_BOT_TOKEN` (and `TELEGRAM_TEST_CHAT_ID` where applicable).
|
||||
- Covers `getMe`, `sendMessage`, `setWebhook`/`deleteWebhook`, `getUpdates` loop with short timeout.
|
||||
- Not part of default CI to avoid flakes.
|
||||
|
||||
## 10. CI
|
||||
|
||||
`.github/workflows/ci.yml` (every push and PR):
|
||||
|
||||
- `actions/setup-go` matrix: 1.23, 1.24
|
||||
- `go vet ./...`
|
||||
- `staticcheck ./...`
|
||||
- `go test -race -coverprofile=coverage.out ./...`
|
||||
- Codegen-clean check: `make regen-from-fixture` + `git diff --exit-code` to assert generated files match the committed IR for the snapshot fixture (deterministic).
|
||||
- Upload coverage artifact.
|
||||
|
||||
## 11. Handling API changes & test maintenance
|
||||
|
||||
Telegram ships changes to the Bot API roughly every 1–3 months: new methods, new types, added optional fields, occasional removals or renames, occasional union-variant additions. The design must absorb these with minimum manual work.
|
||||
|
||||
### 11.1 Test-suite invariants under change
|
||||
|
||||
Tests are layered so that the cost of an API change is bounded.
|
||||
|
||||
| Test layer | Affected by API change? | Cost |
|
||||
|---|---|---|
|
||||
| `client/` unit tests (call helper, error mapping, codec, multipart builder) | No — they target `call[Req,Resp]`, not specific methods. | Zero. |
|
||||
| `transport/` unit tests (long-poll loop, webhook server) | No — they target update plumbing, not payload fields. | Zero. |
|
||||
| `dispatch/` unit tests (matchers, middleware, router) | No — generic over `*api.Update`. | Zero. |
|
||||
| Codegen golden tests (`cmd/scrape`, `cmd/genapi`) | Yes — golden `api.json` and `*.gen.go` will diff. | Refresh goldens deliberately (`go test -run TestGen -update`). |
|
||||
| Codegen "shape" smoke tests | Only if a code-generation pattern changes. | One test per pattern, not per method. |
|
||||
| Examples (`examples/echo`, `examples/webhook`) | Only if they reference a removed/renamed symbol. | Hand-fix; rare. |
|
||||
| Integration suite (build tag `integration`) | Only for the ~5 methods it touches. | Hand-fix on removal/rename; rare. |
|
||||
|
||||
The deliberate invariant: **we do not write a test per generated method.** All ~100+ generated wrappers funnel through `call[Req,Resp]` and the multipart builder; coverage of those two paths covers them all. Each generated method is then sanity-checked by being type-checked at compile time against the IR.
|
||||
|
||||
### 11.2 Shape smoke tests
|
||||
|
||||
In `api/api_test.go`, one test per code-generation pattern, hitting a representative method through a mocked `HTTPDoer`:
|
||||
|
||||
- **Simple** — `getMe` (no params, scalar response).
|
||||
- **Typed-struct param** — `sendMessage` (struct in, object out).
|
||||
- **Optional fields** — `sendMessage` with only required fields set; verify omitted fields do not appear in the request body.
|
||||
- **Array result** — `getUpdates` (array of `Update`).
|
||||
- **Bool result** — `setWebhook`.
|
||||
- **Multipart upload** — `sendDocument` with an `InputFile` (verify content-type, boundary, field names).
|
||||
- **OneOf union response** — `getChatMember` (returns `ChatMember` union).
|
||||
- **OneOf union request** — `sendMediaGroup` (accepts `[]InputMedia` union).
|
||||
|
||||
If new code-generation patterns appear (Telegram introducing a new shape we have not seen), one new shape test is added — not one per affected method.
|
||||
|
||||
### 11.3 Categories of change and how each is absorbed
|
||||
|
||||
| Change | Pipeline effect | Test effect |
|
||||
|---|---|---|
|
||||
| New type | Appears in `api.json`, emitted into `types.gen.go`. | Golden diff only. Refresh. |
|
||||
| New optional field | Same. | Golden diff only. |
|
||||
| New required field | Same. Breaking for users who construct that struct literally. | Golden diff only; example code may need update. |
|
||||
| Removed type/method/field | Disappears from emitted Go. Breaking for users referring to it. | Golden diff. Integration test or example may break — fix or skip. |
|
||||
| Renamed field | Old name disappears, new appears. | Same as above; no automatic rename (we treat as remove + add). |
|
||||
| New method | Wrapper generated; no new test required (shape tests cover the call path). | Golden diff. |
|
||||
| Return type changed for an existing method | Wrapper signature changes. Breaking. | Golden diff; integration test for that method may break. |
|
||||
| New OneOf variant | `UnmarshalJSON` switch grows a case. | Golden diff. If a brand-new variant style appears, may require scraper work and a new shape test. |
|
||||
| Telegram doc layout change | Scraper may misparse. | Scraper unit test against the new HTML fixture should be added before regenerating. |
|
||||
|
||||
### 11.4 The change procedure
|
||||
|
||||
When the scraper output changes:
|
||||
|
||||
1. Run `make regen-from-fixture` against the current `testdata/html/snapshot_*.html` (deterministic check) — confirm zero unrelated diffs.
|
||||
2. Capture a fresh HTML snapshot: `make snapshot` (writes `testdata/html/snapshot_<date>.html`).
|
||||
3. Run `make regen` against the new snapshot → IR diff appears in `internal/spec/api.json`.
|
||||
4. Review the IR diff as a Telegram changelog. This is the human read-through; it is the entire point of having an IR.
|
||||
5. Run `go test ./...` → expect golden codegen diffs.
|
||||
6. Refresh goldens: `go test -run TestGen -update`.
|
||||
7. Re-run `go test ./...` → green.
|
||||
8. If shape tests reveal a new code-generation pattern (e.g. a never-before-seen union shape), extend templates and add a shape test before refreshing goldens.
|
||||
9. If `examples/` or integration tests reference a removed symbol, fix them.
|
||||
10. Commit with a clear message: `chore(api): regenerate from Telegram Bot API vX.Y` plus a bullet list extracted from the IR diff (added/changed/removed types and methods).
|
||||
|
||||
This is the same procedure the future auto-regen workflow will run; doing it by hand first ensures the workflow has nothing surprising to do.
|
||||
|
||||
### 11.5 Versioning policy
|
||||
|
||||
- Library SemVer is decoupled from Telegram's API version.
|
||||
- Telegram-side additions → minor bump.
|
||||
- Telegram-side removals or signature changes → major bump (we do not preserve removed symbols as deprecated stubs; the breaking change ships).
|
||||
- Bug fixes in hand-written code → patch bump.
|
||||
- Each release records the Telegram API version it was generated against in the release notes and in a `// Generated from Bot API vX.Y` constant in `api/version.gen.go`.
|
||||
|
||||
## 12. Future work (deferred)
|
||||
|
||||
- **Auto-regen workflow** — daily cron + `workflow_dispatch` that runs `cmd/scrape` against the live URL, regenerates code, opens a PR with a diff summary, and auto-merges on green CI. Implementation sketch retained in design discussion; not part of v1 acceptance.
|
||||
- **Release workflow** — tag-triggered `goreleaser` pipeline producing GH Releases. SemVer for the library; Telegram's API version recorded in release notes.
|
||||
- **Additional codecs/HTTP adapters** as separate sub-packages or contrib modules so users can opt in without bringing transitive deps into the core.
|
||||
|
||||
## 13. Dependency policy
|
||||
|
||||
Production:
|
||||
- Go standard library
|
||||
- `golang.org/x/net/html` (scraper only)
|
||||
|
||||
Test-only:
|
||||
- `github.com/stretchr/testify`
|
||||
|
||||
Explicit non-deps: `goquery`, `cobra`, third-party logging, third-party HTTP clients, third-party JSON codecs (these are user-supplied via `HTTPDoer` and `Codec`).
|
||||
|
||||
## 14. Acceptance criteria
|
||||
|
||||
v1 is done when:
|
||||
|
||||
1. `go test ./...` passes on a clean checkout with no env vars.
|
||||
2. `make regen` produces zero diff against the committed `api.json` and `api/*.gen.go` when run against the committed HTML fixture.
|
||||
3. `examples/echo` and `examples/webhook` build and the echo example runs end-to-end against a real bot when `TELEGRAM_BOT_TOKEN` is set.
|
||||
4. `go vet`, `staticcheck`, and `go test -race` are clean.
|
||||
5. Every exported symbol in hand-written packages has a doc comment.
|
||||
6. README covers Why, Install, Quick Start, Custom HTTP/JSON, Webhooks, Dispatcher, Updating, Contributing.
|
||||
7. The integration test suite (`-tags=integration`) runs cleanly when env is provided.
|
||||
|
||||
## 15. Open questions
|
||||
|
||||
None at sign-off. (Auto-regen behaviour intentionally deferred.)
|
||||
@@ -68,7 +68,7 @@ func TestHandleStart_SendsInitialKeyboard(t *testing.T) {
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
msg := &api.Message{
|
||||
MessageID: 1,
|
||||
Chat: api.Chat{ID: 42, Type: string(api.ChatTypePrivate)},
|
||||
Chat: api.Chat{ID: 42, Type: api.ChatTypePrivate},
|
||||
From: &api.User{ID: 7, FirstName: "Alice"},
|
||||
Text: "/start",
|
||||
}
|
||||
@@ -88,7 +88,7 @@ func callbackCtx(bot *client.Bot, q *api.CallbackQuery, groups []string) *dispat
|
||||
func callbackQuery(data string, msgID int64, chatID int64) *api.CallbackQuery {
|
||||
msg := &api.Message{
|
||||
MessageID: msgID,
|
||||
Chat: api.Chat{ID: chatID, Type: string(api.ChatTypePrivate)},
|
||||
Chat: api.Chat{ID: chatID, Type: api.ChatTypePrivate},
|
||||
}
|
||||
return &api.CallbackQuery{
|
||||
ID: "cb1",
|
||||
|
||||
@@ -46,7 +46,7 @@ func msgUpd(userID, chatID int64, text string) api.Update {
|
||||
}
|
||||
}
|
||||
entities = append(entities, api.MessageEntity{
|
||||
Type: string(api.EntityBotCommand),
|
||||
Type: api.MessageEntityTypeBotCommand,
|
||||
Offset: 0,
|
||||
Length: int64(end),
|
||||
})
|
||||
@@ -56,7 +56,7 @@ func msgUpd(userID, chatID int64, text string) api.Update {
|
||||
Message: &api.Message{
|
||||
MessageID: 1,
|
||||
From: &api.User{ID: userID},
|
||||
Chat: api.Chat{ID: chatID, Type: string(api.ChatTypePrivate)},
|
||||
Chat: api.Chat{ID: chatID, Type: api.ChatTypePrivate},
|
||||
Text: text,
|
||||
Entities: entities,
|
||||
},
|
||||
|
||||
@@ -55,7 +55,7 @@ func TestHandleStart_GreetsUser(t *testing.T) {
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
msg := &api.Message{
|
||||
MessageID: 1,
|
||||
Chat: api.Chat{ID: 42, Type: string(api.ChatTypePrivate)},
|
||||
Chat: api.Chat{ID: 42, Type: api.ChatTypePrivate},
|
||||
From: &api.User{ID: 7, FirstName: "Alice"},
|
||||
Text: "/start",
|
||||
}
|
||||
@@ -82,7 +82,7 @@ func TestHandleEcho_RepliesWithSameText(t *testing.T) {
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
msg := &api.Message{
|
||||
MessageID: 5,
|
||||
Chat: api.Chat{ID: 42, Type: string(api.ChatTypePrivate)},
|
||||
Chat: api.Chat{ID: 42, Type: api.ChatTypePrivate},
|
||||
From: &api.User{ID: 7, FirstName: "Alice"},
|
||||
Text: "hello echo",
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ func main() {
|
||||
// Echo the query as article results.
|
||||
results := []api.InlineQueryResult{
|
||||
&api.InlineQueryResultArticle{
|
||||
Type: "article",
|
||||
ID: "echo",
|
||||
Title: "Echo: " + q.Query,
|
||||
InputMessageContent: &api.InputTextMessageContent{
|
||||
@@ -43,7 +42,6 @@ func main() {
|
||||
},
|
||||
},
|
||||
&api.InlineQueryResultArticle{
|
||||
Type: "article",
|
||||
ID: "upper",
|
||||
Title: "UPPER: " + strings.ToUpper(q.Query),
|
||||
InputMessageContent: &api.InputTextMessageContent{
|
||||
|
||||
+11
-14
@@ -96,11 +96,10 @@ func kickHandler(c *dispatch.Context, m *api.Message) error {
|
||||
}); err != nil {
|
||||
return handleAdminErr(c, m.Chat.ID, err)
|
||||
}
|
||||
truVal := true
|
||||
if _, err := api.UnbanChatMember(c.Ctx, c.Bot, &api.UnbanChatMemberParams{
|
||||
ChatID: api.ChatIDFromInt(m.Chat.ID),
|
||||
UserID: target,
|
||||
OnlyIfBanned: &truVal,
|
||||
OnlyIfBanned: api.Ptr(true),
|
||||
}); err != nil {
|
||||
log.Printf("unban after kick: %v", err)
|
||||
}
|
||||
@@ -130,23 +129,21 @@ func muteHandler(c *dispatch.Context, m *api.Message) error {
|
||||
reply(c, m.Chat.ID, "Reply to a user's message with /mute to silence them for 1 hour.")
|
||||
return nil
|
||||
}
|
||||
until := time.Now().Add(time.Hour).Unix()
|
||||
falseVal := false
|
||||
if _, err := api.RestrictChatMember(c.Ctx, c.Bot, &api.RestrictChatMemberParams{
|
||||
ChatID: api.ChatIDFromInt(m.Chat.ID),
|
||||
UserID: target,
|
||||
Permissions: api.ChatPermissions{
|
||||
CanSendMessages: &falseVal,
|
||||
CanSendAudios: &falseVal,
|
||||
CanSendDocuments: &falseVal,
|
||||
CanSendPhotos: &falseVal,
|
||||
CanSendVideos: &falseVal,
|
||||
CanSendVideoNotes: &falseVal,
|
||||
CanSendVoiceNotes: &falseVal,
|
||||
CanSendPolls: &falseVal,
|
||||
CanSendOtherMessages: &falseVal,
|
||||
CanSendMessages: api.Ptr(false),
|
||||
CanSendAudios: api.Ptr(false),
|
||||
CanSendDocuments: api.Ptr(false),
|
||||
CanSendPhotos: api.Ptr(false),
|
||||
CanSendVideos: api.Ptr(false),
|
||||
CanSendVideoNotes: api.Ptr(false),
|
||||
CanSendVoiceNotes: api.Ptr(false),
|
||||
CanSendPolls: api.Ptr(false),
|
||||
CanSendOtherMessages: api.Ptr(false),
|
||||
},
|
||||
UntilDate: &until,
|
||||
UntilDate: api.Ptr(time.Now().Add(time.Hour).Unix()),
|
||||
}); err != nil {
|
||||
return handleAdminErr(c, m.Chat.ID, err)
|
||||
}
|
||||
|
||||
@@ -94,12 +94,11 @@ func main() {
|
||||
})
|
||||
return nil
|
||||
}
|
||||
isAnon := false
|
||||
msg, err := api.SendPoll(c.Ctx, c.Bot, &api.SendPollParams{
|
||||
ChatID: api.ChatIDFromInt(m.Chat.ID),
|
||||
Question: question,
|
||||
Options: pollOptions,
|
||||
IsAnonymous: &isAnon,
|
||||
IsAnonymous: api.Ptr(false),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -49,7 +49,7 @@ func TestHandlePing_RepliesWithPong(t *testing.T) {
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
msg := &api.Message{
|
||||
MessageID: 1,
|
||||
Chat: api.Chat{ID: 42, Type: string(api.ChatTypePrivate)},
|
||||
Chat: api.Chat{ID: 42, Type: api.ChatTypePrivate},
|
||||
From: &api.User{ID: 7, FirstName: "Alice"},
|
||||
Text: "/ping",
|
||||
}
|
||||
|
||||
+699
-123
File diff suppressed because it is too large
Load Diff
@@ -45,6 +45,11 @@ type Field struct {
|
||||
Type TypeRef `json:"type"`
|
||||
Required bool `json:"required,omitempty"`
|
||||
Doc string `json:"doc,omitempty"`
|
||||
// EnumValues, when non-empty, lists the wire-level string values the
|
||||
// scraper detected for an enum-like description ("can be A, B or C",
|
||||
// "always X", parse_mode special-case). Order is doc order, deduped.
|
||||
// Emitted as a typed Go enum that replaces the field's string type.
|
||||
EnumValues []string `json:"enum_values,omitempty"`
|
||||
}
|
||||
|
||||
// Kind enumerates TypeRef shapes.
|
||||
|
||||
+6
-1
@@ -98,7 +98,12 @@
|
||||
"kind": "primitive",
|
||||
"name": "string"
|
||||
},
|
||||
"doc": "Mode for parsing entities in the message text."
|
||||
"doc": "Mode for parsing entities in the message text.",
|
||||
"enum_values": [
|
||||
"Markdown",
|
||||
"MarkdownV2",
|
||||
"HTML"
|
||||
]
|
||||
}
|
||||
],
|
||||
"returns": {
|
||||
|
||||
Vendored
+1
-48
@@ -4,57 +4,10 @@
|
||||
|
||||
package api
|
||||
|
||||
// ParseMode controls how Telegram interprets formatting in message text.
|
||||
type ParseMode string
|
||||
|
||||
const (
|
||||
ParseModeMarkdown ParseMode = "Markdown" // legacy
|
||||
ParseModeMarkdown ParseMode = "Markdown"
|
||||
ParseModeMarkdownV2 ParseMode = "MarkdownV2"
|
||||
ParseModeHTML ParseMode = "HTML"
|
||||
)
|
||||
|
||||
// ChatType is the type of a Telegram chat.
|
||||
type ChatType string
|
||||
|
||||
const (
|
||||
ChatTypePrivate ChatType = "private"
|
||||
ChatTypeGroup ChatType = "group"
|
||||
ChatTypeSupergroup ChatType = "supergroup"
|
||||
ChatTypeChannel ChatType = "channel"
|
||||
)
|
||||
|
||||
// UpdateType identifies an Update payload variant. Used by allowed_updates
|
||||
// in getUpdates / setWebhook.
|
||||
type UpdateType string
|
||||
|
||||
const (
|
||||
UpdateMessage UpdateType = "message"
|
||||
UpdateEditedMessage UpdateType = "edited_message"
|
||||
UpdateChannelPost UpdateType = "channel_post"
|
||||
UpdateEditedChannelPost UpdateType = "edited_channel_post"
|
||||
UpdateCallbackQuery UpdateType = "callback_query"
|
||||
UpdateInlineQuery UpdateType = "inline_query"
|
||||
)
|
||||
|
||||
// MessageEntityType is the kind of an entity (mention, hashtag, command, ...).
|
||||
type MessageEntityType string
|
||||
|
||||
const (
|
||||
EntityMention MessageEntityType = "mention"
|
||||
EntityHashtag MessageEntityType = "hashtag"
|
||||
EntityCashtag MessageEntityType = "cashtag"
|
||||
EntityBotCommand MessageEntityType = "bot_command"
|
||||
EntityURL MessageEntityType = "url"
|
||||
EntityEmail MessageEntityType = "email"
|
||||
EntityPhoneNumber MessageEntityType = "phone_number"
|
||||
EntityBold MessageEntityType = "bold"
|
||||
EntityItalic MessageEntityType = "italic"
|
||||
EntityUnderline MessageEntityType = "underline"
|
||||
EntityStrike MessageEntityType = "strikethrough"
|
||||
EntitySpoiler MessageEntityType = "spoiler"
|
||||
EntityCode MessageEntityType = "code"
|
||||
EntityPre MessageEntityType = "pre"
|
||||
EntityTextLink MessageEntityType = "text_link"
|
||||
EntityTextMention MessageEntityType = "text_mention"
|
||||
EntityCustomEmoji MessageEntityType = "custom_emoji"
|
||||
)
|
||||
|
||||
Vendored
+1
-1
@@ -37,7 +37,7 @@ type SendMessageParams struct {
|
||||
// Text of the message to be sent.
|
||||
Text string `json:"text"`
|
||||
// Mode for parsing entities in the message text.
|
||||
ParseMode string `json:"parse_mode,omitempty"`
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
}
|
||||
|
||||
// SendMessage calls the sendMessage Telegram Bot API method.
|
||||
|
||||
Reference in New Issue
Block a user