Skip to content

Commit

Permalink
feat(bot): Added antispam
Browse files Browse the repository at this point in the history
  • Loading branch information
Nickelza committed Nov 1, 2023
1 parent b624306 commit aa728f0
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 6 deletions.
3 changes: 3 additions & 0 deletions environment/env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ ERROR_LOG_CHAT_ID=
TG_REST_CHANNEL_ID=
UPDATES_CHANNEL_ID=

ANTI_SPAM_PRIVATE_CHAT_MESSAGE_LIMIT=
ANTI_SPAM_GROUP_CHAT_MESSAGE_LIMIT=
ANTI_SPAM_TIME_INTERVAL_SECONDS=

REDDIT_CLIENT_ID=
REDDIT_CLIENT_SECRET=
Expand Down
8 changes: 8 additions & 0 deletions resources/Environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,14 @@ def get_belly(self):
# TgRest Channel ID
TG_REST_CHANNEL_ID = Environment('TG_REST_CHANNEL_ID')

# ANTI SPAM
# How many messages can be sent in private chat before spam is detected. Default: 10
ANTI_SPAM_PRIVATE_CHAT_MESSAGE_LIMIT = Environment('ANTI_SPAM_PRIVATE_CHAT_MESSAGE_LIMIT', default_value='10')
# How many messages can be sent in group chat before spam is detected. Default: 15
ANTI_SPAM_GROUP_CHAT_MESSAGE_LIMIT = Environment('ANTI_SPAM_GROUP_CHAT_MESSAGE_LIMIT', default_value='15')
# Time interval in seconds to check for spam. Default: 60
ANTI_SPAM_TIME_INTERVAL_SECONDS = Environment('ANTI_SPAM_TIME_INTERVAL_SECONDS', default_value='60')

# REDDIT
# Reddit client id
REDDIT_CLIENT_ID = Environment('REDDIT_CLIENT_ID')
Expand Down
2 changes: 2 additions & 0 deletions resources/phrases.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import src.model.enums.Command as Command
from src.model.enums.Emoji import Emoji

ANTI_SPAM_WARNING = 'Too many messages sent, please slow down...'

COMMAND_NOT_IN_REPLY_ERROR = 'This command can only be used in a reply to a message'
COMMAND_IN_REPLY_TO_BOT_ERROR = "This command can't be used in reply to a bot"
COMMAND_IN_REPLY_TO_ERROR = "This command can't be used in a reply to your own message"
Expand Down
66 changes: 62 additions & 4 deletions src/chat/manage_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from src.model.GroupChat import GroupChat
from src.model.GroupUser import GroupUser
from src.model.User import User
from src.model.enums.ContextDataKey import ContextDataType, ContextDataKey
from src.model.enums.Feature import Feature
from src.model.enums.MessageSource import MessageSource
from src.model.enums.ReservedKeyboardKeys import ReservedKeyboardKeys
Expand All @@ -27,6 +28,8 @@
from src.model.error.GroupChatError import GroupChatException
from src.model.error.PrivateChatError import PrivateChatException
from src.model.pojo.Keyboard import Keyboard
from src.service.bot_service import get_context_data, set_context_data
from src.service.date_service import get_datetime_in_future_seconds
from src.service.group_service import feature_is_enabled, get_group_or_topic_text, is_main_group
from src.service.message_service import full_message_send, is_command, delete_message, get_message_source, \
full_message_or_media_send_or_edit, message_is_reply, escape_valid_markdown_chars
Expand Down Expand Up @@ -87,26 +90,32 @@ async def manage(update: Update, context: ContextTypes.DEFAULT_TYPE, is_callback
:param is_callback: True if the message is a callback
:return: None
"""
# Recast necessary for match case to work, don't ask me why
message_source: MessageSource = MessageSource(get_message_source(update))
if await is_spam(update, context, message_source):
logging.warning(f'Spam detected for chat {update.effective_chat.id}: Ignoring message')
return

db = init()
try:
await manage_after_db(update, context, is_callback)
await manage_after_db(update, context, is_callback, message_source)
except Exception as e:
logging.error(update)
logging.error(e, exc_info=True)
finally:
end(db)


async def manage_after_db(update: Update, context: ContextTypes.DEFAULT_TYPE, is_callback: bool = False) -> None:
async def manage_after_db(update: Update, context: ContextTypes.DEFAULT_TYPE, is_callback: bool,
message_source: MessageSource) -> None:
"""
Manage a regular message after the database is initialized
:param update: The update
:param context: The context
:param is_callback: True if the message is a callback
:param message_source: The message source
:return: None
"""
# Recast necessary for match case to work, don't ask me why
message_source: MessageSource = MessageSource(get_message_source(update))

user = User()
if update.effective_user is not None:
Expand Down Expand Up @@ -488,3 +497,52 @@ def add_or_update_group_chat(update, group: Group) -> GroupChat:
group_chat.save()

return group_chat


async def is_spam(update: Update, context: ContextTypes.DEFAULT_TYPE, message_source: MessageSource) -> bool:
"""
Check if the message is spam, which would cause flooding
:param update: Telegram update
:param context: Telegram context
:param message_source: The message source
:return: True if the message is spam
"""

if message_source is MessageSource.PRIVATE:
context_data_type = ContextDataType.USER
elif message_source is MessageSource.GROUP:
context_data_type = ContextDataType.BOT
else:
return True # Not managing spam for other message sources

# Get past messages date list
try:
past_messages_date_list: list[datetime] = get_context_data(context, context_data_type,
ContextDataKey.PAST_MESSAGES_DATE)
except CommonChatException:
past_messages_date_list = []

# Remove old messages
now = datetime.now()
past_messages_date_list = [
x for x in past_messages_date_list
if now < get_datetime_in_future_seconds(Env.ANTI_SPAM_TIME_INTERVAL_SECONDS.get_int(), start_time=x)]

# Check if the message is spam
spam_limit = (Env.ANTI_SPAM_PRIVATE_CHAT_MESSAGE_LIMIT.get_int() if message_source is MessageSource.PRIVATE
else Env.ANTI_SPAM_GROUP_CHAT_MESSAGE_LIMIT.get_int())

if len(past_messages_date_list) >= spam_limit:
# In case spam limit was just reached, send warning message
if len(past_messages_date_list) == spam_limit:
past_messages_date_list.append(now)
set_context_data(context, context_data_type, ContextDataKey.PAST_MESSAGES_DATE, past_messages_date_list)
await full_message_send(context, phrases.ANTI_SPAM_WARNING, update=update, quote_if_group=False,
new_message=True)
return True

# Add the message to the list
past_messages_date_list.append(now)
set_context_data(context, context_data_type, ContextDataKey.PAST_MESSAGES_DATE, past_messages_date_list)

return False
1 change: 1 addition & 0 deletions src/model/enums/ContextDataKey.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class ContextDataKey(StrEnum):
BOUNTY_LOAN_REPAY_AMOUNT = 'loan_repay_amount'
CREATED_PREDICTION = 'created_prediction'
INLINE_QUERY = 'inline_query'
PAST_MESSAGES_DATE = 'past_messages_date'


class ContextDataType(StrEnum):
Expand Down
8 changes: 6 additions & 2 deletions src/service/date_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,14 +181,18 @@ def get_remaining_time_in_minutes(end_datetime: datetime, start_datetime: dateti
return get_remaining_time_in_seconds(end_datetime, start_datetime) // 60


def get_datetime_in_future_seconds(seconds: int) -> datetime:
def get_datetime_in_future_seconds(seconds: int, start_time: datetime.datetime = None) -> datetime:
"""
Get the datetime in the future
:param seconds: The number of seconds in the future
:param start_time: The start time. If None, the current datetime is used
:return: The datetime in the future
"""

return datetime.datetime.now() + datetime.timedelta(seconds=int(seconds))
if start_time is None:
start_time = datetime.datetime.now()

return start_time + datetime.timedelta(seconds=int(seconds))


def get_datetime_in_future_hours(hours: float) -> datetime:
Expand Down

0 comments on commit aa728f0

Please sign in to comment.