DM Spam Protection Feature
This commit is contained in:
@@ -40,6 +40,9 @@ class Config:
|
||||
self.fail_open = self._parse_bool("FAIL_OPEN", True)
|
||||
self.debug = self._parse_bool("DEBUG", False)
|
||||
|
||||
self.bot_webhook_url = os.getenv("BOT_WEBHOOK_URL", "")
|
||||
self.bot_webhook_secret = os.getenv("BOT_WEBHOOK_SECRET", "")
|
||||
|
||||
def validate(self):
|
||||
return [k for k in self.REQUIRED_KEYS if not os.getenv(k)]
|
||||
|
||||
@@ -95,6 +98,7 @@ if missing:
|
||||
KNOWN_EXTERNAL_USERS = {}
|
||||
RATE_LIMIT = defaultdict(list)
|
||||
METRICS = defaultdict(int)
|
||||
DM_NOTIFY = defaultdict(list)
|
||||
|
||||
METRICS_LOCK = Lock()
|
||||
CACHE_LOCK = Lock()
|
||||
@@ -146,7 +150,10 @@ def periodic_cache_save():
|
||||
|
||||
def extract_domain(user_id):
|
||||
try:
|
||||
return user_id.split(":")[1].lower().rstrip(".")
|
||||
parts = user_id.split(":")
|
||||
if len(parts) < 2:
|
||||
return "unknown"
|
||||
return parts[1].lower().rstrip(".")
|
||||
except Exception:
|
||||
return "unknown"
|
||||
|
||||
@@ -181,6 +188,30 @@ def is_local_room(room_id):
|
||||
def get_role(user_id):
|
||||
return "admin" if user_id in config.admin_users else "user"
|
||||
|
||||
def notify_bot(event_type, sender, room_id):
|
||||
if not config.bot_webhook_url:
|
||||
return
|
||||
|
||||
try:
|
||||
headers = {}
|
||||
|
||||
if config.bot_webhook_secret:
|
||||
headers["Authorization"] = f"Bearer {config.bot_webhook_secret}"
|
||||
|
||||
requests.post(
|
||||
config.bot_webhook_url,
|
||||
json={
|
||||
"type": event_type,
|
||||
"sender": sender,
|
||||
"room_id": room_id,
|
||||
"timestamp": time.time()
|
||||
},
|
||||
headers=headers,
|
||||
timeout=2
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to notify bot: {e}")
|
||||
|
||||
# ============================================================
|
||||
# RATE LIMIT
|
||||
# ============================================================
|
||||
@@ -430,6 +461,11 @@ def invite(room_id, event_id):
|
||||
|
||||
# 🔒 Rate Limit
|
||||
if is_rate_limited(domain, sender):
|
||||
log_event(
|
||||
"rate_limited",
|
||||
actor=sender,
|
||||
domain=domain
|
||||
)
|
||||
return Response(status=429)
|
||||
|
||||
# 🟢 Whitelist
|
||||
@@ -466,17 +502,29 @@ def invite(room_id, event_id):
|
||||
payload
|
||||
)
|
||||
|
||||
else:
|
||||
with METRICS_LOCK:
|
||||
METRICS["invite_blocked"] += 1
|
||||
key = f"{sender}:{room_id}"
|
||||
last_list = RATE_LIMIT.get(f"dm_notify:{key}", [])
|
||||
last = last_list[-1] if last_list else 0
|
||||
|
||||
# 🔥 deduplicated notify
|
||||
if time.time() - last > 5:
|
||||
notify_bot("dm_spam", sender, room_id)
|
||||
DM_NOTIFY[key].append(time.time())
|
||||
|
||||
# 🔥 heavy detection unabhängig davon
|
||||
if is_rate_limited(domain, sender):
|
||||
notify_bot("dm_spam_heavy", sender, room_id)
|
||||
|
||||
with METRICS_LOCK:
|
||||
METRICS["dm_detected"] += 1
|
||||
|
||||
log_event(
|
||||
"dm_detected",
|
||||
actor=sender,
|
||||
domain=domain,
|
||||
room_id=room_id
|
||||
)
|
||||
|
||||
log_event(
|
||||
"invite_blocked",
|
||||
actor=sender,
|
||||
domain=domain,
|
||||
reason="unknown_external_user"
|
||||
)
|
||||
return Response(status=403)
|
||||
|
||||
# 🟢 DEFAULT (alles andere erlauben)
|
||||
remember_user(sender)
|
||||
|
||||
Reference in New Issue
Block a user