[Mod] Enhanced [p]hackban (#2164)

This commit is contained in:
Twentysix 2019-01-05 03:29:05 +01:00 committed by GitHub
parent bcc50557a9
commit 937d2fe0f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -2,14 +2,16 @@ import asyncio
import contextlib
from datetime import datetime, timedelta
from collections import deque, defaultdict, namedtuple
from typing import cast
from typing import Optional, Union, cast
import discord
from discord.ext.commands.converter import Converter, Greedy
from discord.ext.commands.errors import BadArgument
from redbot.core import checks, Config, modlog, commands
from redbot.core.bot import Red
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.chat_formatting import box, escape
from redbot.core.utils.chat_formatting import box, escape, pagify
from .checks import mod_or_voice_permissions, admin_or_voice_permissions, bot_has_voice_permissions
from redbot.core.utils.mod import is_mod_or_superior, is_allowed_by_hierarchy, get_audit_reason
from .log import log
@ -19,6 +21,18 @@ from redbot.core.utils.common_filters import filter_invites, filter_various_ment
_ = T_ = Translator("Mod", __file__)
class RawUserIds(Converter):
async def convert(self, ctx, argument):
# This is for the hackban command, where we receive IDs that
# are most likely not in the guild.
# As long as it's numeric and long enough, it makes a good candidate
# to attempt a ban on
if argument.isnumeric() and len(argument) >= 17:
return int(argument)
raise BadArgument("{} doesn't look like a valid user ID.".format(argument))
@cog_i18n(_)
class Mod(commands.Cog):
"""Moderation tools."""
@ -386,145 +400,144 @@ class Mod(commands.Cog):
@commands.bot_has_permissions(ban_members=True)
@checks.admin_or_permissions(ban_members=True)
async def ban(
self, ctx: commands.Context, user: discord.Member, days: str = None, *, reason: str = None
self,
ctx: commands.Context,
user: discord.Member,
days: Optional[int] = 0,
*,
reason: str = None,
):
"""Ban a user from this server.
Deletes `<days>` worth of messages.
If days is not a number, it's treated as the first word of the reason.
Minimum 0 days, maximum 7. Defaults to 0."""
result = await self.ban_user(
user=user, ctx=ctx, days=days, reason=reason, create_modlog_case=True
)
If `<days>` is not a number, it's treated as the first word of
the reason. Minimum 0 days, maximum 7. Defaults to 0.
"""
author = ctx.author
guild = ctx.guild
if author == user:
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.settings, guild, author, user):
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 <= user.top_role or user == ctx.guild.owner:
await ctx.send(_("I cannot do that due to discord hierarchy rules"))
return
if days:
if days.isdigit():
days = int(days)
else:
if reason:
reason = "{} {}".format(days, reason)
else:
reason = days
days = 0
else:
days = 0
audit_reason = get_audit_reason(author, reason)
if days < 0 or days > 7:
await ctx.send(_("Invalid days. Must be between 0 and 7."))
return
queue_entry = (guild.id, user.id)
self.ban_queue.append(queue_entry)
try:
await guild.ban(user, reason=audit_reason, delete_message_days=days)
log.info(
"{}({}) banned {}({}), deleting {} days worth of messages".format(
author.name, author.id, user.name, user.id, str(days)
)
)
except discord.Forbidden:
self.ban_queue.remove(queue_entry)
await ctx.send(_("I'm not allowed to do that."))
except Exception as e:
self.ban_queue.remove(queue_entry)
print(e)
else:
if result is True:
await ctx.send(_("Done. It was about time."))
try:
await modlog.create_case(
self.bot,
guild,
ctx.message.created_at,
"ban",
user,
author,
reason,
until=None,
channel=None,
)
except RuntimeError as e:
await ctx.send(e)
elif isinstance(result, str):
await ctx.send(result)
@commands.command()
@commands.guild_only()
@commands.bot_has_permissions(ban_members=True)
@checks.admin_or_permissions(ban_members=True)
async def hackban(self, ctx: commands.Context, user_id: int, *, reason: str = None):
"""Pre-emptively ban a user from this server.
async def hackban(
self,
ctx: commands.Context,
user_ids: Greedy[RawUserIds],
days: Optional[int] = 0,
*,
reason: str = None,
):
"""Preemptively bans user(s) from the server
User IDs need to be provided in order to ban
using this command"""
banned = []
errors = {}
async def show_results():
text = _("Banned {} users from the server.".format(len(banned)))
if errors:
text += _("\nErrors:\n")
text += "\n".join(errors.values())
for p in pagify(text):
await ctx.send(p)
def remove_processed(ids):
return [_id for _id in ids if _id not in banned and _id not in errors]
user_ids = list(set(user_ids)) # No dupes
A user ID needs to be provided in order to ban
using this command.
"""
author = ctx.author
guild = ctx.guild
is_banned = False
ban_list = await guild.bans()
for entry in ban_list:
if entry.user.id == user_id:
is_banned = True
break
if is_banned:
await ctx.send(_("User is already banned."))
if not user_ids:
await ctx.send_help()
return
user = guild.get_member(user_id)
if user is not None:
# Instead of replicating all that handling... gets attr from decorator
return await ctx.invoke(self.ban, user, None, reason=reason)
user = discord.Object(id=user_id) # User not in the guild, but
if not (days >= 0 and days <= 7):
await ctx.send(_("Invalid days. Must be between 0 and 7."))
return
audit_reason = get_audit_reason(author, reason)
queue_entry = (guild.id, user_id)
self.ban_queue.append(queue_entry)
try:
await guild.ban(user, reason=audit_reason)
log.info("{}({}) hackbanned {}".format(author.name, author.id, user_id))
except discord.NotFound:
self.ban_queue.remove(queue_entry)
await ctx.send(_("User not found. Have you provided the correct user ID?"))
except discord.Forbidden:
self.ban_queue.remove(queue_entry)
await ctx.send(_("I lack the permissions to do this."))
else:
await ctx.send(_("Done. The user will not be able to join this server."))
if not guild.me.guild_permissions.ban_members:
return await ctx.send(_("I lack the permissions to do this."))
user_info = await self.bot.get_user_info(user_id)
try:
await modlog.create_case(
self.bot,
guild,
ctx.message.created_at,
"hackban",
user_info,
author,
reason,
until=None,
channel=None,
)
except RuntimeError as e:
await ctx.send(e)
ban_list = await guild.bans()
for entry in ban_list:
for user_id in user_ids:
if entry.user.id == user_id:
errors[user_id] = _("User {} is already banned.".format(user_id))
user_ids = remove_processed(user_ids)
if not user_ids:
await show_results()
return
for user_id in user_ids:
user = guild.get_member(user_id)
if user is not None:
# Instead of replicating all that handling... gets attr from decorator
try:
result = await self.ban_user(
user=user, ctx=ctx, days=days, reason=reason, create_modlog_case=True
)
if result is True:
banned.append(user_id)
else:
errors[user_id] = _("Failed to ban user {}: {}".format(user_id, result))
except Exception as e:
errors[user_id] = _("Failed to ban user {}: {}".format(user_id, e))
user_ids = remove_processed(user_ids)
if not user_ids:
await show_results()
return
for user_id in user_ids:
user = discord.Object(id=user_id)
audit_reason = get_audit_reason(author, reason)
queue_entry = (guild.id, user_id)
self.ban_queue.append(queue_entry)
try:
await guild.ban(user, reason=audit_reason, delete_message_days=days)
log.info("{}({}) hackbanned {}".format(author.name, author.id, user_id))
except discord.NotFound:
self.ban_queue.remove(queue_entry)
errors[user_id] = _("User {} does not exist.".format(user_id))
continue
except discord.Forbidden:
self.ban_queue.remove(queue_entry)
errors[user_id] = _("Could not ban {}: missing permissions.".format(user_id))
continue
else:
banned.append(user_id)
user_info = await self.bot.get_user_info(user_id)
try:
await modlog.create_case(
self.bot,
guild,
ctx.message.created_at,
"hackban",
user_info,
author,
reason,
until=None,
channel=None,
)
except RuntimeError as e:
errors["0"] = _("Failed to create modlog case: {}".format(e))
await show_results()
@commands.command()
@commands.guild_only()
@ -1423,6 +1436,69 @@ class Mod(commands.Cog):
else:
await ctx.send(_("That user doesn't have any recorded name or nickname change."))
async def ban_user(
self,
user: discord.Member,
ctx: commands.Context,
days: Optional[int] = 0,
reason: str = None,
create_modlog_case=False,
) -> Union[str, bool]:
author = ctx.author
guild = ctx.guild
if author == user:
return _("I cannot let you do that. Self-harm is bad {}").format("\N{PENSIVE FACE}")
elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user):
return _(
"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 _("I cannot do that due to discord hierarchy rules")
elif not (days >= 0 and days <= 7):
return _("Invalid days. Must be between 0 and 7.")
audit_reason = get_audit_reason(author, reason)
queue_entry = (guild.id, user.id)
self.ban_queue.append(queue_entry)
try:
await guild.ban(user, reason=audit_reason, delete_message_days=days)
log.info(
"{}({}) banned {}({}), deleting {} days worth of messages".format(
author.name, author.id, user.name, user.id, str(days)
)
)
except discord.Forbidden:
self.ban_queue.remove(queue_entry)
return _("I'm not allowed to do that.")
except Exception as e:
self.ban_queue.remove(queue_entry)
return e
if create_modlog_case:
try:
await modlog.create_case(
self.bot,
guild,
ctx.message.created_at,
"ban",
user,
author,
reason,
until=None,
channel=None,
)
except RuntimeError as e:
return _(
"The user was banned but an error occurred when trying to "
"create the modlog entry: {}".format(e)
)
return True
async def get_names_and_nicks(self, user):
names = await self.settings.user(user).past_names()
nicks = await self.settings.member(user).past_nicks()