From ba53228d446d278bb82bd6cccadb2eb66cbbbc41 Mon Sep 17 00:00:00 2001 From: Romain J Date: Sun, 16 May 2021 23:21:27 +0200 Subject: [PATCH] update(core): change to >=3.8 --- .gitignore | 3 + .idea/tuxbot_bot.iml | 3 + Makefile | 2 +- README.rst | 4 +- setup.cfg | 2 +- setup.py | 2 +- tuxbot/cogs/Custom/custom.py | 3 +- tuxbot/cogs/Logs/functions/utils.py | 2 +- tuxbot/cogs/Logs/logs.py | 8 +- tuxbot/cogs/Mod/functions/converters.py | 11 ++ tuxbot/cogs/Mod/functions/exceptions.py | 4 + tuxbot/cogs/Mod/functions/utils.py | 24 ++++- tuxbot/cogs/Mod/mod.py | 132 +++++++++++++++++++++++- tuxbot/cogs/Mod/models/__init__.py | 1 + tuxbot/cogs/Mod/models/mutes.py | 46 +++++++++ tuxbot/cogs/Network/functions/utils.py | 22 ++-- tuxbot/cogs/Network/images/load_fail.py | 2 +- tuxbot/cogs/Network/network.py | 4 +- tuxbot/cogs/Polls/polls.py | 8 +- tuxbot/cogs/Tags/functions/utils.py | 6 +- tuxbot/core/bot.py | 8 +- tuxbot/core/i18n.py | 8 +- tuxbot/core/utils/functions/utils.py | 4 +- tuxbot/setup.py | 18 ++-- 24 files changed, 274 insertions(+), 53 deletions(-) create mode 100644 tuxbot/cogs/Mod/models/mutes.py diff --git a/.gitignore b/.gitignore index b95a218..1d61b3e 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,9 @@ __pycache__/ __pypackages__/ venv +venv3.8 +venv3.9 +venv3.11 dist build *.egg diff --git a/.idea/tuxbot_bot.iml b/.idea/tuxbot_bot.iml index b9c571d..b07b69f 100644 --- a/.idea/tuxbot_bot.iml +++ b/.idea/tuxbot_bot.iml @@ -7,6 +7,9 @@ + + + diff --git a/Makefile b/Makefile index e2d5009..a78a5f6 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,7 @@ update-all: .PHONY: dev dev: style update - tuxbot + $(VIRTUAL_ENV)/bin/tuxbot # Docker .PHONY: docker diff --git a/README.rst b/README.rst index c7c3b71..6a3a98f 100644 --- a/README.rst +++ b/README.rst @@ -14,7 +14,7 @@ Installing the pre-requirements - The pre-requirements are: - - Python 3.10 or greater + - Python 3.8 or greater - Pip - Git @@ -134,7 +134,7 @@ To update the whole bot after a :bash:`git pull`, just execute $ make update -.. |image0| image:: https://img.shields.io/badge/python-3.10-%23007ec6 +.. |image0| image:: https://img.shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10-%23007ec6 .. |image1| image:: https://img.shields.io/github/issues/Rom1-J/tuxbot-bot .. |image2| image:: https://img.shields.io/badge/code%20style-black-000000.svg .. |image3| image:: https://wakatime.com/badge/github/Rom1-J/tuxbot-bot.svg diff --git a/setup.cfg b/setup.cfg index 45993e9..8abc3ff 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,7 +13,7 @@ platforms = linux [options] packages = find_namespace: -python_requires = >=3.10 +python_requires = >=3.9 install_requires = aiocache>=0.11.1 asyncpg>=0.21.0 diff --git a/setup.py b/setup.py index 69e447b..4b57aa4 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,5 @@ from setuptools import setup setup( - python_requires=">=3.10", + python_requires=">=3.8", ) diff --git a/tuxbot/cogs/Custom/custom.py b/tuxbot/cogs/Custom/custom.py index d316cf8..385d6ab 100644 --- a/tuxbot/cogs/Custom/custom.py +++ b/tuxbot/cogs/Custom/custom.py @@ -1,4 +1,5 @@ import logging +from typing import List import discord from discord.ext import commands @@ -76,7 +77,7 @@ class Custom(commands.Cog): @_custom.command(name="alias", aliases=["aliases"]) async def _custom_alias(self, ctx: ContextPlus, *, alias: AliasConvertor): - args: list[str] = str(alias).split(" | ") + args: List[str] = str(alias).split(" | ") command = args[0] custom = args[1] diff --git a/tuxbot/cogs/Logs/functions/utils.py b/tuxbot/cogs/Logs/functions/utils.py index 8627e82..da36727 100644 --- a/tuxbot/cogs/Logs/functions/utils.py +++ b/tuxbot/cogs/Logs/functions/utils.py @@ -2,7 +2,7 @@ from collections import Counter from typing import Dict -def sort_by(_events: Counter) -> dict[str, dict]: +def sort_by(_events: Counter) -> Dict[str, dict]: majors = ( "guild", "channel", diff --git a/tuxbot/cogs/Logs/logs.py b/tuxbot/cogs/Logs/logs.py index 6dd0a8f..2dbf27f 100644 --- a/tuxbot/cogs/Logs/logs.py +++ b/tuxbot/cogs/Logs/logs.py @@ -6,7 +6,7 @@ import textwrap import traceback from collections import defaultdict from logging import LogRecord -from typing import Any, Dict +from typing import Any, Dict, List, DefaultDict import discord import humanize @@ -53,7 +53,7 @@ class Logs(commands.Cog): self.bot = bot self.process = psutil.Process() self._batch_lock = asyncio.Lock() - self._data_batch: list[Dict[str, Any]] = [] + self._data_batch: List[Dict[str, Any]] = [] self._gateway_queue: asyncio.Queue = asyncio.Queue() self.gateway_worker.start() # pylint: disable=no-member @@ -62,8 +62,8 @@ class Logs(commands.Cog): LogsConfig, ).config - self._resumes: list[datetime.datetime] = [] - self._identifies: defaultdict[Any, list] = defaultdict(list) + self._resumes: List[datetime.datetime] = [] + self._identifies: DefaultDict[Any, list] = defaultdict(list) self.old_on_error = bot.on_error bot.on_error = self.on_error diff --git a/tuxbot/cogs/Mod/functions/converters.py b/tuxbot/cogs/Mod/functions/converters.py index f8c4c53..bbca189 100644 --- a/tuxbot/cogs/Mod/functions/converters.py +++ b/tuxbot/cogs/Mod/functions/converters.py @@ -6,6 +6,7 @@ from tuxbot.cogs.Mod.functions.exceptions import ( UnknownRuleException, NonMessageException, NonBotMessageException, + ReasonTooLongException, ) from tuxbot.cogs.Mod.models import Rule @@ -52,3 +53,13 @@ class BotMessageConverter(commands.Converter): raise NonMessageException( _("Please provide a message in this guild") ) + + +class ReasonConverter(commands.Converter): + async def convert(self, ctx: Context, argument: str): # skipcq: PYL-W0613 + if len(argument) > 300: + raise ReasonTooLongException( + _("Reason length must be 300 characters or lower.") + ) + + return argument diff --git a/tuxbot/cogs/Mod/functions/exceptions.py b/tuxbot/cogs/Mod/functions/exceptions.py index 2b87bcc..61c558b 100644 --- a/tuxbot/cogs/Mod/functions/exceptions.py +++ b/tuxbot/cogs/Mod/functions/exceptions.py @@ -19,3 +19,7 @@ class NonMessageException(ModException): class NonBotMessageException(ModException): pass + + +class ReasonTooLongException(ModException): + pass diff --git a/tuxbot/cogs/Mod/functions/utils.py b/tuxbot/cogs/Mod/functions/utils.py index ab8fee3..388b624 100644 --- a/tuxbot/cogs/Mod/functions/utils.py +++ b/tuxbot/cogs/Mod/functions/utils.py @@ -1,3 +1,6 @@ +from typing import Optional, List + +from tuxbot.cogs.Mod.models import MuteRole from tuxbot.cogs.Mod.models.rules import Rule from tuxbot.core.config import set_for_key from tuxbot.core.config import Config @@ -10,15 +13,15 @@ async def save_lang(bot: Tux, ctx: ContextPlus, lang: str) -> None: set_for_key(bot.config.Servers, ctx.guild.id, Config.Server, locale=lang) -async def get_server_rules(guild_id: int) -> list[Rule]: +async def get_server_rules(guild_id: int) -> List[Rule]: return await Rule.filter(server_id=guild_id).all().order_by("rule_id") -def get_most_recent_server_rules(rules: list[Rule]) -> Rule: +def get_most_recent_server_rules(rules: List[Rule]) -> Rule: return sorted(rules, key=lambda r: r.updated_at, reverse=True)[0] -def paginate_server_rules(rules: list[Rule]) -> list[str]: +def paginate_server_rules(rules: List[Rule]) -> List[str]: body = [""] for rule in rules: @@ -32,3 +35,18 @@ def paginate_server_rules(rules: list[Rule]) -> list[str]: def format_rule(rule: Rule) -> str: return f"**{rule.rule_id}** - {rule.content}" + + +async def get_mute_role(guild_id: int) -> Optional[MuteRole]: + return await MuteRole.get_or_none(server_id=guild_id) + + +async def create_mute_role(guild_id: int, role_id: int) -> MuteRole: + role_row = await MuteRole() + + role_row.server_id = guild_id # type: ignore + role_row.role_id = role_id # type: ignore + + await role_row.save() + + return role_row diff --git a/tuxbot/cogs/Mod/mod.py b/tuxbot/cogs/Mod/mod.py index 2ae0202..ea8b31d 100644 --- a/tuxbot/cogs/Mod/mod.py +++ b/tuxbot/cogs/Mod/mod.py @@ -8,12 +8,14 @@ from tuxbot.cogs.Mod.functions.converters import ( RuleConverter, RuleIDConverter, BotMessageConverter, + ReasonConverter, ) from tuxbot.cogs.Mod.functions.exceptions import ( RuleTooLongException, UnknownRuleException, NonMessageException, NonBotMessageException, + ReasonTooLongException, ) from tuxbot.cogs.Mod.functions.utils import ( save_lang, @@ -21,6 +23,8 @@ from tuxbot.cogs.Mod.functions.utils import ( format_rule, get_most_recent_server_rules, paginate_server_rules, + get_mute_role, + create_mute_role, ) from tuxbot.cogs.Mod.models.rules import Rule from tuxbot.core.utils import checks @@ -35,6 +39,7 @@ from tuxbot.core.i18n import ( from tuxbot.core.utils.functions.extra import ( group_extra, ContextPlus, + command_extra, ) log = logging.getLogger("tuxbot.cogs.Mod") @@ -53,9 +58,10 @@ class Mod(commands.Cog): UnknownRuleException, NonMessageException, NonBotMessageException, + ReasonTooLongException, ), ): - await ctx.send(_(str(error), ctx, self.bot.config)) + return await ctx.send(_(str(error), ctx, self.bot.config)) # ========================================================================= # ========================================================================= @@ -163,7 +169,9 @@ class Mod(commands.Cog): rule_row.server_id = ctx.guild.id rule_row.author_id = ctx.message.author.id - rule_row.rule_id = len(await get_server_rules(ctx.guild.id)) + 1 # type: ignore + rule_row.rule_id = ( + len(await get_server_rules(ctx.guild.id)) + 1 # type: ignore + ) rule_row.content = str(rule) # type: ignore await rule_row.save() @@ -243,10 +251,13 @@ class Mod(commands.Cog): pages = paginate_server_rules(rules) + # noinspection PyTypeChecker + to_edit: discord.Message = message + if len(pages) == 1: embed.description = pages[0] - await message.edit(content="", embed=embed) + await to_edit.edit(content="", embed=embed) else: for i, page in enumerate(pages): embed.title = _( @@ -254,4 +265,117 @@ class Mod(commands.Cog): ).format(ctx.guild.name, str(i + 1), str(len(pages))) embed.description = page - await message.edit(content="", embed=embed) + await to_edit.edit(content="", embed=embed) + + # ========================================================================= + + @group_extra( + name="mute", + deletable=True, + invoke_without_command=True, + ) + @commands.guild_only() + @checks.is_admin() + async def _mute( + self, + ctx: ContextPlus, + members: commands.Greedy[discord.Member], + *, + reason: ReasonConverter, + ): + if not members: + return await ctx.send(_("Missing members", ctx, self.bot.config)) + + role_row = await get_mute_role(ctx.guild.id) + + if role_row is None: + return await ctx.send( + _( + "No mute role has been specified for this guild", + ctx, + self.bot.config, + ) + ) + + for member in members: + await member.add_roles( + discord.Object(id=int(role_row.role_id)), reason=reason + ) + + await ctx.send("\N{THUMBS UP SIGN}") + + @_mute.command(name="show", aliases=["role"]) + async def _mute_show( + self, + ctx: ContextPlus, + ): + role_row = await get_mute_role(ctx.guild.id) + + if ( + role_row is None + or (role := ctx.guild.get_role(int(role_row.role_id))) is None + ): + return await ctx.send( + _( + "No mute role has been specified for this guild", + ctx, + self.bot.config, + ) + ) + + muted_members = [m for m in ctx.guild.members if role in m.roles] + + e = discord.Embed( + title=f"Role: {role.name} (ID: {role.id})", color=role.color + ) + e.add_field(name="Total mute:", value=len(muted_members)) + + await ctx.send(embed=e) + + @_mute.command(name="set", aliases=["define"]) + async def _mute_set(self, ctx: ContextPlus, role: discord.Role): + role_row = await get_mute_role(ctx.guild.id) + + if role_row is None: + await create_mute_role(ctx.guild.id, role.id) + else: + role_row.role_id = role.id # type: ignore + await role_row.save() + + await ctx.send( + _("Mute role successfully defined", ctx, self.bot.config) + ) + + @command_extra( + name="unmute", + deletable=True, + ) + @commands.guild_only() + @checks.is_admin() + async def _unmute( + self, + ctx: ContextPlus, + members: commands.Greedy[discord.Member], + *, + reason: ReasonConverter, + ): + if not members: + return await ctx.send(_("Missing members", ctx, self.bot.config)) + + role_row = await get_mute_role(ctx.guild.id) + + if role_row is None: + return await ctx.send( + _( + "No mute role has been specified for this guild", + ctx, + self.bot.config, + ) + ) + + for member in members: + await member.remove_roles( + discord.Object(id=int(role_row.role_id)), reason=reason + ) + + await ctx.send("\N{THUMBS UP SIGN}") diff --git a/tuxbot/cogs/Mod/models/__init__.py b/tuxbot/cogs/Mod/models/__init__.py index c5f0bb2..2b576e4 100644 --- a/tuxbot/cogs/Mod/models/__init__.py +++ b/tuxbot/cogs/Mod/models/__init__.py @@ -1,2 +1,3 @@ from .rules import * from .warns import * +from .mutes import * diff --git a/tuxbot/cogs/Mod/models/mutes.py b/tuxbot/cogs/Mod/models/mutes.py new file mode 100644 index 0000000..3d7126f --- /dev/null +++ b/tuxbot/cogs/Mod/models/mutes.py @@ -0,0 +1,46 @@ +import tortoise +from tortoise import fields + + +class MuteRole(tortoise.Model): + id = fields.BigIntField(pk=True) + server_id = fields.BigIntField() + role_id = fields.BigIntField() + + class Meta: + table = "mute_role" + + def __str__(self): + return ( + f"" + ) + + __repr__ = __str__ + + +class Mute(tortoise.Model): + id = fields.BigIntField(pk=True) + server_id = fields.BigIntField() + author_id = fields.BigIntField() + reason = fields.TextField(max_length=300) + member_id = fields.BigIntField() + created_at = fields.DatetimeField(auto_now_add=True) + expire_at = fields.DatetimeField(null=True) + + class Meta: + table = "mutes" + + def __str__(self): + return ( + f"" + ) + + __repr__ = __str__ diff --git a/tuxbot/cogs/Network/functions/utils.py b/tuxbot/cogs/Network/functions/utils.py index fe146de..dbc80f2 100644 --- a/tuxbot/cogs/Network/functions/utils.py +++ b/tuxbot/cogs/Network/functions/utils.py @@ -1,6 +1,6 @@ import io import socket -from typing import NoReturn, Optional +from typing import NoReturn, Optional, Union import asyncio import re @@ -37,7 +37,7 @@ def _(x): namespace="network", ) async def get_ip(loop, ip: str, inet: Optional[dict]) -> str: - _inet: socket.AddressFamily | int = 0 # pylint: disable=no-member + _inet: Union[socket.AddressFamily, int] = 0 # pylint: disable=no-member if inet: if inet["inet"] == "6": @@ -89,8 +89,8 @@ async def get_hostname(loop, ip: str) -> str: cache=Cache.MEMORY, namespace="network", ) -async def get_ipwhois_result(loop, ip: str) -> NoReturn | dict: - def _get_ipwhois_result(_ip: str) -> NoReturn | dict: +async def get_ipwhois_result(loop, ip: str) -> Union[NoReturn, dict]: + def _get_ipwhois_result(_ip: str) -> Union[NoReturn, dict]: try: net = Net(ip) obj = IPASN(net) @@ -121,7 +121,7 @@ async def get_ipwhois_result(loop, ip: str) -> NoReturn | dict: namespace="network", ) async def get_ipinfo_result(loop, apikey: str, ip: str) -> dict: - def _get_ipinfo_result(_ip: str) -> NoReturn | dict: + def _get_ipinfo_result(_ip: str) -> Union[NoReturn, dict]: """ Q. Why no getHandlerAsync ? A. Use of this return "Unclosed client session" and "Unclosed connector" @@ -251,11 +251,11 @@ async def get_map_bytes(apikey: str, latlon: str) -> Optional[io.BytesIO]: namespace="network", ) async def get_pydig_result( - loop, domain: str, query_type: str, dnssec: str | bool + loop, domain: str, query_type: str, dnssec: Union[str, bool] ) -> list: additional_args = [] if dnssec is False else ["+dnssec"] - def _get_pydig_result(_domain: str) -> NoReturn | dict: + def _get_pydig_result(_domain: str) -> Union[NoReturn, dict]: resolver = pydig.Resolver( nameservers=[ "80.67.169.40", @@ -275,14 +275,16 @@ async def get_pydig_result( return [] -def check_ip_version_or_raise(version: Optional[dict]) -> bool | NoReturn: +def check_ip_version_or_raise( + version: Optional[dict], +) -> Union[bool, NoReturn]: if version is None or version["inet"] in ("4", "6", ""): return True raise InvalidIp(_("Invalid ip version")) -def check_query_type_or_raise(query_type: str) -> bool | NoReturn: +def check_query_type_or_raise(query_type: str) -> Union[bool, NoReturn]: query_types = ( "a", "aaaa", @@ -306,7 +308,7 @@ def check_query_type_or_raise(query_type: str) -> bool | NoReturn: ) -def check_asn_or_raise(asn: str) -> bool | NoReturn: +def check_asn_or_raise(asn: str) -> Union[bool, NoReturn]: if asn.isdigit() and int(asn) < 4_294_967_295: return True diff --git a/tuxbot/cogs/Network/images/load_fail.py b/tuxbot/cogs/Network/images/load_fail.py index eaa5564..d9ea07b 100644 --- a/tuxbot/cogs/Network/images/load_fail.py +++ b/tuxbot/cogs/Network/images/load_fail.py @@ -1 +1 @@ -value = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x01M\x00\x00\x00\x96\x08\x06\x00\x00\x00Z~\xda\x9c\x00\x00\x01\x85iCCPICC Profile\x00\x00x\x9c}\x91=H\xc3@\x18\x86\xdf\xb6\xd6\x8aT\x14\xec "\x92\xa1:Y\x10\x15q\xd4*\x14\xa1B\xa8\x15Zu0\xb9\xf4\x0f\x9a4$).\x8e\x82k\xc1\xc1\x9f\xc5\xaa\x83\x8b\xb3\xae\x0e\xae\x82 \xf8\x03\xe2\xe6\xe6\xa4\xe8"%~\x97\x14Z\xc4x\xc7q\x0f\xef}\xef\xcb\xddw\x80\xbf^f\xaa\xd91\x0e\xa8\x9ae\xa4\x12q!\x93]\x15B\xaf\x08\xa2\x8ff\'\x86%f\xeas\xa2\x98\x84\xe7\xf8\xba\x87\x8f\xefw1\x9e\xe5]\xf7\xe7\xe8Qr&\x03|\x02\xf1,\xd3\r\x8bx\x83xz\xd3\xd29\xef\x13GXQR\x88\xcf\x89\xc7\x0c\xba \xf1#\xd7e\x97\xdf8\x17\x1c\xf6\xf3\xcc\x88\x91N\xcd\x13G\x88\x85B\x1b\xcbm\xcc\x8a\x86J\x0f\xbc\x9f\xd17e\x81\xfe[\xa0{\xcd\xed[\xf3\x1c\xa7\x0f@\x9az\x95\xbc\x01\x0e\x0e\x81\xd1\x02e\xaf{\xbc\xbb\xab\xbdo\xff\xd64\xfb\xf7\x03#\x1fr\x87\xd8m(%\x00\x00\x0b\xf3IDATx\x9c\xed\xdd\x7fp\xd3\xf5\x1d\xc7\xf1w\x80\xf5\xe7\x80P[\xa0-$MSI\xd3\xa4I\xdb\x90\xfe\xfe\x11\xe8\x0f\'-m)\x14\xb4T\x89X\xb4\xd8\x02\x07l\xb2\x02;\xbe\xca\xce\xc2\x8d\x13\xb7\x1b\xf3\xd4\r\x0e\xfcq\xd3\xde\xdc\xe9\xf4\xd4\x81\xc78\xe7NP\x07\xfe8\xd1\xbb9\xc6\xd0:\x1dXQ<\xd6\xe3\xc7w\x7f\xb0\xd4\xb4\xa6\xa5M\x93&\xcc\xe7\xe3\xees\xd7K\xbe\x9f\xcf\xe7\xfd\xb9\xe3^|\xbe\xf9~\xbf\x89\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\\;4\xa1.\x00\x10\x11y\xea\xa9\xa7\x8cw\xdcq\xc7\xcc%K\x96|\xff\xfd\xf7\xdf\x9f}\xe8\xd0!\x9f\xc7\xad\\\xb9\xf2RWW\xd7\x9f\xcdf\xb3\xda\xd1\xd1\xf1\xee\xbcy\xf3N\x8fq\xa9\xf8\x8e#41\xe6V\xae\\\xa9?s\xe6LEwww\xc1\xe1\xc3\x87g%%%\x95\x9c\xbd|\xf9\xf2\t\xa1\xac\t\xd7\xb6q\xa1.\x00\xff_N\x9d:\xd5\xe6\xf9\xbb\xb4\xb4\xf4O\xa7O\x9f\xbe-\x94\xf5\\\xbe|\xf9\x8b\xde\xde\xde9\x0e\x87\xe3\x8c\x88\xc8\xf1\xe3\xc7\xaf\xeb\xe9\xe9Y\x1c\xca\x9a\x00\xc0\xdbA\xf9\xe6T8l.\xbe\x98\xcd\xe6\xed\xf2\xcd\x0eX\tm5\xb8\x96\xb1\xd3D\xd0\xa4\xa6\xa6^\x08u\r\x1e111aS\x0b\xaem\x84&\x82\xe6\xec\xd9\xb3+C]\x83\x88\xc8\x87\x1f~\xa8\xfd\xf8\xe3\x8f\x97\x86\xba\x0e\x00\xf0\xa5\xef\xf4\\DTEQ:CY\xcc\xd1\xa3G\'766>#\xfdo?RBY\x13\x00x\xeb\x17\x9a"\xa2fgg\xbfx\xd3M7\xa5\x8cu!UUU.\x87\xc3\xf1\xfe\xc0z\x84\xd0\x04\x10F\xbe\x15\x9a"\xa2\xce\x981C\x9d6m\xda\x1e\x9b\xcd\x96\x17\xec\x02l6\xdb\x12\x87\xc3\xe1\xb3\x0e!4\x01\x84\x99\xbe\xb0\xb2\xdb\xed>\x83\xab\xb8\xb8\xf8_V\xab\xf5\xc1\xfa\xfa\xfa\xa5;w\xee\xbc~\xb4\x13n\xd9\xb2\xc5\xb9h\xd1\xa26\x9dN\xf7xII\xc9\x7f\x06\xce\x97\x93\x93s^\xaf\xd7\x1f\x15B\x13@\x18\xf2\x0eJWkkk\xb1\xc1`\x18j\xd7\xa7\x16\x16\x16^\x14\x91\x83UUU\xbf\x17\x11E\xa3\xd1(\x9b6m\xda4u\xea\xd4r\xab\xd5Zn6\x9b\xcbo\xb9\xe5\x96\xeae\xcb\x96)"\xa2\xe4\xe7\xe7w:\x1c\x8e\x17***\xfe\x1a\x15\x15ui\xb0q\xa3\xa3\xa3\xd5\xe4\xe4\xe4=\xed\xed\xed)\x93&MR\x84\xd0\x04\x10\x86\xfa\x85\xa6\xe7\xc5\xc6\xc6\xc6\xec\xd4\xd4\xd4G222zd\x88\x00\rD\xd3\xeb\xf5\xff0\x9b\xcdJuu\xb5\xce3?\xa1\x89@\xe1q2\x8c\x89\xae\xae\xae\xa3"\xb2BDV\x98L\xa6\xbc\xd8\xd8\xd8\xea\xe8\xe8\xe8\x92\xde\xde^\xe7\x1bo\xbc1\xaa/\xf0(..\xfeL\xa3\xd1\xbc\xfe\xde{\xef\xfdQU\xd5\x97N\x9e<\xf9\x81\x88\xc8\xf1\xe3\xc7\x03Q:\xd0\x0f\xa1\x891\xd1\xdc\xdc\xac\x8d\x8c\x8ct]\xb8p\xe1\xd8\xbe}\xfb\x0e\x8b\xc8a\xcf{\x8a\xa2$(\x8abq\xbb\xdd\xa9\xcf<\xf3\x8c\xee\xec\xd9\xb3R[[\xab\xbd|\xf9\xb2\xfd\xcd7\xdf\x14\x8dF#UUU\xb2w\xef\xdeC\x97.]\x12\x87\xc3!\t\t\towww\x7f\xbe{\xf7\xee\xc3\xb3g\xcf>\x1f\xc2\xa5\x01\xc0\xa8\xf8<=///? "\xaaN\xa7\xfb\xfc\xa3\x8f>\x1a\xf3\xff\xac\xa3\xa3\xa3\x15\xe1\xf4\x1c\x01\xc0\x13A\x08\x9aE\x8b\x16\x95y\xfe\x8e\x89\x89\x99!"r\xf1\xe2\xc5I\'N\x9c\x08hh\xba\\\xaeT\x11q\xad_\xbf>n\xb0c\x1c\x0e\xc7\xec@\xce\t\x00\x81\xe2\xbd\xd3\xec\x89\x8e\x8e\xce\x12\x11\x99?\x7f~V\\\\\xdc\xd6\xf9\xf3\xe7\xd7\x07r\xb29s\xe64$%%y\xbe\x11\xbe\xa7\xb6\xb66e\xe01Z\xad\xd6-\xfd/\x16)\x81\xac\x01\x00F\xa3/4\r\x06\x83\xaa\xd1hz\x9a\x9a\x9a\x96\x05c\xa2\r\x1b6\x8c\xcb\xce\xce\xee\x16\xaf@4\x1a\x8d\x8fy\xde?w\xee\\TQQ\xd1V\x11Q\xaf\xbb\xee:B\x13@X\xea\x0b\xcd\xdbo\xbf}\xa5\xe7\xa6r\xa3\xd1\xf8\xca\xe6\xcd\x9bo\x0e\xe4D\x95\x95\x95\x1b\xc4\xc7-G{\xf7\xee-\xb2\xd9l)z\xbd\xfe\x9f"\xa2\x96\x95\x95=\x9d\x93\x93\xb3S\x08M\x00a\xa8\xdf\x85\xa0\x0b\x17.L\xb8\xf3\xce;7\xa5\xa5\xa5\xfd[D\xd4Y\xb3f}d\xb5Z\x7f\xb9n\xdd\xba\xf9\xaf\xbd\xf6Z\xa4\xbf\x93\xb8\xddn\xad\xc9d\xf2y\xcf\xa7\xd9l\xde/"R^^\xfebCC\xc3\x02\x11\xee\xd3\x04\x10\xbe|^=\xef\xed\xed\x8d*,,t\x17\x15\x15\xbd\x14\x15\x15\xd5\x17pUUU\xc7\xd2\xd3\xd3\x1f\xca\xcb\xcbS,\x16K\x95\xd5jum\xd9\xb2e\xd0G+;::\xd2DDRRR\xee\x11\x1f\x81\xe9i\x05\x05\x05.\xef~\x84&\x80p\xe534\xbd\xdds\xcf=\x13\xf3\xf3\xf3\x17\xc4\xc5\xc5m\x8b\x8f\x8f?XQQ1\xe8\xa3\x90\x03[FF\xc6\x16\xb7\xdb====\xfd\xeb\xa1\x8e\xb3Z\xad\x7f\xf1\x9e\x93\xd0\x04\x10\xae|\x86\xe6\xf6\xed\xdbgM\x980A)---\xf4\xd5i\xc7\x8e\x1d\xb1eee.\xb9\xf2\xbc\xfa\x0f\r\x06\x83"W\xc2\xad\xaf\xe5\xe6\xe6*mmm\xb1555\xbbe\x18\x01\xdb\xd2\xd2\xb2\xc83>\xa1\t \\\xf9\x0c\xcd\x9a\x9a\x9a7\xe5\xcaE\x99\x8b\x8f=\xf6\xd8\xf7\xfc\x1d\xbc\xa8\xa8\xc8\x90\x98\x988\xac]\xa9\xd3\xe9<\xe1\xe9\x17\x11\x11\xa1\x08\xa1\x89\x00\xe0\xe6v\x04\x8d\xc3\xe1\xf8\x81\xe7\xef\xee\xee\xee\x1e\x11\x91#G\x8e|\xa5\xd5jU\x7f\xc7\xfc\xf4\xd3O\xef\xfb\xe4\x93O\xfa\xbd\x96\x99\x99\xf9\xd0\xaaU\xab\xd6{.\x00y\xbc\xfe\xfa\xeb)&\x93\xc9-"b4\x1a\xab\xfc\x9d\x13\x00\x82\xa9o\xa7\x99\x9c\x9c\xac\xae]\xbb\xb6ID\xe4\xfe\xfb\xef\x9f\xbad\xc9\x92\x9fl\xdc\xb81\xc3\xdf\x81\xef\xbb\xef\xbe\x12\x19\xb0\x9bt8\x1c\xf7z\x1f\x93\x95\x95\xf5\xaa\xf7\xfb:\x9d\xee\xd4s\xcf=\x17\x95\x9b\x9b\xfb\x84\xb0\xd3\x04\x10\x86\xfa=\x11\x14\x19\x19\xa9\xdex\xe3\x8d\x9d/\xbf\xfcr\xcch\x07NOO\xf7\x1e{\xb0\x8bM\xca\xc0c\x1a\x1b\x1b7644\xa4\xe8\xf5zB\x13@\xd8\xe9\x0b\xb6\xc4\xc4\xc4\xfa\xfa\xfa\xfa\x17\xe4\xca\x139\xa7ZZZ~l\xb7\xdb\xb5\xfe\x0c\xeap8\xe6\x8a\x8f\xcf-e\x18\xa1\x99\x94\x94\xf4yCC\xc3\x14\x93\xc9\xf4s!4\x01\x84\x99o]\x08Z\xb3f\xcd\xfc\xa4\xa4\xa4?\xcb\x95\xd3e5--\xed\x0f\x19\x19\x19m\xdb\xb7o\x1f\xf6\x97h\x94\x95\x95\xbd-~\x86\xa6\x88\xa8\x93\'OV\xba\xba\xbaf\x18\x0c\x86sBh\x02\x08#\x83\xde\xa7\xb9f\xcd\x9a\xfc\xca\xca\xca\x87\xecv{\xdf\xf3\xe2N\xa7\xf3\xa2\x88\x1c,--}XD\x14\xb7\xdb\xed\xfe_?\x97\xcb\xe5r\x89\x88\xdcv\xdbm\xb7\x8a\xef\xc0\x1cvh\x1a\x0c\x86\xaf\x9f|\xf2I\xdd\x94)S\x14!4\x01\x84\x91\xab\xde\xdc."r\xf7\xddwg\x1a\x8d\xc6\x96\xd2\xd2\xd2\x87\xb5Z\xed!\x8b\xc5\xf2\xad\x9b\xd5\xf5z\xfd\x13""\xb1\xb1\xb1\'\x06\xbe7\xc4\x1c\xca`\xc7\xd6\xd5\xd5\xfd\xba\xb6\xb6Vk\xb3\xd9V\x07f\xa9\x000z>C\xd3h4f\xc7\xc7\xc7?;a\xc2\x84\xfa\xc1:\x9e?\x7f~\xfc\x8a\x15+\xb2\xff\xd7\xcfu\xef\xbd\xf7&477\xaf\x95\xc1\x03sD\xa19}\xfatu\xee\xdc\xb9)\xbbv\xed\x1a\xd5\xcfk\x00@ \xf5\x85fQQQ\x85\xe7\xc5\xa2\xa2\xa2\x0fD\xae\xfcB\xe4\xe3\x8f?\x1e5\x9c\x816l\xd80\xb1\xa0\xa0\xa0G\x02\x14\x9a"\xa2\x16\x16\x16\xfe\xd6\xef\x95\x01@\xa0\xe9t\xba\x07\xe5\x9b\x80z\xda\xf3\xfa\xdc\xb9s\x7f\'"juu\xf5\xa7\xc3\x1d+55U\x91\xa1\x03s\xc4\xa1)"\xea\xe6\xcd\x9b\xf3F\xbc0\x00\x08\x86\xe5\xcb\x97\xbb\xc4+\xa0V\xadZ\xb5ED\xe4\x95W^\xf9\x9eF\xa3q\xa5\xa5\xa5i\x873\xce]w\xdd\x950~\xfc\xf8\xab\xed2\xfd\n\xcd\x82\x82\x82\x83~/\x10\x00\x02m\xe1\xc2\x85]\xe2\x15R\xf9\xf9\xf9/.]\xba4e$c\xdc|\xf3\xcd\xbf\x92\xab\x07\xa6:o\xde\xbcj\xef~\x16\x8b\xe5g\xc3\xe9\xa7\xd3\xe9\\~.\x0f\x00\x02\xab\xb3\xb3s\xf2\r7\xdc\xf0\x86x\x85T||\xbc\x9a\x9c\x9c\xbc\xa7\xbd\xbd\xdd\xe7\xb7\x1cys:\x9d)6\x9b\xed\xaa\xc1\'"jMM\xcd\x91\xe6\xe6\xe6\xef\x8b\x88\xb4\xb7\xb7\x9b\xf4z\xfdpv\xa7\xaa\xc9dz5\xa0\x8b\x06\x80\xd1\xd8\xb1c\xc7x\xa7\xd3\xb9S|\x07\xd6\xc7\x15\x15\x15\xbb\xf3\xf2\xf2nZ\xbf~\xbda`\xdf\xec\xec\xecG}\xf5\x1b\xac\xe5\xe6\xe6\x9e\x11\x91\x83&\x93\xe9\xc2H\xfa\xad^\xbd\xba)X\xeb\x07\x00\xbf\xb4\xb4\xb4\x94\x98\xcd\xe6C2Dx\xe5\xe4\xe4\xf4\x8a\xc8\xc1\xd5\xabW\xbb\xb6m\xdb\x963\xd4\xb1\x81l\xe9\xe9\xe9\x7f\x0b\xf6\xfa\x01\xc0/\xed\xed\xedy6\x9b\xed7N\xa7\xd3\xe7\xe9sqq\xf1\x07""yyy\xfb}\xbd?T\xcb\xc8\xc8\xd8]SS\xa3X,\x96!\xc3\xd9W3\x99L\xcb\xc7`\xf9\x00\xe0\xbf\xe6\xe6\xe6\x92\xc2\xc2B\xc5h4\xee\xf7|\x06i\xb7\xdb+rss]2\xc2\xd0\xb3\xd9l\xfd\xbe\x1a.;;\xfb\xc8H\xfa\x9b\xcd\xe6O\xbe\xfc\xf2KmpW\x0c\x00\x01\xf4\xe8\xa3\x8fN\x15\x11\xc9\xcd\xcd=,#?\xcdv\r\x18N\x19\xe9\x18\x99\x99\x99w\x07km\x00\x10\x14\xeb\xd6\xad[,~|.i\xb1X~\xe2=Njj\xea\xc1\x91\x8e\x91\x98\x98\xd8\xb3p\xe1BmpW\x08\x00\x01d\xb5Z\xff.~\x84fRR\x92z\xfd\xf5\xd7o\xeb\xe8\xe8h2\x1a\x8d\xcf\xfa3\x86\\\xd9m\xfet\x8c\x96\n\x00\xa33g\xce\x9c\x16\xf13\xec\x02\xd5\xecv\xfb\xf9\x07\x1ex`f\xf0W\x0b\x00\xa3\x14\x19\x199\xe2S\xea`\xb4\xba\xba\xba\x8dA_,\xaey\xfc\x1a%Bn\xe6\xcc\x99gC]\x83\x88\xc8\x89\x13\'\xbaC]\x03\x00\\UkkkVqq\xf1\xdf$D;\xcc\xc8\xc8H\xb5\xa5\xa5\xe5\xc9\xd3\xa7O\x8f\x1f\x8b\xf5\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbe\xed\xbfB\x8d\x95\xd2j0\xc9\xb9\x00\x00\x00\x00IEND\xaeB`\x82' \ No newline at end of file +value = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x01M\x00\x00\x00\x96\x08\x06\x00\x00\x00Z~\xda\x9c\x00\x00\x01\x85iCCPICC Profile\x00\x00x\x9c}\x91=H\xc3@\x18\x86\xdf\xb6\xd6\x8aT\x14\xec "\x92\xa1:Y\x10\x15q\xd4*\x14\xa1B\xa8\x15Zu0\xb9\xf4\x0f\x9a4$).\x8e\x82k\xc1\xc1\x9f\xc5\xaa\x83\x8b\xb3\xae\x0e\xae\x82 \xf8\x03\xe2\xe6\xe6\xa4\xe8"%~\x97\x14Z\xc4x\xc7q\x0f\xef}\xef\xcb\xddw\x80\xbf^f\xaa\xd91\x0e\xa8\x9ae\xa4\x12q!\x93]\x15B\xaf\x08\xa2\x8ff\'\x86%f\xeas\xa2\x98\x84\xe7\xf8\xba\x87\x8f\xefw1\x9e\xe5]\xf7\xe7\xe8Qr&\x03|\x02\xf1,\xd3\r\x8bx\x83xz\xd3\xd29\xef\x13GXQR\x88\xcf\x89\xc7\x0c\xba \xf1#\xd7e\x97\xdf8\x17\x1c\xf6\xf3\xcc\x88\x91N\xcd\x13G\x88\x85B\x1b\xcbm\xcc\x8a\x86J\x0f\xbc\x9f\xd17e\x81\xfe[\xa0{\xcd\xed[\xf3\x1c\xa7\x0f@\x9az\x95\xbc\x01\x0e\x0e\x81\xd1\x02e\xaf{\xbc\xbb\xab\xbdo\xff\xd64\xfb\xf7\x03#\x1fr\x87\xd8m(%\x00\x00\x0b\xf3IDATx\x9c\xed\xdd\x7fp\xd3\xf5\x1d\xc7\xf1w\x80\xf5\xe7\x80P[\xa0-$MSI\xd3\xa4I\xdb\x90\xfe\xfe\x11\xe8\x0f\'-m)\x14\xb4T\x89X\xb4\xd8\x02\x07l\xb2\x02;\xbe\xca\xce\xc2\x8d\x13\xb7\x1b\xf3\xd4\r\x0e\xfcq\xd3\xde\xdc\xe9\xf4\xd4\x81\xc78\xe7NP\x07\xfe8\xd1\xbb9\xc6\xd0:\x1dXQ<\xd6\xe3\xc7w\x7f\xb0\xd4\xb4\xa6\xa5M\x93&\xcc\xe7\xe3\xees\xd7K\xbe\x9f\xcf\xe7\xfd\xb9\xe3^|\xbe\xf9~\xbf\x89\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\\;4\xa1.\x00\x10\x11y\xea\xa9\xa7\x8cw\xdcq\xc7\xcc%K\x96|\xff\xfd\xf7\xdf\x9f}\xe8\xd0!\x9f\xc7\xad\\\xb9\xf2RWW\xd7\x9f\xcdf\xb3\xda\xd1\xd1\xf1\xee\xbcy\xf3N\x8fq\xa9\xf8\x8e#41\xe6V\xae\\\xa9?s\xe6LEwww\xc1\xe1\xc3\x87g%%%\x95\x9c\xbd|\xf9\xf2\t\xa1\xac\t\xd7\xb6q\xa1.\x00\xff_N\x9d:\xd5\xe6\xf9\xbb\xb4\xb4\xf4O\xa7O\x9f\xbe-\x94\xf5\\\xbe|\xf9\x8b\xde\xde\xde9\x0e\x87\xe3\x8c\x88\xc8\xf1\xe3\xc7\xaf\xeb\xe9\xe9Y\x1c\xca\x9a\x00\xc0\xdbA\xf9\xe6T8l.\xbe\x98\xcd\xe6\xed\xf2\xcd\x0eX\tm5\xb8\x96\xb1\xd3D\xd0\xa4\xa6\xa6^\x08u\r\x1e111aS\x0b\xaem\x84&\x82\xe6\xec\xd9\xb3+C]\x83\x88\xc8\x87\x1f~\xa8\xfd\xf8\xe3\x8f\x97\x86\xba\x0e\x00\xf0\xa5\xef\xf4\\DTEQ:CY\xcc\xd1\xa3G\'766>#\xfdo?RBY\x13\x00x\xeb\x17\x9a"\xa2fgg\xbfx\xd3M7\xa5\x8cu!UUU.\x87\xc3\xf1\xfe\xc0z\x84\xd0\x04\x10F\xbe\x15\x9a"\xa2\xce\x981C\x9d6m\xda\x1e\x9b\xcd\x96\x17\xec\x02l6\xdb\x12\x87\xc3\xe1\xb3\x0e!4\x01\x84\x99\xbe\xb0\xb2\xdb\xed>\x83\xab\xb8\xb8\xf8_V\xab\xf5\xc1\xfa\xfa\xfa\xa5;w\xee\xbc~\xb4\x13n\xd9\xb2\xc5\xb9h\xd1\xa26\x9dN\xf7xII\xc9\x7f\x06\xce\x97\x93\x93s^\xaf\xd7\x1f\x15B\x13@\x18\xf2\x0eJWkkk\xb1\xc1`\x18j\xd7\xa7\x16\x16\x16^\x14\x91\x83UUU\xbf\x17\x11E\xa3\xd1(\x9b6m\xda4u\xea\xd4r\xab\xd5Zn6\x9b\xcbo\xb9\xe5\x96\xeae\xcb\x96)"\xa2\xe4\xe7\xe7w:\x1c\x8e\x17***\xfe\x1a\x15\x15ui\xb0q\xa3\xa3\xa3\xd5\xe4\xe4\xe4=\xed\xed\xed)\x93&MR\x84\xd0\x04\x10\x86\xfa\x85\xa6\xe7\xc5\xc6\xc6\xc6\xec\xd4\xd4\xd4G222zd\x88\x00\rD\xd3\xeb\xf5\xff0\x9b\xcdJuu\xb5\xce3?\xa1\x89@\xe1q2\x8c\x89\xae\xae\xae\xa3"\xb2BDV\x98L\xa6\xbc\xd8\xd8\xd8\xea\xe8\xe8\xe8\x92\xde\xde^\xe7\x1bo\xbc1\xaa/\xf0(..\xfeL\xa3\xd1\xbc\xfe\xde{\xef\xfdQU\xd5\x97N\x9e<\xf9\x81\x88\xc8\xf1\xe3\xc7\x03Q:\xd0\x0f\xa1\x891\xd1\xdc\xdc\xac\x8d\x8c\x8ct]\xb8p\xe1\xd8\xbe}\xfb\x0e\x8b\xc8a\xcf{\x8a\xa2$(\x8abq\xbb\xdd\xa9\xcf<\xf3\x8c\xee\xec\xd9\xb3R[[\xab\xbd|\xf9\xb2\xfd\xcd7\xdf\x14\x8dF#UUU\xb2w\xef\xdeC\x97.]\x12\x87\xc3!\t\t\towww\x7f\xbe{\xf7\xee\xc3\xb3g\xcf>\x1f\xc2\xa5\x01\xc0\xa8\xf8<=///? "\xaaN\xa7\xfb\xfc\xa3\x8f>\x1a\xf3\xff\xac\xa3\xa3\xa3\x15\xe1\xf4\x1c\x01\xc0\x13A\x08\x9aE\x8b\x16\x95y\xfe\x8e\x89\x89\x99!"r\xf1\xe2\xc5I\'N\x9c\x08hh\xba\\\xaeT\x11q\xad_\xbf>n\xb0c\x1c\x0e\xc7\xec@\xce\t\x00\x81\xe2\xbd\xd3\xec\x89\x8e\x8e\xce\x12\x11\x99?\x7f~V\\\\\xdc\xd6\xf9\xf3\xe7\xd7\x07r\xb29s\xe64$%%y\xbe\x11\xbe\xa7\xb6\xb66e\xe01Z\xad\xd6-\xfd/\x16)\x81\xac\x01\x00F\xa3/4\r\x06\x83\xaa\xd1hz\x9a\x9a\x9a\x96\x05c\xa2\r\x1b6\x8c\xcb\xce\xce\xee\x16\xaf@4\x1a\x8d\x8fy\xde?w\xee\\TQQ\xd1V\x11Q\xaf\xbb\xee:B\x13@X\xea\x0b\xcd\xdbo\xbf}\xa5\xe7\xa6r\xa3\xd1\xf8\xca\xe6\xcd\x9bo\x0e\xe4D\x95\x95\x95\x1b\xc4\xc7-G{\xf7\xee-\xb2\xd9l)z\xbd\xfe\x9f"\xa2\x96\x95\x95=\x9d\x93\x93\xb3S\x08M\x00a\xa8\xdf\x85\xa0\x0b\x17.L\xb8\xf3\xce;7\xa5\xa5\xa5\xfd[D\xd4Y\xb3f}d\xb5Z\x7f\xb9n\xdd\xba\xf9\xaf\xbd\xf6Z\xa4\xbf\x93\xb8\xddn\xad\xc9d\xf2y\xcf\xa7\xd9l\xde/"R^^\xfebCC\xc3\x02\x11\xee\xd3\x04\x10\xbe|^=\xef\xed\xed\x8d*,,t\x17\x15\x15\xbd\x14\x15\x15\xd5\x17pUUU\xc7\xd2\xd3\xd3\x1f\xca\xcb\xcbS,\x16K\x95\xd5jum\xd9\xb2e\xd0G+;::\xd2DDRRR\xee\x11\x1f\x81\xe9i\x05\x05\x05.\xef~\x84&\x80p\xe534\xbd\xdds\xcf=\x13\xf3\xf3\xf3\x17\xc4\xc5\xc5m\x8b\x8f\x8f?XQQ1\xe8\xa3\x90\x03[FF\xc6\x16\xb7\xdb====\xfd\xeb\xa1\x8e\xb3Z\xad\x7f\xf1\x9e\x93\xd0\x04\x10\xae|\x86\xe6\xf6\xed\xdbgM\x980A)---\xf4\xd5i\xc7\x8e\x1d\xb1eee.\xb9\xf2\xbc\xfa\x0f\r\x06\x83"W\xc2\xad\xaf\xe5\xe6\xe6*mmm\xb1555\xbbe\x18\x01\xdb\xd2\xd2\xb2\xc83>\xa1\t \\\xf9\x0c\xcd\x9a\x9a\x9a7\xe5\xcaE\x99\x8b\x8f=\xf6\xd8\xf7\xfc\x1d\xbc\xa8\xa8\xc8\x90\x98\x988\xac]\xa9\xd3\xe9<\xe1\xe9\x17\x11\x11\xa1\x08\xa1\x89\x00\xe0\xe6v\x04\x8d\xc3\xe1\xf8\x81\xe7\xef\xee\xee\xee\x1e\x11\x91#G\x8e|\xa5\xd5jU\x7f\xc7\xfc\xf4\xd3O\xef\xfb\xe4\x93O\xfa\xbd\x96\x99\x99\xf9\xd0\xaaU\xab\xd6{.\x00y\xbc\xfe\xfa\xeb)&\x93\xc9-"b4\x1a\xab\xfc\x9d\x13\x00\x82\xa9o\xa7\x99\x9c\x9c\xac\xae]\xbb\xb6ID\xe4\xfe\xfb\xef\x9f\xbad\xc9\x92\x9fl\xdc\xb81\xc3\xdf\x81\xef\xbb\xef\xbe\x12\x19\xb0\x9bt8\x1c\xf7z\x1f\x93\x95\x95\xf5\xaa\xf7\xfb:\x9d\xee\xd4s\xcf=\x17\x95\x9b\x9b\xfb\x84\xb0\xd3\x04\x10\x86\xfa=\x11\x14\x19\x19\xa9\xdex\xe3\x8d\x9d/\xbf\xfcr\xcch\x07NOO\xf7\x1e{\xb0\x8bM\xca\xc0c\x1a\x1b\x1b7644\xa4\xe8\xf5zB\x13@\xd8\xe9\x0b\xb6\xc4\xc4\xc4\xfa\xfa\xfa\xfa\x17\xe4\xca\x139\xa7ZZZ~l\xb7\xdb\xb5\xfe\x0c\xeap8\xe6\x8a\x8f\xcf-e\x18\xa1\x99\x94\x94\xf4yCC\xc3\x14\x93\xc9\xf4s!4\x01\x84\x99o]\x08Z\xb3f\xcd\xfc\xa4\xa4\xa4?\xcb\x95\xd3e5--\xed\x0f\x19\x19\x19m\xdb\xb7o\x1f\xf6\x97h\x94\x95\x95\xbd-~\x86\xa6\x88\xa8\x93\'OV\xba\xba\xbaf\x18\x0c\x86sBh\x02\x08#\x83\xde\xa7\xb9f\xcd\x9a\xfc\xca\xca\xca\x87\xecv{\xdf\xf3\xe2N\xa7\xf3\xa2\x88\x1c,--}XD\x14\xb7\xdb\xed\xfe_?\x97\xcb\xe5r\x89\x88\xdcv\xdbm\xb7\x8a\xef\xc0\x1cvh\x1a\x0c\x86\xaf\x9f|\xf2I\xdd\x94)S\x14!4\x01\x84\x91\xab\xde\xdc."r\xf7\xddwg\x1a\x8d\xc6\x96\xd2\xd2\xd2\x87\xb5Z\xed!\x8b\xc5\xf2\xad\x9b\xd5\xf5z\xfd\x13""\xb1\xb1\xb1\'\x06\xbe7\xc4\x1c\xca`\xc7\xd6\xd5\xd5\xfd\xba\xb6\xb6Vk\xb3\xd9V\x07f\xa9\x000z>C\xd3h4f\xc7\xc7\xc7?;a\xc2\x84\xfa\xc1:\x9e?\x7f~\xfc\x8a\x15+\xb2\xff\xd7\xcfu\xef\xbd\xf7&477\xaf\x95\xc1\x03sD\xa19}\xfatu\xee\xdc\xb9)\xbbv\xed\x1a\xd5\xcfk\x00@ \xf5\x85fQQQ\x85\xe7\xc5\xa2\xa2\xa2\x0fD\xae\xfcB\xe4\xe3\x8f?\x1e5\x9c\x816l\xd80\xb1\xa0\xa0\xa0G\x02\x14\x9a"\xa2\x16\x16\x16\xfe\xd6\xef\x95\x01@\xa0\xe9t\xba\x07\xe5\x9b\x80z\xda\xf3\xfa\xdc\xb9s\x7f\'"juu\xf5\xa7\xc3\x1d+55U\x91\xa1\x03s\xc4\xa1)"\xea\xe6\xcd\x9b\xf3F\xbc0\x00\x08\x86\xe5\xcb\x97\xbb\xc4+\xa0V\xadZ\xb5ED\xe4\x95W^\xf9\x9eF\xa3q\xa5\xa5\xa5i\x873\xce]w\xdd\x950~\xfc\xf8\xab\xed2\xfd\n\xcd\x82\x82\x82\x83~/\x10\x00\x02m\xe1\xc2\x85]\xe2\x15R\xf9\xf9\xf9/.]\xba4e$c\xdc|\xf3\xcd\xbf\x92\xab\x07\xa6:o\xde\xbcj\xef~\x16\x8b\xe5g\xc3\xe9\xa7\xd3\xe9\\~.\x0f\x00\x02\xab\xb3\xb3s\xf2\r7\xdc\xf0\x86x\x85T||\xbc\x9a\x9c\x9c\xbc\xa7\xbd\xbd\xdd\xe7\xb7\x1cys:\x9d)6\x9b\xed\xaa\xc1\'"jMM\xcd\x91\xe6\xe6\xe6\xef\x8b\x88\xb4\xb7\xb7\x9b\xf4z\xfdpv\xa7\xaa\xc9dz5\xa0\x8b\x06\x80\xd1\xd8\xb1c\xc7x\xa7\xd3\xb9S|\x07\xd6\xc7\x15\x15\x15\xbb\xf3\xf2\xf2nZ\xbf~\xbda`\xdf\xec\xec\xecG}\xf5\x1b\xac\xe5\xe6\xe6\x9e\x11\x91\x83&\x93\xe9\xc2H\xfa\xad^\xbd\xba)X\xeb\x07\x00\xbf\xb4\xb4\xb4\x94\x98\xcd\xe6C2Dx\xe5\xe4\xe4\xf4\x8a\xc8\xc1\xd5\xabW\xbb\xb6m\xdb\x963\xd4\xb1\x81l\xe9\xe9\xe9\x7f\x0b\xf6\xfa\x01\xc0/\xed\xed\xedy6\x9b\xed7N\xa7\xd3\xe7\xe9sqq\xf1\x07""yyy\xfb}\xbd?T\xcb\xc8\xc8\xd8]SS\xa3X,\x96!\xc3\xd9W3\x99L\xcb\xc7`\xf9\x00\xe0\xbf\xe6\xe6\xe6\x92\xc2\xc2B\xc5h4\xee\xf7|\x06i\xb7\xdb+rss]2\xc2\xd0\xb3\xd9l\xfd\xbe\x1a.;;\xfb\xc8H\xfa\x9b\xcd\xe6O\xbe\xfc\xf2KmpW\x0c\x00\x01\xf4\xe8\xa3\x8fN\x15\x11\xc9\xcd\xcd=,#?\xcdv\r\x18N\x19\xe9\x18\x99\x99\x99w\x07km\x00\x10\x14\xeb\xd6\xad[,~|.i\xb1X~\xe2=Njj\xea\xc1\x91\x8e\x91\x98\x98\xd8\xb3p\xe1BmpW\x08\x00\x01d\xb5Z\xff.~\x84fRR\x92z\xfd\xf5\xd7o\xeb\xe8\xe8h2\x1a\x8d\xcf\xfa3\x86\\\xd9m\xfet\x8c\x96\n\x00\xa33g\xce\x9c\x16\xf13\xec\x02\xd5\xecv\xfb\xf9\x07\x1ex`f\xf0W\x0b\x00\xa3\x14\x19\x199\xe2S\xea`\xb4\xba\xba\xba\x8dA_,\xaey\xfc\x1a%Bn\xe6\xcc\x99gC]\x83\x88\xc8\x89\x13\'\xbaC]\x03\x00\\UkkkVqq\xf1\xdf$D;\xcc\xc8\xc8H\xb5\xa5\xa5\xe5\xc9\xd3\xa7O\x8f\x1f\x8b\xf5\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbe\xed\xbfB\x8d\x95\xd2j0\xc9\xb9\x00\x00\x00\x00IEND\xaeB`\x82' diff --git a/tuxbot/cogs/Network/network.py b/tuxbot/cogs/Network/network.py index 656ec20..22a9876 100644 --- a/tuxbot/cogs/Network/network.py +++ b/tuxbot/cogs/Network/network.py @@ -2,7 +2,7 @@ import asyncio import logging import time from datetime import datetime -from typing import Optional +from typing import Optional, Union import aiohttp import discord @@ -272,7 +272,7 @@ class Network(commands.Cog): ctx: ContextPlus, domain: IPConverter, query_type: QueryTypeConverter, - dnssec: str | bool = False, + dnssec: Union[str, bool] = False, ): check_query_type_or_raise(str(query_type)) diff --git a/tuxbot/cogs/Polls/polls.py b/tuxbot/cogs/Polls/polls.py index 93dabd7..a95062b 100644 --- a/tuxbot/cogs/Polls/polls.py +++ b/tuxbot/cogs/Polls/polls.py @@ -1,6 +1,6 @@ import json import logging -from typing import Dict +from typing import Dict, Union, List import discord from discord.ext import commands @@ -45,7 +45,7 @@ class Polls(commands.Cog): self, ctx: ContextPlus, question: str, - answers: list[str], + answers: List[str], anonymous=False, ): emotes = utils_emotes.get(len(answers)) @@ -90,7 +90,7 @@ class Polls(commands.Cog): async def get_poll( self, pld: discord.RawReactionActionEvent - ) -> bool | Poll: + ) -> Union[bool, Poll]: if pld.user_id != self.bot.user.id: poll = await Poll.get_or_none(message_id=pld.message_id) @@ -225,7 +225,7 @@ class Polls(commands.Cog): async def get_suggest( self, pld: discord.RawReactionActionEvent - ) -> bool | Suggest: + ) -> Union[bool, Suggest]: if pld.user_id != self.bot.user.id: suggest = await Suggest.get_or_none(message_id=pld.message_id) diff --git a/tuxbot/cogs/Tags/functions/utils.py b/tuxbot/cogs/Tags/functions/utils.py index 3b3197b..bc1ed75 100644 --- a/tuxbot/cogs/Tags/functions/utils.py +++ b/tuxbot/cogs/Tags/functions/utils.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Optional, List import discord @@ -12,7 +12,7 @@ async def get_tag(guild_id: int, name: str) -> Tag: async def get_all_tags( guild_id: int, author: Optional[discord.Member] = None -) -> list[Tag]: +) -> List[Tag]: if author is not None: return ( await Tag.filter(server_id=guild_id, author_id=author.id) @@ -23,7 +23,7 @@ async def get_all_tags( return await Tag.filter(server_id=guild_id).all().order_by("-uses") -async def search_tags(guild_id: int, q: str) -> list[Tag]: +async def search_tags(guild_id: int, q: str) -> List[Tag]: return ( await Tag.filter(server_id=guild_id, name__icontains=q) .all() diff --git a/tuxbot/core/bot.py b/tuxbot/core/bot.py index 75f1710..a82eb3e 100644 --- a/tuxbot/core/bot.py +++ b/tuxbot/core/bot.py @@ -2,8 +2,9 @@ import asyncio import datetime import importlib import logging +import sys from collections import Counter -from typing import List, Tuple +from typing import List, Tuple, Union import aiohttp import discord @@ -177,6 +178,7 @@ class Tux(commands.AutoShardedBot): table.add_row(f"Language: {self.config.Core.locale}") table.add_row(f"Tuxbot Version: {__version__}") table.add_row(f"Discord.py Version: {discord.__version__}") + table.add_row(f"Python Version: {sys.version.split(' ')[0]}") table.add_row(f"Instance name: {self.instance_name}") table.add_row(f"Shards: {self.shard_count}") table.add_row(f"Servers: {len(self.guilds)}") @@ -200,13 +202,13 @@ class Tux(commands.AutoShardedBot): self.console.print() async def is_owner( - self, user: discord.User | discord.Member | discord.Object + self, user: Union[discord.User, discord.Member, discord.Object] ) -> bool: """Determines if the user is a bot owner. Parameters ---------- - user: discord.User | discord.Member + user: Union[discord.User, discord.Member] Returns ------- diff --git a/tuxbot/core/i18n.py b/tuxbot/core/i18n.py index 465dd7b..9017a3a 100644 --- a/tuxbot/core/i18n.py +++ b/tuxbot/core/i18n.py @@ -1,7 +1,7 @@ import logging import os from pathlib import Path -from typing import Dict, NoReturn, Any, Tuple +from typing import Dict, NoReturn, Any, Tuple, Union from babel.messages.pofile import read_po @@ -19,7 +19,7 @@ available_locales: Dict[str, Tuple] = { } -def find_locale(locale: str) -> str | NoReturn: +def find_locale(locale: str) -> Union[str, NoReturn]: """We suppose `locale` is in `_available_locales.values()`""" for key, val in available_locales.items(): @@ -46,7 +46,9 @@ def get_locale_name(locale: str) -> str: class Translator: """Class to load texts at init.""" - def __init__(self, name: str, file_location: Path | os.PathLike | str): + def __init__( + self, name: str, file_location: Union[Path, os.PathLike, str] + ): """Initializes the Translator object. Parameters diff --git a/tuxbot/core/utils/functions/utils.py b/tuxbot/core/utils/functions/utils.py index 52597de..3f9a147 100644 --- a/tuxbot/core/utils/functions/utils.py +++ b/tuxbot/core/utils/functions/utils.py @@ -1,6 +1,6 @@ import asyncio import functools -from typing import Dict, Optional +from typing import Dict, Optional, Tuple import aiohttp from discord.ext import commands @@ -29,7 +29,7 @@ def typing(func): async def shorten( text: str, length: int, fail: bool = False -) -> tuple[bool, dict]: +) -> Tuple[bool, dict]: output: Dict[str, str] = {"text": text[:length], "link": ""} if len(text) > length: diff --git a/tuxbot/setup.py b/tuxbot/setup.py index ce29cf2..f070e4b 100644 --- a/tuxbot/setup.py +++ b/tuxbot/setup.py @@ -7,7 +7,7 @@ import sys import json from argparse import Namespace from pathlib import Path -from typing import List +from typing import List, Union from urllib import request from rich.prompt import Prompt, IntPrompt @@ -121,7 +121,7 @@ def get_ip() -> str: def get_multiple( question: str, confirmation: str, value_type: type -) -> List[str | int]: +) -> List[Union[str, int]]: """Give possibility to user to fill multiple value. Parameters @@ -135,10 +135,12 @@ def get_multiple( Returns ------- - List[str | int] + List[Union[str, int]] List containing user filled values. """ - prompt: IntPrompt | Prompt = IntPrompt() if value_type is int else Prompt() + prompt: Union[IntPrompt, Prompt] = ( + IntPrompt() if value_type is int else Prompt() + ) user_input = prompt.ask(question, console=console) @@ -166,12 +168,14 @@ def get_multiple( return values -def get_extra(question: str, value_type: type) -> str | int: - prompt: IntPrompt | Prompt = IntPrompt() if value_type is int else Prompt() +def get_extra(question: str, value_type: type) -> Union[str, int]: + prompt: Union[IntPrompt, Prompt] = ( + IntPrompt() if value_type is int else Prompt() + ) return prompt.ask(question, console=console) -def additional_config(cogs: str | list = "**"): +def additional_config(cogs: Union[str, list] = "**"): """Asking for additional configs in cogs. Returns