diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 1bf00d47..164711c7 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -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/bot.py b/aiogram/bot/bot.py index 66af31c7..8c08c856 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -1840,6 +1840,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. @@ -1861,6 +1863,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` """ @@ -1876,6 +1885,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. @@ -1899,6 +1910,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) @@ -1929,6 +1948,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: """ @@ -2129,7 +2201,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/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..17b28e8d 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -80,6 +80,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) @@ -159,13 +160,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 +178,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 +205,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 +309,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: @@ -980,14 +989,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 +1016,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 +1035,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 +1152,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 @@ -1382,6 +1447,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 +1477,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin): asyncio.get_running_loop().run_in_executor( None, partial_func ) + return wrapped return decorator 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..2893a715 --- /dev/null +++ b/aiogram/types/chat_join_request.py @@ -0,0 +1,21 @@ +from datetime import datetime + +from . import base +from . import fields +from .user import User +from .chat import Chat +from .chat_invite_link import ChatInviteLink + + +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) 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. "