[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 import contextlib
from datetime import datetime, timedelta from datetime import datetime, timedelta
from collections import deque, defaultdict, namedtuple from collections import deque, defaultdict, namedtuple
from typing import cast from typing import Optional, Union, cast
import discord 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 import checks, Config, modlog, commands
from redbot.core.bot import Red from redbot.core.bot import Red
from redbot.core.i18n import Translator, cog_i18n 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 .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 redbot.core.utils.mod import is_mod_or_superior, is_allowed_by_hierarchy, get_audit_reason
from .log import log from .log import log
@ -19,6 +21,18 @@ from redbot.core.utils.common_filters import filter_invites, filter_various_ment
_ = T_ = Translator("Mod", __file__) _ = 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(_) @cog_i18n(_)
class Mod(commands.Cog): class Mod(commands.Cog):
"""Moderation tools.""" """Moderation tools."""
@ -386,131 +400,128 @@ class Mod(commands.Cog):
@commands.bot_has_permissions(ban_members=True) @commands.bot_has_permissions(ban_members=True)
@checks.admin_or_permissions(ban_members=True) @checks.admin_or_permissions(ban_members=True)
async def ban( 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. """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."""
If `<days>` is not a number, it's treated as the first word of result = await self.ban_user(
the reason. Minimum 0 days, maximum 7. Defaults to 0. user=user, ctx=ctx, days=days, reason=reason, create_modlog_case=True
"""
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 result is True:
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:
await ctx.send(_("Done. It was about time.")) await ctx.send(_("Done. It was about time."))
elif isinstance(result, str):
try: await ctx.send(result)
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)
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
@commands.bot_has_permissions(ban_members=True) @commands.bot_has_permissions(ban_members=True)
@checks.admin_or_permissions(ban_members=True) @checks.admin_or_permissions(ban_members=True)
async def hackban(self, ctx: commands.Context, user_id: int, *, reason: str = None): async def hackban(
"""Pre-emptively ban a user from this server. 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 author = ctx.author
guild = ctx.guild 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: if not user_ids:
await ctx.send(_("User is already banned.")) await ctx.send_help()
return return
if not (days >= 0 and days <= 7):
await ctx.send(_("Invalid days. Must be between 0 and 7."))
return
if not guild.me.guild_permissions.ban_members:
return await ctx.send(_("I lack the permissions to do this."))
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) user = guild.get_member(user_id)
if user is not None: if user is not None:
# Instead of replicating all that handling... gets attr from decorator # Instead of replicating all that handling... gets attr from decorator
return await ctx.invoke(self.ban, user, None, reason=reason) try:
user = discord.Object(id=user_id) # User not in the guild, but 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) audit_reason = get_audit_reason(author, reason)
queue_entry = (guild.id, user_id) queue_entry = (guild.id, user_id)
self.ban_queue.append(queue_entry) self.ban_queue.append(queue_entry)
try: try:
await guild.ban(user, reason=audit_reason) await guild.ban(user, reason=audit_reason, delete_message_days=days)
log.info("{}({}) hackbanned {}".format(author.name, author.id, user_id)) log.info("{}({}) hackbanned {}".format(author.name, author.id, user_id))
except discord.NotFound: except discord.NotFound:
self.ban_queue.remove(queue_entry) self.ban_queue.remove(queue_entry)
await ctx.send(_("User not found. Have you provided the correct user ID?")) errors[user_id] = _("User {} does not exist.".format(user_id))
continue
except discord.Forbidden: except discord.Forbidden:
self.ban_queue.remove(queue_entry) self.ban_queue.remove(queue_entry)
await ctx.send(_("I lack the permissions to do this.")) errors[user_id] = _("Could not ban {}: missing permissions.".format(user_id))
continue
else: else:
await ctx.send(_("Done. The user will not be able to join this server.")) banned.append(user_id)
user_info = await self.bot.get_user_info(user_id) user_info = await self.bot.get_user_info(user_id)
try: try:
await modlog.create_case( await modlog.create_case(
self.bot, self.bot,
@ -524,7 +535,9 @@ class Mod(commands.Cog):
channel=None, channel=None,
) )
except RuntimeError as e: except RuntimeError as e:
await ctx.send(e) errors["0"] = _("Failed to create modlog case: {}".format(e))
await show_results()
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
@ -1423,6 +1436,69 @@ class Mod(commands.Cog):
else: else:
await ctx.send(_("That user doesn't have any recorded name or nickname change.")) 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): async def get_names_and_nicks(self, user):
names = await self.settings.user(user).past_names() names = await self.settings.user(user).past_names()
nicks = await self.settings.member(user).past_nicks() nicks = await self.settings.member(user).past_nicks()