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