mirror of
https://github.com/lukaszraczylo/go-telegram.git
synced 2026-06-11 23:19:31 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 931ea7ebe6 | |||
| 140ea13bde | |||
| 0731f10907 | |||
| 6685414374 | |||
| 404411b20c | |||
| 8d85e61da5 | |||
| 609c4ce649 | |||
| d39be13822 | |||
| d6ecbdea48 | |||
| 99c906c3c5 | |||
| f5250197b7 | |||
| bbbeb8461b | |||
| bfb7e9875e |
+25
-25
@@ -25,12 +25,12 @@ jobs:
|
||||
vet:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.25.x'
|
||||
check-latest: true
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
@@ -41,12 +41,12 @@ jobs:
|
||||
staticcheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.25.x'
|
||||
check-latest: true
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
@@ -58,12 +58,12 @@ jobs:
|
||||
govulncheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.25.x'
|
||||
check-latest: true
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
@@ -75,12 +75,12 @@ jobs:
|
||||
gosec:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.25.x'
|
||||
check-latest: true
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
@@ -98,12 +98,12 @@ jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.25.x'
|
||||
check-latest: true
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
@@ -112,7 +112,7 @@ jobs:
|
||||
- run: go test -race -coverprofile=coverage.out ./...
|
||||
- name: Build all examples
|
||||
run: go build ./examples/...
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: coverage
|
||||
path: coverage.out
|
||||
@@ -120,12 +120,12 @@ jobs:
|
||||
codegen-clean:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.25.x'
|
||||
check-latest: true
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
@@ -139,14 +139,14 @@ jobs:
|
||||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0 # need history for drift comparison
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.25.x'
|
||||
check-latest: true
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
@@ -196,11 +196,11 @@ jobs:
|
||||
github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.25.x'
|
||||
check-latest: true
|
||||
@@ -258,7 +258,7 @@ jobs:
|
||||
|
||||
- name: Run GoReleaser
|
||||
if: github.event_name != 'workflow_dispatch' || inputs.dry-run-release == false
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
uses: goreleaser/goreleaser-action@v7
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: '~> v2'
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/configure-pages@v5
|
||||
- uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
|
||||
@@ -13,12 +13,12 @@ jobs:
|
||||
regen:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0 # full history so audit -drift can compare against main
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.25.x'
|
||||
check-latest: true
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
|
||||
- name: Open PR
|
||||
if: steps.diff.outputs.no_changes != 'true'
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@v8
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: |
|
||||
|
||||
@@ -35,7 +35,10 @@ lint: vet
|
||||
integration:
|
||||
$(GO) test -tags=integration -v ./test/integration/...
|
||||
|
||||
SCRAPE_INPUT ?= testdata/html/snapshot_2026-05-08.html
|
||||
# Resolve via testdata/html/latest.html symlink so the pinned-fixture target
|
||||
# auto-tracks whatever snapshot regen.yml last committed. Override with
|
||||
# SCRAPE_INPUT=path/to/snapshot.html for ad-hoc replays.
|
||||
SCRAPE_INPUT ?= testdata/html/$(shell readlink testdata/html/latest.html)
|
||||
SCRAPE_OUTPUT ?= internal/spec/api.json
|
||||
|
||||
snapshot:
|
||||
|
||||
@@ -328,9 +328,12 @@ Apples-to-apples micro-benchmarks against the five most-starred Go Telegram libr
|
||||
|------|---------|--------------|
|
||||
| Webhook decode (small Update) | **ours** — 1.83 µs / 11 allocs | 1st of 6 |
|
||||
| Large Update unmarshal (unions + reply markup) | **ours** — 6.73 µs / 34 allocs | 1st of 6 |
|
||||
| `sendMessage` round-trip (mock server) | telego — 35.8 µs / 48 allocs | 2nd of 5 |
|
||||
| `sendMessage` round-trip — `net/http` default | telego — 35.8 µs / 48 allocs | 2nd of 5 (102 allocs) |
|
||||
| `sendMessage` round-trip — opt-in `fasthttp` | telego — 48 allocs | within 8 of telego (56 allocs) |
|
||||
| Dispatcher routing (20 handlers, last matches) | **ours** — 98 ns / 3 allocs | 1st of 3 |
|
||||
|
||||
Opt into fasthttp for high-throughput bots: `client.WithHTTPClient(client.NewFastHTTPDoer())`. Trade-off: HTTP/1.1 only, no `RoundTripper` middleware composition.
|
||||
|
||||
Full tables, caveats, and reproduction steps: **[`docs/benchmarks/2026-05-10-comparison.md`](docs/benchmarks/2026-05-10-comparison.md)**.
|
||||
|
||||
</details>
|
||||
@@ -368,6 +371,20 @@ bot := client.New("token", client.WithHTTPClient(fakeDoer{
|
||||
|
||||
The library's own generated test suite (`api/methods_gen_test.go`) covers 176 methods × 8 scenarios each: Success, APIError, NetworkError, ParseError, ContextCanceled, MissingRequiredFields, Forbidden, ServerError.
|
||||
|
||||
## Telemetry
|
||||
|
||||
On the **first call to `client.New`** in a process, this library sends a
|
||||
single anonymous adoption ping — project name, version, timestamp; no
|
||||
identifiers, no message contents, no API call metadata. Fire-and-forget
|
||||
with a 2-second timeout; cannot block `New` or panic.
|
||||
|
||||
Local source: [`client/telemetry.go`](client/telemetry.go). Upstream
|
||||
implementation, exact wire format, and full opt-out documentation:
|
||||
**[oss-telemetry — Disabling telemetry](https://github.com/lukaszraczylo/oss-telemetry#disabling-telemetry)**.
|
||||
|
||||
Quick opt-out: set any of `DO_NOT_TRACK=1`, `OSS_TELEMETRY_DISABLED=1`,
|
||||
or `GO_TELEGRAM_DISABLE_TELEMETRY=1`.
|
||||
|
||||
## Contributing
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
@@ -158,6 +158,7 @@ type InputPollOptionMediaType string
|
||||
|
||||
const (
|
||||
InputPollOptionMediaTypeAnimation InputPollOptionMediaType = "animation"
|
||||
InputPollOptionMediaTypeLink InputPollOptionMediaType = "link"
|
||||
InputPollOptionMediaTypeLivePhoto InputPollOptionMediaType = "live_photo"
|
||||
InputPollOptionMediaTypeLocation InputPollOptionMediaType = "location"
|
||||
InputPollOptionMediaTypePhoto InputPollOptionMediaType = "photo"
|
||||
@@ -357,6 +358,14 @@ const (
|
||||
RefundedPaymentCurrencyXTR RefundedPaymentCurrency = "XTR"
|
||||
)
|
||||
|
||||
type Result string
|
||||
|
||||
const (
|
||||
ResultApprove Result = "approve"
|
||||
ResultDecline Result = "decline"
|
||||
ResultQueue Result = "queue"
|
||||
)
|
||||
|
||||
type RevenueWithdrawalStateKind string
|
||||
|
||||
const (
|
||||
@@ -365,6 +374,88 @@ const (
|
||||
RevenueWithdrawalStateKindFailed RevenueWithdrawalStateKind = "failed"
|
||||
)
|
||||
|
||||
type RichBlockListItemType string
|
||||
|
||||
const (
|
||||
RichBlockListItemTypeLowerA RichBlockListItemType = "a"
|
||||
RichBlockListItemTypeUpperA RichBlockListItemType = "A"
|
||||
RichBlockListItemTypeLowerI RichBlockListItemType = "i"
|
||||
RichBlockListItemTypeUpperI RichBlockListItemType = "I"
|
||||
RichBlockListItemType1 RichBlockListItemType = "1"
|
||||
)
|
||||
|
||||
type RichBlockTableCellAlign string
|
||||
|
||||
const (
|
||||
RichBlockTableCellAlignLeft RichBlockTableCellAlign = "left"
|
||||
RichBlockTableCellAlignCenter RichBlockTableCellAlign = "center"
|
||||
RichBlockTableCellAlignRight RichBlockTableCellAlign = "right"
|
||||
)
|
||||
|
||||
type RichBlockTableCellValign string
|
||||
|
||||
const (
|
||||
RichBlockTableCellValignTop RichBlockTableCellValign = "top"
|
||||
RichBlockTableCellValignMiddle RichBlockTableCellValign = "middle"
|
||||
RichBlockTableCellValignBottom RichBlockTableCellValign = "bottom"
|
||||
)
|
||||
|
||||
type RichBlockType string
|
||||
|
||||
const (
|
||||
RichBlockTypeParagraph RichBlockType = "paragraph"
|
||||
RichBlockTypeHeading RichBlockType = "heading"
|
||||
RichBlockTypePre RichBlockType = "pre"
|
||||
RichBlockTypeFooter RichBlockType = "footer"
|
||||
RichBlockTypeDivider RichBlockType = "divider"
|
||||
RichBlockTypeMathematicalExpression RichBlockType = "mathematical_expression"
|
||||
RichBlockTypeAnchor RichBlockType = "anchor"
|
||||
RichBlockTypeList RichBlockType = "list"
|
||||
RichBlockTypeBlockquote RichBlockType = "blockquote"
|
||||
RichBlockTypePullquote RichBlockType = "pullquote"
|
||||
RichBlockTypeCollage RichBlockType = "collage"
|
||||
RichBlockTypeSlideshow RichBlockType = "slideshow"
|
||||
RichBlockTypeTable RichBlockType = "table"
|
||||
RichBlockTypeDetails RichBlockType = "details"
|
||||
RichBlockTypeMap RichBlockType = "map"
|
||||
RichBlockTypeAnimation RichBlockType = "animation"
|
||||
RichBlockTypeAudio RichBlockType = "audio"
|
||||
RichBlockTypePhoto RichBlockType = "photo"
|
||||
RichBlockTypeVideo RichBlockType = "video"
|
||||
RichBlockTypeVoiceNote RichBlockType = "voice_note"
|
||||
RichBlockTypeThinking RichBlockType = "thinking"
|
||||
)
|
||||
|
||||
type RichTextType string
|
||||
|
||||
const (
|
||||
RichTextTypeBold RichTextType = "bold"
|
||||
RichTextTypeItalic RichTextType = "italic"
|
||||
RichTextTypeUnderline RichTextType = "underline"
|
||||
RichTextTypeStrikethrough RichTextType = "strikethrough"
|
||||
RichTextTypeSpoiler RichTextType = "spoiler"
|
||||
RichTextTypeDateTime RichTextType = "date_time"
|
||||
RichTextTypeTextMention RichTextType = "text_mention"
|
||||
RichTextTypeSubscript RichTextType = "subscript"
|
||||
RichTextTypeSuperscript RichTextType = "superscript"
|
||||
RichTextTypeMarked RichTextType = "marked"
|
||||
RichTextTypeCode RichTextType = "code"
|
||||
RichTextTypeCustomEmoji RichTextType = "custom_emoji"
|
||||
RichTextTypeMathematicalExpression RichTextType = "mathematical_expression"
|
||||
RichTextTypeURL RichTextType = "url"
|
||||
RichTextTypeEmailAddress RichTextType = "email_address"
|
||||
RichTextTypePhoneNumber RichTextType = "phone_number"
|
||||
RichTextTypeBankCardNumber RichTextType = "bank_card_number"
|
||||
RichTextTypeMention RichTextType = "mention"
|
||||
RichTextTypeHashtag RichTextType = "hashtag"
|
||||
RichTextTypeCashtag RichTextType = "cashtag"
|
||||
RichTextTypeBotCommand RichTextType = "bot_command"
|
||||
RichTextTypeAnchor RichTextType = "anchor"
|
||||
RichTextTypeAnchorLink RichTextType = "anchor_link"
|
||||
RichTextTypeReference RichTextType = "reference"
|
||||
RichTextTypeReferenceLink RichTextType = "reference_link"
|
||||
)
|
||||
|
||||
type StickerType string
|
||||
|
||||
const (
|
||||
|
||||
+177
-83
@@ -45,7 +45,7 @@ func GetUpdates(ctx context.Context, b *client.Bot, p *GetUpdatesParams) ([]Upda
|
||||
// Notes1. You will not be able to receive updates using getUpdates for as long as an outgoing webhook is set up.2. To use a self-signed certificate, you need to upload your public key certificate using certificate parameter. Please upload as InputFile, sending a String will not work.3. Ports currently supported for webhooks: 443, 80, 88, 8443.
|
||||
// If you're having any trouble setting up webhooks, please check out this amazing guide to webhooks.
|
||||
type SetWebhookParams struct {
|
||||
// HTTPS URL to send updates to. Use an empty string to remove webhook integration
|
||||
// HTTPS URL to send updates to. Use an empty string to remove webhook integration.
|
||||
URL string `json:"url"`
|
||||
// Upload your public key certificate so that the root certificate in use can be checked. See our self-signed guide for details.
|
||||
Certificate *InputFile `json:"certificate,omitempty"`
|
||||
@@ -215,7 +215,7 @@ type SendMessageParams struct {
|
||||
SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
|
||||
// Description of the message to reply to
|
||||
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user.
|
||||
ReplyMarkup any `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -302,7 +302,7 @@ type CopyMessageParams struct {
|
||||
MessageID int64 `json:"message_id"`
|
||||
// New start timestamp for the copied video in the message
|
||||
VideoStartTimestamp *int64 `json:"video_start_timestamp,omitempty"`
|
||||
// New caption for media, 0-1024 characters after entities parsing. If not specified, the original caption is kept
|
||||
// New caption for media, 0-1024 characters after entities parsing. If not specified, the original caption is kept.
|
||||
Caption string `json:"caption,omitempty"`
|
||||
// Mode for parsing entities in the new caption. See formatting options for more details.
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
@@ -322,7 +322,7 @@ type CopyMessageParams struct {
|
||||
SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
|
||||
// Description of the message to reply to
|
||||
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user.
|
||||
ReplyMarkup any `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -398,7 +398,7 @@ type SendPhotoParams struct {
|
||||
SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
|
||||
// Description of the message to reply to
|
||||
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user.
|
||||
ReplyMarkup any `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -671,7 +671,7 @@ type SendAudioParams struct {
|
||||
SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
|
||||
// Description of the message to reply to
|
||||
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user.
|
||||
ReplyMarkup any `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -813,7 +813,7 @@ type SendDocumentParams struct {
|
||||
SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
|
||||
// Description of the message to reply to
|
||||
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user.
|
||||
ReplyMarkup any `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -962,7 +962,7 @@ type SendVideoParams struct {
|
||||
SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
|
||||
// Description of the message to reply to
|
||||
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user.
|
||||
ReplyMarkup any `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -1133,7 +1133,7 @@ type SendAnimationParams struct {
|
||||
SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
|
||||
// Description of the message to reply to
|
||||
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user.
|
||||
ReplyMarkup any `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -1278,7 +1278,7 @@ type SendVoiceParams struct {
|
||||
SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
|
||||
// Description of the message to reply to
|
||||
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user.
|
||||
ReplyMarkup any `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -1379,7 +1379,7 @@ type SendVideoNoteParams struct {
|
||||
MessageThreadID *int64 `json:"message_thread_id,omitempty"`
|
||||
// Identifier of the direct messages topic to which the message will be sent; required if the message is sent to a direct messages chat
|
||||
DirectMessagesTopicID *int64 `json:"direct_messages_topic_id,omitempty"`
|
||||
// Video note to send. Pass a file_id as String to send a video note that exists on the Telegram servers (recommended) or upload a new video using multipart/form-data. More information on Sending Files ». Sending video notes by a URL is currently unsupported
|
||||
// Video note to send. Pass a file_id as String to send a video note that exists on the Telegram servers (recommended) or upload a new video using multipart/form-data. More information on Sending Files ». Sending video notes by a URL is currently unsupported.
|
||||
VideoNote *InputFile `json:"video_note"`
|
||||
// Duration of sent video in seconds
|
||||
Duration *int64 `json:"duration,omitempty"`
|
||||
@@ -1399,7 +1399,7 @@ type SendVideoNoteParams struct {
|
||||
SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
|
||||
// Description of the message to reply to
|
||||
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user.
|
||||
ReplyMarkup any `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -1526,7 +1526,7 @@ type SendPaidMediaParams struct {
|
||||
SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
|
||||
// Description of the message to reply to
|
||||
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user.
|
||||
ReplyMarkup any `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -1707,7 +1707,7 @@ type SendLocationParams struct {
|
||||
Longitude float64 `json:"longitude"`
|
||||
// The radius of uncertainty for the location, measured in meters; 0-1500
|
||||
HorizontalAccuracy *float64 `json:"horizontal_accuracy,omitempty"`
|
||||
// Period in seconds during which the location will be updated (see Live Locations, should be between 60 and 86400, or 0x7FFFFFFF for live locations that can be edited indefinitely.
|
||||
// Period in seconds during which the location will be updated (see Live Locations, should be between 60 and 86400, or 0x7FFFFFFF for live locations that can be edited indefinitely
|
||||
LivePeriod *int64 `json:"live_period,omitempty"`
|
||||
// For live locations, a direction in which the user is moving, in degrees. Must be between 1 and 360 if specified.
|
||||
Heading *int64 `json:"heading,omitempty"`
|
||||
@@ -1725,7 +1725,7 @@ type SendLocationParams struct {
|
||||
SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
|
||||
// Description of the message to reply to
|
||||
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user.
|
||||
ReplyMarkup any `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -1776,7 +1776,7 @@ type SendVenueParams struct {
|
||||
SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
|
||||
// Description of the message to reply to
|
||||
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user.
|
||||
ReplyMarkup any `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -1819,7 +1819,7 @@ type SendContactParams struct {
|
||||
SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
|
||||
// Description of the message to reply to
|
||||
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user.
|
||||
ReplyMarkup any `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -1842,9 +1842,9 @@ type SendPollParams struct {
|
||||
MessageThreadID *int64 `json:"message_thread_id,omitempty"`
|
||||
// Poll question, 1-300 characters
|
||||
Question string `json:"question"`
|
||||
// Mode for parsing entities in the question. See formatting options for more details. Currently, only custom emoji entities are allowed
|
||||
// Mode for parsing entities in the question. See formatting options for more details. Currently, only custom emoji entities are allowed.
|
||||
QuestionParseMode ParseMode `json:"question_parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the poll question. It can be specified instead of question_parse_mode
|
||||
// A JSON-serialized list of special entities that appear in the poll question. It can be specified instead of question_parse_mode.
|
||||
QuestionEntities []MessageEntity `json:"question_entities,omitempty"`
|
||||
// A JSON-serialized list of 1-12 answer options
|
||||
Options []InputPollOption `json:"options"`
|
||||
@@ -1864,7 +1864,7 @@ type SendPollParams struct {
|
||||
HideResultsUntilCloses *bool `json:"hide_results_until_closes,omitempty"`
|
||||
// Pass True, if voting is limited to users who have been members of the chat where the poll is being sent for more than 24 hours; for channel chats only
|
||||
MembersOnly *bool `json:"members_only,omitempty"`
|
||||
// A JSON-serialized list of 0-12 two-letter ISO 3166-1 alpha-2 country codes indicating the countries from which users can vote in the poll; for channel chats only. If omitted or empty, then users from any country can participate in the poll.
|
||||
// A JSON-serialized list of 0-12 two-letter ISO 3166-1 alpha-2 country codes indicating the countries from which users can vote in the poll; for channel chats only. Use “FT” as a country code to allow users with anonymous numbers to vote. If omitted or empty, then users from any country can participate in the poll.
|
||||
CountryCodes []string `json:"country_codes,omitempty"`
|
||||
// A JSON-serialized list of monotonically increasing 0-based identifiers of the correct answer options, required for polls in quiz mode
|
||||
CorrectOptionIds []int64 `json:"correct_option_ids,omitempty"`
|
||||
@@ -1872,7 +1872,7 @@ type SendPollParams struct {
|
||||
Explanation string `json:"explanation,omitempty"`
|
||||
// Mode for parsing entities in the explanation. See formatting options for more details.
|
||||
ExplanationParseMode ParseMode `json:"explanation_parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the poll explanation. It can be specified instead of explanation_parse_mode
|
||||
// A JSON-serialized list of special entities that appear in the poll explanation. It can be specified instead of explanation_parse_mode.
|
||||
ExplanationEntities []MessageEntity `json:"explanation_entities,omitempty"`
|
||||
// Media added to the quiz explanation
|
||||
ExplanationMedia InputPollMedia `json:"explanation_media,omitempty"`
|
||||
@@ -1900,7 +1900,7 @@ type SendPollParams struct {
|
||||
MessageEffectID string `json:"message_effect_id,omitempty"`
|
||||
// Description of the message to reply to
|
||||
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user.
|
||||
ReplyMarkup any `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -1952,7 +1952,7 @@ type SendDiceParams struct {
|
||||
MessageThreadID *int64 `json:"message_thread_id,omitempty"`
|
||||
// Identifier of the direct messages topic to which the message will be sent; required if the message is sent to a direct messages chat
|
||||
DirectMessagesTopicID *int64 `json:"direct_messages_topic_id,omitempty"`
|
||||
// Emoji on which the dice throw animation is based. Currently, must be one of “”, “”, “”, “”, “”, or “”. Dice can have values 1-6 for “”, “” and “”, values 1-5 for “” and “”, and values 1-64 for “”. Defaults to “”
|
||||
// Emoji on which the dice throw animation is based. Currently, must be one of “”, “”, “”, “”, “”, or “”. Dice can have values 1-6 for “”, “” and “”, values 1-5 for “” and “”, and values 1-64 for “”. Defaults to “”.
|
||||
Emoji DiceEmoji `json:"emoji,omitempty"`
|
||||
// Sends the message silently. Users will receive a notification with no sound.
|
||||
DisableNotification *bool `json:"disable_notification,omitempty"`
|
||||
@@ -1966,7 +1966,7 @@ type SendDiceParams struct {
|
||||
SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
|
||||
// Description of the message to reply to
|
||||
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user.
|
||||
ReplyMarkup any `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -2010,7 +2010,7 @@ func SendMessageDraft(ctx context.Context, b *client.Bot, p *SendMessageDraftPar
|
||||
type SendChatActionParams struct {
|
||||
// Unique identifier of the business connection on behalf of which the action will be sent
|
||||
BusinessConnectionID string `json:"business_connection_id,omitempty"`
|
||||
// Unique identifier for the target chat or username of the target bot, supergroup or channel in the format @username. Channel chats and channel direct messages chats aren't supported.
|
||||
// Unique identifier for the target chat or username of the target bot or supergroup in the format @username. Channel chats and channel direct messages chats aren't supported.
|
||||
ChatID ChatID `json:"chat_id"`
|
||||
// Unique identifier for the target message thread or topic of a forum; for supergroups and private chats of bots with forum topic mode enabled only
|
||||
MessageThreadID *int64 `json:"message_thread_id,omitempty"`
|
||||
@@ -2174,7 +2174,7 @@ type RestrictChatMemberParams struct {
|
||||
Permissions ChatPermissions `json:"permissions"`
|
||||
// Pass True if chat permissions are set independently. Otherwise, the can_send_other_messages and can_add_web_page_previews permissions will imply the can_send_messages, can_send_audios, can_send_documents, can_send_photos, can_send_videos, can_send_video_notes, and can_send_voice_notes permissions; the can_send_polls permission will imply the can_send_messages permission.
|
||||
UseIndependentChatPermissions *bool `json:"use_independent_chat_permissions,omitempty"`
|
||||
// Date when restrictions will be lifted for the user; Unix time. If user is restricted for more than 366 days or less than 30 seconds from the current time, they are considered to be restricted forever
|
||||
// Date when restrictions will be lifted for the user; Unix time. If user is restricted for more than 366 days or less than 30 seconds from the current time, they are considered to be restricted forever.
|
||||
UntilDate *int64 `json:"until_date,omitempty"`
|
||||
}
|
||||
|
||||
@@ -2201,7 +2201,7 @@ type PromoteChatMemberParams struct {
|
||||
CanDeleteMessages *bool `json:"can_delete_messages,omitempty"`
|
||||
// Pass True if the administrator can manage video chats
|
||||
CanManageVideoChats *bool `json:"can_manage_video_chats,omitempty"`
|
||||
// Pass True if the administrator can restrict, ban or unban chat members, or access supergroup statistics. For backward compatibility, defaults to True for promotions of channel administrators
|
||||
// Pass True if the administrator can restrict, ban or unban chat members, or access supergroup statistics. For backward compatibility, defaults to True for promotions of channel administrators.
|
||||
CanRestrictMembers *bool `json:"can_restrict_members,omitempty"`
|
||||
// Pass True if the administrator can add new administrators with a subset of their own privileges or demote administrators that they have promoted, directly or indirectly (promoted by administrators that were appointed by him)
|
||||
CanPromoteMembers *bool `json:"can_promote_members,omitempty"`
|
||||
@@ -2356,7 +2356,7 @@ type CreateChatInviteLinkParams struct {
|
||||
ExpireDate *int64 `json:"expire_date,omitempty"`
|
||||
// The maximum number of users that can be members of the chat simultaneously after joining the chat via this invite link; 1-99999
|
||||
MemberLimit *int64 `json:"member_limit,omitempty"`
|
||||
// True, if users joining the chat via the link need to be approved by chat administrators. If True, member_limit can't be specified
|
||||
// True, if users joining the chat via the link need to be approved by chat administrators. If True, member_limit can't be specified.
|
||||
CreatesJoinRequest *bool `json:"creates_join_request,omitempty"`
|
||||
}
|
||||
|
||||
@@ -2381,7 +2381,7 @@ type EditChatInviteLinkParams struct {
|
||||
ExpireDate *int64 `json:"expire_date,omitempty"`
|
||||
// The maximum number of users that can be members of the chat simultaneously after joining the chat via this invite link; 1-99999
|
||||
MemberLimit *int64 `json:"member_limit,omitempty"`
|
||||
// True, if users joining the chat via the link need to be approved by chat administrators. If True, member_limit can't be specified
|
||||
// True, if users joining the chat via the link need to be approved by chat administrators. If True, member_limit can't be specified.
|
||||
CreatesJoinRequest *bool `json:"creates_join_request,omitempty"`
|
||||
}
|
||||
|
||||
@@ -2483,6 +2483,40 @@ func DeclineChatJoinRequest(ctx context.Context, b *client.Bot, p *DeclineChatJo
|
||||
return client.Call[*DeclineChatJoinRequestParams, bool](ctx, b, "declineChatJoinRequest", p)
|
||||
}
|
||||
|
||||
// AnswerChatJoinRequestQueryParams is the parameter set for AnswerChatJoinRequestQuery.
|
||||
//
|
||||
// Use this method to process a received chat join request query. Returns True on success.
|
||||
type AnswerChatJoinRequestQueryParams struct {
|
||||
// Unique identifier of the join request query
|
||||
ChatJoinRequestQueryID string `json:"chat_join_request_query_id"`
|
||||
// Result of the query. Must be either “approve” to allow the user to join the chat, “decline” to disallow the user to join the chat, or “queue” to leave the decision to other administrators.
|
||||
Result Result `json:"result"`
|
||||
}
|
||||
|
||||
// AnswerChatJoinRequestQuery calls the answerChatJoinRequestQuery Telegram Bot API method.
|
||||
//
|
||||
// Use this method to process a received chat join request query. Returns True on success.
|
||||
func AnswerChatJoinRequestQuery(ctx context.Context, b *client.Bot, p *AnswerChatJoinRequestQueryParams) (bool, error) {
|
||||
return client.Call[*AnswerChatJoinRequestQueryParams, bool](ctx, b, "answerChatJoinRequestQuery", p)
|
||||
}
|
||||
|
||||
// SendChatJoinRequestWebAppParams is the parameter set for SendChatJoinRequestWebApp.
|
||||
//
|
||||
// Use this method to process a received chat join request query by showing a Mini App to the user before deciding the outcome. Returns True on success.
|
||||
type SendChatJoinRequestWebAppParams struct {
|
||||
// Unique identifier of the join request query
|
||||
ChatJoinRequestQueryID string `json:"chat_join_request_query_id"`
|
||||
// The URL of the Mini App to be opened
|
||||
WebAppURL string `json:"web_app_url"`
|
||||
}
|
||||
|
||||
// SendChatJoinRequestWebApp calls the sendChatJoinRequestWebApp Telegram Bot API method.
|
||||
//
|
||||
// Use this method to process a received chat join request query by showing a Mini App to the user before deciding the outcome. Returns True on success.
|
||||
func SendChatJoinRequestWebApp(ctx context.Context, b *client.Bot, p *SendChatJoinRequestWebAppParams) (bool, error) {
|
||||
return client.Call[*SendChatJoinRequestWebAppParams, bool](ctx, b, "sendChatJoinRequestWebApp", p)
|
||||
}
|
||||
|
||||
// SetChatPhotoParams is the parameter set for SetChatPhoto.
|
||||
//
|
||||
// Use this method to set a new profile photo for the chat. Photos can't be changed for private chats. The bot must be an administrator in the chat for this to work and must have the appropriate administrator rights. Returns True on success.
|
||||
@@ -2801,7 +2835,7 @@ type CreateForumTopicParams struct {
|
||||
ChatID ChatID `json:"chat_id"`
|
||||
// Topic name, 1-128 characters
|
||||
Name string `json:"name"`
|
||||
// Color of the topic icon in RGB format. Currently, must be one of 7322096 (0x6FB9F0), 16766590 (0xFFD67E), 13338331 (0xCB86DB), 9367192 (0x8EEE98), 16749490 (0xFF93B2), or 16478047 (0xFB6F5F)
|
||||
// Color of the topic icon in RGB format. Currently, must be one of 7322096 (0x6FB9F0), 16766590 (0xFFD67E), 13338331 (0xCB86DB), 9367192 (0x8EEE98), 16749490 (0xFF93B2), or 16478047 (0xFB6F5F).
|
||||
IconColor *int64 `json:"icon_color,omitempty"`
|
||||
// Unique identifier of the custom emoji shown as the topic icon. Use getForumTopicIconStickers to get all allowed custom emoji identifiers.
|
||||
IconCustomEmojiID string `json:"icon_custom_emoji_id,omitempty"`
|
||||
@@ -2822,9 +2856,9 @@ type EditForumTopicParams struct {
|
||||
ChatID ChatID `json:"chat_id"`
|
||||
// Unique identifier for the target message thread of the forum topic
|
||||
MessageThreadID int64 `json:"message_thread_id"`
|
||||
// New topic name, 0-128 characters. If not specified or empty, the current name of the topic will be kept
|
||||
// New topic name, 0-128 characters. If not specified or empty, the current name of the topic will be kept.
|
||||
Name string `json:"name,omitempty"`
|
||||
// New unique identifier of the custom emoji shown as the topic icon. Use getForumTopicIconStickers to get all allowed custom emoji identifiers. Pass an empty string to remove the icon. If not specified, the current icon will be kept
|
||||
// New unique identifier of the custom emoji shown as the topic icon. Use getForumTopicIconStickers to get all allowed custom emoji identifiers. Pass an empty string to remove the icon. If not specified, the current icon will be kept.
|
||||
IconCustomEmojiID string `json:"icon_custom_emoji_id,omitempty"`
|
||||
}
|
||||
|
||||
@@ -3002,7 +3036,7 @@ func UnpinAllGeneralForumTopicMessages(ctx context.Context, b *client.Bot, p *Un
|
||||
type AnswerCallbackQueryParams struct {
|
||||
// Unique identifier for the query to be answered
|
||||
CallbackQueryID string `json:"callback_query_id"`
|
||||
// Text of the notification. If not specified, nothing will be shown to the user, 0-200 characters
|
||||
// Text of the notification. If not specified, nothing will be shown to the user, 0-200 characters.
|
||||
Text string `json:"text,omitempty"`
|
||||
// If True, an alert will be shown by the client instead of a notification at the top of the chat screen. Defaults to false.
|
||||
ShowAlert *bool `json:"show_alert,omitempty"`
|
||||
@@ -3141,7 +3175,7 @@ type SetMyCommandsParams struct {
|
||||
Commands []BotCommand `json:"commands"`
|
||||
// A JSON-serialized object, describing scope of users for which the commands are relevant. Defaults to BotCommandScopeDefault.
|
||||
Scope BotCommandScope `json:"scope,omitempty"`
|
||||
// A two-letter ISO 639-1 language code. If empty, commands will be applied to all users from the given scope, for whose language there are no dedicated commands
|
||||
// A two-letter ISO 639-1 language code. If empty, commands will be applied to all users from the given scope, for whose language there are no dedicated commands.
|
||||
LanguageCode string `json:"language_code,omitempty"`
|
||||
}
|
||||
|
||||
@@ -3158,7 +3192,7 @@ func SetMyCommands(ctx context.Context, b *client.Bot, p *SetMyCommandsParams) (
|
||||
type DeleteMyCommandsParams struct {
|
||||
// A JSON-serialized object, describing scope of users for which the commands are relevant. Defaults to BotCommandScopeDefault.
|
||||
Scope BotCommandScope `json:"scope,omitempty"`
|
||||
// A two-letter ISO 639-1 language code. If empty, commands will be applied to all users from the given scope, for whose language there are no dedicated commands
|
||||
// A two-letter ISO 639-1 language code. If empty, commands will be applied to all users from the given scope, for whose language there are no dedicated commands.
|
||||
LanguageCode string `json:"language_code,omitempty"`
|
||||
}
|
||||
|
||||
@@ -3314,9 +3348,9 @@ func RemoveMyProfilePhoto(ctx context.Context, b *client.Bot, p *RemoveMyProfile
|
||||
//
|
||||
// Use this method to change the bot's menu button in a private chat, or the default menu button. Returns True on success.
|
||||
type SetChatMenuButtonParams struct {
|
||||
// Unique identifier for the target private chat. If not specified, default bot's menu button will be changed
|
||||
// Unique identifier for the target private chat. If not specified, the bot's default menu button will be changed.
|
||||
ChatID *int64 `json:"chat_id,omitempty"`
|
||||
// A JSON-serialized object for the bot's new menu button. Defaults to MenuButtonDefault
|
||||
// A JSON-serialized object for the bot's new menu button. Defaults to MenuButtonDefault.
|
||||
MenuButton MenuButton `json:"menu_button,omitempty"`
|
||||
}
|
||||
|
||||
@@ -3331,7 +3365,7 @@ func SetChatMenuButton(ctx context.Context, b *client.Bot, p *SetChatMenuButtonP
|
||||
//
|
||||
// Use this method to get the current value of the bot's menu button in a private chat, or the default menu button. Returns MenuButton on success.
|
||||
type GetChatMenuButtonParams struct {
|
||||
// Unique identifier for the target private chat. If not specified, default bot's menu button will be returned
|
||||
// Unique identifier for the target private chat. If not specified, the bot's default menu button will be returned.
|
||||
ChatID *int64 `json:"chat_id,omitempty"`
|
||||
}
|
||||
|
||||
@@ -3532,7 +3566,7 @@ func ReadBusinessMessage(ctx context.Context, b *client.Bot, p *ReadBusinessMess
|
||||
type DeleteBusinessMessagesParams struct {
|
||||
// Unique identifier of the business connection on behalf of which to delete the messages
|
||||
BusinessConnectionID string `json:"business_connection_id"`
|
||||
// A JSON-serialized list of 1-100 identifiers of messages to delete. All messages must be from the same chat. See deleteMessage for limitations on which messages can be deleted
|
||||
// A JSON-serialized list of 1-100 identifiers of messages to delete. All messages must be from the same chat. See deleteMessage for limitations on which messages can be deleted.
|
||||
MessageIds []int64 `json:"message_ids"`
|
||||
}
|
||||
|
||||
@@ -3707,7 +3741,7 @@ type GetBusinessAccountGiftsParams struct {
|
||||
SortByPrice *bool `json:"sort_by_price,omitempty"`
|
||||
// Offset of the first entry to return as received from the previous request; use empty string to get the first chunk of results
|
||||
Offset string `json:"offset,omitempty"`
|
||||
// The maximum number of gifts to be returned; 1-100. Defaults to 100
|
||||
// The maximum number of gifts to be returned; 1-100. Defaults to 100.
|
||||
Limit *int64 `json:"limit,omitempty"`
|
||||
}
|
||||
|
||||
@@ -3738,7 +3772,7 @@ type GetUserGiftsParams struct {
|
||||
SortByPrice *bool `json:"sort_by_price,omitempty"`
|
||||
// Offset of the first entry to return as received from the previous request; use an empty string to get the first chunk of results
|
||||
Offset string `json:"offset,omitempty"`
|
||||
// The maximum number of gifts to be returned; 1-100. Defaults to 100
|
||||
// The maximum number of gifts to be returned; 1-100. Defaults to 100.
|
||||
Limit *int64 `json:"limit,omitempty"`
|
||||
}
|
||||
|
||||
@@ -3773,7 +3807,7 @@ type GetChatGiftsParams struct {
|
||||
SortByPrice *bool `json:"sort_by_price,omitempty"`
|
||||
// Offset of the first entry to return as received from the previous request; use an empty string to get the first chunk of results
|
||||
Offset string `json:"offset,omitempty"`
|
||||
// The maximum number of gifts to be returned; 1-100. Defaults to 100
|
||||
// The maximum number of gifts to be returned; 1-100. Defaults to 100.
|
||||
Limit *int64 `json:"limit,omitempty"`
|
||||
}
|
||||
|
||||
@@ -3991,7 +4025,7 @@ func SavePreparedInlineMessage(ctx context.Context, b *client.Bot, p *SavePrepar
|
||||
type SavePreparedKeyboardButtonParams struct {
|
||||
// Unique identifier of the target user that can use the button
|
||||
UserID int64 `json:"user_id"`
|
||||
// A JSON-serialized object describing the button to be saved. The button must be of the type request_users, request_chat, or request_managed_bot
|
||||
// A JSON-serialized object describing the button to be saved. The button must be of the type request_users, request_chat, or request_managed_bot.
|
||||
Button KeyboardButton `json:"button"`
|
||||
}
|
||||
|
||||
@@ -4004,31 +4038,33 @@ func SavePreparedKeyboardButton(ctx context.Context, b *client.Bot, p *SavePrepa
|
||||
|
||||
// EditMessageTextParams is the parameter set for EditMessageText.
|
||||
//
|
||||
// Use this method to edit text and game messages. On success, if the edited message is not an inline message, the edited Message is returned, otherwise True is returned. Note that business messages that were not sent by the bot and do not contain an inline keyboard can only be edited within 48 hours from the time they were sent.
|
||||
// Use this method to edit text, rich and game messages. On success, if the edited message is not an inline message, the edited Message is returned, otherwise True is returned. Note that business messages that were not sent by the bot and do not contain an inline keyboard can only be edited within 48 hours from the time they were sent.
|
||||
type EditMessageTextParams struct {
|
||||
// Unique identifier of the business connection on behalf of which the message to be edited was sent
|
||||
BusinessConnectionID string `json:"business_connection_id,omitempty"`
|
||||
// Required if inline_message_id is not specified. Unique identifier for the target chat or username of the target bot, supergroup or channel in the format @username.
|
||||
ChatID *ChatID `json:"chat_id,omitempty"`
|
||||
// Required if inline_message_id is not specified. Identifier of the message to edit
|
||||
// Required if inline_message_id is not specified. Identifier of the message to edit.
|
||||
MessageID *int64 `json:"message_id,omitempty"`
|
||||
// Required if chat_id and message_id are not specified. Identifier of the inline message
|
||||
// Required if chat_id and message_id are not specified. Identifier of the inline message.
|
||||
InlineMessageID string `json:"inline_message_id,omitempty"`
|
||||
// New text of the message, 1-4096 characters after entities parsing
|
||||
Text string `json:"text"`
|
||||
// New text of the message, 1-4096 characters after entity parsing; required if rich_message isn't specified
|
||||
Text string `json:"text,omitempty"`
|
||||
// Mode for parsing entities in the message text. See formatting options for more details.
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in message text, which can be specified instead of parse_mode
|
||||
Entities []MessageEntity `json:"entities,omitempty"`
|
||||
// Link preview generation options for the message
|
||||
LinkPreviewOptions *LinkPreviewOptions `json:"link_preview_options,omitempty"`
|
||||
// A JSON-serialized object for an inline keyboard.
|
||||
// New rich content of the message; required if text isn't specified
|
||||
RichMessage *InputRichMessage `json:"rich_message,omitempty"`
|
||||
// A JSON-serialized object for an inline keyboard
|
||||
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
// EditMessageText calls the editMessageText Telegram Bot API method.
|
||||
//
|
||||
// Use this method to edit text and game messages. On success, if the edited message is not an inline message, the edited Message is returned, otherwise True is returned. Note that business messages that were not sent by the bot and do not contain an inline keyboard can only be edited within 48 hours from the time they were sent.
|
||||
// Use this method to edit text, rich and game messages. On success, if the edited message is not an inline message, the edited Message is returned, otherwise True is returned. Note that business messages that were not sent by the bot and do not contain an inline keyboard can only be edited within 48 hours from the time they were sent.
|
||||
func EditMessageText(ctx context.Context, b *client.Bot, p *EditMessageTextParams) (*MessageOrBool, error) {
|
||||
return client.Call[*EditMessageTextParams, *MessageOrBool](ctx, b, "editMessageText", p)
|
||||
}
|
||||
@@ -4041,9 +4077,9 @@ type EditMessageCaptionParams struct {
|
||||
BusinessConnectionID string `json:"business_connection_id,omitempty"`
|
||||
// Required if inline_message_id is not specified. Unique identifier for the target chat or username of the target bot, supergroup or channel in the format @username.
|
||||
ChatID *ChatID `json:"chat_id,omitempty"`
|
||||
// Required if inline_message_id is not specified. Identifier of the message to edit
|
||||
// Required if inline_message_id is not specified. Identifier of the message to edit.
|
||||
MessageID *int64 `json:"message_id,omitempty"`
|
||||
// Required if chat_id and message_id are not specified. Identifier of the inline message
|
||||
// Required if chat_id and message_id are not specified. Identifier of the inline message.
|
||||
InlineMessageID string `json:"inline_message_id,omitempty"`
|
||||
// New caption of the message, 0-1024 characters after entities parsing
|
||||
Caption string `json:"caption,omitempty"`
|
||||
@@ -4053,7 +4089,7 @@ type EditMessageCaptionParams struct {
|
||||
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
|
||||
// Pass True, if the caption must be shown above the message media. Supported only for animation, photo and video messages.
|
||||
ShowCaptionAboveMedia *bool `json:"show_caption_above_media,omitempty"`
|
||||
// A JSON-serialized object for an inline keyboard.
|
||||
// A JSON-serialized object for an inline keyboard
|
||||
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4066,19 +4102,19 @@ func EditMessageCaption(ctx context.Context, b *client.Bot, p *EditMessageCaptio
|
||||
|
||||
// EditMessageMediaParams is the parameter set for EditMessageMedia.
|
||||
//
|
||||
// Use this method to edit animation, audio, document, live photo, photo, or video messages, or to add media to text messages. If a message is part of a message album, then it can be edited only to an audio for audio albums, only to a document for document albums and to a photo, a live photo, or a video otherwise. When an inline message is edited, a new file can't be uploaded; use a previously uploaded file via its file_id or specify a URL. On success, if the edited message is not an inline message, the edited Message is returned, otherwise True is returned. Note that business messages that were not sent by the bot and do not contain an inline keyboard can only be edited within 48 hours from the time they were sent.
|
||||
// Use this method to edit animation, audio, document, live photo, photo, or video messages, or to replace a text or a rich message with a media. If a message is part of a message album, then it can be edited only to an audio for audio albums, only to a document for document albums and to a photo, a live photo, or a video otherwise. When an inline message is edited, a new file can't be uploaded; use a previously uploaded file via its file_id or specify a URL. On success, if the edited message is not an inline message, the edited Message is returned, otherwise True is returned. Note that business messages that were not sent by the bot and do not contain an inline keyboard can only be edited within 48 hours from the time they were sent.
|
||||
type EditMessageMediaParams struct {
|
||||
// Unique identifier of the business connection on behalf of which the message to be edited was sent
|
||||
BusinessConnectionID string `json:"business_connection_id,omitempty"`
|
||||
// Required if inline_message_id is not specified. Unique identifier for the target chat or username of the target bot, supergroup or channel in the format @username.
|
||||
ChatID *ChatID `json:"chat_id,omitempty"`
|
||||
// Required if inline_message_id is not specified. Identifier of the message to edit
|
||||
// Required if inline_message_id is not specified. Identifier of the message to edit.
|
||||
MessageID *int64 `json:"message_id,omitempty"`
|
||||
// Required if chat_id and message_id are not specified. Identifier of the inline message
|
||||
// Required if chat_id and message_id are not specified. Identifier of the inline message.
|
||||
InlineMessageID string `json:"inline_message_id,omitempty"`
|
||||
// A JSON-serialized object for a new media content of the message
|
||||
Media InputMedia `json:"media"`
|
||||
// A JSON-serialized object for a new inline keyboard.
|
||||
// A JSON-serialized object for a new inline keyboard
|
||||
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4121,7 +4157,7 @@ func (p *EditMessageMediaParams) MultipartFiles() []client.MultipartFile {
|
||||
|
||||
// EditMessageMedia calls the editMessageMedia Telegram Bot API method.
|
||||
//
|
||||
// Use this method to edit animation, audio, document, live photo, photo, or video messages, or to add media to text messages. If a message is part of a message album, then it can be edited only to an audio for audio albums, only to a document for document albums and to a photo, a live photo, or a video otherwise. When an inline message is edited, a new file can't be uploaded; use a previously uploaded file via its file_id or specify a URL. On success, if the edited message is not an inline message, the edited Message is returned, otherwise True is returned. Note that business messages that were not sent by the bot and do not contain an inline keyboard can only be edited within 48 hours from the time they were sent.
|
||||
// Use this method to edit animation, audio, document, live photo, photo, or video messages, or to replace a text or a rich message with a media. If a message is part of a message album, then it can be edited only to an audio for audio albums, only to a document for document albums and to a photo, a live photo, or a video otherwise. When an inline message is edited, a new file can't be uploaded; use a previously uploaded file via its file_id or specify a URL. On success, if the edited message is not an inline message, the edited Message is returned, otherwise True is returned. Note that business messages that were not sent by the bot and do not contain an inline keyboard can only be edited within 48 hours from the time they were sent.
|
||||
func EditMessageMedia(ctx context.Context, b *client.Bot, p *EditMessageMediaParams) (*MessageOrBool, error) {
|
||||
return client.Call[*EditMessageMediaParams, *MessageOrBool](ctx, b, "editMessageMedia", p)
|
||||
}
|
||||
@@ -4134,15 +4170,15 @@ type EditMessageLiveLocationParams struct {
|
||||
BusinessConnectionID string `json:"business_connection_id,omitempty"`
|
||||
// Required if inline_message_id is not specified. Unique identifier for the target chat or username of the target bot, supergroup or channel in the format @username.
|
||||
ChatID *ChatID `json:"chat_id,omitempty"`
|
||||
// Required if inline_message_id is not specified. Identifier of the message to edit
|
||||
// Required if inline_message_id is not specified. Identifier of the message to edit.
|
||||
MessageID *int64 `json:"message_id,omitempty"`
|
||||
// Required if chat_id and message_id are not specified. Identifier of the inline message
|
||||
// Required if chat_id and message_id are not specified. Identifier of the inline message.
|
||||
InlineMessageID string `json:"inline_message_id,omitempty"`
|
||||
// Latitude of new location
|
||||
Latitude float64 `json:"latitude"`
|
||||
// Longitude of new location
|
||||
Longitude float64 `json:"longitude"`
|
||||
// New period in seconds during which the location can be updated, starting from the message send date. If 0x7FFFFFFF is specified, then the location can be updated forever. Otherwise, the new value must not exceed the current live_period by more than a day, and the live location expiration date must remain within the next 90 days. If not specified, then live_period remains unchanged
|
||||
// New period in seconds during which the location can be updated, starting from the message send date. If 0x7FFFFFFF is specified, then the location can be updated forever. Otherwise, the new value must not exceed the current live_period by more than a day, and the live location expiration date must remain within the next 90 days. If not specified, then live_period remains unchanged.
|
||||
LivePeriod *int64 `json:"live_period,omitempty"`
|
||||
// The radius of uncertainty for the location, measured in meters; 0-1500
|
||||
HorizontalAccuracy *float64 `json:"horizontal_accuracy,omitempty"`
|
||||
@@ -4150,7 +4186,7 @@ type EditMessageLiveLocationParams struct {
|
||||
Heading *int64 `json:"heading,omitempty"`
|
||||
// The maximum distance for proximity alerts about approaching another chat member, in meters. Must be between 1 and 100000 if specified.
|
||||
ProximityAlertRadius *int64 `json:"proximity_alert_radius,omitempty"`
|
||||
// A JSON-serialized object for a new inline keyboard.
|
||||
// A JSON-serialized object for a new inline keyboard
|
||||
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4169,11 +4205,11 @@ type StopMessageLiveLocationParams struct {
|
||||
BusinessConnectionID string `json:"business_connection_id,omitempty"`
|
||||
// Required if inline_message_id is not specified. Unique identifier for the target chat or username of the target bot, supergroup or channel in the format @username.
|
||||
ChatID *ChatID `json:"chat_id,omitempty"`
|
||||
// Required if inline_message_id is not specified. Identifier of the message with live location to stop
|
||||
// Required if inline_message_id is not specified. Identifier of the message with live location to stop.
|
||||
MessageID *int64 `json:"message_id,omitempty"`
|
||||
// Required if chat_id and message_id are not specified. Identifier of the inline message
|
||||
// Required if chat_id and message_id are not specified. Identifier of the inline message.
|
||||
InlineMessageID string `json:"inline_message_id,omitempty"`
|
||||
// A JSON-serialized object for a new inline keyboard.
|
||||
// A JSON-serialized object for a new inline keyboard
|
||||
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4215,11 +4251,11 @@ type EditMessageReplyMarkupParams struct {
|
||||
BusinessConnectionID string `json:"business_connection_id,omitempty"`
|
||||
// Required if inline_message_id is not specified. Unique identifier for the target chat or username of the target bot, supergroup or channel in the format @username.
|
||||
ChatID *ChatID `json:"chat_id,omitempty"`
|
||||
// Required if inline_message_id is not specified. Identifier of the message to edit
|
||||
// Required if inline_message_id is not specified. Identifier of the message to edit.
|
||||
MessageID *int64 `json:"message_id,omitempty"`
|
||||
// Required if chat_id and message_id are not specified. Identifier of the inline message
|
||||
// Required if chat_id and message_id are not specified. Identifier of the inline message.
|
||||
InlineMessageID string `json:"inline_message_id,omitempty"`
|
||||
// A JSON-serialized object for an inline keyboard.
|
||||
// A JSON-serialized object for an inline keyboard
|
||||
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4240,7 +4276,7 @@ type StopPollParams struct {
|
||||
ChatID ChatID `json:"chat_id"`
|
||||
// Identifier of the original message with the poll
|
||||
MessageID int64 `json:"message_id"`
|
||||
// A JSON-serialized object for a new message inline keyboard.
|
||||
// A JSON-serialized object for a new message inline keyboard
|
||||
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4259,7 +4295,7 @@ type ApproveSuggestedPostParams struct {
|
||||
ChatID int64 `json:"chat_id"`
|
||||
// Identifier of a suggested post message to approve
|
||||
MessageID int64 `json:"message_id"`
|
||||
// Point in time (Unix timestamp) when the post is expected to be published; omit if the date has already been specified when the suggested post was created. If specified, then the date must be not more than 2678400 seconds (30 days) in the future
|
||||
// Point in time (Unix timestamp) when the post is expected to be published; omit if the date has already been specified when the suggested post was created. If specified, then the date must be not more than 2678400 seconds (30 days) in the future.
|
||||
SendDate *int64 `json:"send_date,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4312,7 +4348,7 @@ func DeleteMessage(ctx context.Context, b *client.Bot, p *DeleteMessageParams) (
|
||||
type DeleteMessagesParams struct {
|
||||
// Unique identifier for the target chat or username of the target bot, supergroup or channel in the format @username
|
||||
ChatID ChatID `json:"chat_id"`
|
||||
// A JSON-serialized list of 1-100 identifiers of messages to delete. See deleteMessage for limitations on which messages can be deleted
|
||||
// A JSON-serialized list of 1-100 identifiers of messages to delete. See deleteMessage for limitations on which messages can be deleted.
|
||||
MessageIds []int64 `json:"message_ids"`
|
||||
}
|
||||
|
||||
@@ -4327,7 +4363,7 @@ func DeleteMessages(ctx context.Context, b *client.Bot, p *DeleteMessagesParams)
|
||||
//
|
||||
// Use this method to remove a reaction from a message in a group or a supergroup chat. The bot must have the 'can_delete_messages' administrator right in the chat. Returns True on success.
|
||||
type DeleteMessageReactionParams struct {
|
||||
// Unique identifier for the target chat or username of the target supergroup (in the format @username)
|
||||
// Unique identifier for the target chat or username of the target supergroup in the format @username
|
||||
ChatID ChatID `json:"chat_id"`
|
||||
// Identifier of the target message
|
||||
MessageID int64 `json:"message_id"`
|
||||
@@ -4348,7 +4384,7 @@ func DeleteMessageReaction(ctx context.Context, b *client.Bot, p *DeleteMessageR
|
||||
//
|
||||
// Use this method to remove up to 10000 recent reactions in a group or a supergroup chat added by a given user or chat. The bot must have the 'can_delete_messages' administrator right in the chat. Returns True on success.
|
||||
type DeleteAllMessageReactionsParams struct {
|
||||
// Unique identifier for the target chat or username of the target supergroup (in the format @username)
|
||||
// Unique identifier for the target chat or username of the target supergroup in the format @username
|
||||
ChatID ChatID `json:"chat_id"`
|
||||
// Identifier of the user whose reactions will be removed, if the reactions were added by a user
|
||||
UserID *int64 `json:"user_id,omitempty"`
|
||||
@@ -4391,7 +4427,7 @@ type SendStickerParams struct {
|
||||
SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
|
||||
// Description of the message to reply to
|
||||
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user.
|
||||
ReplyMarkup any `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4769,7 +4805,7 @@ func SetStickerSetThumbnail(ctx context.Context, b *client.Bot, p *SetStickerSet
|
||||
type SetCustomEmojiStickerSetThumbnailParams struct {
|
||||
// Sticker set name
|
||||
Name string `json:"name"`
|
||||
// Custom emoji identifier of a sticker from the sticker set; pass an empty string to drop the thumbnail and use the first sticker as the thumbnail.
|
||||
// Custom emoji identifier of a sticker from the sticker set; pass an empty string to drop the thumbnail and use the first sticker as the thumbnail
|
||||
CustomEmojiID string `json:"custom_emoji_id,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4795,6 +4831,64 @@ func DeleteStickerSet(ctx context.Context, b *client.Bot, p *DeleteStickerSetPar
|
||||
return client.Call[*DeleteStickerSetParams, bool](ctx, b, "deleteStickerSet", p)
|
||||
}
|
||||
|
||||
// SendRichMessageParams is the parameter set for SendRichMessage.
|
||||
//
|
||||
// Use this method to send rich messages. If the message contains a block with a media element, then the bot must have the right to send the media to the chat. On success, the sent Message is returned.
|
||||
type SendRichMessageParams struct {
|
||||
// Unique identifier of the business connection on behalf of which the message will be sent
|
||||
BusinessConnectionID string `json:"business_connection_id,omitempty"`
|
||||
// Unique identifier for the target chat or username of the target bot, supergroup or channel in the format @username
|
||||
ChatID ChatID `json:"chat_id"`
|
||||
// Unique identifier for the target message thread (topic) of a forum; for forum supergroups and private chats of bots with forum topic mode enabled only
|
||||
MessageThreadID *int64 `json:"message_thread_id,omitempty"`
|
||||
// Identifier of the direct messages topic to which the message will be sent; required if the message is sent to a direct messages chat
|
||||
DirectMessagesTopicID *int64 `json:"direct_messages_topic_id,omitempty"`
|
||||
// The message to be sent
|
||||
RichMessage InputRichMessage `json:"rich_message"`
|
||||
// Sends the message silently. Users will receive a notification with no sound.
|
||||
DisableNotification *bool `json:"disable_notification,omitempty"`
|
||||
// Protects the contents of the sent message from forwarding and saving
|
||||
ProtectContent *bool `json:"protect_content,omitempty"`
|
||||
// Pass True to allow up to 1000 messages per second, ignoring broadcasting limits for a fee of 0.1 Telegram Stars per message. The relevant Stars will be withdrawn from the bot's balance.
|
||||
AllowPaidBroadcast *bool `json:"allow_paid_broadcast,omitempty"`
|
||||
// Unique identifier of the message effect to be added to the message; for private chats only
|
||||
MessageEffectID string `json:"message_effect_id,omitempty"`
|
||||
// A JSON-serialized object containing 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 `json:"suggested_post_parameters,omitempty"`
|
||||
// Description of the message to reply to
|
||||
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user.
|
||||
ReplyMarkup any `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
// SendRichMessage calls the sendRichMessage Telegram Bot API method.
|
||||
//
|
||||
// Use this method to send rich messages. If the message contains a block with a media element, then the bot must have the right to send the media to the chat. On success, the sent Message is returned.
|
||||
func SendRichMessage(ctx context.Context, b *client.Bot, p *SendRichMessageParams) (*Message, error) {
|
||||
return client.Call[*SendRichMessageParams, *Message](ctx, b, "sendRichMessage", p)
|
||||
}
|
||||
|
||||
// SendRichMessageDraftParams is the parameter set for SendRichMessageDraft.
|
||||
//
|
||||
// Use this method to stream a partial rich message to a user while the message is being generated. Note that the streamed draft is ephemeral and acts as a temporary 30-second preview - once the output is finalized, you must call sendRichMessage with the complete message to persist it in the user's chat. Returns True on success.
|
||||
type SendRichMessageDraftParams struct {
|
||||
// Unique identifier for the target private chat
|
||||
ChatID int64 `json:"chat_id"`
|
||||
// Unique identifier for the target message thread
|
||||
MessageThreadID *int64 `json:"message_thread_id,omitempty"`
|
||||
// Unique identifier of the message draft; must be non-zero. Changes to drafts with the same identifier are animated.
|
||||
DraftID int64 `json:"draft_id"`
|
||||
// The partial message to be streamed
|
||||
RichMessage InputRichMessage `json:"rich_message"`
|
||||
}
|
||||
|
||||
// SendRichMessageDraft calls the sendRichMessageDraft Telegram Bot API method.
|
||||
//
|
||||
// Use this method to stream a partial rich message to a user while the message is being generated. Note that the streamed draft is ephemeral and acts as a temporary 30-second preview - once the output is finalized, you must call sendRichMessage with the complete message to persist it in the user's chat. Returns True on success.
|
||||
func SendRichMessageDraft(ctx context.Context, b *client.Bot, p *SendRichMessageDraftParams) (bool, error) {
|
||||
return client.Call[*SendRichMessageDraftParams, bool](ctx, b, "sendRichMessageDraft", p)
|
||||
}
|
||||
|
||||
// AnswerInlineQueryParams is the parameter set for AnswerInlineQuery.
|
||||
//
|
||||
// Use this method to send answers to an inline query. On success, True is returned.No more than 50 results per query are allowed.
|
||||
@@ -4846,7 +4940,7 @@ type SendInvoiceParams struct {
|
||||
MaxTipAmount *int64 `json:"max_tip_amount,omitempty"`
|
||||
// A JSON-serialized array of suggested amounts of tips in the smallest units of the currency (integer, not float/double). At most 4 suggested tip amounts can be specified. The suggested tip amounts must be positive, passed in a strictly increased order and must not exceed max_tip_amount.
|
||||
SuggestedTipAmounts []int64 `json:"suggested_tip_amounts,omitempty"`
|
||||
// Unique deep-linking parameter. If left empty, forwarded copies of the sent message will have a Pay button, allowing multiple users to pay directly from the forwarded message, using the same invoice. If non-empty, forwarded copies of the sent message will have a URL button with a deep link to the bot (instead of a Pay button), with the value used as the start parameter
|
||||
// Unique deep-linking parameter. If left empty, forwarded copies of the sent message will have a Pay button, allowing multiple users to pay directly from the forwarded message, using the same invoice. If non-empty, forwarded copies of the sent message will have a URL button with a deep link to the bot (instead of a Pay button), with the value used as the start parameter.
|
||||
StartParameter string `json:"start_parameter,omitempty"`
|
||||
// JSON-serialized data about the invoice, which will be shared with the payment provider. A detailed description of required fields should be provided by the payment provider.
|
||||
ProviderData string `json:"provider_data,omitempty"`
|
||||
@@ -5118,15 +5212,15 @@ type SetGameScoreParams struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
// New score, must be non-negative
|
||||
Score int64 `json:"score"`
|
||||
// Pass True if the high score is allowed to decrease. This can be useful when fixing mistakes or banning cheaters
|
||||
// Pass True if the high score is allowed to decrease. This can be useful when fixing mistakes or banning cheaters.
|
||||
Force *bool `json:"force,omitempty"`
|
||||
// Pass True if the game message should not be automatically edited to include the current scoreboard
|
||||
DisableEditMessage *bool `json:"disable_edit_message,omitempty"`
|
||||
// Required if inline_message_id is not specified. Unique identifier for the target chat
|
||||
// Required if inline_message_id is not specified. Unique identifier for the target chat.
|
||||
ChatID *int64 `json:"chat_id,omitempty"`
|
||||
// Required if inline_message_id is not specified. Identifier of the sent message
|
||||
// Required if inline_message_id is not specified. Identifier of the sent message.
|
||||
MessageID *int64 `json:"message_id,omitempty"`
|
||||
// Required if chat_id and message_id are not specified. Identifier of the inline message
|
||||
// Required if chat_id and message_id are not specified. Identifier of the inline message.
|
||||
InlineMessageID string `json:"inline_message_id,omitempty"`
|
||||
}
|
||||
|
||||
@@ -5144,11 +5238,11 @@ func SetGameScore(ctx context.Context, b *client.Bot, p *SetGameScoreParams) (*M
|
||||
type GetGameHighScoresParams struct {
|
||||
// Target user id
|
||||
UserID int64 `json:"user_id"`
|
||||
// Required if inline_message_id is not specified. Unique identifier for the target chat
|
||||
// Required if inline_message_id is not specified. Unique identifier for the target chat.
|
||||
ChatID *int64 `json:"chat_id,omitempty"`
|
||||
// Required if inline_message_id is not specified. Identifier of the sent message
|
||||
// Required if inline_message_id is not specified. Identifier of the sent message.
|
||||
MessageID *int64 `json:"message_id,omitempty"`
|
||||
// Required if chat_id and message_id are not specified. Identifier of the inline message
|
||||
// Required if chat_id and message_id are not specified. Identifier of the inline message.
|
||||
InlineMessageID string `json:"inline_message_id,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
+590
-21
@@ -7427,6 +7427,294 @@ func Test_DeclineChatJoinRequest_ServerError(t *testing.T) {
|
||||
require.True(t, ae.IsRetryable(), "5xx must be retryable")
|
||||
}
|
||||
|
||||
func Test_AnswerChatJoinRequestQuery_Success(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.MatchedBy(func(r *http.Request) bool {
|
||||
return strings.HasSuffix(r.URL.Path, "/answerChatJoinRequestQuery")
|
||||
})).Return(genTestResp(200, `{"ok":true,"result":true}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &AnswerChatJoinRequestQueryParams{
|
||||
ChatJoinRequestQueryID: "test_value",
|
||||
Result: ResultApprove,
|
||||
}
|
||||
_, err := AnswerChatJoinRequestQuery(context.Background(), bot, params)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_AnswerChatJoinRequestQuery_APIError(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(
|
||||
genTestResp(200, `{"ok":false,"error_code":429,"description":"Too Many Requests","parameters":{"retry_after":1}}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &AnswerChatJoinRequestQueryParams{
|
||||
ChatJoinRequestQueryID: "test_value",
|
||||
Result: ResultApprove,
|
||||
}
|
||||
_, err := AnswerChatJoinRequestQuery(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ae *client.APIError
|
||||
require.ErrorAs(t, err, &ae)
|
||||
require.Equal(t, 429, ae.Code)
|
||||
require.True(t, ae.IsRetryable())
|
||||
}
|
||||
|
||||
func Test_AnswerChatJoinRequestQuery_NetworkError(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(nil, errors.New("dial tcp: timeout"))
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &AnswerChatJoinRequestQueryParams{
|
||||
ChatJoinRequestQueryID: "test_value",
|
||||
Result: ResultApprove,
|
||||
}
|
||||
_, err := AnswerChatJoinRequestQuery(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ne *client.NetworkError
|
||||
require.ErrorAs(t, err, &ne)
|
||||
}
|
||||
|
||||
func Test_AnswerChatJoinRequestQuery_ParseError(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(genTestResp(200, `not json`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &AnswerChatJoinRequestQueryParams{
|
||||
ChatJoinRequestQueryID: "test_value",
|
||||
Result: ResultApprove,
|
||||
}
|
||||
_, err := AnswerChatJoinRequestQuery(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var pe *client.ParseError
|
||||
require.ErrorAs(t, err, &pe)
|
||||
}
|
||||
|
||||
func Test_AnswerChatJoinRequestQuery_ContextCanceled(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(nil, context.Canceled).Maybe()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &AnswerChatJoinRequestQueryParams{
|
||||
ChatJoinRequestQueryID: "test_value",
|
||||
Result: ResultApprove,
|
||||
}
|
||||
_, err := AnswerChatJoinRequestQuery(ctx, bot, params)
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
}
|
||||
|
||||
// Test_AnswerChatJoinRequestQuery_MissingRequiredFields exercises Telegram's server-side
|
||||
// validation: when a required field is omitted, Telegram returns 400 with
|
||||
// a description like "Bad Request: <field> is empty". The library must
|
||||
// surface this as *APIError with the ErrBadRequest sentinel.
|
||||
func Test_AnswerChatJoinRequestQuery_MissingRequiredFields(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(
|
||||
genTestResp(200, `{"ok":false,"error_code":400,"description":"Bad Request: chat_id is empty"}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
// Send a Params with all required fields zeroed — simulates a caller
|
||||
// that forgot to populate them. The bot library marshals as-is and
|
||||
// surfaces Telegram's 400 reply.
|
||||
_, err := AnswerChatJoinRequestQuery(context.Background(), bot, &AnswerChatJoinRequestQueryParams{})
|
||||
require.Error(t, err)
|
||||
var ae *client.APIError
|
||||
require.ErrorAs(t, err, &ae)
|
||||
require.Equal(t, 400, ae.Code)
|
||||
require.True(t, errors.Is(err, client.ErrBadRequest))
|
||||
require.False(t, ae.IsRetryable())
|
||||
}
|
||||
|
||||
// Test_AnswerChatJoinRequestQuery_Forbidden exercises the 403 path (bot blocked by user,
|
||||
// removed from chat, etc.). The library must surface the ErrForbidden
|
||||
// sentinel.
|
||||
func Test_AnswerChatJoinRequestQuery_Forbidden(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(
|
||||
genTestResp(200, `{"ok":false,"error_code":403,"description":"Forbidden: bot was blocked by the user"}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &AnswerChatJoinRequestQueryParams{
|
||||
ChatJoinRequestQueryID: "test_value",
|
||||
Result: ResultApprove,
|
||||
}
|
||||
_, err := AnswerChatJoinRequestQuery(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ae *client.APIError
|
||||
require.ErrorAs(t, err, &ae)
|
||||
require.Equal(t, 403, ae.Code)
|
||||
require.True(t, errors.Is(err, client.ErrForbidden))
|
||||
require.False(t, ae.IsRetryable())
|
||||
}
|
||||
|
||||
// Test_AnswerChatJoinRequestQuery_ServerError exercises the 5xx path. The library must
|
||||
// classify these as retryable so RetryDoer / user retry logic kicks in.
|
||||
func Test_AnswerChatJoinRequestQuery_ServerError(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(
|
||||
genTestResp(200, `{"ok":false,"error_code":500,"description":"Internal server error"}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &AnswerChatJoinRequestQueryParams{
|
||||
ChatJoinRequestQueryID: "test_value",
|
||||
Result: ResultApprove,
|
||||
}
|
||||
_, err := AnswerChatJoinRequestQuery(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ae *client.APIError
|
||||
require.ErrorAs(t, err, &ae)
|
||||
require.Equal(t, 500, ae.Code)
|
||||
require.True(t, ae.IsRetryable(), "5xx must be retryable")
|
||||
}
|
||||
|
||||
func Test_SendChatJoinRequestWebApp_Success(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.MatchedBy(func(r *http.Request) bool {
|
||||
return strings.HasSuffix(r.URL.Path, "/sendChatJoinRequestWebApp")
|
||||
})).Return(genTestResp(200, `{"ok":true,"result":true}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendChatJoinRequestWebAppParams{
|
||||
ChatJoinRequestQueryID: "test_value",
|
||||
WebAppURL: "test_value",
|
||||
}
|
||||
_, err := SendChatJoinRequestWebApp(context.Background(), bot, params)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_SendChatJoinRequestWebApp_APIError(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(
|
||||
genTestResp(200, `{"ok":false,"error_code":429,"description":"Too Many Requests","parameters":{"retry_after":1}}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendChatJoinRequestWebAppParams{
|
||||
ChatJoinRequestQueryID: "test_value",
|
||||
WebAppURL: "test_value",
|
||||
}
|
||||
_, err := SendChatJoinRequestWebApp(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ae *client.APIError
|
||||
require.ErrorAs(t, err, &ae)
|
||||
require.Equal(t, 429, ae.Code)
|
||||
require.True(t, ae.IsRetryable())
|
||||
}
|
||||
|
||||
func Test_SendChatJoinRequestWebApp_NetworkError(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(nil, errors.New("dial tcp: timeout"))
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendChatJoinRequestWebAppParams{
|
||||
ChatJoinRequestQueryID: "test_value",
|
||||
WebAppURL: "test_value",
|
||||
}
|
||||
_, err := SendChatJoinRequestWebApp(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ne *client.NetworkError
|
||||
require.ErrorAs(t, err, &ne)
|
||||
}
|
||||
|
||||
func Test_SendChatJoinRequestWebApp_ParseError(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(genTestResp(200, `not json`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendChatJoinRequestWebAppParams{
|
||||
ChatJoinRequestQueryID: "test_value",
|
||||
WebAppURL: "test_value",
|
||||
}
|
||||
_, err := SendChatJoinRequestWebApp(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var pe *client.ParseError
|
||||
require.ErrorAs(t, err, &pe)
|
||||
}
|
||||
|
||||
func Test_SendChatJoinRequestWebApp_ContextCanceled(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(nil, context.Canceled).Maybe()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendChatJoinRequestWebAppParams{
|
||||
ChatJoinRequestQueryID: "test_value",
|
||||
WebAppURL: "test_value",
|
||||
}
|
||||
_, err := SendChatJoinRequestWebApp(ctx, bot, params)
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
}
|
||||
|
||||
// Test_SendChatJoinRequestWebApp_MissingRequiredFields exercises Telegram's server-side
|
||||
// validation: when a required field is omitted, Telegram returns 400 with
|
||||
// a description like "Bad Request: <field> is empty". The library must
|
||||
// surface this as *APIError with the ErrBadRequest sentinel.
|
||||
func Test_SendChatJoinRequestWebApp_MissingRequiredFields(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(
|
||||
genTestResp(200, `{"ok":false,"error_code":400,"description":"Bad Request: chat_id is empty"}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
// Send a Params with all required fields zeroed — simulates a caller
|
||||
// that forgot to populate them. The bot library marshals as-is and
|
||||
// surfaces Telegram's 400 reply.
|
||||
_, err := SendChatJoinRequestWebApp(context.Background(), bot, &SendChatJoinRequestWebAppParams{})
|
||||
require.Error(t, err)
|
||||
var ae *client.APIError
|
||||
require.ErrorAs(t, err, &ae)
|
||||
require.Equal(t, 400, ae.Code)
|
||||
require.True(t, errors.Is(err, client.ErrBadRequest))
|
||||
require.False(t, ae.IsRetryable())
|
||||
}
|
||||
|
||||
// Test_SendChatJoinRequestWebApp_Forbidden exercises the 403 path (bot blocked by user,
|
||||
// removed from chat, etc.). The library must surface the ErrForbidden
|
||||
// sentinel.
|
||||
func Test_SendChatJoinRequestWebApp_Forbidden(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(
|
||||
genTestResp(200, `{"ok":false,"error_code":403,"description":"Forbidden: bot was blocked by the user"}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendChatJoinRequestWebAppParams{
|
||||
ChatJoinRequestQueryID: "test_value",
|
||||
WebAppURL: "test_value",
|
||||
}
|
||||
_, err := SendChatJoinRequestWebApp(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ae *client.APIError
|
||||
require.ErrorAs(t, err, &ae)
|
||||
require.Equal(t, 403, ae.Code)
|
||||
require.True(t, errors.Is(err, client.ErrForbidden))
|
||||
require.False(t, ae.IsRetryable())
|
||||
}
|
||||
|
||||
// Test_SendChatJoinRequestWebApp_ServerError exercises the 5xx path. The library must
|
||||
// classify these as retryable so RetryDoer / user retry logic kicks in.
|
||||
func Test_SendChatJoinRequestWebApp_ServerError(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(
|
||||
genTestResp(200, `{"ok":false,"error_code":500,"description":"Internal server error"}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendChatJoinRequestWebAppParams{
|
||||
ChatJoinRequestQueryID: "test_value",
|
||||
WebAppURL: "test_value",
|
||||
}
|
||||
_, err := SendChatJoinRequestWebApp(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ae *client.APIError
|
||||
require.ErrorAs(t, err, &ae)
|
||||
require.Equal(t, 500, ae.Code)
|
||||
require.True(t, ae.IsRetryable(), "5xx must be retryable")
|
||||
}
|
||||
|
||||
func Test_SetChatPhoto_Success(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.MatchedBy(func(r *http.Request) bool {
|
||||
@@ -18587,9 +18875,7 @@ func Test_EditMessageText_Success(t *testing.T) {
|
||||
})).Return(genTestResp(200, `{"ok":true,"result":true}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &EditMessageTextParams{
|
||||
Text: "test_value",
|
||||
}
|
||||
params := &EditMessageTextParams{}
|
||||
_, err := EditMessageText(context.Background(), bot, params)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -18600,9 +18886,7 @@ func Test_EditMessageText_APIError(t *testing.T) {
|
||||
genTestResp(200, `{"ok":false,"error_code":429,"description":"Too Many Requests","parameters":{"retry_after":1}}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &EditMessageTextParams{
|
||||
Text: "test_value",
|
||||
}
|
||||
params := &EditMessageTextParams{}
|
||||
_, err := EditMessageText(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ae *client.APIError
|
||||
@@ -18616,9 +18900,7 @@ func Test_EditMessageText_NetworkError(t *testing.T) {
|
||||
m.On("Do", mock.Anything).Return(nil, errors.New("dial tcp: timeout"))
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &EditMessageTextParams{
|
||||
Text: "test_value",
|
||||
}
|
||||
params := &EditMessageTextParams{}
|
||||
_, err := EditMessageText(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ne *client.NetworkError
|
||||
@@ -18630,9 +18912,7 @@ func Test_EditMessageText_ParseError(t *testing.T) {
|
||||
m.On("Do", mock.Anything).Return(genTestResp(200, `not json`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &EditMessageTextParams{
|
||||
Text: "test_value",
|
||||
}
|
||||
params := &EditMessageTextParams{}
|
||||
_, err := EditMessageText(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var pe *client.ParseError
|
||||
@@ -18647,9 +18927,7 @@ func Test_EditMessageText_ContextCanceled(t *testing.T) {
|
||||
cancel()
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &EditMessageTextParams{
|
||||
Text: "test_value",
|
||||
}
|
||||
params := &EditMessageTextParams{}
|
||||
_, err := EditMessageText(ctx, bot, params)
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
@@ -18686,9 +18964,7 @@ func Test_EditMessageText_Forbidden(t *testing.T) {
|
||||
genTestResp(200, `{"ok":false,"error_code":403,"description":"Forbidden: bot was blocked by the user"}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &EditMessageTextParams{
|
||||
Text: "test_value",
|
||||
}
|
||||
params := &EditMessageTextParams{}
|
||||
_, err := EditMessageText(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ae *client.APIError
|
||||
@@ -18706,9 +18982,7 @@ func Test_EditMessageText_ServerError(t *testing.T) {
|
||||
genTestResp(200, `{"ok":false,"error_code":500,"description":"Internal server error"}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &EditMessageTextParams{
|
||||
Text: "test_value",
|
||||
}
|
||||
params := &EditMessageTextParams{}
|
||||
_, err := EditMessageText(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ae *client.APIError
|
||||
@@ -22830,6 +23104,301 @@ func Test_DeleteStickerSet_ServerError(t *testing.T) {
|
||||
require.True(t, ae.IsRetryable(), "5xx must be retryable")
|
||||
}
|
||||
|
||||
func Test_SendRichMessage_Success(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.MatchedBy(func(r *http.Request) bool {
|
||||
return strings.HasSuffix(r.URL.Path, "/sendRichMessage")
|
||||
})).Return(genTestResp(200, `{"ok":true,"result":{}}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendRichMessageParams{
|
||||
ChatID: ChatIDFromInt(123),
|
||||
RichMessage: InputRichMessage{},
|
||||
}
|
||||
_, err := SendRichMessage(context.Background(), bot, params)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_SendRichMessage_APIError(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(
|
||||
genTestResp(200, `{"ok":false,"error_code":429,"description":"Too Many Requests","parameters":{"retry_after":1}}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendRichMessageParams{
|
||||
ChatID: ChatIDFromInt(123),
|
||||
RichMessage: InputRichMessage{},
|
||||
}
|
||||
_, err := SendRichMessage(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ae *client.APIError
|
||||
require.ErrorAs(t, err, &ae)
|
||||
require.Equal(t, 429, ae.Code)
|
||||
require.True(t, ae.IsRetryable())
|
||||
}
|
||||
|
||||
func Test_SendRichMessage_NetworkError(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(nil, errors.New("dial tcp: timeout"))
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendRichMessageParams{
|
||||
ChatID: ChatIDFromInt(123),
|
||||
RichMessage: InputRichMessage{},
|
||||
}
|
||||
_, err := SendRichMessage(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ne *client.NetworkError
|
||||
require.ErrorAs(t, err, &ne)
|
||||
}
|
||||
|
||||
func Test_SendRichMessage_ParseError(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(genTestResp(200, `not json`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendRichMessageParams{
|
||||
ChatID: ChatIDFromInt(123),
|
||||
RichMessage: InputRichMessage{},
|
||||
}
|
||||
_, err := SendRichMessage(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var pe *client.ParseError
|
||||
require.ErrorAs(t, err, &pe)
|
||||
}
|
||||
|
||||
func Test_SendRichMessage_ContextCanceled(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(nil, context.Canceled).Maybe()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendRichMessageParams{
|
||||
ChatID: ChatIDFromInt(123),
|
||||
RichMessage: InputRichMessage{},
|
||||
}
|
||||
_, err := SendRichMessage(ctx, bot, params)
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
}
|
||||
|
||||
// Test_SendRichMessage_MissingRequiredFields exercises Telegram's server-side
|
||||
// validation: when a required field is omitted, Telegram returns 400 with
|
||||
// a description like "Bad Request: <field> is empty". The library must
|
||||
// surface this as *APIError with the ErrBadRequest sentinel.
|
||||
func Test_SendRichMessage_MissingRequiredFields(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(
|
||||
genTestResp(200, `{"ok":false,"error_code":400,"description":"Bad Request: chat_id is empty"}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
// Send a Params with all required fields zeroed — simulates a caller
|
||||
// that forgot to populate them. The bot library marshals as-is and
|
||||
// surfaces Telegram's 400 reply.
|
||||
_, err := SendRichMessage(context.Background(), bot, &SendRichMessageParams{})
|
||||
require.Error(t, err)
|
||||
var ae *client.APIError
|
||||
require.ErrorAs(t, err, &ae)
|
||||
require.Equal(t, 400, ae.Code)
|
||||
require.True(t, errors.Is(err, client.ErrBadRequest))
|
||||
require.False(t, ae.IsRetryable())
|
||||
}
|
||||
|
||||
// Test_SendRichMessage_Forbidden exercises the 403 path (bot blocked by user,
|
||||
// removed from chat, etc.). The library must surface the ErrForbidden
|
||||
// sentinel.
|
||||
func Test_SendRichMessage_Forbidden(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(
|
||||
genTestResp(200, `{"ok":false,"error_code":403,"description":"Forbidden: bot was blocked by the user"}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendRichMessageParams{
|
||||
ChatID: ChatIDFromInt(123),
|
||||
RichMessage: InputRichMessage{},
|
||||
}
|
||||
_, err := SendRichMessage(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ae *client.APIError
|
||||
require.ErrorAs(t, err, &ae)
|
||||
require.Equal(t, 403, ae.Code)
|
||||
require.True(t, errors.Is(err, client.ErrForbidden))
|
||||
require.False(t, ae.IsRetryable())
|
||||
}
|
||||
|
||||
// Test_SendRichMessage_ServerError exercises the 5xx path. The library must
|
||||
// classify these as retryable so RetryDoer / user retry logic kicks in.
|
||||
func Test_SendRichMessage_ServerError(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(
|
||||
genTestResp(200, `{"ok":false,"error_code":500,"description":"Internal server error"}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendRichMessageParams{
|
||||
ChatID: ChatIDFromInt(123),
|
||||
RichMessage: InputRichMessage{},
|
||||
}
|
||||
_, err := SendRichMessage(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ae *client.APIError
|
||||
require.ErrorAs(t, err, &ae)
|
||||
require.Equal(t, 500, ae.Code)
|
||||
require.True(t, ae.IsRetryable(), "5xx must be retryable")
|
||||
}
|
||||
|
||||
func Test_SendRichMessageDraft_Success(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.MatchedBy(func(r *http.Request) bool {
|
||||
return strings.HasSuffix(r.URL.Path, "/sendRichMessageDraft")
|
||||
})).Return(genTestResp(200, `{"ok":true,"result":true}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendRichMessageDraftParams{
|
||||
ChatID: 42,
|
||||
DraftID: 42,
|
||||
RichMessage: InputRichMessage{},
|
||||
}
|
||||
_, err := SendRichMessageDraft(context.Background(), bot, params)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_SendRichMessageDraft_APIError(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(
|
||||
genTestResp(200, `{"ok":false,"error_code":429,"description":"Too Many Requests","parameters":{"retry_after":1}}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendRichMessageDraftParams{
|
||||
ChatID: 42,
|
||||
DraftID: 42,
|
||||
RichMessage: InputRichMessage{},
|
||||
}
|
||||
_, err := SendRichMessageDraft(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ae *client.APIError
|
||||
require.ErrorAs(t, err, &ae)
|
||||
require.Equal(t, 429, ae.Code)
|
||||
require.True(t, ae.IsRetryable())
|
||||
}
|
||||
|
||||
func Test_SendRichMessageDraft_NetworkError(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(nil, errors.New("dial tcp: timeout"))
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendRichMessageDraftParams{
|
||||
ChatID: 42,
|
||||
DraftID: 42,
|
||||
RichMessage: InputRichMessage{},
|
||||
}
|
||||
_, err := SendRichMessageDraft(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ne *client.NetworkError
|
||||
require.ErrorAs(t, err, &ne)
|
||||
}
|
||||
|
||||
func Test_SendRichMessageDraft_ParseError(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(genTestResp(200, `not json`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendRichMessageDraftParams{
|
||||
ChatID: 42,
|
||||
DraftID: 42,
|
||||
RichMessage: InputRichMessage{},
|
||||
}
|
||||
_, err := SendRichMessageDraft(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var pe *client.ParseError
|
||||
require.ErrorAs(t, err, &pe)
|
||||
}
|
||||
|
||||
func Test_SendRichMessageDraft_ContextCanceled(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(nil, context.Canceled).Maybe()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendRichMessageDraftParams{
|
||||
ChatID: 42,
|
||||
DraftID: 42,
|
||||
RichMessage: InputRichMessage{},
|
||||
}
|
||||
_, err := SendRichMessageDraft(ctx, bot, params)
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
}
|
||||
|
||||
// Test_SendRichMessageDraft_MissingRequiredFields exercises Telegram's server-side
|
||||
// validation: when a required field is omitted, Telegram returns 400 with
|
||||
// a description like "Bad Request: <field> is empty". The library must
|
||||
// surface this as *APIError with the ErrBadRequest sentinel.
|
||||
func Test_SendRichMessageDraft_MissingRequiredFields(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(
|
||||
genTestResp(200, `{"ok":false,"error_code":400,"description":"Bad Request: chat_id is empty"}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
// Send a Params with all required fields zeroed — simulates a caller
|
||||
// that forgot to populate them. The bot library marshals as-is and
|
||||
// surfaces Telegram's 400 reply.
|
||||
_, err := SendRichMessageDraft(context.Background(), bot, &SendRichMessageDraftParams{})
|
||||
require.Error(t, err)
|
||||
var ae *client.APIError
|
||||
require.ErrorAs(t, err, &ae)
|
||||
require.Equal(t, 400, ae.Code)
|
||||
require.True(t, errors.Is(err, client.ErrBadRequest))
|
||||
require.False(t, ae.IsRetryable())
|
||||
}
|
||||
|
||||
// Test_SendRichMessageDraft_Forbidden exercises the 403 path (bot blocked by user,
|
||||
// removed from chat, etc.). The library must surface the ErrForbidden
|
||||
// sentinel.
|
||||
func Test_SendRichMessageDraft_Forbidden(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(
|
||||
genTestResp(200, `{"ok":false,"error_code":403,"description":"Forbidden: bot was blocked by the user"}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendRichMessageDraftParams{
|
||||
ChatID: 42,
|
||||
DraftID: 42,
|
||||
RichMessage: InputRichMessage{},
|
||||
}
|
||||
_, err := SendRichMessageDraft(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ae *client.APIError
|
||||
require.ErrorAs(t, err, &ae)
|
||||
require.Equal(t, 403, ae.Code)
|
||||
require.True(t, errors.Is(err, client.ErrForbidden))
|
||||
require.False(t, ae.IsRetryable())
|
||||
}
|
||||
|
||||
// Test_SendRichMessageDraft_ServerError exercises the 5xx path. The library must
|
||||
// classify these as retryable so RetryDoer / user retry logic kicks in.
|
||||
func Test_SendRichMessageDraft_ServerError(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(
|
||||
genTestResp(200, `{"ok":false,"error_code":500,"description":"Internal server error"}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendRichMessageDraftParams{
|
||||
ChatID: 42,
|
||||
DraftID: 42,
|
||||
RichMessage: InputRichMessage{},
|
||||
}
|
||||
_, err := SendRichMessageDraft(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ae *client.APIError
|
||||
require.ErrorAs(t, err, &ae)
|
||||
require.Equal(t, 500, ae.Code)
|
||||
require.True(t, ae.IsRetryable(), "5xx must be retryable")
|
||||
}
|
||||
|
||||
func Test_AnswerInlineQuery_Success(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.MatchedBy(func(r *http.Request) bool {
|
||||
|
||||
+1554
-66
File diff suppressed because it is too large
Load Diff
+22
-4
@@ -215,6 +215,24 @@ func (b *Bot) buildRequest(ctx context.Context, method string, body io.Reader) (
|
||||
return req.WithContext(ctx), nil
|
||||
}
|
||||
|
||||
// bufferReadCloser exposes a *bytes.Buffer as io.ReadCloser without going
|
||||
// through io.NopCloser. Keeping the concrete *bytes.Buffer accessible lets
|
||||
// alternative HTTPDoers (e.g. FastHTTPDoer) type-assert and pass the
|
||||
// underlying bytes through to their native body-set APIs without copying.
|
||||
type bufferReadCloser struct {
|
||||
*bytes.Buffer
|
||||
}
|
||||
|
||||
func (bufferReadCloser) Close() error { return nil }
|
||||
|
||||
// readerReadCloser is the equivalent wrapper for *bytes.Reader (used by
|
||||
// the Marshal fallback path when the codec doesn't implement BodyEncoder).
|
||||
type readerReadCloser struct {
|
||||
*bytes.Reader
|
||||
}
|
||||
|
||||
func (readerReadCloser) Close() error { return nil }
|
||||
|
||||
// bodyToReadCloser wraps body for assignment to *http.Request.Body. The
|
||||
// type switch covers the body shapes encodeJSONBody returns: a pooled
|
||||
// *bytes.Buffer (BodyEncoder path or {} fast path) or a *bytes.Reader
|
||||
@@ -226,16 +244,16 @@ func bodyToReadCloser(body io.Reader) (io.ReadCloser, int64, func() (io.ReadClos
|
||||
case *bytes.Buffer:
|
||||
buf := v.Bytes()
|
||||
length := int64(len(buf))
|
||||
return io.NopCloser(v), length, func() (io.ReadCloser, error) {
|
||||
return io.NopCloser(bytes.NewReader(buf)), nil
|
||||
return bufferReadCloser{v}, length, func() (io.ReadCloser, error) {
|
||||
return readerReadCloser{bytes.NewReader(buf)}, nil
|
||||
}
|
||||
case *bytes.Reader:
|
||||
length := int64(v.Len())
|
||||
// Snapshot the reader's current data so GetBody returns a fresh one.
|
||||
snapshot := *v
|
||||
return io.NopCloser(v), length, func() (io.ReadCloser, error) {
|
||||
return readerReadCloser{v}, length, func() (io.ReadCloser, error) {
|
||||
s := snapshot
|
||||
return io.NopCloser(&s), nil
|
||||
return readerReadCloser{&s}, nil
|
||||
}
|
||||
default:
|
||||
// Unknown reader: no length, no replay. Should not happen with the
|
||||
|
||||
@@ -45,6 +45,7 @@ func (b *Bot) Logger() Logger { return b.logger }
|
||||
// NewDefaultHTTPDoer); the default codec wraps encoding/json; the default
|
||||
// logger discards records.
|
||||
func New(token string, opts ...Option) *Bot {
|
||||
fireTelemetryOnce()
|
||||
b := &Bot{
|
||||
token: token,
|
||||
base: defaultBaseURL,
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
// FastHTTPDoer is an HTTPDoer backed by github.com/valyala/fasthttp. It
|
||||
// trades net/http compatibility (and HTTP/2 support) for substantially
|
||||
// fewer allocations per request — fasthttp pools its Request and Response
|
||||
// objects and uses a zero-allocation HTTP/1.1 parser.
|
||||
//
|
||||
// Use it for high-throughput bots when GC pressure matters and you don't
|
||||
// need HTTP/2 or any net/http-only middleware (RoundTripper composition,
|
||||
// the OpenTelemetry httptrace family, etc.):
|
||||
//
|
||||
// bot := client.New(token, client.WithHTTPClient(client.NewFastHTTPDoer()))
|
||||
//
|
||||
// Wrap with RetryDoer the same way you would the default doer.
|
||||
type FastHTTPDoer struct {
|
||||
client *fasthttp.Client
|
||||
// readTimeout is the per-request timeout when the inbound ctx has no
|
||||
// deadline. Defaults to 30s; long-poll updates need a longer one — see
|
||||
// WithFastHTTPReadTimeout.
|
||||
readTimeout time.Duration
|
||||
}
|
||||
|
||||
// FastHTTPDoerOption configures a FastHTTPDoer.
|
||||
type FastHTTPDoerOption func(*FastHTTPDoer)
|
||||
|
||||
// WithFastHTTPClient swaps in a pre-configured *fasthttp.Client.
|
||||
// Useful for sharing a connection pool across multiple bots or applying
|
||||
// custom dial / TLS configuration.
|
||||
func WithFastHTTPClient(c *fasthttp.Client) FastHTTPDoerOption {
|
||||
return func(d *FastHTTPDoer) { d.client = c }
|
||||
}
|
||||
|
||||
// WithFastHTTPReadTimeout sets the per-request fallback timeout used when
|
||||
// the inbound context has no deadline. Long-poll callers should pass a
|
||||
// value larger than the long-poll timeout.
|
||||
func WithFastHTTPReadTimeout(t time.Duration) FastHTTPDoerOption {
|
||||
return func(d *FastHTTPDoer) { d.readTimeout = t }
|
||||
}
|
||||
|
||||
// NewFastHTTPDoer constructs a FastHTTPDoer with sensible defaults.
|
||||
func NewFastHTTPDoer(opts ...FastHTTPDoerOption) *FastHTTPDoer {
|
||||
d := &FastHTTPDoer{
|
||||
client: &fasthttp.Client{
|
||||
ReadTimeout: 90 * time.Second,
|
||||
WriteTimeout: 30 * time.Second,
|
||||
MaxIdleConnDuration: 90 * time.Second,
|
||||
},
|
||||
readTimeout: 30 * time.Second,
|
||||
}
|
||||
for _, o := range opts {
|
||||
o(d)
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// Do satisfies HTTPDoer by translating req into a pooled fasthttp.Request,
|
||||
// dispatching it, and returning a *http.Response whose Body releases the
|
||||
// pooled fasthttp.Response when Close is called.
|
||||
//
|
||||
// The conversion is intentionally minimal: URL goes via req.URL.RequestURI()
|
||||
// + Host (avoids re-parsing), header values move byte-for-byte, and the
|
||||
// body is taken straight from req.Body. *bytes.Buffer / *bytes.Reader are
|
||||
// recognised so we can pass the underlying bytes without io.ReadAll.
|
||||
func (d *FastHTTPDoer) Do(req *http.Request) (*http.Response, error) {
|
||||
if req == nil {
|
||||
return nil, errors.New("client: nil http.Request")
|
||||
}
|
||||
|
||||
fReq := fasthttp.AcquireRequest()
|
||||
defer fasthttp.ReleaseRequest(fReq)
|
||||
|
||||
fReq.SetRequestURI(req.URL.String())
|
||||
fReq.Header.SetMethod(req.Method)
|
||||
if req.Host != "" {
|
||||
fReq.Header.SetHost(req.Host)
|
||||
}
|
||||
for name, values := range req.Header {
|
||||
for _, v := range values {
|
||||
fReq.Header.Set(name, v)
|
||||
}
|
||||
}
|
||||
|
||||
if err := setFastHTTPBody(fReq, req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fResp := fasthttp.AcquireResponse()
|
||||
// fResp is released by fasthttpResponseBody.Close — caller is
|
||||
// expected to defer resp.Body.Close() per net/http contract.
|
||||
|
||||
deadline, hasDeadline := req.Context().Deadline()
|
||||
var err error
|
||||
if hasDeadline {
|
||||
err = d.client.DoDeadline(fReq, fResp, deadline)
|
||||
} else {
|
||||
err = d.client.DoTimeout(fReq, fResp, d.readTimeout)
|
||||
}
|
||||
if err != nil {
|
||||
fasthttp.ReleaseResponse(fResp)
|
||||
// Map fasthttp's timeout error to ctx.Err semantics so callers
|
||||
// can errors.Is(err, context.DeadlineExceeded).
|
||||
if hasDeadline && errors.Is(err, fasthttp.ErrTimeout) {
|
||||
return nil, context.DeadlineExceeded
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpResp := &http.Response{
|
||||
StatusCode: fResp.StatusCode(),
|
||||
Status: strconv.Itoa(fResp.StatusCode()) + " " + fastHTTPStatusText(fResp.StatusCode()),
|
||||
Header: make(http.Header, fResp.Header.Len()),
|
||||
ContentLength: int64(fResp.Header.ContentLength()),
|
||||
Body: &fasthttpResponseBody{resp: fResp, body: fResp.Body()},
|
||||
Request: req,
|
||||
Proto: "HTTP/1.1",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
}
|
||||
for k, v := range fResp.Header.All() {
|
||||
httpResp.Header.Add(string(k), string(v))
|
||||
}
|
||||
return httpResp, nil
|
||||
}
|
||||
|
||||
// setFastHTTPBody copies req.Body into fReq with the cheapest path that
|
||||
// preserves correctness. The bufferReadCloser / readerReadCloser shapes
|
||||
// produced by buildRequest expose their backing []byte directly so we
|
||||
// can call SetBodyRaw without io.ReadAll. Other body types fall through
|
||||
// to SetBodyStream when ContentLength is known, otherwise to ReadAll.
|
||||
func setFastHTTPBody(fReq *fasthttp.Request, req *http.Request) error {
|
||||
if req.Body == nil {
|
||||
return nil
|
||||
}
|
||||
switch v := req.Body.(type) {
|
||||
case bufferReadCloser:
|
||||
fReq.SetBodyRaw(v.Bytes())
|
||||
return nil
|
||||
case readerReadCloser:
|
||||
// *bytes.Reader.Bytes() returns the unread portion.
|
||||
size := v.Len()
|
||||
buf := make([]byte, size)
|
||||
_, err := v.Read(buf)
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return err
|
||||
}
|
||||
fReq.SetBodyRaw(buf)
|
||||
return nil
|
||||
default:
|
||||
if req.ContentLength > 0 {
|
||||
fReq.SetBodyStream(v, int(req.ContentLength))
|
||||
} else {
|
||||
body, err := io.ReadAll(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fReq.SetBodyRaw(body)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// fasthttpResponseBody adapts a pooled *fasthttp.Response so it satisfies
|
||||
// io.ReadCloser. The body bytes alias the response's internal buffer; when
|
||||
// Close fires we return the response to the fasthttp pool. Callers must
|
||||
// finish reading before invoking Close (the same contract net/http
|
||||
// requires).
|
||||
type fasthttpResponseBody struct {
|
||||
resp *fasthttp.Response
|
||||
body []byte
|
||||
pos int
|
||||
}
|
||||
|
||||
func (b *fasthttpResponseBody) Read(p []byte) (int, error) {
|
||||
if b.pos >= len(b.body) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n := copy(p, b.body[b.pos:])
|
||||
b.pos += n
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (b *fasthttpResponseBody) Close() error {
|
||||
if b.resp != nil {
|
||||
fasthttp.ReleaseResponse(b.resp)
|
||||
b.resp = nil
|
||||
b.body = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// fastHTTPStatusText returns the textual reason phrase for a status code,
|
||||
// matching the format net/http produces for *http.Response.Status. We
|
||||
// hard-code the common cases the Telegram Bot API actually returns; for
|
||||
// everything else we fall back to the stdlib helper.
|
||||
func fastHTTPStatusText(code int) string {
|
||||
switch code {
|
||||
case http.StatusOK:
|
||||
return "OK"
|
||||
case http.StatusBadRequest:
|
||||
return "Bad Request"
|
||||
case http.StatusUnauthorized:
|
||||
return "Unauthorized"
|
||||
case http.StatusForbidden:
|
||||
return "Forbidden"
|
||||
case http.StatusNotFound:
|
||||
return "Not Found"
|
||||
case http.StatusTooManyRequests:
|
||||
return "Too Many Requests"
|
||||
case http.StatusInternalServerError:
|
||||
return "Internal Server Error"
|
||||
case http.StatusBadGateway:
|
||||
return "Bad Gateway"
|
||||
case http.StatusServiceUnavailable:
|
||||
return "Service Unavailable"
|
||||
case http.StatusGatewayTimeout:
|
||||
return "Gateway Timeout"
|
||||
default:
|
||||
return http.StatusText(code)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestFastHTTPDoer_BasicRoundTrip(t *testing.T) {
|
||||
got := make(chan struct{ method, ct, body string }, 1)
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
got <- struct{ method, ct, body string }{r.Method, r.Header.Get("Content-Type"), string(body)}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = io.WriteString(w, `{"ok":true,"result":42}`)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
d := NewFastHTTPDoer()
|
||||
req, err := http.NewRequest(http.MethodPost, srv.URL+"/sendMessage", strings.NewReader(`{"chat_id":1,"text":"hi"}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req = req.WithContext(context.Background())
|
||||
|
||||
resp, err := d.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
t.Fatalf("status: got %d", resp.StatusCode)
|
||||
}
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if string(body) != `{"ok":true,"result":42}` {
|
||||
t.Fatalf("body: got %q", body)
|
||||
}
|
||||
|
||||
rec := <-got
|
||||
if rec.method != http.MethodPost {
|
||||
t.Fatalf("method: got %q", rec.method)
|
||||
}
|
||||
if rec.ct != "application/json" {
|
||||
t.Fatalf("content-type: got %q", rec.ct)
|
||||
}
|
||||
if rec.body != `{"chat_id":1,"text":"hi"}` {
|
||||
t.Fatalf("body: got %q", rec.body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFastHTTPDoer_HonoursContextDeadline(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
_, _ = io.WriteString(w, "ok")
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
d := NewFastHTTPDoer(WithFastHTTPReadTimeout(time.Hour))
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Millisecond)
|
||||
defer cancel()
|
||||
req, _ := http.NewRequest(http.MethodGet, srv.URL, nil)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
_, err := d.Do(req)
|
||||
if err == nil {
|
||||
t.Fatal("expected timeout error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFastHTTPDoer_IntegratesWithBot(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = io.WriteString(w, `{"ok":true,"result":{"message_id":7,"date":0,"text":"hi"}}`)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
bot := New("123:abc",
|
||||
WithBaseURL(srv.URL),
|
||||
WithHTTPClient(NewFastHTTPDoer()),
|
||||
)
|
||||
req := &benchSendReq{ChatID: 1, Text: "hi"}
|
||||
got, err := Call[*benchSendReq, benchMsgResp](context.Background(), bot, "sendMessage", req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.MessageID != 7 || got.Text != "hi" {
|
||||
t.Fatalf("got %+v", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
telemetry "github.com/lukaszraczylo/oss-telemetry"
|
||||
)
|
||||
|
||||
// telemetryOnce guards the single anonymous "library used" ping that is sent
|
||||
// on the first call to New. Long-running bots typically construct one Bot;
|
||||
// short-lived programs or test suites may construct many, but the Once gate
|
||||
// keeps the fire-and-forget call from amplifying into per-construction pings.
|
||||
var telemetryOnce sync.Once
|
||||
|
||||
// fireTelemetryOnce dispatches a fire-and-forget anonymous adoption ping.
|
||||
//
|
||||
// The call is failproof by contract of oss-telemetry: it never blocks New,
|
||||
// never panics, never returns errors, and silently no-ops if disabled or
|
||||
// if the network is unavailable.
|
||||
//
|
||||
// Opt-out is honored via any of these environment variables (case-insensitive
|
||||
// truthy values "1", "true", "yes", "on"):
|
||||
//
|
||||
// - DO_NOT_TRACK
|
||||
// - OSS_TELEMETRY_DISABLED
|
||||
// - GO_TELEGRAM_DISABLE_TELEMETRY
|
||||
//
|
||||
// See README §Telemetry for the full disclosure.
|
||||
func fireTelemetryOnce() {
|
||||
telemetryOnce.Do(func() {
|
||||
telemetry.SendForModule("go-telegram", "github.com/lukaszraczylo/go-telegram", Version)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
telemetry "github.com/lukaszraczylo/oss-telemetry"
|
||||
)
|
||||
|
||||
// TestMain disables outgoing telemetry for the duration of this package's
|
||||
// test suite. The library's own tests construct many Bot instances; without
|
||||
// this guard they would each contribute a real ping to the public endpoint.
|
||||
// End-user test suites that construct Bot are not affected by this — only
|
||||
// tests inside this package are.
|
||||
func TestMain(m *testing.M) {
|
||||
telemetry.Disable()
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
// TestFireTelemetryOnce_OnlyFiresOnce verifies the sync.Once gate. Even if
|
||||
// New is called repeatedly, the underlying telemetry.Send is invoked at most
|
||||
// once per process. We can't observe the network call directly (telemetry
|
||||
// is disabled here via TestMain) so we assert on the once-Do count via a
|
||||
// fresh local sync.Once paralleling the production one.
|
||||
func TestFireTelemetryOnce_OnlyFiresOnce(t *testing.T) {
|
||||
// Reset the package-level Once so this test starts from a clean state.
|
||||
telemetryOnce = sync.Once{}
|
||||
t.Cleanup(func() { telemetryOnce = sync.Once{} })
|
||||
|
||||
calls := 0
|
||||
probe := func() { telemetryOnce.Do(func() { calls++ }) }
|
||||
|
||||
for i := 0; i < 50; i++ {
|
||||
probe()
|
||||
}
|
||||
if calls != 1 {
|
||||
t.Fatalf("expected exactly 1 Once execution, got %d", calls)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNew_DoesNotPanicUnderRepeatedConstruction is a smoke test that
|
||||
// telemetry wiring does not affect New's existing contract. New must never
|
||||
// panic, regardless of telemetry state.
|
||||
func TestNew_DoesNotPanicUnderRepeatedConstruction(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Fatalf("New panicked: %v", r)
|
||||
}
|
||||
}()
|
||||
for i := 0; i < 20; i++ {
|
||||
_ = New("test-token-" + string(rune('A'+i)))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package client
|
||||
|
||||
// Version is a fallback version string used only when Go's build info is
|
||||
// unavailable (replace directives, detached `go run`) or has been overridden
|
||||
// via linker flags. The authoritative version forwarded to telemetry is
|
||||
// resolved at runtime by [telemetry.SendForModule] from the build info of
|
||||
// whatever binary linked this library, so this constant does NOT need to be
|
||||
// bumped on every release. Exposed as a var (not const) for ldflag override:
|
||||
//
|
||||
// go build -ldflags="-X github.com/lukaszraczylo/go-telegram/client.Version=1.2.3"
|
||||
var Version = "0.0.0-fallback"
|
||||
@@ -420,7 +420,7 @@ func funcs(plan *enumPlan) template.FuncMap {
|
||||
return goField(plan, parent, f)
|
||||
},
|
||||
"goFieldP": func(methodName string, f spec.Field) string {
|
||||
return goFieldX(plan, "", title(methodName)+"Params", f)
|
||||
return goFieldX(plan, methodEnumParent(methodName), title(methodName)+"Params", f)
|
||||
},
|
||||
"docComment": docComment,
|
||||
"isOptional": func(f spec.Field) bool { return !f.Required },
|
||||
@@ -432,7 +432,7 @@ func funcs(plan *enumPlan) template.FuncMap {
|
||||
return multipartFieldEntry(plan, parent, f)
|
||||
},
|
||||
"multipartFieldEntryP": func(methodName string, f spec.Field) string {
|
||||
return multipartFieldEntryX(plan, "", title(methodName)+"Params", f)
|
||||
return multipartFieldEntryX(plan, methodEnumParent(methodName), title(methodName)+"Params", f)
|
||||
},
|
||||
"multipartFileEntry": multipartFileEntry,
|
||||
"returnGoType": returnGoType,
|
||||
@@ -915,15 +915,15 @@ func buildUnionTypeSet(api *spec.API) map[string]bool {
|
||||
// used in generated test param literals. plan supplies typed-enum names
|
||||
// so a method-param sentinel for a ParseMode field becomes a typed
|
||||
// constant rather than a magic string.
|
||||
func makeSentinelValue(unionTypes map[string]bool, plan *enumPlan) func(spec.Field) string {
|
||||
return func(f spec.Field) string {
|
||||
return sentinelForField(f, unionTypes, plan)
|
||||
func makeSentinelValue(unionTypes map[string]bool, plan *enumPlan) func(string, spec.Field) string {
|
||||
return func(methodName string, f spec.Field) string {
|
||||
return sentinelForField(methodName, f, unionTypes, plan)
|
||||
}
|
||||
}
|
||||
|
||||
func sentinelForField(f spec.Field, unionTypes map[string]bool, plan *enumPlan) string {
|
||||
if name := plan.FieldEnum("", f.Name); name != "" && len(f.EnumValues) > 0 {
|
||||
return constName(name, f.EnumValues[0])
|
||||
func sentinelForField(methodName string, f spec.Field, unionTypes map[string]bool, plan *enumPlan) string {
|
||||
if name := plan.FieldEnum(methodEnumParent(methodName), f.Name); name != "" && len(f.EnumValues) > 0 {
|
||||
return plan.ConstFor(name, f.EnumValues[0])
|
||||
}
|
||||
tr := f.Type
|
||||
switch tr.Kind {
|
||||
|
||||
+73
-10
@@ -26,15 +26,21 @@ type enumPlan struct {
|
||||
}
|
||||
|
||||
// enumKey identifies a single Field occurrence so the emitter can look
|
||||
// up the enum name later. Parent is "" for method params (the method
|
||||
// doesn't share a Go type with the field).
|
||||
// up the enum name later. Parent is the type name for struct fields and
|
||||
// methodEnumParent(name) for method params, so two methods sharing a
|
||||
// param name never alias each other's enum (or non-enum) fields.
|
||||
func enumKey(parent, fieldName string) string { return parent + "::" + fieldName }
|
||||
|
||||
// methodEnumParent is the enum-plan key parent for a method's params.
|
||||
// The "method:" prefix keeps it disjoint from type names.
|
||||
func methodEnumParent(methodName string) string { return "method:" + methodName }
|
||||
|
||||
// planEnums walks the IR, decides on enum names, deduplicates, and
|
||||
// returns an enumPlan. All scraper-marked enum fields are covered.
|
||||
func planEnums(api *spec.API) *enumPlan {
|
||||
type ref struct {
|
||||
parent string
|
||||
parent string // naming parent ("" for method params)
|
||||
keyParent string // byField key parent (methodEnumParent(...) for method params)
|
||||
fieldName string
|
||||
jsonName string
|
||||
values []string
|
||||
@@ -52,7 +58,7 @@ func planEnums(api *spec.API) *enumPlan {
|
||||
}
|
||||
|
||||
var refs []ref
|
||||
collect := func(parent string, fields []spec.Field) {
|
||||
collect := func(parent, keyParent string, fields []spec.Field) {
|
||||
for _, f := range fields {
|
||||
if len(f.EnumValues) == 0 {
|
||||
continue
|
||||
@@ -62,6 +68,7 @@ func planEnums(api *spec.API) *enumPlan {
|
||||
}
|
||||
refs = append(refs, ref{
|
||||
parent: parent,
|
||||
keyParent: keyParent,
|
||||
fieldName: f.Name,
|
||||
jsonName: f.JSONName,
|
||||
values: f.EnumValues,
|
||||
@@ -70,13 +77,15 @@ func planEnums(api *spec.API) *enumPlan {
|
||||
}
|
||||
}
|
||||
for _, t := range api.Types {
|
||||
collect(t.Name, t.Fields)
|
||||
collect(t.Name, t.Name, t.Fields)
|
||||
}
|
||||
for _, m := range api.Methods {
|
||||
// Method params have no shared Go parent type, so we pass "" as
|
||||
// the parent. The default-name heuristic still produces the
|
||||
// right answer for ParseMode-style enums.
|
||||
collect("", m.Params)
|
||||
// Method params have no shared Go parent type, so the naming
|
||||
// parent is "" (the default-name heuristic still produces the
|
||||
// right answer for ParseMode-style enums), but the byField key
|
||||
// is method-scoped so a same-named non-enum param on another
|
||||
// method can never pick up this enum.
|
||||
collect("", methodEnumParent(m.Name), m.Params)
|
||||
}
|
||||
|
||||
// candidate name per ref (before collision resolution)
|
||||
@@ -196,7 +205,7 @@ func planEnums(api *spec.API) *enumPlan {
|
||||
}
|
||||
for i, r := range refs {
|
||||
name := groups[r.valueKey].name
|
||||
plan.byField[enumKey(r.parent, r.fieldName)] = name
|
||||
plan.byField[enumKey(r.keyParent, r.fieldName)] = name
|
||||
_ = i
|
||||
}
|
||||
for vk, g := range groups {
|
||||
@@ -389,6 +398,18 @@ func (p *enumPlan) FieldEnum(parent, fieldName string) string {
|
||||
return p.byField[enumKey(parent, fieldName)]
|
||||
}
|
||||
|
||||
// ConstFor returns the collision-resolved constant identifier for value
|
||||
// within the named enum declaration. Falls back to the plain constName
|
||||
// when the declaration is unknown (unit tests with partial plans).
|
||||
func (p *enumPlan) ConstFor(enumName, value string) string {
|
||||
if p != nil {
|
||||
if d, ok := p.decls[enumName]; ok {
|
||||
return d.ConstName(value)
|
||||
}
|
||||
}
|
||||
return constName(enumName, value)
|
||||
}
|
||||
|
||||
// defaultEnumName picks an initial Go enum name for a field. parse_mode
|
||||
// fields collapse to the canonical "ParseMode"; otherwise the name is
|
||||
// parent + PascalCase(jsonName).
|
||||
@@ -406,6 +427,48 @@ func constName(enumName, value string) string {
|
||||
return enumName + valuePascal(value)
|
||||
}
|
||||
|
||||
// ConstName returns the constant identifier for value within this enum,
|
||||
// resolving case-collisions between values that Pascal-case to the same
|
||||
// identifier (e.g. RichBlockListItem.type values "a" and "A"). Colliding
|
||||
// values get a Lower/Upper prefix on the value part based on the case of
|
||||
// the value's first letter; any residual collision gets a numeric suffix.
|
||||
func (e enumDecl) ConstName(value string) string {
|
||||
counts := map[string]int{}
|
||||
for _, v := range e.Values {
|
||||
counts[constName(e.Name, v)]++
|
||||
}
|
||||
plain := constName(e.Name, value)
|
||||
if counts[plain] <= 1 {
|
||||
return plain
|
||||
}
|
||||
cased := e.Name + casePrefix(value) + valuePascal(value)
|
||||
// Residual collision (same value text repeated, or Lower/Upper still
|
||||
// ambiguous): append the value's 1-based position for determinism.
|
||||
casedCounts := map[string]int{}
|
||||
pos := 0
|
||||
for i, v := range e.Values {
|
||||
if counts[constName(e.Name, v)] > 1 {
|
||||
casedCounts[e.Name+casePrefix(v)+valuePascal(v)]++
|
||||
}
|
||||
if v == value {
|
||||
pos = i + 1
|
||||
}
|
||||
}
|
||||
if casedCounts[cased] > 1 {
|
||||
return cased + itoa(pos)
|
||||
}
|
||||
return cased
|
||||
}
|
||||
|
||||
// casePrefix distinguishes case-colliding enum values: "Lower" when the
|
||||
// value starts with a lowercase letter, "Upper" otherwise.
|
||||
func casePrefix(v string) string {
|
||||
if v != "" && v[0] >= 'a' && v[0] <= 'z' {
|
||||
return "Lower"
|
||||
}
|
||||
return "Upper"
|
||||
}
|
||||
|
||||
func valuePascal(v string) string {
|
||||
// "image/jpeg" → "ImageOfJpeg"
|
||||
parts := strings.Split(v, "/")
|
||||
|
||||
@@ -8,6 +8,6 @@ package api
|
||||
type {{$e.Name}} string
|
||||
|
||||
const (
|
||||
{{range $v := $e.Values}} {{enumConstName $e.Name $v}} {{$e.Name}} = {{printf "%q" $v}}
|
||||
{{range $v := $e.Values}} {{$e.ConstName $v}} {{$e.Name}} = {{printf "%q" $v}}
|
||||
{{end}})
|
||||
{{end}}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/lukaszraczylo/go-telegram/internal/spec"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Regression: Bot API 9.x RichBlockListItem.type enumerates "a"/"A"/"i"/"I"/"1",
|
||||
// which Pascal-case to the same identifier. ConstName must disambiguate.
|
||||
func TestEnumDecl_ConstName_CaseCollision(t *testing.T) {
|
||||
d := enumDecl{Name: "RichBlockListItemType", Values: []string{"a", "A", "i", "I", "1"}}
|
||||
got := map[string]bool{}
|
||||
for _, v := range d.Values {
|
||||
n := d.ConstName(v)
|
||||
require.False(t, got[n], "duplicate const ident %q for value %q", n, v)
|
||||
got[n] = true
|
||||
}
|
||||
require.Equal(t, "RichBlockListItemTypeLowerA", d.ConstName("a"))
|
||||
require.Equal(t, "RichBlockListItemTypeUpperA", d.ConstName("A"))
|
||||
// Non-colliding values keep the plain name.
|
||||
require.Equal(t, "RichBlockListItemType1", d.ConstName("1"))
|
||||
}
|
||||
|
||||
// Regression: answerChatJoinRequestQuery has an enum param named "result";
|
||||
// answerGuestQuery has a NON-enum param also named "result". The enum plan
|
||||
// must scope method params per method so the enum never leaks onto the
|
||||
// other method's field.
|
||||
func TestPlanEnums_MethodParamsScopedPerMethod(t *testing.T) {
|
||||
api := &spec.API{
|
||||
Methods: []spec.MethodDecl{
|
||||
{
|
||||
Name: "answerChatJoinRequestQuery",
|
||||
Params: []spec.Field{{
|
||||
Name: "Result", JSONName: "result", Required: true,
|
||||
Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "string"},
|
||||
EnumValues: []string{"approve", "decline", "queue"},
|
||||
}},
|
||||
},
|
||||
{
|
||||
Name: "answerGuestQuery",
|
||||
Params: []spec.Field{{
|
||||
Name: "Result", JSONName: "result", Required: true,
|
||||
Type: spec.TypeRef{Kind: spec.KindNamed, Name: "InlineQueryResult"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
plan := planEnums(api)
|
||||
require.Equal(t, "Result",
|
||||
plan.FieldEnum(methodEnumParent("answerChatJoinRequestQuery"), "Result"))
|
||||
require.Empty(t,
|
||||
plan.FieldEnum(methodEnumParent("answerGuestQuery"), "Result"),
|
||||
"non-enum param must not inherit another method's enum")
|
||||
}
|
||||
@@ -574,7 +574,7 @@ func TestSentinelForField(t *testing.T) {
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
got := sentinelForField(c.field, unionTypes, nil)
|
||||
got := sentinelForField("testMethod", c.field, unionTypes, nil)
|
||||
require.Contains(t, got, c.contains, "sentinelForField for %q", c.name)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ func Test_{{$mName}}_Success(t *testing.T) {
|
||||
{{- if .Params}}
|
||||
params := &{{$mName}}Params{
|
||||
{{- range .Params}}{{if .Required}}
|
||||
{{.Name}}: {{sentinelValue .}},{{end}}
|
||||
{{.Name}}: {{sentinelValue $m.Name .}},{{end}}
|
||||
{{- end}}
|
||||
}
|
||||
_, err := {{$mName}}(context.Background(), bot, params)
|
||||
@@ -67,7 +67,7 @@ func Test_{{$mName}}_APIError(t *testing.T) {
|
||||
{{- if .Params}}
|
||||
params := &{{$mName}}Params{
|
||||
{{- range .Params}}{{if .Required}}
|
||||
{{.Name}}: {{sentinelValue .}},{{end}}
|
||||
{{.Name}}: {{sentinelValue $m.Name .}},{{end}}
|
||||
{{- end}}
|
||||
}
|
||||
_, err := {{$mName}}(context.Background(), bot, params)
|
||||
@@ -89,7 +89,7 @@ func Test_{{$mName}}_NetworkError(t *testing.T) {
|
||||
{{- if .Params}}
|
||||
params := &{{$mName}}Params{
|
||||
{{- range .Params}}{{if .Required}}
|
||||
{{.Name}}: {{sentinelValue .}},{{end}}
|
||||
{{.Name}}: {{sentinelValue $m.Name .}},{{end}}
|
||||
{{- end}}
|
||||
}
|
||||
_, err := {{$mName}}(context.Background(), bot, params)
|
||||
@@ -109,7 +109,7 @@ func Test_{{$mName}}_ParseError(t *testing.T) {
|
||||
{{- if .Params}}
|
||||
params := &{{$mName}}Params{
|
||||
{{- range .Params}}{{if .Required}}
|
||||
{{.Name}}: {{sentinelValue .}},{{end}}
|
||||
{{.Name}}: {{sentinelValue $m.Name .}},{{end}}
|
||||
{{- end}}
|
||||
}
|
||||
_, err := {{$mName}}(context.Background(), bot, params)
|
||||
@@ -132,7 +132,7 @@ func Test_{{$mName}}_ContextCanceled(t *testing.T) {
|
||||
{{- if .Params}}
|
||||
params := &{{$mName}}Params{
|
||||
{{- range .Params}}{{if .Required}}
|
||||
{{.Name}}: {{sentinelValue .}},{{end}}
|
||||
{{.Name}}: {{sentinelValue $m.Name .}},{{end}}
|
||||
{{- end}}
|
||||
}
|
||||
_, err := {{$mName}}(ctx, bot, params)
|
||||
@@ -177,7 +177,7 @@ func Test_{{$mName}}_Forbidden(t *testing.T) {
|
||||
{{- if .Params}}
|
||||
params := &{{$mName}}Params{
|
||||
{{- range .Params}}{{if .Required}}
|
||||
{{.Name}}: {{sentinelValue .}},{{end}}
|
||||
{{.Name}}: {{sentinelValue $m.Name .}},{{end}}
|
||||
{{- end}}
|
||||
}
|
||||
_, err := {{$mName}}(context.Background(), bot, params)
|
||||
@@ -203,7 +203,7 @@ func Test_{{$mName}}_ServerError(t *testing.T) {
|
||||
{{- if .Params}}
|
||||
params := &{{$mName}}Params{
|
||||
{{- range .Params}}{{if .Required}}
|
||||
{{.Name}}: {{sentinelValue .}},{{end}}
|
||||
{{.Name}}: {{sentinelValue $m.Name .}},{{end}}
|
||||
{{- end}}
|
||||
}
|
||||
_, err := {{$mName}}(context.Background(), bot, params)
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
- **Webhook decode** (small Update): ours is **12–20% faster** than every competitor and ties telego for the lowest alloc count (11).
|
||||
- **Large Update unmarshal** (entities + reply markup + photo array): ours is **17–34% faster** with the lowest ns/op of all six. telego edges us on alloc count (31 vs 34) at the cost of ~17% more time.
|
||||
- **API call round-trip** (mock HTTP server): telego wins (35.8 µs / 48 allocs) thanks to its `application/x-www-form-urlencoded` shortcut on simple methods; ours is **second** (39.8 µs / 102 allocs) and beats gotba, telebot, gobot.
|
||||
- **API call round-trip** (mock HTTP server): telego wins on allocs (35.8 µs / 48 allocs) because it uses fasthttp by default. We default to `net/http` (102 allocs / 39.8 µs); with the opt-in `client.NewFastHTTPDoer` we drop to 56 allocs / 6.6 KiB — within 8 of telego while keeping `*http.Request` semantics (RetryDoer, middleware, generated tests).
|
||||
- **Dispatcher routing** (20 handlers, last matches): ours is **2.5–2.8× faster than telebot and gobot** (98 ns vs 271 / 246 ns).
|
||||
|
||||
## How to read these numbers
|
||||
@@ -67,16 +67,19 @@ Build params → POST to local `httptest.Server` returning `{"ok":true,"result":
|
||||
|
||||
| Lib | sec/op | B/op | allocs/op |
|
||||
|-----|--------|------|-----------|
|
||||
| ours | 39.83 µs ±4% | 11.09 KiB | 102 |
|
||||
| ours (default `net/http`) | 39.83 µs ±4% | 11.09 KiB | 102 |
|
||||
| ours (opt-in `fasthttp`) | *time TBD on quiet box* | **6.62 KiB** | **56** |
|
||||
| gotba | 42.03 µs ±4% | 10.97 KiB | 125 |
|
||||
| telebot | 43.41 µs ±1% | 13.15 KiB | 139 |
|
||||
| gobot | 61.19 µs ±1% | 13.50 KiB | 176 |
|
||||
| **telego** | **35.84 µs ±1%** | **6.547 KiB** | **48** |
|
||||
| **telego** (uses fasthttp) | **35.84 µs ±1%** | **6.547 KiB** | **48** |
|
||||
| echotron | *skipped — see below* | — | — |
|
||||
|
||||
**Notes.**
|
||||
- telego wins by sending requests as `application/x-www-form-urlencoded` form data (cheaper than JSON marshal+upload for small payloads), plus an aggressive request-pool. We send JSON over `multipart/form-data` only when needed; for the JSON case our cost lands between gotba and telego.
|
||||
- Our request path runs through a manually-constructed `*http.Request` with a pre-parsed base URL (cached on `*Bot`), and request bodies are stream-encoded into a pooled `*bytes.Buffer` via the optional `BodyEncoder` codec extension. Together those skip the `url.Parse` + `*http.Request` bookkeeping that `http.NewRequestWithContext` runs on every call.
|
||||
- The headline alloc gap to telego turned out to be transport choice: telego defaults to [`fasthttp`](https://github.com/valyala/fasthttp), which pools requests/responses and skips most of `net/http`'s bookkeeping. Most of the other libs (and us, by default) use `net/http`.
|
||||
- We ship an opt-in fasthttp doer (`client.NewFastHTTPDoer`). Plug it via `client.WithHTTPClient(client.NewFastHTTPDoer())` and per-call allocs drop from 102 to **56** — within 8 of telego despite still going through our `*http.Request`-based `HTTPDoer` interface (kept that way so `RetryDoer`, custom transports, observability middleware, and the 1428 generated tests all keep working).
|
||||
- The default stays `net/http` because fasthttp is HTTP/1.1-only, can't be composed with the `RoundTripper` middleware ecosystem, and most users don't have the throughput to notice. Bots making thousands of API calls/sec should opt in.
|
||||
- Our `net/http` request path is already minimised: manually-constructed `*http.Request` with a pre-parsed base URL (cached on `*Bot`), and request bodies stream-encoded into a pooled `*bytes.Buffer` via the optional `BodyEncoder` codec extension. Those skip the `url.Parse` + `*http.Request` bookkeeping that `http.NewRequestWithContext` runs on every call.
|
||||
- gobot's higher cost comes from per-call goroutine + channel plumbing in its dispatcher path even when called directly.
|
||||
- **echotron skip:** echotron ships built-in dual-level rate limiting (30 req/s global, 20 req/min per chat) on its unexported `lclient` field. The setters that disable it (`SetGlobalRequestLimit`, `SetChatRequestLimit`) are methods on the unexported type with no public accessor through the `API` value, so the limiter cannot be bypassed without monkey-patching. A naive run produces ~3 s/op driven entirely by the per-chat token bucket — measuring rate limiting, not the library. We skip rather than publish a misleading number. The rate limiter is a feature of echotron and worth knowing about; it just makes a microbench unfair.
|
||||
|
||||
|
||||
+2637
-874
File diff suppressed because it is too large
Load Diff
@@ -32,6 +32,12 @@ Package client provides HTTP client primitives for the Telegram Bot API.
|
||||
- [func \(DefaultCodec\) Marshal\(v any\) \(\[\]byte, error\)](<#DefaultCodec.Marshal>)
|
||||
- [func \(DefaultCodec\) MarshalTo\(w io.Writer, v any\) error](<#DefaultCodec.MarshalTo>)
|
||||
- [func \(DefaultCodec\) Unmarshal\(data \[\]byte, v any\) error](<#DefaultCodec.Unmarshal>)
|
||||
- [type FastHTTPDoer](<#FastHTTPDoer>)
|
||||
- [func NewFastHTTPDoer\(opts ...FastHTTPDoerOption\) \*FastHTTPDoer](<#NewFastHTTPDoer>)
|
||||
- [func \(d \*FastHTTPDoer\) Do\(req \*http.Request\) \(\*http.Response, error\)](<#FastHTTPDoer.Do>)
|
||||
- [type FastHTTPDoerOption](<#FastHTTPDoerOption>)
|
||||
- [func WithFastHTTPClient\(c \*fasthttp.Client\) FastHTTPDoerOption](<#WithFastHTTPClient>)
|
||||
- [func WithFastHTTPReadTimeout\(t time.Duration\) FastHTTPDoerOption](<#WithFastHTTPReadTimeout>)
|
||||
- [type HTTPDoer](<#HTTPDoer>)
|
||||
- [type Logger](<#Logger>)
|
||||
- [type MultipartFile](<#MultipartFile>)
|
||||
@@ -81,6 +87,16 @@ var (
|
||||
)
|
||||
```
|
||||
|
||||
<a name="Version"></a>Version is a fallback version string used only when Go's build info is unavailable \(replace directives, detached \`go run\`\) or has been overridden via linker flags. The authoritative version forwarded to telemetry is resolved at runtime by \[telemetry.SendForModule\] from the build info of whatever binary linked this library, so this constant does NOT need to be bumped on every release. Exposed as a var \(not const\) for ldflag override:
|
||||
|
||||
```
|
||||
go build -ldflags="-X github.com/lukaszraczylo/go-telegram/client.Version=1.2.3"
|
||||
```
|
||||
|
||||
```go
|
||||
var Version = "0.0.0-fallback"
|
||||
```
|
||||
|
||||
<a name="Call"></a>
|
||||
## func [Call](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/call.go#L68>)
|
||||
|
||||
@@ -292,6 +308,72 @@ func (DefaultCodec) Unmarshal(data []byte, v any) error
|
||||
|
||||
Unmarshal calls json.Unmarshal.
|
||||
|
||||
<a name="FastHTTPDoer"></a>
|
||||
## type [FastHTTPDoer](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/fasthttp_doer.go#L26-L32>)
|
||||
|
||||
FastHTTPDoer is an HTTPDoer backed by github.com/valyala/fasthttp. It trades net/http compatibility \(and HTTP/2 support\) for substantially fewer allocations per request — fasthttp pools its Request and Response objects and uses a zero\-allocation HTTP/1.1 parser.
|
||||
|
||||
Use it for high\-throughput bots when GC pressure matters and you don't need HTTP/2 or any net/http\-only middleware \(RoundTripper composition, the OpenTelemetry httptrace family, etc.\):
|
||||
|
||||
```
|
||||
bot := client.New(token, client.WithHTTPClient(client.NewFastHTTPDoer()))
|
||||
```
|
||||
|
||||
Wrap with RetryDoer the same way you would the default doer.
|
||||
|
||||
```go
|
||||
type FastHTTPDoer struct {
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
```
|
||||
|
||||
<a name="NewFastHTTPDoer"></a>
|
||||
### func [NewFastHTTPDoer](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/fasthttp_doer.go#L52>)
|
||||
|
||||
```go
|
||||
func NewFastHTTPDoer(opts ...FastHTTPDoerOption) *FastHTTPDoer
|
||||
```
|
||||
|
||||
NewFastHTTPDoer constructs a FastHTTPDoer with sensible defaults.
|
||||
|
||||
<a name="FastHTTPDoer.Do"></a>
|
||||
### func \(\*FastHTTPDoer\) [Do](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/fasthttp_doer.go#L75>)
|
||||
|
||||
```go
|
||||
func (d *FastHTTPDoer) Do(req *http.Request) (*http.Response, error)
|
||||
```
|
||||
|
||||
Do satisfies HTTPDoer by translating req into a pooled fasthttp.Request, dispatching it, and returning a \*http.Response whose Body releases the pooled fasthttp.Response when Close is called.
|
||||
|
||||
The conversion is intentionally minimal: URL goes via req.URL.RequestURI\(\) \+ Host \(avoids re\-parsing\), header values move byte\-for\-byte, and the body is taken straight from req.Body. \*bytes.Buffer / \*bytes.Reader are recognised so we can pass the underlying bytes without io.ReadAll.
|
||||
|
||||
<a name="FastHTTPDoerOption"></a>
|
||||
## type [FastHTTPDoerOption](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/fasthttp_doer.go#L35>)
|
||||
|
||||
FastHTTPDoerOption configures a FastHTTPDoer.
|
||||
|
||||
```go
|
||||
type FastHTTPDoerOption func(*FastHTTPDoer)
|
||||
```
|
||||
|
||||
<a name="WithFastHTTPClient"></a>
|
||||
### func [WithFastHTTPClient](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/fasthttp_doer.go#L40>)
|
||||
|
||||
```go
|
||||
func WithFastHTTPClient(c *fasthttp.Client) FastHTTPDoerOption
|
||||
```
|
||||
|
||||
WithFastHTTPClient swaps in a pre\-configured \*fasthttp.Client. Useful for sharing a connection pool across multiple bots or applying custom dial / TLS configuration.
|
||||
|
||||
<a name="WithFastHTTPReadTimeout"></a>
|
||||
### func [WithFastHTTPReadTimeout](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/fasthttp_doer.go#L47>)
|
||||
|
||||
```go
|
||||
func WithFastHTTPReadTimeout(t time.Duration) FastHTTPDoerOption
|
||||
```
|
||||
|
||||
WithFastHTTPReadTimeout sets the per\-request fallback timeout used when the inbound context has no deadline. Long\-poll callers should pass a value larger than the long\-poll timeout.
|
||||
|
||||
<a name="HTTPDoer"></a>
|
||||
## type [HTTPDoer](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/httpclient.go#L13-L15>)
|
||||
|
||||
|
||||
@@ -4,13 +4,18 @@ go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/goccy/go-json v0.10.6
|
||||
github.com/lukaszraczylo/oss-telemetry v0.2.1
|
||||
github.com/stretchr/testify v1.9.0
|
||||
golang.org/x/net v0.54.0
|
||||
github.com/valyala/fasthttp v1.71.0
|
||||
golang.org/x/net v0.55.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.2.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/klauspost/compress v1.18.6 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
|
||||
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/lukaszraczylo/oss-telemetry v0.2.1 h1:6ULyfzXplpdmIY/i01OPM1jeod9+L1RAhI0jtbVnJI0=
|
||||
github.com/lukaszraczylo/oss-telemetry v0.2.1/go.mod h1:+Cn78qZo8rc3T9eZt0v3oICYRdd75wORtSidc8lNjDQ=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
|
||||
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.71.0 h1:tepR7H+Guh9VUqxxcPggYi8R3lGUu2Rsdh+z7/FCY3k=
|
||||
github.com/valyala/fasthttp v1.71.0/go.mod h1:z1sDUvOShhXq/C9mwH/fSm1Vb71tUJwmQdgkBrBNwnA=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
+2513
-148
File diff suppressed because it is too large
Load Diff
@@ -28,7 +28,8 @@ import (
|
||||
// Telegram-format token (digits:[\w-]{35}). telego enforces this format on construction.
|
||||
const benchToken = "1234567890:ABCDEFGHIJKLMNOPQRSTUVWXYZ_ab123456"
|
||||
|
||||
// BenchmarkCall_ours — lukaszraczylo/go-telegram.
|
||||
// BenchmarkCall_ours — lukaszraczylo/go-telegram with default net/http
|
||||
// transport. Most users land here.
|
||||
func BenchmarkCall_ours(b *testing.B) {
|
||||
srv := shared.NewMockServer()
|
||||
defer srv.Close()
|
||||
@@ -47,6 +48,30 @@ func BenchmarkCall_ours(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkCall_ours_fasthttp — lukaszraczylo/go-telegram with the
|
||||
// opt-in fasthttp transport (client.NewFastHTTPDoer). Apples-to-apples
|
||||
// against telego, which also runs on fasthttp by default.
|
||||
func BenchmarkCall_ours_fasthttp(b *testing.B) {
|
||||
srv := shared.NewMockServer()
|
||||
defer srv.Close()
|
||||
bot := client.New(benchToken,
|
||||
client.WithBaseURL(srv.URL),
|
||||
client.WithHTTPClient(client.NewFastHTTPDoer()),
|
||||
)
|
||||
ctx := context.Background()
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
_, err := api.SendMessage(ctx, bot, &api.SendMessageParams{
|
||||
ChatID: api.ChatIDFromInt(42),
|
||||
Text: "hello",
|
||||
})
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkCall_gotba — go-telegram-bot-api/telegram-bot-api/v5.
|
||||
func BenchmarkCall_gotba(b *testing.B) {
|
||||
srv := shared.NewMockServer()
|
||||
|
||||
@@ -14,20 +14,20 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/andybalholm/brotli v1.2.1 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.15.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/goccy/go-json v0.10.6 // indirect
|
||||
github.com/grbit/go-json v0.11.0 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/klauspost/compress v1.18.6 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.69.0 // indirect
|
||||
github.com/valyala/fasthttp v1.71.0 // indirect
|
||||
github.com/valyala/fastjson v1.6.10 // indirect
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
)
|
||||
|
||||
@@ -65,8 +65,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
|
||||
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
@@ -275,8 +275,8 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
||||
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
@@ -392,8 +392,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
|
||||
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
|
||||
github.com/valyala/fasthttp v1.71.0 h1:tepR7H+Guh9VUqxxcPggYi8R3lGUu2Rsdh+z7/FCY3k=
|
||||
github.com/valyala/fasthttp v1.71.0/go.mod h1:z1sDUvOShhXq/C9mwH/fSm1Vb71tUJwmQdgkBrBNwnA=
|
||||
github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4=
|
||||
github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
@@ -624,8 +624,8 @@ golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
snapshot_2026-05-08.html
|
||||
snapshot_2026-06-11.html
|
||||
+19872
File diff suppressed because one or more lines are too long
+19872
File diff suppressed because one or more lines are too long
+21862
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user