Source code for discordSuperUtils.Antispam
from __future__ import annotations
import asyncio
from abc import ABC, abstractmethod
from datetime import timedelta
from difflib import SequenceMatcher
from typing import (
TYPE_CHECKING,
List,
Union,
Any,
Iterable
)
import discord
from .Base import EventManager, get_generator_response
from .Punishments import get_relevant_punishment
if TYPE_CHECKING:
from discord.ext import commands
from .Punishments import Punishment
__all__ = ("SpamManager", "SpamDetectionGenerator", "DefaultSpamDetectionGenerator")
[docs]class SpamDetectionGenerator(ABC):
"""
Represents a SpamManager that filters messages to find spam.
"""
__slots__ = ()
[docs] @abstractmethod
def generate(self, last_messages: List[discord.Message]) -> Union[bool, Any]:
"""
This function is an abstract method.
The generate function of the generator.
:param last_messages: The last messages sent (5 is max).
:type last_messages: List[discord.Message]
:return: A boolean representing if the message is spam.
:rtype: Union[bool, Any]
"""
pass
[docs]class DefaultSpamDetectionGenerator(SpamDetectionGenerator):
[docs] def generate(self, last_messages: List[discord.Message]) -> Union[bool, Any]:
member = last_messages[0].author
if member.guild_permissions.administrator:
return False
return SpamManager.get_messages_similarity([message.content for message in last_messages]) > 0.70
[docs]class SpamManager(EventManager):
"""
Represents a SpamManager which detects spam.
"""
__slots__ = ("bot", "generator", "punishments", "_spam_cache", "_last_messages")
def __init__(self,
bot: commands.Bot,
generator: SpamDetectionGenerator = None,
wipe_cache_delay: timedelta = timedelta(minutes=5)):
super().__init__()
self.bot = bot
self.generator = generator if generator is not None else DefaultSpamDetectionGenerator
self.wipe_cache_delay = wipe_cache_delay
self.punishments = []
self._spam_cache = {}
self._last_messages = {}
self.bot.loop.create_task(self.__wipe_cache())
self.bot.add_listener(self.__handle_messages, "on_message")
self.bot.add_listener(self.__handle_messages, "on_message_edit")
async def __wipe_cache(self):
"""
This function is responsible for wiping the member cache.
:return:
"""
while not self.bot.is_closed():
await asyncio.sleep(self.wipe_cache_delay.total_seconds())
self._spam_cache = {}
[docs] @staticmethod
def get_messages_similarity(messages: Iterable[str]) -> float:
"""
Gets the similarity between messages.
:param messages: Messages to compare.
:type messages: Iterable[str]
:rtype: float
:return: The similarity between messages (0-1)
"""
results = []
for i, message in enumerate(messages):
for second_index, second_message in enumerate(messages):
if i != second_index:
results.append(SequenceMatcher(None, message, second_message).ratio())
return sum(results) / len(results) if results else 0
[docs] def add_punishments(self, punishments: List[Punishment]) -> None:
self.punishments = punishments
async def __handle_messages(self, message, edited_message=None):
message = edited_message or message
if not message.guild or message.author.bot:
return
member_last_messages = self._last_messages.setdefault(message.guild.id, {}).get(message.author.id, [])
member_last_messages.append(message)
member_last_messages = member_last_messages[-5:]
self._last_messages[message.guild.id][message.author.id] = member_last_messages
if len(member_last_messages) <= 3 or not get_generator_response(self.generator, SpamDetectionGenerator, member_last_messages):
return
# member_warnings are the number of times the member has spammed.
member_warnings = self._spam_cache.setdefault(message.guild.id, {}).get(message.author.id, 0) + 1
self._spam_cache[message.guild.id][message.author.id] = member_warnings
await self.call_event("on_message_spam", member_last_messages, member_warnings)
if punishment := get_relevant_punishment(self.punishments, member_warnings):
await punishment.punishment_manager.punish(message, message.author, punishment)