Commit Graph

40 Commits

Author SHA1 Message Date
lukaszraczylo d39be13822 chore(ci): bump remaining Node 20 actions (#6)
actions/cache v4 -> v5 (Node 24 runtime; no API change)
actions/upload-artifact v4 -> v7 (ESM + Node 24; existing name/path usage unaffected)

Clears the last Node 20 deprecation warnings surfaced during the
release run in d6ecbde.
v0.7.11
2026-05-20 23:39:16 +01:00
lukaszraczylo d6ecbdea48 chore(ci): bump goreleaser-action v6 -> v7 (#5)
Node 20 deprecation. v7 release notes call out only node24+ESM
internal changes; inputs/outputs unchanged from v6.
v0.7.9
2026-05-20 23:30:34 +01:00
lukaszraczylo 99c906c3c5 fix(make): SCRAPE_INPUT tracks latest.html symlink (#4)
regen-from-fixture (used by ci.yml codegen-clean gate) hardcoded
testdata/html/snapshot_2026-05-08.html. After regen.yml committed a
newer snapshot via PR, the pinned path drifted from committed api/
output and codegen-clean failed on main.

Resolving SCRAPE_INPUT via `readlink testdata/html/latest.html` lets
each regen automatically advance the fixture pointer without a manual
Makefile bump, while preserving the override (`SCRAPE_INPUT=path \
make regen-from-fixture`) for ad-hoc replays of historical snapshots.
v0.7.7
2026-05-20 23:23:02 +01:00
github-actions[bot] f5250197b7 chore(api): regenerate from Telegram Bot API v10.0 (#3)
Co-authored-by: lukaszraczylo <2182556+lukaszraczylo@users.noreply.github.com>
2026-05-20 23:15:44 +01:00
lukaszraczylo bbbeb8461b chore(ci): bump actions off deprecated Node 20 (#2)
actions/checkout@v4 -> @v6, actions/setup-go@v5 -> @v6,
peter-evans/create-pull-request@v7 -> @v8. Pinned major
versions; all three were flagged by the runner deprecation
notice (Node 20 forced off June 2 2026, removed Sept 16 2026).
v0.7.3
2026-05-20 23:00:25 +01:00
lukaszraczylo bfb7e9875e feat(client): opt-in fasthttp transport (NewFastHTTPDoer)
Adds an alternative HTTPDoer backed by valyala/fasthttp for high-throughput
bots. Cuts per-call allocs from 102 to 56 in the cross-library bench
(within 8 of telego, which uses fasthttp by default), and per-call bytes
from 11.1 KiB to 6.6 KiB.

  bot := client.New(token,
      client.WithHTTPClient(client.NewFastHTTPDoer()),
  )

Implementation notes:
  - Wraps *fasthttp.Client behind the existing HTTPDoer (Do *http.Request)
    interface, so RetryDoer, custom transports, observability middleware,
    and the 1428 generated tests all keep working as-is.
  - Translates *http.Request -> fasthttp.Request once per call and
    returns a *http.Response whose Body releases the pooled fasthttp
    response on Close (net/http contract).
  - Recognises the bufferReadCloser / readerReadCloser shapes produced
    by buildRequest and passes their underlying bytes straight to
    SetBodyRaw -- no io.ReadAll, no copy.
  - Honours ctx.Deadline via DoDeadline, falls back to WithFastHTTPReadTimeout
    when no deadline is set. fasthttp.ErrTimeout maps to
    context.DeadlineExceeded for errors.Is compatibility.

Default stays net/http: fasthttp is HTTP/1.1 only, doesn't compose with
the http.RoundTripper middleware ecosystem, and most users don't have
the throughput to notice. Bots making thousands of API calls/sec should
opt in.

Multipart/file-upload path remains on net/http per the agreed scope --
the perf bottleneck was JSON-method round-trip, not file uploads.

Time numbers in the report deferred until a quiet-system bench run;
allocs/bytes numbers (which are deterministic per code path) are
already updated.
v0.7.1
2026-05-10 23:07:04 +01:00
lukaszraczylo 75c7ce3119 perf(client): pool req-body buffer + manual http.Request with cached URL
Two changes that together cut allocs/call from 15 to 13 (client-internal
bench) and per-call CPU from 600ns to 455ns (-24%) on the no-HTTP path:

1. Codec gets an optional BodyEncoder extension (MarshalTo io.Writer).
   When present, encodeJSONBody stream-encodes the request directly into
   a pooled *bytes.Buffer instead of allocating a [2-step] Marshal+Reader
   pair. DefaultCodec implements it via goccy/go-json.NewEncoder.
2. *Bot caches the parsed base URL on construction. buildRequest skips
   net/http.NewRequestWithContext for the common case and constructs
   *http.Request manually — clones the URL by value, sets the method
   path, and populates ContentLength + GetBody from the body's concrete
   type so RetryDoer's body-replay across attempts still works.

Cross-library bench (sendMessage round-trip vs httptest.Server): -2
allocs/call (104 -> 102), bytes -1.2%, time within noise (real HTTP
plumbing dominates). The CPU win is visible on synthetic stub-doer
benches and translates to lower GC pressure on sustained-throughput
workloads.

Slow-path fallback preserved for codecs that don't implement BodyEncoder
and for *Bot instances where url.Parse on the configured base failed —
they take the original NewRequestWithContext path.
v0.6.3
2026-05-10 22:36:57 +01:00
lukaszraczylo 607c3e8ddd test(bench): cross-library benchmarks vs top 5 go telegram libraries
Adds test/benchmarks/ as a separate Go module so competitor deps
(go-telegram-bot-api/v5, telebot.v3, go-telegram/bot, telego,
echotron/v3) stay out of the root go.mod.

Hot paths covered:
  - Webhook decode  (small Update -> typed Update struct)
  - Large unmarshal (Update with entities + reply markup + photo array)
  - API round-trip  (sendMessage against httptest.Server)
  - Dispatch route  (20 handlers, last-registered matches)

Results on Apple M4 Max / go1.26.2: ours wins 3 of 4 paths and is
2nd of 5 in the round-trip path. Full report at
docs/benchmarks/2026-05-10-comparison.md, raw output committed under
test/benchmarks/results/.

Caveats called out in the report:
  - codec asymmetry (we ship goccy/go-json; competitors mostly stdlib)
  - echotron call bench skipped — built-in rate limiter not externally
    configurable; would measure throttling, not the library
  - dispatch bench limited to libs with a public sync entry point
    (ours, telebot, gobot); gotba has no dispatcher, telego/echotron
    use channel/per-chat paradigms not directly comparable

Also gitignores docs/superpowers/ (local brainstorm/spec scratch)
and regenerates docs/reference/dispatch.md after the new
Router.Process method.
v0.6.1
2026-05-10 21:52:00 +01:00
lukaszraczylo c9a062ea04 feat(dispatch): expose Router.Process for non-Updater entry points
Adds a public synchronous entry point that runs a single update through
the global middleware chain and the dispatch table. Useful for callers
that source updates outside the standard transport.Updater flow:
custom webhook frameworks, message-bus consumers, and cross-library
benchmarks driving the router without spinning up Run.

Honours global middleware (Use); bypasses Run's concurrency semaphore
since the caller controls parallelism.
2026-05-10 21:51:13 +01:00
lukaszraczylo 7eb3398396 docs(reference): regenerate after perf series
Picks up new symbols from the perf commits: Context typed fields and Set method, header/buf pool vars in client and transport.
v0.4.5
2026-05-10 02:57:25 +01:00
lukaszraczylo 26b98a5372 perf(transport): pool *bytes.Buffer + MaxBytesReader for webhook decode
Replace the hand-rolled make([]byte, 0, 1024) + make([]byte, 4096) read loop in WebhookServer.ServeHTTP with a sync.Pool-backed bytes.Buffer drained via ReadFrom, fronted by http.MaxBytesReader for the 1 MiB body cap.

putWebhookBuf caps Cap() at 256 KiB before returning to the pool so a rare oversized update (max body is 1 MiB) doesn't permanently bloat the pool.

Bench delta on Webhook_ServeHTTP: 2564ns -> 2020ns (-21%), 12707B -> 7648B (-40%), 24 -> 23 allocs. The big byte saving is the 4 KiB tmp buffer + 1 KiB initial buf cap, replaced by one reused buffer across requests. The remaining alloc count is dominated by codec.Unmarshal decoding Update's pointer fields (*string, *int64), which is downstream of this change.
2026-05-10 02:47:58 +01:00
lukaszraczylo a416bda5f3 perf(client): pool *bytes.Buffer for response body reads
Replace io.ReadAll(resp.Body) on the typed Call/callMultipart paths with a sync.Pool-backed bytes.Buffer + ReadFrom. Saves the 512B initial allocation that ReadAll grows from on every successful call.

The pool only covers paths whose decoder copies strings out of the input (decodeResult delegates to goccy/go-json, which copies). CallRaw and callMultipartRaw return slices that alias the buffer storage, so they keep the io.ReadAll path; pooling there would need a defensive copy that defeats the saving.

putRespBuf caps Cap() at 64 KiB before returning to the pool so a single oversized response (e.g. large getFile metadata) doesn't bloat the pool for the rest of the process.

Bench delta on Call_BoolResponse: 14 allocs -> 13 allocs, 1842B -> 1331B, 526ns -> 479ns. Same shape on Call_StructResponse (16 -> 15, 1973B -> 1462B).
2026-05-10 02:45:14 +01:00
lukaszraczylo 0ee539e991 perf(dispatch): typed Context.Command/CommandArgs/RegexMatch fields
Move the three conventional Values keys ("command", "command_args", "regex_match") to typed fields on Context. Router and group routing write the fields directly; the Values map is allocated lazily via the new Set method and reserved for user-defined custom keys.

Allocation impact (M4 Max, b.Loop()):

  DispatchCommand:   5 allocs/op -> 1, 153ns -> 69ns (-55%)

  DispatchTextRegex: 5 allocs/op -> 2, 181ns -> 107ns (-41%)

  DispatchFilter:    2 allocs/op -> 1, 32ns -> 19ns (-41%)

  NewContext:        5.79ns -> 1.60ns

Trade-off: Context struct grew from ~48B to ~96B (three new fields), so filter-only paths pay ~50B more per dispatch. Command/regex paths save ~320B + 4 allocs each, which dominates for typical bot workloads.

Handlers reading c.Values["command"], c.Values["command_args"], or c.Values["regex_match"] now get nil; the typed fields c.Command, c.CommandArgs, c.RegexMatch are the new accessors. Custom keys still work via c.Set(k, v) and c.Values[k].
2026-05-10 02:35:24 +01:00
lukaszraczylo da27421521 perf(client): static headers + bool fast-path in decodeResult
Two changes on the Call hot path:

* Replace httpReq.Header.Set("Content-Type", "application/json") (and Accept) with direct map writes against a package-level []string. Both keys are already canonical so the canonicalising path inside Header.Set was pure overhead; saves the per-call []string{val} allocation x2.

* Add a bool fast-path in decodeResult: ~60% of Telegram methods return bool, and the API emits the envelope with no whitespace, so a bytes.Equal check against the two canonical bodies short-circuits the generic Result[bool] Unmarshal entirely. any(true).(Resp) does not box thanks to Go's static bool interface values.

Combined effect on Call_BoolResponse: 18 -> 14 allocs/op, 634ns -> 526ns. DecodeResult_Bool isolation bench: 50ns / 2 allocs -> 2.87ns / 0 allocs.
2026-05-10 02:32:00 +01:00
lukaszraczylo 728b28b0c5 test(bench): add allocation benchmarks for client/transport/dispatch hot paths
Hermetic benchmarks (no network) covering Call encode+decode, webhook ServeHTTP body parse, and Router dispatch (command/regex/filter). Use Go 1.24+ b.Loop() idiom. .benchstats/baseline.txt pins the pre-optimisation numbers for benchstat comparisons.
2026-05-10 02:31:46 +01:00
lukaszraczylo 9cfe193e2e docs(reference): regenerate after typed-allowed-updates merge
PR #1 changed AllowedUpdates type and shifted longpoll.go line numbers but left docs/reference/ stale, breaking the codegen-clean CI gate.
v0.3.4
2026-05-09 21:19:09 +01:00
lukaszraczylo 79c0617867 Merge pull request #1 from lukaszraczylo/feat/typed-allowed-updates
feat(api): type AllowedUpdates as []UpdateType for compile-time typo …
2026-05-09 20:53:45 +01:00
lukaszraczylo 60eb0a89b5 refactor(api): typed enums for emoji-list fields (DiceEmoji, ReactionEmoji)
Two API fields carry restricted emoji-value sets that the scraper's
curly-quote regex strips during IR extraction (multi-byte boundary
issue): ReactionTypeEmoji.Emoji and sendDice.Emoji. They previously
typed as plain string with no compile-time guarantee on values.

Add hand-curated typed-string enums in api/enums.go (the manual file,
not enums.gen.go):

  - DiceEmoji: 6 constants (Dice, Dart, Basketball, Football, Bowling,
    SlotMachine) covering Telegram's full set for sendDice.
  - ReactionEmoji: 73 constants covering the canonical reaction set
    from https://core.telegram.org/bots/api#reactiontypeemoji. Names
    follow Unicode CLDR short names where one exists, otherwise stable
    common-English labels (e.g. ThumbsUp, Heart, Clown, ManTechnologist).

Wire the field-type override via cmd/genapi/emitter.go:

  - fieldTypeOverrides map keyed "<TypeOrParamsName>.<FieldName>".
  - goField/multipartFieldEntry consult the override after the enum-plan
    lookup; falls through to the default goType when nothing matches.
  - methods.tmpl gains goFieldP/multipartFieldEntryP helpers that pass
    the params type name as override-parent (the params struct doesn't
    share a Go type with the field, so the existing parent="" enum-key
    convention is preserved).

Regenerated api/types.gen.go and api/methods.gen.go now type the two
fields as ReactionEmoji and DiceEmoji respectively. No other Emoji
field is affected (override is scoped per parent type). regen-from-
fixture is byte-deterministic across runs.

Add api/emoji_enums_test.go covering const wire values, reflection
checks on field types, and a marshal/unmarshal round-trip for
ReactionTypeEmoji.
v0.3.1
2026-05-09 20:47:16 +01:00
lukaszraczylo fecef22f48 refactor(scrape): detect prose-style "must be X" discriminator values on variants
Sealed-interface union variants whose Type/Source field is declared as
bare prose (e.g. "Type of the result, must be article" or "Scope type,
must be all_private_chats") were skipped by extractEnumValues because
the existing patterns require curly-quoted values. The genapi emitter
already extracted these values via discBareRE for marshal-side
discriminator injection; lifting the same detection into the scraper
populates Field.EnumValues so planUnifiedUnionEnums folds them into
shared union-level enums automatically.

Unions newly unified (10): BotCommandScope, MenuButton, InputMedia,
InputPaidMedia, InputPollMedia, InputPollOptionMedia, InputProfilePhoto,
InputStoryContent, InlineQueryResult, PassportElementError.

InputMessageContent stays excluded — its variants dispatch
structurally on field presence and have no Type/Source field, so
planUnifiedUnionEnums correctly skips it.

Constants added: 60 typed enum constants across the 10 unions; the
corresponding variant struct fields are retyped from string to the
shared enum.

Internal call-site cleanups: 0 — no internal package referenced these
discriminator values via magic strings.

False positives the prose detector explicitly rejects: terminal
prose-word continuations like "must be sent", "must be shown above",
"must be specified", "must be paid", "must be active", "must be one
of 3, 6, or 12", "must be between 5 and 100000", "must be a Pay
button", "must be repainted". Guarded via terminal-position regex
anchor + closed-list isProseWord filter.

Determinism verified across two consecutive make regen-from-fixture
runs. go test -race ./..., go vet ./..., staticcheck ./... all clean.
v0.2.3
2026-05-09 20:37:07 +01:00
lukaszraczylo 5523ed2b06 refactor(api): unify per-variant single-value enums into shared union-level enums
Each sealed-interface union with N variants used to emit N typed-string
enums, one per variant, each holding exactly one wire value. Codegen now
detects this pattern and emits ONE unified enum at the union level,
retyping every variant's discriminator field to point at it.

Unified enums (11 unions, 44 constants total):
  - ChatMemberStatus           (6)
  - MessageOriginType          (4)
  - BackgroundFillType         (3)
  - BackgroundTypeKind         (4)
  - ChatBoostSourceKind        (3)
  - OwnedGiftType              (2)
  - PaidMediaType              (4)
  - ReactionTypeKind           (3)
  - RevenueWithdrawalStateKind (3)
  - StoryAreaTypeKind          (5)
  - TransactionPartnerType     (7)

Naming-collision cases (union name already ends in a discriminator
concept noun, so the natural concat would stutter): BackgroundType,
ReactionType, StoryAreaType, ChatBoostSource, RevenueWithdrawalState.
The unified name ends with 'Kind' instead.

44 obsolete per-variant single-value enum types removed (e.g.
ChatMemberOwnerStatus, MessageOriginUserType). The variant struct types
themselves (ChatMemberOwner etc.) are unchanged; only their per-variant
single-value enum aliases go away.

Auto-inject MarshalJSON (commit 370c9c0) is unaffected — variant wire
values still come from the discriminator-extractor pass.

Call-site cleanup:
  - dispatch/filters/chatmember/chatmember_test.go
  - api/marshaljson_variants_test.go

New test: api/unifiedenum_test.go covers ChatMemberStatus and
MessageOriginType: variant-field retype, direct comparison without
conversion, marshal discriminator preservation, full round-trip,
stutter-suffix Kind sanity check.
v0.2.1
2026-05-09 20:26:08 +01:00
lukaszraczylo 62c76e7e4e feat(api): type AllowedUpdates as []UpdateType for compile-time typo safety
Both SetWebhookParams.AllowedUpdates and GetUpdatesParams.AllowedUpdates
(plus the WebhookInfo.AllowedUpdates field on the response side) were
typed []string, forcing callers to cast every typed constant:

  AllowedUpdates: []string{
      string(api.UpdateMessage),
      string(api.UpdateMyChatMember),
      ...
  }

Switch to []UpdateType so the same call site is typo-safe end-to-end:

  AllowedUpdates: []UpdateType{
      api.UpdateMessage,
      api.UpdateMyChatMember,
      ...
  }

Wire format is unchanged — UpdateType is type UpdateType string, marshals
identically as JSON strings. The MultipartFields()/runtime.go encoding
paths likewise continue to work via json.Marshal on the typed slice.

Implementation note: api/methods.gen.go and api/types.gen.go are
generated by cmd/genapi from internal/spec/api.json, where the field is
described as Array of String. The Telegram docs do not enumerate the
allowed_updates values inline, so the scraper cannot synthesise the
enum and UpdateType lives hand-curated in api/enums.go (see existing
doc comment there). The retype is therefore done as a small pinned
override inside cmd/genapi/emitter.go's goField — keyed on the wire
field name allowed_updates with elem type string — so the change
survives a future regeneration of the .gen.go files. transport/longpoll
drops the now-unnecessary []string conversion.

Backward incompatibility: callers passing untyped []string variables
will need to convert; callers using untyped string literals inside the
slice ARE fine because Go's untyped-literal rule auto-converts.
2026-05-09 20:20:30 +01:00
lukaszraczylo 6f9b29ea0c fix(release): stop tagging bot-api version
Debug log from run 25609309601 confirmed semver-generator picks the
chronologically most-recent tag as the version base. Whenever the
bot-api-vX.Y tag was created after the library tag (or recreated
during a partially-failing release), semver-generator picked
bot-api-v10.0 as the base, couldn't parse 'v10.0' as full SemVer
(no patch number), and restarted numbering from v0.0.x.

Per direct user instruction: drop the bot-api tag entirely. The Bot
API version still appears in the library tag message and release
notes. Also delete the existing bot-api-v10.0 tag from local + remote
so the next release sees only v* tags.
v0.1.8
2026-05-09 20:06:02 +01:00
lukaszraczylo bd80af240d chore(release): add tag-fetch step + debugmode to diagnose semver
Add an explicit git fetch --tags --force origin before semver-generator
runs, plus log visible tags. Enable semver-generator's debugmode so the
failure mode is visible. Diagnostic only — once we understand why
semver-generator computes v0.0.2 despite v0.1.2 being the latest tag,
the debug step can be removed.
2026-05-09 20:02:10 +01:00
lukaszraczylo af180b75c5 fix(release): tag bot-api marker before library version
semver-generator picks the chronologically most-recent tag as the
version base. The previous order tagged the lib version first, then
bot-api-vX.Y, leaving the bot-api tag as the most recent. semver-
generator then treated 'vX.Y' as the base, couldn't parse it as full
SemVer (no patch number), and silently restarted numbering from
v0.0.x — causing surprise version downgrades like v1.1.1 -> v0.0.4
and v0.1.1 -> v0.0.2.

Tagging bot-api-vX.Y first and the library version last keeps the
library tag as the chronologically last one, so subsequent runs see
it as the version base and bump correctly.
2026-05-09 19:58:14 +01:00
lukaszraczylo 29bf575cfd chore(release): bump to v0.1.2
Empty commit to advance the auto-release pipeline. Latest tag was
v0.1.1; semver-generator with the patch-prefix in the subject computes
the next version as v0.1.2.
v0.1.2
2026-05-09 19:51:34 +01:00
lukaszraczylo 370c9c0802 refactor(api): auto-inject discriminator value via generated MarshalJSON
Sealed-interface union variants now hardcode their wire discriminator
inside a generated MarshalJSON method instead of forcing callers to set
the field on every struct literal. Drops a class of silent-rejection
bugs where a typo in the discriminator slipped past the type checker
and through to Telegram, which then rejected the request with no
Go-side signal.

The discriminator field stays exported so incoming-message decoding,
type switches and debugging still see it. MarshalJSON wraps via a
function-local type alias and emits an outer field with the same json
tag; encoding/json (and goccy/go-json) resolve the outer field as the
shallower one and override whatever the caller wrote.

99 variants get MarshalJSON. 7 are skipped because their unions
dispatch structurally rather than by a string field: Message and
InaccessibleMessage (MaybeInaccessibleMessage, dispatched on date),
and the InputMessageContent family (InputTextMessageContent,
InputLocationMessageContent, InputVenueMessageContent,
InputContactMessageContent, InputInvoiceMessageContent — Telegram
identifies these by the presence of message_text / latitude /
phone_number / title etc.).

Discriminator extraction lives in the emitter (cmd/genapi/emitter.go).
Resolution: knownDiscriminators reverse-lookup for the 13 auto-decode
unions, then doc-string analysis ("must be X" / "always “X”")
of the variant's first required string field for marker-only unions
(BotCommandScope, InputMedia, InputPaidMedia, InputProfilePhoto,
InputStoryContent, InputPollMedia, InputPollOptionMedia,
InlineQueryResult, PassportElementError). Variants the emitter cannot
resolve a discriminator for are skipped silently rather than emitting
broken code.

Internal call-site cleanups: 4 manual discriminator assignments
removed (api/unionparam_test.go,
dispatch/filters/message/message_test.go, examples/inline/main.go ×2).
Regression tests added in api/marshaljson_variants_test.go covering
type-keyed variants, source-keyed variants, the override-user-typo
guarantee, round-trip preservation through UnmarshalChatMember, the
no-discriminator InputMessageContent path, and ride-along of
non-discriminator fields.

regen-from-fixture is deterministic across two consecutive runs;
go test -race / go vet / staticcheck all clean.
v0.1.1
2026-05-09 19:27:33 +01:00
lukaszraczylo 6ab80c27e1 fix(api): emit bare interface for all sealed-interface unions
Optional fields whose type was a sealed-interface union without an
auto-decode discriminator (BotCommandScope, InputMedia, InputPaidMedia,
InputProfilePhoto, InputStoryContent, InputMessageContent,
InputPollMedia, InputPollOptionMedia, InlineQueryResult,
PassportElementError) were emitted as *<Union> — pointer to interface,
which is a Go anti-pattern: interfaces are already nil-able, and
callers were forced to take addresses of concrete variants.

The codegen path goType() guarded against pointer-wrapping using
knownDiscriminators (only 13 unions with auto-decode dispatch), missing
the 10 marker-only sealed interfaces. New package var
knownInterfaceTypes is built from buildUnionTypeSet at emitter
construction and covers both kinds. goType() now consults that.

Net effect:
- api/methods.gen.go: 3 *BotCommandScope and 2 *InputPollMedia fields
  become bare interface
- api/types.gen.go: 14 *InputMessageContent and 1 *InputPollOptionMedia
  fields become bare interface

Regression tests in api/unionparam_test.go cover both shapes:
direct concrete-variant assignment, and nil omitempty on the bare
interface field.
2026-05-09 19:06:59 +01:00
lukaszraczylo 13ea7097e1 fix(docs): pin gomarkdoc repository URL flags for deterministic output
CI run 25607949051 failed on the codegen-clean diff check. gomarkdoc
auto-detects the GitHub repository URL from local git config — locally
on a full clone with origin set, it generates source-line links
([func Foo](https://github.com/.../blob/main/...#L4590)); on the CI
runner's fresh checkout, the remote info isn't available the same way
and gomarkdoc emits unlinked headings (## func Foo). The committed
docs (with links) and CI-regenerated docs (without links) diverged,
breaking the gate.

Pin the URL, branch, and path explicitly via gomarkdoc flags so output
is identical regardless of where it runs.
2026-05-09 19:04:23 +01:00
lukaszraczylo f899cc2663 fix(api): generate CallRaw + per-element decode for []<union> returns
GetChatAdministrators returns []ChatMember, where ChatMember is a
sealed-interface union. The codegen template emitted the generic
client.Call[..., []ChatMember] for it — encoding/json cannot unmarshal
a slice of an interface (no discriminator-aware path), so every real
response from Telegram failed at the parse step:

  telegram: parse: json: cannot unmarshal api.ChatMember into
  Go struct field Result[[]ChatMember].Result of type api.ChatMember

Fix is in cmd/genapi/methods.tmpl: add a third branch alongside the
existing single-union branch. When a method returns []<union>,
emit CallRaw + json.Unmarshal into []json.RawMessage + per-element
Unmarshal<Union>(e). Mirrors what GetChatMember (single-element)
already does, applied uniformly so any future slice-of-union method
Telegram introduces inherits the right shape.

Survey of v1.1.1 across all 23 sealed-interface unions confirms
GetChatAdministrators was the only broken site; the fix regenerates
just that one method body. New regression tests in
api/getchatadministrators_test.go cover the typical
admin+owner response and the empty-array case.
2026-05-09 18:57:54 +01:00
lukaszraczylo 5a27b53f30 fix(release): remove tag_prefixes — it filtered all v* tags out
Setting tag_prefixes: ['bot-api-'] caused semver-generator to ignore
every tag without that prefix, including the v0.x.y / v1.x.y release
tags. With no parseable existing version, it restarted numbering at
zero and produced v0.0.4 as a 'next' version after v1.1.1 — a
regression Go modules would never serve.

The bot-api-vX.Y marker is set by a separate workflow step; semver-
generator does not need to see it.
v0.0.2
2026-05-09 18:30:46 +01:00
lukaszraczylo 1ff26006f7 chore(examples): use api.Ptr for optional pointer fields
Replace the throwaway-local-var-then-take-address pattern with
api.Ptr in the polls and moderation examples. Net effect: 14 lines
gone, the pointer wrangling no longer steals visual focus from the
actual API call.
2026-05-09 18:20:00 +01:00
lukaszraczylo f3fbef64ca chore(release): require canonical BREAKING CHANGE trailer for major
The bare 'breaking' major-bump keyword was matching substrings like
'breaking-value drift' in commit bodies, producing surprise major
bumps. Restrict the major trigger to the canonical Conventional
Commits trailer only.
2026-05-09 18:19:03 +01:00
lukaszraczylo e4614d800f feat(api): add api.Ptr helper for optional scalar fields
Telegram's optional int/bool/float fields are pointers so callers can
explicitly send false or 0 to override a chat default — distinct from
'absent', which uses the chat default. The pointer construction has been
ergonomically painful:

  photoLimit := int64(5)
  Limit: &photoLimit

api.Ptr[T any](v T) *T collapses that to a single line:

  Limit: api.Ptr[int64](5)
  DisableNotification: api.Ptr(true)

Pointers stay because the explicit-zero distinction matters for fields
like DisableNotification, ProtectContent, and getUpdates.Offset where
sending 0 / false explicitly is semantically different from omitting
the field.
2026-05-09 18:01:28 +01:00
lukaszraczylo 3c04d7b0b1 feat(api): typed enums for all string-enum fields
The Telegram docs describe many string fields and parameters with
phrases like "can be ..., or ...", "must be one of ...", or "always X",
yet the generated Go API surface used raw `string` for every one of
them. Callers had to write magic strings or `string(api.ChatTypePrivate)`
to satisfy the field type. This change makes those fields typed Go
string enums emitted from the IR, so the IDE autocompletes valid values
and breaking-value drift surfaces at compile time.

Pipeline changes:

- internal/spec/ir.go: Field gains EnumValues []string. Empty for non-
  enum fields; otherwise the wire-level values in doc order, deduped.

- cmd/scrape/enums.go: extractEnumValues recognises the curly-quoted
  patterns Telegram uses ("can be either", "currently can be", "one
  of", "must be", "always X") and rejects free-text quoted refs (e.g.
  "Can be available only for X") via a tight gap check between the
  trigger phrase and the first quoted value. parse_mode parameters
  get the canonical Markdown / MarkdownV2 / HTML triple injected
  because Telegram links to a separate formatting-options section
  instead of listing values inline.

- cmd/genapi/enums.go: planEnums groups fields by sorted value-tuple,
  picks a canonical Go enum name (most-common candidate, parent-
  prefixed beats plain, shortest beats longer, alphabetical for
  determinism), resolves cross-group name collisions by parent prefix.

- cmd/genapi/emitter.go + templates: goField rewrites the field type
  to the planned enum name; multipartFieldEntry casts typed enum
  values back to string when composing the wire map; enums.tmpl now
  iterates the planned enums instead of hardcoding four hand-curated
  ones; sentinelForField produces typed-constant test fixtures.

- api/enums.gen.go: regenerated from the live IR. 66 enum types, 155
  constants. ParseMode, ChatType, MessageEntityType, ChatMember /
  MessageOrigin / PaidMedia / Background / StoryAreaType / Reaction /
  TransactionPartner / PassportElement variant Status & Type fields
  are now typed.

- api/enums.go: hand-coded UpdateType (used by transport.LongPoller).
  The Telegram docs do not enumerate Update payload kinds inline, so
  the codegen pipeline cannot synthesise this enum.

- api/types.gen.go, api/methods.gen.go, api/methods_gen_test.go: 137
  field declarations rewritten string -> typed enum.

- dispatch/, examples/: dropped every string(api.<Const>) cast. The
  HasEntity filter now takes api.MessageEntityType; ChatType filter
  compares typed values directly. ChatMember discriminator filter
  casts variant.Status (typed per variant) to string for comparison.

- internal/spec/api.json, testdata/golden/*: regenerated and
  refreshed. make regen-from-fixture is byte-deterministic across
  runs.

Renames (no compat shims; v1 pre-public):
- EntityX  -> MessageEntityTypeX  (e.g. EntityBotCommand -> MessageEntityTypeBotCommand)
- EntityStrike -> MessageEntityTypeStrikethrough (full wire name)
2026-05-09 17:55:34 +01:00
lukaszraczylo 1da759ba8a docs(pages): add .nojekyll to disable Jekyll
Jekyll's Liquid parser breaks on Go template syntax ({{.Field}}) inside
gomarkdoc-generated reference docs and on planning artefacts in
docs/superpowers/. Pages source set to 'Deploy from branch' triggers a
Jekyll build that fails on these files; .nojekyll makes Pages serve
docs/ as static content, which is what index.html actually wants.
2026-05-09 14:16:36 +01:00
lukaszraczylo 1088b7f4d7 docs: auto-generate markdown reference + soften README
- Add gomarkdoc-driven reference docs in docs/reference/, regenerated
  automatically by 'make regen' alongside the api/ codegen
- New 'make docs' target installs gomarkdoc on first run; 'make
  docs-check' is a CI gate
- Fold doc-clean assertion into existing codegen-clean job (single
  diff check covers spec + api + reference)
- Rewrite README header: logo via <picture>, friendlier tagline,
  emoji-led 'Why you'll like it' bullets instead of Why-table
- Drop duplicate echo snippet, soften 'Codegen pipeline' section into
  'Keeping up with Telegram'
- Link reference from README, Pages nav, and a new Markdown reference
  card on index.html (target = GitHub source view, renders .md natively)
2026-05-09 14:14:37 +01:00
lukaszraczylo 35058dd70b docs(pages): add GitHub Pages landing page mirroring kportal's design
- docs/index.html  — full landing page (Tailwind CDN, FA icons, Inter+JetBrains Mono,
  light/dark mode, glass-morphism nav, hero + features + comparison + install +
  usage + examples grid + codegen pipeline + advanced collapsibles + footer)
- docs/logo-light.svg / logo-dark.svg — paper-plane wordmark SVGs
- docs/CNAME — go-telegram.raczylo.com
- .github/workflows/pages.yml — GitHub Pages deploy from docs/ on push to main

ACTION REQUIRED: enable GitHub Pages in repo Settings → Pages → Source: GitHub Actions.
Add a DNS CNAME record: go-telegram.raczylo.com → lukaszraczylo.github.io
2026-05-09 14:14:37 +01:00
lukaszraczylo 750a7f51f6 Create CNAME 2026-05-09 14:13:12 +01:00
lukaszraczylo b491829267 Add sponsorship 2026-05-09 13:56:40 +01:00
lukaszraczylo ac7cae8fa7 Initial release of go-telegram
A fully-generated, strongly-typed Go client for the Telegram Bot API.

* 176 methods + 301 types generated from Bot API v10.0
* 1408 auto-generated tests (8 scenarios per method)
* Typed unions throughout — no 'any' in the public surface
* Pluggable HTTP transport and JSON codec (default goccy/go-json)
* Built-in retry middleware honouring Telegram's retry_after
* Generic dispatcher with filters and conversation handlers
* Self-verifying codegen pipeline (regen → audit → emit → run tests)
* 14 example bots covering common patterns
2026-05-09 13:09:27 +01:00