commit 99853339c2623f06562d29fba3d5ab1ae57cdf1b Author: AlberLC Date: Thu Jan 6 05:18:42 2022 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e53da30 --- /dev/null +++ b/.gitignore @@ -0,0 +1,161 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST +pyproject.toml +setup.cfg +-------- + + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit tests / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# Visual studio code +.vscode + +#private keys and certs +cert.pem +private.key + +# PyCharm +.idea + +# Databases +*.db + +# Deprecateds +*.dep + +# Telethon sessions +*.session* \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..40ecf9b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 AlberLC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..4c3789f --- /dev/null +++ b/README.rst @@ -0,0 +1,16 @@ +FlanaBot +======== + +.. image:: https://img.shields.io/github/license/AlberLC/flanabot?style=flat + :target: https://github.com/AlberLC/flanabot/blob/main/LICENSE + :alt: License + +Flanagan's bot. + +Installation +------------ +Python 3.10 or higher is required. + +.. code-block:: python + + pip install flanabot \ No newline at end of file diff --git a/flanabot/bots/flana_bot.py b/flanabot/bots/flana_bot.py new file mode 100644 index 0000000..b693e58 --- /dev/null +++ b/flanabot/bots/flana_bot.py @@ -0,0 +1,806 @@ +import asyncio +import datetime +import itertools +import pprint +import random +from abc import ABC +from typing import Iterable, Iterator, Type + +import flanaapis.geolocation.functions +import flanaapis.weather.functions +import flanautils +import plotly.graph_objects +from flanaapis import InstagramLoginError, MediaNotFoundError, Place, PlaceNotFoundError, WeatherEmoji, instagram, tiktok, twitter +from flanautils import Media, MediaType, NotFoundError, OrderedSet, Source, TimeUnits, TraceMetadata, return_if_first_empty +from multibot import Action, BotAction, MultiBot, SendError, admin, bot_mentioned, constants as multibot_constants, group, ignore_self_message, inline, reply + +from flanabot import constants +from flanabot.exceptions import BadRoleError, UserDisconnectedError +from flanabot.models.chat import Chat +from flanabot.models.message import Message +from flanabot.models.punishments import Mute, Punishment, PunishmentBase +from flanabot.models.weather_chart import WeatherChart + + +# ----------------------------------------------------------------------------------------------------- # +# --------------------------------------------- FLANA_BOT --------------------------------------------- # +# ----------------------------------------------------------------------------------------------------- # + + +class FlanaBot(MultiBot, ABC): + # ----------------------------------------------------------- # + # -------------------- PROTECTED METHODS -------------------- # + # ----------------------------------------------------------- # + def _add_handlers(self): + super()._add_handlers() + + self.register(self._on_bye, constants.KEYWORDS['bye'], min_ratio=1) + + self.register(self._on_config_list_show, constants.KEYWORDS['config']) + self.register(self._on_config_list_show, constants.KEYWORDS['help']) + self.register(self._on_config_list_show, (constants.KEYWORDS['show'], constants.KEYWORDS['config'])) + self.register(self._on_config_list_show, (constants.KEYWORDS['help'], constants.KEYWORDS['config'])) + self.register(self._on_config_list_show, (constants.KEYWORDS['show'], constants.KEYWORDS['help'])) + self.register(self._on_config_list_show, (constants.KEYWORDS['show'], constants.KEYWORDS['help'], constants.KEYWORDS['config'])) + + self.register(self._on_covid_chart, constants.KEYWORDS['covid_chart']) + + self.register(self._on_currency_chart, constants.KEYWORDS['currency_chart']) + self.register(self._on_currency_chart, (constants.KEYWORDS['show'], constants.KEYWORDS['currency_chart'])) + + self.register(self._on_currency_chart_config_activate, (constants.KEYWORDS['activate'], constants.KEYWORDS['currency_chart'])) + self.register(self._on_currency_chart_config_activate, (constants.KEYWORDS['activate'], constants.KEYWORDS['currency_chart'], constants.KEYWORDS['config'])) + + self.register(self._on_currency_chart_config_change, (constants.KEYWORDS['change'], constants.KEYWORDS['currency_chart'])) + self.register(self._on_currency_chart_config_change, (constants.KEYWORDS['change'], constants.KEYWORDS['currency_chart'], constants.KEYWORDS['config'])) + + self.register(self._on_currency_chart_config_deactivate, (constants.KEYWORDS['deactivate'], constants.KEYWORDS['currency_chart'])) + self.register(self._on_currency_chart_config_deactivate, (constants.KEYWORDS['deactivate'], constants.KEYWORDS['currency_chart'], constants.KEYWORDS['config'])) + + self.register(self._on_currency_chart_config_show, (constants.KEYWORDS['currency_chart'], constants.KEYWORDS['config'])) + self.register(self._on_currency_chart_config_show, (constants.KEYWORDS['show'], constants.KEYWORDS['currency_chart'], constants.KEYWORDS['config'])) + + self.register(self._on_delete_original_config_activate, (constants.KEYWORDS['activate'], multibot_constants.KEYWORDS['delete'])) + self.register(self._on_delete_original_config_activate, (constants.KEYWORDS['activate'], multibot_constants.KEYWORDS['delete'], multibot_constants.KEYWORDS['message'])) + self.register(self._on_delete_original_config_activate, (constants.KEYWORDS['activate'], multibot_constants.KEYWORDS['delete'], constants.KEYWORDS['config'])) + self.register(self._on_delete_original_config_activate, (constants.KEYWORDS['activate'], multibot_constants.KEYWORDS['delete'], multibot_constants.KEYWORDS['message'], constants.KEYWORDS['config'])) + + self.register(self._on_delete_original_config_change, (constants.KEYWORDS['change'], multibot_constants.KEYWORDS['delete'])) + self.register(self._on_delete_original_config_change, (constants.KEYWORDS['change'], multibot_constants.KEYWORDS['delete'], multibot_constants.KEYWORDS['message'])) + self.register(self._on_delete_original_config_change, (constants.KEYWORDS['change'], multibot_constants.KEYWORDS['delete'], constants.KEYWORDS['config'])) + self.register(self._on_delete_original_config_change, (constants.KEYWORDS['change'], multibot_constants.KEYWORDS['delete'], multibot_constants.KEYWORDS['message'], constants.KEYWORDS['config'])) + + self.register(self._on_delete_original_config_deactivate, (constants.KEYWORDS['deactivate'], multibot_constants.KEYWORDS['delete'])) + self.register(self._on_delete_original_config_deactivate, (constants.KEYWORDS['deactivate'], multibot_constants.KEYWORDS['delete'], multibot_constants.KEYWORDS['message'])) + self.register(self._on_delete_original_config_deactivate, (constants.KEYWORDS['deactivate'], multibot_constants.KEYWORDS['delete'], constants.KEYWORDS['config'])) + self.register(self._on_delete_original_config_deactivate, (constants.KEYWORDS['deactivate'], multibot_constants.KEYWORDS['delete'], multibot_constants.KEYWORDS['message'], constants.KEYWORDS['config'])) + + self.register(self._on_delete_original_config_show, (constants.KEYWORDS['show'], multibot_constants.KEYWORDS['delete'])) + self.register(self._on_delete_original_config_show, (multibot_constants.KEYWORDS['delete'], constants.KEYWORDS['config'])) + self.register(self._on_delete_original_config_show, (constants.KEYWORDS['show'], multibot_constants.KEYWORDS['delete'], multibot_constants.KEYWORDS['message'])) + self.register(self._on_delete_original_config_show, (constants.KEYWORDS['show'], multibot_constants.KEYWORDS['delete'], constants.KEYWORDS['config'])) + self.register(self._on_delete_original_config_show, (multibot_constants.KEYWORDS['delete'], multibot_constants.KEYWORDS['message'], constants.KEYWORDS['config'])) + self.register(self._on_delete_original_config_show, (constants.KEYWORDS['show'], multibot_constants.KEYWORDS['delete'], multibot_constants.KEYWORDS['message'], constants.KEYWORDS['config'])) + + self.register(self._on_hello, constants.KEYWORDS['hello']) + + self.register(self._on_mute, constants.KEYWORDS['mute']) + self.register(self._on_mute, (constants.KEYWORDS['deactivate'], constants.KEYWORDS['unmute'])) + self.register(self._on_mute, (constants.KEYWORDS['deactivate'], constants.KEYWORDS['sound'])) + + self.register(self._on_new_message_default, default=True) + + self.register(self._on_no_delete_original, (constants.KEYWORDS['negate'], multibot_constants.KEYWORDS['delete'])) + self.register(self._on_no_delete_original, (constants.KEYWORDS['negate'], multibot_constants.KEYWORDS['message'])) + self.register(self._on_no_delete_original, (constants.KEYWORDS['negate'], multibot_constants.KEYWORDS['delete'], multibot_constants.KEYWORDS['message'])) + self.register(self._on_no_delete_original, (constants.KEYWORDS['deactivate'], multibot_constants.KEYWORDS['delete'], multibot_constants.KEYWORDS['message'])) + + self.register(self._on_punish, constants.KEYWORDS['punish']) + self.register(self._on_punish, (constants.KEYWORDS['deactivate'], constants.KEYWORDS['unpunish'])) + self.register(self._on_punish, (constants.KEYWORDS['deactivate'], constants.KEYWORDS['permission'])) + + self.register(self._on_recover_message, (constants.KEYWORDS['reset'], multibot_constants.KEYWORDS['message'])) + + self.register(self._on_scraping, constants.KEYWORDS['scraping']) + + self.register(self._on_scraping_config_activate, (constants.KEYWORDS['activate'], constants.KEYWORDS['scraping'])) + self.register(self._on_scraping_config_activate, (constants.KEYWORDS['activate'], constants.KEYWORDS['scraping'], constants.KEYWORDS['config'])) + + self.register(self._on_scraping_config_change, (constants.KEYWORDS['change'], constants.KEYWORDS['scraping'])) + self.register(self._on_scraping_config_change, (constants.KEYWORDS['change'], constants.KEYWORDS['scraping'], constants.KEYWORDS['config'])) + + self.register(self._on_scraping_config_deactivate, (constants.KEYWORDS['deactivate'], constants.KEYWORDS['scraping'])) + self.register(self._on_scraping_config_deactivate, (constants.KEYWORDS['deactivate'], constants.KEYWORDS['scraping'], constants.KEYWORDS['config'])) + + self.register(self._on_scraping_config_show, (constants.KEYWORDS['show'], constants.KEYWORDS['scraping'])) + self.register(self._on_scraping_config_show, (constants.KEYWORDS['scraping'], constants.KEYWORDS['config'])) + self.register(self._on_scraping_config_show, (constants.KEYWORDS['show'], constants.KEYWORDS['scraping'], constants.KEYWORDS['config'])) + + self.register(self._on_song_info, constants.KEYWORDS['song_info']) + + self.register(self._on_unmute, constants.KEYWORDS['unmute']) + self.register(self._on_unmute, (constants.KEYWORDS['deactivate'], constants.KEYWORDS['mute'])) + self.register(self._on_unmute, (constants.KEYWORDS['activate'], constants.KEYWORDS['sound'])) + + self.register(self._on_unpunish, constants.KEYWORDS['unpunish']) + + self.register(self._on_unpunish, (constants.KEYWORDS['deactivate'], constants.KEYWORDS['punish'])) + self.register(self._on_unpunish, (constants.KEYWORDS['activate'], constants.KEYWORDS['permission'])) + + self.register(self._on_weather_chart, constants.KEYWORDS['weather_chart']) + self.register(self._on_weather_chart, (constants.KEYWORDS['show'], constants.KEYWORDS['weather_chart'])) + + self.register(self._on_weather_chart_config_activate, (constants.KEYWORDS['activate'], constants.KEYWORDS['weather_chart'])) + self.register(self._on_weather_chart_config_activate, (constants.KEYWORDS['activate'], constants.KEYWORDS['weather_chart'], constants.KEYWORDS['config'])) + + self.register(self._on_weather_chart_config_change, (constants.KEYWORDS['change'], constants.KEYWORDS['weather_chart'])) + self.register(self._on_weather_chart_config_change, (constants.KEYWORDS['change'], constants.KEYWORDS['weather_chart'], constants.KEYWORDS['config'])) + + self.register(self._on_weather_chart_config_deactivate, (constants.KEYWORDS['deactivate'], constants.KEYWORDS['weather_chart'])) + self.register(self._on_weather_chart_config_deactivate, (constants.KEYWORDS['deactivate'], constants.KEYWORDS['weather_chart'], constants.KEYWORDS['config'])) + + self.register(self._on_weather_chart_config_show, (constants.KEYWORDS['weather_chart'], constants.KEYWORDS['config'])) + self.register(self._on_weather_chart_config_show, (constants.KEYWORDS['show'], constants.KEYWORDS['weather_chart'], constants.KEYWORDS['config'])) + + async def _change_config(self, config_name: str, message: Message, value: bool = None): + if value is None: + value = not message.chat.config[config_name] + + message.chat.config[config_name] = value + message.chat.save() + + await self.send(f"He {'activado ✔' if value else 'desactivado ❌'} {config_name}.", message) + + @admin(False) + @group + async def _check_message_flood(self, message: Message): + if message.author.is_punished: + return + + last_2s_messages = Message.find({ + 'author': message.author.object_id, + 'last_update': { + '$gte': datetime.datetime.now() - datetime.timedelta(seconds=2) + } + }) + last_7s_messages = Message.find({ + 'author': message.author.object_id, + 'last_update': { + '$gte': datetime.datetime.now() - datetime.timedelta(seconds=7), + '$lt': datetime.datetime.now() - datetime.timedelta(seconds=2) + } + }) + + if len(last_2s_messages) >= 5 or len(last_7s_messages) >= 7: + n_punishments = len(Punishment.find({'user_id': message.author.id, 'group_id': message.chat.group_id})) + punishment_seconds = (n_punishments + 2) ** constants.PUNISHMENT_INCREMENT_EXPONENT + try: + await self.punish(message.author.id, message.chat.group_id, punishment_seconds, message) + except BadRoleError as e: + await self._manage_exceptions(e, message) + else: + # noinspection PyTypeChecker + Punishment(message.author.id, message.author.group_id, datetime.datetime.now() + datetime.timedelta(punishment_seconds)).save() + await self.send(f'Castigado durante {TimeUnits(punishment_seconds).to_words()}.', message) + + @staticmethod + async def _check_messages(): + Message.collection.delete_many({'last_update': {'$lte': datetime.datetime.now() - multibot_constants.MESSAGE_EXPIRATION_TIME}}) + + async def _check_mutes(self): + mute_groups = self._get_grouped_punishments(Mute) + + now = datetime.datetime.now() + for (user_id, group_id), sorted_mutes in mute_groups: + if (last_mute := sorted_mutes[-1]).until <= now: + await self.unmute(user_id, group_id) + last_mute.delete() + + async def _check_punishments(self): + punishment_groups = self._get_grouped_punishments(Punishment) + + now = datetime.datetime.now() + for (user_id, group_id), sorted_punishments in punishment_groups: + if now < (last_punishment := sorted_punishments[-1]).until: + continue + + if last_punishment.until + constants.PUNISHMENTS_RESET <= now: + for old_punishment in sorted_punishments: + old_punishment.delete() + + if last_punishment.is_active: + await self.unpunish(user_id, group_id) + last_punishment.is_active = False + last_punishment.save() + + @staticmethod + def _get_grouped_punishments(PunishmentClass: Type[PunishmentBase]) -> tuple[tuple[tuple[int, int], list[PunishmentBase]]]: + sorted_punishments = PunishmentClass.find(sort_keys=('user_id', 'group_id', 'until')) + group_iterator: Iterator[ + tuple[ + tuple[int, int], + Iterator[PunishmentClass] + ] + ] = itertools.groupby(sorted_punishments, key=lambda punishment: (punishment.user_id, punishment.group_id)) + return tuple(((user_id, group_id), list(group_)) for (user_id, group_id), group_ in group_iterator) + + @return_if_first_empty(exclude_self_types='FlanaBot', globals_=globals()) + async def _manage_exceptions(self, exceptions: BaseException | Iterable[BaseException], message: Message): + if not isinstance(exceptions, Iterable): + exceptions = (exceptions,) + + for exception in exceptions: + try: + raise exception + except BadRoleError as e: + await self.send_error(f'Rol no encontrado en {e}', message) + except InstagramLoginError as e: + await self.send_error(f'No me puedo loguear en Instagram {random.choice(multibot_constants.SAD_EMOJIS)} 👉 {e}', message) + except MediaNotFoundError as e: + await self.send_error(f'No he podido sacar nada de {e.source} {random.choice(multibot_constants.SAD_EMOJIS)}', message) + except PlaceNotFoundError as e: + await self.send_error(f'No he podido encontrar "{e}" {random.choice(multibot_constants.SAD_EMOJIS)}', message) + except Exception as e: + await super()._manage_exceptions(e, message) + + @staticmethod + def _medias_sended_info(medias: Iterable[Media]) -> str: + medias_count = { + Source.TWITTER: {MediaType.IMAGE: 0, MediaType.AUDIO: 0, MediaType.GIF: 0, MediaType.VIDEO: 0, None: 0, MediaType.ERROR: 0}, + Source.INSTAGRAM: {MediaType.IMAGE: 0, MediaType.AUDIO: 0, MediaType.GIF: 0, MediaType.VIDEO: 0, None: 0, MediaType.ERROR: 0}, + Source.TIKTOK: {MediaType.IMAGE: 0, MediaType.AUDIO: 0, MediaType.GIF: 0, MediaType.VIDEO: 0, None: 0, MediaType.ERROR: 0}, + Source.REDDIT: {MediaType.IMAGE: 0, MediaType.AUDIO: 0, MediaType.GIF: 0, MediaType.VIDEO: 0, None: 0, MediaType.ERROR: 0} + } + for media in medias: + medias_count[media.source][media.type_] += 1 + + medias_sended_info = [] + for source, media_type_count in medias_count.items(): + source_medias_sended_info = [] + for media_type, count in media_type_count.items(): + if count: + if count == 1: + type_text = {MediaType.IMAGE: 'imagen', + MediaType.AUDIO: 'audio', + MediaType.GIF: 'gif', + MediaType.VIDEO: 'vídeo', + None: 'cosa que no sé que tipo de archivo es', + MediaType.ERROR: 'error'}[media_type] + else: + type_text = {MediaType.IMAGE: 'imágenes', + MediaType.AUDIO: 'audios', + MediaType.GIF: 'gifs', + MediaType.VIDEO: 'vídeos', + None: 'cosas que no sé que tipos de archivos son', + MediaType.ERROR: 'errores'}[media_type] + source_medias_sended_info.append(f'{count} {type_text}') + if source_medias_sended_info: + medias_sended_info.append(f"{flanautils.join_last_separator(source_medias_sended_info, ', ', ' y ')} de {source.name}") + + medias_sended_info_joined = flanautils.join_last_separator(medias_sended_info, ',\n', ' y\n') + new_line = ' ' if len(medias_sended_info) == 1 else '\n' + return f'{new_line}{medias_sended_info_joined}:' + + async def _mute(self, user_id: int, group_id: int): + pass + + async def _punish(self, user_id: int, group_id: int): + pass + + async def _search_and_send_medias(self, message: Message, send_song_info=False) -> list[Message]: + sended_media_messages = [] + if medias := await self._search_medias(message): + sended_media_messages, _ = await self.send_medias(medias, message, send_song_info) + + return sended_media_messages + + async def _search_medias(self, message: Message) -> OrderedSet[Media]: + results: tuple[OrderedSet[Media] | BaseException, ...] = await asyncio.gather( + twitter.get_medias(message.text), + instagram.get_medias(message.text), + tiktok.get_medias(message.text), + return_exceptions=True + ) + results, exceptions = flanautils.filter_exceptions(results) + + await self._manage_exceptions(exceptions, message) + + return OrderedSet(*results) + + async def _show_config(self, config_name: str, message: Message): + await self.send(f"{config_name} está {'activado ✔' if message.chat.config.get(config_name) else 'desactivado ❌'}", message) + + async def _unmute(self, user_id: int, group_id: int): + pass + + async def _unpunish(self, user_id: int, group_id: int): + pass + + # ---------------------------------------------- # + # HANDLERS # + # ---------------------------------------------- # + async def _on_bye(self, message: Message): + if not message.chat.is_group or self.is_bot_mentioned(message): + await self.send_bye(message) + + @group + @bot_mentioned + async def _on_config_list_show(self, message: Message): + config_info = pprint.pformat(message.chat.config) + config_info = flanautils.translate(config_info, {'{': None, '}': None, ',': None, 'True': '✔', "'": None, 'False': '❌'}) + config_info = config_info.splitlines() + config_info = '\n'.join(config_info_.strip() for config_info_ in config_info) + await self.send(f'Estos son los ajustes del grupo:\n\n{config_info}', message) + + async def _on_covid_chart(self, message: Message): # todo2 + pass + + async def _on_currency_chart(self, message: Message): # todo2 + pass + + @admin + @group + @bot_mentioned + async def _on_currency_chart_config_activate(self, message: Message): + await self._change_config('auto_currency_chart', message, True) + + @admin + @group + @bot_mentioned + async def _on_currency_chart_config_change(self, message: Message): + await self._change_config('auto_currency_chart', message) + + @admin + @group + @bot_mentioned + async def _on_currency_chart_config_deactivate(self, message: Message): + await self._change_config('auto_currency_chart', message, False) + + @admin + @group + @bot_mentioned + async def _on_currency_chart_config_show(self, message: Message): + await self._show_config('auto_currency_chart', message) + + @admin + @group + @bot_mentioned + async def _on_delete_original_config_activate(self, message: Message): + await self._change_config('auto_delete_original', message, True) + + @admin + @group + @bot_mentioned + async def _on_delete_original_config_change(self, message: Message): + await self._change_config('auto_delete_original', message) + + @admin + @group + @bot_mentioned + async def _on_delete_original_config_deactivate(self, message: Message): + await self._change_config('auto_delete_original', message, False) + + @admin + @group + @bot_mentioned + async def _on_delete_original_config_show(self, message: Message): + await self._show_config('auto_delete_original', message) + + async def _on_hello(self, message: Message): + if not message.chat.is_group or self.is_bot_mentioned(message): + await self.send_hello(message) + + @group + @bot_mentioned + @admin(send_negative=True) + async def _on_mute(self, message: Message): + await self._update_punishment(self.mute, message, time=flanautils.words_to_time(message.text)) + + async def _on_new_message_default(self, message: Message): + if message.is_inline: + await self._on_scraping(message) + elif ( + message.chat.is_group + and + not self.is_bot_mentioned(message) + and + not message.chat.config['auto_scraping'] + or + not await self._on_scraping(message) + ): + await self.send_bad_phrase(message) + + @ignore_self_message + async def _on_new_message_raw(self, message: Message): + await super()._on_new_message_raw(message) + if not message.is_inline: + await self._check_message_flood(message) + + async def _on_no_delete_original(self, message: Message): + if not await self._on_scraping(message, delete_original=False): + await self._on_recover_message(message) + + @bot_mentioned + @group + @admin(send_negative=True) + async def _on_punish(self, message: Message): + await self._update_punishment(self.punish, message, time=flanautils.words_to_time(message.text)) + + async def _on_ready(self): + await super()._on_ready() + await flanautils.do_every(constants.CHECK_MUTES_EVERY_SECONDS, self._check_mutes) + await flanautils.do_every(constants.CHECK_MESSAGE_EVERY_SECONDS, self._check_messages) + await flanautils.do_every(constants.CHECK_PUNISHMENTS_EVERY_SECONDS, self._check_punishments) + + @inline(False) + async def _on_recover_message(self, message: Message): + if message.replied_message: + message_deleted_bot_action = BotAction.find_one({'action': bytes(Action.MESSAGE_DELETED), 'chat': message.chat.object_id, 'affected_objects': message.replied_message.object_id}) + elif self.is_bot_mentioned(message): + message_deleted_bot_action = BotAction.find_one({'action': bytes(Action.MESSAGE_DELETED), 'chat': message.chat.object_id, 'date': {'$gt': datetime.datetime.now() - constants.RECOVERY_DELETED_MESSAGE_BEFORE}}) + else: + return + + if not message_deleted_bot_action: + return + + affected_object_ids = [affected_message_object_id for affected_message_object_id in message_deleted_bot_action.affected_objects] + deleted_messages: list[Message] = [affected_message for affected_object_id in affected_object_ids if (affected_message := Message.find_one({'_id': affected_object_id})).author.id != self.bot_id] + for deleted_message in deleted_messages: + await self.send(deleted_message.text, message) + + async def _on_scraping(self, message: Message, delete_original: bool = None) -> OrderedSet[Media]: + sended_media_messages = await self._search_and_send_medias(message.replied_message) if message.replied_message else OrderedSet() + sended_media_messages += await self._search_and_send_medias(message) + await self.send_inline_results(message) + + if ( + sended_media_messages + and + message.chat.is_group + and + ( + ( + delete_original is None + and + message.chat.config['auto_delete_original'] + ) + or + ( + delete_original is not None + and + delete_original + ) + ) + ): + # noinspection PyTypeChecker + BotAction(Action.MESSAGE_DELETED, message, affected_objects=[message, *sended_media_messages]).save() + await self.delete_message(message) + + return sended_media_messages + + @admin + @group + @bot_mentioned + async def _on_scraping_config_activate(self, message: Message): + await self._change_config('auto_scraping', message, True) + + @admin + @group + @bot_mentioned + async def _on_scraping_config_change(self, message: Message): + await self._change_config('auto_scraping', message) + + @admin + @group + @bot_mentioned + async def _on_scraping_config_deactivate(self, message: Message): + await self._change_config('auto_scraping', message, False) + + @admin + @group + @bot_mentioned + async def _on_scraping_config_show(self, message: Message): + await self._show_config('auto_scraping', message) + + @reply + async def _on_song_info(self, message: Message): + song_infos = message.replied_message.song_infos if message.replied_message else [] + + if song_infos: + for song_info in song_infos: + await self.send_song_info(song_info, message) + elif self.is_bot_mentioned(message) or not message.chat.is_group: + await self._manage_exceptions(SendError('No hay información musical en ese mensaje.'), message) + + @group + @bot_mentioned + @admin(send_negative=True) + async def _on_unmute(self, message: Message): + await self._update_punishment(self.unmute, message) + + @group + @bot_mentioned + @admin(send_negative=True) + async def _on_unpunish(self, message: Message): + await self._update_punishment(self.unpunish, message) + + async def _on_weather_chart(self, message: Message): + bot_state_message: Message | None = None + if message.is_inline: + show_progress_state = False + elif message.chat.is_group and not self.is_bot_mentioned(message): + if message.chat.config['auto_weather_chart']: + if BotAction.find_one({'action': bytes(Action.AUTO_WEATHER_CHART), 'chat': message.chat.object_id, 'date': {'$gt': datetime.datetime.now() - constants.AUTO_WEATHER_EVERY}}): + return + show_progress_state = False + else: + return + else: + show_progress_state = True + + user_names_with_at_sign = {user.name.lower() for user in message.chat.users} + user_names_without_at_sign = {user.name.lower().replace('@', '') for user in message.chat.users} + original_text_words = flanautils.remove_accents(message.text.lower()) + original_text_words = flanautils.translate( + original_text_words, + {symbol: None for symbol in set(flanautils.SYMBOLS) - {'-', '.'}} + ).split() + place_words = ( + OrderedSet(original_text_words) + - flanautils.cartesian_product_string_matching(original_text_words, constants.KEYWORDS['show'], min_ratio=0.8).keys() + - flanautils.cartesian_product_string_matching(original_text_words, constants.KEYWORDS['weather_chart'], min_ratio=0.8).keys() + - flanautils.cartesian_product_string_matching(original_text_words, constants.KEYWORDS['date'], min_ratio=0.8).keys() + - flanautils.cartesian_product_string_matching(original_text_words, constants.KEYWORDS['thanks'], min_ratio=0.8).keys() + - user_names_with_at_sign + - user_names_without_at_sign + - flanautils.CommonWords.words + ) + if not place_words: + await self.send_error(random.choice(('¿Tiempo dónde?', 'Indica el sitio.', 'Y el sitio?', 'y el sitio? me lo invento?')), message) + return + + if 'calle' in original_text_words: + place_words.insert(0, 'calle') + place_query = ' '.join(place_words) + if len(place_query) >= constants.MAX_PLACE_QUERY_LENGTH: + await self.send_error(Media('resources/mucho_texto.png'), message) + return + if show_progress_state: + bot_state_message = await self.send(f'Buscando "{place_query}" en el mapa 🧐...', message) + + result: str | Place | None = None + async for result in flanaapis.geolocation.functions.find_place_showing_progress(place_query): + if isinstance(result, str) and bot_state_message: + await self.edit(result, bot_state_message) + + place: Place = result + if not place: + if bot_state_message: + await self.delete_message(bot_state_message) + await self._manage_exceptions(PlaceNotFoundError(place_query), message) + return + + if bot_state_message: + bot_state_message = await self.edit(f'Obteniendo datos del tiempo para "{place_query}"...', bot_state_message) + current_weather, day_weathers = await flanaapis.weather.functions.get_day_weathers_by_place(place) + + if bot_state_message: + bot_state_message = await self.edit('Creando gráficas del tiempo...', bot_state_message) + + weather_chart = WeatherChart( + _font={'size': 30}, + _title={ + 'text': place.name[:40].strip(' ,-'), + 'xref': 'paper', + 'yref': 'paper', + 'xanchor': 'left', + 'yanchor': 'top', + 'x': 0.025, + 'y': 0.975, + 'font': { + 'size': 50, + 'family': 'open sans' + } + }, + _legend={'x': 0.99, 'y': 0.99, 'xanchor': 'right', 'yanchor': 'top', 'bgcolor': 'rgba(0,0,0,0)'}, + _margin={'l': 20, 'r': 20, 't': 20, 'b': 20}, + trace_metadatas={ + 'temperature': TraceMetadata(name='temperature', group='temperature', legend='Temperatura', show=False, color='#ff8400', default_min=0, default_max=40, y_tick_suffix=' ºC', y_axis_width=130), + 'temperature_feel': TraceMetadata(name='temperature_feel', group='temperature', legend='Sensación de temperatura', show=True, color='red', default_min=0, default_max=40, y_tick_suffix=' ºC', y_axis_width=130), + 'clouds': TraceMetadata(name='clouds', legend='Nubes', show=False, color='#86abe3', default_min=-100, default_max=100, y_tick_suffix=' %', hide_y_ticks_if='{tick} < 0'), + 'visibility': TraceMetadata(name='visibility', legend='Visibilidad', show=False, color='#c99a34', default_min=0, default_max='{max_y_data} * 2', y_tick_suffix=' km', y_delta_tick=2, hide_y_ticks_if='{tick} > {max_y_data}'), + 'uvi': TraceMetadata(name='uvi', legend='UVI', show=False, color='#ffd000', default_min=-12, default_max=12, hide_y_ticks_if='{tick} < 0', y_delta_tick=1, y_axis_width=75), + 'humidity': TraceMetadata(name='humidity', legend='Humedad', show=False, color='#2baab5', default_min=0, default_max=100, y_tick_suffix=' %'), + 'precipitation_probability': TraceMetadata(name='precipitation_probability', legend='Probabilidad de precipitaciones', show=True, color='#0033ff', default_min=-100, default_max=100, y_tick_suffix=' %', hide_y_ticks_if='{tick} < 0'), + 'rain_volume': TraceMetadata(plotly.graph_objects.Histogram, name='rain_volume', group='precipitation', legend='Volumen de lluvia', show=True, color='#34a4eb', opacity=0.3, default_min=-10, default_max=10, y_tick_suffix=' mm', y_delta_tick=1, hide_y_ticks_if='{tick} < 0', y_axis_width=130), + 'snow_volume': TraceMetadata(plotly.graph_objects.Histogram, name='snow_volume', group='precipitation', legend='Volumen de nieve', show=True, color='#34a4eb', opacity=0.8, pattern={'shape': '.', 'fgcolor': '#ffffff', 'bgcolor': '#b0d6f3', 'solidity': 0.5, 'size': 14}, default_min=-10, default_max=10, y_tick_suffix=' mm', y_delta_tick=1, hide_y_ticks_if='{tick} < 0', y_axis_width=130), + 'pressure': TraceMetadata(name='pressure', legend='Presión', show=False, color='#31a339', default_min=1013.25 - 90, default_max=1013.25 + 90, y_tick_suffix=' hPa', y_axis_width=225), + 'wind_speed': TraceMetadata(name='wind_speed', legend='Velocidad del viento', show=False, color='#d8abff', default_min=-120, default_max=120, y_tick_suffix=' km/h', hide_y_ticks_if='{tick} < 0', y_axis_width=165) + }, + x_data=[instant_weather.date_time for day_weather in day_weathers for instant_weather in day_weather.instant_weathers], + all_y_data=[], + current_weather=current_weather, + day_weathers=day_weathers, + timezone=(timezone := next(iter(day_weathers)).timezone), + place=place, + view_position=datetime.datetime.now(timezone) + ) + + weather_chart.apply_zoom() + weather_chart.draw() + if not (image_bytes := weather_chart.to_image()): + if bot_state_message: + await self.delete_message(bot_state_message) + raise NotFoundError('No hay suficientes datos del tiempo.') + + if bot_state_message: + bot_state_message = await self.edit('Enviando...', bot_state_message) + bot_message: Message = await self.send( + Media(image_bytes, MediaType.IMAGE), + [ + [WeatherEmoji.ZOOM_IN.value, WeatherEmoji.ZOOM_OUT.value, WeatherEmoji.LEFT.value, WeatherEmoji.RIGHT.value], + [WeatherEmoji.TEMPERATURE.value, WeatherEmoji.TEMPERATURE_FEEL.value, WeatherEmoji.CLOUDS.value, WeatherEmoji.VISIBILITY.value, WeatherEmoji.UVI.value], + [WeatherEmoji.HUMIDITY.value, WeatherEmoji.PRECIPITATION_PROBABILITY.value, WeatherEmoji.PRECIPITATION_VOLUME.value, WeatherEmoji.PRESSURE.value, WeatherEmoji.WIND_SPEED.value] + ], + message, + send_as_file=False + ) + await self.send_inline_results(message) + + if bot_state_message: + await self.delete_message(bot_state_message) + + if bot_message: + bot_message.weather_chart = weather_chart + bot_message.save() + if not self.is_bot_mentioned(message): + # noinspection PyTypeChecker + BotAction(Action.AUTO_WEATHER_CHART, message, affected_objects=[bot_message]).save() + + @admin + @group + @bot_mentioned + async def _on_weather_chart_config_activate(self, message: Message): + await self._change_config('auto_weather_chart', message, True) + + @admin + @group + @bot_mentioned + async def _on_weather_chart_config_change(self, message: Message): + await self._change_config('auto_weather_chart', message) + + @admin + @group + @bot_mentioned + async def _on_weather_chart_config_deactivate(self, message: Message): + await self._change_config('auto_weather_chart', message, False) + + @admin + @group + @bot_mentioned + async def _on_weather_chart_config_show(self, message: Message): + await self._show_config('auto_weather_chart', message) + + # -------------------------------------------------------- # + # -------------------- PUBLIC METHODS -------------------- # + # -------------------------------------------------------- # + async def is_deaf(self, user_id: int, group_id: int) -> bool: + pass + + async def is_muted(self, user_id: int, group_id: int) -> bool: + pass + + async def mute(self, user_id: int, group_id: int, time: int | datetime.timedelta, message: Message = None): + if isinstance(time, int): + time = datetime.timedelta(seconds=time) + + try: + await self._mute(user_id, group_id) + except UserDisconnectedError as e: + await self._manage_exceptions(e, message if message else Message(chat=Chat(group_id=group_id))) + else: + if time: + # noinspection PyTypeChecker + Mute(user_id, group_id, until=datetime.datetime.now() + time).save() + if datetime.timedelta() < time <= constants.TIME_THRESHOLD_TO_MANUAL_UNMUTE: + await flanautils.do_later(time, self._check_mutes) + else: + # noinspection PyTypeChecker + Mute(user_id, group_id).save() + + async def punish(self, user_id: int, group_id: int, time: int | datetime.timedelta, message: Message = None): + if isinstance(time, int): + time = datetime.timedelta(seconds=time) + + try: + await self._punish(user_id, group_id) + except BadRoleError as e: + await self._manage_exceptions(e, message if message else Message(chat=Chat(group_id=group_id))) + else: + if time: + # noinspection PyTypeChecker + Punishment(user_id, group_id, until=datetime.datetime.now() + time).save() + if datetime.timedelta() < time <= constants.TIME_THRESHOLD_TO_MANUAL_UNPUNISH: + await flanautils.do_later(time, self._check_punishments) + else: + # noinspection PyTypeChecker + Punishment(user_id, group_id).save() + + async def send_bad_phrase(self, message: Message, probability=0.00166666667) -> multibot_constants.ORIGINAL_MESSAGE | None: + if not self.is_bot_mentioned(message) and random.random() >= probability or message.author.id == self.owner_id: + return + + await self.typing_delay(message) + + return await self.send(random.choice(constants.BAD_PHRASES), message) + + async def send_bye(self, message: Message) -> multibot_constants.ORIGINAL_MESSAGE: + return await self.send(random.choice((*constants.BYE_PHRASES, flanautils.CommonWords.random_time_greeting())), message) + + async def send_hello(self, message: Message) -> multibot_constants.ORIGINAL_MESSAGE: + return await self.send(random.choice((*constants.HELLO_PHRASES, flanautils.CommonWords.random_time_greeting())), message) + + @return_if_first_empty(exclude_self_types='FlanaBot', globals_=globals()) + async def send_medias(self, medias: OrderedSet[Media], message: Message, send_song_info=False) -> tuple[list[Message], int]: + sended_media_messages = [] + fails = 0 + bot_state_message: Message | None = None + sended_info_message: Message | None = None + + if not message.is_inline: + bot_state_message: Message = await self.send('Descargando...', message) + + if message.chat.is_group: + sended_info_message = await self.send(f'{message.author.name} compartió{self._medias_sended_info(medias)}', message) + + for media in medias: + if media.song_info: + message.song_infos.add(media.song_info) + message.save() + + try: + bot_message = await self.send(media, message) + except SendError as e: + await self._manage_exceptions(e, message) + fails += 1 + else: + sended_media_messages.append(bot_message) + if media.song_info and bot_message: + bot_message.song_infos.add(media.song_info) + bot_message.save() + + if send_song_info and media.song_info: + await self.send_song_info(media.song_info, message) + + if fails and sended_info_message: + if fails == len(medias): + await self.delete_message(sended_info_message) + if bot_state_message: + await self.delete_message(bot_state_message) + + return sended_media_messages, fails + + @return_if_first_empty(exclude_self_types='FlanaBot', globals_=globals()) + async def send_song_info(self, song_info: Media, message: Message): + attributes = ( + f'Título: {song_info.title}\n' if song_info.title else '', + f'Autor: {song_info.author}\n' if song_info.author else '', + f"{f'Álbum: {song_info.album}'}\n" if song_info.album else '', + f'Previa:' + ) + await self.send(''.join(attributes), message) + if song_info: + await self.send(song_info, message) + + async def unmute(self, user_id: int, group_id: int, message: Message = None): + try: + await self._unmute(user_id, group_id) + except UserDisconnectedError as e: + await self._manage_exceptions(e, message if message else Message(chat=Chat(group_id=group_id))) + + async def unpunish(self, user_id: int, group_id: int, message: Message = None): + try: + await self._unpunish(user_id, group_id) + except BadRoleError as e: + await self._manage_exceptions(e, message if message else Message(chat=Chat(group_id=group_id))) diff --git a/flanabot/bots/flana_disc_bot.py b/flanabot/bots/flana_disc_bot.py new file mode 100644 index 0000000..7f05b2b --- /dev/null +++ b/flanabot/bots/flana_disc_bot.py @@ -0,0 +1,133 @@ +import asyncio +import os + +import discord +from multibot import DiscordBot + +from flanabot import constants +from flanabot.bots.flana_bot import FlanaBot +from flanabot.exceptions import BadRoleError, UserDisconnectedError +from flanabot.models import User + +HEAT_NAMES = [ + 'Canal Congelado', + 'Canal Fresquito', + 'Canal Templaillo', + 'Canal Calentito', + 'Canal Caloret', + 'Canal Caliente', + 'Canal Olor a Vasco', + 'Verano Cordobés al Sol', + 'Canal Ardiendo', + 'abrid las putas ventanas y traed el extintor', + 'Canal INFIERNO', + 'La Palma 🌋' +] +HOT_CHANNEL_ID = 493530483045564417 +ROLES = { + 'Administrador': 387344390030360587, + 'Carroñero': 493523298429435905, + 'al lol': 881238165476741161, + 'Persona': 866046517998387220, + 'Castigado': 877662459568209921, + 'Bot': 493784221085597706 +} + + +# ---------------------------------------------------------------------------------------------------- # +# ------------------------------------------ FLANA_DISC_BOT ------------------------------------------ # +# ---------------------------------------------------------------------------------------------------- # +class FlanaDiscBot(DiscordBot, FlanaBot): + def __init__(self): + super().__init__(os.environ['DISCORD_BOT_TOKEN']) + self.heating = False + self.heat_level = 0 + + # ----------------------------------------------------------- # + # -------------------- PROTECTED METHODS -------------------- # + # ----------------------------------------------------------- # + def _add_handlers(self): + super()._add_handlers() + self.bot_client.add_listener(self._on_voice_state_update, 'on_voice_state_update') + + async def _heat_channel(self, channel: discord.VoiceChannel): + while True: + await asyncio.sleep(constants.HEAT_PERIOD_SECONDS) + + if channel.members: + if self.heat_level == len(HEAT_NAMES) - 1: + return + self.heat_level += 0.5 + elif not channel.members: + if not self.heat_level: + return + self.heat_level -= 0.5 + else: + continue + + await channel.edit(name=HEAT_NAMES[int(self.heat_level)]) + + async def _mute(self, user_id: int, group_id: int): + user = await self.get_user(user_id, group_id) + try: + await user.original_object.edit(mute=True) + except discord.errors.HTTPException: + raise UserDisconnectedError + + async def _punish(self, user_id: int, group_id: int): + user = await self.get_user(user_id, group_id) + try: + await user.original_object.remove_roles(self._find_role_by_id(ROLES['Persona'], user.original_object.guild.roles)) + await user.original_object.add_roles(self._find_role_by_id(ROLES['Castigado'], user.original_object.guild.roles)) + except AttributeError: + raise BadRoleError(str(self._punish)) + + async def _unmute(self, user_id: int, group_id: int): + user = await self.get_user(user_id, group_id) + try: + await user.original_object.edit(mute=False) + except discord.errors.HTTPException: + raise UserDisconnectedError + + async def _unpunish(self, user_id: int, group_id: int): + user = await self.get_user(user_id, group_id) + try: + await user.original_object.remove_roles(self._find_role_by_id(ROLES['Castigado'], user.original_object.guild.roles)) + await user.original_object.add_roles(self._find_role_by_id(ROLES['Persona'], user.original_object.guild.roles)) + except AttributeError: + raise BadRoleError(str(self._unpunish)) + + # ---------------------------------------------- # + # HANDLERS # + # ---------------------------------------------- # + async def _on_voice_state_update(self, _: discord.Member, before: discord.VoiceState, after: discord.VoiceState): + if getattr(before.channel, 'id', None) == HOT_CHANNEL_ID: + channel = before.channel + elif getattr(after.channel, 'id', None) == HOT_CHANNEL_ID: + channel = after.channel + else: + return + + if not self.heating: + self.heating = True + await self._heat_channel(channel) + self.heating = False + + # -------------------------------------------------------- # + # -------------------- PUBLIC METHODS -------------------- # + # -------------------------------------------------------- # + async def is_deaf(self, user_id: int, group_id: int) -> bool: + user = await self.get_user(user_id, group_id) + return user.original_object.voice.deaf + + async def is_muted(self, user_id: int, group_id: int) -> bool: + user = await self.get_user(user_id, group_id) + return user.original_object.voice.mute + + @staticmethod + def is_self_deaf(user: User) -> bool: + return user.original_object.voice.self_deaf + + @staticmethod + def is_self_muted(user: User) -> bool: + return user.original_object.voice.self_mute diff --git a/flanabot/bots/flana_tele_bot.py b/flanabot/bots/flana_tele_bot.py new file mode 100644 index 0000000..8a04944 --- /dev/null +++ b/flanabot/bots/flana_tele_bot.py @@ -0,0 +1,127 @@ +from __future__ import annotations # todo0 remove in 3.11 + +import functools +import os +from typing import Any, Callable + +import telethon.tl.functions +from flanaapis.weather.constants import WeatherEmoji +from flanautils import Media, MediaType, return_if_first_empty +from multibot import TelegramBot, constants as multibot_constants, find_message, user_client + +from flanabot.bots.flana_bot import FlanaBot +from flanabot.models.chat import Chat +from flanabot.models.message import Message +from flanabot.models.user import User + + +# ---------------------------------------------------------- # +# ----------------------- DECORATORS ----------------------- # +# ---------------------------------------------------------- # + + +def whitelisted_event(func: Callable) -> Callable: + @functools.wraps(func) + @find_message + async def wrapper(self: FlanaTeleBot, message: Message): + if message.author.id not in self.whitelist_ids: + return + + return await func(self, message) + + return wrapper + + +# ---------------------------------------------------------------------------------------------------- # +# ------------------------------------------ FLANA_TELE_BOT ------------------------------------------ # +# ---------------------------------------------------------------------------------------------------- # +class FlanaTeleBot(TelegramBot, FlanaBot): + def __init__(self): + super().__init__( + api_id=os.environ['TELEGRAM_API_ID'], + api_hash=os.environ['TELEGRAM_API_HASH'], + bot_session=os.environ['TELEGRAM_BOT_SESSION'], + user_session=os.environ['TELEGRAM_USER_SESSION'] + ) + self.whitelist_ids = [] + + # ----------------------------------------------------------- # + # -------------------- PROTECTED METHODS -------------------- # + # ----------------------------------------------------------- # + def _add_handlers(self): + super()._add_handlers() + self.register_button(self._on_button_press) + + @return_if_first_empty(exclude_self_types='FlanaTeleBot', globals_=globals()) + async def _create_chat_from_telegram_chat(self, telegram_chat: multibot_constants.TELEGRAM_CHAT) -> Chat | None: + return Chat.from_event_component(await super()._create_chat_from_telegram_chat(telegram_chat)) + + @return_if_first_empty(exclude_self_types='FlanaTeleBot', globals_=globals()) + async def _create_bot_message_from_telegram_bot_message(self, original_message: multibot_constants.TELEGRAM_MESSAGE, message: Message, content: Any = None) -> Message | None: + return Message.from_event_component(await super()._create_bot_message_from_telegram_bot_message(original_message, message)) + + @return_if_first_empty(exclude_self_types='FlanaTeleBot', globals_=globals()) + async def _create_user_from_telegram_user(self, original_user: multibot_constants.TELEGRAM_USER, group_id: int = None) -> User | None: + return User.from_event_component(await super()._create_user_from_telegram_user(original_user, group_id)) + + @user_client + async def _get_contacts_ids(self) -> list[int]: + async with self.user_client: + contacts_data = await self.user_client(telethon.tl.functions.contacts.GetContactsRequest(hash=0)) + + return [contact.user_id for contact in contacts_data.contacts] + + @user_client + async def _update_whitelist(self): + self.whitelist_ids = [self.owner_id, self.bot_id] + await self._get_contacts_ids() + + # ---------------------------------------------- # + # HANDLERS # + # ---------------------------------------------- # + @whitelisted_event + async def _on_button_press(self, message: Message): + await message.original_event.answer() + + match message.button_text: + case WeatherEmoji.ZOOM_IN.value: + message.weather_chart.zoom_in() + case WeatherEmoji.ZOOM_OUT.value: + message.weather_chart.zoom_out() + case WeatherEmoji.LEFT.value: + message.weather_chart.move_left() + case WeatherEmoji.RIGHT.value: + message.weather_chart.move_right() + case WeatherEmoji.PRECIPITATION_VOLUME.value: + message.weather_chart.trace_metadatas['rain_volume'].show = not message.weather_chart.trace_metadatas['rain_volume'].show + message.weather_chart.trace_metadatas['snow_volume'].show = not message.weather_chart.trace_metadatas['snow_volume'].show + case emoji if emoji in WeatherEmoji.values: + trace_metadata_name = WeatherEmoji(emoji).name.lower() + message.weather_chart.trace_metadatas[trace_metadata_name].show = not message.weather_chart.trace_metadatas[trace_metadata_name].show + + message.weather_chart.apply_zoom() + message.weather_chart.draw() + message.save() + + image_bytes = message.weather_chart.to_image() + file = await self._prepare_media_to_send(Media(image_bytes, MediaType.IMAGE)) + + try: + await message.original_object.edit(file=file) + except telethon.errors.rpcerrorlist.MessageNotModifiedError: + pass + + @whitelisted_event + async def _on_inline_query_raw(self, message: Message): + await super()._on_new_message_raw(message) + + @whitelisted_event + async def _on_new_message_raw(self, message: Message): + await super()._on_new_message_raw(message) + + async def _on_ready(self): + await super()._on_ready() + await self._update_whitelist() + + # -------------------------------------------------------- # + # -------------------- PUBLIC METHODS -------------------- # + # -------------------------------------------------------- # diff --git a/flanabot/constants.py b/flanabot/constants.py new file mode 100644 index 0000000..b1a22c3 --- /dev/null +++ b/flanabot/constants.py @@ -0,0 +1,100 @@ +import datetime + +import flanautils + +AUTO_WEATHER_EVERY = datetime.timedelta(hours=6) +CHECK_MESSAGE_EVERY_SECONDS = datetime.timedelta(days=1).total_seconds() +CHECK_MUTES_EVERY_SECONDS = datetime.timedelta(hours=1).total_seconds() +CHECK_PUNISHMENTS_EVERY_SECONDS = datetime.timedelta(hours=1).total_seconds() +HEAT_PERIOD_SECONDS = datetime.timedelta(minutes=15).total_seconds() +MAX_PLACE_QUERY_LENGTH = 50 +PUNISHMENT_INCREMENT_EXPONENT = 6 +PUNISHMENTS_RESET = datetime.timedelta(weeks=6 * flanautils.WEEKS_IN_A_MONTH) +RECOVERY_DELETED_MESSAGE_BEFORE = datetime.timedelta(hours=1) +TIME_THRESHOLD_TO_MANUAL_UNMUTE = datetime.timedelta(days=3) +TIME_THRESHOLD_TO_MANUAL_UNPUNISH = datetime.timedelta(days=3) + +BAD_PHRASES = ( + 'Cállate ya anda.', + '¿Quién te ha preguntado?', + '¿Tú eres así o te dan apagones cerebrales?', + 'Ante la duda mi dedo corazón te saluda.', + 'Enjoy cancer brain.', + 'Calla noob.', + 'Hablas tanta mierda que tu culo tiene envidia de tu boca.', + 'jAJjajAJjajAJjajAJajJAJajJA', + 'enjoy xd', + 'Reported.', + 'Baneito pa ti en breve.', + 'Despídete de tu cuenta.', + 'Flanagan es más guapo que tú.', + 'jajaj', + 'xd', + 'Hay un concurso de hostias y tienes todas las papeletas.', + '¿Por qué no te callas?', + 'Das penilla.', + 'Deberían hacerte la táctica del C4.', + 'Te voy romper las pelotas.', + 'Más tonto y no naces.', + 'Eres más tonto que peinar bombillas.', + 'Eres más tonto que pellizcar cristales.', + 'Eres más malo que pegarle a un padre.' +) + +BYE_PHRASES = ('Adiós.', 'adieu', 'adio', 'adioh', 'adios', 'adió', 'adiós', 'agur', 'bye', 'byyeeee', 'chao', + 'hasta la vista', 'hasta luego', 'hasta nunca', ' hasta pronto', 'hasta la próxima', + 'nos vemos', 'taluego') + +HELLO_PHRASES = ('alo', 'aloh', 'buenas', 'Hola.', 'hello', 'hey', 'hi', 'hola', 'holaaaa', 'holaaaaaaa', 'ola', + 'ola k ase', 'pa ti mi cola', 'saludos') + +KEYWORDS = { + 'activate': ('activa', 'activar', 'activate', 'deja', 'dejale', 'devuelve', 'devuelvele', 'enable', 'encender', + 'enciende', 'habilita', 'habilitar'), + 'bye': ('adieu', 'adio', 'adiooooo', 'adios', 'agur', 'buenas', 'bye', 'cama', 'chao', 'dias', 'farewell', + 'goodbye', 'hasta', 'luego', 'noches', 'pronto', 'taluego', 'taluegorl', 'tenga', 'vemos', 'vista', 'voy'), + 'change': ('alter', 'alternar', 'alternate', 'cambiar', 'change', 'default', 'defecto', 'edit', 'editar', + 'exchange', 'modificar', 'modify', 'permutar', 'predeterminado', 'shift', 'swap', 'switch', 'turn', + 'vary'), + 'config': ('ajustar', 'ajuste', 'ajustes', 'automatico', 'automatic', 'config', 'configs', 'configuracion', + 'configuration', 'default', 'defecto', 'setting', 'settings'), + 'covid_chart': ('case', 'caso', 'contagiado', 'contagio', 'corona', 'coronavirus', 'covid', 'covid19', 'death', + 'disease', 'enfermedad', 'enfermos', 'fallecido', 'incidencia', 'jacovid', 'mascarilla', 'muerte', + 'muerto', 'pandemia', 'sick', 'virus'), + 'currency_chart': ('argentina', 'bitcoin', 'cardano', 'cripto', 'crypto', 'criptodivisa', 'cryptodivisa', + 'cryptomoneda', 'cryptocurrency', 'currency', 'dinero', 'divisa', 'ethereum', 'inversion', + 'moneda', 'pasta'), + 'date': ('ayer', 'de', 'domingo', 'fin', 'finde', 'friday', 'hoy', 'jueves', 'lunes', 'martes', 'mañana', + 'miercoles', 'monday', 'pasado', 'sabado', 'saturday', 'semana', 'sunday', 'thursday', 'today', 'tomorrow', + 'tuesday', 'viernes', 'wednesday', 'week', 'weekend', 'yesterday'), + 'deactivate': ('apaga', 'apagar', 'deactivate', 'deactivate', 'desactivar', 'deshabilita', 'deshabilitar', + 'disable', 'forbids', 'prohibe', 'quita', 'remove', 'return'), + 'hello': ('alo', 'aloh', 'buenas', 'dias', 'hello', 'hey', 'hi', 'hola', 'holaaaaaa', 'ola', 'saludos', 'tardes'), + 'help': ('ayuda', 'help'), + 'mute': ('calla', 'calle', 'cierra', 'close', 'mute', 'mutea', 'mutealo', 'noise', 'ruido', 'shut', 'silence', + 'silencia'), + 'negate': ('no', 'ocurra', 'ocurre'), + 'permission': ('permiso', 'permission'), + 'punish': ('acaba', 'aprende', 'ataca', 'atalo', 'azota', 'boss', 'castiga', 'castigo', 'condena', 'controla', + 'destroy', 'destroza', 'duro', 'ejecuta', 'enseña', 'escarmiento', 'execute', 'finish', 'fuck', 'fusila', + 'hell', 'humos', 'infierno', 'jefe', 'jode', 'learn', 'leccion', 'lesson', 'manda', 'purgatorio', + 'sancion', 'shoot', 'teach', 'termina', 'whip'), + 'reset': ('recover', 'recovery', 'recupera', 'reinicia', 'reset', 'resetea', 'restart'), + 'scraping': ('api', 'aqui', 'busca', 'contenido', 'content', 'descarga', 'descargar', 'download', 'envia', 'habia', + 'media', 'redes', 'scrap', 'scraping', 'search', 'send', 'social', 'sociales', 'tenia', 'video', + 'videos'), + 'show': ('actual', 'enseña', 'estado', 'how', 'is', 'muestra', 'show', 'como'), + 'song_info': ('aqui', 'cancion', 'data', 'datos', 'info', 'informacion', 'information', 'llama', 'media', 'name', + 'nombre', 'sonaba', 'sonando', 'song', 'sono', 'sound', 'suena', 'title', 'titulo', + 'video'), + 'sound': ('hablar', 'hable', 'micro', 'microfono', 'microphone', 'sonido', 'sound', 'talk', 'volumen'), + 'thanks': ('gracia', 'gracias', 'grasia', 'grasias', 'grax', 'thank', 'thanks', 'ty'), + 'unmute': ('desilencia', 'desmutea', 'desmutealo', 'unmute'), + 'unpunish': ('absolve', 'forgive', 'innocent', 'inocente', 'perdona', 'spare'), + 'weather_chart': ('atmosfera', 'atmosferico', 'calle', 'calor', 'caloret', 'clima', 'climatologia', 'cloud', + 'cloudless', 'cloudy', 'cold', 'congelar', 'congelado', 'denbora', 'despejado', 'diluvio', 'frio', + 'frost', 'hielo', 'humedad', 'llover', 'llueva', 'llueve', 'lluvia', 'nevada', 'nieva', 'nieve', + 'nube', 'nubes', 'nublado', 'meteorologia', 'rain', 'snow', 'snowfall', 'snowstorm', 'sol', + 'solano', 'storm', 'sun', 'temperatura', 'tempo', 'tiempo', 'tormenta', 've', 'ventisca', + 'weather', 'wetter') +} diff --git a/flanabot/exceptions.py b/flanabot/exceptions.py new file mode 100644 index 0000000..4f55fc5 --- /dev/null +++ b/flanabot/exceptions.py @@ -0,0 +1,6 @@ +class BadRoleError(Exception): + pass + + +class UserDisconnectedError(Exception): + pass diff --git a/flanabot/main.py b/flanabot/main.py new file mode 100644 index 0000000..5efca58 --- /dev/null +++ b/flanabot/main.py @@ -0,0 +1,18 @@ +import asyncio + +from flanabot.bots.flana_disc_bot import FlanaDiscBot +from flanabot.bots.flana_tele_bot import FlanaTeleBot + + +async def main(): + flana_disc_bot = FlanaDiscBot() + flana_tele_bot = FlanaTeleBot() + + await asyncio.gather( + # flana_disc_bot.start(), + flana_tele_bot.start(), + ) + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/flanabot/models/__init__.py b/flanabot/models/__init__.py new file mode 100644 index 0000000..3fa55be --- /dev/null +++ b/flanabot/models/__init__.py @@ -0,0 +1,5 @@ +from flanabot.models.chat import * +from flanabot.models.message import * +from flanabot.models.punishments import * +from flanabot.models.user import * +from flanabot.models.weather_chart import * diff --git a/flanabot/models/chat.py b/flanabot/models/chat.py new file mode 100644 index 0000000..6937171 --- /dev/null +++ b/flanabot/models/chat.py @@ -0,0 +1,21 @@ +from dataclasses import dataclass, field + +from multibot import Chat as MultiBotChat, EventComponent, T + +DEFAULT_CONFIG = {'auto_clear': False, + 'auto_covid_chart': True, + 'auto_currency_chart': True, + 'auto_delete_original': True, + 'auto_scraping': True, + 'auto_weather_chart': True} + + +@dataclass(eq=False) +class Chat(MultiBotChat): + config: dict[str, bool] = field(default_factory=lambda: DEFAULT_CONFIG) + + @classmethod + def from_event_component(cls, event_component: EventComponent) -> T: + chat = super().from_event_component(event_component) + chat.config = DEFAULT_CONFIG + return chat diff --git a/flanabot/models/message.py b/flanabot/models/message.py new file mode 100644 index 0000000..221abe6 --- /dev/null +++ b/flanabot/models/message.py @@ -0,0 +1,39 @@ +from __future__ import annotations # todo0 remove in 3.11 + +import datetime +from dataclasses import dataclass, field +from typing import Iterable + +from flanautils import Media, OrderedSet +from multibot import EventComponent, constants as multibot_constants, db + +from flanabot.models.chat import Chat +from flanabot.models.user import User +from flanabot.models.weather_chart import WeatherChart + + +@dataclass(eq=False) +class Message(EventComponent): + collection = db.message + _unique_keys = ('id', 'author') + _nullable_unique_keys = ('id', 'author') + + id: int | str = None + author: User = None + text: str = None + button_text: str = None + mentions: Iterable[User] = field(default_factory=list) + chat: Chat = None + replied_message: Message = None + weather_chart: WeatherChart = None + last_update: datetime.datetime = None + song_infos: OrderedSet[Media] = field(default_factory=OrderedSet) + is_inline: bool = None + contents: list = field(default_factory=list) + is_deleted: bool = False + original_object: multibot_constants.ORIGINAL_MESSAGE = None + original_event: multibot_constants.MESSAGE_EVENT = None + + def save(self, pull_exclude: Iterable[str] = (), pull_database_priority=False, references=True): + self.last_update = datetime.datetime.now() + super().save(pull_exclude, pull_database_priority, references) diff --git a/flanabot/models/punishments.py b/flanabot/models/punishments.py new file mode 100644 index 0000000..c61745b --- /dev/null +++ b/flanabot/models/punishments.py @@ -0,0 +1,23 @@ +import datetime +from dataclasses import dataclass + +from flanautils import FlanaBase, MongoBase +from multibot.models.database import db + + +@dataclass(eq=False) +class PunishmentBase(MongoBase, FlanaBase): + user_id: int = None + group_id: int = None + until: datetime.datetime = None + is_active: bool = True + + +@dataclass(eq=False) +class Punishment(PunishmentBase): + collection = db.punishment + + +@dataclass(eq=False) +class Mute(PunishmentBase): + collection = db.mute diff --git a/flanabot/models/user.py b/flanabot/models/user.py new file mode 100644 index 0000000..f07590a --- /dev/null +++ b/flanabot/models/user.py @@ -0,0 +1,30 @@ +from dataclasses import dataclass + +from multibot import User as MultiBotUser, constants as multibot_constants, db + +from flanabot.models.punishments import Mute, Punishment + + +@dataclass(eq=False) +class User(MultiBotUser): + collection = db.user + _unique_keys = 'id' + + id: int = None + name: str = None + is_admin: bool = None + original_object: multibot_constants.ORIGINAL_USER = None + + def is_muted_on(self, group_id: int): + return group_id in self.muted_on + + def is_punished_on(self, group_id: int): + return group_id in self.punished_on + + @property + def muted_on(self): + return {mute.group_id for mute in Mute.find({'user_id': self.id, 'is_active': True})} + + @property + def punished_on(self): + return {punishment for punishment in Punishment.find({'user_id': self.id, 'is_active': True})} diff --git a/flanabot/models/weather_chart.py b/flanabot/models/weather_chart.py new file mode 100644 index 0000000..9650db5 --- /dev/null +++ b/flanabot/models/weather_chart.py @@ -0,0 +1,125 @@ +import datetime +from dataclasses import dataclass, field + +from flanaapis import InstantWeather, Place +from flanautils import DateChart, FlanaEnum + + +class Direction(FlanaEnum): + LEFT = -1 + RIGHT = 1 + + +@dataclass(unsafe_hash=True) +class WeatherChart(DateChart): + current_weather: InstantWeather = None + day_weathers: list = field(default_factory=list) + timezone: datetime.timezone = None + place: Place = None + day_separator_width: float = 1 + zoom_level: int = 1 + view_position: datetime.datetime = None + + def __post_init__(self): + super().__post_init__() + self.legend = self._legend + self.x_data = [instant_weather.date_time for day_weather in self.day_weathers for instant_weather in day_weather.instant_weathers] + for trace_metadata in self.trace_metadatas.values(): + y_data = [] + for day_weather in self.day_weathers: + for instant_weather in day_weather.instant_weathers: + y_data.append(getattr(instant_weather, trace_metadata.name)) + self.all_y_data.append(y_data) + + def add_lines(self): + super().add_lines() + + try: + first_dt = self.day_weathers[0].instant_weathers[0].date_time + last_dt = self.day_weathers[-1].instant_weathers[-1].date_time + except IndexError: + return + + now = datetime.datetime.now(self.timezone) + + if self.show_now_vertical_line and first_dt <= now <= last_dt: + self.figure.add_vline(x=now, line_width=1, line_dash='dot') + self.figure.add_annotation(text="Ahora", yref="paper", x=now, y=0.01, showarrow=False) + + for day_weather in self.day_weathers: + date_time = datetime.datetime(year=day_weather.date.year, month=day_weather.date.month, day=day_weather.date.day, tzinfo=self.timezone) + if first_dt <= date_time <= last_dt: + self.figure.add_vline(x=date_time, line_width=self.day_separator_width) + + # noinspection PyAttributeOutsideInit + # noinspection PyUnboundLocalVariable + def apply_zoom(self): + self.clear() + match self.zoom_level: + case 0: + self.xaxis = { + 'tickformat': '%A %-d\n%B\n%Y', + 'dtick': 24 * 60 * 60 * 1000, + 'ticklabelmode': 'period', + 'tickangle': None + } + self.day_separator_width = 1 + case _: + match self.zoom_level: + case 1: + start_date = self.view_position - datetime.timedelta(days=3) + end_date = self.view_position + datetime.timedelta(days=3) + self.xaxis = {'tickformat': '%A %-d\n%B\n%Y', 'dtick': 24 * 60 * 60 * 1000, 'ticklabelmode': 'period'} + self.day_separator_width = 1 + case 2: + start_date = self.view_position - datetime.timedelta(days=1) + end_date = self.view_position + datetime.timedelta(days=2) + self.xaxis = {'tickformat': '%-H\n%A %-d', 'dtick': 6 * 60 * 60 * 1000} + self.day_separator_width = 2 + case 3: + start_date = self.view_position - datetime.timedelta(days=1) + end_date = self.view_position + datetime.timedelta(days=1) + self.xaxis = {'tickformat': '%-H\n%A %-d', 'dtick': 4 * 60 * 60 * 1000} + self.day_separator_width = 2 + case 4: + start_date = self.view_position + end_date = self.view_position + datetime.timedelta(days=1) + self.xaxis = {'tickformat': '%-H\n%A %-d', 'dtick': 2 * 60 * 60 * 1000} + self.day_separator_width = 2 + case 5: + start_date = self.view_position + end_date = self.view_position + self.xaxis = {'tickformat': '%-H\n%A %-d', 'dtick': 1 * 60 * 60 * 1000} + self.day_separator_width = 2 + + self.xaxis = { + 'range': ( + (start_date - datetime.timedelta(days=1)).replace(hour=23), + (end_date + datetime.timedelta(days=1)).replace(hour=0) + ), + 'tickangle': 0 + } + + def move_left(self): + if self.zoom_level > 0 and self.x_data[0] <= self.view_position - datetime.timedelta(days=1): + self.move_view_position(Direction.LEFT) + + def move_right(self): + if self.zoom_level > 0 and self.view_position + datetime.timedelta(days=1) <= self.x_data[-1]: + self.move_view_position(Direction.RIGHT) + + def move_view_position(self, direction: Direction): + match self.zoom_level: + case 0: + return + case _: + time_delta = datetime.timedelta(days=1) + self.view_position += time_delta * direction.value + + def zoom_in(self): + if self.zoom_level < 5: + self.zoom_level += 1 + + def zoom_out(self): + if 0 < self.zoom_level: + self.zoom_level -= 1 diff --git a/flanabot/resources/mucho_texto.png b/flanabot/resources/mucho_texto.png new file mode 100644 index 0000000..e3be727 Binary files /dev/null and b/flanabot/resources/mucho_texto.png differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e65a570 Binary files /dev/null and b/requirements.txt differ diff --git a/tests/unit/test_parse_callbacks.py b/tests/unit/test_parse_callbacks.py new file mode 100644 index 0000000..03d4478 --- /dev/null +++ b/tests/unit/test_parse_callbacks.py @@ -0,0 +1,290 @@ +import unittest +from typing import Iterable + +from flanabot import constants +import flanautils +from flanabot.bots.flana_bots.flana_tele_bot import FlanaTeleBot + + +class TestParseCallbacks(unittest.TestCase): + def _test_no_always_callbacks(self, phrases: Iterable[str], callback: callable): + for i, phrase in enumerate(phrases): + with self.subTest(phrase): + callbacks = [registered_callback.callback for registered_callback in self.flana_tele_bot._parse_callbacks(phrase, constants.RATIO_REWARD_EXPONENT, constants.KEYWORDS_LENGHT_PENALTY, constants.MINIMUM_RATIO_TO_MATCH) + if not registered_callback.always] + self.assertEqual(1, len(callbacks)) + self.assertEqual(callback, callbacks[0], f'\n\nExpected: {callback.__name__}\nActual: {callbacks[0].__name__}') + + def setUp(self) -> None: + self.flana_tele_bot = FlanaTeleBot() + + def test_on_bye(self): + phrases = ['adios', 'taluego', 'adiooo', 'hasta la proxima', 'nos vemos', 'hasta la vista', 'hasta pronto'] + self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_bye) + + def test_on_config_list_show(self): + phrases = [ + 'flanabot ajustes', + 'Flanabot ajustes', + 'Flanabot qué puedo ajustar?', + 'flanabot ayuda' + ] + self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_config_list_show) + + def test_on_covid_chart(self): + phrases = [ + 'cuantos contagios', + 'casos', + 'enfermos', + 'muerte', + 'pandemia', + 'enfermedad', + 'fallecidos', + 'mascarillas', + 'virus', + 'covid-19', + 'como va el covid', + 'lo peta el corona' + ] + self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_covid_chart) + + def test_on_currency_chart(self): + phrases = [ + 'como van esos dineros', + 'critodivisa', + 'esas cryptos', + 'inversion', + 'moneda', + 'mas caro en argentina?', + 'el puto bitcoin', + 'divisa' + ] + self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_currency_chart) + + def test_on_currency_chart_config_activate(self): + phrases = ['activa el bitcoin automatico'] + self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_currency_chart_config_activate) + + def test_on_currency_chart_config_change(self): + phrases = ['cambia la config del bitcoin automatico'] + self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_currency_chart_config_change) + + def test_on_currency_chart_config_deactivate(self): + phrases = ['desactiva el bitcoin automatico'] + self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_currency_chart_config_deactivate) + + def test_on_currency_chart_config_show(self): + phrases = ['enseña el bitcoin automatico', 'como esta el bitcoin automatico', 'flanabot ajustes bitcoin'] + self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_currency_chart_config_show) + + def test_on_delete(self): + phrases = ['borra ese mensaje', 'borra ese mensaje puto', 'borra', 'borra el mensaje', 'borra eso', 'borres'] + self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_delete) + + def test_on_delete_original_config_activate(self): + phrases = [ + 'activa el borrado automatico', + 'flanabot pon el auto delete activado', + 'flanabot activa el autodelete' + ] + self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_delete_original_config_activate) + + def test_on_delete_original_config_change(self): + phrases = ['cambia la config del borrado automatico'] + self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_delete_original_config_change) + + def test_on_delete_original_config_deactivate(self): + phrases = [ + 'desactiva el borrado automatico', + 'flanabot pon el auto delete desactivado', + 'flanabot desactiva el autodelete' + ] + self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_delete_original_config_deactivate) + + def test_on_delete_original_config_show(self): + phrases = ['enseña el borrado automatico', 'como esta el borrado automatico', 'flanabot ajustes delete'] + self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_delete_original_config_show) + + def test_on_hello(self): + phrases = ['hola', 'hello', 'buenos dias', 'holaaaaaa', 'hi', 'holaaaaa', 'saludos', 'ola k ase'] + self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_hello) + + def test_on_mute(self): + phrases = [ + 'silencia', + 'silencia al pavo ese', + 'calla a ese pesao', + 'haz que se calle', + 'quitale el microfono a ese', + 'quitale el micro', + 'quitale el sonido', + 'mutealo', + 'mutea', + 'mutea a ese' + ] + self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_mute) + + def test_on_new_message(self): + for i in range(10): + phrase = flanautils.random_string(0, 30, n_spaces=20) + with self.subTest(phrase): + callbacks = [registered_callback.callback for registered_callback in self.flana_tele_bot._parse_callbacks(phrase)] + self.assertIn(self.flana_tele_bot._on_new_message, callbacks, f'\n\nExpected: {self.flana_tele_bot._on_new_message.__name__} in {callbacks}') + + def test_on_new_message_default(self): + phrases = [ + 'asdqwergf', + 'ytk8', + 'htr', + 'hmj', + 'aaaaaaa' + ] + self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_new_message_default) + + def test_on_no_delete_original(self): + phrases = [ + 'no obrres', + 'no borres el original', + 'no borres', + 'no borres el oringal', + 'no oringial', + 'Alberto, [30/11/2021 5:59]\nno borres el original', + 'no borrres el original', + 'no borra ese mensaje', + 'no borres el original joder' + ] + self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_no_delete_original) + + def test_on_punish(self): + phrases = [ + 'acabo con el', + 'acaba con el', + 'destrozalo', + 'ataca', + 'acaba', + 'acaba con', + 'termina con el', + 'acabaq con su sufri,iento', + 'acaba con ese apvo', + 'castigalo', + 'castiga a', + 'castiga', + 'banealo', + 'banea', + 'enseña quien manda' + ] + self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_punish) + + def test_on_scraping(self): + phrases = [ + 'scraping', + 'descarga lo que hay ahi', + 'descarga lo que hubiera ahi', + 'que habia ahi?', + 'que habia ahi', + 'que media habia', + 'descarga el video', + 'descarga la media', + 'descarga', + 'busca', + 'busca y descarga', + 'descarga el contenido' + ] + self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_scraping) + + def test_on_scraping_config_activate(self): + phrases = ['activa el scraping automatico', 'activa el scraping'] + self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_scraping_config_activate) + + def test_on_scraping_config_change(self): + phrases = ['cambia la config del scraping'] + self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_scraping_config_change) + + def test_on_scraping_config_deactivate(self): + phrases = ['desactiva el scraping automatico'] + self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_scraping_config_deactivate) + + def test_on_scraping_config_show(self): + phrases = ['enseña el scraping automatico', 'como esta el scraping automatico', 'flanabot ajustes scraping'] + self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_scraping_config_show) + + def test_on_song_info(self): + phrases = [ + 'que sonaba ahi', + 'suena ahi', + 'que suena', + 'nombre de la cancion', + 'nombre cancion', + 'que cancion suena ahi', + 'sonaba', + 'informacion de la cancion', + 'info de la cancion', + 'titulo', + 'nombre', + 'titulo de la cancion', + 'como se llama esa cancion', + 'como se llama', + 'como se llama la cancion', + 'la cancion que suena en el video', + 'suena en el video', + 'suena' + ] + self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_song_info) + + def test_on_unmute(self): + phrases = [ + 'desmutealo', + 'quitale el mute', + 'devuelvele el sonido', + 'quitale el silencio', + 'desilencialo', + 'dejale hablar', + 'unmute' + ] + self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_unmute) + + def test_on_unpunish(self): + phrases = [ + 'perdonalo', + 'perdona a', + 'illo quitale a @flanagan el castigo', + 'quita castigo', + 'devuelve los permisos', + 'desbanea' + ] + self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_unpunish) + + def test_on_weather_chart(self): + phrases = [ + 'que calor', + 'llovera', + 'que lluvia ni que', + 'que probabilidad hay de que llueva', + 'que tiempo hara', + 'solano', + 'sol', + 'temperatura', + 'humedad', + 'que tiempo hace en malaga', + 'que tiempo hace en calle larios', + 'tiempo rusia', + 'hara mucho calor en egipto este fin de semana?', + 'pfff no ve que frio ahi en oviedo este finde' + ] + self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_weather_chart) + + def test_on_weather_chart_config_activate(self): + phrases = ['activa el tiempo automatico', 'activa el tiempo'] + self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_weather_chart_config_activate) + + def test_on_weather_chart_config_change(self): + phrases = ['cambia la config del tiempo'] + self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_weather_chart_config_change) + + def test_on_weather_chart_config_deactivate(self): + phrases = ['desactiva el tiempo automatico'] + self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_weather_chart_config_deactivate) + + def test_on_weather_chart_config_show(self): + phrases = ['enseña el tiempo automatico', 'como esta el tiempo automatico', 'flanabot ajustes tiempo'] + self._test_no_always_callbacks(phrases, self.flana_tele_bot._on_weather_chart_config_show)