From 99853339c2623f06562d29fba3d5ab1ae57cdf1b Mon Sep 17 00:00:00 2001 From: AlberLC Date: Thu, 6 Jan 2022 05:18:42 +0100 Subject: [PATCH] Initial commit --- .gitignore | 161 ++++++ LICENSE | 21 + README.rst | 16 + flanabot/bots/flana_bot.py | 806 +++++++++++++++++++++++++++++ flanabot/bots/flana_disc_bot.py | 133 +++++ flanabot/bots/flana_tele_bot.py | 127 +++++ flanabot/constants.py | 100 ++++ flanabot/exceptions.py | 6 + flanabot/main.py | 18 + flanabot/models/__init__.py | 5 + flanabot/models/chat.py | 21 + flanabot/models/message.py | 39 ++ flanabot/models/punishments.py | 23 + flanabot/models/user.py | 30 ++ flanabot/models/weather_chart.py | 125 +++++ flanabot/resources/mucho_texto.png | Bin 0 -> 82944 bytes requirements.txt | Bin 0 -> 74 bytes tests/unit/test_parse_callbacks.py | 290 +++++++++++ 18 files changed, 1921 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.rst create mode 100644 flanabot/bots/flana_bot.py create mode 100644 flanabot/bots/flana_disc_bot.py create mode 100644 flanabot/bots/flana_tele_bot.py create mode 100644 flanabot/constants.py create mode 100644 flanabot/exceptions.py create mode 100644 flanabot/main.py create mode 100644 flanabot/models/__init__.py create mode 100644 flanabot/models/chat.py create mode 100644 flanabot/models/message.py create mode 100644 flanabot/models/punishments.py create mode 100644 flanabot/models/user.py create mode 100644 flanabot/models/weather_chart.py create mode 100644 flanabot/resources/mucho_texto.png create mode 100644 requirements.txt create mode 100644 tests/unit/test_parse_callbacks.py 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 0000000000000000000000000000000000000000..e3be727065193c8521d669deeb22f902cb9b8b7c GIT binary patch literal 82944 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

WWlSF1;%uZSz3M1$9)#np3 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 z zWwnR#HGu~J2e29Yd=Q%=|L6<{#_8231}${|ADE{Wx~ZJ3?9A?QebH;`dqwxWN!~nV z$|C!-0nG!lK&fwO== zE*Anobw0U^{!U{iX~szSZ2lg2dIFBV&$RH)?D-0}9lYHb9?CXKC7++#RRib{QU2iU z?>z9_!w(C*YG75^l9R1tQ*xmGT3dT3azqzRHIUp~@}- z$jTdFiFPQ6%Zf&`4)|%qe>D;|=PRiW<;n@m-8u+%_vBnX49MXmIw68{kNi}H8J!^CrUlPf@^ zJ%MNmvv@i|#p^O`%KJXy^_gKx*Ebt3$mnmX8j2$BILd??C7`e@ij?7-dA3z|0VM1n zf!yveRYbudy?2K4WgcA|x97Jw{TOo8V}}g_OZM6a-~YlNKPO6)_Bgx*zRA&=W2O(& zHS2s6AxlC0-LGyrk!m{~l9qJA|H*tUQzGYr?T9~Xh59~Ie0JaH;8#(k3_$^!@dydw z%ayH{`BCJh3^CO*Ykzu6OA?zy&JvY;QY^ny>J!-<%SP#%R+5ZOnHaufVX=3SFQ*%9 zW|>3%vvX4iv5oVeZhkVt^y!g8kD=k12pkH>!r#*lDdSt?-q54Ur@)2oQ3hS1&n<*T z{%iiCEiBnJntRA}d-^p07zjTYn+-I!mnv^yL^2YZ8Pja)944n7YJkRUo;<#kn;$Al z>}~hUmoVtv-{tTY7%yMi-MdLq>QN64-J0ppjsQpq^j;(+4A5gcil>c2@~p z^|zmvxx@wfBqPeYRI0coxLhC9u^9zt)mCT8pZFTR7~INb5G|4hOn%=d+`!mHZDlN} zG#*Vm01*zZk;{tq8wp8dZ_&%ExU%eFY|`g-=P8U)O&utx%II6ow=Y$GmG8{tzRW~+ z`@aUfww49&!!IPxti*CUN`|Zs9DgnG`f=37GpQIzpNr`BtL3vJ4GD%lq$hHDMM}(s z{F8&N=Ny0jZ>?#&HLV|x$#a+?Jz=vX_&UzxQioCVPy#)I_>nc2GuutLbypy~(937j zi3wt`tva{IYfBpZuFw`i8qa;xE5o}ml2XZ$1wPd>S9WrGKRVz@2Tff3>KK59-=^Y^GoUYvkru7ykV z0mMzfSEZt*NJ6d^n7yaVSXE;;(6XpeF>GoQtN!FopYNi%**W!9W2>)eJ!seNg4I7{ z)C82&lA+?zlgpW!7MoDmt$2(GUG)7e1x5SVvv=RoS*28@s5qEArD#`V1pUJ0A(8Wu zh8PZ|bDv#OzMZbV4c+T3P`Mep5PS=4meXF!&W(B(UTmr0G8InnB{2FDkI3B)c&}>D z_%3TurEd zh!&qncje_{>GDs(w977=H8GHx%w$B5owQyou>c0@SA^xBXWqRh-M)VIN2G)m9! z{+1i+71vTR2rBW>o19BFnyMmAQmE2fhgG0@V9FAHF;B)a&sX?7do6v}|I(K@;%3TM0L`pv4q>B$W3+ z_p-kwIYacSB`1C)i;l|-^1MafJcIU%Ou)nQFBvWqE?IJ9PeP(6L_4cU#?V@-e8Ncq zS+#11&l{!uW!fjo6F8Y<2oDwFtgRdQ{QIKFu=*De=yKyTuBC&n1hLR-3)%YiYbzTaK+5Er2Z7hu-;xgDN!~`%}QCac#uUW$pMYIq>CGRx&P#F z$Xgo1W7s5NI0ik{TM3Nv0yZ*n$x_KoqrUA!hT>)KLoN}^SyWbtT~BkWEEdrtIQLW~ zSejO>|HkeM4e#l3U+;0tLghDwU}feXuc=o7%F}I~x+$J<^SrXVm!dNg$pV6fYM-QZ z6G2H3n{(Oz?UKn`>M9SW6-a}R@@f#&pZ7y0;b=EB1fycEg)zxHkNJdnoqDIz^mg6I zt-f=uNg|W1`5N?E9J}{t$HnG3dTpa_(weiL=mXW;NaZl5sa8jE(OEFJ8@RCYh32+G zP!-)pTh;)nzCKH!I8i!njy|)}F;CgO+YEUBFXc@1(egoBFLlt4wzRHjfW}DHbvERw zV)_e`oVLWYtToB`{ucHqGu@snRvt^`FACyOI_x;|%4lok67jJkK`paWMd^ArAF2*P z@= zFK*T7|Ak&WPvgUwxMrj@^6Vcl{blv{@%&auIDzMap6dhkd}P7{4opA6+6?rTov>fc zBTOAIZ*~gb zMd#B-S@iIW5gt{@AZszF_Z0O~3<+99(Hzf6#)pppIUQNghp++eq?e9x#tOI^>IN$t zg2Me78;e(V3;j$#jJXjH$J*2(TTLi+w9CS{O`Qu?xTZUq3qLcmN?+*mk>4})e`FVU z7mxdO-|zmT>%Y~xcU=b>WIMBcThr`LEkiiAXV-S8F~A9q1ZeCI=se(eQuxQiT}`9v z`2@oV5F9?Oj;b)Wh*hh>`Pvi2XU&i_V>11nt@na_b_a!g+=K3W1y&?%TjRp*+lR{RPYN zE0nT=-!84@2E`pAg0mSrfs5_VRL!TS-6^yJEd|-ej@*nghEXQ7KQS`+Ra)Zn8G|$u zB{U!h@hgI_)92M&jL*x4&qLd}q!}n;jPy9ADXa4Dfb;E5(-Q+A!Nl${N0IfAe@KuQ z0i=4(u{Ccowaz|o^{t&E?SK|_7+@HV!{+B{!00oLsk_5cid)A7-$GQb{k>nQbuGgb zQd=|GYGEe{qTHQi0z08Fj>2NkLv=B4dhSR02p0OhtYK&X2;skZ6AI{pv#Xjrm$-S% zv$yY)UhQY;jY$kb=7F1v9lCgrTbNu>f1BQ!_5(j6wlIgl~55=9Ui2ef}v zfO)j~)0bmc+h4u=n@=%SjUc3N_CFJVU%G{PBB&5A$&nhAO2-4cVJdHp`3=`3Rk=6& zO1t|#3_~lp3KFDa+*-m*&2Yx*1#ANr?8G2l_4rbvF~KY`>C?{Er-k*H5Z4pSe@{eF zzkw!CIeNMB^&?Pzy=k$&=xyAcnxwNHn9);s9T@;G=wYO#Vo3X_SVgvb z9EP~OnwIJDo76g_WdZ!!D=JpWs5KexqZANE)NiH{>l@!aCL%~8tZul?_hbYqK?d;m zO@5!6;{ZSc@+9-9xGHD-y51G?^V<2QyfsoAHuM@gd{21cxnMEdNcaE#I=X{7X*ALU z2Jq2=`IG&ia=g8qJ8{se>aFz7>!ZG5CUdfjh+Py`)bOCW8UE+D!KdJGiLdM$kC)!v z2AmOf!%RaLNA2iVeh3t{D9{ls=MD$Gl4Me;1CRINwlrw4ZqT3OtK|)6O2O0nO@hOA3 zaLWKHf9eKYse9Yqs%ezKoG)R1s8W?PQo?%xI(kSZm)-h221%eb#OY-m>ezTllQkyi z=RBkkSt%bOmgYEo{+n>ztJYP`5Ta9t|m%xn8(J z^~26bUa|}e2gFWrELdrVz>Q}z4@1w|O{Q&-ZRIFmuAHmfm(&L;?L`$srCwK3{p6>< zF{M@}M-!u+S%vLAo)*3m-oPoYD+X0-{~-V473-{pkoF(|ir}$Xu$50+G9wxKAcBjl zd6#AfmGjFNn%EI|a(p$iTxm{@c7)L)S6}Mg|1u>Iik@D`M5NobzHit7eK0(|8fblV zH!T#E7=lNynr5_ewXeug_WQK!n^&Ti;y+?b+VW0TL(xsSnum)viyX3UXc7BqhJscW z5Yyj)(s(g`fEoFy(Cl{%l}k}DlZ%@<8wk0=#N5yx8@H6HyC=i_s(xgY&;EviB|!G9 zfA`+=oCEN18YDzW-mU^4OWelNw;%QPP{^jOLt%>?y^sV@W<({%`*PKCPp#dXdN@=! z?Ew{%Ex*3hpw|^ei^^rzMHXmO+N)sBRi&u{Yc&GD$J5Hi_)4|9g_lwKi~Q3M=PhS{ zDywOENYN4;32)lS@0=wphqHH1nFv(DviF`q8GWJD+w0-s-8HWF>yE6eEWT$usP7tl zSO^F#pe5?a{$s(8mIl7P#gZTH$1uo*HkP4%En#k(+Bn5mu!`VWdP+dc+{zj{odZUN zn*>F{j343d#NO}K71jKTq2_XrnGScvqI_|_DR{#4bh+jF|MEu|2MJS)a{>`AEQMsl zlk3rhhKP|P1HnSRN|*#-aH{)BCrMT>T2r$GG)54BRJgUDpL4m z5S#+9>YlGW0P~p9q*$oG_UD;h(f6N`e3B5FH?SJx{udV!j<&sXp zd29?7m%>-%U)fgwF9bs+3se#0c~n2W1dS z{A{WRP!MIs+Q7&&p(*NIPeai=0ikeK*x2g8{`Q8rjJMGT_&2Em-?KnrVpW^6)q=*? zC+CtiFcP@JOwjSBi3V4J|``lJ#?g_;0i1x<|}d znnzqqKD?+|KAr&Ug+vF@U7$Y)gKAOb64DCb&=`T+KCp;Is@vPRSns>FX+3KHIpnOn zG!apceZD>fX7E4IJC(HiY8RiK)oV{2BJz5CpqbehLW8jK9QPCDn)0(RU;(4xyYkm> zM_kW2{VNKeoL+hK~;DHeT{7==CPn2yJ2TPH0xl@p6C%yhjv(=6%T7auZo5D|`+F&LjkHRI$(sIITu+hvY(2VF#rZY>pwj**x8lbGks~}|^ z@6OOWO_D}|yr3KM`YhU!1mExv#+TXTmbDZ;5(2e?mELfbcl@3G#S0*Q8C}^6ebBiP zXt~r&K=K$)QBtx;X-f&#{r3}dA$!gnNDNx{mmD;n-XhfKL*ny#DMh&663b`7Y;v!q@zDBk7uFS%It?ksxhIay z2pXdGoyyIqo+%dK1Dyl28Wa1XaA)Vnrfn4u?tqd1#PcqT>D6zsnn|U4_HSEcuaJs< zappGKhbgr=P_IA7?OKhv{@**P^(N~mk_CJ4uI_)V*nyiAR}JajEJj#vJs@({XTROBsHxT&+?7pThOzP>u+$0A>HNizry!<;MBf7ldx9eHVOH+I0S=5 zEA?Y~b)q@*Gh>`hDj5f_;!B+$1;2nL+^Tn8CpixFynx`JK8_)^JAY~Vt3R_zLx%_( zD0$xR{y7c?e}70-WMA#Ji-MSRM?Da}ylAX6`f)YT1^0a*_$|%I=MSO7jLT9$yIuP0 zlG;(-FsG8_vU@ez@{rmT+itK1{omr?EOuhajxb;T2<@X3<_YPFn&Gh4*CR#Pa+91a z%efLky|E7k((Q9o>U0z$$Rt19-?o3?JXUm7q-%Qk2*I~VXby9woZWdNYRc_D&R?}c z{v~_d$*VYQyAa0pxBr661zjaI%{6_fE;8@^c zCoJ6p%hH&*p)oz1P5b+Js2?hyydju-N7>)aJ+Qx_WQ@VA=_|&`7C1;?k@oZFb#dC~ z`-$&mR1TR$OKyXd z?j7yQ^7@B|b!l$+Sld*rr~c*v^7NT7XTt8q11DQymK5K zok5=TrS`=YZhj0rT>0Z}skq?wj#$GK(*-pTRJ73^T70Nk^{Oe_dB1Sn zWygQ|itfZzBrnleh7UFsv3wq3)4e^bQ1f4sa(|IC132t4b8%UMlr#{#d~vQ9#y1IM z6zI4;I6TM02<6WL{8xjd%%jLjRm1PWwkbY$ z0gv))Ak6`6#z>64&o#cQMcdS+cPC~nk9b_PB>$0exA~E(luRrF8#>p#0xs%e(#d7M zs8nK=IuLRWNdK@`6@Yn=67Ws^ByUbz08TCko08L{AVJ}JmZ zA^CyR6v3p)>}wd6QpIbgew>~FdJJ@Hxi8X~LfL6VF@M}};{tAzmO*~YPoR5H!@75w z1w1@;mngUIYN6i-6i$E#Ei~{2d(qTgA71|}5QNh9e-_gncNKG%Rux`B-4caOijC#} zg?*ihu>dam`(NA8d8HTJSr>e1M_K|~rK?NhK_4yaq$s>lLayZgOaJDSN+rBT&*#8I z1=8@l`|VmU)eS7HetCRZwg)uipqsi{X!rKUOKAdxt=tyoTvg@Nqn-OxKs+nHrd~Iu zLqbxhsdy1{bT&}yy{c1%)FV}Xl^E;0N2=X4+rb*t#%=?DloMUA*(NSa&$Zi>0j zhgr;;SK29g4*PjKyt?ybizXM+XaWZobB^nc_2rmDRmEnp+#U*bZr1js_iws3!6y?cyHObgSL|3TZ5^7-)RX4pY^-BSmjdj$yUNx`p`uK!V1yAvyky-`%d{^-MG zaI~w`qZ{Yj5>jh?o_H3^d@LGP_YZ(SUS92;8&_N~4jxVpa>GD0=p?;i_ey`%Y24HL zh^-^P=9!=flOJl4O%(JaK&Fze>V(59%gdGRhjdQ{nNepBJ2^ zSk`ZR`ovne>Ca_w{SYl`tUx;cUwh9YKe8{>Rbc;XmuzWguXgQ^@0x+5iL>S}?kZPT z5$~JxAaJ8=Lrks8C6;v!F9*CvW#~AnE*Vp|$275L?@a293usEzvya{ni9w^xXoym% zY@XzL>NaHYkx~Y`<3=XIR1$?Hqu--Hj5g6&dAe(jOzX&ozLKg>j>4)N8Uv&7IK~F%#wZQtP6SJ9LSMDB366v~m+$hUM}!oR&WmVL#S+HmOSco5LRt?OJ>!Gj7cCmf=~mJKwH zH%8X9onA)xPxalM-DE=MXrw0i?M#1+o(i--mGKG&jAbDkW$aUy0G~(d#w#Oy+oh}x z9CI}!R^R&0C4^5u_Rx*YM6l2p5?}lKE339i?~kzjy|Z&&o+*pIkGLO zdMh28hFaK9>hzw9z-{Da*0JB5hBdAK@23=V^RO=*j&eu&D0y^hQ6Wk)6WnSB*FP9G z-P2PqtE=@Xw41m>Vh5LHDUjUm zvUaxeTI6@HA!vTEfJ%&C~TCkFT)LA2F5^j06iq zgu^Ns{sn>ys>ED-Bx0`EGJtVq$!|X2Ya^XMw6UU87M^!8TM$WV>K=d8`^F*ZR_*+| zAB0C(fS~YkG@=ews1ac2y|9TGWQiToq`IL`GNwE$7?7({3)pQTgX9MyXmx!+7l-9_ zZTimfdbR~{V|Nq~aHieueigHe*T)BrS=AMl* zR}n+V6*(dy3gu?*wJ~G57;~pXh`D9%^ZaiLd53xq$EGW1CF zjp&PywJ0(ki^#myqE|I$-Frwxz;S+Y!%wz+z1EtKmEp-X8= zS}Q`#qre1g_|Wrs)yK=k)};HQBQh7WNgBiROQC}bVr#ND&KS*boLGq%^s3uGa$gyD zp^Purbw{T+PC5ub@HXLmu<{_-w}|6iaHm_1;4xKi(9@yh3r4+*&{NMc#cztMh(Mx^ zPAbLTbUEu5-;wyyDBbEr={13*t0lsJxQ(Sg8xLH&bHeqGhWXWQ*S_Eh+J;VHSY+X3 z{ZLRXH*Gp%7;dvXPvsu2t_&i3MGURfF4rIN85PI$hL7Yn?~6dqbZF{P(w>fkouuHP zOC--#aBoG9$OS+-9Hac>#*H&~)8#-`S^-M^-xQ|z%N?tpxLV|gJ(buc35_$G7rTEo zk?ReYa{jtc9ipBdVDqwR)GH6h96NG{e6V@9?wHRRTXyb4+JNoVv(?C!GfsBznadUh z@B%pbY9AkiOeO&w=mFC+{%7f+YgW^U$7Q@L5~78As2dl%8JWKH%K^--)L#>&0(g|L zcWl!D>_klC36H>ZtLxPD=qJYtsHTtLz*u$iZ3XQ&>Q=kKa}!Ib$BbY!lQ(mx{JL?**#Fy8Es46AX_LL-ZD1 zkqGV`5R`!N;Y-*>`W9qfCHi~B-hZ5>efa0H2lqG!PQGZ7{LAZ?Z)0e{*{Y%7;kh3U zdsOGST{{%Ut&5gX%hyy5nH{o!TUJt0d^M-2_>dfE9FjOuhd-EUBG8Amby07n!ml}X z?#MR3fCxRk;QR~;F3K=3&8w3fW5<|lw!kvQ)p*n#(+y*J$hnon_lcR_$~BZuLykTf z&X!YuuXf11vbQTUH$;oo308nU2}1c&1@JrLHn=@1`TJZSufm7^qhG%crq25r!{93{KK-vuRp*?4T!pr=NF z#|lgi7ZWz8a#e@wI!(EY$oF&7`O)pmkJG~v%^^SH&kpuV_p=V!sqw8e7DCzat^Kya z{$=L6q}Tll;B7S5b%uRY}bV;(kFik&8N~6}{-+9j-nuV0GT2-5XwWiz)>?X_a|B_6l zHLo@9JB-(nB|N9gkdSN<; z$Ggm4PC3PBp2_ZZrfOFViXu7q`SPPx_hm?tMLal^FccFI_t`bkLvAP1YdDW|ObGA7 zy7F&lf5sZep)?DqX7M_H^MV+8P?dhQbOHt6EBTZVQ?eQ>UslC2W`o;LT)n3kD6fSE z{XsKW!ghj{1eHZ&>>v&VORvrpB0ECqlohi_*Ra>$J(6nBFG@fQxJOQu(`|`7c(}NQ z_MBQw41Btd{tK$%MONj#J!bUJsdCEAeOuD2f(L6~+nl;JNEN`E6f4Oy{JX!gP|$aNZ+kY(XScG1goK_ z8$sO{YuI~L{tSE|xKEl2crV-k2+mmWxAv*gF3&ZW=AUb|Ch=pr^fcJ3JxyXeRVch0 zw((Nm*fF7dI`h*)<|KQ^ouiS=*$Gxt63efP-l5haGOp zs-W=ms#`iwJc51ulvI3^7!`jSE*d&>)WVBFJy_10@ovvrs%T(cXXDE8tcYb7>xdm8 z9i2vUPgm@>$*BB`UIqyjor&IAb0lowW#xtN;?AgEJs4-IZ7WNL$|s;yDw(>pwPP{MYV^PSHvVk)Sq66ZOV8z! zptFNVWWLk1DbO{i9hI70#<;~q_-JtR>fHJL=}D!=rqz7kT;~qo9i4eWJnAa#v!NM; z+30-4Qm3vKR0DA3B8?cnpdYpbHI82D{mjf5di1=_OG<-(Rv$Zs;*RF*5mZJylIbet z>$my`2PGEevu)z*&=18 zjfYRnWNbaGo(pEWz$=yN*1FcHd+^k!=~v`7oe?QvHPx}ji!*8_KK2Ut%$RZrch#9@ z?)!Jw^(A^^7VNK?l>4A)-IDGL9|E|FWOD%Foex9sX4^(PG?Up`0OSd;3lN^{ zeJ;&}SPN@lxdB!Gux4|hqtxG)|Xoy&(5r2mHV* z{vR-7%9QIjw&&>xaSiE+ZsS)d$p*F`>h5@9FO8buh18Q(EXv|`2GVw1TI33i(7zPK z(FR4=`iDuF8>N`TGTD8%A&`wk84ILXB|TrKj%M%QOX)?-E{d8AG8;wR`DhvsiDmir z^l*(fUglxduCqh-rI+2^;cA<%0a3_&?A0+^%~0ahYxW-?m{s zCmI5}p|S7Qp$ooS?2J^=pl&Kr@^J|sB@Qtvm&oQJGkF^qsyy87J1jOyHXOWojTw9* zmY9lhG=UGoi#7N>IJzP%8HE8{hVGtYC$yQ}G?=V*a<^V;pu6>K4mt2IBA0TdUoIwa zUP8_P?eIOn^Q5pQ&AxO^d)HKQWqCM!_G7;e3At%WKKE6i=vJ7RC}|ux&$}!7io2cX zBDb^Hh+NVEi(`_;Rr97*f;f{@%w<+Y^^?taJYBKxkO_*+b_O?j3p99lg1PbXm&a`? zFeHB^t_+tdCan5hv}Ws{(M40&^UPLLB#0WpQXB&1dkI}J^xes~W{-cQI-)|mm&=e~ zGqURSRQvq)c)d)W`c92V`OgibnI<3N!&kF1jt&CVgE$7k#^~AW3uUz#qgBLQH z-Par`RmLuke+&P4g(R#t*veAl&N-N4WHE~j3fR}g_b7poMl6O)SuLmDG;NM^Plo?` z5k9>>QggKQdIaU2Cz}MeN;Yd)@G1=H>auP8J2*ekx-Snr58G*2*rgwUTu_X`X7v7WX?+lS3ZqH4P9y(>A6 zJLf|Xz}NR_PR-@f{0Yk5`9_8M${&$c4_Us%%yUd^#c6uR%Djedl~1hy#LvyH49MV- zi<61Lhn0SKm+Pkiuc9iTHao%6TVT6n!#y71td&K7tzXq7b4FEB|!%XC+1lPbn|C~r2H zrn}YL;KE_06Sfz*M`3KC95k7MaR?*P?lvtu#al-Oxsfw%&3{FVb{-v?t6UaiCnhq| z;r$Po*uMy$VdBRu!$HO{IuFC z$80y9L%Q?gmbk_|GQH1=zVhxQDLD)%fmEcFXY{GB9qWe+J(tq;0avX3y8zIIS>QNY ztuaN#+q^_5*-J8e0z&INWxO&h9Y=?4%^smB`npmJ2v!!}4R~x3gOCfch~{PPhKNw^ zZ1aiw8bQ9z00sL%;*77z?=K}2uM)3z%tb@W^#>9S(Y&(XX1|T!;2mEyO7Ag!Bjs$( z(w|cjR|i~*WMhFmn_YR-XD@L(;C%6H^wa{Sa!YtoPH+4Q#s8;UElnN0U=U35dtAaR z6UhokRoBOWYMN~Mq>xeI#`LS~KWRr@D_BS*rGd1~50#=wUK{B|`O)){D2?Zc($n<|wBwH_P-MZ|l| zk`&924sRa18JWbjoJK=Vq^OpW+);zwddi~D_Rm{h2x37nk$!@dAF>xY9qc_i!sC1h z#->l-ja`~zs5LQGl1~43F@mm4>+7e3vZD4AxAI1-XTL9w$s;haZq{9AnaDiF6ecVQ z4LpH7>je*Kfrw)_d#WA-Li)L(($Gff=w^5^Dk;~4IYU1qb(jgMN%XXD0YS@{_`{@` zRHj)HPK`a3OjAU*TL;M({X}{6(GaLy9s#_~QlS7Ya@QifYrTN$hTGMvDn$o~{7(bd zm@JdqqEk&B4TS$iTN{BphGw-z8bUG{A#(e~@Xs!XS3?eew;fo44JxqSItKHa!+|d| zWBc5r0~T}NytYBRknGVs-h=0$@7;2D%)AU1(`)Sz80}+XmD87`Vs3!V;z?f)N}3Pn z91>+F)u;_`^Fg$9EoRqN#C%b@ve)U(bo*(W`tmiNLRRy%%YSlId&e{d;ve;%byP#; zzpiy#gVo3L;=WAahD84>h2N3hc=*Lp6&Y@pI#X3cwO!9U^6TB+FJ-dTz0WGpQ@)K9 z#{#F=6I04W79*!(##zV$U;0=uUa#)G$oa#E8_XeAI}wLd`)b-0^CSmriC)Yysiiz- zDwQOv-?|}v?$*0RE9VB#LN`IHFC}hm-(YS2c z$DF4=@Iu*HPODVK|A8>K*sWUa6Lb0S!BKeelye4z`oP2j39KSH=eeEnLjU_}cTO{l zo1#y7zX;$}{63`edy*OZF*TgoU@Px(>bwU#@wSK*w#M5rl9ogKx73H;#>L2%l8=^B z21ePtkCL|58%>Fp+QEEHbZ9W+uzRY^-N&rTQf+B765luWxKv-6Z*a19rK!+RSk(MF zuwn2TMZGsaY(5jzFmZv%m!22%M=it319}B>F&Wo^LlVC*#)~_2Ahcwv`OvQe6(gai zZv0lsS~>7OZ+RySftmYa213dXEOpn~HejO)XYl;=XCO1#fnC<4KXTIU7sSE$4@7y1 z=W60g*84U#rm#`o4;dx!n&m~D=T`pYwi%z+xSDc}@#qG?XAMgkAvXBJi%Zxfj1T?{ z{JXG%&vOH8UzQ*#qX)09ovnTlc8lJ4_oQXk02KhlEWm@6**s=ZHtx7U84`~JVupgG zp6SnFnjITyE}8xHuy1Jn5vV6!ztMK%X4I3Fp_iGN{8%29w=8eW&@y5dt6+>Pd>8Nm zmSz<_sfcp8ns$hiMWwnw`Mb{J`H$SX;XZspkYPp`H6?f=&6E|hc>&S}BkYZxRXhAu z-1l#JZk%s{-e88$vQet%c%U*oY8P8H%25)Kx*%*Zj?I^;(d#cZQoIY%mDGDQ$M!jw1)*rq>AJ34>@rb5?ZyIghcgXMP1+`PIu`_3 z(=}2_9QZRF8l|aZ$T+w^#$B#`Uv&CXP1IrDQ>SaF2eG|hI)AMd9z`>>9aoxH4lT6y ztbH7|O2a$4ncK;8Y@R5pm&Io%=b!)Ws2gWl_qp1mFE`5P?~sJUc)Gs#g6j?RG&pFP zFnXH4984)E+|h~b#e1m=-(!mF8NI2As#UoX4hLq)l1fbD{k287qY&l%i+*+pqF=Fm zJmT@f%8_qVh;+hVG(n%4W1Y=ILtvwCdjkp-d*=h53Vyt`IOIW#Tzneb@cG9FeNYT+mkgm-z1?pzJxW{b^3qR4%`QBi z^1!)_7m%xLeV=vTF&E3zr49Ge-(e#{{@TD>1!3beRq*2d<+BC*d5nqs4WUc)U6TAd zV`VSeqekGPT})|Y^Sn^gop->TmrbsN^~<5FY?lX!-!E9rL(Z@mA957L)SUjoa#M+_ zAB%~5A0jeX9arwk5+;W(&yjC8HEz%WO1gqa2E51deHMpRXZyYArv2ARHShnd3moT; z|1TtqFM8U0AQP%cqH@5X;4FL}8A!B4{uPrZ?3i*-)+OU&*`Uydx(uubu3+sJ6|qoN{NP4LF3)V3KL2r68GYkN3mg-{YplBKUG=qH z=ZuspB>6gq6$SHSCsv;xe(jzBef#zdR&S=Z{q_8QgNHa&pu{(PepIS<4eTn-SXZaz z|1f-b#JK!$#yr)QEC*v0#>hMvV*Ur1-Q|PjSgzYuOw>gWwAT^(^3!C{ugO?TpNQA& zy+ATp@V$=Jq;oBVt02f=g$BmjD579nFhrMhs)Eu1} z`_YQz%`V++5^{&rOj?}a2#o7W)KA;(@Qv`~=qZi-BcGPW&%{oFUoz~>TmRcR(P%1Ek?HQ0gGvPQ3* zhg|}|-8kP<=`8P5n1NMyU3Y~>(scol{^4x5-hfM~aLsIn*SlR1oLl>1EUCi~l}oZ` zrXyyhV9bec@JUVwb+#6Y-^P)q!)-@Fwt~&|qgs2H0*@^)B$V~m*=axL*`G0`c3fcc z;E&fiba1++H?XKvLL~11`osz6kN$K;9=txB?TXyyK0K`Qr~xuJnGCO2j~3sr9Bm)> z14?i!L2>vyb_uX&-M(%7rpR-|g(c%m?L9Y(XAWs)0966?BahQI#{}QwZ`rX_g^2YM z#Q)Ysk?q~ti9KEqx22%vS-xLlY-EuMLvI8J(&c5>$-28?%TdI^q?%yT82ZibKe*%x zkz_A+9O_YiCMLv*28{CKCBh4suGN_5s($%0_t3;2_7~rEjooVJ>}7jqI66v(DWYqu z*@MFpj{yAEOABzz`GXb?pTk|Mt4seDp9J?x1f8lpiA8; z=05zY5Mvlm(l2>^{Q0!@+I(`N;XgaN1be$z%X%!84`C>^V7@Yz2~jH^w*IK_p{n$Y z0}G~83M*z7MR~^sve3is`9gXJul0}V%MobE=}jimr<>aTVI#>gm>{XXrjFGB+Cbu! zjbJ_)EmdwRot6JFSE@`AKaLLND>5n$ZN2~kTeU2>eMe4x-3jd0!}43E;f0E2MOcga z&pBtGt6`}k1u&xzlUF=B2mm?yS)k|XWS-BOS(GWWL^J;Q3;6}3ReRr7;qfekDVx5) z)xCAnO3F(dI|`GPyc=Q?{UOm%{smn<%#FsiSRUjvY3clZbhn~9V}$B(vs_^spgOzUwErh5c*MU(e|tTz3>m@j-Ng^aL)TGM9iQqGlmbn+K~kgr+G(l!G`o%V&A)I*h$%18P+cMnorC@g z&QzU#5Js{2yHn?#W4umwOcU$+xiZKJYz@4S9MWauL>Rc1wJH@4Oo{eh-x{dd%4>-HV7?R@fS!zhzaPv%;OXfceU^5!Cd@ps;Ueq;|sVg6a z&_<)G<$EZ}gK5xm35X@zg6l^CA~Owa#f)7oj3$>%nFuV~fRrTxoVmtLn*=)hjhb8Z zVA|3J=-Y#>oLL$yJB4|18c3eo(zA`}3&bdkcAs}v1%w`JLb(JuFI`_cVPnWQ>de%P zFTsL&@nJ<38J-f2lucg&00b1TN`v77tJRiJSH8nlg@M3l8bW5qve$93{~}PCqOk-l z{}UfhrGXGfK`tyoH$UwpHK4_o)cGW1w6DYSTd$~b76J*oqM0Vf1ai-~n0&IDuSi=H z1IGcGQYuWz-pw4mbXOBDi)_n_5q?>=ni&@-6TzZ2PzisjFE8#HS8keoe};OwVI~iX zbQcpZ@GAfT`h>LD+J-3#|Fd9)cK%GU#VdiASa1VlkR(q*Vz`BI*r)%G>mB&pd^F`PsE+KLCY5%0&rWXS zsoXZuwur?9$+VVTFS->gi#X4lOELOrwjIu4Uaq*8uBpaa1BrFJ=T6tJZig_lHj&~8 z@@EQCH<45ERrccW{3kNrEJY=!@(^M=f4*HP^10Hsaq1!%b|a({fZea6126fvCFXq! zkj}FClAo!JP0i%}ou}q|nG5-{&c!s=gS+7Q#tA$sX*5l%|1puh$Npx8)r^9kGHpd& zT`0;_r4q6s>`vl`oQNWdRZCfxMe(;-cKyBB5L2m%!_Sz({aR3$Z#G4mr)oVBi!u$X z5c_N_BaXe5!gi%C?+5y0a&E><2}DgCUVmb`Ps(%j%vR2)3*s7g{<|%o@6elX=5dUlTK-t;!F9mT?CCS5u-wx)cdSBa-*i4*1SkOA{O1~`b*Vs z9zV~70p7Esz7}YztyC@s?$v$Ch5%JhY3Q6QzyMO=eWrh`PIa6*{Y&GXv)8{UJM!nqN;euOOWdZjxR`wY&e9@cie@5L>sPsR2uyi! z11MCx9(RFk1pPb<{|4#6Qk>2b2^=%#ojk~D!mI3ynQCFP$x~c@8tn{>xtM1*!WpD| z^n>H5ughth>eR3k5IOH_QU9*taKq^xYvpP|=q!uX^n2F6SKP}3)Wf+v<^Z}-Z2%+}f~X=B9_@LN^`52l z2w7vDgM-Mxth+Meq21?pEl11ZCs|yycQ`9ADi$#?!5%uWOerq5k1Krw8C6ja*-AmdM$V~Uu_emGF?xVeJ+Xk|{g4#7g~ zS?Lu{7K-{sxl`WFM@@UWdb6jkPj|A;k2(Rk5Rt+8zevzHSZ^xkdhhmC8cmSXRiGW-Z^`}&kfmYG-xk0$zDVW zt6^-Cn~KRnlf!JpAn1SXO`_t%a6l++cLz=oD3O0Tbo#pVPWttED4q2s553H=@MgE^ zK(sr%4ecSueAiUPPk#Uq77~H+E$S;Aqxtoq|(ac;L^E2^HLYU)M0-*v(DPK+t|1u63vOA%C8(nG>a!V1~!@v)2T(4(J> z9+ouvpQWU=@V8x*I*kgGzdQk!m)hMom-A@3_c!hj$(yCx2sijxW?G%YaW*{p zAx^=OweMTWfM;P}=rX_3{NO$G-#U^ue~^?T#R%7u@<-qvzGdM*=;Z!OSmd1VewQ(p z)u`d`nPh!gDk!fZGUv`qns#tku2<&8Nsf~5_9+r6M$8vdFBej?+x@ehPuVU?ex>Od|xfXaxxQPj#=IQ(fvmo6L-V^ zSA$E-;=u!}UlT~J$Ran0o)iRCBAZl_|N4uq6fCRgR->cRhkDPx825|x!nhzzeGQN5CZV}Vfuyd@9^u_v!i@fZplhO$DVK+>Yi z8GUxGN~5!_pR^j!*rc_KuCpA=n|sY(6y@M+2cejM7>*_`I!M?I)I#~95LWYj50bN( z@>?Em)(gT!Uk9xW#mHc@{&R)1t_ay_#qV&d2=&#?AAd+ENg1Y%%RFh0Q~_Mh$ubrd zz7E@X%G5}zmXew``+|t65~W(|C6UCaTe6dzjwC(RzE1Lg?5M%dIF5%M4~dfEl}^Qq zvErJy*P6BT29iiA#~XvsbmKj#q{1E37tVvFQrDQUQvTbN;AVFg5ygW)PY*i+b)fM2 zsES3~wwLDjKnP^7!f`}wCri{8wK5$@|A$O9Cc!~g8J%*RurFI7egE>{G{M38Yodgc z5ZeuP<7&iwHHF*618Fe;?#&xRcf%U@tK>O|0J?<$aMkLkL-}4~7pt=lU;8u=6jSnEZh9+nHiYsGFy zp^ar1EaFS}c$nH)Z+l(K?fukCa?+ifWF+*ZP-rPEhk!Fz5C)Pp>l zyFR-4T+A$9%7_nlIc$I0N%;TQ322tBe|34$aB{@~Q9`g=vTyzS-4zS^ZkdeLc^!R` z05lMkBqf<;_~lFwx<#N~e`7TJPEznae|o$Dg3K!vuUWQR>mdYy1_+C9GIkt;_nZsv zp3w)hD;qtQddUGd@{MU?x`;D`3u=^4^hvq!xX7`^sA<0t zv7o*;JCc1WE}Z)h>+Pvd-&_1YxBu<|I1SE22>6!5<`-4WKYu~sU&2QPN;C7n9BQZn z$cMY0BZfiuPT_;B?>U93`>bgW9@F?`H1PEqGB~04A9PK55MT^6q=1e%|1*hflKoOK z$jbT2dKosuR>ENFI=fLgZSNICjoCBxxTk*l3fStA9REW@6erAVqU8uW5I|w7Tf=Mb zP5zpyk2<#P7@Td}yLMR@D+^n?L7+7> zfvrBGZ?b_Tp==V5r6+2>>#KA0m3qz~--4my*^FZW`#{kj~iU;IW7u@5C_FOI!$`eHKB(KGhV zH^NrEK}0=MUkpPK<%60p;fJF#u)Js+f_brBKcje#0AYsYGiNxs5o-$sVO?q`Ofwc@ zWLntrh^ZOzL0XF*Ql1rF$!FBqXkNr=#T+fiDD#mleO^>XXV9aKUyhAWFwy^?gWIy; zWCb>y>m-s?^TWEGzr-lOcDzY}Xf8PUPY%#|`Hqm+zP~~wkmZd7%OPr#z2HroaEz2D z5kW-QZ#bIO<49u@7fSS%QB8fDMN)=Qc5=p(dy8n+>Rc6?Bfxsbx z_D7hZZKHwfZSppCu7KQ!*=Qp3cuNKVKa9=I!h|Oi%_#TjKB_CxIU&2p<)X)v& zs(z2g$V^^|F)X_*e*Fm4YeR31Ax<0SM8HqaCM&RL-?nqn=fi17&4rb{Tl%2%0lR7W zXzR=5!B?)LhB_I5FPOfnS?~iW(P#TQ(nF@3_KqA-&9XT*Pg5XCZtxaCqiVU5w4{fA zJN2Z%u+#-n&ChJm_fn>7M*xNpDzJuqS=qi?4yobj9ZM>ajJ0WhO$yjCA8(XMd*iO- zQsdBHUaKJ}uUDm^iIP>$k4EI94G?xK1PMDAO(1gUXH%HFYnqof>aXD7w((qH|1pIb z>_IopYQ+G?XA`*p#6yp=2mL0fHj(%9-)4X2uMGelu^ntAjk)SBr}ykPa9%ls*~7cdH+-Ztz4`3sC%*HDBn2bT8-ddYsJ3!S|pi zt-%5~*#te! z&Tpq@C)s-rYAtCu_%0w7ljH#a({j_{CP}%T>+11yf_|jlVf`wfI}^_x%si-4*Nqg1 z*w#$|CM9HCD#{vYDBH;Lqn6)1{U0Ov&$rL@Z+*N`N^MA4S#8tm7gtwj&ttc`z%BCff0IiP{2I8N0ys&0-xzMfH_Od5-9Nop(>f9{|}_ zVwFMAa;n(?1cpZVC!KOkbHL1#!vsV7Q+1SR-km0D;)~H0@Zv$i-32eF!|9aE|2c>O z{g-tG)H9~d>B(KjI-*Hq{a0&#c1ZlCPS-p>3Qb&$l{G>qaQ5ZM=*fN$trnW-ONF5| z-?%~=1Gr4sar78#aDLuh4Uk|cPuJ&!EBVyhbqE4O$jsqp+MG1({oh=mC&XdyjtlS{ z`k|cufCfb3Td}+)^0La2GdC^4WF{5WK%@VifRO4{H*}RSf8mZlI{8A5*qEnSbFM+@ zfQvv;!9~K&!4F@wDR^4uzhfw_8p8iLk?(+ zFDDK;eJi+l+L7ovlpr(O%Q}|_3pKCaTTnXNvKB1o7an@HwBLFT=@VNm7R!bHe-osQ z#bCy(KOcnwBVNC~$zm%*Jw45g6|;F}cJmuy)$7CiCWR3LG-RBrhz(7|?tcL3uKkoM z>hqABG2p{BNC8nnr7Suu&c-9%OSRJ)PY_0?^}M997rEEZ;&i z13W8cl6)Y3=PXeDC^Rl0VId}@rSua_1rIvGeSv~klCM0@rkdr51k5K1!eU=LHj%fx zGrO#zmyC<$qe|H9ZqqxvM&XCfcKV)jBjIt+YCoR=co^h_v8B%S@Jh&d`gK}a^K%K$ z$pQPikuQsq=;B-6Gb&X5k|i%@WRMTMJXxuWiRjaBCHJ0xnyjhi|KB4A>+JaN`XuuL z^LwT}pguN?35h5+dl2J+ny!X+xOPYs^5P?FnCMR!!tplQlzTXpk5)lYQhnEg7OMZp zY(Xwm$;CH}2bG`eNMlT2g7tqOvI9uu?dt%r(&cQOvDZ?n%nZ4vln~x!K2Wzshq7Z% z29Zc(!_9L_ar_z`gDKEttCdaF@ugU?jsIQ_4F3EfD~ezL4n`S4OFY%A2kutfK3UxB zhC%(MO9sC{MyOouO||Z|&%~*RVLC6?9!l}e3>z0d>ih+F&tF91)$Voi4n`BrP2g!# z6^diC_V7>Fx4E17{-QE8IeOD!>E^(< zN^CIjkK_~k2MzYqhr^&FtC32h+Oj9vqWfMvt{`|C(88hx7rH5Ui%?_Ep zq%e7&M{c-loCM1KGikXzD$HTamLE6n?!q955DnMr-LF;8HTVbFcmUtyT~Au5yRFx> zQjOw+!V9&d5E~aN{fcMW$0K_iGFZc3s0&Xf(83uex<>`9a{1RermnS4J;2>z=}h?5 z6FS-yFyoTvCCe_P7Jb%wH1KyF!EDQO`f=-*bY3?WB-(9!uXp7qOvY#@544`W#hcBP zXeu-L{sl$Mj`?P=w}v~2?{Sz~5{JdN?A+gIM&*Jz)kzDu4t}3h*-_CBanWkJ_sIt~0albx$ z{uf!Wt2Z=*9{76HfO;PFSxSsA5!8#^a@z(^>Z(G?h5N?brkQY%#bmgyy7)S^?)s9R~zq@e+{>$!(|0vSGYR0g8$Nc)a_uHCR4)NvG1DKW*mj2Z)~M z^;1(pXLvCt7ue6utkyN^-wk>l@Y1n1E~hH>;_=?GFW-z`xQG}m)gW`m`Wcv$M&-dv z_TI)omvxW}lI{67!IOimIlW*sz`7t9PZD0zQ&xldK0H~woO!B)J#Tu zXtpaQAZqEPMa21AV`CW)Dy`|lHPQSZf4~*CTS?pwXx|t9O{;rLVx`vP+vPDoheR- zCt=Prx<=E0j6~?|Y24Kk2rTIdiuG#W-jZ!d(~G6OzTBA=5<3?wSu!^D&Tq2Qj)2-f z0^fl|hD$)Qyhc!Tck}Mn!C_i8L*93%(fWIfLuCJ!J>dtwc+p&wcwc#vT#%>2&>!DT zOn6N?%8vgBZ~mUUakzZL=dhJ_@WXKfXnYnn0BT&^YJ}5Df7YGr+?1B9%d(JTql}8@@*Q)Lno5GHMs6CpQE9>w*9Gz^~`3IqGfHPNCoey-6wj#AW zazp;0=bMMUUqSg38bW`%A=avS?ncjRlEe0ebhop(nx1Oak_D=BLSsDS#+_PKwvH-p z#rJuXX54(lRRjVeDWO}PcQ7zgKE978l|4x~uQi&B+ABG54pTC_ts#bzt)iGDq*Fxp z91nj1ZR%3PfM2%;+GUjZWFP;{*?(fXP~(R5y^1& zU$H2jE=bo;pS*0=@bLBUM6lH4#NzOS+D6-P!l&U1DOrs7yZlLG(6ASBmUwdD&`1_} zn)!tH^Fb=H^T+Ojs^ zb-xRdPlxQoA@LI^dZXj~hM1kS(%~e>y`#yay+WzMaXaJm!Q~f^rdYdNmN(%26 z$~2Zou0C8EkDPf$^3QQi+EJc$)K@MRZ@BuLtBs0@zsfi}k-}IRvZ?X<6=5Tumu$mG z$aTJB@1DAo8ET8!eaKEyG>dmra5|(N=t;{eBL_jfj>%(-DY@Pl?T_Rzs@w9PBlURy zB``*>uvhW%3C?^lQuSo++*_fOR&-ayu1nedmDz!y zfZj8u(K0hL->)=eJ>b5~hl9S|DykBD_Obo09x_7#a@-ky_s@nS8SN`IS*YoA?&v#f ztP5ZF$Ejo33Ujl!n7LG>xaX7pw=#U0PKNPw685MPeVMJYdxK+2Doc$5L=V2oKNXvX zyVWJ?-$c>=*2^TiS&yGWu)A@XH_1OkKVa{DpcY)VYIbS>el@a}XZHIZQ~G@Dm1H(o zNWCN2%6d2lmp=ca$546R{qWDe7i4mF3@ro-k7U?u%@2&yf=4SJz~h&Hxg+-82Awy% z;tl-*o4gYbLwr3mmvC8!M=q84?`ZuBfQ|LjabF5(<3$) zVr>p5$r1VY-#5Nsg#)~Zij z5mXMH77gFp^z{kd0c(g8nC16C^TO@<3QWhUceIt-O-`m2g0**_XEsakW&d4yl)Mm_4K23-WePB6~grSXYhe=uS z4p%z*5)0q-Lo1rdJV`NVZP!}ceVzIFeH6B|4jADbs9=NCO?+Pa0xTBhUQ%yrygqX@ zUE})*c(ogT3MX-QJ8fmBa{1vgSeET##X`t}f}GIXZZWs4TYQ3k8DdzmB|X&3%~l-+ z9Lq`~A1Z?rwgYhVqcbN5USz%y|@2-)p+8t5x z{;%y24V(whzZM|pyBhz>JC#VlvpL@un&dp+-lsJm-LU!i2gwuUWa2bT9~t60w(=4C z)R*xcj&BL*{E6_sx~`q(L2hu`+Oy}xO>E@z;uvp~f<^a?@EZs6AV6i1UK26KL2uC~ zxO1D)?A4Yk5tz6E3DdREc{<>XvF^DbZqS`h7C*CQ>tFBR3;{|dP1^1U^;>lt`;GIp z^9_{1-`4sA`3_a9J}BeYIi*APE^>K(X-EJcZBy`SwO=SI++O7_Y>*G&AtbH0Hw`|0 zH(%Ql^LFL!s=bdW+C84o{#oz(S&ud$t({%jL7{fV!-25oV7nGYal3@!gXOJ9^9m6c zxrM#&x14*2)m3(}ik0DpEa3`!LV0q3Z`>Ev`e`ri#@=+XDicblm)z^Bo;R>?iu^}{ z-cVWkajVE|Q)vmt`fHo|e``F&SsbxXkgv)((JT=6KJd_53T^aq@kC9@ zVh`Y_*XS$u8I6AjcTb@ZDga)xo!rf4z(?9o2}hOe3Vj`_R7T6R>0h%>|A= zIh<^^b@7_|1J5s@hhEGIe96#p4!k_$ZdW5P|LWH(r)(Lj0-auWuJTDj^x@ABGg|>J zsgEj+xibf9TF3TjT0cjIO%6==9vu`O?eYtL*-(63ajS~yy30-4Y=`_#Ab>CM>D0cP zv5^q**sV ztn9Dpyj;fGduN9L^MdBWowJbvo7)fX(FaX3a(Kr5kB0OEJ0pJP;I$JYUvsBT^=Zv& z|I}cXUn2($jOCS4X5au>C-4g|)50~4iqauXf=1Q@zco(^o;*DMJb|#1gG9GEDjCV1 z;94FQj0amBli-$NT|K>(&XF9}!8)1tvWCdCg_;NIk;{5*eODH;CUDZCB~_8#8;y$Y zEOU;csYfGWAK$mL&Bipb^*QWgejav7tdSc{+1=sAP2KVxY~OSi#~=wqEbYonp1X>y z)Ntakj+htyWAJHW#TbKDjbL06hdZ&ZMYOR-%1yt1>q6U5;ZwiT8xV0zMb&FB4Wk<`G9kqG)v)RKY zw|#!LESe@Mr3i??S{^-2#l#RmFpq-Z>--d0|gP%qoDc@8d_);-7^P`+iff z!da%3Xj1Rfw;wr!|5JFnKccjxBJv!zQI*56{+%KICX{P9s%dSiq0hok6Ah;UH|GZO zfG-^`O=3Os^HaKKI+rB*al~Gkd}n0ptydK}m)$7zHZw#x2|7Vl8c~6*O~Iy}Sn-^k z;q$qXksmy)lQbs2$ypKhNGYb`!&!B>nShIG!x)#73;pG@X)MF4{mU^q9jf}7HAJ&6 zW_MWWeQrQC;PhVUoac_?4riCsdA%|=q8A)~(2oWzL_>zyfR}+}b_)L;rME%z77X4p zXygq=l*4AysV{(wXj!}{RL{ZXcyuWs>Fdpf_hJ_Pke^Xq{r(AKNjUeuK&1nP9<~XC z$sYd9GSXq;M4hOP5JD-$fJd<`UKHn-z&gs+g=!zyZ z4R{aFOXJh5m1640H;Ovn+7zF4kR)?{arSTC8BbsSj1OO69z51N+~0fU;T(%u&Nn&W zR(wMIVU)euyS&IiA=N0EbXctz=WGXUA*fa?6H79-n6<@S37b{*DbJMJ3K?pUwWHsB zn`a;+AP=E3mOaGbybj%IJ^RoFG_}>>i$?9a6de@kkDPz1NYh;Z8>(k%rGQB23>gC% zESKpb6g?QxMN-h_EwbsC2uP-%c`F1j*FgU4i_-L>oB1E)KmURoK=AL1`)k7gi?Zpy z$Oa`&X~?qOn;;@E4BvH?*dBVzAJt-(%8EGqE{YRM3ck#GGf^vw1yuUg!=FPn%|2$| zsUQ|7Lf*ZF_S}ThC3D_3&e(0tkrtsgRyk)(gd1MTG}G?ZvwV`Ys;X*x&(9Y-+NXQL zZfYREv6cjNB0xVnhKH`y z?~?^>c3wIcr~D2gEllub{xtQLB|FCrUzgR$qK*WN;)nck>g^4{e$Ydo#{C|Zn1~22 z=zDfJ=I`GW8Bqfg>1?2tY-r?kLLPo+)pIF|;WgWp7Rg}#u-`6bN6$8UsK)GA_AI2i z)XM*1<{u|1zjwxD&Tr$~c}GL@m_LziLO^V^e^__~AF$yw_+`H8D4SRy(UaXMa{ICU z6hz2W(Pm#4*YdVfpbtH&^)PXNW+qq@Z?PBEH~1`ef`gacL0|*qz}9Qsgn)4+EX-Js z8sp}x!{vlwSPQn;iz$@ZY;&QwfZ zxcPIyy(;*%{^L_n$5Lnv0k6<2@3H|aqnuIMWKJqoBt0W3oG)i8nZ#uzve4S7fOZrq zESuiXH{mIX$MybP61!+p3caEls5Efze@5T^Gku-M?jY5Zap%d+Z>2bjWsNPK`EkZ`kr4} z3{g}wo$;O&yt5}wGaDXZrr@>rdqKT;*5pYIRDgd|*kTYi)R8BY#QQmPOOmzLkfy|9 zp=B!{G4G!!YSZ;R`65YlD_Gb=Z9hiwL)+&!ZfY~v_S`1{#*oGXmJK&MTA3);PaRHs zwZwAlWJ|WS@7OwQ1mPp946_6E+fP87he(uUhT$Dwj@-x#B$RUcYJCorHx&E5v0{!E zZi=@Z*lk}Km)2yGpXMX1()e2li#w~TGEp7%(+R#%!8>PdYo~7w!-)zm{us^t4qpPbAZWhH1)&i`yak_a zRbLqaiS*5}Ncb=H1I_zHnagi=;r2rshnc2D$ceb~Kf2cnUL!tw8vAx@^9x0hqRl=F zZ%Oq?$c1Fm;7!`?u7QAYQy~?jevh?9&se!cr6Ui(jY=C(G?kn~Wvhf}$}MFf_8|OO zSSjQyUs!cig_UPKdUqn41X-6Mx@plsZL-($w)sGEGTYR&P$=WX5Z#cS+gbkkCD-i$ zvwE-j`*54$!g>0q8dudW3UF3QKt6oum9@zmz~~OxK-G$!PMGV`z`)!4d|ZS-sK$Qy zu@uNY8)?S7GzmCRj`K`eS}*P;a_;o66LH5S-nprr&PoCupQYAcj^|Qll9J#I5;9x; zc|^R=i+a8QY1g!pZ#myimdnkkV_o@VVqNu881Tk}|2zdMdz~G5ug|QlkN}XmI<(2> zkAS?_$_72vTx*hQp^RFi9aUb6a+0?|2JcW_lC)mF`C zHkO>{!vmhRZ%7x_e$ziHiVoYJ@$lQ|ybigrPUehW=WDeO&G0F*co5H~_GG7Ov&N@R z4Q~2*SCMkblG6O;@mO#t1-Mxi{*NAz%{9<)ecm;j!sAT4#J1}s6^+WUgZEHd%%1|j z{j+lJD0lL`8^|-i_zIpyz-IrT1I7WzK7SA$$mRUHRN;w(m&VN(hr#v%FF6gP&VG3- zxwEFp9?V=-eDt=7+F2$@CQU1~zG%xzYvOQ77n}X9od~c?3Wgp~g69bz99>9z3n=6T zLRngYdM4XR2Ul;z+4Qnch7Fumw~e)mmUXpz;!jOtu|H$ z)89pM-#JV=ej@G^`6CtE?3_P**yOw(Of9;p9n0io;5uppk3!OK&4{{%Z>L{e{A)!P zY|G|&0>h)(`PE_sumA=v`8K#*e*li>9kDKwT=B~6BPV4=oqDlM+QAGN+%!k0F1#X9 z={nbn58Qlq4otdf60icSgNssCGc*ktO6PPdB;&zilNO}oR!O}n>hMRg!QVF@Mxc0U zU{y?`F+~BYa4d@?>o`jsAn6v{szsQ|_(jT6HUXce(=)jY4@CDqg zn7W%Dg-(^@DkJ){#PP1%b`m(yhh$EQ{TUD`hg`^l#roPpbZCr71 zi8IbZ1^uGe$OK1*23+^u>Tya7u~IedzH#c#VZeec(~GzhhRgLxwUWEvDLS3&yjvdP zH0IRk7m857CCW!5BRvt${Lb(AIanE+2$%8U*NRWHa8piYsfn|~Lq9E|X!rA~SOd&i zxOsWNSk`fY(0P}nf|x1DLAxi}bwz9ax& z>S41zIHBJ)xIs)0*EQ%g5u!7Q`o*R)g)<97EIT-CQ5Q8h zS{KV5m#?3<(3*tS2mVOw`>$khrQg=F)~;E|kiVdOz>I`Sfk6e#RHBjsx+&T#3q zevw8T_A+W43aqb?Hba@Ft>vZ-_VMjjWO z(;YH_I@Uo;9On!a(mXBs1Pg8Ny?%#n_sxT2SOa|Lz{scz(TxN7Ct()8Ion*!0Tq%N zm=huZHn4cPENW5V>A~6R97~?F{ZT7b8-f2}fF?#tl=gte-s$`>ECpuxzn2`G;k0aV z|9hfn7HKC*!v=QhTqM}`Zi-{-U2gHxxs0-RwRcxt5Z6AusT?#X9e+>NL7eVD?z-<SB5W#QQMZ&HXrf#zWvtl2)pj<&6*eR6u7L2b(3hMj2O(FikPc} zphV2R5giA!ju(w~u&R0OxxOI(Q<~;O^QWQ_z)G>ik*fh$VQ{P`pUA*R(0=$?T9T}I z$1`%6Ib*zWkdR^6($I7NzO#VpuFLt?r9jd26`TiaO;oyE`MaGFvXim=hIQPjaCB3I zii6ku=Y*C+aQ}foWj}8}|aP+3l(VR6K?!}%$hkFml6`jR0){Mb#V2O&3jJ_|m&pa98 z0f11^6^XD`r`$;7qnW+df(pU1gUYNq6qQ z#lSI)%P1oTxU_S57^nM$kgwd?I_r3I`{rf7L8(hPX91boS&*e!nU$V~_mEKOV7lll zy4Ng%@5h;Ne%`#(y(5 zXnQBi9xDamj%DEfULfg93f8F{)pCGP^N-PNq|0tvm=bc{0;}sQslKHQ{Ze6vLL*s~;JvoT|= zq?*#a-GH_`->8pW-4Y2YGCjc6-6<{j9ozxCM3p-+e-bU9zFQXh`C7mUvX}uM-u*ov3#^brv@w4f&S|x(F7LHB=y9OnANJK*01uA-vTK5{V=*!M0Q6sO8`ua6 zMZ^OWA6E7(0%+4Ya}~8I;@S4{YgImJp{#pU6Ka#Kl@PvIwG58trj1SRtV4K4*#n*@ zwKKtFzE?AWqzTB&LNTM`P@U=4i~JK!AzCDjKs(w$5Phfy#%Uy6;RA{Ay4R>*rh-=fq;8teQU@$8b|R$dyyau9wlhPUH^pq? zn?e1i>`2brUV_n)!|`UG$4u7}w_e?5hj!OvlR6ovJ4x7kQ?Em&d#g_daS~mBaF9 znG#+|30`?GjjV|6PIR?$u&vS4$M~uekV+|f5Xlv8kayZ+7(`W1DkID*O z1)j02ty@fnu@hKcatg*-*SDqr7Vl&s_&vdh_~t){MnRYc-`&Nr>pu$@zlh<@tC{&N zG3szrQYzoA-!58zHC<6HGGN8j+tJ{;59LlWVhJhvjX-XVtv(&qhMPx)1;;*d~Vk) z$v-yu^@c8L{@@G;lJU>a(z`ZgST;qzqoA4{mm|0rQI9FQ(8Iovum0*@*Qx~}8>0LZ zz}@@|t+J3c<$dHZpVwRgqobFp#HY7cfT($?w}(8?gSJwam@1fEc<{pOz81>RSfSJj z+PnJ3;Mn>>@Yu)25W72LE0w*a2sTwdBxy1U6+Zu zPXGRO=;X4{0ejsnDCWV-eT(edYo0#jw+O%xG=FV#kM%49$^o^%sXz{9eGETNZsttm z9w^q6VkXkEx}MD4GQ(LMXj9lFM2JuwLYw0-2W$e}A|P)(TZJ{rfa5k(X31tUntYVp zS(-pg-n$}oc;X}tSjUE{hC6f$E|=K%4(jjb&7a-YEPZ`(jd?wL9W+|`u#k5i$J7Fa z0>LQ)WQm;?YC+#wKQoc&tD?h2tI5zgZH5-8&_#4`3D&S$phr=_;XXx2 z_UaR9bKJ9MEOO-aae|;g?hS)EFrB7GQze_!@*=ae^~Mqt*pwdbR)2hk>8QJXVKl=B zz4300+p_NLYm%{hv*H^xRbSStPYSfCEKgTIGlBs+tToFTmz?PUO?o?wf^mf@BCbij zxY9+U-+$)fdhyn3ZQ^_J&LiY+TKyF_KBlJ|rdaIE+#Im5N57}(hYazA4m{`>Ly^-_ zA^ruPa_6J|^agMxZ+AA$1ZYckaM(UKrS!&=^|CrfPE6Ia>Rnq^xj0r4G(su&9Uj+K zv9>Dr7$*bBxlaK833DBQSQH2uI9!x};>|zCCnwlmiUFJMnsuVB7x9#%UH4zcHT^N* zOXG+A=-iz}Zp9$c_~J6+l_3_6PELArf-EHl)9EPoyn=K{E0^@2)8WQ{<%HwNiy^Dw zH%{0k2?5)ScfskKa26BT_JM)8iq5bsAvlkf!aD%$*+yx{aO+o~nL-Xs%1cSJ|pyIrzlXGKmgl;l%8ci4vhpM-my zCvzsq+^TdhBc^vT6%OcE&@#ds6Ul>X5YQ6$=X`xiEw_#~HA^lUjdcil&1OFmJsac>DGX7+Gzb}Lfl#>-bf%P?#1vSJs zIxFF*n>N^vD$ohSF1AUexjJ=C8mPii9ZRz00qFJu4UfjQU+F_1Ims4Uvt0Qq#QC4W zF)ZY2deg1-(ww~9fcl8LYE;B=$v`f(K9VG)_VcVc<%qKwl+Z^H&;umgTZD;I`{9k( z1)ldx#jaFsQICA&QKrxkY*G#g5Z>w-PK)Bg^ zKG*0khHO2M82Wv*R9YsTH5cSD2$Cu7`7{@)A%pfuHN>EWE86jqi=v{yMcV_Yog%*((B;Un+t$b>v>e0 z1`gaY2$T}d14aw-GTndG7_@oE%U0B)NKz_&GWXKW)I4KG#T+q#eE#J2)#h6<{!LhEavx?Yub_ z5$jqT7|R08#rzhyCR9YNHX!WzvHKX?<1U~<^NcoDcqUtpBHTU?XXfXP z%oSXx>#9>sx+w)5&bf?Ig`d#{3xM|?EXt@sR!7<1WWkywMZvJ3S&6MokPR6MIC*&RR?61AJGqvZD9Q&X*x{k>d|B?+H20X)H6x-4UhE14(v^uLjl zEP>!1>}eDY_;TJO>Ibq>aG^EEERR)aLuqer$E2$?v({;ZfUEjaPvexH!;0$FbNx(k zvz%}p?ic*EngzlUbNZB}?19a>D?{^#9tVb&3@>MZ!oMo-NlX+HaV9cGH~Z1>J(;oY zM##0(a}4eiwX>wIg9eUo>v=;z(?NNpE^4vdaTDpwGUdUVkdCOIm_0W(Q{#ze<8SQo zFGggiOn%~HQ6+aVlB@_9Q>oFg{q!YglhU-0)(_fPgZ~cvT@2{A6ZkLy1p{2%29c8tzoA66T(1_Wd-g@s7 z>~+PDmLD)G))Xcx>3yA!w%u}zHb;8X6)VrI(*Sd!`BwwQvTN_XUYHBjAIWqkL5AQT z0L98~3Lz*MRr>tgBsRQXias`BaZokV`l~Ji<#|!HFv+%fzK{l<(|fU&Gly%0{_4{g}>XPjdl(IlwsfG>Bz*J%2&-?cd%<(VE_RN1sx zUW**rhy4-Z(c=$VX*xXpA}N0wRu8u-EbRGkKEjj2&NH+eQSWAHT`)ZVqe;D7F4pP& zYrLnY0VmllhMltuY}N$<|8rR6ox#B?4^Iv8D>tWa7NmUg-yI*@IsC2B%;g15=#pFGwELwr@%b_culg88ST=+m4 zq!QGT{IC<*-$mUl8{QP5{f*ccZ_O{Jv6j|3;nc069MT+$cj)DbA*mq5r(HrMg->a& zd!=RzEgy;@(3$j?K+q3ZSx{qlT;zj z2Hj%mN@4qlvyRnDRfPYN%1^(e&R#AI!*$IlvSfK*SF{hRfhJ8(Q_YLfCUK>d) zK6LGjh)K1Ua6(jriB^9U%Q^t>YG5;D@ z5cM|ePv%)Bj(4h@ce9W0wc;P0xsSNbqY9cWs&LI;ngQ(-%OWh#t}$I-K3&uc)!@=l zJ_ivhCs5&h%6_66ys<%{B~^Nq7azvqf|ILSI7OmYI)2%HS3jUVcu6`k>5br?~u{mqO# z@V!?iD~;=!>Qi789N2CwoaLZ08N=5pMAUA$)kUbCJ}n$y3yafKW5-1_z5o+))dARx zH_}?I*ODxAw5k|!8h2Sn3u9rNGsSSkzJVXRTvYkFX@Q?T3C|j}eLak%ONs_!PlPZI zyRvLw@1ts@g5xx@#Z73-`YO8iOQF@v`TzZ=^zg!|KW>AIO{brJI`Gp20>AaeA0JY# z)d~zKy!ne{D%{>2S>BeyjsS`a4$?U;O(aX{yMj z$6=2W9vi=$c~ISn#LC9C)6e(MMsN&1b}pE;vM=PpFhWu1SHV?v4T@icJN z*>Xs$ye+a~k9V!~ZBO1!yz5zA^X_>^xvtzS>b4ckWWPFOJys&O`QGk)7{5+fw<_iF z3NGX(F^=S2p{zk#Iu$(?V@kfr8aD(w@LCV=1G^Rm&}~*F(nEd}r0~v;oG*|k9(tr1 zR`bP@=(CYR+5;}fS2RCCFnuCYToaZ}X?JUG?Cz|ty;LpQtf}`0bvHVbJhQ4&lQ0mH z2UK*@Ouedio6vRIAQ48QC;u<6;Qoje_vFn4H>pq&LUAJ7>toQY%WxQbSR-XWFr#r} zb+}Sbiw{a~<(|fJ2TzP8a9J=81@#BxSe(^SmTx2sUSIH8{iq-IXvtVK+F#vq84f;s zdwk{Yd@Qk%YlWjaIwn&hsEkv-oDqb!i}NU)1%Q}s(seh7Fa(+tJ7|YYboHnEC>ALumz2V|u+}eZdXzq_U7DrL_5~LTpqg=_0zc1$lQ5T)RXrCM@c$=P zP+1Rx%K?lvfhrIZaiF9?7+Vo6!Q`S^VKhLy^Vt4SF8G1&yDWW9IVH)>hDdvi0!kU@ z>xQ0|a{sNQE1l)yJwfHJUq3xF?6GQrr)K9+%db?qX|91e=Y!|yJ{pZpx5OQP2BO8b zNS4cj)1$!-yFWt9bOpTG)rolw8Vm(iHk@D6j@g2)683D{ahEEAFyQt3+e$I%^ANd! zI-Q?5dQz`mHJ!XU#P2Y;c#Pg+$R%yDrT@;@cxpYd#o6peK%tZ-m&J^vdp(cyik+C7 zf{#L)DvY9}%KaUh#E>g-TKZ`7y~-!=@7e6MKEvWw4lq1roKJC)hcNzjj?cmC;<*|- z2~Ed^q{c>@Tz$vE-MPp9nl#!bW~5jvcu_EQZ6#$B$q0>jGajVdC`=!wVC`9azhn0= z3I^lr?v``saK@l7^PWmPKyOI8E&P&|K4-fnQpL?;UYaKz>vjMv#OiSMJiztC4gUHt z-2NlvaG4u7&>}ZFk`qmos-6qTwr~f1ar_XT5)^10^0OQ@s24MQ1#-^w6%X_;<8GAj z`R#s(E{}YuBhf64+Xy5)tK!FTZ)UTgMT?O_x<{35kH=`{*0N_3osgr+UsJNkjcB99 zG+poNN|v>bDZ`4Z)p@)Lvp02afkQ_3rZh|VWyJ{-UlgJ$skrY?q^l_7=xkFR3X*{l6A$_cd z*o2+Z=(px4O+n(B{GU4|sYoJ~BJLQ8hAs>qHyD3YiQk5%JBd7}j&vcX_kf88_k6n%mj@PL0M5G$49!sLz;mH9#r29F^ zguov8w(nJy9UGK_J6d)O`k!p{DizIt*tYB^x)(}MwWSN}X;9CHl8hWz!}KLNp?)OV z2NI#Md%DA*B&*->#N#dFHH*_ANTo2Xr+OK~*d9PDxe}+{FU)Jqdp+jOT`GIqFLo;f ziBttEV(2#Wc^(BY;Xs6YQsN8(AE)r7JkT%H7EsT?sG88p`4yN+D#3G{yIDDNZmycy2Xxlw;VAAZ1PL>+o&&3_`Q7 zJkCYH(^@5-W5rI%;?>xk#)=251lQX47VgAK^rvDJxfwU}CwDdo(LgNU-%Qmkai|81 zJf87X@LUx)Z;f=&P5Ph*MBrrgneBJm_{y7=cfOO~MqH9}snS!@!)ka!+3U{A+GXa0 z{xUe9`)H~)NxB{7EjVE+)FwiS+2cn|B}m!Lu_p;t@HO?V8MJI(2yC(f)IGgR55IUnq4Jxv4-X0`o zR|TSN?wexjr7_}T0OSP8{j~V`?VpwND!C-$CpdTp>`)whcTSp zJr{v#d@DRJt-h>&%(ZqDUy3DZleU+{4+#&*HxeVmQ&p}Ws1$38q0ZpcIEReA!BTc1 z;V(nX57wcp%x-(Jc%}}?D{0`|ysecluX3iXvwzmYPOAsB??BMBDiR~41H!%P3AbFO z9H{cKpGkN{R>VYQtP()5X~#*n;(Xu4jNqR2dHCyp*aRuACl&TdMxP97hN==x&SFTp z<%+Sqp{GGOPg24R3^bBAxOzY6JAKtwQg6vtGGji@V*m1J41vjr^klTmjR;J_EKAQY zSXK(H4V-(*mTHEK8P5nUA3mk&ui~~*TuD`@Ai)4cuD!BBX!esr%|DveIFKIzcZm$x z1-O(@DxMNP;SMhzxir~u3yi!0$%uQ7Ly-@A)QMDL#s&1wC@pZbay#x6w0Xl7Wjkt$ zKk&e7 z;|Xg$lqB{jIR9o7)V}R2)1<-sLwvkA&xo<+tZ_Cg&OdzRX;ZP6V^GtVnbW{9uhhT? z@SkLlRU!#Kh&V?QaVVN5(2iukf^vJ~=@7+}R3nda@!yk{7_PGhqPK%*l5O{#&a1ry zVbir7)oT)AqXW?hqIxPl9Y!;klFAb(<)+^v2fH?=ht;;dMoKgDD13m1xq`pCJ`(U$ z2D~wNM(xv`fQWG#s^qMia0FKQj`+w6W&~gQ;n8)YH=c7C;O~fPB5-!VcL69?4KG%W zf76w*WwsDqd=^!@6BC_hG{!Mn8Yo3L3gtI$^8Wt&%Az%(wmiMW=CT?1=}n>E5*15F z6Odc&0p(7eN6%3A3B(&|OPoJ`3GbJ6KC{fR!W=Qk{Xa0`Jp*k_?>MKg%@5o#`O%1O z{OTC?R}hj{0}7;t+U^D&=jM5F;-lI&hGy4<_8RAs97?s}7KaTtQn-@kcou9cH9 zS^8m2y_$oFu&jjhkSBhca-qNT4zu1l=2~hz+Y~CalY0Z2VMn76yuS}JqDT9;V0IFcyHN`4%6l!XXPTst%?fe^8<)X+-AGvxU(;JXsqvA<$GL}V3F zhgLty!){4Caz!qF#&){@lPYy$jDPxEy)wGwxY^O#fQy`%OlNBhscRnY-=$0VBL$Y&+`l5wUa8xbtup5(hm$FmyN6 zotfoxw;-Le*#0Q`iv7iYBHk;PD$V~s3KymC8iv$;O5CugC_Q21@MPyre7{bdRBrIe z@m(*Qph4;{8^N=XfY3DXWXMh;Uji8pn`V^lXIyA-mGGXQdKh<9YgkT-0!M7;u9ENX zL6KX_p2drCj{av-vY*K|(>kZiT>ezhhZVnF6~jX>`3Xw1v5h9Mi97-8m|jcucKXB} z&@*8xn`-9k2sp66^i6KfaLHqe@1?x@;Ghp+rEI>aLZvAo%g zr#Fx)yLoyR1huK6qUhRab7cBkBe%6|lc}!bl0*EoS}vRL$>rE}E+q^06`H@U=kxJT z@%7;Ti2W1)&3}LSH^TM|Z-*roaWL)z&~DybSo<#9s2#87_K+I<4gSutJ}(}n6(?3{dX_6q=UxdjMYG@fFL1K_X(W8CDOTO+CO&XX#{Vz!DlBk&Vnk@ z2VbBhrM9(Z084#PiA1FD`GszYEY1{W8m&vtxZYmgP9eN)#AvrI1sgocs=OG+u)W)c zovxg`(FR9XPJz2P;nh-U?AB47E*ZijB$68z_c zNE-hA!BJ2K=UiK4DL?#P(+q)5H5B4-(I9iqgF!Hq&nDy5WQfM_v^hE*8ys!80F!4_ zWEWK#m(5dj`R#zY5`>rv z)AKri<$&o1F&TFQJn!RajoPp$CQy_$^I+sDlw?{I2umOb%O!+}g&2xRyHAX(nA&61 zIJ~WY>P!F=^&=Iv^OlLX?1e-jlCRjLt2!nt4oCtyAB3`SOQ$L1 zoizrxE+`>fD*I21dPID*V;(*C+xMMT6zP5AH}`g@cx#_TiQW9l^H+*G#%a}B|ME2{Y zXCVs}RP<{CFIjpyXInvCeV<<8W(US>MfYh3P2V}pynu?avY%))w*! z15ICUutD08FJu#F^Xc|Cj)Ki{F0>EwEJn{F1~wUcvl4p2)F0*Cc84!E@cN9FvF##t zE{R2u*_nE)iAWL-w$Wkg8Z__9V@I>PY-UC8#X3Vb^TeDrtupGf8}__k{;_v!l7J~` z^hnd``x*oP+;G;&ae@v_bs6!W{UwP(&RKY&_K!-==@#lj?I4G?L4;AA8g)XF6 zi&#?khVLp)+eA0CWK_;4;8dU?j{m7GH+pM&MGHO6-eBFf$i{FO)Wo;W)WbhdVosK8 z9T&aW;jK@dqL5DyfqiuglpFwVJD5_Srz_AtKp2hiaCR!-;Jn#T1It5la*tCm1fN~B zYoaNu;_ihs|0a#Jl&Mf-{(yY~uOQDZNcmY`lJA2%jvG)oX`c7I7=S8o)pl=1G)6xZ( zuesVc+g0)XiiPa+3yOW*W}A)N6hWc6;L2=N8Bp>k6UwmflTIg}C8cB_%vpBbzH07> zv3I8`;M!x}N;HucajD)JcAfZRxZi@INLO1y#B~BY*c4}fx-n)~H&Uz?WMkR@0YV=L zIq&iH^5ULS0XueKyVNSrEUpS3fwNye%@7$%_c4&qUSoyG1d!oAZVb@L@#{V|7CZC# z!|DGlxkg7xaKR1(%9_fv7aZ>_L|z#soD+pQ+|_WI&L6X(c5;AEJ-HDXEt|= z^7{@aBz!(ud=>}E5#HqEQdDD0xBgEP(=D=_YD4q#&S*j`JtOgCY_-wfbttoCk^QAU@U!vPc zef@p{SAG|+fV8qDn@Ft0PnPh86=+|Z3=D~D8gO!#dP-xMHIe7D&dNQL*Vc0n?@t3q zKRbN%`NpaH@(1dXm0mW4`**9i=#WyDzC>{Mr2$(t!M?9IgMQ8ryvUWk>vyUNQY6Rp zR>j`k6JNE^9BB3~EApNn9+4dhp8F)P;S`Gibc7B+`#mpR=LehBmo6kMVI@Le=-zq3 ze=%j#L4w|W9^aULB#H44dYDF@_paK3RFIu-zuG%g7it0N;&Fz-4rclnr;p1eaiOLa zI4(bsowEjOWqiU0KX=x0J@WfGvj?^>K5n2#{nfK&loy_*G$blH7!kf;2d!o@L=YRq z;)!j<@qi7UA-DVxb&|Z$g>CI!nTD7b9_-++sv|%Ejnawmj|h+I#xbRs_p%hhqu4{l znzUIteP@;Axy(NWjuJyrZYulj%-?0C(nJYjY>Eer=vjx+UE5Q2*hizxHb21(q}u5f z--^}a-=YzXQBI||()2)50}j6{^i;qf^;I`U0l& z3`>9Zegxa6Kb3=f4Mq%CX+JzjbN%&;%YNsFghGAi z9_V?d{CN-bRZ~0c6j@4MYielYYF0Py{t3c3p)6)2TKrX?b5=kuttIt_&=Yaeh`f3O z@(_aMDSD30AtprPK2B5&Jk}?vA#H4In1zI*cEsGlX~23EjErgZnPR43rprED5KIrB4$c$cG2|@b*CZh`-TC+*z#ky+Nt0| zZ_1NPb(r*-^jy$^E2Zrxk5kvgVzHvCom6G0jcoDBZT-^q+s~->{iwfIZE-)Oo9J98 z7rx}g4STucr4uTt*Y1ex|9Fd=v$N*td6k%Zgi#ROXINVAw9VJ>9;{?XrbgNxx^uhexG3`7BVzB&9|Cgv7KxlzbAPdy1scIF1 z<_C!8eErW@i}sU3e3wIkNB&tuGIzW|f0kAJ5kq2}x)_xl4wP2R!OTgt@ZbYu(L zoU@d0%|zKbgi9JZ0VhRF4lb$uw4ro`dO_HawA%bH(y1XT*I$>v21sP5q^R|>5|nXU zO;=_ArzSUD6Zm}BuQwt)(0ArmiZCt5po-iO&s$&rlNPDQ_d8(q^DWa$xwy^;tyDPPL` zmwPxVnph*oVZ}NryK77^?C98~*;zQ)?0VAC@HHCHD&zH&j6VVt5oJ#XJ?Of2qK1)r z`xMikvYAp9o(D~9_(1=bMSStm?-#rjbNMSx8X&r-R`oC41NV%;I-6z@)%UvRKH%y^ z3`E#+W3iLkb=#pv<+Zhz?VzFnDd!mBR}eOM(7Hh(QmUuL5e?r_I?VlH45z4P43zCa zYrU?BV-EjwQwU?8;AAgj`JCvv&+It-PBO5fRmd+V2BPv}8rAM*%ObWh;wQrKJ}+Td zYcOJm#K`@1kxjDq>S~0eS+0j`>%P!H=JGJ3>di`fBSH$M@+8mwucmPsBbu12O2V#E z;&JP4(@7WZQM019Y`X#O>@XN$Et*WX@QZKrI?xinaG%%k%;pI^s1G22elyb^F&P`SMPoeg_=#K$3ec-}l2g z-OWI~>HYua`;22`PWQsx#^11l@h~fQo$#yBYZ?=oz!snAo>v+U zb`pPD_-*^%yW_6T+m0MoUp3;DbsG^&r?dB&8-CN#-xGTB1U3Oaq;1LelcCQUf}pv}oCZ z6Zu?~hj+m)K_+HKZPXhj2ZcE@ZDLZ50~JbV9a#?IdLPGwEAm;~5}1-E1W5GgOrG7G zb(~u`d+Tj(p`f|;uDDnA&n8Yb$seZ|`hze5tyPpmfyp1XN)^x#$pW#z@rd70{s%b^ z&5|=0y{#>WPrV$@^YeI7(MH-MRqC{pF4+)H^oj6k%O2T2*NGp+W| zSdx}`01Sy4!NIzjF@`_^IH&d(f#3(w=zWkC) z4}z2&Sk~hk?dJQ6PPY!fFseZIUPvGk)Tpv1-vzwUd*#4tvA)MzUn00A&1~Sr%e|IB zAsIxoyZ&}I{fETH29;0}iBZV6_Qt*7VyCWnH!8t0i1sc|%_}n6bz#nz_k8muqPki@ zl&dL`VKQj5C%6UL6PO#l7(HPmlQ!saIpHzfYBnL)2H7oVltKzNA8&-&QEr6!ewW4+ zA!|mMd}YL$!&XnQ+NmP7X$ZhfEuK|brzs6e(PXzU6#c|d-GxO?uMbWE#J^&Lw&PCZ z!guoYPKMx*b?}5~XTwj79D28Jj0RJUElwG;-a5{>bl16uLA5AN5lm@_hF7C_B0_zU zFtVJ%!<2cpqfH)0+?wSR@LxfhW9&^8fIDBA%;WT*dNYyktxCC0Y20l8# zKCFM$(;?@Zi$Drv9QPYPMye7Xz{k`AeG`_;H-LPN&yQd@eOiKEUz0_` zJAFKPFhgO5?~0*Z=XT@B*;jy5@S!|PqSC{_;EBg?i-J|0q3z~ulVpp|_Yz5l(cNvI z%e6Hz7LNQYdNmmubch$c-N%22?e^050Q=Fwe zri;g_I_L31pe4}&f|5}>g}0}Cas4DH(VF@@q=G6kwF>!z*BUO1#4f}ttFDAKnzon0W7%x1H zojEPPcay^b80c;yu<#_t5eva}ssoU_q9+C<`_VQ}f)QVyEUoUSCeU!7FRu-y3fO=9#qED{>P(qb(SxZ?9H(t zW1GSXp$jhi43+wl7wje#R5Y-aDS7k=B8ri>`Ri{DNT~G*m@0Wrfcr&_VNL3RsZ!2x zvwjoh<64hqRpIT=Pr_IHp^SJh+{^m5uO@6DA7Ze}lVN*oPlY>W+4F4R;3gD}2)$B# z+**Ac(P904bNcVM9IOlnH5MktQ1v~mH_B#mew$-k0UXnltTD2~x-=Z?(wM%E9dTCf z{rTwQ`Z^J=(9Qf2MLFL?x`>egG7b0#aZ-9 zcu6AQ2EtGN#LO?1oSxzE8T+iZ&dqf?D&iO+x6mC^__SOUmn|gyOvL{gN-?|Vr+?s` z{J|tolW1kP4Y##mV?2SgF-ioyR^g4=2_q;_Zsj6Vp#iKg3iPaV*)$G#AKy+@aNmBw zF9Md{=Unz&cT9Oqk4p?#tkeqA6pvR|KZe=a=btOnrpBvTC`-96OR=JB)YT9p@%}mK zKe3GZCF57ODV@E6cAr=#w}2y>yTTTgGpn7I^lX=?UKbPoE0PwHAh*bV?E!= zwF>io2Wk$;AnnKkFup=e2B!g$16$^q&qSzUO%qph0bzL-{F2my$OQdR331cRvj(}T zuCzLr$1@d%^bET6&A_&B}gy&Y<0|S7%djMYkGe9hg)2{qfUY*j8R}+jnnlU7z5{P ziMtQ*FmftK_V*hz3rWwz=njksV;hG~yzD*wY>mGww?IGh#=a@?^dwozSgImrd^LpY z$c0-rUWjs6Be@ym2_UGr#`K+=tQ@-Hr3oFPS(3BMEI#DUh=KIleOC3cw5#Jkcv!_%|N>+5bZ*=vQ&Ix;OK@o9|0unH#Q21z{Z*n6KZ z1l6z@rhPEy^5CN|kle&^j&RdrtpLd_c(6F;Lg96ba(rLEONtLu-FF2GZBPxmM@7^9 zm4jGYsdw^<8?CLkd$c)Rpy}J5s}RMxHFe-|BE8VZP}nFRyEOK@hLB6|gvIXxu|QM9 zlMvDT$l>kZ@c7@WkhE)>cVvK2=SDUl3b(_^UvXFFXr>JrVnX7Kje*)NRay`VVdSd+#fFWdTLB#~7Nx zI6KEuyf2h!K6#eWs{!i?<13F5CsnC5Kra2+0LGcYfZQ3g>PNI-9YOqp)QWpx!LSoc zko_yxFA-b>!s`^hm)?~NH&?uIc2AF(Hl=}?zxO3l;#t{l1A{M;59$ttb?|u$3u)6G zzI@X9XvNy+^o^PnfASDvHsn}9oFc@Dn0HR)z`dN0FzP1y0?tnlfC$;!48ZG1@fcXx zU2pWZMNJ#Nd}GMzVybNR_>&N#CLrw_!q&yM*tqw?&TuqMEGaz;>#1!B7mK`ZH_Fbz^@fYfA!U!_n_nd}9`Wmz!wb?p=?T{$@&l!^J^?^_O*&CE%|Uee+|TCc49_0OR-e zc>RcZE3Y}Qj_rW)NwlZXrIEokmHLs_*ETF|YSkB+!FjZ9o%3E<2JN*e5Q>K*zj89= zGkpo#7+Tk{v_YY+z5~;EtLLiV6giVBXjQeoe8{l!uO;-hH#!B=Y6r~mezR-aW2 z^>V2gp+dJvU<0DVj%UP?K_)AB|K9qYRYizZgXUbtm0>}L%G+PIQfo=SEvKi~QUqDO zFa~R?#?g&_1e_I2r~c5r{gkqY_JB4M<*-kt*kh||39aMjNaAcHn6WKhZ`F#%&cSQ` zbxp4Sd9r*eF{^A(pLFnkh5W{F=qre+_t$H2vy%63 z*1TL(F6yJvZ5G@@u_7)w*(B)#<6K`#_6$;@?gBG>vx;8;mI9*%3|b*wN6{Coznirs zXn#BjWU=oS(XxN2vKb05+xK6j%-rwC1QCs*mKZ* z%^)}E`Kp0I{Fk|_*ij|GCTcfRI_Ufc*x)w38?m=HMr^(B`n@rzV=5`*?&%~oqh~0H z@^26Vyt*pPUfMjYEJPl$|!>ivRxSdB1ELzka?eq-Q5lh!GLlA1fW?#Mh}n(2Y*BiRJcc{|5z1;SK1 zAS5dEl7j5{Tomo7rOtEZsc=4W`ah&p*Xh|EXUekO$Akj{R%Me97)^aAb-EbhR@WRg z+u?gC@?9OCdcYl1z%(m1XLyc2f2$4!W4~hFi1~mk9dVQC@lCm}gPg%CWzNB}AeM|3 z*Ykch4(idxS!+6eBoDzt7gUXxZu6~`VcS;w>J9So_Q4wc@Kh0aar)2MCx4q65NZs| zc@jpe?_K2mfkzux@gvC_6L&NDEaoR4To$uC1g$C_z8ONrd5pVyKx4()wAe}dv~U}& ziWSCbmDTIFTB(t}o~_-@?&E16NPhk1+@eSm{Tu9jX9jz6cqO0T`~WZ5>h13xliR_c zIy#xY^hY#R#hASE_UzPiw?uAf;&Jk;`XQF zm5f#wsX)!i7=V)8Cu5GBNT6?6`d&vS^c(Fb*S&1!>kPU@A3>iu6Oudxz8;)wBS(c2 zsEDc;^ZY>w0*BEi!V-rVVx$z&*8ll&8ZE9cQ}k>fp$ z6A5~QR!n@rJz1{*%ISpSze5FI;{L;34+)hIC%g7z{mB8|1I6ANWm2J@{`GasVu;^8D6{<&zu@?LIf1PQLI$hc0zid&H0rj#}g|$s&JR!?wlS)s7uyN9zji=jVj^#=%RbeJ z`V49vYd#Fxy(%-NCj;2uc!p4ig-7IV1Q5%FXwsYUL6n}eCA5}(xhIwNBr9yPaPq{+ z%JKo<^s~k$*lvA*v#(s*$f}XPJNppk1Y9Ig2j6>iObU6pEHUccOj3b<#tO$=$+I2Y z3OHeZIgFH99Um)D0>S7Y6I34N-YMBzQlWI^&hGLO{p5fZ{7*PcN?{h#7g4!+w| z(W)63hm?-$fy{lKDF!``K|QHWr;I1t3^;oGRQoYI7WQN&;d$ZqQlGa}zG)tHL$5+N zKfG<_5V&24dw;niLvX(n(1qT;q=mxvHczL*$}F5eGLFD_%X&iNha?soUVhs%_xZEt zb+(uM`lK_Kqoa`Lm#g6uw+x#I$5??Xr_nUG@6t*c-!HnlG7sb=d`-U?Z_71Fbx)y9 zzZ>@}Yif$(bYRB^Ga1)v=>-k3^G0TKM5}D3FPqH=`qToSa}EIAc6olOEIi^tB0gWL zV!K?wUL6WTmdUMa+)mwe67SdW605f>4BPjHA4p;*_Q1uT!86!XvvJz`Qw-yC5wd{) z(wo7`o0~jl@3G2!{Io0IT~J&AI9j z!b8-FA8b3;`JXzQa^TX?2eg9c5Yx>Yn1=obG69`*-v2-n>KAWRa51hu^kFER^64*F zTg7d9mS$(FvRxrme4|jfyoR4_esF}A24TjQIpQ)q6_9KgKuN-lb4esr#fu?Lp*(=Z zua(D!vI-YXRVfzV3(qA>`HuXerJKWd_1pJb$>`B^v1ZAiEKts zh|`ESrSu|0+d&Onz5oG|JRx@D{xt4Z{1 zNA*(Afjrd(qYDZGG!~QY(oS8=*%wJA4ITdTr|2mYj^PMnr^(45x<~FO5|80@U8*8( z6XQhxA^ijXFwQ_c3BE+GibG)abp|=EQFTJ6(Mg@P;>mIJs;Xwg6rX;p`NpaMsb!_H z@C&@lolv^{EtR64<`cO-YhKvConsRU=)c0S;{wOX{YcwH!V1NlrOfecn>MiCwFg~s z)(Wo5IURP@IhSylZ0)vDQ_Z>508*s_A8^%)0{CGSxO%3fHAR_Sga@=%lc|)aw>@E( z_P@1s6!tFYgi9|LbT>IC*B)a+_bCo%%ENw8vtTC96!bT{gBevrw7@kVX6r`ZOwKcx z`ruR+=-$zqK)xvNP1V;6vmtZCTk#TNxBqF{RH-qjnu8Q4p@>Ug9UkC#f_-ogkNo%_ zBLk<2R!}jELo?}PBsAI1F0Mo4m*aiW14vClut%Xrh-2EyK$Jt5wVW`k%_^C9Ymvp=8_0m`?b7=( zhM0k@4Q||arqu>Kxw^HYa{Xzoesn2^HNCFv#{1V|jSJOI)?61Vu~rQ;iFIa3ucV)e z;FVLliLCAXlyOK>_W7g^kAAI{ts6PjIK|4SrUi zUXh2OzcQw`?ywWnE~52J+Lphy)HU_Geu>L?^$8%pDXVZQ3N~G;!jP(5^o&8sCvg$AL_6<2%FBSADj#iGH=EJD1S4nC?CBr7SED=3POD~WwSE1=$E}k#o zNyR~I*@qo5b)P8yO58Ugn^eRiz@1mlv-g3)&)1hzFC$+^yk-3B`(dfp2X`#tyFTqN z&B{Kkq$gNJ2!)@4pQwJ8IR$#b4*2(?(WyHfz0o$2CM8}uT;1Epk1x#%MY_qv#1dz#r^P7*b z^juXICUlBlDng|um+pWHO^<%&e{_XKJ9krR?53>Ts?4}m(yPwutcA-({K%S zeb2+A4eWt-b?2a{RySsr+@M{DdGWsr>wjXdSG7I}V8cjH8IhuptnOIpi6P?c+X^yn z_3i`S>KBo(wrQ7kiP-?6JyyTy+0`8I0)v(F={xSN94LP&NoF#{eaTuZ%CXf~#cPdb z?>FVrK)fQeF>tEzV|exJ(lU!b?5oyOPADb>G6t6v#V4N(s=D0bmnl5{?@Z3lEwDiPN(+VOOQ&;8JmiRjQ_Tf zhT8C7q8q7)t(c5Y>_6CzFPR_+DdQAE6}Zfm)l9bSQl{US?Bm0gh4a&;`GVrzvmF)z zl+lZ3pS>~`Fs>te*MgXsDx8!^y+$8MK+dvL4TYyYam*CNRM zJ}Mh}-H49IE51qG$CUr+DyjRG{E3^xe$Rjj=0UD7qbiJ^5)7}=r47VCTRz9|^gt-Eo0W;XjS z!f)=^sW-ZN0dMfbZJT3_JecQr-$d42N;F~Uz;%NG{CO(VDM!H@nG*vgK`;e7! z(7xRD81@J&Eiqe?A4;-aJsdmj(s9Q4FB}Z@H=gtkVkSo)F5d3wpe0s41V={#P>eqZ zFF1?Pfx+(qTJB0^{rra74I|TWvp&dVxhN&!(kZT=J*l?`k33$F*I#)o2>2%RQpzjw zZApZy7N-}II zBiu=)KmXnoZ&++4;UaRfH&fe2L1GZ-`x9IH#W~b>np}6#>FP`vj;GH%tWS!$yazBSp_62Ec-WlL9plxyMk&leqxKj zpE}DAbIzSN@tN=oyEveaOhu@xuSh2@&Ed~DE1+RD{TmM@1IZ^P0~;A?L77daaxU)^ zi_~6fx?}-dHnoXW&|*cMa1-xH+{s4Q^)0#Lw-HW4;&WgGTcnH|OIDLovr6nePAJKD zDY*Agfm{vx`;=f&qudzL8NBl#rFi0bm0xRQjlxn}j6YHw*>4V^ezV?OjZ*M}-;Y-FC zVTpi=OT5`2U>nKDL8g4b^k>tu9b%5`vwTM&s$c+$TM(nNl*a^vvmKx#ND?YxNrUI# zU7I`Nb}loU`3_y)T;_PFiZJ4X&?|$@k(ueSzR{_S-AgThS@oYDG-B1)%_;Z7uj%#Y`> zW&d4v;!pA9DF4g&&&^K!joefZ?h#9mzSIGD5ExVstB&Q>8J)Qc2>qArmA^oG`0NDv z$Ot}U*C02ICQUG7H}fmJQZZFv^L%>Ngm*Ld$@BB)wy%5=3#nlEawbPTT~ zZ;Y8XRC4(x?!6*0Z#;AJzlzk;t^E79s+uVcw^e6WPgu#;2rZBWaxWvE z#PJI!$#G)SFzs3esBWwL{C4$Ox}w3Bds5s+<}1MF1C2_ng1^!(w~nv1J*YjJB)hTA^~3x>!MVxD&Ih&b$7?UPEaeoB z*D7*Tqvjyo%&%z1JjIEjFTWF`qRT!D;jpq%9eX4_Z;~CtKRa^gkePxicIqsBHP6td z7(H6Nv>-A=7xFkNRgsJSQN8Tc0l{_PGaQ}2cZ)ux*X&;l8+h z)*ZIl$9i3`e@knQTju_&*|Of@w8V~y9z8~gS(< zFUv|gHp4T;$U^n0!94(5`7p?4FRvgkF)hXs0|XrdV> zyMu(veqY#onEqi2Xk46lG5MPeh|Yl;mK60Zw;9Vmd&h`G`XIaum4_0AdcHRQF~!R+W&aEA$&zM_!O_3W z*W}knDk~ogdV!x61$A}byxBBcT_Wo=`$h4fYc?(RoIKD>&d}#0FL9nUbq9FPafcB_ zTjU*8Z1>!6!KxEz7-A@+x=$P_)`Q$l?_@hWwXwwAB*aR~>;E-Csmgy)UnFCRTVrsXY5x0VlfmTJ;yKoWg6+LU_6j(6GqQ8K zzx>Z#53Gx&%1!W*>bwCXnP&=Jsh@m=@A^;RTE6YRe1SXeeR>T$4=zeETeR(*5_l&J zILZS2R7^gFf(mDTQj8xVz7vCgS+4n7`$|tO*0?P-`lBp^SWFquG4$9Y{!yiFjhxKM zl$|z8_Zbbh43N6z=0Dw|<@B)&nfKiOKS);G*n7Xvmjxv(%syC1*z5e!-^4;Uxy^nc z%t}X%FvY0Br^BFy7R_^!^;H|ylxDL=9MF`P=M{wMhN-nc~PU!XN-k5-cx`fCN% zQOoSyc3oFts7$L`ck*2koW^FqBQ*uxXpK(Xm7C5;Itb?jE;AagUnHb%8W$yBiPHE=e?Ve^juYrAN3nIwh)O_)WOjgb1< z&k=K|-|h;)mR;Li@Xs0C#+V1M{L zyE?tUlHmj8xfP!JUejvx@Z0y>FV9(Z)|NL9gSX#<$7ttn2}PXaLQMM|-2`i8S>foN z66x=Cd86;-7g{mEWF6JYFZ;0!Te0{|gx|n*6&VXFmeDH~w4zI;=yFkZG`-Co9gEX$ zKFz9XSdzY#`M4F>^x)1t3;Xlk2N!RBk(62FLc>|yoap4RzN6Bi7)!wN=9=ex3$o77GsglJpUv@P4w}XyrKb zch%4pCCuO5sYOcXOp+QaOXWp|CAe)J0WRikER_U>7 zj>^^_hZG*CdC1=Rl7pT$;Nq6umYPlaBAU4K2I3g4{KRJwsRC-t3O<@^xS<% z?mgq^UTfoEHcOc@<_TXlT}02)PaRVA{kGULBCrCQNc@w~uft`TY4{b6LP8n|-s?`u2{dUDx zy>y1q9~U|LHklOAaI+r(pSQIyHo_ILmvnNKqIxL^az!bg)lv;Je5~&4*Qc&HV*b{n zu4RtwWg7c*!5~kEwpcy#QpH2-!+qRID-yoI~J_Uix<&mNV3z#M6N&hfPE4oev)7^&UzM z9WJ>+E`RB+4kZ|@V6IhN)W?>|4d@R$swQO*6(d)Flyznx-a#vz=kC&MM*YfZf#(2G zNdl~rCoy$BmCMsiddvmHKrKZ*+t9Tq^pd(K*+ATTClcGSIoak**9lK~r=wH1X?!Qk z57#Iqeu!Mx2YtQ47~10<5Q3oAY--F23)hw#?vgvVK0vHkIP>T_D1H_BJ}d7&S(_YW zo*^8AV>?)IEq#$N98u||5UxQ{cF{Mj`I(cA0c=fx__}m!6;G>G@zSZXm`wR|>!Ia(!078{^q2 z%qAb}*?#&D8z9vhR4u}1w+MlMi{_Fl*l()Cc29|;b>5zH*%4MZ3$~29XL!lzMqCN>nXf3y$Z}42_K5R?NaX za^h8CM~=GTIII#G7WOFWona`pwZ(?=49=*@Lk8@`3JH)*j{{fCkO)!4l)gc$Ey5#m zFzDSv1h_HIOzAdZAUt;+!;*66q6q>kj>S^bS*YDSLz@9fwdCRO+dq=WiUo zHfr+hw-KRnlwR7$Nt}&=2|HE!KgjFC55Q1-z#qHrBrFyVb(~;JMxN?lzr0GX@>{{cVg? zvQka#H#bOh&qc+{yZywF6axI8#>ZX0$*fkG^i~2s-+Ujw#~`c(AJ%n04WY`@>PpHV zB5+)j^s>VLjHU5}LHB3l<$X^oU7I65l#n~bPpbKcIJrqPX)n=PmbVOaa3e3#zwCrI z(I$;qli@A#Sg4ZW+l22?jd&e>|czLCh4fcDD$kM8Mpi5{4YwcSg}(Y^tfSY8k6R}Hdz!2Vt)gzf;HjU`0ob$tnw*`M!}lu8(U>k1yaLzb8o8;l=yxC@EDK3C zq&5{viBqAy?XX8-QxD^d;UG`!s~sI#<9yq-{m1iGWp|w01DznKPY2MjEaMH+iVaxZ zj(jwztZr|+MP55r`|cJI`uMg*LFdbxT>YG+@_7>>RnK#W*JlyZtIu-%GApwy)Ocgy z$jIRNKUmE64fdqKZqJR$=184dczN6U<(+g9gnZU&GX;4rj=*eE=vz(rzI>kStCs7I z*&x`hLf)M2hC@&;8zi$^-32diyXPqurmO8lEh?{rR`tk8+*&*RgK1&AUxJ2MV89Y| z<8O6Ke>zMfRH>K&d*AxzhnHe>xQ>>Vqc9YK5}5Tg$Fl6yZUq%f^r}0>l->K0-qR8s ziAPJ%y4h7C+`>`2VU;RjIQ|ZN?wYLakSQrhn?|hfuc_PD3je(qUv9VDE;4PeZnut`qVOsH%fSAtn$4uTVtC7 z?;NDKvfo>Z&I;xG`nAbg8}$`F|61EU!MQQ#-d3w{?b#a6Gw|;o8Uxd>V=rwxccD3M zIJ>~3XB~GV5-&tVQRkO2+cEnXfCSvJ)XVLSRyKAYS%brOGn#K0u{uXXkCoMvJ3HMQPb?g8eAon3jA2 zr8A3I>qq3%yTjeotftjR-q1T{VXNLWTH~-K9HCD~A{+ooo@p25S(4kNM5iUc)pLu~ zN!iwM3iZIF(MK!w<6j@Hmq6pVlX8uUFJ(CX+a z^Js`ce7ey`y^|2vMJpkz@kfLPIdT_zg=VN@T0c}%yn8F~NC*p&JU{#m-l_i5AuJ2K zXk^{g1O}oWx|3t?%@F5}5e1)V{Y!6YWG6Woqw`*dso79mV@(Wgno~?`DIZC9pRXwL zHV<*iAJ%8x@n$@76Rf!+;79q#kwM+V{!w9w-d^t<<(ja+kQ%vu;IYVKG3o(@uqQE< z{@j*+Kjt&d)H&*BwZ9nh)q>t@mn(v39SKz6u2l2cMEspv64TnLX$Sive79M+ z3pq<~&Acup1=lK5zZ%4>H*UYkrCREqd{R;dD#Mos-q))v;{u;1d(nKN6?bvlUcNfd zeY|_xboAJ*@$KAqeEjin;qLcPh>&Hpb;<7T=IIS#3K}!wKnGm{x14Wd=XEnCKa$(O j(hw@8?H$0vJg-%>K{d)Y;s1yK|HJ?P;s5_1`2YU^tT}im literal 0 HcmV?d00001 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..e65a5708f48d0b1249f4b0090e441cec52f1999d GIT binary patch literal 74 zcmezWFO4CGA(0^uh!YtK7%~}(8Mqh_Vx 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)