mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Delete all messages after a spam ban
This commit is contained in:
+42
-10
@@ -50,8 +50,8 @@ _BURST_COUNT_THRESHOLD = 20
|
|||||||
_SHORT_MESSAGE_LENGTH = 8
|
_SHORT_MESSAGE_LENGTH = 8
|
||||||
_SHORT_MESSAGE_REPEAT_LIMIT = 4
|
_SHORT_MESSAGE_REPEAT_LIMIT = 4
|
||||||
|
|
||||||
_recent_message_cache: dict[int, deque[tuple[str, str, float]]] = defaultdict(deque)
|
_recent_message_cache: dict[int, deque[tuple[str, str, float, int]]] = defaultdict(deque) # (normalized, content, timestamp, message_id)
|
||||||
_message_rate_cache: dict[int, deque[float]] = defaultdict(deque)
|
_message_rate_cache: dict[int, deque[tuple[float, int]]] = defaultdict(deque) # (timestamp, message_id)
|
||||||
_burst_last_logged: dict[int, float] = {}
|
_burst_last_logged: dict[int, float] = {}
|
||||||
|
|
||||||
|
|
||||||
@@ -62,12 +62,23 @@ def _normalize_for_spam(text: str) -> str:
|
|||||||
return cleaned
|
return cleaned
|
||||||
|
|
||||||
|
|
||||||
def _monitor_public_message_activity(user: User, content: str, db: Session) -> None:
|
def _monitor_public_message_activity(user: User, content: str, message_id: int, db: Session) -> None:
|
||||||
now = time.time()
|
now = time.time()
|
||||||
|
|
||||||
def suspend(reason: str, event: str, **extra: Any) -> None:
|
def suspend(reason: str, event: str, message_ids_to_delete: list[int] = None, **extra: Any) -> None:
|
||||||
if user.suspended or user.id == 1:
|
if user.suspended or user.id == 1:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Delete spam messages that triggered the ban
|
||||||
|
if message_ids_to_delete:
|
||||||
|
try:
|
||||||
|
deleted_count = db.query(Message).filter(Message.id.in_(message_ids_to_delete)).delete(synchronize_session=False)
|
||||||
|
db.commit()
|
||||||
|
logger.info(f"Deleted {deleted_count} spam messages for user {user.id}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to delete spam messages: {e}")
|
||||||
|
db.rollback()
|
||||||
|
|
||||||
user.suspended = True
|
user.suspended = True
|
||||||
user.suspension_reason = reason
|
user.suspension_reason = reason
|
||||||
db.commit()
|
db.commit()
|
||||||
@@ -77,6 +88,7 @@ def _monitor_public_message_activity(user: User, content: str, db: Session) -> N
|
|||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
username=user.username,
|
username=user.username,
|
||||||
reason=reason,
|
reason=reason,
|
||||||
|
deleted_messages=len(message_ids_to_delete) if message_ids_to_delete else 0,
|
||||||
**extra,
|
**extra,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
@@ -86,8 +98,8 @@ def _monitor_public_message_activity(user: User, content: str, db: Session) -> N
|
|||||||
|
|
||||||
# Rate tracking for burst detection
|
# Rate tracking for burst detection
|
||||||
rate_bucket = _message_rate_cache[user.id]
|
rate_bucket = _message_rate_cache[user.id]
|
||||||
rate_bucket.append(now)
|
rate_bucket.append((now, message_id))
|
||||||
while rate_bucket and now - rate_bucket[0] > _BURST_WINDOW_SECONDS:
|
while rate_bucket and now - rate_bucket[0][0] > _BURST_WINDOW_SECONDS:
|
||||||
rate_bucket.popleft()
|
rate_bucket.popleft()
|
||||||
|
|
||||||
burst_count = len(rate_bucket)
|
burst_count = len(rate_bucket)
|
||||||
@@ -103,12 +115,17 @@ def _monitor_public_message_activity(user: User, content: str, db: Session) -> N
|
|||||||
window_seconds=_BURST_WINDOW_SECONDS,
|
window_seconds=_BURST_WINDOW_SECONDS,
|
||||||
)
|
)
|
||||||
_burst_last_logged[user.id] = now
|
_burst_last_logged[user.id] = now
|
||||||
|
|
||||||
|
# Get all message IDs from the burst window
|
||||||
|
burst_message_ids = [msg_id for _, msg_id in rate_bucket]
|
||||||
suspend(
|
suspend(
|
||||||
"Automatic suspension: excessive message rate",
|
"Automatic suspension: excessive message rate",
|
||||||
"auto_suspension_public_burst",
|
"auto_suspension_public_burst",
|
||||||
|
message_ids_to_delete=burst_message_ids,
|
||||||
count=burst_count,
|
count=burst_count,
|
||||||
window_seconds=_BURST_WINDOW_SECONDS,
|
window_seconds=_BURST_WINDOW_SECONDS,
|
||||||
)
|
)
|
||||||
|
return
|
||||||
|
|
||||||
# Similarity-based spam detection
|
# Similarity-based spam detection
|
||||||
normalized = _normalize_for_spam(content)
|
normalized = _normalize_for_spam(content)
|
||||||
@@ -116,21 +133,25 @@ def _monitor_public_message_activity(user: User, content: str, db: Session) -> N
|
|||||||
while history and now - history[0][2] > _SPAM_WINDOW_SECONDS:
|
while history and now - history[0][2] > _SPAM_WINDOW_SECONDS:
|
||||||
history.popleft()
|
history.popleft()
|
||||||
|
|
||||||
prior_same = sum(1 for prev_norm, _, _ in history if prev_norm == normalized)
|
prior_same = sum(1 for prev_norm, _, _, _ in history if prev_norm == normalized)
|
||||||
prior_similar = sum(
|
prior_similar = sum(
|
||||||
1
|
1
|
||||||
for prev_norm, _, _ in history
|
for prev_norm, _, _, _ in history
|
||||||
if prev_norm and normalized and prev_norm != normalized and SequenceMatcher(None, normalized, prev_norm).ratio() >= _SPAM_SIMILARITY_THRESHOLD
|
if prev_norm and normalized and prev_norm != normalized and SequenceMatcher(None, normalized, prev_norm).ratio() >= _SPAM_SIMILARITY_THRESHOLD
|
||||||
)
|
)
|
||||||
|
|
||||||
history.append((normalized, content, now))
|
history.append((normalized, content, now, message_id))
|
||||||
|
|
||||||
total_matches = prior_same + prior_similar + 1
|
total_matches = prior_same + prior_similar + 1
|
||||||
|
|
||||||
if len(normalized) <= _SHORT_MESSAGE_LENGTH and prior_same + 1 >= _SHORT_MESSAGE_REPEAT_LIMIT:
|
if len(normalized) <= _SHORT_MESSAGE_LENGTH and prior_same + 1 >= _SHORT_MESSAGE_REPEAT_LIMIT:
|
||||||
|
# Get message IDs of all matching short messages
|
||||||
|
spam_message_ids = [msg_id for prev_norm, _, _, msg_id in history if prev_norm == normalized]
|
||||||
|
spam_message_ids.append(message_id) # Include current message
|
||||||
suspend(
|
suspend(
|
||||||
"Automatic suspension: repeated short messages",
|
"Automatic suspension: repeated short messages",
|
||||||
"auto_suspension_public_spam",
|
"auto_suspension_public_spam",
|
||||||
|
message_ids_to_delete=spam_message_ids,
|
||||||
occurrences=prior_same + 1,
|
occurrences=prior_same + 1,
|
||||||
window_seconds=_SPAM_WINDOW_SECONDS,
|
window_seconds=_SPAM_WINDOW_SECONDS,
|
||||||
match_type="short",
|
match_type="short",
|
||||||
@@ -138,9 +159,20 @@ def _monitor_public_message_activity(user: User, content: str, db: Session) -> N
|
|||||||
return
|
return
|
||||||
|
|
||||||
if total_matches >= _SPAM_MESSAGE_LIMIT:
|
if total_matches >= _SPAM_MESSAGE_LIMIT:
|
||||||
|
# Get message IDs of all matching similar messages
|
||||||
|
spam_message_ids = []
|
||||||
|
for prev_norm, _, _, msg_id in history:
|
||||||
|
if prev_norm == normalized:
|
||||||
|
spam_message_ids.append(msg_id)
|
||||||
|
elif prev_norm and normalized and prev_norm != normalized:
|
||||||
|
similarity = SequenceMatcher(None, normalized, prev_norm).ratio()
|
||||||
|
if similarity >= _SPAM_SIMILARITY_THRESHOLD:
|
||||||
|
spam_message_ids.append(msg_id)
|
||||||
|
spam_message_ids.append(message_id) # Include current message
|
||||||
suspend(
|
suspend(
|
||||||
"Automatic suspension: repeated similar public messages",
|
"Automatic suspension: repeated similar public messages",
|
||||||
"auto_suspension_public_spam",
|
"auto_suspension_public_spam",
|
||||||
|
message_ids_to_delete=spam_message_ids,
|
||||||
similar_messages=total_matches,
|
similar_messages=total_matches,
|
||||||
window_seconds=_SPAM_WINDOW_SECONDS,
|
window_seconds=_SPAM_WINDOW_SECONDS,
|
||||||
match_type="similar",
|
match_type="similar",
|
||||||
@@ -371,7 +403,7 @@ async def _send_message_internal(
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
_monitor_public_message_activity(current_user, raw_content, db)
|
_monitor_public_message_activity(current_user, raw_content, new_message.id, db)
|
||||||
|
|
||||||
message_payload = convert_message(new_message)
|
message_payload = convert_message(new_message)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user