diff --git a/redbot/cogs/mod/kickban.py b/redbot/cogs/mod/kickban.py index 76bf3f916..884898e28 100644 --- a/redbot/cogs/mod/kickban.py +++ b/redbot/cogs/mod/kickban.py @@ -2,7 +2,7 @@ import asyncio import contextlib import logging from datetime import datetime, timedelta, timezone -from typing import Optional, Union +from typing import Optional, Tuple, Union import discord from redbot.core import commands, i18n, checks, modlog @@ -62,67 +62,109 @@ class KickBanMixin(MixinMeta): async def ban_user( self, - user: discord.Member, + user: Union[discord.Member, discord.User, discord.Object], ctx: commands.Context, days: int = 0, reason: str = None, create_modlog_case=False, - ) -> Union[str, bool]: + ) -> Tuple[bool, str]: 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.config, 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 (0 <= days <= 7): - return _("Invalid days. Must be between 0 and 7.") + removed_temp = False - 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)) + 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}"), ) - em.add_field( - name=_("**Reason**"), - value=reason if reason is not None else _("No reason was given."), - inline=False, + 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." + ), ) - await user.send(embed=em) + 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)) + ) + 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() + + ban_list = [ban.user.id for ban in await guild.bans()] + if user.id in ban_list: + 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) queue_entry = (guild.id, user.id) - try: - await guild.ban(user, reason=audit_reason, delete_message_days=days) + if removed_temp: log.info( - "{}({}) banned {}({}), deleting {} days worth of messages.".format( - author.name, author.id, user.name, user.id, str(days) + "{}({}) upgraded the tempban for {} to a permaban.".format( + author.name, author.id, user.id ) ) - except discord.Forbidden: - return _("I'm not allowed to do that.") - except Exception as e: - log.exception( - "{}({}) attempted to kick {}({}), but an error occurred.".format( - author.name, author.id, user.name, user.id + success_message = _( + "User with ID {user_id} was upgraded from a temporary to a permanent ban." + ).format(user_id=user.id) + 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) + ) ) - ) - return _("An unexpected error occurred.") + success_message = _("Done. That felt good.") + 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 as e: + 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.replace(tzinfo=timezone.utc), - "ban", + ban_type, user, author, reason, @@ -130,7 +172,7 @@ class KickBanMixin(MixinMeta): channel=None, ) - return True + return True, success_message async def check_tempban_expirations(self): while self == self.bot.get_cog("Mod"): @@ -247,13 +289,15 @@ class KickBanMixin(MixinMeta): async def ban( self, ctx: commands.Context, - user: discord.Member, + user: Union[discord.Member, RawUserIds], days: Optional[int] = None, *, reason: str = None, ): """Ban a user from this server and optionally delete days 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, defaultdays setting will be used instead.""" @@ -261,21 +305,20 @@ class KickBanMixin(MixinMeta): 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) - result = await self.ban_user( + success_, message = await self.ban_user( user=user, ctx=ctx, days=days, reason=reason, create_modlog_case=True ) - if result is True: - await ctx.send(_("Done. It was about time.")) - elif isinstance(result, str): - await ctx.send(result) + await ctx.send(message) - @commands.command() + @commands.command(aliases=["hackban"]) @commands.guild_only() @commands.bot_has_permissions(ban_members=True) @checks.admin_or_permissions(ban_members=True) - async def hackban( + async def massban( self, ctx: commands.Context, user_ids: commands.Greedy[RawUserIds], @@ -283,7 +326,7 @@ class KickBanMixin(MixinMeta): *, reason: str = None, ): - """Preemptively bans user(s) from the server. + """Mass bans user(s) from the server. User IDs need to be provided in order to ban using this command.""" @@ -339,7 +382,7 @@ class KickBanMixin(MixinMeta): # We need to check if a user is tempbanned here because otherwise they won't be processed later on. continue else: - errors[user_id] = _("User {user_id} is already banned.").format( + errors[user_id] = _("User with ID {user_id} is already banned.").format( user_id=user_id ) @@ -358,14 +401,14 @@ class KickBanMixin(MixinMeta): else: # Instead of replicating all that handling... gets attr from decorator try: - result = await self.ban_user( + success, reason = await self.ban_user( user=user, ctx=ctx, days=days, reason=reason, create_modlog_case=True ) - if result is True: + if success: banned.append(user_id) else: errors[user_id] = _("Failed to ban user {user_id}: {reason}").format( - user_id=user_id, reason=result + user_id=user_id, reason=reason ) except Exception as e: errors[user_id] = _("Failed to ban user {user_id}: {reason}").format( @@ -397,13 +440,13 @@ class KickBanMixin(MixinMeta): await guild.ban(user, reason=audit_reason, delete_message_days=days) log.info("{}({}) hackbanned {}".format(author.name, author.id, user_id)) except discord.NotFound: - errors[user_id] = _("User {user_id} does not exist.").format( + errors[user_id] = _("User with ID {user_id} not found").format( user_id=user_id ) continue except discord.Forbidden: errors[user_id] = _( - "Could not ban {user_id}: missing permissions." + "Could not ban user with ID {user_id}: missing permissions." ).format(user_id=user_id) continue else: