async def role_joinable(self, ctx, *roles: RoleConv): """ Allows a moderator to add roles to the self-assignable group. """ logger.info( "Adding joinable roles for guild '%s' (%d): [%s]", ctx.guild.name, ctx.guild.id, ", ".join(role.name for role in roles), ) if not roles: raise CommandFailed() # Get special roles special_roles = self.bot.sql.settings.get_special_roles(ctx.guild) # Ensure none of the roles grant any permissions for role in roles: embed = permissions.elevated_role_embed(ctx.guild, role, "error") if embed is not None: raise ManualCheckFailure(embed=embed) for attr in ("member", "guest", "mute", "jail"): if role == getattr(special_roles, attr): embed = discord.Embed(colour=discord.Colour.red()) embed.set_author(name="Cannot add role as assignable") embed.description = ( f"{role.mention} cannot be self-assignable, " f"it is already used as the **{attr}** role!") raise ManualCheckFailure(embed=embed) # Get roles that are already assignable assignable_roles = self.bot.sql.roles.get_assignable_roles(ctx.guild) # Add roles to database with self.bot.sql.transaction(): for role in roles: if role not in assignable_roles: self.bot.sql.roles.add_assignable_role(ctx.guild, role) # Send response embed = discord.Embed(colour=discord.Colour.dark_teal()) embed.set_author(name="Made roles joinable") descr = StringBuilder(sep=", ") for role in roles: descr.write(role.mention) embed.description = str(descr) await ctx.send(embed=embed) # Send journal event content = f"Roles were set as joinable: {self.str_roles(roles)}" self.journal.send("joinable/add", ctx.guild, content, icon="role", roles=roles)
async def alert_add(self, ctx, attribute: str, relationship: str, *, amount: str): """ Adds a join alert with the given condition. Possible attributes: id, created, name, discrim, avatar, status Possible relationships: > >= = != < <= ~ """ logging.info( "Got request to add new join alert for '%s' (%d)", ctx.guild.name, ctx.guild.id, ) try: key = JoinAlertKey.parse(attribute) except ValueError: raise ManualCheckFailure(content=f"Invalid attribute: {attribute}") try: op = ValueRelationship(relationship) except ValueError: raise ManualCheckFailure( content=f"Invalid relationship: {relationship}") try: value = key.parse_value(amount) except ValueError as error: raise ManualCheckFailure(content=str(error)) alert = JoinAlert(ctx.guild, None, key, op, value) logging.info("Adding join alert: %s", alert) with self.bot.sql.transaction(): self.bot.sql.welcome.add_alert(ctx.guild, alert) self.alerts[alert.id] = alert # Notify the user embed = discord.Embed(colour=discord.Colour.dark_teal()) embed.set_author(name="Successfully added join alert") descr = StringBuilder() descr.writeln(f"ID: #`{alert.id:05}`, Condition: `{alert}`") descr.writeln( "To get these notifications in a channel, add a logger for path `/welcome/alert`" ) embed.description = str(descr) await ctx.send(embed=embed)
async def remove_other_roles_on_punish(self, ctx, value: bool = None): """ Gets the current setting for whether punishment actions remove all other roles, or leaves them. If you're an administrator, you can change this value. """ if value is None: remove_other_roles = self.bot.sql.settings.get_remove_other_roles( ctx.guild) embed = discord.Embed(colour=discord.Colour.dark_teal()) state = "are removed" if remove_other_roles else "are kept" embed.description = ( f"When punishment roles are added other roles **{state}**") elif not admin_perm(ctx): # Lacking authority to set remove other roles embed = discord.Embed(colour=discord.Colour.red()) embed.description = "You do not have permissions to change the removal of non-punishment roles" raise ManualCheckFailure(embed=embed) else: with self.bot.sql.transaction(): self.bot.sql.settings.set_remove_other_roles(ctx.guild, value) embed = discord.Embed(colour=discord.Colour.teal()) embed.description = ( f"Set removal of other non-punishment roles to `{value}`") await ctx.send(embed=embed)
async def reapply_auto(self, ctx, value: bool = None): """ Tells whether automatic role reapplication is enabled. Only self-assignable and punishment roles are re-applied. If you're an administrator, you can change this value. """ if value is None: # Get reapplication roles reapply = self.bot.sql.settings.get_auto_reapply(ctx.guild) embed = discord.Embed(colour=discord.Colour.dark_teal()) enabled = "enabled" if reapply else "disabled" embed.description = ( f"Automatic role reapplication is **{enabled}** on this server" ) elif not admin_perm(ctx): # Lacking authority to set reapplication embed = discord.Embed(colour=discord.Colour.red()) embed.description = ( "You do not have permission to set automatic role reapplication" ) raise ManualCheckFailure(embed=embed) else: # Set role reapplication with self.bot.sql.transaction(): self.bot.sql.settings.set_auto_reapply(ctx.guild, value) embed = discord.Embed(colour=discord.Colour.dark_teal()) embed.description = ( f"{'Enabled' if value else 'Disabled'} automatic role reapplication" ) await ctx.send(embed=embed)
async def perform_jail(self, ctx, member, minutes, reason): roles = self.bot.sql.settings.get_special_roles(ctx.guild) if roles.jail is None: raise CommandFailed(content="No configured jail role") if member.top_role >= ctx.me.top_role: raise ManualCheckFailure( "I don't have permission to jail this user") if roles.jail_role in member.roles: raise CommandFailed(content="User is already jailed") # Check that users top role is not the same as the requesters top role. if member != ctx.author: if member.top_role == ctx.author.top_role: raise CommandFailed( content="You can not jail a user with the same role as you" ) minutes = max(minutes, 0) reason = self.build_reason(ctx, "Jailed", minutes, reason) await self.bot.punish.jail(ctx.guild, member, reason) # If a delayed event, schedule a Navi task if minutes: await self.remove_roles(ctx, member, minutes, PunishAction.RELIEVE_JAIL, reason)
async def filter_manage_messages(self, ctx, value: bool = None): """ Gets the current setting for whether manage message members are filter immune. If you're an administrator, you can change this value. """ if value is None: filter_settings = self.bot.sql.filter.get_settings(ctx.guild) if filter_settings.manage_messages_immune: result = "**are filter immune**" else: result = "are **not** filter immune" embed = discord.Embed(colour=discord.Colour.dark_teal()) embed.description = f"Those with the `Manage Messages` permission {result}." elif not admin_perm(ctx): # Lacking authority to set warn manual mod action embed = discord.Embed(colour=discord.Colour.red()) embed.description = "You do not have permission to enable or disable manage messages filter immunity" raise ManualCheckFailure(embed=embed) else: with self.bot.sql.transaction(): self.bot.sql.filter.set_bot_filter_immunity( ctx.guild, manage_messages_immune=value) embed = discord.Embed(colour=discord.Colour.teal()) embed.description = ( f"Set filter immunity for those with manage messages to `{value}`" ) await ctx.send(embed=embed)
async def unban(self, ctx, user: UserConv, *, reason: str): """ Unbans the id from the guild with a reason. If guild has moderation logging enabled, it is logged """ try: embed = discord.Embed(description="Done! User Unbanned") embed.add_field(name="Reason", value=reason) mod = user_discrim(ctx.author) unbanned = user_discrim(user) clean_reason = escape_backticks(reason) content = f"{mod} unbanned {user.mention} ({unbanned}) with reason: `{clean_reason}`" await ctx.guild.unban(user, reason=f"{reason} - {mod}") await ctx.send(embed=embed) self.journal.send("member/unban", ctx.guild, content, icon="unban", user=user) except discord.errors.Forbidden: raise ManualCheckFailure( content="I don't have permission to unban this user")
async def perform_ban(self, ctx, user, delete_days, reason): if delete_days < 0 or delete_days > 7: embed = discord.Embed(colour=discord.Colour.red()) embed.description = ( f"Invalid specification for number of days to delete: `{delete_days}`. " "Must be between 0 and 7 inclusive.") await ctx.send(embed=embed) return try: embed = discord.Embed(colour=discord.Colour.teal()) embed.description = ( f"Done! {user.mention} ({user_discrim(user)}) was banned") embed.add_field(name="Reason", value=reason) embed.add_field(name="Deleted messages", value=f"{delete_days} days") # Don't send a journal event, that is handled by the moderation journal listener await ctx.guild.ban( user, reason=f"{reason} - {user_discrim(ctx.author)}", delete_message_days=delete_days, ) await ctx.send(embed=embed) except discord.errors.Forbidden: raise ManualCheckFailure( content="I don't have permission to ban this user")
async def warn_manual_mod_action(self, ctx, value: bool = None): """ Gets the current setting for warning about manual mod actions. If you're an administrator, you can change this value. """ if value is None: warn_manual_mod_action = self.bot.sql.settings.get_warn_manual_mod_action( ctx.guild) embed = discord.Embed(colour=discord.Colour.dark_teal()) state = "enabled" if warn_manual_mod_action else "disabled" embed.description = f"Warning moderators about performing mod actions manually is **{state}**" elif not admin_perm(ctx): # Lacking authority to set warn manual mod action embed = discord.Embed(colour=discord.Colour.red()) embed.description = "You do not have permission to enable or disable manual mod action warning" raise ManualCheckFailure(embed=embed) else: with self.bot.sql.transaction(): self.bot.sql.settings.set_warn_manual_mod_action( ctx.guild, value) embed = discord.Embed(colour=discord.Colour.teal()) embed.description = f"Set warning moderators about performing mod actions manually to `{value}`" await ctx.send(embed=embed)
async def unmute( self, ctx, member: MemberConv, minutes: int = 0, *, reason: str = None ): """ Unmutes the user, with an optional delay in minutes. Requires a mute role to be configured. Set 'minutes' to 0 to unmute immediately. """ logger.info( "Unmuting user '%s' (%d) in %d minutes", member.name, member.id, minutes ) roles = self.bot.sql.settings.get_special_roles(ctx.guild) if roles.mute is None: raise CommandFailed(content="No configured mute role") if member.top_role >= ctx.me.top_role: raise ManualCheckFailure("I don't have permission to unmute this user") minutes = max(minutes, 0) reason = self.build_reason(ctx, "Unmuted", minutes, reason, past=True) if minutes: await self.remove_roles( ctx, member, minutes, PunishAction.RELIEVE_MUTE, reason ) else: await self.bot.punish.unjail(ctx.guild, member, reason)
async def perform_mute(self, ctx, member: MemberConv, minutes: int, reason: str = None): logger.info( "Muting user '%s' (%d) for %d minutes", member.name, member.id, minutes ) if minutes <= 0: # Since muting prevents members from responding or petitioning staff, # a timed release is mandatory. Otherwise they might be forgotten # and muted forever. raise CommandFailed() roles = self.bot.sql.settings.get_special_roles(ctx.guild) if roles.mute is None: raise CommandFailed(content="No configured mute role") if member.top_role >= ctx.me.top_role: raise ManualCheckFailure("I don't have permission to mute this user") minutes = max(minutes, 0) reason = self.build_reason(ctx, "Muted", minutes, reason, past=True) await self.bot.punish.mute(ctx.guild, member, reason) # If a delayed event, schedule a Navi task if minutes: await self.remove_roles( ctx, member, minutes, PunishAction.RELIEVE_MUTE, reason )
async def perform_unjail(self, ctx, member, minutes, reason): roles = self.bot.sql.settings.get_special_roles(ctx.guild) if roles.jail is None: raise CommandFailed(content="No configured jail role") if member.top_role >= ctx.me.top_role: raise ManualCheckFailure( "I don't have permission to unjail this user") if roles.jail_role not in member.roles: raise CommandFailed(content="User is not jailed") if member != ctx.author: if member.top_role == ctx.author.top_role: raise CommandFailed( content= "You can not unjail a user with the same role as you") minutes = max(minutes, 0) reason = self.build_reason(ctx, "Released", minutes, reason, past=True) if minutes: await self.remove_roles(ctx, member, minutes, PunishAction.RELIEVE_JAIL, reason) else: await self.bot.punish.unjail(ctx.guild, member, reason)
async def nick(self, ctx, member: MemberConv, nick: str = None): """ Changes or reset a member's nickname. """ logger.info("Setting the nickname of user '%s' (%d) to %r", member.name, member.id, nick) if member.top_role >= ctx.me.top_role: raise ManualCheckFailure( "I don't have permission to nick this user") mod = user_discrim(ctx.author) await member.edit( nick=nick, reason=f"{mod} {'un' if nick is None else ''}set nickname")
async def perform_focus(self, ctx, member, minutes, reason): roles = self.bot.sql.settings.get_special_roles(ctx.guild) if roles.focus is None: raise CommandFailed(content="No configured focus role") if member.top_role >= ctx.me.top_role: raise ManualCheckFailure( "I don't have permission to focus this user") if roles.focus_role in member.roles: raise CommandFailed(content="User is already focused") minutes = max(minutes, 0) reason = self.build_reason(ctx, "Focus", minutes, reason) await self.bot.punish.focus(ctx.guild, member, reason)
async def perform_jail(self, ctx, member, minutes, reason): roles = self.bot.sql.settings.get_special_roles(ctx.guild) if roles.jail is None: raise CommandFailed(content="No configured jail role") if member.top_role >= ctx.me.top_role: raise ManualCheckFailure("I don't have permission to jail this user") minutes = max(minutes, 0) reason = self.build_reason(ctx, "Jailed", minutes, reason) await self.bot.punish.jail(ctx.guild, member, reason) # If a delayed event, schedule a Navi task if minutes: await self.remove_roles( ctx, member, minutes, PunishAction.RELIEVE_JAIL, reason )
def check_roles(self, ctx, roles): if not roles: raise CommandFailed() assignable_roles = self.bot.sql.roles.get_assignable_roles(ctx.guild) for role in roles: if role not in assignable_roles: embed = discord.Embed(colour=discord.Colour.red()) embed.set_author(name="Role not assignable") embed.description = f"The role {role.mention} cannot be self-assigned" raise CommandFailed(embed=embed) if role >= ctx.me.top_role: embed = discord.Embed(colour=discord.Colour.red()) embed.set_author(name="Error assigning roles") embed.description = ( f"Cannot assign {role.mention}, which is above me in the hierarchy" ) raise ManualCheckFailure(embed=embed)
async def kick(self, ctx, user: UserConv, *, reason: str): """ Kicks the user from the guild with a reason If guild has moderation logging enabled, it is logged """ try: embed = discord.Embed(description="Done! User Kicked") embed.add_field(name="Reason", value=reason) # Don't send a journal event, that is handled by the moderation journal listener await ctx.guild.kick( user, reason=f"{reason} - {user_discrim(ctx.author)}") await ctx.send(embed=embed) except discord.errors.Forbidden: raise ManualCheckFailure( content="I don't have permission to kick this user")
async def list_emojis(self, ctx, all_guilds=False): contents = [] content = StringBuilder() if all_guilds: if not mod_perm(ctx): raise ManualCheckFailure( content="Only moderators can do this.") guild_emojis = (guild.emojis for guild in self.bot.guilds) emojis = chain(*guild_emojis) else: emojis = ctx.guild.emojis logger.info("Listing all emojis within the guild") for emoji in emojis: managed = "M" if emoji.managed else "" content.writeln( f"- [{emoji}]({emoji.url}) id: `{emoji.id}`, name: `{emoji.name}` {managed}" ) if len(content) > 1900: # Too long, break into new embed contents.append(str(content)) # Start content over content.clear() if content: contents.append(str(content)) for i, content in enumerate(contents): embed = discord.Embed(description=content, colour=discord.Colour.dark_teal()) embed.set_footer(text=f"Page {i + 1}/{len(contents)}") if i == 0: if all_guilds: embed.set_author(name="Emojis in all guilds") else: embed.set_author(name=f"Emojis within {ctx.guild.name}") await ctx.send(embed=embed)
async def max_delete(self, ctx, count: int = None): """ Gets the current setting for maximum messages to bulk delete. If you're an administrator, you can change this value. """ if count is None: # Get max delete messages max_delete_messages = self.bot.sql.settings.get_max_delete_messages( ctx.guild) embed = discord.Embed(colour=discord.Colour.dark_teal()) embed.description = f"Maximum number of messages that can be deleted in bulk is `{max_delete_messages}`" elif not admin_perm(ctx): # Lacking authority to set max delete messages embed = discord.Embed(colour=discord.Colour.red()) embed.description = ( "You do not have permission to set the maximum deletable messages" ) raise ManualCheckFailure(embed=embed) elif count <= 0: # Negative value embed = discord.Embed(colour=discord.Colour.red()) embed.description = "This value must be a positive, non-zero integer" raise CommandFailed(embed=embed) elif count >= 2**32 - 1: # Over a sane upper limit embed = discord.Embed(colour=discord.Colour.red()) embed.description = ( "This value is way too high. Try a more reasonable value.") raise CommandFailed(embed=embed) else: # Set max delete messages with self.bot.sql.transaction(): self.bot.sql.settings.set_max_delete_messages(ctx.guild, count) embed = discord.Embed(colour=discord.Colour.dark_teal()) embed.description = f"Set maximum deletable messages to `{count}`" await ctx.send(embed=embed)
async def mentionable_prefix(self, ctx, value: int = None): """ Gets the current number of typeable characters required for your mention. If you're a moderator, you can change this value. (Set to 0 to disable) """ if value is None: # Get prefix value = self.bot.sql.settings.get_mentionable_name_prefix( ctx.guild) embed = discord.Embed(colour=discord.Colour.dark_teal()) embed.description = ( f"Names must begin with at least {value} typeable character{plural(value)}" if value else "No guild requirement for mentionable names") elif not mod_perm(ctx): # Lacking authority to set mentionable prefix embed = discord.Embed(colour=discord.Colour.red()) embed.description = ( "You do not have permission to set the mentionable name prefix" ) raise ManualCheckFailure(embed=embed) else: # Set prefix if value < 0 or value > 32: embed = discord.Embed() embed.colour = discord.Colour.red() embed.description = "Prefix lengths must be between `0` and `32`." raise CommandFailed(embed=embed) with self.bot.sql.transaction(): self.bot.sql.settings.set_mentionable_name_prefix( ctx.guild, value) embed = discord.Embed(colour=discord.Colour.dark_teal()) embed.description = ( f"Set mentionable prefix to {value} character{plural(value)}" if value else "Disabled mentionable prefix requirement") await ctx.send(embed=embed)
async def softban(self, ctx, user: UserConv, *, reason: str): """ Soft-bans the user from the guild with a reason. If guild has moderation logging enabled, it is logged Soft-ban is a kick that cleans up the chat """ try: embed = discord.Embed(description="Done! User Soft-banned") embed.add_field(name="Reason", value=reason) mod = user_discrim(ctx.author) banned = user_discrim(user) clean_reason = escape_backticks(reason) content = f"{mod} soft-banned {user.mention} ({banned}) with reason: `{clean_reason}`" await ctx.guild.ban(user, reason=f"{reason} - {mod}", delete_message_days=1) await asyncio.sleep(0.1) await ctx.guild.unban(user, reason=f"{reason} - {mod}") await ctx.send(embed=embed) self.journal.send( "member/softban", ctx.guild, content, icon="soft", user=user, reason=reason, cause=ctx.author, ) except discord.errors.Forbidden: raise ManualCheckFailure( content="I don't have permission to soft-ban this user")
async def prefix(self, ctx, *, prefix: str = None): """ Gets the current prefix. If you're a moderator, you can set it too. A trailing underscore is converted into spaces. A single '_' unsets the bot's prefix, and uses the default one. """ if prefix is None: # Get prefix bot_prefix = self.bot.prefix(ctx.guild) embed = discord.Embed(colour=discord.Colour.dark_teal()) if ctx.guild is None: embed.description = "No command prefix, all messages are commands" else: embed.description = f"Prefix for {ctx.guild.name} is `{bot_prefix}`" elif ctx.guild is None and prefix is not None: # Attempt to set prefix outside of guild embed = discord.Embed(colour=discord.Colour.red()) embed.description = "Cannot set a command prefix outside of a server!" raise CommandFailed(embed=embed) elif not mod_perm(ctx): # Lacking authority to set prefix embed = discord.Embed(colour=discord.Colour.red()) embed.description = "You do not have permission to set the prefix" raise ManualCheckFailure(embed=embed) elif prefix == "_": # Unset prefix with self.bot.sql.transaction(): self.bot.sql.settings.set_prefix(ctx.guild, None) bot_prefix = self.bot.prefix(ctx.guild) embed = discord.Embed(colour=discord.Colour.dark_teal()) embed.description = ( f"Unset prefix for {ctx.guild.name}. (Default prefix: `{bot_prefix}`)" ) self.journal.send( "prefix", ctx.guild, "Unset bot command prefix", icon="settings", prefix=None, default_prefix=self.bot.config.default_prefix, ) else: # Set prefix bot_prefix = re.sub(r"_$", " ", prefix) with self.bot.sql.transaction(): self.bot.sql.settings.set_prefix(ctx.guild, bot_prefix) embed = discord.Embed(colour=discord.Colour.dark_teal()) embed.description = f"Set prefix for {ctx.guild.name} to `{bot_prefix}`" self.journal.send( "prefix", ctx.guild, "Unset bot command prefix", icon="settings", prefix=bot_prefix, default_prefix=self.bot.config.default_prefix, ) await ctx.send(embed=embed)