diff --git a/.github/workflows/bot-api-spec.yml b/.github/workflows/bot-api-spec.yml new file mode 100644 index 00000000..9fa058f7 --- /dev/null +++ b/.github/workflows/bot-api-spec.yml @@ -0,0 +1,23 @@ +name: Bot API spec + +# Compares the library with the latest published Telegram Bot API +# specification, so that new Bot API releases are noticed quickly. + +on: + schedule: + - cron: '17 6 * * 1' + workflow_dispatch: + +jobs: + specdiff: + name: Compare with the Bot API specification + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: '1.24' + + - name: Report missing types, fields, methods and parameters + run: go run ./internal/cmd/specdiff -strict diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..cb8d8867 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,22 @@ +name: Lint + +on: + push: + branches: + - master + pull_request: + +jobs: + golangci: + name: golangci-lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: '1.24' + + - uses: golangci/golangci-lint-action@v7 + with: + version: v2.5.0 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 48b2859b..d6b19e3f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,33 +1,33 @@ -name: Test - -on: - push: - branches: - - master - - develop - pull_request: - -jobs: - build: - name: Test - runs-on: ubuntu-latest - steps: - - name: Set up Go 1.x - uses: actions/setup-go@v2 - with: - go-version: ^1.15 - id: go - - - name: Check out code into the Go module directory - uses: actions/checkout@v2 - - - name: Build - run: go build -v . - - - name: Test - run: go test -coverprofile=coverage.out -covermode=atomic -v . - - - name: Upload coverage report - uses: codecov/codecov-action@v1 - with: - file: ./coverage.out +name: Test + +on: + push: + branches: + - master + - develop + pull_request: + +jobs: + build: + name: Test (Go ${{ matrix.go }}) + runs-on: ubuntu-latest + strategy: + matrix: + go: ['1.24', 'stable'] + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go }} + + - name: Build + run: go build -v ./... + + - name: Vet + run: go vet ./... + + - name: Test + run: go test -race -coverprofile=coverage.out -covermode=atomic ./... diff --git a/.gitignore b/.gitignore index eb7a23b2..6f4da007 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ coverage.out tmp/ book/ +.claude/ diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 00000000..a69f1fa6 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,8 @@ +version: "2" + +linters: + default: none + +formatters: + enable: + - gofmt diff --git a/BREAKING.md b/BREAKING.md new file mode 100644 index 00000000..cb060a0a --- /dev/null +++ b/BREAKING.md @@ -0,0 +1,523 @@ +# Breaking Changes + +This file lists every source-incompatible change between upstream +`github.com/go-telegram-bot-api/telegram-bot-api/v5` v5.5.1 (Bot API 6.0) +and this fork (Bot API 10.3). Upgrades are grouped by topic so you can jump +straight to the area your code touches. + +Each section lists **what to rename** or **what to replace** — the fastest +way to migrate is `grep` for the old identifier in your code and apply the +rewrite. Nothing here changes runtime semantics beyond what the Telegram +Bot API itself changed. + +--- + +## Module path + +```go +// Before +import tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + +// After +import tgbotapi "github.com/bssth/telegram-bot-api/v6" +``` + +Both the owner and the major version change: the fork is published as `/v6` +because it is not source-compatible with upstream v5. The package name is +still `tgbotapi`. See the README for a one-line `sed` command that rewrites +the imports. + +--- + +## `thumb` → `thumbnail` (Bot API 6.6) + +The single largest mechanical rename. Applies everywhere media has a +thumbnail. + +### Config fields +```go +PhotoConfig.Thumb → PhotoConfig.Thumbnail +AudioConfig.Thumb → AudioConfig.Thumbnail +DocumentConfig.Thumb → DocumentConfig.Thumbnail +VideoConfig.Thumb → VideoConfig.Thumbnail +AnimationConfig.Thumb → AnimationConfig.Thumbnail +VideoNoteConfig.Thumb → VideoNoteConfig.Thumbnail +VoiceConfig.Thumb → VoiceConfig.Thumbnail +``` + +### `InputMedia*` types +```go +InputMediaVideo.Thumb → InputMediaVideo.Thumbnail +InputMediaAnimation.Thumb → InputMediaAnimation.Thumbnail +InputMediaAudio.Thumb → InputMediaAudio.Thumbnail +InputMediaDocument.Thumb → InputMediaDocument.Thumbnail +``` + +### Inline query result types +```go +InlineQueryResult*.ThumbURL → .ThumbnailURL +InlineQueryResult*.ThumbWidth → .ThumbnailWidth +InlineQueryResult*.ThumbHeight → .ThumbnailHeight +``` +(Applies to Article, Contact, Document, Location, Venue, Photo, Video, +GIF, MPEG4GIF.) + +### Helper functions +```go +NewInlineQueryResultPhotoWithThumb → NewInlineQueryResultPhotoWithThumbnail +``` + +### Config types +```go +SetStickerSetThumbConfig → SetStickerSetThumbnailConfig +SetStickerSetThumbConfig.Thumb → SetStickerSetThumbnailConfig.Thumbnail +``` + +--- + +## `reply_to_message_id` → `ReplyParameters` (Bot API 7.0) + +`ReplyToMessageID` and `AllowSendingWithoutReply` were removed from +`BaseChat` and `MediaGroupConfig`. Replies now go through a structured +`ReplyParameters` value that also supports cross-chat replies and quoting. + +### Before +```go +msg := tgbotapi.NewMessage(chatID, "hi") +msg.ReplyToMessageID = origMsgID +msg.AllowSendingWithoutReply = true +``` + +### After +```go +msg := tgbotapi.NewMessage(chatID, "hi") +msg.ReplyParameters = &tgbotapi.ReplyParameters{ + MessageID: origMsgID, + AllowSendingWithoutReply: true, +} +``` + +--- + +## `DisableWebPagePreview` → `LinkPreviewOptions` (Bot API 7.0) + +### Send / edit configs +```go +MessageConfig.DisableWebPagePreview → LinkPreviewOptions +EditMessageTextConfig.DisableWebPagePreview → LinkPreviewOptions +``` + +### Inline content +```go +InputTextMessageContent.DisableWebPagePreview → LinkPreviewOptions +``` + +### Before +```go +msg := tgbotapi.NewMessage(chatID, "see https://example.com") +msg.DisableWebPagePreview = true +``` + +### After +```go +msg := tgbotapi.NewMessage(chatID, "see https://example.com") +msg.LinkPreviewOptions = &tgbotapi.LinkPreviewOptions{IsDisabled: true} +``` + +--- + +## `forward_from` fields → `ForwardOrigin` (Bot API 7.0) + +Removed from `Message`: +- `ForwardFrom *User` +- `ForwardFromChat *Chat` +- `ForwardFromMessageID int` +- `ForwardSignature string` +- `ForwardSenderName string` +- `ForwardDate int` + +Added: `ForwardOrigin *MessageOrigin` — a flat polymorphic struct with a +`Type` discriminator ("user", "hidden_user", "chat", "channel") and all +variant fields as optional. + +### Before +```go +if msg.ForwardFrom != nil { + fmt.Println("forwarded from user", msg.ForwardFrom.UserName) +} +``` + +### After +```go +if msg.ForwardOrigin != nil && msg.ForwardOrigin.Type == tgbotapi.MessageOriginTypeUser { + fmt.Println("forwarded from user", msg.ForwardOrigin.SenderUser.UserName) +} +``` + +--- + +## Sticker API rewrite (Bot API 6.6) + +`createNewStickerSet`, `uploadStickerFile`, and `addStickerToSet` all +changed shape. The old per-format parameters (`png_sticker`, `tgs_sticker`, +`webm_sticker`) were replaced with the new `InputSticker` type. + +### `UploadStickerConfig` +```go +// Before +cfg := tgbotapi.UploadStickerConfig{ + UserID: userID, + PNGSticker: tgbotapi.FilePath("sticker.png"), +} + +// After +cfg := tgbotapi.UploadStickerConfig{ + UserID: userID, + Sticker: tgbotapi.FilePath("sticker.png"), + StickerFormat: tgbotapi.StickerFormatStatic, +} +``` + +### `NewStickerSetConfig` +```go +// Before +cfg := tgbotapi.NewStickerSetConfig{ + UserID: userID, + Name: "my_pack", + Title: "My Pack", + PNGSticker: tgbotapi.FilePath("s.png"), + Emojis: "😀", +} + +// After — Stickers is a slice of InputSticker, each with its own Format (7.2) +cfg := tgbotapi.NewStickerSetConfig{ + UserID: userID, + Name: "my_pack", + Title: "My Pack", + Stickers: []tgbotapi.InputSticker{{ + Sticker: tgbotapi.FilePath("s.png"), + Format: tgbotapi.StickerFormatStatic, + EmojiList: []string{"😀"}, + }}, +} +``` +Dropped fields: `PNGSticker`, `TGSSticker`, `Emojis`, `MaskPosition`, +`StickerFormat` (moved per-sticker in 7.2), `ContainsMasks` (deprecated 6.2). + +### `AddStickerConfig` +```go +// Before +cfg := tgbotapi.AddStickerConfig{ + UserID: userID, + Name: "my_pack", + PNGSticker: tgbotapi.FilePath("new.png"), + Emojis: "🎉", +} + +// After +cfg := tgbotapi.AddStickerConfig{ + UserID: userID, + Name: "my_pack", + Sticker: tgbotapi.InputSticker{ + Sticker: tgbotapi.FilePath("new.png"), + Format: tgbotapi.StickerFormatStatic, + EmojiList: []string{"🎉"}, + }, +} +``` + +### Removed fields +```go +StickerSet.IsAnimated // (removed 7.2; mixed-format packs) +StickerSet.IsVideo // (removed 7.2) +StickerSet.ContainsMasks // (removed 6.2; use StickerType instead) +``` + +--- + +## Chat permissions: granular media (Bot API 6.5) + +`CanSendMediaMessages` was replaced with six per-type fields on both +`ChatPermissions` and `ChatMember`: + +```go +CanSendMediaMessages → + CanSendAudios + + CanSendDocuments + + CanSendPhotos + + CanSendVideos + + CanSendVideoNotes + + CanSendVoiceNotes +``` + +--- + +## `UserShared` → `UsersShared` (Bot API 7.0, 7.2) + +### 7.0 — rename + slice of IDs +```go +KeyboardButtonRequestUser → KeyboardButtonRequestUsers +UserShared (type) → UsersShared +KeyboardButton.RequestUser → KeyboardButton.RequestUsers +Message.UserShared → Message.UsersShared +UsersShared.UserID (int64) → UsersShared.UserIDs ([]int64) +``` + +### 7.2 — slice of user IDs becomes slice of `SharedUser` +```go +UsersShared.UserIDs ([]int64) → UsersShared.Users ([]SharedUser) +``` + +`SharedUser` has the basic profile info (first/last name, username, photo) +when the bot requested it via `KeyboardButtonRequestUsers`. + +--- + +## `BusinessConnection.CanReply` → `Rights` (Bot API 9.0) + +`BusinessConnection.CanReply bool` was replaced with +`BusinessConnection.Rights *BusinessBotRights`, which carries ~14 +granular permissions (including a new `CanReply` field inside the +sub-struct). + +### Before +```go +if bc.CanReply { + ... +} +``` + +### After +```go +if bc.Rights != nil && bc.Rights.CanReply { + ... +} +``` + +--- + +## `ChatFullInfo.CanSendGift` → `AcceptedGiftTypes` (Bot API 9.0) + +The single-bool `can_send_gift` was replaced with a struct describing which +specific kinds of gifts a chat accepts: + +```go +ChatFullInfo.CanSendGift (bool) → ChatFullInfo.AcceptedGiftTypes (AcceptedGiftTypes) +``` + +`AcceptedGiftTypes` has four bools: `UnlimitedGifts`, `LimitedGifts`, +`UniqueGifts`, `PremiumSubscription` (plus `GiftsFromChannels` added in 9.3). + +--- + +## `Poll.CorrectOptionID` → `CorrectOptionIDs` (Bot API 9.6) + +Multi-answer quizzes meant the singular field had to become a slice: + +```go +Poll.CorrectOptionID (int) → Poll.CorrectOptionIDs ([]int) +SendPollConfig.CorrectOptionID → SendPollConfig.CorrectOptionIDs +``` + +`SendPollConfig.CorrectOptionID` also changed from `int64` to `int` as part +of the rewrite. + +--- + +## `SendPollConfig.Options` type change (Bot API 7.3) + +```go +SendPollConfig.Options ([]string) → SendPollConfig.Options ([]InputPollOption) +``` + +The `NewPoll` helper still takes `options ...string` and wraps them +internally, so callers that use the helper don't need to change. +Direct struct literal callers do: + +### Before +```go +SendPollConfig{Options: []string{"yes", "no"}} +``` + +### After +```go +SendPollConfig{Options: []tgbotapi.InputPollOption{ + {Text: "yes"}, + {Text: "no"}, +}} +``` + +--- + +## `UniqueGiftInfo.LastResaleStarCount` → currency+amount (Bot API 9.3) + +The Stars-only resale field was generalized when TON payments arrived: + +```go +UniqueGiftInfo.LastResaleStarCount (int) → + UniqueGiftInfo.LastResaleCurrency (string) + + UniqueGiftInfo.LastResaleAmount (int) +``` + +--- + +## `GetBusinessAccountGiftsConfig.ExcludeLimited` split (Bot API 9.3) + +```go +ExcludeLimited (bool) → + ExcludeLimitedUpgradable (bool) + + ExcludeLimitedNonUpgradable (bool) +``` + +Plus `ExcludeFromBlockchain (bool)` was added in the same version. + +--- + +## `switch_pm_*` → `InlineQueryResultsButton` (Bot API 6.7) + +```go +InlineConfig.SwitchPMText → InlineConfig.Button.Text +InlineConfig.SwitchPMParameter → InlineConfig.Button.StartParameter +``` + +`InlineConfig.Button` is a `*InlineQueryResultsButton` which additionally +supports launching a Web App: + +```go +cfg := tgbotapi.InlineConfig{ + // ... + Button: &tgbotapi.InlineQueryResultsButton{ + Text: "Show help", + StartParameter: "help", + }, +} +``` + +--- + +## Removed fields (no direct replacement) + +- **`InlineQueryResultArticle.HideURL`** (Bot API 8.2) — pass an empty + string as the `URL` instead. +- **`Gift.ContainsMasks`** / **`StickerSet.ContainsMasks`** — + use the `StickerType` / `Type` field (`"regular"`, `"mask"`, + `"custom_emoji"`) instead. + +--- + +## `InvoiceConfig.ProviderData` type change (Bot API 6.1 era) + +```go +InvoiceConfig.ProviderData (string) → json.RawMessage +InvoiceLinkConfig.ProviderData (string) → json.RawMessage +``` + +If you had a pre-marshalled JSON string, wrap it with `json.RawMessage(s)`. + +--- + +## Method return type change: `GetChat` (Bot API 7.3) + +```go +bot.GetChat(cfg) (Chat, error) → (ChatFullInfo, error) +``` + +`ChatFullInfo` embeds `Chat`, so field access via `.ID`, `.Title`, `.Bio`, +etc. still works through Go's field promotion. The only thing that changes +is the variable's declared type: + +```go +// Before +var c tgbotapi.Chat +c, err = bot.GetChat(cfg) + +// After +var c tgbotapi.ChatFullInfo +c, err = bot.GetChat(cfg) +``` + +--- + +## `getChat`-only fields moved from `Chat` to `ChatFullInfo` (Bot API 7.3) + +Since Bot API 7.3 Telegram returns these fields only from `getChat`, as part +of `ChatFullInfo`; the `Chat` objects inside messages and other updates never +contain them. They were removed from `Chat` so that reading them there is a +compile error instead of a silent zero value: + +```go +ActiveUsernames, EmojiStatusCustomEmojiID, EmojiStatusExpirationDate, +AccentColorID, BackgroundCustomEmojiID, ProfileAccentColorID, +ProfileBackgroundCustomEmojiID, HasVisibleHistory, HasHiddenMembers, +HasAggressiveAntiSpamEnabled, UnrestrictBoostCount, +CustomEmojiStickerSetName, Birthdate, BusinessIntro, BusinessLocation, +BusinessOpeningHours, PersonalChat, Photo, Bio, HasPrivateForwards, +HasRestrictedVoiceAndVideoMessages, Description, JoinToSendMessages, +JoinByRequest, InviteLink, PinnedMessage, AvailableReactions, Permissions, +SlowModeDelay, MessageAutoDeleteTime, HasProtectedContent, StickerSetName, +CanSetStickerSet, LinkedChatID, Location +``` + +Code that reads them from the result of `bot.GetChat` keeps compiling, since +it already returns `ChatFullInfo`: + +```go +// Before (always empty) +update.Message.Chat.Description + +// After +info, err := bot.GetChat(tgbotapi.ChatInfoConfig{ChatConfig: update.Message.Chat.ChatConfig()}) +info.Description +``` + +--- + +## `Message.PremiumAnimation` removed + +`premium_animation` is a field of `Sticker`, not `Message`, so +`Message.PremiumAnimation` was never populated. Use +`Message.Sticker.PremiumAnimation` instead. + +--- + +## Types that gained fields (soft-breaking) + +A few types changed from empty structs to carrying fields. Anyone who used +them via pointer (`*Story`) is unaffected; callers who constructed them by +value may need to initialize new fields. + +- **`Story`** — 6.8 empty struct → 7.1 `{Chat, ID}` +- **`GiveawayCreated`** — 7.0 empty struct → 7.10 `{PrizeStarCount}` +- **`WriteAccessAllowed`** — 6.4 empty struct → grew `WebAppName` (6.7), + `FromRequest`, `FromAttachmentMenu` (6.9) + +--- + +## Internal helper `AddFirstValid` error propagation + +Not API-breaking for callers of the public surface, but if you wrote +custom `Chattable` implementations and copied the old pattern of +ignoring the `Params.AddFirstValid` return value, know that the fork +now consistently captures and returns that error from `params()`. Your +custom configs should do the same. + +--- + +## Checklist for upgrading + +1. Change the import path (see "Module path"), then do a global + find-replace for the `Thumb` → `Thumbnail` renames. +2. Search for `ReplyToMessageID` and migrate each to `ReplyParameters`. +3. Search for `DisableWebPagePreview` and migrate to `LinkPreviewOptions`. +4. Search for `ForwardFrom` / `ForwardDate` and switch to `ForwardOrigin`. +5. Anything touching sticker set creation needs the `InputSticker` rewrite. +6. If you read `ChatMember.CanSendMediaMessages` or + `ChatPermissions.CanSendMediaMessages`, switch to the six granular + `CanSend*` fields. +7. If you handle business connections, check `Rights` instead of `CanReply`. +8. If you read quiz correctness, use `CorrectOptionIDs[0]` (or loop for + multi-answer). +9. If you read `getChat`-only fields such as `Bio` or `Description` from a + `Chat`, read them from the `ChatFullInfo` returned by `bot.GetChat`. + +Everything else is additive and should compile unchanged. diff --git a/README.md b/README.md index b18d15dd..3945df4f 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,75 @@ # Golang bindings for the Telegram Bot API -[![Go Reference](https://pkg.go.dev/badge/github.com/go-telegram-bot-api/telegram-bot-api/v5.svg)](https://pkg.go.dev/github.com/go-telegram-bot-api/telegram-bot-api/v5) -[![Test](https://github.com/go-telegram-bot-api/telegram-bot-api/actions/workflows/test.yml/badge.svg)](https://github.com/go-telegram-bot-api/telegram-bot-api/actions/workflows/test.yml) +[![Go Reference](https://pkg.go.dev/badge/github.com/bssth/telegram-bot-api/v6.svg)](https://pkg.go.dev/github.com/bssth/telegram-bot-api/v6) +[![Test](https://github.com/bssth/telegram-bot-api/actions/workflows/test.yml/badge.svg)](https://github.com/bssth/telegram-bot-api/actions/workflows/test.yml) +[![Bot API](https://img.shields.io/badge/Bot%20API-10.3-blue.svg)](https://core.telegram.org/bots/api-changelog) -All methods are fairly self-explanatory, and reading the [godoc](https://pkg.go.dev/github.com/go-telegram-bot-api/telegram-bot-api/v5) page should -explain everything. If something isn't clear, open an issue or submit -a pull request. +> **This is the maintained continuation of +> [`go-telegram-bot-api/telegram-bot-api`](https://github.com/go-telegram-bot-api/telegram-bot-api).** +> The original project stopped at Bot API 6.0 (April 2022) and no longer +> accepts changes. This fork supports **Bot API 10.3** (August 24, 2026) and +> follows the [official changelog](https://core.telegram.org/bots/api-changelog). -There are more tutorials and high-level information on the website, [go-telegram-bot-api.dev](https://go-telegram-bot-api.dev). +All methods are fairly self-explanatory, and reading the +[godoc](https://pkg.go.dev/github.com/bssth/telegram-bot-api/v6) page should +explain everything. If something isn't clear, open an +[issue](https://github.com/bssth/telegram-bot-api/issues) or submit a pull +request. -The scope of this project is just to provide a wrapper around the API -without any additional features. There are other projects for creating -something with plugins and command handlers without having to design -all that yourself. +The scope of this project is just to provide a wrapper around the API without +any additional features. There are other projects for creating something with +plugins and command handlers without having to design all that yourself. -Join [the development group](https://telegram.me/go_telegram_bot_api) if -you want to ask questions or discuss development. +More tutorials and high-level information live in the [`docs`](./docs) +directory. -## Example +## Installing + +```sh +go get github.com/bssth/telegram-bot-api/v6@latest +``` + +```go +import tgbotapi "github.com/bssth/telegram-bot-api/v6" +``` + +Go 1.24 or newer is required. + +The major version is **v6**: the API is not source-compatible with upstream +v5 (see below), so the fork starts a new major version instead of breaking +code that pins `/v5`. + +## Migrating from `go-telegram-bot-api/telegram-bot-api` -First, ensure the library is installed and up to date by running -`go get -u github.com/go-telegram-bot-api/telegram-bot-api/v5`. +1. Change the import path. The package name stays `tgbotapi`, so nothing else + in your code needs to be renamed: -This is a very simple bot that just displays any gotten updates, -then replies it to that chat. + ```sh + grep -rl 'github.com/go-telegram-bot-api/telegram-bot-api/v5' --include='*.go' . \ + | xargs sed -i 's#github.com/go-telegram-bot-api/telegram-bot-api/v5#github.com/bssth/telegram-bot-api/v6#g' + go get github.com/bssth/telegram-bot-api/v6@latest + go mod tidy + ``` + + (On macOS use `sed -i ''`.) A `replace` directive in `go.mod` is not + enough: Go requires a replacement module to declare the same module path + as the one it replaces. + +2. Fix whatever no longer compiles using [**BREAKING.md**](./BREAKING.md). It + lists every source-incompatible change between upstream v5.5.1 (Bot API + 6.0) and this fork, grouped by topic, with before/after snippets. Most of + them come from Telegram itself: `Thumb` became `Thumbnail`, + `ReplyToMessageID` became `ReplyParameters`, `DisableWebPagePreview` became + `LinkPreviewOptions`, and so on. + +`BREAKING.md` is written so it can be handed to a coding assistant together +with the files that use `tgbotapi`; it handles most of the mechanical +rewrites in one pass. Review the diff, run `go build ./...`, done. + +## Example + +This is a very simple bot that just displays any gotten updates, then replies +it to that chat. ```go package main @@ -31,7 +77,7 @@ package main import ( "log" - tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + tgbotapi "github.com/bssth/telegram-bot-api/v6" ) func main() { @@ -54,7 +100,9 @@ func main() { log.Printf("[%s] %s", update.Message.From.UserName, update.Message.Text) msg := tgbotapi.NewMessage(update.Message.Chat.ID, update.Message.Text) - msg.ReplyToMessageID = update.Message.MessageID + msg.ReplyParameters = &tgbotapi.ReplyParameters{ + MessageID: update.Message.MessageID, + } bot.Send(msg) } @@ -62,8 +110,7 @@ func main() { } ``` -If you need to use webhooks (if you wish to run on Google App Engine), -you may use a slightly different method. +If you need to use webhooks, you may use a slightly different method. ```go package main @@ -72,7 +119,7 @@ import ( "log" "net/http" - "github.com/go-telegram-bot-api/telegram-bot-api/v5" + tgbotapi "github.com/bssth/telegram-bot-api/v6" ) func main() { @@ -85,7 +132,7 @@ func main() { log.Printf("Authorized on account %s", bot.Self.UserName) - wh, _ := tgbotapi.NewWebhookWithCert("https://www.example.com:8443/"+bot.Token, "cert.pem") + wh, _ := tgbotapi.NewWebhookWithCert("https://www.example.com:8443/"+bot.Token, tgbotapi.FilePath("cert.pem")) _, err = bot.Request(wh) if err != nil { @@ -111,11 +158,148 @@ func main() { ``` If you need, you may generate a self-signed certificate, as this requires -HTTPS / TLS. The above example tells Telegram that this is your -certificate and that it should be trusted, even though it is not -properly signed. +HTTPS / TLS. The above example tells Telegram that this is your certificate +and that it should be trusted, even though it is not properly signed. openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 3560 -subj "//O=Org\CN=Test" -nodes -Now that [Let's Encrypt](https://letsencrypt.org) is available, -you may wish to generate your free TLS certificate there. +Now that [Let's Encrypt](https://letsencrypt.org) is available, you may wish +to generate your free TLS certificate there. + +--- + +## What changed since upstream + +### Bot API coverage + +Every Bot API version from **6.1 through 10.3** is supported: all types, +fields, methods and parameters of the current specification are present. +Highlights of the last releases: + +- **10.3** — rich message buttons (`RichMessageButton`, the "buttons", + "document" and "expandable_blockquote" blocks), structured ephemeral send + parameters (`EphemeralMessageParameters`), disabled buttons and force-reply + keyboards (`DisabledButton`, `InlineKeyboardButton.Disabled`), stoppable + drafts (`CanStop` / `KeepOnStop`, `Update.StoppedMessageGeneration`), + welcome message rights, `CommunityChatJoined`, gift text fields. +- **10.2** — block-structured rich messages (`InputRichBlock`), explicit media + for rich messages (`InputRichMessageMedia`, `InputMediaVoiceNote`), + ephemeral messages (`ReplyParameters.EphemeralMessageID`, the + `EditEphemeralMessage*` and `DeleteEphemeralMessage` configs, + `Message.ReceiverUser`), communities, payment subscription updates. +- **10.1** — rich messages (`NewRichMessage`, `SendRichMessageDraftConfig`, + `RichMessage` / `RichText` / `RichBlock`), join request queries + (`AnswerChatJoinRequestQueryConfig`, `SendChatJoinRequestWebAppConfig`), + poll links (`InputMediaLink`). +- **10.0** — guest mode (`AnswerGuestQuery`, `Update.GuestMessage`), live + photos (`NewLivePhoto`, `InputMediaLivePhoto`), poll media for questions, + options and explanations, reaction administration, managed bot access + settings. +- **9.x** — managed bots, checklists, suggested posts, direct messages in + channels, gifts and Telegram Stars, business accounts, stories. +- **7.x – 8.x** — replies 2.0 (`ReplyParameters`), link preview options, + reactions, boosts, giveaways, business connections, paid media, Mini App + improvements, and much more. + +See the git history for the per-version commits; each `Full support of API X` +commit message lists what that version added. + +### Keeping up with the Bot API + +`internal/cmd/specdiff` compares the code with the machine-readable +[Bot API specification](https://github.com/PaulSonOfLars/telegram-bot-api-spec) +and lists every missing type, field, method and parameter: + +```sh +go run ./internal/cmd/specdiff # add -v to also list non-spec extras +``` + +The [Bot API spec](./.github/workflows/bot-api-spec.yml) workflow runs it +weekly, so a new Bot API release shows up as a failed run. + +### Credits + +The bulk of the Bot API 6.1 → 10.3 work comes from +[go-telegram-bot-api/telegram-bot-api#794](https://github.com/go-telegram-bot-api/telegram-bot-api/pull/794) +by [@kirugan](https://github.com/kirugan), which was never merged upstream. +This fork merges it with authorship preserved and builds on top of it with a +spec-driven audit (see below), plus ideas from other unmerged upstream pull +requests. + +### Fixes on top of the upstream pull request + +- Placeholder types that only kept raw JSON (`VideoQuality`, `UserRating`, + `UserProfileAudios`, `GiftBackground`, `UniqueGiftColors`) are real structs. +- `OwnedGift` decodes unique gifts correctly; `repostStory`, + `getUserGifts`, `getChatGifts`, `setStickerSetThumbnail`, `setGameScore` + (`force`) and `sendDocument` (`caption_entities`) send the parameters the + Bot API expects; `suggested_post_parameters` is supported by every send + method; `setPassportDataErrors` is available. +- Files nested in polls and rich messages are uploaded via `attach://` + instead of being serialized into the JSON. +- `Update.FromChat` / `SentFrom` cover all update kinds and no longer panic + on callback queries from inline messages. +- Transport errors no longer leak the bot token, and `GetFileDirectURL` + honours `SetFileEndpoint`. + +### Upstream issues fixed + +Real bugs and gaps reported against the original repository that are now +closed in this fork: + +- [#781](https://github.com/go-telegram-bot-api/telegram-bot-api/issues/781) + / [#745](https://github.com/go-telegram-bot-api/telegram-bot-api/issues/745) + — `SetGameScoreConfig` serialized the score under the key `scrore`; game + scores never reached Telegram. +- [#628](https://github.com/go-telegram-bot-api/telegram-bot-api/issues/628) + — `GetUpdatesChan` logged raw `http.Post` errors, which embed the full + request URL including the bot token. The token is now redacted before + logging. +- [#683](https://github.com/go-telegram-bot-api/telegram-bot-api/issues/683) + — `FileEndpoint` was a hard-coded package constant, so + `GetFileDirectURL` still pointed at `api.telegram.org` when you were + running a local Bot API server. There is now a `SetFileEndpoint` / + `FileLink` pair and the field is per-bot. +- [#740](https://github.com/go-telegram-bot-api/telegram-bot-api/issues/740) + — `InlineConfig.CacheTime = 0` was silently dropped by `AddNonZero`, so + Telegram applied its 300s default instead of disabling the cache. + `cache_time` is now serialized unconditionally. +- [#639](https://github.com/go-telegram-bot-api/telegram-bot-api/issues/639) + / [#705](https://github.com/go-telegram-bot-api/telegram-bot-api/issues/705) + — `Send()` tried to unmarshal the bare `true` returned by methods like + `banChatMember` / `setChatTitle` / `sendChatAction`, producing + `json: cannot unmarshal bool into Message`. It now returns a zero + `Message, nil` for those shapes. (Prefer `Request` for methods whose + documented return type is not a `Message`.) +- [#624](https://github.com/go-telegram-bot-api/telegram-bot-api/issues/624) + — README webhook example passed `"cert.pem"` as a `string` to + `NewWebhookWithCert`, which takes a `RequestFileData`. Example now uses + `tgbotapi.FilePath("cert.pem")`. + +### Drive-by improvements + +Small enhancements that didn't correspond to a filed issue but were worth +doing while the code was already open: + +- **Multipart upload name collision fix.** `prepareInputMediaFile` was using + `file-%d` for *both* the main media and the thumbnail on `InputMediaAudio` + / `InputMediaDocument`. The two multipart fields collided in the same form + body; any audio or document upload with a thumbnail probably didn't work on + the wire. Thumbnails now use `file-%d-thumbnail`, matching the existing + `InputMediaVideo` pattern. +- **`closeBody` drains response body before close.** `json.Decoder` can stop + short of EOF, and `net/http` will discard the underlying TCP connection if + the body isn't fully consumed — no keep-alive reuse. The fork drains the + remainder before `Close()`. +- **`Params.AddAny(key, value any) error`.** Replaces the older + `AddInterface` — same behavior, but uses `any` and has a clearer name. + `AddInterface` is kept as an alias for existing callers. +- **`Params.AddFirstValid` errors are propagated.** The upstream pattern + ignored its return value inside `params()` methods, so JSON marshaling + errors in complex fields were silently swallowed. The fork consistently + captures and returns it. +- **`json.RawMessage` for JSON-serialized string fields.** Fields the + Telegram docs describe as "JSON-serialized object" (e.g. `provider_data` + on `InvoiceConfig` / `InvoiceLinkConfig`) are now `json.RawMessage` + instead of `string`, so you can hand them an already-marshaled payload + without double-encoding. diff --git a/book.toml b/book.toml index 841d5ba6..1c0d4b4a 100644 --- a/book.toml +++ b/book.toml @@ -6,4 +6,4 @@ src = "docs" title = "Go Telegram Bot API" [output.html] -git-repository-url = "https://github.com/go-telegram-bot-api/telegram-bot-api" +git-repository-url = "https://github.com/bssth/telegram-bot-api" diff --git a/bot.go b/bot.go index 39037b8d..43dab377 100644 --- a/bot.go +++ b/bot.go @@ -3,6 +3,7 @@ package tgbotapi import ( + "bytes" "encoding/json" "errors" "fmt" @@ -29,7 +30,8 @@ type BotAPI struct { Client HTTPClient `json:"-"` shutdownChannel chan interface{} - apiEndpoint string + apiEndpoint string + fileEndpoint string } // NewBotAPI creates a new BotAPI instance. @@ -58,7 +60,8 @@ func NewBotAPIWithClient(token, apiEndpoint string, client HTTPClient) (*BotAPI, Buffer: 100, shutdownChannel: make(chan interface{}), - apiEndpoint: apiEndpoint, + apiEndpoint: apiEndpoint, + fileEndpoint: FileEndpoint, } self, err := bot.GetMe() @@ -76,20 +79,55 @@ func (bot *BotAPI) SetAPIEndpoint(apiEndpoint string) { bot.apiEndpoint = apiEndpoint } -func buildParams(in Params) url.Values { - if in == nil { - return url.Values{} +// SetFileEndpoint changes the file download endpoint used by the instance. +// The value must be a format string with two %s placeholders for token and +// file path, matching the default FileEndpoint constant. +func (bot *BotAPI) SetFileEndpoint(fileEndpoint string) { + bot.fileEndpoint = fileEndpoint +} + +// FileLink returns the full download URL for the given File, using the +// file endpoint configured on this BotAPI instance. +func (bot *BotAPI) FileLink(f File) string { + endpoint := bot.fileEndpoint + if endpoint == "" { + endpoint = FileEndpoint } + return fmt.Sprintf(endpoint, bot.Token, f.FilePath) +} +func buildParams(in Params) url.Values { out := url.Values{} - for key, value := range in { out.Set(key, value) } - return out } +// redactToken hides the bot token in transport errors. A *url.Error embeds +// the full request URL in its message, and the URL contains the token, so +// returning (and then logging) it verbatim would leak the token. +func (bot *BotAPI) redactToken(err error) error { + urlErr, ok := err.(*url.Error) + if !ok || bot.Token == "" { + return err + } + + redacted := *urlErr + redacted.URL = strings.ReplaceAll(redacted.URL, bot.Token, "") + + return &redacted +} + +// closeBody drains any unread bytes before closing the response body so that +// net/http can return the underlying connection to the keep-alive pool. +// json.Decoder may stop short of EOF (trailing whitespace, partial errors), +// and an undrained body forces the transport to discard the connection. +func closeBody(body io.ReadCloser) { + _, _ = io.Copy(io.Discard, body) + body.Close() +} + // MakeRequest makes a request to a specific endpoint with our token. func (bot *BotAPI) MakeRequest(endpoint string, params Params) (*APIResponse, error) { if bot.Debug { @@ -100,7 +138,7 @@ func (bot *BotAPI) MakeRequest(endpoint string, params Params) (*APIResponse, er values := buildParams(params) - req, err := http.NewRequest("POST", method, strings.NewReader(values.Encode())) + req, err := http.NewRequest(http.MethodPost, method, strings.NewReader(values.Encode())) if err != nil { return &APIResponse{}, err } @@ -108,9 +146,9 @@ func (bot *BotAPI) MakeRequest(endpoint string, params Params) (*APIResponse, er resp, err := bot.Client.Do(req) if err != nil { - return nil, err + return nil, bot.redactToken(err) } - defer resp.Body.Close() + defer closeBody(resp.Body) var apiResp APIResponse bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp) @@ -223,7 +261,7 @@ func (bot *BotAPI) UploadFiles(endpoint string, params Params, files []RequestFi method := fmt.Sprintf(bot.apiEndpoint, bot.Token, endpoint) - req, err := http.NewRequest("POST", method, r) + req, err := http.NewRequest(http.MethodPost, method, r) if err != nil { return nil, err } @@ -232,9 +270,9 @@ func (bot *BotAPI) UploadFiles(endpoint string, params Params, files []RequestFi resp, err := bot.Client.Do(req) if err != nil { - return nil, err + return nil, bot.redactToken(err) } - defer resp.Body.Close() + defer closeBody(resp.Body) var apiResp APIResponse bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp) @@ -254,6 +292,7 @@ func (bot *BotAPI) UploadFiles(endpoint string, params Params, files []RequestFi } return &apiResp, &Error{ + Code: apiResp.ErrorCode, Message: apiResp.Description, ResponseParameters: parameters, } @@ -272,7 +311,7 @@ func (bot *BotAPI) GetFileDirectURL(fileID string) (string, error) { return "", err } - return file.Link(bot.Token), nil + return bot.FileLink(file), nil } // GetMe fetches the currently authenticated bot. @@ -337,12 +376,23 @@ func (bot *BotAPI) Request(c Chattable) (*APIResponse, error) { // Send will send a Chattable item to Telegram and provides the // returned Message. +// +// Some Bot API methods (banChatMember, setChatTitle, sendChatAction, +// etc.) return a bare `true` on success rather than a Message. Send +// tolerates that shape and returns a zero Message with nil error, so +// callers that reach for Send by reflex don't get a confusing JSON +// unmarshal error. Prefer Request for methods whose documented return +// type is not a Message. func (bot *BotAPI) Send(c Chattable) (Message, error) { resp, err := bot.Request(c) if err != nil { return Message{}, err } + if len(resp.Result) == 0 || bytes.Equal(resp.Result, []byte("true")) { + return Message{}, nil + } + var message Message err = json.Unmarshal(resp.Result, &message) @@ -441,7 +491,11 @@ func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) UpdatesChannel { updates, err := bot.GetUpdates(config) if err != nil { - log.Println(err) + // Network errors from http.Post embed the full request URL, + // which in our case contains bot.Token. Strip it before + // logging so the token can't leak to logs. + redacted := strings.ReplaceAll(err.Error(), bot.Token, "") + log.Println(redacted) log.Println("Failed to get updates, retrying in 3 seconds...") time.Sleep(time.Second * 3) @@ -552,17 +606,20 @@ func WriteToHTTPResponse(w http.ResponseWriter, c Chattable) error { return err } -// GetChat gets information about a chat. -func (bot *BotAPI) GetChat(config ChatInfoConfig) (Chat, error) { +// GetChat gets full information about a chat. +// +// As of Bot API 7.3 the return type is ChatFullInfo, which embeds Chat and +// adds the fields that are only populated by getChat. +func (bot *BotAPI) GetChat(config ChatInfoConfig) (ChatFullInfo, error) { resp, err := bot.Request(config) if err != nil { - return Chat{}, err + return ChatFullInfo{}, err } - var chat Chat - err = json.Unmarshal(resp.Result, &chat) + var info ChatFullInfo + err = json.Unmarshal(resp.Result, &info) - return chat, err + return info, err } // GetChatAdministrators gets a list of administrators in the chat. @@ -582,7 +639,14 @@ func (bot *BotAPI) GetChatAdministrators(config ChatAdministratorsConfig) ([]Cha } // GetChatMembersCount gets the number of users in a chat. +// +// Deprecated: use GetChatMemberCount, which matches the Bot API method name. func (bot *BotAPI) GetChatMembersCount(config ChatMemberCountConfig) (int, error) { + return bot.GetChatMemberCount(config) +} + +// GetChatMemberCount gets the number of members in a chat. +func (bot *BotAPI) GetChatMemberCount(config ChatMemberCountConfig) (int, error) { resp, err := bot.Request(config) if err != nil { return -1, err @@ -646,6 +710,307 @@ func (bot *BotAPI) GetStickerSet(config GetStickerSetConfig) (StickerSet, error) return stickers, err } +// GetCustomEmojiStickers returns information about custom emoji stickers by their identifiers. +func (bot *BotAPI) GetCustomEmojiStickers(config GetCustomEmojiStickersConfig) ([]Sticker, error) { + resp, err := bot.Request(config) + if err != nil { + return nil, err + } + + var stickers []Sticker + err = json.Unmarshal(resp.Result, &stickers) + + return stickers, err +} + +// CreateForumTopic creates a topic in a forum supergroup chat and returns +// information about the created topic. +func (bot *BotAPI) CreateForumTopic(config CreateForumTopicConfig) (ForumTopic, error) { + resp, err := bot.Request(config) + if err != nil { + return ForumTopic{}, err + } + + var topic ForumTopic + err = json.Unmarshal(resp.Result, &topic) + + return topic, err +} + +// GetForumTopicIconStickers returns custom emoji stickers, which can be used +// as a forum topic icon by any user. +func (bot *BotAPI) GetForumTopicIconStickers() ([]Sticker, error) { + resp, err := bot.Request(GetForumTopicIconStickersConfig{}) + if err != nil { + return nil, err + } + + var stickers []Sticker + err = json.Unmarshal(resp.Result, &stickers) + + return stickers, err +} + +// GetUserChatBoosts returns the list of boosts added to a chat by a user. +func (bot *BotAPI) GetUserChatBoosts(config GetUserChatBoostsConfig) (UserChatBoosts, error) { + resp, err := bot.Request(config) + if err != nil { + return UserChatBoosts{}, err + } + + var boosts UserChatBoosts + err = json.Unmarshal(resp.Result, &boosts) + + return boosts, err +} + +// ForwardMessages forwards multiple messages of any kind and returns the +// identifiers of the sent messages. +func (bot *BotAPI) ForwardMessages(config ForwardMessagesConfig) ([]MessageID, error) { + resp, err := bot.Request(config) + if err != nil { + return nil, err + } + + var ids []MessageID + err = json.Unmarshal(resp.Result, &ids) + + return ids, err +} + +// CopyMessages copies messages of any kind and returns the identifiers of +// the sent messages. +func (bot *BotAPI) CopyMessages(config CopyMessagesConfig) ([]MessageID, error) { + resp, err := bot.Request(config) + if err != nil { + return nil, err + } + + var ids []MessageID + err = json.Unmarshal(resp.Result, &ids) + + return ids, err +} + +// GetAvailableGifts returns the list of gifts that can be sent by the bot +// to users. +func (bot *BotAPI) GetAvailableGifts() (Gifts, error) { + resp, err := bot.Request(GetAvailableGiftsConfig{}) + if err != nil { + return Gifts{}, err + } + + var gifts Gifts + err = json.Unmarshal(resp.Result, &gifts) + + return gifts, err +} + +// SavePreparedInlineMessage stores a message that can be sent by a user of +// a Mini App and returns a PreparedInlineMessage. +func (bot *BotAPI) SavePreparedInlineMessage(config SavePreparedInlineMessageConfig) (PreparedInlineMessage, error) { + resp, err := bot.Request(config) + if err != nil { + return PreparedInlineMessage{}, err + } + + var msg PreparedInlineMessage + err = json.Unmarshal(resp.Result, &msg) + + return msg, err +} + +// GetStarTransactions returns the bot's Telegram Star transactions in +// chronological order. +func (bot *BotAPI) GetStarTransactions(config GetStarTransactionsConfig) (StarTransactions, error) { + resp, err := bot.Request(config) + if err != nil { + return StarTransactions{}, err + } + + var st StarTransactions + err = json.Unmarshal(resp.Result, &st) + + return st, err +} + +// GetUserProfileAudios fetches a list of audios added to the profile of +// a user. +func (bot *BotAPI) GetUserProfileAudios(config GetUserProfileAudiosConfig) (UserProfileAudios, error) { + resp, err := bot.Request(config) + if err != nil { + return UserProfileAudios{}, err + } + + var audios UserProfileAudios + err = json.Unmarshal(resp.Result, &audios) + + return audios, err +} + +// GetMyStarBalance returns the current Telegram Stars balance of the bot. +func (bot *BotAPI) GetMyStarBalance() (StarAmount, error) { + resp, err := bot.Request(GetMyStarBalanceConfig{}) + if err != nil { + return StarAmount{}, err + } + + var amount StarAmount + err = json.Unmarshal(resp.Result, &amount) + + return amount, err +} + +// GetBusinessAccountStarBalance returns the amount of Telegram Stars owned +// by a managed business account. +func (bot *BotAPI) GetBusinessAccountStarBalance(config GetBusinessAccountStarBalanceConfig) (StarAmount, error) { + resp, err := bot.Request(config) + if err != nil { + return StarAmount{}, err + } + + var amount StarAmount + err = json.Unmarshal(resp.Result, &amount) + + return amount, err +} + +// GetUserGifts returns the list of gifts received and owned by a user. +func (bot *BotAPI) GetUserGifts(config GetUserGiftsConfig) (OwnedGifts, error) { + resp, err := bot.Request(config) + if err != nil { + return OwnedGifts{}, err + } + + var gifts OwnedGifts + err = json.Unmarshal(resp.Result, &gifts) + + return gifts, err +} + +// GetChatGifts returns the list of gifts received and owned by a chat. +func (bot *BotAPI) GetChatGifts(config GetChatGiftsConfig) (OwnedGifts, error) { + resp, err := bot.Request(config) + if err != nil { + return OwnedGifts{}, err + } + + var gifts OwnedGifts + err = json.Unmarshal(resp.Result, &gifts) + + return gifts, err +} + +// RepostStory reposts a story across different business accounts managed +// by the bot and returns the newly posted Story. +func (bot *BotAPI) RepostStory(config RepostStoryConfig) (Story, error) { + resp, err := bot.Request(config) + if err != nil { + return Story{}, err + } + + var story Story + err = json.Unmarshal(resp.Result, &story) + + return story, err +} + +// GetBusinessAccountGifts returns the gifts received and owned by a managed +// business account. +func (bot *BotAPI) GetBusinessAccountGifts(config GetBusinessAccountGiftsConfig) (OwnedGifts, error) { + resp, err := bot.Request(config) + if err != nil { + return OwnedGifts{}, err + } + + var gifts OwnedGifts + err = json.Unmarshal(resp.Result, &gifts) + + return gifts, err +} + +// PostStory posts a story on behalf of a managed business account and +// returns the posted Story. +func (bot *BotAPI) PostStory(config PostStoryConfig) (Story, error) { + resp, err := bot.Request(config) + if err != nil { + return Story{}, err + } + + var story Story + err = json.Unmarshal(resp.Result, &story) + + return story, err +} + +// EditStory edits a story previously posted by the bot and returns the +// edited Story. +func (bot *BotAPI) EditStory(config EditStoryConfig) (Story, error) { + resp, err := bot.Request(config) + if err != nil { + return Story{}, err + } + + var story Story + err = json.Unmarshal(resp.Result, &story) + + return story, err +} + +// GetBusinessConnection returns information about the connection of the bot +// with a business account. +func (bot *BotAPI) GetBusinessConnection(config GetBusinessConnectionConfig) (BusinessConnection, error) { + resp, err := bot.Request(config) + if err != nil { + return BusinessConnection{}, err + } + + var bc BusinessConnection + err = json.Unmarshal(resp.Result, &bc) + + return bc, err +} + +// GetMyName returns the current bot name for the given user language. +func (bot *BotAPI) GetMyName(config GetMyNameConfig) (BotName, error) { + resp, err := bot.Request(config) + if err != nil { + return BotName{}, err + } + + var name BotName + err = json.Unmarshal(resp.Result, &name) + + return name, err +} + +// GetMyDescription returns the current bot description for the given user language. +func (bot *BotAPI) GetMyDescription(config GetMyDescriptionConfig) (BotDescription, error) { + resp, err := bot.Request(config) + if err != nil { + return BotDescription{}, err + } + + var desc BotDescription + err = json.Unmarshal(resp.Result, &desc) + + return desc, err +} + +// GetMyShortDescription returns the current bot short description for the +// given user language. +func (bot *BotAPI) GetMyShortDescription(config GetMyShortDescriptionConfig) (BotShortDescription, error) { + resp, err := bot.Request(config) + if err != nil { + return BotShortDescription{}, err + } + + var desc BotShortDescription + err = json.Unmarshal(resp.Result, &desc) + + return desc, err +} + // StopPoll stops a poll and returns the result. func (bot *BotAPI) StopPoll(config StopPollConfig) (Poll, error) { resp, err := bot.Request(config) @@ -719,6 +1084,88 @@ func (bot *BotAPI) GetMyDefaultAdministratorRights(config GetMyDefaultAdministra return rights, err } +// GetManagedBotToken returns the token of a managed bot. +func (bot *BotAPI) GetManagedBotToken(config GetManagedBotTokenConfig) (string, error) { + resp, err := bot.Request(config) + if err != nil { + return "", err + } + + var token string + err = json.Unmarshal(resp.Result, &token) + + return token, err +} + +// ReplaceManagedBotToken revokes the current token of a managed bot +// and generates a new one. Returns the new token. +func (bot *BotAPI) ReplaceManagedBotToken(config ReplaceManagedBotTokenConfig) (string, error) { + resp, err := bot.Request(config) + if err != nil { + return "", err + } + + var token string + err = json.Unmarshal(resp.Result, &token) + + return token, err +} + +// SavePreparedKeyboardButton stores a keyboard button that can be used +// by a user within a Mini App. Returns a PreparedKeyboardButton. +func (bot *BotAPI) SavePreparedKeyboardButton(config SavePreparedKeyboardButtonConfig) (PreparedKeyboardButton, error) { + resp, err := bot.Request(config) + if err != nil { + return PreparedKeyboardButton{}, err + } + + var button PreparedKeyboardButton + err = json.Unmarshal(resp.Result, &button) + + return button, err +} + +// AnswerGuestQuery replies to a received guest message and returns the +// resulting SentGuestMessage. +func (bot *BotAPI) AnswerGuestQuery(config AnswerGuestQueryConfig) (SentGuestMessage, error) { + var sent SentGuestMessage + + resp, err := bot.Request(config) + if err != nil { + return sent, err + } + + err = json.Unmarshal(resp.Result, &sent) + return sent, err +} + +// GetManagedBotAccessSettings returns the access settings of a managed bot. +func (bot *BotAPI) GetManagedBotAccessSettings(config GetManagedBotAccessSettingsConfig) (BotAccessSettings, error) { + var settings BotAccessSettings + + resp, err := bot.Request(config) + if err != nil { + return settings, err + } + + err = json.Unmarshal(resp.Result, &settings) + return settings, err +} + +// GetUserPersonalChatMessages returns the last messages from the personal +// chat of a user. +func (bot *BotAPI) GetUserPersonalChatMessages(config GetUserPersonalChatMessagesConfig) ([]Message, error) { + resp, err := bot.Request(config) + if err != nil { + return nil, err + } + + var messages []Message + err = json.Unmarshal(resp.Result, &messages) + + return messages, err +} + // EscapeText takes an input text and escape Telegram markup symbols. // In this way we can send a text without being afraid of having to escape the characters manually. // Note that you don't have to include the formatting style in the input text, or it will be escaped too. diff --git a/bot_test.go b/bot_test.go index 7abd790a..5a6ace87 100644 --- a/bot_test.go +++ b/bot_test.go @@ -35,17 +35,21 @@ func (t testLogger) Printf(format string, v ...interface{}) { } func getBot(t *testing.T) (*BotAPI, error) { - bot, err := NewBotAPI(TestToken) - bot.Debug = true - - logger := testLogger{t} - SetLogger(logger) + token := os.Getenv("TELEGRAM_BOT_TOKEN") + if token == "" { + token = TestToken + } + bot, err := NewBotAPI(token) if err != nil { - t.Error(err) + t.Skipf("skipping: NewBotAPI failed (set TELEGRAM_BOT_TOKEN to a real token to run): %v", err) + return nil, err } - return bot, err + bot.Debug = true + SetLogger(testLogger{t}) + + return bot, nil } func TestNewBotAPI_notoken(t *testing.T) { @@ -84,7 +88,7 @@ func TestSendWithMessageReply(t *testing.T) { bot, _ := getBot(t) msg := NewMessage(ChatID, "A test message from the test library in telegram-bot-api") - msg.ReplyToMessageID = ReplyToMessageID + msg.ReplyParameters = &ReplyParameters{MessageID: ReplyToMessageID} _, err := bot.Send(msg) if err != nil { @@ -169,7 +173,7 @@ func TestSendWithNewPhotoReply(t *testing.T) { bot, _ := getBot(t) msg := NewPhoto(ChatID, FilePath("tests/image.jpg")) - msg.ReplyToMessageID = ReplyToMessageID + msg.ReplyParameters = &ReplyParameters{MessageID: ReplyToMessageID} _, err := bot.Send(msg) @@ -246,11 +250,11 @@ func TestSendWithNewDocument(t *testing.T) { } } -func TestSendWithNewDocumentAndThumb(t *testing.T) { +func TestSendWithNewDocumentAndThumbnail(t *testing.T) { bot, _ := getBot(t) msg := NewDocument(ChatID, FilePath("tests/voice.ogg")) - msg.Thumb = FilePath("tests/image.jpg") + msg.Thumbnail = FilePath("tests/image.jpg") _, err := bot.Send(msg) if err != nil { @@ -699,7 +703,7 @@ func ExampleNewBotAPI() { log.Printf("[%s] %s", update.Message.From.UserName, update.Message.Text) msg := NewMessage(update.Message.Chat.ID, update.Message.Text) - msg.ReplyToMessageID = update.Message.MessageID + msg.ReplyParameters = &ReplyParameters{MessageID: update.Message.MessageID} bot.Send(msg) } @@ -745,7 +749,7 @@ func ExampleNewWebhook() { } } -func ExampleWebhookHandler() { +func ExampleNewWebhookWithCert() { bot, err := NewBotAPI("MyAwesomeBotToken") if err != nil { panic(err) diff --git a/configs.go b/configs.go index 1831337b..dfa99b99 100644 --- a/configs.go +++ b/configs.go @@ -2,6 +2,7 @@ package tgbotapi import ( "bytes" + "encoding/json" "fmt" "io" "net/url" @@ -85,6 +86,39 @@ const ( // only in polls that were sent by the bot itself. UpdateTypePollAnswer = "poll_answer" + // UpdateTypeMessageReaction is when a reaction to a message was changed by a user. + UpdateTypeMessageReaction = "message_reaction" + + // UpdateTypeMessageReactionCount is when reactions to a message with anonymous reactions were changed. + UpdateTypeMessageReactionCount = "message_reaction_count" + + // UpdateTypeChatBoost is when a boost was added to a chat or changed. + UpdateTypeChatBoost = "chat_boost" + + // UpdateTypeRemovedChatBoost is when a boost was removed from a chat. + UpdateTypeRemovedChatBoost = "removed_chat_boost" + + // UpdateTypeBusinessConnection is when the bot was connected to or + // disconnected from a business account, or a user edited an existing + // connection with the bot. + UpdateTypeBusinessConnection = "business_connection" + + // UpdateTypeBusinessMessage is a new non-service message from a connected + // business account. + UpdateTypeBusinessMessage = "business_message" + + // UpdateTypeEditedBusinessMessage is a new version of a message from a + // connected business account. + UpdateTypeEditedBusinessMessage = "edited_business_message" + + // UpdateTypeDeletedBusinessMessages is when messages were deleted from a + // connected business account. + UpdateTypeDeletedBusinessMessages = "deleted_business_messages" + + // UpdateTypePurchasedPaidMedia is when a user purchased paid media with + // a non-empty payload sent by the bot in a non-channel chat. + UpdateTypePurchasedPaidMedia = "purchased_paid_media" + // UpdateTypeMyChatMember is when the bot's chat member status was updated in a chat. For private chats, this // update is received only when the bot is blocked or unblocked by the user. UpdateTypeMyChatMember = "my_chat_member" @@ -92,6 +126,27 @@ const ( // UpdateTypeChatMember is when the bot must be an administrator in the chat and must explicitly specify // this update in the list of allowed_updates to receive these updates. UpdateTypeChatMember = "chat_member" + + // UpdateTypeChatJoinRequest is when a request to join the chat has been sent. + // The bot must have the can_invite_users administrator right in the chat to + // receive these updates. + UpdateTypeChatJoinRequest = "chat_join_request" + + // UpdateTypeGuestMessage is a new guest message. The bot can use + // Message.GuestQueryID and the answerGuestQuery method to reply to it. + UpdateTypeGuestMessage = "guest_message" + + // UpdateTypeManagedBot is when a new bot was created to be managed by the bot, + // or token or owner of a managed bot was changed. + UpdateTypeManagedBot = "managed_bot" + + // UpdateTypeSubscription is when a user payment subscription toward the + // bot was changed. + UpdateTypeSubscription = "subscription" + + // UpdateTypeStoppedMessageGeneration is when a user asked the bot to stop + // the generation of a message. + UpdateTypeStoppedMessageGeneration = "stopped_message_generation" ) // Library errors @@ -264,29 +319,89 @@ func (CloseConfig) params() (Params, error) { // BaseChat is base type for all chat config types. type BaseChat struct { - ChatID int64 // required - ChannelUsername string - ProtectContent bool - ReplyToMessageID int - ReplyMarkup interface{} - DisableNotification bool - AllowSendingWithoutReply bool + ChatID int64 // required + ChannelUsername string + BusinessConnectionID string + MessageThreadID int + DirectMessagesTopicID int + MessageEffectID string + ProtectContent bool + AllowPaidBroadcast bool + ReplyParameters *ReplyParameters + ReplyMarkup interface{} + DisableNotification bool + // SuggestedPostParameters contains the parameters of the suggested post + // to send; for direct messages chats only. If the message is sent as a + // reply to another suggested post, then that suggested post is + // automatically declined. + SuggestedPostParameters *SuggestedPostParameters } func (chat *BaseChat) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", chat.ChatID, chat.ChannelUsername) - params.AddNonZero("reply_to_message_id", chat.ReplyToMessageID) + if err := params.AddFirstValid("chat_id", chat.ChatID, chat.ChannelUsername); err != nil { + return params, err + } + params.AddNonEmpty("business_connection_id", chat.BusinessConnectionID) + params.AddNonZero("message_thread_id", chat.MessageThreadID) + params.AddNonZero("direct_messages_topic_id", chat.DirectMessagesTopicID) + params.AddNonEmpty("message_effect_id", chat.MessageEffectID) params.AddBool("disable_notification", chat.DisableNotification) - params.AddBool("allow_sending_without_reply", chat.AllowSendingWithoutReply) params.AddBool("protect_content", chat.ProtectContent) + params.AddBool("allow_paid_broadcast", chat.AllowPaidBroadcast) - err := params.AddInterface("reply_markup", chat.ReplyMarkup) + if err := params.AddAny("reply_parameters", chat.ReplyParameters); err != nil { + return params, err + } + if err := params.AddAny("suggested_post_parameters", chat.SuggestedPostParameters); err != nil { + return params, err + } + err := params.AddAny("reply_markup", chat.ReplyMarkup) return params, err } +// EphemeralMessageParameters holds the parameters that make an outgoing +// message ephemeral, i.e. visible only to a single recipient and the bot. It +// is embedded (as EphemeralSendParams) by the send configs whose methods +// support ephemeral messages and is sent on the wire as the JSON-serialized +// ephemeral_message_parameters parameter. +// +// Leaving ReceiverUserID zero sends an ordinary, non-ephemeral message. +type EphemeralMessageParameters struct { + // ReceiverUserID is the unique identifier of the user who will receive + // the ephemeral message; for group and supergroup chats only. Delivery is + // not guaranteed, especially if the user is offline. + ReceiverUserID int64 `json:"receiver_user_id"` + // CallbackQueryID is the identifier of the callback query which triggered + // the ephemeral message, if any. + // + // optional + CallbackQueryID string `json:"callback_query_id,omitempty"` + // ReplaceCallbackQueryMessage, if true, shows the ephemeral message in + // place of the original message. Must be false for callback queries from + // ephemeral messages, which must be edited with the editEphemeralMessage* + // methods. + // + // optional + ReplaceCallbackQueryMessage bool `json:"replace_callback_query_message,omitempty"` +} + +// EphemeralSendParams is the embedded form of EphemeralMessageParameters kept +// for source compatibility with Bot API 10.2 code. +type EphemeralSendParams = EphemeralMessageParameters + +// addTo writes the ephemeral_message_parameters parameter into params. It is +// a named method rather than params() so that it does not collide with the +// params() promoted from BaseChat in configs that embed both. +func (e EphemeralMessageParameters) addTo(params Params) error { + if e == (EphemeralMessageParameters{}) { + return nil + } + return params.AddAny("ephemeral_message_parameters", e) +} + // BaseFile is a base type for all file config types. type BaseFile struct { BaseChat @@ -299,24 +414,29 @@ func (file BaseFile) params() (Params, error) { // BaseEdit is base type of all chat edits. type BaseEdit struct { - ChatID int64 - ChannelUsername string - MessageID int - InlineMessageID string - ReplyMarkup *InlineKeyboardMarkup + BusinessConnectionID string + ChatID int64 + ChannelUsername string + MessageID int + InlineMessageID string + ReplyMarkup *InlineKeyboardMarkup } func (edit BaseEdit) params() (Params, error) { params := make(Params) + params.AddNonEmpty("business_connection_id", edit.BusinessConnectionID) + if edit.InlineMessageID != "" { params["inline_message_id"] = edit.InlineMessageID } else { - params.AddFirstValid("chat_id", edit.ChatID, edit.ChannelUsername) + if err := params.AddFirstValid("chat_id", edit.ChatID, edit.ChannelUsername); err != nil { + return params, err + } params.AddNonZero("message_id", edit.MessageID) } - err := params.AddInterface("reply_markup", edit.ReplyMarkup) + err := params.AddAny("reply_markup", edit.ReplyMarkup) return params, err } @@ -324,10 +444,11 @@ func (edit BaseEdit) params() (Params, error) { // MessageConfig contains information about a SendMessage request. type MessageConfig struct { BaseChat - Text string - ParseMode string - Entities []MessageEntity - DisableWebPagePreview bool + EphemeralSendParams + Text string + ParseMode string + Entities []MessageEntity + LinkPreviewOptions *LinkPreviewOptions } func (config MessageConfig) params() (Params, error) { @@ -337,9 +458,14 @@ func (config MessageConfig) params() (Params, error) { } params.AddNonEmpty("text", config.Text) - params.AddBool("disable_web_page_preview", config.DisableWebPagePreview) params.AddNonEmpty("parse_mode", config.ParseMode) - err = params.AddInterface("entities", config.Entities) + if err = params.AddAny("link_preview_options", config.LinkPreviewOptions); err != nil { + return params, err + } + if err = params.AddAny("entities", config.Entities); err != nil { + return params, err + } + err = config.EphemeralSendParams.addTo(params) return params, err } @@ -354,6 +480,7 @@ type ForwardConfig struct { FromChatID int64 // required FromChannelUsername string MessageID int // required + VideoStartTimestamp int } func (config ForwardConfig) params() (Params, error) { @@ -364,6 +491,7 @@ func (config ForwardConfig) params() (Params, error) { params.AddNonZero64("from_chat_id", config.FromChatID) params.AddNonZero("message_id", config.MessageID) + params.AddNonZero("video_start_timestamp", config.VideoStartTimestamp) return params, nil } @@ -375,12 +503,14 @@ func (config ForwardConfig) method() string { // CopyMessageConfig contains information about a copyMessage request. type CopyMessageConfig struct { BaseChat - FromChatID int64 - FromChannelUsername string - MessageID int - Caption string - ParseMode string - CaptionEntities []MessageEntity + FromChatID int64 + FromChannelUsername string + MessageID int + VideoStartTimestamp int + Caption string + ParseMode string + CaptionEntities []MessageEntity + ShowCaptionAboveMedia bool } func (config CopyMessageConfig) params() (Params, error) { @@ -389,11 +519,15 @@ func (config CopyMessageConfig) params() (Params, error) { return params, err } - params.AddFirstValid("from_chat_id", config.FromChatID, config.FromChannelUsername) + if err = params.AddFirstValid("from_chat_id", config.FromChatID, config.FromChannelUsername); err != nil { + return params, err + } params.AddNonZero("message_id", config.MessageID) + params.AddNonZero("video_start_timestamp", config.VideoStartTimestamp) params.AddNonEmpty("caption", config.Caption) params.AddNonEmpty("parse_mode", config.ParseMode) - err = params.AddInterface("caption_entities", config.CaptionEntities) + params.AddBool("show_caption_above_media", config.ShowCaptionAboveMedia) + err = params.AddAny("caption_entities", config.CaptionEntities) return params, err } @@ -405,10 +539,13 @@ func (config CopyMessageConfig) method() string { // PhotoConfig contains information about a SendPhoto request. type PhotoConfig struct { BaseFile - Thumb RequestFileData - Caption string - ParseMode string - CaptionEntities []MessageEntity + EphemeralSendParams + Thumbnail RequestFileData + Caption string + ParseMode string + CaptionEntities []MessageEntity + ShowCaptionAboveMedia bool + HasSpoiler bool } func (config PhotoConfig) params() (Params, error) { @@ -419,7 +556,12 @@ func (config PhotoConfig) params() (Params, error) { params.AddNonEmpty("caption", config.Caption) params.AddNonEmpty("parse_mode", config.ParseMode) - err = params.AddInterface("caption_entities", config.CaptionEntities) + params.AddBool("show_caption_above_media", config.ShowCaptionAboveMedia) + params.AddBool("has_spoiler", config.HasSpoiler) + if err = params.AddAny("caption_entities", config.CaptionEntities); err != nil { + return params, err + } + err = config.EphemeralSendParams.addTo(params) return params, err } @@ -434,10 +576,10 @@ func (config PhotoConfig) files() []RequestFile { Data: config.File, }} - if config.Thumb != nil { + if config.Thumbnail != nil { files = append(files, RequestFile{ - Name: "thumb", - Data: config.Thumb, + Name: "thumbnail", + Data: config.Thumbnail, }) } @@ -447,7 +589,8 @@ func (config PhotoConfig) files() []RequestFile { // AudioConfig contains information about a SendAudio request. type AudioConfig struct { BaseFile - Thumb RequestFileData + EphemeralSendParams + Thumbnail RequestFileData Caption string ParseMode string CaptionEntities []MessageEntity @@ -467,7 +610,10 @@ func (config AudioConfig) params() (Params, error) { params.AddNonEmpty("title", config.Title) params.AddNonEmpty("caption", config.Caption) params.AddNonEmpty("parse_mode", config.ParseMode) - err = params.AddInterface("caption_entities", config.CaptionEntities) + if err = params.AddInterface("caption_entities", config.CaptionEntities); err != nil { + return params, err + } + err = config.EphemeralSendParams.addTo(params) return params, err } @@ -482,10 +628,10 @@ func (config AudioConfig) files() []RequestFile { Data: config.File, }} - if config.Thumb != nil { + if config.Thumbnail != nil { files = append(files, RequestFile{ - Name: "thumb", - Data: config.Thumb, + Name: "thumbnail", + Data: config.Thumbnail, }) } @@ -495,7 +641,8 @@ func (config AudioConfig) files() []RequestFile { // DocumentConfig contains information about a SendDocument request. type DocumentConfig struct { BaseFile - Thumb RequestFileData + EphemeralSendParams + Thumbnail RequestFileData Caption string ParseMode string CaptionEntities []MessageEntity @@ -504,10 +651,18 @@ type DocumentConfig struct { func (config DocumentConfig) params() (Params, error) { params, err := config.BaseFile.params() + if err != nil { + return params, err + } params.AddNonEmpty("caption", config.Caption) params.AddNonEmpty("parse_mode", config.ParseMode) params.AddBool("disable_content_type_detection", config.DisableContentTypeDetection) + if err = params.AddAny("caption_entities", config.CaptionEntities); err != nil { + return params, err + } + + err = config.EphemeralSendParams.addTo(params) return params, err } @@ -522,10 +677,10 @@ func (config DocumentConfig) files() []RequestFile { Data: config.File, }} - if config.Thumb != nil { + if config.Thumbnail != nil { files = append(files, RequestFile{ - Name: "thumb", - Data: config.Thumb, + Name: "thumbnail", + Data: config.Thumbnail, }) } @@ -535,10 +690,22 @@ func (config DocumentConfig) files() []RequestFile { // StickerConfig contains information about a SendSticker request. type StickerConfig struct { BaseFile + EphemeralSendParams + // Emoji associated with the sticker; only for just uploaded stickers. + Emoji string } func (config StickerConfig) params() (Params, error) { - return config.BaseChat.params() + params, err := config.BaseChat.params() + if err != nil { + return params, err + } + + params.AddNonEmpty("emoji", config.Emoji) + + err = config.EphemeralSendParams.addTo(params) + + return params, err } func (config StickerConfig) method() string { @@ -555,12 +722,19 @@ func (config StickerConfig) files() []RequestFile { // VideoConfig contains information about a SendVideo request. type VideoConfig struct { BaseFile - Thumb RequestFileData - Duration int - Caption string - ParseMode string - CaptionEntities []MessageEntity - SupportsStreaming bool + EphemeralSendParams + Thumbnail RequestFileData + Cover RequestFileData + StartTimestamp int + Duration int + Width int + Height int + Caption string + ParseMode string + CaptionEntities []MessageEntity + ShowCaptionAboveMedia bool + SupportsStreaming bool + HasSpoiler bool } func (config VideoConfig) params() (Params, error) { @@ -570,10 +744,18 @@ func (config VideoConfig) params() (Params, error) { } params.AddNonZero("duration", config.Duration) + params.AddNonZero("width", config.Width) + params.AddNonZero("height", config.Height) + params.AddNonZero("start_timestamp", config.StartTimestamp) params.AddNonEmpty("caption", config.Caption) params.AddNonEmpty("parse_mode", config.ParseMode) + params.AddBool("show_caption_above_media", config.ShowCaptionAboveMedia) params.AddBool("supports_streaming", config.SupportsStreaming) - err = params.AddInterface("caption_entities", config.CaptionEntities) + params.AddBool("has_spoiler", config.HasSpoiler) + if err = params.AddAny("caption_entities", config.CaptionEntities); err != nil { + return params, err + } + err = config.EphemeralSendParams.addTo(params) return params, err } @@ -588,10 +770,71 @@ func (config VideoConfig) files() []RequestFile { Data: config.File, }} - if config.Thumb != nil { + if config.Thumbnail != nil { + files = append(files, RequestFile{ + Name: "thumbnail", + Data: config.Thumbnail, + }) + } + + if config.Cover != nil { + files = append(files, RequestFile{ + Name: "cover", + Data: config.Cover, + }) + } + + return files +} + +// LivePhotoConfig contains information about a SendLivePhoto request. +// +// File holds the live photo video (must be 10 seconds or less and 10 MB or +// less). Photo holds the static image. Sending live photos by URL is not +// currently supported. +type LivePhotoConfig struct { + BaseFile + EphemeralSendParams + Photo RequestFileData + Caption string + ParseMode string + CaptionEntities []MessageEntity + ShowCaptionAboveMedia bool + HasSpoiler bool +} + +func (config LivePhotoConfig) params() (Params, error) { + params, err := config.BaseChat.params() + if err != nil { + return params, err + } + + params.AddNonEmpty("caption", config.Caption) + params.AddNonEmpty("parse_mode", config.ParseMode) + params.AddBool("show_caption_above_media", config.ShowCaptionAboveMedia) + params.AddBool("has_spoiler", config.HasSpoiler) + if err = params.AddAny("caption_entities", config.CaptionEntities); err != nil { + return params, err + } + err = config.EphemeralSendParams.addTo(params) + + return params, err +} + +func (config LivePhotoConfig) method() string { + return "sendLivePhoto" +} + +func (config LivePhotoConfig) files() []RequestFile { + files := []RequestFile{{ + Name: "live_photo", + Data: config.File, + }} + + if config.Photo != nil { files = append(files, RequestFile{ - Name: "thumb", - Data: config.Thumb, + Name: "photo", + Data: config.Photo, }) } @@ -601,11 +844,16 @@ func (config VideoConfig) files() []RequestFile { // AnimationConfig contains information about a SendAnimation request. type AnimationConfig struct { BaseFile - Duration int - Thumb RequestFileData - Caption string - ParseMode string - CaptionEntities []MessageEntity + EphemeralSendParams + Duration int + Width int + Height int + Thumbnail RequestFileData + Caption string + ParseMode string + CaptionEntities []MessageEntity + ShowCaptionAboveMedia bool + HasSpoiler bool } func (config AnimationConfig) params() (Params, error) { @@ -615,9 +863,16 @@ func (config AnimationConfig) params() (Params, error) { } params.AddNonZero("duration", config.Duration) + params.AddNonZero("width", config.Width) + params.AddNonZero("height", config.Height) params.AddNonEmpty("caption", config.Caption) params.AddNonEmpty("parse_mode", config.ParseMode) - err = params.AddInterface("caption_entities", config.CaptionEntities) + params.AddBool("show_caption_above_media", config.ShowCaptionAboveMedia) + params.AddBool("has_spoiler", config.HasSpoiler) + if err = params.AddAny("caption_entities", config.CaptionEntities); err != nil { + return params, err + } + err = config.EphemeralSendParams.addTo(params) return params, err } @@ -632,10 +887,10 @@ func (config AnimationConfig) files() []RequestFile { Data: config.File, }} - if config.Thumb != nil { + if config.Thumbnail != nil { files = append(files, RequestFile{ - Name: "thumb", - Data: config.Thumb, + Name: "thumbnail", + Data: config.Thumbnail, }) } @@ -645,17 +900,23 @@ func (config AnimationConfig) files() []RequestFile { // VideoNoteConfig contains information about a SendVideoNote request. type VideoNoteConfig struct { BaseFile - Thumb RequestFileData - Duration int - Length int + EphemeralSendParams + Thumbnail RequestFileData + Duration int + Length int } func (config VideoNoteConfig) params() (Params, error) { params, err := config.BaseChat.params() + if err != nil { + return params, err + } params.AddNonZero("duration", config.Duration) params.AddNonZero("length", config.Length) + err = config.EphemeralSendParams.addTo(params) + return params, err } @@ -669,10 +930,10 @@ func (config VideoNoteConfig) files() []RequestFile { Data: config.File, }} - if config.Thumb != nil { + if config.Thumbnail != nil { files = append(files, RequestFile{ - Name: "thumb", - Data: config.Thumb, + Name: "thumbnail", + Data: config.Thumbnail, }) } @@ -682,7 +943,8 @@ func (config VideoNoteConfig) files() []RequestFile { // VoiceConfig contains information about a SendVoice request. type VoiceConfig struct { BaseFile - Thumb RequestFileData + EphemeralSendParams + Thumbnail RequestFileData Caption string ParseMode string CaptionEntities []MessageEntity @@ -698,7 +960,10 @@ func (config VoiceConfig) params() (Params, error) { params.AddNonZero("duration", config.Duration) params.AddNonEmpty("caption", config.Caption) params.AddNonEmpty("parse_mode", config.ParseMode) - err = params.AddInterface("caption_entities", config.CaptionEntities) + if err = params.AddInterface("caption_entities", config.CaptionEntities); err != nil { + return params, err + } + err = config.EphemeralSendParams.addTo(params) return params, err } @@ -713,10 +978,10 @@ func (config VoiceConfig) files() []RequestFile { Data: config.File, }} - if config.Thumb != nil { + if config.Thumbnail != nil { files = append(files, RequestFile{ - Name: "thumb", - Data: config.Thumb, + Name: "thumbnail", + Data: config.Thumbnail, }) } @@ -726,6 +991,7 @@ func (config VoiceConfig) files() []RequestFile { // LocationConfig contains information about a SendLocation request. type LocationConfig struct { BaseChat + EphemeralSendParams Latitude float64 // required Longitude float64 // required HorizontalAccuracy float64 // optional @@ -736,6 +1002,9 @@ type LocationConfig struct { func (config LocationConfig) params() (Params, error) { params, err := config.BaseChat.params() + if err != nil { + return params, err + } params.AddNonZeroFloat("latitude", config.Latitude) params.AddNonZeroFloat("longitude", config.Longitude) @@ -744,6 +1013,8 @@ func (config LocationConfig) params() (Params, error) { params.AddNonZero("heading", config.Heading) params.AddNonZero("proximity_alert_radius", config.ProximityAlertRadius) + err = config.EphemeralSendParams.addTo(params) + return params, err } @@ -756,6 +1027,7 @@ type EditMessageLiveLocationConfig struct { BaseEdit Latitude float64 // required Longitude float64 // required + LivePeriod int // optional; pass 0x7FFFFFFF to keep live indefinitely HorizontalAccuracy float64 // optional Heading int // optional ProximityAlertRadius int // optional @@ -766,6 +1038,7 @@ func (config EditMessageLiveLocationConfig) params() (Params, error) { params.AddNonZeroFloat("latitude", config.Latitude) params.AddNonZeroFloat("longitude", config.Longitude) + params.AddNonZero("live_period", config.LivePeriod) params.AddNonZeroFloat("horizontal_accuracy", config.HorizontalAccuracy) params.AddNonZero("heading", config.Heading) params.AddNonZero("proximity_alert_radius", config.ProximityAlertRadius) @@ -793,6 +1066,7 @@ func (config StopMessageLiveLocationConfig) method() string { // VenueConfig contains information about a SendVenue request. type VenueConfig struct { BaseChat + EphemeralSendParams Latitude float64 // required Longitude float64 // required Title string // required @@ -805,6 +1079,9 @@ type VenueConfig struct { func (config VenueConfig) params() (Params, error) { params, err := config.BaseChat.params() + if err != nil { + return params, err + } params.AddNonZeroFloat("latitude", config.Latitude) params.AddNonZeroFloat("longitude", config.Longitude) @@ -815,6 +1092,8 @@ func (config VenueConfig) params() (Params, error) { params.AddNonEmpty("google_place_id", config.GooglePlaceID) params.AddNonEmpty("google_place_type", config.GooglePlaceType) + err = config.EphemeralSendParams.addTo(params) + return params, err } @@ -825,6 +1104,7 @@ func (config VenueConfig) method() string { // ContactConfig allows you to send a contact. type ContactConfig struct { BaseChat + EphemeralSendParams PhoneNumber string FirstName string LastName string @@ -833,6 +1113,9 @@ type ContactConfig struct { func (config ContactConfig) params() (Params, error) { params, err := config.BaseChat.params() + if err != nil { + return params, err + } params["phone_number"] = config.PhoneNumber params["first_name"] = config.FirstName @@ -840,6 +1123,8 @@ func (config ContactConfig) params() (Params, error) { params.AddNonEmpty("last_name", config.LastName) params.AddNonEmpty("vcard", config.VCard) + err = config.EphemeralSendParams.addTo(params) + return params, err } @@ -848,20 +1133,57 @@ func (config ContactConfig) method() string { } // SendPollConfig allows you to send a poll. +// +// Media and ExplanationMedia accept any of the InputMedia* variants +// allowed by InputPollMedia (InputMediaAnimation, InputMediaAudio, +// InputMediaDocument, InputMediaLivePhoto, InputMediaLocation, +// InputMediaPhoto, InputMediaVenue, InputMediaVideo). type SendPollConfig struct { BaseChat - Question string - Options []string - IsAnonymous bool - Type string - AllowsMultipleAnswers bool - CorrectOptionID int64 - Explanation string - ExplanationParseMode string - ExplanationEntities []MessageEntity - OpenPeriod int - CloseDate int - IsClosed bool + Question string + QuestionParseMode string + QuestionEntities []MessageEntity + Description string + DescriptionParseMode string + DescriptionEntities []MessageEntity + Media any + Options []InputPollOption + IsAnonymous bool + Type string + AllowsMultipleAnswers bool + AllowsRevoting bool + ShuffleOptions bool + AllowAddingOptions bool + HideResultsUntilCloses bool + MembersOnly bool + CountryCodes []string + CorrectOptionIDs []int + Explanation string + ExplanationParseMode string + ExplanationEntities []MessageEntity + ExplanationMedia any + OpenPeriod int + CloseDate int + IsClosed bool +} + +// prepareMedia returns copies of the poll's media, explanation media and +// options in which files that need uploading are replaced by attach:// +// references, together with the files to upload. +func (config SendPollConfig) prepareMedia() (media, explanationMedia any, options []InputPollOption, files []RequestFile) { + u := nestedMediaUploader{prefix: "poll-media"} + + media = u.media(config.Media) + if config.Options != nil { + options = make([]InputPollOption, len(config.Options)) + for i, option := range config.Options { + option.Media = u.media(option.Media) + options[i] = option + } + } + explanationMedia = u.media(config.ExplanationMedia) + + return media, explanationMedia, options, u.files } func (config SendPollConfig) params() (Params, error) { @@ -870,20 +1192,51 @@ func (config SendPollConfig) params() (Params, error) { return params, err } + media, explanationMedia, options, _ := config.prepareMedia() + params["question"] = config.Question - if err = params.AddInterface("options", config.Options); err != nil { + params.AddNonEmpty("question_parse_mode", config.QuestionParseMode) + if err = params.AddAny("question_entities", config.QuestionEntities); err != nil { + return params, err + } + params.AddNonEmpty("description", config.Description) + params.AddNonEmpty("description_parse_mode", config.DescriptionParseMode) + if err = params.AddAny("description_entities", config.DescriptionEntities); err != nil { + return params, err + } + if err = params.AddAny("media", media); err != nil { + return params, err + } + if err = params.AddAny("options", options); err != nil { return params, err } params["is_anonymous"] = strconv.FormatBool(config.IsAnonymous) params.AddNonEmpty("type", config.Type) params["allows_multiple_answers"] = strconv.FormatBool(config.AllowsMultipleAnswers) - params["correct_option_id"] = strconv.FormatInt(config.CorrectOptionID, 10) + params.AddBool("allows_revoting", config.AllowsRevoting) + params.AddBool("shuffle_options", config.ShuffleOptions) + params.AddBool("allow_adding_options", config.AllowAddingOptions) + params.AddBool("hide_results_until_closes", config.HideResultsUntilCloses) + params.AddBool("members_only", config.MembersOnly) + if len(config.CountryCodes) > 0 { + if err = params.AddAny("country_codes", config.CountryCodes); err != nil { + return params, err + } + } + if len(config.CorrectOptionIDs) > 0 { + if err = params.AddAny("correct_option_ids", config.CorrectOptionIDs); err != nil { + return params, err + } + } params.AddBool("is_closed", config.IsClosed) params.AddNonEmpty("explanation", config.Explanation) params.AddNonEmpty("explanation_parse_mode", config.ExplanationParseMode) params.AddNonZero("open_period", config.OpenPeriod) params.AddNonZero("close_date", config.CloseDate) - err = params.AddInterface("explanation_entities", config.ExplanationEntities) + if err = params.AddAny("explanation_entities", config.ExplanationEntities); err != nil { + return params, err + } + err = params.AddAny("explanation_media", explanationMedia) return params, err } @@ -892,6 +1245,11 @@ func (SendPollConfig) method() string { return "sendPoll" } +func (config SendPollConfig) files() []RequestFile { + _, _, _, files := config.prepareMedia() + return files +} + // GameConfig allows you to send a game. type GameConfig struct { BaseChat @@ -926,13 +1284,16 @@ func (config SetGameScoreConfig) params() (Params, error) { params := make(Params) params.AddNonZero64("user_id", config.UserID) - params.AddNonZero("scrore", config.Score) + params["score"] = strconv.Itoa(config.Score) + params.AddBool("force", config.Force) params.AddBool("disable_edit_message", config.DisableEditMessage) if config.InlineMessageID != "" { params["inline_message_id"] = config.InlineMessageID } else { - params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } params.AddNonZero("message_id", config.MessageID) } @@ -960,7 +1321,9 @@ func (config GetGameHighScoresConfig) params() (Params, error) { if config.InlineMessageID != "" { params["inline_message_id"] = config.InlineMessageID } else { - params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } params.AddNonZero("message_id", config.MessageID) } @@ -992,10 +1355,13 @@ func (config ChatActionConfig) method() string { // EditMessageTextConfig allows you to modify the text in a message. type EditMessageTextConfig struct { BaseEdit - Text string - ParseMode string - Entities []MessageEntity - DisableWebPagePreview bool + Text string + ParseMode string + Entities []MessageEntity + LinkPreviewOptions *LinkPreviewOptions + // RichMessage is the new rich content of the message. Required if Text + // is not specified. + RichMessage *InputRichMessage } func (config EditMessageTextConfig) params() (Params, error) { @@ -1004,10 +1370,22 @@ func (config EditMessageTextConfig) params() (Params, error) { return params, err } - params["text"] = config.Text + // text stays present when RichMessage is not set; an empty string is + // rejected by the API rather than silently dropped. + if config.RichMessage == nil { + params["text"] = config.Text + } else { + params.AddNonEmpty("text", config.Text) + richMessage, _ := prepareRichMessage(config.RichMessage) + if err = params.AddAny("rich_message", richMessage); err != nil { + return params, err + } + } params.AddNonEmpty("parse_mode", config.ParseMode) - params.AddBool("disable_web_page_preview", config.DisableWebPagePreview) - err = params.AddInterface("entities", config.Entities) + if err = params.AddAny("link_preview_options", config.LinkPreviewOptions); err != nil { + return params, err + } + err = params.AddAny("entities", config.Entities) return params, err } @@ -1016,12 +1394,18 @@ func (config EditMessageTextConfig) method() string { return "editMessageText" } +func (config EditMessageTextConfig) files() []RequestFile { + _, files := prepareRichMessage(config.RichMessage) + return files +} + // EditMessageCaptionConfig allows you to modify the caption of a message. type EditMessageCaptionConfig struct { BaseEdit - Caption string - ParseMode string - CaptionEntities []MessageEntity + Caption string + ParseMode string + CaptionEntities []MessageEntity + ShowCaptionAboveMedia bool } func (config EditMessageCaptionConfig) params() (Params, error) { @@ -1032,7 +1416,8 @@ func (config EditMessageCaptionConfig) params() (Params, error) { params["caption"] = config.Caption params.AddNonEmpty("parse_mode", config.ParseMode) - err = params.AddInterface("caption_entities", config.CaptionEntities) + params.AddBool("show_caption_above_media", config.ShowCaptionAboveMedia) + err = params.AddAny("caption_entities", config.CaptionEntities) return params, err } @@ -1081,6 +1466,208 @@ func (config EditMessageReplyMarkupConfig) method() string { return "editMessageReplyMarkup" } +// BaseEphemeralEdit is the base type for edits and deletions of ephemeral +// messages. Ephemeral messages are addressed by the chat they were sent to, +// the user who received them, and their per-chat ephemeral identifier, so they +// do not use BaseEdit. +// +// Either ChatID or ChannelUsername must be set; ChannelUsername targets a +// supergroup in the @username format. +type BaseEphemeralEdit struct { + // ChatID is the unique identifier for the target chat. + ChatID int64 + // ChannelUsername is the username of the target supergroup, in the + // @username format. Used when ChatID is not set. + ChannelUsername string + // ReceiverUserID is the identifier of the user who received the message. + ReceiverUserID int64 + // EphemeralMessageID is the identifier of the ephemeral message. + EphemeralMessageID int +} + +func (edit BaseEphemeralEdit) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", edit.ChatID, edit.ChannelUsername); err != nil { + return params, err + } + params.AddNonZero64("receiver_user_id", edit.ReceiverUserID) + params.AddNonZero("ephemeral_message_id", edit.EphemeralMessageID) + + return params, nil +} + +// EditEphemeralMessageTextConfig allows you to edit the text of an ephemeral +// message. Delivery of the edit is not guaranteed, especially if the user is +// offline. +type EditEphemeralMessageTextConfig struct { + BaseEphemeralEdit + // Text is the new text of the message, 1-4096 characters after entity + // parsing. Required if RichMessage is not set. + Text string + // RichMessage is the new rich content of the message. Required if Text is + // not set. + RichMessage *InputRichMessage + // ParseMode is the mode for parsing entities in the message text. + ParseMode string + // Entities is a list of special entities that appear in the message text, + // which can be specified instead of ParseMode. + Entities []MessageEntity + // LinkPreviewOptions are the link preview generation options. + LinkPreviewOptions *LinkPreviewOptions + // ReplyMarkup is the new inline keyboard for the message. + ReplyMarkup *InlineKeyboardMarkup +} + +func (config EditEphemeralMessageTextConfig) params() (Params, error) { + params, err := config.BaseEphemeralEdit.params() + if err != nil { + return params, err + } + + // text stays present when RichMessage is not set; an empty string is + // rejected by the API rather than silently dropped. + if config.RichMessage == nil { + params["text"] = config.Text + } else { + params.AddNonEmpty("text", config.Text) + richMessage, _ := prepareRichMessage(config.RichMessage) + if err = params.AddAny("rich_message", richMessage); err != nil { + return params, err + } + } + params.AddNonEmpty("parse_mode", config.ParseMode) + if err = params.AddAny("entities", config.Entities); err != nil { + return params, err + } + if err = params.AddAny("link_preview_options", config.LinkPreviewOptions); err != nil { + return params, err + } + err = params.AddAny("reply_markup", config.ReplyMarkup) + + return params, err +} + +func (config EditEphemeralMessageTextConfig) method() string { + return "editEphemeralMessageText" +} + +func (config EditEphemeralMessageTextConfig) files() []RequestFile { + _, files := prepareRichMessage(config.RichMessage) + return files +} + +// EditEphemeralMessageMediaConfig allows you to edit the media of an ephemeral +// message. A new file can be uploaded, or use a previously uploaded file via +// its file_id, or specify a URL. +type EditEphemeralMessageMediaConfig struct { + BaseEphemeralEdit + // Media is the new media content of the message. + Media interface{} + // ReplyMarkup is the new inline keyboard for the message. + ReplyMarkup *InlineKeyboardMarkup +} + +func (config EditEphemeralMessageMediaConfig) params() (Params, error) { + params, err := config.BaseEphemeralEdit.params() + if err != nil { + return params, err + } + + if err = params.AddAny("media", prepareInputMediaParam(config.Media, 0)); err != nil { + return params, err + } + err = params.AddAny("reply_markup", config.ReplyMarkup) + + return params, err +} + +func (config EditEphemeralMessageMediaConfig) files() []RequestFile { + return prepareInputMediaFile(config.Media, 0) +} + +func (config EditEphemeralMessageMediaConfig) method() string { + return "editEphemeralMessageMedia" +} + +// EditEphemeralMessageCaptionConfig allows you to edit the caption of an +// ephemeral message. +type EditEphemeralMessageCaptionConfig struct { + BaseEphemeralEdit + // Caption is the new caption of the message, 0-1024 characters after + // entities parsing. + Caption string + // ParseMode is the mode for parsing entities in the message caption. + ParseMode string + // CaptionEntities is a list of special entities that appear in the + // caption, which can be specified instead of ParseMode. + CaptionEntities []MessageEntity + // ShowCaptionAboveMedia shows the caption above the message media. + // Supported only for animation, photo, and video messages. + ShowCaptionAboveMedia bool + // ReplyMarkup is the new inline keyboard for the message. + ReplyMarkup *InlineKeyboardMarkup +} + +func (config EditEphemeralMessageCaptionConfig) params() (Params, error) { + params, err := config.BaseEphemeralEdit.params() + if err != nil { + return params, err + } + + params.AddNonEmpty("caption", config.Caption) + params.AddNonEmpty("parse_mode", config.ParseMode) + params.AddBool("show_caption_above_media", config.ShowCaptionAboveMedia) + if err = params.AddAny("caption_entities", config.CaptionEntities); err != nil { + return params, err + } + err = params.AddAny("reply_markup", config.ReplyMarkup) + + return params, err +} + +func (config EditEphemeralMessageCaptionConfig) method() string { + return "editEphemeralMessageCaption" +} + +// EditEphemeralMessageReplyMarkupConfig allows you to edit only the reply +// markup of an ephemeral message. +type EditEphemeralMessageReplyMarkupConfig struct { + BaseEphemeralEdit + // ReplyMarkup is the new inline keyboard for the message. + ReplyMarkup *InlineKeyboardMarkup +} + +func (config EditEphemeralMessageReplyMarkupConfig) params() (Params, error) { + params, err := config.BaseEphemeralEdit.params() + if err != nil { + return params, err + } + + err = params.AddAny("reply_markup", config.ReplyMarkup) + + return params, err +} + +func (config EditEphemeralMessageReplyMarkupConfig) method() string { + return "editEphemeralMessageReplyMarkup" +} + +// DeleteEphemeralMessageConfig allows you to delete an ephemeral message. +// Delivery of the deletion is not guaranteed, especially if the user is +// offline. +type DeleteEphemeralMessageConfig struct { + BaseEphemeralEdit +} + +func (config DeleteEphemeralMessageConfig) params() (Params, error) { + return config.BaseEphemeralEdit.params() +} + +func (config DeleteEphemeralMessageConfig) method() string { + return "deleteEphemeralMessage" +} + // StopPollConfig allows you to stop a poll sent by the bot. type StopPollConfig struct { BaseEdit @@ -1164,6 +1751,7 @@ type WebhookConfig struct { MaxConnections int AllowedUpdates []string DropPendingUpdates bool + SecretToken string } func (config WebhookConfig) method() string { @@ -1181,6 +1769,7 @@ func (config WebhookConfig) params() (Params, error) { params.AddNonZero("max_connections", config.MaxConnections) err := params.AddInterface("allowed_updates", config.AllowedUpdates) params.AddBool("drop_pending_updates", config.DropPendingUpdates) + params.AddNonEmpty("secret_token", config.SecretToken) return params, err } @@ -1215,13 +1804,12 @@ func (config DeleteWebhookConfig) params() (Params, error) { // InlineConfig contains information on making an InlineQuery response. type InlineConfig struct { - InlineQueryID string `json:"inline_query_id"` - Results []interface{} `json:"results"` - CacheTime int `json:"cache_time"` - IsPersonal bool `json:"is_personal"` - NextOffset string `json:"next_offset"` - SwitchPMText string `json:"switch_pm_text"` - SwitchPMParameter string `json:"switch_pm_parameter"` + InlineQueryID string `json:"inline_query_id"` + Results []interface{} `json:"results"` + CacheTime int `json:"cache_time"` + IsPersonal bool `json:"is_personal"` + NextOffset string `json:"next_offset"` + Button *InlineQueryResultsButton `json:"button,omitempty"` } func (config InlineConfig) method() string { @@ -1232,12 +1820,16 @@ func (config InlineConfig) params() (Params, error) { params := make(Params) params["inline_query_id"] = config.InlineQueryID - params.AddNonZero("cache_time", config.CacheTime) + // answerInlineQuery defaults cache_time to 300 seconds server-side, so + // we must serialize the zero value explicitly when the caller wants + // to disable caching. + params["cache_time"] = strconv.Itoa(config.CacheTime) params.AddBool("is_personal", config.IsPersonal) params.AddNonEmpty("next_offset", config.NextOffset) - params.AddNonEmpty("switch_pm_text", config.SwitchPMText) - params.AddNonEmpty("switch_pm_parameter", config.SwitchPMParameter) - err := params.AddInterface("results", config.Results) + if err := params.AddAny("button", config.Button); err != nil { + return params, err + } + err := params.AddAny("results", config.Results) return params, err } @@ -1312,7 +1904,9 @@ func (config UnbanChatMemberConfig) method() string { func (config UnbanChatMemberConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername, config.ChannelUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername, config.ChannelUsername); err != nil { + return params, err + } params.AddNonZero64("user_id", config.UserID) params.AddBool("only_if_banned", config.OnlyIfBanned) @@ -1333,7 +1927,9 @@ func (config BanChatMemberConfig) method() string { func (config BanChatMemberConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } params.AddNonZero64("user_id", config.UserID) params.AddNonZero64("until_date", config.UntilDate) params.AddBool("revoke_messages", config.RevokeMessages) @@ -1349,8 +1945,9 @@ type KickChatMemberConfig = BanChatMemberConfig // RestrictChatMemberConfig contains fields to restrict members of chat type RestrictChatMemberConfig struct { ChatMemberConfig - UntilDate int64 - Permissions *ChatPermissions + UntilDate int64 + Permissions *ChatPermissions + UseIndependentChatPermissions bool } func (config RestrictChatMemberConfig) method() string { @@ -1360,10 +1957,13 @@ func (config RestrictChatMemberConfig) method() string { func (config RestrictChatMemberConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername, config.ChannelUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername, config.ChannelUsername); err != nil { + return params, err + } params.AddNonZero64("user_id", config.UserID) err := params.AddInterface("permissions", config.Permissions) + params.AddBool("use_independent_chat_permissions", config.UseIndependentChatPermissions) params.AddNonZero64("until_date", config.UntilDate) return params, err @@ -1372,17 +1972,24 @@ func (config RestrictChatMemberConfig) params() (Params, error) { // PromoteChatMemberConfig contains fields to promote members of chat type PromoteChatMemberConfig struct { ChatMemberConfig - IsAnonymous bool - CanManageChat bool - CanChangeInfo bool - CanPostMessages bool - CanEditMessages bool - CanDeleteMessages bool - CanManageVideoChats bool - CanInviteUsers bool - CanRestrictMembers bool - CanPinMessages bool - CanPromoteMembers bool + IsAnonymous bool + CanManageChat bool + CanChangeInfo bool + CanPostMessages bool + CanEditMessages bool + CanDeleteMessages bool + CanManageVideoChats bool + CanInviteUsers bool + CanRestrictMembers bool + CanPinMessages bool + CanPromoteMembers bool + CanPostStories bool + CanEditStories bool + CanDeleteStories bool + CanManageTopics bool + CanManageDirectMessages bool + CanManageTags bool + CanSendWelcomeMessages bool } func (config PromoteChatMemberConfig) method() string { @@ -1392,7 +1999,9 @@ func (config PromoteChatMemberConfig) method() string { func (config PromoteChatMemberConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername, config.ChannelUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername, config.ChannelUsername); err != nil { + return params, err + } params.AddNonZero64("user_id", config.UserID) params.AddBool("is_anonymous", config.IsAnonymous) @@ -1406,6 +2015,35 @@ func (config PromoteChatMemberConfig) params() (Params, error) { params.AddBool("can_restrict_members", config.CanRestrictMembers) params.AddBool("can_pin_messages", config.CanPinMessages) params.AddBool("can_promote_members", config.CanPromoteMembers) + params.AddBool("can_post_stories", config.CanPostStories) + params.AddBool("can_edit_stories", config.CanEditStories) + params.AddBool("can_delete_stories", config.CanDeleteStories) + params.AddBool("can_manage_topics", config.CanManageTopics) + params.AddBool("can_manage_direct_messages", config.CanManageDirectMessages) + params.AddBool("can_manage_tags", config.CanManageTags) + params.AddBool("can_send_welcome_messages", config.CanSendWelcomeMessages) + + return params, nil +} + +// SetChatMemberTagConfig sets the custom tag for a chat member. +type SetChatMemberTagConfig struct { + ChatMemberConfig + Tag string +} + +func (SetChatMemberTagConfig) method() string { + return "setChatMemberTag" +} + +func (config SetChatMemberTagConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername, config.ChannelUsername); err != nil { + return params, err + } + params.AddNonZero64("user_id", config.UserID) + params.AddNonEmpty("tag", config.Tag) return params, nil } @@ -1424,7 +2062,9 @@ func (SetChatAdministratorCustomTitle) method() string { func (config SetChatAdministratorCustomTitle) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername, config.ChannelUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername, config.ChannelUsername); err != nil { + return params, err + } params.AddNonZero64("user_id", config.UserID) params.AddNonEmpty("custom_title", config.CustomTitle) @@ -1440,7 +2080,10 @@ type BanChatSenderChatConfig struct { ChatID int64 ChannelUsername string SenderChatID int64 - UntilDate int + // Deprecated: banChatSenderChat no longer accepts until_date; sender chats + // are banned until they are explicitly unbanned. The value is ignored by + // Telegram. + UntilDate int } func (config BanChatSenderChatConfig) method() string { @@ -1450,7 +2093,9 @@ func (config BanChatSenderChatConfig) method() string { func (config BanChatSenderChatConfig) params() (Params, error) { params := make(Params) - _ = params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } params.AddNonZero64("sender_chat_id", config.SenderChatID) params.AddNonZero("until_date", config.UntilDate) @@ -1473,7 +2118,9 @@ func (config UnbanChatSenderChatConfig) method() string { func (config UnbanChatSenderChatConfig) params() (Params, error) { params := make(Params) - _ = params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } params.AddNonZero64("sender_chat_id", config.SenderChatID) return params, nil @@ -1488,7 +2135,9 @@ type ChatConfig struct { func (config ChatConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } return params, nil } @@ -1508,24 +2157,37 @@ type ChatMemberCountConfig struct { } func (ChatMemberCountConfig) method() string { - return "getChatMembersCount" + return "getChatMemberCount" } // ChatAdministratorsConfig contains information about getting chat administrators. type ChatAdministratorsConfig struct { ChatConfig + // ReturnBots, if true, additionally returns bots that are administrators + // of the chat. By default, bots other than the current bot are omitted. + ReturnBots bool } func (ChatAdministratorsConfig) method() string { return "getChatAdministrators" } +func (config ChatAdministratorsConfig) params() (Params, error) { + params, err := config.ChatConfig.params() + if err != nil { + return params, err + } + params.AddBool("return_bots", config.ReturnBots) + return params, nil +} + // SetChatPermissionsConfig allows you to set default permissions for the // members in a group. The bot must be an administrator and have rights to // restrict members. type SetChatPermissionsConfig struct { ChatConfig - Permissions *ChatPermissions + Permissions *ChatPermissions + UseIndependentChatPermissions bool } func (SetChatPermissionsConfig) method() string { @@ -1535,8 +2197,11 @@ func (SetChatPermissionsConfig) method() string { func (config SetChatPermissionsConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } err := params.AddInterface("permissions", config.Permissions) + params.AddBool("use_independent_chat_permissions", config.UseIndependentChatPermissions) return params, err } @@ -1555,7 +2220,9 @@ func (ChatInviteLinkConfig) method() string { func (config ChatInviteLinkConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } return params, nil } @@ -1580,7 +2247,9 @@ func (config CreateChatInviteLinkConfig) params() (Params, error) { params := make(Params) params.AddNonEmpty("name", config.Name) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } params.AddNonZero("expire_date", config.ExpireDate) params.AddNonZero("member_limit", config.MemberLimit) params.AddBool("creates_join_request", config.CreatesJoinRequest) @@ -1607,7 +2276,9 @@ func (EditChatInviteLinkConfig) method() string { func (config EditChatInviteLinkConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } params.AddNonEmpty("name", config.Name) params["invite_link"] = config.InviteLink params.AddNonZero("expire_date", config.ExpireDate) @@ -1617,6 +2288,58 @@ func (config EditChatInviteLinkConfig) params() (Params, error) { return params, nil } +// CreateChatSubscriptionInviteLinkConfig creates a subscription invite link +// for a channel chat. The bot must have the can_invite_users administrator +// rights. The link can be edited using EditChatSubscriptionInviteLinkConfig +// or revoked using RevokeChatInviteLinkConfig. +type CreateChatSubscriptionInviteLinkConfig struct { + ChatConfig + Name string + SubscriptionPeriod int // required, in seconds; currently must be 2592000 (30 days) + SubscriptionPrice int // required, 1-10000 Telegram Stars +} + +func (CreateChatSubscriptionInviteLinkConfig) method() string { + return "createChatSubscriptionInviteLink" +} + +func (config CreateChatSubscriptionInviteLinkConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } + params.AddNonEmpty("name", config.Name) + params.AddNonZero("subscription_period", config.SubscriptionPeriod) + params.AddNonZero("subscription_price", config.SubscriptionPrice) + + return params, nil +} + +// EditChatSubscriptionInviteLinkConfig edits a subscription invite link +// created by the bot. Only the Name field can be edited. +type EditChatSubscriptionInviteLinkConfig struct { + ChatConfig + InviteLink string + Name string +} + +func (EditChatSubscriptionInviteLinkConfig) method() string { + return "editChatSubscriptionInviteLink" +} + +func (config EditChatSubscriptionInviteLinkConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } + params["invite_link"] = config.InviteLink + params.AddNonEmpty("name", config.Name) + + return params, nil +} + // RevokeChatInviteLinkConfig allows you to revoke an invite link created by the // bot. If the primary link is revoked, a new link is automatically generated. // The bot must be an administrator in the chat for this to work and must have @@ -1633,7 +2356,9 @@ func (RevokeChatInviteLinkConfig) method() string { func (config RevokeChatInviteLinkConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } params["invite_link"] = config.InviteLink return params, nil @@ -1652,7 +2377,9 @@ func (ApproveChatJoinRequestConfig) method() string { func (config ApproveChatJoinRequestConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } params.AddNonZero("user_id", int(config.UserID)) return params, nil @@ -1671,7 +2398,9 @@ func (DeclineChatJoinRequest) method() string { func (config DeclineChatJoinRequest) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } params.AddNonZero("user_id", int(config.UserID)) return params, nil @@ -1690,7 +2419,9 @@ func (config LeaveChatConfig) method() string { func (config LeaveChatConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } return params, nil } @@ -1705,7 +2436,9 @@ type ChatConfigWithUser struct { func (config ChatConfigWithUser) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } params.AddNonZero64("user_id", config.UserID) return params, nil @@ -1726,13 +2459,13 @@ type InvoiceConfig struct { Title string // required Description string // required Payload string // required - ProviderToken string // required - Currency string // required + ProviderToken string // omit for payments in Telegram Stars + Currency string // required ("XTR" for Telegram Stars) Prices []LabeledPrice // required MaxTipAmount int SuggestedTipAmounts []int StartParameter string - ProviderData string + ProviderData json.RawMessage PhotoURL string PhotoSize int PhotoWidth int @@ -1755,16 +2488,18 @@ func (config InvoiceConfig) params() (Params, error) { params["title"] = config.Title params["description"] = config.Description params["payload"] = config.Payload - params["provider_token"] = config.ProviderToken + params.AddNonEmpty("provider_token", config.ProviderToken) params["currency"] = config.Currency - if err = params.AddInterface("prices", config.Prices); err != nil { + if err = params.AddAny("prices", config.Prices); err != nil { return params, err } params.AddNonZero("max_tip_amount", config.MaxTipAmount) - err = params.AddInterface("suggested_tip_amounts", config.SuggestedTipAmounts) + err = params.AddAny("suggested_tip_amounts", config.SuggestedTipAmounts) params.AddNonEmpty("start_parameter", config.StartParameter) - params.AddNonEmpty("provider_data", config.ProviderData) + if len(config.ProviderData) > 0 { + params["provider_data"] = string(config.ProviderData) + } params.AddNonEmpty("photo_url", config.PhotoURL) params.AddNonZero("photo_size", config.PhotoSize) params.AddNonZero("photo_width", config.PhotoWidth) @@ -1784,558 +2519,2666 @@ func (config InvoiceConfig) method() string { return "sendInvoice" } -// ShippingConfig contains information for answerShippingQuery request. -type ShippingConfig struct { - ShippingQueryID string // required - OK bool // required - ShippingOptions []ShippingOption - ErrorMessage string -} - -func (config ShippingConfig) method() string { - return "answerShippingQuery" +// InvoiceLinkConfig contains information for createInvoiceLink request. +type InvoiceLinkConfig struct { + BusinessConnectionID string + Title string // required + Description string // required + Payload string // required + ProviderToken string // omit for payments in Telegram Stars + Currency string // required ("XTR" for Telegram Stars) + Prices []LabeledPrice // required + SubscriptionPeriod int // in seconds; currently must be 2592000 (30 days) if set + MaxTipAmount int + SuggestedTipAmounts []int + ProviderData json.RawMessage + PhotoURL string + PhotoSize int + PhotoWidth int + PhotoHeight int + NeedName bool + NeedPhoneNumber bool + NeedEmail bool + NeedShippingAddress bool + SendPhoneNumberToProvider bool + SendEmailToProvider bool + IsFlexible bool } -func (config ShippingConfig) params() (Params, error) { +func (config InvoiceLinkConfig) params() (Params, error) { params := make(Params) - params["shipping_query_id"] = config.ShippingQueryID - params.AddBool("ok", config.OK) - err := params.AddInterface("shipping_options", config.ShippingOptions) - params.AddNonEmpty("error_message", config.ErrorMessage) + params.AddNonEmpty("business_connection_id", config.BusinessConnectionID) + params["title"] = config.Title + params["description"] = config.Description + params["payload"] = config.Payload + params.AddNonEmpty("provider_token", config.ProviderToken) + params["currency"] = config.Currency + params.AddNonZero("subscription_period", config.SubscriptionPeriod) + if err := params.AddAny("prices", config.Prices); err != nil { + return params, err + } - return params, err -} + params.AddNonZero("max_tip_amount", config.MaxTipAmount) + if err := params.AddAny("suggested_tip_amounts", config.SuggestedTipAmounts); err != nil { + return params, err + } + if len(config.ProviderData) > 0 { + params["provider_data"] = string(config.ProviderData) + } + params.AddNonEmpty("photo_url", config.PhotoURL) + params.AddNonZero("photo_size", config.PhotoSize) + params.AddNonZero("photo_width", config.PhotoWidth) + params.AddNonZero("photo_height", config.PhotoHeight) + params.AddBool("need_name", config.NeedName) + params.AddBool("need_phone_number", config.NeedPhoneNumber) + params.AddBool("need_email", config.NeedEmail) + params.AddBool("need_shipping_address", config.NeedShippingAddress) + params.AddBool("send_phone_number_to_provider", config.SendPhoneNumberToProvider) + params.AddBool("send_email_to_provider", config.SendEmailToProvider) + params.AddBool("is_flexible", config.IsFlexible) -// PreCheckoutConfig contains information for answerPreCheckoutQuery request. -type PreCheckoutConfig struct { - PreCheckoutQueryID string // required - OK bool // required - ErrorMessage string + return params, nil } -func (config PreCheckoutConfig) method() string { - return "answerPreCheckoutQuery" +func (config InvoiceLinkConfig) method() string { + return "createInvoiceLink" } -func (config PreCheckoutConfig) params() (Params, error) { - params := make(Params) +// GetAvailableGiftsConfig returns the list of gifts that can be sent by the +// bot to users. +type GetAvailableGiftsConfig struct{} - params["pre_checkout_query_id"] = config.PreCheckoutQueryID - params.AddBool("ok", config.OK) - params.AddNonEmpty("error_message", config.ErrorMessage) +func (GetAvailableGiftsConfig) method() string { + return "getAvailableGifts" +} - return params, nil +func (GetAvailableGiftsConfig) params() (Params, error) { + return make(Params), nil } -// DeleteMessageConfig contains information of a message in a chat to delete. -type DeleteMessageConfig struct { - ChannelUsername string +// SendGiftConfig sends a gift to a user or channel chat. +// +// Provide the recipient via either UserID (for a user) or ChatID / +// ChannelUsername (for a channel chat); exactly one of UserID or the chat +// identifier should be set. +type SendGiftConfig struct { + UserID int64 ChatID int64 - MessageID int + ChannelUsername string + GiftID string + PayForUpgrade bool + Text string + TextParseMode string + TextEntities []MessageEntity } -func (config DeleteMessageConfig) method() string { - return "deleteMessage" +func (SendGiftConfig) method() string { + return "sendGift" } -func (config DeleteMessageConfig) params() (Params, error) { +func (config SendGiftConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) - params.AddNonZero("message_id", config.MessageID) + params.AddNonZero64("user_id", config.UserID) + if config.ChatID != 0 || config.ChannelUsername != "" { + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } + } + params["gift_id"] = config.GiftID + params.AddBool("pay_for_upgrade", config.PayForUpgrade) + params.AddNonEmpty("text", config.Text) + params.AddNonEmpty("text_parse_mode", config.TextParseMode) + err := params.AddAny("text_entities", config.TextEntities) - return params, nil + return params, err } -// PinChatMessageConfig contains information of a message in a chat to pin. -type PinChatMessageConfig struct { - ChatID int64 - ChannelUsername string - MessageID int - DisableNotification bool +// VerifyUserConfig verifies a user on behalf of the organization which is +// represented by the bot. +type VerifyUserConfig struct { + UserID int64 + CustomDescription string } -func (config PinChatMessageConfig) method() string { - return "pinChatMessage" +func (VerifyUserConfig) method() string { + return "verifyUser" } -func (config PinChatMessageConfig) params() (Params, error) { +func (config VerifyUserConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) - params.AddNonZero("message_id", config.MessageID) - params.AddBool("disable_notification", config.DisableNotification) + params.AddNonZero64("user_id", config.UserID) + params.AddNonEmpty("custom_description", config.CustomDescription) return params, nil } -// UnpinChatMessageConfig contains information of a chat message to unpin. +// VerifyChatConfig verifies a chat on behalf of the organization which is +// represented by the bot. // -// If MessageID is not specified, it will unpin the most recent pin. -type UnpinChatMessageConfig struct { - ChatID int64 - ChannelUsername string - MessageID int +// Provide the target chat via either ChatID (numeric identifier) or +// ChannelUsername ("@channelusername"); the first non-zero / non-empty +// value is used. +type VerifyChatConfig struct { + ChatID int64 + ChannelUsername string + CustomDescription string } -func (config UnpinChatMessageConfig) method() string { - return "unpinChatMessage" +func (VerifyChatConfig) method() string { + return "verifyChat" } -func (config UnpinChatMessageConfig) params() (Params, error) { +func (config VerifyChatConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) - params.AddNonZero("message_id", config.MessageID) + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } + params.AddNonEmpty("custom_description", config.CustomDescription) return params, nil } -// UnpinAllChatMessagesConfig contains information of all messages to unpin in -// a chat. -type UnpinAllChatMessagesConfig struct { - ChatID int64 - ChannelUsername string +// RemoveUserVerificationConfig removes verification from a user who is +// currently verified on behalf of the organization represented by the bot. +type RemoveUserVerificationConfig struct { + UserID int64 } -func (config UnpinAllChatMessagesConfig) method() string { - return "unpinAllChatMessages" +func (RemoveUserVerificationConfig) method() string { + return "removeUserVerification" } -func (config UnpinAllChatMessagesConfig) params() (Params, error) { +func (config RemoveUserVerificationConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) + params.AddNonZero64("user_id", config.UserID) return params, nil } -// SetChatPhotoConfig allows you to set a group, supergroup, or channel's photo. -type SetChatPhotoConfig struct { - BaseFile -} - -func (config SetChatPhotoConfig) method() string { - return "setChatPhoto" -} - -func (config SetChatPhotoConfig) files() []RequestFile { - return []RequestFile{{ - Name: "photo", - Data: config.File, - }} -} - -// DeleteChatPhotoConfig allows you to delete a group, supergroup, or channel's photo. -type DeleteChatPhotoConfig struct { +// RemoveChatVerificationConfig removes verification from a chat that is +// currently verified on behalf of the organization represented by the bot. +// +// Provide the target chat via either ChatID (numeric identifier) or +// ChannelUsername ("@channelusername"); the first non-zero / non-empty +// value is used. +type RemoveChatVerificationConfig struct { ChatID int64 ChannelUsername string } -func (config DeleteChatPhotoConfig) method() string { - return "deleteChatPhoto" +func (RemoveChatVerificationConfig) method() string { + return "removeChatVerification" } -func (config DeleteChatPhotoConfig) params() (Params, error) { +func (config RemoveChatVerificationConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } return params, nil } -// SetChatTitleConfig allows you to set the title of something other than a private chat. -type SetChatTitleConfig struct { - ChatID int64 - ChannelUsername string - - Title string +// SetUserEmojiStatusConfig changes the emoji status for a given user that +// previously allowed the bot to manage their emoji status. +type SetUserEmojiStatusConfig struct { + UserID int64 + EmojiStatusCustomEmojiID string + EmojiStatusExpirationDate int } -func (config SetChatTitleConfig) method() string { - return "setChatTitle" +func (SetUserEmojiStatusConfig) method() string { + return "setUserEmojiStatus" } -func (config SetChatTitleConfig) params() (Params, error) { +func (config SetUserEmojiStatusConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) - params["title"] = config.Title + params.AddNonZero64("user_id", config.UserID) + params.AddNonEmpty("emoji_status_custom_emoji_id", config.EmojiStatusCustomEmojiID) + params.AddNonZero("emoji_status_expiration_date", config.EmojiStatusExpirationDate) return params, nil } -// SetChatDescriptionConfig allows you to set the description of a supergroup or channel. -type SetChatDescriptionConfig struct { - ChatID int64 - ChannelUsername string - - Description string +// EditUserStarSubscriptionConfig cancels or reenables an active Telegram +// Star subscription. +type EditUserStarSubscriptionConfig struct { + UserID int64 + TelegramPaymentChargeID string + IsCanceled bool } -func (config SetChatDescriptionConfig) method() string { - return "setChatDescription" +func (EditUserStarSubscriptionConfig) method() string { + return "editUserStarSubscription" } -func (config SetChatDescriptionConfig) params() (Params, error) { +func (config EditUserStarSubscriptionConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) - params["description"] = config.Description + params.AddNonZero64("user_id", config.UserID) + params["telegram_payment_charge_id"] = config.TelegramPaymentChargeID + params.AddBool("is_canceled", config.IsCanceled) return params, nil } -// GetStickerSetConfig allows you to get the stickers in a set. -type GetStickerSetConfig struct { - Name string +// SavePreparedInlineMessageConfig stores a message that can be sent by a +// user of a Mini App. +type SavePreparedInlineMessageConfig struct { + UserID int64 + Result interface{} // InlineQueryResult + AllowUserChats bool + AllowBotChats bool + AllowGroupChats bool + AllowChannelChats bool } -func (config GetStickerSetConfig) method() string { - return "getStickerSet" +func (SavePreparedInlineMessageConfig) method() string { + return "savePreparedInlineMessage" } -func (config GetStickerSetConfig) params() (Params, error) { +func (config SavePreparedInlineMessageConfig) params() (Params, error) { params := make(Params) - params["name"] = config.Name + params.AddNonZero64("user_id", config.UserID) + if err := params.AddAny("result", config.Result); err != nil { + return params, err + } + params.AddBool("allow_user_chats", config.AllowUserChats) + params.AddBool("allow_bot_chats", config.AllowBotChats) + params.AddBool("allow_group_chats", config.AllowGroupChats) + params.AddBool("allow_channel_chats", config.AllowChannelChats) return params, nil } -// UploadStickerConfig allows you to upload a sticker for use in a set later. -type UploadStickerConfig struct { - UserID int64 - PNGSticker RequestFileData +// GetStarTransactionsConfig returns the bot's Telegram Star transactions in +// chronological order. +type GetStarTransactionsConfig struct { + // Offset is the number of transactions to skip in the response. + Offset int + // Limit is the maximum number of transactions to be retrieved; 1-100. + // Defaults to 100. + Limit int } -func (config UploadStickerConfig) method() string { - return "uploadStickerFile" +func (config GetStarTransactionsConfig) method() string { + return "getStarTransactions" } -func (config UploadStickerConfig) params() (Params, error) { +func (config GetStarTransactionsConfig) params() (Params, error) { params := make(Params) - params.AddNonZero64("user_id", config.UserID) + params.AddNonZero("offset", config.Offset) + params.AddNonZero("limit", config.Limit) return params, nil } -func (config UploadStickerConfig) files() []RequestFile { - return []RequestFile{{ - Name: "png_sticker", - Data: config.PNGSticker, - }} -} - -// NewStickerSetConfig allows creating a new sticker set. -// -// You must set either PNGSticker or TGSSticker. -type NewStickerSetConfig struct { - UserID int64 - Name string - Title string - PNGSticker RequestFileData - TGSSticker RequestFileData - Emojis string - ContainsMasks bool - MaskPosition *MaskPosition +// RefundStarPaymentConfig refunds a successful payment in Telegram Stars. +type RefundStarPaymentConfig struct { + UserID int64 + TelegramPaymentChargeID string } -func (config NewStickerSetConfig) method() string { - return "createNewStickerSet" +func (config RefundStarPaymentConfig) method() string { + return "refundStarPayment" } -func (config NewStickerSetConfig) params() (Params, error) { +func (config RefundStarPaymentConfig) params() (Params, error) { params := make(Params) params.AddNonZero64("user_id", config.UserID) - params["name"] = config.Name - params["title"] = config.Title + params["telegram_payment_charge_id"] = config.TelegramPaymentChargeID + + return params, nil +} + +// ShippingConfig contains information for answerShippingQuery request. +type ShippingConfig struct { + ShippingQueryID string // required + OK bool // required + ShippingOptions []ShippingOption + ErrorMessage string +} - params["emojis"] = config.Emojis +func (config ShippingConfig) method() string { + return "answerShippingQuery" +} - params.AddBool("contains_masks", config.ContainsMasks) +func (config ShippingConfig) params() (Params, error) { + params := make(Params) - err := params.AddInterface("mask_position", config.MaskPosition) + params["shipping_query_id"] = config.ShippingQueryID + params.AddBool("ok", config.OK) + err := params.AddInterface("shipping_options", config.ShippingOptions) + params.AddNonEmpty("error_message", config.ErrorMessage) return params, err } -func (config NewStickerSetConfig) files() []RequestFile { - if config.PNGSticker != nil { - return []RequestFile{{ - Name: "png_sticker", - Data: config.PNGSticker, - }} - } +// PreCheckoutConfig contains information for answerPreCheckoutQuery request. +type PreCheckoutConfig struct { + PreCheckoutQueryID string // required + OK bool // required + ErrorMessage string +} - return []RequestFile{{ - Name: "tgs_sticker", - Data: config.TGSSticker, - }} +func (config PreCheckoutConfig) method() string { + return "answerPreCheckoutQuery" } -// AddStickerConfig allows you to add a sticker to a set. -type AddStickerConfig struct { - UserID int64 - Name string - PNGSticker RequestFileData - TGSSticker RequestFileData - Emojis string - MaskPosition *MaskPosition +func (config PreCheckoutConfig) params() (Params, error) { + params := make(Params) + + params["pre_checkout_query_id"] = config.PreCheckoutQueryID + params.AddBool("ok", config.OK) + params.AddNonEmpty("error_message", config.ErrorMessage) + + return params, nil } -func (config AddStickerConfig) method() string { - return "addStickerToSet" +// DeleteMessageConfig contains information of a message in a chat to delete. +type DeleteMessageConfig struct { + ChannelUsername string + ChatID int64 + MessageID int } -func (config AddStickerConfig) params() (Params, error) { +func (config DeleteMessageConfig) method() string { + return "deleteMessage" +} + +func (config DeleteMessageConfig) params() (Params, error) { params := make(Params) - params.AddNonZero64("user_id", config.UserID) - params["name"] = config.Name - params["emojis"] = config.Emojis + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } + params.AddNonZero("message_id", config.MessageID) - err := params.AddInterface("mask_position", config.MaskPosition) + return params, nil +} - return params, err +// DeleteMessagesConfig deletes multiple messages simultaneously. If some of +// the specified messages can't be found, they are skipped. +type DeleteMessagesConfig struct { + ChatID int64 + ChannelUsername string + MessageIDs []int // 1-100 message identifiers } -func (config AddStickerConfig) files() []RequestFile { - if config.PNGSticker != nil { - return []RequestFile{{ - Name: "png_sticker", - Data: config.PNGSticker, - }} +func (config DeleteMessagesConfig) method() string { + return "deleteMessages" +} + +func (config DeleteMessagesConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err } + err := params.AddAny("message_ids", config.MessageIDs) - return []RequestFile{{ - Name: "tgs_sticker", - Data: config.TGSSticker, - }} + return params, err +} +// ForwardMessagesConfig forwards multiple messages of any kind. If some of the +// specified messages can't be found or forwarded, they are skipped. Service +// messages and messages with protected content can't be forwarded. Album +// grouping is kept for forwarded messages. +type ForwardMessagesConfig struct { + ChatID int64 + ChannelUsername string + MessageThreadID int + DirectMessagesTopicID int + FromChatID int64 + FromChannelUsername string + MessageIDs []int // 1-100 message identifiers, must be in strictly increasing order + DisableNotification bool + ProtectContent bool } -// SetStickerPositionConfig allows you to change the position of a sticker in a set. -type SetStickerPositionConfig struct { - Sticker string - Position int +func (config ForwardMessagesConfig) method() string { + return "forwardMessages" } -func (config SetStickerPositionConfig) method() string { - return "setStickerPositionInSet" +func (config ForwardMessagesConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } + if err := params.AddFirstValid("from_chat_id", config.FromChatID, config.FromChannelUsername); err != nil { + return params, err + } + params.AddNonZero("message_thread_id", config.MessageThreadID) + params.AddNonZero("direct_messages_topic_id", config.DirectMessagesTopicID) + params.AddBool("disable_notification", config.DisableNotification) + params.AddBool("protect_content", config.ProtectContent) + err := params.AddAny("message_ids", config.MessageIDs) + + return params, err } -func (config SetStickerPositionConfig) params() (Params, error) { +// CopyMessagesConfig copies messages of any kind. If some of the specified +// messages can't be found or copied, they are skipped. Service messages, +// giveaway messages, giveaway winners messages, and invoice messages can't +// be copied. Album grouping is kept for copied messages. +type CopyMessagesConfig struct { + ChatID int64 + ChannelUsername string + MessageThreadID int + DirectMessagesTopicID int + FromChatID int64 + FromChannelUsername string + MessageIDs []int // 1-100 message identifiers, must be in strictly increasing order + DisableNotification bool + ProtectContent bool + RemoveCaption bool +} + +func (config CopyMessagesConfig) method() string { + return "copyMessages" +} + +func (config CopyMessagesConfig) params() (Params, error) { params := make(Params) - params["sticker"] = config.Sticker - params.AddNonZero("position", config.Position) + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } + if err := params.AddFirstValid("from_chat_id", config.FromChatID, config.FromChannelUsername); err != nil { + return params, err + } + params.AddNonZero("message_thread_id", config.MessageThreadID) + params.AddNonZero("direct_messages_topic_id", config.DirectMessagesTopicID) + params.AddBool("disable_notification", config.DisableNotification) + params.AddBool("protect_content", config.ProtectContent) + params.AddBool("remove_caption", config.RemoveCaption) + err := params.AddAny("message_ids", config.MessageIDs) - return params, nil + return params, err } -// DeleteStickerConfig allows you to delete a sticker from a set. -type DeleteStickerConfig struct { - Sticker string +// ApproveSuggestedPostConfig approves a suggested post in a direct messages +// chat. +type ApproveSuggestedPostConfig struct { + ChatID int64 + MessageID int + SendDate int } -func (config DeleteStickerConfig) method() string { - return "deleteStickerFromSet" +func (ApproveSuggestedPostConfig) method() string { + return "approveSuggestedPost" } -func (config DeleteStickerConfig) params() (Params, error) { +func (config ApproveSuggestedPostConfig) params() (Params, error) { params := make(Params) - params["sticker"] = config.Sticker + params.AddNonZero64("chat_id", config.ChatID) + params.AddNonZero("message_id", config.MessageID) + params.AddNonZero("send_date", config.SendDate) return params, nil } -// SetStickerSetThumbConfig allows you to set the thumbnail for a sticker set. -type SetStickerSetThumbConfig struct { - Name string - UserID int64 - Thumb RequestFileData +// DeclineSuggestedPostConfig declines a suggested post in a direct messages +// chat. +type DeclineSuggestedPostConfig struct { + ChatID int64 + MessageID int + Comment string } -func (config SetStickerSetThumbConfig) method() string { - return "setStickerSetThumb" +func (DeclineSuggestedPostConfig) method() string { + return "declineSuggestedPost" } -func (config SetStickerSetThumbConfig) params() (Params, error) { +func (config DeclineSuggestedPostConfig) params() (Params, error) { params := make(Params) - params["name"] = config.Name - params.AddNonZero64("user_id", config.UserID) + params.AddNonZero64("chat_id", config.ChatID) + params.AddNonZero("message_id", config.MessageID) + params.AddNonEmpty("comment", config.Comment) return params, nil } -func (config SetStickerSetThumbConfig) files() []RequestFile { - return []RequestFile{{ - Name: "thumb", - Data: config.Thumb, - }} +// SetMessageReactionConfig sets the bot's reaction to a message. Pass an +// empty Reaction to remove all reactions from the message. +type SetMessageReactionConfig struct { + ChatID int64 + ChannelUsername string + MessageID int + Reaction []ReactionType + IsBig bool } -// SetChatStickerSetConfig allows you to set the sticker set for a supergroup. -type SetChatStickerSetConfig struct { - ChatID int64 - SuperGroupUsername string +func (config SetMessageReactionConfig) method() string { + return "setMessageReaction" +} - StickerSetName string +func (config SetMessageReactionConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } + params.AddNonZero("message_id", config.MessageID) + params.AddBool("is_big", config.IsBig) + + err := params.AddAny("reaction", config.Reaction) + + return params, err } -func (config SetChatStickerSetConfig) method() string { - return "setChatStickerSet" +// DeleteMessageReactionConfig removes a reaction from a message in a group +// or supergroup. The bot must have the can_delete_messages administrator +// right. Either UserID or ActorChatID identifies whose reaction is removed. +type DeleteMessageReactionConfig struct { + ChatID int64 + ChannelUsername string + MessageID int + UserID int64 + ActorChatID int64 } -func (config SetChatStickerSetConfig) params() (Params, error) { +func (config DeleteMessageReactionConfig) method() string { + return "deleteMessageReaction" +} + +func (config DeleteMessageReactionConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) - params["sticker_set_name"] = config.StickerSetName + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } + params.AddNonZero("message_id", config.MessageID) + params.AddNonZero64("user_id", config.UserID) + params.AddNonZero64("actor_chat_id", config.ActorChatID) return params, nil } -// DeleteChatStickerSetConfig allows you to remove a supergroup's sticker set. -type DeleteChatStickerSetConfig struct { - ChatID int64 - SuperGroupUsername string +// DeleteAllMessageReactionsConfig removes up to 10000 recent reactions in a +// group or supergroup added by a given user or chat. The bot must have the +// can_delete_messages administrator right. Either UserID or ActorChatID +// identifies whose reactions are removed. +type DeleteAllMessageReactionsConfig struct { + ChatID int64 + ChannelUsername string + UserID int64 + ActorChatID int64 } -func (config DeleteChatStickerSetConfig) method() string { - return "deleteChatStickerSet" +func (config DeleteAllMessageReactionsConfig) method() string { + return "deleteAllMessageReactions" } -func (config DeleteChatStickerSetConfig) params() (Params, error) { +func (config DeleteAllMessageReactionsConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } + params.AddNonZero64("user_id", config.UserID) + params.AddNonZero64("actor_chat_id", config.ActorChatID) return params, nil } -// MediaGroupConfig allows you to send a group of media. +// GetUserChatBoostsConfig returns the list of boosts added to a chat by a +// user. The bot must be an administrator in the chat. // -// Media consist of InputMedia items (InputMediaPhoto, InputMediaVideo). -type MediaGroupConfig struct { +// Provide the target chat via either ChatID (numeric identifier) or +// ChannelUsername ("@channelusername"); the first non-zero / non-empty +// value is used. +type GetUserChatBoostsConfig struct { ChatID int64 ChannelUsername string + UserID int64 +} - Media []interface{} - DisableNotification bool - ReplyToMessageID int +func (config GetUserChatBoostsConfig) method() string { + return "getUserChatBoosts" } -func (config MediaGroupConfig) method() string { - return "sendMediaGroup" +func (config GetUserChatBoostsConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } + params.AddNonZero64("user_id", config.UserID) + + return params, nil } -func (config MediaGroupConfig) params() (Params, error) { +// PinChatMessageConfig contains information of a message in a chat to pin. +// +// Provide the target chat via either ChatID (numeric identifier) or +// ChannelUsername ("@channelusername"); the first non-zero / non-empty +// value is used. +type PinChatMessageConfig struct { + BusinessConnectionID string + ChatID int64 + ChannelUsername string + MessageID int + DisableNotification bool +} + +func (config PinChatMessageConfig) method() string { + return "pinChatMessage" +} + +func (config PinChatMessageConfig) params() (Params, error) { params := make(Params) - params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) + params.AddNonEmpty("business_connection_id", config.BusinessConnectionID) + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } + params.AddNonZero("message_id", config.MessageID) params.AddBool("disable_notification", config.DisableNotification) - params.AddNonZero("reply_to_message_id", config.ReplyToMessageID) - err := params.AddInterface("media", prepareInputMediaForParams(config.Media)) + return params, nil +} - return params, err +// UnpinChatMessageConfig contains information of a chat message to unpin. +// +// If MessageID is not specified, it will unpin the most recent pin. +// +// Provide the target chat via either ChatID (numeric identifier) or +// ChannelUsername ("@channelusername"); the first non-zero / non-empty +// value is used. +type UnpinChatMessageConfig struct { + BusinessConnectionID string + ChatID int64 + ChannelUsername string + MessageID int } -func (config MediaGroupConfig) files() []RequestFile { - return prepareInputMediaForFiles(config.Media) +func (config UnpinChatMessageConfig) method() string { + return "unpinChatMessage" } -// DiceConfig contains information about a sendDice request. -type DiceConfig struct { - BaseChat - // Emoji on which the dice throw animation is based. - // Currently, must be one of 🎲, 🎯, 🏀, ⚽, 🎳, or 🎰. - // Dice can have values 1-6 for 🎲, 🎯, and 🎳, values 1-5 for 🏀 and ⚽, - // and values 1-64 for 🎰. - // Defaults to “🎲” - Emoji string +func (config UnpinChatMessageConfig) params() (Params, error) { + params := make(Params) + + params.AddNonEmpty("business_connection_id", config.BusinessConnectionID) + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } + params.AddNonZero("message_id", config.MessageID) + + return params, nil +} + +// UnpinAllChatMessagesConfig contains information of all messages to unpin in +// a chat. +type UnpinAllChatMessagesConfig struct { + ChatID int64 + ChannelUsername string +} + +func (config UnpinAllChatMessagesConfig) method() string { + return "unpinAllChatMessages" +} + +func (config UnpinAllChatMessagesConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } + + return params, nil +} + +// CreateForumTopicConfig creates a topic in a forum supergroup chat. +// The bot must have the can_manage_topics administrator rights. +// Returns information about the created topic as a ForumTopic object. +type CreateForumTopicConfig struct { + ChatID int64 + SuperGroupUsername string + Name string // required, 1-128 characters + IconColor int + IconCustomEmojiID string +} + +func (config CreateForumTopicConfig) method() string { + return "createForumTopic" +} + +func (config CreateForumTopicConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } + params["name"] = config.Name + params.AddNonZero("icon_color", config.IconColor) + params.AddNonEmpty("icon_custom_emoji_id", config.IconCustomEmojiID) + + return params, nil +} + +// EditForumTopicConfig edits the name and icon of a topic in a forum +// supergroup chat. The bot must have the can_manage_topics administrator +// rights, unless it is the creator of the topic. +type EditForumTopicConfig struct { + ChatID int64 + SuperGroupUsername string + MessageThreadID int // required + Name string + IconCustomEmojiID string +} + +func (config EditForumTopicConfig) method() string { + return "editForumTopic" +} + +func (config EditForumTopicConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } + params.AddNonZero("message_thread_id", config.MessageThreadID) + params.AddNonEmpty("name", config.Name) + params.AddNonEmpty("icon_custom_emoji_id", config.IconCustomEmojiID) + + return params, nil +} + +// CloseForumTopicConfig closes an open topic in a forum supergroup chat. +// The bot must have the can_manage_topics administrator rights, unless it +// is the creator of the topic. +type CloseForumTopicConfig struct { + ChatID int64 + SuperGroupUsername string + MessageThreadID int // required +} + +func (config CloseForumTopicConfig) method() string { + return "closeForumTopic" +} + +func (config CloseForumTopicConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } + params.AddNonZero("message_thread_id", config.MessageThreadID) + + return params, nil +} + +// ReopenForumTopicConfig reopens a closed topic in a forum supergroup chat. +// The bot must have the can_manage_topics administrator rights, unless it +// is the creator of the topic. +type ReopenForumTopicConfig struct { + ChatID int64 + SuperGroupUsername string + MessageThreadID int // required +} + +func (config ReopenForumTopicConfig) method() string { + return "reopenForumTopic" +} + +func (config ReopenForumTopicConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } + params.AddNonZero("message_thread_id", config.MessageThreadID) + + return params, nil +} + +// DeleteForumTopicConfig deletes a forum topic along with all its messages +// in a forum supergroup chat. The bot must have the can_delete_messages +// administrator rights. +type DeleteForumTopicConfig struct { + ChatID int64 + SuperGroupUsername string + MessageThreadID int // required +} + +func (config DeleteForumTopicConfig) method() string { + return "deleteForumTopic" +} + +func (config DeleteForumTopicConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } + params.AddNonZero("message_thread_id", config.MessageThreadID) + + return params, nil +} + +// UnpinAllForumTopicMessagesConfig clears the list of pinned messages in a +// forum topic. The bot must have the can_pin_messages administrator right +// in the supergroup. +type UnpinAllForumTopicMessagesConfig struct { + ChatID int64 + SuperGroupUsername string + MessageThreadID int // required +} + +func (config UnpinAllForumTopicMessagesConfig) method() string { + return "unpinAllForumTopicMessages" +} + +func (config UnpinAllForumTopicMessagesConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } + params.AddNonZero("message_thread_id", config.MessageThreadID) + + return params, nil +} + +// GetForumTopicIconStickersConfig gets custom emoji stickers, which can be +// used as a forum topic icon by any user. Requires no parameters. +// Returns an Array of Sticker objects. +type GetForumTopicIconStickersConfig struct{} + +func (config GetForumTopicIconStickersConfig) method() string { + return "getForumTopicIconStickers" +} + +func (config GetForumTopicIconStickersConfig) params() (Params, error) { + return make(Params), nil +} + +// EditGeneralForumTopicConfig edits the name of the 'General' topic in a +// forum supergroup chat. The bot must have the can_manage_topics +// administrator rights. +type EditGeneralForumTopicConfig struct { + ChatID int64 + SuperGroupUsername string + Name string // required, 1-128 characters +} + +func (config EditGeneralForumTopicConfig) method() string { + return "editGeneralForumTopic" +} + +func (config EditGeneralForumTopicConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } + params["name"] = config.Name + + return params, nil +} + +// CloseGeneralForumTopicConfig closes an open 'General' topic in a forum +// supergroup chat. The bot must have the can_manage_topics administrator rights. +type CloseGeneralForumTopicConfig struct { + ChatID int64 + SuperGroupUsername string +} + +func (config CloseGeneralForumTopicConfig) method() string { + return "closeGeneralForumTopic" +} + +func (config CloseGeneralForumTopicConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } + + return params, nil +} + +// ReopenGeneralForumTopicConfig reopens a closed 'General' topic in a forum +// supergroup chat. The bot must have the can_manage_topics administrator +// rights. The topic will be automatically unhidden if it was hidden. +type ReopenGeneralForumTopicConfig struct { + ChatID int64 + SuperGroupUsername string +} + +func (config ReopenGeneralForumTopicConfig) method() string { + return "reopenGeneralForumTopic" +} + +func (config ReopenGeneralForumTopicConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } + + return params, nil +} + +// HideGeneralForumTopicConfig hides the 'General' topic in a forum supergroup +// chat. The bot must have the can_manage_topics administrator rights. The +// topic will be automatically closed if it was open. +type HideGeneralForumTopicConfig struct { + ChatID int64 + SuperGroupUsername string +} + +func (config HideGeneralForumTopicConfig) method() string { + return "hideGeneralForumTopic" +} + +func (config HideGeneralForumTopicConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } + + return params, nil +} + +// UnhideGeneralForumTopicConfig unhides the 'General' topic in a forum +// supergroup chat. The bot must have the can_manage_topics administrator rights. +type UnhideGeneralForumTopicConfig struct { + ChatID int64 + SuperGroupUsername string +} + +func (config UnhideGeneralForumTopicConfig) method() string { + return "unhideGeneralForumTopic" +} + +func (config UnhideGeneralForumTopicConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } + + return params, nil +} + +// UnpinAllGeneralForumTopicMessagesConfig clears the list of pinned messages +// in the General forum topic. The bot must be an administrator in the chat +// with the can_pin_messages administrator right in the supergroup. +type UnpinAllGeneralForumTopicMessagesConfig struct { + ChatID int64 + SuperGroupUsername string +} + +func (config UnpinAllGeneralForumTopicMessagesConfig) method() string { + return "unpinAllGeneralForumTopicMessages" +} + +func (config UnpinAllGeneralForumTopicMessagesConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } + + return params, nil +} + +// SetChatPhotoConfig allows you to set a group, supergroup, or channel's photo. +type SetChatPhotoConfig struct { + BaseFile +} + +func (config SetChatPhotoConfig) method() string { + return "setChatPhoto" +} + +func (config SetChatPhotoConfig) files() []RequestFile { + return []RequestFile{{ + Name: "photo", + Data: config.File, + }} +} + +// DeleteChatPhotoConfig allows you to delete a group, supergroup, or channel's photo. +type DeleteChatPhotoConfig struct { + ChatID int64 + ChannelUsername string +} + +func (config DeleteChatPhotoConfig) method() string { + return "deleteChatPhoto" +} + +func (config DeleteChatPhotoConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } + + return params, nil +} + +// SetChatTitleConfig allows you to set the title of something other than a private chat. +type SetChatTitleConfig struct { + ChatID int64 + ChannelUsername string + + Title string +} + +func (config SetChatTitleConfig) method() string { + return "setChatTitle" +} + +func (config SetChatTitleConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } + params["title"] = config.Title + + return params, nil +} + +// SetChatDescriptionConfig allows you to set the description of a supergroup or channel. +type SetChatDescriptionConfig struct { + ChatID int64 + ChannelUsername string + + Description string +} + +func (config SetChatDescriptionConfig) method() string { + return "setChatDescription" +} + +func (config SetChatDescriptionConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } + params["description"] = config.Description + + return params, nil +} + +// GetStickerSetConfig allows you to get the stickers in a set. +type GetStickerSetConfig struct { + Name string +} + +func (config GetStickerSetConfig) method() string { + return "getStickerSet" +} + +func (config GetStickerSetConfig) params() (Params, error) { + params := make(Params) + + params["name"] = config.Name + + return params, nil +} + +// GetCustomEmojiStickersConfig get information about custom emoji stickers +// by their identifiers. +type GetCustomEmojiStickersConfig struct { + CustomEmojiIDs []string +} + +func (config GetCustomEmojiStickersConfig) method() string { + return "getCustomEmojiStickers" +} + +func (config GetCustomEmojiStickersConfig) params() (Params, error) { + params := make(Params) + + err := params.AddInterface("custom_emoji_ids", config.CustomEmojiIDs) + + return params, err +} + +// UploadStickerConfig uploads a sticker file for later use in a sticker set. +type UploadStickerConfig struct { + UserID int64 + Sticker RequestFileData // required + StickerFormat string // required, one of StickerFormatStatic, StickerFormatAnimated, StickerFormatVideo +} + +func (config UploadStickerConfig) method() string { + return "uploadStickerFile" +} + +func (config UploadStickerConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("user_id", config.UserID) + params.AddNonEmpty("sticker_format", config.StickerFormat) + + return params, nil +} + +func (config UploadStickerConfig) files() []RequestFile { + return []RequestFile{{ + Name: "sticker", + Data: config.Sticker, + }} +} + +// NewStickerSetConfig creates a new sticker set owned by a user. +// +// Each sticker's format is specified on the InputSticker itself via its +// Format field, allowing mixed-format sticker packs. +type NewStickerSetConfig struct { + UserID int64 + Name string + Title string + Stickers []InputSticker + StickerType string // one of StickerTypeRegular, StickerTypeMask, StickerTypeCustomEmoji + NeedsRepainting bool +} + +func (config NewStickerSetConfig) method() string { + return "createNewStickerSet" +} + +func (config NewStickerSetConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("user_id", config.UserID) + params["name"] = config.Name + params["title"] = config.Title + params.AddNonEmpty("sticker_type", config.StickerType) + params.AddBool("needs_repainting", config.NeedsRepainting) + + err := params.AddAny("stickers", prepareInputStickersForParams(config.Stickers)) + + return params, err +} + +func (config NewStickerSetConfig) files() []RequestFile { + return prepareInputStickersForFiles(config.Stickers) +} + +// AddStickerConfig adds a new sticker to an existing sticker set. +type AddStickerConfig struct { + UserID int64 + Name string + Sticker InputSticker +} + +func (config AddStickerConfig) method() string { + return "addStickerToSet" +} + +func (config AddStickerConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("user_id", config.UserID) + params["name"] = config.Name + + err := params.AddAny("sticker", prepareInputStickerForParams(config.Sticker, 0)) + + return params, err +} + +func (config AddStickerConfig) files() []RequestFile { + return prepareInputStickerForFiles(config.Sticker, 0) +} + +// prepareInputStickerForParams returns a copy of the sticker with the +// Sticker field replaced by an attach:// reference if it needs uploading. +func prepareInputStickerForParams(s InputSticker, idx int) InputSticker { + if s.Sticker != nil && s.Sticker.NeedsUpload() { + s.Sticker = fileAttach(fmt.Sprintf("attach://sticker-%d", idx)) + } + return s +} + +// prepareInputStickerForFiles returns the upload entries for a single sticker. +func prepareInputStickerForFiles(s InputSticker, idx int) []RequestFile { + if s.Sticker != nil && s.Sticker.NeedsUpload() { + return []RequestFile{{ + Name: fmt.Sprintf("sticker-%d", idx), + Data: s.Sticker, + }} + } + return nil +} + +// prepareInputStickersForParams applies prepareInputStickerForParams to a slice. +func prepareInputStickersForParams(stickers []InputSticker) []InputSticker { + out := make([]InputSticker, len(stickers)) + for i, s := range stickers { + out[i] = prepareInputStickerForParams(s, i) + } + return out +} + +// prepareInputStickersForFiles flattens the upload entries for a slice of stickers. +func prepareInputStickersForFiles(stickers []InputSticker) []RequestFile { + var files []RequestFile + for i, s := range stickers { + files = append(files, prepareInputStickerForFiles(s, i)...) + } + return files +} + +// SetStickerPositionConfig allows you to change the position of a sticker in a set. +type SetStickerPositionConfig struct { + Sticker string + Position int +} + +func (config SetStickerPositionConfig) method() string { + return "setStickerPositionInSet" +} + +func (config SetStickerPositionConfig) params() (Params, error) { + params := make(Params) + + params["sticker"] = config.Sticker + params.AddNonZero("position", config.Position) + + return params, nil +} + +// DeleteStickerConfig allows you to delete a sticker from a set. +type DeleteStickerConfig struct { + Sticker string +} + +func (config DeleteStickerConfig) method() string { + return "deleteStickerFromSet" +} + +func (config DeleteStickerConfig) params() (Params, error) { + params := make(Params) + + params["sticker"] = config.Sticker + + return params, nil +} + +// SetStickerSetThumbnailConfig sets the thumbnail of a sticker set. +type SetStickerSetThumbnailConfig struct { + Name string + UserID int64 + Thumbnail RequestFileData + // Format of the thumbnail, must be one of StickerFormatStatic for a .WEBP + // or .PNG image, StickerFormatAnimated for a .TGS animation, or + // StickerFormatVideo for a .WEBM video. + Format string +} + +func (config SetStickerSetThumbnailConfig) method() string { + return "setStickerSetThumbnail" +} + +func (config SetStickerSetThumbnailConfig) params() (Params, error) { + params := make(Params) + + params["name"] = config.Name + params.AddNonZero64("user_id", config.UserID) + params.AddNonEmpty("format", config.Format) + + return params, nil +} + +func (config SetStickerSetThumbnailConfig) files() []RequestFile { + if config.Thumbnail == nil { + return nil + } + + return []RequestFile{{ + Name: "thumbnail", + Data: config.Thumbnail, + }} +} + +// SetCustomEmojiStickerSetThumbnailConfig sets the thumbnail of a custom +// emoji sticker set. The bot must own the sticker set. +type SetCustomEmojiStickerSetThumbnailConfig struct { + Name string + CustomEmojiID string // pass an empty string to drop the thumbnail +} + +func (config SetCustomEmojiStickerSetThumbnailConfig) method() string { + return "setCustomEmojiStickerSetThumbnail" +} + +func (config SetCustomEmojiStickerSetThumbnailConfig) params() (Params, error) { + params := make(Params) + + params["name"] = config.Name + params.AddNonEmpty("custom_emoji_id", config.CustomEmojiID) + + return params, nil +} + +// SetStickerSetTitleConfig sets the title of a sticker set created by the bot. +type SetStickerSetTitleConfig struct { + Name string + Title string +} + +func (config SetStickerSetTitleConfig) method() string { + return "setStickerSetTitle" +} + +func (config SetStickerSetTitleConfig) params() (Params, error) { + params := make(Params) + + params["name"] = config.Name + params["title"] = config.Title + + return params, nil +} + +// DeleteStickerSetConfig deletes a sticker set created by the bot. +type DeleteStickerSetConfig struct { + Name string +} + +func (config DeleteStickerSetConfig) method() string { + return "deleteStickerSet" +} + +func (config DeleteStickerSetConfig) params() (Params, error) { + params := make(Params) + + params["name"] = config.Name + + return params, nil +} + +// SetStickerEmojiListConfig changes the list of emoji assigned to a regular +// or custom emoji sticker. The sticker must belong to a sticker set created +// by the bot. +type SetStickerEmojiListConfig struct { + Sticker string // file identifier of the sticker + EmojiList []string +} + +func (config SetStickerEmojiListConfig) method() string { + return "setStickerEmojiList" +} + +func (config SetStickerEmojiListConfig) params() (Params, error) { + params := make(Params) + + params["sticker"] = config.Sticker + err := params.AddAny("emoji_list", config.EmojiList) + + return params, err +} + +// SetStickerKeywordsConfig changes the search keywords assigned to a regular +// or custom emoji sticker. The sticker must belong to a sticker set created +// by the bot. +type SetStickerKeywordsConfig struct { + Sticker string // file identifier of the sticker + Keywords []string +} + +func (config SetStickerKeywordsConfig) method() string { + return "setStickerKeywords" +} + +func (config SetStickerKeywordsConfig) params() (Params, error) { + params := make(Params) + + params["sticker"] = config.Sticker + err := params.AddAny("keywords", config.Keywords) + + return params, err +} + +// SetStickerMaskPositionConfig changes the mask position of a mask sticker. +// The sticker must belong to a sticker set created by the bot. +type SetStickerMaskPositionConfig struct { + Sticker string // file identifier of the sticker + MaskPosition *MaskPosition +} + +func (config SetStickerMaskPositionConfig) method() string { + return "setStickerMaskPosition" +} + +func (config SetStickerMaskPositionConfig) params() (Params, error) { + params := make(Params) + + params["sticker"] = config.Sticker + err := params.AddAny("mask_position", config.MaskPosition) + + return params, err +} + +// ReplaceStickerInSetConfig replaces an existing sticker in a sticker set +// with a new one. The sticker set must have been created by the bot. +type ReplaceStickerInSetConfig struct { + UserID int64 + Name string + OldSticker string // file identifier of the replaced sticker + Sticker InputSticker +} + +func (config ReplaceStickerInSetConfig) method() string { + return "replaceStickerInSet" +} + +func (config ReplaceStickerInSetConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("user_id", config.UserID) + params["name"] = config.Name + params["old_sticker"] = config.OldSticker + + err := params.AddAny("sticker", prepareInputStickerForParams(config.Sticker, 0)) + + return params, err +} + +func (config ReplaceStickerInSetConfig) files() []RequestFile { + return prepareInputStickerForFiles(config.Sticker, 0) +} + +// SetChatStickerSetConfig allows you to set the sticker set for a supergroup. +type SetChatStickerSetConfig struct { + ChatID int64 + SuperGroupUsername string + + StickerSetName string +} + +func (config SetChatStickerSetConfig) method() string { + return "setChatStickerSet" +} + +func (config SetChatStickerSetConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } + params["sticker_set_name"] = config.StickerSetName + + return params, nil +} + +// DeleteChatStickerSetConfig allows you to remove a supergroup's sticker set. +type DeleteChatStickerSetConfig struct { + ChatID int64 + SuperGroupUsername string +} + +func (config DeleteChatStickerSetConfig) method() string { + return "deleteChatStickerSet" +} + +func (config DeleteChatStickerSetConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername); err != nil { + return params, err + } + + return params, nil +} + +// SendPaidMediaConfig sends paid media to a channel or private chat. +type SendPaidMediaConfig struct { + BaseChat + // StarCount is the number of Telegram Stars that must be paid to buy + // access to the media; 1-2500. + StarCount int + // Media is the list of media to be sent; 1-10 items. + Media []InputPaidMedia + // Payload is the bot-defined paid media payload, 0-128 bytes. Received + // back in a PurchasedPaidMedia update and in TransactionPartner. + Payload string + // Caption of the media to be sent, 0-1024 characters after entities parsing. + Caption string + // ParseMode mode for parsing entities in the caption. + ParseMode string + // CaptionEntities is a list of special entities that appear in the caption. + CaptionEntities []MessageEntity + // ShowCaptionAboveMedia pass True if the caption must be shown above the message media. + ShowCaptionAboveMedia bool +} + +func (config SendPaidMediaConfig) method() string { + return "sendPaidMedia" +} + +func (config SendPaidMediaConfig) params() (Params, error) { + params, err := config.BaseChat.params() + if err != nil { + return params, err + } + + params.AddNonZero("star_count", config.StarCount) + params.AddNonEmpty("payload", config.Payload) + params.AddNonEmpty("caption", config.Caption) + params.AddNonEmpty("parse_mode", config.ParseMode) + params.AddBool("show_caption_above_media", config.ShowCaptionAboveMedia) + if err = params.AddAny("caption_entities", config.CaptionEntities); err != nil { + return params, err + } + err = params.AddAny("media", prepareInputPaidMediaForParams(config.Media)) + + return params, err +} + +func (config SendPaidMediaConfig) files() []RequestFile { + return prepareInputPaidMediaForFiles(config.Media) +} + +// prepareInputPaidMediaForParams rewrites InputPaidMedia entries whose Media, +// Photo, or Thumbnail need uploading to attach:// references, mirroring +// prepareInputMediaForParams for regular media groups. +func prepareInputPaidMediaForParams(items []InputPaidMedia) []InputPaidMedia { + out := make([]InputPaidMedia, len(items)) + for i, m := range items { + if m.Media != nil && m.Media.NeedsUpload() { + m.Media = fileAttach(fmt.Sprintf("attach://paid-media-%d", i)) + } + if m.Photo != nil && m.Photo.NeedsUpload() { + m.Photo = fileAttach(fmt.Sprintf("attach://paid-media-%d-photo", i)) + } + if m.Thumbnail != nil && m.Thumbnail.NeedsUpload() { + m.Thumbnail = fileAttach(fmt.Sprintf("attach://paid-media-%d-thumbnail", i)) + } + out[i] = m + } + return out +} + +// prepareInputPaidMediaForFiles returns the upload entries for items in the +// slice whose Media, Photo, or Thumbnail need uploading. +func prepareInputPaidMediaForFiles(items []InputPaidMedia) []RequestFile { + var files []RequestFile + for i, m := range items { + if m.Media != nil && m.Media.NeedsUpload() { + files = append(files, RequestFile{ + Name: fmt.Sprintf("paid-media-%d", i), + Data: m.Media, + }) + } + if m.Photo != nil && m.Photo.NeedsUpload() { + files = append(files, RequestFile{ + Name: fmt.Sprintf("paid-media-%d-photo", i), + Data: m.Photo, + }) + } + if m.Thumbnail != nil && m.Thumbnail.NeedsUpload() { + files = append(files, RequestFile{ + Name: fmt.Sprintf("paid-media-%d-thumbnail", i), + Data: m.Thumbnail, + }) + } + } + return files +} + +// MediaGroupConfig allows you to send a group of media. +// +// Media consist of InputMedia items (InputMediaPhoto, InputMediaVideo). +type MediaGroupConfig struct { + ChatID int64 + ChannelUsername string + BusinessConnectionID string + MessageThreadID int + DirectMessagesTopicID int + MessageEffectID string + + Media []interface{} + DisableNotification bool + ProtectContent bool + AllowPaidBroadcast bool + ReplyParameters *ReplyParameters +} + +func (config MediaGroupConfig) method() string { + return "sendMediaGroup" +} + +func (config MediaGroupConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } + params.AddNonEmpty("business_connection_id", config.BusinessConnectionID) + params.AddNonZero("message_thread_id", config.MessageThreadID) + params.AddNonZero("direct_messages_topic_id", config.DirectMessagesTopicID) + params.AddNonEmpty("message_effect_id", config.MessageEffectID) + params.AddBool("disable_notification", config.DisableNotification) + params.AddBool("protect_content", config.ProtectContent) + params.AddBool("allow_paid_broadcast", config.AllowPaidBroadcast) + if err := params.AddAny("reply_parameters", config.ReplyParameters); err != nil { + return params, err + } + + err := params.AddAny("media", prepareInputMediaForParams(config.Media)) + + return params, err +} + +func (config MediaGroupConfig) files() []RequestFile { + return prepareInputMediaForFiles(config.Media) +} + +// DiceConfig contains information about a sendDice request. +type DiceConfig struct { + BaseChat + // Emoji on which the dice throw animation is based. + // Currently, must be one of 🎲, 🎯, 🏀, ⚽, 🎳, or 🎰. + // Dice can have values 1-6 for 🎲, 🎯, and 🎳, values 1-5 for 🏀 and ⚽, + // and values 1-64 for 🎰. + // Defaults to “🎲” + Emoji string +} + +func (config DiceConfig) method() string { + return "sendDice" +} + +func (config DiceConfig) params() (Params, error) { + params, err := config.BaseChat.params() + if err != nil { + return params, err + } + + params.AddNonEmpty("emoji", config.Emoji) + + return params, err +} + +// GetMyCommandsConfig gets a list of the currently registered commands. +type GetMyCommandsConfig struct { + Scope *BotCommandScope + LanguageCode string +} + +func (config GetMyCommandsConfig) method() string { + return "getMyCommands" +} + +func (config GetMyCommandsConfig) params() (Params, error) { + params := make(Params) + + err := params.AddInterface("scope", config.Scope) + params.AddNonEmpty("language_code", config.LanguageCode) + + return params, err +} + +// SetMyCommandsConfig sets a list of commands the bot understands. +type SetMyCommandsConfig struct { + Commands []BotCommand + Scope *BotCommandScope + LanguageCode string +} + +func (config SetMyCommandsConfig) method() string { + return "setMyCommands" +} + +func (config SetMyCommandsConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddInterface("commands", config.Commands); err != nil { + return params, err + } + err := params.AddInterface("scope", config.Scope) + params.AddNonEmpty("language_code", config.LanguageCode) + + return params, err +} + +type DeleteMyCommandsConfig struct { + Scope *BotCommandScope + LanguageCode string +} + +func (config DeleteMyCommandsConfig) method() string { + return "deleteMyCommands" +} + +func (config DeleteMyCommandsConfig) params() (Params, error) { + params := make(Params) + + err := params.AddInterface("scope", config.Scope) + params.AddNonEmpty("language_code", config.LanguageCode) + + return params, err +} + +// SetMyNameConfig changes the bot's name. Different names can be set for +// different user languages. +type SetMyNameConfig struct { + Name string + LanguageCode string +} + +func (config SetMyNameConfig) method() string { + return "setMyName" +} + +func (config SetMyNameConfig) params() (Params, error) { + params := make(Params) + + params.AddNonEmpty("name", config.Name) + params.AddNonEmpty("language_code", config.LanguageCode) + + return params, nil +} + +// GetMyNameConfig returns the current bot name for the given user language. +type GetMyNameConfig struct { + LanguageCode string +} + +func (config GetMyNameConfig) method() string { + return "getMyName" +} + +func (config GetMyNameConfig) params() (Params, error) { + params := make(Params) + + params.AddNonEmpty("language_code", config.LanguageCode) + + return params, nil +} + +// SetMyProfilePhotoConfig changes the profile photo of the bot. +type SetMyProfilePhotoConfig struct { + Photo InputProfilePhoto +} + +func (SetMyProfilePhotoConfig) method() string { + return "setMyProfilePhoto" +} + +func (config SetMyProfilePhotoConfig) params() (Params, error) { + params := make(Params) + + err := params.AddAny("photo", prepareInputProfilePhotoForParams(config.Photo)) + + return params, err +} + +func (config SetMyProfilePhotoConfig) files() []RequestFile { + return prepareInputProfilePhotoForFiles(config.Photo) +} + +// RemoveMyProfilePhotoConfig removes the current profile photo of the bot. +type RemoveMyProfilePhotoConfig struct{} + +func (RemoveMyProfilePhotoConfig) method() string { + return "removeMyProfilePhoto" +} + +func (RemoveMyProfilePhotoConfig) params() (Params, error) { + return make(Params), nil +} + +// GetUserProfileAudiosConfig fetches a list of audios added to the profile +// of a user. +type GetUserProfileAudiosConfig struct { + UserID int64 + Offset int + Limit int +} + +func (GetUserProfileAudiosConfig) method() string { + return "getUserProfileAudios" +} + +func (config GetUserProfileAudiosConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("user_id", config.UserID) + params.AddNonZero("offset", config.Offset) + params.AddNonZero("limit", config.Limit) + + return params, nil +} + +// SetMyDescriptionConfig changes the bot's description, which is shown in the +// chat with the bot if the chat is empty. +type SetMyDescriptionConfig struct { + Description string + LanguageCode string +} + +func (config SetMyDescriptionConfig) method() string { + return "setMyDescription" +} + +func (config SetMyDescriptionConfig) params() (Params, error) { + params := make(Params) + + params.AddNonEmpty("description", config.Description) + params.AddNonEmpty("language_code", config.LanguageCode) + + return params, nil +} + +// GetMyDescriptionConfig returns the current bot description for the given +// user language. +type GetMyDescriptionConfig struct { + LanguageCode string +} + +func (config GetMyDescriptionConfig) method() string { + return "getMyDescription" +} + +func (config GetMyDescriptionConfig) params() (Params, error) { + params := make(Params) + + params.AddNonEmpty("language_code", config.LanguageCode) + + return params, nil +} + +// SetMyShortDescriptionConfig changes the bot's short description, which is +// shown on the bot's profile page and is sent together with the link when +// users share the bot. +type SetMyShortDescriptionConfig struct { + ShortDescription string + LanguageCode string +} + +func (config SetMyShortDescriptionConfig) method() string { + return "setMyShortDescription" +} + +func (config SetMyShortDescriptionConfig) params() (Params, error) { + params := make(Params) + + params.AddNonEmpty("short_description", config.ShortDescription) + params.AddNonEmpty("language_code", config.LanguageCode) + + return params, nil +} + +// GetBusinessConnectionConfig returns information about the connection of +// the bot with a business account. +type GetBusinessConnectionConfig struct { + BusinessConnectionID string +} + +func (config GetBusinessConnectionConfig) method() string { + return "getBusinessConnection" +} + +func (config GetBusinessConnectionConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + + return params, nil +} + +// ReadBusinessMessageConfig marks an incoming message as read on behalf of +// a business account. +type ReadBusinessMessageConfig struct { + BusinessConnectionID string + ChatID int64 + MessageID int +} + +func (ReadBusinessMessageConfig) method() string { + return "readBusinessMessage" +} + +func (config ReadBusinessMessageConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + params.AddNonZero64("chat_id", config.ChatID) + params.AddNonZero("message_id", config.MessageID) + + return params, nil +} + +// DeleteBusinessMessagesConfig deletes messages on behalf of a business account. +type DeleteBusinessMessagesConfig struct { + BusinessConnectionID string + MessageIDs []int +} + +func (DeleteBusinessMessagesConfig) method() string { + return "deleteBusinessMessages" +} + +func (config DeleteBusinessMessagesConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + err := params.AddAny("message_ids", config.MessageIDs) + + return params, err +} + +// SetBusinessAccountNameConfig changes the first and last name of a managed +// business account. +type SetBusinessAccountNameConfig struct { + BusinessConnectionID string + FirstName string + LastName string +} + +func (SetBusinessAccountNameConfig) method() string { + return "setBusinessAccountName" +} + +func (config SetBusinessAccountNameConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + params["first_name"] = config.FirstName + params.AddNonEmpty("last_name", config.LastName) + + return params, nil +} + +// SetBusinessAccountUsernameConfig changes the username of a managed +// business account. +type SetBusinessAccountUsernameConfig struct { + BusinessConnectionID string + Username string +} + +func (SetBusinessAccountUsernameConfig) method() string { + return "setBusinessAccountUsername" +} + +func (config SetBusinessAccountUsernameConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + params.AddNonEmpty("username", config.Username) + + return params, nil +} + +// SetBusinessAccountBioConfig changes the bio of a managed business account. +type SetBusinessAccountBioConfig struct { + BusinessConnectionID string + Bio string +} + +func (SetBusinessAccountBioConfig) method() string { + return "setBusinessAccountBio" +} + +func (config SetBusinessAccountBioConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + params.AddNonEmpty("bio", config.Bio) + + return params, nil +} + +// SetBusinessAccountProfilePhotoConfig changes the profile photo of a +// managed business account. +type SetBusinessAccountProfilePhotoConfig struct { + BusinessConnectionID string + Photo InputProfilePhoto + IsPublic bool +} + +func (SetBusinessAccountProfilePhotoConfig) method() string { + return "setBusinessAccountProfilePhoto" +} + +func (config SetBusinessAccountProfilePhotoConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + params.AddBool("is_public", config.IsPublic) + err := params.AddAny("photo", prepareInputProfilePhotoForParams(config.Photo)) + + return params, err +} + +func (config SetBusinessAccountProfilePhotoConfig) files() []RequestFile { + return prepareInputProfilePhotoForFiles(config.Photo) +} + +// RemoveBusinessAccountProfilePhotoConfig removes the current profile photo +// of a managed business account. +type RemoveBusinessAccountProfilePhotoConfig struct { + BusinessConnectionID string + IsPublic bool +} + +func (RemoveBusinessAccountProfilePhotoConfig) method() string { + return "removeBusinessAccountProfilePhoto" +} + +func (config RemoveBusinessAccountProfilePhotoConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + params.AddBool("is_public", config.IsPublic) + + return params, nil +} + +// SetBusinessAccountGiftSettingsConfig changes the privacy settings pertaining +// to incoming gifts in a managed business account. +type SetBusinessAccountGiftSettingsConfig struct { + BusinessConnectionID string + ShowGiftButton bool + AcceptedGiftTypes AcceptedGiftTypes +} + +func (SetBusinessAccountGiftSettingsConfig) method() string { + return "setBusinessAccountGiftSettings" +} + +func (config SetBusinessAccountGiftSettingsConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + params.AddBool("show_gift_button", config.ShowGiftButton) + err := params.AddAny("accepted_gift_types", config.AcceptedGiftTypes) + + return params, err +} + +// GetBusinessAccountStarBalanceConfig returns the amount of Telegram Stars +// owned by a managed business account. +type GetBusinessAccountStarBalanceConfig struct { + BusinessConnectionID string +} + +func (GetBusinessAccountStarBalanceConfig) method() string { + return "getBusinessAccountStarBalance" +} + +func (config GetBusinessAccountStarBalanceConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + + return params, nil +} + +// TransferBusinessAccountStarsConfig transfers Telegram Stars from the +// business account balance to the bot's balance. +type TransferBusinessAccountStarsConfig struct { + BusinessConnectionID string + StarCount int +} + +func (TransferBusinessAccountStarsConfig) method() string { + return "transferBusinessAccountStars" +} + +func (config TransferBusinessAccountStarsConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + params.AddNonZero("star_count", config.StarCount) + + return params, nil +} + +// GetBusinessAccountGiftsConfig returns the gifts received and owned by a +// managed business account. +type GetBusinessAccountGiftsConfig struct { + BusinessConnectionID string + ExcludeUnsaved bool + ExcludeSaved bool + ExcludeUnlimited bool + ExcludeLimitedUpgradable bool + ExcludeLimitedNonUpgradable bool + ExcludeUnique bool + ExcludeFromBlockchain bool + SortByPrice bool + Offset string + Limit int +} + +func (GetBusinessAccountGiftsConfig) method() string { + return "getBusinessAccountGifts" +} + +func (config GetBusinessAccountGiftsConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + params.AddBool("exclude_unsaved", config.ExcludeUnsaved) + params.AddBool("exclude_saved", config.ExcludeSaved) + params.AddBool("exclude_unlimited", config.ExcludeUnlimited) + params.AddBool("exclude_limited_upgradable", config.ExcludeLimitedUpgradable) + params.AddBool("exclude_limited_non_upgradable", config.ExcludeLimitedNonUpgradable) + params.AddBool("exclude_unique", config.ExcludeUnique) + params.AddBool("exclude_from_blockchain", config.ExcludeFromBlockchain) + params.AddBool("sort_by_price", config.SortByPrice) + params.AddNonEmpty("offset", config.Offset) + params.AddNonZero("limit", config.Limit) + + return params, nil +} + +// ConvertGiftToStarsConfig converts a given regular gift to Telegram Stars. +type ConvertGiftToStarsConfig struct { + BusinessConnectionID string + OwnedGiftID string +} + +func (ConvertGiftToStarsConfig) method() string { + return "convertGiftToStars" +} + +func (config ConvertGiftToStarsConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + params["owned_gift_id"] = config.OwnedGiftID + + return params, nil +} + +// UpgradeGiftConfig upgrades a regular gift to a unique one. +type UpgradeGiftConfig struct { + BusinessConnectionID string + OwnedGiftID string + KeepOriginalDetails bool + StarCount int +} + +func (UpgradeGiftConfig) method() string { + return "upgradeGift" +} + +func (config UpgradeGiftConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + params["owned_gift_id"] = config.OwnedGiftID + params.AddBool("keep_original_details", config.KeepOriginalDetails) + params.AddNonZero("star_count", config.StarCount) + + return params, nil +} + +// TransferGiftConfig transfers an owned unique gift to another user. +type TransferGiftConfig struct { + BusinessConnectionID string + OwnedGiftID string + NewOwnerChatID int64 + StarCount int +} + +func (TransferGiftConfig) method() string { + return "transferGift" +} + +func (config TransferGiftConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + params["owned_gift_id"] = config.OwnedGiftID + params.AddNonZero64("new_owner_chat_id", config.NewOwnerChatID) + params.AddNonZero("star_count", config.StarCount) + + return params, nil +} + +// GiftPremiumSubscriptionConfig gifts a Telegram Premium subscription to +// the given user. +type GiftPremiumSubscriptionConfig struct { + UserID int64 + MonthCount int + StarCount int + Text string + TextParseMode string + TextEntities []MessageEntity +} + +func (GiftPremiumSubscriptionConfig) method() string { + return "giftPremiumSubscription" +} + +func (config GiftPremiumSubscriptionConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("user_id", config.UserID) + params.AddNonZero("month_count", config.MonthCount) + params.AddNonZero("star_count", config.StarCount) + params.AddNonEmpty("text", config.Text) + params.AddNonEmpty("text_parse_mode", config.TextParseMode) + err := params.AddAny("text_entities", config.TextEntities) + + return params, err +} + +// prepareInputProfilePhotoForParams rewrites the photo reference to attach:// +// if the data needs uploading. +func prepareInputProfilePhotoForParams(p InputProfilePhoto) InputProfilePhoto { + if p.Photo != nil && p.Photo.NeedsUpload() { + p.Photo = fileAttach("attach://profile-photo") + } + if p.Animation != nil && p.Animation.NeedsUpload() { + p.Animation = fileAttach("attach://profile-photo") + } + return p +} + +// prepareInputProfilePhotoForFiles returns the upload entry for a profile +// photo whose data needs uploading. +func prepareInputProfilePhotoForFiles(p InputProfilePhoto) []RequestFile { + var files []RequestFile + if p.Photo != nil && p.Photo.NeedsUpload() { + files = append(files, RequestFile{Name: "profile-photo", Data: p.Photo}) + } + if p.Animation != nil && p.Animation.NeedsUpload() { + files = append(files, RequestFile{Name: "profile-photo", Data: p.Animation}) + } + return files +} + +// PostStoryConfig posts a story on behalf of a managed business account. +type PostStoryConfig struct { + BusinessConnectionID string + Content InputStoryContent + ActivePeriod int + Caption string + ParseMode string + CaptionEntities []MessageEntity + Areas []StoryArea + PostToChatPage bool + ProtectContent bool +} + +func (PostStoryConfig) method() string { + return "postStory" +} + +func (config PostStoryConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + params.AddNonZero("active_period", config.ActivePeriod) + params.AddNonEmpty("caption", config.Caption) + params.AddNonEmpty("parse_mode", config.ParseMode) + params.AddBool("post_to_chat_page", config.PostToChatPage) + params.AddBool("protect_content", config.ProtectContent) + if err := params.AddAny("content", prepareInputStoryContentForParams(config.Content)); err != nil { + return params, err + } + if err := params.AddAny("caption_entities", config.CaptionEntities); err != nil { + return params, err + } + err := params.AddAny("areas", config.Areas) + + return params, err +} + +func (config PostStoryConfig) files() []RequestFile { + return prepareInputStoryContentForFiles(config.Content) +} + +// EditStoryConfig edits a story previously posted by the bot on behalf of +// a managed business account. +type EditStoryConfig struct { + BusinessConnectionID string + StoryID int + Content InputStoryContent + Caption string + ParseMode string + CaptionEntities []MessageEntity + Areas []StoryArea +} + +func (EditStoryConfig) method() string { + return "editStory" +} + +func (config EditStoryConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + params.AddNonZero("story_id", config.StoryID) + params.AddNonEmpty("caption", config.Caption) + params.AddNonEmpty("parse_mode", config.ParseMode) + if err := params.AddAny("content", prepareInputStoryContentForParams(config.Content)); err != nil { + return params, err + } + if err := params.AddAny("caption_entities", config.CaptionEntities); err != nil { + return params, err + } + err := params.AddAny("areas", config.Areas) + + return params, err +} + +func (config EditStoryConfig) files() []RequestFile { + return prepareInputStoryContentForFiles(config.Content) +} + +// SendChecklistConfig sends a checklist message on behalf of a connected +// business account. +type SendChecklistConfig struct { + BusinessConnectionID string + ChatID int64 + Checklist InputChecklist + DisableNotification bool + ProtectContent bool + MessageEffectID string + ReplyParameters *ReplyParameters + ReplyMarkup interface{} +} + +func (SendChecklistConfig) method() string { + return "sendChecklist" +} + +func (config SendChecklistConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + params.AddNonZero64("chat_id", config.ChatID) + params.AddBool("disable_notification", config.DisableNotification) + params.AddBool("protect_content", config.ProtectContent) + params.AddNonEmpty("message_effect_id", config.MessageEffectID) + if err := params.AddAny("checklist", config.Checklist); err != nil { + return params, err + } + if err := params.AddAny("reply_parameters", config.ReplyParameters); err != nil { + return params, err + } + err := params.AddAny("reply_markup", config.ReplyMarkup) + + return params, err +} + +// EditMessageChecklistConfig edits a checklist previously sent by the bot. +type EditMessageChecklistConfig struct { + BusinessConnectionID string + ChatID int64 + MessageID int + Checklist InputChecklist + ReplyMarkup *InlineKeyboardMarkup +} + +func (EditMessageChecklistConfig) method() string { + return "editMessageChecklist" +} + +func (config EditMessageChecklistConfig) params() (Params, error) { + params := make(Params) + + params["business_connection_id"] = config.BusinessConnectionID + params.AddNonZero64("chat_id", config.ChatID) + params.AddNonZero("message_id", config.MessageID) + if err := params.AddAny("checklist", config.Checklist); err != nil { + return params, err + } + err := params.AddAny("reply_markup", config.ReplyMarkup) + + return params, err +} + +// GetMyStarBalanceConfig returns the current Telegram Stars balance of the +// bot. +type GetMyStarBalanceConfig struct{} + +func (GetMyStarBalanceConfig) method() string { + return "getMyStarBalance" +} + +func (GetMyStarBalanceConfig) params() (Params, error) { + return make(Params), nil +} + +// SendMessageDraftConfig streams a partial text message to a user while the +// content is still being generated. The streamed draft is ephemeral and acts +// as a 30-second preview; to persist the message, call sendMessage with the +// finalized text. Pass an empty Text to show a "Thinking..." placeholder. +// +// DraftID must be non-zero; updates with the same DraftID animate together. +type SendMessageDraftConfig struct { + ChatID int64 + MessageThreadID int + DraftID int64 + Text string + ParseMode string + Entities []MessageEntity + // CanStop shows the user a button to stop further drafts. The bot + // receives an Update with StoppedMessageGeneration if the user presses + // the button. + CanStop bool + // KeepOnStop keeps the draft in the chat when the stop button is pressed. + // The draft still disappears after a short time or if the bot sends a + // message. To fully preserve the partial draft, send it as a new message. + KeepOnStop bool +} + +func (SendMessageDraftConfig) method() string { + return "sendMessageDraft" +} + +func (config SendMessageDraftConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("chat_id", config.ChatID) + params.AddNonZero("message_thread_id", config.MessageThreadID) + params.AddNonZero64("draft_id", config.DraftID) + // text is optional; an empty string is meaningful (placeholder). + params["text"] = config.Text + params.AddNonEmpty("parse_mode", config.ParseMode) + params.AddBool("can_stop", config.CanStop) + params.AddBool("keep_on_stop", config.KeepOnStop) + err := params.AddAny("entities", config.Entities) + + return params, err } -func (config DiceConfig) method() string { - return "sendDice" +// GetUserGiftsConfig returns the list of gifts received and owned by a user. +type GetUserGiftsConfig struct { + UserID int64 + ExcludeUnlimited bool + ExcludeLimitedUpgradable bool + ExcludeLimitedNonUpgradable bool + ExcludeFromBlockchain bool + ExcludeUnique bool + SortByPrice bool + Offset string + Limit int } -func (config DiceConfig) params() (Params, error) { - params, err := config.BaseChat.params() - if err != nil { - return params, err - } +func (GetUserGiftsConfig) method() string { + return "getUserGifts" +} - params.AddNonEmpty("emoji", config.Emoji) +func (config GetUserGiftsConfig) params() (Params, error) { + params := make(Params) - return params, err -} + params.AddNonZero64("user_id", config.UserID) + params.AddBool("exclude_unlimited", config.ExcludeUnlimited) + params.AddBool("exclude_limited_upgradable", config.ExcludeLimitedUpgradable) + params.AddBool("exclude_limited_non_upgradable", config.ExcludeLimitedNonUpgradable) + params.AddBool("exclude_from_blockchain", config.ExcludeFromBlockchain) + params.AddBool("exclude_unique", config.ExcludeUnique) + params.AddBool("sort_by_price", config.SortByPrice) + params.AddNonEmpty("offset", config.Offset) + params.AddNonZero("limit", config.Limit) -// GetMyCommandsConfig gets a list of the currently registered commands. -type GetMyCommandsConfig struct { - Scope *BotCommandScope - LanguageCode string + return params, nil } -func (config GetMyCommandsConfig) method() string { - return "getMyCommands" +// GetChatGiftsConfig returns the list of gifts received and owned by a chat. +// +// Provide the target chat via either ChatID (numeric identifier) or +// ChannelUsername ("@channelusername"); the first non-zero / non-empty +// value is used. +type GetChatGiftsConfig struct { + ChatID int64 + ChannelUsername string + ExcludeUnsaved bool + ExcludeSaved bool + ExcludeUnlimited bool + ExcludeLimitedUpgradable bool + ExcludeLimitedNonUpgradable bool + ExcludeFromBlockchain bool + ExcludeUnique bool + SortByPrice bool + Offset string + Limit int +} + +func (GetChatGiftsConfig) method() string { + return "getChatGifts" +} + +func (config GetChatGiftsConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername); err != nil { + return params, err + } + params.AddBool("exclude_unsaved", config.ExcludeUnsaved) + params.AddBool("exclude_saved", config.ExcludeSaved) + params.AddBool("exclude_unlimited", config.ExcludeUnlimited) + params.AddBool("exclude_limited_upgradable", config.ExcludeLimitedUpgradable) + params.AddBool("exclude_limited_non_upgradable", config.ExcludeLimitedNonUpgradable) + params.AddBool("exclude_from_blockchain", config.ExcludeFromBlockchain) + params.AddBool("exclude_unique", config.ExcludeUnique) + params.AddBool("sort_by_price", config.SortByPrice) + params.AddNonEmpty("offset", config.Offset) + params.AddNonZero("limit", config.Limit) + + return params, nil } -func (config GetMyCommandsConfig) params() (Params, error) { +// RepostStoryConfig reposts a story on behalf of a business account from +// another business account. Both business accounts must be managed by the +// same bot, and the story on the source account must have been posted (or +// reposted) by the bot. Requires the can_manage_stories business bot right +// for both business accounts. +type RepostStoryConfig struct { + BusinessConnectionID string + // FromChatID is the unique identifier of the chat which posted the story + // that should be reposted. + FromChatID int64 + // FromStoryID is the unique identifier of the story that should be + // reposted. + FromStoryID int + // ActivePeriod is the period after which the story is moved to the + // archive, in seconds; must be one of 6 * 3600, 12 * 3600, 86400, or + // 2 * 86400. + ActivePeriod int + PostToChatPage bool + ProtectContent bool +} + +func (RepostStoryConfig) method() string { + return "repostStory" +} + +func (config RepostStoryConfig) params() (Params, error) { params := make(Params) - err := params.AddInterface("scope", config.Scope) - params.AddNonEmpty("language_code", config.LanguageCode) + params["business_connection_id"] = config.BusinessConnectionID + params.AddNonZero64("from_chat_id", config.FromChatID) + params.AddNonZero("from_story_id", config.FromStoryID) + params.AddNonZero("active_period", config.ActivePeriod) + params.AddBool("post_to_chat_page", config.PostToChatPage) + params.AddBool("protect_content", config.ProtectContent) - return params, err + return params, nil } -// SetMyCommandsConfig sets a list of commands the bot understands. -type SetMyCommandsConfig struct { - Commands []BotCommand - Scope *BotCommandScope - LanguageCode string +// DeleteStoryConfig deletes a story previously posted by the bot on behalf +// of a managed business account. +type DeleteStoryConfig struct { + BusinessConnectionID string + StoryID int } -func (config SetMyCommandsConfig) method() string { - return "setMyCommands" +func (DeleteStoryConfig) method() string { + return "deleteStory" } -func (config SetMyCommandsConfig) params() (Params, error) { +func (config DeleteStoryConfig) params() (Params, error) { params := make(Params) - if err := params.AddInterface("commands", config.Commands); err != nil { - return params, err + params["business_connection_id"] = config.BusinessConnectionID + params.AddNonZero("story_id", config.StoryID) + + return params, nil +} + +// prepareInputStoryContentForParams rewrites the story content's photo/video +// reference to attach:// if the data needs uploading. +func prepareInputStoryContentForParams(c InputStoryContent) InputStoryContent { + if c.Photo != nil && c.Photo.NeedsUpload() { + c.Photo = fileAttach("attach://story-content") } - err := params.AddInterface("scope", config.Scope) - params.AddNonEmpty("language_code", config.LanguageCode) + if c.Video != nil && c.Video.NeedsUpload() { + c.Video = fileAttach("attach://story-content") + } + return c +} - return params, err +// prepareInputStoryContentForFiles returns the upload entries for a story +// content. +func prepareInputStoryContentForFiles(c InputStoryContent) []RequestFile { + var files []RequestFile + if c.Photo != nil && c.Photo.NeedsUpload() { + files = append(files, RequestFile{Name: "story-content", Data: c.Photo}) + } + if c.Video != nil && c.Video.NeedsUpload() { + files = append(files, RequestFile{Name: "story-content", Data: c.Video}) + } + return files } -type DeleteMyCommandsConfig struct { - Scope *BotCommandScope +// GetMyShortDescriptionConfig returns the current bot short description for +// the given user language. +type GetMyShortDescriptionConfig struct { LanguageCode string } -func (config DeleteMyCommandsConfig) method() string { - return "deleteMyCommands" +func (config GetMyShortDescriptionConfig) method() string { + return "getMyShortDescription" } -func (config DeleteMyCommandsConfig) params() (Params, error) { +func (config GetMyShortDescriptionConfig) params() (Params, error) { params := make(Params) - err := params.AddInterface("scope", config.Scope) params.AddNonEmpty("language_code", config.LanguageCode) - return params, err + return params, nil } // SetChatMenuButtonConfig changes the bot's menu button in a private chat, @@ -2423,6 +5266,154 @@ func (config GetMyDefaultAdministratorRightsConfig) params() (Params, error) { // media and "attach://file-%d-thumb" for thumbnails. // // It is expected to be used in conjunction with prepareInputMediaFile. +// GetManagedBotTokenConfig contains the parameters for the getManagedBotToken method. +type GetManagedBotTokenConfig struct { + // UserID is the user identifier of the managed bot whose token will be returned. + UserID int64 +} + +func (GetManagedBotTokenConfig) method() string { + return "getManagedBotToken" +} + +func (config GetManagedBotTokenConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("user_id", config.UserID) + + return params, nil +} + +// ReplaceManagedBotTokenConfig contains the parameters for the replaceManagedBotToken method. +type ReplaceManagedBotTokenConfig struct { + // UserID is the user identifier of the managed bot whose token will be replaced. + UserID int64 +} + +func (ReplaceManagedBotTokenConfig) method() string { + return "replaceManagedBotToken" +} + +func (config ReplaceManagedBotTokenConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("user_id", config.UserID) + + return params, nil +} + +// GetManagedBotAccessSettingsConfig returns the access settings of a managed +// bot. +type GetManagedBotAccessSettingsConfig struct { + // UserID is the user identifier of the managed bot whose access + // settings will be returned. + UserID int64 +} + +func (GetManagedBotAccessSettingsConfig) method() string { + return "getManagedBotAccessSettings" +} + +func (config GetManagedBotAccessSettingsConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("user_id", config.UserID) + + return params, nil +} + +// SetManagedBotAccessSettingsConfig updates the access settings of a managed +// bot. AddedUserIDs is a list of up to 10 users that will gain access in +// addition to the bot's owner; it is ignored when IsAccessRestricted is +// false. +type SetManagedBotAccessSettingsConfig struct { + UserID int64 + IsAccessRestricted bool + AddedUserIDs []int64 +} + +func (SetManagedBotAccessSettingsConfig) method() string { + return "setManagedBotAccessSettings" +} + +func (config SetManagedBotAccessSettingsConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("user_id", config.UserID) + params["is_access_restricted"] = strconv.FormatBool(config.IsAccessRestricted) + if len(config.AddedUserIDs) > 0 { + if err := params.AddAny("added_user_ids", config.AddedUserIDs); err != nil { + return params, err + } + } + + return params, nil +} + +// GetUserPersonalChatMessagesConfig returns the last messages from a user's +// personal chat. +type GetUserPersonalChatMessagesConfig struct { + // UserID is the unique identifier of the target user. + UserID int64 + // Limit is the maximum number of messages to return; 1-20. + Limit int +} + +func (GetUserPersonalChatMessagesConfig) method() string { + return "getUserPersonalChatMessages" +} + +func (config GetUserPersonalChatMessagesConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("user_id", config.UserID) + params.AddNonZero("limit", config.Limit) + + return params, nil +} + +// AnswerGuestQueryConfig replies to a received guest message. The Result is +// any of the InlineQueryResult* variants describing the message to be sent. +type AnswerGuestQueryConfig struct { + GuestQueryID string + Result any +} + +func (AnswerGuestQueryConfig) method() string { + return "answerGuestQuery" +} + +func (config AnswerGuestQueryConfig) params() (Params, error) { + params := make(Params) + + params["guest_query_id"] = config.GuestQueryID + err := params.AddAny("result", config.Result) + + return params, err +} + +// SavePreparedKeyboardButtonConfig contains the parameters for the savePreparedKeyboardButton method. +type SavePreparedKeyboardButtonConfig struct { + // UserID is the unique identifier of the target user that can use the button. + UserID int64 + // Button is a KeyboardButton describing the button to be saved. + // The button must be of the type request_users, request_chat, or request_managed_bot. + Button KeyboardButton +} + +func (SavePreparedKeyboardButtonConfig) method() string { + return "savePreparedKeyboardButton" +} + +func (config SavePreparedKeyboardButtonConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("user_id", config.UserID) + err := params.AddInterface("button", config.Button) + + return params, err +} + func prepareInputMediaParam(inputMedia interface{}, idx int) interface{} { switch m := inputMedia.(type) { case InputMediaPhoto: @@ -2436,8 +5427,22 @@ func prepareInputMediaParam(inputMedia interface{}, idx int) interface{} { m.Media = fileAttach(fmt.Sprintf("attach://file-%d", idx)) } - if m.Thumb != nil && m.Thumb.NeedsUpload() { - m.Thumb = fileAttach(fmt.Sprintf("attach://file-%d-thumb", idx)) + if m.Thumbnail != nil && m.Thumbnail.NeedsUpload() { + m.Thumbnail = fileAttach(fmt.Sprintf("attach://file-%d-thumbnail", idx)) + } + + if m.Cover != nil && m.Cover.NeedsUpload() { + m.Cover = fileAttach(fmt.Sprintf("attach://file-%d-cover", idx)) + } + + return m + case InputMediaAnimation: + if m.Media.NeedsUpload() { + m.Media = fileAttach(fmt.Sprintf("attach://file-%d", idx)) + } + + if m.Thumbnail != nil && m.Thumbnail.NeedsUpload() { + m.Thumbnail = fileAttach(fmt.Sprintf("attach://file-%d-thumbnail", idx)) } return m @@ -2446,8 +5451,8 @@ func prepareInputMediaParam(inputMedia interface{}, idx int) interface{} { m.Media = fileAttach(fmt.Sprintf("attach://file-%d", idx)) } - if m.Thumb != nil && m.Thumb.NeedsUpload() { - m.Thumb = fileAttach(fmt.Sprintf("attach://file-%d-thumb", idx)) + if m.Thumbnail != nil && m.Thumbnail.NeedsUpload() { + m.Thumbnail = fileAttach(fmt.Sprintf("attach://file-%d-thumbnail", idx)) } return m @@ -2456,8 +5461,18 @@ func prepareInputMediaParam(inputMedia interface{}, idx int) interface{} { m.Media = fileAttach(fmt.Sprintf("attach://file-%d", idx)) } - if m.Thumb != nil && m.Thumb.NeedsUpload() { - m.Thumb = fileAttach(fmt.Sprintf("attach://file-%d-thumb", idx)) + if m.Thumbnail != nil && m.Thumbnail.NeedsUpload() { + m.Thumbnail = fileAttach(fmt.Sprintf("attach://file-%d-thumbnail", idx)) + } + + return m + case InputMediaLivePhoto: + if m.Media != nil && m.Media.NeedsUpload() { + m.Media = fileAttach(fmt.Sprintf("attach://file-%d", idx)) + } + + if m.Photo != nil && m.Photo.NeedsUpload() { + m.Photo = fileAttach(fmt.Sprintf("attach://file-%d-photo", idx)) } return m @@ -2493,10 +5508,17 @@ func prepareInputMediaFile(inputMedia interface{}, idx int) []RequestFile { }) } - if m.Thumb != nil && m.Thumb.NeedsUpload() { + if m.Thumbnail != nil && m.Thumbnail.NeedsUpload() { files = append(files, RequestFile{ - Name: fmt.Sprintf("file-%d", idx), - Data: m.Thumb, + Name: fmt.Sprintf("file-%d-thumbnail", idx), + Data: m.Thumbnail, + }) + } + + if m.Cover != nil && m.Cover.NeedsUpload() { + files = append(files, RequestFile{ + Name: fmt.Sprintf("file-%d-cover", idx), + Data: m.Cover, }) } case InputMediaDocument: @@ -2507,13 +5529,26 @@ func prepareInputMediaFile(inputMedia interface{}, idx int) []RequestFile { }) } - if m.Thumb != nil && m.Thumb.NeedsUpload() { + if m.Thumbnail != nil && m.Thumbnail.NeedsUpload() { + files = append(files, RequestFile{ + Name: fmt.Sprintf("file-%d-thumbnail", idx), + Data: m.Thumbnail, + }) + } + case InputMediaLivePhoto: + if m.Media != nil && m.Media.NeedsUpload() { files = append(files, RequestFile{ Name: fmt.Sprintf("file-%d", idx), - Data: m.Thumb, + Data: m.Media, }) } - case InputMediaAudio: + if m.Photo != nil && m.Photo.NeedsUpload() { + files = append(files, RequestFile{ + Name: fmt.Sprintf("file-%d-photo", idx), + Data: m.Photo, + }) + } + case InputMediaAnimation: if m.Media.NeedsUpload() { files = append(files, RequestFile{ Name: fmt.Sprintf("file-%d", idx), @@ -2521,10 +5556,24 @@ func prepareInputMediaFile(inputMedia interface{}, idx int) []RequestFile { }) } - if m.Thumb != nil && m.Thumb.NeedsUpload() { + if m.Thumbnail != nil && m.Thumbnail.NeedsUpload() { + files = append(files, RequestFile{ + Name: fmt.Sprintf("file-%d-thumbnail", idx), + Data: m.Thumbnail, + }) + } + case InputMediaAudio: + if m.Media.NeedsUpload() { files = append(files, RequestFile{ Name: fmt.Sprintf("file-%d", idx), - Data: m.Thumb, + Data: m.Media, + }) + } + + if m.Thumbnail != nil && m.Thumbnail.NeedsUpload() { + files = append(files, RequestFile{ + Name: fmt.Sprintf("file-%d-thumbnail", idx), + Data: m.Thumbnail, }) } } @@ -2566,3 +5615,179 @@ func prepareInputMediaForFiles(inputMedia []interface{}) []RequestFile { return files } + +// SendRichMessageConfig contains information about a sendRichMessage request. +// If the message contains a block with a media element, the bot must have the +// right to send that media to the chat. On success the sent Message is +// returned, so it can be passed to BotAPI.Send. +type SendRichMessageConfig struct { + BaseChat + EphemeralSendParams + // RichMessage is the message to be sent. + RichMessage *InputRichMessage +} + +func (config SendRichMessageConfig) params() (Params, error) { + params, err := config.BaseChat.params() + if err != nil { + return params, err + } + + richMessage, _ := prepareRichMessage(config.RichMessage) + if err = params.AddAny("rich_message", richMessage); err != nil { + return params, err + } + err = config.EphemeralSendParams.addTo(params) + + return params, err +} + +func (config SendRichMessageConfig) method() string { + return "sendRichMessage" +} + +func (config SendRichMessageConfig) files() []RequestFile { + _, files := prepareRichMessage(config.RichMessage) + return files +} + +// SendRichMessageDraftConfig streams a partial rich message to a user while +// the message is being generated. The streamed draft is ephemeral and acts as +// a temporary 30-second preview; once the output is finalized, sendRichMessage +// must be called with the complete message to persist it. Returns True on +// success, so use BotAPI.Request. +type SendRichMessageDraftConfig struct { + // ChatID is the unique identifier for the target private chat. Required. + ChatID int64 + // MessageThreadID is the unique identifier for the target message thread. + MessageThreadID int + // DraftID is the unique identifier of the message draft; must be + // non-zero. Changes to drafts with the same identifier are animated. + // Required. + DraftID int + // RichMessage is the partial message to be streamed. Required. + RichMessage *InputRichMessage + // CanStop shows the user a button to stop further drafts. The bot + // receives an Update with StoppedMessageGeneration if the user presses + // the button. + CanStop bool + // KeepOnStop keeps the draft in the chat when the stop button is pressed. + // The draft still disappears after a short time or if the bot sends a + // message. To fully preserve the partial draft, send it as a new message. + KeepOnStop bool +} + +func (config SendRichMessageDraftConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("chat_id", config.ChatID) + params.AddNonZero("message_thread_id", config.MessageThreadID) + params.AddNonZero("draft_id", config.DraftID) + params.AddBool("can_stop", config.CanStop) + params.AddBool("keep_on_stop", config.KeepOnStop) + richMessage, _ := prepareRichMessage(config.RichMessage) + err := params.AddAny("rich_message", richMessage) + + return params, err +} + +func (config SendRichMessageDraftConfig) method() string { + return "sendRichMessageDraft" +} + +func (config SendRichMessageDraftConfig) files() []RequestFile { + _, files := prepareRichMessage(config.RichMessage) + return files +} + +// Result values for AnswerChatJoinRequestQueryConfig. +const ( + // ChatJoinRequestApprove allows the user to join the chat. + ChatJoinRequestApprove = "approve" + // ChatJoinRequestDecline disallows the user from joining the chat. + ChatJoinRequestDecline = "decline" + // ChatJoinRequestQueue leaves the decision to other administrators. + ChatJoinRequestQueue = "queue" +) + +// AnswerChatJoinRequestQueryConfig processes a received chat join request +// query. Returns True on success, so use BotAPI.Request. +type AnswerChatJoinRequestQueryConfig struct { + // ChatJoinRequestQueryID is the unique identifier of the join request + // query. Required. + ChatJoinRequestQueryID string + // Result of the query. Must be one of ChatJoinRequestApprove, + // ChatJoinRequestDecline, or ChatJoinRequestQueue. Required. + Result string +} + +func (config AnswerChatJoinRequestQueryConfig) params() (Params, error) { + params := make(Params) + + params["chat_join_request_query_id"] = config.ChatJoinRequestQueryID + params["result"] = config.Result + + return params, nil +} + +func (config AnswerChatJoinRequestQueryConfig) method() string { + return "answerChatJoinRequestQuery" +} + +// SendChatJoinRequestWebAppConfig processes a received chat join request query +// by showing a Mini App to the user before deciding the outcome. Call +// AnswerChatJoinRequestQueryConfig afterwards to resolve the join request based +// on the user's interaction with the Mini App. Returns True on success, so use +// BotAPI.Request. +type SendChatJoinRequestWebAppConfig struct { + // ChatJoinRequestQueryID is the unique identifier of the join request + // query. Required. + ChatJoinRequestQueryID string + // WebAppURL is the URL of the Mini App to be opened. Required. + WebAppURL string +} + +func (config SendChatJoinRequestWebAppConfig) params() (Params, error) { + params := make(Params) + + params["chat_join_request_query_id"] = config.ChatJoinRequestQueryID + params["web_app_url"] = config.WebAppURL + + return params, nil +} + +func (config SendChatJoinRequestWebAppConfig) method() string { + return "sendChatJoinRequestWebApp" +} + +// SetPassportDataErrorsConfig informs a user that some of the Telegram +// Passport elements they provided contains errors. The user will not be able +// to re-submit their Passport to you until the errors are fixed (the contents +// of the field for which you returned the error must change). +// +// Errors must hold PassportElementError* values, for example +// PassportElementErrorDataField or PassportElementErrorUnspecified. +type SetPassportDataErrorsConfig struct { + // UserID is the user identifier. + UserID int64 + // Errors is the list of errors describing what is wrong with the + // submitted Passport data. + Errors []PassportElementError +} + +func (config SetPassportDataErrorsConfig) method() string { + return "setPassportDataErrors" +} + +func (config SetPassportDataErrorsConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("user_id", config.UserID) + if config.Errors == nil { + params["errors"] = "[]" + return params, nil + } + err := params.AddAny("errors", config.Errors) + + return params, err +} diff --git a/docs/changelog.md b/docs/changelog.md index dd6399f9..88b9e542 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,17 +1,36 @@ -# Change Log - -## v5.4.0 - -- Remove all methods that return `(APIResponse, error)`. - - Use the `Request` method instead. - - For more information, see [Library Structure][library-structure]. -- Remove all `New*Upload` and `New*Share` methods, replace with `New*`. - - Use different [file types][files] to specify if upload or share. -- Rename `UploadFile` to `UploadFiles`, accept `[]RequestFile` instead of a - single fieldname and file. -- Fix methods returning `APIResponse` and errors to always use pointers. -- Update user IDs to `int64` because of Bot API changes. -- Add missing Bot API features. - -[library-structure]: ./getting-started/library-structure.md#methods -[files]: ./getting-started/files.md +# Change Log + +## v6.0.0 (unreleased) + +- Module path is now `github.com/bssth/telegram-bot-api/v6`: the upstream + repository is no longer maintained, and the new major version reflects + the source-incompatible changes listed in BREAKING.md. +- Support every Bot API version from 6.1 to 10.3, based on the unmerged + upstream pull request + [#794](https://github.com/go-telegram-bot-api/telegram-bot-api/pull/794). +- Audit against the machine-readable Bot API 10.3 specification: all types, + fields, methods and parameters are present. +- Upload files nested in polls and rich messages via `attach://`. +- Redact the bot token from transport errors. +- Add `internal/cmd/specdiff` and a weekly workflow that report gaps against + the latest Bot API specification. +- Require Go 1.24. +- See [BREAKING.md][breaking] for source-incompatible changes. + +[breaking]: https://github.com/bssth/telegram-bot-api/blob/master/BREAKING.md + +## v5.4.0 + +- Remove all methods that return `(APIResponse, error)`. + - Use the `Request` method instead. + - For more information, see [Library Structure][library-structure]. +- Remove all `New*Upload` and `New*Share` methods, replace with `New*`. + - Use different [file types][files] to specify if upload or share. +- Rename `UploadFile` to `UploadFiles`, accept `[]RequestFile` instead of a + single fieldname and file. +- Fix methods returning `APIResponse` and errors to always use pointers. +- Update user IDs to `int64` because of Bot API changes. +- Add missing Bot API features. + +[library-structure]: ./getting-started/library-structure.md#methods +[files]: ./getting-started/files.md diff --git a/docs/examples/command-handling.md b/docs/examples/command-handling.md index d1d8b29d..17a37a9a 100644 --- a/docs/examples/command-handling.md +++ b/docs/examples/command-handling.md @@ -9,7 +9,7 @@ import ( "log" "os" - tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + tgbotapi "github.com/bssth/telegram-bot-api/v6" ) func main() { diff --git a/docs/examples/inline-keyboard.md b/docs/examples/inline-keyboard.md index e14ee631..973d897f 100644 --- a/docs/examples/inline-keyboard.md +++ b/docs/examples/inline-keyboard.md @@ -11,7 +11,7 @@ import ( "log" "os" - tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + tgbotapi "github.com/bssth/telegram-bot-api/v6" ) var numericKeyboard = tgbotapi.NewInlineKeyboardMarkup( diff --git a/docs/examples/keyboard.md b/docs/examples/keyboard.md index d67a9154..6880e360 100644 --- a/docs/examples/keyboard.md +++ b/docs/examples/keyboard.md @@ -10,7 +10,7 @@ import ( "log" "os" - tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + tgbotapi "github.com/bssth/telegram-bot-api/v6" ) var numericKeyboard = tgbotapi.NewReplyKeyboard( diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md index 25b77f74..e5a506bd 100644 --- a/docs/getting-started/README.md +++ b/docs/getting-started/README.md @@ -10,7 +10,7 @@ approaches to solve common problems. ## Installing ```bash -go get -u github.com/go-telegram-bot-api/telegram-bot-api/v5 +go get github.com/bssth/telegram-bot-api/v6 ``` ## A Simple Bot @@ -22,7 +22,7 @@ messages repeating what you said. Make sure you get an API token from Let's start by constructing a new [BotAPI][bot-api-docs]. [botfather]: https://t.me/Botfather -[bot-api-docs]: https://pkg.go.dev/github.com/go-telegram-bot-api/telegram-bot-api/v5?tab=doc#BotAPI +[bot-api-docs]: https://pkg.go.dev/github.com/bssth/telegram-bot-api/v6?tab=doc#BotAPI ```go package main @@ -30,7 +30,7 @@ package main import ( "os" - tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + tgbotapi "github.com/bssth/telegram-bot-api/v6" ) func main() { @@ -90,7 +90,9 @@ things. We can add this code in right after the line enabling debug mode. // We'll also say that this message is a reply to the previous message. // For any other specifications than Chat ID or Text, you'll need to // set fields on the `MessageConfig`. - msg.ReplyToMessageID = update.Message.MessageID + msg.ReplyParameters = &tgbotapi.ReplyParameters{ + MessageID: update.Message.MessageID, + } // Okay, we're sending our message off! We don't care about the message // we just sent, so we'll discard it. diff --git a/docs/internals/adding-endpoints.md b/docs/internals/adding-endpoints.md index c4ff59a6..5e5fc6de 100644 --- a/docs/internals/adding-endpoints.md +++ b/docs/internals/adding-endpoints.md @@ -105,12 +105,12 @@ have similar fields for their files. ChatID int64 MessageID int + Delete RequestFileData -+ Thumb RequestFileData ++ Thumbnail RequestFileData } ``` Adding another method is pretty simple. We'll always add a file named `delete` -and add the `thumb` file if we have one. +and add the `thumbnail` file if we have one. ```go func (config DeleteMessageConfig) files() []RequestFile { @@ -119,10 +119,10 @@ func (config DeleteMessageConfig) files() []RequestFile { Data: config.Delete, }} - if config.Thumb != nil { + if config.Thumbnail != nil { files = append(files, RequestFile{ - Name: "thumb", - Data: config.Thumb, + Name: "thumbnail", + Data: config.Thumbnail, }) } @@ -136,7 +136,7 @@ is a `FilePath`, `FileURL`, `FileBytes`, `FileReader`, or `FileID`. ### Base Configs Certain Configs have repeated elements. For example, many of the items sent to a -chat have `ChatID` or `ChannelUsername` fields, along with `ReplyToMessageID`, +chat have `ChatID` or `ChannelUsername` fields, along with `ReplyParameters`, `ReplyMarkup`, and `DisableNotification`. Instead of implementing all of this code for each item, there's a `BaseChat` that handles it for your Config. Simply embed it in your struct to get all of those fields. @@ -147,9 +147,10 @@ embedding the `BaseChat` struct. ```go type MessageConfig struct { BaseChat - Text string - ParseMode string - DisableWebPagePreview bool + Text string + ParseMode string + Entities []MessageEntity + LinkPreviewOptions *LinkPreviewOptions } ``` diff --git a/docs/internals/uploading-files.md b/docs/internals/uploading-files.md index 1845269c..e7d7ad19 100644 --- a/docs/internals/uploading-files.md +++ b/docs/internals/uploading-files.md @@ -17,7 +17,7 @@ Config. Most endpoints use static file fields. For example, `sendPhoto` expects a single file named `photo`. All we have to do is set that single field with the correct value (either a string or multipart file). Methods like `sendDocument` take two -file uploads, a `document` and a `thumb`. These are pretty straightforward. +file uploads, a `document` and a `thumbnail`. These are pretty straightforward. Remembering that the `Fileable` interface only requires one method, let's implement it for `DocumentConfig`. @@ -32,10 +32,10 @@ func (config DocumentConfig) files() []RequestFile { }} // We'll only add a file if we have one. - if config.Thumb != nil { + if config.Thumbnail != nil { files = append(files, RequestFile{ - Name: "thumb", - Data: config.Thumb, + Name: "thumbnail", + Data: config.Thumbnail, }) } @@ -85,3 +85,28 @@ are all changed into `attach://file-%d`. When collecting a list of files to upload, it names them the same way. This creates a nearly transparent way of handling multiple files in the background without the user having to consider what's going on. + +### Nested Media + +Newer methods accept media nested inside JSON-serialized parameters: a poll can +carry media for its question, each option and the quiz explanation, and a rich +message can embed photos, videos, documents and so on in its `media` list and +in its blocks. Telegram resolves `attach://` references anywhere in the request, +so these work the same way as media groups. + +`SendPollConfig`, `SendRichMessageConfig`, `SendRichMessageDraftConfig`, +`EditMessageTextConfig` and `EditEphemeralMessageTextConfig` walk their nested +media with a `nestedMediaUploader` (see `nested_media.go`). Every file that +needs uploading is replaced by `attach://poll-media-%d` or +`attach://rich-media-%d` in a copy of the media, and the matching +`RequestFile` is collected for `files()`. Because `params()` and `files()` +walk the media in the same order, the names always line up, and the caller's +config is never modified. + +```go +poll := tgbotapi.NewPoll(chatID, "Which one?", "Left", "Right") +poll.Options[0].Media = tgbotapi.NewInputMediaPhoto(tgbotapi.FilePath("left.jpg")) +poll.Options[1].Media = tgbotapi.NewInputMediaPhoto(tgbotapi.FileID(rightFileID)) + +bot.Send(poll) +``` diff --git a/ephemeral_test.go b/ephemeral_test.go new file mode 100644 index 00000000..2456ef1f --- /dev/null +++ b/ephemeral_test.go @@ -0,0 +1,289 @@ +package tgbotapi + +import ( + "encoding/json" + "reflect" + "testing" +) + +// TestInputRichBlockCaptionRouting verifies that the shared "caption" wire +// field is routed to TableCaption for a table block and to Caption for a media +// block, mirroring RichBlock. +func TestInputRichBlockCaptionRouting(t *testing.T) { + var table InputRichBlock + if err := json.Unmarshal([]byte(`{"type":"table","cells":[],"caption":"a table"}`), &table); err != nil { + t.Fatalf("table unmarshal: %v", err) + } + if table.TableCaption == nil || table.TableCaption.PlainText != "a table" { + t.Fatalf("table caption not routed to TableCaption: %+v", table) + } + if table.Caption != nil { + t.Fatalf("table caption also set Caption: %+v", table.Caption) + } + + var photo InputRichBlock + if err := json.Unmarshal([]byte(`{"type":"photo","caption":{"text":"a photo"}}`), &photo); err != nil { + t.Fatalf("photo unmarshal: %v", err) + } + if photo.Caption == nil || !photo.Caption.Text.IsPlain || photo.Caption.Text.PlainText != "a photo" { + t.Fatalf("photo caption not routed to Caption: %+v", photo) + } + if photo.TableCaption != nil { + t.Fatalf("photo caption also set TableCaption: %+v", photo.TableCaption) + } +} + +// TestInputRichBlockRoundTrip verifies that the block variants survive a +// decode/encode/decode cycle unchanged. +func TestInputRichBlockRoundTrip(t *testing.T) { + cases := []string{ + `{"type":"paragraph","text":"hello"}`, + `{"type":"heading","text":"title","size":2}`, + `{"type":"pre","text":"x := 1","language":"go"}`, + `{"type":"divider"}`, + `{"type":"mathematical_expression","expression":"e^{i\\pi}+1=0"}`, + `{"type":"anchor","name":"intro"}`, + `{"type":"list","items":[{"blocks":[{"type":"paragraph","text":"a"}],"value":1,"type":"1"}]}`, + `{"type":"blockquote","blocks":[{"type":"paragraph","text":"q"}],"credit":"someone"}`, + `{"type":"details","summary":"more","blocks":[{"type":"paragraph","text":"d"}],"is_open":true}`, + `{"type":"table","cells":[[{"text":"c","align":"left","valign":"top"}]],"is_bordered":true,"caption":"cap"}`, + `{"type":"map","location":{"latitude":1.5,"longitude":2.5},"zoom":10,"width":600,"height":400}`, + `{"type":"thinking","text":"working"}`, + } + + for _, in := range cases { + var first InputRichBlock + if err := json.Unmarshal([]byte(in), &first); err != nil { + t.Fatalf("unmarshal %s: %v", in, err) + } + out, err := json.Marshal(first) + if err != nil { + t.Fatalf("marshal %s: %v", in, err) + } + var second InputRichBlock + if err := json.Unmarshal(out, &second); err != nil { + t.Fatalf("re-unmarshal %s: %v", out, err) + } + if !reflect.DeepEqual(first, second) { + t.Errorf("round trip mismatch:\n in: %s\n out: %s", in, out) + } + } +} + +// TestInputRichBlockMediaMarshal verifies the encoding of the media-bearing +// block variants. These are marshal-only: the embedded InputMedia* types hold +// the file as a RequestFileData interface, which cannot be decoded, so they +// are not part of the round-trip test above. +func TestInputRichBlockMediaMarshal(t *testing.T) { + photo := NewInputMediaPhoto(FileID("file_id")) + block := InputRichBlock{ + Type: RichBlockTypePhoto, + Photo: &photo, + Caption: &RichBlockCaption{Text: RichText{IsPlain: true, PlainText: "pic"}}, + } + out, err := json.Marshal(block) + if err != nil { + t.Fatalf("marshal photo block: %v", err) + } + want := `{"type":"photo","photo":{"type":"photo","media":"file_id"},"caption":{"text":"pic"}}` + if string(out) != want { + t.Errorf("photo block encoding:\n got: %s\n want: %s", out, want) + } + + voice := NewInputMediaVoiceNote(FileID("file_id")) + voice.Duration = 5 + out, err = json.Marshal(InputRichBlock{Type: RichBlockTypeVoiceNote, VoiceNote: &voice}) + if err != nil { + t.Fatalf("marshal voice note block: %v", err) + } + want = `{"type":"voice_note","voice_note":{"type":"voice_note","media":"file_id","duration":5}}` + if string(out) != want { + t.Errorf("voice note block encoding:\n got: %s\n want: %s", out, want) + } +} + +// TestInputRichMessageBlocks verifies that a block-formatted rich message and +// a markdown rich message with explicit media both encode as expected. +func TestInputRichMessageBlocks(t *testing.T) { + msg := InputRichMessage{ + Blocks: []InputRichBlock{ + {Type: RichBlockTypeParagraph, Text: &RichText{IsPlain: true, PlainText: "hi"}}, + }, + } + out, err := json.Marshal(msg) + if err != nil { + t.Fatalf("marshal blocks: %v", err) + } + if got, want := string(out), `{"blocks":[{"type":"paragraph","text":"hi"}]}`; got != want { + t.Errorf("blocks encoding:\n got: %s\n want: %s", got, want) + } + + withMedia := InputRichMessage{ + Markdown: "![pic](tg://photo?id=p1)", + Media: []InputRichMessageMedia{ + {ID: "p1", Media: NewInputMediaPhoto(FileID("file_id"))}, + }, + } + out, err = json.Marshal(withMedia) + if err != nil { + t.Fatalf("marshal media: %v", err) + } + want := `{"markdown":"![pic](tg://photo?id=p1)","media":[{"id":"p1","media":{"type":"photo","media":"file_id"}}]}` + if got := string(out); got != want { + t.Errorf("media encoding:\n got: %s\n want: %s", got, want) + } +} + +// TestEphemeralSendParams verifies that the ephemeral parameters are only +// emitted when set, and that they reach the wire for a send config. +func TestEphemeralSendParams(t *testing.T) { + plain := NewMessage(12345, "hello") + params, err := plain.params() + if err != nil { + t.Fatalf("plain params: %v", err) + } + if _, ok := params["ephemeral_message_parameters"]; ok { + t.Error("ephemeral_message_parameters emitted for a non-ephemeral message") + } + + ephemeral := NewMessage(12345, "hello") + ephemeral.ReceiverUserID = 777 + ephemeral.CallbackQueryID = "cbq" + ephemeral.ReplaceCallbackQueryMessage = true + params, err = ephemeral.params() + if err != nil { + t.Fatalf("ephemeral params: %v", err) + } + want := `{"receiver_user_id":777,"callback_query_id":"cbq","replace_callback_query_message":true}` + if params["ephemeral_message_parameters"] != want { + t.Errorf("ephemeral_message_parameters = %q, want %q", params["ephemeral_message_parameters"], want) + } +} + +// TestEphemeralEditConfigs verifies the params and methods of the ephemeral +// edit and delete configs. +func TestEphemeralEditConfigs(t *testing.T) { + edit := NewEditEphemeralMessageText(12345, 777, 42, "updated") + params, err := edit.params() + if err != nil { + t.Fatalf("edit params: %v", err) + } + for key, want := range map[string]string{ + "chat_id": "12345", + "receiver_user_id": "777", + "ephemeral_message_id": "42", + "text": "updated", + } { + if params[key] != want { + t.Errorf("%s = %q, want %q", key, params[key], want) + } + } + if edit.method() != "editEphemeralMessageText" { + t.Errorf("method = %q", edit.method()) + } + + del := NewDeleteEphemeralMessage(12345, 777, 42) + params, err = del.params() + if err != nil { + t.Fatalf("delete params: %v", err) + } + if _, ok := params["reply_markup"]; ok { + t.Error("deleteEphemeralMessage must not send reply_markup") + } + if del.method() != "deleteEphemeralMessage" { + t.Errorf("method = %q", del.method()) + } +} + +// TestReplyParametersEphemeral verifies that ReplyParameters can address an +// ephemeral message and that message_id is omitted when unset. +func TestReplyParametersEphemeral(t *testing.T) { + out, err := json.Marshal(ReplyParameters{EphemeralMessageID: 42}) + if err != nil { + t.Fatalf("marshal: %v", err) + } + if got, want := string(out), `{"ephemeral_message_id":42}`; got != want { + t.Errorf("encoding:\n got: %s\n want: %s", got, want) + } +} + +// TestPrepareInputMediaAnimation verifies that InputMediaAnimation is handled +// by the input media preparation helpers. It was previously missing from both +// switches, so animations were silently dropped from the request. +func TestPrepareInputMediaAnimation(t *testing.T) { + byID := NewInputMediaAnimation(FileID("file_id")) + if param := prepareInputMediaParam(byID, 0); param == nil { + t.Fatal("prepareInputMediaParam dropped InputMediaAnimation") + } + + upload := NewInputMediaAnimation(FilePath("/tmp/anim.gif")) + upload.Thumbnail = FilePath("/tmp/thumb.jpg") + + param := prepareInputMediaParam(upload, 3) + animation, ok := param.(InputMediaAnimation) + if !ok { + t.Fatalf("prepareInputMediaParam returned %T, want InputMediaAnimation", param) + } + if got := animation.Media.SendData(); got != "attach://file-3" { + t.Errorf("media = %q, want attach://file-3", got) + } + if got := animation.Thumbnail.SendData(); got != "attach://file-3-thumbnail" { + t.Errorf("thumbnail = %q, want attach://file-3-thumbnail", got) + } + + files := prepareInputMediaFile(upload, 3) + if len(files) != 2 { + t.Fatalf("prepareInputMediaFile returned %d files, want 2", len(files)) + } + if files[0].Name != "file-3" || files[1].Name != "file-3-thumbnail" { + t.Errorf("file names = %q, %q", files[0].Name, files[1].Name) + } +} + +// TestUpdateSentFromSubscription verifies that SentFrom resolves the user of a +// subscription update. +func TestUpdateSentFromSubscription(t *testing.T) { + update := Update{ + Subscription: &BotSubscriptionUpdated{ + User: User{ID: 5, FirstName: "A"}, + State: BotSubscriptionStateCanceled, + }, + } + from := update.SentFrom() + if from == nil { + t.Fatal("SentFrom returned nil for a subscription update") + } + if from.ID != 5 { + t.Errorf("SentFrom().ID = %d, want 5", from.ID) + } +} + +// TestCommunityServiceMessages verifies decoding of the community service +// messages and the subscription update. +func TestCommunityServiceMessages(t *testing.T) { + var msg Message + raw := `{"message_id":1,"date":1,"chat":{"id":1,"type":"supergroup"},` + + `"community_chat_added":{"community":{"id":99,"name":"Gophers"}}}` + if err := json.Unmarshal([]byte(raw), &msg); err != nil { + t.Fatalf("unmarshal message: %v", err) + } + if msg.CommunityChatAdded == nil { + t.Fatal("community_chat_added not decoded") + } + if msg.CommunityChatAdded.Community.ID != 99 || msg.CommunityChatAdded.Community.Name != "Gophers" { + t.Errorf("community not decoded: %+v", msg.CommunityChatAdded.Community) + } + + var update Update + rawUpdate := `{"update_id":1,"subscription":{"user":{"id":5,"is_bot":false,"first_name":"A"},` + + `"invoice_payload":"p","state":"active"}}` + if err := json.Unmarshal([]byte(rawUpdate), &update); err != nil { + t.Fatalf("unmarshal update: %v", err) + } + if update.Subscription == nil { + t.Fatal("subscription not decoded") + } + if update.Subscription.State != BotSubscriptionStateActive || update.Subscription.User.ID != 5 { + t.Errorf("subscription not decoded: %+v", update.Subscription) + } +} diff --git a/go.mod b/go.mod index 167e5e45..adcf5d47 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module github.com/go-telegram-bot-api/telegram-bot-api/v5 +module github.com/bssth/telegram-bot-api/v6 -go 1.16 +go 1.24 diff --git a/helpers.go b/helpers.go index 3a0e8187..5f84151e 100644 --- a/helpers.go +++ b/helpers.go @@ -17,11 +17,9 @@ import ( func NewMessage(chatID int64, text string) MessageConfig { return MessageConfig{ BaseChat: BaseChat{ - ChatID: chatID, - ReplyToMessageID: 0, + ChatID: chatID, }, - Text: text, - DisableWebPagePreview: false, + Text: text, } } @@ -140,6 +138,30 @@ func NewVideo(chatID int64, file RequestFileData) VideoConfig { } } +// NewLivePhoto creates a new sendLivePhoto request. +// +// video is the live photo's video portion (≤10 s, ≤10 MB) and photo is the +// static image. Sending live photos by URL is not currently supported. +func NewLivePhoto(chatID int64, video, photo RequestFileData) LivePhotoConfig { + return LivePhotoConfig{ + BaseFile: BaseFile{ + BaseChat: BaseChat{ChatID: chatID}, + File: video, + }, + Photo: photo, + } +} + +// NewRichMessage creates a new sendRichMessage request for the given chat. +// Exactly one of message.HTML, message.Markdown, or message.Blocks must be +// set. +func NewRichMessage(chatID int64, message InputRichMessage) SendRichMessageConfig { + return SendRichMessageConfig{ + BaseChat: BaseChat{ChatID: chatID}, + RichMessage: &message, + } +} + // NewAnimation creates a new sendAnimation request. func NewAnimation(chatID int64, file RequestFileData) AnimationConfig { return AnimationConfig{ @@ -186,7 +208,7 @@ func NewMediaGroup(chatID int64, files []interface{}) MediaGroupConfig { // NewInputMediaPhoto creates a new InputMediaPhoto. func NewInputMediaPhoto(media RequestFileData) InputMediaPhoto { return InputMediaPhoto{ - BaseInputMedia{ + BaseInputMedia: BaseInputMedia{ Type: "photo", Media: media, }, @@ -223,6 +245,16 @@ func NewInputMediaAudio(media RequestFileData) InputMediaAudio { } } +// NewInputMediaVoiceNote creates a new InputMediaVoiceNote. +func NewInputMediaVoiceNote(media RequestFileData) InputMediaVoiceNote { + return InputMediaVoiceNote{ + BaseInputMedia: BaseInputMedia{ + Type: "voice_note", + Media: media, + }, + } +} + // NewInputMediaDocument creates a new InputMediaDocument. func NewInputMediaDocument(media RequestFileData) InputMediaDocument { return InputMediaDocument{ @@ -233,6 +265,58 @@ func NewInputMediaDocument(media RequestFileData) InputMediaDocument { } } +// NewInputMediaLivePhoto creates a new InputMediaLivePhoto. Media is the +// live photo video and photo is its static image. +func NewInputMediaLivePhoto(media, photo RequestFileData) InputMediaLivePhoto { + return InputMediaLivePhoto{ + BaseInputMedia: BaseInputMedia{ + Type: "live_photo", + Media: media, + }, + Photo: photo, + } +} + +// NewInputMediaSticker creates a new InputMediaSticker, which can be used as +// the media of a poll option. +func NewInputMediaSticker(media RequestFileData) InputMediaSticker { + return InputMediaSticker{ + Type: "sticker", + Media: media, + } +} + +// NewInputMediaLocation creates a new InputMediaLocation, which can be used +// as the media of a poll or a poll option. +func NewInputMediaLocation(latitude, longitude float64) InputMediaLocation { + return InputMediaLocation{ + Type: "location", + Latitude: latitude, + Longitude: longitude, + } +} + +// NewInputMediaVenue creates a new InputMediaVenue, which can be used as the +// media of a poll or a poll option. +func NewInputMediaVenue(latitude, longitude float64, title, address string) InputMediaVenue { + return InputMediaVenue{ + Type: "venue", + Latitude: latitude, + Longitude: longitude, + Title: title, + Address: address, + } +} + +// NewInputMediaLink creates a new InputMediaLink, which can be used as the +// media of a poll option. +func NewInputMediaLink(url string) InputMediaLink { + return InputMediaLink{ + Type: "link", + URL: url, + } +} + // NewContact allows you to send a shared contact. func NewContact(chatID int64, phoneNumber, firstName string) ContactConfig { return ContactConfig{ @@ -432,13 +516,13 @@ func NewInlineQueryResultPhoto(id, url string) InlineQueryResultPhoto { } } -// NewInlineQueryResultPhotoWithThumb creates a new inline query photo. -func NewInlineQueryResultPhotoWithThumb(id, url, thumb string) InlineQueryResultPhoto { +// NewInlineQueryResultPhotoWithThumbnail creates a new inline query photo. +func NewInlineQueryResultPhotoWithThumbnail(id, url, thumbnail string) InlineQueryResultPhoto { return InlineQueryResultPhoto{ - Type: "photo", - ID: id, - URL: url, - ThumbURL: thumb, + Type: "photo", + ID: id, + URL: url, + ThumbnailURL: thumbnail, } } @@ -597,6 +681,19 @@ func NewEditMessageCaption(chatID int64, messageID int, caption string) EditMess } } +// NewEditMessageMedia allows you to replace the media of a message. Media +// must be one of InputMediaAnimation, InputMediaAudio, InputMediaDocument, +// InputMediaLivePhoto, InputMediaPhoto or InputMediaVideo. +func NewEditMessageMedia(chatID int64, messageID int, media interface{}) EditMessageMediaConfig { + return EditMessageMediaConfig{ + BaseEdit: BaseEdit{ + ChatID: chatID, + MessageID: messageID, + }, + Media: media, + } +} + // NewEditMessageReplyMarkup allows you to edit the inline // keyboard markup. func NewEditMessageReplyMarkup(chatID int64, messageID int, replyMarkup InlineKeyboardMarkup) EditMessageReplyMarkupConfig { @@ -609,6 +706,71 @@ func NewEditMessageReplyMarkup(chatID int64, messageID int, replyMarkup InlineKe } } +// NewEditEphemeralMessageText allows you to edit the text of an ephemeral +// message sent to receiverUserID in the given chat. +func NewEditEphemeralMessageText(chatID, receiverUserID int64, ephemeralMessageID int, text string) EditEphemeralMessageTextConfig { + return EditEphemeralMessageTextConfig{ + BaseEphemeralEdit: BaseEphemeralEdit{ + ChatID: chatID, + ReceiverUserID: receiverUserID, + EphemeralMessageID: ephemeralMessageID, + }, + Text: text, + } +} + +// NewEditEphemeralMessageCaption allows you to edit the caption of an +// ephemeral message sent to receiverUserID in the given chat. +func NewEditEphemeralMessageCaption(chatID, receiverUserID int64, ephemeralMessageID int, caption string) EditEphemeralMessageCaptionConfig { + return EditEphemeralMessageCaptionConfig{ + BaseEphemeralEdit: BaseEphemeralEdit{ + ChatID: chatID, + ReceiverUserID: receiverUserID, + EphemeralMessageID: ephemeralMessageID, + }, + Caption: caption, + } +} + +// NewEditEphemeralMessageMedia allows you to edit the media of an ephemeral +// message sent to receiverUserID in the given chat. A new file can't be +// uploaded; use a previously uploaded file via its file_id, or specify a URL. +func NewEditEphemeralMessageMedia(chatID, receiverUserID int64, ephemeralMessageID int, media interface{}) EditEphemeralMessageMediaConfig { + return EditEphemeralMessageMediaConfig{ + BaseEphemeralEdit: BaseEphemeralEdit{ + ChatID: chatID, + ReceiverUserID: receiverUserID, + EphemeralMessageID: ephemeralMessageID, + }, + Media: media, + } +} + +// NewEditEphemeralMessageReplyMarkup allows you to edit the reply markup of an +// ephemeral message sent to receiverUserID in the given chat. +func NewEditEphemeralMessageReplyMarkup(chatID, receiverUserID int64, ephemeralMessageID int, replyMarkup InlineKeyboardMarkup) EditEphemeralMessageReplyMarkupConfig { + return EditEphemeralMessageReplyMarkupConfig{ + BaseEphemeralEdit: BaseEphemeralEdit{ + ChatID: chatID, + ReceiverUserID: receiverUserID, + EphemeralMessageID: ephemeralMessageID, + }, + ReplyMarkup: &replyMarkup, + } +} + +// NewDeleteEphemeralMessage allows you to delete an ephemeral message sent to +// receiverUserID in the given chat. +func NewDeleteEphemeralMessage(chatID, receiverUserID int64, ephemeralMessageID int) DeleteEphemeralMessageConfig { + return DeleteEphemeralMessageConfig{ + BaseEphemeralEdit: BaseEphemeralEdit{ + ChatID: chatID, + ReceiverUserID: receiverUserID, + EphemeralMessageID: ephemeralMessageID, + }, + } +} + // NewRemoveKeyboard hides the keyboard, with the option for being selective // or hiding for everyone. func NewRemoveKeyboard(selective bool) ReplyKeyboardRemove { @@ -813,13 +975,20 @@ func NewDeleteChatPhoto(chatID int64) DeleteChatPhotoConfig { } // NewPoll allows you to create a new poll. +// +// Option texts are wrapped into InputPollOption values. To set custom_emoji +// entities on options, build the SendPollConfig directly. func NewPoll(chatID int64, question string, options ...string) SendPollConfig { + opts := make([]InputPollOption, 0, len(options)) + for _, text := range options { + opts = append(opts, InputPollOption{Text: text}) + } return SendPollConfig{ BaseChat: BaseChat{ ChatID: chatID, }, Question: question, - Options: options, + Options: opts, IsAnonymous: true, // This is Telegram's default. } } @@ -985,3 +1154,12 @@ func ValidateWebAppData(token, telegramInitData string) (bool, error) { return true, nil } + +// todo add comment +func NewManagedBotLink(managerUsername, suggestedUsername, suggestedName string) string { + link := fmt.Sprintf("https://t.me/newbot/%s/%s", managerUsername, suggestedUsername) + if suggestedName != "" { + link += "?name=" + url.QueryEscape(suggestedName) + } + return link +} diff --git a/helpers_test.go b/helpers_test.go index 9119543d..8f42ec07 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -94,13 +94,13 @@ func TestNewInlineQueryResultPhoto(t *testing.T) { } } -func TestNewInlineQueryResultPhotoWithThumb(t *testing.T) { - result := NewInlineQueryResultPhotoWithThumb("id", "google.com", "thumb.com") +func TestNewInlineQueryResultPhotoWithThumbnail(t *testing.T) { + result := NewInlineQueryResultPhotoWithThumbnail("id", "google.com", "thumbnail.com") if result.Type != "photo" || result.ID != "id" || result.URL != "google.com" || - result.ThumbURL != "thumb.com" { + result.ThumbnailURL != "thumbnail.com" { t.Fail() } } diff --git a/internal/cmd/specdiff/main.go b/internal/cmd/specdiff/main.go new file mode 100644 index 00000000..9e982d47 --- /dev/null +++ b/internal/cmd/specdiff/main.go @@ -0,0 +1,609 @@ +// Command specdiff compares this library with the machine-readable Telegram +// Bot API specification and reports types, fields, methods and parameters +// that are missing from the Go code. +// +// Usage (from the repository root): +// +// go run ./internal/cmd/specdiff # report against the latest spec +// go run ./internal/cmd/specdiff -strict # exit with status 1 on gaps +// go run ./internal/cmd/specdiff -v # also list extra fields/params +// go run ./internal/cmd/specdiff -spec api.json +// +// The specification is the api.json published by +// https://github.com/PaulSonOfLars/telegram-bot-api-spec, which is generated +// from https://core.telegram.org/bots/api. +package main + +import ( + "encoding/json" + "flag" + "fmt" + "go/ast" + "go/parser" + "go/token" + "io" + "net/http" + "os" + "reflect" + "regexp" + "sort" + "strconv" + "strings" +) + +const defaultSpec = "https://raw.githubusercontent.com/PaulSonOfLars/telegram-bot-api-spec/main/api.json" + +// Go names that differ from the Bot API names. +var typeAliases = map[string]string{ + "MessageId": "MessageID", + "LoginUrl": "LoginURL", + "InlineQueryResultGif": "InlineQueryResultGIF", + "InlineQueryResultMpeg4Gif": "InlineQueryResultMPEG4GIF", + "InlineQueryResultCachedGif": "InlineQueryResultCachedGIF", + "InlineQueryResultCachedMpeg4Gif": "InlineQueryResultCachedMPEG4GIF", +} + +// Types that are not represented by a struct. +var skipTypes = map[string]bool{ + "InputFile": true, // RequestFileData +} + +// Wire fields that are routed by a custom (Un)MarshalJSON instead of a tag. +var customFields = map[string][]string{ + "OwnedGift": {"gift"}, + "RichBlock": {"caption"}, + "InputRichBlock": {"caption"}, +} + +// Methods implemented directly on BotAPI rather than through a config. +var directMethods = map[string]bool{ + "getMe": true, + "getWebhookInfo": true, +} + +type specField struct { + Name string `json:"name"` + Types []string `json:"types"` + Required bool `json:"required"` +} + +type specEntry struct { + Name string `json:"name"` + Fields []specField `json:"fields"` + Subtypes []string `json:"subtypes"` +} + +type spec struct { + Version string `json:"version"` + ReleaseDate string `json:"release_date"` + Methods map[string]specEntry `json:"methods"` + Types map[string]specEntry `json:"types"` +} + +type goField struct { + name string + json string + typ string + embedded bool +} + +type pkg struct { + structs map[string][]goField + aliases map[string]string // type X = Y + methods map[string]string // config type -> Bot API method + keys map[string][]string // config type -> parameter keys + calls map[string][]string // config type -> fields whose params()/addTo() are called + params map[string]bool // config types that define their own params() + consts map[string]string +} + +func main() { + specSource := flag.String("spec", defaultSpec, "URL or path of the Bot API api.json") + dir := flag.String("dir", ".", "directory of the tgbotapi package") + strict := flag.Bool("strict", false, "exit with status 1 if anything is missing") + verbose := flag.Bool("v", false, "also report fields and parameters that are not in the spec") + flag.Parse() + + s, err := loadSpec(*specSource) + if err != nil { + fmt.Fprintln(os.Stderr, "loading spec:", err) + os.Exit(2) + } + + p, err := loadPackage(*dir) + if err != nil { + fmt.Fprintln(os.Stderr, "parsing package:", err) + os.Exit(2) + } + + missing, extra := compare(s, p) + + fmt.Printf("Telegram %s (%s)\n\n", s.Version, s.ReleaseDate) + if len(missing) == 0 { + fmt.Println("Nothing is missing.") + } else { + fmt.Printf("Missing (%d):\n", len(missing)) + for _, m := range missing { + fmt.Println(" -", m) + } + } + if *verbose && len(extra) > 0 { + fmt.Printf("\nNot in the spec (%d):\n", len(extra)) + for _, e := range extra { + fmt.Println(" -", e) + } + } + + if *strict && len(missing) > 0 { + os.Exit(1) + } +} + +func loadSpec(source string) (*spec, error) { + var r io.Reader + if strings.HasPrefix(source, "http://") || strings.HasPrefix(source, "https://") { + resp, err := http.Get(source) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("GET %s: %s", source, resp.Status) + } + r = resp.Body + } else { + f, err := os.Open(source) + if err != nil { + return nil, err + } + defer f.Close() + r = f + } + + var s spec + if err := json.NewDecoder(r).Decode(&s); err != nil { + return nil, err + } + + return &s, nil +} + +func exprString(e ast.Expr) string { + switch t := e.(type) { + case *ast.Ident: + return t.Name + case *ast.StarExpr: + return "*" + exprString(t.X) + case *ast.ArrayType: + return "[]" + exprString(t.Elt) + case *ast.SelectorExpr: + return exprString(t.X) + "." + t.Sel.Name + case *ast.MapType: + return "map[" + exprString(t.Key) + "]" + exprString(t.Value) + case *ast.InterfaceType: + return "interface{}" + } + return "?" +} + +func loadPackage(dir string) (*pkg, error) { + fset := token.NewFileSet() + pkgs, err := parser.ParseDir(fset, dir, func(fi os.FileInfo) bool { + return !strings.HasSuffix(fi.Name(), "_test.go") + }, 0) + if err != nil { + return nil, err + } + + p := &pkg{ + structs: map[string][]goField{}, + aliases: map[string]string{}, + methods: map[string]string{}, + keys: map[string][]string{}, + calls: map[string][]string{}, + params: map[string]bool{}, + consts: map[string]string{}, + } + + for _, astPkg := range pkgs { + for _, f := range astPkg.Files { + for _, decl := range f.Decls { + switch d := decl.(type) { + case *ast.GenDecl: + p.addGenDecl(d) + case *ast.FuncDecl: + p.addFuncDecl(d) + } + } + } + } + + return p, nil +} + +func (p *pkg) addGenDecl(d *ast.GenDecl) { + for _, spec := range d.Specs { + switch s := spec.(type) { + case *ast.TypeSpec: + if s.Assign != 0 { + p.aliases[s.Name.Name] = exprString(s.Type) + continue + } + st, ok := s.Type.(*ast.StructType) + if !ok { + continue + } + var fields []goField + for _, fl := range st.Fields.List { + tag := "" + if fl.Tag != nil { + v, _ := strconv.Unquote(fl.Tag.Value) + tag = strings.Split(reflect.StructTag(v).Get("json"), ",")[0] + } + typ := exprString(fl.Type) + if len(fl.Names) == 0 { + fields = append(fields, goField{name: strings.TrimPrefix(typ, "*"), json: tag, typ: typ, embedded: true}) + } + for _, n := range fl.Names { + fields = append(fields, goField{name: n.Name, json: tag, typ: typ}) + } + } + p.structs[s.Name.Name] = fields + case *ast.ValueSpec: + for i, n := range s.Names { + if i >= len(s.Values) { + continue + } + if lit, ok := s.Values[i].(*ast.BasicLit); ok && lit.Kind == token.STRING { + p.consts[n.Name], _ = strconv.Unquote(lit.Value) + } + } + } + } +} + +func (p *pkg) addFuncDecl(d *ast.FuncDecl) { + if d.Recv == nil || len(d.Recv.List) == 0 || d.Body == nil { + return + } + recv := strings.TrimPrefix(exprString(d.Recv.List[0].Type), "*") + + switch d.Name.Name { + case "method": + ast.Inspect(d.Body, func(n ast.Node) bool { + ret, ok := n.(*ast.ReturnStmt) + if !ok || len(ret.Results) != 1 { + return true + } + switch v := ret.Results[0].(type) { + case *ast.BasicLit: + p.methods[recv], _ = strconv.Unquote(v.Value) + case *ast.Ident: + p.methods[recv] = "$" + v.Name + } + return true + }) + case "params", "addTo": + ast.Inspect(d.Body, func(n ast.Node) bool { + switch c := n.(type) { + case *ast.CallExpr: + sel, ok := c.Fun.(*ast.SelectorExpr) + if !ok { + return true + } + if sel.Sel.Name == "params" || sel.Sel.Name == "addTo" { + if inner, ok := sel.X.(*ast.SelectorExpr); ok { + p.calls[recv] = append(p.calls[recv], inner.Sel.Name) + } + return true + } + if len(c.Args) > 0 { + if lit, ok := c.Args[0].(*ast.BasicLit); ok && lit.Kind == token.STRING { + key, _ := strconv.Unquote(lit.Value) + p.keys[recv] = append(p.keys[recv], key) + } + } + case *ast.IndexExpr: + if lit, ok := c.Index.(*ast.BasicLit); ok && lit.Kind == token.STRING { + key, _ := strconv.Unquote(lit.Value) + p.keys[recv] = append(p.keys[recv], key) + } + } + return true + }) + if d.Name.Name == "params" { + p.params[recv] = true + } + case "files": + ast.Inspect(d.Body, func(n ast.Node) bool { + kv, ok := n.(*ast.KeyValueExpr) + if !ok { + return true + } + if id, ok := kv.Key.(*ast.Ident); ok && id.Name == "Name" { + if lit, ok := kv.Value.(*ast.BasicLit); ok && lit.Kind == token.STRING { + key, _ := strconv.Unquote(lit.Value) + p.keys[recv] = append(p.keys[recv], key) + } + } + return true + }) + } +} + +func (p *pkg) resolve(name string) string { + for { + target, ok := p.aliases[name] + if !ok { + return name + } + name = target + } +} + +// jsonFields returns the wire fields of a struct, including embedded ones. +func (p *pkg) jsonFields(name string, seen map[string]bool) map[string]goField { + name = p.resolve(name) + out := map[string]goField{} + if seen[name] { + return out + } + seen[name] = true + + for _, f := range p.structs[name] { + switch { + case f.embedded && f.json == "": + for k, v := range p.jsonFields(f.name, seen) { + out[k] = v + } + case f.json != "" && f.json != "-": + out[f.json] = f + } + } + for _, k := range customFields[name] { + out[k] = goField{name: k, json: k, typ: "custom"} + } + + return out +} + +// paramKeys returns the parameter keys sent by a config, following calls to +// the params() of embedded base configs. +func (p *pkg) paramKeys(cfg string, seen map[string]bool) map[string]bool { + cfg = p.resolve(cfg) + out := map[string]bool{} + if seen[cfg] { + return out + } + seen[cfg] = true + + for _, k := range p.keys[cfg] { + out[k] = true + } + + fieldTypes := map[string]string{} + var embedded []string + for _, f := range p.structs[cfg] { + fieldTypes[f.name] = strings.TrimPrefix(f.typ, "*") + if f.embedded { + embedded = append(embedded, f.name) + } + } + + calls := p.calls[cfg] + if !p.params[cfg] { + // params() is promoted from the embedded structs. + calls = append(calls, embedded...) + } + for _, c := range calls { + typ, ok := fieldTypes[c] + if !ok { + typ = c + } + for k := range p.paramKeys(typ, seen) { + out[k] = true + } + } + + return out +} + +var arrayOf = regexp.MustCompile(`^Array of (.+)$`) + +func (p *pkg) goTypeFor(specType string) string { + if m := arrayOf.FindStringSubmatch(specType); m != nil { + return "[]" + p.goTypeFor(m[1]) + } + switch specType { + case "Integer": + return "int" + case "String": + return "string" + case "Boolean", "True": + return "bool" + case "Float": + return "float64" + case "InputFile": + return "RequestFileData" + } + if alias, ok := typeAliases[specType]; ok { + return alias + } + return specType +} + +func normalize(t string) string { + t = strings.TrimPrefix(t, "*") + t = strings.ReplaceAll(t, "[]*", "[]") + t = strings.ReplaceAll(t, "int64", "int") + return t +} + +func (p *pkg) typeMatches(specTypes []string, goType string) bool { + g := normalize(goType) + switch g { + case "interface{}", "any", "json.RawMessage", "custom": + return true + } + for _, st := range specTypes { + if normalize(p.goTypeFor(st)) == g { + return true + } + // Files are always represented as RequestFileData, and a message that + // may be inaccessible is represented as a Message. + if st == "String" && g == "RequestFileData" { + return true + } + if st == "MaybeInaccessibleMessage" && g == "Message" { + return true + } + } + return false +} + +func compare(s *spec, p *pkg) (missing, extra []string) { + // Union types are represented either as a single flat struct with a type + // discriminator, or as one struct per variant. + covered := map[string]bool{} + var markCovered func(string) + markCovered = func(name string) { + covered[name] = true + for _, sub := range s.Types[name].Subtypes { + markCovered(sub) + } + } + + unionFields := func(name string) map[string][]specField { + out := map[string][]specField{} + var walk func(string) + walk = func(n string) { + t, ok := s.Types[n] + if !ok { + return + } + for _, sub := range t.Subtypes { + walk(sub) + } + for _, f := range t.Fields { + out[f.Name] = append(out[f.Name], f) + } + } + walk(name) + return out + } + + for _, name := range sortedKeys(s.Types) { + t := s.Types[name] + goName := p.goTypeFor(name) + if len(t.Subtypes) == 0 || len(p.structs[goName]) == 0 { + continue + } + markCovered(name) + have := p.jsonFields(goName, map[string]bool{}) + want := unionFields(name) + for _, k := range sortedKeys(want) { + f, ok := have[k] + if !ok { + missing = append(missing, fmt.Sprintf("field %s.%s", name, k)) + continue + } + var types []string + for _, sf := range want[k] { + types = append(types, sf.Types...) + } + if !p.typeMatches(types, f.typ) { + missing = append(missing, fmt.Sprintf("field %s.%s has type %s, want %s", name, k, f.typ, strings.Join(types, " or "))) + } + } + for _, k := range sortedKeys(have) { + if _, ok := want[k]; !ok { + extra = append(extra, fmt.Sprintf("field %s.%s", name, k)) + } + } + } + + for _, name := range sortedKeys(s.Types) { + t := s.Types[name] + if skipTypes[name] || len(t.Subtypes) > 0 { + continue + } + goName := p.goTypeFor(name) + if _, ok := p.structs[goName]; !ok { + if !covered[name] { + missing = append(missing, "type "+name) + } + continue + } + have := p.jsonFields(goName, map[string]bool{}) + want := map[string]bool{} + for _, f := range t.Fields { + want[f.Name] = true + gf, ok := have[f.Name] + if !ok { + missing = append(missing, fmt.Sprintf("field %s.%s", name, f.Name)) + continue + } + if !p.typeMatches(f.Types, gf.typ) { + missing = append(missing, fmt.Sprintf("field %s.%s has type %s, want %s", name, f.Name, gf.typ, strings.Join(f.Types, " or "))) + } + } + for _, k := range sortedKeys(have) { + if !want[k] { + extra = append(extra, fmt.Sprintf("field %s.%s", name, k)) + } + } + } + + configs := map[string][]string{} + for cfg, method := range p.methods { + if strings.HasPrefix(method, "$") { + method = p.consts[method[1:]] + } + configs[method] = append(configs[method], cfg) + } + + for _, name := range sortedKeys(s.Methods) { + cfgs := configs[name] + if len(cfgs) == 0 { + if !directMethods[name] { + missing = append(missing, "method "+name) + } + continue + } + sort.Strings(cfgs) + want := map[string]bool{} + for _, f := range s.Methods[name].Fields { + want[f.Name] = true + } + for _, cfg := range cfgs { + have := p.paramKeys(cfg, map[string]bool{}) + for _, k := range sortedKeys(want) { + if !have[k] { + missing = append(missing, fmt.Sprintf("parameter %s(%s) in %s", name, k, cfg)) + } + } + for _, k := range sortedKeys(have) { + if !want[k] { + extra = append(extra, fmt.Sprintf("parameter %s(%s) in %s", name, k, cfg)) + } + } + } + } + + for _, method := range sortedKeys(configs) { + if _, ok := s.Methods[method]; !ok { + missing = append(missing, fmt.Sprintf("method %s used by %s is not in the spec", method, strings.Join(configs[method], ", "))) + } + } + + return missing, extra +} + +func sortedKeys[V any](m map[string]V) []string { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} diff --git a/nested_media.go b/nested_media.go new file mode 100644 index 00000000..29f30095 --- /dev/null +++ b/nested_media.go @@ -0,0 +1,209 @@ +package tgbotapi + +import "fmt" + +// nestedMediaUploader rewrites InputMedia* values nested inside JSON-serialized +// parameters (poll media, rich messages) so that every file which needs +// uploading is replaced by an "attach://" reference, and collects the +// matching RequestFile entries. +// +// Names are generated sequentially from prefix, so a config's params() and +// files() produce the same names as long as both walk the media in the same +// order (which they do by calling the same prepare function). +type nestedMediaUploader struct { + prefix string + files []RequestFile +} + +func (u *nestedMediaUploader) attach(data RequestFileData) RequestFileData { + if data == nil || !data.NeedsUpload() { + return data + } + + name := fmt.Sprintf("%s-%d", u.prefix, len(u.files)) + u.files = append(u.files, RequestFile{Name: name, Data: data}) + + return fileAttach("attach://" + name) +} + +func (u *nestedMediaUploader) base(b BaseInputMedia) BaseInputMedia { + b.Media = u.attach(b.Media) + return b +} + +func (u *nestedMediaUploader) photo(m *InputMediaPhoto) *InputMediaPhoto { + if m == nil { + return nil + } + c := *m + c.BaseInputMedia = u.base(c.BaseInputMedia) + return &c +} + +func (u *nestedMediaUploader) video(m *InputMediaVideo) *InputMediaVideo { + if m == nil { + return nil + } + c := *m + c.BaseInputMedia = u.base(c.BaseInputMedia) + c.Thumbnail = u.attach(c.Thumbnail) + c.Cover = u.attach(c.Cover) + return &c +} + +func (u *nestedMediaUploader) animation(m *InputMediaAnimation) *InputMediaAnimation { + if m == nil { + return nil + } + c := *m + c.BaseInputMedia = u.base(c.BaseInputMedia) + c.Thumbnail = u.attach(c.Thumbnail) + return &c +} + +func (u *nestedMediaUploader) audio(m *InputMediaAudio) *InputMediaAudio { + if m == nil { + return nil + } + c := *m + c.BaseInputMedia = u.base(c.BaseInputMedia) + c.Thumbnail = u.attach(c.Thumbnail) + return &c +} + +func (u *nestedMediaUploader) document(m *InputMediaDocument) *InputMediaDocument { + if m == nil { + return nil + } + c := *m + c.BaseInputMedia = u.base(c.BaseInputMedia) + c.Thumbnail = u.attach(c.Thumbnail) + return &c +} + +func (u *nestedMediaUploader) voiceNote(m *InputMediaVoiceNote) *InputMediaVoiceNote { + if m == nil { + return nil + } + c := *m + c.BaseInputMedia = u.base(c.BaseInputMedia) + return &c +} + +func (u *nestedMediaUploader) livePhoto(m *InputMediaLivePhoto) *InputMediaLivePhoto { + if m == nil { + return nil + } + c := *m + c.BaseInputMedia = u.base(c.BaseInputMedia) + c.Photo = u.attach(c.Photo) + return &c +} + +func (u *nestedMediaUploader) sticker(m *InputMediaSticker) *InputMediaSticker { + if m == nil { + return nil + } + c := *m + c.Media = u.attach(c.Media) + return &c +} + +// media prepares an InputMedia* value or pointer of any kind. Values keep +// their value type and pointers keep their pointer type; anything else (for +// example InputMediaLocation, which carries no file) is returned unchanged. +func (u *nestedMediaUploader) media(m any) any { + switch v := m.(type) { + case InputMediaPhoto: + return *u.photo(&v) + case *InputMediaPhoto: + return u.photo(v) + case InputMediaVideo: + return *u.video(&v) + case *InputMediaVideo: + return u.video(v) + case InputMediaAnimation: + return *u.animation(&v) + case *InputMediaAnimation: + return u.animation(v) + case InputMediaAudio: + return *u.audio(&v) + case *InputMediaAudio: + return u.audio(v) + case InputMediaDocument: + return *u.document(&v) + case *InputMediaDocument: + return u.document(v) + case InputMediaVoiceNote: + return *u.voiceNote(&v) + case *InputMediaVoiceNote: + return u.voiceNote(v) + case InputMediaLivePhoto: + return *u.livePhoto(&v) + case *InputMediaLivePhoto: + return u.livePhoto(v) + case InputMediaSticker: + return *u.sticker(&v) + case *InputMediaSticker: + return u.sticker(v) + } + + return m +} + +func (u *nestedMediaUploader) richBlocks(blocks []InputRichBlock) []InputRichBlock { + if blocks == nil { + return nil + } + + out := make([]InputRichBlock, len(blocks)) + for i, b := range blocks { + b.Blocks = u.richBlocks(b.Blocks) + if b.Items != nil { + items := make([]InputRichBlockListItem, len(b.Items)) + for j, item := range b.Items { + item.Blocks = u.richBlocks(item.Blocks) + items[j] = item + } + b.Items = items + } + b.Animation = u.animation(b.Animation) + b.Audio = u.audio(b.Audio) + b.Document = u.document(b.Document) + b.Photo = u.photo(b.Photo) + b.Video = u.video(b.Video) + b.VoiceNote = u.voiceNote(b.VoiceNote) + out[i] = b + } + + return out +} + +// richMessage returns a copy of m in which every file that needs uploading, +// in Media and anywhere in Blocks, is replaced by an attach:// reference. +func (u *nestedMediaUploader) richMessage(m *InputRichMessage) *InputRichMessage { + if m == nil { + return nil + } + + c := *m + c.Blocks = u.richBlocks(c.Blocks) + if c.Media != nil { + media := make([]InputRichMessageMedia, len(c.Media)) + for i, item := range c.Media { + item.Media = u.media(item.Media) + media[i] = item + } + c.Media = media + } + + return &c +} + +// prepareRichMessage returns the rich message to serialize and the files +// that must be uploaded alongside it. +func prepareRichMessage(m *InputRichMessage) (*InputRichMessage, []RequestFile) { + u := nestedMediaUploader{prefix: "rich-media"} + prepared := u.richMessage(m) + return prepared, u.files +} diff --git a/params.go b/params.go index 134f85e4..df93967e 100644 --- a/params.go +++ b/params.go @@ -60,6 +60,11 @@ func (p Params) AddInterface(key string, value interface{}) error { return nil } +// AddAny adds a value of any type by JSON-marshalling it. +func (p Params) AddAny(key string, value any) error { + return p.AddInterface(key, value) +} + // AddFirstValid attempts to add the first item that is not a default value. // // For example, AddFirstValid(0, "", "test") would add "test". diff --git a/passport.go b/passport.go index 4fedb965..73a076b0 100644 --- a/passport.go +++ b/passport.go @@ -111,6 +111,19 @@ type ( // "identity_card" and "internal_passport". The file can be decrypted // and verified using the accompanying EncryptedCredentials. Selfie *PassportFile `json:"selfie,omitempty"` + + // Array of encrypted files with translated versions of documents + // provided by the user; available if requested for "passport", + // "driver_license", "identity_card", "internal_passport", + // "utility_bill", "bank_statement", "rental_agreement", + // "passport_registration" and "temporary_registration" types. Files + // can be decrypted and verified using the accompanying + // EncryptedCredentials. + Translation []PassportFile `json:"translation,omitempty"` + + // Base64-encoded element hash for using in + // PassportElementErrorUnspecified + Hash string `json:"hash"` } // EncryptedCredentials contains data required for decrypting and @@ -248,6 +261,64 @@ type ( Message string `json:"message"` } + // PassportElementErrorTranslationFile represents an issue with one of the + // files that constitute the translation of a document. The error is + // considered resolved when the file changes. + PassportElementErrorTranslationFile struct { + // Error source, must be translation_file + Source string `json:"source"` + + // Type of element of the user's Telegram Passport which has the + // issue, one of "passport", "driver_license", "identity_card", + // "internal_passport", "utility_bill", "bank_statement", + // "rental_agreement", "passport_registration", + // "temporary_registration" + Type string `json:"type"` + + // Base64-encoded file hash + FileHash string `json:"file_hash"` + + // Error message + Message string `json:"message"` + } + + // PassportElementErrorTranslationFiles represents an issue with the + // translated version of a document. The error is considered resolved + // when a file with the document translation changes. + PassportElementErrorTranslationFiles struct { + // Error source, must be translation_files + Source string `json:"source"` + + // Type of element of the user's Telegram Passport which has the + // issue, one of "passport", "driver_license", "identity_card", + // "internal_passport", "utility_bill", "bank_statement", + // "rental_agreement", "passport_registration", + // "temporary_registration" + Type string `json:"type"` + + // List of base64-encoded file hashes + FileHashes []string `json:"file_hashes"` + + // Error message + Message string `json:"message"` + } + + // PassportElementErrorUnspecified represents an issue in an unspecified + // place. The error is considered resolved when new data is added. + PassportElementErrorUnspecified struct { + // Error source, must be unspecified + Source string `json:"source"` + + // Type of element of the user's Telegram Passport which has the issue + Type string `json:"type"` + + // Base64-encoded element hash + ElementHash string `json:"element_hash"` + + // Error message + Message string `json:"message"` + } + // Credentials contains encrypted data. Credentials struct { Data SecureData `json:"secure_data"` diff --git a/richmessage_test.go b/richmessage_test.go new file mode 100644 index 00000000..78360013 --- /dev/null +++ b/richmessage_test.go @@ -0,0 +1,222 @@ +package tgbotapi + +import ( + "encoding/json" + "reflect" + "testing" +) + +// TestRichTextUnmarshalForms verifies that the polymorphic RichText type +// decodes the three wire forms: plain string, array, and styled span. +func TestRichTextUnmarshalForms(t *testing.T) { + var plain RichText + if err := json.Unmarshal([]byte(`"hello"`), &plain); err != nil { + t.Fatalf("plain unmarshal: %v", err) + } + if !plain.IsPlain || plain.PlainText != "hello" { + t.Fatalf("plain form not decoded: %+v", plain) + } + + var arr RichText + if err := json.Unmarshal([]byte(`["a","b"]`), &arr); err != nil { + t.Fatalf("array unmarshal: %v", err) + } + if len(arr.Parts) != 2 || arr.Parts[0].PlainText != "a" { + t.Fatalf("array form not decoded: %+v", arr) + } + + var span RichText + if err := json.Unmarshal([]byte(`{"type":"url","text":"site","url":"https://example.com"}`), &span); err != nil { + t.Fatalf("span unmarshal: %v", err) + } + if span.Type != RichTextTypeURL || span.URL != "https://example.com" { + t.Fatalf("span form not decoded: %+v", span) + } + if span.Text == nil || !span.Text.IsPlain || span.Text.PlainText != "site" { + t.Fatalf("nested span text not decoded: %+v", span.Text) + } +} + +// TestRichTextMarshalRoundTrip verifies that each RichText form survives a +// decode/encode/decode cycle unchanged. +func TestRichTextMarshalRoundTrip(t *testing.T) { + cases := []string{ + `"plain"`, + `["a","b"]`, + `{"type":"bold","text":"x"}`, + `{"type":"date_time","text":"now","unix_time":1700000000,"date_time_format":"d MMM"}`, + `{"type":"text_mention","text":"u","user":{"id":1,"is_bot":true,"first_name":"A"}}`, + } + for _, in := range cases { + var first RichText + if err := json.Unmarshal([]byte(in), &first); err != nil { + t.Fatalf("unmarshal %s: %v", in, err) + } + out, err := json.Marshal(first) + if err != nil { + t.Fatalf("marshal %s: %v", in, err) + } + var second RichText + if err := json.Unmarshal(out, &second); err != nil { + t.Fatalf("re-unmarshal %s: %v", out, err) + } + // Raw differs by construction (only set on decode), so clear it. + first.Raw, second.Raw = nil, nil + if !reflect.DeepEqual(first, second) { + t.Errorf("round trip mismatch:\n in: %s\n out: %s", in, out) + } + } +} + +// TestRichBlockCaptionRouting verifies that the shared "caption" wire field is +// routed to TableCaption for a table block and to Caption for a media block. +func TestRichBlockCaptionRouting(t *testing.T) { + var table RichBlock + if err := json.Unmarshal([]byte(`{"type":"table","cells":[],"caption":"a table"}`), &table); err != nil { + t.Fatalf("table unmarshal: %v", err) + } + if table.TableCaption == nil || table.TableCaption.PlainText != "a table" { + t.Fatalf("table caption not routed to TableCaption: %+v", table) + } + if table.Caption != nil { + t.Fatalf("table caption wrongly set Caption: %+v", table.Caption) + } + + var photo RichBlock + if err := json.Unmarshal([]byte(`{"type":"photo","photo":[],"caption":{"text":"pic"}}`), &photo); err != nil { + t.Fatalf("photo unmarshal: %v", err) + } + if photo.Caption == nil || !photo.Caption.Text.IsPlain || photo.Caption.Text.PlainText != "pic" { + t.Fatalf("photo caption not routed to Caption: %+v", photo) + } + if photo.TableCaption != nil { + t.Fatalf("photo caption wrongly set TableCaption: %+v", photo.TableCaption) + } + + // The table caption must marshal back out under the shared "caption" key. + out, err := json.Marshal(table) + if err != nil { + t.Fatalf("table marshal: %v", err) + } + var check map[string]json.RawMessage + if err := json.Unmarshal(out, &check); err != nil { + t.Fatalf("remarshal check: %v", err) + } + if string(check["caption"]) != `"a table"` { + t.Errorf("table caption not emitted under caption key: %s", out) + } +} + +// TestRichMessageDecode verifies a full RichMessage with nested blocks decodes. +func TestRichMessageDecode(t *testing.T) { + raw := `{"blocks":[{"type":"heading","text":"Title","size":1},{"type":"paragraph","text":["plain ",{"type":"bold","text":"bold"}]}],"is_rtl":false}` + var rm RichMessage + if err := json.Unmarshal([]byte(raw), &rm); err != nil { + t.Fatalf("rich message unmarshal: %v", err) + } + if len(rm.Blocks) != 2 { + t.Fatalf("expected 2 blocks, got %d", len(rm.Blocks)) + } + if rm.Blocks[0].Type != RichBlockTypeHeading || rm.Blocks[0].Size != 1 { + t.Errorf("heading block not decoded: %+v", rm.Blocks[0]) + } + para := rm.Blocks[1] + if para.Type != RichBlockTypeParagraph || para.Text == nil || len(para.Text.Parts) != 2 { + t.Fatalf("paragraph block not decoded: %+v", para) + } + if para.Text.Parts[1].Type != RichTextTypeBold { + t.Errorf("nested bold span not decoded: %+v", para.Text.Parts[1]) + } +} + +// TestInputRichMessageParams verifies the send-side config serializes the +// rich_message parameter as JSON. +func TestInputRichMessageParams(t *testing.T) { + cfg := NewRichMessage(123, InputRichMessage{HTML: "hi"}) + params, err := cfg.params() + if err != nil { + t.Fatalf("params: %v", err) + } + if params["chat_id"] != "123" { + t.Errorf("chat_id = %q", params["chat_id"]) + } + if cfg.method() != "sendRichMessage" { + t.Errorf("method = %q", cfg.method()) + } + var got InputRichMessage + if err := json.Unmarshal([]byte(params["rich_message"]), &got); err != nil { + t.Fatalf("rich_message param not valid JSON: %v", err) + } + if got.HTML != "hi" { + t.Errorf("rich_message html = %q", got.HTML) + } +} + +// TestRichBlockButtonsAndDocument verifies the Bot API 10.3 block types: +// "buttons", "document", and "expandable_blockquote", plus the "button" span +// and the is_compact table flag. +func TestRichBlockButtonsAndDocument(t *testing.T) { + buttons := InputRichBlock{ + Type: RichBlockTypeButtons, + Align: "center", + Buttons: []RichMessageButton{{ + Text: RichText{IsPlain: true, PlainText: "Press"}, + Style: "primary", + CallbackData: "cb", + }}, + } + out, err := json.Marshal(buttons) + if err != nil { + t.Fatalf("marshal buttons block: %v", err) + } + want := `{"type":"buttons","buttons":[{"text":"Press","style":"primary","callback_data":"cb"}],"align":"center"}` + if string(out) != want { + t.Errorf("buttons block encoding:\n got: %s\n want: %s", out, want) + } + + raw := `{"type":"document","document":{"file_id":"f1","file_unique_id":"u1"},` + + `"caption":{"text":"a file"}}` + var doc RichBlock + if err := json.Unmarshal([]byte(raw), &doc); err != nil { + t.Fatalf("unmarshal document block: %v", err) + } + if doc.Document == nil || doc.Document.FileID != "f1" { + t.Errorf("document not decoded: %+v", doc.Document) + } + if doc.Caption == nil || !doc.Caption.Text.IsPlain || doc.Caption.Text.PlainText != "a file" { + t.Errorf("document caption not routed: %+v", doc.Caption) + } + + quote := InputRichBlock{ + Type: RichBlockTypeExpandableBlockquote, + Text: &RichText{IsPlain: true, PlainText: "long quote"}, + } + out, err = json.Marshal(quote) + if err != nil { + t.Fatalf("marshal expandable blockquote: %v", err) + } + if got, want := string(out), `{"type":"expandable_blockquote","text":"long quote"}`; got != want { + t.Errorf("expandable blockquote encoding:\n got: %s\n want: %s", got, want) + } + + table := InputRichBlock{Type: RichBlockTypeTable, IsCompact: true} + out, err = json.Marshal(table) + if err != nil { + t.Fatalf("marshal table: %v", err) + } + if got, want := string(out), `{"type":"table","is_compact":true}`; got != want { + t.Errorf("compact table encoding:\n got: %s\n want: %s", got, want) + } + + span := RichText{Type: RichTextTypeButton, Button: &RichMessageButton{ + Text: RichText{IsPlain: true, PlainText: "Go"}, + URL: "https://example.com", + }} + out, err = json.Marshal(span) + if err != nil { + t.Fatalf("marshal button span: %v", err) + } + if got, want := string(out), `{"type":"button","button":{"text":"Go","url":"https://example.com"}}`; got != want { + t.Errorf("button span encoding:\n got: %s\n want: %s", got, want) + } +} diff --git a/spec_fixes_test.go b/spec_fixes_test.go new file mode 100644 index 00000000..ef7b66d6 --- /dev/null +++ b/spec_fixes_test.go @@ -0,0 +1,241 @@ +package tgbotapi + +import ( + "encoding/json" + "strings" + "testing" +) + +func TestOwnedGiftGiftRouting(t *testing.T) { + var regular OwnedGift + if err := json.Unmarshal([]byte(`{"type":"regular","gift":{"id":"g1","star_count":10},"send_date":1}`), ®ular); err != nil { + t.Fatalf("regular unmarshal: %v", err) + } + if regular.Gift == nil || regular.Gift.ID != "g1" || regular.UniqueGift != nil { + t.Fatalf("regular gift not routed to Gift: %+v", regular) + } + + var unique OwnedGift + if err := json.Unmarshal([]byte(`{"type":"unique","gift":{"base_name":"Cap","name":"Cap-1","number":1},"send_date":1}`), &unique); err != nil { + t.Fatalf("unique unmarshal: %v", err) + } + if unique.UniqueGift == nil || unique.UniqueGift.Name != "Cap-1" || unique.Gift != nil { + t.Fatalf("unique gift not routed to UniqueGift: %+v", unique) + } + + out, err := json.Marshal(unique) + if err != nil { + t.Fatalf("marshal: %v", err) + } + if !strings.Contains(string(out), `"gift":{`) || strings.Contains(string(out), "unique_gift") { + t.Fatalf("unique gift not emitted under \"gift\": %s", out) + } +} + +func TestSpecTypesDecode(t *testing.T) { + var video Video + if err := json.Unmarshal([]byte(`{"file_id":"v","file_unique_id":"u","width":1,"height":1,"duration":1, + "qualities":[{"file_id":"q","file_unique_id":"qu","width":1920,"height":1080,"codec":"av01","file_size":42}]}`), &video); err != nil { + t.Fatalf("video unmarshal: %v", err) + } + if len(video.Qualities) != 1 || video.Qualities[0].Codec != "av01" || video.Qualities[0].Height != 1080 || video.Qualities[0].FileSize != 42 { + t.Fatalf("video qualities not decoded: %+v", video.Qualities) + } + + var chat ChatFullInfo + if err := json.Unmarshal([]byte(`{"id":1,"type":"private","bio":"hi","accent_color_id":3,"max_reaction_count":11, + "accepted_gift_types":{},"rating":{"level":2,"rating":150,"current_level_rating":100,"next_level_rating":200}, + "unique_gift_colors":{"model_custom_emoji_id":"m","symbol_custom_emoji_id":"s","light_theme_main_color":1, + "light_theme_other_colors":[2,3],"dark_theme_main_color":4,"dark_theme_other_colors":[5]}}`), &chat); err != nil { + t.Fatalf("chat unmarshal: %v", err) + } + if chat.ID != 1 || chat.Bio != "hi" || chat.AccentColorID != 3 { + t.Fatalf("chat full info fields not decoded: %+v", chat) + } + if chat.Rating == nil || chat.Rating.Level != 2 || chat.Rating.NextLevelRating != 200 { + t.Fatalf("user rating not decoded: %+v", chat.Rating) + } + if chat.UniqueGiftColors == nil || len(chat.UniqueGiftColors.LightThemeOtherColors) != 2 { + t.Fatalf("unique gift colors not decoded: %+v", chat.UniqueGiftColors) + } + + var msg Message + if err := json.Unmarshal([]byte(`{"message_id":1,"date":1,"chat":{"id":1,"type":"group"}, + "poll_option_added":{"option_persistent_id":"p","option_text":"new","option_text_entities":[{"type":"bold","offset":0,"length":3}]}, + "chat_owner_left":{"new_owner":{"id":7,"is_bot":false,"first_name":"N"}}, + "entities":[{"type":"date_time","offset":0,"length":1,"unix_time":1700000000,"date_time_format":"wDT"}]}`), &msg); err != nil { + t.Fatalf("message unmarshal: %v", err) + } + if msg.PollOptionAdded == nil || msg.PollOptionAdded.OptionText != "new" || len(msg.PollOptionAdded.OptionTextEntities) != 1 { + t.Fatalf("poll option added not decoded: %+v", msg.PollOptionAdded) + } + if msg.ChatOwnerLeft == nil || msg.ChatOwnerLeft.NewOwner == nil || msg.ChatOwnerLeft.NewOwner.ID != 7 { + t.Fatalf("chat owner left not decoded: %+v", msg.ChatOwnerLeft) + } + if len(msg.Entities) != 1 || msg.Entities[0].UnixTime != 1700000000 || msg.Entities[0].DateTimeFormat != "wDT" { + t.Fatalf("date_time entity not decoded: %+v", msg.Entities) + } +} + +func TestUpdateFromChatInlineCallback(t *testing.T) { + // Callback queries from inline messages carry no message; FromChat must + // not panic. + update := Update{CallbackQuery: &CallbackQuery{ID: "1", InlineMessageID: "inline"}} + if chat := update.FromChat(); chat != nil { + t.Fatalf("FromChat() = %+v, want nil", chat) + } + + update = Update{ChatJoinRequest: &ChatJoinRequest{Chat: Chat{ID: 5}, From: User{ID: 6}}} + if chat := update.FromChat(); chat == nil || chat.ID != 5 { + t.Fatalf("FromChat() for chat join request = %+v, want chat 5", chat) + } + if from := update.SentFrom(); from == nil || from.ID != 6 { + t.Fatalf("SentFrom() for chat join request = %+v, want user 6", from) + } +} + +func paramsOf(t *testing.T, c Chattable) Params { + t.Helper() + params, err := c.params() + if err != nil { + t.Fatalf("%T.params(): %v", c, err) + } + return params +} + +func TestSpecMethodParams(t *testing.T) { + params := paramsOf(t, RepostStoryConfig{BusinessConnectionID: "bc", FromChatID: 1, FromStoryID: 2, ActivePeriod: 86400}) + for key, want := range map[string]string{"business_connection_id": "bc", "from_chat_id": "1", "from_story_id": "2", "active_period": "86400"} { + if params[key] != want { + t.Errorf("repostStory %s = %q, want %q", key, params[key], want) + } + } + + params = paramsOf(t, GetChatGiftsConfig{ChatID: 1, ExcludeUnsaved: true, SortByPrice: true, ExcludeFromBlockchain: true}) + for _, key := range []string{"exclude_unsaved", "sort_by_price", "exclude_from_blockchain"} { + if params[key] != "true" { + t.Errorf("getChatGifts %s = %q, want true", key, params[key]) + } + } + + params = paramsOf(t, SetGameScoreConfig{UserID: 1, Score: 0, Force: true, ChatID: 2, MessageID: 3}) + if params["score"] != "0" || params["force"] != "true" { + t.Errorf("setGameScore params = %v, want score=0 and force=true", params) + } + + params = paramsOf(t, SetStickerSetThumbnailConfig{Name: "set", UserID: 1, Format: StickerFormatStatic}) + if params["format"] != StickerFormatStatic { + t.Errorf("setStickerSetThumbnail format = %q", params["format"]) + } + if files := (SetStickerSetThumbnailConfig{Name: "set"}).files(); len(files) != 0 { + t.Errorf("setStickerSetThumbnail without thumbnail has files: %v", files) + } + + params = paramsOf(t, DocumentConfig{ + BaseFile: BaseFile{BaseChat: BaseChat{ChatID: 1}}, + CaptionEntities: []MessageEntity{{Type: "bold", Offset: 0, Length: 1}}, + }) + if !strings.Contains(params["caption_entities"], `"bold"`) { + t.Errorf("sendDocument caption_entities = %q", params["caption_entities"]) + } + + params = paramsOf(t, VideoConfig{BaseFile: BaseFile{BaseChat: BaseChat{ChatID: 1}}, Width: 640, Height: 480}) + if params["width"] != "640" || params["height"] != "480" { + t.Errorf("sendVideo width/height = %q/%q", params["width"], params["height"]) + } + + params = paramsOf(t, MessageConfig{ + BaseChat: BaseChat{ChatID: 1, SuggestedPostParameters: &SuggestedPostParameters{SendDate: 100}}, + Text: "hi", + }) + if !strings.Contains(params["suggested_post_parameters"], `"send_date":100`) { + t.Errorf("sendMessage suggested_post_parameters = %q", params["suggested_post_parameters"]) + } + + params = paramsOf(t, SetPassportDataErrorsConfig{UserID: 1, Errors: []PassportElementError{ + PassportElementErrorUnspecified{Source: "unspecified", Type: "passport", ElementHash: "h", Message: "m"}, + }}) + if !strings.Contains(params["errors"], `"element_hash":"h"`) { + t.Errorf("setPassportDataErrors errors = %q", params["errors"]) + } + if (SetPassportDataErrorsConfig{}).method() != "setPassportDataErrors" { + t.Error("SetPassportDataErrorsConfig has the wrong method") + } + if (ChatMemberCountConfig{}).method() != "getChatMemberCount" { + t.Error("ChatMemberCountConfig uses the deprecated method name") + } +} + +func TestSendPollMediaUpload(t *testing.T) { + config := SendPollConfig{ + BaseChat: BaseChat{ChatID: 1}, + Question: "q", + Media: NewInputMediaPhoto(FileBytes{Name: "q.jpg", Bytes: []byte("q")}), + Options: []InputPollOption{ + {Text: "a", Media: NewInputMediaPhoto(FileID("existing"))}, + {Text: "b", Media: &InputMediaSticker{Type: "sticker", Media: FilePath("tests/image.jpg")}}, + }, + ExplanationMedia: InputMediaLocation{Type: "location", Latitude: 1, Longitude: 2}, + } + + params := paramsOf(t, config) + if !strings.Contains(params["media"], `"media":"attach://poll-media-0"`) { + t.Errorf("media = %s", params["media"]) + } + if !strings.Contains(params["options"], `"media":"existing"`) || !strings.Contains(params["options"], `"media":"attach://poll-media-1"`) { + t.Errorf("options = %s", params["options"]) + } + if !strings.Contains(params["explanation_media"], `"latitude":1`) { + t.Errorf("explanation_media = %s", params["explanation_media"]) + } + + files := config.files() + if len(files) != 2 || files[0].Name != "poll-media-0" || files[1].Name != "poll-media-1" { + t.Fatalf("files = %+v", files) + } + + // The caller's config must not be modified. + if _, ok := config.Media.(InputMediaPhoto).Media.(FileBytes); !ok { + t.Errorf("SendPollConfig.Media was modified: %+v", config.Media) + } +} + +func TestRichMessageMediaUpload(t *testing.T) { + photo := NewInputMediaPhoto(FilePath("tests/image.jpg")) + config := SendRichMessageConfig{ + BaseChat: BaseChat{ChatID: 1}, + RichMessage: &InputRichMessage{ + Markdown: "![](tg://photo?id=p1)", + Media: []InputRichMessageMedia{{ID: "p1", Media: NewInputMediaPhoto(FileBytes{Name: "a.jpg", Bytes: []byte("a")})}}, + Blocks: []InputRichBlock{{ + Type: "list", + Items: []InputRichBlockListItem{{Blocks: []InputRichBlock{{ + Type: "photo", + Photo: &photo, + }}}}, + }}, + }, + } + + params := paramsOf(t, config) + if !strings.Contains(params["rich_message"], `"attach://rich-media-0"`) || !strings.Contains(params["rich_message"], `"attach://rich-media-1"`) { + t.Errorf("rich_message = %s", params["rich_message"]) + } + + files := config.files() + if len(files) != 2 { + t.Fatalf("files = %+v", files) + } + if _, ok := photo.Media.(FilePath); !ok { + t.Errorf("nested block photo was modified: %+v", photo) + } + + edit := EditMessageTextConfig{BaseEdit: BaseEdit{ChatID: 1, MessageID: 2}, RichMessage: config.RichMessage} + params = paramsOf(t, edit) + if _, ok := params["text"]; ok { + t.Errorf("editMessageText sends an empty text alongside rich_message: %v", params) + } + if len(edit.files()) != 2 { + t.Errorf("editMessageText files = %+v", edit.files()) + } +} diff --git a/types.go b/types.go index 36c174b8..e16be689 100644 --- a/types.go +++ b/types.go @@ -9,6 +9,15 @@ import ( "time" ) +const ( + EffectFire = "5104841245755180586" // 🔥 + EffectThumbsUp = "5107584321108051014" // 👍 + EffectThumbsDown = "5104858069142078462" // 👎 + EffectHeart = "5159385139981059251" // ❤️ + EffectParty = "5046509860389126442" // 🎉 + EffectPoop = "5046589136895476101" // 💩 +) + // APIResponse is a response from the Telegram API with the result // stored raw. type APIResponse struct { @@ -96,6 +105,53 @@ type Update struct { // // optional PollAnswer *PollAnswer `json:"poll_answer,omitempty"` + // MessageReaction is a reaction to a message that was changed by a user. + // The bot must be an administrator in the chat and must explicitly specify + // "message_reaction" in the list of allowed_updates to receive these updates. + // The update isn't received for reactions set by bots. + // + // optional + MessageReaction *MessageReactionUpdated `json:"message_reaction,omitempty"` + // MessageReactionCount is reactions to a message with anonymous reactions + // were changed. The bot must be an administrator in the chat and must + // explicitly specify "message_reaction_count" in the list of allowed_updates. + // + // optional + MessageReactionCount *MessageReactionCountUpdated `json:"message_reaction_count,omitempty"` + // BusinessConnection is the bot was connected to or disconnected from a + // business account, or a user edited an existing connection with the bot. + // + // optional + BusinessConnection *BusinessConnection `json:"business_connection,omitempty"` + // BusinessMessage is a new message from a connected business account. + // + // optional + BusinessMessage *Message `json:"business_message,omitempty"` + // EditedBusinessMessage is a new version of a message from a connected + // business account that is known to the bot and was edited. + // + // optional + EditedBusinessMessage *Message `json:"edited_business_message,omitempty"` + // DeletedBusinessMessages are messages that were deleted from a connected + // business account. + // + // optional + DeletedBusinessMessages *BusinessMessagesDeleted `json:"deleted_business_messages,omitempty"` + // PurchasedPaidMedia is a user purchased paid media with a non-empty + // payload sent by the bot in a non-channel chat. + // + // optional + PurchasedPaidMedia *PaidMediaPurchased `json:"purchased_paid_media,omitempty"` + // ChatBoost is a boost added to a chat or changed. The bot must be an + // administrator in the chat to receive these updates. + // + // optional + ChatBoost *ChatBoostUpdated `json:"chat_boost,omitempty"` + // RemovedChatBoost is a boost removed from a chat. The bot must be an + // administrator in the chat to receive these updates. + // + // optional + RemovedChatBoost *ChatBoostRemoved `json:"removed_chat_boost,omitempty"` // MyChatMember is the bot's chat member status was updated in a chat. For // private chats, this update is received only when the bot is blocked or // unblocked by the user. @@ -114,6 +170,27 @@ type Update struct { // // optional ChatJoinRequest *ChatJoinRequest `json:"chat_join_request,omitempty"` + // ManagedBot is a new bot was created to be managed by the bot, + // or token or owner of a managed bot was changed. + // + // optional + ManagedBot *ManagedBotUpdated `json:"managed_bot,omitempty"` + // GuestMessage is a message received from a chat where the bot is not + // a member, delivered via guest mode. Reply with answerGuestQuery + // using Message.GuestQueryID. + // + // optional + GuestMessage *Message `json:"guest_message,omitempty"` + // Subscription is a change to a user payment subscription toward the + // bot. + // + // optional + Subscription *BotSubscriptionUpdated `json:"subscription,omitempty"` + // StoppedMessageGeneration means a user asked the bot to stop the + // generation of a message. + // + // optional + StoppedMessageGeneration *MessageGenerationStopped `json:"stopped_message_generation,omitempty"` } // SentFrom returns the user who sent an update. Can be nil, if Telegram did not provide information @@ -124,6 +201,20 @@ func (u *Update) SentFrom() *User { return u.Message.From case u.EditedMessage != nil: return u.EditedMessage.From + case u.ChannelPost != nil: + return u.ChannelPost.From + case u.EditedChannelPost != nil: + return u.EditedChannelPost.From + case u.BusinessConnection != nil: + return &u.BusinessConnection.User + case u.BusinessMessage != nil: + return u.BusinessMessage.From + case u.EditedBusinessMessage != nil: + return u.EditedBusinessMessage.From + case u.GuestMessage != nil: + return u.GuestMessage.From + case u.MessageReaction != nil: + return u.MessageReaction.User case u.InlineQuery != nil: return u.InlineQuery.From case u.ChosenInlineResult != nil: @@ -134,6 +225,23 @@ func (u *Update) SentFrom() *User { return u.ShippingQuery.From case u.PreCheckoutQuery != nil: return u.PreCheckoutQuery.From + case u.PurchasedPaidMedia != nil: + return &u.PurchasedPaidMedia.From + case u.PollAnswer != nil: + if u.PollAnswer.VoterChat != nil { + return nil + } + return &u.PollAnswer.User + case u.MyChatMember != nil: + return &u.MyChatMember.From + case u.ChatMember != nil: + return &u.ChatMember.From + case u.ChatJoinRequest != nil: + return &u.ChatJoinRequest.From + case u.ManagedBot != nil: + return &u.ManagedBot.User + case u.Subscription != nil: + return &u.Subscription.User default: return nil } @@ -158,8 +266,38 @@ func (u *Update) FromChat() *Chat { return u.ChannelPost.Chat case u.EditedChannelPost != nil: return u.EditedChannelPost.Chat + case u.BusinessMessage != nil: + return u.BusinessMessage.Chat + case u.EditedBusinessMessage != nil: + return u.EditedBusinessMessage.Chat + case u.DeletedBusinessMessages != nil: + return &u.DeletedBusinessMessages.Chat + case u.GuestMessage != nil: + return u.GuestMessage.Chat + case u.MessageReaction != nil: + return &u.MessageReaction.Chat + case u.MessageReactionCount != nil: + return &u.MessageReactionCount.Chat case u.CallbackQuery != nil: + if u.CallbackQuery.Message == nil { + // Callback queries from inline messages carry no message. + return nil + } return u.CallbackQuery.Message.Chat + case u.PollAnswer != nil: + return u.PollAnswer.VoterChat + case u.MyChatMember != nil: + return &u.MyChatMember.Chat + case u.ChatMember != nil: + return &u.ChatMember.Chat + case u.ChatJoinRequest != nil: + return &u.ChatJoinRequest.Chat + case u.ChatBoost != nil: + return &u.ChatBoost.Chat + case u.RemovedChatBoost != nil: + return &u.RemovedChatBoost.Chat + case u.StoppedMessageGeneration != nil: + return &u.StoppedMessageGeneration.Chat default: return nil } @@ -179,14 +317,35 @@ func (ch UpdatesChannel) Clear() { type User struct { // ID is a unique identifier for this user or bot ID int64 `json:"id"` - // IsBot true, if this user is a bot - // - // optional + // IsBot is true if this user is a bot. IsBot bool `json:"is_bot,omitempty"` // IsPremium true, if user has Telegram Premium // // optional IsPremium bool `json:"is_premium,omitempty"` + // AddedToAttachmentMenu true, if this user added the bot to the attachment menu + // + // optional + AddedToAttachmentMenu bool `json:"added_to_attachment_menu,omitempty"` + // CanConnectToBusiness true, if the bot can be connected to a Telegram + // Business account to receive its messages. Returned only in getMe. + // + // optional + CanConnectToBusiness bool `json:"can_connect_to_business,omitempty"` + // HasMainWebApp true, if the bot has a main Web App. Returned only in getMe. + // + // optional + HasMainWebApp bool `json:"has_main_web_app,omitempty"` + // HasTopicsEnabled is true, if forum topic mode is enabled for the bot + // in private chats. + // + // optional + HasTopicsEnabled bool `json:"has_topics_enabled,omitempty"` + // AllowsUsersToCreateTopics is true, if users can create forum topics + // in the private chat with the bot. + // + // optional + AllowsUsersToCreateTopics bool `json:"allows_users_to_create_topics,omitempty"` // FirstName user's or bot's first name FirstName string `json:"first_name"` // LastName user's or bot's last name @@ -217,6 +376,21 @@ type User struct { // // optional SupportsInlineQueries bool `json:"supports_inline_queries,omitempty"` + // SupportsGuestQueries is true, if the bot supports guest queries from + // chats it is not a member of. Returned only in getMe. + // + // optional + SupportsGuestQueries bool `json:"supports_guest_queries,omitempty"` + // CanManageBots is true, if other bots can be created to be controlled by the bot. + // Returned only in getMe. + // + // optional + CanManageBots bool `json:"can_manage_bots,omitempty"` + // SupportsJoinRequestQueries is true, if the bot supports join request + // queries and can be assigned to process them. Returned only in getMe. + // + // optional + SupportsJoinRequestQueries bool `json:"supports_join_request_queries,omitempty"` } // String displays a simple text version of a user. @@ -261,8 +435,135 @@ type Chat struct { // // optional LastName string `json:"last_name,omitempty"` + // IsForum is true, if the supergroup chat is a forum (has topics enabled) + // + // optional + IsForum bool `json:"is_forum,omitempty"` + // IsDirectMessages is true, if the chat is a supergroup that serves as + // a direct messages chat for a channel. + // + // optional + IsDirectMessages bool `json:"is_direct_messages,omitempty"` +} + +// IsPrivate returns if the Chat is a private conversation. +func (c Chat) IsPrivate() bool { + return c.Type == "private" +} + +// IsGroup returns if the Chat is a group. +func (c Chat) IsGroup() bool { + return c.Type == "group" +} + +// IsSuperGroup returns if the Chat is a supergroup. +func (c Chat) IsSuperGroup() bool { + return c.Type == "supergroup" +} + +// IsChannel returns if the Chat is a channel. +func (c Chat) IsChannel() bool { + return c.Type == "channel" +} + +// ChatConfig returns a ChatConfig struct for chat related methods. +func (c Chat) ChatConfig() ChatConfig { + return ChatConfig{ChatID: c.ID} +} + +// ChatFullInfo contains full information about a chat. This is the return +// type of getChat as of Bot API 7.3. It embeds Chat so all Chat fields are +// accessible via field promotion. +type ChatFullInfo struct { + Chat + // ActiveUsernames is a list of all active chat usernames; for private chats, + // supergroups and channels. + // + // optional + ActiveUsernames []string `json:"active_usernames,omitempty"` + // EmojiStatusCustomEmojiID is the custom emoji identifier of emoji status of + // the other party in a private chat. + // + // optional + EmojiStatusCustomEmojiID string `json:"emoji_status_custom_emoji_id,omitempty"` + // EmojiStatusExpirationDate is the expiration date of the emoji status of + // the other party in a private chat in Unix time, if any. + // + // optional + EmojiStatusExpirationDate int64 `json:"emoji_status_expiration_date,omitempty"` + // AccentColorID is the identifier of the accent color for the chat name + // and backgrounds of the chat photo, reply header, and link preview. + // + // optional + AccentColorID int `json:"accent_color_id,omitempty"` + // BackgroundCustomEmojiID is the custom emoji identifier of the emoji + // chosen by the chat for the reply header and link preview background. + // + // optional + BackgroundCustomEmojiID string `json:"background_custom_emoji_id,omitempty"` + // ProfileAccentColorID is the identifier of the accent color for the + // chat's profile background. + // + // optional + ProfileAccentColorID int `json:"profile_accent_color_id,omitempty"` + // ProfileBackgroundCustomEmojiID is the custom emoji identifier of the + // emoji chosen by the chat for its profile background. + // + // optional + ProfileBackgroundCustomEmojiID string `json:"profile_background_custom_emoji_id,omitempty"` + // HasVisibleHistory is true, if new chat members will have access to old + // messages; available only to chat administrators. + // + // optional + HasVisibleHistory bool `json:"has_visible_history,omitempty"` + // HasHiddenMembers is true, if non-administrators can only see bots and + // administrators in the chat. + // + // optional + HasHiddenMembers bool `json:"has_hidden_members,omitempty"` + // HasAggressiveAntiSpamEnabled is true, if aggressive anti-spam checks are + // enabled in the supergroup. Visible only to chat administrators. Returned + // only in getChat. + // + // optional + HasAggressiveAntiSpamEnabled bool `json:"has_aggressive_anti_spam_enabled,omitempty"` + // UnrestrictBoostCount is the minimum number of boosts that a non-administrator + // user needs to add to the chat in order to ignore slow mode and chat + // permissions. + // + // optional + UnrestrictBoostCount int `json:"unrestrict_boost_count,omitempty"` + // CustomEmojiStickerSetName is the name of the chat's custom emoji sticker + // set. + // + // optional + CustomEmojiStickerSetName string `json:"custom_emoji_sticker_set_name,omitempty"` + // Birthdate of the other party in a private chat. + // + // optional + Birthdate *Birthdate `json:"birthdate,omitempty"` + // BusinessIntro is the intro of the business account. Returned only in + // getChat for business accounts. + // + // optional + BusinessIntro *BusinessIntro `json:"business_intro,omitempty"` + // BusinessLocation is the location of the business account. Returned only + // in getChat for business accounts. + // + // optional + BusinessLocation *BusinessLocation `json:"business_location,omitempty"` + // BusinessOpeningHours is the opening hours of the business account. + // Returned only in getChat for business accounts. + // + // optional + BusinessOpeningHours *BusinessOpeningHours `json:"business_opening_hours,omitempty"` + // PersonalChat is the personal channel of the private chat's user. + // Returned only in getChat for private chats. + // + // optional + PersonalChat *Chat `json:"personal_chat,omitempty"` // Photo is a chat photo - Photo *ChatPhoto `json:"photo"` + Photo *ChatPhoto `json:"photo,omitempty"` // Bio is the bio of the other party in a private chat. Returned only in // getChat // @@ -270,14 +571,30 @@ type Chat struct { Bio string `json:"bio,omitempty"` // HasPrivateForwards is true if privacy settings of the other party in the // private chat allows to use tg://user?id= links only in chats - // with the user. Returned only in getChat. + // with the user. // // optional HasPrivateForwards bool `json:"has_private_forwards,omitempty"` + // HasRestrictedVoiceAndVideoMessages is true, if the privacy settings of the + // other party restrict sending voice and video note messages in the private + // chat. + // + // optional + HasRestrictedVoiceAndVideoMessages bool `json:"has_restricted_voice_and_video_messages,omitempty"` // Description for groups, supergroups and channel chats // // optional Description string `json:"description,omitempty"` + // JoinToSendMessages is true, if users need to join the supergroup before + // they can send messages. + // + // optional + JoinToSendMessages bool `json:"join_to_send_messages,omitempty"` + // JoinByRequest is true, if all users directly joining the supergroup need + // to be approved by supergroup administrators. + // + // optional + JoinByRequest bool `json:"join_by_request,omitempty"` // InviteLink is a chat invite link, for groups, supergroups and channel chats. // Each administrator in a chat generates their own invite links, // so the bot must first generate the link using exportChatInviteLink @@ -288,24 +605,29 @@ type Chat struct { // // optional PinnedMessage *Message `json:"pinned_message,omitempty"` + // AvailableReactions is the list of available reactions allowed in the + // chat. If omitted, then all emoji reactions are allowed. Returned only + // in getChat. + // + // optional + AvailableReactions []ReactionType `json:"available_reactions,omitempty"` // Permissions are default chat member permissions, for groups and - // supergroups. Returned only in getChat. + // supergroups. // // optional Permissions *ChatPermissions `json:"permissions,omitempty"` // SlowModeDelay is for supergroups, the minimum allowed delay between - // consecutive messages sent by each unprivileged user. Returned only in - // getChat. + // consecutive messages sent by each unprivileged user. // // optional SlowModeDelay int `json:"slow_mode_delay,omitempty"` // MessageAutoDeleteTime is the time after which all messages sent to the - // chat will be automatically deleted; in seconds. Returned only in getChat. + // chat will be automatically deleted; in seconds. // // optional MessageAutoDeleteTime int `json:"message_auto_delete_time,omitempty"` // HasProtectedContent is true if messages from the chat can't be forwarded - // to other chats. Returned only in getChat. + // to other chats. // // optional HasProtectedContent bool `json:"has_protected_content,omitempty"` @@ -315,7 +637,6 @@ type Chat struct { // optional StickerSetName string `json:"sticker_set_name,omitempty"` // CanSetStickerSet is true, if the bot can change the group sticker set. - // Returned only in getChat. // // optional CanSetStickerSet bool `json:"can_set_sticker_set,omitempty"` @@ -326,41 +647,81 @@ type Chat struct { // optional LinkedChatID int64 `json:"linked_chat_id,omitempty"` // Location is for supergroups, the location to which the supergroup is - // connected. Returned only in getChat. + // connected. // // optional Location *ChatLocation `json:"location,omitempty"` -} - -// IsPrivate returns if the Chat is a private conversation. -func (c Chat) IsPrivate() bool { - return c.Type == "private" -} - -// IsGroup returns if the Chat is a group. -func (c Chat) IsGroup() bool { - return c.Type == "group" -} - -// IsSuperGroup returns if the Chat is a supergroup. -func (c Chat) IsSuperGroup() bool { - return c.Type == "supergroup" -} - -// IsChannel returns if the Chat is a channel. -func (c Chat) IsChannel() bool { - return c.Type == "channel" -} - -// ChatConfig returns a ChatConfig struct for chat related methods. -func (c Chat) ChatConfig() ChatConfig { - return ChatConfig{ChatID: c.ID} + // MaxReactionCount is the maximum number of reactions that can be set + // on a message in the chat. + MaxReactionCount int `json:"max_reaction_count"` + // CanSendPaidMedia is true, if paid media messages can be sent or + // forwarded to the channel chat. Channel chats only. + // + // optional + CanSendPaidMedia bool `json:"can_send_paid_media,omitempty"` + // AcceptedGiftTypes are the types of gifts accepted by the chat. + AcceptedGiftTypes AcceptedGiftTypes `json:"accepted_gift_types"` + // ParentChat is the parent channel chat for a channel direct messages chat. + // + // optional + ParentChat *Chat `json:"parent_chat,omitempty"` + // Community is the community the chat belongs to. + // + // optional + Community *Community `json:"community,omitempty"` + // Rating is the rating of the user in a private chat. + // + // optional + Rating *UserRating `json:"rating,omitempty"` + // PaidMessageStarCount is the number of Telegram Stars that must be + // paid by non-administrator users of the supergroup chat for each sent + // message. + // + // optional + PaidMessageStarCount int `json:"paid_message_star_count,omitempty"` + // UniqueGiftColors defines the color scheme for the chat's name, replies + // to messages, and link previews based on a unique gift. + // + // optional + UniqueGiftColors *UniqueGiftColors `json:"unique_gift_colors,omitempty"` + // FirstProfileAudio is the first audio on the user profile. + // + // optional + FirstProfileAudio *Audio `json:"first_profile_audio,omitempty"` + // GuardBot is the bot that processes join request queries in the chat. + // The field is only available to chat administrators. + // + // optional + GuardBot *User `json:"guard_bot,omitempty"` } // Message represents a message. type Message struct { - // MessageID is a unique message identifier inside this chat + // MessageID is a unique message identifier inside this chat. + // + // Note: starting December 1, 2024, video messages sent, copied or + // forwarded to groups and channels with a sufficiently large audience + // may be scheduled by the server until the video is reencoded. Such + // messages come back with MessageID == 0 and cannot be referenced + // (replied to, edited, forwarded) until Telegram finishes processing. MessageID int `json:"message_id"` + // BusinessConnectionID is the unique identifier of the business + // connection from which the message was received. If non-empty, the + // message belongs to a chat of the corresponding business account that + // is independent from any potential bot chat which might share the same + // identifier. + // + // optional + BusinessConnectionID string `json:"business_connection_id,omitempty"` + // MessageThreadID is the unique identifier of a message thread to which + // the message belongs; for supergroups only. + // + // optional + MessageThreadID int `json:"message_thread_id,omitempty"` + // IsTopicMessage is true, if the message is sent to a forum topic. + // + // optional + IsTopicMessage bool `json:"is_topic_message,omitempty"` // From is a sender, empty for messages sent to channels; // // optional @@ -376,49 +737,98 @@ type Message struct { Date int `json:"date"` // Chat is the conversation the message belongs to Chat *Chat `json:"chat"` - // ForwardFrom for forwarded messages, sender of the original message; + // ForwardOrigin is information about the original message for forwarded + // messages. // // optional - ForwardFrom *User `json:"forward_from,omitempty"` - // ForwardFromChat for messages forwarded from channels, - // information about the original channel; + ForwardOrigin *MessageOrigin `json:"forward_origin,omitempty"` + // IsAutomaticForward is true if the message is a channel post that was + // automatically forwarded to the connected discussion group. // // optional - ForwardFromChat *Chat `json:"forward_from_chat,omitempty"` - // ForwardFromMessageID for messages forwarded from channels, - // identifier of the original message in the channel; + IsAutomaticForward bool `json:"is_automatic_forward,omitempty"` + // SenderBoostCount is the number of boosts added by the user, if the + // sender of the message boosted the chat. // // optional - ForwardFromMessageID int `json:"forward_from_message_id,omitempty"` - // ForwardSignature for messages forwarded from channels, signature of the - // post author if present + SenderBoostCount int `json:"sender_boost_count,omitempty"` + // SenderTag is the custom tag assigned to the sender in the chat. // // optional - ForwardSignature string `json:"forward_signature,omitempty"` - // ForwardSenderName is the sender's name for messages forwarded from users - // who disallow adding a link to their account in forwarded messages + SenderTag string `json:"sender_tag,omitempty"` + // ReceiverUser is, for ephemeral messages, the user who received the + // message. // // optional - ForwardSenderName string `json:"forward_sender_name,omitempty"` - // ForwardDate for forwarded messages, date the original message was sent in Unix time; + ReceiverUser *User `json:"receiver_user,omitempty"` + // EphemeralMessageID is, for ephemeral messages, the identifier of the + // ephemeral message inside this chat. The identifier may be reused for + // another ephemeral message after the message is deleted or expires. // // optional - ForwardDate int `json:"forward_date,omitempty"` - // IsAutomaticForward is true if the message is a channel post that was - // automatically forwarded to the connected discussion group. + EphemeralMessageID int `json:"ephemeral_message_id,omitempty"` + // SenderBusinessBot is the bot that actually sent the message on behalf + // of the business account. Available only for outgoing messages sent on + // behalf of the connected business account. // // optional - IsAutomaticForward bool `json:"is_automatic_forward,omitempty"` + SenderBusinessBot *User `json:"sender_business_bot,omitempty"` + // IsFromOffline is true, if the message was sent by an implicit action, + // for example, as an away or a greeting business message, or as a + // scheduled message. + // + // optional + IsFromOffline bool `json:"is_from_offline,omitempty"` // ReplyToMessage for replies, the original message. // Note that the Message object in this field will not contain further ReplyToMessage fields // even if it itself is a reply; // // optional ReplyToMessage *Message `json:"reply_to_message,omitempty"` + // ExternalReply is information about the message that is being replied to, + // which may come from another chat or forum topic. + // + // optional + ExternalReply *ExternalReplyInfo `json:"external_reply,omitempty"` + // Quote is the part of the message that is actually quoted in the reply. + // + // optional + Quote *TextQuote `json:"quote,omitempty"` + // ReplyToStory is the story that this message is a reply to. + // + // optional + ReplyToStory *Story `json:"reply_to_story,omitempty"` + // ReplyToChecklistTaskID is the identifier of the specific checklist + // task that is being replied to. + // + // optional + ReplyToChecklistTaskID int `json:"reply_to_checklist_task_id,omitempty"` // ViaBot through which the message was sent; // // optional ViaBot *User `json:"via_bot,omitempty"` + // RichMessage is set if the message is a rich formatted message. + // + // optional + RichMessage *RichMessage `json:"rich_message,omitempty"` + // GuestQueryID is the unique identifier for the guest query. Use this + // identifier with the method answerGuestQuery to send a response + // message. If non-empty, the message belongs to the chat where the + // guest bot was summoned, which may not coincide with other existing + // bot chats sharing the same identifier. + // + // optional + GuestQueryID string `json:"guest_query_id,omitempty"` + // GuestBotCallerUser is, for a message sent by a guest bot, the user + // whose original message triggered the bot's response. + // + // optional + GuestBotCallerUser *User `json:"guest_bot_caller_user,omitempty"` + // GuestBotCallerChat is, for a message sent by a guest bot, the chat + // whose original message triggered the bot's response. + // + // optional + GuestBotCallerChat *Chat `json:"guest_bot_caller_chat,omitempty"` // EditDate of the message was last edited in Unix time; // // optional @@ -431,6 +841,10 @@ type Message struct { // // optional MediaGroupID string `json:"media_group_id,omitempty"` + // EffectID is the unique identifier of the message effect added to the message; + // + // optional + EffectID string `json:"effect_id,omitempty"` // AuthorSignature is the signature of the post author for messages in channels; // // optional @@ -444,16 +858,16 @@ type Message struct { // // optional Entities []MessageEntity `json:"entities,omitempty"` - // Animation message is an animation, information about the animation. - // For backward compatibility, when this field is set, the document field will also be set; + // LinkPreviewOptions are options used for link preview generation for the + // message, if it is a text message and link preview options were changed. // // optional - Animation *Animation `json:"animation,omitempty"` - // PremiumAnimation message is an animation, information about the animation. + LinkPreviewOptions *LinkPreviewOptions `json:"link_preview_options,omitempty"` + // Animation message is an animation, information about the animation. // For backward compatibility, when this field is set, the document field will also be set; // // optional - PremiumAnimation *Animation `json:"premium_animation,omitempty"` + Animation *Animation `json:"animation,omitempty"` // Audio message is an audio file, information about the file; // // optional @@ -466,6 +880,16 @@ type Message struct { // // optional Photo []PhotoSize `json:"photo,omitempty"` + // LivePhoto message is a live photo, information about the live photo. + // For backward compatibility, when this field is set, the photo field + // will also be set. + // + // optional + LivePhoto *LivePhoto `json:"live_photo,omitempty"` + // PaidMedia is the paid media attached to the message. + // + // optional + PaidMedia *PaidMediaInfo `json:"paid_media,omitempty"` // Sticker message is a sticker, information about the sticker; // // optional @@ -478,6 +902,10 @@ type Message struct { // // optional VideoNote *VideoNote `json:"video_note,omitempty"` + // Story is a forwarded story. + // + // optional + Story *Story `json:"story,omitempty"` // Voice message is a voice message, information about the file; // // optional @@ -486,6 +914,11 @@ type Message struct { // // optional Caption string `json:"caption,omitempty"` + // ShowCaptionAboveMedia is true, if the caption must be shown above the + // message media. + // + // optional + ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"` // CaptionEntities; // // optional @@ -594,6 +1027,11 @@ type Message struct { // // optional SuccessfulPayment *SuccessfulPayment `json:"successful_payment,omitempty"` + // RefundedPayment is a service message about a refunded payment, + // information about the payment. + // + // optional + RefundedPayment *RefundedPayment `json:"refunded_payment,omitempty"` // ConnectedWebsite is the domain name of the website on which the user has // logged in; // @@ -629,53 +1067,246 @@ type Message struct { // // optional WebAppData *WebAppData `json:"web_app_data,omitempty"` - // ReplyMarkup is the Inline keyboard attached to the message. - // login_url buttons are represented as ordinary url buttons. + // UsersShared is a service message: users were shared with the bot. // // optional - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` -} - -// Time converts the message timestamp into a Time. -func (m *Message) Time() time.Time { - return time.Unix(int64(m.Date), 0) -} - -// IsCommand returns true if message starts with a "bot_command" entity. -func (m *Message) IsCommand() bool { - if m.Entities == nil || len(m.Entities) == 0 { - return false - } - - entity := m.Entities[0] - return entity.Offset == 0 && entity.IsCommand() -} - -// Command checks if the message was a command and if it was, returns the -// command. If the Message was not a command, it returns an empty string. -// -// If the command contains the at name syntax, it is removed. Use -// CommandWithAt() if you do not want that. -func (m *Message) Command() string { - command := m.CommandWithAt() - - if i := strings.Index(command, "@"); i != -1 { - command = command[:i] - } - - return command -} - -// CommandWithAt checks if the message was a command and if it was, returns the -// command. If the Message was not a command, it returns an empty string. -// -// If the command contains the at name syntax, it is not removed. Use Command() -// if you want that. -func (m *Message) CommandWithAt() string { - if !m.IsCommand() { - return "" - } - + UsersShared *UsersShared `json:"users_shared,omitempty"` + // ChatShared is a service message: a chat was shared with the bot. + // + // optional + ChatShared *ChatShared `json:"chat_shared,omitempty"` + // ForumTopicCreated is a service message: forum topic created + // + // optional + ForumTopicCreated *ForumTopicCreated `json:"forum_topic_created,omitempty"` + // ForumTopicEdited is a service message: forum topic edited + // + // optional + ForumTopicEdited *ForumTopicEdited `json:"forum_topic_edited,omitempty"` + // ForumTopicClosed is a service message: forum topic closed + // + // optional + ForumTopicClosed *ForumTopicClosed `json:"forum_topic_closed,omitempty"` + // ForumTopicReopened is a service message: forum topic reopened + // + // optional + ForumTopicReopened *ForumTopicReopened `json:"forum_topic_reopened,omitempty"` + // GeneralForumTopicHidden is a service message: the General forum topic hidden + // + // optional + GeneralForumTopicHidden *GeneralForumTopicHidden `json:"general_forum_topic_hidden,omitempty"` + // GeneralForumTopicUnhidden is a service message: the General forum topic unhidden + // + // optional + GeneralForumTopicUnhidden *GeneralForumTopicUnhidden `json:"general_forum_topic_unhidden,omitempty"` + // WriteAccessAllowed is a service message: the user allowed the bot added to + // the attachment menu to write messages + // + // optional + WriteAccessAllowed *WriteAccessAllowed `json:"write_access_allowed,omitempty"` + // BoostAdded is a service message: a user boosted the chat. + // + // optional + BoostAdded *ChatBoostAdded `json:"boost_added,omitempty"` + // ChatBackgroundSet is a service message: chat background set. + // + // optional + ChatBackgroundSet *ChatBackground `json:"chat_background_set,omitempty"` + // Gift is a service message: a gift was sent or received. + // + // optional + Gift *GiftInfo `json:"gift,omitempty"` + // UniqueGift is a service message: a unique gift was sent or received. + // + // optional + UniqueGift *UniqueGiftInfo `json:"unique_gift,omitempty"` + // GiftUpgradeSent is a service message about a gift upgrade sent to + // another user. + // + // optional + GiftUpgradeSent *GiftInfo `json:"gift_upgrade_sent,omitempty"` + // ChatOwnerLeft is a service message about the owner leaving the chat. + // + // optional + ChatOwnerLeft *ChatOwnerLeft `json:"chat_owner_left,omitempty"` + // ChatOwnerChanged is a service message about a change of the chat + // owner. + // + // optional + ChatOwnerChanged *ChatOwnerChanged `json:"chat_owner_changed,omitempty"` + // PollOptionAdded is a service message about a poll option being added + // to a poll. + // + // optional + PollOptionAdded *PollOptionAdded `json:"poll_option_added,omitempty"` + // PollOptionDeleted is a service message about a poll option being + // deleted from a poll. + // + // optional + PollOptionDeleted *PollOptionDeleted `json:"poll_option_deleted,omitempty"` + // CommunityChatAdded is a service message about the chat being added to a + // community. + // + // optional + CommunityChatAdded *CommunityChatAdded `json:"community_chat_added,omitempty"` + // CommunityChatRemoved is a service message about the chat being removed + // from a community. + // + // optional + CommunityChatRemoved *CommunityChatRemoved `json:"community_chat_removed,omitempty"` + // CommunityChatJoined is a service message about the chat being joined by + // a user from a community. + // + // optional + CommunityChatJoined *CommunityChatJoined `json:"community_chat_joined,omitempty"` + // ReplyToPollOptionID is the persistent identifier of the poll option + // that this message is a reply to. + // + // optional + ReplyToPollOptionID string `json:"reply_to_poll_option_id,omitempty"` + // PaidMessagePriceChanged is a service message about a change in the + // price of paid messages within the chat. + // + // optional + PaidMessagePriceChanged *PaidMessagePriceChanged `json:"paid_message_price_changed,omitempty"` + // DirectMessagePriceChanged is a service message about a change in the + // pricing of direct messages sent to a channel chat. + // + // optional + DirectMessagePriceChanged *DirectMessagePriceChanged `json:"direct_message_price_changed,omitempty"` + // Checklist is the checklist that was sent, if the message contains a + // checklist. + // + // optional + Checklist *Checklist `json:"checklist,omitempty"` + // ChecklistTasksDone is a service message about tasks in a checklist + // being marked as done or not done. + // + // optional + ChecklistTasksDone *ChecklistTasksDone `json:"checklist_tasks_done,omitempty"` + // ChecklistTasksAdded is a service message about tasks added to a + // checklist. + // + // optional + ChecklistTasksAdded *ChecklistTasksAdded `json:"checklist_tasks_added,omitempty"` + // DirectMessagesTopic is the topic of a direct messages chat to which + // the message belongs. + // + // optional + DirectMessagesTopic *DirectMessagesTopic `json:"direct_messages_topic,omitempty"` + // IsPaidPost is true, if the message is a paid post. Note that such + // posts must not be deleted for 24 hours after the payment. + // + // optional + IsPaidPost bool `json:"is_paid_post,omitempty"` + // SuggestedPostInfo describes the suggested post if this message is a + // suggested post. + // + // optional + SuggestedPostInfo *SuggestedPostInfo `json:"suggested_post_info,omitempty"` + // SuggestedPostApproved is a service message about the approval of a + // suggested post. + // + // optional + SuggestedPostApproved *SuggestedPostApproved `json:"suggested_post_approved,omitempty"` + // SuggestedPostApprovalFailed is a service message about a failure to + // approve a suggested post. + // + // optional + SuggestedPostApprovalFailed *SuggestedPostApprovalFailed `json:"suggested_post_approval_failed,omitempty"` + // SuggestedPostDeclined is a service message about the rejection of a + // suggested post. + // + // optional + SuggestedPostDeclined *SuggestedPostDeclined `json:"suggested_post_declined,omitempty"` + // SuggestedPostPaid is a service message about a successful payment + // for a suggested post. + // + // optional + SuggestedPostPaid *SuggestedPostPaid `json:"suggested_post_paid,omitempty"` + // SuggestedPostRefunded is a service message about a payment refund + // for a suggested post. + // + // optional + SuggestedPostRefunded *SuggestedPostRefunded `json:"suggested_post_refunded,omitempty"` + // PaidStarCount is the number of Telegram Stars that were paid by the + // sender of the message to send it. + // + // optional + PaidStarCount int `json:"paid_star_count,omitempty"` + // GiveawayCreated is a service message: a scheduled giveaway was created. + // + // optional + GiveawayCreated *GiveawayCreated `json:"giveaway_created,omitempty"` + // Giveaway is a scheduled giveaway message. + // + // optional + Giveaway *Giveaway `json:"giveaway,omitempty"` + // GiveawayWinners is a giveaway with public winners that was completed. + // + // optional + GiveawayWinners *GiveawayWinners `json:"giveaway_winners,omitempty"` + // GiveawayCompleted is a service message about the completion of a + // giveaway without public winners. + // + // optional + GiveawayCompleted *GiveawayCompleted `json:"giveaway_completed,omitempty"` + // HasMediaSpoiler is true, if the message media is covered by a spoiler animation + // + // optional + HasMediaSpoiler bool `json:"has_media_spoiler,omitempty"` + // ManagedBotCreated is a service message: user created a bot + // that will be managed by the current bot. + // + // optional + ManagedBotCreated *ManagedBotCreated `json:"managed_bot_created,omitempty"` + // ReplyMarkup is the Inline keyboard attached to the message. + // login_url buttons are represented as ordinary url buttons. + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` +} + +// Time converts the message timestamp into a Time. +func (m *Message) Time() time.Time { + return time.Unix(int64(m.Date), 0) +} + +// IsCommand returns true if message starts with a "bot_command" entity. +func (m *Message) IsCommand() bool { + if len(m.Entities) == 0 { + return false + } + + entity := m.Entities[0] + return entity.Offset == 0 && entity.IsCommand() +} + +// Command checks if the message was a command and if it was, returns the +// command. If the Message was not a command, it returns an empty string. +// +// If the command contains the at name syntax, it is removed. Use +// CommandWithAt() if you do not want that. +func (m *Message) Command() string { + command := m.CommandWithAt() + + if i := strings.Index(command, "@"); i != -1 { + command = command[:i] + } + + return command +} + +// CommandWithAt checks if the message was a command and if it was, returns the +// command. If the Message was not a command, it returns an empty string. +// +// If the command contains the at name syntax, it is not removed. Use Command() +// if you want that. +func (m *Message) CommandWithAt() string { + if !m.IsCommand() { + return "" + } + // IsCommand() checks that the message begins with a bot_command entity entity := m.Entities[0] return m.Text[1:entity.Length] @@ -705,7 +1336,13 @@ func (m *Message) CommandArguments() string { return m.Text[entity.Length+1:] } -// MessageID represents a unique message identifier. +// MessageID represents a unique message identifier. Returned by +// forwardMessage(s), copyMessage(s), and sendMediaGroup. +// +// Note: for video forwards/copies to large groups and channels, Telegram +// may schedule the message until the video finishes reencoding (since +// December 1, 2024). In that case MessageID == 0 and the message cannot +// be referenced until it is actually sent. type MessageID struct { MessageID int `json:"message_id"` } @@ -726,10 +1363,14 @@ type MessageEntity struct { // “underline” (underlined text), // “strikethrough” (strikethrough text), // "spoiler" (spoiler message), + // “blockquote” (block quotation), + // “expandable_blockquote” (collapsed-by-default block quotation), // “code” (monowidth string), // “pre” (monowidth block), // “text_link” (for clickable text URLs), - // “text_mention” (for users without usernames) + // “text_mention” (for users without usernames), + // “custom_emoji” (for inline custom emoji stickers), + // “date_time” (for formatted date and time) Type string `json:"type"` // Offset in UTF-16 code units to the start of the entity Offset int `json:"offset"` @@ -747,6 +1388,21 @@ type MessageEntity struct { // // optional Language string `json:"language,omitempty"` + // CustomEmojiID for "custom_emoji" only, unique identifier of the custom emoji. + // Use getCustomEmojiStickers to get full information about the sticker. + // + // optional + CustomEmojiID string `json:"custom_emoji_id,omitempty"` + // UnixTime for "date_time" only, the Unix time associated with the entity. + // + // optional + UnixTime int64 `json:"unix_time,omitempty"` + // DateTimeFormat for "date_time" only, the string that defines the + // formatting of the date and time. See date-time entity formatting for + // more details. + // + // optional + DateTimeFormat string `json:"date_time_format,omitempty"` } // ParseURL attempts to parse a URL contained within a MessageEntity. @@ -851,7 +1507,7 @@ type Animation struct { // Thumbnail animation thumbnail as defined by sender // // optional - Thumbnail *PhotoSize `json:"thumb,omitempty"` + Thumbnail *PhotoSize `json:"thumbnail,omitempty"` // FileName original animation filename as defined by sender // // optional @@ -863,7 +1519,7 @@ type Animation struct { // FileSize file size // // optional - FileSize int `json:"file_size,omitempty"` + FileSize int64 `json:"file_size,omitempty"` } // Audio represents an audio file to be treated as music by the Telegram clients. @@ -896,11 +1552,11 @@ type Audio struct { // FileSize file size // // optional - FileSize int `json:"file_size,omitempty"` + FileSize int64 `json:"file_size,omitempty"` // Thumbnail is the album cover to which the music file belongs // // optional - Thumbnail *PhotoSize `json:"thumb,omitempty"` + Thumbnail *PhotoSize `json:"thumbnail,omitempty"` } // Document represents a general file. @@ -915,7 +1571,7 @@ type Document struct { // Thumbnail document thumbnail as defined by sender // // optional - Thumbnail *PhotoSize `json:"thumb,omitempty"` + Thumbnail *PhotoSize `json:"thumbnail,omitempty"` // FileName original filename as defined by sender // // optional @@ -927,7 +1583,36 @@ type Document struct { // FileSize file size // // optional - FileSize int `json:"file_size,omitempty"` + FileSize int64 `json:"file_size,omitempty"` +} + +// LivePhoto represents a live photo (a photo with a short video). +type LivePhoto struct { + // FileID is an identifier for the video file which can be used to + // download or reuse the file. + FileID string `json:"file_id"` + // FileUniqueID is the unique identifier for the video file which is + // supposed to be the same over time and for different bots. Can't be + // used to download or reuse the file. + FileUniqueID string `json:"file_unique_id"` + // Width is the video width as defined by the sender. + Width int `json:"width"` + // Height is the video height as defined by the sender. + Height int `json:"height"` + // Duration of the video in seconds as defined by the sender. + Duration int `json:"duration"` + // Photo are the available sizes of the corresponding static photo. + // + // optional + Photo []PhotoSize `json:"photo,omitempty"` + // MimeType is the MIME type of the file as defined by the sender. + // + // optional + MimeType string `json:"mime_type,omitempty"` + // FileSize is the file size in bytes. + // + // optional + FileSize int64 `json:"file_size,omitempty"` } // Video represents a video file. @@ -948,7 +1633,20 @@ type Video struct { // Thumbnail video thumbnail // // optional - Thumbnail *PhotoSize `json:"thumb,omitempty"` + Thumbnail *PhotoSize `json:"thumbnail,omitempty"` + // Cover is the available sizes of the cover of the video in the message. + // + // optional + Cover []PhotoSize `json:"cover,omitempty"` + // StartTimestamp is the timestamp in seconds from which the video will + // play in the message. + // + // optional + StartTimestamp int `json:"start_timestamp,omitempty"` + // Qualities lists the other available qualities of this video. + // + // optional + Qualities []VideoQuality `json:"qualities,omitempty"` // FileName is the original filename as defined by sender // // optional @@ -960,7 +1658,7 @@ type Video struct { // FileSize file size // // optional - FileSize int `json:"file_size,omitempty"` + FileSize int64 `json:"file_size,omitempty"` } // VideoNote object represents a video message. @@ -978,7 +1676,7 @@ type VideoNote struct { // Thumbnail video thumbnail // // optional - Thumbnail *PhotoSize `json:"thumb,omitempty"` + Thumbnail *PhotoSize `json:"thumbnail,omitempty"` // FileSize file size // // optional @@ -1002,7 +1700,7 @@ type Voice struct { // FileSize file size // // optional - FileSize int `json:"file_size,omitempty"` + FileSize int64 `json:"file_size,omitempty"` } // Contact represents a phone contact. @@ -1035,31 +1733,159 @@ type Dice struct { Value int `json:"value"` } +// PollMedia represents media attached to a poll, poll option, or quiz +// explanation. At most one of the optional fields can be present in a given +// PollMedia value. +type PollMedia struct { + // Animation is set if the media is an animation. + // + // optional + Animation *Animation `json:"animation,omitempty"` + // Audio is set if the media is an audio file. Currently, can't appear + // in a poll option. + // + // optional + Audio *Audio `json:"audio,omitempty"` + // Document is set if the media is a general file. Currently, can't + // appear in a poll option. + // + // optional + Document *Document `json:"document,omitempty"` + // Link is set if the media is an HTTP link. Currently, only valid for + // poll options. + // + // optional + Link *Link `json:"link,omitempty"` + // LivePhoto is set if the media is a live photo. + // + // optional + LivePhoto *LivePhoto `json:"live_photo,omitempty"` + // Location is set if the media is a shared location. + // + // optional + Location *Location `json:"location,omitempty"` + // Photo is set if the media is a photo. + // + // optional + Photo []PhotoSize `json:"photo,omitempty"` + // Sticker is set if the media is a sticker. Currently, only valid for + // poll options. + // + // optional + Sticker *Sticker `json:"sticker,omitempty"` + // Venue is set if the media is a venue. + // + // optional + Venue *Venue `json:"venue,omitempty"` + // Video is set if the media is a video. + // + // optional + Video *Video `json:"video,omitempty"` +} + // PollOption contains information about one answer option in a poll. type PollOption struct { // Text is the option text, 1-100 characters Text string `json:"text"` + // TextEntities are the special entities that appear in the option text. + // Currently, only custom_emoji entities are allowed in poll option texts. + // + // optional + TextEntities []MessageEntity `json:"text_entities,omitempty"` + // Media is the optional media attached to the poll option. + // + // optional + Media *PollMedia `json:"media,omitempty"` // VoterCount is the number of users that voted for this option VoterCount int `json:"voter_count"` + // PersistentID is the persistent identifier of the poll option; stays + // the same even if the option is moved, renamed, added, or deleted. + // + // optional + PersistentID string `json:"persistent_id,omitempty"` + // AddedByUser is the user that added the option to the poll. + // + // optional + AddedByUser *User `json:"added_by_user,omitempty"` + // AddedByChat is the chat that added the option to the poll anonymously. + // + // optional + AddedByChat *Chat `json:"added_by_chat,omitempty"` + // AdditionDate is the point in time (Unix timestamp) when the option + // was added to the poll. + // + // optional + AdditionDate int `json:"addition_date,omitempty"` +} + +// PollOptionAdded describes a service message about a poll option being +// added to a poll. +type PollOptionAdded struct { + // PollMessage is the message containing the poll. + // + // optional + PollMessage *Message `json:"poll_message,omitempty"` + // OptionPersistentID is the unique identifier of the added option. + OptionPersistentID string `json:"option_persistent_id"` + // OptionText is the option text. + OptionText string `json:"option_text"` + // OptionTextEntities are special entities that appear in the OptionText. + // + // optional + OptionTextEntities []MessageEntity `json:"option_text_entities,omitempty"` +} + +// PollOptionDeleted describes a service message about a poll option being +// deleted from a poll. +type PollOptionDeleted struct { + // PollMessage is the message containing the poll. + // + // optional + PollMessage *Message `json:"poll_message,omitempty"` + // OptionPersistentID is the unique identifier of the deleted option. + OptionPersistentID string `json:"option_persistent_id"` + // OptionText is the option text. + OptionText string `json:"option_text"` + // OptionTextEntities are special entities that appear in the OptionText. + // + // optional + OptionTextEntities []MessageEntity `json:"option_text_entities,omitempty"` } // PollAnswer represents an answer of a user in a non-anonymous poll. type PollAnswer struct { // PollID is the unique poll identifier PollID string `json:"poll_id"` - // User who changed the answer to the poll + // VoterChat is the chat that changed the answer to the poll, if the voter + // is anonymous. + // + // optional + VoterChat *Chat `json:"voter_chat,omitempty"` + // User who changed the answer to the poll, if the voter isn't anonymous. + // For backward compatibility, the field user in such objects will contain + // the user 136817688 (@Channel_Bot). User User `json:"user"` // OptionIDs is the 0-based identifiers of poll options chosen by the user. // May be empty if user retracted vote. OptionIDs []int `json:"option_ids"` + // OptionPersistentIDs is the persistent identifiers of the poll options + // chosen by the user, matching PollOption.PersistentID. + // + // optional + OptionPersistentIDs []string `json:"option_persistent_ids,omitempty"` } // Poll contains information about a poll. type Poll struct { // ID is the unique poll identifier ID string `json:"id"` - // Question is the poll question, 1-255 characters + // Question is the poll question, 1-300 characters Question string `json:"question"` + // QuestionEntities are the special entities that appear in the question. + // Currently, only custom_emoji entities are allowed in poll questions. + // + // optional + QuestionEntities []MessageEntity `json:"question_entities,omitempty"` // Options is the list of poll options Options []PollOption `json:"options"` // TotalVoterCount is the total numbers of users who voted in the poll @@ -1072,12 +1898,44 @@ type Poll struct { Type string `json:"type"` // AllowsMultipleAnswers is true, if the poll allows multiple answers AllowsMultipleAnswers bool `json:"allows_multiple_answers"` - // CorrectOptionID is the 0-based identifier of the correct answer option. - // Available only for polls in quiz mode, which are closed, or was sent (not - // forwarded) by the bot or to the private chat with the bot. + // CorrectOptionIDs lists the 0-based identifiers of the correct answer + // options. Available only for polls in quiz mode, which are closed, or + // was sent (not forwarded) by the bot or to the private chat with the + // bot. Multi-answer quizzes may have more than one correct option. // // optional - CorrectOptionID int `json:"correct_option_id,omitempty"` + CorrectOptionIDs []int `json:"correct_option_ids,omitempty"` + // AllowsRevoting is true, if the poll allows users to change their vote. + // + // optional + AllowsRevoting bool `json:"allows_revoting,omitempty"` + // MembersOnly is true if voting is limited to users who have been + // members of the chat where the poll was originally sent for more than + // 24 hours. + // + // optional + MembersOnly bool `json:"members_only,omitempty"` + // CountryCodes is a list of two-letter ISO 3166-1 alpha-2 country codes + // indicating the countries from which users can vote in the poll. If + // empty, then users from any country can participate in the poll. + // + // optional + CountryCodes []string `json:"country_codes,omitempty"` + // Description is the text of the poll description; for polls inside + // the Message object only. + // + // optional + Description string `json:"description,omitempty"` + // DescriptionEntities are the special entities that appear in the + // description. + // + // optional + DescriptionEntities []MessageEntity `json:"description_entities,omitempty"` + // Media is the media attached to the poll description; for polls inside + // the Message object only. + // + // optional + Media *PollMedia `json:"media,omitempty"` // Explanation is 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 // @@ -1088,6 +1946,10 @@ type Poll struct { // // optional ExplanationEntities []MessageEntity `json:"explanation_entities,omitempty"` + // ExplanationMedia is the media attached to the quiz explanation. + // + // optional + ExplanationMedia *PollMedia `json:"explanation_media,omitempty"` // OpenPeriod is the amount of time in seconds the poll will be active // after creation // @@ -1214,6 +2076,31 @@ type VideoChatParticipantsInvited struct { Users []User `json:"users,omitempty"` } +// ManagedBotCreated contains information about the bot that was created +// to be managed by the current bot. +type ManagedBotCreated struct { + // Bot is information about the bot. The bot's token can be fetched + // using the method getManagedBotToken. + Bot User `json:"bot"` +} + +// ManagedBotUpdated contains information about the creation, token update, +// or owner update of a bot that is managed by the current bot. +type ManagedBotUpdated struct { + // User that created the bot. + User User `json:"user"` + // Bot is information about the bot. Token of the bot can be fetched + // using the method getManagedBotToken. + Bot User `json:"bot"` +} + +// PreparedKeyboardButton describes a keyboard button to be used +// by a user of a Mini App. +type PreparedKeyboardButton struct { + // ID is the unique identifier of the keyboard button. + ID string `json:"id"` +} + // UserProfilePhotos contains a set of user profile photos. type UserProfilePhotos struct { // TotalCount total number of profile pictures the target user has @@ -1234,7 +2121,7 @@ type File struct { // FileSize file size, if known // // optional - FileSize int `json:"file_size,omitempty"` + FileSize int64 `json:"file_size,omitempty"` // FilePath file path // // optional @@ -1259,6 +2146,12 @@ type WebAppInfo struct { type ReplyKeyboardMarkup struct { // Keyboard is an array of button rows, each represented by an Array of KeyboardButton objects Keyboard [][]KeyboardButton `json:"keyboard"` + // IsPersistent requests clients to always show the keyboard when the regular + // keyboard is hidden. Defaults to false, in which case the custom keyboard + // can be hidden and opened with a keyboard icon. + // + // optional + IsPersistent bool `json:"is_persistent,omitempty"` // ResizeKeyboard requests clients to resize the keyboard vertically for optimal fit // (e.g., make the keyboard smaller if there are just two rows of buttons). // Defaults to false, in which case the custom keyboard @@ -1290,6 +2183,11 @@ type ReplyKeyboardMarkup struct { // // optional Selective bool `json:"selective,omitempty"` + // ForceReply requests clients to show the reply interface to the user, as + // if they had manually selected the bot's message and tapped 'Reply'. + // + // optional + ForceReply bool `json:"force_reply,omitempty"` } // KeyboardButton represents one button of the reply keyboard. For simple text @@ -1300,6 +2198,18 @@ type KeyboardButton struct { // Text of the button. If none of the optional fields are used, // it will be sent as a message when the button is pressed. Text string `json:"text"` + // RequestUsers if specified, pressing the button will open a list of + // suitable users. Identifiers of the selected users will be shared with the + // bot in a "users_shared" service message. Available in private chats only. + // + // optional + RequestUsers *KeyboardButtonRequestUsers `json:"request_users,omitempty"` + // RequestChat if specified, pressing the button will open a list of + // suitable chats. Tapping on a chat will send its identifier to the bot in + // a "chat_shared" service message. Available in private chats only. + // + // optional + RequestChat *KeyboardButtonRequestChat `json:"request_chat,omitempty"` // RequestContact if True, the user's phone number will be sent // as a contact when the button is pressed. // Available in private chats only. @@ -1323,6 +2233,163 @@ type KeyboardButton struct { // // optional WebApp *WebAppInfo `json:"web_app,omitempty"` + // RequestManagedBot if specified, pressing the button will ask the user + // to create and share a bot that will be managed by the current bot. + // Available in private chats only. + // + // optional + RequestManagedBot *KeyboardButtonRequestManagedBot `json:"request_managed_bot,omitempty"` + // IconCustomEmojiID is the unique identifier of the custom emoji to be + // displayed on the button. Available only if the bot can use custom + // emoji in the message. + // + // optional + IconCustomEmojiID string `json:"icon_custom_emoji_id,omitempty"` + // Style of the button. Currently, one of "default", "primary", + // "destructive". Defaults to "default". + // + // optional + Style string `json:"style,omitempty"` +} + +// KeyboardButtonRequestUsers defines the criteria used to request suitable +// users. The identifiers of the selected users will be shared with the bot +// when the corresponding button is pressed. +type KeyboardButtonRequestUsers struct { + // RequestID is a signed 32-bit identifier of the request, which will be + // received back in the UsersShared object. Must be unique within the message. + RequestID int `json:"request_id"` + // UserIsBot pass True to request bots, pass False to request regular + // users. If not specified, no additional restrictions are applied. + // + // optional + UserIsBot *bool `json:"user_is_bot,omitempty"` + // UserIsPremium pass True to request premium users, pass False to request + // non-premium users. If not specified, no additional restrictions are applied. + // + // optional + UserIsPremium *bool `json:"user_is_premium,omitempty"` + // MaxQuantity is the maximum number of users to be selected; 1-10. Defaults to 1. + // + // optional + MaxQuantity int `json:"max_quantity,omitempty"` + // RequestName pass true to request the users' first and last names. + // + // optional + RequestName bool `json:"request_name,omitempty"` + // RequestUsername pass true to request the users' usernames. + // + // optional + RequestUsername bool `json:"request_username,omitempty"` + // RequestPhoto pass true to request the users' photos. + // + // optional + RequestPhoto bool `json:"request_photo,omitempty"` +} + +// KeyboardButtonRequestChat defines the criteria used to request a suitable +// chat. The identifier of the selected chat will be shared with the bot when +// the corresponding button is pressed. +type KeyboardButtonRequestChat struct { + // RequestID is a signed 32-bit identifier of the request, which will be + // received back in the ChatShared object. Must be unique within the message. + RequestID int `json:"request_id"` + // ChatIsChannel pass True to request a channel chat, pass False to request + // a group or a supergroup chat. + ChatIsChannel bool `json:"chat_is_channel"` + // ChatIsForum pass True to request a forum supergroup, pass False to + // request a non-forum chat. If not specified, no additional restrictions + // are applied. + // + // optional + ChatIsForum *bool `json:"chat_is_forum,omitempty"` + // ChatHasUsername pass True to request a supergroup or a channel with a + // username, pass False to request a chat without a username. If not + // specified, no additional restrictions are applied. + // + // optional + ChatHasUsername *bool `json:"chat_has_username,omitempty"` + // ChatIsCreated pass True to request a chat owned by the user. Otherwise, + // no additional restrictions are applied. + // + // optional + ChatIsCreated bool `json:"chat_is_created,omitempty"` + // UserAdministratorRights is the required administrator rights of the + // user in the chat. If not specified, no additional restrictions are applied. + // + // optional + UserAdministratorRights *ChatAdministratorRights `json:"user_administrator_rights,omitempty"` + // BotAdministratorRights is the required administrator rights of the bot + // in the chat. The rights must be a subset of UserAdministratorRights. + // If not specified, no additional restrictions are applied. + // + // optional + BotAdministratorRights *ChatAdministratorRights `json:"bot_administrator_rights,omitempty"` + // BotIsMember pass True to request a chat with the bot as a member. + // Otherwise, no additional restrictions are applied. + // + // optional + BotIsMember bool `json:"bot_is_member,omitempty"` + // RequestTitle pass true to request the chat's title. + // + // optional + RequestTitle bool `json:"request_title,omitempty"` + // RequestUsername pass true to request the chat's username. + // + // optional + RequestUsername bool `json:"request_username,omitempty"` + // RequestPhoto pass true to request the chat's photo. + // + // optional + RequestPhoto bool `json:"request_photo,omitempty"` +} + +// UsersShared contains information about the users whose identifiers were +// shared with the bot using a KeyboardButtonRequestUsers button. +type UsersShared struct { + // RequestID is the identifier of the request. + RequestID int `json:"request_id"` + // Users is the list of shared users. + Users []SharedUser `json:"users"` +} + +// ChatShared contains information about the chat whose identifier was shared +// with the bot using a KeyboardButtonRequestChat button. +type ChatShared struct { + // RequestID is the identifier of the request. + RequestID int `json:"request_id"` + // ChatID is the identifier of the shared chat. + ChatID int64 `json:"chat_id"` + // Title of the chat, if the title was requested by the bot. + // + // optional + Title string `json:"title,omitempty"` + // Username of the chat, if the username was requested by the bot and + // available. + // + // optional + Username string `json:"username,omitempty"` + // Photo of the chat, if the photo was requested by the bot. + // + // optional + Photo []PhotoSize `json:"photo,omitempty"` +} + +// KeyboardButtonRequestManagedBot defines the parameters for the creation +// of a managed bot. Information about the created bot will be shared with the +// bot using the update managed_bot and a Message with the field managed_bot_created. +type KeyboardButtonRequestManagedBot struct { + // RequestID is a signed 32-bit identifier of the request. + // Must be unique within the message. + RequestID int `json:"request_id"` + // SuggestedName is the suggested name for the bot. + // + // optional + SuggestedName string `json:"suggested_name,omitempty"` + // SuggestedUsername is the suggested username for the bot. + // + // optional + SuggestedUsername string `json:"suggested_username,omitempty"` } // KeyboardButtonPollType represents type of poll, which is allowed to @@ -1364,6 +2431,12 @@ type InlineKeyboardMarkup struct { // InlineKeyboard array of button rows, each represented by an Array of // InlineKeyboardButton objects InlineKeyboard [][]InlineKeyboardButton `json:"inline_keyboard"` + // ForceReply requests clients to show the reply interface to the user, as + // if they had manually selected the bot's message and tapped 'Reply'. The + // value of the field can't be changed when the inline keyboard is edited. + // + // optional + ForceReply bool `json:"force_reply,omitempty"` } // InlineKeyboardButton represents one button of an inline keyboard. You must @@ -1416,6 +2489,29 @@ type InlineKeyboardButton struct { // // optional SwitchInlineQueryCurrentChat *string `json:"switch_inline_query_current_chat,omitempty"` + // SwitchInlineQueryChosenChat if set, pressing the button will prompt the + // user to select one of their chats of the specified type, open that chat + // and insert the bot's username and the specified inline query in the + // input field. + // + // optional + SwitchInlineQueryChosenChat *SwitchInlineQueryChosenChat `json:"switch_inline_query_chosen_chat,omitempty"` + // CopyText if set, pressing the button will copy the specified text to + // the clipboard. + // + // optional + CopyText *CopyTextButton `json:"copy_text,omitempty"` + // IconCustomEmojiID is the unique identifier of the custom emoji to be + // displayed on the button. Available only if the bot can use custom + // emoji in the message. + // + // optional + IconCustomEmojiID string `json:"icon_custom_emoji_id,omitempty"` + // Style of the button. Currently, one of "default", "primary", + // "destructive". Defaults to "default". + // + // optional + Style string `json:"style,omitempty"` // CallbackGame description of the game that will be launched when the user presses the button. // // optional @@ -1426,6 +2522,65 @@ type InlineKeyboardButton struct { // // optional Pay bool `json:"pay,omitempty"` + // Disabled if set, then the button is disabled and does nothing. + // + // optional + Disabled *DisabledButton `json:"disabled,omitempty"` +} + +// CopyTextButton represents an inline keyboard button that copies specified +// text to the clipboard when pressed. +type CopyTextButton struct { + // Text is the text to be copied to the clipboard; 1-256 characters. + Text string `json:"text"` +} + +// DisabledButton represents a disabled button which does nothing. Currently +// holds no information. +type DisabledButton struct{} + +// SwitchInlineQueryChosenChat represents an inline button that switches the +// current user to inline mode in a chosen chat, with an optional default +// inline query. +type SwitchInlineQueryChosenChat struct { + // Query is the default inline query to be inserted in the input field. + // If left empty, only the bot's username will be inserted. + // + // optional + Query string `json:"query,omitempty"` + // AllowUserChats is true if private chats with users can be chosen. + // + // optional + AllowUserChats bool `json:"allow_user_chats,omitempty"` + // AllowBotChats is true if private chats with bots can be chosen. + // + // optional + AllowBotChats bool `json:"allow_bot_chats,omitempty"` + // AllowGroupChats is true if group and supergroup chats can be chosen. + // + // optional + AllowGroupChats bool `json:"allow_group_chats,omitempty"` + // AllowChannelChats is true if channel chats can be chosen. + // + // optional + AllowChannelChats bool `json:"allow_channel_chats,omitempty"` +} + +// InlineQueryResultsButton represents a button to be shown above inline query +// results. Exactly one of WebApp or StartParameter must be set. +type InlineQueryResultsButton struct { + // Text label of the button. + Text string `json:"text"` + // WebApp is the description of the Web App that will be launched when the + // user presses the button. + // + // optional + WebApp *WebAppInfo `json:"web_app,omitempty"` + // StartParameter is the deep-linking parameter for the /start message + // sent to the bot when the user presses the button. 1-64 characters. + // + // optional + StartParameter string `json:"start_parameter,omitempty"` } // LoginURL represents a parameter of the inline keyboard button used to @@ -1473,9 +2628,10 @@ type CallbackQuery struct { ID string `json:"id"` // From sender From *User `json:"from"` - // Message with the callback button that originated the query. - // Note that message content and message date will not be available if the - // message is too old. + // Message with the callback button that originated the query. The message + // can be inaccessible (deleted or otherwise unreachable) — in that case + // only Message.MessageID and Message.Chat are populated and Message.Date + // is 0. See InaccessibleMessage for the equivalent dedicated type. // // optional Message *Message `json:"message,omitempty"` @@ -1577,20 +2733,38 @@ type ChatInviteLink struct { // // optional PendingJoinRequestCount int `json:"pending_join_request_count,omitempty"` + // SubscriptionPeriod is the number of seconds the subscription will be + // active for before the next payment. + // + // optional + SubscriptionPeriod int `json:"subscription_period,omitempty"` + // SubscriptionPrice is the amount of Telegram Stars a user must pay + // initially and after each subsequent subscription period to be a member + // of the chat using the link. + // + // optional + SubscriptionPrice int `json:"subscription_price,omitempty"` } type ChatAdministratorRights struct { - IsAnonymous bool `json:"is_anonymous"` - CanManageChat bool `json:"can_manage_chat"` - CanDeleteMessages bool `json:"can_delete_messages"` - CanManageVideoChats bool `json:"can_manage_video_chats"` - CanRestrictMembers bool `json:"can_restrict_members"` - CanPromoteMembers bool `json:"can_promote_members"` - CanChangeInfo bool `json:"can_change_info"` - CanInviteUsers bool `json:"can_invite_users"` - CanPostMessages bool `json:"can_post_messages"` - CanEditMessages bool `json:"can_edit_messages"` - CanPinMessages bool `json:"can_pin_messages"` + IsAnonymous bool `json:"is_anonymous"` + CanManageChat bool `json:"can_manage_chat"` + CanDeleteMessages bool `json:"can_delete_messages"` + CanManageVideoChats bool `json:"can_manage_video_chats"` + CanRestrictMembers bool `json:"can_restrict_members"` + CanPromoteMembers bool `json:"can_promote_members"` + CanChangeInfo bool `json:"can_change_info"` + CanInviteUsers bool `json:"can_invite_users"` + CanPostMessages bool `json:"can_post_messages"` + CanEditMessages bool `json:"can_edit_messages"` + CanPinMessages bool `json:"can_pin_messages"` + CanPostStories bool `json:"can_post_stories"` + CanEditStories bool `json:"can_edit_stories"` + CanDeleteStories bool `json:"can_delete_stories"` + CanManageTopics bool `json:"can_manage_topics"` + CanManageDirectMessages bool `json:"can_manage_direct_messages"` + CanManageTags bool `json:"can_manage_tags"` + CanSendWelcomeMessages bool `json:"can_send_welcome_messages"` } // ChatMember contains information about one member of a chat. @@ -1683,26 +2857,92 @@ type ChatMember struct { // // optional CanPinMessages bool `json:"can_pin_messages,omitempty"` + // CanPostStories administrators only. + // True, if the administrator can post stories to the chat. + // + // optional + CanPostStories bool `json:"can_post_stories,omitempty"` + // CanEditStories administrators only. + // True, if the administrator can edit stories posted by other users. + // + // optional + CanEditStories bool `json:"can_edit_stories,omitempty"` + // CanDeleteStories administrators only. + // True, if the administrator can delete stories posted by other users. + // + // optional + CanDeleteStories bool `json:"can_delete_stories,omitempty"` + // CanManageTopics administrators and restricted only. + // True, if the user is allowed to create, rename, close, and reopen + // forum topics; supergroups only. + // + // optional + CanManageTopics bool `json:"can_manage_topics,omitempty"` + // CanManageDirectMessages administrators only. + // True, if the administrator can manage direct messages within the + // channel and decline suggested posts; for channels only. + // + // optional + CanManageDirectMessages bool `json:"can_manage_direct_messages,omitempty"` + // CanManageTags administrators only. + // True, if the administrator can manage tags of chat members. + // + // optional + CanManageTags bool `json:"can_manage_tags,omitempty"` + // CanSendWelcomeMessages administrators only. + // True, if the administrator can manage chat welcome messages or directly + // send them in the case of bots. + // + // optional + CanSendWelcomeMessages bool `json:"can_send_welcome_messages,omitempty"` + // Tag is the member's custom tag in the chat, if any. + // + // optional + Tag string `json:"tag,omitempty"` + // CanEditTag is true, if the user is allowed to edit their own tag. + // + // optional + CanEditTag bool `json:"can_edit_tag,omitempty"` // IsMember is true, if the user is a member of the chat at the moment of // the request IsMember bool `json:"is_member"` - // CanSendMessages + // CanSendMessages restricted only. + // True, if the user is allowed to send text messages, contacts, + // invoices, locations and venues. // // optional CanSendMessages bool `json:"can_send_messages,omitempty"` - // CanSendMediaMessages restricted only. - // True, if the user is allowed to send text messages, contacts, locations and venues + // CanSendAudios restricted only. True, if the user is allowed to send audios. + // + // optional + CanSendAudios bool `json:"can_send_audios,omitempty"` + // CanSendDocuments restricted only. True, if the user is allowed to send documents. + // + // optional + CanSendDocuments bool `json:"can_send_documents,omitempty"` + // CanSendPhotos restricted only. True, if the user is allowed to send photos. + // + // optional + CanSendPhotos bool `json:"can_send_photos,omitempty"` + // CanSendVideos restricted only. True, if the user is allowed to send videos. + // + // optional + CanSendVideos bool `json:"can_send_videos,omitempty"` + // CanSendVideoNotes restricted only. True, if the user is allowed to send video notes. + // + // optional + CanSendVideoNotes bool `json:"can_send_video_notes,omitempty"` + // CanSendVoiceNotes restricted only. True, if the user is allowed to send voice notes. // // optional - CanSendMediaMessages bool `json:"can_send_media_messages,omitempty"` + CanSendVoiceNotes bool `json:"can_send_voice_notes,omitempty"` // CanSendPolls restricted only. // True, if the user is allowed to send polls // // optional CanSendPolls bool `json:"can_send_polls,omitempty"` // CanSendOtherMessages restricted only. - // True, if the user is allowed to send audios, documents, - // photos, videos, video notes and voice notes. + // True, if the user is allowed to send animations, games, stickers and use inline bots. // // optional CanSendOtherMessages bool `json:"can_send_other_messages,omitempty"` @@ -1711,6 +2951,11 @@ type ChatMember struct { // // optional CanAddWebPagePreviews bool `json:"can_add_web_page_previews,omitempty"` + // CanReactToMessages restricted only. + // True, if the user is allowed to react to messages. + // + // optional + CanReactToMessages bool `json:"can_react_to_messages,omitempty"` } // IsCreator returns if the ChatMember was the creator of the chat. @@ -1742,6 +2987,17 @@ type ChatMemberUpdated struct { // // optional InviteLink *ChatInviteLink `json:"invite_link,omitempty"` + // ViaJoinRequest is true, if the user joined the chat after sending a + // direct join request without using an invite link and being approved + // by an administrator. + // + // optional + ViaJoinRequest bool `json:"via_join_request,omitempty"` + // ViaChatFolderInviteLink is true, if the user joined the chat via a chat + // folder invite link. + // + // optional + ViaChatFolderInviteLink bool `json:"via_chat_folder_invite_link,omitempty"` } // ChatJoinRequest represents a join request sent to a chat. @@ -1750,6 +3006,11 @@ type ChatJoinRequest struct { Chat Chat `json:"chat"` // User that sent the join request. From User `json:"from"` + // UserChatID is the identifier of a private chat with the user who sent the + // join request. The bot can use this identifier for 5 minutes to send messages + // until the join request is processed, assuming no other administrator + // contacted the user. + UserChatID int64 `json:"user_chat_id"` // Date the request was sent in Unix time. Date int `json:"date"` // Bio of the user. @@ -1760,37 +3021,66 @@ type ChatJoinRequest struct { // // optional InviteLink *ChatInviteLink `json:"invite_link,omitempty"` + // QueryID is the identifier of the join request query; for bots assigned + // to process join requests only. If present, then the bot must call + // sendChatJoinRequestWebApp or directly call answerChatJoinRequestQuery + // within 10 seconds. + // + // optional + QueryID string `json:"query_id,omitempty"` } // ChatPermissions describes actions that a non-administrator user is // allowed to take in a chat. All fields are optional. type ChatPermissions struct { // CanSendMessages is true, if the user is allowed to send text messages, - // contacts, locations and venues + // contacts, invoices, locations and venues // // optional CanSendMessages bool `json:"can_send_messages,omitempty"` - // CanSendMediaMessages is true, if the user is allowed to send audios, - // documents, photos, videos, video notes and voice notes, implies - // can_send_messages + // CanSendAudios is true, if the user is allowed to send audios. + // + // optional + CanSendAudios bool `json:"can_send_audios,omitempty"` + // CanSendDocuments is true, if the user is allowed to send documents. + // + // optional + CanSendDocuments bool `json:"can_send_documents,omitempty"` + // CanSendPhotos is true, if the user is allowed to send photos. + // + // optional + CanSendPhotos bool `json:"can_send_photos,omitempty"` + // CanSendVideos is true, if the user is allowed to send videos. + // + // optional + CanSendVideos bool `json:"can_send_videos,omitempty"` + // CanSendVideoNotes is true, if the user is allowed to send video notes. // // optional - CanSendMediaMessages bool `json:"can_send_media_messages,omitempty"` - // CanSendPolls is true, if the user is allowed to send polls, implies - // can_send_messages + CanSendVideoNotes bool `json:"can_send_video_notes,omitempty"` + // CanSendVoiceNotes is true, if the user is allowed to send voice notes. + // + // optional + CanSendVoiceNotes bool `json:"can_send_voice_notes,omitempty"` + // CanSendPolls is true, if the user is allowed to send polls. // // optional CanSendPolls bool `json:"can_send_polls,omitempty"` // CanSendOtherMessages is true, if the user is allowed to send animations, - // games, stickers and use inline bots, implies can_send_media_messages + // games, stickers and use inline bots. // // optional CanSendOtherMessages bool `json:"can_send_other_messages,omitempty"` // CanAddWebPagePreviews is true, if the user is allowed to add web page - // previews to their messages, implies can_send_media_messages + // previews to their messages. // // optional CanAddWebPagePreviews bool `json:"can_add_web_page_previews,omitempty"` + // CanReactToMessages is true, if the user is allowed to react to + // messages. If omitted, defaults to the value of CanSendMessages. + // + // optional + CanReactToMessages bool `json:"can_react_to_messages,omitempty"` // CanChangeInfo is true, if the user is allowed to change the chat title, // photo and other settings. Ignored in public supergroups // @@ -1806,1525 +3096,5072 @@ type ChatPermissions struct { // // optional CanPinMessages bool `json:"can_pin_messages,omitempty"` + // CanManageTopics is true, if the user is allowed to create forum topics. + // If omitted defaults to the value of can_pin_messages + // + // optional + CanManageTopics bool `json:"can_manage_topics,omitempty"` + // CanEditTag is true, if users are allowed to edit their own tag. + // + // optional + CanEditTag bool `json:"can_edit_tag,omitempty"` } -// ChatLocation represents a location to which a chat is connected. -type ChatLocation struct { - // Location is the location to which the supergroup is connected. Can't be a - // live location. - Location Location `json:"location"` - // Address is the location address; 1-64 characters, as defined by the chat - // owner - Address string `json:"address"` +// Story represents a story. +type Story struct { + // Chat that posted the story. + Chat Chat `json:"chat"` + // ID is the unique identifier of the story in the chat. + ID int `json:"id"` } -// BotCommand represents a bot command. -type BotCommand struct { - // Command text of the command, 1-32 characters. - // Can contain only lowercase English letters, digits and underscores. - Command string `json:"command"` - // Description of the command, 3-256 characters. - Description string `json:"description"` +// ChatOwnerLeft represents a service message about the chat owner leaving the +// chat. +type ChatOwnerLeft struct { + // NewOwner is the user who will become the new owner of the chat if the + // previous owner does not return to the chat. + // + // optional + NewOwner *User `json:"new_owner,omitempty"` } -// BotCommandScope represents the scope to which bot commands are applied. -// -// It contains the fields for all types of scopes, different types only support -// specific (or no) fields. -type BotCommandScope struct { - Type string `json:"type"` - ChatID int64 `json:"chat_id,omitempty"` - UserID int64 `json:"user_id,omitempty"` +// ChatOwnerChanged represents a service message about a change of the chat +// owner. +type ChatOwnerChanged struct { + // NewOwner is the new owner of the chat. + NewOwner User `json:"new_owner"` } -// MenuButton describes the bot's menu button in a private chat. -type MenuButton struct { - // Type is the type of menu button, must be one of: - // - `commands` - // - `web_app` - // - `default` - Type string `json:"type"` - // Text is the text on the button, for `web_app` type. - Text string `json:"text,omitempty"` - // WebApp is the description of the Web App that will be launched when the - // user presses the button for the `web_app` type. - WebApp *WebAppInfo `json:"web_app,omitempty"` +// VideoQuality represents a video file of a specific quality. +type VideoQuality struct { + // FileID is the identifier for this file, which can be used to download + // or reuse the file. + FileID string `json:"file_id"` + // FileUniqueID is the unique identifier for this file, which is supposed + // to be the same over time and for different bots. Can't be used to + // download or reuse the file. + FileUniqueID string `json:"file_unique_id"` + // Width is the video width. + Width int `json:"width"` + // Height is the video height. + Height int `json:"height"` + // Codec is the codec that was used to encode the video, for example, + // "h264", "h265", or "av01". + Codec string `json:"codec"` + // FileSize is the file size in bytes. + // + // optional + FileSize int64 `json:"file_size,omitempty"` } -// ResponseParameters are various errors that can be returned in APIResponse. -type ResponseParameters struct { - // The group has been migrated to a supergroup with the specified identifier. +// UserProfileAudios represents the audios displayed on a user's profile. +type UserProfileAudios struct { + // TotalCount is the total number of profile audios for the target user. + TotalCount int `json:"total_count"` + // Audios are the requested profile audios. + Audios []Audio `json:"audios"` +} + +// ChatBoostAdded represents a service message about a user boosting a chat. +type ChatBoostAdded struct { + // BoostCount is the number of boosts added by the user. + BoostCount int `json:"boost_count"` +} + +// BusinessBotRights represents the rights of a business bot. +type BusinessBotRights struct { + // CanReply is true, if the bot can send and edit messages in the chats + // that were active in the last 24 hours. // // optional - MigrateToChatID int64 `json:"migrate_to_chat_id,omitempty"` - // In case of exceeding flood control, the number of seconds left to wait - // before the request can be repeated. + CanReply bool `json:"can_reply,omitempty"` + // CanReadMessages is true, if the bot can mark incoming private messages as read. // // optional - RetryAfter int `json:"retry_after,omitempty"` -} - -// BaseInputMedia is a base type for the InputMedia types. -type BaseInputMedia struct { - // Type of the result. - Type string `json:"type"` - // Media file to send. Pass a file_id to send a file - // that exists on the Telegram servers (recommended), - // pass an HTTP URL for Telegram to get a file from the Internet, - // or pass “attach://” to upload a new one - // using multipart/form-data under name. - Media RequestFileData `json:"media"` - // thumb intentionally missing as it is not currently compatible - - // Caption of the video to be sent, 0-1024 characters after entities parsing. + CanReadMessages bool `json:"can_read_messages,omitempty"` + // CanDeleteSentMessages is true, if the bot can delete sent messages. // // optional - Caption string `json:"caption,omitempty"` - // ParseMode mode for parsing entities in the video caption. - // See formatting options for more details - // (https://core.telegram.org/bots/api#formatting-options). + CanDeleteSentMessages bool `json:"can_delete_sent_messages,omitempty"` + // CanDeleteAllMessages is true, if the bot can delete any message. // // optional - ParseMode string `json:"parse_mode,omitempty"` - // CaptionEntities is a list of special entities that appear in the caption, - // which can be specified instead of parse_mode + CanDeleteAllMessages bool `json:"can_delete_all_messages,omitempty"` + // CanEditName is true, if the bot can edit the first and last name of the account. // // optional - CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` -} - -// InputMediaPhoto is a photo to send as part of a media group. -type InputMediaPhoto struct { - BaseInputMedia -} - -// InputMediaVideo is a video to send as part of a media group. -type InputMediaVideo struct { - BaseInputMedia - // Thumbnail of the file sent; can be ignored if thumbnail generation for - // the file is supported server-side. + CanEditName bool `json:"can_edit_name,omitempty"` + // CanEditBio is true, if the bot can edit the bio of the account. // // optional - Thumb RequestFileData `json:"thumb,omitempty"` - // Width video width + CanEditBio bool `json:"can_edit_bio,omitempty"` + // CanEditProfilePhoto is true, if the bot can edit the profile photo of the account. // // optional - Width int `json:"width,omitempty"` - // Height video height + CanEditProfilePhoto bool `json:"can_edit_profile_photo,omitempty"` + // CanEditUsername is true, if the bot can edit the username of the account. // // optional - Height int `json:"height,omitempty"` - // Duration video duration + CanEditUsername bool `json:"can_edit_username,omitempty"` + // CanChangeGiftSettings is true, if the bot can change the privacy settings + // pertaining to gifts for the account. // // optional - Duration int `json:"duration,omitempty"` - // SupportsStreaming pass True, if the uploaded video is suitable for streaming. + CanChangeGiftSettings bool `json:"can_change_gift_settings,omitempty"` + // CanViewGiftsAndStars is true, if the bot can view gifts and the amount + // of Telegram Stars owned by the account. // // optional - SupportsStreaming bool `json:"supports_streaming,omitempty"` -} - -// InputMediaAnimation is an animation to send as part of a media group. -type InputMediaAnimation struct { - BaseInputMedia - // Thumbnail of the file sent; can be ignored if thumbnail generation for - // the file is supported server-side. + CanViewGiftsAndStars bool `json:"can_view_gifts_and_stars,omitempty"` + // CanConvertGiftsToStars is true, if the bot can convert regular gifts owned + // by the account to Telegram Stars. // // optional - Thumb RequestFileData `json:"thumb,omitempty"` - // Width video width + CanConvertGiftsToStars bool `json:"can_convert_gifts_to_stars,omitempty"` + // CanTransferAndUpgradeGifts is true, if the bot can transfer and upgrade + // gifts owned by the account. // // optional - Width int `json:"width,omitempty"` - // Height video height + CanTransferAndUpgradeGifts bool `json:"can_transfer_and_upgrade_gifts,omitempty"` + // CanTransferStars is true, if the bot can transfer Telegram Stars received + // by the account. // // optional - Height int `json:"height,omitempty"` - // Duration video duration + CanTransferStars bool `json:"can_transfer_stars,omitempty"` + // CanManageStories is true, if the bot can post, edit and delete stories + // on behalf of the account. // // optional - Duration int `json:"duration,omitempty"` + CanManageStories bool `json:"can_manage_stories,omitempty"` } -// InputMediaAudio is an audio to send as part of a media group. -type InputMediaAudio struct { - BaseInputMedia - // Thumbnail of the file sent; can be ignored if thumbnail generation for - // the file is supported server-side. +// BusinessConnection describes the connection of the bot with a business account. +type BusinessConnection struct { + // ID is the unique identifier of the business connection. + ID string `json:"id"` + // User is the business account user that created the business connection. + User User `json:"user"` + // UserChatID is the identifier of a private chat with the user who + // created the business connection. + UserChatID int64 `json:"user_chat_id"` + // Date the connection was established in Unix time. + Date int `json:"date"` + // Rights is the rights of the business bot. // // optional - Thumb RequestFileData `json:"thumb,omitempty"` - // Duration of the audio in seconds + Rights *BusinessBotRights `json:"rights,omitempty"` + // IsEnabled is true, if the connection is active. + IsEnabled bool `json:"is_enabled"` +} + +// StarAmount describes an amount of Telegram Stars. +type StarAmount struct { + // Amount is the integer amount of Telegram Stars, rounded to 0; can be negative. + Amount int `json:"amount"` + // NanostarAmount is the number of 1/1000000000 shares of Telegram Stars; + // from -999999999 to 999999999. // // optional - Duration int `json:"duration,omitempty"` - // Performer of the audio + NanostarAmount int `json:"nanostar_amount,omitempty"` +} + +// AcceptedGiftTypes describes the types of gifts accepted by a user or chat. +type AcceptedGiftTypes struct { + // UnlimitedGifts is true, if unlimited regular gifts are accepted. + UnlimitedGifts bool `json:"unlimited_gifts"` + // LimitedGifts is true, if limited regular gifts are accepted. + LimitedGifts bool `json:"limited_gifts"` + // UniqueGifts is true, if unique gifts or gifts that can be upgraded to + // unique for free are accepted. + UniqueGifts bool `json:"unique_gifts"` + // PremiumSubscription is true, if a Telegram Premium subscription is + // accepted. + PremiumSubscription bool `json:"premium_subscription"` + // GiftsFromChannels is true, if gifts published by channels are accepted. // // optional - Performer string `json:"performer,omitempty"` - // Title of the audio + GiftsFromChannels bool `json:"gifts_from_channels,omitempty"` +} + +// BusinessMessagesDeleted is received when messages are deleted from a +// connected business account. +type BusinessMessagesDeleted struct { + // BusinessConnectionID is the unique identifier of the business connection. + BusinessConnectionID string `json:"business_connection_id"` + // Chat in which the messages were deleted. The bot may not have access to + // the chat or the corresponding user. + Chat Chat `json:"chat"` + // MessageIDs is the list of identifiers of deleted messages in the chat. + MessageIDs []int `json:"message_ids"` +} + +// BusinessIntro contains information about the intro of a business. +type BusinessIntro struct { + // Title text of the business intro. // // optional Title string `json:"title,omitempty"` -} - -// InputMediaDocument is a general file to send as part of a media group. -type InputMediaDocument struct { - BaseInputMedia - // Thumbnail of the file sent; can be ignored if thumbnail generation for - // the file is supported server-side. + // Message text of the business intro. // // optional - Thumb RequestFileData `json:"thumb,omitempty"` - // DisableContentTypeDetection disables automatic server-side content type - // detection for files uploaded using multipart/form-data. Always true, if - // the document is sent as part of an album + Message string `json:"message,omitempty"` + // Sticker of the business intro. // // optional - DisableContentTypeDetection bool `json:"disable_content_type_detection,omitempty"` + Sticker *Sticker `json:"sticker,omitempty"` } -// Sticker represents a sticker. -type Sticker struct { - // FileID is an identifier for this file, which can be used to download or - // reuse the file - FileID string `json:"file_id"` - // FileUniqueID is a unique identifier for this file, - // which is supposed to be the same over time and for different bots. - // Can't be used to download or reuse the file. - FileUniqueID string `json:"file_unique_id"` - // Width sticker width - Width int `json:"width"` - // Height sticker height - Height int `json:"height"` - // IsAnimated true, if the sticker is animated +// BusinessLocation contains information about the location of a business. +type BusinessLocation struct { + // Address of the business. + Address string `json:"address"` + // Location of the business. // // optional - IsAnimated bool `json:"is_animated,omitempty"` - // IsVideo true, if the sticker is a video sticker + Location *Location `json:"location,omitempty"` +} + +// BusinessOpeningHoursInterval describes an interval of time during which a +// business is open. +type BusinessOpeningHoursInterval struct { + // OpeningMinute is the minute's sequence number in a week, starting on + // Monday, marking the start of the time interval during which the + // business is open; 0 - 7 * 24 * 60. + OpeningMinute int `json:"opening_minute"` + // ClosingMinute is the minute's sequence number in a week, starting on + // Monday, marking the end of the time interval during which the business + // is open; 0 - 8 * 24 * 60. + ClosingMinute int `json:"closing_minute"` +} + +// BusinessOpeningHours describes the opening hours of a business. +type BusinessOpeningHours struct { + // TimeZoneName is the unique name of the time zone for which the opening + // hours are defined. + TimeZoneName string `json:"time_zone_name"` + // OpeningHours is the list of time intervals describing business opening + // hours. + OpeningHours []BusinessOpeningHoursInterval `json:"opening_hours"` +} + +// Background fill type constants. +const ( + BackgroundFillTypeSolid = "solid" + BackgroundFillTypeGradient = "gradient" + BackgroundFillTypeFreeformGradient = "freeform_gradient" +) + +// Background type constants. +const ( + BackgroundTypeFill = "fill" + BackgroundTypeWallpaper = "wallpaper" + BackgroundTypePattern = "pattern" + BackgroundTypeChatTheme = "chat_theme" +) + +// BackgroundFill describes the way a background is filled based on the +// selected colors. Flat polymorphic by Type: +// - "solid" → Color +// - "gradient" → TopColor, BottomColor, RotationAngle +// - "freeform_gradient" → Colors (3 or 4 RGB colors) +type BackgroundFill struct { + // Type of the fill. One of "solid", "gradient", "freeform_gradient". + Type string `json:"type"` + // Color is the fill color in RGB format. Set when Type is "solid". // // optional - IsVideo bool `json:"is_video,omitempty"` - // Thumbnail sticker thumbnail in the .WEBP or .JPG format + Color int `json:"color,omitempty"` + // TopColor is the top color of the gradient in RGB format. Set when Type + // is "gradient". // // optional - Thumbnail *PhotoSize `json:"thumb,omitempty"` - // Emoji associated with the sticker + TopColor int `json:"top_color,omitempty"` + // BottomColor is the bottom color of the gradient in RGB format. Set + // when Type is "gradient". // // optional - Emoji string `json:"emoji,omitempty"` - // SetName of the sticker set to which the sticker belongs + BottomColor int `json:"bottom_color,omitempty"` + // RotationAngle is the clockwise rotation angle of the background fill + // in degrees; 0-359. Set when Type is "gradient". // // optional - SetName string `json:"set_name,omitempty"` - // PremiumAnimation for premium regular stickers, premium animation for the sticker + RotationAngle int `json:"rotation_angle,omitempty"` + // Colors is a list of 3 or 4 RGB colors. Set when Type is + // "freeform_gradient". // // optional - PremiumAnimation *File `json:"premium_animation,omitempty"` - // MaskPosition is for mask stickers, the position where the mask should be - // placed + Colors []int `json:"colors,omitempty"` +} + +// BackgroundType describes the type of a chat background. Flat polymorphic +// by Type: +// - "fill" → Fill, DarkThemeDimming +// - "wallpaper" → Document, DarkThemeDimming, IsBlurred, IsMoving +// - "pattern" → Document, Fill, Intensity, IsInverted, IsMoving +// - "chat_theme" → ThemeName +type BackgroundType struct { + // Type of the background. One of "fill", "wallpaper", "pattern", + // "chat_theme". + Type string `json:"type"` + // Fill is the background fill. Set when Type is "fill" or "pattern". // // optional - MaskPosition *MaskPosition `json:"mask_position,omitempty"` - // CustomEmojiID for custom emoji stickers, unique identifier of the custom emoji + Fill *BackgroundFill `json:"fill,omitempty"` + // DarkThemeDimming is the dimming of the background in dark themes, as + // a percentage; 0-100. Set when Type is "fill" or "wallpaper". // // optional - CustomEmojiID string `json:"custom_emoji_id,omitempty"` - // FileSize + DarkThemeDimming int `json:"dark_theme_dimming,omitempty"` + // Document is the document with the wallpaper/pattern. Set when Type is + // "wallpaper" or "pattern". // // optional - FileSize int `json:"file_size,omitempty"` -} - -// StickerSet represents a sticker set. -type StickerSet struct { - // Name sticker set name - Name string `json:"name"` - // Title sticker set title - Title string `json:"title"` - // StickerType of stickers in the set, currently one of “regular”, “mask”, “custom_emoji” - StickerType string `json:"sticker_type"` - // IsAnimated true, if the sticker set contains animated stickers - IsAnimated bool `json:"is_animated"` - // IsVideo true, if the sticker set contains video stickers - IsVideo bool `json:"is_video"` - // ContainsMasks true, if the sticker set contains masks - ContainsMasks bool `json:"contains_masks"` - // Stickers list of all set stickers - Stickers []Sticker `json:"stickers"` - // Thumb is the sticker set thumbnail in the .WEBP or .TGS format - Thumbnail *PhotoSize `json:"thumb"` + Document *Document `json:"document,omitempty"` + // IsBlurred is true, if the wallpaper is downscaled to fit in a 450x450 + // square and then box-blurred with radius 12. Set when Type is "wallpaper". + // + // optional + IsBlurred bool `json:"is_blurred,omitempty"` + // IsMoving is true, if the background moves slightly when the device is + // tilted. Set when Type is "wallpaper" or "pattern". + // + // optional + IsMoving bool `json:"is_moving,omitempty"` + // Intensity is the intensity of the pattern when it is shown above the + // filled background; 0-100. Set when Type is "pattern". + // + // optional + Intensity int `json:"intensity,omitempty"` + // IsInverted is true, if the background fill must be applied only to + // the pattern itself. All other pixels are black. Set when Type is + // "pattern". + // + // optional + IsInverted bool `json:"is_inverted,omitempty"` + // ThemeName is the name of the chat theme, which is usually an emoji. + // Set when Type is "chat_theme". + // + // optional + ThemeName string `json:"theme_name,omitempty"` } -// MaskPosition describes the position on faces where a mask should be placed -// by default. -type MaskPosition struct { - // The part of the face relative to which the mask should be placed. - // One of “forehead”, “eyes”, “mouth”, or “chin”. - Point string `json:"point"` - // Shift by X-axis measured in widths of the mask scaled to the face size, - // from left to right. For example, choosing -1.0 will place mask just to - // the left of the default mask position. - XShift float64 `json:"x_shift"` - // Shift by Y-axis measured in heights of the mask scaled to the face size, - // from top to bottom. For example, 1.0 will place the mask just below the - // default mask position. - YShift float64 `json:"y_shift"` - // Mask scaling coefficient. For example, 2.0 means double size. - Scale float64 `json:"scale"` +// ChatBackground represents a chat background. +type ChatBackground struct { + // Type of the background. + Type BackgroundType `json:"type"` } -// Game represents a game. Use BotFather to create and edit games, their short -// names will act as unique identifiers. -type Game struct { - // Title of the game - Title string `json:"title"` - // Description of the game - Description string `json:"description"` - // Photo that will be displayed in the game message in chats. - Photo []PhotoSize `json:"photo"` - // Text a brief description of the game or high scores included in the game message. - // Can be automatically edited to include current high scores for the game - // when the bot calls setGameScore, or manually edited using editMessageText. 0-4096 characters. +// InputPollOption contains information about one answer option in a poll +// to be sent. +type InputPollOption struct { + // Text is the option text, 1-100 characters. + Text string `json:"text"` + // TextParseMode is the mode for parsing entities in the text. Currently, + // only custom_emoji entities are allowed. // // optional - Text string `json:"text,omitempty"` - // TextEntities special entities that appear in text, such as usernames, URLs, bot commands, etc. + TextParseMode string `json:"text_parse_mode,omitempty"` + // TextEntities is a list of special entities that appear in the poll + // option text. It can be specified instead of TextParseMode. // // optional TextEntities []MessageEntity `json:"text_entities,omitempty"` - // Animation is an animation that will be displayed in the game message in chats. - // Upload via BotFather (https://t.me/botfather). + // Media is the optional media attached to the poll option. The value + // must be one of the InputMedia* variants accepted by InputPollOption + // (InputMediaAnimation, InputMediaLink, InputMediaLivePhoto, + // InputMediaLocation, InputMediaPhoto, InputMediaSticker, + // InputMediaVenue, InputMediaVideo). // // optional - Animation Animation `json:"animation,omitempty"` + Media any `json:"media,omitempty"` } -// GameHighScore is a user's score and position on the leaderboard. -type GameHighScore struct { - // Position in high score table for the game - Position int `json:"position"` - // User user - User User `json:"user"` - // Score score - Score int `json:"score"` +// Birthdate contains information about a user's birthdate. +type Birthdate struct { + // Day of the user's birth; 1-31. + Day int `json:"day"` + // Month of the user's birth; 1-12. + Month int `json:"month"` + // Year of the user's birth. + // + // optional + Year int `json:"year,omitempty"` } -// CallbackGame is for starting a game in an inline keyboard button. -type CallbackGame struct{} +// Revenue withdrawal state type constants. +const ( + RevenueWithdrawalStateTypePending = "pending" + RevenueWithdrawalStateTypeSucceeded = "succeeded" + RevenueWithdrawalStateTypeFailed = "failed" +) -// WebhookInfo is information about a currently set webhook. -type WebhookInfo struct { - // URL webhook URL, may be empty if webhook is not set up. - URL string `json:"url"` - // HasCustomCertificate true, if a custom certificate was provided for webhook certificate checks. - HasCustomCertificate bool `json:"has_custom_certificate"` - // PendingUpdateCount number of updates awaiting delivery. - PendingUpdateCount int `json:"pending_update_count"` - // IPAddress is the currently used webhook IP address +// RevenueWithdrawalState describes the state of a revenue withdrawal. Flat +// polymorphic by Type: +// - "pending" → (no extra fields) +// - "succeeded" → Date, URL +// - "failed" → (no extra fields) +type RevenueWithdrawalState struct { + // Type of the state. One of "pending", "succeeded", "failed". + Type string `json:"type"` + // Date of the withdrawal in Unix time. Set when Type is "succeeded". // // optional - IPAddress string `json:"ip_address,omitempty"` - // LastErrorDate unix time for the most recent error - // that happened when trying to deliver an update via webhook. + Date int `json:"date,omitempty"` + // URL is an HTTPS URL where the withdrawal transaction can be seen. + // Set when Type is "succeeded". // // optional - LastErrorDate int `json:"last_error_date,omitempty"` - // LastErrorMessage error message in human-readable format for the most recent error - // that happened when trying to deliver an update via webhook. + URL string `json:"url,omitempty"` +} + +// Transaction partner type constants. +const ( + TransactionPartnerTypeUser = "user" + TransactionPartnerTypeChat = "chat" + TransactionPartnerTypeAffiliateProgram = "affiliate_program" + TransactionPartnerTypeFragment = "fragment" + TransactionPartnerTypeTelegramAds = "telegram_ads" + TransactionPartnerTypeTelegramApi = "telegram_api" + TransactionPartnerTypeOther = "other" +) + +// AffiliateInfo contains information about the affiliate that received a +// commission via an affiliate program. +type AffiliateInfo struct { + // AffiliateUser is the bot or user that received the commission. // // optional - LastErrorMessage string `json:"last_error_message,omitempty"` - // LastSynchronizationErrorDate is the unix time of the most recent error that - // happened when trying to synchronize available updates with Telegram datacenters. - LastSynchronizationErrorDate int `json:"last_synchronization_error_date,omitempty"` - // MaxConnections maximum allowed number of simultaneous - // HTTPS connections to the webhook for update delivery. + AffiliateUser *User `json:"affiliate_user,omitempty"` + // AffiliateChat is the chat that received the commission. // // optional - MaxConnections int `json:"max_connections,omitempty"` - // AllowedUpdates is a list of update types the bot is subscribed to. - // Defaults to all update types + AffiliateChat *Chat `json:"affiliate_chat,omitempty"` + // CommissionPerMille is the number of Telegram Stars received by the + // affiliate for each 1000 Telegram Stars received by the bot from + // referred users. + CommissionPerMille int `json:"commission_per_mille"` + // Amount is the integer amount of Telegram Stars received by the + // affiliate from the transaction, rounded to 0; can be negative. + Amount int `json:"amount"` + // NanostarAmount is the number of 1/1000000000 shares of Telegram Stars + // received by the affiliate. Can be negative. + // + // optional + NanostarAmount int `json:"nanostar_amount,omitempty"` +} + +// TransactionPartner describes the source or recipient of a StarTransaction. +// Flat polymorphic by Type: +// - "user" → User is set; Affiliate / InvoicePayload / PaidMedia / Gift optionally set +// - "chat" → Chat is set; Gift optionally set +// - "affiliate_program" → SponsorUser, CommissionPerMille +// - "fragment" → WithdrawalState is set +// - "telegram_ads" → (no extra fields) +// - "telegram_api" → RequestCount is set +// - "other" → (no extra fields) +type TransactionPartner struct { + // Type of the transaction partner. One of "user", "chat", + // "affiliate_program", "fragment", "telegram_ads", "telegram_api", + // "other". + Type string `json:"type"` + // Chat that the transaction involves. Set when Type is "chat". // // optional - AllowedUpdates []string `json:"allowed_updates,omitempty"` -} - -// IsSet returns true if a webhook is currently set. -func (info WebhookInfo) IsSet() bool { - return info.URL != "" -} - -// InlineQuery is a Query from Telegram for an inline request. -type InlineQuery struct { - // ID unique identifier for this query - ID string `json:"id"` - // From sender - From *User `json:"from"` - // Query text of the query (up to 256 characters). - Query string `json:"query"` - // Offset of the results to be returned, can be controlled by the bot. - Offset string `json:"offset"` - // Type of the chat, from which the inline query was sent. Can be either - // “sender” for a private chat with the inline query sender, “private”, - // “group”, “supergroup”, or “channel”. The chat type should be always known - // for requests sent from official clients and most third-party clients, - // unless the request was sent from a secret chat + Chat *Chat `json:"chat,omitempty"` + // RequestCount is the number of successful requests that caused the + // transaction. Set when Type is "telegram_api". // // optional - ChatType string `json:"chat_type,omitempty"` - // Location sender location, only for bots that request user location. + RequestCount int `json:"request_count,omitempty"` + // User that the transaction involves. Set when Type is "user". // // optional - Location *Location `json:"location,omitempty"` -} - -// InlineQueryResultCachedAudio is an inline query response with cached audio. -type InlineQueryResultCachedAudio struct { - // Type of the result, must be audio - Type string `json:"type"` - // ID unique identifier for this result, 1-64 bytes - ID string `json:"id"` - // AudioID a valid file identifier for the audio file - AudioID string `json:"audio_file_id"` - // Caption 0-1024 characters after entities parsing + User *User `json:"user,omitempty"` + // Affiliate information if the transaction involves an affiliate + // commission. Set when Type is "user". // // optional - Caption string `json:"caption,omitempty"` - // ParseMode mode for parsing entities in the video caption. - // See formatting options for more details - // (https://core.telegram.org/bots/api#formatting-options). + Affiliate *AffiliateInfo `json:"affiliate,omitempty"` + // InvoicePayload is the bot-specified invoice payload. Set when Type is + // "user". // // optional - ParseMode string `json:"parse_mode,omitempty"` - // CaptionEntities is a list of special entities that appear in the caption, - // which can be specified instead of parse_mode + InvoicePayload string `json:"invoice_payload,omitempty"` + // SubscriptionPeriod is the number of seconds the subscription will be + // active for. Set when Type is "user" and the transaction is the + // payment for a subscription. // // optional - CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` - // ReplyMarkup inline keyboard attached to the message + SubscriptionPeriod int `json:"subscription_period,omitempty"` + // PremiumSubscriptionDuration is the duration of a paid Telegram + // Premium subscription, in months. Set when Type is "user" and the + // transaction is a premium subscription gift. // // optional - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - // InputMessageContent content of the message to be sent instead of the audio + PremiumSubscriptionDuration int `json:"premium_subscription_duration,omitempty"` + // TransactionType describes the type of the transaction. Set when + // Type is "user". Known values: "invoice_payment", "paid_media_payment", + // "gift_purchase", "premium_purchase", "business_account_transfer". // // optional - InputMessageContent interface{} `json:"input_message_content,omitempty"` -} - -// InlineQueryResultCachedDocument is an inline query response with cached document. -type InlineQueryResultCachedDocument struct { - // Type of the result, must be a document - Type string `json:"type"` - // ID unique identifier for this result, 1-64 bytes - ID string `json:"id"` - // DocumentID a valid file identifier for the file - DocumentID string `json:"document_file_id"` - // Title for the result + TransactionType string `json:"transaction_type,omitempty"` + // PaidMedia is the information about the paid media bought by the user. + // Set when Type is "user" and the transaction involves paid media. // // optional - Title string `json:"title,omitempty"` - // Caption of the document to be sent, 0-1024 characters after entities parsing + PaidMedia []PaidMedia `json:"paid_media,omitempty"` + // PaidMediaPayload is the bot-specified paid media payload. Set when + // Type is "user" and TransactionType is "paid_media_payment". // // optional - Caption string `json:"caption,omitempty"` - // Description short description of the result + PaidMediaPayload string `json:"paid_media_payload,omitempty"` + // Gift is the gift sent to the user by the bot. Set when Type is + // "user" and the transaction is a gift purchase. // // optional - Description string `json:"description,omitempty"` - // ParseMode mode for parsing entities in the video caption. - // // See formatting options for more details - // // (https://core.telegram.org/bots/api#formatting-options). + Gift *Gift `json:"gift,omitempty"` + // SponsorUser is the bot owning the affiliate program. Set when Type + // is "affiliate_program". // // optional - ParseMode string `json:"parse_mode,omitempty"` - // CaptionEntities is a list of special entities that appear in the caption, - // which can be specified instead of parse_mode + SponsorUser *User `json:"sponsor_user,omitempty"` + // CommissionPerMille is the number of Telegram Stars received by the + // bot for each 1000 Telegram Stars received by the affiliate program + // sponsor from referred users. Set when Type is "affiliate_program". // // optional - CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` - // ReplyMarkup inline keyboard attached to the message + CommissionPerMille int `json:"commission_per_mille,omitempty"` + // WithdrawalState is the state of the transaction if the transaction is + // outgoing. Set when Type is "fragment". // // optional - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - // InputMessageContent content of the message to be sent instead of the file + WithdrawalState *RevenueWithdrawalState `json:"withdrawal_state,omitempty"` +} + +// StarTransaction describes a Telegram Star transaction. +type StarTransaction struct { + // ID is the unique identifier of the transaction. Coincides with the + // identifier of the original transaction for refund transactions. Can + // be used to match transactions to refunds. + ID string `json:"id"` + // Amount of Telegram Stars transferred by the transaction. + Amount int `json:"amount"` + // NanostarAmount is the number of 1/1000000000 shares of Telegram Stars + // transferred by the transaction; from 0 to 999999999. // // optional - InputMessageContent interface{} `json:"input_message_content,omitempty"` + NanostarAmount int `json:"nanostar_amount,omitempty"` + // Date the transaction was created in Unix time. + Date int `json:"date"` + // Source of an incoming transaction (e.g. a user purchasing goods or + // services, Fragment refunding a failed withdrawal). Only for incoming + // transactions. + // + // optional + Source *TransactionPartner `json:"source,omitempty"` + // Receiver of an outgoing transaction (e.g. a user for a purchase + // refund, Fragment for a withdrawal). Only for outgoing transactions. + // + // optional + Receiver *TransactionPartner `json:"receiver,omitempty"` } -// InlineQueryResultCachedGIF is an inline query response with cached gif. -type InlineQueryResultCachedGIF struct { - // Type of the result, must be gif. +// StarTransactions contains a list of Telegram Star transactions. +type StarTransactions struct { + // Transactions is the list of transactions. + Transactions []StarTransaction `json:"transactions"` +} + +// Paid media type constants. +const ( + PaidMediaTypePreview = "preview" + PaidMediaTypePhoto = "photo" + PaidMediaTypeVideo = "video" + PaidMediaTypeLivePhoto = "live_photo" +) + +// PaidMedia describes a media received in a paid message. Flat polymorphic +// by Type: +// - "preview" → Width, Height, Duration (optional) +// - "photo" → Photo is set +// - "video" → Video is set +// - "live_photo" → LivePhoto is set +type PaidMedia struct { + // Type of the paid media. One of "preview", "photo", "video", "live_photo". Type string `json:"type"` - // ID unique identifier for this result, 1-64 bytes. - ID string `json:"id"` - // GifID a valid file identifier for the GIF file. - GIFID string `json:"gif_file_id"` - // Title for the result + // Width is the media width as defined by the sender. Set when Type is + // "preview". // // optional - Title string `json:"title,omitempty"` - // Caption of the GIF file to be sent, 0-1024 characters after entities parsing. + Width int `json:"width,omitempty"` + // Height is the media height as defined by the sender. Set when Type is + // "preview". // // optional - Caption string `json:"caption,omitempty"` - // ParseMode mode for parsing entities in the caption. - // See formatting options for more details - // (https://core.telegram.org/bots/api#formatting-options). + Height int `json:"height,omitempty"` + // Duration is the duration of the media in seconds as defined by the + // sender. Set when Type is "preview". // // optional - ParseMode string `json:"parse_mode,omitempty"` - // CaptionEntities is a list of special entities that appear in the caption, - // which can be specified instead of parse_mode + Duration int `json:"duration,omitempty"` + // Photo is the photo media. Set when Type is "photo". // // optional - CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` - // ReplyMarkup inline keyboard attached to the message. + Photo []PhotoSize `json:"photo,omitempty"` + // Video is the video media. Set when Type is "video". // // optional - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - // InputMessageContent content of the message to be sent instead of the GIF animation. + Video *Video `json:"video,omitempty"` + // LivePhoto is the live photo media. Set when Type is "live_photo". // // optional - InputMessageContent interface{} `json:"input_message_content,omitempty"` + LivePhoto *LivePhoto `json:"live_photo,omitempty"` } -// InlineQueryResultCachedMPEG4GIF is an inline query response with cached -// H.264/MPEG-4 AVC video without sound gif. -type InlineQueryResultCachedMPEG4GIF struct { - // Type of the result, must be mpeg4_gif - Type string `json:"type"` - // ID unique identifier for this result, 1-64 bytes +// PaidMediaInfo describes the paid media added to a message. +type PaidMediaInfo struct { + // StarCount is the number of Telegram Stars that must be paid to buy + // access to the media. + StarCount int `json:"star_count"` + // PaidMedia is the information about the paid media. + PaidMedia []PaidMedia `json:"paid_media"` +} + +// PaidMediaPurchased is received when a user purchases paid media with a +// non-empty payload sent by the bot in a non-channel chat. +type PaidMediaPurchased struct { + // From is the user who purchased the media. + From User `json:"from"` + // PaidMediaPayload is the bot-specified paid media payload. + PaidMediaPayload string `json:"paid_media_payload"` +} + +// UniqueGiftColors contains information about the color scheme for a user's +// name, message replies and link previews based on a unique gift. +type UniqueGiftColors struct { + // ModelCustomEmojiID is the custom emoji identifier of the unique gift's + // model. + ModelCustomEmojiID string `json:"model_custom_emoji_id"` + // SymbolCustomEmojiID is the custom emoji identifier of the unique + // gift's symbol. + SymbolCustomEmojiID string `json:"symbol_custom_emoji_id"` + // LightThemeMainColor is the main color used in light themes; RGB format. + LightThemeMainColor int `json:"light_theme_main_color"` + // LightThemeOtherColors is the list of 1-3 additional colors used in + // light themes; RGB format. + LightThemeOtherColors []int `json:"light_theme_other_colors"` + // DarkThemeMainColor is the main color used in dark themes; RGB format. + DarkThemeMainColor int `json:"dark_theme_main_color"` + // DarkThemeOtherColors is the list of 1-3 additional colors used in dark + // themes; RGB format. + DarkThemeOtherColors []int `json:"dark_theme_other_colors"` +} + +// GiftBackground describes the background of a gift. +type GiftBackground struct { + // CenterColor is the center color of the background in RGB format. + CenterColor int `json:"center_color"` + // EdgeColor is the edge color of the background in RGB format. + EdgeColor int `json:"edge_color"` + // TextColor is the text color of the background in RGB format. + TextColor int `json:"text_color"` +} + +// UserRating describes the rating of a user based on their Telegram Star +// spendings. +type UserRating struct { + // Level is the current level of the user, indicating their reliability + // when purchasing digital goods and services. A higher level suggests a + // more trustworthy customer; a negative level is likely reason for + // concern. + Level int `json:"level"` + // Rating is the numerical value of the user's rating; the higher the + // rating, the better. + Rating int `json:"rating"` + // CurrentLevelRating is the rating value required to get the current + // level. + CurrentLevelRating int `json:"current_level_rating"` + // NextLevelRating is the rating value required to get to the next level; + // omitted if the maximum level was reached. + // + // optional + NextLevelRating int `json:"next_level_rating,omitempty"` +} + +// Gift represents a gift that can be sent by the bot. +type Gift struct { + // ID is the unique identifier of the gift. ID string `json:"id"` - // MPEG4FileID a valid file identifier for the MP4 file - MPEG4FileID string `json:"mpeg4_file_id"` - // Title for the result + // Sticker representing the gift. + Sticker Sticker `json:"sticker"` + // StarCount is the number of Telegram Stars that must be paid to send + // the sticker. + StarCount int `json:"star_count"` + // UpgradeStarCount is the number of Telegram Stars that must be paid + // to upgrade the gift to a unique one. // // optional - Title string `json:"title,omitempty"` - // Caption of the MPEG-4 file to be sent, 0-1024 characters after entities parsing. + UpgradeStarCount int `json:"upgrade_star_count,omitempty"` + // TotalCount is the total number of the gifts of this type that can + // be sent; for limited gifts only. // // optional - Caption string `json:"caption,omitempty"` - // ParseMode mode for parsing entities in the caption. - // See formatting options for more details - // (https://core.telegram.org/bots/api#formatting-options). + TotalCount int `json:"total_count,omitempty"` + // RemainingCount is the number of remaining gifts of this type that + // can be sent; for limited gifts only. // // optional - ParseMode string `json:"parse_mode,omitempty"` - // ParseMode mode for parsing entities in the video caption. - // See formatting options for more details - // (https://core.telegram.org/bots/api#formatting-options). + RemainingCount int `json:"remaining_count,omitempty"` + // PersonalTotalCount is the total number of gifts of this type that + // can be sent by the bot to each user. // // optional - CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` - // ReplyMarkup inline keyboard attached to the message. + PersonalTotalCount int `json:"personal_total_count,omitempty"` + // PersonalRemainingCount is the number of remaining gifts of this type + // that can be sent by the bot to each user. // // optional - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - // InputMessageContent content of the message to be sent instead of the video animation. + PersonalRemainingCount int `json:"personal_remaining_count,omitempty"` + // IsPremium is true, if the gift can only be sent by bots owned by + // users with an active Telegram Premium subscription. // // optional - InputMessageContent interface{} `json:"input_message_content,omitempty"` + IsPremium bool `json:"is_premium,omitempty"` + // HasColors is true, if the gift defines a color scheme. + // + // optional + HasColors bool `json:"has_colors,omitempty"` + // UniqueGiftVariantCount is the total number of variants of unique + // gifts that can be upgraded from the gift. + // + // optional + UniqueGiftVariantCount int `json:"unique_gift_variant_count,omitempty"` + // Background describing the gift background. + // + // optional + Background *GiftBackground `json:"background,omitempty"` + // PublisherChat is the chat that published the gift. + // + // optional + PublisherChat *Chat `json:"publisher_chat,omitempty"` } -// InlineQueryResultCachedPhoto is an inline query response with cached photo. -type InlineQueryResultCachedPhoto struct { - // Type of the result, must be a photo. - Type string `json:"type"` - // ID unique identifier for this result, 1-64 bytes. - ID string `json:"id"` - // PhotoID a valid file identifier of the photo. - PhotoID string `json:"photo_file_id"` - // Title for the result. +// Gifts represents a list of gifts. +type Gifts struct { + // Gifts is the list of gifts. + Gifts []Gift `json:"gifts"` +} + +// UniqueGiftModel describes the model of a unique gift. +type UniqueGiftModel struct { + // Name of the model. + Name string `json:"name"` + // Sticker representing the model. + Sticker Sticker `json:"sticker"` + // RarityPerMille is the number of unique gifts that receive this model + // for every 1000 gifts upgraded. + RarityPerMille int `json:"rarity_per_mille"` + // Rarity is a human-readable name for the rarity of the model. // // optional - Title string `json:"title,omitempty"` - // Description short description of the result. + Rarity string `json:"rarity,omitempty"` +} + +// UniqueGiftSymbol describes the symbol shown on the pattern of a unique gift. +type UniqueGiftSymbol struct { + // Name of the symbol. + Name string `json:"name"` + // Sticker representing the symbol. + Sticker Sticker `json:"sticker"` + // RarityPerMille is the number of unique gifts that receive this symbol + // for every 1000 gifts upgraded. + RarityPerMille int `json:"rarity_per_mille"` +} + +// UniqueGiftBackdropColors describes the colors of a unique gift backdrop. +type UniqueGiftBackdropColors struct { + // CenterColor is the color in the center of the backdrop in RGB format. + CenterColor int `json:"center_color"` + // EdgeColor is the color on the edges of the backdrop in RGB format. + EdgeColor int `json:"edge_color"` + // SymbolColor is the color used to paint the symbol in RGB format. + SymbolColor int `json:"symbol_color"` + // TextColor is the color for the text on the backdrop in RGB format. + TextColor int `json:"text_color"` +} + +// UniqueGiftBackdrop describes the backdrop of a unique gift. +type UniqueGiftBackdrop struct { + // Name of the backdrop. + Name string `json:"name"` + // Colors of the backdrop. + Colors UniqueGiftBackdropColors `json:"colors"` + // RarityPerMille is the number of unique gifts that receive this backdrop + // for every 1000 gifts upgraded. + RarityPerMille int `json:"rarity_per_mille"` +} + +// UniqueGift describes a unique gift that was upgraded from a regular gift. +type UniqueGift struct { + // BaseName is the human-readable name of the regular gift from which + // this unique gift was upgraded. + BaseName string `json:"base_name"` + // Name is the unique name of the gift. + Name string `json:"name"` + // Number is the unique number of the upgraded gift among gifts upgraded + // from the same regular gift. + Number int `json:"number"` + // GiftID is the identifier of the regular gift that was upgraded to + // this unique gift. // // optional - Description string `json:"description,omitempty"` - // Caption of the photo to be sent, 0-1024 characters after entities parsing. + GiftID string `json:"gift_id,omitempty"` + // IsFromBlockchain is true, if the gift was assigned from the TON + // blockchain. // // optional - Caption string `json:"caption,omitempty"` - // ParseMode mode for parsing entities in the photo caption. - // See formatting options for more details - // (https://core.telegram.org/bots/api#formatting-options). + IsFromBlockchain bool `json:"is_from_blockchain,omitempty"` + // IsBurned is true, if the gift was burned by the owner. // // optional - ParseMode string `json:"parse_mode,omitempty"` - // CaptionEntities is a list of special entities that appear in the caption, - // which can be specified instead of parse_mode + IsBurned bool `json:"is_burned,omitempty"` + // IsPremium is true, if the gift can only be owned by users with an + // active Telegram Premium subscription. // // optional - CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` - // ReplyMarkup inline keyboard attached to the message. + IsPremium bool `json:"is_premium,omitempty"` + // Model of the unique gift. + Model UniqueGiftModel `json:"model"` + // Symbol of the unique gift. + Symbol UniqueGiftSymbol `json:"symbol"` + // Backdrop of the unique gift. + Backdrop UniqueGiftBackdrop `json:"backdrop"` + // Colors defines the color scheme based on this unique gift. // // optional - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - // InputMessageContent content of the message to be sent instead of the photo. + Colors *UniqueGiftColors `json:"colors,omitempty"` + // PublisherChat is the chat that published the underlying regular gift. // // optional - InputMessageContent interface{} `json:"input_message_content,omitempty"` + PublisherChat *Chat `json:"publisher_chat,omitempty"` } -// InlineQueryResultCachedSticker is an inline query response with cached sticker. -type InlineQueryResultCachedSticker struct { - // Type of the result, must be a sticker - Type string `json:"type"` - // ID unique identifier for this result, 1-64 bytes - ID string `json:"id"` - // StickerID a valid file identifier of the sticker - StickerID string `json:"sticker_file_id"` - // Title is a title - Title string `json:"title"` - // ReplyMarkup inline keyboard attached to the message +// GiftInfo describes a service message about a regular gift that was sent +// or received. +type GiftInfo struct { + // Gift is the information about the gift. + Gift Gift `json:"gift"` + // OwnedGiftID is the unique identifier of the received gift for the + // bot; only present for gifts received on behalf of business accounts. // // optional - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - // InputMessageContent content of the message to be sent instead of the sticker + OwnedGiftID string `json:"owned_gift_id,omitempty"` + // ConvertStarCount is the number of Telegram Stars that can be claimed + // by the receiver instead of the gift. // // optional - InputMessageContent interface{} `json:"input_message_content,omitempty"` -} - -// InlineQueryResultCachedVideo is an inline query response with cached video. -type InlineQueryResultCachedVideo struct { - // Type of the result, must be video - Type string `json:"type"` - // ID unique identifier for this result, 1-64 bytes - ID string `json:"id"` - // VideoID a valid file identifier for the video file - VideoID string `json:"video_file_id"` - // Title for the result - Title string `json:"title"` - // Description short description of the result + ConvertStarCount int `json:"convert_star_count,omitempty"` + // PrepaidUpgradeStarCount is the number of Telegram Stars that were + // prepaid by the sender for the ability to upgrade the gift. // // optional - Description string `json:"description,omitempty"` - // Caption of the video to be sent, 0-1024 characters after entities parsing + PrepaidUpgradeStarCount int `json:"prepaid_upgrade_star_count,omitempty"` + // CanBeUpgraded is true, if the gift can be upgraded to a unique gift. // // optional - Caption string `json:"caption,omitempty"` - // ParseMode mode for parsing entities in the video caption. - // See formatting options for more details - // (https://core.telegram.org/bots/api#formatting-options). + CanBeUpgraded bool `json:"can_be_upgraded,omitempty"` + // Text is the text of the message that was added to the gift. // // optional - ParseMode string `json:"parse_mode,omitempty"` - // CaptionEntities is a list of special entities that appear in the caption, - // which can be specified instead of parse_mode + Text string `json:"text,omitempty"` + // Entities are the special entities that appear in the text. // // optional - CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` - // ReplyMarkup inline keyboard attached to the message + Entities []MessageEntity `json:"entities,omitempty"` + // IsPrivate is true, if the sender and gift text are shown only to the + // gift receiver; otherwise, everyone able to access the chat with the + // receiver will be able to see them. // // optional - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - // InputMessageContent content of the message to be sent instead of the video + IsPrivate bool `json:"is_private,omitempty"` + // IsUpgradeSeparate is true, if the gift upgrade to a unique gift can + // be requested separately by the receiver. // // optional - InputMessageContent interface{} `json:"input_message_content,omitempty"` + IsUpgradeSeparate bool `json:"is_upgrade_separate,omitempty"` + // UniqueGiftNumber is the sequential number of the unique gift among + // gifts upgraded from the same regular gift. + // + // optional + UniqueGiftNumber int `json:"unique_gift_number,omitempty"` } -// InlineQueryResultCachedVoice is an inline query response with cached voice. -type InlineQueryResultCachedVoice struct { - // Type of the result, must be voice - Type string `json:"type"` - // ID unique identifier for this result, 1-64 bytes - ID string `json:"id"` - // VoiceID a valid file identifier for the voice message - VoiceID string `json:"voice_file_id"` - // Title voice message title - Title string `json:"title"` - // Caption 0-1024 characters after entities parsing +// Unique gift origin constants. +const ( + UniqueGiftOriginUpgrade = "upgrade" + UniqueGiftOriginTransfer = "transfer" + UniqueGiftOriginResale = "resale" + UniqueGiftOriginGiftedUpgrade = "gifted_upgrade" + UniqueGiftOriginOffer = "offer" +) + +// UniqueGiftInfo describes a service message about a unique gift that was +// sent or received. +type UniqueGiftInfo struct { + // Gift is the information about the unique gift. + Gift UniqueGift `json:"gift"` + // Origin of the gift. One of "upgrade", "transfer", "resale", + // "gifted_upgrade", "offer". + Origin string `json:"origin"` + // Text of the message that was added to the gift. // // optional - Caption string `json:"caption,omitempty"` - // ParseMode mode for parsing entities in the video caption. - // See formatting options for more details - // (https://core.telegram.org/bots/api#formatting-options). + Text string `json:"text,omitempty"` + // Entities are the special entities that appear in the text. // // optional - ParseMode string `json:"parse_mode,omitempty"` - // CaptionEntities is a list of special entities that appear in the caption, - // which can be specified instead of parse_mode + Entities []MessageEntity `json:"entities,omitempty"` + // IsPrivate is true, if the sender and gift text are shown only to the + // gift receiver; otherwise, everyone can see them. // // optional - CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` - // ReplyMarkup inline keyboard attached to the message + IsPrivate bool `json:"is_private,omitempty"` + // LastResaleCurrency is the currency in which the gift was last resold + // on a resale market; for gifts with origin "resale" only. // // optional - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - // InputMessageContent content of the message to be sent instead of the voice message + LastResaleCurrency string `json:"last_resale_currency,omitempty"` + // LastResaleAmount is the amount in the smallest units of the currency + // for which the gift was last resold; for gifts with origin "resale" only. // // optional - InputMessageContent interface{} `json:"input_message_content,omitempty"` + LastResaleAmount int `json:"last_resale_amount,omitempty"` + // OwnedGiftID is the unique identifier of the received gift for the + // bot; only present for gifts received on behalf of business accounts. + // + // optional + OwnedGiftID string `json:"owned_gift_id,omitempty"` + // TransferStarCount is the number of Telegram Stars that must be paid + // to transfer the gift; omitted if the bot cannot transfer the gift. + // + // optional + TransferStarCount int `json:"transfer_star_count,omitempty"` + // NextTransferDate is the point in time (Unix timestamp) when the gift + // can be transferred. If it is in the past, the gift can be transferred + // now. + // + // optional + NextTransferDate int `json:"next_transfer_date,omitempty"` } -// InlineQueryResultArticle represents a link to an article or web page. -type InlineQueryResultArticle struct { - // Type of the result, must be article. +// Owned gift type constants. +const ( + OwnedGiftTypeRegular = "regular" + OwnedGiftTypeUnique = "unique" +) + +// OwnedGift describes a gift received and owned by a user or chat. Flat +// polymorphic by Type: +// - "regular" → Gift (Gift), fields: ConvertStarCount, CanBeUpgraded, etc. +// - "unique" → Gift (UniqueGift), fields: CanBeTransferred, TransferStarCount +type OwnedGift struct { + // Type of the gift. One of "regular", "unique". Type string `json:"type"` - // ID unique identifier for this result, 1-64 Bytes. - ID string `json:"id"` - // Title of the result - Title string `json:"title"` - // InputMessageContent content of the message to be sent. - InputMessageContent interface{} `json:"input_message_content,omitempty"` - // ReplyMarkup Inline keyboard attached to the message. + // Gift is the regular gift, when Type is "regular". It shares the "gift" + // wire field with UniqueGift. // // optional - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - // URL of the result. + Gift *Gift `json:"-"` + // UniqueGift is the unique gift, when Type is "unique". It shares the + // "gift" wire field with Gift. // // optional - URL string `json:"url,omitempty"` - // HideURL pass True, if you don't want the URL to be shown in the message. + UniqueGift *UniqueGift `json:"-"` + // OwnedGiftID is the unique identifier of the gift for the bot; for + // gifts received on behalf of business accounts only. // // optional - HideURL bool `json:"hide_url,omitempty"` - // Description short description of the result. + OwnedGiftID string `json:"owned_gift_id,omitempty"` + // SenderUser is the sender of the gift, if it was sent by a user. // // optional - Description string `json:"description,omitempty"` - // ThumbURL url of the thumbnail for the result + SenderUser *User `json:"sender_user,omitempty"` + // SendDate is the date the gift was sent in Unix time. + SendDate int `json:"send_date"` + // Text is the text of the message that was added to the gift. // // optional - ThumbURL string `json:"thumb_url,omitempty"` - // ThumbWidth thumbnail width + Text string `json:"text,omitempty"` + // Entities are the special entities that appear in the text. // // optional - ThumbWidth int `json:"thumb_width,omitempty"` - // ThumbHeight thumbnail height + Entities []MessageEntity `json:"entities,omitempty"` + // IsPrivate is true, if the sender and gift text are shown only to the + // gift receiver. // // optional - ThumbHeight int `json:"thumb_height,omitempty"` -} - -// InlineQueryResultAudio is an inline query response audio. -type InlineQueryResultAudio struct { - // Type of the result, must be audio - Type string `json:"type"` - // ID unique identifier for this result, 1-64 bytes - ID string `json:"id"` - // URL a valid url for the audio file - URL string `json:"audio_url"` - // Title is a title - Title string `json:"title"` - // Caption 0-1024 characters after entities parsing + IsPrivate bool `json:"is_private,omitempty"` + // IsSaved is true, if the gift is displayed on the account's profile + // page; for gifts received on behalf of business accounts only. // // optional - Caption string `json:"caption,omitempty"` - // ParseMode mode for parsing entities in the video caption. - // See formatting options for more details - // (https://core.telegram.org/bots/api#formatting-options). + IsSaved bool `json:"is_saved,omitempty"` + // CanBeUpgraded is true, if the regular gift can be upgraded to a + // unique gift; for gifts received on behalf of business accounts only. + // Regular only. // // optional - ParseMode string `json:"parse_mode,omitempty"` - // CaptionEntities is a list of special entities that appear in the caption, - // which can be specified instead of parse_mode + CanBeUpgraded bool `json:"can_be_upgraded,omitempty"` + // WasRefunded is true, if the gift was refunded and isn't available + // anymore. Regular only. // // optional - CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` - // Performer is a performer + WasRefunded bool `json:"was_refunded,omitempty"` + // ConvertStarCount is the number of Telegram Stars that can be claimed + // by the receiver instead of the gift. Regular only. // // optional - Performer string `json:"performer,omitempty"` - // Duration audio duration in seconds + ConvertStarCount int `json:"convert_star_count,omitempty"` + // PrepaidUpgradeStarCount is the number of Telegram Stars that were + // paid by the sender for the ability to upgrade the gift. Regular only. // // optional - Duration int `json:"audio_duration,omitempty"` - // ReplyMarkup inline keyboard attached to the message + PrepaidUpgradeStarCount int `json:"prepaid_upgrade_star_count,omitempty"` + // IsUpgradeSeparate is true, if the gift upgrade to a unique gift can + // be requested separately by the receiver. Regular only. // // optional - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - // InputMessageContent content of the message to be sent instead of the audio + IsUpgradeSeparate bool `json:"is_upgrade_separate,omitempty"` + // UniqueGiftNumber is the sequential number of the unique gift among + // gifts upgraded from the same regular gift. Regular only. // // optional - InputMessageContent interface{} `json:"input_message_content,omitempty"` -} - -// InlineQueryResultContact is an inline query response contact. -type InlineQueryResultContact struct { - Type string `json:"type"` // required - ID string `json:"id"` // required - PhoneNumber string `json:"phone_number"` // required - FirstName string `json:"first_name"` // required - LastName string `json:"last_name"` - VCard string `json:"vcard"` - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - InputMessageContent interface{} `json:"input_message_content,omitempty"` - ThumbURL string `json:"thumb_url"` - ThumbWidth int `json:"thumb_width"` - ThumbHeight int `json:"thumb_height"` + UniqueGiftNumber int `json:"unique_gift_number,omitempty"` + // CanBeTransferred is true, if the gift can be transferred to another + // owner; for gifts received on behalf of business accounts only. + // Unique only. + // + // optional + CanBeTransferred bool `json:"can_be_transferred,omitempty"` + // TransferStarCount is the number of Telegram Stars that must be paid + // to transfer the gift; omitted if the bot cannot transfer the gift. + // Unique only. + // + // optional + TransferStarCount int `json:"transfer_star_count,omitempty"` + // NextTransferDate is the point in time (Unix timestamp) when the gift + // can be transferred. If it is in the past, then the gift can be + // transferred now. Unique only. + // + // optional + NextTransferDate int `json:"next_transfer_date,omitempty"` } -// InlineQueryResultGame is an inline query response game. -type InlineQueryResultGame struct { - // Type of the result, must be game - Type string `json:"type"` - // ID unique identifier for this result, 1-64 bytes +// UnmarshalJSON decodes an OwnedGift, routing the polymorphic "gift" field to +// Gift or UniqueGift based on Type. +func (g *OwnedGift) UnmarshalJSON(data []byte) error { + type alias OwnedGift + aux := struct { + *alias + Gift json.RawMessage `json:"gift,omitempty"` + }{alias: (*alias)(g)} + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + if len(aux.Gift) == 0 || string(aux.Gift) == "null" { + return nil + } + + if g.Type == OwnedGiftTypeUnique { + g.UniqueGift = new(UniqueGift) + return json.Unmarshal(aux.Gift, g.UniqueGift) + } + + g.Gift = new(Gift) + return json.Unmarshal(aux.Gift, g.Gift) +} + +// MarshalJSON encodes an OwnedGift, emitting Gift or UniqueGift under the +// shared "gift" wire field. +func (g OwnedGift) MarshalJSON() ([]byte, error) { + type alias OwnedGift + aux := struct { + alias + Gift any `json:"gift,omitempty"` + }{alias: alias(g)} + + switch { + case g.UniqueGift != nil: + aux.Gift = g.UniqueGift + case g.Gift != nil: + aux.Gift = g.Gift + } + + return json.Marshal(aux) +} + +// OwnedGifts contains the list of gifts received and owned by a user or chat. +type OwnedGifts struct { + // TotalCount is the total number of gifts owned by the user or chat. + TotalCount int `json:"total_count"` + // Gifts is the list of gifts. + Gifts []OwnedGift `json:"gifts"` + // NextOffset is the offset for the next request. Empty if there are no + // more results. + // + // optional + NextOffset string `json:"next_offset,omitempty"` +} + +// PreparedInlineMessage describes an inline message to be sent by a user of +// a Mini App. +type PreparedInlineMessage struct { + // ID is the unique identifier of the prepared message. ID string `json:"id"` - // GameShortName short name of the game - GameShortName string `json:"game_short_name"` - // ReplyMarkup inline keyboard attached to the message + // ExpirationDate is the Unix timestamp at which the message can no + // longer be used. + ExpirationDate int `json:"expiration_date"` +} + +// InputProfilePhoto type constants. +const ( + InputProfilePhotoTypeStatic = "static" + InputProfilePhotoTypeAnimated = "animated" +) + +// InputProfilePhoto describes a profile photo to be set. Flat polymorphic +// by Type: +// - "static" → Photo is set +// - "animated" → Animation is set, MainFrameTimestamp optional +type InputProfilePhoto struct { + // Type of the photo. One of "static", "animated". + Type string `json:"type"` + // Photo is the static profile photo. Profile photos can't be reused and + // can only be uploaded as a new file. Set when Type is "static". // // optional - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + Photo RequestFileData `json:"photo,omitempty"` + // Animation is the animated profile photo. Set when Type is "animated". + // + // optional + Animation RequestFileData `json:"animation,omitempty"` + // MainFrameTimestamp is the timestamp in seconds of the frame that will + // be used as the static profile photo. Set when Type is "animated". + // Defaults to 0.0. + // + // optional + MainFrameTimestamp float64 `json:"main_frame_timestamp,omitempty"` } -// InlineQueryResultDocument is an inline query response document. -type InlineQueryResultDocument struct { - // Type of the result, must be a document +// Input story content type constants. +const ( + InputStoryContentTypePhoto = "photo" + InputStoryContentTypeVideo = "video" +) + +// InputStoryContent describes the content of a story to be posted. Flat +// polymorphic by Type: +// - "photo" → Photo is set +// - "video" → Video is set; Duration, CoverFrameTimestamp, IsAnimation optional +type InputStoryContent struct { + // Type of the content. One of "photo", "video". Type string `json:"type"` - // ID unique identifier for this result, 1-64 bytes - ID string `json:"id"` - // Title for the result - Title string `json:"title"` - // Caption of the document to be sent, 0-1024 characters after entities parsing + // Photo is the photo content. Set when Type is "photo". // // optional - Caption string `json:"caption,omitempty"` - // URL a valid url for the file - URL string `json:"document_url"` - // MimeType of the content of the file, either “application/pdf” or “application/zip” - MimeType string `json:"mime_type"` - // Description short description of the result + Photo RequestFileData `json:"photo,omitempty"` + // Video is the video content. Set when Type is "video". // // optional - Description string `json:"description,omitempty"` - // ReplyMarkup inline keyboard attached to the message + Video RequestFileData `json:"video,omitempty"` + // Duration is the precise duration of the video in seconds. 0-60. + // Video only. // // optional - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - // InputMessageContent content of the message to be sent instead of the file + Duration float64 `json:"duration,omitempty"` + // CoverFrameTimestamp is the timestamp in seconds of the frame that + // will be used as the static cover for the story. Defaults to 0.0. + // Video only. // // optional - InputMessageContent interface{} `json:"input_message_content,omitempty"` - // ThumbURL url of the thumbnail (jpeg only) for the file + CoverFrameTimestamp float64 `json:"cover_frame_timestamp,omitempty"` + // IsAnimation is true if the video has no sound. Video only. // // optional - ThumbURL string `json:"thumb_url,omitempty"` - // ThumbWidth thumbnail width + IsAnimation bool `json:"is_animation,omitempty"` +} + +// StoryAreaPosition describes the position of a clickable area within a +// story. +type StoryAreaPosition struct { + // XPercentage is the abscissa of the area's center, as a percentage of + // the media width. + XPercentage float64 `json:"x_percentage"` + // YPercentage is the ordinate of the area's center, as a percentage of + // the media height. + YPercentage float64 `json:"y_percentage"` + // WidthPercentage is the width of the area's rectangle, as a percentage + // of the media width. + WidthPercentage float64 `json:"width_percentage"` + // HeightPercentage is the height of the area's rectangle, as a percentage + // of the media height. + HeightPercentage float64 `json:"height_percentage"` + // RotationAngle is the clockwise rotation angle of the rectangle, in + // degrees; 0-360. + RotationAngle float64 `json:"rotation_angle"` + // CornerRadiusPercentage is the radius of the rectangle corner rounding, + // as a percentage of the media width. + CornerRadiusPercentage float64 `json:"corner_radius_percentage"` +} + +// LocationAddress describes the physical address of a location. +type LocationAddress struct { + // CountryCode is the two-letter ISO 3166-1 alpha-2 country code of the + // country where the location is located. + CountryCode string `json:"country_code"` + // State is the state of the location. Optional. // // optional - ThumbWidth int `json:"thumb_width,omitempty"` - // ThumbHeight thumbnail height + State string `json:"state,omitempty"` + // City is the city of the location. Optional. + // + // optional + City string `json:"city,omitempty"` + // Street is the street address of the location. Optional. // // optional - ThumbHeight int `json:"thumb_height,omitempty"` + Street string `json:"street,omitempty"` } -// InlineQueryResultGIF is an inline query response GIF. -type InlineQueryResultGIF struct { - // Type of the result, must be gif. +// Story area type constants. +const ( + StoryAreaTypeLocation = "location" + StoryAreaTypeSuggestedReaction = "suggested_reaction" + StoryAreaTypeLink = "link" + StoryAreaTypeWeather = "weather" + StoryAreaTypeUniqueGift = "unique_gift" +) + +// StoryAreaType describes the type of a clickable area on a story. Flat +// polymorphic by Type: +// - "location" → Latitude, Longitude, Address +// - "suggested_reaction" → ReactionType, IsDark, IsFlipped +// - "link" → URL +// - "weather" → Temperature, Emoji, BackgroundColor +// - "unique_gift" → Name +type StoryAreaType struct { + // Type of the area. One of "location", "suggested_reaction", "link", + // "weather", "unique_gift". Type string `json:"type"` - // ID unique identifier for this result, 1-64 bytes. - ID string `json:"id"` - // URL a valid URL for the GIF file. File size must not exceed 1MB. - URL string `json:"gif_url"` - // ThumbURL url of the static (JPEG or GIF) or animated (MPEG4) thumbnail for the result. - ThumbURL string `json:"thumb_url"` - // Width of the GIF + // Latitude in degrees. Set when Type is "location". // // optional - Width int `json:"gif_width,omitempty"` - // Height of the GIF + Latitude float64 `json:"latitude,omitempty"` + // Longitude in degrees. Set when Type is "location". // // optional - Height int `json:"gif_height,omitempty"` - // Duration of the GIF + Longitude float64 `json:"longitude,omitempty"` + // Address of the location. Set when Type is "location". // // optional - Duration int `json:"gif_duration,omitempty"` - // Title for the result + Address *LocationAddress `json:"address,omitempty"` + // ReactionType is the type of the reaction. Set when Type is + // "suggested_reaction". // // optional - Title string `json:"title,omitempty"` - // Caption of the GIF file to be sent, 0-1024 characters after entities parsing. + ReactionType *ReactionType `json:"reaction_type,omitempty"` + // IsDark is true, if the reaction area has a dark background. Set + // when Type is "suggested_reaction". // // optional - Caption string `json:"caption,omitempty"` - // ParseMode mode for parsing entities in the video caption. - // See formatting options for more details - // (https://core.telegram.org/bots/api#formatting-options). + IsDark bool `json:"is_dark,omitempty"` + // IsFlipped is true, if reaction area corner is flipped. Set when + // Type is "suggested_reaction". // // optional - ParseMode string `json:"parse_mode,omitempty"` - // CaptionEntities is a list of special entities that appear in the caption, - // which can be specified instead of parse_mode + IsFlipped bool `json:"is_flipped,omitempty"` + // URL to be opened. Set when Type is "link". // // optional - CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` - // ReplyMarkup inline keyboard attached to the message + URL string `json:"url,omitempty"` + // Temperature in degree Celsius. Set when Type is "weather". // // optional - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - // InputMessageContent content of the message to be sent instead of the GIF animation. + Temperature float64 `json:"temperature,omitempty"` + // Emoji representing the weather. Set when Type is "weather". // // optional - InputMessageContent interface{} `json:"input_message_content,omitempty"` + Emoji string `json:"emoji,omitempty"` + // BackgroundColor is the color of the area background in the ARGB + // format. Set when Type is "weather". + // + // optional + BackgroundColor int `json:"background_color,omitempty"` + // Name of the unique gift. Set when Type is "unique_gift". + // + // optional + Name string `json:"name,omitempty"` } -// InlineQueryResultLocation is an inline query response location. -type InlineQueryResultLocation struct { - // Type of the result, must be location - Type string `json:"type"` - // ID unique identifier for this result, 1-64 Bytes - ID string `json:"id"` - // Latitude of the location in degrees - Latitude float64 `json:"latitude"` - // Longitude of the location in degrees - Longitude float64 `json:"longitude"` - // Title of the location - Title string `json:"title"` - // HorizontalAccuracy is the radius of uncertainty for the location, - // measured in meters; 0-1500 +// StoryArea describes a clickable area on a story. +type StoryArea struct { + // Position of the area. + Position StoryAreaPosition `json:"position"` + // Type of the area. + Type StoryAreaType `json:"type"` +} + +// PaidMessagePriceChanged describes a service message about a change in the +// price of paid messages within a chat. +type PaidMessagePriceChanged struct { + // PaidMessageStarCount is the new number of Telegram Stars that must + // be paid by non-administrator users of the supergroup chat for each + // sent message. + PaidMessageStarCount int `json:"paid_message_star_count"` +} + +// DirectMessagePriceChanged describes a service message about a change in +// the pricing of direct messages sent to a channel chat. +type DirectMessagePriceChanged struct { + // AreDirectMessagesEnabled is true, if direct messages are enabled + // for the channel chat. + AreDirectMessagesEnabled bool `json:"are_direct_messages_enabled"` + // DirectMessageStarCount is the new number of Telegram Stars that + // must be paid to send each direct message. // // optional - HorizontalAccuracy float64 `json:"horizontal_accuracy,omitempty"` - // LivePeriod is the period in seconds for which the location can be - // updated, should be between 60 and 86400. + DirectMessageStarCount int `json:"direct_message_star_count,omitempty"` +} + +// DirectMessagesTopic describes a topic of a direct messages chat. +type DirectMessagesTopic struct { + // TopicID is the unique identifier of the topic. + TopicID int `json:"topic_id"` + // User is the information about the user that created the topic. + // Currently, the user is always present for topics in direct messages + // chats of channel direct messages chats. // // optional - LivePeriod int `json:"live_period,omitempty"` - // Heading is for live locations, a direction in which the user is moving, - // in degrees. Must be between 1 and 360 if specified. + User *User `json:"user,omitempty"` +} + +// SuggestedPostPrice describes the price of a suggested post. +type SuggestedPostPrice struct { + // Currency is the currency in which the price is expressed. Currently, + // always "XTR" (Telegram Stars) or "TON" (Toncoin). + Currency string `json:"currency"` + // Amount is the amount of the currency to be paid for the post. + Amount int `json:"amount"` +} + +// SuggestedPostParameters contains parameters of a post that is suggested +// by the bot. +type SuggestedPostParameters struct { + // Price of the suggested post. If the field is omitted, then the post + // is unpaid. // // optional - Heading int `json:"heading,omitempty"` - // ProximityAlertRadius is for live locations, a maximum distance for - // proximity alerts about approaching another chat member, in meters. Must - // be between 1 and 100000 if specified. + Price *SuggestedPostPrice `json:"price,omitempty"` + // SendDate is the Unix timestamp when the post is suggested to be + // published. If specified, then the date must be between 300 and + // 2678400 seconds (30 days) in the future. If omitted, then the post + // can be published at any time within 30 days at the sole discretion + // of the administrator. // // optional - ProximityAlertRadius int `json:"proximity_alert_radius,omitempty"` - // ReplyMarkup inline keyboard attached to the message + SendDate int `json:"send_date,omitempty"` +} + +// SuggestedPostInfo contains information about a suggested post. +type SuggestedPostInfo struct { + // State of the suggested post. Currently, one of "pending", "approved", + // "declined". + State string `json:"state"` + // Price of the suggested post. // // optional - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - // InputMessageContent content of the message to be sent instead of the location + Price *SuggestedPostPrice `json:"price,omitempty"` + // SendDate is the Unix timestamp when the post is suggested to be + // published. // // optional - InputMessageContent interface{} `json:"input_message_content,omitempty"` - // ThumbURL url of the thumbnail for the result + SendDate int `json:"send_date,omitempty"` +} + +// SuggestedPostApproved describes a service message about the approval of +// a suggested post. +type SuggestedPostApproved struct { + // SuggestedPostMessage is the message containing the suggested post + // that was approved. // // optional - ThumbURL string `json:"thumb_url,omitempty"` - // ThumbWidth thumbnail width + SuggestedPostMessage *Message `json:"suggested_post_message,omitempty"` + // Price at which the post was approved. // // optional - ThumbWidth int `json:"thumb_width,omitempty"` - // ThumbHeight thumbnail height + Price *SuggestedPostPrice `json:"price,omitempty"` + // SendDate is the Unix timestamp when the post will be published. + SendDate int `json:"send_date"` +} + +// SuggestedPostApprovalFailed describes a service message about the failure +// to approve a suggested post due to insufficient funds at the time of +// payment. +type SuggestedPostApprovalFailed struct { + // SuggestedPostMessage is the message containing the suggested post + // whose approval has failed. // // optional - ThumbHeight int `json:"thumb_height,omitempty"` + SuggestedPostMessage *Message `json:"suggested_post_message,omitempty"` + // Price at which the post would have been approved. + Price SuggestedPostPrice `json:"price"` } -// InlineQueryResultMPEG4GIF is an inline query response MPEG4 GIF. -type InlineQueryResultMPEG4GIF struct { - // Type of the result, must be mpeg4_gif - Type string `json:"type"` - // ID unique identifier for this result, 1-64 bytes - ID string `json:"id"` - // URL a valid URL for the MP4 file. File size must not exceed 1MB - URL string `json:"mpeg4_url"` - // Width video width +// SuggestedPostDeclined describes a service message about the rejection of +// a suggested post. +type SuggestedPostDeclined struct { + // SuggestedPostMessage is the message containing the suggested post + // that was declined. // // optional - Width int `json:"mpeg4_width,omitempty"` - // Height vVideo height + SuggestedPostMessage *Message `json:"suggested_post_message,omitempty"` + // Comment with which the post was declined. // // optional - Height int `json:"mpeg4_height,omitempty"` - // Duration video duration + Comment string `json:"comment,omitempty"` +} + +// SuggestedPostPaid describes a service message about a successful payment +// for a suggested post. +type SuggestedPostPaid struct { + // SuggestedPostMessage is the message containing the suggested post. // // optional - Duration int `json:"mpeg4_duration,omitempty"` - // ThumbURL url of the static (JPEG or GIF) or animated (MPEG4) thumbnail for the result. - ThumbURL string `json:"thumb_url"` - // Title for the result + SuggestedPostMessage *Message `json:"suggested_post_message,omitempty"` + // Currency in which the payment was made. Currently, one of "XTR" or "TON". + Currency string `json:"currency"` + // Amount in the smallest units of the currency that was received by + // the channel in nanotoncoins; for payments in toncoins only. // // optional - Title string `json:"title,omitempty"` - // Caption of the MPEG-4 file to be sent, 0-1024 characters after entities parsing. + Amount int `json:"amount,omitempty"` + // StarAmount is the amount of Telegram Stars that was received by the + // channel; for payments in Telegram Stars only. // // optional - Caption string `json:"caption,omitempty"` - // ParseMode mode for parsing entities in the video caption. - // See formatting options for more details - // (https://core.telegram.org/bots/api#formatting-options). + StarAmount *StarAmount `json:"star_amount,omitempty"` +} + +// SuggestedPostRefunded describes a service message about a payment refund +// for a suggested post. +type SuggestedPostRefunded struct { + // SuggestedPostMessage is the message containing the suggested post + // that was refunded. // // optional - ParseMode string `json:"parse_mode,omitempty"` - // CaptionEntities is a list of special entities that appear in the caption, - // which can be specified instead of parse_mode + SuggestedPostMessage *Message `json:"suggested_post_message,omitempty"` + // Reason for the refund. Currently, one of "post_deleted", + // "payment_refunded". + Reason string `json:"reason"` +} + +// ChecklistTask describes a task in a checklist. +type ChecklistTask struct { + // ID is the unique identifier of the task. + ID int `json:"id"` + // Text is the text of the task. + Text string `json:"text"` + // TextEntities are the special entities that appear in the task text. // // optional - CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` - // ReplyMarkup inline keyboard attached to the message + TextEntities []MessageEntity `json:"text_entities,omitempty"` + // CompletedByUser is the user that completed the task; for tasks that + // were completed. // // optional - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - // InputMessageContent content of the message to be sent instead of the video animation + CompletedByUser *User `json:"completed_by_user,omitempty"` + // CompletedByChat is the chat that completed the task on behalf of + // an anonymous user. // // optional - InputMessageContent interface{} `json:"input_message_content,omitempty"` -} - -// InlineQueryResultPhoto is an inline query response photo. -type InlineQueryResultPhoto struct { - // Type of the result, must be article. - Type string `json:"type"` - // ID unique identifier for this result, 1-64 Bytes. - ID string `json:"id"` - // URL a valid URL of the photo. Photo must be in jpeg format. - // Photo size must not exceed 5MB. - URL string `json:"photo_url"` - // MimeType - MimeType string `json:"mime_type"` - // Width of the photo + CompletedByChat *Chat `json:"completed_by_chat,omitempty"` + // CompletionDate is the point in time (Unix timestamp) when the task + // was completed; 0 for tasks that weren't completed. // // optional - Width int `json:"photo_width,omitempty"` - // Height of the photo + CompletionDate int `json:"completion_date,omitempty"` +} + +// Checklist describes a checklist on a message. +type Checklist struct { + // Title is the title of the checklist. + Title string `json:"title"` + // TitleEntities are the special entities that appear in the title. // // optional - Height int `json:"photo_height,omitempty"` - // ThumbURL url of the thumbnail for the photo. + TitleEntities []MessageEntity `json:"title_entities,omitempty"` + // Tasks is the list of tasks in the checklist. + Tasks []ChecklistTask `json:"tasks"` + // OthersCanAddTasks is true, if users other than the creator can add + // tasks to the checklist. // // optional - ThumbURL string `json:"thumb_url,omitempty"` - // Title for the result + OthersCanAddTasks bool `json:"others_can_add_tasks,omitempty"` + // OthersCanMarkTasksAsDone is true, if users other than the creator + // can mark tasks as done or not done. // // optional - Title string `json:"title,omitempty"` - // Description short description of the result + OthersCanMarkTasksAsDone bool `json:"others_can_mark_tasks_as_done,omitempty"` +} + +// InputChecklistTask describes a task to be added to a checklist. +type InputChecklistTask struct { + // ID is the unique identifier of the task; must be positive and unique + // among all task identifiers in the checklist. + ID int `json:"id"` + // Text is the text of the task. + Text string `json:"text"` + // ParseMode is the mode for parsing entities in the text. // // optional - Description string `json:"description,omitempty"` - // Caption of the photo to be sent, 0-1024 characters after entities parsing. + ParseMode string `json:"parse_mode,omitempty"` + // TextEntities are the special entities that appear in the text. // // optional - Caption string `json:"caption,omitempty"` - // ParseMode mode for parsing entities in the photo caption. - // See formatting options for more details - // (https://core.telegram.org/bots/api#formatting-options). + TextEntities []MessageEntity `json:"text_entities,omitempty"` +} + +// InputChecklist describes a checklist to create. +type InputChecklist struct { + // Title is the title of the checklist. + Title string `json:"title"` + // ParseMode is the mode for parsing entities in the title. // // optional ParseMode string `json:"parse_mode,omitempty"` - // ReplyMarkup inline keyboard attached to the message. + // TitleEntities are the special entities that appear in the title. // // optional - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - // CaptionEntities is a list of special entities that appear in the caption, - // which can be specified instead of parse_mode + TitleEntities []MessageEntity `json:"title_entities,omitempty"` + // Tasks is the list of 1-30 tasks in the checklist. + Tasks []InputChecklistTask `json:"tasks"` + // OthersCanAddTasks pass true if users other than the creator can add + // tasks to the checklist. // // optional - CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` - // InputMessageContent content of the message to be sent instead of the photo. + OthersCanAddTasks bool `json:"others_can_add_tasks,omitempty"` + // OthersCanMarkTasksAsDone pass true if users other than the creator + // can mark tasks as done or not done. // // optional - InputMessageContent interface{} `json:"input_message_content,omitempty"` + OthersCanMarkTasksAsDone bool `json:"others_can_mark_tasks_as_done,omitempty"` } -// InlineQueryResultVenue is an inline query response venue. -type InlineQueryResultVenue struct { - // Type of the result, must be venue - Type string `json:"type"` - // ID unique identifier for this result, 1-64 Bytes - ID string `json:"id"` - // Latitude of the venue location in degrees - Latitude float64 `json:"latitude"` - // Longitude of the venue location in degrees - Longitude float64 `json:"longitude"` - // Title of the venue - Title string `json:"title"` - // Address of the venue - Address string `json:"address"` - // FoursquareID foursquare identifier of the venue if known - // - // optional - FoursquareID string `json:"foursquare_id,omitempty"` - // FoursquareType foursquare type of the venue, if known. - // (For example, “arts_entertainment/default”, “arts_entertainment/aquarium” or “food/icecream”.) - // - // optional - FoursquareType string `json:"foursquare_type,omitempty"` - // GooglePlaceID is the Google Places identifier of the venue - // - // optional - GooglePlaceID string `json:"google_place_id,omitempty"` - // GooglePlaceType is the Google Places type of the venue - // - // optional - GooglePlaceType string `json:"google_place_type,omitempty"` - // ReplyMarkup inline keyboard attached to the message - // - // optional - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - // InputMessageContent content of the message to be sent instead of the venue +// ChecklistTasksDone describes a service message about tasks in a checklist +// being marked as done or not done. +type ChecklistTasksDone struct { + // ChecklistMessage is the message containing the checklist whose tasks + // were marked as done or not done. Note that the Message object in this + // field will not contain the ReplyToMessage field even if it itself is + // a reply. // // optional - InputMessageContent interface{} `json:"input_message_content,omitempty"` - // ThumbURL url of the thumbnail for the result + ChecklistMessage *Message `json:"checklist_message,omitempty"` + // MarkedAsDoneTaskIDs are the IDs of tasks that were marked as done. // // optional - ThumbURL string `json:"thumb_url,omitempty"` - // ThumbWidth thumbnail width + MarkedAsDoneTaskIDs []int `json:"marked_as_done_task_ids,omitempty"` + // MarkedAsNotDoneTaskIDs are the IDs of tasks that were marked as not + // done. // // optional - ThumbWidth int `json:"thumb_width,omitempty"` - // ThumbHeight thumbnail height + MarkedAsNotDoneTaskIDs []int `json:"marked_as_not_done_task_ids,omitempty"` +} + +// ChecklistTasksAdded describes a service message about tasks added to a +// checklist. +type ChecklistTasksAdded struct { + // ChecklistMessage is the message containing the checklist to which + // the tasks were added. // // optional - ThumbHeight int `json:"thumb_height,omitempty"` + ChecklistMessage *Message `json:"checklist_message,omitempty"` + // Tasks is the list of tasks added to the checklist. + Tasks []ChecklistTask `json:"tasks"` } -// InlineQueryResultVideo is an inline query response video. -type InlineQueryResultVideo struct { - // Type of the result, must be video +// Input paid media type constants. +const ( + InputPaidMediaTypePhoto = "photo" + InputPaidMediaTypeVideo = "video" + InputPaidMediaTypeLivePhoto = "live_photo" +) + +// InputPaidMedia describes the paid media to be sent. Flat polymorphic by +// Type: +// - "photo" → Media is set +// - "video" → Media, Thumbnail, Cover, StartTimestamp, Width, Height, +// Duration, SupportsStreaming +// - "live_photo" → Media (the video portion of the live photo) and Photo +// (the static photo) are set; sending live photos by URL is not +// supported. +type InputPaidMedia struct { + // Type of the media. One of "photo", "video", "live_photo". Type string `json:"type"` - // ID unique identifier for this result, 1-64 bytes - ID string `json:"id"` - // URL a valid url for the embedded video player or video file - URL string `json:"video_url"` - // MimeType of the content of video url, “text/html” or “video/mp4” - MimeType string `json:"mime_type"` + // Media is the file to send. Pass a file_id or URL, or upload a new one + // via FilePath/FileBytes/FileReader. For live_photo, this is the video + // portion of the live photo. + Media RequestFileData `json:"media"` + // Photo is the static photo to send when Type is "live_photo". // - // ThumbURL url of the thumbnail (jpeg only) for the video // optional - ThumbURL string `json:"thumb_url,omitempty"` - // Title for the result - Title string `json:"title"` - // Caption of the video to be sent, 0-1024 characters after entities parsing + Photo RequestFileData `json:"photo,omitempty"` + // Thumbnail of the file (video only). // // optional - Caption string `json:"caption,omitempty"` - // Width video width + Thumbnail RequestFileData `json:"thumbnail,omitempty"` + // Cover for the video in the message (video only). // // optional - Width int `json:"video_width,omitempty"` - // Height video height + Cover RequestFileData `json:"cover,omitempty"` + // StartTimestamp is the timestamp in seconds from which the video will + // play in the message (video only). // // optional - Height int `json:"video_height,omitempty"` - // Duration video duration in seconds + StartTimestamp int `json:"start_timestamp,omitempty"` + // Width of the video. // // optional - Duration int `json:"video_duration,omitempty"` - // Description short description of the result + Width int `json:"width,omitempty"` + // Height of the video. // // optional - Description string `json:"description,omitempty"` - // ReplyMarkup inline keyboard attached to the message + Height int `json:"height,omitempty"` + // Duration of the video in seconds. // // optional - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - // InputMessageContent content of the message to be sent instead of the video. - // This field is required if InlineQueryResultVideo is used to send - // an HTML-page as a result (e.g., a YouTube video). + Duration int `json:"duration,omitempty"` + // SupportsStreaming is true if the uploaded video is suitable for + // streaming. Video only. // // optional - InputMessageContent interface{} `json:"input_message_content,omitempty"` + SupportsStreaming bool `json:"supports_streaming,omitempty"` } -// InlineQueryResultVoice is an inline query response voice. -type InlineQueryResultVoice struct { - // Type of the result, must be voice - Type string `json:"type"` - // ID unique identifier for this result, 1-64 bytes - ID string `json:"id"` - // URL a valid URL for the voice recording - URL string `json:"voice_url"` - // Title recording title - Title string `json:"title"` - // Caption 0-1024 characters after entities parsing +// SharedUser contains information about a user that was shared with the bot +// using a KeyboardButtonRequestUsers button. +type SharedUser struct { + // UserID is the identifier of the shared user. + UserID int64 `json:"user_id"` + // FirstName of the user, if the name was requested by the bot. // // optional - Caption string `json:"caption,omitempty"` - // ParseMode mode for parsing entities in the video caption. - // See formatting options for more details - // (https://core.telegram.org/bots/api#formatting-options). + FirstName string `json:"first_name,omitempty"` + // LastName of the user, if the name was requested by the bot. // // optional - ParseMode string `json:"parse_mode,omitempty"` - // CaptionEntities is a list of special entities that appear in the caption, - // which can be specified instead of parse_mode + LastName string `json:"last_name,omitempty"` + // Username of the user, if the username was requested by the bot. // // optional - CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` - // Duration recording duration in seconds + Username string `json:"username,omitempty"` + // Photo of the user, if the photo was requested by the bot. // // optional - Duration int `json:"voice_duration,omitempty"` - // ReplyMarkup inline keyboard attached to the message + Photo []PhotoSize `json:"photo,omitempty"` +} + +// Reaction type constants. +const ( + ReactionTypeEmoji = "emoji" + ReactionTypeCustomEmoji = "custom_emoji" + ReactionTypePaid = "paid" +) + +// ReactionType describes the type of a reaction. The Type field discriminates +// between concrete variants: +// - "emoji" → Emoji is set +// - "custom_emoji" → CustomEmojiID is set +// - "paid" → no extra fields +type ReactionType struct { + // Type of the reaction. One of "emoji", "custom_emoji", "paid". + Type string `json:"type"` + // Emoji is set when Type == "emoji". One of the supported emoji. // // optional - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - // InputMessageContent content of the message to be sent instead of the voice recording + Emoji string `json:"emoji,omitempty"` + // CustomEmojiID is set when Type == "custom_emoji". // // optional - InputMessageContent interface{} `json:"input_message_content,omitempty"` + CustomEmojiID string `json:"custom_emoji_id,omitempty"` } -// ChosenInlineResult is an inline query result chosen by a User -type ChosenInlineResult struct { - // ResultID the unique identifier for the result that was chosen - ResultID string `json:"result_id"` - // From the user that chose the result - From *User `json:"from"` - // Location sender location, only for bots that require user location +// ReactionCount represents a reaction added to a message along with the +// number of times it was added. +type ReactionCount struct { + // Type of the reaction. + Type ReactionType `json:"type"` + // TotalCount is the number of times the reaction was added. + TotalCount int `json:"total_count"` +} + +// MessageReactionUpdated represents a change of a reaction on a message +// performed by a user. +type MessageReactionUpdated struct { + // Chat containing the message the user reacted to. + Chat Chat `json:"chat"` + // MessageID is the unique identifier of the message inside the chat. + MessageID int `json:"message_id"` + // User that changed the reaction, if the user isn't anonymous. // // optional - Location *Location `json:"location,omitempty"` - // InlineMessageID identifier of the sent inline message. - // Available only if there is an inline keyboard attached to the message. - // Will be also received in callback queries and can be used to edit the message. + User *User `json:"user,omitempty"` + // ActorChat is the chat on behalf of which the reaction was changed, + // if the user is anonymous. // // optional - InlineMessageID string `json:"inline_message_id,omitempty"` - // Query the query that was used to obtain the result - Query string `json:"query"` + ActorChat *Chat `json:"actor_chat,omitempty"` + // Date of the change in Unix time. + Date int `json:"date"` + // OldReaction is the previous list of reaction types that were set by the user. + OldReaction []ReactionType `json:"old_reaction"` + // NewReaction is the new list of reaction types that have been set by the user. + NewReaction []ReactionType `json:"new_reaction"` } -// SentWebAppMessage contains information about an inline message sent by a Web App -// on behalf of a user. -type SentWebAppMessage struct { - // Identifier of the sent inline message. Available only if there is an inline - // keyboard attached to the message. - // - // optional - InlineMessageID string `json:"inline_message_id,omitempty"` +// MessageReactionCountUpdated represents reaction changes on a message with +// anonymous reactions. +type MessageReactionCountUpdated struct { + // Chat containing the message. + Chat Chat `json:"chat"` + // MessageID is the unique identifier of the message inside the chat. + MessageID int `json:"message_id"` + // Date of the change in Unix time. + Date int `json:"date"` + // Reactions is the list of reactions that are present on the message. + Reactions []ReactionCount `json:"reactions"` } -// InputTextMessageContent contains text for displaying -// as an inline query result. -type InputTextMessageContent struct { - // Text of the message to be sent, 1-4096 characters - Text string `json:"message_text"` - // ParseMode mode for parsing entities in the message text. - // See formatting options for more details - // (https://core.telegram.org/bots/api#formatting-options). - // - // optional - ParseMode string `json:"parse_mode,omitempty"` - // Entities is a list of special entities that appear in message text, which - // can be specified instead of parse_mode +// TextQuote contains information about the quoted part of a message that is +// replied to by the given message. +type TextQuote struct { + // Text of the quoted part of the message that is replied to by the given + // message. + Text string `json:"text"` + // Entities are special entities that appear in the quote. Currently, only + // bold, italic, underline, strikethrough, spoiler, and custom_emoji + // entities are kept in quotes. // // optional Entities []MessageEntity `json:"entities,omitempty"` - // DisableWebPagePreview disables link previews for links in the sent message + // Position is the approximate quote position in the original message in + // UTF-16 code units as specified by the sender. + Position int `json:"position"` + // IsManual is true, if the quote was chosen manually by the message + // sender. Otherwise, the quote was added automatically by the server. // // optional - DisableWebPagePreview bool `json:"disable_web_page_preview,omitempty"` + IsManual bool `json:"is_manual,omitempty"` } -// InputLocationMessageContent contains a location for displaying -// as an inline query result. -type InputLocationMessageContent struct { - // Latitude of the location in degrees - Latitude float64 `json:"latitude"` - // Longitude of the location in degrees - Longitude float64 `json:"longitude"` - // HorizontalAccuracy is the radius of uncertainty for the location, - // measured in meters; 0-1500 +// ExternalReplyInfo contains information about a message that is being replied +// to, which may come from another chat or forum topic. +type ExternalReplyInfo struct { + // Origin is the origin of the message replied to by the given message. + Origin MessageOrigin `json:"origin"` + // Chat is the conversation the original message belongs to. Available + // only if the chat is a supergroup or a channel. // // optional - HorizontalAccuracy float64 `json:"horizontal_accuracy,omitempty"` - // LivePeriod is the period in seconds for which the location can be - // updated, should be between 60 and 86400 + Chat *Chat `json:"chat,omitempty"` + // MessageID is the unique message identifier inside the original chat. + // Available only if the original chat is a supergroup or a channel. // // optional - LivePeriod int `json:"live_period,omitempty"` - // Heading is for live locations, a direction in which the user is moving, - // in degrees. Must be between 1 and 360 if specified. + MessageID int `json:"message_id,omitempty"` + // LinkPreviewOptions are options used for link preview generation for the + // original message, if it is a text message. // // optional - Heading int `json:"heading,omitempty"` - // ProximityAlertRadius is for live locations, a maximum distance for - // proximity alerts about approaching another chat member, in meters. Must - // be between 1 and 100000 if specified. + LinkPreviewOptions *LinkPreviewOptions `json:"link_preview_options,omitempty"` + // Animation is set if the message is an animation. // // optional - ProximityAlertRadius int `json:"proximity_alert_radius,omitempty"` -} - -// InputVenueMessageContent contains a venue for displaying -// as an inline query result. -type InputVenueMessageContent struct { - // Latitude of the venue in degrees - Latitude float64 `json:"latitude"` - // Longitude of the venue in degrees - Longitude float64 `json:"longitude"` - // Title name of the venue - Title string `json:"title"` - // Address of the venue - Address string `json:"address"` - // FoursquareID foursquare identifier of the venue, if known + Animation *Animation `json:"animation,omitempty"` + // Audio is set if the message is an audio file. // // optional - FoursquareID string `json:"foursquare_id,omitempty"` - // FoursquareType Foursquare type of the venue, if known + Audio *Audio `json:"audio,omitempty"` + // Document is set if the message is a general file. // // optional - FoursquareType string `json:"foursquare_type,omitempty"` - // GooglePlaceID is the Google Places identifier of the venue + Document *Document `json:"document,omitempty"` + // Photo is set if the message is a photo. // // optional - GooglePlaceID string `json:"google_place_id,omitempty"` - // GooglePlaceType is the Google Places type of the venue + Photo []PhotoSize `json:"photo,omitempty"` + // LivePhoto is set if the message is a live photo. // // optional - GooglePlaceType string `json:"google_place_type,omitempty"` -} - -// InputContactMessageContent contains a contact for displaying -// as an inline query result. -type InputContactMessageContent struct { - // PhoneNumber contact's phone number - PhoneNumber string `json:"phone_number"` - // FirstName contact's first name - FirstName string `json:"first_name"` - // LastName contact's last name + LivePhoto *LivePhoto `json:"live_photo,omitempty"` + // PaidMedia is set if the message contains paid media. // // optional - LastName string `json:"last_name,omitempty"` - // Additional data about the contact in the form of a vCard + PaidMedia *PaidMediaInfo `json:"paid_media,omitempty"` + // Sticker is set if the message is a sticker. // // optional - VCard string `json:"vcard,omitempty"` -} - -// InputInvoiceMessageContent represents the content of an invoice message to be -// sent as the result of an inline query. -type InputInvoiceMessageContent struct { - // Product name, 1-32 characters - Title string `json:"title"` - // Product description, 1-255 characters - Description string `json:"description"` - // Bot-defined invoice payload, 1-128 bytes. This will not be displayed to - // the user, use for your internal processes. - Payload string `json:"payload"` - // Payment provider token, obtained via Botfather - ProviderToken string `json:"provider_token"` - // Three-letter ISO 4217 currency code - Currency string `json:"currency"` - // Price breakdown, a JSON-serialized list of components (e.g. product - // price, tax, discount, delivery cost, delivery tax, bonus, etc.) - Prices []LabeledPrice `json:"prices"` - // The maximum accepted amount for tips in the smallest units of the - // currency (integer, not float/double). + Sticker *Sticker `json:"sticker,omitempty"` + // Story is set if the message is a forwarded story. // // optional - MaxTipAmount int `json:"max_tip_amount,omitempty"` - // An array of suggested amounts of tip in the smallest units of the - // currency (integer, not float/double). At most 4 suggested tip amounts can - // be specified. The suggested tip amounts must be positive, passed in a - // strictly increased order and must not exceed max_tip_amount. + Story *Story `json:"story,omitempty"` + // Video is set if the message is a video. // // optional - SuggestedTipAmounts []int `json:"suggested_tip_amounts,omitempty"` - // A JSON-serialized object for data about the invoice, which will be shared - // with the payment provider. A detailed description of the required fields - // should be provided by the payment provider. + Video *Video `json:"video,omitempty"` + // VideoNote is set if the message is a video note. // // optional - ProviderData string `json:"provider_data,omitempty"` - // URL of the product photo for the invoice. Can be a photo of the goods or - // a marketing image for a service. People like it better when they see what - // they are paying for. + VideoNote *VideoNote `json:"video_note,omitempty"` + // Voice is set if the message is a voice message. // // optional - PhotoURL string `json:"photo_url,omitempty"` - // Photo size + Voice *Voice `json:"voice,omitempty"` + // HasMediaSpoiler is true, if the message media is covered by a spoiler animation. // // optional - PhotoSize int `json:"photo_size,omitempty"` - // Photo width + HasMediaSpoiler bool `json:"has_media_spoiler,omitempty"` + // Checklist is set if the message is a checklist. // // optional - PhotoWidth int `json:"photo_width,omitempty"` - // Photo height + Checklist *Checklist `json:"checklist,omitempty"` + // Contact is set if the message is a shared contact. // // optional - PhotoHeight int `json:"photo_height,omitempty"` - // Pass True, if you require the user's full name to complete the order + Contact *Contact `json:"contact,omitempty"` + // Dice is set if the message is a dice with random value. // // optional - NeedName bool `json:"need_name,omitempty"` - // Pass True, if you require the user's phone number to complete the order + Dice *Dice `json:"dice,omitempty"` + // Game is set if the message is a game. // // optional - NeedPhoneNumber bool `json:"need_phone_number,omitempty"` - // Pass True, if you require the user's email address to complete the order + Game *Game `json:"game,omitempty"` + // Giveaway is set if the message is a scheduled giveaway. // // optional - NeedEmail bool `json:"need_email,omitempty"` - // Pass True, if you require the user's shipping address to complete the order + Giveaway *Giveaway `json:"giveaway,omitempty"` + // GiveawayWinners is set if the message is a giveaway with public winners. // // optional - NeedShippingAddress bool `json:"need_shipping_address,omitempty"` - // Pass True, if user's phone number should be sent to provider + GiveawayWinners *GiveawayWinners `json:"giveaway_winners,omitempty"` + // Invoice is set if the message is an invoice for a payment. // // optional - SendPhoneNumberToProvider bool `json:"send_phone_number_to_provider,omitempty"` - // Pass True, if user's email address should be sent to provider + Invoice *Invoice `json:"invoice,omitempty"` + // Location is set if the message is a shared location. // // optional - SendEmailToProvider bool `json:"send_email_to_provider,omitempty"` - // Pass True, if the final price depends on the shipping method + Location *Location `json:"location,omitempty"` + // Poll is set if the message is a native poll. // // optional - IsFlexible bool `json:"is_flexible,omitempty"` + Poll *Poll `json:"poll,omitempty"` + // Venue is set if the message is a venue. + // + // optional + Venue *Venue `json:"venue,omitempty"` } -// LabeledPrice represents a portion of the price for goods or services. -type LabeledPrice struct { - // Label portion label - Label string `json:"label"` - // Amount price of the product in the smallest units of the currency (integer, not float/double). - // For example, for a price of US$ 1.45 pass amount = 145. - // See the exp parameter in currencies.json - // (https://core.telegram.org/bots/payments/currencies.json), - // it shows the number of digits past the decimal point - // for each currency (2 for the majority of currencies). - Amount int `json:"amount"` +// Chat boost source constants. +const ( + ChatBoostSourcePremium = "premium" + ChatBoostSourceGiftCode = "gift_code" + ChatBoostSourceGiveaway = "giveaway" +) + +// ChatBoostSource describes the source of a chat boost. The Source field +// discriminates between concrete variants: +// - "premium" → User is set (the user that boosted the chat) +// - "gift_code" → User is set (the user for which the gift code was created) +// - "giveaway" → GiveawayMessageID is set; User is set for the prize winner; +// IsUnclaimed is true if the giveaway prize wasn't claimed +type ChatBoostSource struct { + // Source of the boost. One of "premium", "gift_code", "giveaway". + Source string `json:"source"` + // User that boosted the chat. Set for "premium" and "gift_code"; for + // "giveaway" set to the user that won the prize, if any. + // + // optional + User *User `json:"user,omitempty"` + // GiveawayMessageID is the identifier of the message with the giveaway in + // the chat. Set for "giveaway". + // + // optional + GiveawayMessageID int `json:"giveaway_message_id,omitempty"` + // IsUnclaimed is true if the giveaway was completed but no user won the + // prize. Set for "giveaway". + // + // optional + IsUnclaimed bool `json:"is_unclaimed,omitempty"` + // PrizeStarCount is the number of Telegram Stars to be split between + // giveaway winners. Set for "giveaway" and only for Telegram Star + // giveaways. + // + // optional + PrizeStarCount int `json:"prize_star_count,omitempty"` } -// Invoice contains basic information about an invoice. -type Invoice struct { - // Title product name - Title string `json:"title"` - // Description product description - Description string `json:"description"` - // StartParameter unique bot deep-linking parameter that can be used to generate this invoice - StartParameter string `json:"start_parameter"` - // Currency three-letter ISO 4217 currency code - // (see https://core.telegram.org/bots/payments#supported-currencies) - Currency string `json:"currency"` - // TotalAmount total price in the smallest units of the currency (integer, not float/double). - // For example, for a price of US$ 1.45 pass amount = 145. - // See the exp parameter in currencies.json - // (https://core.telegram.org/bots/payments/currencies.json), - // it shows the number of digits past the decimal point - // for each currency (2 for the majority of currencies). - TotalAmount int `json:"total_amount"` +// ChatBoost contains information about a chat boost. +type ChatBoost struct { + // BoostID is the unique identifier of the boost. + BoostID string `json:"boost_id"` + // AddDate is the point in time (Unix timestamp) when the chat was boosted. + AddDate int `json:"add_date"` + // ExpirationDate is the point in time (Unix timestamp) when the boost + // will automatically expire, unless it's renewed by the premium subscriber. + ExpirationDate int `json:"expiration_date"` + // Source of the added boost. + Source ChatBoostSource `json:"source"` } -// ShippingAddress represents a shipping address. -type ShippingAddress struct { - // CountryCode ISO 3166-1 alpha-2 country code - CountryCode string `json:"country_code"` - // State if applicable - State string `json:"state"` - // City city - City string `json:"city"` - // StreetLine1 first line for the address - StreetLine1 string `json:"street_line1"` - // StreetLine2 second line for the address - StreetLine2 string `json:"street_line2"` - // PostCode address post code - PostCode string `json:"post_code"` +// ChatBoostUpdated represents a boost added to a chat or changed. +type ChatBoostUpdated struct { + // Chat which was boosted. + Chat Chat `json:"chat"` + // Boost is information about the chat boost. + Boost ChatBoost `json:"boost"` } -// OrderInfo represents information about an order. -type OrderInfo struct { - // Name user name +// ChatBoostRemoved represents a boost removed from a chat. +type ChatBoostRemoved struct { + // Chat which was boosted. + Chat Chat `json:"chat"` + // BoostID is the unique identifier of the boost. + BoostID string `json:"boost_id"` + // RemoveDate is the point in time (Unix timestamp) when the boost was removed. + RemoveDate int `json:"remove_date"` + // Source of the removed boost. + Source ChatBoostSource `json:"source"` +} + +// UserChatBoosts represents a list of boosts added to a chat by a user. +type UserChatBoosts struct { + // Boosts is the list of boosts added to the chat by the user. + Boosts []ChatBoost `json:"boosts"` +} + +// Giveaway represents a message about a scheduled giveaway. +type Giveaway struct { + // Chats is the list of chats which the user must join to participate in + // the giveaway. + Chats []Chat `json:"chats"` + // WinnersSelectionDate is the point in time (Unix timestamp) when winners + // of the giveaway will be selected. + WinnersSelectionDate int `json:"winners_selection_date"` + // WinnerCount is the number of users which are supposed to be selected + // as winners of the giveaway. + WinnerCount int `json:"winner_count"` + // OnlyNewMembers is true, if only users who join the chats after the + // giveaway started should be eligible to win. // // optional - Name string `json:"name,omitempty"` - // PhoneNumber user's phone number + OnlyNewMembers bool `json:"only_new_members,omitempty"` + // HasPublicWinners is true, if the list of giveaway winners will be + // visible to everyone. // // optional - PhoneNumber string `json:"phone_number,omitempty"` - // Email user email + HasPublicWinners bool `json:"has_public_winners,omitempty"` + // PrizeDescription is the description of additional giveaway prize. // // optional - Email string `json:"email,omitempty"` - // ShippingAddress user shipping address + PrizeDescription string `json:"prize_description,omitempty"` + // CountryCodes is a list of two-letter ISO 3166-1 alpha-2 country codes + // indicating the countries from which eligible users for the giveaway + // must come. // // optional - ShippingAddress *ShippingAddress `json:"shipping_address,omitempty"` + CountryCodes []string `json:"country_codes,omitempty"` + // PremiumSubscriptionMonthCount is the number of months the Telegram + // Premium subscription won from the giveaway will be active for. + // + // optional + PremiumSubscriptionMonthCount int `json:"premium_subscription_month_count,omitempty"` + // PrizeStarCount is the number of Telegram Stars to be split between + // giveaway winners. For Telegram Star giveaways only. + // + // optional + PrizeStarCount int `json:"prize_star_count,omitempty"` } -// ShippingOption represents one shipping option. -type ShippingOption struct { - // ID shipping option identifier - ID string `json:"id"` - // Title option title - Title string `json:"title"` - // Prices list of price portions - Prices []LabeledPrice `json:"prices"` +// GiveawayCreated represents a service message about the creation of a +// scheduled giveaway. +type GiveawayCreated struct { + // PrizeStarCount is the number of Telegram Stars to be split between + // giveaway winners. For Telegram Star giveaways only. + // + // optional + PrizeStarCount int `json:"prize_star_count,omitempty"` } -// SuccessfulPayment contains basic information about a successful payment. -type SuccessfulPayment struct { - // Currency three-letter ISO 4217 currency code - // (see https://core.telegram.org/bots/payments#supported-currencies) - Currency string `json:"currency"` - // TotalAmount total price in the smallest units of the currency (integer, not float/double). - // For example, for a price of US$ 1.45 pass amount = 145. - // See the exp parameter in currencies.json, - // (https://core.telegram.org/bots/payments/currencies.json) - // it shows the number of digits past the decimal point - // for each currency (2 for the majority of currencies). - TotalAmount int `json:"total_amount"` - // InvoicePayload bot specified invoice payload - InvoicePayload string `json:"invoice_payload"` - // ShippingOptionID identifier of the shipping option chosen by the user +// GiveawayWinners represents a message about the completion of a giveaway +// with public winners. +// +// The PrizeStarCount field holds the number of Telegram Stars split between +// winners, if this was a Telegram Star giveaway. +type GiveawayWinners struct { + // Chat that created the giveaway. + Chat Chat `json:"chat"` + // GiveawayMessageID is the identifier of the message with the giveaway + // in the chat. + GiveawayMessageID int `json:"giveaway_message_id"` + // WinnersSelectionDate is the point in time (Unix timestamp) when winners + // of the giveaway were selected. + WinnersSelectionDate int `json:"winners_selection_date"` + // WinnerCount is the total number of winners in the giveaway. + WinnerCount int `json:"winner_count"` + // Winners is the list of up to 100 winners of the giveaway. + Winners []User `json:"winners"` + // AdditionalChatCount is the number of other chats the user had to join + // in order to be eligible for the giveaway. // // optional - ShippingOptionID string `json:"shipping_option_id,omitempty"` - // OrderInfo order info provided by the user + AdditionalChatCount int `json:"additional_chat_count,omitempty"` + // PremiumSubscriptionMonthCount is the number of months the Telegram + // Premium subscription won from the giveaway will be active for. // // optional - OrderInfo *OrderInfo `json:"order_info,omitempty"` - // TelegramPaymentChargeID telegram payment identifier - TelegramPaymentChargeID string `json:"telegram_payment_charge_id"` - // ProviderPaymentChargeID provider payment identifier - ProviderPaymentChargeID string `json:"provider_payment_charge_id"` + PremiumSubscriptionMonthCount int `json:"premium_subscription_month_count,omitempty"` + // PrizeStarCount is the number of Telegram Stars that were split between + // giveaway winners. For Telegram Star giveaways only. + // + // optional + PrizeStarCount int `json:"prize_star_count,omitempty"` + // UnclaimedPrizeCount is the number of undistributed prizes. + // + // optional + UnclaimedPrizeCount int `json:"unclaimed_prize_count,omitempty"` + // OnlyNewMembers is true, if only users who joined the chats after the + // giveaway started were eligible to win. + // + // optional + OnlyNewMembers bool `json:"only_new_members,omitempty"` + // WasRefunded is true, if the giveaway was canceled because the payment + // for it was refunded. + // + // optional + WasRefunded bool `json:"was_refunded,omitempty"` + // PrizeDescription is the description of additional giveaway prize. + // + // optional + PrizeDescription string `json:"prize_description,omitempty"` } -// ShippingQuery contains information about an incoming shipping query. -type ShippingQuery struct { - // ID unique query identifier - ID string `json:"id"` - // From user who sent the query - From *User `json:"from"` - // InvoicePayload bot specified invoice payload - InvoicePayload string `json:"invoice_payload"` - // ShippingAddress user specified shipping address - ShippingAddress *ShippingAddress `json:"shipping_address"` +// GiveawayCompleted represents a service message about the completion of a +// giveaway without public winners. +type GiveawayCompleted struct { + // WinnerCount is the number of winners in the giveaway. + WinnerCount int `json:"winner_count"` + // UnclaimedPrizeCount is the number of undistributed prizes. + // + // optional + UnclaimedPrizeCount int `json:"unclaimed_prize_count,omitempty"` + // GiveawayMessage is the message with the giveaway that was completed, + // if it wasn't deleted. + // + // optional + GiveawayMessage *Message `json:"giveaway_message,omitempty"` + // IsStarGiveaway is true, if the giveaway is a Telegram Star giveaway. + // Otherwise, currently, the giveaway is a Telegram Premium giveaway. + // + // optional + IsStarGiveaway bool `json:"is_star_giveaway,omitempty"` } -// PreCheckoutQuery contains information about an incoming pre-checkout query. -type PreCheckoutQuery struct { - // ID unique query identifier - ID string `json:"id"` - // From user who sent the query - From *User `json:"from"` - // Currency three-letter ISO 4217 currency code - // // (see https://core.telegram.org/bots/payments#supported-currencies) - Currency string `json:"currency"` - // TotalAmount total price in the smallest units of the currency (integer, not float/double). - // // For example, for a price of US$ 1.45 pass amount = 145. - // // See the exp parameter in currencies.json, - // // (https://core.telegram.org/bots/payments/currencies.json) - // // it shows the number of digits past the decimal point - // // for each currency (2 for the majority of currencies). - TotalAmount int `json:"total_amount"` - // InvoicePayload bot specified invoice payload - InvoicePayload string `json:"invoice_payload"` - // ShippingOptionID identifier of the shipping option chosen by the user +// Message origin type constants. +const ( + MessageOriginTypeUser = "user" + MessageOriginTypeHiddenUser = "hidden_user" + MessageOriginTypeChat = "chat" + MessageOriginTypeChannel = "channel" +) + +// MessageOrigin describes the origin of a message. The Type field discriminates +// between concrete variants: +// - "user" → SenderUser is set +// - "hidden_user" → SenderUserName is set +// - "chat" → SenderChat is set; AuthorSignature optional +// - "channel" → Chat and MessageID are set; AuthorSignature optional +type MessageOrigin struct { + // Type of the message origin. One of "user", "hidden_user", "chat", "channel". + Type string `json:"type"` + // Date the message was sent originally in Unix time. + Date int `json:"date"` + // SenderUser is the user that sent the message originally. Set when Type + // is "user". // // optional - ShippingOptionID string `json:"shipping_option_id,omitempty"` - // OrderInfo order info provided by the user + SenderUser *User `json:"sender_user,omitempty"` + // SenderUserName is the name of the user that sent the message originally. + // Set when Type is "hidden_user". // // optional - OrderInfo *OrderInfo `json:"order_info,omitempty"` + SenderUserName string `json:"sender_user_name,omitempty"` + // SenderChat is the chat that sent the message originally. Set when Type + // is "chat". + // + // optional + SenderChat *Chat `json:"sender_chat,omitempty"` + // AuthorSignature is the signature of the original message author or + // the post author for messages from a channel. Optional for "chat" and + // "channel". + // + // optional + AuthorSignature string `json:"author_signature,omitempty"` + // Chat is the channel chat to which the message was originally sent. + // Set when Type is "channel". + // + // optional + Chat *Chat `json:"chat,omitempty"` + // MessageID is the unique message identifier inside the chat. Set when + // Type is "channel". + // + // optional + MessageID int `json:"message_id,omitempty"` +} + +// InaccessibleMessage describes a message that was deleted or otherwise +// inaccessible to the bot. +type InaccessibleMessage struct { + // Chat the message belonged to. + Chat Chat `json:"chat"` + // MessageID is the unique message identifier inside the chat. + MessageID int `json:"message_id"` + // Date is always 0. The field is present in order to mimic the Message + // type so that callers can distinguish accessible from inaccessible + // messages by checking Date == 0. + Date int `json:"date"` +} + +// LinkPreviewOptions describes the options used for link preview generation. +type LinkPreviewOptions struct { + // IsDisabled is true, if the link preview is disabled. + // + // optional + IsDisabled bool `json:"is_disabled,omitempty"` + // URL to use for the link preview. If empty, then the first URL found in + // the message text will be used. + // + // optional + URL string `json:"url,omitempty"` + // PreferSmallMedia is true, if the media in the link preview is supposed + // to be shrunk; ignored if the URL isn't explicitly specified or media + // size change isn't supported for the preview. + // + // optional + PreferSmallMedia bool `json:"prefer_small_media,omitempty"` + // PreferLargeMedia is true, if the media in the link preview is supposed + // to be enlarged; ignored if the URL isn't explicitly specified or media + // size change isn't supported for the preview. + // + // optional + PreferLargeMedia bool `json:"prefer_large_media,omitempty"` + // ShowAboveText is true, if the link preview must be shown above the + // message text; otherwise, the link preview will be shown below the + // message text. + // + // optional + ShowAboveText bool `json:"show_above_text,omitempty"` +} + +// ReplyParameters describes reply parameters for the message that is being sent. +type ReplyParameters struct { + // MessageID is the identifier of the message that will be replied to in + // the current chat, or in the chat ChatID if it is specified. Required if + // EphemeralMessageID is not specified. + // + // optional + MessageID int `json:"message_id,omitempty"` + // ChatID, if the message to be replied to is from a different chat, is + // the unique identifier for the chat or username of the channel + // (@channelusername). Pass int64 or string. Not supported for messages + // sent on behalf of a business account, messages from channel direct + // messages chats, and ephemeral messages. + // + // optional + ChatID any `json:"chat_id,omitempty"` + // EphemeralMessageID is the identifier of the incoming ephemeral message + // that will be replied to in the current chat. A reply to an ephemeral + // message must itself be an ephemeral message, and may only be sent + // within 15 seconds of the original. Required if MessageID is not + // specified. + // + // optional + EphemeralMessageID int `json:"ephemeral_message_id,omitempty"` + // AllowSendingWithoutReply is true if the message should be sent even if + // the specified message to be replied to is not found. + // + // optional + AllowSendingWithoutReply bool `json:"allow_sending_without_reply,omitempty"` + // Quote is the quoted part of the message to be replied to. Should be a + // substring of the original message, including bold, italic, underline, + // strikethrough, spoiler, and custom_emoji entities. + // + // optional + Quote string `json:"quote,omitempty"` + // QuoteParseMode is the mode for parsing entities in the quote. + // + // optional + QuoteParseMode string `json:"quote_parse_mode,omitempty"` + // QuoteEntities is a list of special entities that appear in the quote. + // It can be specified instead of QuoteParseMode. + // + // optional + QuoteEntities []MessageEntity `json:"quote_entities,omitempty"` + // QuotePosition is the position of the quote in the original message in + // UTF-16 code units. + // + // optional + QuotePosition int `json:"quote_position,omitempty"` + // ChecklistTaskID is the identifier of the specific checklist task to + // reply to. Required if replying to a task in a checklist. + // + // optional + ChecklistTaskID int `json:"checklist_task_id,omitempty"` + // PollOptionID is the persistent identifier of the specific poll + // option to reply to. Required if replying to a poll option. + // + // optional + PollOptionID string `json:"poll_option_id,omitempty"` +} + +// ChatLocation represents a location to which a chat is connected. +type ChatLocation struct { + // Location is the location to which the supergroup is connected. Can't be a + // live location. + Location Location `json:"location"` + // Address is the location address; 1-64 characters, as defined by the chat + // owner + Address string `json:"address"` +} + +// ForumTopic represents a forum topic. +type ForumTopic struct { + // MessageThreadID is the unique identifier of the forum topic + MessageThreadID int `json:"message_thread_id"` + // Name of the topic + Name string `json:"name"` + // IsNameImplicit is true, if the name of the topic was implicitly + // derived from the content of the first message, e.g. for topics + // created automatically in private chats. + // + // optional + IsNameImplicit bool `json:"is_name_implicit,omitempty"` + // IconColor is the color of the topic icon in RGB format + IconColor int `json:"icon_color"` + // IconCustomEmojiID is the unique identifier of the custom emoji shown + // as the topic icon. + // + // optional + IconCustomEmojiID string `json:"icon_custom_emoji_id,omitempty"` } + +// ForumTopicCreated represents a service message about a new forum topic +// created in the chat. +type ForumTopicCreated struct { + // Name of the topic + Name string `json:"name"` + // IsNameImplicit is true, if the name of the topic was implicitly + // derived from the content of the first message. + // + // optional + IsNameImplicit bool `json:"is_name_implicit,omitempty"` + // IconColor is the color of the topic icon in RGB format + IconColor int `json:"icon_color"` + // IconCustomEmojiID is the unique identifier of the custom emoji shown + // as the topic icon. + // + // optional + IconCustomEmojiID string `json:"icon_custom_emoji_id,omitempty"` +} + +// ForumTopicEdited represents a service message about an edited forum topic. +type ForumTopicEdited struct { + // Name is the new name of the topic, if it was edited. + // + // optional + Name string `json:"name,omitempty"` + // IconCustomEmojiID is the new identifier of the custom emoji shown as the + // topic icon, if it was edited; an empty string if the icon was removed. + // + // optional + IconCustomEmojiID string `json:"icon_custom_emoji_id,omitempty"` +} + +// ForumTopicClosed represents a service message about a forum topic closed +// in the chat. Currently holds no information. +type ForumTopicClosed struct{} + +// ForumTopicReopened represents a service message about a forum topic +// reopened in the chat. Currently holds no information. +type ForumTopicReopened struct{} + +// GeneralForumTopicHidden represents a service message about General forum +// topic hidden in the chat. Currently holds no information. +type GeneralForumTopicHidden struct{} + +// GeneralForumTopicUnhidden represents a service message about General forum +// topic unhidden in the chat. Currently holds no information. +type GeneralForumTopicUnhidden struct{} + +// WriteAccessAllowed represents a service message about a user allowing a bot +// to write messages after adding it to the attachment menu, launching a Web +// App from a link, or accepting an explicit request. +type WriteAccessAllowed struct { + // FromRequest is true if the access was granted after the user accepted + // an explicit request from a Web App sent by the method requestWriteAccess. + // + // optional + FromRequest bool `json:"from_request,omitempty"` + // WebAppName is the name of the Web App which was launched from a link. + // + // optional + WebAppName string `json:"web_app_name,omitempty"` + // FromAttachmentMenu is true if the access was granted when the bot was + // added to the attachment or side menu. + // + // optional + FromAttachmentMenu bool `json:"from_attachment_menu,omitempty"` +} + +// BotCommand represents a bot command. +type BotCommand struct { + // Command text of the command, 1-32 characters. + // Can contain only lowercase English letters, digits and underscores. + Command string `json:"command"` + // Description of the command, 3-256 characters. + Description string `json:"description"` + // IsEphemeral is true if the result of the command is an ephemeral + // message, visible only to the user who sent the command and the bot. + // + // optional + IsEphemeral bool `json:"is_ephemeral,omitempty"` +} + +// BotCommandScope represents the scope to which bot commands are applied. +// +// It contains the fields for all types of scopes, different types only support +// specific (or no) fields. +type BotCommandScope struct { + Type string `json:"type"` + ChatID int64 `json:"chat_id,omitempty"` + UserID int64 `json:"user_id,omitempty"` +} + +// MenuButton describes the bot's menu button in a private chat. +type MenuButton struct { + // Type is the type of menu button, must be one of: + // - `commands` + // - `web_app` + // - `default` + Type string `json:"type"` + // Text is the text on the button, for `web_app` type. + Text string `json:"text,omitempty"` + // WebApp is the description of the Web App that will be launched when the + // user presses the button for the `web_app` type. + WebApp *WebAppInfo `json:"web_app,omitempty"` +} + +// ResponseParameters are various errors that can be returned in APIResponse. +type ResponseParameters struct { + // The group has been migrated to a supergroup with the specified identifier. + // + // optional + MigrateToChatID int64 `json:"migrate_to_chat_id,omitempty"` + // In case of exceeding flood control, the number of seconds left to wait + // before the request can be repeated. + // + // optional + RetryAfter int `json:"retry_after,omitempty"` +} + +// BaseInputMedia is a base type for the InputMedia types. +type BaseInputMedia struct { + // Type of the result. + Type string `json:"type"` + // Media file to send. Pass a file_id to send a file + // that exists on the Telegram servers (recommended), + // pass an HTTP URL for Telegram to get a file from the Internet, + // or pass “attach://” to upload a new one + // using multipart/form-data under name. + Media RequestFileData `json:"media"` + // thumb intentionally missing as it is not currently compatible + + // Caption of the video to be sent, 0-1024 characters after entities parsing. + // + // optional + Caption string `json:"caption,omitempty"` + // ParseMode mode for parsing entities in the video caption. + // See formatting options for more details + // (https://core.telegram.org/bots/api#formatting-options). + // + // optional + ParseMode string `json:"parse_mode,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` + // ShowCaptionAboveMedia pass True if the caption must be shown above the + // message media. Valid for photo, video, and animation variants only; the + // field is ignored for audio and document. + // + // optional + ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"` + // HasSpoiler pass True if the media needs to be covered with a spoiler + // animation. Valid for photo, video, and animation variants only; the + // field is ignored for audio and document. + // + // optional + HasSpoiler bool `json:"has_spoiler,omitempty"` +} + +// InputMediaPhoto is a photo to send as part of a media group. +type InputMediaPhoto struct { + BaseInputMedia +} + +// InputMediaVideo is a video to send as part of a media group. +type InputMediaVideo struct { + BaseInputMedia + // Thumbnail of the file sent; can be ignored if thumbnail generation for + // the file is supported server-side. + // + // optional + Thumbnail RequestFileData `json:"thumbnail,omitempty"` + // Cover for the video in the message. + // + // optional + Cover RequestFileData `json:"cover,omitempty"` + // StartTimestamp is the timestamp in seconds from which the video will + // play in the message. + // + // optional + StartTimestamp int `json:"start_timestamp,omitempty"` + // Width video width + // + // optional + Width int `json:"width,omitempty"` + // Height video height + // + // optional + Height int `json:"height,omitempty"` + // Duration video duration + // + // optional + Duration int `json:"duration,omitempty"` + // SupportsStreaming pass True, if the uploaded video is suitable for streaming. + // + // optional + SupportsStreaming bool `json:"supports_streaming,omitempty"` +} + +// InputMediaAnimation is an animation to send as part of a media group. +type InputMediaAnimation struct { + BaseInputMedia + // Thumbnail of the file sent; can be ignored if thumbnail generation for + // the file is supported server-side. + // + // optional + Thumbnail RequestFileData `json:"thumbnail,omitempty"` + // Width video width + // + // optional + Width int `json:"width,omitempty"` + // Height video height + // + // optional + Height int `json:"height,omitempty"` + // Duration video duration + // + // optional + Duration int `json:"duration,omitempty"` +} + +// InputMediaAudio is an audio to send as part of a media group. +type InputMediaAudio struct { + BaseInputMedia + // Thumbnail of the file sent; can be ignored if thumbnail generation for + // the file is supported server-side. + // + // optional + Thumbnail RequestFileData `json:"thumbnail,omitempty"` + // Duration of the audio in seconds + // + // optional + Duration int `json:"duration,omitempty"` + // Performer of the audio + // + // optional + Performer string `json:"performer,omitempty"` + // Title of the audio + // + // optional + Title string `json:"title,omitempty"` +} + +// InputMediaVoiceNote represents a voice message file to be sent. It is +// accepted as the media of an InputRichBlock "voice_note" block and by +// InputRichMessageMedia. +type InputMediaVoiceNote struct { + BaseInputMedia + // Duration of the voice message in seconds + // + // optional + Duration int `json:"duration,omitempty"` +} + +// InputMediaDocument is a general file to send as part of a media group. +type InputMediaDocument struct { + BaseInputMedia + // Thumbnail of the file sent; can be ignored if thumbnail generation for + // the file is supported server-side. + // + // optional + Thumbnail RequestFileData `json:"thumbnail,omitempty"` + // DisableContentTypeDetection disables automatic server-side content type + // detection for files uploaded using multipart/form-data. Always true, if + // the document is sent as part of an album + // + // optional + DisableContentTypeDetection bool `json:"disable_content_type_detection,omitempty"` +} + +// InputMediaLivePhoto represents a live photo to be sent as part of a media +// group, sendLivePhoto, or as media in a poll/option/explanation. +// +// Sending live photos by URL is not currently supported; both Media and +// Photo must be either a file_id or a multipart upload. +type InputMediaLivePhoto struct { + BaseInputMedia + // Photo is the static photo of the live photo. Pass a file_id to send + // a file that exists on the Telegram servers (recommended) or pass an + // upload via "attach://". + Photo RequestFileData `json:"photo"` +} + +// InputMediaSticker represents a sticker file to be sent. Currently used as +// poll-option media. +type InputMediaSticker struct { + // Type must be "sticker". + Type string `json:"type"` + // Media is the file to send. + Media RequestFileData `json:"media"` + // Emoji associated with the sticker; only for just-uploaded stickers. + // + // optional + Emoji string `json:"emoji,omitempty"` +} + +// InputMediaLocation represents a shared location to be sent as media in a +// poll, poll option, or quiz explanation. +type InputMediaLocation struct { + // Type must be "location". + Type string `json:"type"` + // Latitude of the location. + Latitude float64 `json:"latitude"` + // Longitude of the location. + Longitude float64 `json:"longitude"` + // HorizontalAccuracy is the radius of uncertainty for the location, + // measured in meters; 0-1500. + // + // optional + HorizontalAccuracy float64 `json:"horizontal_accuracy,omitempty"` +} + +// InputMediaVenue represents a venue to be sent as media in a poll, poll +// option, or quiz explanation. +type InputMediaVenue struct { + // Type must be "venue". + Type string `json:"type"` + // Latitude of the venue. + Latitude float64 `json:"latitude"` + // Longitude of the venue. + Longitude float64 `json:"longitude"` + // Title is the name of the venue. + Title string `json:"title"` + // Address of the venue. + Address string `json:"address"` + // FoursquareID is the Foursquare identifier of the venue. + // + // optional + FoursquareID string `json:"foursquare_id,omitempty"` + // FoursquareType is the Foursquare type of the venue. + // + // optional + FoursquareType string `json:"foursquare_type,omitempty"` + // GooglePlaceID is the Google Places identifier of the venue. + // + // optional + GooglePlaceID string `json:"google_place_id,omitempty"` + // GooglePlaceType is the Google Places type of the venue. + // + // optional + GooglePlaceType string `json:"google_place_type,omitempty"` +} + +// Sticker type constants. +const ( + StickerTypeRegular = "regular" + StickerTypeMask = "mask" + StickerTypeCustomEmoji = "custom_emoji" +) + +// Sticker represents a sticker. +type Sticker struct { + // FileID is an identifier for this file, which can be used to download or + // reuse the file + FileID string `json:"file_id"` + // FileUniqueID is a unique identifier for this file, + // which is supposed to be the same over time and for different bots. + // Can't be used to download or reuse the file. + FileUniqueID string `json:"file_unique_id"` + // Type of the sticker, currently one of "regular", "mask", "custom_emoji". + // The type of the sticker is independent from its format, + // which is determined by the fields IsAnimated and IsVideo. + Type string `json:"type"` + // Width sticker width + Width int `json:"width"` + // Height sticker height + Height int `json:"height"` + // IsAnimated true, if the sticker is animated + // + // optional + IsAnimated bool `json:"is_animated,omitempty"` + // IsVideo true, if the sticker is a video sticker + // + // optional + IsVideo bool `json:"is_video,omitempty"` + // Thumbnail sticker thumbnail in the .WEBP or .JPG format + // + // optional + Thumbnail *PhotoSize `json:"thumbnail,omitempty"` + // Emoji associated with the sticker + // + // optional + Emoji string `json:"emoji,omitempty"` + // SetName of the sticker set to which the sticker belongs + // + // optional + SetName string `json:"set_name,omitempty"` + // PremiumAnimation for premium regular stickers, premium animation for the sticker + // + // optional + PremiumAnimation *File `json:"premium_animation,omitempty"` + // MaskPosition is for mask stickers, the position where the mask should be + // placed + // + // optional + MaskPosition *MaskPosition `json:"mask_position,omitempty"` + // CustomEmojiID for custom emoji stickers, unique identifier of the custom emoji + // + // optional + CustomEmojiID string `json:"custom_emoji_id,omitempty"` + // NeedsRepainting is true if the sticker must be repainted to a text color + // in messages, the color of the Telegram Premium badge in emoji status, + // white color on chat photos, or another appropriate color in other places. + // + // optional + NeedsRepainting bool `json:"needs_repainting,omitempty"` + // FileSize + // + // optional + FileSize int `json:"file_size,omitempty"` +} + +// BotName represents the bot's name. +type BotName struct { + Name string `json:"name"` +} + +// BotDescription represents the bot's description. +type BotDescription struct { + Description string `json:"description"` +} + +// Sticker format constants for the createNewStickerSet method +// and the uploadStickerFile method. +const ( + StickerFormatStatic = "static" + StickerFormatAnimated = "animated" + StickerFormatVideo = "video" +) + +// InputSticker describes a sticker to be added to a sticker set. +type InputSticker struct { + // Sticker is the file to upload. May be a file ID, an HTTP URL, or + // new file data via FilePath/FileBytes/FileReader. Animated and video + // stickers can't be uploaded via HTTP URL. + Sticker RequestFileData `json:"sticker"` + // Format of the added sticker. One of StickerFormatStatic, + // StickerFormatAnimated, StickerFormatVideo. + Format string `json:"format"` + // EmojiList is the list of 1-20 emoji associated with the sticker. + EmojiList []string `json:"emoji_list"` + // MaskPosition for "mask" stickers, the position where the mask should + // be placed on faces. + // + // optional + MaskPosition *MaskPosition `json:"mask_position,omitempty"` + // Keywords is the list of 0-20 search keywords for the sticker. + // + // optional + Keywords []string `json:"keywords,omitempty"` +} + +// BotShortDescription represents the bot's short description, shown on the +// bot's profile page and sent together with the link when users share the bot. +type BotShortDescription struct { + ShortDescription string `json:"short_description"` +} + +// StickerSet represents a sticker set. +type StickerSet struct { + // Name sticker set name + Name string `json:"name"` + // Title sticker set title + Title string `json:"title"` + // StickerType of stickers in the set, currently one of “regular”, “mask”, “custom_emoji” + StickerType string `json:"sticker_type"` + // Stickers list of all set stickers + Stickers []Sticker `json:"stickers"` + // Thumb is the sticker set thumbnail in the .WEBP or .TGS format + Thumbnail *PhotoSize `json:"thumbnail"` +} + +// MaskPosition describes the position on faces where a mask should be placed +// by default. +type MaskPosition struct { + // The part of the face relative to which the mask should be placed. + // One of “forehead”, “eyes”, “mouth”, or “chin”. + Point string `json:"point"` + // Shift by X-axis measured in widths of the mask scaled to the face size, + // from left to right. For example, choosing -1.0 will place mask just to + // the left of the default mask position. + XShift float64 `json:"x_shift"` + // Shift by Y-axis measured in heights of the mask scaled to the face size, + // from top to bottom. For example, 1.0 will place the mask just below the + // default mask position. + YShift float64 `json:"y_shift"` + // Mask scaling coefficient. For example, 2.0 means double size. + Scale float64 `json:"scale"` +} + +// Game represents a game. Use BotFather to create and edit games, their short +// names will act as unique identifiers. +type Game struct { + // Title of the game + Title string `json:"title"` + // Description of the game + Description string `json:"description"` + // Photo that will be displayed in the game message in chats. + Photo []PhotoSize `json:"photo"` + // Text a brief description of the game or high scores included in the game message. + // Can be automatically edited to include current high scores for the game + // when the bot calls setGameScore, or manually edited using editMessageText. 0-4096 characters. + // + // optional + Text string `json:"text,omitempty"` + // TextEntities special entities that appear in text, such as usernames, URLs, bot commands, etc. + // + // optional + TextEntities []MessageEntity `json:"text_entities,omitempty"` + // Animation is an animation that will be displayed in the game message in chats. + // Upload via BotFather (https://t.me/botfather). + // + // optional + Animation Animation `json:"animation,omitempty"` +} + +// GameHighScore is a user's score and position on the leaderboard. +type GameHighScore struct { + // Position in high score table for the game + Position int `json:"position"` + // User user + User User `json:"user"` + // Score score + Score int `json:"score"` +} + +// CallbackGame is for starting a game in an inline keyboard button. +type CallbackGame struct{} + +// WebhookInfo is information about a currently set webhook. +type WebhookInfo struct { + // URL webhook URL, may be empty if webhook is not set up. + URL string `json:"url"` + // HasCustomCertificate true, if a custom certificate was provided for webhook certificate checks. + HasCustomCertificate bool `json:"has_custom_certificate"` + // PendingUpdateCount number of updates awaiting delivery. + PendingUpdateCount int `json:"pending_update_count"` + // IPAddress is the currently used webhook IP address + // + // optional + IPAddress string `json:"ip_address,omitempty"` + // LastErrorDate unix time for the most recent error + // that happened when trying to deliver an update via webhook. + // + // optional + LastErrorDate int `json:"last_error_date,omitempty"` + // LastErrorMessage error message in human-readable format for the most recent error + // that happened when trying to deliver an update via webhook. + // + // optional + LastErrorMessage string `json:"last_error_message,omitempty"` + // LastSynchronizationErrorDate is the unix time of the most recent error that + // happened when trying to synchronize available updates with Telegram datacenters. + LastSynchronizationErrorDate int `json:"last_synchronization_error_date,omitempty"` + // MaxConnections maximum allowed number of simultaneous + // HTTPS connections to the webhook for update delivery. + // + // optional + MaxConnections int `json:"max_connections,omitempty"` + // AllowedUpdates is a list of update types the bot is subscribed to. + // Defaults to all update types + // + // optional + AllowedUpdates []string `json:"allowed_updates,omitempty"` +} + +// IsSet returns true if a webhook is currently set. +func (info WebhookInfo) IsSet() bool { + return info.URL != "" +} + +// InlineQuery is a Query from Telegram for an inline request. +type InlineQuery struct { + // ID unique identifier for this query + ID string `json:"id"` + // From sender + From *User `json:"from"` + // Query text of the query (up to 256 characters). + Query string `json:"query"` + // Offset of the results to be returned, can be controlled by the bot. + Offset string `json:"offset"` + // Type of the chat, from which the inline query was sent. Can be either + // “sender” for a private chat with the inline query sender, “private”, + // “group”, “supergroup”, or “channel”. The chat type should be always known + // for requests sent from official clients and most third-party clients, + // unless the request was sent from a secret chat + // + // optional + ChatType string `json:"chat_type,omitempty"` + // Location sender location, only for bots that request user location. + // + // optional + Location *Location `json:"location,omitempty"` +} + +// InlineQueryResultCachedAudio is an inline query response with cached audio. +type InlineQueryResultCachedAudio struct { + // Type of the result, must be audio + Type string `json:"type"` + // ID unique identifier for this result, 1-64 bytes + ID string `json:"id"` + // AudioID a valid file identifier for the audio file + AudioID string `json:"audio_file_id"` + // Caption 0-1024 characters after entities parsing + // + // optional + Caption string `json:"caption,omitempty"` + // ParseMode mode for parsing entities in the video caption. + // See formatting options for more details + // (https://core.telegram.org/bots/api#formatting-options). + // + // optional + ParseMode string `json:"parse_mode,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` + // ReplyMarkup inline keyboard attached to the message + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // InputMessageContent content of the message to be sent instead of the audio + // + // optional + InputMessageContent interface{} `json:"input_message_content,omitempty"` +} + +// InlineQueryResultCachedDocument is an inline query response with cached document. +type InlineQueryResultCachedDocument struct { + // Type of the result, must be a document + Type string `json:"type"` + // ID unique identifier for this result, 1-64 bytes + ID string `json:"id"` + // DocumentID a valid file identifier for the file + DocumentID string `json:"document_file_id"` + // Title for the result + // + // optional + Title string `json:"title,omitempty"` + // Caption of the document to be sent, 0-1024 characters after entities parsing + // + // optional + Caption string `json:"caption,omitempty"` + // Description short description of the result + // + // optional + Description string `json:"description,omitempty"` + // ParseMode mode for parsing entities in the video caption. + // // See formatting options for more details + // // (https://core.telegram.org/bots/api#formatting-options). + // + // optional + ParseMode string `json:"parse_mode,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` + // ReplyMarkup inline keyboard attached to the message + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // InputMessageContent content of the message to be sent instead of the file + // + // optional + InputMessageContent interface{} `json:"input_message_content,omitempty"` +} + +// InlineQueryResultCachedGIF is an inline query response with cached gif. +type InlineQueryResultCachedGIF struct { + // Type of the result, must be gif. + Type string `json:"type"` + // ID unique identifier for this result, 1-64 bytes. + ID string `json:"id"` + // GifID a valid file identifier for the GIF file. + GIFID string `json:"gif_file_id"` + // Title for the result + // + // optional + Title string `json:"title,omitempty"` + // Caption of the GIF file to be sent, 0-1024 characters after entities parsing. + // + // optional + Caption string `json:"caption,omitempty"` + // ParseMode mode for parsing entities in the caption. + // See formatting options for more details + // (https://core.telegram.org/bots/api#formatting-options). + // + // optional + ParseMode string `json:"parse_mode,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` + // ShowCaptionAboveMedia pass True if the caption must be shown above the + // message media. + // + // optional + ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"` + // ReplyMarkup inline keyboard attached to the message. + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // InputMessageContent content of the message to be sent instead of the GIF animation. + // + // optional + InputMessageContent interface{} `json:"input_message_content,omitempty"` +} + +// InlineQueryResultCachedMPEG4GIF is an inline query response with cached +// H.264/MPEG-4 AVC video without sound gif. +type InlineQueryResultCachedMPEG4GIF struct { + // Type of the result, must be mpeg4_gif + Type string `json:"type"` + // ID unique identifier for this result, 1-64 bytes + ID string `json:"id"` + // MPEG4FileID a valid file identifier for the MP4 file + MPEG4FileID string `json:"mpeg4_file_id"` + // Title for the result + // + // optional + Title string `json:"title,omitempty"` + // Caption of the MPEG-4 file to be sent, 0-1024 characters after entities parsing. + // + // optional + Caption string `json:"caption,omitempty"` + // ParseMode mode for parsing entities in the caption. + // See formatting options for more details + // (https://core.telegram.org/bots/api#formatting-options). + // + // optional + ParseMode string `json:"parse_mode,omitempty"` + // ParseMode mode for parsing entities in the video caption. + // See formatting options for more details + // (https://core.telegram.org/bots/api#formatting-options). + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` + // ShowCaptionAboveMedia pass True if the caption must be shown above the + // message media. + // + // optional + ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"` + // ReplyMarkup inline keyboard attached to the message. + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // InputMessageContent content of the message to be sent instead of the video animation. + // + // optional + InputMessageContent interface{} `json:"input_message_content,omitempty"` +} + +// InlineQueryResultCachedPhoto is an inline query response with cached photo. +type InlineQueryResultCachedPhoto struct { + // Type of the result, must be a photo. + Type string `json:"type"` + // ID unique identifier for this result, 1-64 bytes. + ID string `json:"id"` + // PhotoID a valid file identifier of the photo. + PhotoID string `json:"photo_file_id"` + // Title for the result. + // + // optional + Title string `json:"title,omitempty"` + // Description short description of the result. + // + // optional + Description string `json:"description,omitempty"` + // Caption of the photo to be sent, 0-1024 characters after entities parsing. + // + // optional + Caption string `json:"caption,omitempty"` + // ParseMode mode for parsing entities in the photo caption. + // See formatting options for more details + // (https://core.telegram.org/bots/api#formatting-options). + // + // optional + ParseMode string `json:"parse_mode,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` + // ShowCaptionAboveMedia pass True if the caption must be shown above the + // message media. + // + // optional + ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"` + // ReplyMarkup inline keyboard attached to the message. + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // InputMessageContent content of the message to be sent instead of the photo. + // + // optional + InputMessageContent interface{} `json:"input_message_content,omitempty"` +} + +// InlineQueryResultCachedSticker is an inline query response with cached sticker. +type InlineQueryResultCachedSticker struct { + // Type of the result, must be a sticker + Type string `json:"type"` + // ID unique identifier for this result, 1-64 bytes + ID string `json:"id"` + // StickerID a valid file identifier of the sticker + StickerID string `json:"sticker_file_id"` + // Title is not a part of the Bot API for cached sticker results and is + // ignored by Telegram. + // + // Deprecated: Telegram does not support this field. It is kept only for + // source compatibility and will be removed in a future release. + Title string `json:"title,omitempty"` + // ReplyMarkup inline keyboard attached to the message + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // InputMessageContent content of the message to be sent instead of the sticker + // + // optional + InputMessageContent interface{} `json:"input_message_content,omitempty"` +} + +// InlineQueryResultCachedVideo is an inline query response with cached video. +type InlineQueryResultCachedVideo struct { + // Type of the result, must be video + Type string `json:"type"` + // ID unique identifier for this result, 1-64 bytes + ID string `json:"id"` + // VideoID a valid file identifier for the video file + VideoID string `json:"video_file_id"` + // Title for the result + Title string `json:"title"` + // Description short description of the result + // + // optional + Description string `json:"description,omitempty"` + // Caption of the video to be sent, 0-1024 characters after entities parsing + // + // optional + Caption string `json:"caption,omitempty"` + // ParseMode mode for parsing entities in the video caption. + // See formatting options for more details + // (https://core.telegram.org/bots/api#formatting-options). + // + // optional + ParseMode string `json:"parse_mode,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` + // ShowCaptionAboveMedia pass True if the caption must be shown above the + // message media. + // + // optional + ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"` + // ReplyMarkup inline keyboard attached to the message + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // InputMessageContent content of the message to be sent instead of the video + // + // optional + InputMessageContent interface{} `json:"input_message_content,omitempty"` +} + +// InlineQueryResultCachedVoice is an inline query response with cached voice. +type InlineQueryResultCachedVoice struct { + // Type of the result, must be voice + Type string `json:"type"` + // ID unique identifier for this result, 1-64 bytes + ID string `json:"id"` + // VoiceID a valid file identifier for the voice message + VoiceID string `json:"voice_file_id"` + // Title voice message title + Title string `json:"title"` + // Caption 0-1024 characters after entities parsing + // + // optional + Caption string `json:"caption,omitempty"` + // ParseMode mode for parsing entities in the video caption. + // See formatting options for more details + // (https://core.telegram.org/bots/api#formatting-options). + // + // optional + ParseMode string `json:"parse_mode,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` + // ReplyMarkup inline keyboard attached to the message + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // InputMessageContent content of the message to be sent instead of the voice message + // + // optional + InputMessageContent interface{} `json:"input_message_content,omitempty"` +} + +// InlineQueryResultArticle represents a link to an article or web page. +type InlineQueryResultArticle struct { + // Type of the result, must be article. + Type string `json:"type"` + // ID unique identifier for this result, 1-64 Bytes. + ID string `json:"id"` + // Title of the result + Title string `json:"title"` + // InputMessageContent content of the message to be sent. + InputMessageContent interface{} `json:"input_message_content,omitempty"` + // ReplyMarkup Inline keyboard attached to the message. + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // URL of the result. + // + // optional + URL string `json:"url,omitempty"` + // Description short description of the result. + // + // optional + Description string `json:"description,omitempty"` + // ThumbURL url of the thumbnail for the result + // + // optional + ThumbnailURL string `json:"thumbnail_url,omitempty"` + // ThumbWidth thumbnail width + // + // optional + ThumbnailWidth int `json:"thumbnail_width,omitempty"` + // ThumbHeight thumbnail height + // + // optional + ThumbnailHeight int `json:"thumbnail_height,omitempty"` +} + +// InlineQueryResultAudio is an inline query response audio. +type InlineQueryResultAudio struct { + // Type of the result, must be audio + Type string `json:"type"` + // ID unique identifier for this result, 1-64 bytes + ID string `json:"id"` + // URL a valid url for the audio file + URL string `json:"audio_url"` + // Title is a title + Title string `json:"title"` + // Caption 0-1024 characters after entities parsing + // + // optional + Caption string `json:"caption,omitempty"` + // ParseMode mode for parsing entities in the video caption. + // See formatting options for more details + // (https://core.telegram.org/bots/api#formatting-options). + // + // optional + ParseMode string `json:"parse_mode,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` + // Performer is a performer + // + // optional + Performer string `json:"performer,omitempty"` + // Duration audio duration in seconds + // + // optional + Duration int `json:"audio_duration,omitempty"` + // ReplyMarkup inline keyboard attached to the message + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // InputMessageContent content of the message to be sent instead of the audio + // + // optional + InputMessageContent interface{} `json:"input_message_content,omitempty"` +} + +// InlineQueryResultContact is an inline query response contact. +type InlineQueryResultContact struct { + Type string `json:"type"` // required + ID string `json:"id"` // required + PhoneNumber string `json:"phone_number"` // required + FirstName string `json:"first_name"` // required + LastName string `json:"last_name"` + VCard string `json:"vcard"` + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + InputMessageContent interface{} `json:"input_message_content,omitempty"` + ThumbnailURL string `json:"thumbnail_url"` + ThumbnailWidth int `json:"thumbnail_width"` + ThumbnailHeight int `json:"thumbnail_height"` +} + +// InlineQueryResultGame is an inline query response game. +type InlineQueryResultGame struct { + // Type of the result, must be game + Type string `json:"type"` + // ID unique identifier for this result, 1-64 bytes + ID string `json:"id"` + // GameShortName short name of the game + GameShortName string `json:"game_short_name"` + // ReplyMarkup inline keyboard attached to the message + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` +} + +// InlineQueryResultDocument is an inline query response document. +type InlineQueryResultDocument struct { + // Type of the result, must be a document + Type string `json:"type"` + // ID unique identifier for this result, 1-64 bytes + ID string `json:"id"` + // Title for the result + Title string `json:"title"` + // Caption of the document to be sent, 0-1024 characters after entities parsing + // + // optional + Caption string `json:"caption,omitempty"` + // ParseMode mode for parsing entities in the document caption. + // See formatting options for more details + // (https://core.telegram.org/bots/api#formatting-options). + // + // optional + ParseMode string `json:"parse_mode,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` + // URL a valid url for the file + URL string `json:"document_url"` + // MimeType of the content of the file, either “application/pdf” or “application/zip” + MimeType string `json:"mime_type"` + // Description short description of the result + // + // optional + Description string `json:"description,omitempty"` + // ReplyMarkup inline keyboard attached to the message + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // InputMessageContent content of the message to be sent instead of the file + // + // optional + InputMessageContent interface{} `json:"input_message_content,omitempty"` + // ThumbURL url of the thumbnail (jpeg only) for the file + // + // optional + ThumbnailURL string `json:"thumbnail_url,omitempty"` + // ThumbWidth thumbnail width + // + // optional + ThumbnailWidth int `json:"thumbnail_width,omitempty"` + // ThumbHeight thumbnail height + // + // optional + ThumbnailHeight int `json:"thumbnail_height,omitempty"` +} + +// InlineQueryResultGIF is an inline query response GIF. +type InlineQueryResultGIF struct { + // Type of the result, must be gif. + Type string `json:"type"` + // ID unique identifier for this result, 1-64 bytes. + ID string `json:"id"` + // URL a valid URL for the GIF file. File size must not exceed 1MB. + URL string `json:"gif_url"` + // ThumbnailURL is the URL of the static (JPEG or GIF) or animated (MPEG4) + // thumbnail for the result. + ThumbnailURL string `json:"thumbnail_url"` + // ThumbnailMimeType is the MIME type of the thumbnail. Must be one of + // "image/jpeg", "image/gif", or "video/mp4". Defaults to "image/jpeg". + // + // optional + ThumbnailMimeType string `json:"thumbnail_mime_type,omitempty"` + // Width of the GIF + // + // optional + Width int `json:"gif_width,omitempty"` + // Height of the GIF + // + // optional + Height int `json:"gif_height,omitempty"` + // Duration of the GIF + // + // optional + Duration int `json:"gif_duration,omitempty"` + // Title for the result + // + // optional + Title string `json:"title,omitempty"` + // Caption of the GIF file to be sent, 0-1024 characters after entities parsing. + // + // optional + Caption string `json:"caption,omitempty"` + // ParseMode mode for parsing entities in the video caption. + // See formatting options for more details + // (https://core.telegram.org/bots/api#formatting-options). + // + // optional + ParseMode string `json:"parse_mode,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` + // ShowCaptionAboveMedia pass True if the caption must be shown above the + // message media. + // + // optional + ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"` + // ReplyMarkup inline keyboard attached to the message + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // InputMessageContent content of the message to be sent instead of the GIF animation. + // + // optional + InputMessageContent interface{} `json:"input_message_content,omitempty"` +} + +// InlineQueryResultLocation is an inline query response location. +type InlineQueryResultLocation struct { + // Type of the result, must be location + Type string `json:"type"` + // ID unique identifier for this result, 1-64 Bytes + ID string `json:"id"` + // Latitude of the location in degrees + Latitude float64 `json:"latitude"` + // Longitude of the location in degrees + Longitude float64 `json:"longitude"` + // Title of the location + Title string `json:"title"` + // HorizontalAccuracy is the radius of uncertainty for the location, + // measured in meters; 0-1500 + // + // optional + HorizontalAccuracy float64 `json:"horizontal_accuracy,omitempty"` + // LivePeriod is the period in seconds for which the location can be + // updated, should be between 60 and 86400. + // + // optional + LivePeriod int `json:"live_period,omitempty"` + // Heading is for live locations, a direction in which the user is moving, + // in degrees. Must be between 1 and 360 if specified. + // + // optional + Heading int `json:"heading,omitempty"` + // ProximityAlertRadius is for live locations, a maximum distance for + // proximity alerts about approaching another chat member, in meters. Must + // be between 1 and 100000 if specified. + // + // optional + ProximityAlertRadius int `json:"proximity_alert_radius,omitempty"` + // ReplyMarkup inline keyboard attached to the message + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // InputMessageContent content of the message to be sent instead of the location + // + // optional + InputMessageContent interface{} `json:"input_message_content,omitempty"` + // ThumbURL url of the thumbnail for the result + // + // optional + ThumbnailURL string `json:"thumbnail_url,omitempty"` + // ThumbWidth thumbnail width + // + // optional + ThumbnailWidth int `json:"thumbnail_width,omitempty"` + // ThumbHeight thumbnail height + // + // optional + ThumbnailHeight int `json:"thumbnail_height,omitempty"` +} + +// InlineQueryResultMPEG4GIF is an inline query response MPEG4 GIF. +type InlineQueryResultMPEG4GIF struct { + // Type of the result, must be mpeg4_gif + Type string `json:"type"` + // ID unique identifier for this result, 1-64 bytes + ID string `json:"id"` + // URL a valid URL for the MP4 file. File size must not exceed 1MB + URL string `json:"mpeg4_url"` + // Width video width + // + // optional + Width int `json:"mpeg4_width,omitempty"` + // Height vVideo height + // + // optional + Height int `json:"mpeg4_height,omitempty"` + // Duration video duration + // + // optional + Duration int `json:"mpeg4_duration,omitempty"` + // ThumbnailURL is the URL of the static (JPEG or GIF) or animated (MPEG4) + // thumbnail for the result. + ThumbnailURL string `json:"thumbnail_url"` + // ThumbnailMimeType is the MIME type of the thumbnail. Must be one of + // "image/jpeg", "image/gif", or "video/mp4". Defaults to "image/jpeg". + // + // optional + ThumbnailMimeType string `json:"thumbnail_mime_type,omitempty"` + // Title for the result + // + // optional + Title string `json:"title,omitempty"` + // Caption of the MPEG-4 file to be sent, 0-1024 characters after entities parsing. + // + // optional + Caption string `json:"caption,omitempty"` + // ParseMode mode for parsing entities in the video caption. + // See formatting options for more details + // (https://core.telegram.org/bots/api#formatting-options). + // + // optional + ParseMode string `json:"parse_mode,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` + // ShowCaptionAboveMedia pass True if the caption must be shown above the + // message media. + // + // optional + ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"` + // ReplyMarkup inline keyboard attached to the message + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // InputMessageContent content of the message to be sent instead of the video animation + // + // optional + InputMessageContent interface{} `json:"input_message_content,omitempty"` +} + +// InlineQueryResultPhoto is an inline query response photo. +type InlineQueryResultPhoto struct { + // Type of the result, must be article. + Type string `json:"type"` + // ID unique identifier for this result, 1-64 Bytes. + ID string `json:"id"` + // URL a valid URL of the photo. Photo must be in jpeg format. + // Photo size must not exceed 5MB. + URL string `json:"photo_url"` + // MimeType is not a part of the Bot API for photo results and is ignored + // by Telegram. + // + // Deprecated: Telegram does not support this field. It is kept only for + // source compatibility and will be removed in a future release. + MimeType string `json:"mime_type,omitempty"` + // Width of the photo + // + // optional + Width int `json:"photo_width,omitempty"` + // Height of the photo + // + // optional + Height int `json:"photo_height,omitempty"` + // ThumbURL url of the thumbnail for the photo. + // + // optional + ThumbnailURL string `json:"thumbnail_url,omitempty"` + // Title for the result + // + // optional + Title string `json:"title,omitempty"` + // Description short description of the result + // + // optional + Description string `json:"description,omitempty"` + // Caption of the photo to be sent, 0-1024 characters after entities parsing. + // + // optional + Caption string `json:"caption,omitempty"` + // ParseMode mode for parsing entities in the photo caption. + // See formatting options for more details + // (https://core.telegram.org/bots/api#formatting-options). + // + // optional + ParseMode string `json:"parse_mode,omitempty"` + // ReplyMarkup inline keyboard attached to the message. + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` + // ShowCaptionAboveMedia pass True if the caption must be shown above the + // message media. + // + // optional + ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"` + // InputMessageContent content of the message to be sent instead of the photo. + // + // optional + InputMessageContent interface{} `json:"input_message_content,omitempty"` +} + +// InlineQueryResultVenue is an inline query response venue. +type InlineQueryResultVenue struct { + // Type of the result, must be venue + Type string `json:"type"` + // ID unique identifier for this result, 1-64 Bytes + ID string `json:"id"` + // Latitude of the venue location in degrees + Latitude float64 `json:"latitude"` + // Longitude of the venue location in degrees + Longitude float64 `json:"longitude"` + // Title of the venue + Title string `json:"title"` + // Address of the venue + Address string `json:"address"` + // FoursquareID foursquare identifier of the venue if known + // + // optional + FoursquareID string `json:"foursquare_id,omitempty"` + // FoursquareType foursquare type of the venue, if known. + // (For example, “arts_entertainment/default”, “arts_entertainment/aquarium” or “food/icecream”.) + // + // optional + FoursquareType string `json:"foursquare_type,omitempty"` + // GooglePlaceID is the Google Places identifier of the venue + // + // optional + GooglePlaceID string `json:"google_place_id,omitempty"` + // GooglePlaceType is the Google Places type of the venue + // + // optional + GooglePlaceType string `json:"google_place_type,omitempty"` + // ReplyMarkup inline keyboard attached to the message + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // InputMessageContent content of the message to be sent instead of the venue + // + // optional + InputMessageContent interface{} `json:"input_message_content,omitempty"` + // ThumbURL url of the thumbnail for the result + // + // optional + ThumbnailURL string `json:"thumbnail_url,omitempty"` + // ThumbWidth thumbnail width + // + // optional + ThumbnailWidth int `json:"thumbnail_width,omitempty"` + // ThumbHeight thumbnail height + // + // optional + ThumbnailHeight int `json:"thumbnail_height,omitempty"` +} + +// InlineQueryResultVideo is an inline query response video. +type InlineQueryResultVideo struct { + // Type of the result, must be video + Type string `json:"type"` + // ID unique identifier for this result, 1-64 bytes + ID string `json:"id"` + // URL a valid url for the embedded video player or video file + URL string `json:"video_url"` + // MimeType of the content of video url, “text/html” or “video/mp4” + MimeType string `json:"mime_type"` + // + // ThumbURL url of the thumbnail (jpeg only) for the video + // optional + ThumbnailURL string `json:"thumbnail_url,omitempty"` + // Title for the result + Title string `json:"title"` + // Caption of the video to be sent, 0-1024 characters after entities parsing + // + // optional + Caption string `json:"caption,omitempty"` + // ParseMode mode for parsing entities in the video caption. + // See formatting options for more details + // (https://core.telegram.org/bots/api#formatting-options). + // + // optional + ParseMode string `json:"parse_mode,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` + // ShowCaptionAboveMedia pass True if the caption must be shown above the + // message media. + // + // optional + ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"` + // Width video width + // + // optional + Width int `json:"video_width,omitempty"` + // Height video height + // + // optional + Height int `json:"video_height,omitempty"` + // Duration video duration in seconds + // + // optional + Duration int `json:"video_duration,omitempty"` + // Description short description of the result + // + // optional + Description string `json:"description,omitempty"` + // ReplyMarkup inline keyboard attached to the message + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // InputMessageContent content of the message to be sent instead of the video. + // This field is required if InlineQueryResultVideo is used to send + // an HTML-page as a result (e.g., a YouTube video). + // + // optional + InputMessageContent interface{} `json:"input_message_content,omitempty"` +} + +// InlineQueryResultVoice is an inline query response voice. +type InlineQueryResultVoice struct { + // Type of the result, must be voice + Type string `json:"type"` + // ID unique identifier for this result, 1-64 bytes + ID string `json:"id"` + // URL a valid URL for the voice recording + URL string `json:"voice_url"` + // Title recording title + Title string `json:"title"` + // Caption 0-1024 characters after entities parsing + // + // optional + Caption string `json:"caption,omitempty"` + // ParseMode mode for parsing entities in the video caption. + // See formatting options for more details + // (https://core.telegram.org/bots/api#formatting-options). + // + // optional + ParseMode string `json:"parse_mode,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` + // Duration recording duration in seconds + // + // optional + Duration int `json:"voice_duration,omitempty"` + // ReplyMarkup inline keyboard attached to the message + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // InputMessageContent content of the message to be sent instead of the voice recording + // + // optional + InputMessageContent interface{} `json:"input_message_content,omitempty"` +} + +// ChosenInlineResult is an inline query result chosen by a User +type ChosenInlineResult struct { + // ResultID the unique identifier for the result that was chosen + ResultID string `json:"result_id"` + // From the user that chose the result + From *User `json:"from"` + // Location sender location, only for bots that require user location + // + // optional + Location *Location `json:"location,omitempty"` + // InlineMessageID identifier of the sent inline message. + // Available only if there is an inline keyboard attached to the message. + // Will be also received in callback queries and can be used to edit the message. + // + // optional + InlineMessageID string `json:"inline_message_id,omitempty"` + // Query the query that was used to obtain the result + Query string `json:"query"` +} + +// SentWebAppMessage contains information about an inline message sent by a Web App +// on behalf of a user. +type SentWebAppMessage struct { + // Identifier of the sent inline message. Available only if there is an inline + // keyboard attached to the message. + // + // optional + InlineMessageID string `json:"inline_message_id,omitempty"` +} + +// SentGuestMessage describes an inline message sent by a guest bot in +// response to a guest query. +type SentGuestMessage struct { + // InlineMessageID is the identifier of the sent inline message. + InlineMessageID string `json:"inline_message_id"` +} + +// BotAccessSettings describes the access settings of a managed bot. +type BotAccessSettings struct { + // IsAccessRestricted is true, if only selected users can access the + // bot. The bot's owner can always access it. + IsAccessRestricted bool `json:"is_access_restricted"` + // AddedUsers is the list of other users who have access to the bot if + // the access is restricted. + // + // optional + AddedUsers []User `json:"added_users,omitempty"` +} + +// InputTextMessageContent contains text for displaying +// as an inline query result. +type InputTextMessageContent struct { + // Text of the message to be sent, 1-4096 characters + Text string `json:"message_text"` + // ParseMode mode for parsing entities in the message text. + // See formatting options for more details + // (https://core.telegram.org/bots/api#formatting-options). + // + // optional + ParseMode string `json:"parse_mode,omitempty"` + // Entities is a list of special entities that appear in message text, which + // can be specified instead of parse_mode + // + // optional + Entities []MessageEntity `json:"entities,omitempty"` + // LinkPreviewOptions are options used for link preview generation for the + // message. + // + // optional + LinkPreviewOptions *LinkPreviewOptions `json:"link_preview_options,omitempty"` +} + +// InputLocationMessageContent contains a location for displaying +// as an inline query result. +type InputLocationMessageContent struct { + // Latitude of the location in degrees + Latitude float64 `json:"latitude"` + // Longitude of the location in degrees + Longitude float64 `json:"longitude"` + // HorizontalAccuracy is the radius of uncertainty for the location, + // measured in meters; 0-1500 + // + // optional + HorizontalAccuracy float64 `json:"horizontal_accuracy,omitempty"` + // LivePeriod is the period in seconds for which the location can be + // updated, should be between 60 and 86400 + // + // optional + LivePeriod int `json:"live_period,omitempty"` + // Heading is for live locations, a direction in which the user is moving, + // in degrees. Must be between 1 and 360 if specified. + // + // optional + Heading int `json:"heading,omitempty"` + // ProximityAlertRadius is for live locations, a maximum distance for + // proximity alerts about approaching another chat member, in meters. Must + // be between 1 and 100000 if specified. + // + // optional + ProximityAlertRadius int `json:"proximity_alert_radius,omitempty"` +} + +// InputVenueMessageContent contains a venue for displaying +// as an inline query result. +type InputVenueMessageContent struct { + // Latitude of the venue in degrees + Latitude float64 `json:"latitude"` + // Longitude of the venue in degrees + Longitude float64 `json:"longitude"` + // Title name of the venue + Title string `json:"title"` + // Address of the venue + Address string `json:"address"` + // FoursquareID foursquare identifier of the venue, if known + // + // optional + FoursquareID string `json:"foursquare_id,omitempty"` + // FoursquareType Foursquare type of the venue, if known + // + // optional + FoursquareType string `json:"foursquare_type,omitempty"` + // GooglePlaceID is the Google Places identifier of the venue + // + // optional + GooglePlaceID string `json:"google_place_id,omitempty"` + // GooglePlaceType is the Google Places type of the venue + // + // optional + GooglePlaceType string `json:"google_place_type,omitempty"` +} + +// InputContactMessageContent contains a contact for displaying +// as an inline query result. +type InputContactMessageContent struct { + // PhoneNumber contact's phone number + PhoneNumber string `json:"phone_number"` + // FirstName contact's first name + FirstName string `json:"first_name"` + // LastName contact's last name + // + // optional + LastName string `json:"last_name,omitempty"` + // Additional data about the contact in the form of a vCard + // + // optional + VCard string `json:"vcard,omitempty"` +} + +// InputInvoiceMessageContent represents the content of an invoice message to be +// sent as the result of an inline query. +type InputInvoiceMessageContent struct { + // Product name, 1-32 characters + Title string `json:"title"` + // Product description, 1-255 characters + Description string `json:"description"` + // Bot-defined invoice payload, 1-128 bytes. This will not be displayed to + // the user, use for your internal processes. + Payload string `json:"payload"` + // Payment provider token, obtained via Botfather. Omit for payments in + // Telegram Stars. + // + // optional + ProviderToken string `json:"provider_token,omitempty"` + // Three-letter ISO 4217 currency code + Currency string `json:"currency"` + // Price breakdown, a JSON-serialized list of components (e.g. product + // price, tax, discount, delivery cost, delivery tax, bonus, etc.) + Prices []LabeledPrice `json:"prices"` + // The maximum accepted amount for tips in the smallest units of the + // currency (integer, not float/double). + // + // optional + MaxTipAmount int `json:"max_tip_amount,omitempty"` + // An array of suggested amounts of tip in the smallest units of the + // currency (integer, not float/double). At most 4 suggested tip amounts can + // be specified. The suggested tip amounts must be positive, passed in a + // strictly increased order and must not exceed max_tip_amount. + // + // optional + SuggestedTipAmounts []int `json:"suggested_tip_amounts,omitempty"` + // A JSON-serialized object for data about the invoice, which will be shared + // with the payment provider. A detailed description of the required fields + // should be provided by the payment provider. + // + // optional + ProviderData string `json:"provider_data,omitempty"` + // URL of the product photo for the invoice. Can be a photo of the goods or + // a marketing image for a service. People like it better when they see what + // they are paying for. + // + // optional + PhotoURL string `json:"photo_url,omitempty"` + // Photo size + // + // optional + PhotoSize int `json:"photo_size,omitempty"` + // Photo width + // + // optional + PhotoWidth int `json:"photo_width,omitempty"` + // Photo height + // + // optional + PhotoHeight int `json:"photo_height,omitempty"` + // Pass True, if you require the user's full name to complete the order + // + // optional + NeedName bool `json:"need_name,omitempty"` + // Pass True, if you require the user's phone number to complete the order + // + // optional + NeedPhoneNumber bool `json:"need_phone_number,omitempty"` + // Pass True, if you require the user's email address to complete the order + // + // optional + NeedEmail bool `json:"need_email,omitempty"` + // Pass True, if you require the user's shipping address to complete the order + // + // optional + NeedShippingAddress bool `json:"need_shipping_address,omitempty"` + // Pass True, if user's phone number should be sent to provider + // + // optional + SendPhoneNumberToProvider bool `json:"send_phone_number_to_provider,omitempty"` + // Pass True, if user's email address should be sent to provider + // + // optional + SendEmailToProvider bool `json:"send_email_to_provider,omitempty"` + // Pass True, if the final price depends on the shipping method + // + // optional + IsFlexible bool `json:"is_flexible,omitempty"` +} + +// LabeledPrice represents a portion of the price for goods or services. +type LabeledPrice struct { + // Label portion label + Label string `json:"label"` + // Amount price of the product in the smallest units of the currency (integer, not float/double). + // For example, for a price of US$ 1.45 pass amount = 145. + // See the exp parameter in currencies.json + // (https://core.telegram.org/bots/payments/currencies.json), + // it shows the number of digits past the decimal point + // for each currency (2 for the majority of currencies). + Amount int `json:"amount"` +} + +// Invoice contains basic information about an invoice. +type Invoice struct { + // Title product name + Title string `json:"title"` + // Description product description + Description string `json:"description"` + // StartParameter unique bot deep-linking parameter that can be used to generate this invoice + StartParameter string `json:"start_parameter"` + // Currency three-letter ISO 4217 currency code + // (see https://core.telegram.org/bots/payments#supported-currencies) + Currency string `json:"currency"` + // TotalAmount total price in the smallest units of the currency (integer, not float/double). + // For example, for a price of US$ 1.45 pass amount = 145. + // See the exp parameter in currencies.json + // (https://core.telegram.org/bots/payments/currencies.json), + // it shows the number of digits past the decimal point + // for each currency (2 for the majority of currencies). + TotalAmount int `json:"total_amount"` +} + +// ShippingAddress represents a shipping address. +type ShippingAddress struct { + // CountryCode ISO 3166-1 alpha-2 country code + CountryCode string `json:"country_code"` + // State if applicable + State string `json:"state"` + // City city + City string `json:"city"` + // StreetLine1 first line for the address + StreetLine1 string `json:"street_line1"` + // StreetLine2 second line for the address + StreetLine2 string `json:"street_line2"` + // PostCode address post code + PostCode string `json:"post_code"` +} + +// OrderInfo represents information about an order. +type OrderInfo struct { + // Name user name + // + // optional + Name string `json:"name,omitempty"` + // PhoneNumber user's phone number + // + // optional + PhoneNumber string `json:"phone_number,omitempty"` + // Email user email + // + // optional + Email string `json:"email,omitempty"` + // ShippingAddress user shipping address + // + // optional + ShippingAddress *ShippingAddress `json:"shipping_address,omitempty"` +} + +// ShippingOption represents one shipping option. +type ShippingOption struct { + // ID shipping option identifier + ID string `json:"id"` + // Title option title + Title string `json:"title"` + // Prices list of price portions + Prices []LabeledPrice `json:"prices"` +} + +// SuccessfulPayment contains basic information about a successful payment. +type SuccessfulPayment struct { + // Currency three-letter ISO 4217 currency code + // (see https://core.telegram.org/bots/payments#supported-currencies) + Currency string `json:"currency"` + // TotalAmount total price in the smallest units of the currency (integer, not float/double). + // For example, for a price of US$ 1.45 pass amount = 145. + // See the exp parameter in currencies.json, + // (https://core.telegram.org/bots/payments/currencies.json) + // it shows the number of digits past the decimal point + // for each currency (2 for the majority of currencies). + TotalAmount int `json:"total_amount"` + // InvoicePayload bot specified invoice payload + InvoicePayload string `json:"invoice_payload"` + // SubscriptionExpirationDate is the expiration date of the subscription, + // in Unix time. For recurring payments only. + // + // optional + SubscriptionExpirationDate int `json:"subscription_expiration_date,omitempty"` + // IsRecurring is true, if the payment is a recurring payment for a + // subscription. + // + // optional + IsRecurring bool `json:"is_recurring,omitempty"` + // IsFirstRecurring is true, if the payment is the first payment for a + // subscription. + // + // optional + IsFirstRecurring bool `json:"is_first_recurring,omitempty"` + // ShippingOptionID identifier of the shipping option chosen by the user + // + // optional + ShippingOptionID string `json:"shipping_option_id,omitempty"` + // OrderInfo order info provided by the user + // + // optional + OrderInfo *OrderInfo `json:"order_info,omitempty"` + // TelegramPaymentChargeID telegram payment identifier + TelegramPaymentChargeID string `json:"telegram_payment_charge_id"` + // ProviderPaymentChargeID provider payment identifier + ProviderPaymentChargeID string `json:"provider_payment_charge_id"` +} + +// RefundedPayment contains information about a refunded payment. +type RefundedPayment struct { + // Currency is the three-letter ISO 4217 currency code, or "XTR" for + // payments in Telegram Stars. Currently, always "XTR". + Currency string `json:"currency"` + // TotalAmount is the total refunded price in the smallest units of the + // currency (integer, not float/double). For example, for a price of + // US$ 1.45, total_amount == 145. + TotalAmount int `json:"total_amount"` + // InvoicePayload is the bot-specified invoice payload. + InvoicePayload string `json:"invoice_payload"` + // TelegramPaymentChargeID is the Telegram payment identifier. + TelegramPaymentChargeID string `json:"telegram_payment_charge_id"` + // ProviderPaymentChargeID is the provider payment identifier. + // + // optional + ProviderPaymentChargeID string `json:"provider_payment_charge_id,omitempty"` +} + +// ShippingQuery contains information about an incoming shipping query. +type ShippingQuery struct { + // ID unique query identifier + ID string `json:"id"` + // From user who sent the query + From *User `json:"from"` + // InvoicePayload bot specified invoice payload + InvoicePayload string `json:"invoice_payload"` + // ShippingAddress user specified shipping address + ShippingAddress *ShippingAddress `json:"shipping_address"` +} + +// PreCheckoutQuery contains information about an incoming pre-checkout query. +type PreCheckoutQuery struct { + // ID unique query identifier + ID string `json:"id"` + // From user who sent the query + From *User `json:"from"` + // Currency three-letter ISO 4217 currency code + // // (see https://core.telegram.org/bots/payments#supported-currencies) + Currency string `json:"currency"` + // TotalAmount total price in the smallest units of the currency (integer, not float/double). + // // For example, for a price of US$ 1.45 pass amount = 145. + // // See the exp parameter in currencies.json, + // // (https://core.telegram.org/bots/payments/currencies.json) + // // it shows the number of digits past the decimal point + // // for each currency (2 for the majority of currencies). + TotalAmount int `json:"total_amount"` + // InvoicePayload bot specified invoice payload + InvoicePayload string `json:"invoice_payload"` + // ShippingOptionID identifier of the shipping option chosen by the user + // + // optional + ShippingOptionID string `json:"shipping_option_id,omitempty"` + // OrderInfo order info provided by the user + // + // optional + OrderInfo *OrderInfo `json:"order_info,omitempty"` +} + +// =========================================================================== +// Bot API 10.1 — Links and Rich Messages +// =========================================================================== + +// Link represents an HTTP link. +type Link struct { + // URL of the link. + URL string `json:"url"` +} + +// InputMediaLinkType is the discriminator value for InputMediaLink. +const InputMediaLinkType = "link" + +// InputMediaLink represents an HTTP link to be sent. It is one of the +// InputMedia* variants accepted by InputPollOption.Media. +type InputMediaLink struct { + // Type of the media, must be "link". + Type string `json:"type"` + // URL is the HTTP URL of the link. + URL string `json:"url"` +} + +// InputRichMessage describes a rich message to be sent. Exactly one of HTML, +// Markdown, or Blocks must be set. See the Telegram "rich message formatting +// options" documentation for the supported markup. +type InputRichMessage struct { + // Blocks is the content of the rich message described as a list of + // blocks. Exactly one of HTML, Markdown, or Blocks must be set. + // + // optional + Blocks []InputRichBlock `json:"blocks,omitempty"` + // HTML is the content of the rich message described using HTML + // formatting. Exactly one of HTML, Markdown, or Blocks must be set. Use + // Media to specify the media used in the message. + // + // optional + HTML string `json:"html,omitempty"` + // Markdown is the content of the rich message described using Markdown + // formatting. Exactly one of HTML, Markdown, or Blocks must be set. Use + // Media to specify the media used in the message. + // + // optional + Markdown string `json:"markdown,omitempty"` + // Media is the list of media referenced from the Markdown or HTML fields + // using tg://photo?id=, tg://video?id=, and tg://audio?id= links. + // + // optional + Media []InputRichMessageMedia `json:"media,omitempty"` + // IsRTL, if true, requests that the rich message be shown right-to-left. + // + // optional + IsRTL bool `json:"is_rtl,omitempty"` + // SkipEntityDetection, if true, skips automatic detection of entities + // (URLs, email addresses, username mentions, hashtags, cashtags, bot + // commands, or phone numbers) in the text. + // + // optional + SkipEntityDetection bool `json:"skip_entity_detection,omitempty"` +} + +// InputRichMessageContent represents the content of a rich message to be sent +// as the result of an inline query. +type InputRichMessageContent struct { + // RichMessage is the message to be sent. + RichMessage InputRichMessage `json:"rich_message"` +} + +// RichMessage represents a received rich formatted message, exposed via +// Message.RichMessage. +type RichMessage struct { + // Blocks is the content of the message. + Blocks []RichBlock `json:"blocks"` + // IsRTL is true if the rich message must be shown right-to-left. + // + // optional + IsRTL bool `json:"is_rtl,omitempty"` +} + +// RichText styled-span (object form) discriminators. +const ( + RichTextTypeBold = "bold" + RichTextTypeItalic = "italic" + RichTextTypeUnderline = "underline" + RichTextTypeStrikethrough = "strikethrough" + RichTextTypeSpoiler = "spoiler" + RichTextTypeDateTime = "date_time" + RichTextTypeTextMention = "text_mention" + RichTextTypeSubscript = "subscript" + RichTextTypeSuperscript = "superscript" + RichTextTypeMarked = "marked" + RichTextTypeCode = "code" + RichTextTypeCustomEmoji = "custom_emoji" + RichTextTypeMathematicalExpression = "mathematical_expression" + RichTextTypeURL = "url" + RichTextTypeEmailAddress = "email_address" + RichTextTypePhoneNumber = "phone_number" + RichTextTypeBankCardNumber = "bank_card_number" + RichTextTypeMention = "mention" + RichTextTypeHashtag = "hashtag" + RichTextTypeCashtag = "cashtag" + RichTextTypeBotCommand = "bot_command" + RichTextTypeButton = "button" + RichTextTypeAnchor = "anchor" + RichTextTypeAnchorLink = "anchor_link" + RichTextTypeReference = "reference" + RichTextTypeReferenceLink = "reference_link" +) + +// RichText represents a rich formatted text. It is polymorphic: on the wire it +// is one of +// - a plain string (PlainText, with IsPlain set true), +// - an array of RichText (Parts), or +// - a styled span identified by Type, with nested Text and type-specific +// fields. +// +// Use the field that matches the form of the value; RichText marshals back to +// whichever form is populated. +type RichText struct { + // PlainText holds the value when the rich text is a bare string. + // + // optional + PlainText string `json:"-"` + // IsPlain reports whether the value was a bare string, so that an empty + // PlainText can be distinguished from an unset value. + // + // optional + IsPlain bool `json:"-"` + // Parts holds the value when the rich text is an array of RichText. + // + // optional + Parts []RichText `json:"-"` + + // Type is the styled-span discriminator (one of the RichTextType* + // constants) when the value is an object; empty for the string and array + // forms. + // + // optional + Type string `json:"type,omitempty"` + // Text is the nested content of the styled span. Present for most span + // types; absent for "anchor". + // + // optional + Text *RichText `json:"text,omitempty"` + // UnixTime is the Unix time associated with a "date_time" span. + // + // optional + UnixTime int64 `json:"unix_time,omitempty"` + // DateTimeFormat defines the formatting of a "date_time" span. + // + // optional + DateTimeFormat string `json:"date_time_format,omitempty"` + // User is the mentioned user of a "text_mention" span. + // + // optional + User *User `json:"user,omitempty"` + // CustomEmojiID is the identifier of the custom emoji of a "custom_emoji" + // span. + // + // optional + CustomEmojiID string `json:"custom_emoji_id,omitempty"` + // AlternativeText is the fallback emoji of a "custom_emoji" span. + // + // optional + AlternativeText string `json:"alternative_text,omitempty"` + // Expression is the LaTeX expression of a "mathematical_expression" span. + // + // optional + Expression string `json:"expression,omitempty"` + // URL is the link target of a "url" span. + // + // optional + URL string `json:"url,omitempty"` + // EmailAddress is the email address of an "email_address" span. + // + // optional + EmailAddress string `json:"email_address,omitempty"` + // PhoneNumber is the phone number of a "phone_number" span. + // + // optional + PhoneNumber string `json:"phone_number,omitempty"` + // BankCardNumber is the bank card number of a "bank_card_number" span. + // + // optional + BankCardNumber string `json:"bank_card_number,omitempty"` + // Username is the username of a "mention" span. + // + // optional + Username string `json:"username,omitempty"` + // Hashtag is the hashtag of a "hashtag" span. + // + // optional + Hashtag string `json:"hashtag,omitempty"` + // Cashtag is the cashtag of a "cashtag" span. + // + // optional + Cashtag string `json:"cashtag,omitempty"` + // BotCommand is the bot command of a "bot_command" span. + // + // optional + BotCommand string `json:"bot_command,omitempty"` + // Name is the anchor name of an "anchor" span or the reference name of a + // "reference" span. + // + // optional + Name string `json:"name,omitempty"` + // AnchorName is the target anchor name of an "anchor_link" span. If empty, + // the link points back to the top of the message. + // + // optional + AnchorName string `json:"anchor_name,omitempty"` + // ReferenceName is the target reference name of a "reference_link" span. + // + // optional + ReferenceName string `json:"reference_name,omitempty"` + // Button is the button of a "button" span. + // + // optional + Button *RichMessageButton `json:"button,omitempty"` + + // Raw preserves the original JSON of the object form for forward + // compatibility with span types not yet modeled. + // + // optional + Raw json.RawMessage `json:"-"` +} + +// UnmarshalJSON decodes a RichText from its string, array, or object form. +func (t *RichText) UnmarshalJSON(data []byte) error { + *t = RichText{} + + i := 0 + for i < len(data) && (data[i] == ' ' || data[i] == '\t' || data[i] == '\n' || data[i] == '\r') { + i++ + } + if i >= len(data) { + return nil + } + + switch data[i] { + case 'n': // null + return nil + case '"': + t.IsPlain = true + return json.Unmarshal(data, &t.PlainText) + case '[': + return json.Unmarshal(data, &t.Parts) + default: + type span RichText + if err := json.Unmarshal(data, (*span)(t)); err != nil { + return err + } + t.Raw = append(t.Raw[:0], data...) + return nil + } +} + +// MarshalJSON encodes a RichText in whichever form is populated. +func (t RichText) MarshalJSON() ([]byte, error) { + switch { + case t.IsPlain: + return json.Marshal(t.PlainText) + case t.Parts != nil: + return json.Marshal(t.Parts) + case t.Type != "": + type span RichText + return json.Marshal(span(t)) + case len(t.Raw) > 0: + return t.Raw, nil + default: + return []byte("null"), nil + } +} + +// RichBlock block-type discriminators. +const ( + RichBlockTypeParagraph = "paragraph" + RichBlockTypeHeading = "heading" + RichBlockTypePre = "pre" + RichBlockTypeFooter = "footer" + RichBlockTypeDivider = "divider" + RichBlockTypeMathematicalExpression = "mathematical_expression" + RichBlockTypeAnchor = "anchor" + RichBlockTypeList = "list" + RichBlockTypeBlockquote = "blockquote" + RichBlockTypeExpandableBlockquote = "expandable_blockquote" + RichBlockTypePullquote = "pullquote" + RichBlockTypeCollage = "collage" + RichBlockTypeSlideshow = "slideshow" + RichBlockTypeTable = "table" + RichBlockTypeDetails = "details" + RichBlockTypeMap = "map" + RichBlockTypeButtons = "buttons" + RichBlockTypeAnimation = "animation" + RichBlockTypeAudio = "audio" + RichBlockTypeDocument = "document" + RichBlockTypePhoto = "photo" + RichBlockTypeVideo = "video" + RichBlockTypeVoiceNote = "voice_note" + RichBlockTypeThinking = "thinking" +) + +// RichBlock represents a block in a rich formatted message. It is a flat +// polymorphic type keyed by Type; only the fields relevant to a given Type are +// set. The "caption" wire field is split into Caption (for media blocks) and +// TableCaption (for the "table" block), which have different shapes; at most +// one is ever set. +type RichBlock struct { + // Type of the block, one of the RichBlockType* constants. + Type string `json:"type"` + // Text is the block text. Set for "paragraph", "heading", "pre", + // "footer", "expandable_blockquote", "pullquote", and "thinking" blocks. + // + // optional + Text *RichText `json:"text,omitempty"` + // Size is the relative font size of a "heading" block; 1-6, 1 is largest. + // + // optional + Size int `json:"size,omitempty"` + // Language is the programming language of a "pre" block. + // + // optional + Language string `json:"language,omitempty"` + // Expression is the LaTeX expression of a "mathematical_expression" + // block. + // + // optional + Expression string `json:"expression,omitempty"` + // Name is the anchor name of an "anchor" block. + // + // optional + Name string `json:"name,omitempty"` + // Items are the items of a "list" block. + // + // optional + Items []RichBlockListItem `json:"items,omitempty"` + // Blocks is the nested content of "blockquote", "collage", "slideshow", + // and "details" blocks. + // + // optional + Blocks []RichBlock `json:"blocks,omitempty"` + // Credit is the credit of "blockquote", "expandable_blockquote", and + // "pullquote" blocks. + // + // optional + Credit *RichText `json:"credit,omitempty"` + // Cells are the rows of cells of a "table" block. + // + // optional + Cells [][]RichBlockTableCell `json:"cells,omitempty"` + // IsBordered is true if a "table" block has borders. + // + // optional + IsBordered bool `json:"is_bordered,omitempty"` + // IsStriped is true if a "table" block is striped. + // + // optional + IsStriped bool `json:"is_striped,omitempty"` + // IsCompact is true if the cells of a "table" block have smaller indents. + // + // optional + IsCompact bool `json:"is_compact,omitempty"` + // Buttons are the buttons of a "buttons" block, shown in one row. + // + // optional + Buttons []RichMessageButton `json:"buttons,omitempty"` + // Align is the horizontal alignment of the buttons of a "buttons" block; + // one of "left", "center", or "right". + // + // optional + Align string `json:"align,omitempty"` + // Summary is the always-shown summary of a "details" block. + // + // optional + Summary *RichText `json:"summary,omitempty"` + // IsOpen is true if a "details" block is visible by default. + // + // optional + IsOpen bool `json:"is_open,omitempty"` + // Location is the center of a "map" block. + // + // optional + Location *Location `json:"location,omitempty"` + // Zoom is the zoom level of a "map" block; 13-20. + // + // optional + Zoom int `json:"zoom,omitempty"` + // Width is the expected width of a "map" block. + // + // optional + Width int `json:"width,omitempty"` + // Height is the expected height of a "map" block. + // + // optional + Height int `json:"height,omitempty"` + // Animation is the animation of an "animation" block. + // + // optional + Animation *Animation `json:"animation,omitempty"` + // HasSpoiler is true if the preview of an "animation", "photo", or + // "video" block is covered by a spoiler. + // + // optional + HasSpoiler bool `json:"has_spoiler,omitempty"` + // Audio is the audio of an "audio" block. + // + // optional + Audio *Audio `json:"audio,omitempty"` + // Document is the general file of a "document" block. + // + // optional + Document *Document `json:"document,omitempty"` + // Photo are the available sizes of a "photo" block. + // + // optional + Photo []PhotoSize `json:"photo,omitempty"` + // Video is the video of a "video" block. + // + // optional + Video *Video `json:"video,omitempty"` + // VoiceNote is the voice note of a "voice_note" block. + // + // optional + VoiceNote *Voice `json:"voice_note,omitempty"` + // Caption is the caption of a media block ("collage", "slideshow", "map", + // "animation", "audio", "document", "photo", "video", "voice_note"). It + // shares the "caption" wire field with TableCaption. + // + // optional + Caption *RichBlockCaption `json:"-"` + // TableCaption is the caption of a "table" block. It shares the "caption" + // wire field with Caption. + // + // optional + TableCaption *RichText `json:"-"` +} + +// UnmarshalJSON decodes a RichBlock, routing the polymorphic "caption" field +// to Caption or TableCaption based on Type. +func (b *RichBlock) UnmarshalJSON(data []byte) error { + type alias RichBlock + aux := struct { + *alias + Caption json.RawMessage `json:"caption,omitempty"` + }{alias: (*alias)(b)} + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + if len(aux.Caption) == 0 || string(aux.Caption) == "null" { + return nil + } + + if b.Type == RichBlockTypeTable { + b.TableCaption = new(RichText) + return json.Unmarshal(aux.Caption, b.TableCaption) + } + + b.Caption = new(RichBlockCaption) + return json.Unmarshal(aux.Caption, b.Caption) +} + +// MarshalJSON encodes a RichBlock, emitting Caption or TableCaption under the +// shared "caption" wire field. +func (b RichBlock) MarshalJSON() ([]byte, error) { + type alias RichBlock + aux := struct { + alias + Caption json.RawMessage `json:"caption,omitempty"` + }{alias: alias(b)} + + switch { + case b.TableCaption != nil: + raw, err := json.Marshal(b.TableCaption) + if err != nil { + return nil, err + } + aux.Caption = raw + case b.Caption != nil: + raw, err := json.Marshal(b.Caption) + if err != nil { + return nil, err + } + aux.Caption = raw + } + + return json.Marshal(aux) +} + +// RichBlockCaption is the caption of a rich formatted block. +type RichBlockCaption struct { + // Text is the block caption. + Text RichText `json:"text"` + // Credit is the block credit, corresponding to the HTML tag . + // + // optional + Credit *RichText `json:"credit,omitempty"` +} + +// RichBlockTableCell is a cell in a rich formatted table. +type RichBlockTableCell struct { + // Text in the cell. If omitted, the cell is invisible. + // + // optional + Text *RichText `json:"text,omitempty"` + // IsHeader is true if the cell is a header cell. + // + // optional + IsHeader bool `json:"is_header,omitempty"` + // Colspan is the number of columns the cell spans if it is bigger than 1. + // + // optional + Colspan int `json:"colspan,omitempty"` + // Rowspan is the number of rows the cell spans if it is bigger than 1. + // + // optional + Rowspan int `json:"rowspan,omitempty"` + // Align is the horizontal cell content alignment; one of "left", + // "center", or "right". + Align string `json:"align"` + // Valign is the vertical cell content alignment; one of "top", "middle", + // or "bottom". + Valign string `json:"valign"` +} + +// RichMessageButton represents a button in a rich message. Use exactly one of +// the fields other than Text and Style to specify the type of the button. +type RichMessageButton struct { + // Text of the button. May contain only plain text, "custom_emoji" and + // "date_time" entities. + Text RichText `json:"text"` + // Style of the button. One of "danger", "success", "primary", or "link" + // (the button is shown as a regular link without borders). Apps may use + // theme-specific colors for the button background and text based on the + // style. The style "link" is allowed only for callback buttons. + // + // optional + Style string `json:"style,omitempty"` + // URL is an HTTP or tg:// URL to be opened when the button is pressed. + // Links tg://user?id= can be used to mention a user by their + // identifier without using a username, if this is allowed by their + // privacy settings. + // + // optional + URL string `json:"url,omitempty"` + // CallbackData is the data to be sent in a callback query to the bot when + // the button is pressed, 1-64 bytes. + // + // optional + CallbackData string `json:"callback_data,omitempty"` + // WebApp is the description of the Web App that will be launched when the + // user presses the button. The Web App will be able to send an arbitrary + // message on behalf of the user using the method answerWebAppQuery. + // Available only in private chats between a user and the bot. Not + // supported for messages sent on behalf of a business account. + // + // optional + WebApp *WebAppInfo `json:"web_app,omitempty"` + // LoginURL is an HTTPS URL used to automatically authorize the user. Can + // be used as a replacement for the Telegram Login Widget. Not supported + // for ephemeral messages. + // + // optional + LoginURL *LoginURL `json:"login_url,omitempty"` + // SwitchInlineQuery, if set, prompts the user to select one of their + // chats, open that chat and insert the bot's username and the specified + // inline query in the input field. May be empty, in which case just the + // bot's username will be inserted. Not supported for messages sent in + // channel direct messages chats and on behalf of a business account. + // + // optional + SwitchInlineQuery *string `json:"switch_inline_query,omitempty"` + // SwitchInlineQueryCurrentChat, if set, inserts the bot's username and + // the specified inline query in the current chat's input field. May be + // empty, in which case only the bot's username will be inserted. Not + // supported in channels and for messages sent in channel direct messages + // chats and on behalf of a business account. + // + // optional + SwitchInlineQueryCurrentChat *string `json:"switch_inline_query_current_chat,omitempty"` + // SwitchInlineQueryChosenChat, if set, prompts the user to select one of + // their chats of the specified type, open that chat and insert the bot's + // username and the specified inline query in the input field. Not + // supported for messages sent in channel direct messages chats and on + // behalf of a business account. + // + // optional + SwitchInlineQueryChosenChat *SwitchInlineQueryChosenChat `json:"switch_inline_query_chosen_chat,omitempty"` + // CopyText is a button that copies the specified text to the clipboard. + // + // optional + CopyText *CopyTextButton `json:"copy_text,omitempty"` + // Disabled if set, then the button is disabled and does nothing. + // + // optional + Disabled *DisabledButton `json:"disabled,omitempty"` +} + +// RichBlockListItem is an item of a rich formatted list. +type RichBlockListItem struct { + // Label of the item. + Label string `json:"label"` + // Blocks is the content of the item. + Blocks []RichBlock `json:"blocks"` + // HasCheckbox is true if the item has a checkbox. + // + // optional + HasCheckbox bool `json:"has_checkbox,omitempty"` + // IsChecked is true if the item has a checked checkbox. + // + // optional + IsChecked bool `json:"is_checked,omitempty"` + // Value is, for ordered lists, the numeric value of the item label. + // + // optional + Value int `json:"value,omitempty"` + // Type is, for ordered lists, the type of the item label; one of "a", "A", + // "i", "I", or "1". + // + // optional + Type string `json:"type,omitempty"` +} + +// InputRichMessageMedia describes a media element embedded in an outgoing rich +// message, referenced from InputRichMessage.HTML or InputRichMessage.Markdown. +type InputRichMessageMedia struct { + // ID is the unique identifier of the media used in a tg://photo?id=, + // tg://video?id=, tg://document?id=, or tg://audio?id= link. 1-64 + // characters; only A-Z, a-z, 0-9, _ and - are allowed. + ID string `json:"id"` + // Media to be sent; one of InputMediaAnimation, InputMediaAudio, + // InputMediaDocument, InputMediaPhoto, InputMediaVideo, or + // InputMediaVoiceNote. Everything except the media itself and its + // properties is ignored. + Media interface{} `json:"media"` +} + +// InputRichBlockListItem is an item of a rich formatted list to be sent. +type InputRichBlockListItem struct { + // Blocks is the content of the item. + Blocks []InputRichBlock `json:"blocks"` + // HasCheckbox, if true, gives the item a checkbox. + // + // optional + HasCheckbox bool `json:"has_checkbox,omitempty"` + // IsChecked, if true, gives the item a checked checkbox. + // + // optional + IsChecked bool `json:"is_checked,omitempty"` + // Value is, for ordered lists, the numeric value of the item label. + // + // optional + Value int `json:"value,omitempty"` + // Type is, for ordered lists, the type of the item label; one of "a", "A", + // "i", "I", or "1". + // + // optional + Type string `json:"type,omitempty"` +} + +// InputRichBlock represents a block in a rich formatted message to be sent. It +// is a flat polymorphic type keyed by Type; only the fields relevant to a given +// Type are set. Type takes the same values as RichBlock.Type, so the +// RichBlockType* constants apply here too. +// +// The "caption" wire field is split into Caption (for media blocks) and +// TableCaption (for the "table" block), which have different shapes; at most +// one is ever set. +type InputRichBlock struct { + // Type of the block, one of the RichBlockType* constants. + Type string `json:"type"` + // Text is the block text. Set for "paragraph", "heading", "pre", + // "footer", "expandable_blockquote", "pullquote", and "thinking" blocks. + // + // optional + Text *RichText `json:"text,omitempty"` + // Size is the relative font size of a "heading" block; 1-6, 1 is largest. + // + // optional + Size int `json:"size,omitempty"` + // Language is the programming language of a "pre" block. + // + // optional + Language string `json:"language,omitempty"` + // Expression is the LaTeX expression of a "mathematical_expression" + // block. + // + // optional + Expression string `json:"expression,omitempty"` + // Name is the anchor name of an "anchor" block. + // + // optional + Name string `json:"name,omitempty"` + // Items are the items of a "list" block. + // + // optional + Items []InputRichBlockListItem `json:"items,omitempty"` + // Blocks is the nested content of "blockquote", "collage", "slideshow", + // and "details" blocks. + // + // optional + Blocks []InputRichBlock `json:"blocks,omitempty"` + // Credit is the credit of "blockquote", "expandable_blockquote", and + // "pullquote" blocks. + // + // optional + Credit *RichText `json:"credit,omitempty"` + // Cells are the rows of cells of a "table" block. + // + // optional + Cells [][]RichBlockTableCell `json:"cells,omitempty"` + // IsBordered, if true, gives a "table" block borders. + // + // optional + IsBordered bool `json:"is_bordered,omitempty"` + // IsStriped, if true, makes a "table" block striped. + // + // optional + IsStriped bool `json:"is_striped,omitempty"` + // IsCompact, if true, gives the cells of a "table" block smaller indents. + // + // optional + IsCompact bool `json:"is_compact,omitempty"` + // Buttons is the list of 1-8 buttons of a "buttons" block, shown in one + // row. + // + // optional + Buttons []RichMessageButton `json:"buttons,omitempty"` + // Align is the horizontal alignment of the buttons of a "buttons" block; + // one of "left", "center", or "right". + // + // optional + Align string `json:"align,omitempty"` + // Summary is the always-shown summary of a "details" block. + // + // optional + Summary *RichText `json:"summary,omitempty"` + // IsOpen, if true, makes the content of a "details" block visible by + // default. + // + // optional + IsOpen bool `json:"is_open,omitempty"` + // Location is the center of a "map" block. + // + // optional + Location *Location `json:"location,omitempty"` + // Zoom is the zoom level of a "map" block; 0-24. + // + // optional + Zoom int `json:"zoom,omitempty"` + // Width is the width of a "map" block; 0-10000. + // + // optional + Width int `json:"width,omitempty"` + // Height is the height of a "map" block; 0-10000. + // + // optional + Height int `json:"height,omitempty"` + // Animation is the animation of an "animation" block. Its caption is + // ignored. + // + // optional + Animation *InputMediaAnimation `json:"animation,omitempty"` + // Audio is the audio of an "audio" block. Its caption is ignored. + // + // optional + Audio *InputMediaAudio `json:"audio,omitempty"` + // Document is the general file of a "document" block. Its caption is + // ignored. + // + // optional + Document *InputMediaDocument `json:"document,omitempty"` + // Photo is the photo of a "photo" block. Its caption is ignored. + // + // optional + Photo *InputMediaPhoto `json:"photo,omitempty"` + // Video is the video of a "video" block. Its caption is ignored. + // + // optional + Video *InputMediaVideo `json:"video,omitempty"` + // VoiceNote is the voice note of a "voice_note" block. Its caption is + // ignored. + // + // optional + VoiceNote *InputMediaVoiceNote `json:"voice_note,omitempty"` + // Caption is the caption of a media block ("collage", "slideshow", "map", + // "animation", "audio", "document", "photo", "video", "voice_note"). It + // shares the "caption" wire field with TableCaption. + // + // optional + Caption *RichBlockCaption `json:"-"` + // TableCaption is the caption of a "table" block. It shares the "caption" + // wire field with Caption. + // + // optional + TableCaption *RichText `json:"-"` +} + +// UnmarshalJSON decodes an InputRichBlock, routing the polymorphic "caption" +// field to Caption or TableCaption based on Type. +func (b *InputRichBlock) UnmarshalJSON(data []byte) error { + type alias InputRichBlock + aux := struct { + *alias + Caption json.RawMessage `json:"caption,omitempty"` + }{alias: (*alias)(b)} + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + if len(aux.Caption) == 0 || string(aux.Caption) == "null" { + return nil + } + + if b.Type == RichBlockTypeTable { + b.TableCaption = new(RichText) + return json.Unmarshal(aux.Caption, b.TableCaption) + } + + b.Caption = new(RichBlockCaption) + return json.Unmarshal(aux.Caption, b.Caption) +} + +// MarshalJSON encodes an InputRichBlock, emitting Caption or TableCaption +// under the shared "caption" wire field. +func (b InputRichBlock) MarshalJSON() ([]byte, error) { + type alias InputRichBlock + aux := struct { + alias + Caption json.RawMessage `json:"caption,omitempty"` + }{alias: alias(b)} + + switch { + case b.TableCaption != nil: + raw, err := json.Marshal(b.TableCaption) + if err != nil { + return nil, err + } + aux.Caption = raw + case b.Caption != nil: + raw, err := json.Marshal(b.Caption) + if err != nil { + return nil, err + } + aux.Caption = raw + } + + return json.Marshal(aux) +} + +// Community represents a community, a group of chats linked together around a +// shared topic or audience. +type Community struct { + // ID is the unique identifier for this community. + ID int64 `json:"id"` + // Name of the community. + Name string `json:"name"` +} + +// CommunityChatAdded describes a service message about a chat being added to a +// community. +type CommunityChatAdded struct { + // Community is the new community to which the chat belongs. + Community Community `json:"community"` +} + +// CommunityChatRemoved describes a service message about a chat being removed +// from a community. Currently holds no information. +type CommunityChatRemoved struct{} + +// CommunityChatJoined describes a service message about a chat being joined by +// a user from a community. +type CommunityChatJoined struct { + // Community is the community from which the chat was joined. + Community Community `json:"community"` +} + +// MessageGenerationStopped describes an update about a user stopping message +// generation. +type MessageGenerationStopped struct { + // Chat in which the message is generated. + Chat Chat `json:"chat"` + // MessageThreadID is the unique identifier of the message thread in which + // the message is generated. + // + // optional + MessageThreadID int `json:"message_thread_id,omitempty"` + // DraftID is the unique identifier of the message draft which was + // stopped. + DraftID int64 `json:"draft_id"` +} + +// BotSubscriptionUpdated contains information about changes to a user payment +// subscription toward the current bot. +type BotSubscriptionUpdated struct { + // User who subscribed for payments toward the bot. + User User `json:"user"` + // InvoicePayload is the bot-specified invoice payload. + InvoicePayload string `json:"invoice_payload"` + // State is the new state of the subscription, one of the + // BotSubscriptionState* constants. + State string `json:"state"` +} + +// BotSubscriptionUpdated.State values. +const ( + // BotSubscriptionStateCanceled means the user canceled the subscription. + BotSubscriptionStateCanceled = "canceled" + // BotSubscriptionStateActive means the user re-enabled a previously + // canceled subscription. + BotSubscriptionStateActive = "active" + // BotSubscriptionStateFailed means payment for the subscription failed. + BotSubscriptionStateFailed = "failed" +) diff --git a/types_test.go b/types_test.go index 0c6ba4ab..70f2f6b7 100644 --- a/types_test.go +++ b/types_test.go @@ -367,7 +367,7 @@ var ( _ Fileable = (*AddStickerConfig)(nil) _ Fileable = (*MediaGroupConfig)(nil) _ Fileable = (*WebhookConfig)(nil) - _ Fileable = (*SetStickerSetThumbConfig)(nil) + _ Fileable = (*SetStickerSetThumbnailConfig)(nil) ) // Ensure all RequestFileData types are correct. diff --git a/upload_e2e_test.go b/upload_e2e_test.go new file mode 100644 index 00000000..5be1dbd8 --- /dev/null +++ b/upload_e2e_test.go @@ -0,0 +1,174 @@ +package tgbotapi + +import ( + "io" + "net/http" + "net/http/httptest" + "strings" + "sync" + "testing" +) + +// fakeBotAPI is a minimal stand-in for the Bot API server. It answers getMe, +// records every other request (including multipart uploads) and replies with +// a successful empty message. +type fakeBotAPI struct { + mu sync.Mutex + requests map[string]*http.Request + fields map[string]map[string]string + files map[string]map[string]string +} + +func newFakeBotAPI(t *testing.T) (*BotAPI, *fakeBotAPI) { + t.Helper() + + fake := &fakeBotAPI{ + requests: map[string]*http.Request{}, + fields: map[string]map[string]string{}, + files: map[string]map[string]string{}, + } + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + method := r.URL.Path[strings.LastIndex(r.URL.Path, "/")+1:] + if method == "getMe" { + io.WriteString(w, `{"ok":true,"result":{"id":1,"is_bot":true,"first_name":"Bot","username":"bot"}}`) + return + } + + fields, files := map[string]string{}, map[string]string{} + if strings.HasPrefix(r.Header.Get("Content-Type"), "multipart/form-data") { + if err := r.ParseMultipartForm(1 << 20); err != nil { + t.Errorf("parse multipart: %v", err) + } + for k, v := range r.MultipartForm.Value { + fields[k] = v[0] + } + for k, v := range r.MultipartForm.File { + f, _ := v[0].Open() + b, _ := io.ReadAll(f) + files[k] = string(b) + } + } else { + r.ParseForm() + for k, v := range r.PostForm { + fields[k] = v[0] + } + } + + fake.mu.Lock() + fake.requests[method] = r + fake.fields[method] = fields + fake.files[method] = files + fake.mu.Unlock() + + io.WriteString(w, `{"ok":true,"result":{"message_id":1,"date":1,"chat":{"id":1,"type":"private"}}}`) + })) + t.Cleanup(srv.Close) + + bot, err := NewBotAPIWithAPIEndpoint("TOKEN", srv.URL+"/bot%s/%s") + if err != nil { + t.Fatalf("NewBotAPIWithAPIEndpoint: %v", err) + } + + return bot, fake +} + +func TestUploadNestedPollMedia(t *testing.T) { + bot, fake := newFakeBotAPI(t) + + _, err := bot.Send(SendPollConfig{ + BaseChat: BaseChat{ChatID: 1}, + Question: "Which one?", + Options: []InputPollOption{ + {Text: "first", Media: NewInputMediaPhoto(FileBytes{Name: "a.jpg", Bytes: []byte("AAA")})}, + {Text: "second"}, + }, + }) + if err != nil { + t.Fatalf("Send: %v", err) + } + + if got := fake.files["sendPoll"]["poll-media-0"]; got != "AAA" { + t.Fatalf("uploaded file = %q, want AAA (files: %v)", got, fake.files["sendPoll"]) + } + if !strings.Contains(fake.fields["sendPoll"]["options"], "attach://poll-media-0") { + t.Fatalf("options field = %s", fake.fields["sendPoll"]["options"]) + } +} + +func TestUploadRichMessageMedia(t *testing.T) { + bot, fake := newFakeBotAPI(t) + + _, err := bot.Send(SendRichMessageConfig{ + BaseChat: BaseChat{ChatID: 1}, + RichMessage: &InputRichMessage{ + Markdown: "![](tg://photo?id=p1)", + Media: []InputRichMessageMedia{{ID: "p1", Media: NewInputMediaPhoto(FileBytes{Name: "p.jpg", Bytes: []byte("PPP")})}}, + }, + }) + if err != nil { + t.Fatalf("Send: %v", err) + } + + if got := fake.files["sendRichMessage"]["rich-media-0"]; got != "PPP" { + t.Fatalf("uploaded file = %q, want PPP (files: %v)", got, fake.files["sendRichMessage"]) + } +} + +func TestSendWithoutUploadUsesPlainForm(t *testing.T) { + bot, fake := newFakeBotAPI(t) + + _, err := bot.Send(SendPollConfig{ + BaseChat: BaseChat{ChatID: 1}, + Question: "Which one?", + Options: []InputPollOption{{Text: "first", Media: NewInputMediaPhoto(FileID("abc"))}, {Text: "second"}}, + }) + if err != nil { + t.Fatalf("Send: %v", err) + } + + if len(fake.files["sendPoll"]) != 0 { + t.Fatalf("unexpected uploads: %v", fake.files["sendPoll"]) + } + if !strings.Contains(fake.fields["sendPoll"]["options"], `"media":"abc"`) { + t.Fatalf("options field = %s", fake.fields["sendPoll"]["options"]) + } +} + +func TestTransportErrorsDoNotLeakToken(t *testing.T) { + bot, _ := newFakeBotAPI(t) + bot.Token = "123456:SECRET" + bot.SetAPIEndpoint("http://127.0.0.1:1/bot%s/%s") + + _, err := bot.Request(NewMessage(1, "hi")) + if err == nil { + t.Fatal("expected a transport error") + } + if strings.Contains(err.Error(), "SECRET") { + t.Fatalf("error leaks the token: %v", err) + } +} + +func TestGetFileDirectURLUsesFileEndpoint(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasSuffix(r.URL.Path, "/getMe") { + io.WriteString(w, `{"ok":true,"result":{"id":1,"is_bot":true,"first_name":"Bot"}}`) + return + } + io.WriteString(w, `{"ok":true,"result":{"file_id":"f","file_unique_id":"u","file_path":"photos/1.jpg"}}`) + })) + defer srv.Close() + + bot, err := NewBotAPIWithAPIEndpoint("TOKEN", srv.URL+"/bot%s/%s") + if err != nil { + t.Fatal(err) + } + bot.SetFileEndpoint("http://local-bot-api/file/bot%s/%s") + + link, err := bot.GetFileDirectURL("f") + if err != nil { + t.Fatal(err) + } + if link != "http://local-bot-api/file/botTOKEN/photos/1.jpg" { + t.Fatalf("GetFileDirectURL() = %q", link) + } +}