836 lines
34 KiB
Python
836 lines
34 KiB
Python
import contextlib
|
|
import logging
|
|
import re
|
|
from datetime import datetime, timedelta, timezone
|
|
from typing import Literal, Optional, Tuple, Union
|
|
|
|
import discord
|
|
import TagScriptEngine as tse
|
|
from redbot.cogs.mod.mod import Mod as ModClass
|
|
from redbot.cogs.mod.utils import is_allowed_by_hierarchy
|
|
from redbot.core import Config, app_commands, checks, commands, modlog
|
|
from redbot.core.bot import Red
|
|
from redbot.core.utils.chat_formatting import bold, box, humanize_timedelta
|
|
from redbot.core.utils.mod import get_audit_reason
|
|
|
|
from ._tagscript import (
|
|
TagScriptConverter,
|
|
ban_message,
|
|
kick_message,
|
|
process_tagscript,
|
|
tempban_message,
|
|
unban_message,
|
|
)
|
|
|
|
log = logging.getLogger("red.flarecogs.mod")
|
|
|
|
from discord.ext import commands as dpy_commands
|
|
from discord.ext.commands import BadArgument
|
|
|
|
ID_REGEX = re.compile(r"([0-9]{15,20})")
|
|
USER_MENTION_REGEX = re.compile(r"<@!?([0-9]{15,21})>$")
|
|
|
|
|
|
# https://github.com/flaree/Red-DiscordBot/blob/FR-custom-bankick-msgs/redbot/core/commands/converter.py#L207
|
|
class RawUserIdConverter(dpy_commands.Converter):
|
|
"""
|
|
Converts ID or user mention to an `int`.
|
|
Useful for commands like ``[p]ban`` or ``[p]unban`` where the bot is not necessarily
|
|
going to share any servers with the user that a moderator wants to ban/unban.
|
|
This converter doesn't check if the ID/mention points to an actual user
|
|
but it won't match IDs and mentions that couldn't possibly be valid.
|
|
For example, the converter will not match on "123" because the number doesn't have
|
|
enough digits to be valid ID but, it will match on "12345678901234567" even though
|
|
there is no user with such ID.
|
|
"""
|
|
|
|
async def convert(self, ctx, argument: str) -> int:
|
|
# This is for the hackban and unban commands, where we receive IDs that
|
|
# are most likely not in the guild.
|
|
# Mentions are supported, but most likely won't ever be in cache.
|
|
|
|
if match := ID_REGEX.match(argument) or USER_MENTION_REGEX.match(argument):
|
|
return int(match.group(1))
|
|
|
|
raise BadArgument(("'{input}' doesn't look like a valid user ID.").format(input=argument))
|
|
|
|
|
|
class Mod(ModClass):
|
|
"""Mod with custom messages."""
|
|
|
|
modset = ModClass.modset.copy()
|
|
|
|
__version__ = "1.3.0"
|
|
|
|
def format_help_for_context(self, ctx: commands.Context):
|
|
pre_processed = super().format_help_for_context(ctx)
|
|
return f"{pre_processed}\nCog Version: {self.__version__}"
|
|
|
|
def __init__(self, bot: Red):
|
|
super().__init__(bot)
|
|
self.bot: Red = bot
|
|
self._config: Config = Config.get_conf(self, 95932766180343808, force_registration=True)
|
|
self._config.register_guild(
|
|
**{
|
|
"kick_message": kick_message,
|
|
"ban_message": ban_message,
|
|
"tempban_message": tempban_message,
|
|
"unban_message": unban_message,
|
|
"require_reason": False,
|
|
}
|
|
)
|
|
|
|
async def red_get_data_for_user(self, *, user_id: int):
|
|
# this cog does not story any data
|
|
return {}
|
|
|
|
async def red_delete_data_for_user(
|
|
self,
|
|
*,
|
|
requester: Literal["discord_deleted_user", "owner", "user", "user_strict"],
|
|
user_id: int,
|
|
):
|
|
return None
|
|
|
|
@modset.command()
|
|
@commands.guild_only()
|
|
async def kickmessage(self, ctx: commands.Context, *, message: TagScriptConverter):
|
|
"""Set the message sent when a user is kicked.
|
|
|
|
**Blocks:**
|
|
- [Assignment Block](https://seina-cogs.readthedocs.io/en/latest/tags/tse_blocks.html#assignment-block)
|
|
- [Embed Block](https://seina-cogs.readthedocs.io/en/latest/tags/parsing_blocks.html#embed-block)
|
|
|
|
**Variables:**
|
|
- `{user}`: [member that was kicked.](https://seina-cogs.readthedocs.io/en/latest/tags/default_variables.html#author-block)
|
|
- `{moderator}`: [modrator that kicked the member.](https://seina-cogs.readthedocs.io/en/latest/tags/default_variables.html#author-block)
|
|
- `{reason}`: reason for the kick.
|
|
- `{guild}`: [server](https://seina-cogs.readthedocs.io/en/latest/tags/default_variables.html#server-block)
|
|
"""
|
|
guild = ctx.guild
|
|
await self._config.guild(guild).kick_message.set(message)
|
|
await ctx.send("Kick message updated:\n{}".format(box(str(message), lang="json")))
|
|
|
|
@modset.command()
|
|
@commands.guild_only()
|
|
async def banmessage(self, ctx: commands.Context, *, message: TagScriptConverter):
|
|
"""Set the message sent when a user is banned.
|
|
|
|
**Blocks:**
|
|
- [Assignment Block](https://seina-cogs.readthedocs.io/en/latest/tags/tse_blocks.html#assignment-block)
|
|
- [Embed Block](https://seina-cogs.readthedocs.io/en/latest/tags/parsing_blocks.html#embed-block)
|
|
|
|
**Variables:**
|
|
- `{user}`: [member that was banned.](https://seina-cogs.readthedocs.io/en/latest/tags/default_variables.html#author-block)
|
|
- `{moderator}`: [modrator that banned the member.](https://seina-cogs.readthedocs.io/en/latest/tags/default_variables.html#author-block)
|
|
- `{reason}`: reason for the ban.
|
|
- `{guild}`: [server](https://seina-cogs.readthedocs.io/en/latest/tags/default_variables.html#server-block)
|
|
- `{days}`: number of days of messages deleted.
|
|
"""
|
|
guild = ctx.guild
|
|
await self._config.guild(guild).ban_message.set(message)
|
|
await ctx.send("Ban message updated:\n{}".format(box(str(message), lang="json")))
|
|
|
|
@modset.command()
|
|
@commands.guild_only()
|
|
async def tempbanmessage(self, ctx: commands.Context, *, message: TagScriptConverter):
|
|
"""Set the message sent when a user is tempbanned.
|
|
|
|
**Blocks:**
|
|
- [Assignment Block](https://seina-cogs.readthedocs.io/en/latest/tags/tse_blocks.html#assignment-block)
|
|
- [Embed Block](https://seina-cogs.readthedocs.io/en/latest/tags/parsing_blocks.html#embed-block)
|
|
|
|
**Variables:**
|
|
- `{user}`: [member that was tempbanned.](https://seina-cogs.readthedocs.io/en/latest/tags/default_variables.html#author-block)
|
|
- `{moderator}`: [modrator that tempbanned the member.](https://seina-cogs.readthedocs.io/en/latest/tags/default_variables.html#author-block)
|
|
- `{reason}`: reason for the tempban.
|
|
- `{guild}`: [server](https://seina-cogs.readthedocs.io/en/latest/tags/default_variables.html#server-block)
|
|
- `{days}`: number of days of messages deleted.
|
|
- `{duration}`: duration of the tempban.
|
|
"""
|
|
guild = ctx.guild
|
|
await self._config.guild(guild).tempban_message.set(message)
|
|
await ctx.send("Tempban message updated:\n{}".format(box(str(message), lang="json")))
|
|
|
|
@modset.command()
|
|
@commands.guild_only()
|
|
async def unbanmessage(self, ctx: commands.Context, *, message: TagScriptConverter):
|
|
"""Set the message sent when a user is unbanned.
|
|
|
|
**Blocks:**
|
|
- [Assignment Block](https://seina-cogs.readthedocs.io/en/latest/tags/tse_blocks.html#assignment-block)
|
|
- [Embed Block](https://seina-cogs.readthedocs.io/en/latest/tags/parsing_blocks.html#embed-block)
|
|
|
|
**Variables:**
|
|
- `{user}`: [member that was tempbanned.](https://seina-cogs.readthedocs.io/en/latest/tags/default_variables.html#author-block)
|
|
- `{moderator}`: [modrator that tempbanned the member.](https://seina-cogs.readthedocs.io/en/latest/tags/default_variables.html#author-block)
|
|
- `{reason}`: reason for the tempban.
|
|
- `{guild}`: [server](https://seina-cogs.readthedocs.io/en/latest/tags/default_variables.html#server-block)
|
|
"""
|
|
guild = ctx.guild
|
|
await self._config.guild(guild).unban_message.set(message)
|
|
await ctx.send("Unban message updated:\n{}".format(box(str(message), lang="json")))
|
|
|
|
@modset.command(name="showmessages")
|
|
async def modset_showmessages(self, ctx: commands.Context):
|
|
"""Show the current messages for moderation commands."""
|
|
messageData = await self._config.guild(ctx.guild).all()
|
|
msg = "Kick Message: {kick_message}\n".format(kick_message=messageData["kick_message"])
|
|
msg += "Ban Message: {ban_message}\n".format(ban_message=messageData["ban_message"])
|
|
msg += "Tempban Message: {tempban_message}\n".format(
|
|
tempban_message=messageData["tempban_message"]
|
|
)
|
|
msg += "Unban Message: {unban_message}\n".format(
|
|
unban_message=messageData["unban_message"]
|
|
)
|
|
await ctx.send(box(msg))
|
|
|
|
@modset.command(name="reasons")
|
|
async def modset_require_reason(self, ctx: commands.Context, value: bool):
|
|
"""Set whether a reason is required for moderation actions."""
|
|
await self._config.guild(ctx.guild).require_reason.set(value)
|
|
await ctx.send(f"Reason requirement set to {value}")
|
|
|
|
kick = None
|
|
|
|
@commands.hybrid_command()
|
|
@app_commands.describe(member="The member to kick.", reason="The reason for kicking the user.")
|
|
@commands.guild_only()
|
|
@commands.bot_has_permissions(kick_members=True)
|
|
@checks.admin_or_permissions(kick_members=True)
|
|
async def kick(self, ctx: commands.Context, member: discord.Member, *, reason: str = None):
|
|
"""
|
|
Kick a user.
|
|
Examples:
|
|
- `[p]kick 428675506947227648 wanted to be kicked.`
|
|
This will kick the user with ID 428675506947227648 from the server.
|
|
- `[p]kick @Twentysix wanted to be kicked.`
|
|
This will kick Twentysix from the server.
|
|
If a reason is specified, it will be the reason that shows up
|
|
in the audit log.
|
|
"""
|
|
require_reason = await self._config.guild(ctx.guild).require_reason()
|
|
if require_reason and reason is None:
|
|
await ctx.send("You must provide a reason for this action.")
|
|
return
|
|
author = ctx.author
|
|
guild = ctx.guild
|
|
|
|
if author == member:
|
|
await ctx.send(
|
|
("I cannot let you do that. Self-harm is bad {emoji}").format(
|
|
emoji="\N{PENSIVE FACE}"
|
|
)
|
|
)
|
|
return
|
|
elif not await is_allowed_by_hierarchy(self.bot, self.config, guild, author, member):
|
|
await ctx.send(
|
|
(
|
|
"I cannot let you do that. You are "
|
|
"not higher than the user in the role "
|
|
"hierarchy."
|
|
)
|
|
)
|
|
return
|
|
elif ctx.guild.me.top_role <= member.top_role or member == ctx.guild.owner:
|
|
await ctx.send(("I cannot do that due to Discord hierarchy rules."))
|
|
return
|
|
audit_reason = get_audit_reason(author, reason, shorten=True)
|
|
toggle = await self.config.guild(guild).dm_on_kickban()
|
|
if toggle:
|
|
with contextlib.suppress(discord.HTTPException):
|
|
em = discord.Embed(
|
|
title=bold(("You have been kicked from {guild}.").format(guild=guild)),
|
|
color=await self.bot.get_embed_color(member),
|
|
)
|
|
em.add_field(
|
|
name=("**Reason**"),
|
|
value=reason if reason is not None else ("No reason was given."),
|
|
inline=False,
|
|
)
|
|
await member.send(embed=em)
|
|
try:
|
|
await guild.kick(member, reason=audit_reason)
|
|
log.info("{}({}) kicked {}({})".format(author.name, author.id, member.name, member.id))
|
|
except discord.errors.Forbidden:
|
|
await ctx.send("I'm not allowed to do that.")
|
|
except Exception:
|
|
log.exception(
|
|
"{}({}) attempted to kick {}({}), but an error occurred.".format(
|
|
author.name, author.id, member.name, member.id
|
|
)
|
|
)
|
|
else:
|
|
await modlog.create_case(
|
|
self.bot,
|
|
guild,
|
|
ctx.message.created_at,
|
|
"kick",
|
|
member,
|
|
author,
|
|
reason,
|
|
until=None,
|
|
channel=None,
|
|
)
|
|
message = await self._config.guild(ctx.guild).kick_message()
|
|
kwargs = process_tagscript(
|
|
message,
|
|
{
|
|
"user": tse.MemberAdapter(member),
|
|
"moderator": tse.MemberAdapter(author),
|
|
"reason": tse.StringAdapter(str(reason)),
|
|
"guild": tse.GuildAdapter(guild),
|
|
},
|
|
)
|
|
if not kwargs:
|
|
await self._config.guild(ctx.guild).kick_message.clear()
|
|
kwargs = process_tagscript(
|
|
kick_message,
|
|
{
|
|
"user": tse.MemberAdapter(member),
|
|
"moderator": tse.MemberAdapter(author),
|
|
"reason": tse.StringAdapter(str(reason)),
|
|
"guild": tse.GuildAdapter(guild),
|
|
},
|
|
)
|
|
await ctx.send(**kwargs)
|
|
|
|
tempban = None
|
|
|
|
@commands.hybrid_command()
|
|
@app_commands.describe(
|
|
member="The member to tempban.",
|
|
reason="The reason for tempbanning the user.",
|
|
duration="The duration of the tempban.",
|
|
days="The number of days of messages to delete.",
|
|
)
|
|
@commands.guild_only()
|
|
@commands.bot_has_permissions(ban_members=True)
|
|
@checks.admin_or_permissions(ban_members=True)
|
|
async def tempban(
|
|
self,
|
|
ctx: commands.Context,
|
|
member: discord.Member,
|
|
duration: Optional[commands.TimedeltaConverter] = None,
|
|
days: Optional[int] = None,
|
|
*,
|
|
reason: str = None,
|
|
):
|
|
"""Temporarily ban a user from this server.
|
|
`duration` is the amount of time the user should be banned for.
|
|
`days` is the amount of days of messages to cleanup on tempban.
|
|
Examples:
|
|
- `[p]tempban @Twentysix Because I say so`
|
|
This will ban Twentysix for the default amount of time set by an administrator.
|
|
- `[p]tempban @Twentysix 15m You need a timeout`
|
|
This will ban Twentysix for 15 minutes.
|
|
- `[p]tempban 428675506947227648 1d2h15m 5 Evil person`
|
|
This will ban the user with ID 428675506947227648 for 1 day 2 hours 15 minutes and will delete the last 5 days of their messages.
|
|
"""
|
|
require_reason = await self._config.guild(ctx.guild).require_reason()
|
|
if require_reason and reason is None:
|
|
await ctx.send("You must provide a reason for this action.")
|
|
return
|
|
guild = ctx.guild
|
|
author = ctx.author
|
|
|
|
if author == member:
|
|
await ctx.send(
|
|
("I cannot let you do that. Self-harm is bad {}").format("\N{PENSIVE FACE}")
|
|
)
|
|
return
|
|
elif not await is_allowed_by_hierarchy(self.bot, self.config, guild, author, member):
|
|
await ctx.send(
|
|
(
|
|
"I cannot let you do that. You are "
|
|
"not higher than the user in the role "
|
|
"hierarchy."
|
|
)
|
|
)
|
|
return
|
|
elif guild.me.top_role <= member.top_role or member == guild.owner:
|
|
await ctx.send(("I cannot do that due to Discord hierarchy rules."))
|
|
return
|
|
|
|
guild_data = await self.config.guild(guild).all()
|
|
|
|
if duration is None:
|
|
duration = timedelta(seconds=guild_data["default_tempban_duration"])
|
|
unban_time = datetime.now(timezone.utc) + duration
|
|
|
|
if days is None:
|
|
days = guild_data["default_days"]
|
|
|
|
if not (0 <= days <= 7):
|
|
await ctx.send(("Invalid days. Must be between 0 and 7."))
|
|
return
|
|
invite = await self.get_invite_for_reinvite(ctx, int(duration.total_seconds() + 86400))
|
|
|
|
await self.config.member(member).banned_until.set(unban_time.timestamp())
|
|
async with self.config.guild(guild).current_tempbans() as current_tempbans:
|
|
current_tempbans.append(member.id)
|
|
|
|
with contextlib.suppress(discord.HTTPException):
|
|
# We don't want blocked DMs preventing us from banning
|
|
msg = ("You have been temporarily banned from {server_name} until {date}.").format(
|
|
server_name=guild.name, date=discord.utils.format_dt(unban_time)
|
|
)
|
|
if guild_data["dm_on_kickban"] and reason:
|
|
msg += ("\n\n**Reason:** {reason}").format(reason=reason)
|
|
if invite:
|
|
msg += ("\n\nHere is an invite for when your ban expires: {invite_link}").format(
|
|
invite_link=invite
|
|
)
|
|
await member.send(msg)
|
|
|
|
audit_reason = get_audit_reason(author, reason, shorten=True)
|
|
|
|
try:
|
|
await guild.ban(member, reason=audit_reason, delete_message_days=days)
|
|
except discord.Forbidden:
|
|
await ctx.send(("I can't do that for some reason."))
|
|
except discord.HTTPException:
|
|
await ctx.send(("Something went wrong while banning."))
|
|
else:
|
|
await modlog.create_case(
|
|
self.bot,
|
|
guild,
|
|
ctx.message.created_at,
|
|
"tempban",
|
|
member,
|
|
author,
|
|
reason,
|
|
unban_time,
|
|
)
|
|
message = await self._config.guild(ctx.guild).tempban_message()
|
|
humanized_duration = humanize_timedelta(timedelta=duration)
|
|
kwargs = process_tagscript(
|
|
message,
|
|
{
|
|
"user": tse.MemberAdapter(member),
|
|
"moderator": tse.MemberAdapter(author),
|
|
"reason": tse.StringAdapter(str(reason)),
|
|
"guild": tse.GuildAdapter(guild),
|
|
"duration": tse.StringAdapter(humanized_duration),
|
|
"days": tse.IntAdapter(days),
|
|
},
|
|
)
|
|
if not kwargs:
|
|
await self._config.guild(ctx.guild).tempban_message.clear()
|
|
kwargs = process_tagscript(
|
|
tempban_message,
|
|
{
|
|
"user": tse.MemberAdapter(member),
|
|
"moderator": tse.MemberAdapter(author),
|
|
"reason": tse.StringAdapter(str(reason)),
|
|
"guild": tse.GuildAdapter(guild),
|
|
"duration": tse.StringAdapter(humanized_duration),
|
|
"days": tse.IntAdapter(days),
|
|
},
|
|
)
|
|
await ctx.send(**kwargs)
|
|
|
|
softban = None
|
|
|
|
@commands.hybrid_command()
|
|
@app_commands.describe(
|
|
member="The member to softban.", reason="The reason for softbanning the user."
|
|
)
|
|
@commands.guild_only()
|
|
@commands.bot_has_permissions(ban_members=True)
|
|
@checks.admin_or_permissions(ban_members=True)
|
|
async def softban(self, ctx: commands.Context, member: discord.Member, *, reason: str = None):
|
|
"""Kick a user and delete 1 day's worth of their messages."""
|
|
require_reason = await self._config.guild(ctx.guild).require_reason()
|
|
if require_reason and reason is None:
|
|
await ctx.send("You must provide a reason for this action.")
|
|
return
|
|
guild = ctx.guild
|
|
author = ctx.author
|
|
|
|
if author == member:
|
|
await ctx.send(
|
|
("I cannot let you do that. Self-harm is bad {emoji}").format(
|
|
emoji="\N{PENSIVE FACE}"
|
|
)
|
|
)
|
|
return
|
|
elif not await is_allowed_by_hierarchy(self.bot, self.config, guild, author, member):
|
|
await ctx.send(
|
|
(
|
|
"I cannot let you do that. You are "
|
|
"not higher than the user in the role "
|
|
"hierarchy."
|
|
)
|
|
)
|
|
return
|
|
|
|
audit_reason = get_audit_reason(author, reason, shorten=True)
|
|
|
|
invite = await self.get_invite_for_reinvite(ctx)
|
|
|
|
try: # We don't want blocked DMs preventing us from banning
|
|
msg = await member.send(
|
|
(
|
|
"You have been banned and "
|
|
"then unbanned as a quick way to delete your messages.\n"
|
|
"You can now join the server again. {invite_link}"
|
|
).format(invite_link=invite)
|
|
)
|
|
except discord.HTTPException:
|
|
msg = None
|
|
try:
|
|
await guild.ban(member, reason=audit_reason, delete_message_days=1)
|
|
except discord.errors.Forbidden:
|
|
await ctx.send(("My role is not high enough to softban that user."))
|
|
if msg is not None:
|
|
await msg.delete()
|
|
return
|
|
except discord.HTTPException:
|
|
log.exception(
|
|
"{}({}) attempted to softban {}({}), but an error occurred trying to ban them.".format(
|
|
author.name, author.id, member.name, member.id
|
|
)
|
|
)
|
|
return
|
|
try:
|
|
await guild.unban(member)
|
|
except discord.HTTPException:
|
|
log.exception(
|
|
"{}({}) attempted to softban {}({}), but an error occurred trying to unban them.".format(
|
|
author.name, author.id, member.name, member.id
|
|
)
|
|
)
|
|
return
|
|
else:
|
|
log.info(
|
|
"{}({}) softbanned {}({}), deleting 1 day worth "
|
|
"of messages.".format(author.name, author.id, member.name, member.id)
|
|
)
|
|
await modlog.create_case(
|
|
self.bot,
|
|
guild,
|
|
ctx.message.created_at,
|
|
"softban",
|
|
member,
|
|
author,
|
|
reason,
|
|
until=None,
|
|
channel=None,
|
|
)
|
|
message = await self._config.guild(ctx.guild).kick_message()
|
|
kwargs = process_tagscript(
|
|
message,
|
|
{
|
|
"user": tse.MemberAdapter(member),
|
|
"moderator": tse.MemberAdapter(author),
|
|
"reason": tse.StringAdapter(str(reason)),
|
|
"guild": tse.GuildAdapter(guild),
|
|
},
|
|
)
|
|
if not kwargs:
|
|
await self._config.guild(ctx.guild).kick_message.clear()
|
|
kwargs = process_tagscript(
|
|
kick_message,
|
|
{
|
|
"user": tse.MemberAdapter(member),
|
|
"moderator": tse.MemberAdapter(author),
|
|
"reason": tse.StringAdapter(str(reason)),
|
|
"guild": tse.GuildAdapter(guild),
|
|
},
|
|
)
|
|
await ctx.send(**kwargs)
|
|
|
|
@commands.hybrid_command()
|
|
@app_commands.describe(
|
|
user="The member to ban.",
|
|
reason="The reason for banning the user.",
|
|
days="The number of days of messages to delete.",
|
|
)
|
|
@commands.guild_only()
|
|
@commands.bot_has_permissions(ban_members=True)
|
|
@commands.admin_or_permissions(ban_members=True)
|
|
async def ban(
|
|
self,
|
|
ctx: commands.Context,
|
|
user: discord.Member,
|
|
days: Optional[int] = None,
|
|
*,
|
|
reason: str = None,
|
|
):
|
|
"""Ban a user from this server and optionally delete days of messages.
|
|
|
|
`days` is the amount of days of messages to cleanup on ban.
|
|
|
|
Examples:
|
|
- `[p]ban 428675506947227648 7 Continued to spam after told to stop.`
|
|
This will ban the user with ID 428675506947227648 and it will delete 7 days worth of messages.
|
|
- `[p]ban @Twentysix 7 Continued to spam after told to stop.`
|
|
This will ban Twentysix and it will delete 7 days worth of messages.
|
|
|
|
A user ID should be provided if the user is not a member of this server.
|
|
If days is not a number, it's treated as the first word of the reason.
|
|
Minimum 0 days, maximum 7. If not specified, the defaultdays setting will be used instead.
|
|
"""
|
|
require_reason = await self._config.guild(ctx.guild).require_reason()
|
|
if require_reason and reason is None:
|
|
await ctx.send("You must provide a reason for this action.")
|
|
return
|
|
guild = ctx.guild
|
|
if days is None:
|
|
days = await self.config.guild(guild).default_days()
|
|
if isinstance(user, int):
|
|
user = self.bot.get_user(user) or discord.Object(id=user)
|
|
|
|
success, message = await self.ban_user(
|
|
user=user, ctx=ctx, days=days, reason=reason, create_modlog_case=True
|
|
)
|
|
|
|
if not success:
|
|
await ctx.send(message)
|
|
return
|
|
|
|
kwargs = process_tagscript(
|
|
message,
|
|
{
|
|
"user": tse.MemberAdapter(user),
|
|
"moderator": tse.MemberAdapter(ctx.author),
|
|
"reason": tse.StringAdapter(str(reason)),
|
|
"guild": tse.GuildAdapter(guild),
|
|
"days": tse.IntAdapter(int(days)),
|
|
},
|
|
)
|
|
if not kwargs:
|
|
await self._config.guild(ctx.guild).ban_message.clear()
|
|
kwargs = process_tagscript(
|
|
ban_message,
|
|
{
|
|
"user": tse.MemberAdapter(user),
|
|
"moderator": tse.MemberAdapter(ctx.author),
|
|
"reason": tse.StringAdapter(str(reason)),
|
|
"guild": tse.GuildAdapter(guild),
|
|
"days": tse.IntAdapter(int(days)),
|
|
},
|
|
)
|
|
|
|
await ctx.send(**kwargs)
|
|
|
|
ban_user = None
|
|
|
|
async def ban_user(
|
|
self,
|
|
user: Union[discord.Member, discord.User, discord.Object],
|
|
ctx: commands.Context,
|
|
days: int = 0,
|
|
reason: str = None,
|
|
create_modlog_case=False,
|
|
) -> Tuple[bool, str]:
|
|
author = ctx.author
|
|
guild = ctx.guild
|
|
|
|
removed_temp = False
|
|
|
|
if not (0 <= days <= 7):
|
|
return False, ("Invalid days. Must be between 0 and 7.")
|
|
|
|
if isinstance(user, discord.Member):
|
|
if author == user:
|
|
return (
|
|
False,
|
|
("I cannot let you do that. Self-harm is bad {}").format("\N{PENSIVE FACE}"),
|
|
)
|
|
elif not await is_allowed_by_hierarchy(self.bot, self.config, guild, author, user):
|
|
return (
|
|
False,
|
|
(
|
|
"I cannot let you do that. You are "
|
|
"not higher than the user in the role "
|
|
"hierarchy."
|
|
),
|
|
)
|
|
elif guild.me.top_role <= user.top_role or user == guild.owner:
|
|
return False, ("I cannot do that due to Discord hierarchy rules.")
|
|
|
|
toggle = await self.config.guild(guild).dm_on_kickban()
|
|
if toggle:
|
|
with contextlib.suppress(discord.HTTPException):
|
|
em = discord.Embed(
|
|
title=bold(("You have been banned from {guild}.").format(guild=guild)),
|
|
color=await self.bot.get_embed_color(user),
|
|
)
|
|
em.add_field(
|
|
name=("**Reason**"),
|
|
value=reason if reason is not None else ("No reason was given."),
|
|
inline=False,
|
|
)
|
|
await user.send(embed=em)
|
|
|
|
ban_type = "ban"
|
|
else:
|
|
tempbans = await self.config.guild(guild).current_tempbans()
|
|
|
|
try:
|
|
await guild.fetch_ban(user)
|
|
except discord.NotFound:
|
|
pass
|
|
else:
|
|
if user.id in tempbans:
|
|
async with self.config.guild(guild).current_tempbans() as tempbans:
|
|
tempbans.remove(user.id)
|
|
removed_temp = True
|
|
else:
|
|
return (
|
|
False,
|
|
("User with ID {user_id} is already banned.").format(user_id=user.id),
|
|
)
|
|
|
|
ban_type = "hackban"
|
|
|
|
audit_reason = get_audit_reason(author, reason, shorten=True)
|
|
|
|
if removed_temp:
|
|
log.info(
|
|
"{}({}) upgraded the tempban for {} to a permaban.".format(
|
|
author.name, author.id, user.id
|
|
)
|
|
)
|
|
success_message = (
|
|
"User with ID {user(id)} was upgraded from a temporary to a permanent ban."
|
|
)
|
|
else:
|
|
username = user.name if hasattr(user, "name") else "Unknown"
|
|
try:
|
|
await guild.ban(user, reason=audit_reason, delete_message_days=days)
|
|
log.info(
|
|
"{}({}) {}ned {}({}), deleting {} days worth of messages.".format(
|
|
author.name, author.id, ban_type, username, user.id, str(days)
|
|
)
|
|
)
|
|
success_message = await self._config.guild(ctx.guild).ban_message()
|
|
except discord.Forbidden:
|
|
return False, ("I'm not allowed to do that.")
|
|
except discord.NotFound:
|
|
return False, ("User with ID {user_id} not found").format(user_id=user.id)
|
|
except Exception:
|
|
log.exception(
|
|
"{}({}) attempted to {} {}({}), but an error occurred.".format(
|
|
author.name, author.id, ban_type, username, user.id
|
|
)
|
|
)
|
|
return False, ("An unexpected error occurred.")
|
|
if create_modlog_case:
|
|
await modlog.create_case(
|
|
self.bot,
|
|
guild,
|
|
ctx.message.created_at,
|
|
ban_type,
|
|
user,
|
|
author,
|
|
reason,
|
|
until=None,
|
|
channel=None,
|
|
)
|
|
|
|
return True, success_message
|
|
|
|
unban = None
|
|
|
|
@commands.hybrid_command()
|
|
@app_commands.describe(
|
|
user_id="The ID of the user to unban.", reason="The reason for unbanning the user."
|
|
)
|
|
@commands.guild_only()
|
|
@commands.bot_has_permissions(ban_members=True)
|
|
@checks.admin_or_permissions(ban_members=True)
|
|
async def unban(
|
|
self, ctx: commands.Context, user_id: RawUserIdConverter, *, reason: str = None
|
|
):
|
|
"""Unban a user from this server.
|
|
Requires specifying the target user's ID. To find this, you may either:
|
|
1. Copy it from the mod log case (if one was created), or
|
|
2. enable developer mode, go to Bans in this server's settings, right-
|
|
click the user and select 'Copy ID'."""
|
|
require_reason = await self._config.guild(ctx.guild).require_reason()
|
|
if require_reason and reason is None:
|
|
await ctx.send("You must provide a reason for this action.")
|
|
return
|
|
guild = ctx.guild
|
|
author = ctx.author
|
|
audit_reason = get_audit_reason(ctx.author, reason, shorten=True)
|
|
try:
|
|
ban_entry = await guild.fetch_ban(discord.Object(user_id))
|
|
except discord.NotFound:
|
|
await ctx.send(("It seems that user isn't banned!"))
|
|
return
|
|
try:
|
|
await guild.unban(ban_entry.user, reason=audit_reason)
|
|
except discord.HTTPException:
|
|
await ctx.send(("Something went wrong while attempting to unban that user."))
|
|
return
|
|
else:
|
|
await modlog.create_case(
|
|
self.bot,
|
|
guild,
|
|
ctx.message.created_at,
|
|
"unban",
|
|
ban_entry.user,
|
|
author,
|
|
reason,
|
|
until=None,
|
|
channel=None,
|
|
)
|
|
message = await self._config.guild(ctx.guild).unban_message()
|
|
kwargs = process_tagscript(
|
|
message,
|
|
{
|
|
"user": tse.MemberAdapter(ban_entry.user),
|
|
"moderator": tse.MemberAdapter(author),
|
|
"reason": tse.StringAdapter(str(reason)),
|
|
"guild": tse.GuildAdapter(guild),
|
|
},
|
|
)
|
|
if not kwargs:
|
|
await self._config.guild(ctx.guild).unban_message.clear()
|
|
kwargs = process_tagscript(
|
|
ban_message,
|
|
{
|
|
"user": tse.MemberAdapter(user),
|
|
"moderator": tse.MemberAdapter(ctx.author),
|
|
"reason": tse.StringAdapter(str(reason)),
|
|
"guild": tse.GuildAdapter(guild),
|
|
},
|
|
)
|
|
await ctx.send(**kwargs)
|
|
|
|
if await self.config.guild(guild).reinvite_on_unban():
|
|
user = ctx.bot.get_user(user_id)
|
|
if not user:
|
|
await ctx.send(
|
|
("I don't share another server with this user. I can't reinvite them.")
|
|
)
|
|
return
|
|
|
|
invite = await self.get_invite_for_reinvite(ctx)
|
|
if invite:
|
|
try:
|
|
await user.send(
|
|
(
|
|
"You've been unbanned from {server}.\n"
|
|
"Here is an invite for that server: {invite_link}"
|
|
).format(server=guild.name, invite_link=invite)
|
|
)
|
|
except discord.Forbidden:
|
|
await ctx.send(
|
|
(
|
|
"I failed to send an invite to that user. "
|
|
"Perhaps you may be able to send it for me?\n"
|
|
"Here's the invite link: {invite_link}"
|
|
).format(invite_link=invite)
|
|
)
|
|
except discord.HTTPException:
|
|
await ctx.send(
|
|
(
|
|
"Something went wrong when attempting to send that user "
|
|
"an invite. Here's the link so you can try: {invite_link}"
|
|
).format(invite_link=invite)
|
|
)
|