diff --git a/bridget/cogs/antiraid.py b/bridget/cogs/antiraid.py index 9d20206..6c2d0fd 100644 --- a/bridget/cogs/antiraid.py +++ b/bridget/cogs/antiraid.py @@ -11,6 +11,7 @@ from bridget.utils.enums import PermissionLevel from bridget.utils.pfpcalc import calculate_hash, hamming_distance +from bridget.utils.utils import send_success from model import Infraction, Guild from utils.services import guild_service, user_service from utils.config import cfg @@ -110,22 +111,23 @@ async def on_member_join(self, member: discord.Member) -> None: this_hash = calculate_hash((await member.avatar.to_file()).fp) for pfphash in self.last30pfps: - distance = hamming_distance(pfphash, this_hash) + distance = hamming_distance(pfphash[0], this_hash) similarity = (distance / (pfpcalc.s ** 2)) * 100 if similarity <= 10: # 90% chance of similar image! - await report_raid(member) - member.ban(reason="Similar profile picture spam raid detected.") - self.last30pfps.append(this_hash) + raid_alert_bucket = self.raid_alert_cooldown.get_bucket(member) + if not raid_alert_bucket.update_rate_limit(current): + await report_raid(member) + await self.freeze_server(member.guild) + await member.ban(reason=f"Similar profile picture spam raid detected.") + await self.bot.get_guild(cfg.guild_id).get_member(pfphash[1]).ban(reason=f"Similar profile picture spam raid detected.") + self.last30pfps.append((this_hash, member.id)) return - self.last30pfps.append(this_hash) + self.last30pfps.append((this_hash, member.id)) if len(self.last30pfps) > 30: del self.last30pfps[0] - - with open("./bot_data/last30pfps.json", "w") as f: - json.dump(self.last30pfps, f) # if ratelimit is triggered, we should ban all the users that joined in the past 8 seconds if join_spam_detection_bucket.update_rate_limit(current): @@ -332,6 +334,74 @@ async def handle_raid_detection(self, message: discord.Message, raid_type: RaidT except Exception: pass + async def lock_unlock_channel(self, ctx: discord.Interaction, channel: discord.TextChannel, lock=None): + db_guild = guild_service.get_guild() + + default_role = ctx.guild.default_role + member_plus = ctx.guild.get_role(db_guild.role_memberplus) + + default_perms = channel.overwrites_for(default_role) + memberplus_perms = channel.overwrites_for(member_plus) + + if lock and default_perms.send_messages is None and memberplus_perms.send_messages is None: + default_perms.send_messages = False + memberplus_perms.send_messages = True + elif lock is None and (not default_perms.send_messages) and memberplus_perms.send_messages: + default_perms.send_messages = None + memberplus_perms.send_messages = None + else: + return + + try: + await channel.set_permissions(default_role, overwrite=default_perms, reason="Locked!" if lock else "Unlocked!") + await channel.set_permissions(member_plus, overwrite=memberplus_perms, reason="Locked!" if lock else "Unlocked!") + return True + except Exception: + return + + @PermissionLevel.ADMIN + @app_commands.command() + async def freezeable(self, ctx: discord.Interaction, channel: discord.TextChannel = None): + channel = channel or ctx.channel + if channel.id in guild_service.get_locked_channels(): + raise commands.BadArgument("That channel is already lockable.") + + guild_service.add_locked_channels(channel.id) + await send_success(ctx, f"Added {channel.mention} as lockable channel!") + + + @PermissionLevel.ADMIN + @app_commands.command() + async def unfreezeable(self, ctx: discord.Interaction, channel: discord.TextChannel = None): + channel = channel or ctx.channel + if channel.id not in guild_service.get_locked_channels(): + raise commands.BadArgument("That channel isn't already lockable.") + + guild_service.remove_locked_channels(channel.id) + await send_success(ctx, f"Removed {channel.mention} as lockable channel!") + + @PermissionLevel.MOD + @app_commands.command() + async def unfreeze(self, ctx: discord.Interaction): + channels = guild_service.get_locked_channels() + if not channels: + raise commands.BadArgument( + "No unfreezeable channels! Set some using `/freezeable`.") + + unlocked = [] + await ctx.response.defer() + for channel in channels: + channel = ctx.guild.get_channel(channel) + if channel is not None: + if await self.lock_unlock_channel(ctx, channel, lock=None): + unlocked.append(channel) + + if unlocked: + await send_success(ctx, f"Unlocked {len(unlocked)} channels!", ephemeral=False) + else: + raise commands.BadArgument( + "Server is already unlocked or my permissions are wrong.") + async def ping_spam(self, message: discord.Message): """If a user pings more than 5 people, or pings more than 2 roles, mute them. A report is generated which a mod must review (either unmute or ban the user using a react) diff --git a/bridget/utils/pfpcalc.py b/bridget/utils/pfpcalc.py index 59b97d0..b286773 100644 --- a/bridget/utils/pfpcalc.py +++ b/bridget/utils/pfpcalc.py @@ -8,8 +8,7 @@ def calculate_hash(image_path: Union[str, bytes, BytesIO]) -> str: # Open image using PIL image = Image.open(image_path) - # Resize image to a fixed size (e.g., 8x8 pixels) - image = image.resize((s, s), Image.LANCZOS) + image = image.resize((s, s), Image.BICUBIC) # Convert image to grayscale image = image.convert('L') diff --git a/bridget/utils/reports.py b/bridget/utils/reports.py index 35ecb31..cefd256 100644 --- a/bridget/utils/reports.py +++ b/bridget/utils/reports.py @@ -137,7 +137,7 @@ async def report_spam(bot, msg, user, title): await channel.send(ping_string, embed=embed, view=view, allowed_mentions=discord.AllowedMentions(everyone=False, users=True, roles=True)) -async def report_raid(user, msg=None): +async def report_raid(user: discord.Member, msg=None): embed = discord.Embed() embed.title = "Possible raid occurring" embed.description = "The raid filter has been triggered 5 or more times in the past 10 seconds. I am automatically locking all the channels. Use `/unfreeze` when you're done." @@ -152,7 +152,7 @@ async def report_raid(user, msg=None): await reports_channel.send(f"<@&{db_guild.role_moderator}>", embed=embed, allowed_mentions=discord.AllowedMentions(roles=True)) -def prepare_ping_string(db_guild, message): +def prepare_ping_string(*_args, **_kwargs): """Prepares modping string Parameters