From 7feb0cade0f20f321469023b5d313443ea9df24c Mon Sep 17 00:00:00 2001 From: Oleg Koloskov Date: Wed, 28 Jul 2021 22:11:26 +0300 Subject: [PATCH 01/43] Fix between condition in send_media_group method (#642) --- aiogram/bot/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 435def3e..66ea8af5 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -1006,7 +1006,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): media = types.MediaGroup(media) # check MediaGroup quantity - if 2 > len(media.media) > 10: + if not 2 <= len(media.media) <= 10: raise ValidationError("Media group must include 2-10 items") files = dict(media.get_files()) From f6f2972a1152039a3564d904b6272987101ef593 Mon Sep 17 00:00:00 2001 From: Ramzan Bekbulatov Date: Sat, 31 Jul 2021 21:47:37 +0300 Subject: [PATCH 02/43] Add to ChatMemberOwner new default fields (#645) * new: add field type ConstField * new: add const fields for ChatMemberOwner * new: better typings for get_chat_administrators * new: add tests for chat owner fields * fix: Type typing for class * enh: alias is_chat_owner for is_chat_creator --- aiogram/bot/bot.py | 3 ++- aiogram/types/chat.py | 6 +++--- aiogram/types/chat_member.py | 33 ++++++++++++++++++++++++++++----- aiogram/types/fields.py | 12 ++++++++++-- tests/test_bot.py | 9 +++++++-- tests/types/dataset.py | 9 +++++++++ 6 files changed, 59 insertions(+), 13 deletions(-) diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 66ea8af5..66af31c7 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -2129,7 +2129,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): return types.Chat(**result) async def get_chat_administrators(self, chat_id: typing.Union[base.Integer, base.String] - ) -> typing.List[types.ChatMember]: + ) -> typing.List[typing.Union[types.ChatMemberOwner, types.ChatMemberAdministrator]]: + """ Use this method to get a list of administrators in a chat. diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index 2cd19a0f..c00a3b79 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -7,7 +7,7 @@ import typing from . import base, fields from .chat_invite_link import ChatInviteLink from .chat_location import ChatLocation -from .chat_member import ChatMember +from .chat_member import ChatMember, ChatMemberAdministrator, ChatMemberOwner from .chat_permissions import ChatPermissions from .chat_photo import ChatPhoto from .input_file import InputFile @@ -470,7 +470,7 @@ class Chat(base.TelegramObject): """ return await self.bot.leave_chat(self.id) - async def get_administrators(self) -> typing.List[ChatMember]: + async def get_administrators(self) -> typing.List[typing.Union[ChatMemberOwner, ChatMemberAdministrator]]: """ Use this method to get a list of administrators in a chat. @@ -480,7 +480,7 @@ class Chat(base.TelegramObject): chat administrators except other bots. If the chat is a group or a supergroup and no administrators were appointed, only the creator will be returned. - :rtype: :obj:`typing.List[types.ChatMember]` + :rtype: :obj:`typing.List[typing.Union[types.ChatMemberOwner, types.ChatMemberAdministrator]]` """ return await self.bot.get_chat_administrators(self.id) diff --git a/aiogram/types/chat_member.py b/aiogram/types/chat_member.py index 372b3468..ecbf9d2c 100644 --- a/aiogram/types/chat_member.py +++ b/aiogram/types/chat_member.py @@ -1,6 +1,5 @@ import datetime import typing -from typing import Optional from . import base, fields from .user import User @@ -29,6 +28,8 @@ class ChatMemberStatus(helper.Helper): def is_chat_creator(cls, role: str) -> bool: return role == cls.CREATOR + is_chat_owner = is_chat_creator + @classmethod def is_chat_admin(cls, role: str) -> bool: return role in (cls.ADMINISTRATOR, cls.CREATOR) @@ -38,7 +39,7 @@ class ChatMemberStatus(helper.Helper): return role in (cls.MEMBER, cls.ADMINISTRATOR, cls.CREATOR, cls.RESTRICTED) @classmethod - def get_class_by_status(cls, status: str) -> Optional["ChatMember"]: + def get_class_by_status(cls, status: str) -> typing.Optional[typing.Type["ChatMember"]]: return { cls.OWNER: ChatMemberOwner, cls.ADMINISTRATOR: ChatMemberAdministrator, @@ -69,7 +70,9 @@ class ChatMember(base.TelegramObject): return self.user.id @classmethod - def resolve(cls, **kwargs) -> "ChatMember": + def resolve(cls, **kwargs) -> typing.Union["ChatMemberOwner", "ChatMemberAdministrator", + "ChatMemberMember", "ChatMemberRestricted", + "ChatMemberLeft", "ChatMemberBanned"]: status = kwargs.get("status") mapping = { ChatMemberStatus.OWNER: ChatMemberOwner, @@ -89,12 +92,16 @@ class ChatMember(base.TelegramObject): def to_object(cls, data: typing.Dict[str, typing.Any], conf: typing.Dict[str, typing.Any] = None - ) -> "ChatMember": - return cls.resolve(**data) + ) -> typing.Union["ChatMemberOwner", "ChatMemberAdministrator", + "ChatMemberMember", "ChatMemberRestricted", + "ChatMemberLeft", "ChatMemberBanned"]: + return cls.resolve(conf=conf, **data) def is_chat_creator(self) -> bool: return ChatMemberStatus.is_chat_creator(self.status) + is_chat_owner = is_chat_creator + def is_chat_admin(self) -> bool: return ChatMemberStatus.is_chat_admin(self.status) @@ -113,6 +120,22 @@ class ChatMemberOwner(ChatMember): custom_title: base.String = fields.Field() is_anonymous: base.Boolean = fields.Field() + # Next fields cannot be received from API but + # it useful for compatibility and cleaner code: + # >>> if member.is_admin() and member.can_promote_members: + # >>> await message.reply('You can promote me') + can_be_edited: base.Boolean = fields.ConstField(False) + can_manage_chat: base.Boolean = fields.ConstField(True) + can_post_messages: base.Boolean = fields.ConstField(True) + can_edit_messages: base.Boolean = fields.ConstField(True) + can_delete_messages: base.Boolean = fields.ConstField(True) + can_manage_voice_chats: base.Boolean = fields.ConstField(True) + can_restrict_members: base.Boolean = fields.ConstField(True) + can_promote_members: base.Boolean = fields.ConstField(True) + can_change_info: base.Boolean = fields.ConstField(True) + can_invite_users: base.Boolean = fields.ConstField(True) + can_pin_messages: base.Boolean = fields.ConstField(True) + class ChatMemberAdministrator(ChatMember): """ diff --git a/aiogram/types/fields.py b/aiogram/types/fields.py index f898fc62..11c83eab 100644 --- a/aiogram/types/fields.py +++ b/aiogram/types/fields.py @@ -2,7 +2,7 @@ import abc import datetime import weakref -__all__ = ('BaseField', 'Field', 'ListField', 'DateTimeField', 'TextField', 'ListOfLists') +__all__ = ('BaseField', 'Field', 'ListField', 'DateTimeField', 'TextField', 'ListOfLists', 'ConstField') class BaseField(metaclass=abc.ABCMeta): @@ -192,5 +192,13 @@ class TextField(Field): def deserialize(self, value, parent=None): if value is not None and not isinstance(value, str): - raise TypeError(f"Field '{self.alias}' should be str not {type(value).__name__}") + raise TypeError(f"Field {self.alias!r} should be str not {type(value).__name__!r}") return value + + +class ConstField(Field): + def __init__(self, default=None, **kwargs): + super(ConstField, self).__init__(default=default, **kwargs) + + def __set__(self, instance, value): + raise TypeError(f"Field {self.alias!r} is not mutable") diff --git a/tests/test_bot.py b/tests/test_bot.py index 61abe962..b2da7952 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -425,14 +425,19 @@ async def test_get_chat(bot: Bot): async def test_get_chat_administrators(bot: Bot): """ getChatAdministrators method test """ - from .types.dataset import CHAT, CHAT_MEMBER + from .types.dataset import CHAT, CHAT_MEMBER, CHAT_MEMBER_OWNER chat = types.Chat(**CHAT) member = types.ChatMember.resolve(**CHAT_MEMBER) + owner = types.ChatMember.resolve(**CHAT_MEMBER_OWNER) - async with FakeTelegram(message_data=[CHAT_MEMBER, CHAT_MEMBER]): + async with FakeTelegram(message_data=[CHAT_MEMBER, CHAT_MEMBER_OWNER]): result = await bot.get_chat_administrators(chat_id=chat.id) assert result[0] == member + assert result[1] == owner assert len(result) == 2 + for m in result: + assert m.is_chat_admin() + assert hasattr(m, "can_be_edited") async def test_get_chat_member_count(bot: Bot): diff --git a/tests/types/dataset.py b/tests/types/dataset.py index a14ce316..be23da9e 100644 --- a/tests/types/dataset.py +++ b/tests/types/dataset.py @@ -44,12 +44,21 @@ CHAT_MEMBER = { "user": USER, "status": "administrator", "can_be_edited": False, + "can_manage_chat": True, "can_change_info": True, "can_delete_messages": True, "can_invite_users": True, "can_restrict_members": True, "can_pin_messages": True, "can_promote_members": False, + "can_manage_voice_chats": True, + "is_anonymous": False, +} + +CHAT_MEMBER_OWNER = { + "user": USER, + "status": "creator", + "is_anonymous": False, } CONTACT = { From 580fa2e49913e68412eae51840fa73aeb2e9fa36 Mon Sep 17 00:00:00 2001 From: Ramzan Bekbulatov Date: Sun, 1 Aug 2021 00:05:21 +0300 Subject: [PATCH 03/43] Allow empty and zero parts in CallbackData (#646) * enh: allow empty parts in CallbackData * enh: allow zero parts in CallbackData * new: tests for CallbackData --- aiogram/utils/callback_data.py | 4 --- tests/test_utils/test_callback_data.py | 39 ++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 tests/test_utils/test_callback_data.py diff --git a/aiogram/utils/callback_data.py b/aiogram/utils/callback_data.py index d44fa5b9..34ba8b71 100644 --- a/aiogram/utils/callback_data.py +++ b/aiogram/utils/callback_data.py @@ -33,8 +33,6 @@ class CallbackData: raise ValueError("Prefix can't be empty") if sep in prefix: raise ValueError(f"Separator {sep!r} can't be used in prefix") - if not parts: - raise TypeError('Parts were not passed!') self.prefix = prefix self.sep = sep @@ -64,8 +62,6 @@ class CallbackData: if value is not None and not isinstance(value, str): value = str(value) - if not value: - raise ValueError(f"Value for part {part!r} can't be empty!'") if self.sep in value: raise ValueError(f"Symbol {self.sep!r} is defined as the separator and can't be used in parts' values") diff --git a/tests/test_utils/test_callback_data.py b/tests/test_utils/test_callback_data.py new file mode 100644 index 00000000..c337c527 --- /dev/null +++ b/tests/test_utils/test_callback_data.py @@ -0,0 +1,39 @@ +import pytest + +from aiogram.types import CallbackQuery +from aiogram.utils.callback_data import CallbackData + + +class TestCallbackData: + @pytest.mark.asyncio + async def test_cb(self): + cb = CallbackData('simple', 'action') + assert cb.new('x') == 'simple:x' + assert cb.new(action='y') == 'simple:y' + assert cb.new('') == 'simple:' + + assert (await cb.filter().check(CallbackQuery(data='simple:'))) == {'callback_data': {'@': 'simple', 'action': ''}} + assert (await cb.filter().check(CallbackQuery(data='simple:x'))) == {'callback_data': {'@': 'simple', 'action': 'x'}} + assert (await cb.filter(action='y').check(CallbackQuery(data='simple:x'))) is False + + @pytest.mark.asyncio + async def test_cb_double(self): + cb = CallbackData('double', 'pid', 'action') + assert cb.new('123', 'x') == 'double:123:x' + assert cb.new(pid=456, action='y') == 'double:456:y' + assert cb.new('', 'z') == 'double::z' + assert cb.new('789', '') == 'double:789:' + + assert (await cb.filter().check(CallbackQuery(data='double::'))) == {'callback_data': {'@': 'double', 'pid': '', 'action': ''}} + assert (await cb.filter().check(CallbackQuery(data='double:x:'))) == {'callback_data': {'@': 'double', 'pid': 'x', 'action': ''}} + assert (await cb.filter().check(CallbackQuery(data='double::y'))) == {'callback_data': {'@': 'double', 'pid': '', 'action': 'y'}} + assert (await cb.filter(action='x').check(CallbackQuery(data='double:123:x'))) == {'callback_data': {'@': 'double', 'pid': '123', 'action': 'x'}} + + @pytest.mark.asyncio + async def test_cb_zero(self): + cb = CallbackData('zero') + assert cb.new() == 'zero' + + assert (await cb.filter().check(CallbackQuery(data='zero'))) == {'callback_data': {'@': 'zero'}} + assert (await cb.filter().check(CallbackQuery(data='zero:'))) is False + assert (await cb.filter().check(CallbackQuery(data='bla'))) is False From aaf0b42acf4183f801a199668a7a16c0c4e59092 Mon Sep 17 00:00:00 2001 From: Ramzan Bekbulatov Date: Thu, 5 Aug 2021 22:32:30 +0300 Subject: [PATCH 04/43] Fix: method Chat.get_member_count usage without argument (#643) * fix: wrong argument passed to get_member_count() * fix: not async .close() for aiohttp.ClientResponse * fix: wrong parameter passed to InputFile * enh: implement all abstract methods for DisabledStorage * ref: style fixes --- aiogram/dispatcher/filters/builtin.py | 2 +- aiogram/dispatcher/storage.py | 20 +++++++++++++++++++- aiogram/types/base.py | 2 +- aiogram/types/bot_command.py | 2 +- aiogram/types/chat.py | 2 +- aiogram/types/input_file.py | 12 ++++++------ aiogram/types/message.py | 8 ++++---- aiogram/types/message_entity.py | 4 ++-- aiogram/types/reply_keyboard.py | 4 +++- aiogram/types/sticker.py | 2 -- aiogram/types/voice_chat_scheduled.py | 1 - 11 files changed, 38 insertions(+), 21 deletions(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index 7a21ca3f..ebd38f08 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -4,7 +4,7 @@ import typing import warnings from contextvars import ContextVar from dataclasses import dataclass, field -from typing import Any, Dict, Iterable, List, Optional, Union +from typing import Any, Dict, Iterable, Optional, Union from babel.support import LazyProxy diff --git a/aiogram/dispatcher/storage.py b/aiogram/dispatcher/storage.py index eb248e34..340b6352 100644 --- a/aiogram/dispatcher/storage.py +++ b/aiogram/dispatcher/storage.py @@ -461,7 +461,6 @@ class DisabledStorage(BaseStorage): """ Empty storage. Use it if you don't want to use Finite-State Machine """ - async def close(self): pass @@ -499,6 +498,25 @@ class DisabledStorage(BaseStorage): data: typing.Dict = None): self._warn() + async def get_bucket(self, *, + chat: typing.Union[str, int, None] = None, + user: typing.Union[str, int, None] = None, + default: typing.Optional[dict] = None) -> typing.Dict: + self._warn() + return {} + + async def set_bucket(self, *, + chat: typing.Union[str, int, None] = None, + user: typing.Union[str, int, None] = None, + bucket: typing.Dict = None): + self._warn() + + async def update_bucket(self, *, + chat: typing.Union[str, int, None] = None, + user: typing.Union[str, int, None] = None, + bucket: typing.Dict = None, **kwargs): + self._warn() + @staticmethod def _warn(): warn(f"You haven’t set any storage yet so no states and no data will be saved. \n" diff --git a/aiogram/types/base.py b/aiogram/types/base.py index 5ef774dd..3f8cda60 100644 --- a/aiogram/types/base.py +++ b/aiogram/types/base.py @@ -78,7 +78,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject): Abstract class for telegram objects """ - def __init__(self, conf: typing.Dict[str, typing.Any]=None, **kwargs: typing.Any) -> None: + def __init__(self, conf: typing.Dict[str, typing.Any] = None, **kwargs: typing.Any) -> None: """ Deserialize object diff --git a/aiogram/types/bot_command.py b/aiogram/types/bot_command.py index 39e38e4f..996d1735 100644 --- a/aiogram/types/bot_command.py +++ b/aiogram/types/bot_command.py @@ -12,4 +12,4 @@ class BotCommand(base.TelegramObject): description: base.String = fields.Field() def __init__(self, command: base.String, description: base.String): - super(BotCommand, self).__init__(command=command, description=description) \ No newline at end of file + super(BotCommand, self).__init__(command=command, description=description) diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index c00a3b79..12f6f0fd 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -497,7 +497,7 @@ class Chat(base.TelegramObject): async def get_members_count(self) -> base.Integer: """Renamed to get_member_count.""" - return await self.get_member_count(self.id) + return await self.get_member_count() async def get_member(self, user_id: base.Integer) -> ChatMember: """ diff --git a/aiogram/types/input_file.py b/aiogram/types/input_file.py index c974025a..09ebcfa9 100644 --- a/aiogram/types/input_file.py +++ b/aiogram/types/input_file.py @@ -5,7 +5,7 @@ import logging import os import secrets from pathlib import Path -from typing import Union +from typing import Union, Optional import aiohttp @@ -27,7 +27,7 @@ class InputFile(base.TelegramObject): https://core.telegram.org/bots/api#inputfile """ - def __init__(self, path_or_bytesio: Union[str, io.IOBase, Path], filename=None, conf=None): + def __init__(self, path_or_bytesio: Union[str, io.IOBase, Path, '_WebPipe'], filename=None, conf=None): """ :param path_or_bytesio: @@ -118,7 +118,7 @@ class InputFile(base.TelegramObject): if filename is None: filename = pipe.name - return cls(pipe, filename, chunk_size) + return cls(pipe, filename) def save(self, filename, chunk_size=CHUNK_SIZE): """ @@ -159,8 +159,8 @@ class _WebPipe: self.url = url self.chunk_size = chunk_size - self._session: aiohttp.ClientSession = None - self._response: aiohttp.ClientResponse = None + self._session: Optional[aiohttp.ClientSession] = None + self._response: Optional[aiohttp.ClientResponse] = None self._reader = None self._name = None @@ -182,7 +182,7 @@ class _WebPipe: async def close(self): if self._response and not self._response.closed: - await self._response.close() + self._response.close() if self._session and not self._session.closed: await self._session.close() if self._lock.locked(): diff --git a/aiogram/types/message.py b/aiogram/types/message.py index c95b14b1..8c5d5d3a 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -3039,10 +3039,10 @@ class ContentType(helper.Helper): GROUP_CHAT_CREATED = helper.Item() # group_chat_created PASSPORT_DATA = helper.Item() # passport_data PROXIMITY_ALERT_TRIGGERED = helper.Item() # proximity_alert_triggered - VOICE_CHAT_SCHEDULED = helper.Item() # voice_chat_scheduled - VOICE_CHAT_STARTED = helper.Item() # voice_chat_started - VOICE_CHAT_ENDED = helper.Item() # voice_chat_ended - VOICE_CHAT_PARTICIPANTS_INVITED = helper.Item() # voice_chat_participants_invited + VOICE_CHAT_SCHEDULED = helper.Item() # voice_chat_scheduled + VOICE_CHAT_STARTED = helper.Item() # voice_chat_started + VOICE_CHAT_ENDED = helper.Item() # voice_chat_ended + VOICE_CHAT_PARTICIPANTS_INVITED = helper.Item() # voice_chat_participants_invited UNKNOWN = helper.Item() # unknown ANY = helper.Item() # any diff --git a/aiogram/types/message_entity.py b/aiogram/types/message_entity.py index 9ee98c11..b2aaf425 100644 --- a/aiogram/types/message_entity.py +++ b/aiogram/types/message_entity.py @@ -48,12 +48,12 @@ class MessageEntity(base.TelegramObject): :return: part of text """ if sys.maxunicode == 0xFFFF: - return text[self.offset : self.offset + self.length] + return text[self.offset: self.offset + self.length] entity_text = ( text.encode("utf-16-le") if not isinstance(text, bytes) else text ) - entity_text = entity_text[self.offset * 2 : (self.offset + self.length) * 2] + entity_text = entity_text[self.offset * 2: (self.offset + self.length) * 2] return entity_text.decode("utf-16-le") @deprecated( diff --git a/aiogram/types/reply_keyboard.py b/aiogram/types/reply_keyboard.py index 8455aff6..47efdbbe 100644 --- a/aiogram/types/reply_keyboard.py +++ b/aiogram/types/reply_keyboard.py @@ -129,7 +129,9 @@ class KeyboardButton(base.TelegramObject): class ReplyKeyboardRemove(base.TelegramObject): """ - Upon receiving a message with this object, Telegram clients will remove the current custom keyboard and display the default letter-keyboard. By default, custom keyboards are displayed until a new keyboard is sent by a bot. An exception is made for one-time keyboards that are hidden immediately after the user presses a button (see ReplyKeyboardMarkup). + Upon receiving a message with this object, Telegram clients will remove the current custom keyboard + and display the default letter-keyboard. By default, custom keyboards are displayed until a new keyboard is sent by a bot. + An exception is made for one-time keyboards that are hidden immediately after the user presses a button (see ReplyKeyboardMarkup). https://core.telegram.org/bots/api#replykeyboardremove """ diff --git a/aiogram/types/sticker.py b/aiogram/types/sticker.py index ea222831..afaeb31c 100644 --- a/aiogram/types/sticker.py +++ b/aiogram/types/sticker.py @@ -41,8 +41,6 @@ class Sticker(base.TelegramObject, mixins.Downloadable): Source: https://core.telegram.org/bots/api#deletestickerfromset - :param sticker: File identifier of the sticker - :type sticker: :obj:`base.String` :return: Returns True on success :rtype: :obj:`base.Boolean` """ diff --git a/aiogram/types/voice_chat_scheduled.py b/aiogram/types/voice_chat_scheduled.py index c134eb0f..60655ebf 100644 --- a/aiogram/types/voice_chat_scheduled.py +++ b/aiogram/types/voice_chat_scheduled.py @@ -2,7 +2,6 @@ from datetime import datetime from . import base from . import fields -from .user import User class VoiceChatScheduled(base.TelegramObject): From 3aa40224a23f4cd0d8b824c07ebefceae913be07 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Thu, 5 Aug 2021 22:34:15 +0300 Subject: [PATCH 05/43] aioredis v2 support (#649) * feat: aioredis v1-v2 adapters #648 * chore: aioredis version without importlib * chore: refactor _get_redis for adapter * chore: proxy Redis methods * chore: adapter.get_redis become public * fix: add missed redis methods * chore: separate get_adapter method * chore: remove method proxy * chore: add docstrings * chore: add redis deprecations * docs: correct redis storage version * chore: encoding one style * refactor: remove redundant import * fix: int version --- aiogram/contrib/fsm_storage/redis.py | 230 ++++++++++++++++++++++----- docs/source/dispatcher/fsm.rst | 2 +- 2 files changed, 192 insertions(+), 40 deletions(-) diff --git a/aiogram/contrib/fsm_storage/redis.py b/aiogram/contrib/fsm_storage/redis.py index 5d0b762c..c8b95517 100644 --- a/aiogram/contrib/fsm_storage/redis.py +++ b/aiogram/contrib/fsm_storage/redis.py @@ -5,11 +5,13 @@ This module has redis storage for finite-state machine based on `aioredis aioredis.Redis: + """Get Redis connection.""" + pass + + def close(self): + """Grace shutdown.""" + pass + + async def wait_closed(self): + """Wait for grace shutdown finishes.""" + pass + + async def set(self, name, value, ex=None, **kwargs): + """Set the value at key ``name`` to ``value``.""" + return await self._redis.set(name, value, ex=ex, **kwargs) + + async def get(self, name, **kwargs): + """Return the value at key ``name`` or None.""" + return await self._redis.get(name, **kwargs) + + async def delete(self, *names): + """Delete one or more keys specified by ``names``""" + return await self._redis.delete(*names) + + async def keys(self, pattern, **kwargs): + """Returns a list of keys matching ``pattern``.""" + return await self._redis.keys(pattern, **kwargs) + + async def flushdb(self): + """Delete all keys in the current database.""" + return await self._redis.flushdb() + + +class AioRedisAdapterV1(AioRedisAdapterBase): + """Redis adapter for aioredis v1.""" + + async def get_redis(self) -> aioredis.Redis: + """Get Redis connection.""" + async with self._connection_lock: # to prevent race + if self._redis is None or self._redis.closed: + self._redis = await aioredis.create_redis_pool( + (self._host, self._port), + db=self._db, + password=self._password, + ssl=self._ssl, + minsize=1, + maxsize=self._pool_size, + loop=self._loop, + **self._kwargs, + ) + return self._redis + + def close(self): + async with self._connection_lock: + if self._redis and not self._redis.closed: + self._redis.close() + + async def wait_closed(self): + async with self._connection_lock: + if self._redis: + return await self._redis.wait_closed() + return True + + async def get(self, name, **kwargs): + return await self._redis.get(name, encoding="utf8", **kwargs) + + async def set(self, name, value, ex=None, **kwargs): + return await self._redis.set(name, value, expire=ex, **kwargs) + + async def keys(self, pattern, **kwargs): + """Returns a list of keys matching ``pattern``.""" + return await self._redis.keys(pattern, encoding="utf8", **kwargs) + + +class AioRedisAdapterV2(AioRedisAdapterBase): + """Redis adapter for aioredis v2.""" + + async def get_redis(self) -> aioredis.Redis: + """Get Redis connection.""" + async with self._connection_lock: # to prevent race + if self._redis is None: + self._redis = aioredis.Redis( + host=self._host, + port=self._port, + db=self._db, + password=self._password, + ssl=self._ssl, + max_connections=self._pool_size, + **self._kwargs, + ) + return self._redis + + class RedisStorage2(BaseStorage): """ Busted Redis-base storage for FSM. @@ -224,12 +356,22 @@ class RedisStorage2(BaseStorage): await dp.storage.wait_closed() """ - def __init__(self, host: str = 'localhost', port=6379, db=None, password=None, - ssl=None, pool_size=10, loop=None, prefix='fsm', - state_ttl: int = 0, - data_ttl: int = 0, - bucket_ttl: int = 0, - **kwargs): + + def __init__( + self, + host: str = "localhost", + port: int = 6379, + db: typing.Optional[int] = None, + password: typing.Optional[str] = None, + ssl: typing.Optional[bool] = None, + pool_size: int = 10, + loop: typing.Optional[asyncio.AbstractEventLoop] = None, + prefix: str = "fsm", + state_ttl: int = 0, + data_ttl: int = 0, + bucket_ttl: int = 0, + **kwargs, + ): self._host = host self._port = port self._db = db @@ -244,49 +386,59 @@ class RedisStorage2(BaseStorage): self._data_ttl = data_ttl self._bucket_ttl = bucket_ttl - self._redis: typing.Optional[aioredis.RedisConnection] = None + self._redis: typing.Optional[AioRedisAdapterBase] = None self._connection_lock = asyncio.Lock(loop=self._loop) + @deprecated("This method will be removed in aiogram v3.0. " + "You should use your own instance of Redis.", stacklevel=3) async def redis(self) -> aioredis.Redis: - """ - Get Redis connection - """ - # Use thread-safe asyncio Lock because this method without that is not safe - async with self._connection_lock: - if self._redis is None or self._redis.closed: - self._redis = await aioredis.create_redis_pool((self._host, self._port), - db=self._db, password=self._password, ssl=self._ssl, - minsize=1, maxsize=self._pool_size, - loop=self._loop, **self._kwargs) + adapter = await self._get_adapter() + return await adapter.get_redis() + + async def _get_adapter(self) -> AioRedisAdapterBase: + """Get adapter based on aioredis version.""" + if self._redis is None: + redis_version = int(aioredis.__version__.split(".")[0]) + connection_data = dict( + host=self._host, + port=self._port, + db=self._db, + password=self._password, + ssl=self._ssl, + pool_size=self._pool_size, + loop=self._loop, + **self._kwargs, + ) + if redis_version == 1: + self._redis = AioRedisAdapterV1(**connection_data) + elif redis_version == 2: + self._redis = AioRedisAdapterV2(**connection_data) return self._redis def generate_key(self, *parts): return ':'.join(self._prefix + tuple(map(str, parts))) async def close(self): - async with self._connection_lock: - if self._redis and not self._redis.closed: - self._redis.close() + if self._redis: + return self._redis.close() async def wait_closed(self): - async with self._connection_lock: - if self._redis: - return await self._redis.wait_closed() - return True + if self._redis: + return await self._redis.wait_closed() async def get_state(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, default: typing.Optional[str] = None) -> typing.Optional[str]: chat, user = self.check_address(chat=chat, user=user) key = self.generate_key(chat, user, STATE_KEY) - redis = await self.redis() - return await redis.get(key, encoding='utf8') or self.resolve_state(default) + redis = await self._get_adapter() + return await redis.get(key) or self.resolve_state(default) async def get_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, default: typing.Optional[dict] = None) -> typing.Dict: chat, user = self.check_address(chat=chat, user=user) key = self.generate_key(chat, user, STATE_DATA_KEY) - redis = await self.redis() - raw_result = await redis.get(key, encoding='utf8') + redis = await self._get_adapter() + raw_result = await redis.get(key) if raw_result: return json.loads(raw_result) return default or {} @@ -295,7 +447,7 @@ class RedisStorage2(BaseStorage): state: typing.Optional[typing.AnyStr] = None): chat, user = self.check_address(chat=chat, user=user) key = self.generate_key(chat, user, STATE_KEY) - redis = await self.redis() + redis = await self._get_adapter() if state is None: await redis.delete(key) else: @@ -305,7 +457,7 @@ class RedisStorage2(BaseStorage): data: typing.Dict = None): chat, user = self.check_address(chat=chat, user=user) key = self.generate_key(chat, user, STATE_DATA_KEY) - redis = await self.redis() + redis = await self._get_adapter() if data: await redis.set(key, json.dumps(data), expire=self._data_ttl) else: @@ -326,8 +478,8 @@ class RedisStorage2(BaseStorage): default: typing.Optional[dict] = None) -> typing.Dict: chat, user = self.check_address(chat=chat, user=user) key = self.generate_key(chat, user, STATE_BUCKET_KEY) - redis = await self.redis() - raw_result = await redis.get(key, encoding='utf8') + redis = await self._get_adapter() + raw_result = await redis.get(key) if raw_result: return json.loads(raw_result) return default or {} @@ -336,7 +488,7 @@ class RedisStorage2(BaseStorage): bucket: typing.Dict = None): chat, user = self.check_address(chat=chat, user=user) key = self.generate_key(chat, user, STATE_BUCKET_KEY) - redis = await self.redis() + redis = await self._get_adapter() if bucket: await redis.set(key, json.dumps(bucket), expire=self._bucket_ttl) else: @@ -358,13 +510,13 @@ class RedisStorage2(BaseStorage): :param full: clean DB or clean only states :return: """ - conn = await self.redis() + redis = await self._get_adapter() if full: - await conn.flushdb() + await redis.flushdb() else: - keys = await conn.keys(self.generate_key('*')) - await conn.delete(*keys) + keys = await redis.keys(self.generate_key('*')) + await redis.delete(*keys) async def get_states_list(self) -> typing.List[typing.Tuple[str, str]]: """ @@ -372,10 +524,10 @@ class RedisStorage2(BaseStorage): :return: list of tuples where first element is chat id and second is user id """ - conn = await self.redis() + redis = await self._get_adapter() result = [] - keys = await conn.keys(self.generate_key('*', '*', STATE_KEY), encoding='utf8') + keys = await redis.keys(self.generate_key('*', '*', STATE_KEY)) for item in keys: *_, chat, user, _ = item.split(':') result.append((chat, user)) diff --git a/docs/source/dispatcher/fsm.rst b/docs/source/dispatcher/fsm.rst index 1b00e81e..dc3a868e 100644 --- a/docs/source/dispatcher/fsm.rst +++ b/docs/source/dispatcher/fsm.rst @@ -19,7 +19,7 @@ Memory storage Redis storage ~~~~~~~~~~~~~ -.. autoclass:: aiogram.contrib.fsm_storage.redis.RedisStorage +.. autoclass:: aiogram.contrib.fsm_storage.redis.RedisStorage2 :show-inheritance: Mongo storage From c89bf6fbf85a7eb2ecf5ac0f1c9232c74f9712b0 Mon Sep 17 00:00:00 2001 From: Sina Ebrahimi Date: Wed, 25 Aug 2021 23:58:38 +0430 Subject: [PATCH 06/43] Fix: get the left-most ip when there is multiple (#673) X-Forwarded-For: , , get the part when there is multiple proxies or load balancers --- aiogram/dispatcher/webhook.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aiogram/dispatcher/webhook.py b/aiogram/dispatcher/webhook.py index c76ffae2..4254c72c 100644 --- a/aiogram/dispatcher/webhook.py +++ b/aiogram/dispatcher/webhook.py @@ -241,6 +241,8 @@ class WebhookRequestHandler(web.View): # For reverse proxy (nginx) forwarded_for = self.request.headers.get('X-Forwarded-For', None) if forwarded_for: + # get the left-most ip when there is multiple ips (request got through multiple proxy/load balancers) + forwarded_for = forwarded_for.split(",")[0] return forwarded_for, _check_ip(forwarded_for) # For default method From 3ee28281c46f16083535f5f9f9fe131299d9314a Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sun, 5 Sep 2021 23:46:50 +0300 Subject: [PATCH 07/43] RedisStorage major fixes + tests updates (#676) * fix: redis async close * fix: redis adapter async close * fix: aioredis pytest update * fix: tests remove unused old_storage mark * fix: tests remove redundant linebreak * fix: tests remove redundant s.redis call --- aiogram/contrib/fsm_storage/redis.py | 38 +++++++++------ tests/conftest.py | 59 +++++++++++++++++++---- tests/contrib/fsm_storage/test_storage.py | 19 +++++--- 3 files changed, 84 insertions(+), 32 deletions(-) diff --git a/aiogram/contrib/fsm_storage/redis.py b/aiogram/contrib/fsm_storage/redis.py index c8b95517..7037fadf 100644 --- a/aiogram/contrib/fsm_storage/redis.py +++ b/aiogram/contrib/fsm_storage/redis.py @@ -48,7 +48,7 @@ class RedisStorage(BaseStorage): self._loop = loop or asyncio.get_event_loop() self._kwargs = kwargs - self._redis: typing.Optional[aioredis.RedisConnection] = None + self._redis: typing.Optional["aioredis.RedisConnection"] = None self._connection_lock = asyncio.Lock(loop=self._loop) async def close(self): @@ -62,7 +62,7 @@ class RedisStorage(BaseStorage): return await self._redis.wait_closed() return True - async def redis(self) -> aioredis.RedisConnection: + async def redis(self) -> "aioredis.RedisConnection": """ Get Redis connection """ @@ -220,9 +220,9 @@ class AioRedisAdapterBase(ABC): pool_size: int = 10, loop: typing.Optional[asyncio.AbstractEventLoop] = None, prefix: str = "fsm", - state_ttl: int = 0, - data_ttl: int = 0, - bucket_ttl: int = 0, + state_ttl: typing.Optional[int] = None, + data_ttl: typing.Optional[int] = None, + bucket_ttl: typing.Optional[int] = None, **kwargs, ): self._host = host @@ -247,7 +247,7 @@ class AioRedisAdapterBase(ABC): """Get Redis connection.""" pass - def close(self): + async def close(self): """Grace shutdown.""" pass @@ -257,6 +257,8 @@ class AioRedisAdapterBase(ABC): async def set(self, name, value, ex=None, **kwargs): """Set the value at key ``name`` to ``value``.""" + if ex == 0: + ex = None return await self._redis.set(name, value, ex=ex, **kwargs) async def get(self, name, **kwargs): @@ -295,7 +297,7 @@ class AioRedisAdapterV1(AioRedisAdapterBase): ) return self._redis - def close(self): + async def close(self): async with self._connection_lock: if self._redis and not self._redis.closed: self._redis.close() @@ -310,6 +312,8 @@ class AioRedisAdapterV1(AioRedisAdapterBase): return await self._redis.get(name, encoding="utf8", **kwargs) async def set(self, name, value, ex=None, **kwargs): + if ex == 0: + ex = None return await self._redis.set(name, value, expire=ex, **kwargs) async def keys(self, pattern, **kwargs): @@ -367,9 +371,9 @@ class RedisStorage2(BaseStorage): pool_size: int = 10, loop: typing.Optional[asyncio.AbstractEventLoop] = None, prefix: str = "fsm", - state_ttl: int = 0, - data_ttl: int = 0, - bucket_ttl: int = 0, + state_ttl: typing.Optional[int] = None, + data_ttl: typing.Optional[int] = None, + bucket_ttl: typing.Optional[int] = None, **kwargs, ): self._host = host @@ -413,6 +417,9 @@ class RedisStorage2(BaseStorage): self._redis = AioRedisAdapterV1(**connection_data) elif redis_version == 2: self._redis = AioRedisAdapterV2(**connection_data) + else: + raise RuntimeError(f"Unsupported aioredis version: {redis_version}") + await self._redis.get_redis() return self._redis def generate_key(self, *parts): @@ -420,11 +427,12 @@ class RedisStorage2(BaseStorage): async def close(self): if self._redis: - return self._redis.close() + return await self._redis.close() async def wait_closed(self): if self._redis: - return await self._redis.wait_closed() + await self._redis.wait_closed() + self._redis = None async def get_state(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, default: typing.Optional[str] = None) -> typing.Optional[str]: @@ -451,7 +459,7 @@ class RedisStorage2(BaseStorage): if state is None: await redis.delete(key) else: - await redis.set(key, self.resolve_state(state), expire=self._state_ttl) + await redis.set(key, self.resolve_state(state), ex=self._state_ttl) async def set_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, data: typing.Dict = None): @@ -459,7 +467,7 @@ class RedisStorage2(BaseStorage): key = self.generate_key(chat, user, STATE_DATA_KEY) redis = await self._get_adapter() if data: - await redis.set(key, json.dumps(data), expire=self._data_ttl) + await redis.set(key, json.dumps(data), ex=self._data_ttl) else: await redis.delete(key) @@ -490,7 +498,7 @@ class RedisStorage2(BaseStorage): key = self.generate_key(chat, user, STATE_BUCKET_KEY) redis = await self._get_adapter() if bucket: - await redis.set(key, json.dumps(bucket), expire=self._bucket_ttl) + await redis.set(key, json.dumps(bucket), ex=self._bucket_ttl) else: await redis.delete(key) diff --git a/tests/conftest.py b/tests/conftest.py index 03c8dbe4..b56c7b77 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,28 +1,53 @@ +import aioredis import pytest from _pytest.config import UsageError -import aioredis.util + +try: + import aioredis.util +except ImportError: + pass def pytest_addoption(parser): - parser.addoption("--redis", default=None, - help="run tests which require redis connection") + parser.addoption( + "--redis", + default=None, + help="run tests which require redis connection", + ) def pytest_configure(config): - config.addinivalue_line("markers", "redis: marked tests require redis connection to run") + config.addinivalue_line( + "markers", + "redis: marked tests require redis connection to run", + ) def pytest_collection_modifyitems(config, items): redis_uri = config.getoption("--redis") if redis_uri is None: - skip_redis = pytest.mark.skip(reason="need --redis option with redis URI to run") + skip_redis = pytest.mark.skip( + reason="need --redis option with redis URI to run" + ) for item in items: if "redis" in item.keywords: item.add_marker(skip_redis) return + + redis_version = int(aioredis.__version__.split(".")[0]) + options = None + if redis_version == 1: + (host, port), options = aioredis.util.parse_url(redis_uri) + options.update({'host': host, 'port': port}) + elif redis_version == 2: + try: + options = aioredis.connection.parse_url(redis_uri) + except ValueError as e: + raise UsageError(f"Invalid redis URI {redis_uri!r}: {e}") + try: - address, options = aioredis.util.parse_url(redis_uri) - assert isinstance(address, tuple), "Only redis and rediss schemas are supported, eg redis://foo." + assert isinstance(options, dict), \ + "Only redis and rediss schemas are supported, eg redis://foo." except AssertionError as e: raise UsageError(f"Invalid redis URI {redis_uri!r}: {e}") @@ -30,6 +55,20 @@ def pytest_collection_modifyitems(config, items): @pytest.fixture(scope='session') def redis_options(request): redis_uri = request.config.getoption("--redis") - (host, port), options = aioredis.util.parse_url(redis_uri) - options.update({'host': host, 'port': port}) - return options + if redis_uri is None: + pytest.skip("need --redis option with redis URI to run") + return + + redis_version = int(aioredis.__version__.split(".")[0]) + if redis_version == 1: + (host, port), options = aioredis.util.parse_url(redis_uri) + options.update({'host': host, 'port': port}) + return options + + if redis_version == 2: + try: + return aioredis.connection.parse_url(redis_uri) + except ValueError as e: + raise UsageError(f"Invalid redis URI {redis_uri!r}: {e}") + + raise UsageError("Unsupported aioredis version") diff --git a/tests/contrib/fsm_storage/test_storage.py b/tests/contrib/fsm_storage/test_storage.py index 0cde2de2..2668cdab 100644 --- a/tests/contrib/fsm_storage/test_storage.py +++ b/tests/contrib/fsm_storage/test_storage.py @@ -1,12 +1,16 @@ +import aioredis import pytest - +from pytest_lazyfixture import lazy_fixture from aiogram.contrib.fsm_storage.memory import MemoryStorage -from aiogram.contrib.fsm_storage.redis import RedisStorage2, RedisStorage +from aiogram.contrib.fsm_storage.redis import RedisStorage, RedisStorage2 @pytest.fixture() @pytest.mark.redis async def redis_store(redis_options): + if int(aioredis.__version__.split(".")[0]) == 2: + pytest.skip('aioredis v2 is not supported.') + return s = RedisStorage(**redis_options) try: yield s @@ -37,9 +41,9 @@ async def memory_store(): @pytest.mark.parametrize( "store", [ - pytest.lazy_fixture('redis_store'), - pytest.lazy_fixture('redis_store2'), - pytest.lazy_fixture('memory_store'), + lazy_fixture('redis_store'), + lazy_fixture('redis_store2'), + lazy_fixture('memory_store'), ] ) class TestStorage: @@ -63,8 +67,8 @@ class TestStorage: @pytest.mark.parametrize( "store", [ - pytest.lazy_fixture('redis_store'), - pytest.lazy_fixture('redis_store2'), + lazy_fixture('redis_store'), + lazy_fixture('redis_store2'), ] ) class TestRedisStorage2: @@ -74,6 +78,7 @@ class TestRedisStorage2: assert await store.get_data(chat='1234') == {'foo': 'bar'} pool_id = id(store._redis) await store.close() + await store.wait_closed() assert await store.get_data(chat='1234') == { 'foo': 'bar'} # new pool was opened at this point assert id(store._redis) != pool_id From adaa21c0cc01647189f78089efca121ab788a906 Mon Sep 17 00:00:00 2001 From: Andrew <11490628+andrew000@users.noreply.github.com> Date: Sun, 5 Sep 2021 23:47:12 +0300 Subject: [PATCH 08/43] Add type hints to `url` & `get_mention` in `User` (#679) --- aiogram/types/user.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aiogram/types/user.py b/aiogram/types/user.py index 8263cfc2..6bde2dcd 100644 --- a/aiogram/types/user.py +++ b/aiogram/types/user.py @@ -64,10 +64,10 @@ class User(base.TelegramObject): return getattr(self, '_locale') @property - def url(self): + def url(self) -> str: return f"tg://user?id={self.id}" - def get_mention(self, name=None, as_html=None): + def get_mention(self, name: Optional[str] = None, as_html: Optional[bool] = None) -> str: if as_html is None and self.bot.parse_mode and self.bot.parse_mode.lower() == 'html': as_html = True From 82b1b1ab03c7b958a41b305259654b94a820aa7d Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sun, 5 Sep 2021 23:48:38 +0300 Subject: [PATCH 09/43] fix: decode aioredis v2 responses (#675) --- aiogram/contrib/fsm_storage/redis.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aiogram/contrib/fsm_storage/redis.py b/aiogram/contrib/fsm_storage/redis.py index 7037fadf..ce25ee07 100644 --- a/aiogram/contrib/fsm_storage/redis.py +++ b/aiogram/contrib/fsm_storage/redis.py @@ -335,6 +335,7 @@ class AioRedisAdapterV2(AioRedisAdapterBase): password=self._password, ssl=self._ssl, max_connections=self._pool_size, + decode_responses=True, **self._kwargs, ) return self._redis From 358ecc78213183adeb70809d7f3e95f1524657fb Mon Sep 17 00:00:00 2001 From: darksidecat <58224121+darksidecat@users.noreply.github.com> Date: Mon, 6 Sep 2021 00:05:52 +0300 Subject: [PATCH 10/43] Fix #665, add separate parametrs for saving to directory and file (#677) * close #665 * add backward compatibility * improve doc, codestyle * warning text update * use tmpdir fixture in tests --- aiogram/types/mixins.py | 80 +++++++++++++++++++++++++---- tests/types/test_mixins.py | 102 +++++++++++++++++++++++++++++++++++++ 2 files changed, 171 insertions(+), 11 deletions(-) create mode 100644 tests/types/test_mixins.py diff --git a/aiogram/types/mixins.py b/aiogram/types/mixins.py index 13f8412f..83c65032 100644 --- a/aiogram/types/mixins.py +++ b/aiogram/types/mixins.py @@ -1,5 +1,9 @@ import os import pathlib +from io import IOBase +from typing import Union, Optional + +from aiogram.utils.deprecated import warn_deprecated class Downloadable: @@ -7,32 +11,86 @@ class Downloadable: Mixin for files """ - async def download(self, destination=None, timeout=30, chunk_size=65536, seek=True, make_dirs=True): + async def download( + self, + destination=None, + timeout=30, + chunk_size=65536, + seek=True, + make_dirs=True, + *, + destination_dir: Optional[Union[str, pathlib.Path]] = None, + destination_file: Optional[Union[str, pathlib.Path, IOBase]] = None + ): """ Download file - :param destination: filename or instance of :class:`io.IOBase`. For e. g. :class:`io.BytesIO` + At most one of these parameters can be used: :param destination_dir:, :param destination_file: + + :param destination: deprecated, use :param destination_dir: or :param destination_file: instead :param timeout: Integer :param chunk_size: Integer :param seek: Boolean - go to start of file when downloading is finished. :param make_dirs: Make dirs if not exist + :param destination_dir: directory for saving files + :param destination_file: path to the file or instance of :class:`io.IOBase`. For e. g. :class:`io.BytesIO` :return: destination """ + if destination: + warn_deprecated( + "destination parameter is deprecated, please use destination_dir or destination_file." + ) + if destination_dir and destination_file: + raise ValueError( + "Use only one of the parameters: destination_dir or destination_file." + ) + + file, destination = await self._prepare_destination( + destination, + destination_dir, + destination_file, + make_dirs + ) + + return await self.bot.download_file( + file_path=file.file_path, + destination=destination, + timeout=timeout, + chunk_size=chunk_size, + seek=seek, + ) + + async def _prepare_destination(self, dest, destination_dir, destination_file, make_dirs): file = await self.get_file() - is_path = True - if destination is None: + if not(any((dest, destination_dir, destination_file))): destination = file.file_path - elif isinstance(destination, (str, pathlib.Path)) and os.path.isdir(destination): - destination = os.path.join(destination, file.file_path) - else: - is_path = False - if is_path and make_dirs: + elif dest: # backward compatibility + if isinstance(dest, IOBase): + return file, dest + if isinstance(dest, (str, pathlib.Path)) and os.path.isdir(dest): + destination = os.path.join(dest, file.file_path) + else: + destination = dest + + elif destination_dir: + if isinstance(destination_dir, (str, pathlib.Path)): + destination = os.path.join(destination_dir, file.file_path) + else: + raise TypeError("destination_dir must be str or pathlib.Path") + else: + if isinstance(destination_file, IOBase): + return file, destination_file + elif isinstance(destination_file, (str, pathlib.Path)): + destination = destination_file + else: + raise TypeError("destination_file must be str, pathlib.Path or io.IOBase type") + + if make_dirs and os.path.dirname(destination): os.makedirs(os.path.dirname(destination), exist_ok=True) - return await self.bot.download_file(file_path=file.file_path, destination=destination, timeout=timeout, - chunk_size=chunk_size, seek=seek) + return file, destination async def get_file(self): """ diff --git a/tests/types/test_mixins.py b/tests/types/test_mixins.py new file mode 100644 index 00000000..4327e8aa --- /dev/null +++ b/tests/types/test_mixins.py @@ -0,0 +1,102 @@ +import os +from io import BytesIO +from pathlib import Path + +import pytest + +from aiogram import Bot +from aiogram.types import File +from aiogram.types.mixins import Downloadable +from tests import TOKEN +from tests.types.dataset import FILE + +pytestmark = pytest.mark.asyncio + + +@pytest.fixture(name='bot') +async def bot_fixture(): + """ Bot fixture """ + _bot = Bot(TOKEN) + yield _bot + await _bot.session.close() + + +@pytest.fixture +def tmppath(tmpdir, request): + os.chdir(tmpdir) + yield Path(tmpdir) + os.chdir(request.config.invocation_dir) + + +@pytest.fixture +def downloadable(bot): + async def get_file(): + return File(**FILE) + + downloadable = Downloadable() + downloadable.get_file = get_file + downloadable.bot = bot + + return downloadable + + +class TestDownloadable: + async def test_download_make_dirs_false_nodir(self, tmppath, downloadable): + with pytest.raises(FileNotFoundError): + await downloadable.download(make_dirs=False) + + async def test_download_make_dirs_false_mkdir(self, tmppath, downloadable): + os.mkdir('voice') + await downloadable.download(make_dirs=False) + assert os.path.isfile(tmppath.joinpath(FILE["file_path"])) + + async def test_download_make_dirs_true(self, tmppath, downloadable): + await downloadable.download(make_dirs=True) + assert os.path.isfile(tmppath.joinpath(FILE["file_path"])) + + async def test_download_deprecation_warning(self, tmppath, downloadable): + with pytest.deprecated_call(): + await downloadable.download("test.file") + + async def test_download_destination(self, tmppath, downloadable): + with pytest.deprecated_call(): + await downloadable.download("test.file") + assert os.path.isfile(tmppath.joinpath('test.file')) + + async def test_download_destination_dir_exist(self, tmppath, downloadable): + os.mkdir("test_folder") + with pytest.deprecated_call(): + await downloadable.download("test_folder") + assert os.path.isfile(tmppath.joinpath('test_folder', FILE["file_path"])) + + async def test_download_destination_with_dir(self, tmppath, downloadable): + with pytest.deprecated_call(): + await downloadable.download(os.path.join('dir_name', 'file_name')) + assert os.path.isfile(tmppath.joinpath('dir_name', 'file_name')) + + async def test_download_destination_io_bytes(self, tmppath, downloadable): + file = BytesIO() + with pytest.deprecated_call(): + await downloadable.download(file) + assert len(file.read()) != 0 + + async def test_download_raise_value_error(self, tmppath, downloadable): + with pytest.raises(ValueError): + await downloadable.download(destination_dir="a", destination_file="b") + + async def test_download_destination_dir(self, tmppath, downloadable): + await downloadable.download(destination_dir='test_dir') + assert os.path.isfile(tmppath.joinpath('test_dir', FILE["file_path"])) + + async def test_download_destination_file(self, tmppath, downloadable): + await downloadable.download(destination_file='file_name') + assert os.path.isfile(tmppath.joinpath('file_name')) + + async def test_download_destination_file_with_dir(self, tmppath, downloadable): + await downloadable.download(destination_file=os.path.join('dir_name', 'file_name')) + assert os.path.isfile(tmppath.joinpath('dir_name', 'file_name')) + + async def test_download_io_bytes(self, tmppath, downloadable): + file = BytesIO() + await downloadable.download(destination_file=file) + assert len(file.read()) != 0 From e5cce6edf8622f2406f077ab45743bda1f4fd9fb Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 19 Sep 2021 12:38:06 +0300 Subject: [PATCH 11/43] Bump version --- aiogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index ceca7f58..3ab83d9d 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -43,5 +43,5 @@ __all__ = ( 'utils', ) -__version__ = '2.14.3' +__version__ = '2.15' __api_version__ = '5.3' From daf085ff9eb4c2812b6e86ae3d5f3ce131031246 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Wed, 6 Oct 2021 00:59:04 +0300 Subject: [PATCH 12/43] chore: show callback_query.data in logging (#714) --- aiogram/contrib/middlewares/logging.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/aiogram/contrib/middlewares/logging.py b/aiogram/contrib/middlewares/logging.py index 82c2b50a..edf53f8c 100644 --- a/aiogram/contrib/middlewares/logging.py +++ b/aiogram/contrib/middlewares/logging.py @@ -89,13 +89,15 @@ class LoggingMiddleware(BaseMiddleware): async def on_pre_process_callback_query(self, callback_query: types.CallbackQuery, data: dict): if callback_query.message: + message = callback_query.message text = (f"Received callback query [ID:{callback_query.id}] " f"from user [ID:{callback_query.from_user.id}] " - f"for message [ID:{callback_query.message.message_id}] " - f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]") + f"for message [ID:{message.message_id}] " + f"in chat [{message.chat.type}:{message.chat.id}]" + f"with data: {callback_query.data}") - if callback_query.message.from_user: - text += f" originally posted by user [ID:{callback_query.message.from_user.id}]" + if message.from_user: + text = f"{text} originally posted by user [ID:{message.from_user.id}]" self.logger.info(text) @@ -106,14 +108,16 @@ class LoggingMiddleware(BaseMiddleware): async def on_post_process_callback_query(self, callback_query, results, data: dict): if callback_query.message: + message = callback_query.message text = (f"{HANDLED_STR[bool(len(results))]} " f"callback query [ID:{callback_query.id}] " f"from user [ID:{callback_query.from_user.id}] " - f"for message [ID:{callback_query.message.message_id}] " - f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]") + f"for message [ID:{message.message_id}] " + f"in chat [{message.chat.type}:{message.chat.id}] " + f"with data: {callback_query.data}") - if callback_query.message.from_user: - text += f" originally posted by user [ID:{callback_query.message.from_user.id}]" + if message.from_user: + text = f"{text} originally posted by user [ID:{message.from_user.id}]" self.logger.info(text) From 100848b889f8d651651a4d24d369f0d2edea4334 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Wed, 6 Oct 2021 00:59:43 +0300 Subject: [PATCH 13/43] chore: reuse json util #712 (#713) --- aiogram/contrib/fsm_storage/files.py | 2 +- aiogram/utils/json.py | 24 +++++++++++++++++++++--- tests/__init__.py | 2 +- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/aiogram/contrib/fsm_storage/files.py b/aiogram/contrib/fsm_storage/files.py index 455ca3f0..c28723a9 100644 --- a/aiogram/contrib/fsm_storage/files.py +++ b/aiogram/contrib/fsm_storage/files.py @@ -1,8 +1,8 @@ -import json import pathlib import pickle import typing +from aiogram.utils import json from .memory import MemoryStorage diff --git a/aiogram/utils/json.py b/aiogram/utils/json.py index 56f122e4..cf6087ed 100644 --- a/aiogram/utils/json.py +++ b/aiogram/utils/json.py @@ -20,28 +20,46 @@ for json_lib in (RAPIDJSON, UJSON): break if mode == RAPIDJSON: + + def dump(*args, **kwargs): + return json.dump(*args, **kwargs) + + def load(*args, **kwargs): + return json.load(*args, **kwargs) + def dumps(data): return json.dumps(data, ensure_ascii=False) - def loads(data): return json.loads(data, number_mode=json.NM_NATIVE) + elif mode == UJSON: + + def dump(*args, **kwargs): + return json.dump(*args, **kwargs) + + def load(*args, **kwargs): + return json.load(*args, **kwargs) + def loads(data): return json.loads(data) - def dumps(data): return json.dumps(data, ensure_ascii=False) + else: import json + def dump(*args, **kwargs): + return json.dump(*args, **kwargs) + + def load(*args, **kwargs): + return json.load(*args, **kwargs) def dumps(data): return json.dumps(data, ensure_ascii=False) - def loads(data): return json.loads(data) diff --git a/tests/__init__.py b/tests/__init__.py index 920d5663..fe025192 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -26,7 +26,7 @@ class FakeTelegram(aresponses.ResponsesMockServer): @staticmethod def parse_data(message_data): - import json + from aiogram.utils import json from aiogram.utils.payload import _normalize _body = '{"ok":true,"result":' + json.dumps(_normalize(message_data)) + '}' From a57f9cfc70280de49dcff1c62f1ce8fd317c40ed Mon Sep 17 00:00:00 2001 From: Gabben <43146729+gabbhack@users.noreply.github.com> Date: Sat, 9 Oct 2021 20:22:06 +0300 Subject: [PATCH 14/43] Fixed send_copy: added caption to send_voice (#722) --- aiogram/types/message.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 8c5d5d3a..403ef954 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -2899,7 +2899,9 @@ class Message(base.TelegramObject): video_note=self.video_note.file_id, **kwargs ) elif self.voice: - return await self.bot.send_voice(voice=self.voice.file_id, **kwargs) + return await self.bot.send_voice( + voice=self.voice.file_id, caption=text, **kwargs + ) elif self.contact: kwargs.pop("parse_mode") return await self.bot.send_contact( From 57aea755a2bb2d5efe8122b88460f72d26b9e7b6 Mon Sep 17 00:00:00 2001 From: Ramzan Bekbulatov Date: Sun, 7 Nov 2021 02:24:05 +0300 Subject: [PATCH 15/43] enh: remove unnecessarily strict condition in send_media_group (#742) * enh: remove unneccesarily strict condition in send_media_group * enh: return check but less strict * fix: remove trailing whitespace --- aiogram/bot/bot.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 66af31c7..304b000d 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -1005,9 +1005,9 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): if isinstance(media, list): media = types.MediaGroup(media) - # check MediaGroup quantity - if not 2 <= len(media.media) <= 10: - raise ValidationError("Media group must include 2-10 items") + # Check MediaGroup quantity + if not (1 <= len(media.media) <= 10): + raise ValidationError("Media group must include 2-10 items as written in docs, but also it works with 1 element") files = dict(media.get_files()) From 204a2a1ec00c91eff79995201b2d7fa3853a7ab2 Mon Sep 17 00:00:00 2001 From: Ramzan Bekbulatov Date: Sun, 7 Nov 2021 02:26:59 +0300 Subject: [PATCH 16/43] enh: change invisible symbol in hide_link (#738) There is a problem with the current symbol on android devices. When trying to change the text of a post in a channel, the hidden link often disappeared when editing. Word joiner symbol seems to be more stable in this case. https://en.wikipedia.org/wiki/Word_joiner --- aiogram/utils/markdown.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/utils/markdown.py b/aiogram/utils/markdown.py index da27bc39..75f5fea0 100644 --- a/aiogram/utils/markdown.py +++ b/aiogram/utils/markdown.py @@ -247,4 +247,4 @@ def hide_link(url: str) -> str: :param url: :return: """ - return f'' + return f'' From b98ec3efad66d4357057c22e71e416cbb5a8f9b9 Mon Sep 17 00:00:00 2001 From: darksidecat <58224121+darksidecat@users.noreply.github.com> Date: Sun, 7 Nov 2021 01:28:12 +0200 Subject: [PATCH 17/43] add `Bot.download_file` aliases ability to save files to a directory and automatically create directories (#694) * add destination_dir and make_dirs parameters to bot download aliases * add the ability to save files to a directory with path completion based on file_path, * add an option to automatically create directories in the file path * Downloadable mixin uses directory creation parameter in bot methods --- aiogram/bot/base.py | 36 ++++++++--- aiogram/bot/bot.py | 23 +++++-- aiogram/types/mixins.py | 7 +-- tests/test_bot/test_bot_download_file.py | 78 ++++++++++++++++++++++++ 4 files changed, 127 insertions(+), 17 deletions(-) create mode 100644 tests/test_bot/test_bot_download_file.py diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py index 07e44c1c..1e9bcc15 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -1,6 +1,8 @@ import asyncio import contextlib import io +import os +import pathlib import ssl import typing import warnings @@ -208,28 +210,48 @@ class BaseBot: return await api.make_request(self.session, self.server, self.__token, method, data, files, proxy=self.proxy, proxy_auth=self.proxy_auth, timeout=self.timeout, **kwargs) - async def download_file(self, file_path: base.String, - destination: Optional[base.InputFile] = None, - timeout: Optional[base.Integer] = sentinel, - chunk_size: Optional[base.Integer] = 65536, - seek: Optional[base.Boolean] = True) -> Union[io.BytesIO, io.FileIO]: + async def download_file( + self, + file_path: base.String, + destination: Optional[Union[base.InputFile, pathlib.Path]] = None, + timeout: Optional[base.Integer] = sentinel, + chunk_size: Optional[base.Integer] = 65536, + seek: Optional[base.Boolean] = True, + destination_dir: Optional[Union[str, pathlib.Path]] = None, + make_dirs: Optional[base.Boolean] = True, + ) -> Union[io.BytesIO, io.FileIO]: """ - Download file by file_path to destination + Download file by file_path to destination file or directory if You want to automatically create destination (:class:`io.BytesIO`) use default value of destination and handle result of this method. + At most one of these parameters can be used: :param destination:, :param destination_dir: + :param file_path: file path on telegram server (You can get it from :obj:`aiogram.types.File`) :type file_path: :obj:`str` :param destination: filename or instance of :class:`io.IOBase`. For e. g. :class:`io.BytesIO` :param timeout: Integer :param chunk_size: Integer :param seek: Boolean - go to start of file when downloading is finished. + :param destination_dir: directory for saving files + :param make_dirs: Make dirs if not exist :return: destination """ - if destination is None: + if destination and destination_dir: + raise ValueError( + "Use only one of the parameters:destination or destination_dir." + ) + + if destination is None and destination_dir is None: destination = io.BytesIO() + elif destination_dir: + destination = os.path.join(destination_dir, file_path) + + if make_dirs and not isinstance(destination, io.IOBase) and os.path.dirname(destination): + os.makedirs(os.path.dirname(destination), exist_ok=True) + url = self.get_file_url(file_path) dest = destination if isinstance(destination, io.IOBase) else open(destination, 'wb') diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 304b000d..22b1c91c 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -1,6 +1,7 @@ from __future__ import annotations import datetime +import pathlib import typing import warnings @@ -43,25 +44,37 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): if hasattr(self, '_me'): delattr(self, '_me') - async def download_file_by_id(self, file_id: base.String, destination=None, - timeout: base.Integer = 30, chunk_size: base.Integer = 65536, - seek: base.Boolean = True): + async def download_file_by_id( + self, + file_id: base.String, + destination: typing.Optional[base.InputFile, pathlib.Path] = None, + timeout: base.Integer = 30, + chunk_size: base.Integer = 65536, + seek: base.Boolean = True, + destination_dir: typing.Optional[typing.Union[str, pathlib.Path]] = None, + make_dirs: typing.Optional[base.Boolean] = True, + ): """ - Download file by file_id to destination + Download file by file_id to destination file or directory if You want to automatically create destination (:class:`io.BytesIO`) use default value of destination and handle result of this method. + At most one of these parameters can be used: :param destination:, :param destination_dir: + :param file_id: str :param destination: filename or instance of :class:`io.IOBase`. For e. g. :class:`io.BytesIO` :param timeout: int :param chunk_size: int :param seek: bool - go to start of file when downloading is finished + :param destination_dir: directory for saving files + :param make_dirs: Make dirs if not exist :return: destination """ file = await self.get_file(file_id) return await self.download_file(file_path=file.file_path, destination=destination, - timeout=timeout, chunk_size=chunk_size, seek=seek) + timeout=timeout, chunk_size=chunk_size, seek=seek, + destination_dir=destination_dir, make_dirs=make_dirs) # === Getting updates === # https://core.telegram.org/bots/api#getting-updates diff --git a/aiogram/types/mixins.py b/aiogram/types/mixins.py index 83c65032..7d06d4c4 100644 --- a/aiogram/types/mixins.py +++ b/aiogram/types/mixins.py @@ -49,7 +49,6 @@ class Downloadable: destination, destination_dir, destination_file, - make_dirs ) return await self.bot.download_file( @@ -58,9 +57,10 @@ class Downloadable: timeout=timeout, chunk_size=chunk_size, seek=seek, + make_dirs=make_dirs ) - async def _prepare_destination(self, dest, destination_dir, destination_file, make_dirs): + async def _prepare_destination(self, dest, destination_dir, destination_file): file = await self.get_file() if not(any((dest, destination_dir, destination_file))): @@ -87,9 +87,6 @@ class Downloadable: else: raise TypeError("destination_file must be str, pathlib.Path or io.IOBase type") - if make_dirs and os.path.dirname(destination): - os.makedirs(os.path.dirname(destination), exist_ok=True) - return file, destination async def get_file(self): diff --git a/tests/test_bot/test_bot_download_file.py b/tests/test_bot/test_bot_download_file.py new file mode 100644 index 00000000..75710fcc --- /dev/null +++ b/tests/test_bot/test_bot_download_file.py @@ -0,0 +1,78 @@ +import os +from io import BytesIO +from pathlib import Path + +import pytest + +from aiogram import Bot +from aiogram.types import File +from tests import TOKEN +from tests.types.dataset import FILE + +pytestmark = pytest.mark.asyncio + + +@pytest.fixture(name='bot') +async def bot_fixture(): + async def get_file(): + return File(**FILE) + + """ Bot fixture """ + _bot = Bot(TOKEN) + _bot.get_file = get_file + yield _bot + await _bot.session.close() + + +@pytest.fixture +def file(): + return File(**FILE) + + +@pytest.fixture +def tmppath(tmpdir, request): + os.chdir(tmpdir) + yield Path(tmpdir) + os.chdir(request.config.invocation_dir) + + +class TestBotDownload: + async def test_download_file(self, tmppath, bot, file): + f = await bot.download_file(file_path=file.file_path) + assert len(f.read()) != 0 + + async def test_download_file_destination(self, tmppath, bot, file): + await bot.download_file(file_path=file.file_path, destination="test.file") + assert os.path.isfile(tmppath.joinpath('test.file')) + + async def test_download_file_destination_with_dir(self, tmppath, bot, file): + await bot.download_file(file_path=file.file_path, + destination=os.path.join('dir_name', 'file_name')) + assert os.path.isfile(tmppath.joinpath('dir_name', 'file_name')) + + async def test_download_file_destination_raise_file_not_found(self, tmppath, bot, file): + with pytest.raises(FileNotFoundError): + await bot.download_file(file_path=file.file_path, + destination=os.path.join('dir_name', 'file_name'), + make_dirs=False) + + async def test_download_file_destination_io_bytes(self, tmppath, bot, file): + f = BytesIO() + await bot.download_file(file_path=file.file_path, + destination=f) + assert len(f.read()) != 0 + + async def test_download_file_raise_value_error(self, tmppath, bot, file): + with pytest.raises(ValueError): + await bot.download_file(file_path=file.file_path, destination="a", destination_dir="b") + + async def test_download_file_destination_dir(self, tmppath, bot, file): + await bot.download_file(file_path=file.file_path, destination_dir='test_dir') + assert os.path.isfile(tmppath.joinpath('test_dir', file.file_path)) + + async def test_download_file_destination_dir_raise_file_not_found(self, tmppath, bot, file): + with pytest.raises(FileNotFoundError): + await bot.download_file(file_path=file.file_path, + destination_dir='test_dir', + make_dirs=False) + assert os.path.isfile(tmppath.joinpath('test_dir', file.file_path)) From b190bbba1915ed3b7f311a780f34723ebd6b5acd Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 7 Nov 2021 01:39:51 +0200 Subject: [PATCH 18/43] Dev 2.x api 5.4 (#741) * Added support of Bot API 5.4 * Bump version * Added aliases for ChatJoinRequest object * Create aiohttp session inside async function * Try to fix compatibility with aiohttp 3.8 * Fixed compatibility with Python 3.10 --- README.md | 2 +- aiogram/__init__.py | 4 +- aiogram/bot/api.py | 4 +- aiogram/bot/base.py | 35 +++++-- aiogram/bot/bot.py | 75 ++++++++++++++- aiogram/contrib/fsm_storage/redis.py | 66 +++++++------ aiogram/contrib/middlewares/environment.py | 2 +- aiogram/contrib/middlewares/logging.py | 13 ++- aiogram/dispatcher/dispatcher.py | 103 ++++++++++++++++----- aiogram/dispatcher/webhook.py | 8 +- aiogram/types/__init__.py | 2 + aiogram/types/chat.py | 11 +++ aiogram/types/chat_invite_link.py | 3 + aiogram/types/chat_join_request.py | 33 +++++++ aiogram/types/reply_keyboard.py | 7 +- aiogram/types/update.py | 3 + aiogram/utils/executor.py | 5 +- docs/source/index.rst | 2 +- examples/proxy_and_emojize.py | 2 +- requirements.txt | 6 +- tests/test_bot/test_session.py | 5 +- tests/types/test_mixins.py | 2 +- 22 files changed, 302 insertions(+), 91 deletions(-) create mode 100644 aiogram/types/chat_join_request.py diff --git a/README.md b/README.md index fca118a0..2dd5394a 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![PyPi status](https://img.shields.io/pypi/status/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) [![Downloads](https://img.shields.io/pypi/dm/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) [![Supported python versions](https://img.shields.io/pypi/pyversions/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) -[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-5.3-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api) +[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-5.4-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api) [![Documentation Status](https://img.shields.io/readthedocs/aiogram?style=flat-square)](http://docs.aiogram.dev/en/latest/?badge=latest) [![Github issues](https://img.shields.io/github/issues/aiogram/aiogram.svg?style=flat-square)](https://github.com/aiogram/aiogram/issues) [![MIT License](https://img.shields.io/pypi/l/aiogram.svg?style=flat-square)](https://opensource.org/licenses/MIT) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 3ab83d9d..a9581081 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -43,5 +43,5 @@ __all__ = ( 'utils', ) -__version__ = '2.15' -__api_version__ = '5.3' +__version__ = '2.16' +__api_version__ = '5.4' diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 1bf00d47..b0bb790c 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -189,7 +189,7 @@ class Methods(Helper): """ Helper for Telegram API Methods listed on https://core.telegram.org/bots/api - List is updated to Bot API 5.3 + List is updated to Bot API 5.4 """ mode = HelperMode.lowerCamelCase @@ -235,6 +235,8 @@ class Methods(Helper): CREATE_CHAT_INVITE_LINK = Item() # createChatInviteLink EDIT_CHAT_INVITE_LINK = Item() # editChatInviteLink REVOKE_CHAT_INVITE_LINK = Item() # revokeChatInviteLink + APPROVE_CHAT_JOIN_REQUEST = Item() # approveChatJoinRequest + DECLINE_CHAT_JOIN_REQUEST = Item() # declineChatJoinRequest SET_CHAT_PHOTO = Item() # setChatPhoto DELETE_CHAT_PHOTO = Item() # deleteChatPhoto SET_CHAT_TITLE = Item() # setChatTitle diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py index 1e9bcc15..600152f6 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -107,10 +107,9 @@ class BaseBot: self.parse_mode = parse_mode - def get_new_session(self) -> aiohttp.ClientSession: + async def get_new_session(self) -> aiohttp.ClientSession: return aiohttp.ClientSession( - connector=self._connector_class(**self._connector_init, loop=self._main_loop), - loop=self._main_loop, + connector=self._connector_class(**self._connector_init), json_serialize=json.dumps ) @@ -118,10 +117,25 @@ class BaseBot: def loop(self) -> Optional[asyncio.AbstractEventLoop]: return self._main_loop - @property - def session(self) -> Optional[aiohttp.ClientSession]: + async def get_session(self) -> Optional[aiohttp.ClientSession]: if self._session is None or self._session.closed: - self._session = self.get_new_session() + self._session = await self.get_new_session() + + if not self._session._loop.is_running(): # NOQA + # Hate `aiohttp` devs because it juggles event-loops and breaks already opened session + # So... when we detect a broken session need to fix it by re-creating it + # @asvetlov, if you read this, please no more juggle event-loop inside aiohttp, it breaks the brain. + await self._session.close() + self._session = await self.get_new_session() + + return self._session + + @property + @deprecated( + reason="Client session should be created inside async function, use `await bot.get_session()` instead", + stacklevel=3, + ) + def session(self) -> Optional[aiohttp.ClientSession]: return self._session @staticmethod @@ -187,7 +201,8 @@ class BaseBot: """ Close all client sessions """ - await self.session.close() + if self._session: + await self._session.close() async def request(self, method: base.String, data: Optional[Dict] = None, @@ -207,7 +222,8 @@ class BaseBot: :rtype: Union[List, Dict] :raise: :obj:`aiogram.exceptions.TelegramApiError` """ - return await api.make_request(self.session, self.server, self.__token, method, data, files, + + return await api.make_request(await self.get_session(), self.server, self.__token, method, data, files, proxy=self.proxy, proxy_auth=self.proxy_auth, timeout=self.timeout, **kwargs) async def download_file( @@ -255,7 +271,8 @@ class BaseBot: url = self.get_file_url(file_path) dest = destination if isinstance(destination, io.IOBase) else open(destination, 'wb') - async with self.session.get(url, timeout=timeout, proxy=self.proxy, proxy_auth=self.proxy_auth) as response: + session = await self.get_session() + async with session.get(url, timeout=timeout, proxy=self.proxy, proxy_auth=self.proxy_auth) as response: while True: chunk = await response.content.read(chunk_size) if not chunk: diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 22b1c91c..436b83a4 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -1853,6 +1853,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): expire_date: typing.Union[base.Integer, datetime.datetime, datetime.timedelta, None] = None, member_limit: typing.Optional[base.Integer] = None, + name: typing.Optional[base.String] = None, + creates_join_request: typing.Optional[base.Boolean] = None, ) -> types.ChatInviteLink: """ Use this method to create an additional invite link for a chat. @@ -1874,6 +1876,13 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): simultaneously after joining the chat via this invite link; 1-99999 :type member_limit: :obj:`typing.Optional[base.Integer]` + :param name: Invite link name; 0-32 characters + :type name: :obj:`typing.Optional[base.String]` + + :param creates_join_request: True, if users joining the chat via the link need + to be approved by chat administrators. If True, member_limit can't be specified + :type creates_join_request: :obj:`typing.Optional[base.Boolean]` + :return: the new invite link as ChatInviteLink object. :rtype: :obj:`types.ChatInviteLink` """ @@ -1889,6 +1898,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): expire_date: typing.Union[base.Integer, datetime.datetime, datetime.timedelta, None] = None, member_limit: typing.Optional[base.Integer] = None, + name: typing.Optional[base.String] = None, + creates_join_request: typing.Optional[base.Boolean] = None, ) -> types.ChatInviteLink: """ Use this method to edit a non-primary invite link created by the bot. @@ -1912,6 +1923,14 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): simultaneously after joining the chat via this invite link; 1-99999 :type member_limit: :obj:`typing.Optional[base.Integer]` + :param name: Invite link name; 0-32 characters + :type name: :obj:`typing.Optional[base.String]` + + :param creates_join_request: True, if users joining the chat via the link need + to be approved by chat administrators. If True, member_limit can't be specified + :type creates_join_request: :obj:`typing.Optional[base.Boolean]` + + :return: edited invite link as a ChatInviteLink object. """ expire_date = prepare_arg(expire_date) @@ -1942,6 +1961,59 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): result = await self.request(api.Methods.REVOKE_CHAT_INVITE_LINK, payload) return types.ChatInviteLink(**result) + async def approve_chat_join_request(self, + chat_id: typing.Union[base.Integer, base.String], + user_id: base.Integer, + ) -> base.Boolean: + """ + Use this method to approve a chat join request. + The bot must be an administrator in the chat for this to work and must have the + can_invite_users administrator right. + + Returns True on success. + + Source: https://core.telegram.org/bots/api#approvechatjoinrequest + + :param chat_id: Unique identifier for the target chat or username of the target channel + (in the format @channelusername) + :type chat_id: typing.Union[base.Integer, base.String] + + :param user_id: Unique identifier of the target user + :type user_id: base.Integer + + :return: + """ + payload = generate_payload(**locals()) + + return await self.request(api.Methods.APPROVE_CHAT_JOIN_REQUEST, payload) + + async def decline_chat_join_request(self, + chat_id: typing.Union[base.Integer, base.String], + user_id: base.Integer, + ) -> base.Boolean: + """ + Use this method to decline a chat join request. + The bot must be an administrator in the chat for this to work and + must have the can_invite_users administrator right. + Returns True on success. + + Returns True on success. + + Source: https://core.telegram.org/bots/api#declinechatjoinrequest + + :param chat_id: Unique identifier for the target chat or username of the target channel + (in the format @channelusername) + :type chat_id: typing.Union[base.Integer, base.String] + + :param user_id: Unique identifier of the target user + :type user_id: base.Integer + + :return: + """ + payload = generate_payload(**locals()) + + return await self.request(api.Methods.DECLINE_CHAT_JOIN_REQUEST, payload) + async def set_chat_photo(self, chat_id: typing.Union[base.Integer, base.String], photo: base.InputFile) -> base.Boolean: """ @@ -2142,7 +2214,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): return types.Chat(**result) async def get_chat_administrators(self, chat_id: typing.Union[base.Integer, base.String] - ) -> typing.List[typing.Union[types.ChatMemberOwner, types.ChatMemberAdministrator]]: + ) -> typing.List[ + typing.Union[types.ChatMemberOwner, types.ChatMemberAdministrator]]: """ Use this method to get a list of administrators in a chat. diff --git a/aiogram/contrib/fsm_storage/redis.py b/aiogram/contrib/fsm_storage/redis.py index ce25ee07..87d76374 100644 --- a/aiogram/contrib/fsm_storage/redis.py +++ b/aiogram/contrib/fsm_storage/redis.py @@ -37,6 +37,7 @@ class RedisStorage(BaseStorage): await dp.storage.wait_closed() """ + @deprecated("`RedisStorage` will be removed in aiogram v3.0. " "Use `RedisStorage2` instead.", stacklevel=3) def __init__(self, host='localhost', port=6379, db=None, password=None, ssl=None, loop=None, **kwargs): @@ -45,11 +46,10 @@ class RedisStorage(BaseStorage): self._db = db self._password = password self._ssl = ssl - self._loop = loop or asyncio.get_event_loop() self._kwargs = kwargs self._redis: typing.Optional["aioredis.RedisConnection"] = None - self._connection_lock = asyncio.Lock(loop=self._loop) + self._connection_lock = asyncio.Lock() async def close(self): async with self._connection_lock: @@ -71,7 +71,6 @@ class RedisStorage(BaseStorage): if self._redis is None or self._redis.closed: self._redis = await aioredis.create_connection((self._host, self._port), db=self._db, password=self._password, ssl=self._ssl, - loop=self._loop, **self._kwargs) return self._redis @@ -210,20 +209,21 @@ class RedisStorage(BaseStorage): class AioRedisAdapterBase(ABC): """Base aioredis adapter class.""" + def __init__( - self, - host: str = "localhost", - port: int = 6379, - db: typing.Optional[int] = None, - password: typing.Optional[str] = None, - ssl: typing.Optional[bool] = None, - pool_size: int = 10, - loop: typing.Optional[asyncio.AbstractEventLoop] = None, - prefix: str = "fsm", - state_ttl: typing.Optional[int] = None, - data_ttl: typing.Optional[int] = None, - bucket_ttl: typing.Optional[int] = None, - **kwargs, + self, + host: str = "localhost", + port: int = 6379, + db: typing.Optional[int] = None, + password: typing.Optional[str] = None, + ssl: typing.Optional[bool] = None, + pool_size: int = 10, + loop: typing.Optional[asyncio.AbstractEventLoop] = None, + prefix: str = "fsm", + state_ttl: typing.Optional[int] = None, + data_ttl: typing.Optional[int] = None, + bucket_ttl: typing.Optional[int] = None, + **kwargs, ): self._host = host self._port = port @@ -231,7 +231,6 @@ class AioRedisAdapterBase(ABC): self._password = password self._ssl = ssl self._pool_size = pool_size - self._loop = loop or asyncio.get_event_loop() self._kwargs = kwargs self._prefix = (prefix,) @@ -240,7 +239,7 @@ class AioRedisAdapterBase(ABC): self._bucket_ttl = bucket_ttl self._redis: typing.Optional["aioredis.Redis"] = None - self._connection_lock = asyncio.Lock(loop=self._loop) + self._connection_lock = asyncio.Lock() @abstractmethod async def get_redis(self) -> aioredis.Redis: @@ -292,7 +291,6 @@ class AioRedisAdapterV1(AioRedisAdapterBase): ssl=self._ssl, minsize=1, maxsize=self._pool_size, - loop=self._loop, **self._kwargs, ) return self._redis @@ -363,19 +361,19 @@ class RedisStorage2(BaseStorage): """ def __init__( - self, - host: str = "localhost", - port: int = 6379, - db: typing.Optional[int] = None, - password: typing.Optional[str] = None, - ssl: typing.Optional[bool] = None, - pool_size: int = 10, - loop: typing.Optional[asyncio.AbstractEventLoop] = None, - prefix: str = "fsm", - state_ttl: typing.Optional[int] = None, - data_ttl: typing.Optional[int] = None, - bucket_ttl: typing.Optional[int] = None, - **kwargs, + self, + host: str = "localhost", + port: int = 6379, + db: typing.Optional[int] = None, + password: typing.Optional[str] = None, + ssl: typing.Optional[bool] = None, + pool_size: int = 10, + loop: typing.Optional[asyncio.AbstractEventLoop] = None, + prefix: str = "fsm", + state_ttl: typing.Optional[int] = None, + data_ttl: typing.Optional[int] = None, + bucket_ttl: typing.Optional[int] = None, + **kwargs, ): self._host = host self._port = port @@ -383,7 +381,6 @@ class RedisStorage2(BaseStorage): self._password = password self._ssl = ssl self._pool_size = pool_size - self._loop = loop or asyncio.get_event_loop() self._kwargs = kwargs self._prefix = (prefix,) @@ -392,7 +389,7 @@ class RedisStorage2(BaseStorage): self._bucket_ttl = bucket_ttl self._redis: typing.Optional[AioRedisAdapterBase] = None - self._connection_lock = asyncio.Lock(loop=self._loop) + self._connection_lock = asyncio.Lock() @deprecated("This method will be removed in aiogram v3.0. " "You should use your own instance of Redis.", stacklevel=3) @@ -411,7 +408,6 @@ class RedisStorage2(BaseStorage): password=self._password, ssl=self._ssl, pool_size=self._pool_size, - loop=self._loop, **self._kwargs, ) if redis_version == 1: diff --git a/aiogram/contrib/middlewares/environment.py b/aiogram/contrib/middlewares/environment.py index f6ad56dd..976ed886 100644 --- a/aiogram/contrib/middlewares/environment.py +++ b/aiogram/contrib/middlewares/environment.py @@ -16,7 +16,7 @@ class EnvironmentMiddleware(BaseMiddleware): data.update( bot=dp.bot, dispatcher=dp, - loop=dp.loop or asyncio.get_event_loop() + loop=asyncio.get_event_loop() ) if self.context: data.update(self.context) diff --git a/aiogram/contrib/middlewares/logging.py b/aiogram/contrib/middlewares/logging.py index edf53f8c..7f21eb41 100644 --- a/aiogram/contrib/middlewares/logging.py +++ b/aiogram/contrib/middlewares/logging.py @@ -1,6 +1,5 @@ -import time - import logging +import time from aiogram import types from aiogram.dispatcher.middlewares import BaseMiddleware @@ -184,6 +183,16 @@ class LoggingMiddleware(BaseMiddleware): self.logger.debug(f"{HANDLED_STR[bool(len(results))]} chat_member " f"for user [ID:{chat_member_update.from_user.id}]") + async def on_pre_chat_join_request(self, chat_join_request, data): + self.logger.info(f"Received chat join request " + f"for user [ID:{chat_join_request.from_user.id}] " + f"in chat [ID:{chat_join_request.chat.id}]") + + async def on_post_chat_join_request(self, chat_join_request, results, data): + self.logger.debug(f"{HANDLED_STR[bool(len(results))]} chat join request " + f"for user [ID:{chat_join_request.from_user.id}] " + f"in chat [ID:{chat_join_request.chat.id}]") + class LoggingFilter(logging.Filter): """ diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index 1e36f202..e6160b3e 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -56,8 +56,6 @@ class Dispatcher(DataMixin, ContextInstanceMixin): filters_factory = FiltersFactory(self) self.bot: Bot = bot - if loop is not None: - _ensure_loop(loop) self._main_loop = loop self.storage = storage self.run_tasks_by_default = run_tasks_by_default @@ -80,6 +78,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin): self.poll_answer_handlers = Handler(self, middleware_key='poll_answer') self.my_chat_member_handlers = Handler(self, middleware_key='my_chat_member') self.chat_member_handlers = Handler(self, middleware_key='chat_member') + self.chat_join_request_handlers = Handler(self, middleware_key='chat_join_request') self.errors_handlers = Handler(self, once=False, middleware_key='error') self.middleware = MiddlewareManager(self) @@ -103,10 +102,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin): @property def _close_waiter(self) -> "asyncio.Future": if self._dispatcher_close_waiter is None: - if self._main_loop is not None: - self._dispatcher_close_waiter = self._main_loop.create_future() - else: - self._dispatcher_close_waiter = asyncio.get_event_loop().create_future() + self._dispatcher_close_waiter = asyncio.get_event_loop().create_future() return self._dispatcher_close_waiter def _setup_filters(self): @@ -159,13 +155,14 @@ class Dispatcher(DataMixin, ContextInstanceMixin): self.errors_handlers, ]) filters_factory.bind(AdminFilter, event_handlers=[ - self.message_handlers, + self.message_handlers, self.edited_message_handlers, - self.channel_post_handlers, + self.channel_post_handlers, self.edited_channel_post_handlers, - self.callback_query_handlers, + self.callback_query_handlers, self.inline_query_handlers, self.chat_member_handlers, + self.chat_join_request_handlers, ]) filters_factory.bind(IDFilter, event_handlers=[ self.message_handlers, @@ -176,6 +173,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin): self.inline_query_handlers, self.chat_member_handlers, self.my_chat_member_handlers, + self.chat_join_request_handlers, ]) filters_factory.bind(IsReplyFilter, event_handlers=[ self.message_handlers, @@ -202,7 +200,8 @@ class Dispatcher(DataMixin, ContextInstanceMixin): self.edited_channel_post_handlers, self.callback_query_handlers, self.my_chat_member_handlers, - self.chat_member_handlers + self.chat_member_handlers, + self.chat_join_request_handlers, ]) filters_factory.bind(MediaGroupFilter, event_handlers=[ self.message_handlers, @@ -305,6 +304,11 @@ class Dispatcher(DataMixin, ContextInstanceMixin): types.ChatMemberUpdated.set_current(update.chat_member) types.User.set_current(update.chat_member.from_user) return await self.chat_member_handlers.notify(update.chat_member) + if update.chat_join_request: + types.ChatJoinRequest.set_current(update.chat_join_request) + types.Chat.set_current(update.chat_join_request.chat) + types.User.set_current(update.chat_join_request.from_user) + return await self.chat_join_request_handlers.notify(update.chat_join_request) except Exception as e: err = await self.errors_handlers.notify(update, e) if err: @@ -326,10 +330,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin): return await self.bot.delete_webhook() def _loop_create_task(self, coro): - if self._main_loop is None: - return asyncio.create_task(coro) - _ensure_loop(self._main_loop) - return self._main_loop.create_task(coro) + return asyncio.create_task(coro) async def start_polling(self, timeout=20, @@ -394,7 +395,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin): log.debug(f"Received {len(updates)} updates.") offset = updates[-1].update_id + 1 - self._loop_create_task(self._process_polling_updates(updates, fast)) + asyncio.create_task(self._process_polling_updates(updates, fast)) if relax: await asyncio.sleep(relax) @@ -980,14 +981,14 @@ class Dispatcher(DataMixin, ContextInstanceMixin): :param run_task: run callback in task (no wait results) :param kwargs: """ - + def decorator(callback): self.register_poll_handler(callback, *custom_filters, run_task=run_task, **kwargs) return callback return decorator - + def register_poll_answer_handler(self, callback, *custom_filters, run_task=None, **kwargs): """ Register handler for poll_answer @@ -1007,7 +1008,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin): *custom_filters, **kwargs) self.poll_answer_handlers.register(self._wrap_async_task(callback, run_task), filters_set) - + def poll_answer_handler(self, *custom_filters, run_task=None, **kwargs): """ Decorator for poll_answer handler @@ -1026,7 +1027,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin): def decorator(callback): self.register_poll_answer_handler(callback, *custom_filters, run_task=run_task, - **kwargs) + **kwargs) return callback return decorator @@ -1143,6 +1144,62 @@ class Dispatcher(DataMixin, ContextInstanceMixin): return decorator + def register_chat_join_request_handler(self, + callback: typing.Callable, + *custom_filters, + run_task: typing.Optional[bool] = None, + **kwargs) -> None: + """ + Register handler for chat_join_request + + Example: + + .. code-block:: python3 + + dp.register_chat_join_request(some_chat_join_request) + + :param callback: + :param custom_filters: + :param run_task: run callback in task (no wait results) + :param kwargs: + """ + filters_set = self.filters_factory.resolve( + self.chat_join_request_handlers, + *custom_filters, + **kwargs, + ) + self.chat_join_request_handlers.register( + handler=self._wrap_async_task(callback, run_task), + filters=filters_set, + ) + + def chat_join_request_handler(self, *custom_filters, run_task=None, **kwargs): + """ + Decorator for chat_join_request handler + + Example: + + .. code-block:: python3 + + @dp.chat_join_request() + async def some_handler(chat_member: types.ChatJoinRequest) + + :param custom_filters: + :param run_task: run callback in task (no wait results) + :param kwargs: + """ + + def decorator(callback): + self.register_chat_join_request_handler( + callback, + *custom_filters, + run_task=run_task, + **kwargs, + ) + return callback + + return decorator + def register_errors_handler(self, callback, *custom_filters, exception=None, run_task=None, **kwargs): """ Register handler for errors @@ -1336,15 +1393,15 @@ class Dispatcher(DataMixin, ContextInstanceMixin): try: response = task.result() except Exception as e: - self._loop_create_task( + asyncio.create_task( self.errors_handlers.notify(types.Update.get_current(), e)) else: if isinstance(response, BaseResponse): - self._loop_create_task(response.execute_response(self.bot)) + asyncio.create_task(response.execute_response(self.bot)) @functools.wraps(func) async def wrapper(*args, **kwargs): - task = self._loop_create_task(func(*args, **kwargs)) + task = asyncio.create_task(func(*args, **kwargs)) task.add_done_callback(process_response) return wrapper @@ -1382,6 +1439,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin): :param chat_id: chat id :return: decorator """ + def decorator(func): @functools.wraps(func) async def wrapped(*args, **kwargs): @@ -1411,6 +1469,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin): asyncio.get_running_loop().run_in_executor( None, partial_func ) + return wrapped return decorator diff --git a/aiogram/dispatcher/webhook.py b/aiogram/dispatcher/webhook.py index 4254c72c..db5efd7b 100644 --- a/aiogram/dispatcher/webhook.py +++ b/aiogram/dispatcher/webhook.py @@ -168,14 +168,14 @@ class WebhookRequestHandler(web.View): :return: """ dispatcher = self.get_dispatcher() - loop = dispatcher.loop or asyncio.get_event_loop() + loop = asyncio.get_event_loop() # Analog of `asyncio.wait_for` but without cancelling task waiter = loop.create_future() timeout_handle = loop.call_later(RESPONSE_TIMEOUT, asyncio.tasks._release_waiter, waiter) cb = functools.partial(asyncio.tasks._release_waiter, waiter) - fut = asyncio.ensure_future(dispatcher.updates_handler.notify(update), loop=loop) + fut = asyncio.ensure_future(dispatcher.updates_handler.notify(update)) fut.add_done_callback(cb) try: @@ -207,7 +207,7 @@ class WebhookRequestHandler(web.View): TimeoutWarning) dispatcher = self.get_dispatcher() - loop = dispatcher.loop or asyncio.get_event_loop() + loop = asyncio.get_running_loop() try: results = task.result() @@ -217,7 +217,7 @@ class WebhookRequestHandler(web.View): else: response = self.get_response(results) if response is not None: - asyncio.ensure_future(response.execute_response(dispatcher.bot), loop=loop) + asyncio.ensure_future(response.execute_response(dispatcher.bot)) def get_response(self, results): """ diff --git a/aiogram/types/__init__.py b/aiogram/types/__init__.py index 1b289698..9378b32b 100644 --- a/aiogram/types/__init__.py +++ b/aiogram/types/__init__.py @@ -12,6 +12,7 @@ from .callback_game import CallbackGame from .callback_query import CallbackQuery from .chat import Chat, ChatActions, ChatType from .chat_invite_link import ChatInviteLink +from .chat_join_request import ChatJoinRequest from .chat_location import ChatLocation from .chat_member import ChatMember, ChatMemberAdministrator, ChatMemberBanned, \ ChatMemberLeft, ChatMemberMember, ChatMemberOwner, ChatMemberRestricted, \ @@ -102,6 +103,7 @@ __all__ = ( 'Chat', 'ChatActions', 'ChatInviteLink', + 'ChatJoinRequest', 'ChatLocation', 'ChatMember', 'ChatMemberStatus', diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index 12f6f0fd..a2487b59 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -742,6 +742,7 @@ class ChatActions(helper.Helper): FIND_LOCATION: str = helper.Item() # find_location RECORD_VIDEO_NOTE: str = helper.Item() # record_video_note UPLOAD_VIDEO_NOTE: str = helper.Item() # upload_video_note + CHOOSE_STICKER: str = helper.Item() # choose_sticker @classmethod async def _do(cls, action: str, sleep=None): @@ -882,3 +883,13 @@ class ChatActions(helper.Helper): :return: """ await cls._do(cls.UPLOAD_VIDEO_NOTE, sleep) + + @classmethod + async def choose_sticker(cls, sleep=None): + """ + Do choose sticker + + :param sleep: sleep timeout + :return: + """ + await cls._do(cls.CHOOSE_STICKER, sleep) diff --git a/aiogram/types/chat_invite_link.py b/aiogram/types/chat_invite_link.py index 55794780..46d505e8 100644 --- a/aiogram/types/chat_invite_link.py +++ b/aiogram/types/chat_invite_link.py @@ -16,5 +16,8 @@ class ChatInviteLink(base.TelegramObject): creator: User = fields.Field(base=User) is_primary: base.Boolean = fields.Field() is_revoked: base.Boolean = fields.Field() + name: base.String = fields.Field() expire_date: datetime = fields.DateTimeField() member_limit: base.Integer = fields.Field() + creates_join_request: datetime = fields.DateTimeField() + pending_join_request_count: base.Integer = fields.Field() diff --git a/aiogram/types/chat_join_request.py b/aiogram/types/chat_join_request.py new file mode 100644 index 00000000..71ee964a --- /dev/null +++ b/aiogram/types/chat_join_request.py @@ -0,0 +1,33 @@ +from datetime import datetime + +from . import base +from . import fields +from .chat import Chat +from .chat_invite_link import ChatInviteLink +from .user import User + + +class ChatJoinRequest(base.TelegramObject): + """ + Represents a join request sent to a chat. + + https://core.telegram.org/bots/api#chatinvitelink + """ + + chat: Chat = fields.Field(base=Chat) + from_user: User = fields.Field(alias="from", base=User) + date: datetime = fields.DateTimeField() + bio: base.String = fields.Field() + invite_link: ChatInviteLink = fields.Field(base=ChatInviteLink) + + async def approve(self) -> base.Boolean: + return await self.bot.approve_chat_join_request( + chat_id=self.chat.id, + user_id=self.from_user.id, + ) + + async def decline(self) -> base.Boolean: + return await self.bot.decline_chat_join_request( + chat_id=self.chat.id, + user_id=self.from_user.id, + ) diff --git a/aiogram/types/reply_keyboard.py b/aiogram/types/reply_keyboard.py index 47efdbbe..17b0a353 100644 --- a/aiogram/types/reply_keyboard.py +++ b/aiogram/types/reply_keyboard.py @@ -35,14 +35,17 @@ class ReplyKeyboardMarkup(base.TelegramObject): one_time_keyboard: base.Boolean = None, input_field_placeholder: base.String = None, selective: base.Boolean = None, - row_width: base.Integer = 3): + row_width: base.Integer = 3, + conf=None): + if conf is None: + conf = {} super().__init__( keyboard=keyboard, resize_keyboard=resize_keyboard, one_time_keyboard=one_time_keyboard, input_field_placeholder=input_field_placeholder, selective=selective, - conf={'row_width': row_width}, + conf={'row_width': row_width, **conf}, ) @property diff --git a/aiogram/types/update.py b/aiogram/types/update.py index e2fd3a55..4d5a74d5 100644 --- a/aiogram/types/update.py +++ b/aiogram/types/update.py @@ -10,6 +10,7 @@ from .message import Message from .poll import Poll, PollAnswer from .pre_checkout_query import PreCheckoutQuery from .shipping_query import ShippingQuery +from .chat_join_request import ChatJoinRequest from ..utils import helper, deprecated @@ -34,6 +35,7 @@ class Update(base.TelegramObject): poll_answer: PollAnswer = fields.Field(base=PollAnswer) my_chat_member: ChatMemberUpdated = fields.Field(base=ChatMemberUpdated) chat_member: ChatMemberUpdated = fields.Field(base=ChatMemberUpdated) + chat_join_request: ChatJoinRequest = fields.Field(base=ChatJoinRequest) def __hash__(self): return self.update_id @@ -66,6 +68,7 @@ class AllowedUpdates(helper.Helper): POLL_ANSWER = helper.ListItem() # poll_answer MY_CHAT_MEMBER = helper.ListItem() # my_chat_member CHAT_MEMBER = helper.ListItem() # chat_member + CHAT_JOIN_REQUEST = helper.ListItem() # chat_join_request CHOSEN_INLINE_QUERY = deprecated.DeprecatedReadOnlyClassVar( "`CHOSEN_INLINE_QUERY` is a deprecated value for allowed update. " diff --git a/aiogram/utils/executor.py b/aiogram/utils/executor.py index c74827b0..d93af29a 100644 --- a/aiogram/utils/executor.py +++ b/aiogram/utils/executor.py @@ -314,7 +314,7 @@ class Executor: :param timeout: """ self._prepare_polling() - loop: asyncio.AbstractEventLoop = self.loop + loop = asyncio.get_event_loop() try: loop.run_until_complete(self._startup_polling()) @@ -365,7 +365,8 @@ class Executor: self.dispatcher.stop_polling() await self.dispatcher.storage.close() await self.dispatcher.storage.wait_closed() - await self.dispatcher.bot.session.close() + session = await self.dispatcher.bot.get_session() + await session.close() async def _startup_polling(self): await self._welcome() diff --git a/docs/source/index.rst b/docs/source/index.rst index cd4b99d0..1b9c752d 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -22,7 +22,7 @@ Welcome to aiogram's documentation! :target: https://pypi.python.org/pypi/aiogram :alt: Supported python versions - .. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.3-blue.svg?style=flat-square&logo=telegram + .. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.4-blue.svg?style=flat-square&logo=telegram :target: https://core.telegram.org/bots/api :alt: Telegram Bot API diff --git a/examples/proxy_and_emojize.py b/examples/proxy_and_emojize.py index 5ef40608..84ad74a8 100644 --- a/examples/proxy_and_emojize.py +++ b/examples/proxy_and_emojize.py @@ -50,7 +50,7 @@ async def cmd_start(message: types.Message): # This line is formatted to '🌎 *IP:* `YOUR IP`' # Make request through bot's proxy - ip = await fetch(GET_IP_URL, bot.session) + ip = await fetch(GET_IP_URL, await bot.get_session()) content.append(text(':locked_with_key:', bold('IP:'), code(ip), italic('via proxy'))) # This line is formatted to '🔐 *IP:* `YOUR IP` _via proxy_' diff --git a/requirements.txt b/requirements.txt index 6f393257..396e9526 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -aiohttp>=3.7.2,<4.0.0 -Babel>=2.8.0 -certifi>=2020.6.20 +aiohttp>=3.8.0,<3.9.0 +Babel>=2.9.1,<2.10.0 +certifi>=2021.10.8 diff --git a/tests/test_bot/test_session.py b/tests/test_bot/test_session.py index dec6379c..1f8417e7 100644 --- a/tests/test_bot/test_session.py +++ b/tests/test_bot/test_session.py @@ -23,7 +23,6 @@ class TestAiohttpSession: assert bot._session is None - assert isinstance(bot.session, aiohttp.ClientSession) assert bot.session == bot._session @pytest.mark.asyncio @@ -51,11 +50,11 @@ class TestAiohttpSession: @pytest.mark.asyncio async def test_close_session(self): bot = BaseBot(token="42:correct",) - aiohttp_client_0 = bot.session + aiohttp_client_0 = await bot.get_session() with patch("aiohttp.ClientSession.close", new=CoroutineMock()) as mocked_close: await aiohttp_client_0.close() mocked_close.assert_called_once() await aiohttp_client_0.close() - assert aiohttp_client_0 != bot.session # will create new session + assert aiohttp_client_0 != await bot.get_session() # will create new session diff --git a/tests/types/test_mixins.py b/tests/types/test_mixins.py index 4327e8aa..4ee4381a 100644 --- a/tests/types/test_mixins.py +++ b/tests/types/test_mixins.py @@ -18,7 +18,7 @@ async def bot_fixture(): """ Bot fixture """ _bot = Bot(TOKEN) yield _bot - await _bot.session.close() + await (await _bot.get_session()).close() @pytest.fixture From c00005f41af51fb5e7aa7cf8064cfbfc7feb3015 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 7 Nov 2021 01:44:13 +0200 Subject: [PATCH 19/43] Update dependencies in setup.py --- setup.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 6f12b437..9cb6b6c6 100755 --- a/setup.py +++ b/setup.py @@ -61,16 +61,16 @@ setup( 'Topic :: Software Development :: Libraries :: Application Frameworks', ], install_requires=[ - 'aiohttp>=3.7.2,<4.0.0', - 'Babel>=2.8.0', - 'certifi>=2020.6.20', + 'aiohttp>=3.8.0,<3.9.0', + 'Babel>=2.9.1,<2.10.0', + 'certifi>=2021.10.8', ], extras_require={ 'proxy': [ 'aiohttp-socks>=0.5.3,<0.6.0', ], 'fast': [ - 'uvloop>=0.14.0,<0.15.0', + 'uvloop>=0.16.0,<0.17.0', 'ujson>=1.35', ], }, From 720a5451b766ec492c5ca5508aa4d8148f5f5a49 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 7 Nov 2021 01:47:04 +0200 Subject: [PATCH 20/43] Fixed fixture --- tests/test_bot/test_bot_download_file.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_bot/test_bot_download_file.py b/tests/test_bot/test_bot_download_file.py index 75710fcc..195a06f7 100644 --- a/tests/test_bot/test_bot_download_file.py +++ b/tests/test_bot/test_bot_download_file.py @@ -21,7 +21,8 @@ async def bot_fixture(): _bot = Bot(TOKEN) _bot.get_file = get_file yield _bot - await _bot.session.close() + session = await _bot.get_session() + await session.close() @pytest.fixture From 64393048dd82a9a3c6fe7e54f068bf4aaba000e2 Mon Sep 17 00:00:00 2001 From: Fenicu Date: Sun, 7 Nov 2021 03:49:31 +0300 Subject: [PATCH 21/43] custom filter for filters_factory example (#688) * custom filter for filters_factory example * Shortened the code * added new example of filter registration * simplifying Filters and more handlers * upgrade example * black reformat --- examples/custom_filter_example.py | 125 ++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 examples/custom_filter_example.py diff --git a/examples/custom_filter_example.py b/examples/custom_filter_example.py new file mode 100644 index 00000000..beebbfcf --- /dev/null +++ b/examples/custom_filter_example.py @@ -0,0 +1,125 @@ +from typing import List, Union +from aiogram import Bot, Dispatcher, executor, types +from aiogram.dispatcher.filters import BoundFilter + +API_TOKEN = "BOT_TOKEN_HERE" + + +ADMIN_IDS = [ + 000000000, + 111111111, + 222222222, + 333333333, + 444444444, +] + + +bot = Bot(token=API_TOKEN) +dp = Dispatcher(bot) + + +class GlobalAdminFilter(BoundFilter): + """ + Check if the user is a bot admin + """ + + key = "global_admin" + + def __init__(self, global_admin: bool): + self.global_admin = global_admin + + async def check(self, obj: Union[types.Message, types.CallbackQuery]): + user = obj.from_user + if user.id in ADMIN_IDS: + return self.global_admin is True + return self.global_admin is False + + +class MimeTypeFilter(BoundFilter): + """ + Check document mime_type + """ + + key = "mime_type" + + def __init__(self, mime_type: Union[str, List[str]]): + if isinstance(mime_type, str): + self.mime_types = [mime_type] + + elif isinstance(mime_type, list): + self.mime_types = mime_type + + else: + raise ValueError( + f"filter mime_types must be a str or list of str, not {type(mime_type).__name__}" + ) + + async def check(self, obj: types.Message): + if not obj.document: + return False + + if obj.document.mime_type in self.mime_types: + return True + + return False + + +class LettersInMessageFilter(BoundFilter): + """ + Checking for the number of characters in a message/callback_data + """ + + key = "letters" + + def __init__(self, letters: int): + if isinstance(letters, int): + self.letters = letters + else: + raise ValueError( + f"filter letters must be a int, not {type(letters).__name__}" + ) + + async def check(self, obj: Union[types.Message, types.CallbackQuery]): + data = obj.text or obj.data + if data: + letters_in_message = len(data) + if letters_in_message > self.letters: + return False + return {"letters": letters_in_message} + return False + + +# Binding filters +dp.filters_factory.bind( + GlobalAdminFilter, + exclude_event_handlers=[dp.channel_post_handlers, dp.edited_channel_post_handlers], +) +dp.filters_factory.bind(MimeTypeFilter, event_handlers=[dp.message_handlers]) +dp.filters_factory.bind(LettersInMessageFilter) + + +@dp.message_handler(letters=5) +async def handle_letters_in_message(message: types.Message, letters: int): + await message.answer(f"Message too short!\nYou sent only {letters} letters") + + +@dp.message_handler(content_types=types.ContentTypes.DOCUMENT, mime_type="text/plain") +async def handle_txt_documents(message: types.Message): + await message.answer("This is a text file!") + + +@dp.message_handler( + content_types=types.ContentTypes.DOCUMENT, mime_type=["image/jpeg", "image/png"] +) +async def handle_photo_documents(message: types.Message): + await message.answer("This is a photo file!") + + +@dp.message_handler(global_admin=True) +async def handle_admins(message: types.Message): + await message.answer("Congratulations, you are global admin!") + + +if __name__ == "__main__": + allowed_updates = types.AllowedUpdates.MESSAGE | types.AllowedUpdates.CALLBACK_QUERY + executor.start_polling(dp, allowed_updates=allowed_updates, skip_updates=True) From bccfca7caefe784f2baadb7251753374060a0fc5 Mon Sep 17 00:00:00 2001 From: barbashovtd Date: Tue, 9 Nov 2021 00:28:30 +0300 Subject: [PATCH 22/43] Project typos fix (#691) * Fixed typo in fields.py * Update fields.py * Whole project typos fix --- aiogram/dispatcher/webhook.py | 2 +- aiogram/types/fields.py | 4 ++-- aiogram/utils/helper.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/aiogram/dispatcher/webhook.py b/aiogram/dispatcher/webhook.py index db5efd7b..e8246b59 100644 --- a/aiogram/dispatcher/webhook.py +++ b/aiogram/dispatcher/webhook.py @@ -74,7 +74,7 @@ allow_ip(TELEGRAM_SUBNET_1, TELEGRAM_SUBNET_2) class WebhookRequestHandler(web.View): """ - Simple Wehhook request handler for aiohttp web server. + Simple Webhook request handler for aiohttp web server. You need to register that in app: diff --git a/aiogram/types/fields.py b/aiogram/types/fields.py index 11c83eab..d7a1d8ca 100644 --- a/aiogram/types/fields.py +++ b/aiogram/types/fields.py @@ -118,7 +118,7 @@ class Field(BaseField): class ListField(Field): """ - Field contains list ob objects + The field contains a list of objects """ def __init__(self, *args, **kwargs): @@ -162,7 +162,7 @@ class ListOfLists(Field): class DateTimeField(Field): """ - In this field st_ored datetime + In this field stored datetime in: unixtime out: datetime diff --git a/aiogram/utils/helper.py b/aiogram/utils/helper.py index 55a134a3..3a9df56e 100644 --- a/aiogram/utils/helper.py +++ b/aiogram/utils/helper.py @@ -79,7 +79,7 @@ class HelperMode(Helper): @classmethod def _snake_case(cls, text): """ - Transform text to snake cale (Based on SCREAMING_SNAKE_CASE) + Transform text to snake case (Based on SCREAMING_SNAKE_CASE) :param text: :return: From d646e198520e4076f713ca841f45f4e07e2391eb Mon Sep 17 00:00:00 2001 From: Cyril Margorin Date: Wed, 1 Dec 2021 03:43:12 +0300 Subject: [PATCH 23/43] Fix fsmcontextproxy update (#755) * Create test case for invalid 'FSMContextProxy.update()' function * Fix invalid 'FSMContextProxy.update()' function * Verify that written data has been stored and read back correctly --- aiogram/dispatcher/storage.py | 2 ++ tests/test_dispatcher/test_fsm_context.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 tests/test_dispatcher/test_fsm_context.py diff --git a/aiogram/dispatcher/storage.py b/aiogram/dispatcher/storage.py index 340b6352..63ce25b2 100644 --- a/aiogram/dispatcher/storage.py +++ b/aiogram/dispatcher/storage.py @@ -408,6 +408,8 @@ class FSMContextProxy: def update(self, data=None, **kwargs): self._check_closed() + if data is None: + data = {} self._data.update(data, **kwargs) def pop(self, key, default=None): diff --git a/tests/test_dispatcher/test_fsm_context.py b/tests/test_dispatcher/test_fsm_context.py new file mode 100644 index 00000000..3189e4e6 --- /dev/null +++ b/tests/test_dispatcher/test_fsm_context.py @@ -0,0 +1,14 @@ +import pytest +from aiogram.contrib.fsm_storage.memory import MemoryStorage +from aiogram.dispatcher import FSMContext + + +class TestFSMContext: + @pytest.mark.asyncio + async def test_update_data(self): + context = FSMContext(MemoryStorage(), chat=1, user=1) + async with context.proxy() as data: + data.update(key1="value1", key2="value2") + async with context.proxy() as data: + assert data['key1'] == "value1" + assert data['key2'] == "value2" From 5bcbcedebad98ccae0f8bd1c97c81fb2e05041e9 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Tue, 7 Dec 2021 01:41:07 +0300 Subject: [PATCH 24/43] fix: header values must be str instances (#765) #762 --- aiogram/dispatcher/webhook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/dispatcher/webhook.py b/aiogram/dispatcher/webhook.py index e8246b59..11e3238c 100644 --- a/aiogram/dispatcher/webhook.py +++ b/aiogram/dispatcher/webhook.py @@ -145,7 +145,7 @@ class WebhookRequestHandler(web.View): web_response = web.Response(text='ok') if self.request.app.get('RETRY_AFTER', None): - web_response.headers['Retry-After'] = self.request.app['RETRY_AFTER'] + web_response.headers['Retry-After'] = str(self.request.app['RETRY_AFTER']) return web_response From 910e4d778443436f895326472ab03ab9b4deebb7 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Tue, 7 Dec 2021 21:03:13 +0300 Subject: [PATCH 25/43] Bot API 5.5 (#768) * chore: bump version 5.5 * feat: add banChatSenderChat and unbanChatSenderChat Added the methods banChatSenderChat and unbanChatSenderChat for banning and unbanning channel chats in supergroups and channels. * feat: add has_private_forwards Added the field has_private_forwards to the class Chat for private chats, which can be used to check the possibility of mentioning the user by their ID. * feat: add has_protected_content Added the field has_protected_content to the classes Chat and Message. * feat: add is_automatic_forward Added the field is_automatic_forward to the class Message. * feat: add shortcuts Added Chat.ban_sender_chat() and Chat.unban_sender_chat() shortcuts. * docs: add new methods Source --- README.md | 2 +- aiogram/__init__.py | 4 +-- aiogram/bot/api.py | 4 ++- aiogram/bot/bot.py | 56 ++++++++++++++++++++++++++++++++++++++++ aiogram/types/chat.py | 26 +++++++++++++++++++ aiogram/types/message.py | 2 ++ docs/source/index.rst | 2 +- 7 files changed, 91 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 2dd5394a..f9052665 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![PyPi status](https://img.shields.io/pypi/status/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) [![Downloads](https://img.shields.io/pypi/dm/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) [![Supported python versions](https://img.shields.io/pypi/pyversions/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) -[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-5.4-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api) +[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-5.5-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api) [![Documentation Status](https://img.shields.io/readthedocs/aiogram?style=flat-square)](http://docs.aiogram.dev/en/latest/?badge=latest) [![Github issues](https://img.shields.io/github/issues/aiogram/aiogram.svg?style=flat-square)](https://github.com/aiogram/aiogram/issues) [![MIT License](https://img.shields.io/pypi/l/aiogram.svg?style=flat-square)](https://opensource.org/licenses/MIT) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index a9581081..5216689a 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -43,5 +43,5 @@ __all__ = ( 'utils', ) -__version__ = '2.16' -__api_version__ = '5.4' +__version__ = '2.17' +__api_version__ = '5.5' diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index b0bb790c..fca5b738 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -189,7 +189,7 @@ class Methods(Helper): """ Helper for Telegram API Methods listed on https://core.telegram.org/bots/api - List is updated to Bot API 5.4 + List is updated to Bot API 5.5 """ mode = HelperMode.lowerCamelCase @@ -230,6 +230,8 @@ class Methods(Helper): RESTRICT_CHAT_MEMBER = Item() # restrictChatMember PROMOTE_CHAT_MEMBER = Item() # promoteChatMember SET_CHAT_ADMINISTRATOR_CUSTOM_TITLE = Item() # setChatAdministratorCustomTitle + BAN_CHAT_SENDER_CHAT = Item() # banChatSenderChat + UNBAN_CHAT_SENDER_CHAT = Item() # unbanChatSenderChat SET_CHAT_PERMISSIONS = Item() # setChatPermissions EXPORT_CHAT_INVITE_LINK = Item() # exportChatInviteLink CREATE_CHAT_INVITE_LINK = Item() # createChatInviteLink diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 436b83a4..d5aa5d65 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -1814,6 +1814,62 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): return await self.request(api.Methods.SET_CHAT_ADMINISTRATOR_CUSTOM_TITLE, payload) + async def ban_chat_sender_chat( + self, + chat_id: typing.Union[base.Integer, base.String], + sender_chat_id: base.Integer, + until_date: typing.Union[ + base.Integer, datetime.datetime, datetime.timedelta, None + ] = None, + ): + """Ban a channel chat in a supergroup or a channel. + + The owner of the chat will not be able to send messages and join + live streams on behalf of the chat, unless it is unbanned first. + The bot must be an administrator in the supergroup or channel + for this to work and must have the appropriate administrator + rights. Returns True on success. + + Source: https://core.telegram.org/bots/api#banchatsenderchat + + :param chat_id: Unique identifier for the target chat or + username of the target channel (in the format + @channelusername) + :param sender_chat_id: Unique identifier of the target sender + chat + :param until_date: Date when the sender chat will be unbanned, + unix time. If the chat is banned for more than 366 days or + less than 30 seconds from the current time they are + considered to be banned forever. + """ + until_date = prepare_arg(until_date) + payload = generate_payload(**locals()) + + return await self.request(api.Methods.BAN_CHAT_SENDER_CHAT, payload) + + async def unban_chat_sender_chat( + self, + chat_id: typing.Union[base.Integer, base.String], + sender_chat_id: base.Integer, + ): + """Unban a previously banned channel chat in a supergroup or + channel. + + The bot must be an administrator for this to work and must have + the appropriate administrator rights. Returns True on success. + + Source: https://core.telegram.org/bots/api#unbanchatsenderchat + + :param chat_id: Unique identifier for the target chat or + username of the target channel (in the format + @channelusername) + :param sender_chat_id: Unique identifier of the target sender + chat + """ + payload = generate_payload(**locals()) + + return await self.request(api.Methods.UNBAN_CHAT_SENDER_CHAT, payload) + async def set_chat_permissions(self, chat_id: typing.Union[base.Integer, base.String], permissions: types.ChatPermissions) -> base.Boolean: """ diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index a2487b59..00b7a656 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -30,12 +30,14 @@ class Chat(base.TelegramObject): all_members_are_administrators: base.Boolean = fields.Field() photo: ChatPhoto = fields.Field(base=ChatPhoto) bio: base.String = fields.Field() + has_private_forwards: base.Boolean = fields.Field() description: base.String = fields.Field() invite_link: base.String = fields.Field() pinned_message: 'Message' = fields.Field(base='Message') permissions: ChatPermissions = fields.Field(base=ChatPermissions) slow_mode_delay: base.Integer = fields.Field() message_auto_delete_time: base.Integer = fields.Field() + has_protected_content: base.Boolean = fields.Field() sticker_set_name: base.String = fields.Field() can_set_sticker_set: base.Boolean = fields.Field() linked_chat_id: base.Integer = fields.Field() @@ -621,6 +623,30 @@ class Chat(base.TelegramObject): message_id=message_id, ) + async def ban_sender_chat( + self, + sender_chat_id: base.Integer, + until_date: typing.Union[ + base.Integer, datetime.datetime, datetime.timedelta, None + ] = None, + ): + """Shortcut for banChatSenderChat method.""" + return await self.bot.ban_chat_sender_chat( + chat_id=self.id, + sender_chat_id=sender_chat_id, + until_date=until_date, + ) + + async def unban_sender_chat( + self, + sender_chat_id: base.Integer, + ): + """Shortcut for unbanChatSenderChat method.""" + return await self.bot.unban_chat_sender_chat( + chat_id=self.id, + sender_chat_id=sender_chat_id, + ) + def __int__(self): return self.id diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 403ef954..fa73f2e4 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -58,9 +58,11 @@ class Message(base.TelegramObject): forward_from_message_id: base.Integer = fields.Field() forward_signature: base.String = fields.Field() forward_date: datetime.datetime = fields.DateTimeField() + is_automatic_forward: base.Boolean = fields.Field() reply_to_message: Message = fields.Field(base="Message") via_bot: User = fields.Field(base=User) edit_date: datetime.datetime = fields.DateTimeField() + has_protected_content: base.Boolean = fields.Field() media_group_id: base.String = fields.Field() author_signature: base.String = fields.Field() forward_sender_name: base.String = fields.Field() diff --git a/docs/source/index.rst b/docs/source/index.rst index 1b9c752d..a4ce2354 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -22,7 +22,7 @@ Welcome to aiogram's documentation! :target: https://pypi.python.org/pypi/aiogram :alt: Supported python versions - .. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.4-blue.svg?style=flat-square&logo=telegram + .. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.5-blue.svg?style=flat-square&logo=telegram :target: https://core.telegram.org/bots/api :alt: Telegram Bot API From 2ece1ba140822abd65b1ce85593eb694cb9ae829 Mon Sep 17 00:00:00 2001 From: abdullaev388 <78722918+abdullaev388@users.noreply.github.com> Date: Thu, 9 Dec 2021 04:52:54 +0500 Subject: [PATCH 26/43] Removed legacy code (markdown v1) (#771) --- examples/check_user_language.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/check_user_language.py b/examples/check_user_language.py index 98bed8a6..31c2a5f3 100644 --- a/examples/check_user_language.py +++ b/examples/check_user_language.py @@ -11,7 +11,7 @@ API_TOKEN = 'BOT TOKEN HERE' logging.basicConfig(level=logging.INFO) -bot = Bot(token=API_TOKEN, parse_mode=types.ParseMode.MARKDOWN) +bot = Bot(token=API_TOKEN, parse_mode=types.ParseMode.MARKDOWN_V2) dp = Dispatcher(bot) From 3b93f337d19383b163ff82d09658e6245a2eec18 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Thu, 9 Dec 2021 03:00:51 +0300 Subject: [PATCH 27/43] Telegram API silent update (#772) * fix: silent update Special thanks for @levlam for silent Telegram API updates with removing fields. * chore: add removed_argument deprecation * Mark removed argument in method alias Co-authored-by: Alex Root Junior --- aiogram/bot/bot.py | 15 +++-------- aiogram/types/chat.py | 7 ++--- aiogram/utils/deprecated.py | 51 +++++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 16 deletions(-) diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index d5aa5d65..6653cf89 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -8,7 +8,7 @@ import warnings from .base import BaseBot, api from .. import types from ..types import base -from ..utils.deprecated import deprecated +from ..utils.deprecated import deprecated, removed_argument from ..utils.exceptions import ValidationError from ..utils.mixins import DataMixin, ContextInstanceMixin from ..utils.payload import generate_payload, prepare_arg, prepare_attachment, prepare_file @@ -1814,18 +1814,16 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): return await self.request(api.Methods.SET_CHAT_ADMINISTRATOR_CUSTOM_TITLE, payload) + @removed_argument("until_date", "2.19") async def ban_chat_sender_chat( self, chat_id: typing.Union[base.Integer, base.String], sender_chat_id: base.Integer, - until_date: typing.Union[ - base.Integer, datetime.datetime, datetime.timedelta, None - ] = None, ): """Ban a channel chat in a supergroup or a channel. - The owner of the chat will not be able to send messages and join - live streams on behalf of the chat, unless it is unbanned first. + Until the chat is unbanned, the owner of the banned chat won't + be able to send messages on behalf of any of their channels. The bot must be an administrator in the supergroup or channel for this to work and must have the appropriate administrator rights. Returns True on success. @@ -1837,12 +1835,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): @channelusername) :param sender_chat_id: Unique identifier of the target sender chat - :param until_date: Date when the sender chat will be unbanned, - unix time. If the chat is banned for more than 366 days or - less than 30 seconds from the current time they are - considered to be banned forever. """ - until_date = prepare_arg(until_date) payload = generate_payload(**locals()) return await self.request(api.Methods.BAN_CHAT_SENDER_CHAT, payload) diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index 00b7a656..c18ad88b 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -12,7 +12,7 @@ from .chat_permissions import ChatPermissions from .chat_photo import ChatPhoto from .input_file import InputFile from ..utils import helper, markdown -from ..utils.deprecated import deprecated, DeprecatedReadOnlyClassVar +from ..utils.deprecated import deprecated, DeprecatedReadOnlyClassVar, removed_argument class Chat(base.TelegramObject): @@ -623,18 +623,15 @@ class Chat(base.TelegramObject): message_id=message_id, ) + @removed_argument("until_date", "2.19") async def ban_sender_chat( self, sender_chat_id: base.Integer, - until_date: typing.Union[ - base.Integer, datetime.datetime, datetime.timedelta, None - ] = None, ): """Shortcut for banChatSenderChat method.""" return await self.bot.ban_chat_sender_chat( chat_id=self.id, sender_chat_id=sender_chat_id, - until_date=until_date, ) async def unban_sender_chat( diff --git a/aiogram/utils/deprecated.py b/aiogram/utils/deprecated.py index 6d0d7ee3..186fe6cc 100644 --- a/aiogram/utils/deprecated.py +++ b/aiogram/utils/deprecated.py @@ -131,6 +131,57 @@ def renamed_argument(old_name: str, new_name: str, until_version: str, stackleve return decorator +def removed_argument(name: str, until_version: str, stacklevel: int = 3): + """ + A meta-decorator to mark an argument as removed. + + .. code-block:: python3 + + @removed_argument("until_date", "3.0") # stacklevel=3 by default + def some_function(user_id, chat_id=None): + print(f"user_id={user_id}, chat_id={chat_id}") + + :param name: + :param until_version: the version in which the argument is scheduled to be removed + :param stacklevel: leave it to default if it's the first decorator used. + Increment with any new decorator used. + :return: decorator + """ + + def decorator(func): + is_coroutine = asyncio.iscoroutinefunction(func) + + def _handling(kwargs): + """ + Returns updated version of kwargs. + """ + routine_type = 'coroutine' if is_coroutine else 'function' + if name in kwargs: + warn_deprecated( + f"In {routine_type} {func.__name__!r} argument {name!r} " + f"is planned to be removed in aiogram {until_version}", + stacklevel=stacklevel, + ) + kwargs = kwargs.copy() + del kwargs[name] + return kwargs + + if is_coroutine: + @functools.wraps(func) + async def wrapped(*args, **kwargs): + kwargs = _handling(kwargs) + return await func(*args, **kwargs) + else: + @functools.wraps(func) + def wrapped(*args, **kwargs): + kwargs = _handling(kwargs) + return func(*args, **kwargs) + + return wrapped + + return decorator + + _VT = TypeVar("_VT") _OwnerCls = TypeVar("_OwnerCls") From dea94d2574893f4d0e7df358852a543ef9214217 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Thu, 9 Dec 2021 02:06:23 +0200 Subject: [PATCH 28/43] Bump version, added Python 3.10 classifier --- Makefile | 2 +- aiogram/__init__.py | 2 +- setup.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index da6493d9..662ad49f 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ upload: release: make clean - make test + #make test make build make tag @echo "Released aiogram $(AIOGRAM_VERSION)" diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 5216689a..ea77a533 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -43,5 +43,5 @@ __all__ = ( 'utils', ) -__version__ = '2.17' +__version__ = '2.17.1' __api_version__ = '5.5' diff --git a/setup.py b/setup.py index 9cb6b6c6..a381507a 100755 --- a/setup.py +++ b/setup.py @@ -58,6 +58,7 @@ setup( 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', 'Topic :: Software Development :: Libraries :: Application Frameworks', ], install_requires=[ From 763efb77631de07c563868273b1bd22b15f63be5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D1=83=D0=BB=D1=8C=D0=B1=D0=B0?= <81091299+ulbwa@users.noreply.github.com> Date: Sat, 11 Dec 2021 04:00:09 +0500 Subject: [PATCH 29/43] Missing space (#775) --- aiogram/contrib/middlewares/logging.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/contrib/middlewares/logging.py b/aiogram/contrib/middlewares/logging.py index 7f21eb41..92ef7252 100644 --- a/aiogram/contrib/middlewares/logging.py +++ b/aiogram/contrib/middlewares/logging.py @@ -92,7 +92,7 @@ class LoggingMiddleware(BaseMiddleware): text = (f"Received callback query [ID:{callback_query.id}] " f"from user [ID:{callback_query.from_user.id}] " f"for message [ID:{message.message_id}] " - f"in chat [{message.chat.type}:{message.chat.id}]" + f"in chat [{message.chat.type}:{message.chat.id}] " f"with data: {callback_query.data}") if message.from_user: From 583f00ce317dfb4c2c6c4df4bd4694c6f0b0bf18 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Wed, 29 Dec 2021 04:38:37 +0300 Subject: [PATCH 30/43] feat: TelegramObject readable representation (#796) --- aiogram/types/base.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/aiogram/types/base.py b/aiogram/types/base.py index 3f8cda60..5bb29472 100644 --- a/aiogram/types/base.py +++ b/aiogram/types/base.py @@ -211,6 +211,15 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject): """ return self.as_json() + def __repr__(self) -> str: + """ + Return object readable representation. + + Example: + :return: object class name and object data as a string + """ + return f"<{type(self).__name__} {self}>" + def __getitem__(self, item: typing.Union[str, int]) -> typing.Any: """ Item getter (by key) From 3d305816576b2ada04e700cb29ee445ae61f814a Mon Sep 17 00:00:00 2001 From: Oleg A Date: Thu, 30 Dec 2021 16:07:09 +0300 Subject: [PATCH 31/43] docs: bump tg api version --- README.md | 2 +- aiogram/__init__.py | 4 ++-- aiogram/bot/api.py | 2 +- docs/source/index.rst | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f9052665..811ad4c9 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![PyPi status](https://img.shields.io/pypi/status/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) [![Downloads](https://img.shields.io/pypi/dm/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) [![Supported python versions](https://img.shields.io/pypi/pyversions/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) -[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-5.5-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api) +[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-5.6-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api) [![Documentation Status](https://img.shields.io/readthedocs/aiogram?style=flat-square)](http://docs.aiogram.dev/en/latest/?badge=latest) [![Github issues](https://img.shields.io/github/issues/aiogram/aiogram.svg?style=flat-square)](https://github.com/aiogram/aiogram/issues) [![MIT License](https://img.shields.io/pypi/l/aiogram.svg?style=flat-square)](https://opensource.org/licenses/MIT) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index ea77a533..a180b08d 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -43,5 +43,5 @@ __all__ = ( 'utils', ) -__version__ = '2.17.1' -__api_version__ = '5.5' +__version__ = '2.18.0' +__api_version__ = '5.6' diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index fca5b738..f95e35b1 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -189,7 +189,7 @@ class Methods(Helper): """ Helper for Telegram API Methods listed on https://core.telegram.org/bots/api - List is updated to Bot API 5.5 + List is updated to Bot API 5.6 """ mode = HelperMode.lowerCamelCase diff --git a/docs/source/index.rst b/docs/source/index.rst index a4ce2354..98bd08ed 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -22,7 +22,7 @@ Welcome to aiogram's documentation! :target: https://pypi.python.org/pypi/aiogram :alt: Supported python versions - .. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.5-blue.svg?style=flat-square&logo=telegram + .. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.6-blue.svg?style=flat-square&logo=telegram :target: https://core.telegram.org/bots/api :alt: Telegram Bot API From e0ecbc4ec95a1095179fa3d0ae6ce35394972f2e Mon Sep 17 00:00:00 2001 From: Oleg A Date: Thu, 30 Dec 2021 16:26:31 +0300 Subject: [PATCH 32/43] feat: add protect_content param --- aiogram/bot/bot.py | 112 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 106 insertions(+), 6 deletions(-) diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 6653cf89..90beb294 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -276,6 +276,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None] = None, + protect_content: typing.Optional[base.Boolean] = None, ) -> types.Message: """ Use this method to send text messages. @@ -314,6 +315,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -329,7 +334,9 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): async def forward_message(self, chat_id: typing.Union[base.Integer, base.String], from_chat_id: typing.Union[base.Integer, base.String], message_id: base.Integer, - disable_notification: typing.Optional[base.Boolean] = None) -> types.Message: + disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, + ) -> types.Message: """ Use this method to forward messages of any kind. @@ -343,6 +350,9 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :type disable_notification: :obj:`typing.Optional[base.Boolean]` :param message_id: Message identifier in the chat specified in from_chat_id :type message_id: :obj:`base.Integer` + :param protect_content: Protects the contents of the forwarded + message from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -365,6 +375,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None] = None, + protect_content: typing.Optional[base.Boolean] = None, ) -> types.MessageId: """ Use this method to copy messages of any kind. The method is analogous to the @@ -416,6 +427,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` + :param protect_content: Protects the contents of the sent + message from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -442,6 +457,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None] = None, + protect_content: typing.Optional[base.Boolean] = None, ) -> types.Message: """ Use this method to send photos. @@ -480,6 +496,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` + :param protect_content: Protects the contents of the sent + message from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -512,6 +532,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None] = None, + protect_content: typing.Optional[base.Boolean] = None, ) -> types.Message: """ Use this method to send audio files, if you want Telegram clients to display them in the music player. @@ -565,6 +586,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` + :param protect_content: Protects the contents of the sent + message from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -597,6 +622,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): types.ReplyKeyboardRemove, types.ForceReply, None] = None, + protect_content: typing.Optional[base.Boolean] = None, ) -> types.Message: """ Use this method to send general files. On success, the sent Message is @@ -650,6 +676,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply], None]` + :param protect_content: Protects the contents of the sent + message from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -682,7 +712,9 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): reply_markup: typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, - types.ForceReply, None] = None) -> types.Message: + types.ForceReply, None] = None, + protect_content: typing.Optional[base.Boolean] = None, + ) -> types.Message: """ Use this method to send video files, Telegram clients support mp4 videos (other formats may be sent as Document). @@ -736,6 +768,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` + :param protect_content: Protects the contents of the sent + message from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -769,6 +805,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply], None] = None, + protect_content: typing.Optional[base.Boolean] = None, ) -> types.Message: """ Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound). @@ -826,6 +863,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :type reply_markup: :obj:`typing.Union[typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply], None]` + :param protect_content: Protects the contents of the sent + message from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -856,6 +897,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None] = None, + protect_content: typing.Optional[base.Boolean] = None, ) -> types.Message: """ Use this method to send audio files, if you want Telegram clients to display the file @@ -901,6 +943,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` + :param protect_content: Protects the contents of the sent + message from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -927,7 +973,9 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): reply_markup: typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, - types.ForceReply, None] = None) -> types.Message: + types.ForceReply, None] = None, + protect_content: typing.Optional[base.Boolean] = None, + ) -> types.Message: """ As of v.4.0, Telegram clients support rounded square mp4 videos of up to 1 minute long. Use this method to send video messages. @@ -964,6 +1012,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` + :param protect_content: Protects the contents of the sent + message from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -982,6 +1034,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): disable_notification: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, ) -> typing.List[types.Message]: """ Use this method to send a group of photos, videos, documents or audios as @@ -1011,6 +1064,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): even if the specified replied-to message is not found :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of the sent + message from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :return: On success, an array of the sent Messages is returned :rtype: typing.List[types.Message] """ @@ -1042,7 +1099,9 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): reply_markup: typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, - types.ForceReply, None] = None) -> types.Message: + types.ForceReply, None] = None, + protect_content: typing.Optional[base.Boolean] = None, + ) -> types.Message: """ Use this method to send point on the map. @@ -1088,6 +1147,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` + :param protect_content: Protects the contents of the sent + message from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -1207,6 +1270,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None] = None, + protect_content: typing.Optional[base.Boolean] = None, ) -> types.Message: """ Use this method to send information about a venue. @@ -1261,6 +1325,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` + :param protect_content: Protects the contents of the sent + message from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -1280,7 +1348,9 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): reply_markup: typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, - types.ForceReply, None] = None) -> types.Message: + types.ForceReply, None] = None, + protect_content: typing.Optional[base.Boolean] = None, + ) -> types.Message: """ Use this method to send phone contacts. @@ -1316,6 +1386,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` + :param protect_content: Protects the contents of the sent + message from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -1350,6 +1424,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None] = None, + protect_content: typing.Optional[base.Boolean] = None, ) -> types.Message: """ Use this method to send a native poll. On success, the sent Message is @@ -1426,6 +1501,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` + :param protect_content: Protects the contents of the sent + message from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -1450,6 +1529,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None] = None, + protect_content: typing.Optional[base.Boolean] = None, ) -> types.Message: """ Use this method to send an animated emoji that will display a random value. @@ -1484,6 +1564,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` + :param protect_content: Protects the contents of the sent + message from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -2735,7 +2819,9 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): reply_markup: typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, - types.ForceReply, None] = None) -> types.Message: + types.ForceReply, None] = None, + protect_content: typing.Optional[base.Boolean] = None, + ) -> types.Message: """ Use this method to send .webp stickers. @@ -2762,6 +2848,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` + :param protect_content: Protects the contents of the sent + message from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -3049,6 +3139,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): reply_to_message_id: typing.Optional[base.Integer] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Optional[types.InlineKeyboardMarkup] = None, + protect_content: typing.Optional[base.Boolean] = None, ) -> types.Message: """ Use this method to send invoices. @@ -3156,6 +3247,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): If empty, one 'Pay total price' button will be shown. If not empty, the first button must be a Pay button. :type reply_markup: :obj:`typing.Optional[types.InlineKeyboardMarkup]` + :param protect_content: Protects the contents of the sent + message from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -3268,6 +3363,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): reply_to_message_id: typing.Optional[base.Integer] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Optional[types.InlineKeyboardMarkup] = None, + protect_content: typing.Optional[base.Boolean] = None, ) -> types.Message: """ Use this method to send a game. @@ -3295,6 +3391,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): If empty, one ‘Play game_title’ button will be shown. If not empty, the first button must launch the game. :type reply_markup: :obj:`typing.Optional[types.InlineKeyboardMarkup]` + :param protect_content: Protects the contents of the sent + message from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ From 1e86ec064353074a376f2735ef02d977c6160274 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Thu, 30 Dec 2021 16:45:33 +0300 Subject: [PATCH 33/43] feat: add spoiler entities --- aiogram/types/message_entity.py | 13 +++++++++---- aiogram/utils/markdown.py | 27 +++++++++++++++++++++++++++ aiogram/utils/text_decorations.py | 14 ++++++++++++-- 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/aiogram/types/message_entity.py b/aiogram/types/message_entity.py index b2aaf425..9788af05 100644 --- a/aiogram/types/message_entity.py +++ b/aiogram/types/message_entity.py @@ -77,6 +77,9 @@ class MessageEntity(base.TelegramObject): if self.type == MessageEntityType.ITALIC: method = markdown.hitalic if as_html else markdown.italic return method(entity_text) + if self.type == MessageEntityType.SPOILER: + method = markdown.spoiler if as_html else markdown.hspoiler + return method(entity_text) if self.type == MessageEntityType.PRE: method = markdown.hpre if as_html else markdown.pre return method(entity_text) @@ -108,10 +111,11 @@ class MessageEntityType(helper.Helper): :key: PHONE_NUMBER :key: BOLD :key: ITALIC - :key: CODE - :key: PRE :key: UNDERLINE :key: STRIKETHROUGH + :key: SPOILER + :key: CODE + :key: PRE :key: TEXT_LINK :key: TEXT_MENTION """ @@ -127,9 +131,10 @@ class MessageEntityType(helper.Helper): PHONE_NUMBER = helper.Item() # phone_number BOLD = helper.Item() # bold - bold text ITALIC = helper.Item() # italic - italic text - CODE = helper.Item() # code - monowidth string - PRE = helper.Item() # pre - monowidth block UNDERLINE = helper.Item() # underline STRIKETHROUGH = helper.Item() # strikethrough + SPOILER = helper.Item() # spoiler + CODE = helper.Item() # code - monowidth string + PRE = helper.Item() # pre - monowidth block TEXT_LINK = helper.Item() # text_link - for clickable text URLs TEXT_MENTION = helper.Item() # text_mention - for users without usernames diff --git a/aiogram/utils/markdown.py b/aiogram/utils/markdown.py index 75f5fea0..3b50ffd4 100644 --- a/aiogram/utils/markdown.py +++ b/aiogram/utils/markdown.py @@ -7,6 +7,7 @@ MD_SYMBOLS = ( (LIST_MD_SYMBOLS[1], LIST_MD_SYMBOLS[1]), (LIST_MD_SYMBOLS[2], LIST_MD_SYMBOLS[2]), (LIST_MD_SYMBOLS[2] * 3 + "\n", "\n" + LIST_MD_SYMBOLS[2] * 3), + ("||", "||"), ("", ""), ("", ""), ("", ""), @@ -113,6 +114,32 @@ def hitalic(*content, sep=" ") -> str: ) +def spoiler(*content, sep=" ") -> str: + """ + Make spoiler text (Markdown) + + :param content: + :param sep: + :return: + """ + return markdown_decoration.spoiler( + value=markdown_decoration.quote(_join(*content, sep=sep)) + ) + + +def hspoiler(*content, sep=" ") -> str: + """ + Make spoiler text (HTML) + + :param content: + :param sep: + :return: + """ + return html_decoration.spoiler( + value=html_decoration.quote(_join(*content, sep=sep)) + ) + + def code(*content, sep=" ") -> str: """ Make mono-width text (Markdown) diff --git a/aiogram/utils/text_decorations.py b/aiogram/utils/text_decorations.py index 40fe296b..ae9af7d4 100644 --- a/aiogram/utils/text_decorations.py +++ b/aiogram/utils/text_decorations.py @@ -27,9 +27,9 @@ class TextDecoration(ABC): :return: """ if entity.type in {"bot_command", "url", "mention", "phone_number"}: - # This entities should not be changed + # These entities should not be changed return text - if entity.type in {"bold", "italic", "code", "underline", "strikethrough"}: + if entity.type in {"bold", "italic", "spoiler", "code", "underline", "strikethrough"}: return cast(str, getattr(self, entity.type)(value=text)) if entity.type == "pre": return ( @@ -115,6 +115,10 @@ class TextDecoration(ABC): def italic(self, value: str) -> str: # pragma: no cover pass + @abstractmethod + def spoiler(self, value: str) -> str: # pragma: no cover + pass + @abstractmethod def code(self, value: str) -> str: # pragma: no cover pass @@ -150,6 +154,9 @@ class HtmlDecoration(TextDecoration): def italic(self, value: str) -> str: return f"{value}" + def spoiler(self, value: str) -> str: + return f'{value}' + def code(self, value: str) -> str: return f"{value}" @@ -181,6 +188,9 @@ class MarkdownDecoration(TextDecoration): def italic(self, value: str) -> str: return f"_\r{value}_\r" + def spoiler(self, value: str) -> str: + return f"||{value}||" + def code(self, value: str) -> str: return f"`{value}`" From 61ef1cc14f00076fea88d299a0013846632255a4 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Fri, 31 Dec 2021 00:49:47 +0300 Subject: [PATCH 34/43] style: reorder methods similar to docs --- aiogram/bot/bot.py | 205 ++++++++++++++++++++++++--------------------- 1 file changed, 108 insertions(+), 97 deletions(-) diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 90beb294..bf1a6c55 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -270,13 +270,13 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): entities: typing.Optional[typing.List[types.MessageEntity]] = None, disable_web_page_preview: typing.Optional[base.Boolean] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None] = None, - protect_content: typing.Optional[base.Boolean] = None, ) -> types.Message: """ Use this method to send text messages. @@ -303,6 +303,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :param disable_notification: Sends the message silently. Users will receive a notification with no sound :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param reply_to_message_id: If the message is a reply, ID of the original message :type reply_to_message_id: :obj:`typing.Optional[base.Integer]` @@ -315,10 +319,6 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` - :param protect_content: Protects the contents of sent messages - from forwarding and saving - :type protect_content: :obj:`typing.Optional[base.Boolean]` - :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -332,8 +332,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): result = await self.request(api.Methods.SEND_MESSAGE, payload) return types.Message(**result) - async def forward_message(self, chat_id: typing.Union[base.Integer, base.String], - from_chat_id: typing.Union[base.Integer, base.String], message_id: base.Integer, + async def forward_message(self, + chat_id: typing.Union[base.Integer, base.String], + from_chat_id: typing.Union[base.Integer, base.String], + message_id: base.Integer, disable_notification: typing.Optional[base.Boolean] = None, protect_content: typing.Optional[base.Boolean] = None, ) -> types.Message: @@ -342,17 +344,26 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): Source: https://core.telegram.org/bots/api#forwardmessage - :param chat_id: Unique identifier for the target chat or username of the target channel + :param chat_id: Unique identifier for the target chat or + username of the target channel :type chat_id: :obj:`typing.Union[base.Integer, base.String]` - :param from_chat_id: Unique identifier for the chat where the original message was sent + + :param from_chat_id: Unique identifier for the chat where the + original message was sent :type from_chat_id: :obj:`typing.Union[base.Integer, base.String]` - :param disable_notification: Sends the message silently. Users will receive a notification with no sound + + :param disable_notification: Sends the message silently. Users + will receive a notification with no sound :type disable_notification: :obj:`typing.Optional[base.Boolean]` - :param message_id: Message identifier in the chat specified in from_chat_id - :type message_id: :obj:`base.Integer` + :param protect_content: Protects the contents of the forwarded message from forwarding and saving :type protect_content: :obj:`typing.Optional[base.Boolean]` + + :param message_id: Message identifier in the chat specified in + from_chat_id + :type message_id: :obj:`base.Integer` + :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -369,13 +380,13 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): parse_mode: typing.Optional[base.String] = None, caption_entities: typing.Optional[typing.List[types.MessageEntity]] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None] = None, - protect_content: typing.Optional[base.Boolean] = None, ) -> types.MessageId: """ Use this method to copy messages of any kind. The method is analogous to the @@ -412,6 +423,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): a notification with no sound :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param reply_to_message_id: If the message is a reply, ID of the original message :type reply_to_message_id: :obj:`typing.Optional[base.Integer]` @@ -427,10 +442,6 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` - :param protect_content: Protects the contents of the sent - message from forwarding and saving - :type protect_content: :obj:`typing.Optional[base.Boolean]` - :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -451,13 +462,13 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): parse_mode: typing.Optional[base.String] = None, caption_entities: typing.Optional[typing.List[types.MessageEntity]] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None] = None, - protect_content: typing.Optional[base.Boolean] = None, ) -> types.Message: """ Use this method to send photos. @@ -484,6 +495,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :param disable_notification: Sends the message silently. Users will receive a notification with no sound :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param reply_to_message_id: If the message is a reply, ID of the original message :type reply_to_message_id: :obj:`typing.Optional[base.Integer]` @@ -496,10 +511,6 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` - :param protect_content: Protects the contents of the sent - message from forwarding and saving - :type protect_content: :obj:`typing.Optional[base.Boolean]` - :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -526,13 +537,13 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): title: typing.Optional[base.String] = None, thumb: typing.Union[base.InputFile, base.String, None] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None] = None, - protect_content: typing.Optional[base.Boolean] = None, ) -> types.Message: """ Use this method to send audio files, if you want Telegram clients to display them in the music player. @@ -574,6 +585,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :param disable_notification: Sends the message silently. Users will receive a notification with no sound :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param reply_to_message_id: If the message is a reply, ID of the original message :type reply_to_message_id: :obj:`typing.Optional[base.Integer]` @@ -586,10 +601,6 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` - :param protect_content: Protects the contents of the sent - message from forwarding and saving - :type protect_content: :obj:`typing.Optional[base.Boolean]` - :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -615,6 +626,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): caption_entities: typing.Optional[typing.List[types.MessageEntity]] = None, disable_content_type_detection: typing.Optional[base.Boolean] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[types.InlineKeyboardMarkup, @@ -622,7 +634,6 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): types.ReplyKeyboardRemove, types.ForceReply, None] = None, - protect_content: typing.Optional[base.Boolean] = None, ) -> types.Message: """ Use this method to send general files. On success, the sent Message is @@ -661,6 +672,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): notification with no sound :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param reply_to_message_id: If the message is a reply, ID of the original message :type reply_to_message_id: :obj:`typing.Optional[base.Integer]` @@ -676,10 +691,6 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply], None]` - :param protect_content: Protects the contents of the sent - message from forwarding and saving - :type protect_content: :obj:`typing.Optional[base.Boolean]` - :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -707,13 +718,13 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): caption_entities: typing.Optional[typing.List[types.MessageEntity]] = None, supports_streaming: typing.Optional[base.Boolean] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None] = None, - protect_content: typing.Optional[base.Boolean] = None, ) -> types.Message: """ Use this method to send video files, Telegram clients support mp4 videos @@ -756,6 +767,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :param disable_notification: Sends the message silently. Users will receive a notification with no sound :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param reply_to_message_id: If the message is a reply, ID of the original message :type reply_to_message_id: :obj:`typing.Optional[base.Integer]` @@ -768,10 +783,6 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` - :param protect_content: Protects the contents of the sent - message from forwarding and saving - :type protect_content: :obj:`typing.Optional[base.Boolean]` - :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -799,13 +810,13 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): parse_mode: typing.Optional[base.String] = None, caption_entities: typing.Optional[typing.List[types.MessageEntity]] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply], None] = None, - protect_content: typing.Optional[base.Boolean] = None, ) -> types.Message: """ Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound). @@ -851,6 +862,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :param disable_notification: Sends the message silently. Users will receive a notification with no sound :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param reply_to_message_id: If the message is a reply, ID of the original message :type reply_to_message_id: :obj:`typing.Optional[base.Integer]` @@ -863,10 +878,6 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :type reply_markup: :obj:`typing.Union[typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply], None]` - :param protect_content: Protects the contents of the sent - message from forwarding and saving - :type protect_content: :obj:`typing.Optional[base.Boolean]` - :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -891,13 +902,13 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): caption_entities: typing.Optional[typing.List[types.MessageEntity]] = None, duration: typing.Optional[base.Integer] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None] = None, - protect_content: typing.Optional[base.Boolean] = None, ) -> types.Message: """ Use this method to send audio files, if you want Telegram clients to display the file @@ -931,6 +942,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :param disable_notification: Sends the message silently. Users will receive a notification with no sound :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param reply_to_message_id: If the message is a reply, ID of the original message :type reply_to_message_id: :obj:`typing.Optional[base.Integer]` @@ -943,10 +958,6 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` - :param protect_content: Protects the contents of the sent - message from forwarding and saving - :type protect_content: :obj:`typing.Optional[base.Boolean]` - :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -968,13 +979,13 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): length: typing.Optional[base.Integer] = None, thumb: typing.Union[base.InputFile, base.String, None] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None] = None, - protect_content: typing.Optional[base.Boolean] = None, ) -> types.Message: """ As of v.4.0, Telegram clients support rounded square mp4 videos of up to 1 minute long. @@ -1000,6 +1011,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :param disable_notification: Sends the message silently. Users will receive a notification with no sound :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param reply_to_message_id: If the message is a reply, ID of the original message :type reply_to_message_id: :obj:`typing.Optional[base.Integer]` @@ -1012,10 +1027,6 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` - :param protect_content: Protects the contents of the sent - message from forwarding and saving - :type protect_content: :obj:`typing.Optional[base.Boolean]` - :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -1032,9 +1043,9 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): chat_id: typing.Union[base.Integer, base.String], media: typing.Union[types.MediaGroup, typing.List], disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, - protect_content: typing.Optional[base.Boolean] = None, ) -> typing.List[types.Message]: """ Use this method to send a group of photos, videos, documents or audios as @@ -1056,6 +1067,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): notification with no sound. :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param reply_to_message_id: If the messages are a reply, ID of the original message :type reply_to_message_id: :obj:`typing.Optional[base.Integer]` @@ -1064,10 +1079,6 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): even if the specified replied-to message is not found :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` - :param protect_content: Protects the contents of the sent - message from forwarding and saving - :type protect_content: :obj:`typing.Optional[base.Boolean]` - :return: On success, an array of the sent Messages is returned :rtype: typing.List[types.Message] """ @@ -1094,13 +1105,13 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): heading: typing.Optional[base.Integer] = None, proximity_alert_radius: typing.Optional[base.Integer] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None] = None, - protect_content: typing.Optional[base.Boolean] = None, ) -> types.Message: """ Use this method to send point on the map. @@ -1135,6 +1146,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :param disable_notification: Sends the message silently. Users will receive a notification with no sound :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param reply_to_message_id: If the message is a reply, ID of the original message :type reply_to_message_id: :obj:`typing.Optional[base.Integer]` @@ -1147,10 +1162,6 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` - :param protect_content: Protects the contents of the sent - message from forwarding and saving - :type protect_content: :obj:`typing.Optional[base.Boolean]` - :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -1264,13 +1275,13 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): google_place_id: typing.Optional[base.String] = None, google_place_type: typing.Optional[base.String] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None] = None, - protect_content: typing.Optional[base.Boolean] = None, ) -> types.Message: """ Use this method to send information about a venue. @@ -1310,6 +1321,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): a notification with no sound :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param reply_to_message_id: If the message is a reply, ID of the original message :type reply_to_message_id: :obj:`typing.Optional[base.Integer]` @@ -1325,10 +1340,6 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` - :param protect_content: Protects the contents of the sent - message from forwarding and saving - :type protect_content: :obj:`typing.Optional[base.Boolean]` - :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -1343,13 +1354,13 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): last_name: typing.Optional[base.String] = None, vcard: typing.Optional[base.String] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None] = None, - protect_content: typing.Optional[base.Boolean] = None, ) -> types.Message: """ Use this method to send phone contacts. @@ -1374,6 +1385,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :param disable_notification: Sends the message silently. Users will receive a notification with no sound :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param reply_to_message_id: If the message is a reply, ID of the original message :type reply_to_message_id: :obj:`typing.Optional[base.Integer]` @@ -1386,10 +1401,6 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` - :param protect_content: Protects the contents of the sent - message from forwarding and saving - :type protect_content: :obj:`typing.Optional[base.Boolean]` - :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -1418,13 +1429,13 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): None] = None, is_closed: typing.Optional[base.Boolean] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None] = None, - protect_content: typing.Optional[base.Boolean] = None, ) -> types.Message: """ Use this method to send a native poll. On success, the sent Message is @@ -1486,6 +1497,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): a notification with no sound. :type disable_notification: :obj:`typing.Optional[Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param reply_to_message_id: If the message is a reply, ID of the original message :type reply_to_message_id: :obj:`typing.Optional[Integer]` @@ -1501,10 +1516,6 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` - :param protect_content: Protects the contents of the sent - message from forwarding and saving - :type protect_content: :obj:`typing.Optional[base.Boolean]` - :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -1522,6 +1533,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): async def send_dice(self, chat_id: typing.Union[base.Integer, base.String], disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, emoji: typing.Optional[base.String] = None, reply_to_message_id: typing.Optional[base.Integer] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, @@ -1529,7 +1541,6 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None] = None, - protect_content: typing.Optional[base.Boolean] = None, ) -> types.Message: """ Use this method to send an animated emoji that will display a random value. @@ -1551,6 +1562,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): a notification with no sound :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param reply_to_message_id: If the message is a reply, ID of the original message :type reply_to_message_id: :obj:`typing.Optional[base.Integer]` @@ -1564,10 +1579,6 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` - :param protect_content: Protects the contents of the sent - message from forwarding and saving - :type protect_content: :obj:`typing.Optional[base.Boolean]` - :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -2814,13 +2825,13 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): async def send_sticker(self, chat_id: typing.Union[base.Integer, base.String], sticker: typing.Union[base.InputFile, base.String], disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None] = None, - protect_content: typing.Optional[base.Boolean] = None, ) -> types.Message: """ Use this method to send .webp stickers. @@ -2836,6 +2847,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :param disable_notification: Sends the message silently. Users will receive a notification with no sound :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param reply_to_message_id: If the message is a reply, ID of the original message :type reply_to_message_id: :obj:`typing.Optional[base.Integer]` @@ -2848,10 +2863,6 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` - :param protect_content: Protects the contents of the sent - message from forwarding and saving - :type protect_content: :obj:`typing.Optional[base.Boolean]` - :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -3136,10 +3147,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): send_email_to_provider: typing.Optional[base.Boolean] = None, is_flexible: typing.Optional[base.Boolean] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Optional[types.InlineKeyboardMarkup] = None, - protect_content: typing.Optional[base.Boolean] = None, ) -> types.Message: """ Use this method to send invoices. @@ -3236,6 +3247,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :param disable_notification: Sends the message silently. Users will receive a notification with no sound :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param reply_to_message_id: If the message is a reply, ID of the original message :type reply_to_message_id: :obj:`typing.Optional[base.Integer]` @@ -3247,10 +3262,6 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): If empty, one 'Pay total price' button will be shown. If not empty, the first button must be a Pay button. :type reply_markup: :obj:`typing.Optional[types.InlineKeyboardMarkup]` - :param protect_content: Protects the contents of the sent - message from forwarding and saving - :type protect_content: :obj:`typing.Optional[base.Boolean]` - :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -3360,10 +3371,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): chat_id: base.Integer, game_short_name: base.String, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Optional[types.InlineKeyboardMarkup] = None, - protect_content: typing.Optional[base.Boolean] = None, ) -> types.Message: """ Use this method to send a game. @@ -3380,6 +3391,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :param disable_notification: Sends the message silently. Users will receive a notification with no sound :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param reply_to_message_id: If the message is a reply, ID of the original message :type reply_to_message_id: :obj:`typing.Optional[base.Integer]` @@ -3391,10 +3406,6 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): If empty, one ‘Play game_title’ button will be shown. If not empty, the first button must launch the game. :type reply_markup: :obj:`typing.Optional[types.InlineKeyboardMarkup]` - :param protect_content: Protects the contents of the sent - message from forwarding and saving - :type protect_content: :obj:`typing.Optional[base.Boolean]` - :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ From 6b15b1977713a9840610d964ace0e5829e8f4cc1 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Fri, 31 Dec 2021 01:19:45 +0300 Subject: [PATCH 35/43] feat: add protect to shortcuts --- aiogram/types/message.py | 206 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 203 insertions(+), 3 deletions(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index fa73f2e4..10ef8776 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -315,6 +315,7 @@ class Message(base.TelegramObject): entities: typing.Optional[typing.List[MessageEntity]] = None, disable_web_page_preview: typing.Optional[base.Boolean] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, @@ -345,6 +346,10 @@ class Message(base.TelegramObject): :param disable_notification: Sends the message silently. Users will receive a notification with no sound :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param allow_sending_without_reply: Pass True, if the message should be sent even if the specified replied-to message is not found :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` @@ -367,6 +372,7 @@ class Message(base.TelegramObject): entities=entities, disable_web_page_preview=disable_web_page_preview, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=self.message_id if reply else None, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -379,6 +385,7 @@ class Message(base.TelegramObject): parse_mode: typing.Optional[base.String] = None, caption_entities: typing.Optional[typing.List[MessageEntity]] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, @@ -411,6 +418,10 @@ class Message(base.TelegramObject): :param disable_notification: Sends the message silently. Users will receive a notification with no sound :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param allow_sending_without_reply: Pass True, if the message should be sent even if the specified replied-to message is not found :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` @@ -433,6 +444,7 @@ class Message(base.TelegramObject): parse_mode=parse_mode, caption_entities=caption_entities, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=self.message_id if reply else None, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -449,6 +461,7 @@ class Message(base.TelegramObject): title: typing.Optional[base.String] = None, thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, @@ -497,6 +510,10 @@ class Message(base.TelegramObject): :param disable_notification: Sends the message silently. Users will receive a notification with no sound. :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param allow_sending_without_reply: Pass True, if the message should be sent even if the specified replied-to message is not found :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` @@ -523,6 +540,7 @@ class Message(base.TelegramObject): title=title, thumb=thumb, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=self.message_id if reply else None, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -539,6 +557,7 @@ class Message(base.TelegramObject): parse_mode: typing.Optional[base.String] = None, caption_entities: typing.Optional[typing.List[MessageEntity]] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, @@ -589,6 +608,10 @@ class Message(base.TelegramObject): :param disable_notification: Sends the message silently. Users will receive a notification with no sound :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param allow_sending_without_reply: Pass True, if the message should be sent even if the specified replied-to message is not found :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` @@ -615,6 +638,7 @@ class Message(base.TelegramObject): parse_mode=parse_mode, caption_entities=caption_entities, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=self.message_id if reply else None, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -629,6 +653,7 @@ class Message(base.TelegramObject): caption_entities: typing.Optional[typing.List[MessageEntity]] = None, disable_content_type_detection: typing.Optional[base.Boolean] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, @@ -672,6 +697,10 @@ class Message(base.TelegramObject): notification with no sound :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param allow_sending_without_reply: Pass True, if the message should be sent even if the specified replied-to message is not found :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` @@ -698,6 +727,7 @@ class Message(base.TelegramObject): caption_entities=caption_entities, disable_content_type_detection=disable_content_type_detection, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=self.message_id if reply else None, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -715,6 +745,7 @@ class Message(base.TelegramObject): caption_entities: typing.Optional[typing.List[MessageEntity]] = None, supports_streaming: typing.Optional[base.Boolean] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, @@ -764,6 +795,10 @@ class Message(base.TelegramObject): :param disable_notification: Sends the message silently. Users will receive a notification with no sound. :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param allow_sending_without_reply: Pass True, if the message should be sent even if the specified replied-to message is not found :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` @@ -791,6 +826,7 @@ class Message(base.TelegramObject): caption_entities=caption_entities, supports_streaming=supports_streaming, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=self.message_id if reply else None, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -804,6 +840,7 @@ class Message(base.TelegramObject): caption_entities: typing.Optional[typing.List[MessageEntity]] = None, duration: typing.Optional[base.Integer] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, @@ -843,6 +880,10 @@ class Message(base.TelegramObject): :param disable_notification: Sends the message silently. Users will receive a notification with no sound. :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param allow_sending_without_reply: Pass True, if the message should be sent even if the specified replied-to message is not found :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` @@ -866,6 +907,7 @@ class Message(base.TelegramObject): caption_entities=caption_entities, duration=duration, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=self.message_id if reply else None, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -878,6 +920,7 @@ class Message(base.TelegramObject): length: typing.Optional[base.Integer] = None, thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, @@ -910,6 +953,10 @@ class Message(base.TelegramObject): :param disable_notification: Sends the message silently. Users will receive a notification with no sound. :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param allow_sending_without_reply: Pass True, if the message should be sent even if the specified replied-to message is not found :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` @@ -932,6 +979,7 @@ class Message(base.TelegramObject): length=length, thumb=thumb, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=self.message_id if reply else None, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -941,6 +989,7 @@ class Message(base.TelegramObject): self, media: typing.Union[MediaGroup, typing.List], disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply: base.Boolean = False, ) -> typing.List[Message]: @@ -959,6 +1008,10 @@ class Message(base.TelegramObject): a notification with no sound. :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param allow_sending_without_reply: Pass True, if the message should be sent even if the specified replied-to message is not found :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` @@ -973,6 +1026,7 @@ class Message(base.TelegramObject): self.chat.id, media=media, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=self.message_id if reply else None, allow_sending_without_reply=allow_sending_without_reply, ) @@ -983,6 +1037,7 @@ class Message(base.TelegramObject): longitude: base.Float, live_period: typing.Optional[base.Integer] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, horizontal_accuracy: typing.Optional[base.Float] = None, heading: typing.Optional[base.Integer] = None, @@ -1026,6 +1081,10 @@ class Message(base.TelegramObject): :param disable_notification: Sends the message silently. Users will receive a notification with no sound. :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param allow_sending_without_reply: Pass True, if the message should be sent even if the specified replied-to message is not found :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` @@ -1050,6 +1109,7 @@ class Message(base.TelegramObject): heading=heading, proximity_alert_radius=proximity_alert_radius, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=self.message_id if reply else None, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -1066,6 +1126,7 @@ class Message(base.TelegramObject): google_place_id: typing.Optional[base.String] = None, google_place_type: typing.Optional[base.String] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, @@ -1110,6 +1171,10 @@ class Message(base.TelegramObject): a notification with no sound :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param allow_sending_without_reply: Pass True, if the message should be sent even if the specified replied-to message is not found :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` @@ -1138,6 +1203,7 @@ class Message(base.TelegramObject): google_place_id=google_place_id, google_place_type=google_place_type, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=self.message_id if reply else None, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -1149,6 +1215,7 @@ class Message(base.TelegramObject): first_name: base.String, last_name: typing.Optional[base.String] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, @@ -1176,6 +1243,10 @@ class Message(base.TelegramObject): :param disable_notification: Sends the message silently. Users will receive a notification with no sound. :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param allow_sending_without_reply: Pass True, if the message should be sent even if the specified replied-to message is not found :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` @@ -1197,6 +1268,7 @@ class Message(base.TelegramObject): first_name=first_name, last_name=last_name, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=self.message_id if reply else None, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -1206,6 +1278,7 @@ class Message(base.TelegramObject): self, sticker: typing.Union[base.InputFile, base.String], disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, @@ -1227,6 +1300,10 @@ class Message(base.TelegramObject): :param disable_notification: Sends the message silently. Users will receive a notification with no sound. :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param allow_sending_without_reply: Pass True, if the message should be sent even if the specified replied-to message is not found :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` @@ -1246,6 +1323,7 @@ class Message(base.TelegramObject): chat_id=self.chat.id, sticker=sticker, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=self.message_id if reply else None, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -1266,6 +1344,7 @@ class Message(base.TelegramObject): close_date: typing.Union[base.Integer, datetime.datetime, datetime.timedelta, None] = None, is_closed: typing.Optional[base.Boolean] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, @@ -1332,6 +1411,10 @@ class Message(base.TelegramObject): a notification with no sound. :type disable_notification: :obj:`typing.Optional[Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param allow_sending_without_reply: Pass True, if the message should be sent even if the specified replied-to message is not found :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` @@ -1364,6 +1447,7 @@ class Message(base.TelegramObject): close_date=close_date, is_closed=is_closed, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=self.message_id if reply else None, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -1373,6 +1457,7 @@ class Message(base.TelegramObject): self, emoji: typing.Optional[base.String] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, @@ -1399,6 +1484,10 @@ class Message(base.TelegramObject): a notification with no sound :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param allow_sending_without_reply: Pass True, if the message should be sent even if the specified replied-to message is not found :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` @@ -1420,6 +1509,7 @@ class Message(base.TelegramObject): chat_id=self.chat.id, emoji=emoji, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=self.message_id if reply else None, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -1456,6 +1546,7 @@ class Message(base.TelegramObject): entities: typing.Optional[typing.List[MessageEntity]] = None, disable_web_page_preview: typing.Optional[base.Boolean] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, @@ -1486,6 +1577,10 @@ class Message(base.TelegramObject): :param disable_notification: Sends the message silently. Users will receive a notification with no sound :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param allow_sending_without_reply: Pass True, if the message should be sent even if the specified replied-to message is not found :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` @@ -1508,6 +1603,7 @@ class Message(base.TelegramObject): entities=entities, disable_web_page_preview=disable_web_page_preview, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=self.message_id if reply else None, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -1520,6 +1616,7 @@ class Message(base.TelegramObject): parse_mode: typing.Optional[base.String] = None, caption_entities: typing.Optional[typing.List[MessageEntity]] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, @@ -1552,6 +1649,10 @@ class Message(base.TelegramObject): :param disable_notification: Sends the message silently. Users will receive a notification with no sound :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param allow_sending_without_reply: Pass True, if the message should be sent even if the specified replied-to message is not found :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` @@ -1574,6 +1675,7 @@ class Message(base.TelegramObject): parse_mode=parse_mode, caption_entities=caption_entities, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=self.message_id if reply else None, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -1590,6 +1692,7 @@ class Message(base.TelegramObject): title: typing.Optional[base.String] = None, thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, @@ -1638,6 +1741,10 @@ class Message(base.TelegramObject): :param disable_notification: Sends the message silently. Users will receive a notification with no sound. :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param allow_sending_without_reply: Pass True, if the message should be sent even if the specified replied-to message is not found :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` @@ -1664,6 +1771,7 @@ class Message(base.TelegramObject): title=title, thumb=thumb, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=self.message_id if reply else None, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -1680,6 +1788,7 @@ class Message(base.TelegramObject): parse_mode: typing.Optional[base.String] = None, caption_entities: typing.Optional[typing.List[MessageEntity]] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, @@ -1730,6 +1839,10 @@ class Message(base.TelegramObject): :param disable_notification: Sends the message silently. Users will receive a notification with no sound :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param allow_sending_without_reply: Pass True, if the message should be sent even if the specified replied-to message is not found :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` @@ -1756,6 +1869,7 @@ class Message(base.TelegramObject): parse_mode=parse_mode, caption_entities=caption_entities, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=self.message_id if reply else None, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -1770,6 +1884,7 @@ class Message(base.TelegramObject): caption_entities: typing.Optional[typing.List[MessageEntity]] = None, disable_content_type_detection: typing.Optional[base.Boolean] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, @@ -1813,6 +1928,10 @@ class Message(base.TelegramObject): notification with no sound :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param allow_sending_without_reply: Pass True, if the message should be sent even if the specified replied-to message is not found :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` @@ -1839,6 +1958,7 @@ class Message(base.TelegramObject): caption_entities=caption_entities, disable_content_type_detection=disable_content_type_detection, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=self.message_id if reply else None, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -1856,6 +1976,7 @@ class Message(base.TelegramObject): caption_entities: typing.Optional[typing.List[MessageEntity]] = None, supports_streaming: typing.Optional[base.Boolean] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, @@ -1905,6 +2026,10 @@ class Message(base.TelegramObject): :param disable_notification: Sends the message silently. Users will receive a notification with no sound. :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param allow_sending_without_reply: Pass True, if the message should be sent even if the specified replied-to message is not found :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` @@ -1932,6 +2057,7 @@ class Message(base.TelegramObject): caption_entities=caption_entities, supports_streaming=supports_streaming, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=self.message_id if reply else None, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -1945,6 +2071,7 @@ class Message(base.TelegramObject): caption_entities: typing.Optional[typing.List[MessageEntity]] = None, duration: typing.Optional[base.Integer] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, @@ -1984,6 +2111,10 @@ class Message(base.TelegramObject): :param disable_notification: Sends the message silently. Users will receive a notification with no sound. :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param allow_sending_without_reply: Pass True, if the message should be sent even if the specified replied-to message is not found :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` @@ -2007,6 +2138,7 @@ class Message(base.TelegramObject): caption_entities=caption_entities, duration=duration, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=self.message_id if reply else None, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -2019,6 +2151,7 @@ class Message(base.TelegramObject): length: typing.Optional[base.Integer] = None, thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, @@ -2051,6 +2184,10 @@ class Message(base.TelegramObject): :param disable_notification: Sends the message silently. Users will receive a notification with no sound. :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param allow_sending_without_reply: Pass True, if the message should be sent even if the specified replied-to message is not found :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` @@ -2073,6 +2210,7 @@ class Message(base.TelegramObject): length=length, thumb=thumb, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=self.message_id if reply else None, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -2082,6 +2220,7 @@ class Message(base.TelegramObject): self, media: typing.Union[MediaGroup, typing.List], disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply: base.Boolean = True, ) -> typing.List[Message]: @@ -2100,6 +2239,10 @@ class Message(base.TelegramObject): a notification with no sound. :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param allow_sending_without_reply: Pass True, if the message should be sent even if the specified replied-to message is not found :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` @@ -2114,6 +2257,7 @@ class Message(base.TelegramObject): self.chat.id, media=media, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=self.message_id if reply else None, allow_sending_without_reply=allow_sending_without_reply, ) @@ -2124,6 +2268,7 @@ class Message(base.TelegramObject): longitude: base.Float, live_period: typing.Optional[base.Integer] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, horizontal_accuracy: typing.Optional[base.Float] = None, heading: typing.Optional[base.Integer] = None, proximity_alert_radius: typing.Optional[base.Integer] = None, @@ -2166,6 +2311,10 @@ class Message(base.TelegramObject): :param disable_notification: Sends the message silently. Users will receive a notification with no sound. :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, @@ -2186,6 +2335,7 @@ class Message(base.TelegramObject): heading=heading, proximity_alert_radius=proximity_alert_radius, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=self.message_id if reply else None, reply_markup=reply_markup, ) @@ -2201,6 +2351,7 @@ class Message(base.TelegramObject): google_place_id: typing.Optional[base.String] = None, google_place_type: typing.Optional[base.String] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, @@ -2245,6 +2396,10 @@ class Message(base.TelegramObject): a notification with no sound :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param allow_sending_without_reply: Pass True, if the message should be sent even if the specified replied-to message is not found :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` @@ -2273,6 +2428,7 @@ class Message(base.TelegramObject): google_place_id=google_place_id, google_place_type=google_place_type, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=self.message_id if reply else None, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -2284,6 +2440,7 @@ class Message(base.TelegramObject): first_name: base.String, last_name: typing.Optional[base.String] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, @@ -2311,6 +2468,10 @@ class Message(base.TelegramObject): :param disable_notification: Sends the message silently. Users will receive a notification with no sound. :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param allow_sending_without_reply: Pass True, if the message should be sent even if the specified replied-to message is not found :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` @@ -2332,6 +2493,7 @@ class Message(base.TelegramObject): first_name=first_name, last_name=last_name, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=self.message_id if reply else None, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -2352,6 +2514,7 @@ class Message(base.TelegramObject): close_date: typing.Union[base.Integer, datetime.datetime, datetime.timedelta, None] = None, is_closed: typing.Optional[base.Boolean] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, @@ -2418,6 +2581,10 @@ class Message(base.TelegramObject): a notification with no sound. :type disable_notification: :obj:`typing.Optional[Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param allow_sending_without_reply: Pass True, if the message should be sent even if the specified replied-to message is not found :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` @@ -2450,6 +2617,7 @@ class Message(base.TelegramObject): close_date=close_date, is_closed=is_closed, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=self.message_id if reply else None, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -2459,6 +2627,7 @@ class Message(base.TelegramObject): self, sticker: typing.Union[base.InputFile, base.String], disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, @@ -2480,6 +2649,10 @@ class Message(base.TelegramObject): :param disable_notification: Sends the message silently. Users will receive a notification with no sound. :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param allow_sending_without_reply: Pass True, if the message should be sent even if the specified replied-to message is not found :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` @@ -2499,6 +2672,7 @@ class Message(base.TelegramObject): chat_id=self.chat.id, sticker=sticker, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=self.message_id if reply else None, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -2508,6 +2682,7 @@ class Message(base.TelegramObject): self, emoji: typing.Optional[base.String] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, @@ -2534,6 +2709,10 @@ class Message(base.TelegramObject): a notification with no sound :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :param protect_content: Protects the contents of sent messages + from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :param allow_sending_without_reply: Pass True, if the message should be sent even if the specified replied-to message is not found :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` @@ -2555,6 +2734,7 @@ class Message(base.TelegramObject): chat_id=self.chat.id, emoji=emoji, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=self.message_id if reply else None, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -2564,6 +2744,7 @@ class Message(base.TelegramObject): self, chat_id: typing.Union[base.Integer, base.String], disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, ) -> Message: """ Forward this message @@ -2572,13 +2753,23 @@ class Message(base.TelegramObject): :param chat_id: Unique identifier for the target chat or username of the target channel :type chat_id: :obj:`typing.Union[base.Integer, base.String]` + :param disable_notification: Sends the message silently. Users will receive a notification with no sound :type disable_notification: :obj:`typing.Optional[base.Boolean]` + + :param protect_content: Protects the contents of the forwarded + message from forwarding and saving + :type protect_content: :obj:`typing.Optional[base.Boolean]` + :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ return await self.bot.forward_message( - chat_id, self.chat.id, self.message_id, disable_notification + chat_id=chat_id, + from_chat_id=self.chat.id, + message_id=self.message_id, + disable_notification=disable_notification, + protect_content=protect_content, ) async def edit_text( @@ -2795,7 +2986,8 @@ class Message(base.TelegramObject): return await self.bot.delete_message(self.chat.id, self.message_id) async def pin( - self, disable_notification: typing.Optional[base.Boolean] = None, + self, + disable_notification: typing.Optional[base.Boolean] = None, ) -> base.Boolean: """ Use this method to add a message to the list of pinned messages in a chat. @@ -2813,7 +3005,10 @@ class Message(base.TelegramObject): :return: Returns True on success :rtype: :obj:`base.Boolean` """ - return await self.chat.pin_message(self.message_id, disable_notification) + return await self.chat.pin_message( + message_id=self.message_id, + disable_notification=disable_notification, + ) async def unpin(self) -> base.Boolean: """ @@ -2836,6 +3031,7 @@ class Message(base.TelegramObject): self: Message, chat_id: typing.Union[str, int], disable_notification: typing.Optional[bool] = None, + protect_content: typing.Optional[base.Boolean] = None, disable_web_page_preview: typing.Optional[bool] = None, reply_to_message_id: typing.Optional[int] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, @@ -2848,6 +3044,7 @@ class Message(base.TelegramObject): :param chat_id: :param disable_notification: + :param protect_content: :param disable_web_page_preview: for text messages only :param reply_to_message_id: :param allow_sending_without_reply: @@ -2860,6 +3057,7 @@ class Message(base.TelegramObject): "reply_markup": reply_markup or self.reply_markup, "parse_mode": ParseMode.HTML, "disable_notification": disable_notification, + "protect_content": protect_content, "reply_to_message_id": reply_to_message_id, } text = self.html_text if (self.text or self.caption) else None @@ -2956,6 +3154,7 @@ class Message(base.TelegramObject): parse_mode: typing.Optional[base.String] = None, caption_entities: typing.Optional[typing.List[MessageEntity]] = None, disable_notification: typing.Optional[base.Boolean] = None, + protect_content: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[InlineKeyboardMarkup, @@ -2971,6 +3170,7 @@ class Message(base.TelegramObject): parse_mode=parse_mode, caption_entities=caption_entities, disable_notification=disable_notification, + protect_content=protect_content, reply_to_message_id=reply_to_message_id, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup From c61410b5269614f8e26e98362c7d096bb5e843ce Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sat, 1 Jan 2022 13:14:15 +0300 Subject: [PATCH 36/43] fix: add span to MD_SYMBOLS --- aiogram/utils/markdown.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aiogram/utils/markdown.py b/aiogram/utils/markdown.py index 3b50ffd4..1a8b7fa3 100644 --- a/aiogram/utils/markdown.py +++ b/aiogram/utils/markdown.py @@ -12,6 +12,7 @@ MD_SYMBOLS = ( ("", ""), ("", ""), ("
", "
"), + ('', ""), ) HTML_QUOTES_MAP = {"<": "<", ">": ">", "&": "&", '"': """} From c0e8aa34c6fb68af42b05876d23dfa13b81703bd Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sat, 1 Jan 2022 14:41:40 +0300 Subject: [PATCH 37/43] chore: remove redundant part of version Co-authored-by: evgfilim1 --- aiogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index a180b08d..3c8f6014 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -43,5 +43,5 @@ __all__ = ( 'utils', ) -__version__ = '2.18.0' +__version__ = '2.18' __api_version__ = '5.6' From 5cb7ecd4b2b67d77795793f667756b04d338f215 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sat, 1 Jan 2022 22:59:18 +0300 Subject: [PATCH 38/43] chore: add tag --- aiogram/utils/markdown.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aiogram/utils/markdown.py b/aiogram/utils/markdown.py index 1a8b7fa3..dfce1096 100644 --- a/aiogram/utils/markdown.py +++ b/aiogram/utils/markdown.py @@ -13,6 +13,7 @@ MD_SYMBOLS = ( ("", ""), ("
", "
"), ('', ""), + ("", ""), ) HTML_QUOTES_MAP = {"<": "<", ">": ">", "&": "&", '"': """} From 301a43e26b02297fec20b768ac2ce752e690a4d4 Mon Sep 17 00:00:00 2001 From: samuelfirst Date: Mon, 20 Sep 2021 23:11:38 +0300 Subject: [PATCH 39/43] Set default disable_web_page_preview --- aiogram/bot/base.py | 22 ++++++++++++++++++++++ aiogram/bot/bot.py | 5 ++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py index 600152f6..f885e6dc 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -37,6 +37,7 @@ class BaseBot: proxy_auth: Optional[aiohttp.BasicAuth] = None, validate_token: Optional[base.Boolean] = True, parse_mode: typing.Optional[base.String] = None, + disable_web_page_preview: Optional[base.Boolean] = None, timeout: typing.Optional[typing.Union[base.Integer, base.Float, aiohttp.ClientTimeout]] = None, server: TelegramAPIServer = TELEGRAM_PRODUCTION ): @@ -57,6 +58,8 @@ class BaseBot: :type validate_token: :obj:`bool` :param parse_mode: You can set default parse mode :type parse_mode: :obj:`str` + :param disable_web_page_preview: You can set default disable web page preview parameter + :type disable_web_page_preview: :obj:`bool` :param timeout: Request timeout :type timeout: :obj:`typing.Optional[typing.Union[base.Integer, base.Float, aiohttp.ClientTimeout]]` :param server: Telegram Bot API Server endpoint. @@ -107,6 +110,8 @@ class BaseBot: self.parse_mode = parse_mode + self.disable_web_page_preview = disable_web_page_preview + async def get_new_session(self) -> aiohttp.ClientSession: return aiohttp.ClientSession( connector=self._connector_class(**self._connector_init), @@ -333,5 +338,22 @@ class BaseBot: def parse_mode(self): self.parse_mode = None + @property + def disable_web_page_preview(self): + return getattr(self, '_disable_web_page_preview', None) + + @disable_web_page_preview.setter + def disable_web_page_preview(self, value): + if value is None: + setattr(self, '_disable_web_page_preview', None) + else: + if not isinstance(value, bool): + raise TypeError(f"Disable web page preview must be bool, not {type(value)}") + setattr(self, '_disable_web_page_preview', value) + + @disable_web_page_preview.deleter + def disable_web_page_preview(self): + self.disable_web_page_preview = None + def check_auth_widget(self, data): return check_integrity(self.__token, data) diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 6653cf89..e5964fa0 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -323,6 +323,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): payload = generate_payload(**locals()) if self.parse_mode and entities is None: payload.setdefault('parse_mode', self.parse_mode) + if self.disable_web_page_preview: + payload.setdefault('disable_web_page_preview', self.disable_web_page_preview) result = await self.request(api.Methods.SEND_MESSAGE, payload) return types.Message(**result) @@ -422,7 +424,6 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): reply_markup = prepare_arg(reply_markup) caption_entities = prepare_arg(caption_entities) payload = generate_payload(**locals()) - if self.parse_mode and caption_entities is None: payload.setdefault('parse_mode', self.parse_mode) @@ -2543,6 +2544,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): payload = generate_payload(**locals()) if self.parse_mode and entities is None: payload.setdefault('parse_mode', self.parse_mode) + if self.disable_web_page_preview: + payload.setdefault('disable_web_page_preview', self.disable_web_page_preview) result = await self.request(api.Methods.EDIT_MESSAGE_TEXT, payload) if isinstance(result, bool): From 2b1c72c08c7c42a23a39285c3130a26527e90217 Mon Sep 17 00:00:00 2001 From: samuelfirst Date: Mon, 20 Sep 2021 23:11:50 +0300 Subject: [PATCH 40/43] Use global disable_web_page_preview in InputTextMessageContent --- aiogram/types/input_message_content.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/aiogram/types/input_message_content.py b/aiogram/types/input_message_content.py index f0c452cd..8406bd05 100644 --- a/aiogram/types/input_message_content.py +++ b/aiogram/types/input_message_content.py @@ -154,6 +154,12 @@ class InputTextMessageContent(InputMessageContent): except RuntimeError: pass + def safe_get_disable_web_page_preview(self): + try: + return self.bot.disable_web_page_preview + except RuntimeError: + pass + def __init__( self, message_text: base.String, @@ -163,6 +169,8 @@ class InputTextMessageContent(InputMessageContent): ): if parse_mode is None: parse_mode = self.safe_get_parse_mode() + if disable_web_page_preview is None: + disable_web_page_preview = self.safe_get_disable_web_page_preview() super().__init__( message_text=message_text, From 859876bed19a812fd126dc937e91600867b8a2f7 Mon Sep 17 00:00:00 2001 From: samuelfirst Date: Mon, 20 Sep 2021 23:12:05 +0300 Subject: [PATCH 41/43] Use global disable_web_page_preview in SendMessage and EditMessageText classes --- aiogram/dispatcher/webhook.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/aiogram/dispatcher/webhook.py b/aiogram/dispatcher/webhook.py index 11e3238c..92e475ff 100644 --- a/aiogram/dispatcher/webhook.py +++ b/aiogram/dispatcher/webhook.py @@ -435,6 +435,18 @@ class DisableWebPagePreviewMixin: setattr(self, 'disable_web_page_preview', True) return self + @staticmethod + def _global_disable_web_page_preview(): + """ + Detect global disable web page preview value + + :return: + """ + from aiogram import Bot + bot = Bot.get_current() + if bot is not None: + return bot.disable_web_page_preview + class ParseModeMixin: def as_html(self): @@ -506,6 +518,8 @@ class SendMessage(BaseResponse, ReplyToMixin, ParseModeMixin, DisableNotificatio text = '' if parse_mode is None: parse_mode = self._global_parse_mode() + if disable_web_page_preview is None: + disable_web_page_preview = self._global_disable_web_page_preview() self.chat_id = chat_id self.text = text @@ -1591,6 +1605,8 @@ class EditMessageText(BaseResponse, ParseModeMixin, DisableWebPagePreviewMixin): """ if parse_mode is None: parse_mode = self._global_parse_mode() + if disable_web_page_preview is None: + disable_web_page_preview = self._global_disable_web_page_preview() self.chat_id = chat_id self.message_id = message_id From a0510b33b92dda65670d5c20c5bad1020a4e71dd Mon Sep 17 00:00:00 2001 From: Ramzan Bekbulatov Date: Sun, 2 Jan 2022 22:03:07 +0300 Subject: [PATCH 42/43] Remove unused code from fsm.py (#780) --- aiogram/contrib/middlewares/fsm.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/aiogram/contrib/middlewares/fsm.py b/aiogram/contrib/middlewares/fsm.py index e3550a34..d198417f 100644 --- a/aiogram/contrib/middlewares/fsm.py +++ b/aiogram/contrib/middlewares/fsm.py @@ -1,5 +1,4 @@ import copy -import weakref from aiogram.dispatcher.middlewares import LifetimeControllerMiddleware from aiogram.dispatcher.storage import FSMContext @@ -8,10 +7,6 @@ from aiogram.dispatcher.storage import FSMContext class FSMMiddleware(LifetimeControllerMiddleware): skip_patterns = ['error', 'update'] - def __init__(self): - super(FSMMiddleware, self).__init__() - self._proxies = weakref.WeakKeyDictionary() - async def pre_process(self, obj, data, *args): proxy = await FSMSStorageProxy.create(self.manager.dispatcher.current_state()) data['state_data'] = proxy From 4d2d81138681d730270819579f22b3a0001c43a5 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sun, 2 Jan 2022 23:36:44 +0300 Subject: [PATCH 43/43] docs: bot API version update (#802) --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 6df651a2..0ec4b454 100644 --- a/README.rst +++ b/README.rst @@ -21,7 +21,7 @@ AIOGramBot :target: https://pypi.python.org/pypi/aiogram :alt: Supported python versions -.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.3-blue.svg?style=flat-square&logo=telegram +.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.6-blue.svg?style=flat-square&logo=telegram :target: https://core.telegram.org/bots/api :alt: Telegram Bot API