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