mirror of
https://github.com/aiogram/aiogram.git
synced 2025-12-06 07:50:32 +00:00
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:
parent
b98ec3efad
commit
b190bbba19
22 changed files with 302 additions and 91 deletions
|
|
@ -6,7 +6,7 @@
|
|||
[](https://pypi.python.org/pypi/aiogram)
|
||||
[](https://pypi.python.org/pypi/aiogram)
|
||||
[](https://pypi.python.org/pypi/aiogram)
|
||||
[](https://core.telegram.org/bots/api)
|
||||
[](https://core.telegram.org/bots/api)
|
||||
[](http://docs.aiogram.dev/en/latest/?badge=latest)
|
||||
[](https://github.com/aiogram/aiogram/issues)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
|
|
|||
|
|
@ -43,5 +43,5 @@ __all__ = (
|
|||
'utils',
|
||||
)
|
||||
|
||||
__version__ = '2.15'
|
||||
__api_version__ = '5.3'
|
||||
__version__ = '2.16'
|
||||
__api_version__ = '5.4'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
33
aiogram/types/chat_join_request.py
Normal file
33
aiogram/types/chat_join_request.py
Normal 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,
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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. "
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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_'
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue