From c73db32e867b7188c97a44d1a7afbbca90be75a5 Mon Sep 17 00:00:00 2001 From: Kostiantyn Kriuchkov <36363097+Latand@users.noreply.github.com> Date: Sun, 15 Feb 2026 20:22:15 +0200 Subject: [PATCH 1/8] Add AGENTS/CLAUDE contributor instructions for dev-3.x (#1767) * Add AGENTS and CLAUDE contributor guidance * Update AGENTS.md to include code style enforcement via Ruff * Remove unnecessary AGENTS.md guidance --- AGENTS.md | 120 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 3 ++ 2 files changed, 123 insertions(+) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..42e9a383 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,120 @@ +# AGENTS.md + +This file defines how coding agents should contribute to `aiogram` on `dev-3.x`. + +## Scope and defaults + +- Base branch: `dev-3.x` +- Python: `>=3.10` +- Main tooling: `uv`, `ruff`, `mypy`, `pytest`, `towncrier`, `butcher` +- Keep diffs focused; avoid unrelated refactors/reformatting. + +## Setup + +```bash +uv sync --all-extras --group dev --group test +uv run pre-commit install +``` + +Note: `uv run pre-commit install` writes hooks to the shared repository `.git/hooks` +(common for all worktrees), not only for the current worktree. + +## Mandatory local checks before PR + +Code style/lint in this repository is enforced via Ruff (`ruff check` + `ruff format`). + +Quick loop (recommended for most PR iterations): + +```bash +uv run ruff check --show-fixes --preview aiogram examples +uv run ruff format --check --diff aiogram tests scripts examples +uv run mypy aiogram +uv run pytest tests +``` + +Full loop (run before final review request): + +```bash +# Run quick loop first, then: +uv run pytest --redis redis://:/ tests # when Redis storage paths are affected +uv run pytest --mongo mongodb://:@: tests # when Mongo storage paths are affected +uv run --extra docs bash -c 'cd docs && make html' # when docs or generated API docs are affected +``` + +If changes touch Redis/Mongo storage behavior, run integration variants too: + +```bash +uv run pytest --redis redis://:/ tests +uv run pytest --mongo mongodb://:@: tests +``` + +Run these only if you have accessible Redis/Mongo instances in your environment. + + +## Changelog rules (CI-gated) + +- Add `CHANGES/..rst` unless PR has `skip news` label. +- Valid categories: `feature`, `bugfix`, `doc`, `removal`, `misc`. +- Changelog text must describe user-visible behavior changes, not process/org details. +- Do not edit `CHANGES.rst` directly for regular PRs. + +## Bot API/codegen workflow (critical) + +`aiogram` API layers are generated. For Bot API related work: + +- Prefer editing generator inputs (`.butcher/**/*.yml`, aliases, templates) instead of hand-editing generated code. +- Do not manually edit `.butcher/**/entity.json` (parser/codegen will overwrite it). +- For new shortcuts, add alias/config in `.butcher` and regenerate. +- Regeneration flow: + +```bash +uv run --extra cli butcher parse +uv run --extra cli butcher refresh +uv run --extra cli butcher apply all +``` + +For maintainers preparing an API/version bump only: + +```bash +make update-api args=patch +``` + +`make update-api args=...` also runs version bump scripts and updates version-related files +(`aiogram/__meta__.py`, `README.rst`, `docs/index.rst`). + +After regeneration, run lint/type/tests again. + +## Maintainer review signals (recent PRs) + +These patterns repeatedly appeared in maintainer feedback and should be treated as hard constraints: + +- Keep generation path consistent: shortcuts/features should be added through `.butcher` config + generation, not ad-hoc manual edits. +- Keep test style consistent with existing suite; avoid introducing new dependencies for small tests. +- Preserve framework contracts (e.g., dispatcher/workflow data passed to startup/shutdown callbacks). +- When fixing generated API metadata/docs, update the source mapping in `.butcher` so future regenerations keep the fix. + +## Documentation work + +For docs changes: + +```bash +uv run --extra docs sphinx-autobuild --watch aiogram/ --watch CHANGES.rst --watch README.rst docs/ docs/_build/ +``` + +`sphinx-autobuild` is long-running by design. + +Or quick build: + +```bash +uv run --extra docs bash -c 'cd docs && make html' +``` + +## PR quality checklist + +Before requesting review: + +1. Tests added/updated for behavior changes. +2. Local lint/type/tests pass. +3. Changelog fragment added (or `skip news` is justified). +4. If codegen-related: generated files and source config are both updated coherently. +5. PR body includes clear reproduction/validation steps. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..a97d7a44 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,3 @@ +# CLAUDE.md + +Use @AGENTS.md as the source of truth for contribution workflow, checks, and Bot API codegen rules in this repository. From fa844fce591645dc61f4c350a694dc7da6426fe8 Mon Sep 17 00:00:00 2001 From: Kostiantyn Kriuchkov <36363097+Latand@users.noreply.github.com> Date: Sun, 15 Feb 2026 20:22:56 +0200 Subject: [PATCH 2/8] Add icon and style args to keyboard builders (#1769) --- CHANGES/1768.bugfix.rst | 1 + aiogram/utils/keyboard.py | 8 +++ tests/test_utils/test_keyboard.py | 91 +++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+) create mode 100644 CHANGES/1768.bugfix.rst diff --git a/CHANGES/1768.bugfix.rst b/CHANGES/1768.bugfix.rst new file mode 100644 index 00000000..4b8c94bf --- /dev/null +++ b/CHANGES/1768.bugfix.rst @@ -0,0 +1 @@ +Added ``icon_custom_emoji_id`` and ``style`` parameters to ``InlineKeyboardBuilder.button`` and ``ReplyKeyboardBuilder.button`` signatures. diff --git a/aiogram/utils/keyboard.py b/aiogram/utils/keyboard.py index 8824edfd..2dba452c 100644 --- a/aiogram/utils/keyboard.py +++ b/aiogram/utils/keyboard.py @@ -303,6 +303,8 @@ class InlineKeyboardBuilder(KeyboardBuilder[InlineKeyboardButton]): self, *, text: str, + icon_custom_emoji_id: str | None = None, + style: str | None = None, url: str | None = None, callback_data: str | CallbackData | None = None, web_app: WebAppInfo | None = None, @@ -319,6 +321,8 @@ class InlineKeyboardBuilder(KeyboardBuilder[InlineKeyboardButton]): InlineKeyboardBuilder, self._button( text=text, + icon_custom_emoji_id=icon_custom_emoji_id, + style=style, url=url, callback_data=callback_data, web_app=web_app, @@ -375,6 +379,8 @@ class ReplyKeyboardBuilder(KeyboardBuilder[KeyboardButton]): self, *, text: str, + icon_custom_emoji_id: str | None = None, + style: str | None = None, request_users: KeyboardButtonRequestUsers | None = None, request_chat: KeyboardButtonRequestChat | None = None, request_contact: bool | None = None, @@ -387,6 +393,8 @@ class ReplyKeyboardBuilder(KeyboardBuilder[KeyboardButton]): ReplyKeyboardBuilder, self._button( text=text, + icon_custom_emoji_id=icon_custom_emoji_id, + style=style, request_users=request_users, request_chat=request_chat, request_contact=request_contact, diff --git a/tests/test_utils/test_keyboard.py b/tests/test_utils/test_keyboard.py index e80fed8e..b374e7ac 100644 --- a/tests/test_utils/test_keyboard.py +++ b/tests/test_utils/test_keyboard.py @@ -214,11 +214,63 @@ class TestKeyboardBuilder: "builder_type,kwargs,expected", [ [ReplyKeyboardBuilder, {"text": "test"}, KeyboardButton(text="test")], + [ + ReplyKeyboardBuilder, + {"text": "test", "icon_custom_emoji_id": "emoji-id"}, + KeyboardButton(text="test", icon_custom_emoji_id="emoji-id"), + ], + [ + ReplyKeyboardBuilder, + {"text": "test", "style": "success"}, + KeyboardButton(text="test", style="success"), + ], + [ + ReplyKeyboardBuilder, + {"text": "test", "icon_custom_emoji_id": "emoji-id", "style": "success"}, + KeyboardButton( + text="test", + icon_custom_emoji_id="emoji-id", + style="success", + ), + ], [ InlineKeyboardBuilder, {"text": "test", "callback_data": "callback"}, InlineKeyboardButton(text="test", callback_data="callback"), ], + [ + InlineKeyboardBuilder, + { + "text": "test", + "icon_custom_emoji_id": "emoji-id", + "callback_data": "callback", + }, + InlineKeyboardButton( + text="test", + icon_custom_emoji_id="emoji-id", + callback_data="callback", + ), + ], + [ + InlineKeyboardBuilder, + {"text": "test", "style": "primary", "callback_data": "callback"}, + InlineKeyboardButton(text="test", style="primary", callback_data="callback"), + ], + [ + InlineKeyboardBuilder, + { + "text": "test", + "icon_custom_emoji_id": "emoji-id", + "style": "primary", + "callback_data": "callback", + }, + InlineKeyboardButton( + text="test", + icon_custom_emoji_id="emoji-id", + style="primary", + callback_data="callback", + ), + ], [ InlineKeyboardBuilder, {"text": "test", "callback_data": MyCallback(value="test")}, @@ -242,6 +294,45 @@ class TestKeyboardBuilder: def test_as_markup(self, builder, expected): assert isinstance(builder.as_markup(), expected) + @pytest.mark.parametrize( + "builder,button_kwargs,icon_custom_emoji_id,style", + [ + [ + ReplyKeyboardBuilder(), + {"text": "test", "icon_custom_emoji_id": "emoji-id", "style": "success"}, + "emoji-id", + "success", + ], + [ + InlineKeyboardBuilder(), + { + "text": "test", + "icon_custom_emoji_id": "emoji-id", + "style": "primary", + "callback_data": "callback", + }, + "emoji-id", + "primary", + ], + ], + ) + def test_as_markup_preserves_icon_and_style( + self, + builder, + button_kwargs, + icon_custom_emoji_id, + style, + ): + builder.button(**button_kwargs) + markup = builder.as_markup() + if isinstance(markup, ReplyKeyboardMarkup): + button = markup.keyboard[0][0] + else: + button = markup.inline_keyboard[0][0] + + assert button.icon_custom_emoji_id == icon_custom_emoji_id + assert button.style == style + @pytest.mark.parametrize( "markup,builder_type", [ From e37eddbe8c6fccfdfa49fdd249e7ebadc511d6c5 Mon Sep 17 00:00:00 2001 From: Kostiantyn Kriuchkov <36363097+Latand@users.noreply.github.com> Date: Sun, 15 Feb 2026 20:24:15 +0200 Subject: [PATCH 3/8] Document webhook proxy trust model (#47) (#1765) --- CHANGES/47.misc.rst | 1 + docs/dispatcher/webhook.rst | 13 +++++ .../uk_UA/LC_MESSAGES/dispatcher/webhook.po | 58 +++++++++++++++++-- 3 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 CHANGES/47.misc.rst diff --git a/CHANGES/47.misc.rst b/CHANGES/47.misc.rst new file mode 100644 index 00000000..c895c16f --- /dev/null +++ b/CHANGES/47.misc.rst @@ -0,0 +1 @@ +Documented webhook security constraints for proxy deployments, including trust requirements for :code:`X-Forwarded-For` and recommended defense-in-depth checks. diff --git a/docs/dispatcher/webhook.rst b/docs/dispatcher/webhook.rst index 73bcddbc..26c58f36 100644 --- a/docs/dispatcher/webhook.rst +++ b/docs/dispatcher/webhook.rst @@ -66,6 +66,19 @@ It can be acy using firewall rules or nginx configuration or middleware on appli So, aiogram has an implementation of the IP filtering middleware for aiohttp. +`aiogram` IP filtering middleware reads the left-most IP address from `X-Forwarded-For`. + +.. warning:: + + `X-Forwarded-For` is trustworthy only if all webhook traffic goes through a trusted reverse proxy that rewrites this header. + If your application is directly reachable from the Internet, this header can be forged. + +For production deployments, use defense in depth: + +- Always set and verify :code:`X-Telegram-Bot-Api-Secret-Token` +- Restrict network access to the webhook endpoint (firewall, security groups, ACL) +- Ensure the backend app is not publicly reachable and accepts requests only from the trusted proxy + .. autofunction:: aiogram.webhook.aiohttp_server.ip_filter_middleware .. autoclass:: aiogram.webhook.security.IPFilter diff --git a/docs/locale/uk_UA/LC_MESSAGES/dispatcher/webhook.po b/docs/locale/uk_UA/LC_MESSAGES/dispatcher/webhook.po index 880fef46..ee10e8ca 100644 --- a/docs/locale/uk_UA/LC_MESSAGES/dispatcher/webhook.po +++ b/docs/locale/uk_UA/LC_MESSAGES/dispatcher/webhook.po @@ -203,45 +203,95 @@ msgstr "" #: ../../dispatcher/webhook.rst:51 msgid "Security" -msgstr "" +msgstr "Безпека" #: ../../dispatcher/webhook.rst:53 msgid "" "Telegram supports two methods to verify incoming requests that they are " "from Telegram:" -msgstr "" +msgstr "Telegram підтримує два методи перевірки вхідних запитів, що вони надходять від Telegram:" #: ../../dispatcher/webhook.rst:56 msgid "Using a secret token" -msgstr "" +msgstr "Використання секретного токена" #: ../../dispatcher/webhook.rst:58 msgid "" "When you set webhook, you can specify a secret token and then use it to " "verify incoming requests." msgstr "" +"Коли ви налаштовуєте webhook, ви можете вказати секретний токен і потім " +"використовувати його для перевірки вхідних запитів." #: ../../dispatcher/webhook.rst:61 msgid "Using IP filtering" -msgstr "" +msgstr "Використання фільтрації за IP" #: ../../dispatcher/webhook.rst:63 msgid "" "You can specify a list of IP addresses from which you expect incoming " "requests, and then use it to verify incoming requests." msgstr "" +"Ви можете вказати список IP-адрес, з яких очікуєте вхідні запити, і " +"використовувати його для перевірки запитів." #: ../../dispatcher/webhook.rst:65 msgid "" "It can be acy using firewall rules or nginx configuration or middleware " "on application level." msgstr "" +"Це можна зробити за допомогою правил firewall, конфігурації nginx або " +"middleware на рівні застосунку." #: ../../dispatcher/webhook.rst:67 msgid "" "So, aiogram has an implementation of the IP filtering middleware for " "aiohttp." msgstr "" +"Тому в aiogram є реалізація middleware для фільтрації за IP для aiohttp." + +#: ../../dispatcher/webhook.rst:69 +msgid "" +"`aiogram` IP filtering middleware reads the left-most IP address from " +"`X-Forwarded-For`." +msgstr "" +"IP-фільтр middleware в `aiogram` читає крайню ліву IP-адресу з " +"`X-Forwarded-For`." + +#: ../../dispatcher/webhook.rst:73 +msgid "" +"`X-Forwarded-For` is trustworthy only if all webhook traffic goes through a" +" trusted reverse proxy that rewrites this header. If your application is " +"directly reachable from the Internet, this header can be forged." +msgstr "" +"`X-Forwarded-For` можна вважати надійним лише тоді, коли весь webhook-" +"трафік проходить через довірений reverse proxy, який перезаписує цей " +"заголовок. Якщо ваш застосунок напряму доступний з Інтернету, цей " +"заголовок можна підробити." + +#: ../../dispatcher/webhook.rst:76 +msgid "For production deployments, use defense in depth:" +msgstr "Для production-деплойментів використовуйте багаторівневий захист:" + +#: ../../dispatcher/webhook.rst:78 +msgid "Always set and verify :code:`X-Telegram-Bot-Api-Secret-Token`" +msgstr "Завжди встановлюйте та перевіряйте :code:`X-Telegram-Bot-Api-Secret-Token`" + +#: ../../dispatcher/webhook.rst:79 +msgid "" +"Restrict network access to the webhook endpoint (firewall, security " +"groups, ACL)" +msgstr "" +"Обмежуйте мережевий доступ до webhook endpoint (firewall, security groups, " +"ACL)" + +#: ../../dispatcher/webhook.rst:80 +msgid "" +"Ensure the backend app is not publicly reachable and accepts requests only " +"from the trusted proxy" +msgstr "" +"Переконайтеся, що backend-застосунок не доступний публічно та приймає " +"запити лише від довіреного proxy" #: ../../dispatcher/webhook.rst:75 msgid "Examples" From 73710acb4ce8ae37cb177f6edfbb0d1a7eea149f Mon Sep 17 00:00:00 2001 From: Kostiantyn Kriuchkov <36363097+Latand@users.noreply.github.com> Date: Sun, 15 Feb 2026 20:24:34 +0200 Subject: [PATCH 4/8] Preserve middleware data across scene transitions (#1687) (#1766) * Preserve middleware context across scene goto transitions (#1687) * Add After.goto coverage for scene middleware context (#1687) --- CHANGES/1687.bugfix.rst | 1 + aiogram/fsm/scene.py | 4 + tests/test_fsm/test_scene.py | 2 + ...t_1687_scene_goto_loses_middleware_data.py | 106 ++++++++++++++++++ 4 files changed, 113 insertions(+) create mode 100644 CHANGES/1687.bugfix.rst create mode 100644 tests/test_issues/test_1687_scene_goto_loses_middleware_data.py diff --git a/CHANGES/1687.bugfix.rst b/CHANGES/1687.bugfix.rst new file mode 100644 index 00000000..9fac31c9 --- /dev/null +++ b/CHANGES/1687.bugfix.rst @@ -0,0 +1 @@ +Fixed scene transitions to preserve middleware-injected data when moving between scenes via ``SceneWizard.goto``. diff --git a/aiogram/fsm/scene.py b/aiogram/fsm/scene.py index da0a52d2..4c7fa72c 100644 --- a/aiogram/fsm/scene.py +++ b/aiogram/fsm/scene.py @@ -259,6 +259,7 @@ class SceneHandlerWrapper: ) raise SceneException(msg) from None event_update: Update = kwargs["event_update"] + scenes.data = {**scenes.data, **kwargs} scene = self.scene( wizard=SceneWizard( scene_config=self.scene.__scene_config__, @@ -712,6 +713,9 @@ class ScenesManager: :param kwargs: Additional keyword arguments to pass to the scene's wizard.enter() method. :return: None """ + if kwargs: + self.data = {**self.data, **kwargs} + if _check_active: active_scene = await self._get_active_scene() if active_scene is not None: diff --git a/tests/test_fsm/test_scene.py b/tests/test_fsm/test_scene.py index 3a9944b0..ba601325 100644 --- a/tests/test_fsm/test_scene.py +++ b/tests/test_fsm/test_scene.py @@ -253,6 +253,7 @@ class TestSceneHandlerWrapper: state_mock = AsyncMock(spec=FSMContext) scenes_mock = AsyncMock(spec=ScenesManager) + scenes_mock.data = {} event_update_mock = Update( update_id=42, message=Message( @@ -282,6 +283,7 @@ class TestSceneHandlerWrapper: state_mock = AsyncMock(spec=FSMContext) scenes_mock = AsyncMock(spec=ScenesManager) + scenes_mock.data = {} event_update_mock = Update( update_id=42, message=Message( diff --git a/tests/test_issues/test_1687_scene_goto_loses_middleware_data.py b/tests/test_issues/test_1687_scene_goto_loses_middleware_data.py new file mode 100644 index 00000000..d0433298 --- /dev/null +++ b/tests/test_issues/test_1687_scene_goto_loses_middleware_data.py @@ -0,0 +1,106 @@ +from collections.abc import Awaitable, Callable +from datetime import datetime +from typing import Any + +from aiogram import BaseMiddleware, Dispatcher +from aiogram.enums import ChatType +from aiogram.filters import CommandStart +from aiogram.fsm.scene import After, Scene, SceneRegistry, on +from aiogram.types import Chat, Message, TelegramObject, Update, User +from tests.mocked_bot import MockedBot + + +class TestContextMiddleware(BaseMiddleware): + async def __call__( + self, + handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]], + event: TelegramObject, + data: dict[str, Any], + ) -> Any: + data["test_context"] = "context from middleware" + return await handler(event, data) + + +class TargetScene(Scene, state="target"): + entered_with_context: str | None = None + + @on.message.enter() + async def on_enter(self, message: Message, test_context: str) -> None: + type(self).entered_with_context = test_context + + +class StartScene(Scene, state="start"): + @on.message.enter() + async def on_start(self, message: Message) -> None: + await self.wizard.goto(TargetScene) + + +class StartSceneWithAfter(Scene, state="start_with_after"): + @on.message(after=After.goto(TargetScene)) + async def goto_target_with_after(self, message: Message) -> None: + pass + + +async def test_scene_goto_preserves_message_middleware_data(bot: MockedBot) -> None: + dp = Dispatcher() + registry = SceneRegistry(dp) + registry.add(StartScene, TargetScene) + dp.message.register(StartScene.as_handler(), CommandStart()) + dp.message.middleware(TestContextMiddleware()) + + TargetScene.entered_with_context = None + + update = Update( + update_id=1, + message=Message( + message_id=1, + date=datetime.now(), + chat=Chat(id=42, type=ChatType.PRIVATE), + from_user=User(id=42, is_bot=False, first_name="Test"), + text="/start", + ), + ) + + await dp.feed_update(bot, update) + + assert TargetScene.entered_with_context == "context from middleware" + + +async def test_scene_after_goto_preserves_message_middleware_data(bot: MockedBot) -> None: + dp = Dispatcher() + registry = SceneRegistry(dp) + registry.add(StartSceneWithAfter, TargetScene) + dp.message.register(StartSceneWithAfter.as_handler(), CommandStart()) + dp.message.middleware(TestContextMiddleware()) + + TargetScene.entered_with_context = None + + await dp.feed_update( + bot, + Update( + update_id=1, + message=Message( + message_id=1, + date=datetime.now(), + chat=Chat(id=42, type=ChatType.PRIVATE), + from_user=User(id=42, is_bot=False, first_name="Test"), + text="/start", + ), + ), + ) + + await dp.feed_update( + bot, + Update( + update_id=2, + message=Message( + message_id=2, + date=datetime.now(), + chat=Chat(id=42, type=ChatType.PRIVATE), + from_user=User(id=42, is_bot=False, first_name="Test"), + text="go", + ), + ), + ) + + assert TargetScene.entered_with_context == "context from middleware" From f68c24d6206d71b10e0c127c3b4f39c3dd8aed79 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Tue, 3 Mar 2026 01:19:11 +0200 Subject: [PATCH 5/8] Added support for Telegram Bot API 9.5 (#1780) * Update API methods and types for Telegram Bot API 9.5 * Draft: follow-up for Bot API 9.5 (#1780) (#1781) * Add set_chat_member_tag shortcut coverage * Add set_member_tag shortcut tests and align decoration expectations * Fix follow-up test coverage for sender_tag and can_edit_tag * Add changelog fragment for PR 1781 * Align changelog with base PR #1780 * Expand 1780 changelog to cover base and follow-up scope * Treat sender_tag as metadata, not message content type --------- Co-authored-by: Latand Co-authored-by: Codex Agent * Add tests for date_time formatting with Unix time and datetime objects * Update changelog with Telegram Bot API 9.5 changes --------- Co-authored-by: Kostiantyn Kriuchkov <36363097+Latand@users.noreply.github.com> Co-authored-by: Latand Co-authored-by: Codex Agent --- .apiversion | 2 +- .butcher/enums/ContentType.yml | 1 + .../methods/editMessageChecklist/entity.json | 4 +- .../methods/promoteChatMember/entity.json | 8 + .butcher/methods/sendChecklist/entity.json | 4 +- .butcher/methods/sendMessageDraft/entity.json | 6 +- .butcher/methods/setChatMemberTag/entity.json | 41 +++++ .butcher/schema/schema.json | 158 +++++++++++++++--- .butcher/types/Chat/aliases.yml | 4 + .../types/ChatAdministratorRights/entity.json | 8 + .../types/ChatMemberAdministrator/entity.json | 8 + .butcher/types/ChatMemberMember/entity.json | 8 + .../types/ChatMemberRestricted/entity.json | 16 ++ .butcher/types/ChatPermissions/entity.json | 8 + .butcher/types/GameHighScore/entity.json | 6 +- .../InlineQueryResultDocument/entity.json | 4 +- .butcher/types/Message/entity.json | 18 +- .butcher/types/MessageEntity/entity.json | 22 ++- CHANGES/1780.misc.rst | 15 ++ README.rst | 2 +- aiogram/__meta__.py | 4 +- aiogram/client/bot.py | 36 +++- aiogram/enums/message_entity_type.py | 1 + aiogram/methods/__init__.py | 2 + aiogram/methods/edit_message_checklist.py | 2 +- aiogram/methods/promote_chat_member.py | 4 + aiogram/methods/send_checklist.py | 2 +- aiogram/methods/send_message_draft.py | 2 +- aiogram/methods/set_chat_member_tag.py | 40 +++++ aiogram/types/chat.py | 36 ++++ aiogram/types/chat_administrator_rights.py | 4 + aiogram/types/chat_member_administrator.py | 4 + aiogram/types/chat_member_member.py | 7 +- aiogram/types/chat_member_restricted.py | 8 + aiogram/types/chat_permissions.py | 4 + aiogram/types/game_high_score.py | 2 - aiogram/types/inline_query_result_document.py | 2 +- aiogram/types/message.py | 8 +- aiogram/types/message_entity.py | 10 +- aiogram/utils/formatting.py | 22 +++ aiogram/utils/text_decorations.py | 102 +++++++++-- docs/api/methods/get_user_profile_audios.rst | 8 + docs/api/methods/index.rst | 1 + docs/api/methods/set_chat_member_tag.rst | 45 +++++ .../test_methods/test_promote_chat_member.py | 9 +- .../test_methods/test_set_chat_member_tag.py | 14 ++ tests/test_api/test_types/test_chat.py | 8 + .../test_chat_member_tag_permissions.py | 84 ++++++++++ .../test_filters/test_chat_member_updated.py | 1 + tests/test_utils/test_chat_member.py | 1 + tests/test_utils/test_formatting.py | 47 +++++- tests/test_utils/test_text_decorations.py | 88 +++++++++- 52 files changed, 872 insertions(+), 79 deletions(-) create mode 100644 .butcher/methods/setChatMemberTag/entity.json create mode 100644 CHANGES/1780.misc.rst create mode 100644 aiogram/methods/set_chat_member_tag.py create mode 100644 docs/api/methods/set_chat_member_tag.rst create mode 100644 tests/test_api/test_methods/test_set_chat_member_tag.py create mode 100644 tests/test_api/test_types/test_chat_member_tag_permissions.py diff --git a/.apiversion b/.apiversion index 0359f243..592f36ef 100644 --- a/.apiversion +++ b/.apiversion @@ -1 +1 @@ -9.4 +9.5 diff --git a/.butcher/enums/ContentType.yml b/.butcher/enums/ContentType.yml index 8d70ad16..acdae0a1 100644 --- a/.butcher/enums/ContentType.yml +++ b/.butcher/enums/ContentType.yml @@ -39,6 +39,7 @@ extract: - reply_to_story - business_connection_id - sender_business_bot + - sender_tag - is_from_offline - has_media_spoiler - effect_id diff --git a/.butcher/methods/editMessageChecklist/entity.json b/.butcher/methods/editMessageChecklist/entity.json index 9b5912c5..1c75a0bd 100644 --- a/.butcher/methods/editMessageChecklist/entity.json +++ b/.butcher/methods/editMessageChecklist/entity.json @@ -47,8 +47,8 @@ "type": "InlineKeyboardMarkup", "required": false, "description": "A JSON-serialized object for the new inline keyboard for the message", - "html_description": "A JSON-serialized object for the new inline keyboard for the message", - "rst_description": "A JSON-serialized object for the new inline keyboard for the message\n", + "html_description": "A JSON-serialized object for the new inline keyboard for the message", + "rst_description": "A JSON-serialized object for the new `inline keyboard `_ for the message\n", "name": "reply_markup" } ], diff --git a/.butcher/methods/promoteChatMember/entity.json b/.butcher/methods/promoteChatMember/entity.json index 4f0e4480..5495c781 100644 --- a/.butcher/methods/promoteChatMember/entity.json +++ b/.butcher/methods/promoteChatMember/entity.json @@ -154,6 +154,14 @@ "html_description": "Pass True if the administrator can manage direct messages within the channel and decline suggested posts; for channels only", "rst_description": "Pass :code:`True` if the administrator can manage direct messages within the channel and decline suggested posts; for channels only\n", "name": "can_manage_direct_messages" + }, + { + "type": "Boolean", + "required": false, + "description": "Pass True if the administrator can edit the tags of regular members; for groups and supergroups only", + "html_description": "Pass True if the administrator can edit the tags of regular members; for groups and supergroups only", + "rst_description": "Pass :code:`True` if the administrator can edit the tags of regular members; for groups and supergroups only\n", + "name": "can_manage_tags" } ], "category": "methods" diff --git a/.butcher/methods/sendChecklist/entity.json b/.butcher/methods/sendChecklist/entity.json index 119ee882..31161ac7 100644 --- a/.butcher/methods/sendChecklist/entity.json +++ b/.butcher/methods/sendChecklist/entity.json @@ -71,8 +71,8 @@ "type": "InlineKeyboardMarkup", "required": false, "description": "A JSON-serialized object for an inline keyboard", - "html_description": "A JSON-serialized object for an inline keyboard", - "rst_description": "A JSON-serialized object for an inline keyboard\n", + "html_description": "A JSON-serialized object for an inline keyboard", + "rst_description": "A JSON-serialized object for an `inline keyboard `_\n", "name": "reply_markup" } ], diff --git a/.butcher/methods/sendMessageDraft/entity.json b/.butcher/methods/sendMessageDraft/entity.json index 5d64e874..b84bc368 100644 --- a/.butcher/methods/sendMessageDraft/entity.json +++ b/.butcher/methods/sendMessageDraft/entity.json @@ -7,9 +7,9 @@ "object": { "anchor": "sendmessagedraft", "name": "sendMessageDraft", - "description": "Use this method to stream a partial message to a user while the message is being generated; supported only for bots with forum topic mode enabled. Returns True on success.", - "html_description": "

Use this method to stream a partial message to a user while the message is being generated; supported only for bots with forum topic mode enabled. Returns True on success.

", - "rst_description": "Use this method to stream a partial message to a user while the message is being generated; supported only for bots with forum topic mode enabled. Returns :code:`True` on success.", + "description": "Use this method to stream a partial message to a user while the message is being generated. Returns True on success.", + "html_description": "

Use this method to stream a partial message to a user while the message is being generated. Returns True on success.

", + "rst_description": "Use this method to stream a partial message to a user while the message is being generated. Returns :code:`True` on success.", "annotations": [ { "type": "Integer", diff --git a/.butcher/methods/setChatMemberTag/entity.json b/.butcher/methods/setChatMemberTag/entity.json new file mode 100644 index 00000000..5de6b59f --- /dev/null +++ b/.butcher/methods/setChatMemberTag/entity.json @@ -0,0 +1,41 @@ +{ + "meta": {}, + "group": { + "title": "Available methods", + "anchor": "available-methods" + }, + "object": { + "anchor": "setchatmembertag", + "name": "setChatMemberTag", + "description": "Use this method to set a tag for a regular member in a group or a supergroup. The bot must be an administrator in the chat for this to work and must have the can_manage_tags administrator right. Returns True on success.", + "html_description": "

Use this method to set a tag for a regular member in a group or a supergroup. The bot must be an administrator in the chat for this to work and must have the can_manage_tags administrator right. Returns True on success.

", + "rst_description": "Use this method to set a tag for a regular member in a group or a supergroup. The bot must be an administrator in the chat for this to work and must have the *can_manage_tags* administrator right. Returns :code:`True` on success.", + "annotations": [ + { + "type": "Integer or String", + "required": true, + "description": "Unique identifier for the target chat or username of the target supergroup (in the format @supergroupusername)", + "html_description": "Unique identifier for the target chat or username of the target supergroup (in the format @supergroupusername)", + "rst_description": "Unique identifier for the target chat or username of the target supergroup (in the format :code:`@supergroupusername`)\n", + "name": "chat_id" + }, + { + "type": "Integer", + "required": true, + "description": "Unique identifier of the target user", + "html_description": "Unique identifier of the target user", + "rst_description": "Unique identifier of the target user\n", + "name": "user_id" + }, + { + "type": "String", + "required": false, + "description": "New tag for the member; 0-16 characters, emoji are not allowed", + "html_description": "New tag for the member; 0-16 characters, emoji are not allowed", + "rst_description": "New tag for the member; 0-16 characters, emoji are not allowed\n", + "name": "tag" + } + ], + "category": "methods" + } +} diff --git a/.butcher/schema/schema.json b/.butcher/schema/schema.json index 0fca3099..faa2aee0 100644 --- a/.butcher/schema/schema.json +++ b/.butcher/schema/schema.json @@ -1,7 +1,7 @@ { "api": { - "version": "9.4", - "release_date": "2026-02-09" + "version": "9.5", + "release_date": "2026-03-01" }, "items": [ { @@ -1119,6 +1119,14 @@ "name": "sender_business_bot", "required": false }, + { + "type": "String", + "description": "Tag or custom title of the sender of the message; for supergroups only", + "html_description": "Optional. Tag or custom title of the sender of the message; for supergroups only", + "rst_description": "*Optional*. Tag or custom title of the sender of the message; for supergroups only\n", + "name": "sender_tag", + "required": false + }, { "type": "Integer", "description": "Date the message was sent in Unix time. It is always a positive number, representing a valid date.", @@ -1249,9 +1257,9 @@ }, { "type": "String", - "description": "The unique identifier of a media message group this message belongs to", - "html_description": "Optional. The unique identifier of a media message group this message belongs to", - "rst_description": "*Optional*. The unique identifier of a media message group this message belongs to\n", + "description": "The unique identifier inside this chat of a media message group this message belongs to", + "html_description": "Optional. The unique identifier inside this chat of a media message group this message belongs to", + "rst_description": "*Optional*. The unique identifier inside this chat of a media message group this message belongs to\n", "name": "media_group_id", "required": false }, @@ -1898,8 +1906,8 @@ { "type": "InlineKeyboardMarkup", "description": "Inline keyboard attached to the message. login_url buttons are represented as ordinary url buttons.", - "html_description": "Optional. Inline keyboard attached to the message. login_url buttons are represented as ordinary url buttons.", - "rst_description": "*Optional*. Inline keyboard attached to the message. :code:`login_url` buttons are represented as ordinary :code:`url` buttons.\n", + "html_description": "Optional. Inline keyboard attached to the message. login_url buttons are represented as ordinary url buttons.", + "rst_description": "*Optional*. `Inline keyboard `_ attached to the message. :code:`login_url` buttons are represented as ordinary :code:`url` buttons.\n", "name": "reply_markup", "required": false } @@ -1976,9 +1984,9 @@ "annotations": [ { "type": "String", - "description": "Type of the entity. Currently, can be 'mention' (@username), 'hashtag' (#hashtag or #hashtag@chatusername), 'cashtag' ($USD or $USD@chatusername), 'bot_command' (/start@jobs_bot), 'url' (https://telegram.org), 'email' (do-not-reply@telegram.org), 'phone_number' (+1-212-555-0123), 'bold' (bold text), 'italic' (italic text), 'underline' (underlined text), 'strikethrough' (strikethrough text), 'spoiler' (spoiler message), 'blockquote' (block quotation), 'expandable_blockquote' (collapsed-by-default block quotation), 'code' (monowidth string), 'pre' (monowidth block), 'text_link' (for clickable text URLs), 'text_mention' (for users without usernames), 'custom_emoji' (for inline custom emoji stickers)", - "html_description": "Type of the entity. Currently, can be “mention” (@username), “hashtag” (#hashtag or #hashtag@chatusername), “cashtag” ($USD or $USD@chatusername), “bot_command” (/start@jobs_bot), “url” (https://telegram.org), “email” (do-not-reply@telegram.org), “phone_number” (+1-212-555-0123), “bold” (bold text), “italic” (italic text), “underline” (underlined text), “strikethrough” (strikethrough text), “spoiler” (spoiler message), “blockquote” (block quotation), “expandable_blockquote” (collapsed-by-default block quotation), “code” (monowidth string), “pre” (monowidth block), “text_link” (for clickable text URLs), “text_mention” (for users without usernames), “custom_emoji” (for inline custom emoji stickers)", - "rst_description": "Type of the entity. Currently, can be 'mention' (:code:`@username`), 'hashtag' (:code:`#hashtag` or :code:`#hashtag@chatusername`), 'cashtag' (:code:`$USD` or :code:`$USD@chatusername`), 'bot_command' (:code:`/start@jobs_bot`), 'url' (:code:`https://telegram.org`), 'email' (:code:`do-not-reply@telegram.org`), 'phone_number' (:code:`+1-212-555-0123`), 'bold' (**bold text**), 'italic' (*italic text*), 'underline' (underlined text), 'strikethrough' (strikethrough text), 'spoiler' (spoiler message), 'blockquote' (block quotation), 'expandable_blockquote' (collapsed-by-default block quotation), 'code' (monowidth string), 'pre' (monowidth block), 'text_link' (for clickable text URLs), 'text_mention' (for users `without usernames `_), 'custom_emoji' (for inline custom emoji stickers)\n", + "description": "Type of the entity. Currently, can be 'mention' (@username), 'hashtag' (#hashtag or #hashtag@chatusername), 'cashtag' ($USD or $USD@chatusername), 'bot_command' (/start@jobs_bot), 'url' (https://telegram.org), 'email' (do-not-reply@telegram.org), 'phone_number' (+1-212-555-0123), 'bold' (bold text), 'italic' (italic text), 'underline' (underlined text), 'strikethrough' (strikethrough text), 'spoiler' (spoiler message), 'blockquote' (block quotation), 'expandable_blockquote' (collapsed-by-default block quotation), 'code' (monowidth string), 'pre' (monowidth block), 'text_link' (for clickable text URLs), 'text_mention' (for users without usernames), 'custom_emoji' (for inline custom emoji stickers), or 'date_time' (for formatted date and time)", + "html_description": "Type of the entity. Currently, can be “mention” (@username), “hashtag” (#hashtag or #hashtag@chatusername), “cashtag” ($USD or $USD@chatusername), “bot_command” (/start@jobs_bot), “url” (https://telegram.org), “email” (do-not-reply@telegram.org), “phone_number” (+1-212-555-0123), “bold” (bold text), “italic” (italic text), “underline” (underlined text), “strikethrough” (strikethrough text), “spoiler” (spoiler message), “blockquote” (block quotation), “expandable_blockquote” (collapsed-by-default block quotation), “code” (monowidth string), “pre” (monowidth block), “text_link” (for clickable text URLs), “text_mention” (for users without usernames), “custom_emoji” (for inline custom emoji stickers), or “date_time” (for formatted date and time)", + "rst_description": "Type of the entity. Currently, can be 'mention' (:code:`@username`), 'hashtag' (:code:`#hashtag` or :code:`#hashtag@chatusername`), 'cashtag' (:code:`$USD` or :code:`$USD@chatusername`), 'bot_command' (:code:`/start@jobs_bot`), 'url' (:code:`https://telegram.org`), 'email' (:code:`do-not-reply@telegram.org`), 'phone_number' (:code:`+1-212-555-0123`), 'bold' (**bold text**), 'italic' (*italic text*), 'underline' (underlined text), 'strikethrough' (strikethrough text), 'spoiler' (spoiler message), 'blockquote' (block quotation), 'expandable_blockquote' (collapsed-by-default block quotation), 'code' (monowidth string), 'pre' (monowidth block), 'text_link' (for clickable text URLs), 'text_mention' (for users `without usernames `_), 'custom_emoji' (for inline custom emoji stickers), or 'date_time' (for formatted date and time)\n", "name": "type", "required": true }, @@ -2029,6 +2037,22 @@ "rst_description": "*Optional*. For 'custom_emoji' only, unique identifier of the custom emoji. Use :class:`aiogram.methods.get_custom_emoji_stickers.GetCustomEmojiStickers` to get full information about the sticker\n", "name": "custom_emoji_id", "required": false + }, + { + "type": "Integer", + "description": "For 'date_time' only, the Unix time associated with the entity", + "html_description": "Optional. For “date_time” only, the Unix time associated with the entity", + "rst_description": "*Optional*. For 'date_time' only, the Unix time associated with the entity\n", + "name": "unix_time", + "required": false + }, + { + "type": "String", + "description": "For 'date_time' only, the string that defines the formatting of the date and time. See date-time entity formatting for more details.", + "html_description": "Optional. For “date_time” only, the string that defines the formatting of the date and time. See date-time entity formatting for more details.", + "rst_description": "*Optional*. For 'date_time' only, the string that defines the formatting of the date and time. See `date-time entity formatting `_ for more details.\n", + "name": "date_time_format", + "required": false } ], "category": "types" @@ -6332,6 +6356,14 @@ "rst_description": "*Optional*. :code:`True`, if the administrator can manage direct messages of the channel and decline suggested posts; for channels only\n", "name": "can_manage_direct_messages", "required": false + }, + { + "type": "Boolean", + "description": "True, if the administrator can edit the tags of regular members; for groups and supergroups only. If omitted defaults to the value of can_pin_messages.", + "html_description": "Optional. True, if the administrator can edit the tags of regular members; for groups and supergroups only. If omitted defaults to the value of can_pin_messages.", + "rst_description": "*Optional*. :code:`True`, if the administrator can edit the tags of regular members; for groups and supergroups only. If omitted defaults to the value of can_pin_messages.\n", + "name": "can_manage_tags", + "required": false } ], "category": "types" @@ -6620,6 +6652,14 @@ "name": "can_manage_direct_messages", "required": false }, + { + "type": "Boolean", + "description": "True, if the administrator can edit the tags of regular members; for groups and supergroups only. If omitted defaults to the value of can_pin_messages.", + "html_description": "Optional. True, if the administrator can edit the tags of regular members; for groups and supergroups only. If omitted defaults to the value of can_pin_messages.", + "rst_description": "*Optional*. :code:`True`, if the administrator can edit the tags of regular members; for groups and supergroups only. If omitted defaults to the value of can_pin_messages.\n", + "name": "can_manage_tags", + "required": false + }, { "type": "String", "description": "Custom title for this user", @@ -6646,6 +6686,14 @@ "name": "status", "required": true }, + { + "type": "String", + "description": "Tag of the member", + "html_description": "Optional. Tag of the member", + "rst_description": "*Optional*. Tag of the member\n", + "name": "tag", + "required": false + }, { "type": "User", "description": "Information about the user", @@ -6680,6 +6728,14 @@ "name": "status", "required": true }, + { + "type": "String", + "description": "Tag of the member", + "html_description": "Optional. Tag of the member", + "rst_description": "*Optional*. Tag of the member\n", + "name": "tag", + "required": false + }, { "type": "User", "description": "Information about the user", @@ -6776,6 +6832,14 @@ "name": "can_add_web_page_previews", "required": true }, + { + "type": "Boolean", + "description": "True, if the user is allowed to edit their own tag", + "html_description": "True, if the user is allowed to edit their own tag", + "rst_description": ":code:`True`, if the user is allowed to edit their own tag\n", + "name": "can_edit_tag", + "required": true + }, { "type": "Boolean", "description": "True, if the user is allowed to change the chat title, photo and other settings", @@ -7024,6 +7088,14 @@ "name": "can_add_web_page_previews", "required": false }, + { + "type": "Boolean", + "description": "True, if the user is allowed to edit their own tag", + "html_description": "Optional. True, if the user is allowed to edit their own tag", + "rst_description": "*Optional*. :code:`True`, if the user is allowed to edit their own tag\n", + "name": "can_edit_tag", + "required": false + }, { "type": "Boolean", "description": "True, if the user is allowed to change the chat title, photo and other settings. Ignored in public supergroups", @@ -12959,8 +13031,8 @@ "type": "InlineKeyboardMarkup", "required": false, "description": "A JSON-serialized object for an inline keyboard", - "html_description": "A JSON-serialized object for an inline keyboard", - "rst_description": "A JSON-serialized object for an inline keyboard\n", + "html_description": "A JSON-serialized object for an inline keyboard", + "rst_description": "A JSON-serialized object for an `inline keyboard `_\n", "name": "reply_markup" } ], @@ -13075,9 +13147,9 @@ { "anchor": "sendmessagedraft", "name": "sendMessageDraft", - "description": "Use this method to stream a partial message to a user while the message is being generated; supported only for bots with forum topic mode enabled. Returns True on success.", - "html_description": "

Use this method to stream a partial message to a user while the message is being generated; supported only for bots with forum topic mode enabled. Returns True on success.

", - "rst_description": "Use this method to stream a partial message to a user while the message is being generated; supported only for bots with forum topic mode enabled. Returns :code:`True` on success.", + "description": "Use this method to stream a partial message to a user while the message is being generated. Returns True on success.", + "html_description": "

Use this method to stream a partial message to a user while the message is being generated. Returns True on success.

", + "rst_description": "Use this method to stream a partial message to a user while the message is being generated. Returns :code:`True` on success.", "annotations": [ { "type": "Integer", @@ -13610,6 +13682,14 @@ "html_description": "Pass True if the administrator can manage direct messages within the channel and decline suggested posts; for channels only", "rst_description": "Pass :code:`True` if the administrator can manage direct messages within the channel and decline suggested posts; for channels only\n", "name": "can_manage_direct_messages" + }, + { + "type": "Boolean", + "required": false, + "description": "Pass True if the administrator can edit the tags of regular members; for groups and supergroups only", + "html_description": "Pass True if the administrator can edit the tags of regular members; for groups and supergroups only", + "rst_description": "Pass :code:`True` if the administrator can edit the tags of regular members; for groups and supergroups only\n", + "name": "can_manage_tags" } ], "category": "methods" @@ -13648,6 +13728,40 @@ ], "category": "methods" }, + { + "anchor": "setchatmembertag", + "name": "setChatMemberTag", + "description": "Use this method to set a tag for a regular member in a group or a supergroup. The bot must be an administrator in the chat for this to work and must have the can_manage_tags administrator right. Returns True on success.", + "html_description": "

Use this method to set a tag for a regular member in a group or a supergroup. The bot must be an administrator in the chat for this to work and must have the can_manage_tags administrator right. Returns True on success.

", + "rst_description": "Use this method to set a tag for a regular member in a group or a supergroup. The bot must be an administrator in the chat for this to work and must have the *can_manage_tags* administrator right. Returns :code:`True` on success.", + "annotations": [ + { + "type": "Integer or String", + "required": true, + "description": "Unique identifier for the target chat or username of the target supergroup (in the format @supergroupusername)", + "html_description": "Unique identifier for the target chat or username of the target supergroup (in the format @supergroupusername)", + "rst_description": "Unique identifier for the target chat or username of the target supergroup (in the format :code:`@supergroupusername`)\n", + "name": "chat_id" + }, + { + "type": "Integer", + "required": true, + "description": "Unique identifier of the target user", + "html_description": "Unique identifier of the target user", + "rst_description": "Unique identifier of the target user\n", + "name": "user_id" + }, + { + "type": "String", + "required": false, + "description": "New tag for the member; 0-16 characters, emoji are not allowed", + "html_description": "New tag for the member; 0-16 characters, emoji are not allowed", + "rst_description": "New tag for the member; 0-16 characters, emoji are not allowed\n", + "name": "tag" + } + ], + "category": "methods" + }, { "anchor": "banchatsenderchat", "name": "banChatSenderChat", @@ -16631,8 +16745,8 @@ "type": "InlineKeyboardMarkup", "required": false, "description": "A JSON-serialized object for the new inline keyboard for the message", - "html_description": "A JSON-serialized object for the new inline keyboard for the message", - "rst_description": "A JSON-serialized object for the new inline keyboard for the message\n", + "html_description": "A JSON-serialized object for the new inline keyboard for the message", + "rst_description": "A JSON-serialized object for the new `inline keyboard `_ for the message\n", "name": "reply_markup" } ], @@ -18727,8 +18841,8 @@ { "type": "InlineKeyboardMarkup", "description": "Inline keyboard attached to the message", - "html_description": "Optional. Inline keyboard attached to the message", - "rst_description": "*Optional*. Inline keyboard attached to the message\n", + "html_description": "Optional. Inline keyboard attached to the message", + "rst_description": "*Optional*. `Inline keyboard `_ attached to the message\n", "name": "reply_markup", "required": false }, @@ -22865,9 +22979,9 @@ { "anchor": "gamehighscore", "name": "GameHighScore", - "description": "This object represents one row of the high scores table for a game.\nAnd that's about all we've got for now.\nIf you've got any questions, please check out our Bot FAQ\n-", - "html_description": "

This object represents one row of the high scores table for a game.

And that's about all we've got for now.
\nIf you've got any questions, please check out our Bot FAQ »
\n-

", - "rst_description": "This object represents one row of the high scores table for a game.\nAnd that's about all we've got for now.\n\nIf you've got any questions, please check out our `https://core.telegram.org/bots/faq `_ **Bot FAQ »**\n\n-", + "description": "This object represents one row of the high scores table for a game.\nAnd that's about all we've got for now.\nIf you've got any questions, please check out our Bot FAQ", + "html_description": "

This object represents one row of the high scores table for a game.

And that's about all we've got for now.
\nIf you've got any questions, please check out our Bot FAQ »

", + "rst_description": "This object represents one row of the high scores table for a game.\nAnd that's about all we've got for now.\n\nIf you've got any questions, please check out our `https://core.telegram.org/bots/faq `_ **Bot FAQ »**", "annotations": [ { "type": "Integer", diff --git a/.butcher/types/Chat/aliases.yml b/.butcher/types/Chat/aliases.yml index 89b5843c..7a03c4a9 100644 --- a/.butcher/types/Chat/aliases.yml +++ b/.butcher/types/Chat/aliases.yml @@ -71,6 +71,10 @@ set_administrator_custom_title: method: setChatAdministratorCustomTitle fill: *self +set_member_tag: + method: setChatMemberTag + fill: *self + set_permissions: method: setChatPermissions fill: *self diff --git a/.butcher/types/ChatAdministratorRights/entity.json b/.butcher/types/ChatAdministratorRights/entity.json index 45ebc3b5..f271d1fd 100644 --- a/.butcher/types/ChatAdministratorRights/entity.json +++ b/.butcher/types/ChatAdministratorRights/entity.json @@ -138,6 +138,14 @@ "rst_description": "*Optional*. :code:`True`, if the administrator can manage direct messages of the channel and decline suggested posts; for channels only\n", "name": "can_manage_direct_messages", "required": false + }, + { + "type": "Boolean", + "description": "True, if the administrator can edit the tags of regular members; for groups and supergroups only. If omitted defaults to the value of can_pin_messages.", + "html_description": "Optional. True, if the administrator can edit the tags of regular members; for groups and supergroups only. If omitted defaults to the value of can_pin_messages.", + "rst_description": "*Optional*. :code:`True`, if the administrator can edit the tags of regular members; for groups and supergroups only. If omitted defaults to the value of can_pin_messages.\n", + "name": "can_manage_tags", + "required": false } ], "category": "types" diff --git a/.butcher/types/ChatMemberAdministrator/entity.json b/.butcher/types/ChatMemberAdministrator/entity.json index f1278554..7b55cc7a 100644 --- a/.butcher/types/ChatMemberAdministrator/entity.json +++ b/.butcher/types/ChatMemberAdministrator/entity.json @@ -163,6 +163,14 @@ "name": "can_manage_direct_messages", "required": false }, + { + "type": "Boolean", + "description": "True, if the administrator can edit the tags of regular members; for groups and supergroups only. If omitted defaults to the value of can_pin_messages.", + "html_description": "Optional. True, if the administrator can edit the tags of regular members; for groups and supergroups only. If omitted defaults to the value of can_pin_messages.", + "rst_description": "*Optional*. :code:`True`, if the administrator can edit the tags of regular members; for groups and supergroups only. If omitted defaults to the value of can_pin_messages.\n", + "name": "can_manage_tags", + "required": false + }, { "type": "String", "description": "Custom title for this user", diff --git a/.butcher/types/ChatMemberMember/entity.json b/.butcher/types/ChatMemberMember/entity.json index c9988ee7..ed1a8304 100644 --- a/.butcher/types/ChatMemberMember/entity.json +++ b/.butcher/types/ChatMemberMember/entity.json @@ -19,6 +19,14 @@ "name": "status", "required": true }, + { + "type": "String", + "description": "Tag of the member", + "html_description": "Optional. Tag of the member", + "rst_description": "*Optional*. Tag of the member\n", + "name": "tag", + "required": false + }, { "type": "User", "description": "Information about the user", diff --git a/.butcher/types/ChatMemberRestricted/entity.json b/.butcher/types/ChatMemberRestricted/entity.json index 75ea1fc0..f0572284 100644 --- a/.butcher/types/ChatMemberRestricted/entity.json +++ b/.butcher/types/ChatMemberRestricted/entity.json @@ -19,6 +19,14 @@ "name": "status", "required": true }, + { + "type": "String", + "description": "Tag of the member", + "html_description": "Optional. Tag of the member", + "rst_description": "*Optional*. Tag of the member\n", + "name": "tag", + "required": false + }, { "type": "User", "description": "Information about the user", @@ -115,6 +123,14 @@ "name": "can_add_web_page_previews", "required": true }, + { + "type": "Boolean", + "description": "True, if the user is allowed to edit their own tag", + "html_description": "True, if the user is allowed to edit their own tag", + "rst_description": ":code:`True`, if the user is allowed to edit their own tag\n", + "name": "can_edit_tag", + "required": true + }, { "type": "Boolean", "description": "True, if the user is allowed to change the chat title, photo and other settings", diff --git a/.butcher/types/ChatPermissions/entity.json b/.butcher/types/ChatPermissions/entity.json index c488ef9f..d6ad3cc9 100644 --- a/.butcher/types/ChatPermissions/entity.json +++ b/.butcher/types/ChatPermissions/entity.json @@ -91,6 +91,14 @@ "name": "can_add_web_page_previews", "required": false }, + { + "type": "Boolean", + "description": "True, if the user is allowed to edit their own tag", + "html_description": "Optional. True, if the user is allowed to edit their own tag", + "rst_description": "*Optional*. :code:`True`, if the user is allowed to edit their own tag\n", + "name": "can_edit_tag", + "required": false + }, { "type": "Boolean", "description": "True, if the user is allowed to change the chat title, photo and other settings. Ignored in public supergroups", diff --git a/.butcher/types/GameHighScore/entity.json b/.butcher/types/GameHighScore/entity.json index 21a8a5e7..ce3f52d2 100644 --- a/.butcher/types/GameHighScore/entity.json +++ b/.butcher/types/GameHighScore/entity.json @@ -7,9 +7,9 @@ "object": { "anchor": "gamehighscore", "name": "GameHighScore", - "description": "This object represents one row of the high scores table for a game.\nAnd that's about all we've got for now.\nIf you've got any questions, please check out our Bot FAQ\n-", - "html_description": "

This object represents one row of the high scores table for a game.

And that's about all we've got for now.
\nIf you've got any questions, please check out our Bot FAQ »
\n-

", - "rst_description": "This object represents one row of the high scores table for a game.\nAnd that's about all we've got for now.\n\nIf you've got any questions, please check out our `https://core.telegram.org/bots/faq `_ **Bot FAQ »**\n\n-", + "description": "This object represents one row of the high scores table for a game.\nAnd that's about all we've got for now.\nIf you've got any questions, please check out our Bot FAQ", + "html_description": "

This object represents one row of the high scores table for a game.

And that's about all we've got for now.
\nIf you've got any questions, please check out our Bot FAQ »

", + "rst_description": "This object represents one row of the high scores table for a game.\nAnd that's about all we've got for now.\n\nIf you've got any questions, please check out our `https://core.telegram.org/bots/faq `_ **Bot FAQ »**", "annotations": [ { "type": "Integer", diff --git a/.butcher/types/InlineQueryResultDocument/entity.json b/.butcher/types/InlineQueryResultDocument/entity.json index ff703482..97bb05a6 100644 --- a/.butcher/types/InlineQueryResultDocument/entity.json +++ b/.butcher/types/InlineQueryResultDocument/entity.json @@ -86,8 +86,8 @@ { "type": "InlineKeyboardMarkup", "description": "Inline keyboard attached to the message", - "html_description": "Optional. Inline keyboard attached to the message", - "rst_description": "*Optional*. Inline keyboard attached to the message\n", + "html_description": "Optional. Inline keyboard attached to the message", + "rst_description": "*Optional*. `Inline keyboard `_ attached to the message\n", "name": "reply_markup", "required": false }, diff --git a/.butcher/types/Message/entity.json b/.butcher/types/Message/entity.json index 594442fc..7225d275 100644 --- a/.butcher/types/Message/entity.json +++ b/.butcher/types/Message/entity.json @@ -67,6 +67,14 @@ "name": "sender_business_bot", "required": false }, + { + "type": "String", + "description": "Tag or custom title of the sender of the message; for supergroups only", + "html_description": "Optional. Tag or custom title of the sender of the message; for supergroups only", + "rst_description": "*Optional*. Tag or custom title of the sender of the message; for supergroups only\n", + "name": "sender_tag", + "required": false + }, { "type": "Integer", "description": "Date the message was sent in Unix time. It is always a positive number, representing a valid date.", @@ -197,9 +205,9 @@ }, { "type": "String", - "description": "The unique identifier of a media message group this message belongs to", - "html_description": "Optional. The unique identifier of a media message group this message belongs to", - "rst_description": "*Optional*. The unique identifier of a media message group this message belongs to\n", + "description": "The unique identifier inside this chat of a media message group this message belongs to", + "html_description": "Optional. The unique identifier inside this chat of a media message group this message belongs to", + "rst_description": "*Optional*. The unique identifier inside this chat of a media message group this message belongs to\n", "name": "media_group_id", "required": false }, @@ -846,8 +854,8 @@ { "type": "InlineKeyboardMarkup", "description": "Inline keyboard attached to the message. login_url buttons are represented as ordinary url buttons.", - "html_description": "Optional. Inline keyboard attached to the message. login_url buttons are represented as ordinary url buttons.", - "rst_description": "*Optional*. Inline keyboard attached to the message. :code:`login_url` buttons are represented as ordinary :code:`url` buttons.\n", + "html_description": "Optional. Inline keyboard attached to the message. login_url buttons are represented as ordinary url buttons.", + "rst_description": "*Optional*. `Inline keyboard `_ attached to the message. :code:`login_url` buttons are represented as ordinary :code:`url` buttons.\n", "name": "reply_markup", "required": false }, diff --git a/.butcher/types/MessageEntity/entity.json b/.butcher/types/MessageEntity/entity.json index 8d5a1d13..37dc6cf4 100644 --- a/.butcher/types/MessageEntity/entity.json +++ b/.butcher/types/MessageEntity/entity.json @@ -13,9 +13,9 @@ "annotations": [ { "type": "String", - "description": "Type of the entity. Currently, can be 'mention' (@username), 'hashtag' (#hashtag or #hashtag@chatusername), 'cashtag' ($USD or $USD@chatusername), 'bot_command' (/start@jobs_bot), 'url' (https://telegram.org), 'email' (do-not-reply@telegram.org), 'phone_number' (+1-212-555-0123), 'bold' (bold text), 'italic' (italic text), 'underline' (underlined text), 'strikethrough' (strikethrough text), 'spoiler' (spoiler message), 'blockquote' (block quotation), 'expandable_blockquote' (collapsed-by-default block quotation), 'code' (monowidth string), 'pre' (monowidth block), 'text_link' (for clickable text URLs), 'text_mention' (for users without usernames), 'custom_emoji' (for inline custom emoji stickers)", - "html_description": "Type of the entity. Currently, can be “mention” (@username), “hashtag” (#hashtag or #hashtag@chatusername), “cashtag” ($USD or $USD@chatusername), “bot_command” (/start@jobs_bot), “url” (https://telegram.org), “email” (do-not-reply@telegram.org), “phone_number” (+1-212-555-0123), “bold” (bold text), “italic” (italic text), “underline” (underlined text), “strikethrough” (strikethrough text), “spoiler” (spoiler message), “blockquote” (block quotation), “expandable_blockquote” (collapsed-by-default block quotation), “code” (monowidth string), “pre” (monowidth block), “text_link” (for clickable text URLs), “text_mention” (for users without usernames), “custom_emoji” (for inline custom emoji stickers)", - "rst_description": "Type of the entity. Currently, can be 'mention' (:code:`@username`), 'hashtag' (:code:`#hashtag` or :code:`#hashtag@chatusername`), 'cashtag' (:code:`$USD` or :code:`$USD@chatusername`), 'bot_command' (:code:`/start@jobs_bot`), 'url' (:code:`https://telegram.org`), 'email' (:code:`do-not-reply@telegram.org`), 'phone_number' (:code:`+1-212-555-0123`), 'bold' (**bold text**), 'italic' (*italic text*), 'underline' (underlined text), 'strikethrough' (strikethrough text), 'spoiler' (spoiler message), 'blockquote' (block quotation), 'expandable_blockquote' (collapsed-by-default block quotation), 'code' (monowidth string), 'pre' (monowidth block), 'text_link' (for clickable text URLs), 'text_mention' (for users `without usernames `_), 'custom_emoji' (for inline custom emoji stickers)\n", + "description": "Type of the entity. Currently, can be 'mention' (@username), 'hashtag' (#hashtag or #hashtag@chatusername), 'cashtag' ($USD or $USD@chatusername), 'bot_command' (/start@jobs_bot), 'url' (https://telegram.org), 'email' (do-not-reply@telegram.org), 'phone_number' (+1-212-555-0123), 'bold' (bold text), 'italic' (italic text), 'underline' (underlined text), 'strikethrough' (strikethrough text), 'spoiler' (spoiler message), 'blockquote' (block quotation), 'expandable_blockquote' (collapsed-by-default block quotation), 'code' (monowidth string), 'pre' (monowidth block), 'text_link' (for clickable text URLs), 'text_mention' (for users without usernames), 'custom_emoji' (for inline custom emoji stickers), or 'date_time' (for formatted date and time)", + "html_description": "Type of the entity. Currently, can be “mention” (@username), “hashtag” (#hashtag or #hashtag@chatusername), “cashtag” ($USD or $USD@chatusername), “bot_command” (/start@jobs_bot), “url” (https://telegram.org), “email” (do-not-reply@telegram.org), “phone_number” (+1-212-555-0123), “bold” (bold text), “italic” (italic text), “underline” (underlined text), “strikethrough” (strikethrough text), “spoiler” (spoiler message), “blockquote” (block quotation), “expandable_blockquote” (collapsed-by-default block quotation), “code” (monowidth string), “pre” (monowidth block), “text_link” (for clickable text URLs), “text_mention” (for users without usernames), “custom_emoji” (for inline custom emoji stickers), or “date_time” (for formatted date and time)", + "rst_description": "Type of the entity. Currently, can be 'mention' (:code:`@username`), 'hashtag' (:code:`#hashtag` or :code:`#hashtag@chatusername`), 'cashtag' (:code:`$USD` or :code:`$USD@chatusername`), 'bot_command' (:code:`/start@jobs_bot`), 'url' (:code:`https://telegram.org`), 'email' (:code:`do-not-reply@telegram.org`), 'phone_number' (:code:`+1-212-555-0123`), 'bold' (**bold text**), 'italic' (*italic text*), 'underline' (underlined text), 'strikethrough' (strikethrough text), 'spoiler' (spoiler message), 'blockquote' (block quotation), 'expandable_blockquote' (collapsed-by-default block quotation), 'code' (monowidth string), 'pre' (monowidth block), 'text_link' (for clickable text URLs), 'text_mention' (for users `without usernames `_), 'custom_emoji' (for inline custom emoji stickers), or 'date_time' (for formatted date and time)\n", "name": "type", "required": true }, @@ -66,6 +66,22 @@ "rst_description": "*Optional*. For 'custom_emoji' only, unique identifier of the custom emoji. Use :class:`aiogram.methods.get_custom_emoji_stickers.GetCustomEmojiStickers` to get full information about the sticker\n", "name": "custom_emoji_id", "required": false + }, + { + "type": "Integer", + "description": "For 'date_time' only, the Unix time associated with the entity", + "html_description": "Optional. For “date_time” only, the Unix time associated with the entity", + "rst_description": "*Optional*. For 'date_time' only, the Unix time associated with the entity\n", + "name": "unix_time", + "required": false + }, + { + "type": "String", + "description": "For 'date_time' only, the string that defines the formatting of the date and time. See date-time entity formatting for more details.", + "html_description": "Optional. For “date_time” only, the string that defines the formatting of the date and time. See date-time entity formatting for more details.", + "rst_description": "*Optional*. For 'date_time' only, the string that defines the formatting of the date and time. See `date-time entity formatting `_ for more details.\n", + "name": "date_time_format", + "required": false } ], "category": "types" diff --git a/CHANGES/1780.misc.rst b/CHANGES/1780.misc.rst new file mode 100644 index 00000000..6037a9d7 --- /dev/null +++ b/CHANGES/1780.misc.rst @@ -0,0 +1,15 @@ +Updated to `Bot API 9.5 `_ + +**New Methods:** + +- Added :class:`aiogram.methods.send_message_draft.SendMessageDraft` method - allowed for all bots to stream partial messages while they are being generated +- Added :class:`aiogram.methods.set_chat_member_tag.SetChatMemberTag` method - allows bots to set a custom tag for a chat member; available via :meth:`aiogram.types.chat.Chat.set_member_tag` shortcut + +**New Fields:** + +- Added :code:`date_time` type to :class:`aiogram.types.message_entity.MessageEntity` with :code:`unix_time` and :code:`date_time_format` fields - allows bots to display a formatted date and time to the user +- Added :code:`tag` field to :class:`aiogram.types.chat_member_member.ChatMemberMember` and :class:`aiogram.types.chat_member_restricted.ChatMemberRestricted` - the custom tag set for the chat member +- Added :code:`can_edit_tag` field to :class:`aiogram.types.chat_member_restricted.ChatMemberRestricted` and :class:`aiogram.types.chat_permissions.ChatPermissions` - indicates whether the user is allowed to edit their own tag +- Added :code:`can_manage_tags` field to :class:`aiogram.types.chat_member_administrator.ChatMemberAdministrator` and :class:`aiogram.types.chat_administrator_rights.ChatAdministratorRights` - indicates whether the administrator can manage tags of other chat members +- Added :code:`can_manage_tags` parameter to :class:`aiogram.methods.promote_chat_member.PromoteChatMember` method +- Added :code:`sender_tag` field to :class:`aiogram.types.message.Message` - the tag of the message sender in the chat diff --git a/README.rst b/README.rst index c3b660d3..a04ff797 100644 --- a/README.rst +++ b/README.rst @@ -52,7 +52,7 @@ Features - Asynchronous (`asyncio docs `_, :pep:`492`) - Has type hints (:pep:`484`) and can be used with `mypy `_ - Supports `PyPy `_ -- Supports `Telegram Bot API 9.4 `_ and gets fast updates to the latest versions of the Bot API +- Supports `Telegram Bot API 9.5 `_ and gets fast updates to the latest versions of the Bot API - Telegram Bot API integration code was `autogenerated `_ and can be easily re-generated when API gets updated - Updates router (Blueprints) - Has Finite State Machine diff --git a/aiogram/__meta__.py b/aiogram/__meta__.py index 0c33f1a9..00647de5 100644 --- a/aiogram/__meta__.py +++ b/aiogram/__meta__.py @@ -1,2 +1,2 @@ -__version__ = "3.25.0" -__api_version__ = "9.4" +__version__ = "3.26.0" +__api_version__ = "9.5" diff --git a/aiogram/client/bot.py b/aiogram/client/bot.py index d10429f6..195c06dd 100644 --- a/aiogram/client/bot.py +++ b/aiogram/client/bot.py @@ -144,6 +144,7 @@ from ..methods import ( SetBusinessAccountUsername, SetChatAdministratorCustomTitle, SetChatDescription, + SetChatMemberTag, SetChatMenuButton, SetChatPermissions, SetChatPhoto, @@ -2021,6 +2022,7 @@ class Bot: can_pin_messages: bool | None = None, can_manage_topics: bool | None = None, can_manage_direct_messages: bool | None = None, + can_manage_tags: bool | None = None, request_timeout: int | None = None, ) -> bool: """ @@ -2046,6 +2048,7 @@ class Bot: :param can_pin_messages: Pass :code:`True` if the administrator can pin messages; for supergroups only :param can_manage_topics: Pass :code:`True` if the user is allowed to create, rename, close, and reopen forum topics; for supergroups only :param can_manage_direct_messages: Pass :code:`True` if the administrator can manage direct messages within the channel and decline suggested posts; for channels only + :param can_manage_tags: Pass :code:`True` if the administrator can edit the tags of regular members; for groups and supergroups only :param request_timeout: Request timeout :return: Returns :code:`True` on success. """ @@ -2069,6 +2072,7 @@ class Bot: can_pin_messages=can_pin_messages, can_manage_topics=can_manage_topics, can_manage_direct_messages=can_manage_direct_messages, + can_manage_tags=can_manage_tags, ) return await self(call, request_timeout=request_timeout) @@ -5560,7 +5564,7 @@ class Bot: :param chat_id: Unique identifier for the target chat :param message_id: Unique identifier for the target message :param checklist: A JSON-serialized object for the new checklist - :param reply_markup: A JSON-serialized object for the new inline keyboard for the message + :param reply_markup: A JSON-serialized object for the new `inline keyboard `_ for the message :param request_timeout: Request timeout :return: On success, the edited :class:`aiogram.types.message.Message` is returned. """ @@ -5614,7 +5618,7 @@ class Bot: :param protect_content: Protects the contents of the sent message from forwarding and saving :param message_effect_id: Unique identifier of the message effect to be added to the message :param reply_parameters: A JSON-serialized object for description of the message to reply to - :param reply_markup: A JSON-serialized object for an inline keyboard + :param reply_markup: A JSON-serialized object for an `inline keyboard `_ :param request_timeout: Request timeout :return: On success, the sent :class:`aiogram.types.message.Message` is returned. """ @@ -5823,7 +5827,7 @@ class Bot: request_timeout: int | None = None, ) -> bool: """ - Use this method to stream a partial message to a user while the message is being generated; supported only for bots with forum topic mode enabled. Returns :code:`True` on success. + Use this method to stream a partial message to a user while the message is being generated. Returns :code:`True` on success. Source: https://core.telegram.org/bots/api#sendmessagedraft @@ -5908,3 +5912,29 @@ class Bot: photo=photo, ) return await self(call, request_timeout=request_timeout) + + async def set_chat_member_tag( + self, + chat_id: ChatIdUnion, + user_id: int, + tag: str | None = None, + request_timeout: int | None = None, + ) -> bool: + """ + Use this method to set a tag for a regular member in a group or a supergroup. The bot must be an administrator in the chat for this to work and must have the *can_manage_tags* administrator right. Returns :code:`True` on success. + + Source: https://core.telegram.org/bots/api#setchatmembertag + + :param chat_id: Unique identifier for the target chat or username of the target supergroup (in the format :code:`@supergroupusername`) + :param user_id: Unique identifier of the target user + :param tag: New tag for the member; 0-16 characters, emoji are not allowed + :param request_timeout: Request timeout + :return: Returns :code:`True` on success. + """ + + call = SetChatMemberTag( + chat_id=chat_id, + user_id=user_id, + tag=tag, + ) + return await self(call, request_timeout=request_timeout) diff --git a/aiogram/enums/message_entity_type.py b/aiogram/enums/message_entity_type.py index b67dc039..e1dba489 100644 --- a/aiogram/enums/message_entity_type.py +++ b/aiogram/enums/message_entity_type.py @@ -27,3 +27,4 @@ class MessageEntityType(str, Enum): TEXT_LINK = "text_link" TEXT_MENTION = "text_mention" CUSTOM_EMOJI = "custom_emoji" + DATE_TIME = "date_time" diff --git a/aiogram/methods/__init__.py b/aiogram/methods/__init__.py index 786e53e5..b4c93d6c 100644 --- a/aiogram/methods/__init__.py +++ b/aiogram/methods/__init__.py @@ -126,6 +126,7 @@ from .set_business_account_profile_photo import SetBusinessAccountProfilePhoto from .set_business_account_username import SetBusinessAccountUsername from .set_chat_administrator_custom_title import SetChatAdministratorCustomTitle from .set_chat_description import SetChatDescription +from .set_chat_member_tag import SetChatMemberTag from .set_chat_menu_button import SetChatMenuButton from .set_chat_permissions import SetChatPermissions from .set_chat_photo import SetChatPhoto @@ -295,6 +296,7 @@ __all__ = ( "SetBusinessAccountUsername", "SetChatAdministratorCustomTitle", "SetChatDescription", + "SetChatMemberTag", "SetChatMenuButton", "SetChatPermissions", "SetChatPhoto", diff --git a/aiogram/methods/edit_message_checklist.py b/aiogram/methods/edit_message_checklist.py index 6f7bbf7d..83d3815f 100644 --- a/aiogram/methods/edit_message_checklist.py +++ b/aiogram/methods/edit_message_checklist.py @@ -25,7 +25,7 @@ class EditMessageChecklist(TelegramMethod[Message]): checklist: InputChecklist """A JSON-serialized object for the new checklist""" reply_markup: InlineKeyboardMarkup | None = None - """A JSON-serialized object for the new inline keyboard for the message""" + """A JSON-serialized object for the new `inline keyboard `_ for the message""" if TYPE_CHECKING: # DO NOT EDIT MANUALLY!!! diff --git a/aiogram/methods/promote_chat_member.py b/aiogram/methods/promote_chat_member.py index e26f821e..6f8d4dbf 100644 --- a/aiogram/methods/promote_chat_member.py +++ b/aiogram/methods/promote_chat_member.py @@ -52,6 +52,8 @@ class PromoteChatMember(TelegramMethod[bool]): """Pass :code:`True` if the user is allowed to create, rename, close, and reopen forum topics; for supergroups only""" can_manage_direct_messages: bool | None = None """Pass :code:`True` if the administrator can manage direct messages within the channel and decline suggested posts; for channels only""" + can_manage_tags: bool | None = None + """Pass :code:`True` if the administrator can edit the tags of regular members; for groups and supergroups only""" if TYPE_CHECKING: # DO NOT EDIT MANUALLY!!! @@ -78,6 +80,7 @@ class PromoteChatMember(TelegramMethod[bool]): can_pin_messages: bool | None = None, can_manage_topics: bool | None = None, can_manage_direct_messages: bool | None = None, + can_manage_tags: bool | None = None, **__pydantic_kwargs: Any, ) -> None: # DO NOT EDIT MANUALLY!!! @@ -103,5 +106,6 @@ class PromoteChatMember(TelegramMethod[bool]): can_pin_messages=can_pin_messages, can_manage_topics=can_manage_topics, can_manage_direct_messages=can_manage_direct_messages, + can_manage_tags=can_manage_tags, **__pydantic_kwargs, ) diff --git a/aiogram/methods/send_checklist.py b/aiogram/methods/send_checklist.py index 852c8110..7a00317b 100644 --- a/aiogram/methods/send_checklist.py +++ b/aiogram/methods/send_checklist.py @@ -31,7 +31,7 @@ class SendChecklist(TelegramMethod[Message]): reply_parameters: ReplyParameters | None = None """A JSON-serialized object for description of the message to reply to""" reply_markup: InlineKeyboardMarkup | None = None - """A JSON-serialized object for an inline keyboard""" + """A JSON-serialized object for an `inline keyboard `_""" if TYPE_CHECKING: # DO NOT EDIT MANUALLY!!! diff --git a/aiogram/methods/send_message_draft.py b/aiogram/methods/send_message_draft.py index 6124f73e..b93c286c 100644 --- a/aiogram/methods/send_message_draft.py +++ b/aiogram/methods/send_message_draft.py @@ -8,7 +8,7 @@ from .base import TelegramMethod class SendMessageDraft(TelegramMethod[bool]): """ - Use this method to stream a partial message to a user while the message is being generated; supported only for bots with forum topic mode enabled. Returns :code:`True` on success. + Use this method to stream a partial message to a user while the message is being generated. Returns :code:`True` on success. Source: https://core.telegram.org/bots/api#sendmessagedraft """ diff --git a/aiogram/methods/set_chat_member_tag.py b/aiogram/methods/set_chat_member_tag.py new file mode 100644 index 00000000..de8a2d09 --- /dev/null +++ b/aiogram/methods/set_chat_member_tag.py @@ -0,0 +1,40 @@ +from typing import TYPE_CHECKING, Any + +from ..types import ChatIdUnion +from .base import TelegramMethod + + +class SetChatMemberTag(TelegramMethod[bool]): + """ + Use this method to set a tag for a regular member in a group or a supergroup. The bot must be an administrator in the chat for this to work and must have the *can_manage_tags* administrator right. Returns :code:`True` on success. + + Source: https://core.telegram.org/bots/api#setchatmembertag + """ + + __returning__ = bool + __api_method__ = "setChatMemberTag" + + chat_id: ChatIdUnion + """Unique identifier for the target chat or username of the target supergroup (in the format :code:`@supergroupusername`)""" + user_id: int + """Unique identifier of the target user""" + tag: str | None = None + """New tag for the member; 0-16 characters, emoji are not allowed""" + + if TYPE_CHECKING: + # DO NOT EDIT MANUALLY!!! + # This section was auto-generated via `butcher` + + def __init__( + __pydantic__self__, + *, + chat_id: ChatIdUnion, + user_id: int, + tag: str | None = None, + **__pydantic_kwargs: Any, + ) -> None: + # DO NOT EDIT MANUALLY!!! + # This method was auto-generated via `butcher` + # Is needed only for type checking and IDE support without any additional plugins + + super().__init__(chat_id=chat_id, user_id=user_id, tag=tag, **__pydantic_kwargs) diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index bea95afb..cde24548 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -28,6 +28,7 @@ if TYPE_CHECKING: SendChatAction, SetChatAdministratorCustomTitle, SetChatDescription, + SetChatMemberTag, SetChatPermissions, SetChatPhoto, SetChatStickerSet, @@ -967,6 +968,38 @@ class Chat(TelegramObject): **kwargs, ).as_(self._bot) + def set_member_tag( + self, + user_id: int, + tag: str | None = None, + **kwargs: Any, + ) -> SetChatMemberTag: + """ + Shortcut for method :class:`aiogram.methods.set_chat_member_tag.SetChatMemberTag` + will automatically fill method attributes: + + - :code:`chat_id` + + Use this method to set a tag for a regular member in a group or a supergroup. The bot must be an administrator in the chat for this to work and must have the *can_manage_tags* administrator right. Returns :code:`True` on success. + + Source: https://core.telegram.org/bots/api#setchatmembertag + + :param user_id: Unique identifier of the target user + :param tag: New tag for the member; 0-16 characters, emoji are not allowed + :return: instance of method :class:`aiogram.methods.set_chat_member_tag.SetChatMemberTag` + """ + # DO NOT EDIT MANUALLY!!! + # This method was auto-generated via `butcher` + + from aiogram.methods import SetChatMemberTag + + return SetChatMemberTag( + chat_id=self.id, + user_id=user_id, + tag=tag, + **kwargs, + ).as_(self._bot) + def set_permissions( self, permissions: ChatPermissions, @@ -1018,6 +1051,7 @@ class Chat(TelegramObject): can_pin_messages: bool | None = None, can_manage_topics: bool | None = None, can_manage_direct_messages: bool | None = None, + can_manage_tags: bool | None = None, **kwargs: Any, ) -> PromoteChatMember: """ @@ -1047,6 +1081,7 @@ class Chat(TelegramObject): :param can_pin_messages: Pass :code:`True` if the administrator can pin messages; for supergroups only :param can_manage_topics: Pass :code:`True` if the user is allowed to create, rename, close, and reopen forum topics; for supergroups only :param can_manage_direct_messages: Pass :code:`True` if the administrator can manage direct messages within the channel and decline suggested posts; for channels only + :param can_manage_tags: Pass :code:`True` if the administrator can edit the tags of regular members; for groups and supergroups only :return: instance of method :class:`aiogram.methods.promote_chat_member.PromoteChatMember` """ # DO NOT EDIT MANUALLY!!! @@ -1073,6 +1108,7 @@ class Chat(TelegramObject): can_pin_messages=can_pin_messages, can_manage_topics=can_manage_topics, can_manage_direct_messages=can_manage_direct_messages, + can_manage_tags=can_manage_tags, **kwargs, ).as_(self._bot) diff --git a/aiogram/types/chat_administrator_rights.py b/aiogram/types/chat_administrator_rights.py index cac9ae01..4c0811b9 100644 --- a/aiogram/types/chat_administrator_rights.py +++ b/aiogram/types/chat_administrator_rights.py @@ -47,6 +47,8 @@ class ChatAdministratorRights(TelegramObject): """*Optional*. :code:`True`, if the user is allowed to create, rename, close, and reopen forum topics; for supergroups only""" can_manage_direct_messages: bool | None = None """*Optional*. :code:`True`, if the administrator can manage direct messages of the channel and decline suggested posts; for channels only""" + can_manage_tags: bool | None = None + """*Optional*. :code:`True`, if the administrator can edit the tags of regular members; for groups and supergroups only. If omitted defaults to the value of can_pin_messages.""" if TYPE_CHECKING: # DO NOT EDIT MANUALLY!!! @@ -71,6 +73,7 @@ class ChatAdministratorRights(TelegramObject): can_pin_messages: bool | None = None, can_manage_topics: bool | None = None, can_manage_direct_messages: bool | None = None, + can_manage_tags: bool | None = None, **__pydantic_kwargs: Any, ) -> None: # DO NOT EDIT MANUALLY!!! @@ -94,5 +97,6 @@ class ChatAdministratorRights(TelegramObject): can_pin_messages=can_pin_messages, can_manage_topics=can_manage_topics, can_manage_direct_messages=can_manage_direct_messages, + can_manage_tags=can_manage_tags, **__pydantic_kwargs, ) diff --git a/aiogram/types/chat_member_administrator.py b/aiogram/types/chat_member_administrator.py index b24dece3..1d8915bc 100644 --- a/aiogram/types/chat_member_administrator.py +++ b/aiogram/types/chat_member_administrator.py @@ -54,6 +54,8 @@ class ChatMemberAdministrator(ChatMember): """*Optional*. :code:`True`, if the user is allowed to create, rename, close, and reopen forum topics; for supergroups only""" can_manage_direct_messages: bool | None = None """*Optional*. :code:`True`, if the administrator can manage direct messages of the channel and decline suggested posts; for channels only""" + can_manage_tags: bool | None = None + """*Optional*. :code:`True`, if the administrator can edit the tags of regular members; for groups and supergroups only. If omitted defaults to the value of can_pin_messages.""" custom_title: str | None = None """*Optional*. Custom title for this user""" @@ -83,6 +85,7 @@ class ChatMemberAdministrator(ChatMember): can_pin_messages: bool | None = None, can_manage_topics: bool | None = None, can_manage_direct_messages: bool | None = None, + can_manage_tags: bool | None = None, custom_title: str | None = None, **__pydantic_kwargs: Any, ) -> None: @@ -110,6 +113,7 @@ class ChatMemberAdministrator(ChatMember): can_pin_messages=can_pin_messages, can_manage_topics=can_manage_topics, can_manage_direct_messages=can_manage_direct_messages, + can_manage_tags=can_manage_tags, custom_title=custom_title, **__pydantic_kwargs, ) diff --git a/aiogram/types/chat_member_member.py b/aiogram/types/chat_member_member.py index 5b3ce5e0..0cd89e76 100644 --- a/aiogram/types/chat_member_member.py +++ b/aiogram/types/chat_member_member.py @@ -21,6 +21,8 @@ class ChatMemberMember(ChatMember): """The member's status in the chat, always 'member'""" user: User """Information about the user""" + tag: str | None = None + """*Optional*. Tag of the member""" until_date: DateTime | None = None """*Optional*. Date when the user's subscription will expire; Unix time""" @@ -33,6 +35,7 @@ class ChatMemberMember(ChatMember): *, status: Literal[ChatMemberStatus.MEMBER] = ChatMemberStatus.MEMBER, user: User, + tag: str | None = None, until_date: DateTime | None = None, **__pydantic_kwargs: Any, ) -> None: @@ -40,4 +43,6 @@ class ChatMemberMember(ChatMember): # This method was auto-generated via `butcher` # Is needed only for type checking and IDE support without any additional plugins - super().__init__(status=status, user=user, until_date=until_date, **__pydantic_kwargs) + super().__init__( + status=status, user=user, tag=tag, until_date=until_date, **__pydantic_kwargs + ) diff --git a/aiogram/types/chat_member_restricted.py b/aiogram/types/chat_member_restricted.py index 1466350f..0fc162ff 100644 --- a/aiogram/types/chat_member_restricted.py +++ b/aiogram/types/chat_member_restricted.py @@ -43,6 +43,8 @@ class ChatMemberRestricted(ChatMember): """:code:`True`, if the user is allowed to send animations, games, stickers and use inline bots""" can_add_web_page_previews: bool """:code:`True`, if the user is allowed to add web page previews to their messages""" + can_edit_tag: bool + """:code:`True`, if the user is allowed to edit their own tag""" can_change_info: bool """:code:`True`, if the user is allowed to change the chat title, photo and other settings""" can_invite_users: bool @@ -53,6 +55,8 @@ class ChatMemberRestricted(ChatMember): """:code:`True`, if the user is allowed to create forum topics""" until_date: DateTime """Date when restrictions will be lifted for this user; Unix time. If 0, then the user is restricted forever""" + tag: str | None = None + """*Optional*. Tag of the member""" if TYPE_CHECKING: # DO NOT EDIT MANUALLY!!! @@ -74,11 +78,13 @@ class ChatMemberRestricted(ChatMember): can_send_polls: bool, can_send_other_messages: bool, can_add_web_page_previews: bool, + can_edit_tag: bool, can_change_info: bool, can_invite_users: bool, can_pin_messages: bool, can_manage_topics: bool, until_date: DateTime, + tag: str | None = None, **__pydantic_kwargs: Any, ) -> None: # DO NOT EDIT MANUALLY!!! @@ -99,10 +105,12 @@ class ChatMemberRestricted(ChatMember): can_send_polls=can_send_polls, can_send_other_messages=can_send_other_messages, can_add_web_page_previews=can_add_web_page_previews, + can_edit_tag=can_edit_tag, can_change_info=can_change_info, can_invite_users=can_invite_users, can_pin_messages=can_pin_messages, can_manage_topics=can_manage_topics, until_date=until_date, + tag=tag, **__pydantic_kwargs, ) diff --git a/aiogram/types/chat_permissions.py b/aiogram/types/chat_permissions.py index dfb0fb77..473a38bb 100644 --- a/aiogram/types/chat_permissions.py +++ b/aiogram/types/chat_permissions.py @@ -32,6 +32,8 @@ class ChatPermissions(MutableTelegramObject): """*Optional*. :code:`True`, if the user is allowed to send animations, games, stickers and use inline bots""" can_add_web_page_previews: bool | None = None """*Optional*. :code:`True`, if the user is allowed to add web page previews to their messages""" + can_edit_tag: bool | None = None + """*Optional*. :code:`True`, if the user is allowed to edit their own tag""" can_change_info: bool | None = None """*Optional*. :code:`True`, if the user is allowed to change the chat title, photo and other settings. Ignored in public supergroups""" can_invite_users: bool | None = None @@ -58,6 +60,7 @@ class ChatPermissions(MutableTelegramObject): can_send_polls: bool | None = None, can_send_other_messages: bool | None = None, can_add_web_page_previews: bool | None = None, + can_edit_tag: bool | None = None, can_change_info: bool | None = None, can_invite_users: bool | None = None, can_pin_messages: bool | None = None, @@ -79,6 +82,7 @@ class ChatPermissions(MutableTelegramObject): can_send_polls=can_send_polls, can_send_other_messages=can_send_other_messages, can_add_web_page_previews=can_add_web_page_previews, + can_edit_tag=can_edit_tag, can_change_info=can_change_info, can_invite_users=can_invite_users, can_pin_messages=can_pin_messages, diff --git a/aiogram/types/game_high_score.py b/aiogram/types/game_high_score.py index e8fdcf40..5364be6e 100644 --- a/aiogram/types/game_high_score.py +++ b/aiogram/types/game_high_score.py @@ -15,8 +15,6 @@ class GameHighScore(TelegramObject): If you've got any questions, please check out our `https://core.telegram.org/bots/faq `_ **Bot FAQ »** - - - Source: https://core.telegram.org/bots/api#gamehighscore """ diff --git a/aiogram/types/inline_query_result_document.py b/aiogram/types/inline_query_result_document.py index c40d31b8..1665c8b3 100644 --- a/aiogram/types/inline_query_result_document.py +++ b/aiogram/types/inline_query_result_document.py @@ -38,7 +38,7 @@ class InlineQueryResultDocument(InlineQueryResult): description: str | None = None """*Optional*. Short description of the result""" reply_markup: InlineKeyboardMarkup | None = None - """*Optional*. Inline keyboard attached to the message""" + """*Optional*. `Inline keyboard `_ attached to the message""" input_message_content: InputMessageContentUnion | None = None """*Optional*. Content of the message to be sent instead of the file""" thumbnail_url: str | None = None diff --git a/aiogram/types/message.py b/aiogram/types/message.py index b35fe0fb..1b7f9677 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -157,6 +157,8 @@ class Message(MaybeInaccessibleMessage): """*Optional*. If the sender of the message boosted the chat, the number of boosts added by the user""" sender_business_bot: User | None = None """*Optional*. The bot that actually sent the message on behalf of the business account. Available only for outgoing messages sent on behalf of the connected business account.""" + sender_tag: str | None = None + """*Optional*. Tag or custom title of the sender of the message; for supergroups only""" business_connection_id: str | None = None """*Optional*. Unique identifier of the business connection from which the message was received. If non-empty, the message belongs to a chat of the corresponding business account that is independent from any potential bot chat which might share the same identifier.""" forward_origin: MessageOriginUnion | None = None @@ -186,7 +188,7 @@ class Message(MaybeInaccessibleMessage): is_paid_post: bool | None = None """*Optional*. :code:`True`, if the message is a paid post. Note that such posts must not be deleted for 24 hours to receive the payment and can't be edited.""" media_group_id: str | None = None - """*Optional*. The unique identifier of a media message group this message belongs to""" + """*Optional*. The unique identifier inside this chat of a media message group this message belongs to""" author_signature: str | None = None """*Optional*. Signature of the post author for messages in channels, or the custom title of an anonymous group administrator""" paid_star_count: int | None = None @@ -348,7 +350,7 @@ class Message(MaybeInaccessibleMessage): web_app_data: WebAppData | None = None """*Optional*. Service message: data sent by a Web App""" reply_markup: InlineKeyboardMarkup | None = None - """*Optional*. Inline keyboard attached to the message. :code:`login_url` buttons are represented as ordinary :code:`url` buttons.""" + """*Optional*. `Inline keyboard `_ attached to the message. :code:`login_url` buttons are represented as ordinary :code:`url` buttons.""" forward_date: DateTime | None = Field(None, json_schema_extra={"deprecated": True}) """*Optional*. For forwarded messages, date the original message was sent in Unix time @@ -401,6 +403,7 @@ class Message(MaybeInaccessibleMessage): sender_chat: Chat | None = None, sender_boost_count: int | None = None, sender_business_bot: User | None = None, + sender_tag: str | None = None, business_connection_id: str | None = None, forward_origin: MessageOriginUnion | None = None, is_topic_message: bool | None = None, @@ -520,6 +523,7 @@ class Message(MaybeInaccessibleMessage): sender_chat=sender_chat, sender_boost_count=sender_boost_count, sender_business_bot=sender_business_bot, + sender_tag=sender_tag, business_connection_id=business_connection_id, forward_origin=forward_origin, is_topic_message=is_topic_message, diff --git a/aiogram/types/message_entity.py b/aiogram/types/message_entity.py index 608b8435..68ae3025 100644 --- a/aiogram/types/message_entity.py +++ b/aiogram/types/message_entity.py @@ -17,7 +17,7 @@ class MessageEntity(MutableTelegramObject): """ type: str - """Type of the entity. Currently, can be 'mention' (:code:`@username`), 'hashtag' (:code:`#hashtag` or :code:`#hashtag@chatusername`), 'cashtag' (:code:`$USD` or :code:`$USD@chatusername`), 'bot_command' (:code:`/start@jobs_bot`), 'url' (:code:`https://telegram.org`), 'email' (:code:`do-not-reply@telegram.org`), 'phone_number' (:code:`+1-212-555-0123`), 'bold' (**bold text**), 'italic' (*italic text*), 'underline' (underlined text), 'strikethrough' (strikethrough text), 'spoiler' (spoiler message), 'blockquote' (block quotation), 'expandable_blockquote' (collapsed-by-default block quotation), 'code' (monowidth string), 'pre' (monowidth block), 'text_link' (for clickable text URLs), 'text_mention' (for users `without usernames `_), 'custom_emoji' (for inline custom emoji stickers)""" + """Type of the entity. Currently, can be 'mention' (:code:`@username`), 'hashtag' (:code:`#hashtag` or :code:`#hashtag@chatusername`), 'cashtag' (:code:`$USD` or :code:`$USD@chatusername`), 'bot_command' (:code:`/start@jobs_bot`), 'url' (:code:`https://telegram.org`), 'email' (:code:`do-not-reply@telegram.org`), 'phone_number' (:code:`+1-212-555-0123`), 'bold' (**bold text**), 'italic' (*italic text*), 'underline' (underlined text), 'strikethrough' (strikethrough text), 'spoiler' (spoiler message), 'blockquote' (block quotation), 'expandable_blockquote' (collapsed-by-default block quotation), 'code' (monowidth string), 'pre' (monowidth block), 'text_link' (for clickable text URLs), 'text_mention' (for users `without usernames `_), 'custom_emoji' (for inline custom emoji stickers), or 'date_time' (for formatted date and time)""" offset: int """Offset in `UTF-16 code units `_ to the start of the entity""" length: int @@ -30,6 +30,10 @@ class MessageEntity(MutableTelegramObject): """*Optional*. For 'pre' only, the programming language of the entity text""" custom_emoji_id: str | None = None """*Optional*. For 'custom_emoji' only, unique identifier of the custom emoji. Use :class:`aiogram.methods.get_custom_emoji_stickers.GetCustomEmojiStickers` to get full information about the sticker""" + unix_time: int | None = None + """*Optional*. For 'date_time' only, the Unix time associated with the entity""" + date_time_format: str | None = None + """*Optional*. For 'date_time' only, the string that defines the formatting of the date and time. See `date-time entity formatting `_ for more details.""" if TYPE_CHECKING: # DO NOT EDIT MANUALLY!!! @@ -45,6 +49,8 @@ class MessageEntity(MutableTelegramObject): user: User | None = None, language: str | None = None, custom_emoji_id: str | None = None, + unix_time: int | None = None, + date_time_format: str | None = None, **__pydantic_kwargs: Any, ) -> None: # DO NOT EDIT MANUALLY!!! @@ -59,6 +65,8 @@ class MessageEntity(MutableTelegramObject): user=user, language=language, custom_emoji_id=custom_emoji_id, + unix_time=unix_time, + date_time_format=date_time_format, **__pydantic_kwargs, ) diff --git a/aiogram/utils/formatting.py b/aiogram/utils/formatting.py index cbfd9758..4dfccd64 100644 --- a/aiogram/utils/formatting.py +++ b/aiogram/utils/formatting.py @@ -2,6 +2,7 @@ from __future__ import annotations import textwrap from collections.abc import Generator, Iterable, Iterator +from datetime import datetime from typing import Any, ClassVar from typing_extensions import Self @@ -534,6 +535,26 @@ class ExpandableBlockQuote(Text): type = MessageEntityType.EXPANDABLE_BLOCKQUOTE +class DateTime(Text): + type = MessageEntityType.DATE_TIME + + def __init__( + self, + *body: NodeType, + unix_time: int | datetime, + date_time_format: str | None = None, + **params: Any, + ) -> None: + if isinstance(unix_time, datetime): + unix_time = int(unix_time.timestamp()) + super().__init__( + *body, + unix_time=unix_time, + date_time_format=date_time_format, + **params, + ) + + NODE_TYPES: dict[str | None, type[Text]] = { Text.type: Text, HashTag.type: HashTag, @@ -554,6 +575,7 @@ NODE_TYPES: dict[str | None, type[Text]] = { CustomEmoji.type: CustomEmoji, BlockQuote.type: BlockQuote, ExpandableBlockQuote.type: ExpandableBlockQuote, + DateTime.type: DateTime, } diff --git a/aiogram/utils/text_decorations.py b/aiogram/utils/text_decorations.py index 00762c0a..b8edead2 100644 --- a/aiogram/utils/text_decorations.py +++ b/aiogram/utils/text_decorations.py @@ -3,9 +3,11 @@ from __future__ import annotations import html import re from abc import ABC, abstractmethod +from datetime import date, datetime, time from typing import TYPE_CHECKING, cast from aiogram.enums import MessageEntityType +from aiogram.utils.link import create_tg_link if TYPE_CHECKING: from collections.abc import Generator @@ -78,6 +80,12 @@ class TextDecoration(ABC): return self.link(value=text, link=cast(str, entity.url)) if entity.type == MessageEntityType.CUSTOM_EMOJI: return self.custom_emoji(value=text, custom_emoji_id=cast(str, entity.custom_emoji_id)) + if entity.type == MessageEntityType.DATE_TIME: + return self.date_time( + value=text, + unix_time=cast(int, entity.unix_time), + date_time_format=entity.date_time_format, + ) # This case is not possible because of `if` above, but if any new entity is added to # API it will be here too @@ -180,54 +188,105 @@ class TextDecoration(ABC): def expandable_blockquote(self, value: str) -> str: pass + @abstractmethod + def date_time( + self, + value: str, + unix_time: int | datetime, + date_time_format: str | None = None, + ) -> str: + pass + class HtmlDecoration(TextDecoration): BOLD_TAG = "b" ITALIC_TAG = "i" UNDERLINE_TAG = "u" STRIKETHROUGH_TAG = "s" + CODE_TAG = "code" + PRE_TAG = "pre" + LINK_TAG = "a" SPOILER_TAG = "tg-spoiler" EMOJI_TAG = "tg-emoji" + DATE_TIME_TAG = "tg-time" BLOCKQUOTE_TAG = "blockquote" + def _tag( + self, + tag: str, + content: str, + *, + attrs: dict[str, str] | None = None, + flags: list[str] | None = None, + ) -> str: + prepared_attrs: list[str] = [] + if attrs: + prepared_attrs.extend(f'{k}="{v}"' for k, v in attrs.items()) + if flags: + prepared_attrs.extend(f"{flag}" for flag in flags) + + attrs_str = " ".join(prepared_attrs) + if attrs_str: + attrs_str = " " + attrs_str + + return f"<{tag}{attrs_str}>{content}" + def link(self, value: str, link: str) -> str: - return f'{value}' + return self._tag(self.LINK_TAG, value, attrs={"href": link}) def bold(self, value: str) -> str: - return f"<{self.BOLD_TAG}>{value}" + return self._tag(self.BOLD_TAG, value) def italic(self, value: str) -> str: - return f"<{self.ITALIC_TAG}>{value}" + return self._tag(self.ITALIC_TAG, value) def code(self, value: str) -> str: - return f"{value}" + return self._tag(self.CODE_TAG, value) def pre(self, value: str) -> str: - return f"
{value}
" + return self._tag(self.PRE_TAG, value) def pre_language(self, value: str, language: str) -> str: - return f'
{value}
' + return self._tag( + self.PRE_TAG, + self._tag(self.CODE_TAG, value, attrs={"language": f"language-{language}"}), + ) def underline(self, value: str) -> str: - return f"<{self.UNDERLINE_TAG}>{value}" + return self._tag(self.UNDERLINE_TAG, value) def strikethrough(self, value: str) -> str: - return f"<{self.STRIKETHROUGH_TAG}>{value}" + return self._tag(self.STRIKETHROUGH_TAG, value) def spoiler(self, value: str) -> str: - return f"<{self.SPOILER_TAG}>{value}" + return self._tag(self.SPOILER_TAG, value) def quote(self, value: str) -> str: return html.escape(value, quote=False) def custom_emoji(self, value: str, custom_emoji_id: str) -> str: - return f'<{self.EMOJI_TAG} emoji-id="{custom_emoji_id}">{value}' + return self._tag(self.EMOJI_TAG, value, attrs={"emoji_id": custom_emoji_id}) def blockquote(self, value: str) -> str: - return f"<{self.BLOCKQUOTE_TAG}>{value}" + return self._tag(self.BLOCKQUOTE_TAG, value) def expandable_blockquote(self, value: str) -> str: - return f"<{self.BLOCKQUOTE_TAG} expandable>{value}" + return self._tag(self.BLOCKQUOTE_TAG, value, flags=["expandable"]) + + def date_time( + self, + value: str, + unix_time: int | datetime, + date_time_format: str | None = None, + ) -> str: + if isinstance(unix_time, datetime): + unix_time = int(unix_time.timestamp()) + + args = {"unix": str(unix_time)} + if date_time_format: + args["format"] = date_time_format + + return self._tag(self.DATE_TIME_TAG, value, attrs=args) class MarkdownDecoration(TextDecoration): @@ -264,7 +323,8 @@ class MarkdownDecoration(TextDecoration): return re.sub(pattern=self.MARKDOWN_QUOTE_PATTERN, repl=r"\\\1", string=value) def custom_emoji(self, value: str, custom_emoji_id: str) -> str: - return f"!{self.link(value=value, link=f'tg://emoji?id={custom_emoji_id}')}" + link = create_tg_link("emoji", emoji_id=custom_emoji_id) + return f"!{self.link(value=value, link=link)}" def blockquote(self, value: str) -> str: return "\n".join(f">{line}" for line in value.splitlines()) @@ -272,6 +332,22 @@ class MarkdownDecoration(TextDecoration): def expandable_blockquote(self, value: str) -> str: return "\n".join(f">{line}" for line in value.splitlines()) + "||" + def date_time( + self, + value: str, + unix_time: int | datetime, + date_time_format: str | None = None, + ) -> str: + if isinstance(unix_time, datetime): + unix_time = int(unix_time.timestamp()) + + link_params = {"unix": str(unix_time)} + if date_time_format: + link_params["format"] = date_time_format + link = create_tg_link("time", **link_params) + + return f"!{self.link(value, link=link)}" + html_decoration = HtmlDecoration() markdown_decoration = MarkdownDecoration() diff --git a/docs/api/methods/get_user_profile_audios.rst b/docs/api/methods/get_user_profile_audios.rst index f9567228..553ab296 100644 --- a/docs/api/methods/get_user_profile_audios.rst +++ b/docs/api/methods/get_user_profile_audios.rst @@ -36,3 +36,11 @@ With specific bot .. code-block:: python result: UserProfileAudios = await bot(GetUserProfileAudios(...)) + + + + +As shortcut from received object +-------------------------------- + +- :meth:`aiogram.types.user.User.get_profile_audios` diff --git a/docs/api/methods/index.rst b/docs/api/methods/index.rst index bbd6303a..f83a4f10 100644 --- a/docs/api/methods/index.rst +++ b/docs/api/methods/index.rst @@ -127,6 +127,7 @@ Available methods set_business_account_username set_chat_administrator_custom_title set_chat_description + set_chat_member_tag set_chat_menu_button set_chat_permissions set_chat_photo diff --git a/docs/api/methods/set_chat_member_tag.rst b/docs/api/methods/set_chat_member_tag.rst new file mode 100644 index 00000000..79090f6f --- /dev/null +++ b/docs/api/methods/set_chat_member_tag.rst @@ -0,0 +1,45 @@ +################ +setChatMemberTag +################ + +Returns: :obj:`bool` + +.. automodule:: aiogram.methods.set_chat_member_tag + :members: + :member-order: bysource + :undoc-members: True + :exclude-members: model_config,model_fields + + +Usage +===== + +As bot method +------------- + +.. code-block:: + + result: bool = await bot.set_chat_member_tag(...) + + +Method as object +---------------- + +Imports: + +- :code:`from aiogram.methods.set_chat_member_tag import SetChatMemberTag` +- alias: :code:`from aiogram.methods import SetChatMemberTag` + +With specific bot +~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + result: bool = await bot(SetChatMemberTag(...)) + +As reply into Webhook in handler +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + return SetChatMemberTag(...) diff --git a/tests/test_api/test_methods/test_promote_chat_member.py b/tests/test_api/test_methods/test_promote_chat_member.py index ee3b7f4e..1f19f3da 100644 --- a/tests/test_api/test_methods/test_promote_chat_member.py +++ b/tests/test_api/test_methods/test_promote_chat_member.py @@ -6,6 +6,11 @@ class TestPromoteChatMember: async def test_bot_method(self, bot: MockedBot): prepare_result = bot.add_result_for(PromoteChatMember, ok=True, result=True) - response: bool = await bot.promote_chat_member(chat_id=-42, user_id=42) - bot.get_request() + response: bool = await bot.promote_chat_member( + chat_id=-42, + user_id=42, + can_manage_tags=True, + ) + request = bot.get_request() + assert request.can_manage_tags is True assert response == prepare_result.result diff --git a/tests/test_api/test_methods/test_set_chat_member_tag.py b/tests/test_api/test_methods/test_set_chat_member_tag.py new file mode 100644 index 00000000..edc581cd --- /dev/null +++ b/tests/test_api/test_methods/test_set_chat_member_tag.py @@ -0,0 +1,14 @@ +from aiogram.methods import SetChatMemberTag +from tests.mocked_bot import MockedBot + + +class TestSetChatMemberTag: + async def test_bot_method(self, bot: MockedBot): + prepare_result = bot.add_result_for(SetChatMemberTag, ok=True, result=True) + + response: bool = await bot.set_chat_member_tag(chat_id=-42, user_id=42, tag="test") + request = bot.get_request() + assert request.chat_id == -42 + assert request.user_id == 42 + assert request.tag == "test" + assert response == prepare_result.result diff --git a/tests/test_api/test_types/test_chat.py b/tests/test_api/test_types/test_chat.py index 360b2ee1..b3e63854 100644 --- a/tests/test_api/test_types/test_chat.py +++ b/tests/test_api/test_types/test_chat.py @@ -115,6 +115,14 @@ class TestChat: method = chat.set_administrator_custom_title(user_id=1, custom_title="test") assert method.chat_id == chat.id + def test_set_member_tag(self): + chat = Chat(id=-42, type="supergroup") + + method = chat.set_member_tag(user_id=42, tag="test") + assert method.chat_id == chat.id + assert method.user_id == 42 + assert method.tag == "test" + def test_set_permissions(self): chat = Chat(id=-42, type="supergroup") diff --git a/tests/test_api/test_types/test_chat_member_tag_permissions.py b/tests/test_api/test_types/test_chat_member_tag_permissions.py new file mode 100644 index 00000000..30aa5481 --- /dev/null +++ b/tests/test_api/test_types/test_chat_member_tag_permissions.py @@ -0,0 +1,84 @@ +from datetime import datetime + +from aiogram.types import ( + ChatAdministratorRights, + ChatMemberAdministrator, + ChatMemberMember, + ChatMemberRestricted, + ChatPermissions, + User, +) + + +class TestChatMemberTagPermissions: + def test_chat_administrator_rights_can_manage_tags(self): + rights = ChatAdministratorRights( + is_anonymous=False, + can_manage_chat=True, + can_delete_messages=True, + can_manage_video_chats=True, + can_restrict_members=True, + can_promote_members=True, + can_change_info=True, + can_invite_users=True, + can_post_stories=True, + can_edit_stories=True, + can_delete_stories=True, + can_manage_tags=True, + ) + assert rights.can_manage_tags is True + + def test_chat_member_administrator_can_manage_tags(self): + admin = ChatMemberAdministrator( + user=User(id=42, is_bot=False, first_name="User"), + can_be_edited=True, + is_anonymous=False, + can_manage_chat=True, + can_delete_messages=True, + can_manage_video_chats=True, + can_restrict_members=True, + can_promote_members=True, + can_change_info=True, + can_invite_users=True, + can_post_stories=True, + can_edit_stories=True, + can_delete_stories=True, + can_manage_tags=True, + ) + assert admin.can_manage_tags is True + + def test_chat_permissions_can_edit_tag(self): + permissions = ChatPermissions(can_edit_tag=True) + assert permissions.can_edit_tag is True + + def test_chat_member_member_tag(self): + member = ChatMemberMember( + user=User(id=42, is_bot=False, first_name="User"), + tag="premium", + ) + assert member.tag == "premium" + + def test_chat_member_restricted_can_edit_tag_and_tag(self): + restricted = ChatMemberRestricted( + user=User(id=42, is_bot=False, first_name="User"), + is_member=True, + can_send_messages=True, + can_send_audios=True, + can_send_documents=True, + can_send_photos=True, + can_send_videos=True, + can_send_video_notes=True, + can_send_voice_notes=True, + can_send_polls=True, + can_send_other_messages=True, + can_add_web_page_previews=True, + can_edit_tag=True, + can_change_info=True, + can_invite_users=True, + can_pin_messages=True, + can_manage_topics=True, + until_date=datetime.now(), + tag="premium", + ) + assert restricted.can_edit_tag is True + assert restricted.tag == "premium" diff --git a/tests/test_filters/test_chat_member_updated.py b/tests/test_filters/test_chat_member_updated.py index c88b705e..4582f052 100644 --- a/tests/test_filters/test_chat_member_updated.py +++ b/tests/test_filters/test_chat_member_updated.py @@ -314,6 +314,7 @@ class TestChatMemberUpdatedStatusFilter: "can_send_polls": True, "can_send_other_messages": True, "can_add_web_page_previews": True, + "can_edit_tag": True, "can_post_stories": True, "can_edit_stories": True, "can_delete_stories": True, diff --git a/tests/test_utils/test_chat_member.py b/tests/test_utils/test_chat_member.py index 8a42600c..34f32d1c 100644 --- a/tests/test_utils/test_chat_member.py +++ b/tests/test_utils/test_chat_member.py @@ -70,6 +70,7 @@ CHAT_MEMBER_RESTRICTED = ChatMemberRestricted( can_send_polls=False, can_send_other_messages=False, can_add_web_page_previews=False, + can_edit_tag=False, can_change_info=False, can_invite_users=False, can_pin_messages=False, diff --git a/tests/test_utils/test_formatting.py b/tests/test_utils/test_formatting.py index ffaef31e..8efc1598 100644 --- a/tests/test_utils/test_formatting.py +++ b/tests/test_utils/test_formatting.py @@ -1,3 +1,5 @@ +from datetime import datetime, timezone + import pytest from aiogram.enums import MessageEntityType @@ -9,6 +11,7 @@ from aiogram.utils.formatting import ( CashTag, Code, CustomEmoji, + DateTime, Email, ExpandableBlockQuote, HashTag, @@ -93,7 +96,7 @@ class TestNode: ], [ Pre("test", language="python"), - '
test
', + '
test
', ], [ TextLink("test", url="https://example.com"), @@ -105,7 +108,7 @@ class TestNode: ], [ CustomEmoji("test", custom_emoji_id="42"), - 'test', + 'test', ], [ BlockQuote("test"), @@ -115,6 +118,14 @@ class TestNode: ExpandableBlockQuote("test"), "
test
", ], + [ + DateTime("test", unix_time=42, date_time_format="yMd"), + 'test', + ], + [ + DateTime("test", unix_time=42), + 'test', + ], ], ) def test_render_plain_only(self, node: Text, result: str): @@ -358,6 +369,38 @@ class TestUtils: assert isinstance(node, Bold) assert node._body == ("test",) + def test_apply_entity_date_time(self): + node = _apply_entity( + MessageEntity( + type=MessageEntityType.DATE_TIME, + offset=0, + length=4, + unix_time=42, + date_time_format="yMd", + ), + "test", + ) + assert isinstance(node, DateTime) + assert node._body == ("test",) + assert node._params["unix_time"] == 42 + assert node._params["date_time_format"] == "yMd" + + def test_date_time_with_datetime_object(self): + dt = datetime(2024, 1, 1, 0, 0, 0, tzinfo=timezone.utc) + node = DateTime("test", unix_time=dt) + assert isinstance(node, DateTime) + assert node._params["unix_time"] == 1704067200 + + def test_date_time_with_datetime_and_format(self): + dt = datetime(2024, 1, 1, 0, 0, 0, tzinfo=timezone.utc) + node = DateTime("test", unix_time=dt, date_time_format="yMd") + assert node._params["unix_time"] == 1704067200 + assert node._params["date_time_format"] == "yMd" + + def test_date_time_as_markdown(self): + node = DateTime("test", unix_time=42, date_time_format="yMd") + assert node.as_markdown() == "![test](tg://time?unix=42&format=yMd)" + def test_as_line(self): node = as_line("test", "test", "test") assert isinstance(node, Text) diff --git a/tests/test_utils/test_text_decorations.py b/tests/test_utils/test_text_decorations.py index b4ccb5e8..6325edb8 100644 --- a/tests/test_utils/test_text_decorations.py +++ b/tests/test_utils/test_text_decorations.py @@ -1,3 +1,5 @@ +from datetime import datetime, timezone + import pytest from aiogram.types import MessageEntity, User @@ -25,7 +27,7 @@ class TestTextDecoration: [ html_decoration, MessageEntity(type="pre", offset=0, length=5, language="python"), - '
test
', + '
test
', ], [html_decoration, MessageEntity(type="underline", offset=0, length=5), "test"], [ @@ -57,7 +59,7 @@ class TestTextDecoration: [ html_decoration, MessageEntity(type="custom_emoji", offset=0, length=5, custom_emoji_id="42"), - 'test', + 'test', ], [ html_decoration, @@ -74,6 +76,17 @@ class TestTextDecoration: MessageEntity(type="expandable_blockquote", offset=0, length=5), "
test
", ], + [ + html_decoration, + MessageEntity( + type="date_time", + offset=0, + length=5, + unix_time=42, + date_time_format="yMd", + ), + 'test', + ], [markdown_decoration, MessageEntity(type="bold", offset=0, length=5), "*test*"], [markdown_decoration, MessageEntity(type="italic", offset=0, length=5), "_\rtest_\r"], [markdown_decoration, MessageEntity(type="code", offset=0, length=5), "`test`"], @@ -102,7 +115,7 @@ class TestTextDecoration: [ markdown_decoration, MessageEntity(type="custom_emoji", offset=0, length=5, custom_emoji_id="42"), - "![test](tg://emoji?id=42)", + "![test](tg://emoji?emoji_id=42)", ], [ markdown_decoration, @@ -124,6 +137,27 @@ class TestTextDecoration: MessageEntity(type="expandable_blockquote", offset=0, length=5), ">test||", ], + [ + markdown_decoration, + MessageEntity( + type="date_time", + offset=0, + length=5, + unix_time=42, + date_time_format="yMd", + ), + "![test](tg://time?unix=42&format=yMd)", + ], + [ + html_decoration, + MessageEntity(type="date_time", offset=0, length=5, unix_time=42), + 'test', + ], + [ + markdown_decoration, + MessageEntity(type="date_time", offset=0, length=5, unix_time=42), + "![test](tg://time?unix=42)", + ], ], ) def test_apply_single_entity( @@ -131,6 +165,38 @@ class TestTextDecoration: ): assert decorator.apply_entity(entity, "test") == result + @pytest.mark.parametrize( + "decorator,date_time_format,expected", + [ + ( + html_decoration, + None, + 'test', + ), + ( + html_decoration, + "yMd", + 'test', + ), + ( + markdown_decoration, + None, + "![test](tg://time?unix=1704067200)", + ), + ( + markdown_decoration, + "yMd", + "![test](tg://time?unix=1704067200&format=yMd)", + ), + ], + ) + def test_date_time_with_datetime_object( + self, decorator: TextDecoration, date_time_format: str | None, expected: str + ): + dt = datetime(2024, 1, 1, 0, 0, 0, tzinfo=timezone.utc) + result = decorator.date_time("test", unix_time=dt, date_time_format=date_time_format) + assert result == expected + def test_unknown_apply_entity(self): assert ( html_decoration.apply_entity( @@ -296,6 +362,22 @@ class TestTextDecoration: ], "test@example.com", ], + [ + html_decoration, + "test", + [MessageEntity(type="date_time", offset=0, length=4, unix_time=42)], + 'test', + ], + [ + html_decoration, + "test", + [ + MessageEntity( + type="date_time", offset=0, length=4, unix_time=42, date_time_format="yMd" + ) + ], + 'test', + ], ], ) def test_unparse( From bd75ae361e8b81cab6dae69e8d6c730a71fcd76b Mon Sep 17 00:00:00 2001 From: Bogdan I <116835265+N1chegons@users.noreply.github.com> Date: Tue, 3 Mar 2026 04:24:05 +0500 Subject: [PATCH 6/8] Fix protected namespace warning for model_custom_emoji_id (#1775) * fix UserWarning, model_custom_emoji_id * Add changelog for #1772 --- CHANGES/1772.bugfix.rst | 1 + aiogram/types/base.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 CHANGES/1772.bugfix.rst diff --git a/CHANGES/1772.bugfix.rst b/CHANGES/1772.bugfix.rst new file mode 100644 index 00000000..084915d0 --- /dev/null +++ b/CHANGES/1772.bugfix.rst @@ -0,0 +1 @@ +Fixed Pydantic protected namespace warning for `model_custom_emoji_id` by adding `protected_namespaces=()` to `model_config`. diff --git a/aiogram/types/base.py b/aiogram/types/base.py index 7e47b564..ce75e84a 100644 --- a/aiogram/types/base.py +++ b/aiogram/types/base.py @@ -1,10 +1,9 @@ from typing import Any from unittest.mock import sentinel -from pydantic import BaseModel, ConfigDict, model_validator - from aiogram.client.context_controller import BotContextController from aiogram.client.default import Default +from pydantic import BaseModel, ConfigDict, model_validator class TelegramObject(BotContextController, BaseModel): @@ -16,6 +15,7 @@ class TelegramObject(BotContextController, BaseModel): populate_by_name=True, arbitrary_types_allowed=True, defer_build=True, + protected_namespaces=(), ) @model_validator(mode="before") From 49eac319a3bf4afbf9a77c8bfd7b98c8103e8070 Mon Sep 17 00:00:00 2001 From: JRoot Junior Date: Tue, 3 Mar 2026 01:24:55 +0200 Subject: [PATCH 7/8] Optimize imports in `aiogram/types/base.py` --- aiogram/types/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aiogram/types/base.py b/aiogram/types/base.py index ce75e84a..4ba749d8 100644 --- a/aiogram/types/base.py +++ b/aiogram/types/base.py @@ -1,9 +1,10 @@ from typing import Any from unittest.mock import sentinel +from pydantic import BaseModel, ConfigDict, model_validator + from aiogram.client.context_controller import BotContextController from aiogram.client.default import Default -from pydantic import BaseModel, ConfigDict, model_validator class TelegramObject(BotContextController, BaseModel): From 3e2ca5b6b0f0ea5e4c1e384214a0fd581e93e519 Mon Sep 17 00:00:00 2001 From: JRoot Junior Date: Tue, 3 Mar 2026 01:26:18 +0200 Subject: [PATCH 8/8] Bump changelog --- CHANGES.rst | 37 +++++++++++++++++++++++++++++++++++++ CHANGES/1687.bugfix.rst | 1 - CHANGES/1768.bugfix.rst | 1 - CHANGES/1772.bugfix.rst | 1 - CHANGES/1780.misc.rst | 15 --------------- CHANGES/47.misc.rst | 1 - 6 files changed, 37 insertions(+), 19 deletions(-) delete mode 100644 CHANGES/1687.bugfix.rst delete mode 100644 CHANGES/1768.bugfix.rst delete mode 100644 CHANGES/1772.bugfix.rst delete mode 100644 CHANGES/1780.misc.rst delete mode 100644 CHANGES/47.misc.rst diff --git a/CHANGES.rst b/CHANGES.rst index be4cb56e..9888246a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -16,6 +16,43 @@ Changelog .. towncrier release notes start +3.25.0 (2026-03-03) +==================== + +Bugfixes +-------- + +- Fixed scene transitions to preserve middleware-injected data when moving between scenes via ``SceneWizard.goto``. + `#1687 `_ +- Added ``icon_custom_emoji_id`` and ``style`` parameters to ``InlineKeyboardBuilder.button`` and ``ReplyKeyboardBuilder.button`` signatures. + `#1768 `_ +- Fixed Pydantic protected namespace warning for `model_custom_emoji_id` by adding `protected_namespaces=()` to `model_config`. + `#1772 `_ + + +Misc +---- + +- Documented webhook security constraints for proxy deployments, including trust requirements for :code:`X-Forwarded-For` and recommended defense-in-depth checks. + `#47 `_ +- Updated to `Bot API 9.5 `_ + + **New Methods:** + + - Added :class:`aiogram.methods.send_message_draft.SendMessageDraft` method - allowed for all bots to stream partial messages while they are being generated + - Added :class:`aiogram.methods.set_chat_member_tag.SetChatMemberTag` method - allows bots to set a custom tag for a chat member; available via :meth:`aiogram.types.chat.Chat.set_member_tag` shortcut + + **New Fields:** + + - Added :code:`date_time` type to :class:`aiogram.types.message_entity.MessageEntity` with :code:`unix_time` and :code:`date_time_format` fields - allows bots to display a formatted date and time to the user + - Added :code:`tag` field to :class:`aiogram.types.chat_member_member.ChatMemberMember` and :class:`aiogram.types.chat_member_restricted.ChatMemberRestricted` - the custom tag set for the chat member + - Added :code:`can_edit_tag` field to :class:`aiogram.types.chat_member_restricted.ChatMemberRestricted` and :class:`aiogram.types.chat_permissions.ChatPermissions` - indicates whether the user is allowed to edit their own tag + - Added :code:`can_manage_tags` field to :class:`aiogram.types.chat_member_administrator.ChatMemberAdministrator` and :class:`aiogram.types.chat_administrator_rights.ChatAdministratorRights` - indicates whether the administrator can manage tags of other chat members + - Added :code:`can_manage_tags` parameter to :class:`aiogram.methods.promote_chat_member.PromoteChatMember` method + - Added :code:`sender_tag` field to :class:`aiogram.types.message.Message` - the tag of the message sender in the chat + `#1780 `_ + + 3.25.0 (2026-02-10) ==================== diff --git a/CHANGES/1687.bugfix.rst b/CHANGES/1687.bugfix.rst deleted file mode 100644 index 9fac31c9..00000000 --- a/CHANGES/1687.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed scene transitions to preserve middleware-injected data when moving between scenes via ``SceneWizard.goto``. diff --git a/CHANGES/1768.bugfix.rst b/CHANGES/1768.bugfix.rst deleted file mode 100644 index 4b8c94bf..00000000 --- a/CHANGES/1768.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Added ``icon_custom_emoji_id`` and ``style`` parameters to ``InlineKeyboardBuilder.button`` and ``ReplyKeyboardBuilder.button`` signatures. diff --git a/CHANGES/1772.bugfix.rst b/CHANGES/1772.bugfix.rst deleted file mode 100644 index 084915d0..00000000 --- a/CHANGES/1772.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed Pydantic protected namespace warning for `model_custom_emoji_id` by adding `protected_namespaces=()` to `model_config`. diff --git a/CHANGES/1780.misc.rst b/CHANGES/1780.misc.rst deleted file mode 100644 index 6037a9d7..00000000 --- a/CHANGES/1780.misc.rst +++ /dev/null @@ -1,15 +0,0 @@ -Updated to `Bot API 9.5 `_ - -**New Methods:** - -- Added :class:`aiogram.methods.send_message_draft.SendMessageDraft` method - allowed for all bots to stream partial messages while they are being generated -- Added :class:`aiogram.methods.set_chat_member_tag.SetChatMemberTag` method - allows bots to set a custom tag for a chat member; available via :meth:`aiogram.types.chat.Chat.set_member_tag` shortcut - -**New Fields:** - -- Added :code:`date_time` type to :class:`aiogram.types.message_entity.MessageEntity` with :code:`unix_time` and :code:`date_time_format` fields - allows bots to display a formatted date and time to the user -- Added :code:`tag` field to :class:`aiogram.types.chat_member_member.ChatMemberMember` and :class:`aiogram.types.chat_member_restricted.ChatMemberRestricted` - the custom tag set for the chat member -- Added :code:`can_edit_tag` field to :class:`aiogram.types.chat_member_restricted.ChatMemberRestricted` and :class:`aiogram.types.chat_permissions.ChatPermissions` - indicates whether the user is allowed to edit their own tag -- Added :code:`can_manage_tags` field to :class:`aiogram.types.chat_member_administrator.ChatMemberAdministrator` and :class:`aiogram.types.chat_administrator_rights.ChatAdministratorRights` - indicates whether the administrator can manage tags of other chat members -- Added :code:`can_manage_tags` parameter to :class:`aiogram.methods.promote_chat_member.PromoteChatMember` method -- Added :code:`sender_tag` field to :class:`aiogram.types.message.Message` - the tag of the message sender in the chat diff --git a/CHANGES/47.misc.rst b/CHANGES/47.misc.rst deleted file mode 100644 index c895c16f..00000000 --- a/CHANGES/47.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Documented webhook security constraints for proxy deployments, including trust requirements for :code:`X-Forwarded-For` and recommended defense-in-depth checks.