jarvis-bot/jarvis/cogs/admin/ban.py

389 lines
14 KiB
Python

"""J.A.R.V.I.S. BanCog."""
import re
from datetime import datetime, timedelta
from dis_snek import InteractionContext, Permissions, Snake
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,
SlashCommandChoice,
slash_command,
slash_option,
)
from dis_snek.models.snek.command import check
from jarvis.db.models import Ban, Unban
from jarvis.utils import build_embed, find, find_all
from jarvis.utils.cachecog import CacheCog
from jarvis.utils.permissions import admin_or_permissions
class BanCog(CacheCog):
"""J.A.R.V.I.S. BanCog."""
def __init__(self, bot: Snake):
super().__init__(bot)
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)
_ = 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,
).save()
embed = build_embed(
title="User Banned",
description=f"Reason: {reason}",
fields=fields,
)
embed.set_author(
name=user.display_name,
icon_url=user.avatar,
)
embed.set_thumbnail(url=user.avatar)
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)
_ = Unban(
user=user.id,
username=user.username,
discrim=user.discriminator,
guild=ctx.guild.id,
admin=ctx.author.id,
reason=reason,
).save()
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,
)
embed.set_thumbnail(url=user.avatar)
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"),
],
)
@check(admin_or_permissions(Permissions.BAN_MEMBERS))
async def _ban(
self,
ctx: InteractionContext,
reason: str,
user: User = None,
btype: str = "perm",
duration: int = 4,
) -> None:
if not user or user == ctx.author:
await ctx.send("You cannot ban yourself.", ephemeral=True)
return
if user == self.bot.user:
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.bans()
# Try to get ban information out of Discord
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, int):
database_ban_info = Ban.objects(guild=ctx.guild.id, user=user, active=True).first()
else:
search = {
"guild": ctx.guild.id,
"username": user,
"active": True,
}
if discrim:
search["discrim"] = discrim
database_ban_info = Ban.objects(**search).first()
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:
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."
)
@slash_command(
name="bans", description="User bans", 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.INTEGER,
required=False,
choices=[SlashCommandChoice(name="Yes", value=1), SlashCommandChoice(name="No", value=0)],
)
@check(admin_or_permissions(Permissions.BAN_MEMBERS))
async def _bans_list(self, ctx: InteractionContext, type: int = 0, active: int = 1) -> None:
active = bool(active)
exists = self.check_cache(ctx, type=type, active=active)
if exists:
await ctx.defer(ephemeral=True)
await ctx.send(
f"Please use existing interaction: {exists['paginator']._message.jump_url}",
ephemeral=True,
)
return
types = [0, "perm", "temp", "soft"]
search = {"guild": ctx.guild.id}
if active:
search["active"] = True
if type > 0:
search["type"] = types[type]
bans = Ban.objects(**search).order_by("-created_at")
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 type > 0:
title += types[type]
if type == 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)
self.cache[hash(paginator)] = {
"guild": ctx.guild.id,
"user": ctx.author.id,
"timeout": datetime.utcnow() + timedelta(minutes=5),
"command": ctx.subcommand_name,
"type": type,
"active": active,
"paginator": paginator,
}
await paginator.send(ctx)