Compare commits

...
19 Commits
Author SHA1 Message Date
github-actions[bot]andGitHub ae45e17f43 chore(deps): update module github.com/valyala/fasthttp to v1.75.0
Automated dependency update. All tests passed.
2026-10-08 03:27:17 +00:00
github-actions[bot]andGitHub 8cb03ef0e9 chore(deps): update module github.com/molecule-man/go-brrr to v1.2.0
Automated dependency update. All tests passed.
2026-10-06 03:24:29 +00:00
lukaszraczylo d535029beb fix(ci): read regen Go version from go.mod 2026-10-05 10:31:05 +01:00
github-actions[bot]andGitHub 23cca1df8d chore(deps): update dependencies
Automated dependency update. All tests passed.
2026-10-04 04:23:05 +00:00
lukaszraczylo 131bbca0ed ci(deps): update dependencies with Renovate [skip ci] 2026-10-03 18:44:32 +01:00
ba6f76f59f Update go.mod and go.sum (#20)
Co-authored-by: lukaszraczylo <[email protected]>
2026-10-03 03:29:26 +00:00
lukaszraczyloandGitHub 93ebf41a3c ci: use the shared pull request, release and autoupdate workflows (#19)
* ci(workflows): migrate to shared go-pr and go-release workflows

Replace the generic vet, staticcheck, govulncheck, gosec, test and release jobs with the shared go-pr and go-release workflows.
Keep the codegen-clean and audit jobs in ci.yml. Add an examples build job and the ci-success aggregator there.
Add autoupdate.yaml from the shared go-autoupdate workflow.
The release job now runs only on Go source changes and on manual dispatch.
The dry-run input and the Bot API note in the tag message no longer exist.

* ci(release): gate release on repo checks and export Bot API version

Move the codegen-clean, audit and examples jobs into the reusable checks.yml workflow. The ci.yml and release.yml workflows both call it.
The release job now needs the checks job. A failed or cancelled check blocks the release.
Add workflow-prepare.sh. It appends BOT_API_VERSION to GITHUB_ENV so the GoReleaser header shows the Bot API version.
The script does nothing when GITHUB_ENV is unset.

* ci(regen): name release.yml in the merge step comments

The release now runs from release.yml. The comments still named ci.yml as the workflow that cuts the release.
2026-10-02 04:42:00 +01:00
lukaszraczylo 53afd9890f ci: pin govulncheck to v1.7.0
govulncheck@latest resolves to v1.8.0, which declares go 1.26.0 in its
module file. Every job in this workflow pins go-version 1.25.x, so the
install step fails before any scanning runs, which fails ci-success and
skips the release job.

v1.7.0 declares go 1.25.0 and installs on the pinned toolchain. This also
matches the convention used for staticcheck, which is already pinned to
an exact version. Pinning the binary does not freeze the vulnerability
database, which govulncheck fetches at run time.

Claude-Session: https://claude.ai/code/session_01Fu2wXqGUrfVBi5aLafKk8U
2026-09-15 11:34:33 +01:00
lukaszraczylo d666cda3b1 feat(genapi): derive discriminators and decode RichText alternates
The generator gated all union decoder emission behind knownDiscriminators, a
hand-curated map of 13 entries. Any sealed union absent from that map received
no decoder, so decoding must fail. Bot API v10 added RichBlock and RichText,
which were absent. Roughly 8.6 percent of Telegram updates failed with HTTP 400.

The generator now derives discriminators from the specification instead of a
fixed list. A gate check fails the build when a union resolves no discriminator.
Eleven unions gain decoders that never had one: BotCommandScope, InputMedia,
InputPaidMedia, InputPollMedia, InputPollOptionMedia, InputProfilePhoto,
InputRichBlock, InputStoryContent, PassportElementError, RichBlock and RichText.

RichText can also appear as a bare string or an array of RichText. The scraper
now extracts these alternate shapes into the intermediate representation. The
emitter adds RichTextPlain and RichTextSequence. The decoder dispatches on the
first JSON byte before it reads any discriminator.

Unions without alternates must regenerate byte for byte. The golden test proves
this.

Claude-Session: https://claude.ai/code/session_01Fu2wXqGUrfVBi5aLafKk8U
2026-09-15 10:59:48 +01:00
lukaszraczylo fa84f08b25 ci(regen): stop the weekly no-op update PR
The Telegram page ends with a "page generated in NN.NNms" comment that
changes on every fetch. Change detection saw it, so the run dispatched after
the codegen fix opened and merged a PR whose only content was that comment.
With the merge step in place, that becomes a pointless release every week.

Strip the comment on capture, and gate change detection on the generated
surface instead of testdata/html/, because the snapshot file name and the
latest.html symlink change every run by design. A fresh fetch now matches the
committed snapshot byte for byte while the API is unchanged.
2026-09-15 10:55:38 +01:00
54790f3e5b chore(api): regenerate from Telegram Bot API v10.3 (#18)
Co-authored-by: lukaszraczylo <[email protected]>
2026-09-14 06:40:10 +00:00
ab5f2c5700 chore(api): regenerate from Telegram Bot API v10.3 (#17)
Co-authored-by: lukaszraczylo <[email protected]>
2026-09-07 06:39:21 +00:00
926d51881d chore(api): regenerate from Telegram Bot API v10.3 (#16)
Co-authored-by: lukaszraczylo <[email protected]>
2026-08-31 06:39:17 +00:00
4511c823a3 chore(api): regenerate from Telegram Bot API v10.2 (#15)
Co-authored-by: lukaszraczylo <[email protected]>
2026-08-24 06:21:30 +00:00
7c5aca3bf8 chore(api): regenerate from Telegram Bot API v10.2 (#14)
Co-authored-by: lukaszraczylo <[email protected]>
2026-08-17 06:19:14 +00:00
4e6382ceeb chore(api): regenerate from Telegram Bot API v10.2 (#13)
Co-authored-by: lukaszraczylo <[email protected]>
2026-08-10 07:52:19 +00:00
b6881461fd chore(api): regenerate from Telegram Bot API v10.2 (#12)
Co-authored-by: lukaszraczylo <[email protected]>
2026-08-03 09:57:22 +00:00
6612c24dfc chore(api): regenerate from Telegram Bot API v10.2 (#11)
Co-authored-by: lukaszraczylo <[email protected]>
2026-07-27 10:02:19 +00:00
3620f8ce1e chore(api): regenerate from Telegram Bot API v10.2 (#10)
Co-authored-by: lukaszraczylo <[email protected]>
2026-07-21 21:56:30 +00:00
40 changed files with 193077 additions and 1920 deletions
+20
View File
@@ -0,0 +1,20 @@
name: Update dependencies
on:
workflow_dispatch:
schedule:
- cron: "0 3 * * *"
permissions:
actions: write
contents: write
issues: write
pull-requests: write
jobs:
update:
uses: lukaszraczylo/shared-actions/.github/workflows/renovate-autoupdate.yaml@main
with:
go-version: ">=1.25"
node: false
release-workflow: release.yml
+61
View File
@@ -0,0 +1,61 @@
name: checks
# Repo-specific gates. Called by ci.yml (PR/push) and release.yml (release gate).
on:
workflow_call:
permissions:
contents: read
jobs:
examples:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: '>=1.25'
check-latest: true
- name: Build all examples
run: go build ./examples/...
codegen-clean:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: '>=1.25'
check-latest: true
- name: Regenerate against pinned snapshot
run: make regen-from-fixture
- name: Assert clean diff
run: git diff --exit-code internal/spec/api.json api/ docs/reference/
audit:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0 # need history for drift comparison
- uses: actions/setup-go@v6
with:
go-version: '>=1.25'
check-latest: true
- name: Audit fallbacks
run: make audit
- name: Audit drift vs base
# On PRs: compare against the merge base (origin/<base>).
# On push to main: compare against the parent commit.
# Drift is informational; doesn't fail CI.
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE="origin/${{ github.base_ref }}"
else
BASE="HEAD~1"
fi
echo "Drift base: $BASE"
go run ./cmd/audit -ir internal/spec/api.json -drift -against "$BASE" || true
+10 -238
View File
@@ -1,20 +1,16 @@
name: ci
# Repo-specific checks run through checks.yml. Generic vet, staticcheck,
# govulncheck, gosec and tests run in pr.yaml (shared go-pr). Releases run in
# release.yml (shared go-release), gated by the same checks.yml.
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
inputs:
dry-run-release:
description: "Compute release version, do not tag or release"
type: boolean
default: false
permissions:
contents: write
pull-requests: read
packages: write
contents: read
# Cancel any in-flight CI runs for the same branch when a new push lands.
concurrency:
@@ -22,157 +18,15 @@ concurrency:
cancel-in-progress: true
jobs:
vet:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: '1.25.x'
check-latest: true
- uses: actions/cache@v5
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-1.25-${{ hashFiles('**/go.sum') }}
- run: go vet ./...
checks:
uses: ./.github/workflows/checks.yml
staticcheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: '1.25.x'
check-latest: true
- uses: actions/cache@v5
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-1.25-${{ hashFiles('**/go.sum') }}
- run: go install honnef.co/go/tools/cmd/[email protected]
- run: staticcheck ./...
govulncheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: '1.25.x'
check-latest: true
- uses: actions/cache@v5
with:
path: |
~/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
- run: govulncheck ./...
gosec:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: '1.25.x'
check-latest: true
- uses: actions/cache@v5
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-1.25-${{ hashFiles('**/go.sum') }}
- run: go install github.com/securego/gosec/v2/cmd/gosec@latest
# G404: math/rand/v2 jitter in transport/backoff.go — intentional (not crypto)
# G304: os.ReadFile from CLI flag variable — intentional (tool, not server)
# G306: 0o644 on generated doc artifacts in cmd/scrape — intentional
# G204: git subprocess in cmd/audit uses CLI flag path — intentional (operator tool)
# G706: log.Printf with values from Telegram/env in examples — illustrative,
# library users are expected to sanitise before logging in production
- run: gosec -quiet -exclude=G404,G304,G306,G204,G706 -exclude-dir=testdata ./...
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: '1.25.x'
check-latest: true
- uses: actions/cache@v5
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-1.25-${{ hashFiles('**/go.sum') }}
- run: go test -race -coverprofile=coverage.out ./...
- name: Build all examples
run: go build ./examples/...
- uses: actions/upload-artifact@v7
with:
name: coverage
path: coverage.out
codegen-clean:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: '1.25.x'
check-latest: true
- uses: actions/cache@v5
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-1.25-${{ hashFiles('**/go.sum') }}
- name: Regenerate against pinned snapshot
run: make regen-from-fixture
- name: Assert clean diff
run: git diff --exit-code internal/spec/api.json api/ docs/reference/
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0 # need history for drift comparison
- uses: actions/setup-go@v6
with:
go-version: '1.25.x'
check-latest: true
- uses: actions/cache@v5
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-1.25-${{ hashFiles('**/go.sum') }}
- name: Audit fallbacks
run: make audit
- name: Audit drift vs base
# On PRs: compare against the merge base (origin/<base>).
# On push to main: compare against the parent commit.
# Drift is informational; doesn't fail CI.
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE="origin/${{ github.base_ref }}"
else
BASE="HEAD~1"
fi
echo "Drift base: $BASE"
go run ./cmd/audit -ir internal/spec/api.json -drift -against "$BASE" || true
# Aggregate gate — depends on every check above. Used as a single
# required status check in branch protection AND as a dependency for the
# release job below.
# Aggregate gate for the repo-local checks. Keeps the `ci-success` name
# so an existing branch-protection rule keeps resolving.
ci-success:
runs-on: ubuntu-latest
needs: [vet, staticcheck, govulncheck, gosec, test, codegen-clean, audit]
timeout-minutes: 5
needs: [checks]
if: always()
steps:
- name: All checks passed
@@ -184,85 +38,3 @@ jobs:
echo "Failed/cancelled jobs:"
echo '${{ toJSON(needs) }}'
exit 1
# Auto-release fires on every clean push to main (and on manual
# workflow_dispatch for testing). Computes next SemVer from commit
# history via lukaszraczylo/semver-generator, dual-tags
# (v<X.Y.Z> + bot-api-v<A.B>), runs GoReleaser.
release:
needs: ci-success
if: |
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-go@v6
with:
go-version: '1.25.x'
check-latest: true
# Action interface from
# https://github.com/lukaszraczylo/semver-generator/blob/main/action.yml
# Inputs: repository_local: true (use already-cloned repo)
# existing: true (respect existing tags as base)
# Output: semantic_version (bare version string, no "v" prefix)
- name: Compute next SemVer
id: semver
uses: lukaszraczylo/semver-generator@v1
with:
repository_local: true
existing: true
config_file: .semver.yaml
- name: Read Bot API version
id: api_version
run: |
VERSION=$(python3 -c 'import json; print(json.load(open("internal/spec/api.json"))["version"])')
if [ -z "$VERSION" ] || [ "$VERSION" = "null" ]; then
echo "tag=" >> "$GITHUB_OUTPUT"
else
echo "tag=bot-api-v${VERSION}" >> "$GITHUB_OUTPUT"
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Dry-run summary
if: github.event_name == 'workflow_dispatch' && inputs.dry-run-release == true
run: |
echo "Would release: v${{ steps.semver.outputs.semantic_version }}"
if [ -n "${{ steps.api_version.outputs.tag }}" ]; then
echo "Would also tag: ${{ steps.api_version.outputs.tag }} (Bot API ${{ steps.api_version.outputs.version }})"
fi
echo "Skipping tag + release (dry-run)."
- name: Tag library + bot-api versions
if: github.event_name != 'workflow_dispatch' || inputs.dry-run-release == false
env:
LIB_TAG: v${{ steps.semver.outputs.semantic_version }}
API_TAG: ${{ steps.api_version.outputs.tag }}
API_VER: ${{ steps.api_version.outputs.version }}
run: |
# Bot API version (currently $API_VER) is intentionally NOT
# tagged separately. semver-generator picks the most recent
# tag as the version base; a non-SemVer marker like
# bot-api-vX.Y poisons that and restarts numbering from
# v0.0.x. Bot API version stays as a comment in the lib tag
# message and in the release notes.
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -a "$LIB_TAG" -m "Release $LIB_TAG (Bot API $API_VER)"
git push origin "$LIB_TAG"
- name: Run GoReleaser
if: github.event_name != 'workflow_dispatch' || inputs.dry-run-release == false
uses: goreleaser/goreleaser-action@v7
with:
distribution: goreleaser
version: '~> v2'
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BOT_API_VERSION: ${{ steps.api_version.outputs.version }}
+22
View File
@@ -0,0 +1,22 @@
name: Pull Request
on:
pull_request:
branches:
- main
push:
branches:
- "**"
- "!main"
permissions:
contents: write
actions: write
pull-requests: write
security-events: write
jobs:
pr-checks:
uses: lukaszraczylo/shared-actions/.github/workflows/go-pr.yaml@main
with:
go-version: ">=1.25"
+13 -5
View File
@@ -20,7 +20,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: '1.25.x'
go-version-file: go.mod
check-latest: true
- name: Capture latest snapshot
@@ -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
@@ -120,11 +128,11 @@ jobs:
`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
# A PR opened with GITHUB_TOKEN does not trigger `ci.yml` or `pr.yaml`, 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.
# `release.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:
+28
View File
@@ -0,0 +1,28 @@
name: Release
on:
push:
branches:
- main
paths:
- "**.go"
- "go.mod"
- "go.sum"
workflow_dispatch:
permissions:
contents: write
packages: write
id-token: write
jobs:
checks:
uses: ./.github/workflows/checks.yml
release:
needs: checks
uses: lukaszraczylo/shared-actions/.github/workflows/go-release.yaml@main
with:
go-version: ">=1.25"
semver-config: ".semver.yaml"
secrets: inherit
+24 -8
View File
@@ -200,14 +200,17 @@ const (
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"
@@ -414,6 +417,14 @@ const (
RevenueWithdrawalStateKindFailed RevenueWithdrawalStateKind = "failed"
)
type RichBlockButtonsAlign string
const (
RichBlockButtonsAlignLeft RichBlockButtonsAlign = "left"
RichBlockButtonsAlignCenter RichBlockButtonsAlign = "center"
RichBlockButtonsAlignRight RichBlockButtonsAlign = "right"
)
type RichBlockListItemType string
const (
@@ -424,14 +435,6 @@ const (
RichBlockListItemType1 RichBlockListItemType = "1"
)
type RichBlockTableCellAlign string
const (
RichBlockTableCellAlignLeft RichBlockTableCellAlign = "left"
RichBlockTableCellAlignCenter RichBlockTableCellAlign = "center"
RichBlockTableCellAlignRight RichBlockTableCellAlign = "right"
)
type RichBlockTableCellValign string
const (
@@ -452,20 +455,32 @@ const (
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 (
@@ -490,6 +505,7 @@ const (
RichTextTypeHashtag RichTextType = "hashtag"
RichTextTypeCashtag RichTextType = "cashtag"
RichTextTypeBotCommand RichTextType = "bot_command"
RichTextTypeButton RichTextType = "button"
RichTextTypeAnchor RichTextType = "anchor"
RichTextTypeAnchorLink RichTextType = "anchor_link"
RichTextTypeReference RichTextType = "reference"
+90 -109
View File
@@ -195,10 +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"`
// For outgoing ephemeral messages, unique identifier of the user who will receive the message; for group and supergroup chats only. It is not guaranteed that the user will receive the message, especially if they are offline. See ephemeral message sending for more details.
ReceiverUserID *int64 `json:"receiver_user_id,omitempty"`
// For outgoing ephemeral messages, identifier of the callback query which triggered the message if any
CallbackQueryID string `json:"callback_query_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.
@@ -378,10 +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"`
// For outgoing ephemeral messages, unique identifier of the user who will receive the message; for group and supergroup chats only. It is not guaranteed that the user will receive the message, especially if they are offline. See ephemeral message sending for more details.
ReceiverUserID *int64 `json:"receiver_user_id,omitempty"`
// For outgoing ephemeral messages, identifier of the callback query which triggered the message if any
CallbackQueryID string `json:"callback_query_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
@@ -431,11 +427,10 @@ func (p *SendPhotoParams) MultipartFields() map[string]string {
if p.DirectMessagesTopicID != nil {
out["direct_messages_topic_id"] = strconv.FormatInt(*p.DirectMessagesTopicID, 10)
}
if p.ReceiverUserID != nil {
out["receiver_user_id"] = strconv.FormatInt(*p.ReceiverUserID, 10)
}
if p.CallbackQueryID != "" {
out["callback_query_id"] = p.CallbackQueryID
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
@@ -516,10 +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"`
// For outgoing ephemeral messages, unique identifier of the user who will receive the message; for group and supergroup chats only. It is not guaranteed that the user will receive the message, especially if they are offline. See ephemeral message sending for more details.
ReceiverUserID *int64 `json:"receiver_user_id,omitempty"`
// For outgoing ephemeral messages, identifier of the callback query which triggered the message if any
CallbackQueryID string `json:"callback_query_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.
@@ -574,11 +567,10 @@ func (p *SendLivePhotoParams) MultipartFields() map[string]string {
if p.DirectMessagesTopicID != nil {
out["direct_messages_topic_id"] = strconv.FormatInt(*p.DirectMessagesTopicID, 10)
}
if p.ReceiverUserID != nil {
out["receiver_user_id"] = strconv.FormatInt(*p.ReceiverUserID, 10)
}
if p.CallbackQueryID != "" {
out["callback_query_id"] = p.CallbackQueryID
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
@@ -667,10 +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"`
// For outgoing ephemeral messages, unique identifier of the user who will receive the message; for group and supergroup chats only. It is not guaranteed that the user will receive the message, especially if they are offline. See ephemeral message sending for more details.
ReceiverUserID *int64 `json:"receiver_user_id,omitempty"`
// For outgoing ephemeral messages, identifier of the callback query which triggered the message if any
CallbackQueryID string `json:"callback_query_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
@@ -727,11 +717,10 @@ func (p *SendAudioParams) MultipartFields() map[string]string {
if p.DirectMessagesTopicID != nil {
out["direct_messages_topic_id"] = strconv.FormatInt(*p.DirectMessagesTopicID, 10)
}
if p.ReceiverUserID != nil {
out["receiver_user_id"] = strconv.FormatInt(*p.ReceiverUserID, 10)
}
if p.CallbackQueryID != "" {
out["callback_query_id"] = p.CallbackQueryID
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
@@ -823,10 +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"`
// For outgoing ephemeral messages, unique identifier of the user who will receive the message; for group and supergroup chats only. It is not guaranteed that the user will receive the message, especially if they are offline. See ephemeral message sending for more details.
ReceiverUserID *int64 `json:"receiver_user_id,omitempty"`
// For outgoing ephemeral messages, identifier of the callback query which triggered the message if any
CallbackQueryID string `json:"callback_query_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 »
@@ -879,11 +866,10 @@ func (p *SendDocumentParams) MultipartFields() map[string]string {
if p.DirectMessagesTopicID != nil {
out["direct_messages_topic_id"] = strconv.FormatInt(*p.DirectMessagesTopicID, 10)
}
if p.ReceiverUserID != nil {
out["receiver_user_id"] = strconv.FormatInt(*p.ReceiverUserID, 10)
}
if p.CallbackQueryID != "" {
out["callback_query_id"] = p.CallbackQueryID
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
@@ -968,10 +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"`
// For outgoing ephemeral messages, unique identifier of the user who will receive the message; for group and supergroup chats only. It is not guaranteed that the user will receive the message, especially if they are offline. See ephemeral message sending for more details.
ReceiverUserID *int64 `json:"receiver_user_id,omitempty"`
// For outgoing ephemeral messages, identifier of the callback query which triggered the message if any
CallbackQueryID string `json:"callback_query_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
@@ -1041,11 +1025,10 @@ func (p *SendVideoParams) MultipartFields() map[string]string {
if p.DirectMessagesTopicID != nil {
out["direct_messages_topic_id"] = strconv.FormatInt(*p.DirectMessagesTopicID, 10)
}
if p.ReceiverUserID != nil {
out["receiver_user_id"] = strconv.FormatInt(*p.ReceiverUserID, 10)
}
if p.CallbackQueryID != "" {
out["callback_query_id"] = p.CallbackQueryID
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)
@@ -1155,10 +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"`
// For outgoing ephemeral messages, unique identifier of the user who will receive the message; for group and supergroup chats only. It is not guaranteed that the user will receive the message, especially if they are offline. See ephemeral message sending for more details.
ReceiverUserID *int64 `json:"receiver_user_id,omitempty"`
// For outgoing ephemeral messages, identifier of the callback query which triggered the message if any
CallbackQueryID string `json:"callback_query_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
@@ -1219,11 +1200,10 @@ func (p *SendAnimationParams) MultipartFields() map[string]string {
if p.DirectMessagesTopicID != nil {
out["direct_messages_topic_id"] = strconv.FormatInt(*p.DirectMessagesTopicID, 10)
}
if p.ReceiverUserID != nil {
out["receiver_user_id"] = strconv.FormatInt(*p.ReceiverUserID, 10)
}
if p.CallbackQueryID != "" {
out["callback_query_id"] = p.CallbackQueryID
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)
@@ -1320,10 +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"`
// For outgoing ephemeral messages, unique identifier of the user who will receive the message; for group and supergroup chats only. It is not guaranteed that the user will receive the message, especially if they are offline. See ephemeral message sending for more details.
ReceiverUserID *int64 `json:"receiver_user_id,omitempty"`
// For outgoing ephemeral messages, identifier of the callback query which triggered the message if any
CallbackQueryID string `json:"callback_query_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
@@ -1371,11 +1349,10 @@ func (p *SendVoiceParams) MultipartFields() map[string]string {
if p.DirectMessagesTopicID != nil {
out["direct_messages_topic_id"] = strconv.FormatInt(*p.DirectMessagesTopicID, 10)
}
if p.ReceiverUserID != nil {
out["receiver_user_id"] = strconv.FormatInt(*p.ReceiverUserID, 10)
}
if p.CallbackQueryID != "" {
out["callback_query_id"] = p.CallbackQueryID
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
@@ -1443,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"`
@@ -1453,10 +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"`
// For outgoing ephemeral messages, unique identifier of the user who will receive the message; for group and supergroup chats only. It is not guaranteed that the user will receive the message, especially if they are offline. See ephemeral message sending for more details.
ReceiverUserID *int64 `json:"receiver_user_id,omitempty"`
// For outgoing ephemeral messages, identifier of the callback query which triggered the message if any
CallbackQueryID string `json:"callback_query_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
@@ -1505,11 +1480,10 @@ func (p *SendVideoNoteParams) MultipartFields() map[string]string {
if p.DirectMessagesTopicID != nil {
out["direct_messages_topic_id"] = strconv.FormatInt(*p.DirectMessagesTopicID, 10)
}
if p.ReceiverUserID != nil {
out["receiver_user_id"] = strconv.FormatInt(*p.ReceiverUserID, 10)
}
if p.CallbackQueryID != "" {
out["callback_query_id"] = p.CallbackQueryID
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)
@@ -1569,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)
}
@@ -1785,10 +1759,8 @@ 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"`
// For outgoing ephemeral messages, unique identifier of the user who will receive the message; for group and supergroup chats only. It is not guaranteed that the user will receive the message, especially if they are offline. See ephemeral message sending for more details.
ReceiverUserID *int64 `json:"receiver_user_id,omitempty"`
// For outgoing ephemeral messages, identifier of the callback query which triggered the message if any
CallbackQueryID string `json:"callback_query_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
@@ -1836,10 +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"`
// For outgoing ephemeral messages, unique identifier of the user who will receive the message; for group and supergroup chats only. It is not guaranteed that the user will receive the message, especially if they are offline. See ephemeral message sending for more details.
ReceiverUserID *int64 `json:"receiver_user_id,omitempty"`
// For outgoing ephemeral messages, identifier of the callback query which triggered the message if any
CallbackQueryID string `json:"callback_query_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
@@ -1891,10 +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"`
// For outgoing ephemeral messages, unique identifier of the user who will receive the message; for group and supergroup chats only. It is not guaranteed that the user will receive the message, especially if they are offline. See ephemeral message sending for more details.
ReceiverUserID *int64 `json:"receiver_user_id,omitempty"`
// For outgoing ephemeral messages, identifier of the callback query which triggered the message if any
CallbackQueryID string `json:"callback_query_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
@@ -2081,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 to 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"`
@@ -2089,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.
@@ -2323,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.
@@ -3138,7 +3112,7 @@ type AnswerCallbackQueryParams struct {
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"`
}
@@ -4152,7 +4126,7 @@ type EditMessageTextParams struct {
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 isn't supported when an inline message is edited.
// 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"`
@@ -4385,7 +4359,7 @@ func StopPoll(ctx context.Context, b *client.Bot, p *StopPollParams) (*Poll, err
// EditEphemeralMessageTextParams is the parameter set for EditEphemeralMessageText.
//
// Use this method to edit an ephemeral text 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.
// 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"`
@@ -4393,12 +4367,14 @@ type EditEphemeralMessageTextParams struct {
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
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"`
// 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
@@ -4407,7 +4383,7 @@ type EditEphemeralMessageTextParams struct {
// EditEphemeralMessageText calls the editEphemeralMessageText Telegram Bot API method.
//
// Use this method to edit an ephemeral text 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.
// 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)
}
@@ -4422,7 +4398,7 @@ type EditEphemeralMessageMediaParams struct {
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. A new file can't be uploaded; use a previously uploaded file via its file_id or specify a URL.
// 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"`
@@ -4479,6 +4455,8 @@ type EditEphemeralMessageCaptionParams 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.
ShowCaptionAboveMedia *bool `json:"show_caption_above_media,omitempty"`
// A JSON-serialized object for an inline keyboard
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
}
@@ -4654,10 +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"`
// For outgoing ephemeral messages, unique identifier of the user who will receive the message; for group and supergroup chats only. It is not guaranteed that the user will receive the message, especially if they are offline. See ephemeral message sending for more details.
ReceiverUserID *int64 `json:"receiver_user_id,omitempty"`
// For outgoing ephemeral messages, identifier of the callback query which triggered the message if any
CallbackQueryID string `json:"callback_query_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
@@ -4699,11 +4675,10 @@ func (p *SendStickerParams) MultipartFields() map[string]string {
if p.DirectMessagesTopicID != nil {
out["direct_messages_topic_id"] = strconv.FormatInt(*p.DirectMessagesTopicID, 10)
}
if p.ReceiverUserID != nil {
out["receiver_user_id"] = strconv.FormatInt(*p.ReceiverUserID, 10)
}
if p.CallbackQueryID != "" {
out["callback_query_id"] = p.CallbackQueryID
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
@@ -5096,6 +5071,8 @@ type SendRichMessageParams 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"`
// The message to be sent
RichMessage InputRichMessage `json:"rich_message"`
// Sends the message silently. Users will receive a notification with no sound.
@@ -5129,10 +5106,14 @@ type SendRichMessageDraftParams 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 to 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"`
// The partial message to be streamed. Direct upload of new files isn't supported.
// 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.
-7
View File
@@ -19954,7 +19954,6 @@ func Test_EditEphemeralMessageText_Success(t *testing.T) {
ChatID: ChatIDFromInt(123),
ReceiverUserID: 42,
EphemeralMessageID: 42,
Text: "test_value",
}
_, err := EditEphemeralMessageText(context.Background(), bot, params)
require.NoError(t, err)
@@ -19970,7 +19969,6 @@ func Test_EditEphemeralMessageText_APIError(t *testing.T) {
ChatID: ChatIDFromInt(123),
ReceiverUserID: 42,
EphemeralMessageID: 42,
Text: "test_value",
}
_, err := EditEphemeralMessageText(context.Background(), bot, params)
require.Error(t, err)
@@ -19989,7 +19987,6 @@ func Test_EditEphemeralMessageText_NetworkError(t *testing.T) {
ChatID: ChatIDFromInt(123),
ReceiverUserID: 42,
EphemeralMessageID: 42,
Text: "test_value",
}
_, err := EditEphemeralMessageText(context.Background(), bot, params)
require.Error(t, err)
@@ -20006,7 +20003,6 @@ func Test_EditEphemeralMessageText_ParseError(t *testing.T) {
ChatID: ChatIDFromInt(123),
ReceiverUserID: 42,
EphemeralMessageID: 42,
Text: "test_value",
}
_, err := EditEphemeralMessageText(context.Background(), bot, params)
require.Error(t, err)
@@ -20026,7 +20022,6 @@ func Test_EditEphemeralMessageText_ContextCanceled(t *testing.T) {
ChatID: ChatIDFromInt(123),
ReceiverUserID: 42,
EphemeralMessageID: 42,
Text: "test_value",
}
_, err := EditEphemeralMessageText(ctx, bot, params)
require.Error(t, err)
@@ -20068,7 +20063,6 @@ func Test_EditEphemeralMessageText_Forbidden(t *testing.T) {
ChatID: ChatIDFromInt(123),
ReceiverUserID: 42,
EphemeralMessageID: 42,
Text: "test_value",
}
_, err := EditEphemeralMessageText(context.Background(), bot, params)
require.Error(t, err)
@@ -20091,7 +20085,6 @@ func Test_EditEphemeralMessageText_ServerError(t *testing.T) {
ChatID: ChatIDFromInt(123),
ReceiverUserID: 42,
EphemeralMessageID: 42,
Text: "test_value",
}
_, err := EditEphemeralMessageText(context.Background(), bot, params)
require.Error(t, err)
+210
View File
@@ -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")
}
+2319 -28
View File
File diff suppressed because it is too large Load Diff
+144
View File
@@ -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.`)
}
+148
View File
@@ -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))
}
+359 -38
View File
@@ -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,24 +672,28 @@ 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 {
@@ -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) {
@@ -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
+4 -24
View File
@@ -271,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.
+15 -15
View File
@@ -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)
}
@@ -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)
+3
View File
@@ -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
}
+65 -6
View File
@@ -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 {
+50
View File
@@ -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
}
+99
View File
@@ -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)
}
+4
View File
@@ -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
}
+2150 -1127
View File
File diff suppressed because it is too large Load Diff
+10 -12
View File
@@ -1,21 +1,19 @@
module github.com/lukaszraczylo/go-telegram
go 1.25.0
go 1.26.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
github.com/valyala/fasthttp v1.71.0
golang.org/x/net v0.56.0
github.com/goccy/go-json v0.11.2
github.com/lukaszraczylo/oss-telemetry v0.2.5
github.com/stretchr/testify v1.12.1
github.com/valyala/fasthttp v1.75.0
golang.org/x/net v0.59.0
)
require (
github.com/andybalholm/brotli v1.2.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/klauspost/compress v1.18.6 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/klauspost/compress v1.20.1 // indirect
github.com/molecule-man/go-brrr v1.2.0 // indirect
github.com/stretchr/objx v0.5.3 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
)
+20 -30
View File
@@ -1,32 +1,22 @@
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/lukaszraczylo/oss-telemetry v0.2.1 h1:6ULyfzXplpdmIY/i01OPM1jeod9+L1RAhI0jtbVnJI0=
github.com/lukaszraczylo/oss-telemetry v0.2.1/go.mod h1:+Cn78qZo8rc3T9eZt0v3oICYRdd75wORtSidc8lNjDQ=
github.com/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=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/goccy/go-json v0.11.2 h1:jdZv93Tt4ioR8yW1CoNsvSxrcZlCXAUU1aZXN7gpXUA=
github.com/goccy/go-json v0.11.2/go.mod h1:3NdmfEkZlB7YI5UFw/qdFKq8XN1aiWR0YyRPWZNQltY=
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/lukaszraczylo/oss-telemetry v0.2.5 h1:W6Qgwp7IY4yBRO361MmnEAmM59motdQpbAFAx8v6fJM=
github.com/lukaszraczylo/oss-telemetry v0.2.5/go.mod h1:+Cn78qZo8rc3T9eZt0v3oICYRdd75wORtSidc8lNjDQ=
github.com/molecule-man/go-brrr v1.2.0 h1:dOJU45BC3Gc2WXoxAW0rgCAgVuu2gruAOGncfieA0Jw=
github.com/molecule-man/go-brrr v1.2.0/go.mod h1:7ybW6/7gA3oKY45jOfVNjSJDtrr6ea4tzbsTkjmQDC4=
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.71.0 h1:tepR7H+Guh9VUqxxcPggYi8R3lGUu2Rsdh+z7/FCY3k=
github.com/valyala/fasthttp v1.71.0/go.mod h1:z1sDUvOShhXq/C9mwH/fSm1Vb71tUJwmQdgkBrBNwnA=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
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=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
github.com/valyala/fasthttp v1.75.0 h1:W3xrvNJHG60j2ZCjxpP8kkB+oPG8kh9/g1n4+3xUwNA=
github.com/valyala/fasthttp v1.75.0/go.mod h1:JiVkPRZU124PuPsreV/s9dZRgG2lRK7wNEikdQ0Rk4s=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
+768 -215
View File
File diff suppressed because it is too large Load Diff
+25
View File
@@ -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.
+17 -16
View File
@@ -1,33 +1,34 @@
module github.com/lukaszraczylo/go-telegram/test/benchmarks
go 1.25.7
go 1.26.7
replace github.com/lukaszraczylo/go-telegram => ../../
require (
github.com/NicoNex/echotron/v3 v3.45.0
github.com/NicoNex/echotron/v3 v3.46.0
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
github.com/go-telegram/bot v1.20.0
github.com/go-telegram/bot v1.27.0
github.com/lukaszraczylo/go-telegram v0.0.0-00010101000000-000000000000
github.com/mymmrac/telego v1.8.0
github.com/mymmrac/telego v1.12.1
gopkg.in/telebot.v3 v3.3.8
)
require (
github.com/andybalholm/brotli v1.2.1 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/goccy/go-json v0.10.6 // indirect
github.com/bytedance/gopkg v0.1.4 // indirect
github.com/bytedance/sonic v1.15.4 // indirect
github.com/bytedance/sonic/loader v0.5.2 // indirect
github.com/cloudwego/base64x v0.1.7 // indirect
github.com/goccy/go-json v0.11.2 // indirect
github.com/grbit/go-json v0.11.0 // indirect
github.com/klauspost/compress v1.18.6 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/klauspost/compress v1.20.1 // indirect
github.com/klauspost/cpuid/v2 v2.4.0 // indirect
github.com/lukaszraczylo/oss-telemetry v0.2.5 // indirect
github.com/molecule-man/go-brrr v1.2.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.71.0 // indirect
github.com/valyala/fasthttp v1.75.0 // indirect
github.com/valyala/fastjson v1.6.10 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/arch v0.31.0 // indirect
golang.org/x/sys v0.48.0 // indirect
golang.org/x/time v0.16.0 // indirect
)
+40 -37
View File
@@ -57,16 +57,14 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/NicoNex/echotron/v3 v3.45.0 h1:hORujGwc6X2yaiHZIt8/rYZR48L524YtdiHxpqBwlko=
github.com/NicoNex/echotron/v3 v3.45.0/go.mod h1:7LvjveJmezuUOeaoA3nzQduNlSPQYfq219Z+baKY04Q=
github.com/NicoNex/echotron/v3 v3.46.0 h1:IHPhEDU3pbxa3uLnxRz5e247SYEVzIbQS014qvuWFOU=
github.com/NicoNex/echotron/v3 v3.46.0/go.mod h1:7LvjveJmezuUOeaoA3nzQduNlSPQYfq219Z+baKY04Q=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
@@ -77,12 +75,12 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
github.com/bytedance/sonic v1.15.4 h1:FgtV/4aBHpla9AxuMpuuzVUpa/Cf3izufkxNmnEzdI8=
github.com/bytedance/sonic v1.15.4/go.mod h1:8e51yTPdY8M6t+vvGL1c2Y1xL9i+frEeIAQAEl75NUc=
github.com/bytedance/sonic/loader v0.5.2 h1:0QtP1gevc1OZ6/H8Lb9BRZiCXd1Ftjd3OKuj1T1lBIo=
github.com/bytedance/sonic/loader v0.5.2/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -92,8 +90,8 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cloudwego/base64x v0.1.7 h1:NppS+Fgzg5ovhn4NkUXaDT3x9jldgH5ToMCqzBSi2zI=
github.com/cloudwego/base64x v0.1.7/go.mod h1:Cu1PV9zfrSf7ET2tIbWbbEy7jO7HHJ13q4X2SQ8aWYg=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
@@ -143,10 +141,10 @@ github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
github.com/go-telegram/bot v1.20.0 h1:4Pea/qTidSspr4WBJw9FbHUMNhYeqszBqQUfsQEyFbc=
github.com/go-telegram/bot v1.20.0/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM=
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/go-telegram/bot v1.27.0 h1:PG9yBDzM8O26oUerE9SZwWlHon4RpH8WcNslZ7CEZmE=
github.com/go-telegram/bot v1.27.0/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM=
github.com/goccy/go-json v0.11.2 h1:jdZv93Tt4ioR8yW1CoNsvSxrcZlCXAUU1aZXN7gpXUA=
github.com/goccy/go-json v0.11.2/go.mod h1:3NdmfEkZlB7YI5UFw/qdFKq8XN1aiWR0YyRPWZNQltY=
github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@@ -275,10 +273,10 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
@@ -290,6 +288,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/lukaszraczylo/oss-telemetry v0.2.5 h1:W6Qgwp7IY4yBRO361MmnEAmM59motdQpbAFAx8v6fJM=
github.com/lukaszraczylo/oss-telemetry v0.2.5/go.mod h1:+Cn78qZo8rc3T9eZt0v3oICYRdd75wORtSidc8lNjDQ=
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
@@ -318,10 +318,12 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/molecule-man/go-brrr v1.2.0 h1:dOJU45BC3Gc2WXoxAW0rgCAgVuu2gruAOGncfieA0Jw=
github.com/molecule-man/go-brrr v1.2.0/go.mod h1:7ybW6/7gA3oKY45jOfVNjSJDtrr6ea4tzbsTkjmQDC4=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mymmrac/telego v1.8.0 h1:EvIprWo9Cn0MHgumvvqNXPAXO1yJj3pu2cdCCeDxbow=
github.com/mymmrac/telego v1.8.0/go.mod h1:pdLV346EgVuq7Xrh3kMggeBiazeHhsdEoK0RTEOPXRM=
github.com/mymmrac/telego v1.12.1 h1:yx1T5pPSNsU3BjLR7jnfY0D4dtL9caH58Y9e8uzjR88=
github.com/mymmrac/telego v1.12.1/go.mod h1:K4z3Z3Qr6AA8yEjSry3JGScu506NlLl1O4Gqascmop4=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
@@ -330,7 +332,6 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
@@ -371,8 +372,9 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@@ -384,20 +386,18 @@ github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.71.0 h1:tepR7H+Guh9VUqxxcPggYi8R3lGUu2Rsdh+z7/FCY3k=
github.com/valyala/fasthttp v1.71.0/go.mod h1:z1sDUvOShhXq/C9mwH/fSm1Vb71tUJwmQdgkBrBNwnA=
github.com/valyala/fasthttp v1.75.0 h1:W3xrvNJHG60j2ZCjxpP8kkB+oPG8kh9/g1n4+3xUwNA=
github.com/valyala/fasthttp v1.75.0/go.mod h1:JiVkPRZU124PuPsreV/s9dZRgG2lRK7wNEikdQ0Rk4s=
github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4=
github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -420,8 +420,10 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/arch v0.31.0 h1:22MlEb14/O/EPCYHFxsDdv5TuLD5dMjT5e2QeJw4ULk=
golang.org/x/arch v0.31.0/go.mod h1:KcJSod3cqT2dKcjBxqTyGfbumNikqU9p5tHJinPJnuY=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -624,8 +626,8 @@ golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -640,8 +642,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -697,6 +699,8 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -890,7 +894,6 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+5 -3
View File
@@ -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 {
+1 -1
View File
@@ -1 +1 @@
snapshot_2026-07-21.html
snapshot_2026-09-14.html
-1
View File
@@ -23112,4 +23112,3 @@ removePreloadInit();
</script>
</body>
</html>
<!-- page generated in 48.98ms -->
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# Exports BOT_API_VERSION for the .goreleaser.yaml release header.
set -euo pipefail
[ -n "${GITHUB_ENV:-}" ] || exit 0
ver=$(python3 -c 'import json; print(json.load(open("internal/spec/api.json"))["version"])')
if [ -z "$ver" ] || [ "$ver" = "null" ]; then
exit 0
fi
echo "BOT_API_VERSION=${ver}" >> "$GITHUB_ENV"