Skip to content

Commit

Permalink
Merge pull request #51 from ungdev/ue-channels
Browse files Browse the repository at this point in the history
implement more ue channel commands and refactor ue into a service
  • Loading branch information
Zalk0 authored Sep 10, 2024
2 parents f8a2246 + 7bd5e2d commit 6ca2b82
Show file tree
Hide file tree
Showing 19 changed files with 374 additions and 98 deletions.
4 changes: 0 additions & 4 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
version: "3.8"

services:
etuutt_bot:
build: .
Expand All @@ -9,8 +7,6 @@ services:
- "3000:3000"
volumes:
- data:/usr/src/etuutt/data
- logs:/usr/src/etuutt/logs

volumes:
data:
logs:
2 changes: 1 addition & 1 deletion docs/reference/commands/admin.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
::: etuutt_bot.commands.admin
options:
members:
- Admin
- AdminCog
2 changes: 1 addition & 1 deletion docs/reference/commands/misc.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
::: etuutt_bot.commands.misc
options:
members:
- Misc
- MiscCog
2 changes: 1 addition & 1 deletion docs/reference/commands/role.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
::: etuutt_bot.commands.role
options:
members:
- Role
- RoleCog
4 changes: 4 additions & 0 deletions docs/reference/commands/ue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
::: etuutt_bot.commands.ue
options:
members:
- UeCog
5 changes: 5 additions & 0 deletions docs/reference/services/role.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
::: etuutt_bot.services.role
options:
members:
- MergeStrategy
- RoleService
4 changes: 4 additions & 0 deletions docs/reference/services/ue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
::: etuutt_bot.services.ue
options:
members:
- UeService
2 changes: 1 addition & 1 deletion docs/tutorials/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -362,4 +362,4 @@ Rendez-vous dans le salon que vous avez configuré comme étant celui
dédié à l'administration du serveur.
Le bot devrait y avoir posté un message pour signaler sa mise en ligne.

Si ça ne marche pas, vous pouvez trouver les logs dans le fichier `logs/log`.
Si ça ne marche pas, vous pouvez trouver les logs dans le fichier `data/logs/log`.
4 changes: 2 additions & 2 deletions etuutt_bot/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ async def main():
bot = EtuUTTBot()

# Setup the logging
Path("logs").mkdir(exist_ok=True)
Path("data", "logs").mkdir(exist_ok=True)
handler = handlers.RotatingFileHandler(
filename=Path("logs", "log"),
filename=Path("data", "logs", "log"),
maxBytes=10485760, # 10Mo
backupCount=5,
encoding="utf-8",
Expand Down
5 changes: 3 additions & 2 deletions etuutt_bot/commands/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@


class AdminCog(commands.Cog):
"""Commandes pour les administrateurs du bot."""

def __init__(self, bot: EtuUTTBot) -> None:
self.bot = bot

# Add command to sync slash commands for team members and owner of the bot
@commands.is_owner()
@commands.command(name="sync")
async def sync_tree(self, ctx: commands.Context[EtuUTTBot]):
"""Synchronise les commandes slash."""
try:
await self.bot.tree.sync()
for guild in self.bot.guilds:
Expand All @@ -27,7 +29,6 @@ async def sync_tree(self, ctx: commands.Context[EtuUTTBot]):
await ctx.reply(
f"Il y a eu une erreur lors de la synchronisation des commandes slash\n{e}"
)
return

@commands.Cog.listener()
async def on_command_error(
Expand Down
76 changes: 11 additions & 65 deletions etuutt_bot/commands/role.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from typing import TYPE_CHECKING

import discord
from discord import CategoryChannel, Interaction, app_commands
from discord import Interaction, app_commands
from discord.ext import commands

from etuutt_bot.services.channel import ChannelService
Expand All @@ -16,8 +16,8 @@


@app_commands.default_permissions(administrator=True)
class RoleCog(commands.GroupCog, name="role"):
"""Commandes liées à la gestion des rôles (et des salons associés)"""
class RoleCog(commands.GroupCog, group_name="role"):
"""Commandes liées à la gestion des rôles"""

def __init__(self, bot: EtuUTTBot) -> None:
self.bot = bot
Expand All @@ -31,7 +31,7 @@ async def get_roles_with_framed_number_of_members(
"""Affiche les rôles ayant plus de nb_min et moins de nb_max personnes dedans.
Args:
interaction
interaction:
nb_min: Le nombre de personnes minimum ayant le rôle (par défaut : 0)
nb_max: Le nombre de personnes maximum ayant le rôle (par défaut : 1)
"""
Expand Down Expand Up @@ -100,13 +100,12 @@ async def get_duplicates(
await interaction.response.defer(thinking=True)
duplicates = self.role_service.get_duplicates(case_sensitive=case_sensitive)
if not duplicates:
await interaction.followup.send("Aucun rôle dupliqué :thumbs_up:")
await interaction.followup.send("Aucun rôle dupliqué \N{THUMBS UP SIGN}")
return
message = f"{len(duplicates)} rôles dupliqués :\n"
for duplicate in duplicates:
message += f"- **{duplicate[0].name}**. Nombre de membres avec ce rôle : "
message += ", ".join(str(len(role.members)) for role in duplicate)
message += "\n"
message += "\n".join(
f"- **{d[0].name}**. Nombre de duplications : {len(d)}" for d in duplicates
)
await interaction.followup.send(message)

@app_commands.command(name="merge")
Expand All @@ -125,6 +124,7 @@ async def merge_roles(
role: Le rôle qu'on veut fusionner avec tous ceux qui ont le même nom.
case_sensitive: La casse est-elle prise en compte dans la recherche des duplications ?
merge_strategy: La manière de fusionner les permissions associées aux rôles fusionnées.
- Si `Union`, toutes les permissions sont gardées.
- Si `Intersection`, seules les permissions communes à tous les rôles sont gardées.
- Si `Clear`, aucune permission n'est gardée.
Expand All @@ -134,65 +134,11 @@ async def merge_roles(
nb_duplicates = len(duplicates)
if nb_duplicates < 2:
await interaction.followup.send(
"Ce rôle n'est pas dupliqué :thinking:\n"
"Ce rôle n'est pas dupliqué \N{THINKING FACE}\n"
"Utilisez la commande `get_duplicates` pour voir quels rôles sont dupliqués"
)
return
await self.role_service.merge(duplicates, merge_perms_strategy=merge_strategy)
await interaction.followup.send(
f"Commande finie. {nb_duplicates} rôles fusionnés. :thumbs_up:"
)

# define sub command group to manage channels
channel = app_commands.Group(
name="channel",
description="Commandes liées à la gestion des salons associés aux rôles",
)

@app_commands.checks.bot_has_permissions(manage_channels=True)
@channel.command(name="addall")
@app_commands.describe(category="La catégorie dans laquelle créer les salons")
async def add_ues(self, interaction: Interaction[EtuUTTBot], category: CategoryChannel):
""" "Crée les salons textuels pour toutes les UEs d'une catégorie dont le rôle existe.
Args:
interaction:
category: La catégorie dans laquelle on veut créer les salons d'UE
"""
await interaction.response.defer(thinking=True)
settings_cat = next(
(cat for cat in self.bot.settings.categories if cat.id == category.id), None
f"Commande finie. {nb_duplicates} rôles fusionnés. \N{THUMBS UP SIGN}"
)
if settings_cat is None:
await interaction.followup.send("Cette catégorie ne comporte aucune UE.")
return
role_names = {ue.lower() for ue in settings_cat.ues}
msg = ""

# Ensure that channels don't exist yet in order not to overwrite them
existing_channels = [c for c in category.text_channels if c.name in role_names]
if len(existing_channels) > 0:
msg += "\n## \N{SLEEPING SYMBOL} Les salons suivants existent déjà :\n"
msg += "\n".join(f"- {c.name}" for c in existing_channels)
role_names -= {c.name.lower() for c in existing_channels}

# Keep only roles that actually exist
existing_roles = [r for r in interaction.guild.roles if r.name.lower() in role_names]
if len(existing_roles) != len(role_names):
missing = role_names - {r.name for r in existing_roles}
msg += (
"\n## \N{WHITE QUESTION MARK ORNAMENT} "
"Les rôles suivants manquent sur le serveur :\n"
)
msg += "\n".join(f"- {r}" for r in missing)

if len(existing_roles) > 0:
elected = category.guild.get_role(settings_cat.elected_role)
msg += "\n## Salons textuels créés :\n"
for role in existing_roles:
channel = await self.channel_service.create_ue_channel(category, role, elected)
msg += f"\n- {channel.name}"

for chunk in split_msg(msg):
await interaction.channel.send(chunk)
await interaction.followup.send("\N{WHITE HEAVY CHECK MARK} La commande est terminée :")
145 changes: 145 additions & 0 deletions etuutt_bot/commands/ue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
from __future__ import annotations

from typing import TYPE_CHECKING

from discord import CategoryChannel, Interaction, TextChannel, app_commands
from discord.ext import commands

from etuutt_bot.services.ue import (
AlreadyExistsError,
CategoryMissingError,
MissingConfigurationError,
UeService,
)
from etuutt_bot.utils.message import split_msg

if TYPE_CHECKING:
from etuutt_bot.bot import EtuUTTBot


@app_commands.default_permissions(administrator=True)
class UeCog(commands.GroupCog, group_name="ues"):
"""Commandes liées à la gestion des UEs."""

def __init__(self, bot: EtuUTTBot):
self.bot = bot
self.ue_service = UeService(bot)

@app_commands.checks.bot_has_permissions(manage_channels=True, manage_roles=True)
@app_commands.command(name="add_all")
async def add_category(self, interaction: Interaction[EtuUTTBot], category: CategoryChannel):
"""Crée les salons textuels et les rôles pour toutes les UEs d'une catégorie.
Args:
interaction:
category: La catégorie dans laquelle on veut créer les salons d'UE
"""
await interaction.response.defer(thinking=True)
settings_cat = next(
(cat for cat in self.bot.settings.categories if cat.id == category.id), None
)
if settings_cat is None:
await interaction.followup.send(
f"La catégorie {category.name} n'est pas destinée à accueillir des salons d'UE"
)
return
ues_names = {ue.lower() for ue in settings_cat.ues}
msg = ""

# Ensure that channels don't exist yet in order not to overwrite them
to_create = self.ue_service.get_missing_channels(category)
if len(to_create) < len(settings_cat.ues):
msg += "\n## \N{SLEEPING SYMBOL} Les salons suivants existent déjà :\n"
msg += "\n".join(f"- {c}" for c in ues_names - to_create)

if len(to_create) > 0:
msg += "\n## Salons textuels créés :\n"
for channel_name in to_create:
channel, role = await self.ue_service.create_channel(channel_name)
await channel.send(
f"{role.mention} votre salon vient d'être créé \N{WAVING HAND SIGN}"
)
msg += f"\n- {channel.name}"

for chunk in split_msg(msg):
await interaction.channel.send(chunk)
await interaction.followup.send("\N{WHITE HEAVY CHECK MARK} La commande est terminée :")

@app_commands.checks.bot_has_permissions(manage_channels=True, manage_roles=True)
@app_commands.command(name="add")
async def add_one(self, interaction: Interaction[EtuUTTBot], ue: str):
"""Crée le salon correspondant à l'UE donnée.
Args:
interaction:
ue: le nom de l'UE à créer
"""
await interaction.response.defer(thinking=True)
try:
channel, role = await self.ue_service.create_channel(ue)
except AlreadyExistsError:
await interaction.followup.send("Ce salon existe déjà.")
return
except MissingConfigurationError:
await interaction.followup.send(
"Cette UE n'a pas été trouvée dans la configuration du bot.\n"
"Vous avez peut-être fait une faute de frappe, "
"ou bien la configuration du bot n'est pas à jour avec le catalogue des UEs."
)
return
except CategoryMissingError:
await interaction.followup.send(
"La catégorie pour cette UE n'existe pas "
"ou l'ID est incorrect dans la configuration."
)
return
await channel.send(f"{role.mention} votre salon vient d'être créé \N{WAVING HAND SIGN}")
await interaction.followup.send("Salon créé \N{THUMBS UP SIGN}")

@app_commands.checks.bot_has_permissions(manage_channels=True, manage_roles=True)
@app_commands.command(name="remove_all")
async def delete_many(
self,
interaction: Interaction[EtuUTTBot],
category: CategoryChannel,
delete_roles: bool = True,
):
"""Supprime les salons de toutes les UEs de la catégorie donnée.
Args:
category: la catégorie dans laquelle supprimer tous les salons d'UE
delete_roles: si `True`, les rôles associés aux UEs sont également supprimés
"""
await interaction.response.defer(thinking=True)
nb_deleted = await self.ue_service.delete_all_channels(category, delete_roles=delete_roles)
await interaction.followup.send(f"Commande finie. {nb_deleted} salons supprimés.")

@app_commands.checks.bot_has_permissions(manage_channels=True, manage_roles=True)
@app_commands.command(name="remove")
async def delete_one(
self,
interaction: Interaction[EtuUTTBot],
channel: TextChannel,
delete_role: bool = True,
):
"""Supprime le salon d'UE donné.
Args:
channel: le salon d'UE à supprimer
delete_role: si `True`, le rôle associé à l'UE sont également supprimés
"""
await interaction.response.defer(thinking=True)
await self.ue_service.delete_channel(channel, delete_role=delete_role)
await interaction.followup.send("Commande finie. Salon supprimé.")

@add_one.autocomplete("ue")
async def autocomplete_missing_ue(self, interaction: Interaction[EtuUTTBot], current: str):
"""Autocomplétion pour les ues dont le salon n'existe pas.
Comme le but de cette autocomplétion est de suggérer
des noms de salons qui n'ont *pas encore* été créés et pour
lesquels il n'existe *pas encore* forcément de rôle,
on ne peut pas utiliser les fonctions d'autocomplétion par défaut.
"""
ues = self.ue_service.get_missing_channels()
return [app_commands.Choice(name=ue, value=ue) for ue in ues if current.lower() in ue]
13 changes: 10 additions & 3 deletions etuutt_bot/config.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
from enum import Enum
from typing import Annotated, Literal

from pydantic import BaseModel, HttpUrl, SecretStr, UrlConstraints, field_validator
from pydantic import (
BaseModel,
HttpUrl,
SecretStr,
StringConstraints,
UrlConstraints,
field_validator,
)
from pydantic_settings import (
BaseSettings,
PydanticBaseSettingsSource,
Expand Down Expand Up @@ -62,10 +69,10 @@ class GuildConfig(BaseModel):


class CategoryConfig(BaseModel):
name: str
name: Annotated[str, StringConstraints(to_upper=True)] # type: ignore
id: ChannelId
elected_role: RoleId
ues: list[str]
ues: list[Annotated[str, StringConstraints(to_upper=True)]] # type: ignore


class ApiConfig(BaseModel):
Expand Down
Loading

0 comments on commit 6ca2b82

Please sign in to comment.