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
This commit is contained in:
Alex Root Junior 2021-11-07 01:39:51 +02:00 committed by GitHub
parent b98ec3efad
commit b190bbba19
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 302 additions and 91 deletions

View file

@ -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)

View file

@ -43,5 +43,5 @@ __all__ = (
'utils',
)
__version__ = '2.15'
__api_version__ = '5.3'
__version__ = '2.16'
__api_version__ = '5.4'

View file

@ -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

View file

@ -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:

View file

@ -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.

View file

@ -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:

View file

@ -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)

View file

@ -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):
"""

View file

@ -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

View file

@ -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):
"""

View file

@ -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',

View file

@ -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)

View file

@ -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()

View file

@ -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,
)

View file

@ -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

View file

@ -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. "

View file

@ -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()

View file

@ -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

View file

@ -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_'

View file

@ -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

View file

@ -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

View file

@ -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