update(core): change to >=3.8

This commit is contained in:
Romain J 2021-05-16 23:21:27 +02:00
parent b75e5b8a8e
commit ba53228d44
24 changed files with 274 additions and 53 deletions

3
.gitignore vendored
View File

@ -33,6 +33,9 @@ __pycache__/
__pypackages__/
venv
venv3.8
venv3.9
venv3.11
dist
build
*.egg

View File

@ -7,6 +7,9 @@
<excludeFolder url="file://$MODULE_DIR$/venv" />
<excludeFolder url="file://$MODULE_DIR$/data" />
<excludeFolder url="file://$MODULE_DIR$/.mypy_cache" />
<excludeFolder url="file://$MODULE_DIR$/venv3.8" />
<excludeFolder url="file://$MODULE_DIR$/venv3.9" />
<excludeFolder url="file://$MODULE_DIR$/venv3.11" />
</content>
<orderEntry type="jdk" jdkName="Python 3.10 (tuxbot_bot)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />

View File

@ -35,7 +35,7 @@ update-all:
.PHONY: dev
dev: style update
tuxbot
$(VIRTUAL_ENV)/bin/tuxbot
# Docker
.PHONY: docker

View File

@ -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

View File

@ -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

View File

@ -1,5 +1,5 @@
from setuptools import setup
setup(
python_requires=">=3.10",
python_requires=">=3.8",
)

View File

@ -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]

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -19,3 +19,7 @@ class NonMessageException(ModException):
class NonBotMessageException(ModException):
pass
class ReasonTooLongException(ModException):
pass

View File

@ -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

View File

@ -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}")

View File

@ -1,2 +1,3 @@
from .rules import *
from .warns import *
from .mutes import *

View File

@ -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"<MuteRole id={self.id} "
f"server_id={self.server_id} "
f"role_id={self.role_id}>"
)
__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"<Mute id={self.id} "
f"server_id={self.server_id} "
f"author_id={self.author_id} "
f"reason='{self.reason}' "
f"member_id={self.member_id} "
f"created_at={self.created_at} "
f"expire_at={self.expire_at}>"
)
__repr__ = __str__

View File

@ -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

View File

@ -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))

View File

@ -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)

View File

@ -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()

View File

@ -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
-------

View File

@ -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

View File

@ -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:

View File

@ -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