Commit Graph

16 Commits

Author SHA1 Message Date
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