mirror of
https://github.com/lukaszraczylo/go-telegram.git
synced 2026-10-12 00:45:02 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a8f2cb3a4 | ||
|
|
2276ce81ed | ||
|
|
ae45e17f43 | ||
|
|
8cb03ef0e9 | ||
|
|
d535029beb | ||
|
|
23cca1df8d | ||
|
|
131bbca0ed | ||
|
|
ba6f76f59f | ||
|
|
93ebf41a3c | ||
|
|
53afd9890f | ||
|
|
d666cda3b1 | ||
|
|
fa84f08b25 | ||
|
|
54790f3e5b | ||
|
|
ab5f2c5700 | ||
|
|
926d51881d | ||
|
|
4511c823a3 | ||
|
|
7c5aca3bf8 | ||
|
|
4e6382ceeb | ||
|
|
b6881461fd | ||
|
|
6612c24dfc | ||
|
|
3620f8ce1e |
@@ -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
|
||||
@@ -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
@@ -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 }}
|
||||
|
||||
@@ -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"
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,144 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/lukaszraczylo/go-telegram/internal/spec"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestUnionAlternates_NoAlternates confirms a union with no Alternates
|
||||
// (the common case — every union except RichText) produces no synthetic
|
||||
// variants.
|
||||
func TestUnionAlternates_NoAlternates(t *testing.T) {
|
||||
td := spec.TypeDecl{Name: "ChatMember", OneOf: []string{"ChatMemberOwner"}}
|
||||
require.Nil(t, unionAlternates(td))
|
||||
}
|
||||
|
||||
// TestUnionAlternates_StringAndArray confirms both alternate shapes map
|
||||
// to the expected synthetic variant names, in declaration order.
|
||||
func TestUnionAlternates_StringAndArray(t *testing.T) {
|
||||
td := spec.TypeDecl{
|
||||
Name: "RichText",
|
||||
OneOf: []string{"RichTextBold"},
|
||||
Alternates: []spec.Alternate{
|
||||
{Shape: spec.AlternateString},
|
||||
{Shape: spec.AlternateArray},
|
||||
},
|
||||
}
|
||||
got := unionAlternates(td)
|
||||
require.Equal(t, []alternateVariant{
|
||||
{Name: "RichTextPlain", IsString: true},
|
||||
{Name: "RichTextSequence", IsArray: true},
|
||||
}, got)
|
||||
}
|
||||
|
||||
// TestEmitTypes_UnionWithAlternates_EmitsShapeDispatcher builds a minimal
|
||||
// synthetic API with one discriminated union carrying Alternates (mirrors
|
||||
// RichText's shape without depending on the full real spec) and confirms
|
||||
// genapi emits: the two synthetic variant structs, their marker methods
|
||||
// and MarshalJSON, and a shape-dispatching Unmarshal<Union> that branches
|
||||
// on the first non-whitespace byte before falling back to the existing
|
||||
// discriminator switch.
|
||||
func TestEmitTypes_UnionWithAlternates_EmitsShapeDispatcher(t *testing.T) {
|
||||
api := &spec.API{
|
||||
Types: []spec.TypeDecl{
|
||||
{
|
||||
Name: "Widget",
|
||||
OneOf: []string{"WidgetBold"},
|
||||
Alternates: []spec.Alternate{
|
||||
{Shape: spec.AlternateString},
|
||||
{Shape: spec.AlternateArray},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "WidgetBold",
|
||||
Fields: []spec.Field{
|
||||
{Name: "Type", JSONName: "type", Required: true,
|
||||
Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "string"}, EnumValues: []string{"bold"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tmp := t.TempDir()
|
||||
e := newEmitter(api, tmp)
|
||||
require.NoError(t, e.emitTypes())
|
||||
|
||||
got, err := os.ReadFile(filepath.Join(tmp, "types.gen.go"))
|
||||
require.NoError(t, err)
|
||||
src := string(got)
|
||||
|
||||
// Synthetic variants exist and implement the sealed interface.
|
||||
require.Contains(t, src, "type WidgetPlain struct")
|
||||
require.Contains(t, src, "Text string")
|
||||
require.Contains(t, src, "func (*WidgetPlain) isWidget() {}")
|
||||
require.Contains(t, src, "type WidgetSequence struct")
|
||||
require.Contains(t, src, "Items []Widget")
|
||||
require.Contains(t, src, "func (*WidgetSequence) isWidget() {}")
|
||||
|
||||
// Round-trip MarshalJSON for both.
|
||||
require.Contains(t, src, "func (v *WidgetPlain) MarshalJSON() ([]byte, error) {")
|
||||
require.Contains(t, src, "return json.Marshal(v.Text)")
|
||||
require.Contains(t, src, "func (v *WidgetSequence) MarshalJSON() ([]byte, error) {")
|
||||
require.Contains(t, src, "return json.Marshal(v.Items)")
|
||||
|
||||
// Shape-dispatching UnmarshalWidget: trims, branches on '"', '[', '{'.
|
||||
require.Contains(t, src, "func UnmarshalWidget(data []byte) (Widget, error) {")
|
||||
require.Contains(t, src, `trimmed := bytes.TrimLeft(data, " \t\r\n")`)
|
||||
require.Contains(t, src, "case '\"':")
|
||||
require.Contains(t, src, "return &WidgetPlain{Text: s}, nil")
|
||||
require.Contains(t, src, "case '[':")
|
||||
require.Contains(t, src, "return &WidgetSequence{Items: items}, nil")
|
||||
require.Contains(t, src, "case '{':")
|
||||
// The pre-existing discriminator switch is preserved beneath the
|
||||
// shape dispatch, unrecognised discriminator still a clear error.
|
||||
require.Contains(t, src, `fmt.Errorf("Widget: unknown type %q", probe.V)`)
|
||||
|
||||
// The struct-holding-the-union case inherits shape dispatch for free:
|
||||
// WidgetBold has no union-typed field itself, so nothing further to
|
||||
// assert there beyond successful compilation-shaped source (covered
|
||||
// by TestEmit_Types_FixtureGolden's gofmt round trip for the real
|
||||
// fixture, and by the api-package tests for full decode behaviour).
|
||||
require.False(t, strings.Contains(src, "WidgetPlainSequence"), "sanity: no accidental name collision")
|
||||
}
|
||||
|
||||
// TestEmitTypes_UnionWithoutAlternates_Unaffected confirms a discriminated
|
||||
// union with no Alternates emits the original (pre-alternates) shape:
|
||||
// no synthetic variants, no shape dispatch, and the same "decodes a X
|
||||
// from JSON by inspecting" doc comment wording as before this feature.
|
||||
func TestEmitTypes_UnionWithoutAlternates_Unaffected(t *testing.T) {
|
||||
api := &spec.API{
|
||||
Types: []spec.TypeDecl{
|
||||
{Name: "Widget", OneOf: []string{"WidgetBold"}},
|
||||
{
|
||||
Name: "WidgetBold",
|
||||
Fields: []spec.Field{
|
||||
{Name: "Type", JSONName: "type", Required: true,
|
||||
Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "string"}, EnumValues: []string{"bold"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tmp := t.TempDir()
|
||||
e := newEmitter(api, tmp)
|
||||
require.NoError(t, e.emitTypes())
|
||||
|
||||
got, err := os.ReadFile(filepath.Join(tmp, "types.gen.go"))
|
||||
require.NoError(t, err)
|
||||
src := string(got)
|
||||
|
||||
require.NotContains(t, src, "WidgetPlain")
|
||||
require.NotContains(t, src, "WidgetSequence")
|
||||
// The package-level `var _ = bytes.TrimLeft` import guard is always
|
||||
// present (it keeps the "bytes" import valid even when no union in
|
||||
// this API has alternates); what must NOT appear is the per-union
|
||||
// shape-dispatch call site.
|
||||
require.NotContains(t, src, "trimmed := bytes.TrimLeft(data,")
|
||||
require.Contains(t, src, "// UnmarshalWidget decodes a Widget from JSON by inspecting the")
|
||||
require.Contains(t, src, `// "type" field and dispatching to the correct concrete type.`)
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/lukaszraczylo/go-telegram/internal/spec"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestDeriveDiscriminators_RichBlockAndRichText_FromRealSpec is a
|
||||
// regression test for the Bot API v10 rich_message decode failure:
|
||||
// RichBlock and RichText had no knownDiscriminators entry, so genapi
|
||||
// never emitted UnmarshalRichBlock / UnmarshalRichText and any update
|
||||
// whose message carried rich_message failed to decode. Derivation must
|
||||
// pick "type" for both and cover every variant the spec declares.
|
||||
//
|
||||
// The expected variant counts are read from the spec rather than pinned
|
||||
// as literals. Telegram adds variants in most releases - v10.3 took
|
||||
// RichBlock from 21 to 24 and RichText from 25 to 26 - so a frozen
|
||||
// number turns a routine regeneration into a spurious failure while
|
||||
// testing nothing the coverage assertion does not already cover.
|
||||
func TestDeriveDiscriminators_RichBlockAndRichText_FromRealSpec(t *testing.T) {
|
||||
api, err := loadAPI("../../internal/spec/api.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
// declaredVariants returns how many concrete variants the spec lists
|
||||
// for a union, so the assertions below track the spec instead of a
|
||||
// hardcoded count.
|
||||
declaredVariants := func(union string) int {
|
||||
for i := range api.Types {
|
||||
if api.Types[i].Name == union {
|
||||
return len(api.Types[i].OneOf)
|
||||
}
|
||||
}
|
||||
t.Fatalf("union %q not found in spec", union)
|
||||
return 0
|
||||
}
|
||||
|
||||
derived, _ := deriveDiscriminators(api)
|
||||
|
||||
rb, ok := derived["RichBlock"]
|
||||
require.True(t, ok, "RichBlock must derive a discriminator")
|
||||
require.Equal(t, "type", rb.Field)
|
||||
require.Len(t, rb.Variants, declaredVariants("RichBlock"),
|
||||
"every RichBlock variant declared in the spec must be covered")
|
||||
require.Equal(t, "RichBlockParagraph", rb.Variants["paragraph"])
|
||||
require.Equal(t, "RichBlockTable", rb.Variants["table"])
|
||||
require.Equal(t, "RichBlockThinking", rb.Variants["thinking"])
|
||||
|
||||
rt, ok := derived["RichText"]
|
||||
require.True(t, ok, "RichText must derive a discriminator")
|
||||
require.Equal(t, "type", rt.Field)
|
||||
require.Len(t, rt.Variants, declaredVariants("RichText"),
|
||||
"every RichText variant declared in the spec must be covered")
|
||||
require.Equal(t, "RichTextBold", rt.Variants["bold"])
|
||||
require.Equal(t, "RichTextAnchorLink", rt.Variants["anchor_link"])
|
||||
require.Equal(t, "RichTextReferenceLink", rt.Variants["reference_link"])
|
||||
}
|
||||
|
||||
// TestMergeDiscriminators_HandCuratedOverridesDerived confirms a
|
||||
// knownDiscriminators entry always wins the merge even when derivation
|
||||
// would also have produced a (different) answer for the same union —
|
||||
// hand-curation is the escape hatch for a union the generic rule gets
|
||||
// wrong.
|
||||
func TestMergeDiscriminators_HandCuratedOverridesDerived(t *testing.T) {
|
||||
api := &spec.API{
|
||||
Types: []spec.TypeDecl{
|
||||
{Name: "Widget", OneOf: []string{"WidgetA", "WidgetB"}},
|
||||
{Name: "WidgetA", Fields: []spec.Field{
|
||||
{Name: "Type", JSONName: "type", Required: true,
|
||||
Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "string"}, EnumValues: []string{"a"}},
|
||||
}},
|
||||
{Name: "WidgetB", Fields: []spec.Field{
|
||||
{Name: "Type", JSONName: "type", Required: true,
|
||||
Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "string"}, EnumValues: []string{"b"}},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
derived, ungated := deriveDiscriminators(api)
|
||||
require.Empty(t, ungated)
|
||||
require.Equal(t, discriminatorSpec{
|
||||
Field: "type",
|
||||
Variants: map[string]string{"a": "WidgetA", "b": "WidgetB"},
|
||||
}, derived["Widget"])
|
||||
|
||||
// Inject a hand-curated override that disagrees with the derived
|
||||
// spec, merge, and confirm the override wins outright.
|
||||
override := discriminatorSpec{Field: "kind", Variants: map[string]string{"custom": "WidgetA"}}
|
||||
knownDiscriminators["Widget"] = override
|
||||
t.Cleanup(func() { delete(knownDiscriminators, "Widget") })
|
||||
|
||||
merged := mergeDiscriminators(derived)
|
||||
require.Equal(t, override, merged["Widget"])
|
||||
}
|
||||
|
||||
// TestCheckDiscriminatorGate_FailsOnUngatedInboundUnion confirms the
|
||||
// build gate fails generation when a union Telegram can actually send
|
||||
// has no derivable discriminator: the variants share no common
|
||||
// Type/Status/Source field at all.
|
||||
func TestCheckDiscriminatorGate_FailsOnUngatedInboundUnion(t *testing.T) {
|
||||
api := &spec.API{
|
||||
Types: []spec.TypeDecl{
|
||||
{Name: "Widget", OneOf: []string{"WidgetA", "WidgetB"}},
|
||||
{Name: "WidgetA", Fields: []spec.Field{
|
||||
{Name: "Kind", JSONName: "kind", Required: true, Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "string"}},
|
||||
}},
|
||||
{Name: "WidgetB", Fields: []spec.Field{
|
||||
{Name: "Flavor", JSONName: "flavor", Required: true, Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "string"}},
|
||||
}},
|
||||
// Container is a plain (non-Input) type with a field of the
|
||||
// undiscriminated union — this is what makes Widget reachable
|
||||
// from real inbound Telegram data.
|
||||
{Name: "Container", Fields: []spec.Field{
|
||||
{Name: "W", JSONName: "w", Type: spec.TypeRef{Kind: spec.KindNamed, Name: "Widget"}},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
err := checkDiscriminatorGate(api)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "Widget")
|
||||
require.ErrorContains(t, err, "WidgetA")
|
||||
require.ErrorContains(t, err, "WidgetB")
|
||||
}
|
||||
|
||||
// TestCheckDiscriminatorGate_SilentForInputOnlyUnion confirms the same
|
||||
// undiscriminated union does NOT fail the build when its only reference
|
||||
// is a field on an Input*-prefixed type — those are always constructed
|
||||
// and marshalled by the caller, never decoded from a Telegram response.
|
||||
func TestCheckDiscriminatorGate_SilentForInputOnlyUnion(t *testing.T) {
|
||||
api := &spec.API{
|
||||
Types: []spec.TypeDecl{
|
||||
{Name: "Widget", OneOf: []string{"WidgetA", "WidgetB"}},
|
||||
{Name: "WidgetA", Fields: []spec.Field{
|
||||
{Name: "Kind", JSONName: "kind", Required: true, Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "string"}},
|
||||
}},
|
||||
{Name: "WidgetB", Fields: []spec.Field{
|
||||
{Name: "Flavor", JSONName: "flavor", Required: true, Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "string"}},
|
||||
}},
|
||||
{Name: "InputContainer", Fields: []spec.Field{
|
||||
{Name: "W", JSONName: "w", Type: spec.TypeRef{Kind: spec.KindNamed, Name: "Widget"}},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
require.NoError(t, checkDiscriminatorGate(api))
|
||||
}
|
||||
+359
-38
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -7,14 +7,16 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/goccy/go-json"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
var _ = io.Discard // keep import even if no fields use io
|
||||
var _ = json.Marshal // keep import for UnmarshalXxx helpers
|
||||
var _ = fmt.Errorf // keep import for UnmarshalXxx helpers
|
||||
var _ = io.Discard // keep import even if no fields use io
|
||||
var _ = json.Marshal // keep import for UnmarshalXxx helpers
|
||||
var _ = fmt.Errorf // keep import for UnmarshalXxx helpers
|
||||
var _ = bytes.TrimLeft // keep import for shape-dispatching UnmarshalXxx helpers (unions with alternates)
|
||||
|
||||
{{range .Types}}
|
||||
{{- $td := . -}}
|
||||
@@ -22,6 +24,7 @@ var _ = fmt.Errorf // keep import for UnmarshalXxx helpers
|
||||
// {{.Name}} is a union type. The following concrete variants implement
|
||||
// it:
|
||||
{{range .OneOf}}// - {{.}}
|
||||
{{end}}{{range unionAlternates .}}// - {{.Name}} ({{if .IsString}}bare JSON string{{else}}JSON array of {{$td.Name}}{{end}})
|
||||
{{end}}//
|
||||
{{docComment .Doc -}}
|
||||
type {{.Name}} interface{ is{{.Name}}() }
|
||||
@@ -30,11 +33,67 @@ type {{.Name}} interface{ is{{.Name}}() }
|
||||
// is{{$td.Name}} is the marker method that makes {{.}} implement {{$td.Name}}.
|
||||
func (*{{.}}) is{{$td.Name}}() {}
|
||||
{{end}}
|
||||
{{range unionAlternates .}}
|
||||
// {{.Name}} implements {{$td.Name}} for the {{if .IsString}}bare JSON string{{else}}JSON array of {{$td.Name}}{{end}} wire
|
||||
// shape Telegram documents for this union, alongside its object variants.
|
||||
type {{.Name}} struct {
|
||||
{{if .IsString}} Text string
|
||||
{{else}} Items []{{$td.Name}}
|
||||
{{end}}}
|
||||
|
||||
// is{{$td.Name}} is the marker method that makes {{.Name}} implement {{$td.Name}}.
|
||||
func (*{{.Name}}) is{{$td.Name}}() {}
|
||||
|
||||
{{if .IsString}}// MarshalJSON encodes {{.Name}} back to its bare JSON string wire shape.
|
||||
func (v *{{.Name}}) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(v.Text)
|
||||
}
|
||||
{{else}}// MarshalJSON encodes {{.Name}} back to its bare JSON array wire shape.
|
||||
func (v *{{.Name}}) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(v.Items)
|
||||
}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{if hasDiscriminator .Name}}
|
||||
// Unmarshal{{.Name}} decodes a {{.Name}} from JSON by inspecting the
|
||||
{{$alts := unionAlternates .}}{{if $alts}}// Unmarshal{{.Name}} decodes a {{.Name}} from JSON. Telegram documents
|
||||
// this type as either a bare JSON string, a JSON array of {{.Name}}, or
|
||||
// an object dispatched by its "{{discriminatorField .Name}}" field;
|
||||
// leading whitespace is trimmed and the first remaining byte selects
|
||||
// the shape.
|
||||
{{else}}// Unmarshal{{.Name}} decodes a {{.Name}} from JSON by inspecting the
|
||||
// "{{discriminatorField .Name}}" field and dispatching to the correct concrete type.
|
||||
func Unmarshal{{.Name}}(data []byte) ({{.Name}}, error) {
|
||||
var probe struct {
|
||||
{{end}}func Unmarshal{{.Name}}(data []byte) ({{.Name}}, error) {
|
||||
{{if $alts}} trimmed := bytes.TrimLeft(data, " \t\r\n")
|
||||
if len(trimmed) == 0 {
|
||||
return nil, fmt.Errorf("{{.Name}}: empty JSON value")
|
||||
}
|
||||
switch trimmed[0] {
|
||||
{{range $alts}}{{if .IsString}} case '"':
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return nil, fmt.Errorf("{{$td.Name}}: decoding string: %w", err)
|
||||
}
|
||||
return &{{.Name}}{Text: s}, nil
|
||||
{{else}} case '[':
|
||||
var raws []json.RawMessage
|
||||
if err := json.Unmarshal(data, &raws); err != nil {
|
||||
return nil, fmt.Errorf("{{$td.Name}}: decoding array: %w", err)
|
||||
}
|
||||
items := make([]{{$td.Name}}, 0, len(raws))
|
||||
for i, r := range raws {
|
||||
v, err := Unmarshal{{$td.Name}}(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("{{$td.Name}}: decoding array[%d]: %w", i, err)
|
||||
}
|
||||
items = append(items, v)
|
||||
}
|
||||
return &{{.Name}}{Items: items}, nil
|
||||
{{end}}{{end}} case '{':
|
||||
// fall through to the discriminator switch below.
|
||||
default:
|
||||
return nil, fmt.Errorf("{{.Name}}: unrecognised JSON value starting with %q", trimmed[0])
|
||||
}
|
||||
{{end}} var probe struct {
|
||||
V string `json:"{{discriminatorField .Name}}"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &probe); err != nil {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/lukaszraczylo/go-telegram/internal/spec"
|
||||
)
|
||||
|
||||
// altStringRE matches the scalar-string alternate clause Telegram uses
|
||||
// for union types that can also decode as plain text on the wire, e.g.
|
||||
// "a String for plain text". The trailing "for ..." clause is optional
|
||||
// and, when present, is not captured — only the shape matters. "an"
|
||||
// (rather than "a") is tolerated so a future rephrasing ("an String")
|
||||
// doesn't silently stop matching, and the word boundary keeps "Strings"
|
||||
// or "SubString" from misfiring.
|
||||
var altStringRE = regexp.MustCompile(`(?i)\ban?\s+String\b`)
|
||||
|
||||
// altArrayRE builds the pattern that matches the self-referential array
|
||||
// alternate clause Telegram uses for recursive union types, e.g. "an
|
||||
// Array of RichText". Matching is anchored to the union's own name so a
|
||||
// doc mentioning "an Array of" some other type is never mistaken for a
|
||||
// shape alternate.
|
||||
func altArrayRE(unionName string) *regexp.Regexp {
|
||||
return regexp.MustCompile(`(?i)\ban?\s+Array\s+of\s+` + regexp.QuoteMeta(unionName) + `\b`)
|
||||
}
|
||||
|
||||
// extractAlternates detects Telegram's "can be either a String for
|
||||
// plain text, an Array of <Union>, or any of the following types:"
|
||||
// phrasing on a union type's doc string and returns the non-object
|
||||
// shapes it declares, in a fixed (string, then array) order regardless
|
||||
// of how the sentence orders its clauses.
|
||||
//
|
||||
// Conservative by design: callers invoke this only for types that
|
||||
// already have OneOf populated (see typeFromSection in scrape.go), and
|
||||
// it only recognises the two shapes Telegram's docs currently use for
|
||||
// this ("a String" and "an Array of <the union's own name>") — anything
|
||||
// else in the doc is ignored, so free-text mentioning "String" or
|
||||
// "Array" elsewhere in a union's description can't produce a false
|
||||
// positive for a type this pattern wasn't written for. As of Bot API
|
||||
// v10, RichText is the only union whose doc matches either pattern.
|
||||
func extractAlternates(unionName, doc string) []spec.Alternate {
|
||||
var alts []spec.Alternate
|
||||
if altStringRE.MatchString(doc) {
|
||||
alts = append(alts, spec.Alternate{Shape: spec.AlternateString})
|
||||
}
|
||||
if altArrayRE(unionName).MatchString(doc) {
|
||||
alts = append(alts, spec.Alternate{Shape: spec.AlternateArray})
|
||||
}
|
||||
return alts
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/lukaszraczylo/go-telegram/internal/spec"
|
||||
)
|
||||
|
||||
func TestExtractAlternates_RichTextDoc(t *testing.T) {
|
||||
// Exact doc string as scraped from the live Bot API v10 docs page
|
||||
// (internal/spec/api.json / testdata/html/latest.html).
|
||||
doc := "This object represents a rich formatted text. Currently, it can be either a String for plain text, " +
|
||||
"an Array of RichText, or any of the following types:"
|
||||
got := extractAlternates("RichText", doc)
|
||||
require.Equal(t, []spec.Alternate{
|
||||
{Shape: spec.AlternateString},
|
||||
{Shape: spec.AlternateArray},
|
||||
}, got)
|
||||
}
|
||||
|
||||
func TestExtractAlternates_OrderIndependent(t *testing.T) {
|
||||
// Same two clauses, reversed, plus an extra clause in between —
|
||||
// order and surrounding prose must not matter.
|
||||
doc := "Currently, it can be either an Array of RichText, something else entirely, a String for plain text, " +
|
||||
"or any of the following types:"
|
||||
got := extractAlternates("RichText", doc)
|
||||
require.Equal(t, []spec.Alternate{
|
||||
{Shape: spec.AlternateString},
|
||||
{Shape: spec.AlternateArray},
|
||||
}, got)
|
||||
}
|
||||
|
||||
func TestExtractAlternates_ObjectOnlyUnion_NoAlternates(t *testing.T) {
|
||||
// RichBlock's doc is object-only: no String/Array alternate clause.
|
||||
doc := "This object represents a block in a rich formatted message. Currently, it can be any of the following types:"
|
||||
got := extractAlternates("RichBlock", doc)
|
||||
require.Nil(t, got)
|
||||
}
|
||||
|
||||
func TestExtractAlternates_ArrayOfDifferentType_NotSelfReferential(t *testing.T) {
|
||||
// "an Array of X" only counts when X is the union's own name — an
|
||||
// array of some other type mentioned in the doc must not match.
|
||||
doc := "This object can be either an Array of PhotoSize or any of the following types:"
|
||||
got := extractAlternates("RichText", doc)
|
||||
require.Nil(t, got)
|
||||
}
|
||||
|
||||
func TestExtractAlternates_StringOnly(t *testing.T) {
|
||||
doc := "This object can be either a String or any of the following types:"
|
||||
got := extractAlternates("Widget", doc)
|
||||
require.Equal(t, []spec.Alternate{{Shape: spec.AlternateString}}, got)
|
||||
}
|
||||
|
||||
func TestExtractAlternates_PlainTypeDoc_NoFalsePositive(t *testing.T) {
|
||||
// Ordinary field/type prose mentioning neither shape must not match.
|
||||
doc := "This object represents a chat photo."
|
||||
got := extractAlternates("ChatPhoto", doc)
|
||||
require.Nil(t, got)
|
||||
}
|
||||
|
||||
// TestTypeFromSection_RichText_RealSnapshot grounds the extraction in the
|
||||
// real pinned docs snapshot (not just a literal fixture string): scraping
|
||||
// testdata/html/latest.html end-to-end must produce a RichText TypeDecl
|
||||
// with both alternates, matching the same live text that broke decoding
|
||||
// of plain-text and array-shaped rich_message payloads.
|
||||
func TestTypeFromSection_RichText_RealSnapshot(t *testing.T) {
|
||||
htmlBytes, err := os.ReadFile("../../testdata/html/latest.html")
|
||||
require.NoError(t, err)
|
||||
|
||||
api, err := scrape(htmlBytes)
|
||||
require.NoError(t, err)
|
||||
|
||||
var richText *spec.TypeDecl
|
||||
for i := range api.Types {
|
||||
if api.Types[i].Name == "RichText" {
|
||||
richText = &api.Types[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotNil(t, richText, "RichText type must be present in the real docs snapshot")
|
||||
require.Equal(t, []spec.Alternate{
|
||||
{Shape: spec.AlternateString},
|
||||
{Shape: spec.AlternateArray},
|
||||
}, richText.Alternates)
|
||||
|
||||
// RichBlock is the control: object-only union, no alternates.
|
||||
var richBlock *spec.TypeDecl
|
||||
for i := range api.Types {
|
||||
if api.Types[i].Name == "RichBlock" {
|
||||
richBlock = &api.Types[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotNil(t, richBlock)
|
||||
require.Nil(t, richBlock.Alternates)
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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.60.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
|
||||
)
|
||||
|
||||
@@ -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.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
|
||||
golang.org/x/net v0.60.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
File diff suppressed because it is too large
Load Diff
@@ -24,6 +24,31 @@ type TypeDecl struct {
|
||||
// OneOf, when non-empty, indicates this type is a union and lists the concrete variant type names.
|
||||
// Variants are emitted as concrete structs implementing a sealed interface.
|
||||
OneOf []string `json:"one_of,omitempty"`
|
||||
// Alternates lists non-object wire shapes this union may also decode
|
||||
// as, in addition to the object variants in OneOf. Only populated for
|
||||
// unions whose doc declares them (Telegram's "can be either a String
|
||||
// for plain text, an Array of X, or any of the following types:"
|
||||
// phrasing) — as of Bot API v10 that's RichText alone, so every other
|
||||
// type's serialised IR is unaffected by this field's existence.
|
||||
Alternates []Alternate `json:"alternates,omitempty"`
|
||||
}
|
||||
|
||||
// AlternateShape enumerates the non-object wire shapes a union's
|
||||
// Alternates field records.
|
||||
type AlternateShape string
|
||||
|
||||
const (
|
||||
// AlternateString: the union may decode from a bare JSON string.
|
||||
AlternateString AlternateShape = "string"
|
||||
// AlternateArray: the union may decode from a JSON array whose
|
||||
// elements are themselves the same union type.
|
||||
AlternateArray AlternateShape = "array"
|
||||
)
|
||||
|
||||
// Alternate records one non-object shape a union type may appear as on
|
||||
// the wire, beyond its OneOf object variants.
|
||||
type Alternate struct {
|
||||
Shape AlternateShape `json:"shape"`
|
||||
}
|
||||
|
||||
// MethodDecl describes a Telegram API method.
|
||||
|
||||
+17
-16
@@ -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.32.0 // indirect
|
||||
golang.org/x/sys v0.49.0 // indirect
|
||||
golang.org/x/time v0.16.0 // indirect
|
||||
)
|
||||
|
||||
+40
-37
@@ -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.32.0 h1:NjGf8/xGf5JgAJ4dw4NtME5R1L7xatagqAg95T3LgII=
|
||||
golang.org/x/arch v0.32.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.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
|
||||
golang.org/x/sys v0.49.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=
|
||||
|
||||
Vendored
+5
-3
@@ -7,14 +7,16 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/goccy/go-json"
|
||||
"io"
|
||||
)
|
||||
|
||||
var _ = io.Discard // keep import even if no fields use io
|
||||
var _ = json.Marshal // keep import for UnmarshalXxx helpers
|
||||
var _ = fmt.Errorf // keep import for UnmarshalXxx helpers
|
||||
var _ = io.Discard // keep import even if no fields use io
|
||||
var _ = json.Marshal // keep import for UnmarshalXxx helpers
|
||||
var _ = fmt.Errorf // keep import for UnmarshalXxx helpers
|
||||
var _ = bytes.TrimLeft // keep import for shape-dispatching UnmarshalXxx helpers (unions with alternates)
|
||||
|
||||
// This object represents a Telegram user or bot.
|
||||
type User struct {
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
snapshot_2026-07-21.html
|
||||
snapshot_2026-09-14.html
|
||||
-1
@@ -23112,4 +23112,3 @@ removePreloadInit();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<!-- page generated in 48.98ms -->
|
||||
|
||||
+23115
File diff suppressed because one or more lines are too long
+23115
File diff suppressed because one or more lines are too long
+23115
File diff suppressed because one or more lines are too long
+23115
File diff suppressed because one or more lines are too long
+23115
File diff suppressed because one or more lines are too long
+23589
File diff suppressed because one or more lines are too long
+23589
File diff suppressed because one or more lines are too long
+23589
File diff suppressed because one or more lines are too long
Executable
+11
@@ -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"
|
||||
Reference in New Issue
Block a user