diff --git a/.idea/dictionaries/romain.xml b/.idea/dictionaries/romain.xml index 711748e..ed2c692 100644 --- a/.idea/dictionaries/romain.xml +++ b/.idea/dictionaries/romain.xml @@ -42,6 +42,8 @@ pred pydig pylint + regle + regles releaselevel rprint skipcq diff --git a/tuxbot/cogs/Mod/functions/converters.py b/tuxbot/cogs/Mod/functions/converters.py new file mode 100644 index 0000000..6dea812 --- /dev/null +++ b/tuxbot/cogs/Mod/functions/converters.py @@ -0,0 +1,34 @@ +from discord.ext import commands +from discord.ext.commands import Context + +from tuxbot.cogs.Mod.functions.exceptions import ( + RuleTooLongException, + UnknownRuleException, +) +from tuxbot.cogs.Mod.models import Rule + + +def _(x): + return x + + +class RuleIDConverter(commands.Converter): + async def convert(self, ctx: Context, argument: str): # skipcq: PYL-W0613 + arg = int(argument) + + rule_row = await Rule.get_or_none(server_id=ctx.guild.id, rule_id=arg) + + if not rule_row: + raise UnknownRuleException(_("Unknown rule")) + + return arg + + +class RuleConverter(commands.Converter): + async def convert(self, ctx: Context, argument: str): # skipcq: PYL-W0613 + if len(argument) > 300: + raise RuleTooLongException( + _("Rule 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 new file mode 100644 index 0000000..5f48419 --- /dev/null +++ b/tuxbot/cogs/Mod/functions/exceptions.py @@ -0,0 +1,13 @@ +from discord.ext import commands + + +class ModException(commands.BadArgument): + pass + + +class RuleTooLongException(ModException): + pass + + +class UnknownRuleException(ModException): + pass diff --git a/tuxbot/cogs/Mod/functions/utils.py b/tuxbot/cogs/Mod/functions/utils.py index 97cf551..7daa262 100644 --- a/tuxbot/cogs/Mod/functions/utils.py +++ b/tuxbot/cogs/Mod/functions/utils.py @@ -1,3 +1,4 @@ +from tuxbot.cogs.Mod.models.rules import Rule from tuxbot.core.config import set_for_key from tuxbot.core.config import Config @@ -5,7 +6,29 @@ from tuxbot.core.bot import Tux from tuxbot.core.utils.functions.extra import ContextPlus -async def save_lang(bot: Tux, ctx: ContextPlus, lang: str): - set_for_key( - bot.config.Servers, ctx.guild.id, Config.Server, locale=lang - ) +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]: + return await Rule.filter(server_id=guild_id).all() + + +def get_most_recent_server_rules(rules: list[Rule]) -> Rule: + return sorted(rules, key=lambda r: r.updated_at)[0] + + +def paginate_server_rules(rules: list[Rule]) -> list[str]: + body = [""] + + for rule in rules: + if len(body[-1] + format_rule(rule)) > 2000: + body.append(format_rule(rule) + "\n") + else: + body[-1] += format_rule(rule) + "\n" + + return body + + +def format_rule(rule: Rule) -> str: + return f"**{rule.rule_id}** - {rule.content}" diff --git a/tuxbot/cogs/Mod/mod.py b/tuxbot/cogs/Mod/mod.py index d2c3481..7a4cd02 100644 --- a/tuxbot/cogs/Mod/mod.py +++ b/tuxbot/cogs/Mod/mod.py @@ -1,9 +1,22 @@ import logging +from datetime import datetime import discord from discord.ext import commands -from tuxbot.cogs.Mod.functions.utils import save_lang +from tuxbot.cogs.Mod.functions.converters import RuleConverter, RuleIDConverter +from tuxbot.cogs.Mod.functions.exceptions import ( + RuleTooLongException, + UnknownRuleException, +) +from tuxbot.cogs.Mod.functions.utils import ( + save_lang, + get_server_rules, + format_rule, + get_most_recent_server_rules, + paginate_server_rules, +) +from tuxbot.cogs.Mod.models.rules import Rule from tuxbot.core.utils import checks from tuxbot.core.bot import Tux @@ -26,6 +39,16 @@ class Mod(commands.Cog): def __init__(self, bot: Tux): self.bot = bot + async def cog_command_error(self, ctx: ContextPlus, error): + if isinstance( + error, + ( + RuleTooLongException, + UnknownRuleException, + ), + ): + await ctx.send(_(str(error), ctx, self.bot.config)) + # ========================================================================= # ========================================================================= @@ -58,3 +81,123 @@ class Mod(commands.Cog): ) await ctx.send(embed=e) + + # ========================================================================= + + @group_extra( + name="rule", + aliases=["rules", "regle", "regles"], + deletable=False, + invoke_without_command=True, + ) + @commands.guild_only() + async def _rule( + self, + ctx: ContextPlus, + rule: RuleIDConverter, + members: commands.Greedy[discord.Member], + ): + rule_row = await Rule.get(server_id=ctx.guild.id, rule_id=rule) + + message = _( + "{}please read the following rule: \n{}", ctx, self.bot.config + ) + authors = "" + + for member in members: + if member in ctx.message.mentions: + authors += f"{member.name}#{member.discriminator}, " + else: + authors += f"{member.mention}, " + + await ctx.send(message.format(authors, format_rule(rule_row))) + + @_rule.command(name="list", aliases=["show", "all"]) + async def _rule_list(self, ctx: ContextPlus): + rules = await get_server_rules(ctx.guild.id) + + embed = discord.Embed( + title=_("Rules for {}", ctx, self.bot.config).format( + ctx.guild.name + ), + color=discord.Color.blue(), + ) + embed.set_footer( + text=_("Latest change: {}", ctx, self.bot.config).format( + get_most_recent_server_rules(rules).created_at + ) + ) + + pages = paginate_server_rules(rules) + + if len(pages) == 1: + embed.description = pages[0] + + await ctx.send(embed=embed) + else: + for i, page in enumerate(pages): + embed.title = _( + "Rules for {} ({}/{})", ctx, self.bot.config + ).format(ctx.guild.name, str(i + 1), str(len(pages))) + embed.description = page + + await ctx.send(embed=embed) + + @checks.is_admin() + @_rule.command(name="add") + async def _rule_add(self, ctx: ContextPlus, *, rule: RuleConverter): + rule_row = await Rule() + 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.content = str(rule) # type: ignore + + await rule_row.save() + + await ctx.send( + _("Following rule added: \n{}", ctx, self.bot.config).format( + format_rule(rule_row) + ) + ) + + @checks.is_admin() + @_rule.command(name="edit") + async def _rule_edit( + self, + ctx: ContextPlus, + rule: RuleIDConverter, + *, + content: RuleConverter, + ): + # noinspection PyTypeChecker + rule_row = await Rule.get(server_id=ctx.guild.id, rule_id=rule) + + rule_row.content = str(content) # type: ignore + rule_row.updated_at = datetime.now() # type: ignore + + await rule_row.save() + + await ctx.send( + _("Following rule updated: \n{}", ctx, self.bot.config).format( + format_rule(rule_row) + ) + ) + + @checks.is_admin() + @_rule.command(name="delete") + async def _rule_delete( + self, + ctx: ContextPlus, + rule: RuleIDConverter, + ): + # noinspection PyTypeChecker + rule_row = await Rule.get(server_id=ctx.guild.id, rule_id=rule) + + await rule_row.delete() + + await ctx.send( + _("Following rule deleted: \n{}", ctx, self.bot.config).format( + format_rule(rule_row) + ) + ) diff --git a/tuxbot/cogs/Mod/models/__init__.py b/tuxbot/cogs/Mod/models/__init__.py index bc90900..c5f0bb2 100644 --- a/tuxbot/cogs/Mod/models/__init__.py +++ b/tuxbot/cogs/Mod/models/__init__.py @@ -1 +1,2 @@ +from .rules import * from .warns import * diff --git a/tuxbot/cogs/Mod/models/rules.py b/tuxbot/cogs/Mod/models/rules.py new file mode 100644 index 0000000..8f737f8 --- /dev/null +++ b/tuxbot/cogs/Mod/models/rules.py @@ -0,0 +1,28 @@ +import tortoise +from tortoise import fields + + +class Rule(tortoise.Model): + id = fields.BigIntField(pk=True) + server_id = fields.BigIntField() + author_id = fields.BigIntField() + rule_id = fields.IntField() + content = fields.TextField(max_length=300) + created_at = fields.DatetimeField(auto_now_add=True) + updated_at = fields.DatetimeField(auto_now_add=True) + + class Meta: + table = "rules" + + def __str__(self): + return ( + f"" + ) + + __repr__ = __str__ diff --git a/tuxbot/core/utils/functions/extra.py b/tuxbot/core/utils/functions/extra.py index 78ce00e..e103779 100644 --- a/tuxbot/core/utils/functions/extra.py +++ b/tuxbot/core/utils/functions/extra.py @@ -15,17 +15,7 @@ IP_REPLACEMENT = "■" * random.randint(3, 15) class ContextPlus(commands.Context): # noinspection PyTypedDict async def send( - self, - content=None, - *, - tts=False, - embed=None, - file=None, - files=None, - delete_after=None, - nonce=None, - allowed_mentions=None, - deletable=True, + self, content=None, *, embed=None, deletable=True, **kwargs ): # i know *args and **kwargs but, i prefer work with same values from tuxbot.core.utils.functions.utils import ( replace_in_dict, @@ -45,7 +35,6 @@ class ContextPlus(commands.Context): ) if len(content) > 1800: - file = discord.File(BytesIO(content.encode()), "output.txt") content = "output too long..." if embed: e = embed.to_dict() @@ -107,14 +96,7 @@ class ContextPlus(commands.Context): hasattr(self.command, "deletable") and self.command.deletable ) and deletable: message = await super().send( - content=content, - tts=tts, - embed=embed, - file=file, - files=files, - delete_after=delete_after, - nonce=nonce, - allowed_mentions=allowed_mentions, + content=content, embed=embed, **kwargs ) await message.add_reaction("🗑") @@ -135,16 +117,7 @@ class ContextPlus(commands.Context): await message.delete() return message - return await super().send( - content=content, - tts=tts, - embed=embed, - file=file, - files=files, - delete_after=delete_after, - nonce=nonce, - allowed_mentions=allowed_mentions, - ) + return await super().send(content=content, embed=embed, **kwargs) @property def session(self) -> aiohttp.ClientSession: