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: