From f98c8eb01e20da4c45291e7ffc9e58f3bc273b20 Mon Sep 17 00:00:00 2001 From: AlberLC Date: Wed, 22 Jun 2022 09:08:00 +0200 Subject: [PATCH] Upgrade buttons management and add polls --- README.rst | 2 +- multibot/bots/discord_bot.py | 45 +++++++++++++++++++++----- multibot/bots/multi_bot.py | 59 ++++++++++++++++++++++------------- multibot/bots/telegram_bot.py | 52 +++++++++++++++++++++--------- multibot/bots/twitch_bot.py | 1 + multibot/constants.py | 3 ++ multibot/models/message.py | 2 +- 7 files changed, 118 insertions(+), 46 deletions(-) diff --git a/README.rst b/README.rst index df94c2d..d3955dc 100644 --- a/README.rst +++ b/README.rst @@ -430,7 +430,7 @@ For both a normal bot and a user bot (bot using your "human" account) you will n :target: https://www.python.org/downloads/ :alt: PyPI - Python Version -.. |multiBot_class_diagram| image:: https://user-images.githubusercontent.com/37489786/173702357-d53b3d52-987a-447b-9a75-e5520d08f56c.png +.. |multiBot_class_diagram| image:: https://user-images.githubusercontent.com/37489786/174965487-c83fb40a-65f9-4796-b661-2eb93883fe5d.png :alt: multiBot_class_diagram .. |my.telegram.org_app| image:: https://user-images.githubusercontent.com/37489786/149607226-36b0e3d6-6e21-4852-a08f-16ce52d3a7dc.png diff --git a/multibot/bots/discord_bot.py b/multibot/bots/discord_bot.py index 62edb60..5fa4914 100644 --- a/multibot/bots/discord_bot.py +++ b/multibot/bots/discord_bot.py @@ -35,7 +35,10 @@ async def _accept_button_event(self, event: constants.DISCORD_EVENT | Message): case Message(): event = event.original_event - await event.response.defer() + try: + await event.response.defer() + except AttributeError: + pass def _add_handlers(self): super()._add_handlers() @@ -313,7 +316,7 @@ async def find_users_by_roles(self, roles: Iterable[int | str | Role], group_: i return users @return_if_first_empty(exclude_self_types='DiscordBot', globals_=globals()) - async def get_chat(self, chat: int | str | User | Chat | Message = None) -> Chat | None: + async def get_chat(self, chat: int | str | User | Chat | Message) -> Chat | None: match chat: case int(chat_id): pass @@ -339,6 +342,21 @@ async def get_me(self, group_: int | str | Chat | Message = None) -> User | None else: return await self.get_user(user, group_) + @return_if_first_empty(exclude_self_types='DiscordBot', globals_=globals()) + async def get_message(self, chat: int | str | User | Chat | Message, message: int | str | Message) -> Message | None: + match message: + case int(message_id): + pass + case str(message_id): + message_id = int(message_id) + case Message(): + return message + case _: + raise TypeError('bad arguments') + + chat = await self.get_chat(chat) + return await self._get_message(await chat.original_object.fetch_message(message_id)) + @return_if_first_empty(exclude_self_types='DiscordBot', globals_=globals()) async def get_roles(self, group_: int | str | Chat | Message) -> list[Role]: if not (discord_group := await self._get_discord_group(group_)): @@ -416,7 +434,7 @@ async def remove_role(self, user: int | str | User, group_: int | str | Chat | M @parse_arguments async def send( self, - text='', + text: str = None, media: Media = None, buttons: list[str | tuple[str, bool] | Button | list[str | tuple[str, bool] | Button]] | None = None, chat: int | str | User | Chat | Message | None = None, @@ -424,6 +442,7 @@ async def send( *, buttons_key: Any = None, reply_to: int | str | Message = None, + contents: dict = None, silent: bool = False, send_as_file: bool = None, edit=False @@ -442,6 +461,8 @@ async def send( if edit: kwargs = {} + if text is not None: + kwargs['content'] = text if file: kwargs['attachments'] = [file] if buttons is not None: @@ -450,9 +471,16 @@ async def send( if buttons_key is not None: message.buttons_info.key = buttons_key - message.original_object = await message.original_object.edit(content=text, **kwargs) - if content := getattr(media, 'content', None): - message.contents = [content] + message.original_object = await message.original_object.edit(**kwargs) + last_contents = message.contents + if media_content := getattr(media, 'content', None): + del last_contents['media'] + message.contents = {'media': media_content} + if contents is None: + message.contents |= last_contents + else: + message.contents |= contents + message.update_last_edit() message.save() @@ -479,8 +507,9 @@ async def send( raise e bot_message.buttons_info = ButtonsInfo(buttons=buttons, key=buttons_key) - if content := getattr(media, 'content', None): - bot_message.contents = [content] + bot_message.contents = {'media': getattr(media, 'content', None)} + if contents is not None: + bot_message.contents |= contents bot_message.save() diff --git a/multibot/bots/multi_bot.py b/multibot/bots/multi_bot.py index 1d53a38..7ceae55 100644 --- a/multibot/bots/multi_bot.py +++ b/multibot/bots/multi_bot.py @@ -25,6 +25,7 @@ import telethon.events import telethon.events.common from flanautils import AmbiguityError, Media, NotFoundError, OrderedSet, RatioMatch, return_if_first_empty, shift_args_if_called + from multibot import constants from multibot.exceptions import LimitError, SendError from multibot.models import Ban, BotAction, Button, ButtonsGroup, Chat, Message, Mute, Platform, RegisteredButtonCallback, RegisteredCallback, Role, User @@ -59,10 +60,11 @@ def admin(func_: Callable = None, /, is_=True, send_negative=False) -> Callable: def decorator(func: Callable) -> Callable: @functools.wraps(func) @find_message - async def wrapper(self: MultiBot, message: Message, *args, **kwargs): + async def wrapper(self: MultiBot, message: Message): message = message if is_ == message.author.is_admin or message.chat.is_private: - return await func(self, message, *args, **kwargs) + return await func(self, message) + await self._accept_button_event(message) if send_negative: await self.send_negative(message) @@ -73,7 +75,9 @@ async def wrapper(self: MultiBot, message: Message, *args, **kwargs): def block(func: Callable) -> Callable: @functools.wraps(func) - async def wrapper(*_args, **_kwargs): + @find_message + async def wrapper(self: MultiBot, message: Message): + await self._accept_button_event(message) return return wrapper @@ -84,9 +88,10 @@ def bot_mentioned(func_: Callable = None, /, is_=True) -> Callable: def decorator(func: Callable) -> Callable: @functools.wraps(func) @find_message - async def wrapper(self: MultiBot, message: Message, *args, **kwargs): + async def wrapper(self: MultiBot, message: Message): if is_ == self.is_bot_mentioned(message): - return await func(self, message, *args, **kwargs) + return await func(self, message) + await self._accept_button_event(message) return wrapper @@ -98,9 +103,10 @@ def group(func_: Callable = None, /, is_=True) -> Callable: def decorator(func: Callable) -> Callable: @functools.wraps(func) @find_message - async def wrapper(self: MultiBot, message: Message, *args, **kwargs): + async def wrapper(self: MultiBot, message: Message): if is_ == message.chat.is_group: - return await func(self, message, *args, **kwargs) + return await func(self, message) + await self._accept_button_event(message) return wrapper @@ -110,9 +116,10 @@ async def wrapper(self: MultiBot, message: Message, *args, **kwargs): def ignore_self_message(func: Callable) -> Callable: @functools.wraps(func) @find_message - async def wrapper(self: MultiBot, message: Message, *args, **kwargs): + async def wrapper(self: MultiBot, message: Message): if message.author.id != self.id: - return await func(self, message, *args, **kwargs) + return await func(self, message) + await self._accept_button_event(message) return wrapper @@ -122,9 +129,10 @@ def inline(func_: Callable = None, /, is_=True) -> Callable: def decorator(func: Callable) -> Callable: @functools.wraps(func) @find_message - async def wrapper(self: MultiBot, message: Message, *args, **kwargs): + async def wrapper(self: MultiBot, message: Message): if message.is_inline is None or is_ == message.is_inline: - return await func(self, message, *args, **kwargs) + return await func(self, message) + await self._accept_button_event(message) return wrapper @@ -134,9 +142,10 @@ async def wrapper(self: MultiBot, message: Message, *args, **kwargs): def out_of_service(func: Callable) -> Callable: @functools.wraps(func) @find_message - async def wrapper(self: MultiBot, message: Message, *_args, **_kwargs): + async def wrapper(self: MultiBot, message: Message): if self.is_bot_mentioned(message) or message.chat.is_private: await self.send(random.choice(constants.OUT_OF_SERVICES_PHRASES), message) + await self._accept_button_event(message) return wrapper @@ -171,7 +180,7 @@ def parse_buttons(buttons_) -> list[list[Button]] | None: return buttons_ self: MultiBot | None = None - text = '' + text: str | None = None media: Media | None = None buttons: list[str | tuple[str, bool] | Button | list[str | tuple[str, bool] | Button]] | None = None chat: int | str | User | Chat | Message | None = None @@ -199,8 +208,8 @@ def parse_buttons(buttons_) -> list[list[Button]] | None: chat = await self.get_chat(kwargs.get('chat', chat)) if 'buttons' in kwargs: buttons = parse_buttons(kwargs['buttons']) - reply_to = kwargs.get('reply_to', None) - edit = kwargs.get('edit', None) + reply_to = kwargs.get('reply_to') + edit = kwargs.get('edit') if reply_to is not None: if chat: @@ -233,9 +242,10 @@ def reply(func_: Callable = None, /, is_=True) -> Callable: def decorator(func: Callable) -> Callable: @functools.wraps(func) @find_message - async def wrapper(self: MultiBot, message: Message, *args, **kwargs): + async def wrapper(self: MultiBot, message: Message): if is_ == bool(message.replied_message): - return await func(self, message, *args, **kwargs) + return await func(self, message) + await self._accept_button_event(message) return wrapper @@ -439,7 +449,7 @@ def _parse_callbacks( mached_keywords_groups = 0 total_ratio = 0 for keywords_group in registered_callback.keywords: - text_words += [original_text_word for original_text_word in original_text_words if original_text_word in keywords_group] + text_words += [original_text_word for original_text_word in original_text_words if flanautils.cartesian_product_string_matching(original_text_word, keywords_group, min_ratio=registered_callback.min_ratio)] word_matches = flanautils.cartesian_product_string_matching(text_words, keywords_group, min_ratio=registered_callback.min_ratio) ratio = sum((max(matches.values()) + 1) ** ratio_reward_exponent for matches in word_matches.values()) try: @@ -561,9 +571,9 @@ async def _on_users(self, message: Message): user_names = [f'<@{user.id}>' for user in await self.find_users_by_roles([], message)] joined_user_names = ', '.join(user_names) await self.send( - f"{len(user_names)} usuario{'' if len(user_names) == 1 else 's'}:\n" + f"{len(user_names)} usuario{'' if len(user_names) == 1 else 's'}:\n" f"{joined_user_names}\n\n" - f"Filtrar usuarios por roles:", + f"Filtrar usuarios por roles:", flanautils.chunks([f'❌ {role_name}' for role_name in role_names], 5), message, buttons_key=ButtonsGroup.USERS @@ -585,9 +595,9 @@ async def _on_users_button_press(self, message: Message): user_names = [f'<@{user.id}>' for user in await self.find_users_by_roles(selected_role_names, message)] joined_user_names = ', '.join(user_names) await self.edit( - f"{len(user_names)} usuario{'' if len(user_names) == 1 else 's'}:\n" + f"{len(user_names)} usuario{'' if len(user_names) == 1 else 's'}:\n" f"{joined_user_names}\n\n" - f"Filtrar usuarios por roles:", + f"Filtrar usuarios por roles:", message.buttons_info.buttons, message ) @@ -653,6 +663,10 @@ def get_group_name(self, group_: int | str | Chat | Message) -> str | None: async def get_me(self, group_: int | str | Chat | Message = None) -> User | None: pass + @return_if_first_empty(exclude_self_types='MultiBot', globals_=globals()) + async def get_message(self, chat: int | str | User | Chat | Message, message: int | str | Message) -> Message | None: + pass + @return_if_first_empty(exclude_self_types='MultiBot', globals_=globals()) async def get_role(self, role: int | str | Role, group_: int | str | Chat | Message) -> Role | None: if isinstance(role, Role): @@ -769,6 +783,7 @@ async def send( *, buttons_key: Any = None, reply_to: int | str | Message = None, + contents: dict = None, silent: bool = False, send_as_file: bool = None, edit=False diff --git a/multibot/bots/telegram_bot.py b/multibot/bots/telegram_bot.py index 13f0354..67e2555 100644 --- a/multibot/bots/telegram_bot.py +++ b/multibot/bots/telegram_bot.py @@ -73,7 +73,10 @@ async def _accept_button_event(self, event: constants.TELEGRAM_EVENT | Message): case Message(): event = event.original_event - await event.answer() + try: + await event.answer() + except AttributeError: + pass # noinspection PyTypeChecker def _add_handlers(self): @@ -83,12 +86,14 @@ def _add_handlers(self): self.client.add_event_handler(self._on_new_message_raw, telethon.events.NewMessage) @return_if_first_empty(exclude_self_types='TelegramBot', globals_=globals()) - async def _create_bot_message_from_telegram_bot_message(self, original_message: constants.TELEGRAM_MESSAGE, chat: Chat, buttons: list[list[Button]], buttons_key: Any = None, contents: Any = None) -> Message | None: + async def _create_bot_message_from_telegram_bot_message(self, original_message: constants.TELEGRAM_MESSAGE, media: Media, chat: Chat, buttons: list[list[Button]] = None, buttons_key: Any = None, contents: dict = None) -> Message | None: original_message._sender = await self.client.get_entity(self.id) original_message._chat = chat.original_object bot_message = await self._get_message(original_message) bot_message.buttons_info = ButtonsInfo(buttons=buttons, key=buttons_key) - bot_message.contents = contents or [] + bot_message.contents = {'media': getattr(media, 'content', None)} + if contents is not None: + bot_message.contents |= contents bot_message.save() return bot_message @@ -308,7 +313,7 @@ async def delete_message(self, message_to_delete: int | str | Message, chat: int message_to_delete.save() @return_if_first_empty(exclude_self_types='TelegramBot', globals_=globals()) - async def get_chat(self, chat: int | str | User | Chat | Message = None) -> Chat | None: + async def get_chat(self, chat: int | str | User | Chat | Message) -> Chat | None: match chat: case User() as user: if user.original_object: @@ -326,6 +331,21 @@ async def get_chat(self, chat: int | str | User | Chat | Message = None) -> Chat async def get_me(self, group_: int | str | Chat = None): return await self._create_user_from_telegram_user(await self.client.get_me(), group_) + @return_if_first_empty(exclude_self_types='TelegramBot', globals_=globals()) + async def get_message(self, chat: int | str | User | Chat | Message, message: int | str | Message) -> Message | None: + match message: + case int(message_id): + pass + case str(message_id): + pass + case Message(): + return message + case _: + raise TypeError('bad arguments') + + chat = await self.get_chat(chat) + return await self._get_message(await self.client.get_messages(chat.original_object, ids=message_id)) + @return_if_first_empty(exclude_self_types='TelegramBot', globals_=globals()) async def get_user(self, user: int | str | User, group_: int | str | Chat | Message = None) -> User | None: group_id = self.get_group_id(group_) @@ -349,6 +369,7 @@ async def send( *, buttons_key: Any = None, reply_to: int | str | Message = None, + contents: dict = None, silent: bool = False, send_as_file: bool = None, edit=False, @@ -377,10 +398,12 @@ async def send( if message.is_inline: if media: + if 'inline_media' not in message.contents: + message.contents['inline_media'] = [] if media.type_ is MediaType.IMAGE: - message.contents.append(message.original_event.builder.photo(file)) + message.contents['inline_media'].append(message.original_event.builder.photo(file)) else: - message.contents.append(message.original_event.builder.document(file, title=media.type_.name.title(), type=media.type_.name.lower())) + message.contents['inline_media'].append(message.original_event.builder.document(file, title=media.type_.name.title(), type=media.type_.name.lower())) elif edit: if buttons is not None: kwargs['buttons'] = telegram_buttons @@ -397,8 +420,14 @@ async def send( ): return else: - if content := getattr(media, 'content', None): - message.contents = [content] + last_contents = message.contents + if media_content := getattr(media, 'content', None): + del last_contents['media'] + message.contents = {'media': media_content} + if contents is None: + message.contents |= last_contents + else: + message.contents |= contents message.update_last_edit() message.save() @@ -415,12 +444,7 @@ async def send( except (telethon.errors.rpcerrorlist.PeerIdInvalidError, telethon.errors.rpcerrorlist.UserIsBlockedError): return - if content := getattr(media, 'content', None): - contents = [content] - else: - contents = [] - - return await self._create_bot_message_from_telegram_bot_message(original_message, chat, buttons, buttons_key, contents) + return await self._create_bot_message_from_telegram_bot_message(original_message, media, chat, buttons, buttons_key, contents) @inline async def send_inline_results(self, message: Message): diff --git a/multibot/bots/twitch_bot.py b/multibot/bots/twitch_bot.py index 9b40930..b7511a4 100644 --- a/multibot/bots/twitch_bot.py +++ b/multibot/bots/twitch_bot.py @@ -245,6 +245,7 @@ async def send( *, buttons_key: Any = None, reply_to: str | Message = None, + contents: dict = None, silent: bool = False, send_as_file: bool = None, edit=False, diff --git a/multibot/constants.py b/multibot/constants.py index 30a63ec..ef32d81 100644 --- a/multibot/constants.py +++ b/multibot/constants.py @@ -35,6 +35,7 @@ CHECK_MUTES_EVERY_SECONDS = datetime.timedelta(hours=1).total_seconds() COMMAND_MESSAGE_DURATION = 5 DELETE_MESSAGE_LIMIT = 100 +DISCORD_BUTTONS_MAX = 5 DISCORD_COMMAND_PREFIX = '/' DISCORD_MEDIA_MAX_BYTES = 8000000 ERROR_MESSAGE_DURATION = 10 @@ -82,6 +83,8 @@ 'send_as_file': ('arhivo', 'calidad', 'compress', 'compression', 'comprimir', 'file', 'quality'), 'show': ('actual', 'enseña', 'estado', 'how', 'muestra', 'show', 'como'), 'sound': ('hablar', 'hable', 'micro', 'microfono', 'microphone', 'sonido', 'sound', 'talk', 'volumen'), + 'stop': ('acabar', 'caducar', 'detener', 'end', 'expirar', 'expire', 'finalizar', 'finish', 'parar', 'stop', + 'suspend', 'suspender', 'terminar', 'terminate'), 'thanks': ('gracia', 'gracias', 'grasia', 'grasias', 'grax', 'thank', 'thanks', 'ty'), 'unban': ('desbanea', 'unban'), 'unmute': ('desilencia', 'desmutea', 'desmutealo', 'unmute'), diff --git a/multibot/models/message.py b/multibot/models/message.py index 1ab6053..09c66a7 100644 --- a/multibot/models/message.py +++ b/multibot/models/message.py @@ -31,7 +31,7 @@ class Message(EventComponent): date: datetime.datetime = field(default_factory=lambda: datetime.datetime.now(datetime.timezone.utc)) last_edit: datetime.datetime = None is_inline: bool = None - contents: list = field(default_factory=list) + contents: dict = field(default_factory=dict) is_deleted: bool = False original_object: constants.ORIGINAL_MESSAGE = None original_event: constants.MESSAGE_EVENT = None