Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
c520454
ai
vjik Aug 25, 2026
2118ffb
Add DisabledButton type
vjik Aug 25, 2026
17ccf75
Add disabled field to InlineKeyboardButton type
vjik Aug 25, 2026
33e855c
Add forceReply field to InlineKeyboardMarkup type
vjik Aug 25, 2026
8358773
Add forceReply field to ReplyKeyboardMarkup type
vjik Aug 25, 2026
298c24f
Add RichMessageButton type
vjik Aug 25, 2026
7e39f85
Add RichTextButton type
vjik Aug 25, 2026
e668590
Add RichBlockButtons type
vjik Aug 25, 2026
cc5f6a7
Add InputRichBlockButtons type
vjik Aug 25, 2026
8d4978e
Add isCompact field to RichBlockTable type
vjik Aug 25, 2026
eded3ae
Add isCompact field to InputRichBlockTable type
vjik Aug 25, 2026
5d212e4
Add RichBlockExpandableBlockQuotation type
vjik Aug 25, 2026
2648917
Add InputRichBlockExpandableBlockQuotation type
vjik Aug 25, 2026
ff8b189
Add RichBlockDocument type
vjik Aug 25, 2026
9545577
Add InputRichBlockDocument type
vjik Aug 25, 2026
38799e9
Add EphemeralMessageParameters type
vjik Aug 25, 2026
c231cb9
Replace receiverUserId and callbackQueryId parameters with ephemeralM…
vjik Aug 25, 2026
92d46c1
Add ephemeralMessageParameters parameter to SendRichMessage method
vjik Aug 25, 2026
d43e238
Add showCaptionAboveMedia parameter to EditEphemeralMessageCaption me…
vjik Aug 25, 2026
b80639e
Add richMessage parameter to EditEphemeralMessageText method
vjik Aug 25, 2026
b97a306
Add canSendWelcomeMessages field to ChatAdministratorRights type
vjik Aug 25, 2026
8d8f603
Add canSendWelcomeMessages field to ChatMemberAdministrator type
vjik Aug 25, 2026
ffda6b6
Add canSendWelcomeMessages parameter to PromoteChatMember method
vjik Aug 25, 2026
b049074
Add canStop and keepOnStop parameters to SendMessageDraft method
vjik Aug 25, 2026
d3b722d
Add canStop and keepOnStop parameters to SendRichMessageDraft method
vjik Aug 25, 2026
9aa3d5c
Add MessageGenerationStopped type
vjik Aug 25, 2026
3e074ba
Add stoppedMessageGeneration field to Update type
vjik Aug 25, 2026
c60129b
Add CommunityChatJoined type
vjik Aug 25, 2026
d41370a
Add communityChatJoined field to Message type
vjik Aug 25, 2026
6e3131c
Add text, entities and isPrivate fields to UniqueGiftInfo type
vjik Aug 25, 2026
82bf09a
Update supported Telegram Bot API version to 10.3
vjik Aug 25, 2026
2c987a8
Add CHANGELOG entries for Bot API 10.3
vjik Aug 25, 2026
8c708f6
Make canSendWelcomeMessages field non-nullable
vjik Aug 25, 2026
ffec02b
Make canSendWelcomeMessages a required constructor parameter
vjik Aug 25, 2026
3c9eb79
Restrict testFromTelegramResult cases to types the API returns
vjik Aug 25, 2026
22e78fe
fix
vjik Aug 25, 2026
09f93b8
fix rector
vjik Aug 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 199 additions & 0 deletions .claude/skills/bot-api-update/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
---
name: bot-api-update
description: Implement support for a new Telegram Bot API version in this package. Use when asked to update the library to a new Bot API version.
---

# Telegram Bot API version update

Port a new Telegram Bot API release into this package, end to end.

Read `AGENTS.md` before writing any code — it holds the layout, class shapes, test patterns and
changelog rules the resulting diff is judged by. When in doubt, open a class added by the previous
Bot API commit (`git log --oneline --grep='Telegram Bot API'`) and copy its shape.

Copy the *shape* only, never the location: older commits still put new classes into the legacy
subdirectories (`src/Method/UpdatingMessage/DeleteEphemeralMessage.php`,
`src/Type/Payment/BotSubscriptionUpdated.php`). Where a file goes is decided by `AGENTS.md` alone —
every new type lands in `src/Type`, every new method in `src/Method`.

## 1. Repository state

```bash
git status --porcelain # must be empty
git branch --show-current
git fetch origin
```

A dirty working tree stops the skill: report it and ask the user what to do.

If the current branch is already a Bot API branch (`apiXY`) started from the current
`origin/master` tip, stay on it and continue from wherever it left off — do not switch, do not
create a new branch, and skip to the first unfinished item of `runtime/bot-api-update/plan.md`.

Otherwise take the fresh `master` tip; the branch itself is created in step 3, once the target
version is known:

```bash
git switch master
git pull --ff-only origin master
```

## 2. Download the official pages

Everything this skill downloads or generates lives in `runtime/bot-api-update/` — `runtime/` is
fully gitignored, and the own subdirectory keeps these files apart from the other tools' output.

```bash
mkdir -p runtime/bot-api-update
curl -fsS -o runtime/bot-api-update/api-changelog.html https://core.telegram.org/bots/api-changelog
curl -fsS -o runtime/bot-api-update/api.html https://core.telegram.org/bots/api
python3 .claude/skills/bot-api-update/scripts/telegram_page_to_md.py \
runtime/bot-api-update/api-changelog.html runtime/bot-api-update/api-changelog.md
python3 .claude/skills/bot-api-update/scripts/telegram_page_to_md.py \
runtime/bot-api-update/api.html runtime/bot-api-update/api.md
```

`api-changelog.md` says *what* changed; `api.md` (the full reference, several thousand lines — grep
it, never read it whole) gives the exact field names, types and optionality needed to write the
classes. Each type and method is a `#### <Name>` heading followed by one `| field | type |
description` line per field, so `sed -n '/^#### Foo$/,/^#### /p'` prints exactly one definition.

## 3. Is anything actually missing?

- Newest released version: the first `#### <date>` block in
`runtime/bot-api-update/api-changelog.md`, whose first line is `**Bot API X.Y**`.
- Version the package supports: the line in `README.md` that reads
`✔️ Telegram Bot API X.Y (<date>) is **fully supported**.`

If they match, the work is already done: tell the user that the package is up to date with
Bot API X.Y and **stop** — no branch, no plan, no commits.

If the package is behind by more than one release, cover every missing release in this run,
oldest first, in one plan.

Otherwise create the branch, named like the previous ones (`api102` for 10.2, `api10` for 10.0,
`api96` for 9.6 — `api` + the version digits without the dot):

```bash
git switch -c api103
```

## 4. Write the plan

Put the plan in `runtime/bot-api-update/plan.md` (gitignored — it is a working document, never
committed).

Split it as finely as it can reasonably be split. Rules for slicing:

- One new type class = one item. One new method class = one item.
- One new field on an existing type = one item; several new fields on the *same* type coming from
the same changelog bullet may share one item.
- One changelog bullet that adds the same parameter to a dozen methods = one item (it is one
logical change and one `CHANGELOG.md` line).
- A new interface plus its `ValueProcessor` and `ObjectFactory` registration = one item; each
implementation of that interface is its own item.
- Every item covers its own tests. Tests are never a separate trailing item.
- Order items so dependencies come first (types before the methods that use them).

Close the plan with these fixed items:

1. Update the supported-version line in `README.md`.
2. Add the `CHANGELOG.md` entries.
3. `composer cs-fix` + `composer rector` (step 7).
4. Verification (step 8).

Item template:

```markdown
## Bot API 10.3

- [ ] 1. Add `EphemeralMessageParameters` type
Changelog: Added the class EphemeralMessageParameters ...
Files: src/Type/EphemeralMessageParameters.php, tests/Type/EphemeralMessageParametersTest.php
Changelog entry: New: Add `EphemeralMessageParameters` type.
```

Cross-check the finished plan against the changelog bullets: every bullet of the target version
must be traceable to at least one item, including "Supported ...", "Replaced ..." and
"Renamed ..." bullets, which are easy to miss.

The `CHANGELOG.md` entries reference a PR that does not exist yet: this skill never pushes the
branch and never opens a pull request — the user does that themselves once the work is reviewed.
Only the *number* is needed here, and it is the next free issue/PR number:

```bash
gh issue list --state all --limit 1 --json number
gh pr list --state all --limit 1 --json number
```

Take the larger number, add 1, and confirm that number with the user in step 5.

## 5. Agree the plan with the user

Show the user the plan (summary plus the item list) and the PR number you intend to use, and ask
for confirmation or corrections. Do not start implementing before they answer. Apply whatever they
change to `runtime/bot-api-update/plan.md` first.

## 6. Implement, item by item

For each item, in order:

1. Implement the source change following `AGENTS.md` (backward compatibility is preserved; new
constructor parameters go last with a default).
2. Write or extend the tests in the same step.
3. Run the affected tests: `vendor/bin/phpunit --filter=<TestClass>`.
4. Commit only the files of this item:
```bash
git add <files>
git commit -m "Add EphemeralMessageParameters type"
```
English, imperative, one line — the PR is squashed, so these are working notes.
5. Mark the item `[x]` in `runtime/bot-api-update/plan.md`.

Never `git add -A` — `runtime/` is ignored, but stray files are not worth the risk. Never commit
anything under `runtime/`.

If an item turns out to be wrong or to need splitting, update `runtime/bot-api-update/plan.md` and
tell the user what changed, then continue.

## 7. cs-fix and rector

```bash
composer cs-fix
composer rector
composer test
```

Review the diff these tools produce before committing it — rector occasionally rewrites something
in a way that conflicts with the project style. Commit as `Apply cs-fix and rector`.

## 8. Verification

Report all four results to the user, honestly, with the actual output of anything that fails.

1. **Completeness.** Walk the changelog bullets of the target version one by one and confirm each
is implemented in `src/`. Confirm every `[ ]` in `runtime/bot-api-update/plan.md` is now
`[x]`.
2. **Tests.**
```bash
composer test
```
3. **Coverage — must be 100%.**
```bash
XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-text # or `composer coverage` for HTML
```
If PHPUnit warns `No code coverage driver available` (the usual case in this environment —
neither Xdebug nor PCOV is installed), say so plainly instead of claiming coverage. Then fall
back to a manual check: every class added or changed in this branch
(`git diff --stat master...HEAD -- src`) has a corresponding test that exercises the new code,
including every new facade method in `src/TelegramBotApi.php`. Tell the user that CI
(Coveralls) gives the authoritative number.
4. **Static analysis — must be clean.**
```bash
composer psalm
composer dependency-analyser
```

Finish with a short summary: version ported, number of commits, what each verification
step returned, and anything left open. Leave the branch local and unpushed — say that pushing it
and opening the PR is up to the user.
100 changes: 100 additions & 0 deletions .claude/skills/bot-api-update/scripts/telegram_page_to_md.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#!/usr/bin/env python3
"""Convert a saved core.telegram.org page into readable Markdown.

Usage:
python3 telegram_page_to_md.py <input.html> [output.md]

Works both for https://core.telegram.org/bots/api-changelog and for
https://core.telegram.org/bots/api (the full reference is huge, so the
result is meant to be searched with grep, not read as a whole).
"""

from __future__ import annotations

import html
import re
import sys

BLOCK_END = re.compile(r"</(p|ul|ol|h[1-6]|div|blockquote|table|pre)>", re.IGNORECASE)
TABLE_ROW = re.compile(r"<tr[^>]*>(.*?)</tr>", re.IGNORECASE | re.DOTALL)
TABLE_CELL = re.compile(r"<t[dh][^>]*>(.*?)</t[dh]>", re.IGNORECASE | re.DOTALL)
TAG = re.compile(r"<[^>]+>")


def _clean_cell(cell: str) -> str:
return " ".join(TAG.sub("", cell).split())


def _convert_row(match: re.Match[str]) -> str:
cells = [_clean_cell(cell) for cell in TABLE_CELL.findall(match.group(1))]
if not cells:
return "\n"
return "\n| " + " | ".join(cells) + " |\n"


def convert(source: str) -> str:
marker = '<div id="dev_page_content"'
start = source.find(marker)
body = source[start:] if start != -1 else source

body = re.sub(r"<script.*?</script>", "", body, flags=re.IGNORECASE | re.DOTALL)
body = re.sub(r"<style.*?</style>", "", body, flags=re.IGNORECASE | re.DOTALL)

# Anchor icons would otherwise turn into stray emphasis markers.
body = re.sub(r"<i class=\"anchor-icon\"></i>", "", body, flags=re.IGNORECASE)

# Keep inline emphasis: Telegram marks field and parameter names with <em>.
body = re.sub(r"</?(strong|b)>", "**", body, flags=re.IGNORECASE)
body = re.sub(r"</?(em|i)>", "_", body, flags=re.IGNORECASE)
body = re.sub(r"</?code>", "`", body, flags=re.IGNORECASE)

# A table row becomes a single line, so that one field is one grep hit.
body = TABLE_ROW.sub(_convert_row, body)

for level in range(1, 7):
body = re.sub(rf"<h{level}[^>]*>", "\n\n" + "#" * level + " ", body, flags=re.IGNORECASE)
body = re.sub(r"<li[^>]*>", "\n- ", body, flags=re.IGNORECASE)
body = re.sub(r"</li>", "", body, flags=re.IGNORECASE)
body = BLOCK_END.sub("\n", body)
body = re.sub(r"<br\s*/?>", "\n", body, flags=re.IGNORECASE)

text = html.unescape(TAG.sub("", body))
# The anchor icons leave empty emphasis markers behind.
text = text.replace("____", "").replace("****", "")

lines = [line.strip() for line in text.splitlines()]
result: list[str] = []
for index, line in enumerate(lines):
if not line:
if not result or not result[-1]:
continue
# Keep list items and table rows packed together.
following = next((item for item in lines[index + 1:] if item), "")
for prefix in ("- ", "|"):
if result[-1].startswith(prefix) and following.startswith(prefix):
break
else:
result.append(line)
continue
result.append(line)
return "\n".join(result).strip() + "\n"


def main() -> int:
if len(sys.argv) < 2:
print(__doc__, file=sys.stderr)
return 1

with open(sys.argv[1], encoding="utf-8") as handle:
text = convert(handle.read())

if len(sys.argv) > 2:
with open(sys.argv[2], "w", encoding="utf-8") as handle:
handle.write(text)
else:
sys.stdout.write(text)
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading