diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 3f39296..6ad5c5f 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.11"] + python-version: ["3.13"] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} @@ -18,7 +18,8 @@ jobs: run: | python -m pip install --upgrade pip pip install -r ./requirements.txt - cp base_configs.py configs.py + pip install pylint + mv base_configs.py configs.py - name: Analysing the code with pylint run: | pylint $(git ls-files '*.py') diff --git a/.pylintrc b/.pylintrc index 3890a91..0746fdb 100644 --- a/.pylintrc +++ b/.pylintrc @@ -4,7 +4,7 @@ max-line-length=120 min-public-methods=0 [MESSAGES CONTROL] -# Disable the message "C0114: Missing module docstring" +# Disabled the message "C0114: Missing module docstring" # Disabled the message "W0511: fixme" # Disabled the message "R0913: Too many arguments" # Disabled the message "W0212: Access to a protected member _ of a client class" diff --git a/.vscode/settings.json b/.vscode/settings.json index 4612ab7..2b23e74 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,12 @@ { - "python.analysis.importFormat": "absolute", "editor.defaultFormatter": "ms-python.black-formatter", + "files.trimTrailingWhitespace": true, "editor.formatOnSave": true, - "black-formatter.args": ["--line-length", "120", "--target-version", "py311"] + + "isort.args": ["--profile", "black"], + "python.analysis.importFormat": "absolute", + "editor.codeActionsOnSave": { + "source.organizeImports": "always", + "source.fixAll": "always" + } } diff --git a/README.md b/README.md index a3ef573..27f5b94 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ A discord bot for managing your server ### Requirements -- Python 3.10+ -- MongoDB 5.0+ +- Python 3.13+ +- MongoDB 8.0+ - Dependencies (`pip3 install -r requirements.txt`) ### Setting up the bot @@ -46,4 +46,4 @@ mongo> db.createUser({user: "DB_USER", pwd: "DB_PWD", roles: [{role: "root", db: ### Using python -> `python3.10 run.py` +> `python3 run.py` diff --git a/bot/botcommands/__init__.py b/bot/botcommands/__init__.py index 4abeb39..c2e1a35 100644 --- a/bot/botcommands/__init__.py +++ b/bot/botcommands/__init__.py @@ -3,3 +3,4 @@ from .bot_configs import BotConfigsCog from .member import MemberCog from .moderation import ModerationCog +from .reaction_role import ReactionRoleCog diff --git a/bot/botcommands/member.py b/bot/botcommands/member.py index bfcd8d3..9c1a430 100644 --- a/bot/botcommands/member.py +++ b/bot/botcommands/member.py @@ -8,8 +8,7 @@ from bot import tickets, util, welcome from bot.botcommands.utils.validators import has_at_least_role from bot.db.models.user import AdeptMember -from bot.db.services.user_service import UserService -from bot.db.services.reaction_role_service import ReactionRoleService +from bot.db.services import ReactionRoleService, UserService from bot.interactions import TicketOpeningInteraction from bot.interactions import ticket as ticket_interactions from bot.util import AdeptBotException @@ -124,40 +123,3 @@ async def count_students_in_computer_science(self, ctx: Context): + f" - ``{decbac_students_count}`` en **DEC-BAC**\n\n" + f" - ``{former_student_count}`` anciens étudiants" ) - - @commands.command() - @has_at_least_role(configs.ADMIN_ROLE) - async def addreactionrole(self, ctx: Context, message_id: int, emoji: str, role_id: int): - """ - Cette commande permet d'ajouter une réaction à un message et de la lier à un rôle. - - Utilisation: - !addreactionrole - """ - message = await ctx.fetch_message(message_id) - role = ctx.guild.get_role(role_id) - - if not message or not role: - raise AdeptBotException("Message ou rôle invalide!") - - await message.add_reaction(emoji) - await self.reaction_role_service.add_reaction_role(message_id, emoji, role_id) - await ctx.send(f"Réaction {emoji} ajoutée au message {message_id} et liée au rôle {role.name}.") - - @commands.command() - @has_at_least_role(configs.ADMIN_ROLE) - async def removereactionrole(self, ctx: Context, message_id: int, emoji: str): - """ - Cette commande permet de retirer une réaction d'un message et de supprimer le lien avec un rôle. - - Utilisation: - !removereactionrole - """ - message = await ctx.fetch_message(message_id) - - if not message: - raise AdeptBotException("Message invalide!") - - await message.clear_reaction(emoji) - await self.reaction_role_service.remove_reaction_role(message_id, emoji) - await ctx.send(f"Réaction {emoji} retirée du message {message_id}.") diff --git a/bot/botcommands/reaction_role.py b/bot/botcommands/reaction_role.py new file mode 100644 index 0000000..ccdc91a --- /dev/null +++ b/bot/botcommands/reaction_role.py @@ -0,0 +1,79 @@ +"""This module contains the commands related to the members of the server.""" + +import discord +from discord.ext import commands +from discord.ext.commands.context import Context + +import configs +from bot.botcommands.utils.validators import has_at_least_role +from bot.db.services.reaction_role_service import ReactionRoleService +from bot.util import AdeptBotException + + +class ReactionRoleCog(commands.Cog): + """This class contains the commands related to the members of the server.""" + + def __init__(self, bot: discord.Client) -> None: + self.bot = bot + self.reaction_role_service = ReactionRoleService() + + @commands.command() + @has_at_least_role(configs.ADMIN_ROLE) + async def addreactionrole(self, ctx: Context, message_id: int, emoji: str, role: discord.Role): + """ + Cette commande permet d'ajouter une réaction à un message et de la lier à un rôle. + + Utilisation: + !addreactionrole + """ + message = await ctx.fetch_message(message_id) + + if not message or not role: + raise AdeptBotException("Message ou rôle invalide!") + + await message.add_reaction(emoji) + await self.reaction_role_service.add_reaction_role(message_id, emoji, role.id) + await ctx.send(f"Réaction {emoji} ajoutée au message {message.jump_url} et liée au rôle {role.name}.") + + @commands.command() + @has_at_least_role(configs.ADMIN_ROLE) + async def removereactionrole(self, ctx: Context, message_id: int, emoji: str): + """ + Cette commande permet de retirer une réaction d'un message et de supprimer le lien avec un rôle. + + Utilisation: + !removereactionrole + """ + message = await ctx.fetch_message(message_id) + + if not message: + raise AdeptBotException("Message invalide!") + + await message.clear_reaction(emoji) + await self.reaction_role_service.remove_reaction_role(message_id, emoji) + await ctx.send(f"Réaction {emoji} retirée du message {message.jump_url}.") + + @commands.Cog.listener() + async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent): + """This event is called when a reaction is added to a message.""" + if payload.member.bot: + return + + reaction_role = await self.reaction_role_service.get_reaction_role(payload.message_id, str(payload.emoji)) + if reaction_role: + guild = self.bot.get_guild(payload.guild_id) + role = guild.get_role(reaction_role.role_id) + await payload.member.add_roles(role) + + @commands.Cog.listener() + async def on_raw_reaction_remove(self, payload: discord.RawReactionActionEvent): + """This event is called when a reaction is removed from a message.""" + guild = self.bot.get_guild(payload.guild_id) + member = guild.get_member(payload.user_id) + if member.bot: + return + + reaction_role = await self.reaction_role_service.get_reaction_role(payload.message_id, str(payload.emoji)) + if reaction_role: + role = guild.get_role(reaction_role.role_id) + await member.remove_roles(role) diff --git a/bot/db/models/__init__.py b/bot/db/models/__init__.py index b0aaa18..0fb429e 100644 --- a/bot/db/models/__init__.py +++ b/bot/db/models/__init__.py @@ -2,4 +2,5 @@ from .bot_configs import GlobalConfig, SpamConfigs from .entity import Entity +from .reaction_role import ReactionRole from .user import AdeptMember diff --git a/bot/db/models/entity.py b/bot/db/models/entity.py index baba2b1..a8f5d7f 100644 --- a/bot/db/models/entity.py +++ b/bot/db/models/entity.py @@ -1,6 +1,6 @@ """Base entity class for all models""" -from datetime import datetime +from datetime import datetime, timezone class Entity: @@ -19,7 +19,12 @@ class Entity: __slots__ = ("_id", "created_at", "updated_at") - def __init__(self, _id: int, created_at: datetime = datetime.utcnow(), updated_at: datetime = datetime.utcnow()): + def __init__( + self, + _id: int, + created_at: datetime = datetime.now(timezone.utc), + updated_at: datetime = datetime.now(timezone.utc), + ): self._id = _id self.created_at = created_at self.updated_at = updated_at diff --git a/bot/db/services/__init__.py b/bot/db/services/__init__.py index 4bfe99a..58e8534 100644 --- a/bot/db/services/__init__.py +++ b/bot/db/services/__init__.py @@ -2,4 +2,5 @@ from .base_service import BaseService from .configs_service import ConfigsService +from .reaction_role_service import ReactionRoleService from .user_service import UserService diff --git a/bot/db/services/reaction_role_service.py b/bot/db/services/reaction_role_service.py index 032e532..e8a223b 100644 --- a/bot/db/services/reaction_role_service.py +++ b/bot/db/services/reaction_role_service.py @@ -1,7 +1,7 @@ """Service class for ReactionRole model.""" -from bot.db.services.base_service import BaseService from bot.db.models.reaction_role import ReactionRole +from bot.db.services.base_service import BaseService class ReactionRoleService(BaseService): @@ -36,7 +36,7 @@ async def add_reaction_role(self, message_id: int, emoji: str, role_id: int): The ID of the role to assign. """ reaction_role = ReactionRole(None, message_id, emoji, role_id) - await self.insert_one(reaction_role.__getstate__()) + self.insert_one(reaction_role.__getstate__()) async def remove_reaction_role(self, message_id: int, emoji: str): """ @@ -49,9 +49,9 @@ async def remove_reaction_role(self, message_id: int, emoji: str): `emoji` : str The emoji used for the reaction. """ - await self.delete_one({"message_id": message_id, "emoji": emoji}) + self.delete_one({"message_id": message_id, "emoji": emoji}) - async def get_reaction_role(self, message_id: int, emoji: str) -> ReactionRole: + async def get_reaction_role(self, message_id: int, emoji: str) -> ReactionRole | None: """ Get a reaction role from the database. @@ -67,7 +67,7 @@ async def get_reaction_role(self, message_id: int, emoji: str) -> ReactionRole: ReactionRole The reaction role. """ - result = await self.find_one({"message_id": message_id, "emoji": emoji}) + result = self.find_one({"message_id": message_id, "emoji": emoji}) if result: return ReactionRole(result["_id"], result["message_id"], result["emoji"], result["role_id"]) return None diff --git a/bot/management/logging.py b/bot/management/logging.py index 8bbb277..af83f2c 100644 --- a/bot/management/logging.py +++ b/bot/management/logging.py @@ -11,8 +11,9 @@ class LoggingCog(commands.Cog): """This class contains the events related to logging.""" - def __init__(self): + def __init__(self, bot: discord.Client): self.reaction_role_service = ReactionRoleService() + self.bot = bot @commands.Cog.listener() async def on_message_edit(self, before: discord.Message, after: discord.Message): @@ -139,28 +140,3 @@ async def on_guild_channel_update(self, before: discord.abc.GuildChannel, after: embed.description = f"**``#{before}`` a été renommé pour {after.mention}**" await util.say(configs.LOGS_CHANNEL, embed=embed) - - @commands.Cog.listener() - async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent): - """This event is called when a reaction is added to a message.""" - if payload.member.bot: - return - - reaction_role = await self.reaction_role_service.get_reaction_role(payload.message_id, str(payload.emoji)) - if reaction_role: - guild = self.bot.get_guild(payload.guild_id) - role = guild.get_role(reaction_role.role_id) - await payload.member.add_roles(role) - - @commands.Cog.listener() - async def on_raw_reaction_remove(self, payload: discord.RawReactionActionEvent): - """This event is called when a reaction is removed from a message.""" - guild = self.bot.get_guild(payload.guild_id) - member = guild.get_member(payload.user_id) - if member.bot: - return - - reaction_role = await self.reaction_role_service.get_reaction_role(payload.message_id, str(payload.emoji)) - if reaction_role: - role = guild.get_role(reaction_role.role_id) - await member.remove_roles(role) diff --git a/bot/management/welcome.py b/bot/management/welcome.py index 4093d61..f28793a 100644 --- a/bot/management/welcome.py +++ b/bot/management/welcome.py @@ -4,15 +4,16 @@ from discord.ext import commands import configs -from bot import util, welcome +from bot import welcome from bot.db.services import UserService class WelcomeCog(commands.Cog): """This class contains the events related to welcome.""" - def __init__(self) -> None: + def __init__(self, bot: discord.Client) -> None: self.user_service = UserService() + self.bot = bot @commands.command() @commands.guild_only() @@ -38,7 +39,7 @@ async def on_member_join(self, member: discord.Member): if adept_member: return await welcome.process_welcome_result(member, adept_member) - await util.say(configs.WELCOME_CHANNEL, configs.WELCOME_SERVER.format(name=member.mention)) + await self.bot.say(configs.WELCOME_CHANNEL, configs.WELCOME_SERVER.format(name=member.mention)) result = await welcome.walk_through_welcome(member) if not result: return diff --git a/bot/tasks.py b/bot/tasks.py index 551e001..fcae33f 100644 --- a/bot/tasks.py +++ b/bot/tasks.py @@ -91,7 +91,7 @@ async def process_mutes(): process_mutes.stop() -def load_tasks(): +async def load_tasks(bot: discord.Client): """ Loads all tasks from the database. @@ -102,7 +102,7 @@ def load_tasks(): for task in to_process: # Just to make the code more readable - member = util.get_member(task["guild"], task["member"]) + member = bot.get_guild(task["guild"]).get_member(task["member"]) end_date = datetime.datetime.strptime(task["end_date"], "%Y-%m-%d %H:%M:%S.%f") TASK_LIST.append(Task(member, end_date, task["type"])) diff --git a/bot/util.py b/bot/util.py index bab2831..6900267 100644 --- a/bot/util.py +++ b/bot/util.py @@ -8,6 +8,7 @@ import configs from bot.strikes import Strike +from bot.db.services import ReactionRoleService CLIENT: discord.Client = None @@ -34,32 +35,6 @@ def __init__(self, message: str) -> None: super().__init__(self.message) -def get_member(guild_id: int, member_id: int): - """ - Get a member from a guild. - - Parameters - ---------- - `guild_id` : int - The id of the guild. - `member_id` : int - The id of the member. - """ - return CLIENT.get_guild(guild_id).get_member(member_id) - - -def get_guild(guild_id: int): - """ - Get a guild. - - Parameters - ---------- - `guild_id` : int - The id of the guild. - """ - return CLIENT.get_guild(guild_id) - - def get_case_number(): """Get the next case number.""" raise NotImplementedError() diff --git a/dockerfile b/dockerfile index 7f003a8..2822722 100644 --- a/dockerfile +++ b/dockerfile @@ -1,4 +1,4 @@ -FROM python:3.12-slim +FROM python:3.13-slim WORKDIR /usr/src/app diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..fcd5b83 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +[tool.isort] +profile = "black" + +[tool.black] +line-length = 120 +target-version = ["py313"] diff --git a/requirements.txt b/requirements.txt index b2ed48c..79ce488 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,4 @@ discord.py[speed]<3.0.0 jsonpickle pymongo[srv]<5.0.0 - -# Linter -pylint -# Formatter -black +audioop-lts diff --git a/run.py b/run.py index 1f3a112..566ff97 100755 --- a/run.py +++ b/run.py @@ -17,7 +17,7 @@ import configs from bot import tasks, util -from bot.botcommands import BotConfigsCog, MemberCog, ModerationCog +from bot.botcommands import BotConfigsCog, MemberCog, ModerationCog, ReactionRoleCog from bot.interactions import TicketCloseInteraction, TicketOpeningInteraction from bot.interactions.errors import NoReplyException from bot.management import LoggingCog, StrikesCog, WelcomeCog @@ -32,20 +32,21 @@ def __init__(self, prefix: str, intents: discord.Intents): async def on_ready(self): """Called when the bot is ready.""" util.logger.info( - "\nLogged in with account @%s ID:%s \n------------------------------------\n", self.user.name, self.user.id + "\nLogged in with account @%s ID:%s \n------------------------------------", self.user.name, self.user.id ) await self.change_presence(activity=discord.Activity(name="for bad boys!", type=discord.ActivityType.watching)) - tasks.load_tasks() + await tasks.load_tasks(self) async def setup_hook(self) -> None: # Register cogs await self.add_cog(BotConfigsCog()) - await self.add_cog(LoggingCog()) + await self.add_cog(LoggingCog(self)) await self.add_cog(MemberCog()) await self.add_cog(ModerationCog()) await self.add_cog(StrikesCog()) - await self.add_cog(WelcomeCog()) + await self.add_cog(WelcomeCog(self)) + await self.add_cog(ReactionRoleCog(self)) # Register persistent views self.add_view(TicketOpeningInteraction()) @@ -105,9 +106,9 @@ async def on_command_error(self, ctx: commands.Context, exception: commands.erro if isinstance(ctx, CommandNotFound): # We don't care - pass + return - elif isinstance(exception, NoPrivateMessage): + if isinstance(exception, NoPrivateMessage): await ctx.send("Cette commande ne peut pas être utilisée en message privé.") elif isinstance(exception, UserNotFound): @@ -122,7 +123,7 @@ async def on_command_error(self, ctx: commands.Context, exception: commands.erro elif isinstance(exception, BadArgument): await ctx.send(f"Argument invalide: {exception.param.name}") - elif isinstance(exception, NoReplyException): + elif isinstance(exception, (NoReplyException)): # , InsufficientPermissionsError)): await exception.channel.send(exception.message) elif isinstance(exception, discord.Forbidden):