mirror of
https://github.com/lukaszraczylo/go-telegram.git
synced 2026-10-13 00:47:31 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53afd9890f | ||
|
|
d666cda3b1 | ||
|
|
fa84f08b25 | ||
|
|
54790f3e5b | ||
|
|
ab5f2c5700 | ||
|
|
926d51881d | ||
|
|
4511c823a3 | ||
|
|
7c5aca3bf8 | ||
|
|
4e6382ceeb | ||
|
|
b6881461fd | ||
|
|
6612c24dfc | ||
|
|
3620f8ce1e | ||
|
|
7eebdd1f40 | ||
|
|
9c363f470a | ||
|
|
5aabbe6151 | ||
|
|
cd05e6a426 | ||
|
|
140ea13bde | ||
|
|
0731f10907 | ||
|
|
6685414374 | ||
|
|
404411b20c | ||
|
|
8d85e61da5 | ||
|
|
609c4ce649 |
@@ -69,7 +69,13 @@ jobs:
|
||||
~/go/pkg/mod
|
||||
~/.cache/go-build
|
||||
key: ${{ runner.os }}-go-1.25-${{ hashFiles('**/go.sum') }}
|
||||
- run: go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
# Pinned, matching staticcheck above. govulncheck@latest is v1.8.0,
|
||||
# which declares go 1.26.0 and therefore cannot be installed by the
|
||||
# 1.25.x toolchain this workflow pins - the install step fails before
|
||||
# any scanning happens. v1.7.0 declares go 1.25.0. Pinning the binary
|
||||
# does not freeze the vulnerability database, which govulncheck
|
||||
# fetches at run time. Re-pin deliberately when bumping go-version.
|
||||
- run: go install golang.org/x/vuln/cmd/[email protected]
|
||||
- run: govulncheck ./...
|
||||
|
||||
gosec:
|
||||
|
||||
@@ -29,7 +29,12 @@ jobs:
|
||||
DATE=$(date +%Y-%m-%d)
|
||||
DEST="testdata/html/snapshot_${DATE}.html"
|
||||
curl -fsSL --user-agent "go-telegram codegen scraper" \
|
||||
https://core.telegram.org/bots/api > "$DEST"
|
||||
https://core.telegram.org/bots/api -o "$DEST.tmp"
|
||||
# The page ends with a "<!-- page generated in NN.NNms -->" comment
|
||||
# that changes on every fetch. Drop it, so an unchanged API produces
|
||||
# a byte-identical snapshot instead of a no-op update PR.
|
||||
sed '/<!-- page generated in .*ms -->/d' "$DEST.tmp" > "$DEST"
|
||||
rm "$DEST.tmp"
|
||||
ln -sf "snapshot_${DATE}.html" testdata/html/latest.html
|
||||
echo "date=$DATE" >> $GITHUB_OUTPUT
|
||||
echo "dest=$DEST" >> $GITHUB_OUTPUT
|
||||
@@ -76,7 +81,10 @@ jobs:
|
||||
id: diff
|
||||
run: |
|
||||
git status --porcelain
|
||||
if git diff --quiet internal/spec/api.json api/ testdata/html/; then
|
||||
# Gate on the generated surface only. A fresh snapshot file and the
|
||||
# latest.html symlink flip every run, so including testdata/html/
|
||||
# here would open a PR on a week when Telegram changed nothing.
|
||||
if git diff --quiet internal/spec/api.json api/ docs/reference/; then
|
||||
echo "no_changes=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
@@ -88,6 +96,7 @@ jobs:
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Open PR
|
||||
id: pr
|
||||
if: steps.diff.outputs.no_changes != 'true'
|
||||
uses: peter-evans/create-pull-request@v8
|
||||
with:
|
||||
@@ -115,4 +124,17 @@ jobs:
|
||||
|
||||
Inspect the IR diff (`internal/spec/api.json`) for added/changed/removed methods.
|
||||
|
||||
CI must pass before merge. Auto-merge: enable when satisfied with the diff.
|
||||
This PR merges itself once every step above passes. The gates are
|
||||
`make regen` (which fails on any audit fallback) and `go test -race ./...`,
|
||||
both run in this job before the PR opens.
|
||||
|
||||
# A PR opened with GITHUB_TOKEN does not trigger `ci.yml`, so waiting for
|
||||
# PR checks would wait forever. The gates already ran in this job: `make
|
||||
# regen` fails on any audit fallback, and the test step runs the full race
|
||||
# suite. Merging here therefore means "merged because the checks passed".
|
||||
# `ci.yml` then runs on the push to main and cuts the release.
|
||||
- name: Merge regen PR
|
||||
if: steps.diff.outputs.no_changes != 'true' && steps.pr.outputs.pull-request-number != ''
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: gh pr merge "${{ steps.pr.outputs.pull-request-number }}" --squash --delete-branch
|
||||
|
||||
@@ -371,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).
|
||||
|
||||
@@ -33,6 +33,14 @@ const (
|
||||
BotCommandScopeTypeChatMember BotCommandScopeType = "chat_member"
|
||||
)
|
||||
|
||||
type BotSubscriptionUpdatedState string
|
||||
|
||||
const (
|
||||
BotSubscriptionUpdatedStateCanceled BotSubscriptionUpdatedState = "canceled"
|
||||
BotSubscriptionUpdatedStateActive BotSubscriptionUpdatedState = "active"
|
||||
BotSubscriptionUpdatedStateFailed BotSubscriptionUpdatedState = "failed"
|
||||
)
|
||||
|
||||
type ChatBoostSourceKind string
|
||||
|
||||
const (
|
||||
@@ -133,6 +141,12 @@ const (
|
||||
InputMediaTypeVideo InputMediaType = "video"
|
||||
)
|
||||
|
||||
type InputMediaVoiceNoteType string
|
||||
|
||||
const (
|
||||
InputMediaVoiceNoteTypeVoiceNote InputMediaVoiceNoteType = "voice_note"
|
||||
)
|
||||
|
||||
type InputPaidMediaType string
|
||||
|
||||
const (
|
||||
@@ -158,6 +172,7 @@ type InputPollOptionMediaType string
|
||||
|
||||
const (
|
||||
InputPollOptionMediaTypeAnimation InputPollOptionMediaType = "animation"
|
||||
InputPollOptionMediaTypeLink InputPollOptionMediaType = "link"
|
||||
InputPollOptionMediaTypeLivePhoto InputPollOptionMediaType = "live_photo"
|
||||
InputPollOptionMediaTypeLocation InputPollOptionMediaType = "location"
|
||||
InputPollOptionMediaTypePhoto InputPollOptionMediaType = "photo"
|
||||
@@ -173,6 +188,35 @@ const (
|
||||
InputProfilePhotoTypeAnimated InputProfilePhotoType = "animated"
|
||||
)
|
||||
|
||||
type InputRichBlockType string
|
||||
|
||||
const (
|
||||
InputRichBlockTypeParagraph InputRichBlockType = "paragraph"
|
||||
InputRichBlockTypeHeading InputRichBlockType = "heading"
|
||||
InputRichBlockTypePre InputRichBlockType = "pre"
|
||||
InputRichBlockTypeFooter InputRichBlockType = "footer"
|
||||
InputRichBlockTypeDivider InputRichBlockType = "divider"
|
||||
InputRichBlockTypeMathematicalExpression InputRichBlockType = "mathematical_expression"
|
||||
InputRichBlockTypeAnchor InputRichBlockType = "anchor"
|
||||
InputRichBlockTypeList InputRichBlockType = "list"
|
||||
InputRichBlockTypeBlockquote InputRichBlockType = "blockquote"
|
||||
InputRichBlockTypeExpandableBlockquote InputRichBlockType = "expandable_blockquote"
|
||||
InputRichBlockTypePullquote InputRichBlockType = "pullquote"
|
||||
InputRichBlockTypeCollage InputRichBlockType = "collage"
|
||||
InputRichBlockTypeSlideshow InputRichBlockType = "slideshow"
|
||||
InputRichBlockTypeTable InputRichBlockType = "table"
|
||||
InputRichBlockTypeDetails InputRichBlockType = "details"
|
||||
InputRichBlockTypeMap InputRichBlockType = "map"
|
||||
InputRichBlockTypeButtons InputRichBlockType = "buttons"
|
||||
InputRichBlockTypeAnimation InputRichBlockType = "animation"
|
||||
InputRichBlockTypeAudio InputRichBlockType = "audio"
|
||||
InputRichBlockTypeDocument InputRichBlockType = "document"
|
||||
InputRichBlockTypePhoto InputRichBlockType = "photo"
|
||||
InputRichBlockTypeVideo InputRichBlockType = "video"
|
||||
InputRichBlockTypeVoiceNote InputRichBlockType = "voice_note"
|
||||
InputRichBlockTypeThinking InputRichBlockType = "thinking"
|
||||
)
|
||||
|
||||
type InputStickerFormat string
|
||||
|
||||
const (
|
||||
@@ -357,6 +401,14 @@ const (
|
||||
RefundedPaymentCurrencyXTR RefundedPaymentCurrency = "XTR"
|
||||
)
|
||||
|
||||
type Result string
|
||||
|
||||
const (
|
||||
ResultApprove Result = "approve"
|
||||
ResultDecline Result = "decline"
|
||||
ResultQueue Result = "queue"
|
||||
)
|
||||
|
||||
type RevenueWithdrawalStateKind string
|
||||
|
||||
const (
|
||||
@@ -365,6 +417,101 @@ const (
|
||||
RevenueWithdrawalStateKindFailed RevenueWithdrawalStateKind = "failed"
|
||||
)
|
||||
|
||||
type RichBlockButtonsAlign string
|
||||
|
||||
const (
|
||||
RichBlockButtonsAlignLeft RichBlockButtonsAlign = "left"
|
||||
RichBlockButtonsAlignCenter RichBlockButtonsAlign = "center"
|
||||
RichBlockButtonsAlignRight RichBlockButtonsAlign = "right"
|
||||
)
|
||||
|
||||
type RichBlockListItemType string
|
||||
|
||||
const (
|
||||
RichBlockListItemTypeLowerA RichBlockListItemType = "a"
|
||||
RichBlockListItemTypeUpperA RichBlockListItemType = "A"
|
||||
RichBlockListItemTypeLowerI RichBlockListItemType = "i"
|
||||
RichBlockListItemTypeUpperI RichBlockListItemType = "I"
|
||||
RichBlockListItemType1 RichBlockListItemType = "1"
|
||||
)
|
||||
|
||||
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"
|
||||
RichBlockTypeExpandableBlockquote RichBlockType = "expandable_blockquote"
|
||||
RichBlockTypePullquote RichBlockType = "pullquote"
|
||||
RichBlockTypeCollage RichBlockType = "collage"
|
||||
RichBlockTypeSlideshow RichBlockType = "slideshow"
|
||||
RichBlockTypeTable RichBlockType = "table"
|
||||
RichBlockTypeDetails RichBlockType = "details"
|
||||
RichBlockTypeMap RichBlockType = "map"
|
||||
RichBlockTypeButtons RichBlockType = "buttons"
|
||||
RichBlockTypeAnimation RichBlockType = "animation"
|
||||
RichBlockTypeAudio RichBlockType = "audio"
|
||||
RichBlockTypeDocument RichBlockType = "document"
|
||||
RichBlockTypePhoto RichBlockType = "photo"
|
||||
RichBlockTypeVideo RichBlockType = "video"
|
||||
RichBlockTypeVoiceNote RichBlockType = "voice_note"
|
||||
RichBlockTypeThinking RichBlockType = "thinking"
|
||||
)
|
||||
|
||||
type RichMessageButtonStyle string
|
||||
|
||||
const (
|
||||
RichMessageButtonStyleDanger RichMessageButtonStyle = "danger"
|
||||
RichMessageButtonStyleSuccess RichMessageButtonStyle = "success"
|
||||
RichMessageButtonStylePrimary RichMessageButtonStyle = "primary"
|
||||
RichMessageButtonStyleLink RichMessageButtonStyle = "link"
|
||||
)
|
||||
|
||||
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"
|
||||
RichTextTypeButton RichTextType = "button"
|
||||
RichTextTypeAnchor RichTextType = "anchor"
|
||||
RichTextTypeAnchorLink RichTextType = "anchor_link"
|
||||
RichTextTypeReference RichTextType = "reference"
|
||||
RichTextTypeReferenceLink RichTextType = "reference_link"
|
||||
)
|
||||
|
||||
type StickerType string
|
||||
|
||||
const (
|
||||
|
||||
+376
-48
@@ -195,6 +195,8 @@ type SendMessageParams 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"`
|
||||
// A JSON-serialized object containing the parameters of the ephemeral message to send
|
||||
EphemeralMessageParameters *EphemeralMessageParameters `json:"ephemeral_message_parameters,omitempty"`
|
||||
// Text of the message to be sent, 1-4096 characters after entities parsing
|
||||
Text string `json:"text"`
|
||||
// Mode for parsing entities in the message text. See formatting options for more details.
|
||||
@@ -261,7 +263,7 @@ func ForwardMessage(ctx context.Context, b *client.Bot, p *ForwardMessageParams)
|
||||
|
||||
// ForwardMessagesParams is the parameter set for ForwardMessages.
|
||||
//
|
||||
// Use this method to forward multiple messages of any kind. If some of the specified messages can't be found or forwarded, they are skipped. Service messages and messages with protected content can't be forwarded. Album grouping is kept for forwarded messages. On success, an array of MessageId of the sent messages is returned.
|
||||
// Use this method to forward multiple messages of any kind. If some of the specified messages can't be found or forwarded, they are skipped. Service messages and messages with protected content can't be forwarded. Album grouping is kept for forwarded messages. On success, an Array of MessageId of the sent messages is returned.
|
||||
type ForwardMessagesParams 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"`
|
||||
@@ -281,14 +283,14 @@ type ForwardMessagesParams struct {
|
||||
|
||||
// ForwardMessages calls the forwardMessages Telegram Bot API method.
|
||||
//
|
||||
// Use this method to forward multiple messages of any kind. If some of the specified messages can't be found or forwarded, they are skipped. Service messages and messages with protected content can't be forwarded. Album grouping is kept for forwarded messages. On success, an array of MessageId of the sent messages is returned.
|
||||
// Use this method to forward multiple messages of any kind. If some of the specified messages can't be found or forwarded, they are skipped. Service messages and messages with protected content can't be forwarded. Album grouping is kept for forwarded messages. On success, an Array of MessageId of the sent messages is returned.
|
||||
func ForwardMessages(ctx context.Context, b *client.Bot, p *ForwardMessagesParams) ([]MessageId, error) {
|
||||
return client.Call[*ForwardMessagesParams, []MessageId](ctx, b, "forwardMessages", p)
|
||||
}
|
||||
|
||||
// CopyMessageParams is the parameter set for CopyMessage.
|
||||
//
|
||||
// Use this method to copy messages of any kind. Service messages, paid media messages, giveaway messages, giveaway winners messages, and invoice messages can't be copied. A quiz poll can be copied only if the value of the field correct_option_id is known to the bot. The method is analogous to the method forwardMessage, but the copied message doesn't have a link to the original message. Returns the MessageId of the sent message on success.
|
||||
// Use this method to copy messages of any kind. Service messages, paid media messages, giveaway messages, giveaway winners messages, and invoice messages can't be copied. A quiz poll can be copied only if the value of the field correct_option_ids is known to the bot. The method is analogous to the method forwardMessage, but the copied message doesn't have a link to the original message. Returns the MessageId of the sent message on success.
|
||||
type CopyMessageParams 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"`
|
||||
@@ -308,7 +310,7 @@ type CopyMessageParams struct {
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the new caption, which can be specified instead of parse_mode
|
||||
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
|
||||
// Pass True, if the caption must be shown above the message media. Ignored if a new caption isn't specified.
|
||||
// Pass True if the caption must be shown above the message media. Ignored if a new caption isn't specified.
|
||||
ShowCaptionAboveMedia *bool `json:"show_caption_above_media,omitempty"`
|
||||
// Sends the message silently. Users will receive a notification with no sound.
|
||||
DisableNotification *bool `json:"disable_notification,omitempty"`
|
||||
@@ -328,14 +330,14 @@ type CopyMessageParams struct {
|
||||
|
||||
// CopyMessage calls the copyMessage Telegram Bot API method.
|
||||
//
|
||||
// Use this method to copy messages of any kind. Service messages, paid media messages, giveaway messages, giveaway winners messages, and invoice messages can't be copied. A quiz poll can be copied only if the value of the field correct_option_id is known to the bot. The method is analogous to the method forwardMessage, but the copied message doesn't have a link to the original message. Returns the MessageId of the sent message on success.
|
||||
// Use this method to copy messages of any kind. Service messages, paid media messages, giveaway messages, giveaway winners messages, and invoice messages can't be copied. A quiz poll can be copied only if the value of the field correct_option_ids is known to the bot. The method is analogous to the method forwardMessage, but the copied message doesn't have a link to the original message. Returns the MessageId of the sent message on success.
|
||||
func CopyMessage(ctx context.Context, b *client.Bot, p *CopyMessageParams) (*MessageId, error) {
|
||||
return client.Call[*CopyMessageParams, *MessageId](ctx, b, "copyMessage", p)
|
||||
}
|
||||
|
||||
// CopyMessagesParams is the parameter set for CopyMessages.
|
||||
//
|
||||
// Use this method to copy messages of any kind. If some of the specified messages can't be found or copied, they are skipped. Service messages, paid media messages, giveaway messages, giveaway winners messages, and invoice messages can't be copied. A quiz poll can be copied only if the value of the field correct_option_id is known to the bot. The method is analogous to the method forwardMessages, but the copied messages don't have a link to the original message. Album grouping is kept for copied messages. On success, an array of MessageId of the sent messages is returned.
|
||||
// Use this method to copy messages of any kind. If some of the specified messages can't be found or copied, they are skipped. Service messages, paid media messages, giveaway messages, giveaway winners messages, and invoice messages can't be copied. A quiz poll can be copied only if the value of the field correct_option_ids is known to the bot. The method is analogous to the method forwardMessages, but the copied messages don't have a link to the original message. Album grouping is kept for copied messages. On success, an Array of MessageId of the sent messages is returned.
|
||||
type CopyMessagesParams 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"`
|
||||
@@ -357,7 +359,7 @@ type CopyMessagesParams struct {
|
||||
|
||||
// CopyMessages calls the copyMessages Telegram Bot API method.
|
||||
//
|
||||
// Use this method to copy messages of any kind. If some of the specified messages can't be found or copied, they are skipped. Service messages, paid media messages, giveaway messages, giveaway winners messages, and invoice messages can't be copied. A quiz poll can be copied only if the value of the field correct_option_id is known to the bot. The method is analogous to the method forwardMessages, but the copied messages don't have a link to the original message. Album grouping is kept for copied messages. On success, an array of MessageId of the sent messages is returned.
|
||||
// Use this method to copy messages of any kind. If some of the specified messages can't be found or copied, they are skipped. Service messages, paid media messages, giveaway messages, giveaway winners messages, and invoice messages can't be copied. A quiz poll can be copied only if the value of the field correct_option_ids is known to the bot. The method is analogous to the method forwardMessages, but the copied messages don't have a link to the original message. Album grouping is kept for copied messages. On success, an Array of MessageId of the sent messages is returned.
|
||||
func CopyMessages(ctx context.Context, b *client.Bot, p *CopyMessagesParams) ([]MessageId, error) {
|
||||
return client.Call[*CopyMessagesParams, []MessageId](ctx, b, "copyMessages", p)
|
||||
}
|
||||
@@ -374,6 +376,8 @@ type SendPhotoParams 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"`
|
||||
// A JSON-serialized object containing the parameters of the ephemeral message to send
|
||||
EphemeralMessageParameters *EphemeralMessageParameters `json:"ephemeral_message_parameters,omitempty"`
|
||||
// Photo to send. Pass a file_id as String to send a photo that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a photo from the Internet, or upload a new photo using multipart/form-data. The photo must be at most 10 MB in size. The photo's width and height must not exceed 10000 in total. Width and height ratio must be at most 20. More information on Sending Files »
|
||||
Photo *InputFile `json:"photo"`
|
||||
// Photo caption (may also be used when resending photos by file_id), 0-1024 characters after entities parsing
|
||||
@@ -382,7 +386,7 @@ type SendPhotoParams struct {
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the caption, which can be specified instead of parse_mode
|
||||
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
|
||||
// Pass True, if the caption must be shown above the message media
|
||||
// Pass True if the caption must be shown above the message media
|
||||
ShowCaptionAboveMedia *bool `json:"show_caption_above_media,omitempty"`
|
||||
// Pass True if the photo needs to be covered with a spoiler animation
|
||||
HasSpoiler *bool `json:"has_spoiler,omitempty"`
|
||||
@@ -423,6 +427,11 @@ func (p *SendPhotoParams) MultipartFields() map[string]string {
|
||||
if p.DirectMessagesTopicID != nil {
|
||||
out["direct_messages_topic_id"] = strconv.FormatInt(*p.DirectMessagesTopicID, 10)
|
||||
}
|
||||
if p.EphemeralMessageParameters != nil {
|
||||
if b, _ := json.Marshal(p.EphemeralMessageParameters); len(b) > 0 {
|
||||
out["ephemeral_message_parameters"] = string(b)
|
||||
}
|
||||
}
|
||||
if p.Caption != "" {
|
||||
out["caption"] = p.Caption
|
||||
}
|
||||
@@ -502,6 +511,8 @@ type SendLivePhotoParams 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"`
|
||||
// A JSON-serialized object containing the parameters of the ephemeral message to send
|
||||
EphemeralMessageParameters *EphemeralMessageParameters `json:"ephemeral_message_parameters,omitempty"`
|
||||
// Live photo video to send. The video must be no longer than 10 seconds and must not exceed 10 MB in size. Pass a file_id as String to send a video that exists on the Telegram servers (recommended) or upload a new video using multipart/form-data. More information on Sending Files ». Sending live photos by a URL is currently unsupported.
|
||||
LivePhoto *InputFile `json:"live_photo"`
|
||||
// The static photo to send. Pass a file_id as String to send a photo that exists on the Telegram servers (recommended) or upload a new video using multipart/form-data. More information on Sending Files ». Sending live photos by a URL is currently unsupported.
|
||||
@@ -512,7 +523,7 @@ type SendLivePhotoParams struct {
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the caption, which can be specified instead of parse_mode
|
||||
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
|
||||
// Pass True, if the caption must be shown above the message media
|
||||
// Pass True if the caption must be shown above the message media
|
||||
ShowCaptionAboveMedia *bool `json:"show_caption_above_media,omitempty"`
|
||||
// Pass True if the video needs to be covered with a spoiler animation
|
||||
HasSpoiler *bool `json:"has_spoiler,omitempty"`
|
||||
@@ -556,6 +567,11 @@ func (p *SendLivePhotoParams) MultipartFields() map[string]string {
|
||||
if p.DirectMessagesTopicID != nil {
|
||||
out["direct_messages_topic_id"] = strconv.FormatInt(*p.DirectMessagesTopicID, 10)
|
||||
}
|
||||
if p.EphemeralMessageParameters != nil {
|
||||
if b, _ := json.Marshal(p.EphemeralMessageParameters); len(b) > 0 {
|
||||
out["ephemeral_message_parameters"] = string(b)
|
||||
}
|
||||
}
|
||||
if p.Caption != "" {
|
||||
out["caption"] = p.Caption
|
||||
}
|
||||
@@ -643,6 +659,8 @@ type SendAudioParams 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"`
|
||||
// A JSON-serialized object containing the parameters of the ephemeral message to send
|
||||
EphemeralMessageParameters *EphemeralMessageParameters `json:"ephemeral_message_parameters,omitempty"`
|
||||
// Audio file to send. Pass a file_id as String to send an audio file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an audio file from the Internet, or upload a new one using multipart/form-data. More information on Sending Files »
|
||||
Audio *InputFile `json:"audio"`
|
||||
// Audio caption, 0-1024 characters after entities parsing
|
||||
@@ -699,6 +717,11 @@ func (p *SendAudioParams) MultipartFields() map[string]string {
|
||||
if p.DirectMessagesTopicID != nil {
|
||||
out["direct_messages_topic_id"] = strconv.FormatInt(*p.DirectMessagesTopicID, 10)
|
||||
}
|
||||
if p.EphemeralMessageParameters != nil {
|
||||
if b, _ := json.Marshal(p.EphemeralMessageParameters); len(b) > 0 {
|
||||
out["ephemeral_message_parameters"] = string(b)
|
||||
}
|
||||
}
|
||||
if p.Caption != "" {
|
||||
out["caption"] = p.Caption
|
||||
}
|
||||
@@ -789,6 +812,8 @@ type SendDocumentParams 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"`
|
||||
// A JSON-serialized object containing the parameters of the ephemeral message to send
|
||||
EphemeralMessageParameters *EphemeralMessageParameters `json:"ephemeral_message_parameters,omitempty"`
|
||||
// File to send. Pass a file_id as String to send a file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. More information on Sending Files »
|
||||
Document *InputFile `json:"document"`
|
||||
// Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass “attach://<file_attach_name>” if the thumbnail was uploaded using multipart/form-data under <file_attach_name>. More information on Sending Files »
|
||||
@@ -841,6 +866,11 @@ func (p *SendDocumentParams) MultipartFields() map[string]string {
|
||||
if p.DirectMessagesTopicID != nil {
|
||||
out["direct_messages_topic_id"] = strconv.FormatInt(*p.DirectMessagesTopicID, 10)
|
||||
}
|
||||
if p.EphemeralMessageParameters != nil {
|
||||
if b, _ := json.Marshal(p.EphemeralMessageParameters); len(b) > 0 {
|
||||
out["ephemeral_message_parameters"] = string(b)
|
||||
}
|
||||
}
|
||||
if p.Caption != "" {
|
||||
out["caption"] = p.Caption
|
||||
}
|
||||
@@ -924,6 +954,8 @@ type SendVideoParams 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"`
|
||||
// A JSON-serialized object containing the parameters of the ephemeral message to send
|
||||
EphemeralMessageParameters *EphemeralMessageParameters `json:"ephemeral_message_parameters,omitempty"`
|
||||
// Video to send. Pass a file_id as String to send a video that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a video from the Internet, or upload a new video using multipart/form-data. More information on Sending Files »
|
||||
Video *InputFile `json:"video"`
|
||||
// Duration of sent video in seconds
|
||||
@@ -944,7 +976,7 @@ type SendVideoParams struct {
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the caption, which can be specified instead of parse_mode
|
||||
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
|
||||
// Pass True, if the caption must be shown above the message media
|
||||
// Pass True if the caption must be shown above the message media
|
||||
ShowCaptionAboveMedia *bool `json:"show_caption_above_media,omitempty"`
|
||||
// Pass True if the video needs to be covered with a spoiler animation
|
||||
HasSpoiler *bool `json:"has_spoiler,omitempty"`
|
||||
@@ -993,6 +1025,11 @@ func (p *SendVideoParams) MultipartFields() map[string]string {
|
||||
if p.DirectMessagesTopicID != nil {
|
||||
out["direct_messages_topic_id"] = strconv.FormatInt(*p.DirectMessagesTopicID, 10)
|
||||
}
|
||||
if p.EphemeralMessageParameters != nil {
|
||||
if b, _ := json.Marshal(p.EphemeralMessageParameters); len(b) > 0 {
|
||||
out["ephemeral_message_parameters"] = string(b)
|
||||
}
|
||||
}
|
||||
if p.Duration != nil {
|
||||
out["duration"] = strconv.FormatInt(*p.Duration, 10)
|
||||
}
|
||||
@@ -1101,6 +1138,8 @@ type SendAnimationParams 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"`
|
||||
// A JSON-serialized object containing the parameters of the ephemeral message to send
|
||||
EphemeralMessageParameters *EphemeralMessageParameters `json:"ephemeral_message_parameters,omitempty"`
|
||||
// Animation to send. Pass a file_id as String to send an animation that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an animation from the Internet, or upload a new animation using multipart/form-data. More information on Sending Files »
|
||||
Animation *InputFile `json:"animation"`
|
||||
// Duration of sent animation in seconds
|
||||
@@ -1117,7 +1156,7 @@ type SendAnimationParams struct {
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the caption, which can be specified instead of parse_mode
|
||||
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
|
||||
// Pass True, if the caption must be shown above the message media
|
||||
// Pass True if the caption must be shown above the message media
|
||||
ShowCaptionAboveMedia *bool `json:"show_caption_above_media,omitempty"`
|
||||
// Pass True if the animation needs to be covered with a spoiler animation
|
||||
HasSpoiler *bool `json:"has_spoiler,omitempty"`
|
||||
@@ -1161,6 +1200,11 @@ func (p *SendAnimationParams) MultipartFields() map[string]string {
|
||||
if p.DirectMessagesTopicID != nil {
|
||||
out["direct_messages_topic_id"] = strconv.FormatInt(*p.DirectMessagesTopicID, 10)
|
||||
}
|
||||
if p.EphemeralMessageParameters != nil {
|
||||
if b, _ := json.Marshal(p.EphemeralMessageParameters); len(b) > 0 {
|
||||
out["ephemeral_message_parameters"] = string(b)
|
||||
}
|
||||
}
|
||||
if p.Duration != nil {
|
||||
out["duration"] = strconv.FormatInt(*p.Duration, 10)
|
||||
}
|
||||
@@ -1256,6 +1300,8 @@ type SendVoiceParams 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"`
|
||||
// A JSON-serialized object containing the parameters of the ephemeral message to send
|
||||
EphemeralMessageParameters *EphemeralMessageParameters `json:"ephemeral_message_parameters,omitempty"`
|
||||
// Audio file to send. Pass a file_id as String to send a file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. More information on Sending Files »
|
||||
Voice *InputFile `json:"voice"`
|
||||
// Voice message caption, 0-1024 characters after entities parsing
|
||||
@@ -1303,6 +1349,11 @@ func (p *SendVoiceParams) MultipartFields() map[string]string {
|
||||
if p.DirectMessagesTopicID != nil {
|
||||
out["direct_messages_topic_id"] = strconv.FormatInt(*p.DirectMessagesTopicID, 10)
|
||||
}
|
||||
if p.EphemeralMessageParameters != nil {
|
||||
if b, _ := json.Marshal(p.EphemeralMessageParameters); len(b) > 0 {
|
||||
out["ephemeral_message_parameters"] = string(b)
|
||||
}
|
||||
}
|
||||
if p.Caption != "" {
|
||||
out["caption"] = p.Caption
|
||||
}
|
||||
@@ -1369,7 +1420,7 @@ func SendVoice(ctx context.Context, b *client.Bot, p *SendVoiceParams) (*Message
|
||||
|
||||
// SendVideoNoteParams is the parameter set for SendVideoNote.
|
||||
//
|
||||
// As of v.4.0, Telegram clients support rounded square MPEG4 videos of up to 1 minute long. Use this method to send video messages. On success, the sent Message is returned.
|
||||
// Use this method to send a rounded square MPEG4 video of up to 1 minute long. On success, the sent Message is returned.
|
||||
type SendVideoNoteParams struct {
|
||||
// Unique identifier of the business connection on behalf of which the message will be sent
|
||||
BusinessConnectionID string `json:"business_connection_id,omitempty"`
|
||||
@@ -1379,6 +1430,8 @@ 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"`
|
||||
// A JSON-serialized object containing the parameters of the ephemeral message to send
|
||||
EphemeralMessageParameters *EphemeralMessageParameters `json:"ephemeral_message_parameters,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.
|
||||
VideoNote *InputFile `json:"video_note"`
|
||||
// Duration of sent video in seconds
|
||||
@@ -1427,6 +1480,11 @@ func (p *SendVideoNoteParams) MultipartFields() map[string]string {
|
||||
if p.DirectMessagesTopicID != nil {
|
||||
out["direct_messages_topic_id"] = strconv.FormatInt(*p.DirectMessagesTopicID, 10)
|
||||
}
|
||||
if p.EphemeralMessageParameters != nil {
|
||||
if b, _ := json.Marshal(p.EphemeralMessageParameters); len(b) > 0 {
|
||||
out["ephemeral_message_parameters"] = string(b)
|
||||
}
|
||||
}
|
||||
if p.Duration != nil {
|
||||
out["duration"] = strconv.FormatInt(*p.Duration, 10)
|
||||
}
|
||||
@@ -1485,7 +1543,7 @@ func (p *SendVideoNoteParams) MultipartFiles() []client.MultipartFile {
|
||||
|
||||
// SendVideoNote calls the sendVideoNote Telegram Bot API method.
|
||||
//
|
||||
// As of v.4.0, Telegram clients support rounded square MPEG4 videos of up to 1 minute long. Use this method to send video messages. On success, the sent Message is returned.
|
||||
// Use this method to send a rounded square MPEG4 video of up to 1 minute long. On success, the sent Message is returned.
|
||||
func SendVideoNote(ctx context.Context, b *client.Bot, p *SendVideoNoteParams) (*Message, error) {
|
||||
return client.Call[*SendVideoNoteParams, *Message](ctx, b, "sendVideoNote", p)
|
||||
}
|
||||
@@ -1504,7 +1562,7 @@ type SendPaidMediaParams struct {
|
||||
DirectMessagesTopicID *int64 `json:"direct_messages_topic_id,omitempty"`
|
||||
// The number of Telegram Stars that must be paid to buy access to the media; 1-25000
|
||||
StarCount int64 `json:"star_count"`
|
||||
// A JSON-serialized array describing the media to be sent; up to 10 items
|
||||
// A JSON-serialized Array describing the media to be sent; up to 10 items
|
||||
Media []InputPaidMedia `json:"media"`
|
||||
// Bot-defined paid media payload, 0-128 bytes. This will not be displayed to the user, use it for your internal processes.
|
||||
Payload string `json:"payload,omitempty"`
|
||||
@@ -1514,7 +1572,7 @@ type SendPaidMediaParams struct {
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the caption, which can be specified instead of parse_mode
|
||||
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
|
||||
// Pass True, if the caption must be shown above the message media
|
||||
// Pass True if the caption must be shown above the message media
|
||||
ShowCaptionAboveMedia *bool `json:"show_caption_above_media,omitempty"`
|
||||
// Sends the message silently. Users will receive a notification with no sound.
|
||||
DisableNotification *bool `json:"disable_notification,omitempty"`
|
||||
@@ -1611,7 +1669,7 @@ func SendPaidMedia(ctx context.Context, b *client.Bot, p *SendPaidMediaParams) (
|
||||
|
||||
// SendMediaGroupParams is the parameter set for SendMediaGroup.
|
||||
//
|
||||
// Use this method to send a group of photos, live photos, videos, documents or audios as an album. Documents and audio files can be only grouped in an album with messages of the same type. On success, an array of Message objects that were sent is returned.
|
||||
// Use this method to send a group of photos, live photos, videos, documents or audios as an album. Documents and audio files can be only grouped in an album with messages of the same type. On success, an Array of Message objects that were sent is returned.
|
||||
type SendMediaGroupParams struct {
|
||||
// Unique identifier of the business connection on behalf of which the message will be sent
|
||||
BusinessConnectionID string `json:"business_connection_id,omitempty"`
|
||||
@@ -1621,7 +1679,7 @@ type SendMediaGroupParams struct {
|
||||
MessageThreadID *int64 `json:"message_thread_id,omitempty"`
|
||||
// Identifier of the direct messages topic to which the messages will be sent; required if the messages are sent to a direct messages chat
|
||||
DirectMessagesTopicID *int64 `json:"direct_messages_topic_id,omitempty"`
|
||||
// A JSON-serialized array describing messages to be sent, must include 2-10 items
|
||||
// A JSON-serialized Array describing messages to be sent, must include 2-10 items
|
||||
Media []any `json:"media"`
|
||||
// Sends messages silently. Users will receive a notification with no sound.
|
||||
DisableNotification *bool `json:"disable_notification,omitempty"`
|
||||
@@ -1684,7 +1742,7 @@ func (p *SendMediaGroupParams) MultipartFiles() []client.MultipartFile {
|
||||
|
||||
// SendMediaGroup calls the sendMediaGroup Telegram Bot API method.
|
||||
//
|
||||
// Use this method to send a group of photos, live photos, videos, documents or audios as an album. Documents and audio files can be only grouped in an album with messages of the same type. On success, an array of Message objects that were sent is returned.
|
||||
// Use this method to send a group of photos, live photos, videos, documents or audios as an album. Documents and audio files can be only grouped in an album with messages of the same type. On success, an Array of Message objects that were sent is returned.
|
||||
func SendMediaGroup(ctx context.Context, b *client.Bot, p *SendMediaGroupParams) ([]Message, error) {
|
||||
return client.Call[*SendMediaGroupParams, []Message](ctx, b, "sendMediaGroup", p)
|
||||
}
|
||||
@@ -1701,13 +1759,15 @@ type SendLocationParams 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"`
|
||||
// A JSON-serialized object containing the parameters of the ephemeral message to send
|
||||
EphemeralMessageParameters *EphemeralMessageParameters `json:"ephemeral_message_parameters,omitempty"`
|
||||
// Latitude of the location
|
||||
Latitude float64 `json:"latitude"`
|
||||
// Longitude of the location
|
||||
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), must be between 60 and 86400, or 0x7FFFFFFF for live locations that can be edited indefinitely. Must be 0 for ephemeral messages.
|
||||
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"`
|
||||
@@ -1748,6 +1808,8 @@ type SendVenueParams 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"`
|
||||
// A JSON-serialized object containing the parameters of the ephemeral message to send
|
||||
EphemeralMessageParameters *EphemeralMessageParameters `json:"ephemeral_message_parameters,omitempty"`
|
||||
// Latitude of the venue
|
||||
Latitude float64 `json:"latitude"`
|
||||
// Longitude of the venue
|
||||
@@ -1799,6 +1861,8 @@ type SendContactParams 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"`
|
||||
// A JSON-serialized object containing the parameters of the ephemeral message to send
|
||||
EphemeralMessageParameters *EphemeralMessageParameters `json:"ephemeral_message_parameters,omitempty"`
|
||||
// Contact's phone number
|
||||
PhoneNumber string `json:"phone_number"`
|
||||
// Contact's first name
|
||||
@@ -1852,17 +1916,17 @@ type SendPollParams struct {
|
||||
IsAnonymous *bool `json:"is_anonymous,omitempty"`
|
||||
// Poll type, “quiz” or “regular”, defaults to “regular”
|
||||
Type string `json:"type,omitempty"`
|
||||
// Pass True, if the poll allows multiple answers, defaults to False
|
||||
// Pass True if the poll allows multiple answers, defaults to False
|
||||
AllowsMultipleAnswers *bool `json:"allows_multiple_answers,omitempty"`
|
||||
// Pass True, if the poll allows to change chosen answer options, defaults to False for quizzes and to True for regular polls
|
||||
// Pass True if the poll allows to change chosen answer options, defaults to False for quizzes and to True for regular polls
|
||||
AllowsRevoting *bool `json:"allows_revoting,omitempty"`
|
||||
// Pass True, if the poll options must be shown in random order
|
||||
// Pass True if the poll options must be shown in random order
|
||||
ShuffleOptions *bool `json:"shuffle_options,omitempty"`
|
||||
// Pass True, if answer options can be added to the poll after creation; not supported for anonymous polls and quizzes
|
||||
// Pass True if answer options can be added to the poll after creation; not supported for anonymous polls and quizzes
|
||||
AllowAddingOptions *bool `json:"allow_adding_options,omitempty"`
|
||||
// Pass True, if poll results must be shown only after the poll closes
|
||||
// Pass True if poll results must be shown only after the poll closes
|
||||
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
|
||||
// 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. 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"`
|
||||
@@ -1985,7 +2049,7 @@ type SendMessageDraftParams struct {
|
||||
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 of drafts with the same identifier are animated.
|
||||
// Unique identifier of the message draft; must be non-zero. Changes to drafts with the same identifier are animated. Otherwise, the draft is replaced without animation.
|
||||
DraftID int64 `json:"draft_id"`
|
||||
// Text of the message to be sent, 0-4096 characters after entities parsing. Pass an empty text to show a “Thinking…” placeholder.
|
||||
Text string `json:"text,omitempty"`
|
||||
@@ -1993,6 +2057,10 @@ type SendMessageDraftParams struct {
|
||||
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"`
|
||||
// Pass True to show the user a button to stop further drafts. The bot will receive an Update “stopped_message_generation” if the user presses the button.
|
||||
CanStop *bool `json:"can_stop,omitempty"`
|
||||
// Pass True to keep the draft in the chat when the button is pressed. The draft will still disappear after a short time or if the bot sends a message. To fully preserve the partial draft, the bot should send it as a new message.
|
||||
KeepOnStop *bool `json:"keep_on_stop,omitempty"`
|
||||
}
|
||||
|
||||
// SendMessageDraft calls the sendMessageDraft Telegram Bot API method.
|
||||
@@ -2227,6 +2295,8 @@ type PromoteChatMemberParams struct {
|
||||
CanManageDirectMessages *bool `json:"can_manage_direct_messages,omitempty"`
|
||||
// Pass True if the administrator can edit the tags of regular members; for groups and supergroups only
|
||||
CanManageTags *bool `json:"can_manage_tags,omitempty"`
|
||||
// Pass True if the administrator can manage chat welcome messages or directly send them in the case of bots
|
||||
CanSendWelcomeMessages *bool `json:"can_send_welcome_messages,omitempty"`
|
||||
}
|
||||
|
||||
// PromoteChatMember calls the promoteChatMember Telegram Bot API method.
|
||||
@@ -2483,6 +2553,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. Call answerChatJoinRequestQuery to resolve the join request query based on the user interaction with the Mini App. Returns True on success.
|
||||
type SendChatJoinRequestWebAppParams struct {
|
||||
// Unique identifier of the join request query
|
||||
ChatJoinRequestQueryID string `json:"chat_join_request_query_id"`
|
||||
// An HTTPS URL of a Web App to be opened with additional data as specified in Initializing Web Apps
|
||||
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. Call answerChatJoinRequestQuery to resolve the join request query based on the user interaction with the Mini App. 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.
|
||||
@@ -2697,7 +2801,7 @@ func GetChatAdministrators(ctx context.Context, b *client.Bot, p *GetChatAdminis
|
||||
|
||||
// GetChatMemberCountParams is the parameter set for GetChatMemberCount.
|
||||
//
|
||||
// Use this method to get the number of members in a chat. Returns Int on success.
|
||||
// Use this method to get the number of members in a chat. Returns Integer on success.
|
||||
type GetChatMemberCountParams struct {
|
||||
// Unique identifier for the target chat or username of the target supergroup or channel in the format @username
|
||||
ChatID ChatID `json:"chat_id"`
|
||||
@@ -2705,7 +2809,7 @@ type GetChatMemberCountParams struct {
|
||||
|
||||
// GetChatMemberCount calls the getChatMemberCount Telegram Bot API method.
|
||||
//
|
||||
// Use this method to get the number of members in a chat. Returns Int on success.
|
||||
// Use this method to get the number of members in a chat. Returns Integer on success.
|
||||
func GetChatMemberCount(ctx context.Context, b *client.Bot, p *GetChatMemberCountParams) (int64, error) {
|
||||
return client.Call[*GetChatMemberCountParams, int64](ctx, b, "getChatMemberCount", p)
|
||||
}
|
||||
@@ -2733,7 +2837,7 @@ func GetChatMember(ctx context.Context, b *client.Bot, p *GetChatMemberParams) (
|
||||
|
||||
// GetUserPersonalChatMessagesParams is the parameter set for GetUserPersonalChatMessages.
|
||||
//
|
||||
// Use this method to get the last messages from the personal chat (i.e., the chat currently added to their profile) of a given user. On success, an array of Message objects is returned.
|
||||
// Use this method to get the last messages from the personal chat (i.e., the chat currently added to their profile) of a given user. On success, an Array of Message objects is returned.
|
||||
type GetUserPersonalChatMessagesParams struct {
|
||||
// Unique identifier for the target user
|
||||
UserID int64 `json:"user_id"`
|
||||
@@ -2743,7 +2847,7 @@ type GetUserPersonalChatMessagesParams struct {
|
||||
|
||||
// GetUserPersonalChatMessages calls the getUserPersonalChatMessages Telegram Bot API method.
|
||||
//
|
||||
// Use this method to get the last messages from the personal chat (i.e., the chat currently added to their profile) of a given user. On success, an array of Message objects is returned.
|
||||
// Use this method to get the last messages from the personal chat (i.e., the chat currently added to their profile) of a given user. On success, an Array of Message objects is returned.
|
||||
func GetUserPersonalChatMessages(ctx context.Context, b *client.Bot, p *GetUserPersonalChatMessagesParams) ([]Message, error) {
|
||||
return client.Call[*GetUserPersonalChatMessagesParams, []Message](ctx, b, "getUserPersonalChatMessages", p)
|
||||
}
|
||||
@@ -3004,11 +3108,11 @@ type AnswerCallbackQueryParams struct {
|
||||
CallbackQueryID string `json:"callback_query_id"`
|
||||
// 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.
|
||||
// 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"`
|
||||
// URL that will be opened by the user's client. If you have created a Game and accepted the conditions via @BotFather, specify the URL that opens your game - note that this will only work if the query comes from a callback_game button.Otherwise, you may use links like t.me/your_bot?start=XXXX that open your bot with a parameter.
|
||||
URL string `json:"url,omitempty"`
|
||||
// The maximum amount of time in seconds that the result of the callback query may be cached client-side. Telegram apps will support caching starting in version 3.14. Defaults to 0.
|
||||
// The maximum amount of time in seconds that the result of the callback query may be cached client-side. Defaults to 0.
|
||||
CacheTime *int64 `json:"cache_time,omitempty"`
|
||||
}
|
||||
|
||||
@@ -3120,9 +3224,9 @@ func GetManagedBotAccessSettings(ctx context.Context, b *client.Bot, p *GetManag
|
||||
type SetManagedBotAccessSettingsParams struct {
|
||||
// User identifier of the managed bot whose access settings will be changed
|
||||
UserID int64 `json:"user_id"`
|
||||
// Pass True, if only selected users can access the bot. The bot's owner can always access it.
|
||||
// Pass True if only selected users can access the bot. The bot's owner can always access it.
|
||||
IsAccessRestricted bool `json:"is_access_restricted"`
|
||||
// A JSON-serialized list of up to 10 identifiers of users who will have access to the bot in addition to its owner. Ignored if is_access_restricted is false.
|
||||
// A JSON-serialized list of up to 10 identifiers of users who will have access to the bot in addition to its owner. Ignored if is_access_restricted is False.
|
||||
AddedUserIds []int64 `json:"added_user_ids,omitempty"`
|
||||
}
|
||||
|
||||
@@ -3638,7 +3742,7 @@ func RemoveBusinessAccountProfilePhoto(ctx context.Context, b *client.Bot, p *Re
|
||||
type SetBusinessAccountGiftSettingsParams struct {
|
||||
// Unique identifier of the business connection
|
||||
BusinessConnectionID string `json:"business_connection_id"`
|
||||
// Pass True, if a button for sending a gift to the user or by the business account must always be shown in the input field
|
||||
// Pass True if a button for sending a gift to the user or by the business account must always be shown in the input field
|
||||
ShowGiftButton bool `json:"show_gift_button"`
|
||||
// Types of gifts accepted by the business account
|
||||
AcceptedGiftTypes AcceptedGiftTypes `json:"accepted_gift_types"`
|
||||
@@ -4004,7 +4108,7 @@ 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"`
|
||||
@@ -4014,21 +4118,23 @@ type EditMessageTextParams struct {
|
||||
MessageID *int64 `json:"message_id,omitempty"`
|
||||
// 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"`
|
||||
// New rich content of the message; required if text isn't specified. Direct upload of new files and explicit upload of files by a URL isn't supported when an inline message is edited.
|
||||
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)
|
||||
}
|
||||
@@ -4051,7 +4157,7 @@ type EditMessageCaptionParams struct {
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the caption, which can be specified instead of parse_mode
|
||||
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
|
||||
// Pass True, if the caption must be shown above the message media. Supported only for animation, photo and video messages.
|
||||
// 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
|
||||
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
|
||||
@@ -4066,7 +4172,7 @@ 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"`
|
||||
@@ -4076,7 +4182,7 @@ type EditMessageMediaParams struct {
|
||||
MessageID *int64 `json:"message_id,omitempty"`
|
||||
// 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
|
||||
// A JSON-serialized object for the new media content of the message
|
||||
Media InputMedia `json:"media"`
|
||||
// A JSON-serialized object for a new inline keyboard
|
||||
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
|
||||
@@ -4121,7 +4227,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)
|
||||
}
|
||||
@@ -4251,6 +4357,138 @@ func StopPoll(ctx context.Context, b *client.Bot, p *StopPollParams) (*Poll, err
|
||||
return client.Call[*StopPollParams, *Poll](ctx, b, "stopPoll", p)
|
||||
}
|
||||
|
||||
// EditEphemeralMessageTextParams is the parameter set for EditEphemeralMessageText.
|
||||
//
|
||||
// Use this method to edit an ephemeral text or rich message. Note that it is not guaranteed that the user will receive the message edit event, especially if they are offline. On success, True is returned.
|
||||
type EditEphemeralMessageTextParams struct {
|
||||
// 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 who received the message
|
||||
ReceiverUserID int64 `json:"receiver_user_id"`
|
||||
// Identifier of the ephemeral message to edit
|
||||
EphemeralMessageID int64 `json:"ephemeral_message_id"`
|
||||
// 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"`
|
||||
// New rich content of the message; required if text isn't specified
|
||||
RichMessage *InputRichMessage `json:"rich_message,omitempty"`
|
||||
// Link preview generation options for the message
|
||||
LinkPreviewOptions *LinkPreviewOptions `json:"link_preview_options,omitempty"`
|
||||
// A JSON-serialized object for an inline keyboard
|
||||
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
// EditEphemeralMessageText calls the editEphemeralMessageText Telegram Bot API method.
|
||||
//
|
||||
// Use this method to edit an ephemeral text or rich message. Note that it is not guaranteed that the user will receive the message edit event, especially if they are offline. On success, True is returned.
|
||||
func EditEphemeralMessageText(ctx context.Context, b *client.Bot, p *EditEphemeralMessageTextParams) (bool, error) {
|
||||
return client.Call[*EditEphemeralMessageTextParams, bool](ctx, b, "editEphemeralMessageText", p)
|
||||
}
|
||||
|
||||
// EditEphemeralMessageMediaParams is the parameter set for EditEphemeralMessageMedia.
|
||||
//
|
||||
// Use this method to edit the media of an ephemeral message. Note that it is not guaranteed that the user will receive the message edit event, especially if they are offline. On success, True is returned.
|
||||
type EditEphemeralMessageMediaParams struct {
|
||||
// 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 who received the message
|
||||
ReceiverUserID int64 `json:"receiver_user_id"`
|
||||
// Identifier of the ephemeral message to edit
|
||||
EphemeralMessageID int64 `json:"ephemeral_message_id"`
|
||||
// A JSON-serialized object for the new media content of the message
|
||||
Media InputMedia `json:"media"`
|
||||
// A JSON-serialized object for an inline keyboard
|
||||
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
// HasFile reports whether a multipart upload is required.
|
||||
func (p *EditEphemeralMessageMediaParams) HasFile() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// MultipartFields returns the non-file fields used in the multipart body.
|
||||
func (p *EditEphemeralMessageMediaParams) MultipartFields() map[string]string {
|
||||
out := map[string]string{}
|
||||
out["chat_id"] = p.ChatID.String()
|
||||
out["receiver_user_id"] = strconv.FormatInt(p.ReceiverUserID, 10)
|
||||
out["ephemeral_message_id"] = strconv.FormatInt(p.EphemeralMessageID, 10)
|
||||
if b, _ := json.Marshal(p.Media); len(b) > 0 {
|
||||
out["media"] = string(b)
|
||||
}
|
||||
if p.ReplyMarkup != nil {
|
||||
if b, _ := json.Marshal(p.ReplyMarkup); len(b) > 0 {
|
||||
out["reply_markup"] = string(b)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// MultipartFiles returns the file parts.
|
||||
func (p *EditEphemeralMessageMediaParams) MultipartFiles() []client.MultipartFile {
|
||||
var files []client.MultipartFile
|
||||
return files
|
||||
}
|
||||
|
||||
// EditEphemeralMessageMedia calls the editEphemeralMessageMedia Telegram Bot API method.
|
||||
//
|
||||
// Use this method to edit the media of an ephemeral message. Note that it is not guaranteed that the user will receive the message edit event, especially if they are offline. On success, True is returned.
|
||||
func EditEphemeralMessageMedia(ctx context.Context, b *client.Bot, p *EditEphemeralMessageMediaParams) (bool, error) {
|
||||
return client.Call[*EditEphemeralMessageMediaParams, bool](ctx, b, "editEphemeralMessageMedia", p)
|
||||
}
|
||||
|
||||
// EditEphemeralMessageCaptionParams is the parameter set for EditEphemeralMessageCaption.
|
||||
//
|
||||
// Use this method to edit the caption of an ephemeral message. Note that it is not guaranteed that the user will receive the message edit event, especially if they are offline. On success, True is returned.
|
||||
type EditEphemeralMessageCaptionParams struct {
|
||||
// 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 who received the message
|
||||
ReceiverUserID int64 `json:"receiver_user_id"`
|
||||
// Identifier of the ephemeral message to edit
|
||||
EphemeralMessageID int64 `json:"ephemeral_message_id"`
|
||||
// New caption of the message, 0-1024 characters after entities parsing
|
||||
Caption string `json:"caption,omitempty"`
|
||||
// Mode for parsing entities in the message caption. See formatting options for more details.
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in the caption, which can be specified instead of parse_mode
|
||||
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
|
||||
// Pass True if the caption must be shown above the message media. Supported only for animation, photo and video messages.
|
||||
ShowCaptionAboveMedia *bool `json:"show_caption_above_media,omitempty"`
|
||||
// A JSON-serialized object for an inline keyboard
|
||||
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
// EditEphemeralMessageCaption calls the editEphemeralMessageCaption Telegram Bot API method.
|
||||
//
|
||||
// Use this method to edit the caption of an ephemeral message. Note that it is not guaranteed that the user will receive the message edit event, especially if they are offline. On success, True is returned.
|
||||
func EditEphemeralMessageCaption(ctx context.Context, b *client.Bot, p *EditEphemeralMessageCaptionParams) (bool, error) {
|
||||
return client.Call[*EditEphemeralMessageCaptionParams, bool](ctx, b, "editEphemeralMessageCaption", p)
|
||||
}
|
||||
|
||||
// EditEphemeralMessageReplyMarkupParams is the parameter set for EditEphemeralMessageReplyMarkup.
|
||||
//
|
||||
// Use this method to edit only the reply markup of an ephemeral message. Note that it is not guaranteed that the user will receive the message edit event, especially if they are offline. On success, True is returned.
|
||||
type EditEphemeralMessageReplyMarkupParams struct {
|
||||
// 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 who received the message
|
||||
ReceiverUserID int64 `json:"receiver_user_id"`
|
||||
// Identifier of the ephemeral message to edit
|
||||
EphemeralMessageID int64 `json:"ephemeral_message_id"`
|
||||
// A JSON-serialized object for an inline keyboard
|
||||
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
// EditEphemeralMessageReplyMarkup calls the editEphemeralMessageReplyMarkup Telegram Bot API method.
|
||||
//
|
||||
// Use this method to edit only the reply markup of an ephemeral message. Note that it is not guaranteed that the user will receive the message edit event, especially if they are offline. On success, True is returned.
|
||||
func EditEphemeralMessageReplyMarkup(ctx context.Context, b *client.Bot, p *EditEphemeralMessageReplyMarkupParams) (bool, error) {
|
||||
return client.Call[*EditEphemeralMessageReplyMarkupParams, bool](ctx, b, "editEphemeralMessageReplyMarkup", p)
|
||||
}
|
||||
|
||||
// ApproveSuggestedPostParams is the parameter set for ApproveSuggestedPost.
|
||||
//
|
||||
// Use this method to approve a suggested post in a direct messages chat. The bot must have the 'can_post_messages' administrator right in the corresponding channel chat. Returns True on success.
|
||||
@@ -4323,6 +4561,25 @@ func DeleteMessages(ctx context.Context, b *client.Bot, p *DeleteMessagesParams)
|
||||
return client.Call[*DeleteMessagesParams, bool](ctx, b, "deleteMessages", p)
|
||||
}
|
||||
|
||||
// DeleteEphemeralMessageParams is the parameter set for DeleteEphemeralMessage.
|
||||
//
|
||||
// Use this method to delete an ephemeral message. Note that it is not guaranteed that the user will receive the message deletion event, especially if they are offline. Returns True on success.
|
||||
type DeleteEphemeralMessageParams struct {
|
||||
// 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 who received the message
|
||||
ReceiverUserID int64 `json:"receiver_user_id"`
|
||||
// Identifier of the ephemeral message to delete
|
||||
EphemeralMessageID int64 `json:"ephemeral_message_id"`
|
||||
}
|
||||
|
||||
// DeleteEphemeralMessage calls the deleteEphemeralMessage Telegram Bot API method.
|
||||
//
|
||||
// Use this method to delete an ephemeral message. Note that it is not guaranteed that the user will receive the message deletion event, especially if they are offline. Returns True on success.
|
||||
func DeleteEphemeralMessage(ctx context.Context, b *client.Bot, p *DeleteEphemeralMessageParams) (bool, error) {
|
||||
return client.Call[*DeleteEphemeralMessageParams, bool](ctx, b, "deleteEphemeralMessage", p)
|
||||
}
|
||||
|
||||
// DeleteMessageReactionParams is the parameter set for DeleteMessageReaction.
|
||||
//
|
||||
// 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.
|
||||
@@ -4375,6 +4632,8 @@ type SendStickerParams 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"`
|
||||
// A JSON-serialized object containing the parameters of the ephemeral message to send
|
||||
EphemeralMessageParameters *EphemeralMessageParameters `json:"ephemeral_message_parameters,omitempty"`
|
||||
// Sticker to send. Pass a file_id as String to send a file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a .WEBP sticker from the Internet, or upload a new .WEBP, .TGS, or .WEBM sticker using multipart/form-data. More information on Sending Files ». Video and animated stickers can't be sent via an HTTP URL.
|
||||
Sticker *InputFile `json:"sticker"`
|
||||
// Emoji associated with the sticker; only for just uploaded stickers
|
||||
@@ -4416,6 +4675,11 @@ func (p *SendStickerParams) MultipartFields() map[string]string {
|
||||
if p.DirectMessagesTopicID != nil {
|
||||
out["direct_messages_topic_id"] = strconv.FormatInt(*p.DirectMessagesTopicID, 10)
|
||||
}
|
||||
if p.EphemeralMessageParameters != nil {
|
||||
if b, _ := json.Marshal(p.EphemeralMessageParameters); len(b) > 0 {
|
||||
out["ephemeral_message_parameters"] = string(b)
|
||||
}
|
||||
}
|
||||
if p.Emoji != "" {
|
||||
out["emoji"] = p.Emoji
|
||||
}
|
||||
@@ -4795,13 +5059,77 @@ 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. Bot can send rich messages on behalf of a business account only if the corresponding user can send rich messages.
|
||||
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"`
|
||||
// A JSON-serialized object containing the parameters of the ephemeral message to send
|
||||
EphemeralMessageParameters *EphemeralMessageParameters `json:"ephemeral_message_parameters,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. Otherwise, the draft is replaced without animation.
|
||||
DraftID int64 `json:"draft_id"`
|
||||
// The partial message to be streamed. Direct upload of new files and explicit upload of files by a URL isn't supported.
|
||||
RichMessage InputRichMessage `json:"rich_message"`
|
||||
// Pass True to show the user a button to stop further drafts. The bot will receive an Update “stopped_message_generation” if the user presses the button.
|
||||
CanStop *bool `json:"can_stop,omitempty"`
|
||||
// Pass True to keep the draft in the chat when the button is pressed. The draft will still disappear after a short time or if the bot sends a message. To fully preserve the partial draft, the bot should send it as a new message.
|
||||
KeepOnStop *bool `json:"keep_on_stop,omitempty"`
|
||||
}
|
||||
|
||||
// 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.
|
||||
type AnswerInlineQueryParams struct {
|
||||
// Unique identifier for the answered query
|
||||
InlineQueryID string `json:"inline_query_id"`
|
||||
// A JSON-serialized array of results for the inline query
|
||||
// A JSON-serialized Array of results for the inline query
|
||||
Results []InlineQueryResult `json:"results"`
|
||||
// The maximum amount of time in seconds that the result of the inline query may be cached on the server. Defaults to 300.
|
||||
CacheTime *int64 `json:"cache_time,omitempty"`
|
||||
@@ -4844,7 +5172,7 @@ type SendInvoiceParams struct {
|
||||
Prices []LabeledPrice `json:"prices"`
|
||||
// The maximum accepted amount for tips in the smallest units of the currency (integer, not float/double). For example, for a maximum tip of US$ 1.45 pass max_tip_amount = 145. See the exp parameter in currencies.json, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies). Defaults to 0. Not supported for payments in Telegram Stars.
|
||||
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.
|
||||
// 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.
|
||||
StartParameter string `json:"start_parameter,omitempty"`
|
||||
@@ -4917,7 +5245,7 @@ type CreateInvoiceLinkParams struct {
|
||||
SubscriptionPeriod *int64 `json:"subscription_period,omitempty"`
|
||||
// The maximum accepted amount for tips in the smallest units of the currency (integer, not float/double). For example, for a maximum tip of US$ 1.45 pass max_tip_amount = 145. See the exp parameter in currencies.json, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies). Defaults to 0. Not supported for payments in Telegram Stars.
|
||||
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.
|
||||
// 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"`
|
||||
// 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"`
|
||||
@@ -4960,7 +5288,7 @@ type AnswerShippingQueryParams struct {
|
||||
ShippingQueryID string `json:"shipping_query_id"`
|
||||
// Pass True if delivery to the specified address is possible and False if there are any problems (for example, if delivery to the specified address is not possible)
|
||||
Ok bool `json:"ok"`
|
||||
// Required if ok is True. A JSON-serialized array of available shipping options.
|
||||
// Required if ok is True. A JSON-serialized Array of available shipping options.
|
||||
ShippingOptions []ShippingOption `json:"shipping_options,omitempty"`
|
||||
// Required if ok is False. Error message in human readable form that explains why it is impossible to complete the order (e.g. “Sorry, delivery to your desired address is unavailable”). Telegram will display this message to the user.
|
||||
ErrorMessage string `json:"error_message,omitempty"`
|
||||
@@ -5065,7 +5393,7 @@ func EditUserStarSubscription(ctx context.Context, b *client.Bot, p *EditUserSta
|
||||
type SetPassportDataErrorsParams struct {
|
||||
// User identifier
|
||||
UserID int64 `json:"user_id"`
|
||||
// A JSON-serialized array describing the errors
|
||||
// A JSON-serialized Array describing the errors
|
||||
Errors []PassportElementError `json:"errors"`
|
||||
}
|
||||
|
||||
|
||||
+1352
-21
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,210 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
json "github.com/goccy/go-json"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestUpdate_RichMessage_Decodes is a regression test for the Bot API v10
|
||||
// rich_message decode failure: RichBlock (21 variants) and RichText (25
|
||||
// variants) had no knownDiscriminators entry, so genapi never emitted
|
||||
// UnmarshalRichBlock / UnmarshalRichText and any update whose message
|
||||
// carried rich_message failed to decode — in production this rejected
|
||||
// roughly 8.6% of Telegram updates with HTTP 400. Uses goccy/go-json, the
|
||||
// codec production runs; plain encoding/json failed the same way.
|
||||
func TestUpdate_RichMessage_Decodes(t *testing.T) {
|
||||
body := `{"update_id":1,"message":{"message_id":7,"date":1789000000,"chat":{"id":-100123,"type":"supergroup"},` +
|
||||
`"rich_message":{"blocks":[{"type":"paragraph","text":{"type":"bold","text":{"type":"anchor","name":"hi"}}}]}}}`
|
||||
|
||||
var u Update
|
||||
require.NoError(t, json.Unmarshal([]byte(body), &u))
|
||||
|
||||
require.NotNil(t, u.Message)
|
||||
require.NotNil(t, u.Message.RichMessage)
|
||||
require.Len(t, u.Message.RichMessage.Blocks, 1)
|
||||
|
||||
para, ok := u.Message.RichMessage.Blocks[0].(*RichBlockParagraph)
|
||||
require.True(t, ok, "blocks[0] must dispatch to *RichBlockParagraph, got %T", u.Message.RichMessage.Blocks[0])
|
||||
|
||||
bold, ok := para.Text.(*RichTextBold)
|
||||
require.True(t, ok, "paragraph.text must dispatch to *RichTextBold, got %T", para.Text)
|
||||
|
||||
anchor, ok := bold.Text.(*RichTextAnchor)
|
||||
require.True(t, ok, "bold.text must dispatch to *RichTextAnchor, got %T", bold.Text)
|
||||
require.Equal(t, "hi", anchor.Name)
|
||||
}
|
||||
|
||||
// TestUnmarshalRichBlock_UnknownType confirms an unrecognised discriminator
|
||||
// value surfaces a clear error instead of silently decoding a zero-valued
|
||||
// interface.
|
||||
func TestUnmarshalRichBlock_UnknownType(t *testing.T) {
|
||||
_, err := UnmarshalRichBlock([]byte(`{"type":"not_a_real_block"}`))
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "unknown type")
|
||||
}
|
||||
|
||||
// TestUpdate_RichMessage_UnknownBlockType confirms the unknown-type error
|
||||
// from UnmarshalRichBlock propagates all the way up through
|
||||
// RichMessage.UnmarshalJSON to the top-level Update decode.
|
||||
func TestUpdate_RichMessage_UnknownBlockType(t *testing.T) {
|
||||
body := `{"update_id":1,"message":{"message_id":7,"date":1789000000,"chat":{"id":-100123,"type":"supergroup"},` +
|
||||
`"rich_message":{"blocks":[{"type":"not_a_real_block"}]}}}`
|
||||
|
||||
var u Update
|
||||
err := json.Unmarshal([]byte(body), &u)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "unknown type")
|
||||
}
|
||||
|
||||
// TestUpdate_RichMessage_PlainStringText is a regression test for the Bot
|
||||
// API v10 RichText decode gap this task fixes. Telegram declares RichText
|
||||
// as decodable from "either a String for plain text, an Array of
|
||||
// RichText, or any of the following [object] types" — but the IR (and
|
||||
// therefore UnmarshalRichText) only ever recorded the object variants, so
|
||||
// a paragraph whose text is a bare JSON string failed with "invalid
|
||||
// character '\"' looking for beginning of value". A plain-text paragraph
|
||||
// is the most common real rich_message payload, so this was the
|
||||
// production-facing half of the bug.
|
||||
func TestUpdate_RichMessage_PlainStringText(t *testing.T) {
|
||||
body := `{"update_id":1,"message":{"message_id":7,"date":1789000000,"chat":{"id":-100123,"type":"supergroup"},` +
|
||||
`"rich_message":{"blocks":[{"type":"paragraph","text":"hello world"}]}}}`
|
||||
|
||||
var u Update
|
||||
require.NoError(t, json.Unmarshal([]byte(body), &u))
|
||||
|
||||
require.NotNil(t, u.Message)
|
||||
require.NotNil(t, u.Message.RichMessage)
|
||||
require.Len(t, u.Message.RichMessage.Blocks, 1)
|
||||
|
||||
para, ok := u.Message.RichMessage.Blocks[0].(*RichBlockParagraph)
|
||||
require.True(t, ok, "blocks[0] must dispatch to *RichBlockParagraph, got %T", u.Message.RichMessage.Blocks[0])
|
||||
|
||||
plain, ok := para.Text.(*RichTextPlain)
|
||||
require.True(t, ok, "paragraph.text must dispatch to *RichTextPlain for a bare JSON string, got %T", para.Text)
|
||||
require.Equal(t, "hello world", plain.Text)
|
||||
}
|
||||
|
||||
// TestUpdate_RichMessage_ArrayText confirms Telegram's second declared
|
||||
// RichText alternate — a bare JSON array of RichText — decodes into
|
||||
// RichTextSequence, with each element dispatched recursively through
|
||||
// UnmarshalRichText (an object variant in this case).
|
||||
func TestUpdate_RichMessage_ArrayText(t *testing.T) {
|
||||
body := `{"update_id":1,"message":{"message_id":7,"date":1789000000,"chat":{"id":-100123,"type":"supergroup"},` +
|
||||
`"rich_message":{"blocks":[{"type":"paragraph","text":[` +
|
||||
`{"type":"bold","text":"x"},{"type":"italic","text":"y"}` +
|
||||
`]}]}}}`
|
||||
|
||||
var u Update
|
||||
require.NoError(t, json.Unmarshal([]byte(body), &u))
|
||||
|
||||
para, ok := u.Message.RichMessage.Blocks[0].(*RichBlockParagraph)
|
||||
require.True(t, ok)
|
||||
|
||||
seq, ok := para.Text.(*RichTextSequence)
|
||||
require.True(t, ok, "paragraph.text must dispatch to *RichTextSequence for a JSON array, got %T", para.Text)
|
||||
require.Len(t, seq.Items, 2)
|
||||
|
||||
bold, ok := seq.Items[0].(*RichTextBold)
|
||||
require.True(t, ok, "sequence[0] must be *RichTextBold, got %T", seq.Items[0])
|
||||
boldText, ok := bold.Text.(*RichTextPlain)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "x", boldText.Text)
|
||||
|
||||
italic, ok := seq.Items[1].(*RichTextItalic)
|
||||
require.True(t, ok, "sequence[1] must be *RichTextItalic, got %T", seq.Items[1])
|
||||
italicText, ok := italic.Text.(*RichTextPlain)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "y", italicText.Text)
|
||||
}
|
||||
|
||||
// TestUpdate_RichMessage_ArrayText_MixedElements confirms an array whose
|
||||
// elements mix the bare-string shape and the object shape dispatches
|
||||
// each element independently — the array alternate and the string
|
||||
// alternate compose, they aren't mutually exclusive per element.
|
||||
func TestUpdate_RichMessage_ArrayText_MixedElements(t *testing.T) {
|
||||
body := `{"update_id":1,"message":{"message_id":7,"date":1789000000,"chat":{"id":-100123,"type":"supergroup"},` +
|
||||
`"rich_message":{"blocks":[{"type":"paragraph","text":[` +
|
||||
`"plain segment",{"type":"bold","text":"bold segment"}` +
|
||||
`]}]}}}`
|
||||
|
||||
var u Update
|
||||
require.NoError(t, json.Unmarshal([]byte(body), &u))
|
||||
|
||||
para, ok := u.Message.RichMessage.Blocks[0].(*RichBlockParagraph)
|
||||
require.True(t, ok)
|
||||
|
||||
seq, ok := para.Text.(*RichTextSequence)
|
||||
require.True(t, ok)
|
||||
require.Len(t, seq.Items, 2)
|
||||
|
||||
plain, ok := seq.Items[0].(*RichTextPlain)
|
||||
require.True(t, ok, "sequence[0] must be *RichTextPlain, got %T", seq.Items[0])
|
||||
require.Equal(t, "plain segment", plain.Text)
|
||||
|
||||
bold, ok := seq.Items[1].(*RichTextBold)
|
||||
require.True(t, ok, "sequence[1] must be *RichTextBold, got %T", seq.Items[1])
|
||||
boldText, ok := bold.Text.(*RichTextPlain)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "bold segment", boldText.Text)
|
||||
}
|
||||
|
||||
// TestRichTextPlain_MarshalJSON_RoundTrip confirms the plain-text
|
||||
// alternate marshals back to a bare JSON string (not an object), and
|
||||
// that string decodes straight back through UnmarshalRichText.
|
||||
func TestRichTextPlain_MarshalJSON_RoundTrip(t *testing.T) {
|
||||
v := &RichTextPlain{Text: "hello"}
|
||||
|
||||
b, err := json.Marshal(v)
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, `"hello"`, string(b))
|
||||
|
||||
decoded, err := UnmarshalRichText(b)
|
||||
require.NoError(t, err)
|
||||
got, ok := decoded.(*RichTextPlain)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "hello", got.Text)
|
||||
}
|
||||
|
||||
// TestRichTextSequence_MarshalJSON_RoundTrip confirms the array
|
||||
// alternate marshals back to a bare JSON array (not an object wrapping
|
||||
// one), preserving each element's own wire shape, and round-trips
|
||||
// through UnmarshalRichText.
|
||||
func TestRichTextSequence_MarshalJSON_RoundTrip(t *testing.T) {
|
||||
v := &RichTextSequence{
|
||||
Items: []RichText{
|
||||
&RichTextPlain{Text: "a"},
|
||||
&RichTextBold{Text: &RichTextPlain{Text: "b"}},
|
||||
},
|
||||
}
|
||||
|
||||
b, err := json.Marshal(v)
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, `["a",{"type":"bold","text":"b"}]`, string(b))
|
||||
|
||||
decoded, err := UnmarshalRichText(b)
|
||||
require.NoError(t, err)
|
||||
seq, ok := decoded.(*RichTextSequence)
|
||||
require.True(t, ok)
|
||||
require.Len(t, seq.Items, 2)
|
||||
|
||||
p0, ok := seq.Items[0].(*RichTextPlain)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "a", p0.Text)
|
||||
|
||||
b1, ok := seq.Items[1].(*RichTextBold)
|
||||
require.True(t, ok)
|
||||
p1, ok := b1.Text.(*RichTextPlain)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "b", p1.Text)
|
||||
}
|
||||
|
||||
// TestUnmarshalRichText_UnrecognisedShape confirms a JSON value that is
|
||||
// neither a string, an array, nor an object surfaces a clear error
|
||||
// instead of silently producing a zero-valued interface.
|
||||
func TestUnmarshalRichText_UnrecognisedShape(t *testing.T) {
|
||||
_, err := UnmarshalRichText([]byte(`42`))
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "unrecognised JSON value")
|
||||
}
|
||||
+4571
-74
File diff suppressed because it is too large
Load Diff
@@ -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,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"
|
||||
+11
-5
@@ -125,12 +125,18 @@ func TestAuditDrift_InvalidRefReturnsError(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAuditDrift_SameRefNoDrift(t *testing.T) {
|
||||
// Compare HEAD's IR against itself, not against the working tree: during a
|
||||
// regen run the working tree legitimately carries new methods, and drift
|
||||
// against HEAD is the expected outcome rather than a failure.
|
||||
irPath := "../../internal/spec/api.json"
|
||||
cur, err := loadIR(irPath)
|
||||
out, err := exec.Command("git", "show", "HEAD:"+irPath).Output()
|
||||
if err != nil {
|
||||
t.Skip("api.json not available, skipping drift test")
|
||||
t.Skip("api.json not available at HEAD, skipping drift test")
|
||||
}
|
||||
changes, err := auditDrift(irPath, "HEAD", cur)
|
||||
var head spec.API
|
||||
require.NoError(t, json.Unmarshal(out, &head))
|
||||
|
||||
changes, err := auditDrift(irPath, "HEAD", &head)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, changes)
|
||||
}
|
||||
@@ -166,7 +172,7 @@ func TestAuditAny_FlagsUnknownMethodReturn(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
out := auditAny(api)
|
||||
out := auditAny(api, &spec.Overrides{})
|
||||
require.Len(t, out, 1)
|
||||
require.Contains(t, out[0], "any return: weirdMethod")
|
||||
}
|
||||
@@ -182,7 +188,7 @@ func TestAuditAny_FlagsUnknownMethodParam(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
out := auditAny(api)
|
||||
out := auditAny(api, &spec.Overrides{})
|
||||
require.Len(t, out, 1)
|
||||
require.Contains(t, out[0], "any param: weirdMethod.Thing")
|
||||
}
|
||||
|
||||
+7
-6
@@ -54,7 +54,7 @@ func main() {
|
||||
var problems []string
|
||||
|
||||
problems = append(problems, auditBool(api, overrides)...)
|
||||
problems = append(problems, auditAny(api)...)
|
||||
problems = append(problems, auditAny(api, overrides)...)
|
||||
|
||||
driftFound := false
|
||||
if *checkDrift {
|
||||
@@ -140,8 +140,9 @@ func looksGenuinelyBool(doc string) bool {
|
||||
|
||||
// auditAny scans the IR for any KindOneOf TypeRef that would render as
|
||||
// `any` in generated code (not matched by ChatID/InputFile-or-string/known
|
||||
// union heuristics). Reports each occurrence with location.
|
||||
func auditAny(api *spec.API) []string {
|
||||
// union heuristics). Reports each occurrence with location, except the
|
||||
// locations approved in overrides.
|
||||
func auditAny(api *spec.API, ov *spec.Overrides) []string {
|
||||
var out []string
|
||||
isKnownUnion := func(variants []string) bool {
|
||||
if hasVariants(variants, "int64", "string") {
|
||||
@@ -166,17 +167,17 @@ func auditAny(api *spec.API) []string {
|
||||
}
|
||||
for _, t := range api.Types {
|
||||
for _, f := range t.Fields {
|
||||
if isAny(f.Type) {
|
||||
if isAny(f.Type) && !ov.IsAnyApproved(t.Name+"."+f.Name) {
|
||||
out = append(out, fmt.Sprintf("any field: %s.%s (variants=%v)", t.Name, f.Name, f.Type.Variants))
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, m := range api.Methods {
|
||||
if isAny(m.Returns) {
|
||||
if isAny(m.Returns) && !ov.IsAnyApproved(m.Name) {
|
||||
out = append(out, fmt.Sprintf("any return: %s (variants=%v)", m.Name, m.Returns.Variants))
|
||||
}
|
||||
for _, p := range m.Params {
|
||||
if isAny(p.Type) {
|
||||
if isAny(p.Type) && !ov.IsAnyApproved(m.Name+"."+p.Name) {
|
||||
out = append(out, fmt.Sprintf("any param: %s.%s (variants=%v)", m.Name, p.Name, p.Type.Variants))
|
||||
}
|
||||
}
|
||||
|
||||
+31
-5
@@ -66,11 +66,37 @@ func TestAuditAny_FlagsUnrecognisedOneOf(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
out := auditAny(api)
|
||||
out := auditAny(api, &spec.Overrides{})
|
||||
require.Len(t, out, 1)
|
||||
require.Contains(t, out[0], "any field: Foo.Bar")
|
||||
}
|
||||
|
||||
func TestAuditAny_SkipsApprovedLocations(t *testing.T) {
|
||||
api := &spec.API{
|
||||
Types: []spec.TypeDecl{
|
||||
{
|
||||
Name: "Foo",
|
||||
Fields: []spec.Field{
|
||||
{Name: "Bar", JSONName: "bar", Type: spec.TypeRef{Kind: spec.KindOneOf, Variants: []string{"A", "B"}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
Methods: []spec.MethodDecl{
|
||||
{
|
||||
Name: "doThing",
|
||||
Returns: spec.TypeRef{Kind: spec.KindOneOf, Variants: []string{"A", "B"}},
|
||||
Params: []spec.Field{
|
||||
{Name: "Baz", JSONName: "baz", Type: spec.TypeRef{Kind: spec.KindOneOf, Variants: []string{"A", "B"}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
require.Len(t, auditAny(api, &spec.Overrides{}), 3)
|
||||
|
||||
ov := &spec.Overrides{ApprovedAnyLocations: []string{"Foo.Bar", "doThing", "doThing.Baz"}}
|
||||
require.Empty(t, auditAny(api, ov))
|
||||
}
|
||||
|
||||
func TestAuditAny_SkipsChatIDShape(t *testing.T) {
|
||||
api := &spec.API{
|
||||
Types: []spec.TypeDecl{
|
||||
@@ -82,7 +108,7 @@ func TestAuditAny_SkipsChatIDShape(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
require.Empty(t, auditAny(api))
|
||||
require.Empty(t, auditAny(api, &spec.Overrides{}))
|
||||
}
|
||||
|
||||
func TestAuditAny_SkipsKnownUnion(t *testing.T) {
|
||||
@@ -97,7 +123,7 @@ func TestAuditAny_SkipsKnownUnion(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
require.Empty(t, auditAny(api))
|
||||
require.Empty(t, auditAny(api, &spec.Overrides{}))
|
||||
}
|
||||
|
||||
func TestAuditAny_SkipsReplyMarkupShape(t *testing.T) {
|
||||
@@ -111,7 +137,7 @@ func TestAuditAny_SkipsReplyMarkupShape(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
require.Empty(t, auditAny(api))
|
||||
require.Empty(t, auditAny(api, &spec.Overrides{}))
|
||||
}
|
||||
|
||||
func TestAuditAny_SkipsInputFileShape(t *testing.T) {
|
||||
@@ -125,7 +151,7 @@ func TestAuditAny_SkipsInputFileShape(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
require.Empty(t, auditAny(api))
|
||||
require.Empty(t, auditAny(api, &spec.Overrides{}))
|
||||
}
|
||||
|
||||
// ---- diffSignatures ------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/lukaszraczylo/go-telegram/internal/spec"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestUnionAlternates_NoAlternates confirms a union with no Alternates
|
||||
// (the common case — every union except RichText) produces no synthetic
|
||||
// variants.
|
||||
func TestUnionAlternates_NoAlternates(t *testing.T) {
|
||||
td := spec.TypeDecl{Name: "ChatMember", OneOf: []string{"ChatMemberOwner"}}
|
||||
require.Nil(t, unionAlternates(td))
|
||||
}
|
||||
|
||||
// TestUnionAlternates_StringAndArray confirms both alternate shapes map
|
||||
// to the expected synthetic variant names, in declaration order.
|
||||
func TestUnionAlternates_StringAndArray(t *testing.T) {
|
||||
td := spec.TypeDecl{
|
||||
Name: "RichText",
|
||||
OneOf: []string{"RichTextBold"},
|
||||
Alternates: []spec.Alternate{
|
||||
{Shape: spec.AlternateString},
|
||||
{Shape: spec.AlternateArray},
|
||||
},
|
||||
}
|
||||
got := unionAlternates(td)
|
||||
require.Equal(t, []alternateVariant{
|
||||
{Name: "RichTextPlain", IsString: true},
|
||||
{Name: "RichTextSequence", IsArray: true},
|
||||
}, got)
|
||||
}
|
||||
|
||||
// TestEmitTypes_UnionWithAlternates_EmitsShapeDispatcher builds a minimal
|
||||
// synthetic API with one discriminated union carrying Alternates (mirrors
|
||||
// RichText's shape without depending on the full real spec) and confirms
|
||||
// genapi emits: the two synthetic variant structs, their marker methods
|
||||
// and MarshalJSON, and a shape-dispatching Unmarshal<Union> that branches
|
||||
// on the first non-whitespace byte before falling back to the existing
|
||||
// discriminator switch.
|
||||
func TestEmitTypes_UnionWithAlternates_EmitsShapeDispatcher(t *testing.T) {
|
||||
api := &spec.API{
|
||||
Types: []spec.TypeDecl{
|
||||
{
|
||||
Name: "Widget",
|
||||
OneOf: []string{"WidgetBold"},
|
||||
Alternates: []spec.Alternate{
|
||||
{Shape: spec.AlternateString},
|
||||
{Shape: spec.AlternateArray},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "WidgetBold",
|
||||
Fields: []spec.Field{
|
||||
{Name: "Type", JSONName: "type", Required: true,
|
||||
Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "string"}, EnumValues: []string{"bold"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tmp := t.TempDir()
|
||||
e := newEmitter(api, tmp)
|
||||
require.NoError(t, e.emitTypes())
|
||||
|
||||
got, err := os.ReadFile(filepath.Join(tmp, "types.gen.go"))
|
||||
require.NoError(t, err)
|
||||
src := string(got)
|
||||
|
||||
// Synthetic variants exist and implement the sealed interface.
|
||||
require.Contains(t, src, "type WidgetPlain struct")
|
||||
require.Contains(t, src, "Text string")
|
||||
require.Contains(t, src, "func (*WidgetPlain) isWidget() {}")
|
||||
require.Contains(t, src, "type WidgetSequence struct")
|
||||
require.Contains(t, src, "Items []Widget")
|
||||
require.Contains(t, src, "func (*WidgetSequence) isWidget() {}")
|
||||
|
||||
// Round-trip MarshalJSON for both.
|
||||
require.Contains(t, src, "func (v *WidgetPlain) MarshalJSON() ([]byte, error) {")
|
||||
require.Contains(t, src, "return json.Marshal(v.Text)")
|
||||
require.Contains(t, src, "func (v *WidgetSequence) MarshalJSON() ([]byte, error) {")
|
||||
require.Contains(t, src, "return json.Marshal(v.Items)")
|
||||
|
||||
// Shape-dispatching UnmarshalWidget: trims, branches on '"', '[', '{'.
|
||||
require.Contains(t, src, "func UnmarshalWidget(data []byte) (Widget, error) {")
|
||||
require.Contains(t, src, `trimmed := bytes.TrimLeft(data, " \t\r\n")`)
|
||||
require.Contains(t, src, "case '\"':")
|
||||
require.Contains(t, src, "return &WidgetPlain{Text: s}, nil")
|
||||
require.Contains(t, src, "case '[':")
|
||||
require.Contains(t, src, "return &WidgetSequence{Items: items}, nil")
|
||||
require.Contains(t, src, "case '{':")
|
||||
// The pre-existing discriminator switch is preserved beneath the
|
||||
// shape dispatch, unrecognised discriminator still a clear error.
|
||||
require.Contains(t, src, `fmt.Errorf("Widget: unknown type %q", probe.V)`)
|
||||
|
||||
// The struct-holding-the-union case inherits shape dispatch for free:
|
||||
// WidgetBold has no union-typed field itself, so nothing further to
|
||||
// assert there beyond successful compilation-shaped source (covered
|
||||
// by TestEmit_Types_FixtureGolden's gofmt round trip for the real
|
||||
// fixture, and by the api-package tests for full decode behaviour).
|
||||
require.False(t, strings.Contains(src, "WidgetPlainSequence"), "sanity: no accidental name collision")
|
||||
}
|
||||
|
||||
// TestEmitTypes_UnionWithoutAlternates_Unaffected confirms a discriminated
|
||||
// union with no Alternates emits the original (pre-alternates) shape:
|
||||
// no synthetic variants, no shape dispatch, and the same "decodes a X
|
||||
// from JSON by inspecting" doc comment wording as before this feature.
|
||||
func TestEmitTypes_UnionWithoutAlternates_Unaffected(t *testing.T) {
|
||||
api := &spec.API{
|
||||
Types: []spec.TypeDecl{
|
||||
{Name: "Widget", OneOf: []string{"WidgetBold"}},
|
||||
{
|
||||
Name: "WidgetBold",
|
||||
Fields: []spec.Field{
|
||||
{Name: "Type", JSONName: "type", Required: true,
|
||||
Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "string"}, EnumValues: []string{"bold"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tmp := t.TempDir()
|
||||
e := newEmitter(api, tmp)
|
||||
require.NoError(t, e.emitTypes())
|
||||
|
||||
got, err := os.ReadFile(filepath.Join(tmp, "types.gen.go"))
|
||||
require.NoError(t, err)
|
||||
src := string(got)
|
||||
|
||||
require.NotContains(t, src, "WidgetPlain")
|
||||
require.NotContains(t, src, "WidgetSequence")
|
||||
// The package-level `var _ = bytes.TrimLeft` import guard is always
|
||||
// present (it keeps the "bytes" import valid even when no union in
|
||||
// this API has alternates); what must NOT appear is the per-union
|
||||
// shape-dispatch call site.
|
||||
require.NotContains(t, src, "trimmed := bytes.TrimLeft(data,")
|
||||
require.Contains(t, src, "// UnmarshalWidget decodes a Widget from JSON by inspecting the")
|
||||
require.Contains(t, src, `// "type" field and dispatching to the correct concrete type.`)
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/lukaszraczylo/go-telegram/internal/spec"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestDeriveDiscriminators_RichBlockAndRichText_FromRealSpec is a
|
||||
// regression test for the Bot API v10 rich_message decode failure:
|
||||
// RichBlock and RichText had no knownDiscriminators entry, so genapi
|
||||
// never emitted UnmarshalRichBlock / UnmarshalRichText and any update
|
||||
// whose message carried rich_message failed to decode. Derivation must
|
||||
// pick "type" for both and cover every variant the spec declares.
|
||||
//
|
||||
// The expected variant counts are read from the spec rather than pinned
|
||||
// as literals. Telegram adds variants in most releases - v10.3 took
|
||||
// RichBlock from 21 to 24 and RichText from 25 to 26 - so a frozen
|
||||
// number turns a routine regeneration into a spurious failure while
|
||||
// testing nothing the coverage assertion does not already cover.
|
||||
func TestDeriveDiscriminators_RichBlockAndRichText_FromRealSpec(t *testing.T) {
|
||||
api, err := loadAPI("../../internal/spec/api.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
// declaredVariants returns how many concrete variants the spec lists
|
||||
// for a union, so the assertions below track the spec instead of a
|
||||
// hardcoded count.
|
||||
declaredVariants := func(union string) int {
|
||||
for i := range api.Types {
|
||||
if api.Types[i].Name == union {
|
||||
return len(api.Types[i].OneOf)
|
||||
}
|
||||
}
|
||||
t.Fatalf("union %q not found in spec", union)
|
||||
return 0
|
||||
}
|
||||
|
||||
derived, _ := deriveDiscriminators(api)
|
||||
|
||||
rb, ok := derived["RichBlock"]
|
||||
require.True(t, ok, "RichBlock must derive a discriminator")
|
||||
require.Equal(t, "type", rb.Field)
|
||||
require.Len(t, rb.Variants, declaredVariants("RichBlock"),
|
||||
"every RichBlock variant declared in the spec must be covered")
|
||||
require.Equal(t, "RichBlockParagraph", rb.Variants["paragraph"])
|
||||
require.Equal(t, "RichBlockTable", rb.Variants["table"])
|
||||
require.Equal(t, "RichBlockThinking", rb.Variants["thinking"])
|
||||
|
||||
rt, ok := derived["RichText"]
|
||||
require.True(t, ok, "RichText must derive a discriminator")
|
||||
require.Equal(t, "type", rt.Field)
|
||||
require.Len(t, rt.Variants, declaredVariants("RichText"),
|
||||
"every RichText variant declared in the spec must be covered")
|
||||
require.Equal(t, "RichTextBold", rt.Variants["bold"])
|
||||
require.Equal(t, "RichTextAnchorLink", rt.Variants["anchor_link"])
|
||||
require.Equal(t, "RichTextReferenceLink", rt.Variants["reference_link"])
|
||||
}
|
||||
|
||||
// TestMergeDiscriminators_HandCuratedOverridesDerived confirms a
|
||||
// knownDiscriminators entry always wins the merge even when derivation
|
||||
// would also have produced a (different) answer for the same union —
|
||||
// hand-curation is the escape hatch for a union the generic rule gets
|
||||
// wrong.
|
||||
func TestMergeDiscriminators_HandCuratedOverridesDerived(t *testing.T) {
|
||||
api := &spec.API{
|
||||
Types: []spec.TypeDecl{
|
||||
{Name: "Widget", OneOf: []string{"WidgetA", "WidgetB"}},
|
||||
{Name: "WidgetA", Fields: []spec.Field{
|
||||
{Name: "Type", JSONName: "type", Required: true,
|
||||
Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "string"}, EnumValues: []string{"a"}},
|
||||
}},
|
||||
{Name: "WidgetB", Fields: []spec.Field{
|
||||
{Name: "Type", JSONName: "type", Required: true,
|
||||
Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "string"}, EnumValues: []string{"b"}},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
derived, ungated := deriveDiscriminators(api)
|
||||
require.Empty(t, ungated)
|
||||
require.Equal(t, discriminatorSpec{
|
||||
Field: "type",
|
||||
Variants: map[string]string{"a": "WidgetA", "b": "WidgetB"},
|
||||
}, derived["Widget"])
|
||||
|
||||
// Inject a hand-curated override that disagrees with the derived
|
||||
// spec, merge, and confirm the override wins outright.
|
||||
override := discriminatorSpec{Field: "kind", Variants: map[string]string{"custom": "WidgetA"}}
|
||||
knownDiscriminators["Widget"] = override
|
||||
t.Cleanup(func() { delete(knownDiscriminators, "Widget") })
|
||||
|
||||
merged := mergeDiscriminators(derived)
|
||||
require.Equal(t, override, merged["Widget"])
|
||||
}
|
||||
|
||||
// TestCheckDiscriminatorGate_FailsOnUngatedInboundUnion confirms the
|
||||
// build gate fails generation when a union Telegram can actually send
|
||||
// has no derivable discriminator: the variants share no common
|
||||
// Type/Status/Source field at all.
|
||||
func TestCheckDiscriminatorGate_FailsOnUngatedInboundUnion(t *testing.T) {
|
||||
api := &spec.API{
|
||||
Types: []spec.TypeDecl{
|
||||
{Name: "Widget", OneOf: []string{"WidgetA", "WidgetB"}},
|
||||
{Name: "WidgetA", Fields: []spec.Field{
|
||||
{Name: "Kind", JSONName: "kind", Required: true, Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "string"}},
|
||||
}},
|
||||
{Name: "WidgetB", Fields: []spec.Field{
|
||||
{Name: "Flavor", JSONName: "flavor", Required: true, Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "string"}},
|
||||
}},
|
||||
// Container is a plain (non-Input) type with a field of the
|
||||
// undiscriminated union — this is what makes Widget reachable
|
||||
// from real inbound Telegram data.
|
||||
{Name: "Container", Fields: []spec.Field{
|
||||
{Name: "W", JSONName: "w", Type: spec.TypeRef{Kind: spec.KindNamed, Name: "Widget"}},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
err := checkDiscriminatorGate(api)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "Widget")
|
||||
require.ErrorContains(t, err, "WidgetA")
|
||||
require.ErrorContains(t, err, "WidgetB")
|
||||
}
|
||||
|
||||
// TestCheckDiscriminatorGate_SilentForInputOnlyUnion confirms the same
|
||||
// undiscriminated union does NOT fail the build when its only reference
|
||||
// is a field on an Input*-prefixed type — those are always constructed
|
||||
// and marshalled by the caller, never decoded from a Telegram response.
|
||||
func TestCheckDiscriminatorGate_SilentForInputOnlyUnion(t *testing.T) {
|
||||
api := &spec.API{
|
||||
Types: []spec.TypeDecl{
|
||||
{Name: "Widget", OneOf: []string{"WidgetA", "WidgetB"}},
|
||||
{Name: "WidgetA", Fields: []spec.Field{
|
||||
{Name: "Kind", JSONName: "kind", Required: true, Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "string"}},
|
||||
}},
|
||||
{Name: "WidgetB", Fields: []spec.Field{
|
||||
{Name: "Flavor", JSONName: "flavor", Required: true, Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "string"}},
|
||||
}},
|
||||
{Name: "InputContainer", Fields: []spec.Field{
|
||||
{Name: "W", JSONName: "w", Type: spec.TypeRef{Kind: spec.KindNamed, Name: "Widget"}},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
require.NoError(t, checkDiscriminatorGate(api))
|
||||
}
|
||||
+367
-46
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/goccy/go-json"
|
||||
"go/format"
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/lukaszraczylo/go-telegram/internal/spec"
|
||||
@@ -77,9 +79,18 @@ type discriminatorSpec struct {
|
||||
Variants map[string]string // discriminator value → concrete Go type name
|
||||
}
|
||||
|
||||
// knownDiscriminators maps parent union name → discriminator spec.
|
||||
// Used by the template helpers hasDiscriminator / discriminatorField /
|
||||
// discriminatorMap to emit UnmarshalXxx helpers.
|
||||
// knownDiscriminators is the hand-curated OVERRIDE table for sealed-union
|
||||
// discriminators. deriveDiscriminators computes the same kind of map
|
||||
// automatically from the IR for every union that has one (see
|
||||
// mergeDiscriminators); an entry here always wins the merge regardless of
|
||||
// what derivation would have produced. Use this table only for unions the
|
||||
// generic rule can't handle — e.g. MaybeInaccessibleMessage's integer
|
||||
// discriminator, which needs bespoke decode logic no discriminatorSpec
|
||||
// can express (Field is "" and Variants is nil; the template emits a
|
||||
// hand-coded Unmarshal via isMaybeInaccessibleMessage instead). Every
|
||||
// other entry here exists only because it predates derivation — removing
|
||||
// one and re-running `go test -run TestEmit -update ./cmd/genapi/...`
|
||||
// should reproduce it byte-for-byte from deriveDiscriminators.
|
||||
var knownDiscriminators = map[string]discriminatorSpec{
|
||||
"ChatMember": {
|
||||
Field: "status",
|
||||
@@ -205,15 +216,24 @@ type emitter struct {
|
||||
// template to emit per-variant MarshalJSON that hardcodes the
|
||||
// discriminator so callers don't have to set it by hand.
|
||||
variantDiscs map[string]variantDiscriminator
|
||||
// discs is the merged union-level discriminator table: derivation
|
||||
// output with knownDiscriminators layered on top as overrides (see
|
||||
// mergeDiscriminators). Every codegen path that decides whether a
|
||||
// union auto-decodes — and how — consults this instead of the
|
||||
// package-level knownDiscriminators map, so a union qualifies the
|
||||
// moment deriveDiscriminators can resolve it, hand-curated or not.
|
||||
discs map[string]discriminatorSpec
|
||||
}
|
||||
|
||||
func newEmitter(api *spec.API, outDir string) *emitter {
|
||||
knownInterfaceTypes = buildUnionTypeSet(api)
|
||||
derived, _ := deriveDiscriminators(api)
|
||||
return &emitter{
|
||||
api: api,
|
||||
outDir: outDir,
|
||||
enums: planEnums(api),
|
||||
variantDiscs: variantDiscriminators(api),
|
||||
discs: mergeDiscriminators(derived),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,6 +369,262 @@ func parseDiscriminatorDoc(doc string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// discriminatorFieldCandidates returns the Go field names to try, in
|
||||
// priority order, when looking for a discriminator shared by every
|
||||
// variant of a union: the hand-curated field from knownDiscriminators
|
||||
// (resolved to its Go name via the first variant that declares it) when
|
||||
// the union already has an override entry, then "Type", "Status",
|
||||
// "Source" — the three names Telegram's docs consistently use for this
|
||||
// purpose. Shared by planUnifiedUnionEnums (enum naming, enums.go) and
|
||||
// deriveDiscriminators (decode dispatch, below) so the two never disagree
|
||||
// on where to look.
|
||||
func discriminatorFieldCandidates(unionName string, variants []*spec.TypeDecl) []string {
|
||||
var candidates []string
|
||||
seen := map[string]bool{}
|
||||
add := func(name string) {
|
||||
if name == "" || seen[name] {
|
||||
return
|
||||
}
|
||||
seen[name] = true
|
||||
candidates = append(candidates, name)
|
||||
}
|
||||
if ds, ok := knownDiscriminators[unionName]; ok && ds.Field != "" {
|
||||
for _, v := range variants {
|
||||
for _, f := range v.Fields {
|
||||
if f.JSONName == ds.Field {
|
||||
add(f.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
add("Type")
|
||||
add("Status")
|
||||
add("Source")
|
||||
return candidates
|
||||
}
|
||||
|
||||
// fieldByGoName returns the field named name on fields, or nil if none matches.
|
||||
func fieldByGoName(fields []spec.Field, name string) *spec.Field {
|
||||
for i := range fields {
|
||||
if fields[i].Name == name {
|
||||
return &fields[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// discriminatorValue extracts the wire-level value a variant field
|
||||
// contributes to a discriminator: a single-entry EnumValues list (the
|
||||
// scraper's detected value), or a doc-parsed "must be X" / "always “X”"
|
||||
// literal via parseDiscriminatorDoc (the same extraction
|
||||
// extractVariantDiscriminator uses for the first-field case). Returns ""
|
||||
// when the field isn't a required string or carries no resolvable value.
|
||||
func discriminatorValue(f spec.Field) string {
|
||||
if !f.Required || f.Type.Kind != spec.KindPrimitive || f.Type.Name != "string" {
|
||||
return ""
|
||||
}
|
||||
if len(f.EnumValues) == 1 {
|
||||
return f.EnumValues[0]
|
||||
}
|
||||
return parseDiscriminatorDoc(f.Doc)
|
||||
}
|
||||
|
||||
// deriveDiscriminators inspects every union TypeDecl (len(OneOf) > 0) in
|
||||
// api and tries to pick a discriminator JSON field automatically, so the
|
||||
// hand-maintained knownDiscriminators map no longer has to name every
|
||||
// union that qualifies. A union qualifies when, for some candidate field
|
||||
// (see discriminatorFieldCandidates), EVERY variant declares that field
|
||||
// and discriminatorValue resolves a non-empty wire value for it, and
|
||||
// those values are unique across variants.
|
||||
//
|
||||
// Returns the derived map plus the names of unions that did not qualify,
|
||||
// in declaration order. Callers merge the derived map with
|
||||
// knownDiscriminators (mergeDiscriminators) and decide what to do about
|
||||
// the leftovers — rescue via a hand-curated override, or fail the build
|
||||
// if the union is reachable from real Telegram data (checkDiscriminatorGate).
|
||||
func deriveDiscriminators(api *spec.API) (map[string]discriminatorSpec, []string) {
|
||||
typeByName := make(map[string]*spec.TypeDecl, len(api.Types))
|
||||
for i := range api.Types {
|
||||
typeByName[api.Types[i].Name] = &api.Types[i]
|
||||
}
|
||||
|
||||
derived := make(map[string]discriminatorSpec, 32)
|
||||
var ungated []string
|
||||
|
||||
for i := range api.Types {
|
||||
u := &api.Types[i]
|
||||
if len(u.OneOf) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
variants := make([]*spec.TypeDecl, 0, len(u.OneOf))
|
||||
resolved := true
|
||||
for _, vName := range u.OneOf {
|
||||
v, found := typeByName[vName]
|
||||
if !found {
|
||||
resolved = false
|
||||
break
|
||||
}
|
||||
variants = append(variants, v)
|
||||
}
|
||||
if !resolved {
|
||||
ungated = append(ungated, u.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
ds, qualified := deriveUnionDiscriminator(u.Name, variants)
|
||||
if !qualified {
|
||||
ungated = append(ungated, u.Name)
|
||||
continue
|
||||
}
|
||||
derived[u.Name] = ds
|
||||
}
|
||||
return derived, ungated
|
||||
}
|
||||
|
||||
// deriveUnionDiscriminator tries each candidate Go field name in
|
||||
// priority order and returns the first one where every variant declares
|
||||
// that field with a resolvable, unique wire value.
|
||||
func deriveUnionDiscriminator(unionName string, variants []*spec.TypeDecl) (discriminatorSpec, bool) {
|
||||
for _, goName := range discriminatorFieldCandidates(unionName, variants) {
|
||||
jsonField := ""
|
||||
values := make(map[string]string, len(variants)) // wire value -> variant Go type name
|
||||
qualifies := true
|
||||
for _, v := range variants {
|
||||
f := fieldByGoName(v.Fields, goName)
|
||||
if f == nil {
|
||||
qualifies = false
|
||||
break
|
||||
}
|
||||
value := discriminatorValue(*f)
|
||||
if value == "" {
|
||||
qualifies = false
|
||||
break
|
||||
}
|
||||
if jsonField == "" {
|
||||
jsonField = f.JSONName
|
||||
} else if f.JSONName != jsonField {
|
||||
qualifies = false
|
||||
break
|
||||
}
|
||||
if _, dup := values[value]; dup {
|
||||
qualifies = false
|
||||
break
|
||||
}
|
||||
values[value] = v.Name
|
||||
}
|
||||
if qualifies {
|
||||
return discriminatorSpec{Field: jsonField, Variants: values}, true
|
||||
}
|
||||
}
|
||||
return discriminatorSpec{}, false
|
||||
}
|
||||
|
||||
// mergeDiscriminators layers the hand-curated knownDiscriminators table
|
||||
// on top of derived: every entry deriveDiscriminators produces is used
|
||||
// as-is unless knownDiscriminators also names that union, in which case
|
||||
// the hand-curated entry wins outright (the whole discriminatorSpec, not
|
||||
// a per-key merge).
|
||||
func mergeDiscriminators(derived map[string]discriminatorSpec) map[string]discriminatorSpec {
|
||||
merged := make(map[string]discriminatorSpec, len(derived)+len(knownDiscriminators))
|
||||
for name, ds := range derived {
|
||||
merged[name] = ds
|
||||
}
|
||||
for name, ds := range knownDiscriminators {
|
||||
merged[name] = ds
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
// isInboundReachable reports whether Telegram can actually send data
|
||||
// typed as the named union to the bot: as a field on some type that
|
||||
// isn't itself send-only, or as a method's return value. "Input*"-named
|
||||
// unions are excluded outright regardless of where they're referenced —
|
||||
// by Telegram's own naming convention in this API, every such union is
|
||||
// something the bot constructs and marshals, never something it decodes.
|
||||
func isInboundReachable(api *spec.API, unionName string) bool {
|
||||
if strings.HasPrefix(unionName, "Input") {
|
||||
return false
|
||||
}
|
||||
for _, m := range api.Methods {
|
||||
if typeRefIsUnion(m.Returns, api, unionName) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, t := range api.Types {
|
||||
if strings.HasPrefix(t.Name, "Input") {
|
||||
continue // send-only struct; its fields are never decoded either.
|
||||
}
|
||||
for _, f := range t.Fields {
|
||||
if typeRefIsUnion(f.Type, api, unionName) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// typeRefIsUnion reports whether tr resolves to the named union: directly,
|
||||
// as an array element, or via an inline oneOf variant-set match against
|
||||
// unionName's own OneOf list.
|
||||
func typeRefIsUnion(tr spec.TypeRef, api *spec.API, unionName string) bool {
|
||||
switch tr.Kind {
|
||||
case spec.KindNamed:
|
||||
return tr.Name == unionName
|
||||
case spec.KindArray:
|
||||
if tr.ElemType != nil {
|
||||
return typeRefIsUnion(*tr.ElemType, api, unionName)
|
||||
}
|
||||
case spec.KindOneOf:
|
||||
for i := range api.Types {
|
||||
if api.Types[i].Name == unionName && matchesVariants(tr.Variants, api.Types[i].OneOf...) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// checkDiscriminatorGate fails codegen when a union Telegram can actually
|
||||
// send to the bot has no usable discriminator — neither derived nor
|
||||
// hand-curated. Such a union decodes to a bare interface field that no
|
||||
// JSON codec can ever fill (the RichBlock/RichText Bot API v10
|
||||
// regression this generalisation fixes: both had 20+ variants and no
|
||||
// knownDiscriminators entry, so genapi silently emitted no UnmarshalXxx
|
||||
// helper at all). Send-only "Input*" unions are exempt — see
|
||||
// isInboundReachable.
|
||||
func checkDiscriminatorGate(api *spec.API) error {
|
||||
_, ungated := deriveDiscriminators(api)
|
||||
|
||||
typeByName := make(map[string]*spec.TypeDecl, len(api.Types))
|
||||
for i := range api.Types {
|
||||
typeByName[api.Types[i].Name] = &api.Types[i]
|
||||
}
|
||||
|
||||
var offending []string
|
||||
for _, name := range ungated {
|
||||
if _, overridden := knownDiscriminators[name]; overridden {
|
||||
continue // hand-curated rescue, e.g. MaybeInaccessibleMessage.
|
||||
}
|
||||
if isInboundReachable(api, name) {
|
||||
offending = append(offending, name)
|
||||
}
|
||||
}
|
||||
if len(offending) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "genapi: %d inbound union(s) have no derivable or hand-curated discriminator "+
|
||||
"and would decode as an unfillable interface field:", len(offending))
|
||||
for _, name := range offending {
|
||||
fmt.Fprintf(&b, "\n - %s (variants: %s): no single field is present, required, and holds a unique non-empty wire value on every variant",
|
||||
name, strings.Join(typeByName[name].OneOf, ", "))
|
||||
}
|
||||
return errors.New(b.String())
|
||||
}
|
||||
|
||||
// knownInterfaceTypes is the full set of sealed-interface union type names
|
||||
// (both auto-decoded ones in knownDiscriminators and marker-only ones from
|
||||
// types with OneOf). Populated at emitter construction. goType and
|
||||
@@ -359,7 +635,7 @@ var knownInterfaceTypes = map[string]bool{}
|
||||
|
||||
// emitTypes renders types.gen.go.
|
||||
func (e *emitter) emitTypes() error {
|
||||
t, err := template.New("types").Funcs(funcsWithDiscs(e.enums, e.variantDiscs)).Parse(typesTmpl)
|
||||
t, err := template.New("types").Funcs(funcsWithDiscs(e.enums, e.variantDiscs, e.discs)).Parse(typesTmpl)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse types.tmpl: %w", err)
|
||||
}
|
||||
@@ -396,31 +672,35 @@ func loadAPI(path string) (*spec.API, error) {
|
||||
}
|
||||
|
||||
// funcsWithDiscs returns the shared FuncMap with the variant
|
||||
// discriminator helpers bound to discs. types.tmpl uses
|
||||
// discriminator helpers bound to variantDiscs. types.tmpl uses
|
||||
// variantDiscFor/variantHasDisc to emit per-variant MarshalJSON that
|
||||
// hardcodes the wire discriminator value.
|
||||
func funcsWithDiscs(plan *enumPlan, discs map[string]variantDiscriminator) template.FuncMap {
|
||||
fm := funcs(plan)
|
||||
// hardcodes the wire discriminator value. unionDiscs is the merged
|
||||
// union-level table (see mergeDiscriminators), threaded into funcs.
|
||||
func funcsWithDiscs(plan *enumPlan, variantDiscs map[string]variantDiscriminator, unionDiscs map[string]discriminatorSpec) template.FuncMap {
|
||||
fm := funcs(plan, unionDiscs)
|
||||
fm["variantHasDisc"] = func(name string) bool {
|
||||
_, ok := discs[name]
|
||||
_, ok := variantDiscs[name]
|
||||
return ok
|
||||
}
|
||||
fm["variantDiscField"] = func(name string) string { return discs[name].JSONField }
|
||||
fm["variantDiscGoField"] = func(name string) string { return discs[name].GoField }
|
||||
fm["variantDiscValue"] = func(name string) string { return discs[name].Value }
|
||||
fm["variantDiscField"] = func(name string) string { return variantDiscs[name].JSONField }
|
||||
fm["variantDiscGoField"] = func(name string) string { return variantDiscs[name].GoField }
|
||||
fm["variantDiscValue"] = func(name string) string { return variantDiscs[name].Value }
|
||||
return fm
|
||||
}
|
||||
|
||||
// funcs is the FuncMap shared across templates. plan is the resolved
|
||||
// enum plan; pass nil only in unit tests that don't exercise enums.
|
||||
func funcs(plan *enumPlan) template.FuncMap {
|
||||
// enum plan; pass nil only in unit tests that don't exercise enums. discs
|
||||
// is the merged union-level discriminator table (see
|
||||
// mergeDiscriminators); pass nil (or knownDiscriminators directly) in
|
||||
// unit tests that only exercise the hand-curated unions.
|
||||
func funcs(plan *enumPlan, discs map[string]discriminatorSpec) template.FuncMap {
|
||||
return template.FuncMap{
|
||||
"goType": goType,
|
||||
"goField": func(parent string, f spec.Field) string {
|
||||
return goField(plan, parent, f)
|
||||
},
|
||||
"goFieldP": func(methodName string, f spec.Field) string {
|
||||
return goFieldX(plan, "", title(methodName)+"Params", f)
|
||||
return goFieldX(plan, methodEnumParent(methodName), title(methodName)+"Params", f)
|
||||
},
|
||||
"docComment": docComment,
|
||||
"isOptional": func(f spec.Field) bool { return !f.Required },
|
||||
@@ -432,7 +712,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,
|
||||
@@ -444,20 +724,22 @@ func funcs(plan *enumPlan) template.FuncMap {
|
||||
return plan.All()
|
||||
},
|
||||
"enumConstName": constName,
|
||||
// discriminator helpers for types.tmpl
|
||||
"hasDiscriminator": func(name string) bool { s, ok := knownDiscriminators[name]; return ok && len(s.Variants) > 0 },
|
||||
// discriminator helpers for types.tmpl — all consult the merged
|
||||
// table (discs), not the package-level knownDiscriminators, so a
|
||||
// derived-only union gates exactly like a hand-curated one.
|
||||
"hasDiscriminator": func(name string) bool { s, ok := discs[name]; return ok && len(s.Variants) > 0 },
|
||||
"isSealedUnionReturn": func(tr spec.TypeRef) bool {
|
||||
if tr.Kind != spec.KindNamed {
|
||||
return false
|
||||
}
|
||||
s, ok := knownDiscriminators[tr.Name]
|
||||
s, ok := discs[tr.Name]
|
||||
return ok && len(s.Variants) > 0
|
||||
},
|
||||
"isSealedUnionArrayReturn": func(tr spec.TypeRef) bool {
|
||||
if tr.Kind != spec.KindArray || tr.ElemType == nil || tr.ElemType.Kind != spec.KindNamed {
|
||||
return false
|
||||
}
|
||||
s, ok := knownDiscriminators[tr.ElemType.Name]
|
||||
s, ok := discs[tr.ElemType.Name]
|
||||
return ok && len(s.Variants) > 0
|
||||
},
|
||||
"sealedUnionElemName": func(tr spec.TypeRef) string {
|
||||
@@ -467,12 +749,13 @@ func funcs(plan *enumPlan) template.FuncMap {
|
||||
return ""
|
||||
},
|
||||
"isMaybeInaccessibleMessage": func(name string) bool { return name == "MaybeInaccessibleMessage" },
|
||||
"discriminatorField": func(name string) string { return knownDiscriminators[name].Field },
|
||||
"discriminatorMap": func(name string) map[string]string { return knownDiscriminators[name].Variants },
|
||||
"discriminatorField": func(name string) string { return discs[name].Field },
|
||||
"discriminatorMap": func(name string) map[string]string { return discs[name].Variants },
|
||||
// union-field helpers for per-struct UnmarshalJSON emission
|
||||
"unionFields": unionFieldsOf,
|
||||
"isArrayUnion": func(tr spec.TypeRef) bool { return hasUnionElem(tr) },
|
||||
"unionTypeName": func(tr spec.TypeRef) string { name, _ := unionTypeFor(tr); return name },
|
||||
"unionFields": func(t spec.TypeDecl) []unionField { return unionFieldsOf(t, discs) },
|
||||
"isArrayUnion": func(tr spec.TypeRef) bool { return hasUnionElem(tr, discs) },
|
||||
"unionTypeName": func(tr spec.TypeRef) string { name, _ := unionTypeFor(tr, discs); return name },
|
||||
"unionAlternates": unionAlternates,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -645,7 +928,7 @@ func returnGoElem(tr spec.TypeRef) string {
|
||||
|
||||
// emitMethods renders methods.gen.go.
|
||||
func (e *emitter) emitMethods() error {
|
||||
t, err := template.New("methods").Funcs(funcs(e.enums)).Parse(methodsTmpl)
|
||||
t, err := template.New("methods").Funcs(funcs(e.enums, e.discs)).Parse(methodsTmpl)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse methods.tmpl: %w", err)
|
||||
}
|
||||
@@ -662,7 +945,7 @@ func (e *emitter) emitMethods() error {
|
||||
|
||||
// emitEnums renders enums.gen.go.
|
||||
func (e *emitter) emitEnums() error {
|
||||
t, err := template.New("enums").Funcs(funcs(e.enums)).Parse(enumsTmpl)
|
||||
t, err := template.New("enums").Funcs(funcs(e.enums, e.discs)).Parse(enumsTmpl)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse enums.tmpl: %w", err)
|
||||
}
|
||||
@@ -735,11 +1018,12 @@ type unionField struct {
|
||||
}
|
||||
|
||||
// unionFieldsOf returns the subset of t.Fields whose type is a known
|
||||
// discriminated union (directly or as array element).
|
||||
func unionFieldsOf(t spec.TypeDecl) []unionField {
|
||||
// discriminated union (directly or as array element), per discs — the
|
||||
// merged union-level discriminator table (see mergeDiscriminators).
|
||||
func unionFieldsOf(t spec.TypeDecl, discs map[string]discriminatorSpec) []unionField {
|
||||
var out []unionField
|
||||
for _, f := range t.Fields {
|
||||
if u, ok := unionTypeFor(f.Type); ok {
|
||||
if u, ok := unionTypeFor(f.Type, discs); ok {
|
||||
out = append(out, unionField{Field: f, UnionName: u})
|
||||
}
|
||||
}
|
||||
@@ -747,19 +1031,20 @@ func unionFieldsOf(t spec.TypeDecl) []unionField {
|
||||
}
|
||||
|
||||
// unionTypeFor inspects a TypeRef and reports whether it (or its array
|
||||
// element) is a known discriminated union. Returns the union name and true.
|
||||
func unionTypeFor(tr spec.TypeRef) (string, bool) {
|
||||
// element) is a known discriminated union in discs. Returns the union
|
||||
// name and true.
|
||||
func unionTypeFor(tr spec.TypeRef, discs map[string]discriminatorSpec) (string, bool) {
|
||||
switch tr.Kind {
|
||||
case spec.KindNamed:
|
||||
if _, ok := knownDiscriminators[tr.Name]; ok {
|
||||
if _, ok := discs[tr.Name]; ok {
|
||||
return tr.Name, true
|
||||
}
|
||||
case spec.KindArray:
|
||||
if tr.ElemType != nil {
|
||||
return unionTypeFor(*tr.ElemType)
|
||||
return unionTypeFor(*tr.ElemType, discs)
|
||||
}
|
||||
case spec.KindOneOf:
|
||||
if u := unionNameByVariants(tr.Variants); u != "" {
|
||||
if u := unionNameByVariants(tr.Variants, discs); u != "" {
|
||||
return u, true
|
||||
}
|
||||
}
|
||||
@@ -768,8 +1053,8 @@ func unionTypeFor(tr spec.TypeRef) (string, bool) {
|
||||
|
||||
// unionNameByVariants finds the parent union whose variant type names exactly
|
||||
// match the given variant set (order-insensitive).
|
||||
func unionNameByVariants(variants []string) string {
|
||||
for parentName, ds := range knownDiscriminators {
|
||||
func unionNameByVariants(variants []string, discs map[string]discriminatorSpec) string {
|
||||
for parentName, ds := range discs {
|
||||
wanted := make([]string, 0, len(ds.Variants))
|
||||
for _, vt := range ds.Variants {
|
||||
wanted = append(wanted, vt)
|
||||
@@ -782,14 +1067,50 @@ func unionNameByVariants(variants []string) string {
|
||||
}
|
||||
|
||||
// hasUnionElem reports whether tr is an array whose element type is a known union.
|
||||
func hasUnionElem(tr spec.TypeRef) bool {
|
||||
func hasUnionElem(tr spec.TypeRef, discs map[string]discriminatorSpec) bool {
|
||||
if tr.Kind != spec.KindArray || tr.ElemType == nil {
|
||||
return false
|
||||
}
|
||||
_, ok := unionTypeFor(*tr.ElemType)
|
||||
_, ok := unionTypeFor(*tr.ElemType, discs)
|
||||
return ok
|
||||
}
|
||||
|
||||
// alternateVariant describes one synthetic sealed-interface variant the
|
||||
// generator emits for a union's non-object wire shape (see
|
||||
// spec.Alternate): a bare-JSON-string variant, or a variant holding a
|
||||
// JSON array of the union itself. Exactly one of IsString / IsArray is
|
||||
// set. types.tmpl emits the struct, marker method, and MarshalJSON for
|
||||
// each, and Unmarshal<Union> dispatches to it by inspecting the first
|
||||
// non-whitespace byte of the wire value.
|
||||
type alternateVariant struct {
|
||||
Name string // synthetic Go type name, e.g. "RichTextPlain"
|
||||
IsString bool
|
||||
IsArray bool
|
||||
}
|
||||
|
||||
// unionAlternates maps a union TypeDecl's spec.Alternate entries (see
|
||||
// cmd/scrape's extractAlternates) to the synthetic Go variant
|
||||
// descriptors types.tmpl emits alongside the regular OneOf variants:
|
||||
// "<Union>Plain" for the bare-string shape, "<Union>Sequence" for the
|
||||
// array-of-<Union> shape. Returns nil for a union with no alternates,
|
||||
// which is every union except RichText as of Bot API v10 — so this is a
|
||||
// no-op for the rest of the type list.
|
||||
func unionAlternates(t spec.TypeDecl) []alternateVariant {
|
||||
if len(t.Alternates) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]alternateVariant, 0, len(t.Alternates))
|
||||
for _, a := range t.Alternates {
|
||||
switch a.Shape {
|
||||
case spec.AlternateString:
|
||||
out = append(out, alternateVariant{Name: t.Name + "Plain", IsString: true})
|
||||
case spec.AlternateArray:
|
||||
out = append(out, alternateVariant{Name: t.Name + "Sequence", IsArray: true})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// matchesVariants reports whether got equals want as a set (order-insensitive).
|
||||
func matchesVariants(got []string, want ...string) bool {
|
||||
if len(got) != len(want) {
|
||||
@@ -915,15 +1236,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 {
|
||||
@@ -1023,7 +1344,7 @@ func (e *emitter) emitTests() error {
|
||||
unionTypes := buildUnionTypeSet(e.api)
|
||||
|
||||
// Add test-specific helpers to the shared func map.
|
||||
fm := funcs(e.enums)
|
||||
fm := funcs(e.enums, e.discs)
|
||||
fm["sentinelValue"] = makeSentinelValue(unionTypes, e.enums)
|
||||
fm["successResp"] = successResp
|
||||
|
||||
|
||||
+77
-34
@@ -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 {
|
||||
@@ -262,32 +271,12 @@ func planUnifiedUnionEnums(api *spec.API) (map[string]enumDecl, map[string]strin
|
||||
continue
|
||||
}
|
||||
|
||||
// Build the candidate Go-name list. Priority order:
|
||||
// Build the candidate Go-name list (shared with deriveDiscriminators
|
||||
// in emitter.go so enum naming and decode dispatch never disagree
|
||||
// on where to look). Priority order:
|
||||
// 1. discriminator GoField from knownDiscriminators (resolved via JSONName);
|
||||
// 2. "Type", "Status", "Source".
|
||||
var candidateNames []string
|
||||
seen := map[string]bool{}
|
||||
add := func(name string) {
|
||||
if name == "" || seen[name] {
|
||||
return
|
||||
}
|
||||
seen[name] = true
|
||||
candidateNames = append(candidateNames, name)
|
||||
}
|
||||
if ds, ok := knownDiscriminators[u.Name]; ok && ds.Field != "" {
|
||||
// Resolve Go-name from the first variant whose field matches the JSON name.
|
||||
for _, v := range variants {
|
||||
for _, f := range v.Fields {
|
||||
if f.JSONName == ds.Field {
|
||||
add(f.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, n := range []string{"Type", "Status", "Source"} {
|
||||
add(n)
|
||||
}
|
||||
candidateNames := discriminatorFieldCandidates(u.Name, variants)
|
||||
|
||||
// Find the first candidate Go-name where every variant has a
|
||||
// matching single-value string-enum field.
|
||||
@@ -389,6 +378,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 +407,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")
|
||||
}
|
||||
+16
-16
@@ -280,7 +280,7 @@ func TestMultipartFieldEntry_Named_Optional(t *testing.T) {
|
||||
|
||||
func TestUnionTypeFor_DirectNamed(t *testing.T) {
|
||||
tr := spec.TypeRef{Kind: spec.KindNamed, Name: "ChatMember"}
|
||||
name, ok := unionTypeFor(tr)
|
||||
name, ok := unionTypeFor(tr, knownDiscriminators)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "ChatMember", name)
|
||||
}
|
||||
@@ -288,26 +288,26 @@ func TestUnionTypeFor_DirectNamed(t *testing.T) {
|
||||
func TestUnionTypeFor_Array(t *testing.T) {
|
||||
elem := spec.TypeRef{Kind: spec.KindNamed, Name: "ChatMember"}
|
||||
tr := spec.TypeRef{Kind: spec.KindArray, ElemType: &elem}
|
||||
name, ok := unionTypeFor(tr)
|
||||
name, ok := unionTypeFor(tr, knownDiscriminators)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "ChatMember", name)
|
||||
}
|
||||
|
||||
func TestUnionTypeFor_ArrayNilElem(t *testing.T) {
|
||||
tr := spec.TypeRef{Kind: spec.KindArray}
|
||||
_, ok := unionTypeFor(tr)
|
||||
_, ok := unionTypeFor(tr, knownDiscriminators)
|
||||
require.False(t, ok)
|
||||
}
|
||||
|
||||
func TestUnionTypeFor_NotUnion(t *testing.T) {
|
||||
tr := spec.TypeRef{Kind: spec.KindNamed, Name: "Message"}
|
||||
_, ok := unionTypeFor(tr)
|
||||
_, ok := unionTypeFor(tr, knownDiscriminators)
|
||||
require.False(t, ok)
|
||||
}
|
||||
|
||||
func TestUnionTypeFor_Unknown(t *testing.T) {
|
||||
tr := spec.TypeRef{Kind: spec.Kind(99)}
|
||||
_, ok := unionTypeFor(tr)
|
||||
_, ok := unionTypeFor(tr, knownDiscriminators)
|
||||
require.False(t, ok)
|
||||
}
|
||||
|
||||
@@ -321,12 +321,12 @@ func TestUnionNameByVariants_ChatMember(t *testing.T) {
|
||||
"ChatMemberOwner", "ChatMemberAdministrator", "ChatMemberMember",
|
||||
"ChatMemberRestricted", "ChatMemberLeft", "ChatMemberBanned",
|
||||
}
|
||||
name := unionNameByVariants(variants)
|
||||
name := unionNameByVariants(variants, knownDiscriminators)
|
||||
require.Equal(t, "ChatMember", name)
|
||||
}
|
||||
|
||||
func TestUnionNameByVariants_Unknown(t *testing.T) {
|
||||
name := unionNameByVariants([]string{"X", "Y", "Z"})
|
||||
name := unionNameByVariants([]string{"X", "Y", "Z"}, knownDiscriminators)
|
||||
require.Equal(t, "", name)
|
||||
}
|
||||
|
||||
@@ -336,24 +336,24 @@ func TestUnionNameByVariants_Unknown(t *testing.T) {
|
||||
|
||||
func TestHasUnionElem_NonArray(t *testing.T) {
|
||||
tr := spec.TypeRef{Kind: spec.KindNamed, Name: "ChatMember"}
|
||||
require.False(t, hasUnionElem(tr))
|
||||
require.False(t, hasUnionElem(tr, knownDiscriminators))
|
||||
}
|
||||
|
||||
func TestHasUnionElem_ArrayNilElem(t *testing.T) {
|
||||
tr := spec.TypeRef{Kind: spec.KindArray}
|
||||
require.False(t, hasUnionElem(tr))
|
||||
require.False(t, hasUnionElem(tr, knownDiscriminators))
|
||||
}
|
||||
|
||||
func TestHasUnionElem_ArrayUnionElem(t *testing.T) {
|
||||
elem := spec.TypeRef{Kind: spec.KindNamed, Name: "ChatMember"}
|
||||
tr := spec.TypeRef{Kind: spec.KindArray, ElemType: &elem}
|
||||
require.True(t, hasUnionElem(tr))
|
||||
require.True(t, hasUnionElem(tr, knownDiscriminators))
|
||||
}
|
||||
|
||||
func TestHasUnionElem_ArrayNonUnionElem(t *testing.T) {
|
||||
elem := spec.TypeRef{Kind: spec.KindNamed, Name: "Message"}
|
||||
tr := spec.TypeRef{Kind: spec.KindArray, ElemType: &elem}
|
||||
require.False(t, hasUnionElem(tr))
|
||||
require.False(t, hasUnionElem(tr, knownDiscriminators))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -369,7 +369,7 @@ func TestUnionFieldsOf_WithUnionField(t *testing.T) {
|
||||
{Name: "Date", JSONName: "date", Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "int64"}},
|
||||
},
|
||||
}
|
||||
uf := unionFieldsOf(td)
|
||||
uf := unionFieldsOf(td, knownDiscriminators)
|
||||
require.Len(t, uf, 2)
|
||||
require.Equal(t, "ChatMember", uf[0].UnionName)
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -621,14 +621,14 @@ func TestUnionTypeFor_OneOfVariants(t *testing.T) {
|
||||
"ChatMemberRestricted", "ChatMemberLeft", "ChatMemberBanned",
|
||||
},
|
||||
}
|
||||
name, ok := unionTypeFor(tr)
|
||||
name, ok := unionTypeFor(tr, knownDiscriminators)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "ChatMember", name)
|
||||
}
|
||||
|
||||
func TestUnionTypeFor_OneOfNoMatch(t *testing.T) {
|
||||
tr := spec.TypeRef{Kind: spec.KindOneOf, Variants: []string{"Foo", "Bar"}}
|
||||
_, ok := unionTypeFor(tr)
|
||||
_, ok := unionTypeFor(tr, knownDiscriminators)
|
||||
require.False(t, ok)
|
||||
}
|
||||
|
||||
@@ -637,7 +637,7 @@ func TestUnionTypeFor_OneOfNoMatch(t *testing.T) {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestFuncs_HasExpectedKeys(t *testing.T) {
|
||||
fm := funcs(nil)
|
||||
fm := funcs(nil, nil)
|
||||
require.NotNil(t, fm)
|
||||
for _, key := range []string{"goType", "docComment", "returnGoType", "unionFields"} {
|
||||
require.NotNil(t, fm[key], "funcs() missing key %q", key)
|
||||
|
||||
@@ -29,6 +29,9 @@ func run(input, outdir string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("load api: %w", err)
|
||||
}
|
||||
if err := checkDiscriminatorGate(api); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(outdir, 0o750); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
+65
-6
@@ -7,14 +7,16 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/goccy/go-json"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
var _ = io.Discard // keep import even if no fields use io
|
||||
var _ = json.Marshal // keep import for UnmarshalXxx helpers
|
||||
var _ = fmt.Errorf // keep import for UnmarshalXxx helpers
|
||||
var _ = io.Discard // keep import even if no fields use io
|
||||
var _ = json.Marshal // keep import for UnmarshalXxx helpers
|
||||
var _ = fmt.Errorf // keep import for UnmarshalXxx helpers
|
||||
var _ = bytes.TrimLeft // keep import for shape-dispatching UnmarshalXxx helpers (unions with alternates)
|
||||
|
||||
{{range .Types}}
|
||||
{{- $td := . -}}
|
||||
@@ -22,6 +24,7 @@ var _ = fmt.Errorf // keep import for UnmarshalXxx helpers
|
||||
// {{.Name}} is a union type. The following concrete variants implement
|
||||
// it:
|
||||
{{range .OneOf}}// - {{.}}
|
||||
{{end}}{{range unionAlternates .}}// - {{.Name}} ({{if .IsString}}bare JSON string{{else}}JSON array of {{$td.Name}}{{end}})
|
||||
{{end}}//
|
||||
{{docComment .Doc -}}
|
||||
type {{.Name}} interface{ is{{.Name}}() }
|
||||
@@ -30,11 +33,67 @@ type {{.Name}} interface{ is{{.Name}}() }
|
||||
// is{{$td.Name}} is the marker method that makes {{.}} implement {{$td.Name}}.
|
||||
func (*{{.}}) is{{$td.Name}}() {}
|
||||
{{end}}
|
||||
{{range unionAlternates .}}
|
||||
// {{.Name}} implements {{$td.Name}} for the {{if .IsString}}bare JSON string{{else}}JSON array of {{$td.Name}}{{end}} wire
|
||||
// shape Telegram documents for this union, alongside its object variants.
|
||||
type {{.Name}} struct {
|
||||
{{if .IsString}} Text string
|
||||
{{else}} Items []{{$td.Name}}
|
||||
{{end}}}
|
||||
|
||||
// is{{$td.Name}} is the marker method that makes {{.Name}} implement {{$td.Name}}.
|
||||
func (*{{.Name}}) is{{$td.Name}}() {}
|
||||
|
||||
{{if .IsString}}// MarshalJSON encodes {{.Name}} back to its bare JSON string wire shape.
|
||||
func (v *{{.Name}}) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(v.Text)
|
||||
}
|
||||
{{else}}// MarshalJSON encodes {{.Name}} back to its bare JSON array wire shape.
|
||||
func (v *{{.Name}}) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(v.Items)
|
||||
}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{if hasDiscriminator .Name}}
|
||||
// Unmarshal{{.Name}} decodes a {{.Name}} from JSON by inspecting the
|
||||
{{$alts := unionAlternates .}}{{if $alts}}// Unmarshal{{.Name}} decodes a {{.Name}} from JSON. Telegram documents
|
||||
// this type as either a bare JSON string, a JSON array of {{.Name}}, or
|
||||
// an object dispatched by its "{{discriminatorField .Name}}" field;
|
||||
// leading whitespace is trimmed and the first remaining byte selects
|
||||
// the shape.
|
||||
{{else}}// Unmarshal{{.Name}} decodes a {{.Name}} from JSON by inspecting the
|
||||
// "{{discriminatorField .Name}}" field and dispatching to the correct concrete type.
|
||||
func Unmarshal{{.Name}}(data []byte) ({{.Name}}, error) {
|
||||
var probe struct {
|
||||
{{end}}func Unmarshal{{.Name}}(data []byte) ({{.Name}}, error) {
|
||||
{{if $alts}} trimmed := bytes.TrimLeft(data, " \t\r\n")
|
||||
if len(trimmed) == 0 {
|
||||
return nil, fmt.Errorf("{{.Name}}: empty JSON value")
|
||||
}
|
||||
switch trimmed[0] {
|
||||
{{range $alts}}{{if .IsString}} case '"':
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return nil, fmt.Errorf("{{$td.Name}}: decoding string: %w", err)
|
||||
}
|
||||
return &{{.Name}}{Text: s}, nil
|
||||
{{else}} case '[':
|
||||
var raws []json.RawMessage
|
||||
if err := json.Unmarshal(data, &raws); err != nil {
|
||||
return nil, fmt.Errorf("{{$td.Name}}: decoding array: %w", err)
|
||||
}
|
||||
items := make([]{{$td.Name}}, 0, len(raws))
|
||||
for i, r := range raws {
|
||||
v, err := Unmarshal{{$td.Name}}(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("{{$td.Name}}: decoding array[%d]: %w", i, err)
|
||||
}
|
||||
items = append(items, v)
|
||||
}
|
||||
return &{{.Name}}{Items: items}, nil
|
||||
{{end}}{{end}} case '{':
|
||||
// fall through to the discriminator switch below.
|
||||
default:
|
||||
return nil, fmt.Errorf("{{.Name}}: unrecognised JSON value starting with %q", trimmed[0])
|
||||
}
|
||||
{{end}} var probe struct {
|
||||
V string `json:"{{discriminatorField .Name}}"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &probe); err != nil {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/lukaszraczylo/go-telegram/internal/spec"
|
||||
)
|
||||
|
||||
// altStringRE matches the scalar-string alternate clause Telegram uses
|
||||
// for union types that can also decode as plain text on the wire, e.g.
|
||||
// "a String for plain text". The trailing "for ..." clause is optional
|
||||
// and, when present, is not captured — only the shape matters. "an"
|
||||
// (rather than "a") is tolerated so a future rephrasing ("an String")
|
||||
// doesn't silently stop matching, and the word boundary keeps "Strings"
|
||||
// or "SubString" from misfiring.
|
||||
var altStringRE = regexp.MustCompile(`(?i)\ban?\s+String\b`)
|
||||
|
||||
// altArrayRE builds the pattern that matches the self-referential array
|
||||
// alternate clause Telegram uses for recursive union types, e.g. "an
|
||||
// Array of RichText". Matching is anchored to the union's own name so a
|
||||
// doc mentioning "an Array of" some other type is never mistaken for a
|
||||
// shape alternate.
|
||||
func altArrayRE(unionName string) *regexp.Regexp {
|
||||
return regexp.MustCompile(`(?i)\ban?\s+Array\s+of\s+` + regexp.QuoteMeta(unionName) + `\b`)
|
||||
}
|
||||
|
||||
// extractAlternates detects Telegram's "can be either a String for
|
||||
// plain text, an Array of <Union>, or any of the following types:"
|
||||
// phrasing on a union type's doc string and returns the non-object
|
||||
// shapes it declares, in a fixed (string, then array) order regardless
|
||||
// of how the sentence orders its clauses.
|
||||
//
|
||||
// Conservative by design: callers invoke this only for types that
|
||||
// already have OneOf populated (see typeFromSection in scrape.go), and
|
||||
// it only recognises the two shapes Telegram's docs currently use for
|
||||
// this ("a String" and "an Array of <the union's own name>") — anything
|
||||
// else in the doc is ignored, so free-text mentioning "String" or
|
||||
// "Array" elsewhere in a union's description can't produce a false
|
||||
// positive for a type this pattern wasn't written for. As of Bot API
|
||||
// v10, RichText is the only union whose doc matches either pattern.
|
||||
func extractAlternates(unionName, doc string) []spec.Alternate {
|
||||
var alts []spec.Alternate
|
||||
if altStringRE.MatchString(doc) {
|
||||
alts = append(alts, spec.Alternate{Shape: spec.AlternateString})
|
||||
}
|
||||
if altArrayRE(unionName).MatchString(doc) {
|
||||
alts = append(alts, spec.Alternate{Shape: spec.AlternateArray})
|
||||
}
|
||||
return alts
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/lukaszraczylo/go-telegram/internal/spec"
|
||||
)
|
||||
|
||||
func TestExtractAlternates_RichTextDoc(t *testing.T) {
|
||||
// Exact doc string as scraped from the live Bot API v10 docs page
|
||||
// (internal/spec/api.json / testdata/html/latest.html).
|
||||
doc := "This object represents a rich formatted text. Currently, it can be either a String for plain text, " +
|
||||
"an Array of RichText, or any of the following types:"
|
||||
got := extractAlternates("RichText", doc)
|
||||
require.Equal(t, []spec.Alternate{
|
||||
{Shape: spec.AlternateString},
|
||||
{Shape: spec.AlternateArray},
|
||||
}, got)
|
||||
}
|
||||
|
||||
func TestExtractAlternates_OrderIndependent(t *testing.T) {
|
||||
// Same two clauses, reversed, plus an extra clause in between —
|
||||
// order and surrounding prose must not matter.
|
||||
doc := "Currently, it can be either an Array of RichText, something else entirely, a String for plain text, " +
|
||||
"or any of the following types:"
|
||||
got := extractAlternates("RichText", doc)
|
||||
require.Equal(t, []spec.Alternate{
|
||||
{Shape: spec.AlternateString},
|
||||
{Shape: spec.AlternateArray},
|
||||
}, got)
|
||||
}
|
||||
|
||||
func TestExtractAlternates_ObjectOnlyUnion_NoAlternates(t *testing.T) {
|
||||
// RichBlock's doc is object-only: no String/Array alternate clause.
|
||||
doc := "This object represents a block in a rich formatted message. Currently, it can be any of the following types:"
|
||||
got := extractAlternates("RichBlock", doc)
|
||||
require.Nil(t, got)
|
||||
}
|
||||
|
||||
func TestExtractAlternates_ArrayOfDifferentType_NotSelfReferential(t *testing.T) {
|
||||
// "an Array of X" only counts when X is the union's own name — an
|
||||
// array of some other type mentioned in the doc must not match.
|
||||
doc := "This object can be either an Array of PhotoSize or any of the following types:"
|
||||
got := extractAlternates("RichText", doc)
|
||||
require.Nil(t, got)
|
||||
}
|
||||
|
||||
func TestExtractAlternates_StringOnly(t *testing.T) {
|
||||
doc := "This object can be either a String or any of the following types:"
|
||||
got := extractAlternates("Widget", doc)
|
||||
require.Equal(t, []spec.Alternate{{Shape: spec.AlternateString}}, got)
|
||||
}
|
||||
|
||||
func TestExtractAlternates_PlainTypeDoc_NoFalsePositive(t *testing.T) {
|
||||
// Ordinary field/type prose mentioning neither shape must not match.
|
||||
doc := "This object represents a chat photo."
|
||||
got := extractAlternates("ChatPhoto", doc)
|
||||
require.Nil(t, got)
|
||||
}
|
||||
|
||||
// TestTypeFromSection_RichText_RealSnapshot grounds the extraction in the
|
||||
// real pinned docs snapshot (not just a literal fixture string): scraping
|
||||
// testdata/html/latest.html end-to-end must produce a RichText TypeDecl
|
||||
// with both alternates, matching the same live text that broke decoding
|
||||
// of plain-text and array-shaped rich_message payloads.
|
||||
func TestTypeFromSection_RichText_RealSnapshot(t *testing.T) {
|
||||
htmlBytes, err := os.ReadFile("../../testdata/html/latest.html")
|
||||
require.NoError(t, err)
|
||||
|
||||
api, err := scrape(htmlBytes)
|
||||
require.NoError(t, err)
|
||||
|
||||
var richText *spec.TypeDecl
|
||||
for i := range api.Types {
|
||||
if api.Types[i].Name == "RichText" {
|
||||
richText = &api.Types[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotNil(t, richText, "RichText type must be present in the real docs snapshot")
|
||||
require.Equal(t, []spec.Alternate{
|
||||
{Shape: spec.AlternateString},
|
||||
{Shape: spec.AlternateArray},
|
||||
}, richText.Alternates)
|
||||
|
||||
// RichBlock is the control: object-only union, no alternates.
|
||||
var richBlock *spec.TypeDecl
|
||||
for i := range api.Types {
|
||||
if api.Types[i].Name == "RichBlock" {
|
||||
richBlock = &api.Types[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotNil(t, richBlock)
|
||||
require.Nil(t, richBlock.Alternates)
|
||||
}
|
||||
@@ -42,7 +42,7 @@ func extractReturn(desc string) spec.TypeRef {
|
||||
return spec.TypeRef{Kind: spec.KindArray, ElemType: &elem}
|
||||
}},
|
||||
// "an array of X of the sent messages is returned" (ForwardMessages/CopyMessages shape).
|
||||
{regexp.MustCompile(`(?:[Oo]n success[,.]?\s+)?an? array of ([A-Z][A-Za-z0-9]+)(?:\s+of [^.]+?)?\s+(?:objects\s+)?(?:is|are) returned`), func(m []string) spec.TypeRef {
|
||||
{regexp.MustCompile(`(?:[Oo]n success[,.]?\s+)?an? [Aa]rray of ([A-Z][A-Za-z0-9]+)(?:\s+of [^.]+?)?\s+(?:objects\s+)?(?:is|are) returned`), func(m []string) spec.TypeRef {
|
||||
elem := primitiveOrNamed(m[1])
|
||||
return spec.TypeRef{Kind: spec.KindArray, ElemType: &elem}
|
||||
}},
|
||||
|
||||
@@ -29,6 +29,8 @@ func TestExtractReturn(t *testing.T) {
|
||||
{"On success, an array of Message objects that were sent is returned.", spec.TypeRef{Kind: spec.KindArray, ElemType: &spec.TypeRef{Kind: spec.KindNamed, Name: "Message"}}},
|
||||
// ForwardMessages/CopyMessages shape: "an array of X of the sent messages is returned".
|
||||
{"On success, an array of MessageId of the sent messages is returned.", spec.TypeRef{Kind: spec.KindArray, ElemType: &spec.TypeRef{Kind: spec.KindNamed, Name: "MessageId"}}},
|
||||
// Bot API 10.1 recapitalised the same sentence to "an Array of X".
|
||||
{"On success, an Array of MessageId of the sent messages is returned.", spec.TypeRef{Kind: spec.KindArray, ElemType: &spec.TypeRef{Kind: spec.KindNamed, Name: "MessageId"}}},
|
||||
// "Returns X on success" (no article) — OwnedGifts, StarAmount, Story, MenuButton, etc.
|
||||
{"Returns the gifts received and owned by a managed business account. Returns OwnedGifts on success.", spec.TypeRef{Kind: spec.KindNamed, Name: "OwnedGifts"}},
|
||||
{"Returns StarAmount on success.", spec.TypeRef{Kind: spec.KindNamed, Name: "StarAmount"}},
|
||||
|
||||
@@ -38,6 +38,10 @@ func typeFromSection(s section) spec.TypeDecl {
|
||||
} else if len(s.Lists) > 0 {
|
||||
// Union: extract variant names from <li><a>...</a></li>.
|
||||
td.OneOf = extractListLinks(s.Lists[0])
|
||||
// Some unions (e.g. RichText) also declare non-object wire shapes
|
||||
// ("a String for plain text", "an Array of RichText") in the same
|
||||
// sentence as the object-variant list; extract those too.
|
||||
td.Alternates = extractAlternates(td.Name, td.Doc)
|
||||
}
|
||||
return td
|
||||
}
|
||||
|
||||
+4815
-958
File diff suppressed because it is too large
Load Diff
@@ -87,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>)
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ func handleEcho(c *dispatch.Context, m *api.Message) error {
|
||||
_, err := api.SendMessage(c.Ctx, c.Bot, &api.SendMessageParams{
|
||||
ChatID: api.ChatIDFromInt(m.Chat.ID),
|
||||
Text: m.Text,
|
||||
ReplyParameters: &api.ReplyParameters{MessageID: m.MessageID},
|
||||
ReplyParameters: &api.ReplyParameters{MessageID: &m.MessageID},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@ go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/goccy/go-json v0.10.6
|
||||
github.com/lukaszraczylo/oss-telemetry v0.2.3
|
||||
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.56.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -15,6 +17,5 @@ require (
|
||||
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
|
||||
github.com/valyala/fasthttp v1.71.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -6,6 +6,10 @@ 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/lukaszraczylo/oss-telemetry v0.2.3 h1:xoDtBqeZGmXj7IteiE1M5WMuzeoqag58qEleI0Cf2Ms=
|
||||
github.com/lukaszraczylo/oss-telemetry v0.2.3/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=
|
||||
@@ -16,8 +20,12 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
|
||||
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=
|
||||
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/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=
|
||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
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=
|
||||
|
||||
+4833
-158
File diff suppressed because it is too large
Load Diff
@@ -24,6 +24,31 @@ type TypeDecl struct {
|
||||
// OneOf, when non-empty, indicates this type is a union and lists the concrete variant type names.
|
||||
// Variants are emitted as concrete structs implementing a sealed interface.
|
||||
OneOf []string `json:"one_of,omitempty"`
|
||||
// Alternates lists non-object wire shapes this union may also decode
|
||||
// as, in addition to the object variants in OneOf. Only populated for
|
||||
// unions whose doc declares them (Telegram's "can be either a String
|
||||
// for plain text, an Array of X, or any of the following types:"
|
||||
// phrasing) — as of Bot API v10 that's RichText alone, so every other
|
||||
// type's serialised IR is unaffected by this field's existence.
|
||||
Alternates []Alternate `json:"alternates,omitempty"`
|
||||
}
|
||||
|
||||
// AlternateShape enumerates the non-object wire shapes a union's
|
||||
// Alternates field records.
|
||||
type AlternateShape string
|
||||
|
||||
const (
|
||||
// AlternateString: the union may decode from a bare JSON string.
|
||||
AlternateString AlternateShape = "string"
|
||||
// AlternateArray: the union may decode from a JSON array whose
|
||||
// elements are themselves the same union type.
|
||||
AlternateArray AlternateShape = "array"
|
||||
)
|
||||
|
||||
// Alternate records one non-object shape a union type may appear as on
|
||||
// the wire, beyond its OneOf object variants.
|
||||
type Alternate struct {
|
||||
Shape AlternateShape `json:"shape"`
|
||||
}
|
||||
|
||||
// MethodDecl describes a Telegram API method.
|
||||
|
||||
@@ -22,6 +22,12 @@ type Overrides struct {
|
||||
// ApprovedBoolMethods lists methods whose returns are genuinely bool.
|
||||
// The audit tool ignores these.
|
||||
ApprovedBoolMethods []string `json:"approved_bool_methods,omitempty"`
|
||||
|
||||
// ApprovedAnyLocations lists IR locations whose union renders as `any` by
|
||||
// design, keyed "<TypeName>.<FieldName>" for struct fields,
|
||||
// "<methodName>.<ParamName>" for params and "<methodName>" for returns.
|
||||
// The audit tool ignores these.
|
||||
ApprovedAnyLocations []string `json:"approved_any_locations,omitempty"`
|
||||
}
|
||||
|
||||
// LoadOverrides reads and parses overrides.json. Returns an empty Overrides
|
||||
@@ -73,3 +79,17 @@ func (o *Overrides) IsBoolApproved(methodName string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsAnyApproved reports whether location is on the approved `any` list.
|
||||
// See ApprovedAnyLocations for the key format.
|
||||
func (o *Overrides) IsAnyApproved(location string) bool {
|
||||
if o == nil {
|
||||
return false
|
||||
}
|
||||
for _, n := range o.ApprovedAnyLocations {
|
||||
if n == location {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"method_returns": {},
|
||||
"field_types": {},
|
||||
"approved_any_locations": [
|
||||
"InputRichMessageMedia.Media"
|
||||
],
|
||||
"approved_bool_methods": [
|
||||
"setWebhook",
|
||||
"deleteWebhook",
|
||||
|
||||
Vendored
+5
-3
@@ -7,14 +7,16 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/goccy/go-json"
|
||||
"io"
|
||||
)
|
||||
|
||||
var _ = io.Discard // keep import even if no fields use io
|
||||
var _ = json.Marshal // keep import for UnmarshalXxx helpers
|
||||
var _ = fmt.Errorf // keep import for UnmarshalXxx helpers
|
||||
var _ = io.Discard // keep import even if no fields use io
|
||||
var _ = json.Marshal // keep import for UnmarshalXxx helpers
|
||||
var _ = fmt.Errorf // keep import for UnmarshalXxx helpers
|
||||
var _ = bytes.TrimLeft // keep import for shape-dispatching UnmarshalXxx helpers (unions with alternates)
|
||||
|
||||
// This object represents a Telegram user or bot.
|
||||
type User struct {
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
snapshot_2026-05-20.html
|
||||
snapshot_2026-09-14.html
|
||||
+19872
File diff suppressed because one or more lines are too long
+21862
File diff suppressed because one or more lines are too long
+23114
File diff suppressed because one or more lines are too long
+23115
File diff suppressed because one or more lines are too long
+23115
File diff suppressed because one or more lines are too long
+23115
File diff suppressed because one or more lines are too long
+23115
File diff suppressed because one or more lines are too long
+23115
File diff suppressed because one or more lines are too long
+23589
File diff suppressed because one or more lines are too long
+23589
File diff suppressed because one or more lines are too long
+23589
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user