From 7e2f113c524aa74b574f5bf7b1147f7e4d4675bd Mon Sep 17 00:00:00 2001 From: Vegard Berg Date: Wed, 3 Aug 2022 19:48:01 +0200 Subject: [PATCH] Addes self-role groups. --- README.md | 40 +++- commands/self_roles.py | 411 ++++++++++++++++++++++++++++++++++++++++- database.py | 85 +++++++-- 3 files changed, 519 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index ea32c68..20c0a2a 100644 --- a/README.md +++ b/README.md @@ -69,4 +69,42 @@ By default it is accessible to those with the *Kick User* permission. ## User-infractions `user-infractions ` -Displays the infractions of a user. \ No newline at end of file +Displays the infractions of a user. + +## Adm +Server administrative commands. + +# Join/leave messages +Join and leave messages allow the server and the member to be referenced. + +## Member +Can be accessed with `{member}`. + +- `{member.id}` + The member's unique Snowflake ID. +- `{member.bot}` + Whether or not this member is a bot. +- `{member.display_name}` + The member's nickname, if any, or their username. +- `{member.joined_at}` + The time at which the member joined, as a timestamp. +- `{member.username}` + The member's username. +- `{member.discriminator}` + The member's discriminator, i.e. the four digits following their username and the `#` symbol. +- `{member.mention}` + A mention of the member. + +## Guild (server) +Can be accessed with `{guild}`. + +- `{guild.id}` + The unique Snowflake ID of the guild. +- `{guild.member_count}` + The total number of members in the guild. +- `{guild.name}` + The name of the guild. +- `{guild.description}` + The Discovery description of the guild. +- `{guild.created_at}` + A timestamp of when the guild was created. \ No newline at end of file diff --git a/commands/self_roles.py b/commands/self_roles.py index 6361a64..0e3ad59 100644 --- a/commands/self_roles.py +++ b/commands/self_roles.py @@ -1,5 +1,5 @@ from re import S -from typing import List, Optional +from typing import Dict, List, Optional, Tuple import logging from naff import ( Extension, @@ -13,8 +13,18 @@ from naff import ( Embed, EmbedField, AutocompleteContext, + Select, + SelectOption, + listen, + component_callback, +) +from naff.api import events +from naff import components +from database import ( + SelfRoles as SelfRolesModel, + SelfRoleGroups as SelfRoleGroupsModel, + SelfRoleGroupRelations as SelfRoleGroupRelationsModel, ) -from database import SelfRoles as SelfRolesModel class SelfRoles(Extension): @@ -94,6 +104,397 @@ class SelfRoles(Extension): f"Removed self-role {role.mention} from the database.", ephemeral=True ) + @slash_command( + name="role-group", + description="Manage self-role groups", + sub_cmd_name="add-group", + sub_cmd_description="Add a self-role group", + dm_permission=False, + default_member_permissions=Permissions.MANAGE_ROLES, + ) + @slash_option( + name="group", + description="Group to add", + required=True, + opt_type=OptionTypes.STRING, + ) + @slash_option( + name="description", + description="Description of the group", + required=False, + opt_type=OptionTypes.STRING, + ) + async def role_group_add_group( + self, ctx: InteractionContext, group: str, description: str = None + ): + await ctx.defer(ephemeral=True) + + SelfRoleGroupsModel.insert( + guild_id=int(ctx.guild_id), + group_name=group, + group_description=description, + ).on_conflict_replace().execute() + + await ctx.send( + f"Added self-role group {group} to the database." + f"\n{'Description: ' + description if description is not None else ''}", + ephemeral=True, + ) + + @slash_command( + name="role-group", + description="Manage self-role groups", + sub_cmd_name="remove-group", + sub_cmd_description="Remove a self-role group", + dm_permission=False, + default_member_permissions=Permissions.MANAGE_ROLES, + ) + @slash_option( + name="group", + description="Group to remove", + required=True, + opt_type=OptionTypes.STRING, + ) + async def role_group_remove_group(self, ctx: InteractionContext, group: str): + await ctx.defer(ephemeral=True) + + SelfRoleGroupRelationsModel.delete().where( + SelfRoleGroupRelationsModel.guild_id == int(ctx.guild_id), + SelfRoleGroupRelationsModel.group_name == group, + ).execute() + + SelfRoleGroupsModel.delete().where( + SelfRoleGroupsModel.guild_id == int(ctx.guild_id), + SelfRoleGroupsModel.group_name == group, + ).execute() + + await ctx.send( + f"Removed self-role group {group} from the database.", ephemeral=True + ) + + @slash_command( + name="role-group", + description="Manage self-role groups", + sub_cmd_name="add-role", + sub_cmd_description="Add a role to a self-role group", + dm_permission=False, + default_member_permissions=Permissions.MANAGE_ROLES, + ) + @slash_option( + name="group", + description="Group to add the role to", + required=True, + opt_type=OptionTypes.STRING, + autocomplete=True, + ) + @slash_option( + name="role", + description="Role to add to the group", + required=True, + opt_type=OptionTypes.STRING, + autocomplete=True, + ) + async def role_group_add_role(self, ctx: InteractionContext, group: str, role: str): + await ctx.defer(ephemeral=True) + + r: Role = await ctx.guild.fetch_role(role) + if r is None: + await ctx.send(f"Role {role} not found in this server.", ephemeral=True) + return + + SelfRoleGroupRelationsModel.insert( + guild_id=int(ctx.guild_id), + group_name=group, + role_id=int(role), + ).on_conflict_replace().execute() + + await ctx.send(f"Added role {r.mention} to the group {group}.", ephemeral=True) + + @role_group_add_role.autocomplete("group") + async def role_group_add_role_group_autocomplete( + self, ctx: AutocompleteContext, group: str + ): + groups_q: List[SelfRoleGroupsModel] = ( + SelfRoleGroupsModel.select() + .where( + SelfRoleGroupsModel.guild_id == int(ctx.guild_id), + SelfRoleGroupsModel.group_name.startswith(group), + ) + .execute() + ) + + await ctx.send(choices=[g.group_name for g in groups_q]) + + @role_group_add_role.autocomplete("role") + async def role_group_add_role_role_autocomplete( + self, ctx: AutocompleteContext, role: str, **kwargs + ): + roles_q: List[SelfRolesModel] = ( + SelfRolesModel.select() + .where( + SelfRolesModel.guild_id == int(ctx.guild_id), + SelfRolesModel.role_name.startswith(role), + ) + .execute() + ) + + await ctx.send( + choices=[{"name": r.role_name, "value": str(r.role_id)} for r in roles_q] + ) + + @slash_command( + name="role-group", + description="Manage self-role groups", + sub_cmd_name="remove-role", + sub_cmd_description="Remove a role from a self-role group", + dm_permission=False, + default_member_permissions=Permissions.MANAGE_ROLES, + ) + @slash_option( + name="group", + description="Group to remove the role from", + required=True, + opt_type=OptionTypes.STRING, + autocomplete=True, + ) + @slash_option( + name="role", + description="Role to remove from the group", + required=True, + opt_type=OptionTypes.STRING, + autocomplete=True, + ) + async def role_group_remove_role( + self, ctx: InteractionContext, group: str, role: str + ): + await ctx.defer(ephemeral=True) + r: Role = await ctx.guild.fetch_role(role) + if r is None: + await ctx.send(f"Role {role} not found in this server.", ephemeral=True) + return + + SelfRoleGroupRelationsModel.delete().where( + SelfRoleGroupRelationsModel.guild_id == int(ctx.guild_id), + SelfRoleGroupRelationsModel.group_name == group, + SelfRoleGroupRelationsModel.role_id == int(role), + ).execute() + + await ctx.send( + f"Removed role {r.mention} from the group {group}.", ephemeral=True + ) + + @role_group_remove_role.autocomplete("group") + async def role_group_remove_role_group_autocomplete( + self, ctx: AutocompleteContext, group: str + ): + groups_q: List[SelfRoleGroupsModel] = ( + SelfRoleGroupsModel.select() + .where( + SelfRoleGroupsModel.guild_id == int(ctx.guild_id), + SelfRoleGroupsModel.group_name.startswith(group), + ) + .execute() + ) + + await ctx.send(choices=[g.group_name for g in groups_q]) + + @role_group_remove_role.autocomplete("role") + async def role_group_remove_role_role_autocomplete( + self, ctx: AutocompleteContext, role: str, **kwargs + ): + roles_q = ( + SelfRolesModel.select() + .join( + SelfRoleGroupRelationsModel, + on=(SelfRolesModel.role_id == SelfRoleGroupRelationsModel.role_id), + ) + .where( + SelfRolesModel.guild_id == int(ctx.guild_id), + SelfRoleGroupRelationsModel.group_name == kwargs["group"], + SelfRolesModel.role_name.startswith(role), + ) + .execute() + ) + + await ctx.send( + choices=[{"name": r.role_name, "value": str(r.role_id)} for r in roles_q] + ) + + @slash_command( + name="role-group", + description="Manage self-role groups", + sub_cmd_name="list-groups", + sub_cmd_description="List all self-role groups", + dm_permission=False, + default_member_permissions=Permissions.MANAGE_ROLES, + ) + async def role_group_list_groups(self, ctx: InteractionContext): + await ctx.defer(ephemeral=True) + + embeds: List[Embed] = [] + groups_q = ( + SelfRolesModel.select( + SelfRolesModel.role_name, + SelfRolesModel.role_description, + SelfRoleGroupsModel.group_name, + SelfRoleGroupsModel.group_description, + ) + .join( + SelfRoleGroupRelationsModel, + on=(SelfRoleGroupRelationsModel.role_id == SelfRolesModel.role_id), + ) + .join( + SelfRoleGroupsModel, + on=( + SelfRoleGroupRelationsModel.group_name + == SelfRoleGroupsModel.group_name + ), + ) + .where(SelfRoleGroupsModel.guild_id == int(ctx.guild_id)) + .objects() + ) + + groups: Dict[str, List[str, List[Tuple[str, str]]]] = {} + for row in groups_q: + if row.group_name not in groups: + groups[row.group_name] = [ + row.group_description, + [(row.role_name, row.role_description)], + ] + else: + groups[row.group_name][1].append((row.role_name, row.role_description)) + + for group, data in groups.items(): + embed = Embed(title=group, description=data[0]) + for role in data[1]: + embed.add_field(name=role[0], value=role[1], inline=False) + embeds.append(embed) + + await ctx.send( + embeds=embeds, + ephemeral=True, + ) + + @slash_command( + name="role-group", + description="Manage self-role groups", + sub_cmd_name="generate", + sub_cmd_description="Generate a message with a selection of roles from a group", + dm_permission=False, + default_member_permissions=Permissions.MANAGE_ROLES, + ) + @slash_option( + name="group", + description="Group to generate the message from", + required=True, + opt_type=OptionTypes.STRING, + autocomplete=True, + ) + async def role_group_generate(self, ctx: InteractionContext, group: str): + await ctx.defer(ephemeral=False) + + roles_q: List[SelfRolesModel] = ( + SelfRolesModel.select(SelfRolesModel.role_name, SelfRolesModel.role_id, SelfRolesModel.role_description) + .join(SelfRoleGroupRelationsModel, on=(SelfRolesModel.role_id == SelfRoleGroupRelationsModel.role_id)) + .where( + SelfRoleGroupRelationsModel.guild_id == int(ctx.guild_id), + SelfRoleGroupRelationsModel.group_name == group, + ).objects() + ) + + group_q = SelfRoleGroupsModel.get(SelfRoleGroupsModel.group_name == group) + + options = [] + for role in roles_q: + opt = SelectOption( + label=role.role_name, + value=str(role.role_id), + description=role.role_description, + ) + options.append(opt) + + select = Select( + options=options, + placeholder=f"Select roles from the group {group}", + custom_id=f"role-group-assign:{group}", + min_values=0, + max_values=len(options), + ) + + await ctx.send( + embed=Embed( + title=f"{group}", + description=group_q.group_description, + ), + components=select, + ) + + + @role_group_generate.autocomplete("group") + async def role_group_generate_group_autocomplete( + self, ctx: AutocompleteContext, group: str + ): + groups_q: List[SelfRoleGroupsModel] = ( + SelfRoleGroupsModel.select() + .where( + SelfRoleGroupsModel.guild_id == int(ctx.guild_id), + SelfRoleGroupsModel.group_name.startswith(group), + ) + .execute() + ) + + await ctx.send(choices=[g.group_name for g in groups_q]) + + @listen(events.Select) + async def on_role_selected(self, event: events.Select): + ctx = event.context + await ctx.defer(ephemeral=True) + + if not ctx.custom_id.startswith("role-group-assign"): + return + + group = ctx.custom_id.split(":")[1] + role_ids = [int(r) for r in ctx.values] + + roles_q: List[SelfRolesModel] = ( + SelfRolesModel.select(SelfRolesModel.role_name, SelfRolesModel.role_id, SelfRolesModel.requires, SelfRoleGroupRelationsModel.group_name) + .join(SelfRoleGroupRelationsModel, on=(SelfRolesModel.role_id == SelfRoleGroupRelationsModel.role_id)) + .where( + SelfRoleGroupRelationsModel.guild_id == int(ctx.guild_id), + SelfRoleGroupRelationsModel.group_name == group, + ).objects() + ) + + actions = [] + for role in roles_q: + if role.role_id in role_ids: + if not ctx.author.has_role(role.role_id): + if not role.requires is None: + if not ctx.author.has_role(role.requires): + actions.append(f"{role.role_name} requires {role.requires} and was not added.") + continue + + await ctx.author.add_role(role.role_id) + actions.append(f"Added role {role.role_name}") + else: + if ctx.author.has_role(role.role_id): + if not role.requires is None: + if not ctx.author.has_role(role.requires): + actions.append(f"{role.role_name} requires {role.requires} to be managed, and was therefore not removed.") + continue + await ctx.author.remove_role(role.role_id) + actions.append(f"Removed role {role.role_name}") + + await ctx.send( + content="\n".join(actions), + ephemeral=True, + ) + + + + + + @slash_command( name="role", description="Manage your roles", @@ -146,7 +547,7 @@ class SelfRoles(Extension): @role_add.autocomplete("role") async def role_add_autocomplete_role( - self, ctx: AutocompleteContext, role: str = None + self, ctx: AutocompleteContext, role: str = None, **kwargs ): choices = [] @@ -226,7 +627,7 @@ class SelfRoles(Extension): @role_remove.autocomplete("role") async def role_remove_autocomplete_role( - self, ctx: AutocompleteContext, role: str = None + self, ctx: AutocompleteContext, role: str = None, **kwargs ): choices = [] @@ -300,4 +701,6 @@ class SelfRoles(Extension): def setup(client: Client): SelfRoles(client) SelfRolesModel.create_table() + SelfRoleGroupsModel.create_table() + SelfRoleGroupRelationsModel.create_table() logging.info("SelfRoles extension loaded.") diff --git a/database.py b/database.py index 557ad58..4c1c9a6 100644 --- a/database.py +++ b/database.py @@ -19,6 +19,9 @@ from peewee import ( db = None +"""The database connection.""" + +load_dotenv() if (postgres_url := os.getenv("HEIMDALLR_POSTGRES_URL")) is not None: db = PostgresqlDatabase(postgres_url) else: @@ -26,14 +29,24 @@ else: class Infractions(Model): + """A model for infractions.""" + id = AutoField() + """The ID of the infraction.""" guild_id = BigIntegerField() + """The guild ID of the infraction.""" user_id = BigIntegerField() + """The user ID of the user receiving the infraction.""" at_time = DateTimeField(default=datetime.datetime.now) + """The time at which the infraction was received.""" weight = FloatField(default=1.0) + """The weight/severity of the infraction.""" reason = TextField(null=True) + """The reason for the infraction.""" given_by = BigIntegerField() + """The user ID of the user giving the infraction.""" silent = BooleanField(default=False) + """Whether the infraction should be silent.""" class Meta: table_name = "Infractions" @@ -41,11 +54,18 @@ class Infractions(Model): class GuildSettings(Model): + """A model for guild settings.""" + guild_id = BigIntegerField(primary_key=True) + """The guild ID of the guild settings.""" admin_channel = BigIntegerField(null=True) + """The channel ID of the admin channel.""" use_name_filter = BooleanField(default=False) + """Whether the bot should use a name filter.""" use_gatekeep = BooleanField(default=False) + """Whether the bot should use gatekeep.""" use_logging = BooleanField(default=False) + """Whether the bot should use logging.""" class Meta: table_name = "GuildSettings" @@ -53,12 +73,20 @@ class GuildSettings(Model): class JoinLeave(Model): + """A model for join/leave messages.""" + guild_id = BigIntegerField(primary_key=True) + """The guild ID of the guild settings.""" join_message = TextField(null=True) + """The join message.""" leave_message = TextField(null=True) + """The leave message.""" message_channel = BigIntegerField(null=True) + """The channel ID of the channel to send the message in.""" join_message_enabled = BooleanField(default=True) + """Whether the join message is enabled.""" leave_message_enabled = BooleanField(default=False) + """Whether the leave message is enabled.""" class Meta: table_name = "JoinLeave" @@ -66,11 +94,18 @@ class JoinLeave(Model): class SelfRoles(Model): + """A model for self-assignable roles.""" + guild_id = BigIntegerField() + """The guild ID of the guild for the self-role.""" role_id = BigIntegerField() + """The role ID of the self-role.""" role_name = TextField() + """The name of the self-role.""" role_description = TextField(null=True) + """The description of the self-role.""" requires = BigIntegerField(null=True) + """The role ID of the role required to assign the self-role, if any.""" class Meta: primary_key = CompositeKey("guild_id", "role_id") @@ -78,11 +113,48 @@ class SelfRoles(Model): database = db -class ConditionalRoles(Model): +class SelfRoleGroups(Model): + """A model for self-role groups.""" + guild_id = BigIntegerField() + """The guild ID of the guild for the self-role group.""" + group_name = TextField() + """The name of the self-role group.""" + group_description = TextField(null=True) + """The description of the self-role group.""" + + class Meta: + primary_key = CompositeKey("guild_id", "group_name") + table_name = "SelfRoleGroups" + database = db + + +class SelfRoleGroupRelations(Model): + """A model for self-role group relations.""" + + guild_id = ForeignKeyField(SelfRoleGroups, field="guild_id", backref="GuildRelation") + """The guild ID of the guild for the self-role group.""" + group_name = ForeignKeyField(SelfRoleGroups, field="group_name", backref="GroupRelation") + """The name of the self-role group.""" role_id = BigIntegerField() + """The role ID of the self-role.""" + + class Meta: + primary_key = CompositeKey("guild_id", "group_name", "role_id") + table_name = "SelfRoleGroupRelations" + database = db + +class ConditionalRoles(Model): + """A model for conditional roles.""" + + guild_id = BigIntegerField() + """The guild ID of the guild for the conditional role.""" + role_id = BigIntegerField() + """The role ID of the conditional role.""" condition = CharField(max_length=16) + """The condition for the conditional role.""" condition_data = TextField() + """The data for the condition.""" class Meta: primary_key = CompositeKey("guild_id", "role_id") @@ -131,14 +203,3 @@ class BadNames(Model): class Meta: table_name = "BadNames" database = db - - -class NotifyWords(Model): - id = AutoField() - guild_id = BigIntegerField() - word = TextField() - user_to_notify = BigIntegerField() - - class Meta: - table_name = "NotifyWords" - database = db