"""JARVIS BanCog.""" import re from dis_snek import InteractionContext, Permissions from dis_snek.client.utils.misc_utils import find, find_all from dis_snek.ext.paginators import Paginator from dis_snek.models.discord.embed import EmbedField from dis_snek.models.discord.user import User from dis_snek.models.snek.application_commands import ( OptionTypes, SlashCommand, SlashCommandChoice, slash_command, slash_option, ) from dis_snek.models.snek.command import check from jarvis_core.db import q from jarvis_core.db.models import Ban, Unban from jarvis.utils import build_embed from jarvis.utils.cogs import ModcaseCog from jarvis.utils.permissions import admin_or_permissions class BanCog(ModcaseCog): """JARVIS BanCog.""" async def discord_apply_ban( self, ctx: InteractionContext, reason: str, user: User, duration: int, active: bool, fields: list, mtype: str, ) -> None: """Apply a Discord ban.""" await ctx.guild.ban(user, reason=reason) b = Ban( user=user.id, username=user.username, discrim=user.discriminator, reason=reason, admin=ctx.author.id, guild=ctx.guild.id, type=mtype, duration=duration, active=active, ) await b.commit() embed = build_embed( title="User Banned", description=f"Reason: {reason}", fields=fields, ) embed.set_author( name=user.display_name, icon_url=user.avatar.url, ) embed.set_thumbnail(url=user.avatar.url) embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}") await ctx.send(embed=embed) async def discord_apply_unban(self, ctx: InteractionContext, user: User, reason: str) -> None: """Apply a Discord unban.""" await ctx.guild.unban(user, reason=reason) u = Unban( user=user.id, username=user.username, discrim=user.discriminator, guild=ctx.guild.id, admin=ctx.author.id, reason=reason, ) await u.commit() embed = build_embed( title="User Unbanned", description=f"<@{user.id}> was unbanned", fields=[EmbedField(name="Reason", value=reason)], ) embed.set_author( name=user.username, icon_url=user.avatar.url, ) embed.set_thumbnail(url=user.avatar.url) embed.set_footer(text=f"{user.username}#{user.discriminator} | {user.id}") await ctx.send(embed=embed) @slash_command(name="ban", description="Ban a user") @slash_option(name="user", description="User to ban", opt_type=OptionTypes.USER, required=True) @slash_option( name="reason", description="Ban reason", opt_type=OptionTypes.STRING, required=True ) @slash_option( name="btype", description="Ban type", opt_type=OptionTypes.STRING, required=True, choices=[ SlashCommandChoice(name="Permanent", value="perm"), SlashCommandChoice(name="Temporary", value="temp"), SlashCommandChoice(name="Soft", value="soft"), ], ) @slash_option( name="duration", description="Temp ban duration in hours", opt_type=OptionTypes.INTEGER, required=False, ) @check(admin_or_permissions(Permissions.BAN_MEMBERS)) async def _ban( self, ctx: InteractionContext, user: User, reason: str, btype: str = "perm", duration: int = 4, ) -> None: if user.id == ctx.author.id: await ctx.send("You cannot ban yourself.", ephemeral=True) return if user.id == self.bot.user.id: await ctx.send("I'm afraid I can't let you do that", ephemeral=True) return if btype == "temp" and duration < 0: await ctx.send("You cannot set a temp ban to < 0 hours.", ephemeral=True) return elif btype == "temp" and duration > 744: await ctx.send("You cannot set a temp ban to > 1 month", ephemeral=True) return if len(reason) > 100: await ctx.send("Reason must be < 100 characters", ephemeral=True) return await ctx.defer() mtype = btype if mtype == "perm": mtype = "perma" guild_name = ctx.guild.name user_message = f"You have been {mtype}banned from {guild_name}." + f" Reason:\n{reason}" if mtype == "temp": user_message += f"\nDuration: {duration} hours" fields = [EmbedField(name="Type", value=mtype)] if mtype == "temp": fields.append(EmbedField(name="Duration", value=f"{duration} hour(s)")) user_embed = build_embed( title=f"You have been banned from {ctx.guild.name}", description=f"Reason: {reason}", fields=fields, ) user_embed.set_author( name=ctx.author.username + "#" + ctx.author.discriminator, icon_url=ctx.author.avatar, ) user_embed.set_thumbnail(url=ctx.guild.icon.url) try: await user.send(embed=user_embed) except Exception: send_failed = True try: await ctx.guild.ban(user, reason=reason) except Exception as e: await ctx.send(f"Failed to ban user:\n```\n{e}\n```", ephemeral=True) return send_failed = False if mtype == "soft": await ctx.guild.unban(user, reason="Ban was softban") fields.append(EmbedField(name="DM Sent?", value=str(not send_failed))) if btype != "temp": duration = None active = True if btype == "soft": active = False await self.discord_apply_ban(ctx, reason, user, duration, active, fields, mtype) @slash_command(name="unban", description="Unban a user") @slash_option( name="user", description="User to unban", opt_type=OptionTypes.STRING, required=True ) @slash_option( name="reason", description="Unban reason", opt_type=OptionTypes.STRING, required=True ) @check(admin_or_permissions(Permissions.BAN_MEMBERS)) async def _unban( self, ctx: InteractionContext, user: str, reason: str, ) -> None: if len(reason) > 100: await ctx.send("Reason must be < 100 characters", ephemeral=True) return orig_user = user discrim = None discord_ban_info = None database_ban_info = None bans = await ctx.guild.fetch_bans() # Try to get ban information out of Discord self.logger.debug(f"{user}") if re.match(r"^[0-9]{1,}$", user): # User ID user = int(user) discord_ban_info = find(lambda x: x.user.id == user, bans) else: # User name if re.match(r"#[0-9]{4}$", user): # User name has discrim user, discrim = user.split("#") if discrim: discord_ban_info = find( lambda x: x.user.username == user and x.user.discriminator == discrim, bans, ) else: results = find_all(lambda x: x.user.username == user, bans) if results: if len(results) > 1: active_bans = [] for ban in bans: active_bans.append( "{0} ({1}): {2}".format(ban.user.username, ban.user.id, ban.reason) ) ab_message = "\n".join(active_bans) message = ( "More than one result. " f"Please use one of the following IDs:\n```{ab_message}\n```" ) await ctx.send(message) return else: discord_ban_info = results[0] # If we don't have the ban information in Discord, # try to find the relevant information in the database. # We take advantage of the previous checks to save CPU cycles if not discord_ban_info: if isinstance(user, User): database_ban_info = await Ban.find_one( q(guild=ctx.guild.id, user=user.id, active=True) ) else: search = { "guild": ctx.guild.id, "username": user, "active": True, } if discrim: search["discrim"] = discrim database_ban_info = await Ban.find_one(q(**search)) if not discord_ban_info and not database_ban_info: await ctx.send(f"Unable to find user {orig_user}", ephemeral=True) elif discord_ban_info and not database_ban_info: await self.discord_apply_unban(ctx, discord_ban_info.user, reason) else: discord_ban_info = find(lambda x: x.user.id == database_ban_info.id, bans) if discord_ban_info: await self.discord_apply_unban(ctx, discord_ban_info.user, reason) else: database_ban_info.active = False database_ban_info.save() _ = Unban( user=database_ban_info.user, username=database_ban_info.username, discrim=database_ban_info.discrim, guild=ctx.guild.id, admin=ctx.author.id, reason=reason, ).save() await ctx.send("Unable to find user in Discord, but removed entry from database.") bans = SlashCommand(name="bans", description="User bans") @bans.subcommand(sub_cmd_name="list", sub_cmd_description="List bans") @slash_option( name="btype", description="Ban type", opt_type=OptionTypes.INTEGER, required=False, choices=[ SlashCommandChoice(name="All", value=0), SlashCommandChoice(name="Permanent", value=1), SlashCommandChoice(name="Temporary", value=2), SlashCommandChoice(name="Soft", value=3), ], ) @slash_option( name="active", description="Active bans", opt_type=OptionTypes.BOOLEAN, required=False, ) @check(admin_or_permissions(Permissions.BAN_MEMBERS)) async def _bans_list( self, ctx: InteractionContext, btype: int = 0, active: bool = True ) -> None: types = [0, "perm", "temp", "soft"] search = {"guild": ctx.guild.id} if active: search["active"] = True if btype > 0: search["type"] = types[btype] bans = await Ban.find(search).sort([("created_at", -1)]).to_list(None) db_bans = [] fields = [] for ban in bans: if not ban.username: user = await self.bot.fetch_user(ban.user) ban.username = user.username if user else "[deleted user]" fields.append( EmbedField( name=f"Username: {ban.username}#{ban.discrim}", value=( f"Date: {ban.created_at.strftime('%d-%m-%Y')}\n" f"User ID: {ban.user}\n" f"Reason: {ban.reason}\n" f"Type: {ban.type}\n\u200b" ), inline=False, ) ) db_bans.append(ban.user) if type == 0 and active: bans = await ctx.guild.bans() for ban in bans: if ban.user.id not in db_bans: fields.append( EmbedField( name=f"Username: {ban.user.username}#" + f"{ban.user.discriminator}", value=( f"Date: [unknown]\n" f"User ID: {ban.user.id}\n" f"Reason: {ban.reason}\n" "Type: manual\n\u200b" ), inline=False, ) ) pages = [] title = "Active " if active else "Inactive " if btype > 0: title += types[btype] if btype == 1: title += "a" title += "bans" if len(fields) == 0: embed = build_embed( title=title, description=f"No {'in' if not active else ''}active bans", fields=[], ) embed.set_thumbnail(url=ctx.guild.icon.url) pages.append(embed) else: for i in range(0, len(bans), 5): embed = build_embed(title=title, description="", fields=fields[i : i + 5]) embed.set_thumbnail(url=ctx.guild.icon.url) pages.append(embed) paginator = Paginator.create_from_embeds(self.bot, *pages, timeout=300) await paginator.send(ctx)