diff --git a/administrator/__init__.py b/administrator/__init__.py index b948753..5ef856f 100644 --- a/administrator/__init__.py +++ b/administrator/__init__.py @@ -1,10 +1,12 @@ from discord import Intents +from discord_slash import SlashCommand from administrator.config import config import db from discord.ext import commands bot = commands.Bot(command_prefix=config.get("prefix"), intents=Intents.all()) +slash = SlashCommand(bot, auto_register=True, auto_delete=True) import extensions diff --git a/administrator/check.py b/administrator/check.py index 3d1cb6a..7f1d078 100644 --- a/administrator/check.py +++ b/administrator/check.py @@ -1,4 +1,8 @@ +import functools + +from discord import Permissions from discord.ext import commands +from discord.ext.commands import NoPrivateMessage, NotOwner, MissingPermissions import db @@ -8,12 +12,62 @@ class ExtensionDisabled(commands.CheckFailure): def is_enabled(): - async def check(ctx: commands.Context): - if ctx.command.cog and ctx.guild: - s = db.Session() - es = s.query(db.ExtensionState).get((ctx.command.cog.qualified_name, ctx.guild.id)) - s.close() - if es and not es.state: - raise ExtensionDisabled() - return True - return commands.check(check) + def check(func): + @functools.wraps(func) + async def wrapped(*args): + ctx = args[1] + if ctx.guild: + s = db.Session() + es = s.query(db.ExtensionState).get((args[0].qualified_name, ctx.guild.id)) + s.close() + if es and not es.state: + return + # raise ExtensionDisabled() + return await func(*args) + return wrapped + return check + + +def is_owner(): + def check(func): + @functools.wraps(func) + async def wrapped(*args): + ctx = args[1] + if not await ctx._discord.is_owner(ctx.author): + raise NotOwner('You do not own this bot.') + return await func(*args) + return wrapped + return check + + +def guild_only(): + def check(func): + @functools.wraps(func) + async def wrapped(*args): + if args[1].guild is None: + raise NoPrivateMessage() + return await func(*args) + return wrapped + return check + + +def has_permissions(**perms): + invalid = set(perms) - set(Permissions.VALID_FLAGS) + if invalid: + raise TypeError('Invalid permission(s): %s' % (', '.join(invalid))) + + def check(func): + @functools.wraps(func) + async def wrapped(*args): + ctx = args[1] + ch = ctx.channel + permissions = ch.permissions_for(ctx.author) + + missing = [perm for perm, value in perms.items() if getattr(permissions, perm) != value] + + if not missing or permissions.administrator: + return await func(*args) + + raise MissingPermissions(missing) + return wrapped + return check diff --git a/administrator/utils.py b/administrator/utils.py index fba2bdf..6adb048 100644 --- a/administrator/utils.py +++ b/administrator/utils.py @@ -1,12 +1,32 @@ import re from datetime import timedelta +from discord import Message from discord.ext.commands import BadArgument from sqlalchemy.orm import Session import db +msg_url_re = re.compile(r"^https://.*discord.*\.com/channels/[0-9]+/([0-9+]+)/([0-9]+)$") + + +async def get_message_by_url(ctx, url: str) -> Message: + r = msg_url_re.fullmatch(url) + if not r: + raise BadArgument() + r = r.groups() + + c = ctx.guild.get_channel(int(r[0])) + if not c: + raise BadArgument() + + m = await c.fetch_message(int(r[1])) + if not m or m.is_system(): + raise BadArgument() + return m + + def time_pars(s: str) -> timedelta: match = re.fullmatch(r"(?:([0-9]+)W)*(?:([0-9]+)D)*(?:([0-9]+)H)*(?:([0-9]+)M)*(?:([0-9]+)S)*", s.upper().replace(" ", "").strip()) diff --git a/extensions/extension.py b/extensions/extension.py index 653cf90..f6635cb 100644 --- a/extensions/extension.py +++ b/extensions/extension.py @@ -2,9 +2,13 @@ from traceback import format_exc from discord.ext import commands from discord import Embed, Guild -from discord.ext.commands import MissingPermissions, BadArgument, CommandError +from discord.ext.commands import BadArgument +from discord_slash import cog_ext, SlashContext, SlashCommandOptionType +from discord_slash.utils import manage_commands import db +from administrator import slash +from administrator.check import has_permissions from administrator.logger import logger @@ -15,69 +19,66 @@ logger = logger.getChild(extension_name) class Extension(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot + slash.get_cog_commands(self) def description(self): return "Manage bot's extensions" - @commands.group("extension", pass_context=True) - async def extension(self, ctx: commands.Context): - if ctx.invoked_subcommand is None: - await ctx.invoke(self.extension_help) - - @extension.group("help", pass_context=True) - async def extension_help(self, ctx: commands.Context): - embed = Embed(title="Extension help") - for c, n, v in [[self.extension_list, "extension list", "List all enabled extensions"], - [self.extension_enable, "extension enable", "Enable an extensions"], - [self.extension_disable, "extension disable", "Disable an extensions"], - [self.extension_loaded, "extension loaded", "List all loaded extensions"], - [self.extension_load, "extension load ", "Load an extension"], - [self.extension_unload, "extension unload ", "Unload an extension"], - [self.extension_reload, "extension reload ", "Reload an extension"]]: - try: - if await c.can_run(ctx): - embed.add_field(name=n, value=v, inline=False) - except CommandError: - pass - - if not embed.fields: - raise MissingPermissions("") - await ctx.send(embed=embed) - - @extension.group("list", pass_context=True) - @commands.has_guild_permissions(administrator=True) - async def extension_list(self, ctx: commands.Context): + @cog_ext.cog_subcommand(base="extension", name="list", description="List all enabled extensions") + @has_permissions(administrator=True) + async def extension_list(self, ctx: SlashContext): s = db.Session() embed = Embed(title="Extensions list") for es in s.query(db.ExtensionState).filter(db.ExtensionState.guild_id == ctx.guild.id): embed.add_field(name=es.extension_name, value="Enable" if es.state else "Disable") - await ctx.send(embed=embed) + s.close() + await ctx.send(embeds=[embed]) - @extension.group("enable", pass_context=True) - @commands.has_guild_permissions(administrator=True) - async def extension_enable(self, ctx: commands.Context, name: str): + @cog_ext.cog_subcommand(base="extension", + name="enable", + description="Enable an extensions", + options=[manage_commands.create_option("extension", "The extension to enable", + SlashCommandOptionType.STRING, True)]) + @has_permissions(administrator=True) + async def extension_enable(self, ctx: SlashContext, name: str): s = db.Session() es = s.query(db.ExtensionState).get((name, ctx.guild.id)) - if not es or es.state: + if not es: raise BadArgument() - es.state = True - s.add(es) - s.commit() - s.close() - await ctx.message.add_reaction("\U0001f44d") + elif es.state: + message = "Extension already enabled" + else: + es.state = True + s.add(es) + s.commit() + s.close() + message = "\U0001f44d" + await ctx.send(content=message) - @extension.group("disable", pass_context=True) - @commands.has_guild_permissions(administrator=True) - async def extension_disable(self, ctx: commands.Context, name: str): + @cog_ext.cog_subcommand(base="extension", + name="disable", + description="Disable an extensions", + options=[manage_commands.create_option("extension", "The extension to disable", + SlashCommandOptionType.STRING, True)]) + @has_permissions(administrator=True) + async def extension_disable(self, ctx: SlashContext, name: str): s = db.Session() es = s.query(db.ExtensionState).get((name, ctx.guild.id)) - if not es or not es.state: + if not es: raise BadArgument() - es.state = False - s.add(es) - s.commit() - s.close() - await ctx.message.add_reaction("\U0001f44d") + elif not es.state: + message = "Extension already disabled" + else: + es.state = False + s.add(es) + s.commit() + s.close() + message = "\U0001f44d" + await ctx.send(content=message) + + @commands.group("extension", pass_context=True) + async def extension(self, ctx: commands.Context): + pass @extension.group("loaded", pass_context=True) @commands.is_owner() @@ -152,6 +153,9 @@ class Extension(commands.Cog): s.commit() s.close() + def cog_unload(self): + slash.remove_cog_commands(self) + def setup(bot): logger.info(f"Loading...") diff --git a/extensions/greetings.py b/extensions/greetings.py index bd169a1..08469c4 100644 --- a/extensions/greetings.py +++ b/extensions/greetings.py @@ -1,18 +1,14 @@ from discord.ext import commands from discord import Member, Embed, Forbidden -from discord.ext.commands import BadArgument +from discord_slash import cog_ext, SlashContext, SlashCommandOptionType +from discord_slash.utils import manage_commands -from administrator.check import is_enabled +from administrator.check import is_enabled, guild_only, has_permissions from administrator.logger import logger -from administrator import db, config +from administrator import db, slash from administrator.utils import event_is_enabled -def check_greetings_message_type(message_type): - if message_type not in ["join", "leave"]: - raise BadArgument() - - extension_name = "greetings" logger = logger.getChild(extension_name) @@ -20,68 +16,75 @@ logger = logger.getChild(extension_name) class Greetings(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot + slash.get_cog_commands(self) def description(self): return "Setup join and leave message" - @commands.group("greetings", pass_context=True) + @cog_ext.cog_subcommand(base="greetings", name="set", + description="Set the greetings message\n`{}` will be replace by the username", + options=[ + manage_commands.create_option("type", "The join or leave message", + SlashCommandOptionType.STRING, True, + [manage_commands.create_choice("join", "join"), + manage_commands.create_choice("leave", "leave")]), + manage_commands.create_option("message", "The message", SlashCommandOptionType.STRING, + True) + ]) @is_enabled() - @commands.guild_only() - @commands.has_permissions(manage_guild=True) - async def greetings(self, ctx: commands.Context): - if ctx.invoked_subcommand is None: - await ctx.invoke(self.greetings_help) - - @greetings.group("help", pass_context=True) - async def greetings_help(self, ctx: commands.Context): - embed = Embed(title="Greetings help") - embed.add_field(name="set ", value="Set the greetings message\n" - "`{}` will be replace by the username", - inline=False) - embed.add_field(name="show ", value="Show the greetings message", inline=False) - embed.add_field(name="toggle ", value="Enable or disable the greetings message", inline=False) - await ctx.send(embed=embed) - - @greetings.group("set", pass_context=True) - async def greetings_set(self, ctx: commands.Context, message_type: str): - check_greetings_message_type(message_type) - message = ctx.message.content.replace(config.get("prefix")+"greetings set " + message_type, "").strip() + @guild_only() + @has_permissions(manage_guild=True) + async def greetings_set(self, ctx: SlashContext, message_type: str, message: str): s = db.Session() m = s.query(db.Greetings).filter(db.Greetings.guild == ctx.guild.id).first() if not m: m = db.Greetings(ctx.guild.id) s.add(m) setattr(m, message_type+"_enable", True) - setattr(m, message_type+"_message", message) + setattr(m, message_type+"_message", message.replace("\\n", '\n')) s.commit() - await ctx.message.add_reaction("\U0001f44d") + await ctx.send(content="\U0001f44d") - @greetings.group("show", pass_context=True) - async def greetings_show(self, ctx: commands.Context, message_type: str): - check_greetings_message_type(message_type) + @cog_ext.cog_subcommand(base="greetings", name="show", + description="Show the greetings message", + options=[manage_commands.create_option("type", "The join or leave message", + SlashCommandOptionType.STRING, True, + [manage_commands.create_choice("join", "join"), + manage_commands.create_choice("leave", "leave")])]) + @is_enabled() + @guild_only() + @has_permissions(manage_guild=True) + async def greetings_show(self, ctx: SlashContext, message_type: str): s = db.Session() m = s.query(db.Greetings).filter(db.Greetings.guild == ctx.guild.id).first() s.close() if not m: - await ctx.send(f"No {message_type} message set !") + await ctx.send(content=f"No {message_type} message set !") else: if message_type == "join": - await ctx.send(embed=m.join_embed(ctx.guild.name, str(ctx.message.author))) + await ctx.send(embeds=[m.join_embed(ctx.guild.name, str(ctx.author))]) else: - await ctx.send(m.leave_msg(str(ctx.message.author))) + await ctx.send(content=m.leave_msg(str(ctx.author))) - @greetings.group("toggle", pass_context=True) - async def greetings_toggle(self, ctx: commands.Context, message_type: str): - check_greetings_message_type(message_type) + @cog_ext.cog_subcommand(base="greetings", name="toggle", + description="Enable or disable the greetings message", + options=[manage_commands.create_option("type", "The join or leave message", + SlashCommandOptionType.STRING, True, + [manage_commands.create_choice("join", "join"), + manage_commands.create_choice("leave", "leave")])]) + @is_enabled() + @guild_only() + @has_permissions(manage_guild=True) + async def greetings_toggle(self, ctx: SlashContext, message_type: str): s = db.Session() m = s.query(db.Greetings).filter(db.Greetings.guild == ctx.guild.id).first() if not m: - await ctx.send(f"No {message_type} message set !") + await ctx.send(content=f"No {message_type} message set !") else: setattr(m, message_type+"_enable", not getattr(m, message_type+"_enable")) s.commit() - await ctx.send(f"{message_type.title()} message is " + - ("enable" if getattr(m, message_type+"_enable") else "disable")) + await ctx.send(content=f"{message_type.title()} message is " + + ("enable" if getattr(m, message_type+"_enable") else "disable")) s.close() @commands.Cog.listener() @@ -108,6 +111,9 @@ class Greetings(commands.Cog): if m and m.leave_enable: await member.guild.system_channel.send(m.leave_msg(str(member))) + def cog_unload(self): + slash.remove_cog_commands(self) + def setup(bot): logger.info(f"Loading...") diff --git a/extensions/help.py b/extensions/help.py index 3d958f2..591e1de 100644 --- a/extensions/help.py +++ b/extensions/help.py @@ -1,9 +1,8 @@ -from discord import Embed from discord.ext import commands from discord.ext.commands import CommandNotFound, MissingRequiredArgument, BadArgument, MissingPermissions, \ - NoPrivateMessage, CommandError, NotOwner + NoPrivateMessage, NotOwner +from discord_slash import SlashContext -from administrator import config from administrator.check import ExtensionDisabled from administrator.logger import logger @@ -19,38 +18,35 @@ class Help(commands.Cog): def description(self): return "Give help and command list" - @commands.command("help", pass_context=True) - async def help(self, ctx: commands.Context): - embed = Embed(title="Help") - - for c in filter(lambda x: x != "Help", self.bot.cogs): - cog = self.bot.cogs[c] - try: - if await getattr(cog, c.lower()).can_run(ctx): - embed.add_field(name=c, - value=cog.description() + "\n" + - f"`{config.get('prefix')}{c.lower()} help` for more information", - inline=False) - except CommandError: - pass - - await ctx.send(embed=embed) - @commands.Cog.listener() async def on_command_error(self, ctx: commands.Context, error): + await self.error_handler(ctx, error) + + @commands.Cog.listener() + async def on_slash_command_error(self, ctx: SlashContext, error: Exception): + await self.error_handler(ctx, error) + + async def error_handler(self, ctx, error: Exception): if isinstance(error, CommandNotFound): - await ctx.message.add_reaction("\u2753") + await self.reaction(ctx, "\u2753") elif isinstance(error, MissingRequiredArgument) or isinstance(error, BadArgument): - await ctx.message.add_reaction("\u274C") - elif isinstance(error, NotOwner) or isinstance(error, MissingPermissions)\ + await self.reaction(ctx, "\u274C") + elif isinstance(error, NotOwner) or isinstance(error, MissingPermissions) \ or isinstance(error, NoPrivateMessage): - await ctx.message.add_reaction("\U000026D4") + await self.reaction(ctx, "\U000026D4") elif isinstance(error, ExtensionDisabled): - await ctx.message.add_reaction("\U0001F6AB") + await self.reaction(ctx, "\U0001F6AB") else: - await ctx.send("An error occurred !") + await ctx.send(content="An error occurred !") raise error - await ctx.message.delete(delay=30) + + @staticmethod + async def reaction(ctx, react: str): + m = getattr(ctx, "message", None) + if m: + await m.add_reaction(react) + else: + await ctx.send(content=react) def setup(bot): diff --git a/extensions/invite.py b/extensions/invite.py index e569c16..c64cfe3 100644 --- a/extensions/invite.py +++ b/extensions/invite.py @@ -1,12 +1,16 @@ import re -from discord import Embed, Member, Guild +from discord import Embed, Member, Guild, Role, CategoryChannel +from discord.abc import GuildChannel from discord.errors import Forbidden from discord.ext import commands from discord.ext.commands import BadArgument +from discord_slash import cog_ext, SlashContext, SlashCommandOptionType +from discord_slash.utils import manage_commands import db -from administrator.check import is_enabled +from administrator import slash +from administrator.check import is_enabled, guild_only, has_permissions from administrator.logger import logger from administrator.utils import event_is_enabled @@ -20,41 +24,31 @@ class Invite(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot self.invites = {} + slash.get_cog_commands(self) self.bot.loop.create_task(self.update_invites()) def description(self): return "Get role from a special invite link" - @commands.group("invite", pass_context=True) @is_enabled() - @commands.guild_only() - @commands.has_guild_permissions(administrator=True) - async def invite(self, ctx: commands.Context): - if ctx.invoked_subcommand is None: - await ctx.invoke(self.invite_help) - - @invite.group("help", pass_context=True) - async def invite_help(self, ctx: commands.Context): - embed = Embed(title="Invite help") - embed.add_field(name="invite create <#channel> <@role>", value="Create a invite link to a role", inline=False) - embed.add_field(name="invite delete ", value="Remove a invite", inline=False) - await ctx.send(embed=embed) - - @invite.group("create", pass_context=True) - async def invite_add(self, ctx: commands.Context, channel: str, role: str): - if not channel_mention_re.fullmatch(channel) or len(ctx.message.channel_mentions) != 1 or\ - not role_mention_re.fullmatch(role) or len(ctx.message.role_mentions) != 1: + @guild_only() + @has_permissions(administrator=True) + async def invite_add(self, ctx: SlashContext, channel: GuildChannel, role: Role): + if isinstance(channel, CategoryChannel): raise BadArgument() - - inv = await ctx.message.channel_mentions[0].create_invite() + inv = await channel.create_invite() s = db.Session() - s.add(db.InviteRole(ctx.guild.id, inv.code, ctx.message.role_mentions[0].id)) + s.add(db.InviteRole(ctx.guild.id, inv.code, role.id)) s.commit() s.close() - await ctx.send(f"Invite created: `{inv.url}`") + await ctx.send(content=f"Invite created: `{inv.url}`") - @invite.group("delete", pass_context=True) - async def invite_delete(self, ctx: commands.Context, code: str): + @cog_ext.cog_subcommand(base="invite", name="delete", description="Remove a invite", options=[ + manage_commands.create_option("code", "The invitation code", SlashCommandOptionType.STRING, True)]) + @is_enabled() + @guild_only() + @has_permissions(administrator=True) + async def invite_delete(self, ctx: SlashContext, code: str): inv = next(filter(lambda i: i.code == code, await ctx.guild.invites()), None) if not inv: raise BadArgument() @@ -68,7 +62,7 @@ class Invite(commands.Cog): s.commit() s.close() await inv.delete() - await ctx.message.add_reaction("\U0001f44d") + await ctx.send(content="\U0001f44d") async def update_invites(self): for g in self.bot.guilds: diff --git a/extensions/pcp.py b/extensions/pcp.py index 47dad44..2d3f05d 100644 --- a/extensions/pcp.py +++ b/extensions/pcp.py @@ -1,105 +1,74 @@ import re -from discord import Embed, Member +from discord import Member, Role from discord.ext import commands -from discord.ext.commands import BadArgument, MissingPermissions, CommandError +from discord.ext.commands import BadArgument +from discord_slash import cog_ext, SlashCommandOptionType, SlashContext +from discord_slash.utils import manage_commands import db +from administrator import slash +from administrator.check import guild_only, has_permissions from administrator.logger import logger - +from administrator.utils import get_message_by_url extension_name = "PCP" logger = logger.getChild(extension_name) -msg_url_re = re.compile(r"^https://.*discord.*\.com/channels/[0-9]+/([0-9+]+)/([0-9]+)$") -role_mention_re = re.compile(r"^<@&[0-9]+>$") -user_mention_re = re.compile(r"^<@![0-9]+>$") class PCP(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot + slash.get_cog_commands(self) def description(self): return "PCP Univ Lyon 1" - @commands.group("pcp", pass_context=True) - @commands.guild_only() - async def pcp(self, ctx: commands.Context): - group = ctx.message.content.replace(f"{ctx.prefix}{ctx.command} ", "").upper() - if group: - s = db.Session() - p = s.query(db.PCP).get(ctx.guild.id) - s.close() - if p and re.fullmatch(p.roles_re, group): - await ctx.message.add_reaction("\U000023f3") - role = next(filter(lambda r: r.name.upper() == group, ctx.guild.roles), None) - - def roles() -> list: - return list(filter( - lambda r: re.fullmatch(p.roles_re, r.name.upper()) or - (p.start_role_re and re.fullmatch(p.start_role_re, r.name.upper())), - ctx.author.roles - )) - - if not role or role.name in map(lambda r: r.name, roles()): - await ctx.message.remove_reaction("\U000023f3", self.bot.user) - raise BadArgument() - - while roles(): - await ctx.author.remove_roles(*roles()) - - while role not in ctx.author.roles: - await ctx.author.add_roles(role) - await ctx.message.remove_reaction("\U000023f3", self.bot.user) - await ctx.message.add_reaction("\U0001f44d") - return - - if ctx.invoked_subcommand is None: - await ctx.invoke(self.pcp_help) - - @pcp.group("help", pass_context=True) - async def pcp_help(self, ctx: commands.Context): - embed = Embed(title="PCP help") + @cog_ext.cog_subcommand(base="pcp", name="join", description="Join your group", options=[ + manage_commands.create_option("group", "The target group to join", SlashCommandOptionType.ROLE, True)]) + @guild_only() + async def pcp(self, ctx: SlashContext, role: Role): s = db.Session() p = s.query(db.PCP).get(ctx.guild.id) s.close() - if p: - embed.add_field(name="pcp ", value="Join your group", inline=False) - for c, n, v in [[self.pcp_group, "pcp group", "Manage PCP group"], - [self.pcp_pin, "pcp pin ", "Pin a message with the url"], - [self.pcp_unpin, "pcp unpin ", "Unpin a message with the url"]]: - try: - if await c.can_run(ctx): - embed.add_field(name=n, value=v, inline=False) - except CommandError: - pass - if not embed.fields: - raise MissingPermissions("") - await ctx.send(embed=embed) + if p and re.fullmatch(p.roles_re, role.name.upper()): + await ctx.send(content="\U000023f3") - @pcp.group("pin", pass_context=True) - async def pcp_pin(self, ctx: commands.Context, url: str): + async def roles() -> list: + return list(filter( + lambda r: re.fullmatch(p.roles_re, r.name.upper()) or + (p.start_role_re and re.fullmatch(p.start_role_re, r.name.upper())), + (await ctx.guild.fetch_member(ctx.author.id)).roles + )) + + if not role or role.name in map(lambda r: r.name, await roles()): + await ctx.delete() + raise BadArgument() + + while await roles(): + await ctx.author.remove_roles(*(await roles())) + + while role not in (await ctx.guild.fetch_member(ctx.author.id)).roles: + await ctx.author.add_roles(role) + await ctx.edit(content="\U0001f44d") + + @cog_ext.cog_subcommand(base="pcp", name="pin", description="Pin a message with the url", options=[ + manage_commands.create_option("url", "message URL", SlashCommandOptionType.STRING, True) + ]) + @guild_only() + async def pcp_pin(self, ctx: SlashContext, url: str): await self.pin(ctx, url, True) - @pcp.group("unpin", pass_context=True) - async def pcp_unpin(self, ctx: commands.Context, url: str): + @cog_ext.cog_subcommand(base="pcp", name="unpin", description="Unpin a message with the url", options=[ + manage_commands.create_option("url", "message URL", SlashCommandOptionType.STRING, True) + ]) + @guild_only() + async def pcp_unpin(self, ctx: SlashContext, url: str): await self.pin(ctx, url, False) @staticmethod - async def pin(ctx: commands.Context, url: str, action: bool): - r = msg_url_re.fullmatch(url) - if not r: - raise BadArgument() - r = r.groups() - - c = ctx.guild.get_channel(int(r[0])) - if not c: - raise BadArgument() - - m = await c.fetch_message(int(r[1])) - if not m: - raise BadArgument() - + async def pin(ctx: SlashContext, url: str, action: bool): + m = await get_message_by_url(ctx, url) if action: await m.pin() msg = "pinned a message" @@ -107,35 +76,23 @@ class PCP(commands.Cog): await m.unpin() msg = "unpinned a message" - await ctx.send(f"{ctx.author.mention} {msg}") + await ctx.send(content=f"{ctx.author.mention} {msg}") - @pcp.group("group", pass_context=True) - @commands.has_permissions(administrator=True) - async def pcp_group(self, ctx: commands.Context): - if ctx.invoked_subcommand is None: - await ctx.invoke(self.pcp_group_help) - - @pcp_group.group("help", pass_context=True) - async def pcp_group_help(self, ctx: commands.Context): - embed = Embed(title="PCP group help") - embed.add_field(name="pcp group set [Welcome role Regex]", - value="Set regex for group role", inline=False) - embed.add_field(name="pcp group unset", value="Unset regex for group role", inline=False) - embed.add_field(name="pcp group subject", value="Manage subjects for group", inline=False) - embed.add_field(name="pcp group fix_vocal", - value="Check all text channel permissions to reapply vocal permissions", inline=False) - await ctx.send(embed=embed) - - @pcp_group.group("fix_vocal", pass_context=True) - async def pcp_group_fix_vocal(self, ctx: commands.Context): + @cog_ext.cog_subcommand(base="pcp", subcommand_group="group", name="fix_vocal", + description="Check all text channel permissions to reapply vocal permissions") + @has_permissions(administrator=True) + async def pcp_group_fix_vocal(self, ctx: SlashContext): s = db.Session() p = s.query(db.PCP).get(ctx.guild.id) s.close() if not p: raise BadArgument() + message = "\U000023f3" + await ctx.send(content=message) for cat in filter(lambda c: re.fullmatch(p.roles_re, c.name.upper()), ctx.guild.categories): - await ctx.send(f"{cat.name}...") + message += f"\n{cat.name}..." + await ctx.edit(content=message) teachers = [] for t in cat.text_channels: for p in t.overwrites: @@ -144,11 +101,20 @@ class PCP(commands.Cog): voc = next(filter(lambda c: c.name == "vocal-1", cat.voice_channels), None) for t in teachers: await voc.set_permissions(t, view_channel=True) - await ctx.send(f"{cat.name} done") - await ctx.message.add_reaction("\U0001f44d") + message += f"\n{cat.name} done" + await ctx.edit(content=message) + message += "\n\U0001f44d" + await ctx.edit(content=message) - @pcp_group.group("set", pass_context=True) - async def pcp_group_set(self, ctx: commands.Context, roles_re: str, start_role_re: str = None): + @cog_ext.cog_subcommand(base="pcp", subcommand_group="group", name="set", description="Set regex for group role", + options=[ + manage_commands.create_option("role", "Roles regex", + SlashCommandOptionType.STRING, True), + manage_commands.create_option("role2", "Start roles regex", + SlashCommandOptionType.STRING, False) + ]) + @has_permissions(administrator=True) + async def pcp_group_set(self, ctx: SlashContext, roles_re: str, start_role_re: str = None): s = db.Session() p = s.query(db.PCP).get(ctx.guild.id) if p: @@ -159,9 +125,11 @@ class PCP(commands.Cog): s.add(p) s.commit() s.close() - await ctx.message.add_reaction("\U0001f44d") + await ctx.send(content="\U0001f44d") - @pcp_group.group("unset", pass_context=True) + @cog_ext.cog_subcommand(base="pcp", subcommand_group="group", name="unset", + description="Unset regex for group role") + @has_permissions(administrator=True) async def pcp_group_unset(self, ctx: commands.Context): s = db.Session() p = s.query(db.PCP).get(ctx.guild.id) @@ -173,33 +141,21 @@ class PCP(commands.Cog): s.close() await ctx.message.add_reaction("\U0001f44d") - @pcp_group.group("subject", pass_context=True) - async def pcp_group_subject(self, ctx: commands.Context): - if ctx.invoked_subcommand is None: - await ctx.invoke(self.pcp_group_subject_help) - - @pcp_group_subject.group("help", pass_context=True) - async def pcp_group_subject_help(self, ctx: commands.Context): - embed = Embed(title="PCP group subject help") - embed.add_field(name="pcp group subject add <@group> [@teacher]", value="Add a subject to a group", - inline=False) - embed.add_field(name="pcp group subject bulk <@group> [subject1] [subject2] ...", value="Bulk subject add", - inline=False) - embed.add_field(name="pcp group subject remove <@group>", value="Remove a subject to a group", - inline=False) - await ctx.send(embed=embed) - - @pcp_group_subject.group("add", pass_context=True) - async def pcp_group_subject_add(self, ctx: commands.Context, name: str, group: str, teacher: str = None): - if not role_mention_re.fullmatch(group): - raise BadArgument() - if teacher and not user_mention_re.fullmatch(teacher): - raise BadArgument() - elif teacher and\ - not next(filter(lambda r: r.name == "professeurs", ctx.message.mentions[0].roles), None): + @cog_ext.cog_subcommand(base="pcp", subcommand_group="subject", name="add", description="Add a subject to a group", + options=[ + manage_commands.create_option("name", "The subject name", + SlashCommandOptionType.STRING, True), + manage_commands.create_option("group", "The group", + SlashCommandOptionType.ROLE, True), + manage_commands.create_option("teacher", "The teacher", + SlashCommandOptionType.USER, False) + ]) + @has_permissions(administrator=True) + async def pcp_group_subject_add(self, ctx: SlashContext, name: str, group: Role, teacher: Member = None): + if teacher and not next(filter(lambda r: r.name == "professeurs", teacher.roles), None): raise BadArgument() - cat = next(filter(lambda c: c.name.upper() == ctx.message.role_mentions[0].name.upper(), + cat = next(filter(lambda c: c.name.upper() == group.name.upper(), ctx.guild.categories), None) if not cat: raise BadArgument() @@ -210,26 +166,32 @@ class PCP(commands.Cog): voc = next(filter(lambda c: c.name == "vocal-1", cat.voice_channels), None) if not voc: voc = await cat.create_voice_channel("vocal-1") - if ctx.message.mentions: - await chan.set_permissions(ctx.message.mentions[0], read_messages=True) - await voc.set_permissions(ctx.message.mentions[0], view_channel=True) + if teacher: + await chan.set_permissions(teacher, read_messages=True) + await voc.set_permissions(teacher, view_channel=True) - await ctx.message.add_reaction("\U0001f44d") + await ctx.send(content="\U0001f44d") - @pcp_group_subject.group("bulk", pass_context=True) - async def pcp_group_subject_bulk(self, ctx: commands.Context, mention, *names): - if not role_mention_re.fullmatch(mention): - raise BadArgument() - for n in names: - await ctx.invoke(self.pcp_group_subject_add, n, mention) + @cog_ext.cog_subcommand(base="pcp", subcommand_group="subject", name="bulk", + description="Remove a subject to a group", options=[ + manage_commands.create_option("group", "The group", SlashCommandOptionType.ROLE, True), + manage_commands.create_option("names", "Subjects names", SlashCommandOptionType.STRING, True) + ]) + @has_permissions(administrator=True) + async def pcp_group_subject_bulk(self, ctx: SlashContext, group: Role, names: str): + for n in names.split(" "): + await self.pcp_group_subject_add.invoke(ctx, n, group) - @pcp_group_subject.group("remove", pass_context=True) - async def pcp_group_subject_remove(self, ctx: commands.Context, name: str, group: str): - if not role_mention_re.fullmatch(group): - raise BadArgument() - - cat = next(filter(lambda c: c.name.upper() == ctx.message.role_mentions[0].name.upper(), - ctx.guild.categories), None) + @cog_ext.cog_subcommand(base="pcp", subcommand_group="subject", name="remove",description="Bulk subject add", + options=[ + manage_commands.create_option("name", "The subject name", + SlashCommandOptionType.STRING, True), + manage_commands.create_option("group", "The group", + SlashCommandOptionType.ROLE, True) + ]) + @has_permissions(administrator=True) + async def pcp_group_subject_remove(self, ctx: SlashContext, name: str, group: Role): + cat = next(filter(lambda c: c.name.upper() == group.name.upper(), ctx.guild.categories), None) if not cat: raise BadArgument() @@ -239,7 +201,7 @@ class PCP(commands.Cog): await chan.delete() - await ctx.message.add_reaction("\U0001f44d") + await ctx.send(content="\U0001f44d") def setup(bot): diff --git a/extensions/poll.py b/extensions/poll.py index e39d617..86f5ab0 100644 --- a/extensions/poll.py +++ b/extensions/poll.py @@ -1,12 +1,16 @@ +import shlex from datetime import datetime from discord.abc import GuildChannel from discord.ext import commands from discord import Embed, RawReactionActionEvent, RawMessageDeleteEvent, RawBulkMessageDeleteEvent, TextChannel, Guild from discord.ext.commands import BadArgument +from discord_slash import cog_ext, SlashCommandOptionType, SlashContext +from discord_slash.utils import manage_commands import db -from administrator.check import is_enabled +from administrator import slash +from administrator.check import is_enabled, guild_only from administrator.logger import logger from administrator.utils import event_is_enabled @@ -21,47 +25,41 @@ REACTIONS.append("\U0001F51F") class Poll(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot + slash.get_cog_commands(self) def description(self): return "Create poll with a simple command" - @commands.group("poll", pass_context=True) + @cog_ext.cog_slash(name="poll", + description="Create a poll", + options=[ + manage_commands.create_option("name", "Poll name", + SlashCommandOptionType.STRING, True), + manage_commands.create_option("choices", "All pool choice", + SlashCommandOptionType.STRING, True), + manage_commands.create_option("multi", "Allow multiple choice", + SlashCommandOptionType.BOOLEAN, False) + ]) @is_enabled() - @commands.guild_only() - async def poll(self, ctx: commands.Context, name: str, *choices): - if name == "help": - await ctx.invoke(self.poll_help) + @guild_only() + async def poll(self, ctx: SlashContext, name: str, choices: str, multi: bool = False): + choices = shlex.split(choices) + if len(choices) > 11: + raise BadArgument() else: - multi = False - if choices and choices[0] in ["multi", "m"]: - multi = True - choices = choices[1:] - if len(choices) == 0 or len(choices) > 11: - raise BadArgument() - else: - embed = Embed(title=f"Poll: {name}") - embed.set_author(name=str(ctx.author), icon_url=ctx.author.avatar_url) - embed.set_footer(text=f"Created: {ctx.message.created_at.strftime('%d/%m/%Y %H:%M')}") - for i, choice in enumerate(choices): - embed.add_field(name=REACTIONS[i], value=choice, inline=False) - message = await ctx.send(embed=embed) - reactions = REACTIONS[0:len(choices)] + ["\U0001F5D1"] - for reaction in reactions: - await message.add_reaction(reaction) - s = db.Session() - s.add(db.Polls(message.id, ctx.channel.id, ctx.guild.id, ctx.message.author.id, reactions, multi)) - s.commit() - s.close() - await ctx.message.delete() - - @poll.group("help", pass_context=True) - async def poll_help(self, ctx: commands.Context): - embed = Embed(title="Poll help") - embed.add_field(name="poll [multi|m] ... ", - value="Create a poll, the argument multi (or m) after the name allow multiple response\n" - "User the \U0001F5D1 to close the poll", - inline=False) - await ctx.send(embed=embed) + embed = Embed(title=f"Poll: {name}") + embed.set_author(name=str(ctx.author), icon_url=ctx.author.avatar_url) + embed.set_footer(text=f"Created: {datetime.now().strftime('%d/%m/%Y %H:%M')}") + for i, choice in enumerate(choices): + embed.add_field(name=REACTIONS[i], value=choice, inline=False) + message = await ctx.channel.send(embed=embed) + reactions = REACTIONS[0:len(choices)] + ["\U0001F5D1"] + for reaction in reactions: + await message.add_reaction(reaction) + s = db.Session() + s.add(db.Polls(message.id, ctx.channel.id, ctx.guild.id, ctx.author.id, reactions, multi)) + s.commit() + s.close() @commands.Cog.listener() async def on_raw_reaction_add(self, payload: RawReactionActionEvent): diff --git a/extensions/presentation.py b/extensions/presentation.py index ffdaee5..cdfeadb 100644 --- a/extensions/presentation.py +++ b/extensions/presentation.py @@ -1,10 +1,13 @@ +from discord.abc import GuildChannel from discord.ext import commands -from discord import Embed, Message +from discord import Message, Role, TextChannel from discord.ext.commands import BadArgument +from discord_slash import cog_ext, SlashCommandOptionType, SlashContext +from discord_slash.utils import manage_commands -from administrator.check import is_enabled +from administrator.check import is_enabled, guild_only, has_permissions from administrator.logger import logger -from administrator import db +from administrator import db, slash from administrator.utils import event_is_enabled extension_name = "presentation" @@ -14,51 +17,49 @@ logger = logger.getChild(extension_name) class Presentation(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot + slash.get_cog_commands(self) def description(self): return "Give role to user who make a presentation in a dedicated channel" - @commands.group("presentation", pass_context=True) + @cog_ext.cog_subcommand(base="presentation", name="set", + description="Set the presentation channel and the role to give", + options=[ + manage_commands.create_option("channel", "The presentation channel", + SlashCommandOptionType.CHANNEL, True), + manage_commands.create_option("role", "The role to give", + SlashCommandOptionType.ROLE, True) + ]) @is_enabled() - @commands.guild_only() - @commands.has_permissions(manage_guild=True) - async def presentation(self, ctx: commands.Context): - if ctx.invoked_subcommand is None: - await ctx.invoke(self.presentation_help) - - @presentation.group("help", pass_context=True) - async def presentation_help(self, ctx: commands.Context): - embed = Embed(title="Presentation help", description="Give a role to a new member after a presentation") - embed.add_field(name="set <#channel> <@role>", value="Set the presentation channel and the role to give", - inline=False) - embed.add_field(name="disable", value="Disable the auto role give", inline=False) - await ctx.send(embed=embed) - - @presentation.group("set", pass_context=True) - async def presentation_set(self, ctx: commands.Context): - if len(ctx.message.channel_mentions) != 1 and not len(ctx.message.role_mentions) != 1: + @guild_only() + @has_permissions(manage_guild=True) + async def presentation_set(self, ctx: SlashContext, channel: GuildChannel, role: Role): + if not isinstance(channel, TextChannel): raise BadArgument() s = db.Session() p = s.query(db.Presentation).filter(db.Presentation.guild == ctx.guild.id).first() if not p: - p = db.Presentation(ctx.guild.id, ctx.message.channel_mentions[0].id, ctx.message.role_mentions[0].id) + p = db.Presentation(ctx.guild.id, channel.id, role.id) s.add(p) else: - p.channel = ctx.message.channel_mentions[0].id - p.role = ctx.message.role_mentions[0].id + p.channel = channel.id + p.role = role.id s.commit() - await ctx.message.add_reaction("\U0001f44d") + await ctx.send(content="\U0001f44d") - @presentation.group("disable", pass_context=True) - async def presentation_disable(self, ctx: commands.Context): + @cog_ext.cog_subcommand(base="presentation", name="disable", description="Disable the auto role give") + @is_enabled() + @guild_only() + @has_permissions(manage_guild=True) + async def presentation_disable(self, ctx: SlashContext): s = db.Session() p = s.query(db.Presentation).filter(db.Presentation.guild == ctx.guild.id).first() if not p: - await ctx.send(f"Nothing to disable !") + await ctx.send(content="Nothing to disable !") else: s.delete(p) s.commit() - await ctx.message.add_reaction("\U0001f44d") + await ctx.send(content="\U0001f44d") s.close() @commands.Cog.listener() diff --git a/extensions/purge.py b/extensions/purge.py index bba7e33..e09c434 100644 --- a/extensions/purge.py +++ b/extensions/purge.py @@ -2,8 +2,10 @@ from asyncio import sleep from discord.ext import commands from discord import Embed, RawReactionActionEvent +from discord_slash import SlashContext, cog_ext -from administrator.check import is_enabled +from administrator import slash +from administrator.check import is_enabled, guild_only, has_permissions from administrator.logger import logger from administrator.utils import event_is_enabled @@ -15,34 +17,23 @@ class Purge(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot self.purges = {} + slash.get_cog_commands(self) def description(self): return "Purge all messages between the command and the next add reaction" - @commands.group("purge", pass_context=True) + @cog_ext.cog_slash(name="purge", description="Purge all message delimited by the command to your next reaction") @is_enabled() - @commands.guild_only() - @commands.has_permissions(manage_messages=True) - async def purge(self, ctx: commands.Context): - if ctx.invoked_subcommand is None: - self.purges[ctx.message.author.id] = ctx.message - await ctx.message.add_reaction("\U0001f44d") + @guild_only() + @has_permissions(manage_messages=True) + async def purge(self, ctx: SlashContext): + message = await ctx.channel.send(content="\U0001f44d") + self.purges[ctx.author.id] = message - await sleep(2*60) - try: - if self.purges[ctx.message.author.id] == ctx.message: - await ctx.message.clear_reactions() - del self.purges[ctx.message.author.id] - except: - pass - - @purge.group("help", pass_context=True) - @commands.guild_only() - async def purge_help(self, ctx: commands.Context): - embed = Embed(title="Purge help") - embed.add_field(name="purge", value="Purge all message delimited by the command to your next reaction", - inline=False) - await ctx.send(embed=embed) + await sleep(2*60) + if ctx.author.id in self.purges and self.purges[ctx.author.id] == message: + await message.delete() + del self.purges[ctx.author.id] @commands.Cog.listener() async def on_raw_reaction_add(self, payload: RawReactionActionEvent): @@ -55,8 +46,7 @@ class Purge(commands.Cog): if user.id in self.purges: if message.channel == self.purges[user.id].channel: async with message.channel.typing(): - await message.channel.purge(before=self.purges[user.id], after=message, - limit=None) + await message.channel.purge(before=self.purges[user.id], after=message, limit=None) await self.purges[user.id].delete() await message.delete() del self.purges[user.id] diff --git a/extensions/reminder.py b/extensions/reminder.py index bfea5cc..528f579 100644 --- a/extensions/reminder.py +++ b/extensions/reminder.py @@ -1,14 +1,15 @@ -import re -from datetime import datetime, timedelta +from datetime import datetime from discord.ext import commands from discord import Embed from discord.ext.commands import BadArgument from discord.ext import tasks +from discord_slash import SlashContext, cog_ext, SlashCommandOptionType +from discord_slash.utils import manage_commands from administrator.check import is_enabled from administrator.logger import logger -from administrator import db +from administrator import db, slash from administrator.utils import time_pars, seconds_to_time_string extension_name = "reminders" @@ -18,61 +19,52 @@ logger = logger.getChild(extension_name) class Reminders(commands.Cog, name="Reminder"): def __init__(self, bot: commands.Bot): self.bot = bot + slash.get_cog_commands(self) def description(self): return "Create and manage reminders" - @commands.group("reminder", pass_context=True) + @cog_ext.cog_subcommand(base="reminder", name="add", description="Add a reminder to your reminders list", options=[ + manage_commands.create_option("message", "The message", SlashCommandOptionType.STRING, True), + manage_commands.create_option("time", "When, ?D?H?M?S", SlashCommandOptionType.STRING, True) + ]) @is_enabled() - async def reminder(self, ctx: commands.Context): - if ctx.invoked_subcommand is None: - await ctx.invoke(self.reminder_help) - - @reminder.group("help", pass_context=True) - async def reminder_help(self, ctx: commands.Context): - embed = Embed(title="Reminder help") - embed.add_field(name="reminder add