Skip to content
This repository has been archived by the owner on Mar 21, 2024. It is now read-only.

Commit

Permalink
feat: More robust antiraid
Browse files Browse the repository at this point in the history
  • Loading branch information
junepark678 committed Jul 23, 2023
1 parent f798902 commit 82cf364
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 20 deletions.
73 changes: 57 additions & 16 deletions bridget/cogs/antiraid.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import re
import discord
import os

from asyncio import Lock
from datetime import datetime, timedelta, timezone
from discord.ext import commands
from discord import app_commands
from expiringdict import ExpiringDict
from datasketch import MinHash, MinHashLSH

import discord
from bridget.utils.enums import PermissionLevel
from model import Case
from bridget.utils.pfpcalc import calculate_hash, hamming_distance
from model import Infraction
from utils.services import guild_service, user_service
from discord.ext import commands
from expiringdict import ExpiringDict
from utils.config import cfg
from utils.mod import prepare_ban_log
from utils.views import report_raid, report_raid_phrase, report_spam
from utils.reports import report_raid, report_raid_phrase, report_spam

class RaidType:
PingSpam = 1
Expand Down Expand Up @@ -38,6 +43,8 @@ def __init__(self, bot):
# cooldown to monitor if users are spamming a message (8 within 6 seconds)
self.message_spam_detection_threshold = commands.CooldownMapping.from_cooldown(
rate=7, per=6.0, type=commands.BucketType.member)
self.message_spam_aggresive_detection_threshold = commands.CooldownMapping.from_cooldown(
rate=3, per=5.5, type=commands.BucketType.member)
# cooldown to monitor if too many accounts created on the same date are joining within a short period of time
# (5 accounts created on the same date joining within 45 minutes of each other)
self.join_overtime_raid_detection_threshold = commands.CooldownMapping.from_cooldown(
Expand Down Expand Up @@ -67,6 +74,10 @@ def __init__(self, bot):
self.join_overtime_lock = Lock()
self.banning_lock = Lock()

# caches all joined users for profile picture analysis
self.last30pfps = []
self.last30messagecontents = {}

@commands.Cog.listener()
async def on_member_join(self, member: discord.Member) -> None:
"""Antiraid filter for when members join.
Expand All @@ -91,6 +102,24 @@ async def on_member_join(self, member: discord.Member) -> None:
member)
self.join_user_mapping[member.id] = member

if member.avatar != None:
this_hash = calculate_hash((await member.avatar.to_file()).fp)

for pfphash in self.last30pfps:
distance = hamming_distance(pfphash, this_hash)
similarity = (distance / 64)
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)
return


self.last30pfps.append(this_hash)
if len(self.last30pfps) > 30:
del self.last30pfps[0]

# 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):
users = list(self.join_user_mapping.keys())
Expand Down Expand Up @@ -176,11 +205,11 @@ async def on_message(self, message: discord.Message):
return
if message.guild.id != cfg.guild_id:
return
message.author = message.guild.get_member(message.author.id)
if PermissionLevel.MOD == message.guild:
if PermissionLevel.MOD == message.author:
return



if await self.message_spam(message):
await self.handle_raid_detection(message, RaidType.MessageSpam)
elif await self.ping_spam(message):
Expand Down Expand Up @@ -264,10 +293,19 @@ async def ping_spam(self, message: discord.Message):
A report is generated which a mod must review (either unmute or ban the user using a react)
"""

umentioncount, rmentioncount = 4, 2

if PermissionLevel.MEMPLUS == message.author:
return False
umentioncount, rmentioncount = 6, 2
elif PermissionLevel.MEMPRO == message.author:
umentioncount, rmentioncount = 8, 3


if (abs(datetime.now().timestamp() - message.author.joined_at.timestamp()) <= 43200 or datetime.now().timestamp() - (((message.author.id << 22) + 1420070400000) / 1000) <= 432000) and not PermissionLevel.MEMPLUS == message.author:
# Aggresive raid detection target (joined guild in the last 12 hours or created account within the last 5 days and is not a member plus)
umentioncount, rmentioncount = 2, 1

if len(set(message.mentions)) > 4 or len(set(message.role_mentions)) > 2:
if len(set(message.mentions)) > umentioncount or len(set(message.role_mentions)) > rmentioncount:
bucket = self.spam_report_cooldown.get_bucket(message)
current = message.created_at.replace(
tzinfo=timezone.utc).timestamp()
Expand All @@ -289,7 +327,11 @@ async def message_spam(self, message: discord.Message):
if PermissionLevel.MEMPLUS == message.author:
return False

bucket = self.message_spam_detection_threshold.get_bucket(message)
if (abs(datetime.now().timestamp() - message.author.joined_at.timestamp()) <= 43200 or datetime.now().timestamp() - (((message.author.id << 22) + 1420070400000) / 1000) <= 432000) and not PermissionLevel.MEMPLUS == message.author:
# Aggresive raid detection target (joined guild in the last 12 hours or created account within the last 5 days and is not a member plus)
bucket = self.message_spam_aggresive_detection_threshold.get_bucket(message)
else:
bucket = self.message_spam_detection_threshold.get_bucket(message)
current = message.created_at.replace(tzinfo=timezone.utc).timestamp()

if bucket.update_rate_limit(current):
Expand All @@ -298,7 +340,6 @@ async def message_spam(self, message: discord.Message):
tzinfo=timezone.utc).timestamp()
if not bucket.update_rate_limit(current):
user = message.author
ctx = await self.bot.get_context(message)
# await mute(ctx, user, mod=ctx.guild.me, reason="Message spam")
twoweek = datetime.now() + timedelta(days=14)

Expand Down Expand Up @@ -344,8 +385,8 @@ async def raid_ban(self, user: discord.Member, reason="Raid phrase detected", dm

db_guild = guild_service.get_guild()

case = Case(
_id=db_guild.case_id,
infraction = Infraction(
_id=db_guild.infraction_id,
_type="BAN",
date=datetime.now(),
mod_id=self.bot.user.id,
Expand All @@ -354,10 +395,10 @@ async def raid_ban(self, user: discord.Member, reason="Raid phrase detected", dm
reason=reason
)

guild_service.inc_caseid()
user_service.add_case(user.id, case)
guild_service.inc_infractionid()
user_service.add_infraction(user.id, infraction)

log = prepare_ban_log(self.bot.user, user, case)
log = prepare_ban_log(self.bot.user, user, infraction)

if dm_user:
try:
Expand Down
25 changes: 25 additions & 0 deletions bridget/utils/pfpcalc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from PIL import Image
from typing import Union
from io import BytesIO

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((8, 8), Image.ANTIALIAS)

# Convert image to grayscale
image = image.convert('L')

# Calculate average pixel value
average_pixel = sum(image.getdata()) / 64

# Generate binary hash
hash_value = ''.join(['1' if pixel >= average_pixel else '0' for pixel in image.getdata()])

return hash_value

def hamming_distance(hash1: str, hash2: str) -> float:
# Calculate the Hamming distance between two hashes
return sum(c1 != c2 for c1, c2 in zip(hash1, hash2))
6 changes: 3 additions & 3 deletions bridget/utils/reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,11 +268,11 @@ def prepare_embed(target: Union[discord.Message, discord.Member], word: str = No
name="Roles", value=roles if roles else "None", inline=False)

if len(rd) > 0:
embed.add_field(name=f"{len(rd)} most recent cases",
embed.add_field(name=f"{len(rd)} most recent infractions",
value=rd_text, inline=True)
else:
embed.add_field(name=f"Recent cases",
value="This user has no cases.", inline=True)
embed.add_field(name=f"Recent infractions",
value="This user has no infractions.", inline=True)
return embed


Expand Down
47 changes: 46 additions & 1 deletion pdm.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ dependencies = [
"opencv-python>=4.8.0.74",
"expiringdict>=1.2.2",
"pytimeparse>=1.1.8",
"datasketch>=1.5.9",
]
requires-python = ">=3.10,<4.0"
license = {text = "BSD-3-Clause"}
Expand Down

0 comments on commit 82cf364

Please sign in to comment.