diff --git a/.gigaide/gigaide.properties b/.gigaide/gigaide.properties
index 064b3d2..6ef783e 100644
--- a/.gigaide/gigaide.properties
+++ b/.gigaide/gigaide.properties
@@ -1,5 +1,5 @@
-## changed at Sat Oct 18 12:59:47 MSK 2025
-#Sat Oct 18 12:59:47 MSK 2025
+## changed at Sun Oct 19 12:21:52 MSK 2025
+#Sun Oct 19 12:21:52 MSK 2025
com.gigaide.elements.ext.marker.solution.BeanMarkedPsi.shouldMark=true
com.gigaide.elements.ext.marker.solution.ConfigMarkedPsi.shouldMark=true
com.gigaide.elements.ext.marker.solution.DataMarkedPsi.shouldMark=true
diff --git a/src/action_reporter.py b/src/action_reporter.py
index 0811318..eceeeb6 100644
--- a/src/action_reporter.py
+++ b/src/action_reporter.py
@@ -1,5 +1,6 @@
from telebot.async_telebot import AsyncTeleBot
from telebot.types import Message
+from typing import Optional
import logging
import os
from database import db
@@ -33,7 +34,7 @@ class ActionReporter:
return text
# Получает информацию об администраторе
- async def _get_admin_info(self, admin_id: int | None) -> str:
+ async def _get_admin_info(self, admin_id: Optional[int]) -> str:
# Если админ не указан (автоматическое действие)
if admin_id is None:
return "🤖 Администратор: Автоматическое действие"
@@ -58,7 +59,7 @@ class ActionReporter:
return text
# Отправляет лог действия в админ-чат
- async def log_action(self, action: str, user_id: int, admin_id: int | None, reason: str, duration: str, photo_path: str = None):
+ async def log_action(self, action: str, user_id: int, admin_id: Optional[int], reason: str, duration: str, photo_path: Optional[str] = None):
try:
# Получаем информацию о пользователе и администраторе
diff --git a/src/bad_words.py b/src/bad_words.py
index 9684429..cc59c56 100644
--- a/src/bad_words.py
+++ b/src/bad_words.py
@@ -116,15 +116,37 @@ def contains_bad_word(text: str) -> bool:
bad_words = get_bad_words()
exceptions = get_exceptions()
- # Проверяем исключения
- for exception in exceptions:
- if exception in text_lower:
- text_lower = text_lower.replace(exception, '')
-
# Проверяем бранные слова
for bad_word in bad_words:
if bad_word in text_lower:
- return True
+ # Проверяем, не является ли это слово частью исключения
+ # Ищем все вхождения плохого слова
+ start = 0
+ while True:
+ pos = text_lower.find(bad_word, start)
+ if pos == -1:
+ break
+
+ # Проверяем, входит ли это вхождение в какое-либо исключение
+ is_exception = False
+ for exception in exceptions:
+ # Проверяем, находится ли плохое слово внутри слова-исключения
+ # и содержится ли это слово-исключение в тексте в этой позиции
+ if bad_word in exception:
+ # Ищем позицию исключения, которое могло бы содержать это плохое слово
+ exc_start = text_lower.find(exception, max(0, pos - len(exception)))
+ if exc_start != -1:
+ exc_end = exc_start + len(exception)
+ # Если плохое слово находится внутри исключения
+ if exc_start <= pos < exc_end:
+ is_exception = True
+ break
+
+ # Если это не исключение, значит найдено плохое слово
+ if not is_exception:
+ return True
+
+ start = pos + 1
return False
@@ -148,14 +170,37 @@ def get_bad_words_from_text(text: str) -> list:
bad_words = get_bad_words()
exceptions = get_exceptions()
- # Проверяем исключения
- for exception in exceptions:
- if exception in text_lower:
- text_lower = text_lower.replace(exception, '')
-
# Ищем бранные слова
for bad_word in bad_words:
if bad_word in text_lower:
- found_words.append(bad_word)
+ # Проверяем, не является ли это слово частью исключения
+ start = 0
+ word_is_valid = False
+ while True:
+ pos = text_lower.find(bad_word, start)
+ if pos == -1:
+ break
+
+ # Проверяем, входит ли это вхождение в какое-либо исключение
+ is_exception = False
+ for exception in exceptions:
+ if bad_word in exception:
+ exc_start = text_lower.find(exception, max(0, pos - len(exception)))
+ if exc_start != -1:
+ exc_end = exc_start + len(exception)
+ if exc_start <= pos < exc_end:
+ is_exception = True
+ break
+
+ # Если найдено хотя бы одно вхождение, которое не является исключением
+ if not is_exception:
+ word_is_valid = True
+ break
+
+ start = pos + 1
+
+ # Добавляем слово только если оно действительно найдено (не в исключении)
+ if word_is_valid:
+ found_words.append(bad_word)
return found_words
\ No newline at end of file
diff --git a/src/config.py b/src/config.py
index b46f6ec..a7c28c3 100644
--- a/src/config.py
+++ b/src/config.py
@@ -7,6 +7,23 @@ DATABASE_NAME = 'users.db'
# Название файла для логов
LOG_FILE_NAME = 'bot.log'
+# ===========================================
+# Временные константы (в секундах)
+# ===========================================
+
+# Период учёта нарушений (30 дней)
+VIOLATIONS_PERIOD = 2592000
+
+# Кулдаун для благодарностей (1 час)
+THANK_COOLDOWN = 3600
+
+# Периоды для предупреждений
+ONE_WEEK = 604800 # 7 дней
+TWO_WEEKS = 1209600 # 14 дней
+
+# Максимальное время мута (30 дней)
+MAX_MUTE_TIME = 2592000
+
# Сообщения команд
COMMAND_MESSAGES = {
'start': 'Бот-администратор для чата @linux_gaming_ru',
@@ -152,13 +169,13 @@ COMMAND_MESSAGES = {
" /warn 123456789 причина
\n\n"
"📋 Система накопления:\n"
"• 1-й варн: просто предупреждение\n"
- "• 2-й варн за неделю: автомут на сутки\n"
- "• Повтор в течение 2 недель: мут на неделю\n\n"
+ "• 2-й варн за неделю: автомут на 7 дней (строгое)\n"
+ "• 2-й варн за 2 недели: автомут на 1 день (мягкое)\n\n"
"ℹ️ Причину обязательно указывайте для прозрачности"
),
'warned': '⚠️ Пользователь получил предупреждение.',
- 'warned_auto_mute_day': '⚠️ Пользователь получил предупреждение и автомут на 1 день (повторное нарушение за неделю).',
- 'warned_auto_mute_week': '⚠️ Пользователь получил предупреждение и автомут на 7 дней (множественные нарушения).',
+ 'warned_auto_mute_day': '⚠️ Пользователь получил предупреждение и автомут на 1 день (2-е предупреждение за 2 недели).',
+ 'warned_auto_mute_week': '⚠️ Пользователь получил предупреждение и автомут на 7 дней (2-е предупреждение за неделю - строгое наказание).',
'karma_help': (
"⭐ Команда /karma\n\n"
"Показывает карму пользователя в этом чате\n\n"
diff --git a/src/database.py b/src/database.py
index b6ed952..6fb4837 100644
--- a/src/database.py
+++ b/src/database.py
@@ -1,5 +1,6 @@
import sqlite3
import os
+import time
from typing import Optional, Tuple
import logging
@@ -65,7 +66,34 @@ class Database: # Инициализация класса
FOREIGN KEY (to_user_id) REFERENCES users (id)
)
''')
+
+ # Создаём индексы для оптимизации часто используемых запросов
+ # Индекс для проверки нарушений пользователя в чате за период
+ cursor.execute('''
+ CREATE INDEX IF NOT EXISTS idx_violations_user_chat_date
+ ON violations(user_id, chat_id, violation_date)
+ ''')
+
+ # Индекс для проверки предупреждений пользователя в чате за период
+ cursor.execute('''
+ CREATE INDEX IF NOT EXISTS idx_warnings_user_chat_date
+ ON warnings(user_id, chat_id, warn_date)
+ ''')
+
+ # Индекс для проверки истории кармы (кулдаун благодарностей)
+ cursor.execute('''
+ CREATE INDEX IF NOT EXISTS idx_karma_history_cooldown
+ ON karma_history(from_user_id, to_user_id, chat_id, timestamp)
+ ''')
+
+ # Индекс для поиска пользователя по username
+ cursor.execute('''
+ CREATE INDEX IF NOT EXISTS idx_users_tag
+ ON users(tag COLLATE NOCASE)
+ ''')
+
connect.commit()
+ logger.info("База данных и индексы успешно инициализированы")
# Возвращает соединение с базой данных
def _get_connection(self):
@@ -123,7 +151,6 @@ class Database: # Инициализация класса
# Добавляет нарушение в базу данных
def add_violation(self, user_id: int, chat_id: int, violation_type: str = 'bad_language'):
- import time
with self._get_connection() as connect:
cursor = connect.cursor()
cursor.execute('''
@@ -135,7 +162,6 @@ class Database: # Инициализация класса
# Получает количество нарушений за период (по умолчанию - за последний месяц)
def get_violations_count(self, user_id: int, chat_id: int, period_seconds: int = 2592000) -> int:
- import time
with self._get_connection() as connect:
cursor = connect.cursor()
cutoff_time = int(time.time()) - period_seconds
@@ -161,7 +187,6 @@ class Database: # Инициализация класса
# Очищает старые нарушения (старше указанного периода)
def clean_old_violations(self, period_seconds: int = 2592000):
- import time
with self._get_connection() as connect:
cursor = connect.cursor()
cutoff_time = int(time.time()) - period_seconds
@@ -189,7 +214,6 @@ class Database: # Инициализация класса
# Добавляет предупреждение в базу данных
def add_warning(self, user_id: int, chat_id: int, reason: str, admin_id: int):
- import time
with self._get_connection() as connect:
cursor = connect.cursor()
cursor.execute('''
@@ -201,7 +225,6 @@ class Database: # Инициализация класса
# Получает количество предупреждений за период
def get_warnings_count(self, user_id: int, chat_id: int, period_seconds: int) -> int:
- import time
with self._get_connection() as connect:
cursor = connect.cursor()
cutoff_time = int(time.time()) - period_seconds
@@ -290,9 +313,40 @@ class Database: # Инициализация класса
''', (chat_id, limit))
return cursor.fetchall()
- # Проверяет, может ли пользователь поблагодарить другого (проверка кулдауна)
+ # Атомарно проверяет кулдаун и добавляет запись в историю кармы
+ # Возвращает True если благодарность засчитана, False если кулдаун не прошёл
+ def try_add_karma_thank(self, from_user_id: int, to_user_id: int, chat_id: int, cooldown_seconds: int = 3600) -> bool:
+ with self._get_connection() as connect:
+ cursor = connect.cursor()
+ current_time = int(time.time())
+ cutoff_time = current_time - cooldown_seconds
+
+ # Проверяем кулдаун и добавляем запись в одной транзакции
+ # Это предотвращает race condition при параллельных запросах
+ cursor.execute('''
+ SELECT COUNT(*)
+ FROM karma_history
+ WHERE from_user_id = ? AND to_user_id = ? AND chat_id = ? AND timestamp > ?
+ ''', (from_user_id, to_user_id, chat_id, cutoff_time))
+ result = cursor.fetchone()
+
+ # Если кулдаун не прошёл, возвращаем False
+ if result and result[0] > 0:
+ return False
+
+ # Кулдаун прошёл, добавляем запись в той же транзакции
+ cursor.execute('''
+ INSERT INTO karma_history (from_user_id, to_user_id, chat_id, timestamp)
+ VALUES (?, ?, ?, ?)
+ ''', (from_user_id, to_user_id, chat_id, current_time))
+
+ connect.commit()
+ logger.info(f"Пользователь {from_user_id} поблагодарил {to_user_id} в чате {chat_id}")
+ return True
+
+ # УСТАРЕВШИЙ МЕТОД - оставлен для обратной совместимости
+ # Используйте try_add_karma_thank() для атомарной операции без race condition
def can_thank(self, from_user_id: int, to_user_id: int, chat_id: int, cooldown_seconds: int = 3600) -> bool:
- import time
with self._get_connection() as connect:
cursor = connect.cursor()
cutoff_time = int(time.time()) - cooldown_seconds
@@ -304,9 +358,9 @@ class Database: # Инициализация класса
result = cursor.fetchone()
return result[0] == 0 if result else True
- # Добавляет запись в историю кармы
+ # УСТАРЕВШИЙ МЕТОД - оставлен для обратной совместимости
+ # Используйте try_add_karma_thank() для атомарной операции без race condition
def add_karma_history(self, from_user_id: int, to_user_id: int, chat_id: int):
- import time
with self._get_connection() as connect:
cursor = connect.cursor()
cursor.execute('''
diff --git a/src/logger.py b/src/logger.py
index 8832e08..b28beb8 100644
--- a/src/logger.py
+++ b/src/logger.py
@@ -52,7 +52,7 @@ def setup_logging(): # Инициализирует систему логиро
# Создаем корневой логгер
logger = logging.getLogger()
- logger.setLevel(logging.DEBUG) # Временно DEBUG для отладки
+ logger.setLevel(logging.INFO) # INFO для продакшена
# Проверяем, не настроен ли логгер ранее
if not logger.hasHandlers():
diff --git a/src/main.py b/src/main.py
index 01ba25c..dd5c741 100644
--- a/src/main.py
+++ b/src/main.py
@@ -47,7 +47,7 @@ validate_env_vars()
bot = AsyncTeleBot(os.getenv("BOT_TOKEN"), parse_mode="html")
# Загружаем ID админ-чата из .env и инициализируемся для логов в чат
-init_action_reporter(bot, os.getenv("ADMIN_CHAT_ID"), os.getenv("LOG_THREAD_ID"))
+init_action_reporter(bot, int(os.getenv("ADMIN_CHAT_ID")), int(os.getenv("LOG_THREAD_ID")))
# Получаем логгер для текущего модуля
logger = logging.getLogger(__name__)
diff --git a/src/modules/0_karma_tracker.py b/src/modules/0_karma_tracker.py
index 0a979a9..bee08d9 100644
--- a/src/modules/0_karma_tracker.py
+++ b/src/modules/0_karma_tracker.py
@@ -1,16 +1,15 @@
from telebot.async_telebot import AsyncTeleBot
from telebot.types import Message
+import asyncio
import logging
from database import db
from thank_words import contains_thank_word
from bad_words import contains_bad_word
+from config import THANK_COOLDOWN
logger = logging.getLogger(__name__)
-# Кулдаун для благодарностей (в секундах) - 1 час
-THANK_COOLDOWN = 3600
-
def register_handlers(bot: AsyncTeleBot):
"""Регистрирует обработчики для отслеживания благодарностей"""
logger.info("Регистрация обработчика благодарностей (karma_tracker)")
@@ -55,15 +54,15 @@ def register_handlers(bot: AsyncTeleBot):
logger.info(f"Пользователь {from_user.id} попытался поблагодарить бота")
return
- # Проверяем кулдаун (можно благодарить одного пользователя раз в час)
- if not db.can_thank(from_user.id, to_user.id, chat_id, THANK_COOLDOWN):
+ # Атомарно проверяем кулдаун и записываем благодарность
+ # Это предотвращает race condition при параллельных запросах
+ if not db.try_add_karma_thank(from_user.id, to_user.id, chat_id, THANK_COOLDOWN):
logger.info(f"Пользователь {from_user.id} уже благодарил {to_user.id} недавно")
# Молча игнорируем, чтобы не спамить
return
- # Начисляем карму
+ # Начисляем карму (благодарность уже записана атомарно выше)
db.add_karma(to_user.id, chat_id, 1)
- db.add_karma_history(from_user.id, to_user.id, chat_id)
# Получаем новую карму пользователя
new_karma = db.get_karma(to_user.id, chat_id)
@@ -83,7 +82,6 @@ def register_handlers(bot: AsyncTeleBot):
logger.info(f"Пользователь {from_user.id} поблагодарил {to_user.id}, карма: {new_karma}")
# Удаляем уведомление через 5 секунд
- import asyncio
await asyncio.sleep(5)
try:
await bot.delete_message(chat_id, sent_message.message_id)
diff --git a/src/modules/auto_mute.py b/src/modules/auto_mute.py
index 154688e..4fda437 100644
--- a/src/modules/auto_mute.py
+++ b/src/modules/auto_mute.py
@@ -8,6 +8,7 @@ from database import db
from bad_words import contains_bad_word, get_bad_words_from_text
from action_reporter import action_reporter
from utils import delete_messages, format_mute_time
+from config import VIOLATIONS_PERIOD
# Получаем логгер для текущего модуля
logger = logging.getLogger(__name__)
@@ -31,9 +32,6 @@ MUTE_LEVELS = [
None, # 14. Перманентный мут (режим только чтения навсегда)
]
-# Период для подсчета нарушений (30 дней в секундах)
-VIOLATIONS_PERIOD = 2592000
-
def get_mute_duration(violations_count: int) -> int:
"""
Определяет длительность мута на основе количества нарушений.
diff --git a/src/modules/karma_commands.py b/src/modules/karma_commands.py
index 2fdb3a6..02311d7 100644
--- a/src/modules/karma_commands.py
+++ b/src/modules/karma_commands.py
@@ -1,5 +1,6 @@
from telebot.async_telebot import AsyncTeleBot
from telebot.types import Message
+import asyncio
import logging
from database import db
@@ -8,7 +9,6 @@ logger = logging.getLogger(__name__)
async def _delete_message_delayed(bot: AsyncTeleBot, chat_id: int, message_id: int, delay: int):
"""Удаляет сообщение с задержкой"""
- import asyncio
try:
await asyncio.sleep(delay)
await bot.delete_message(chat_id, message_id)
@@ -94,7 +94,6 @@ def register_handlers(bot: AsyncTeleBot):
sent_message = await bot.reply_to(message, response)
# Удаляем команду через 20 секунд и ответ через 60 секунд
- import asyncio
asyncio.create_task(_delete_message_delayed(bot, chat_id, message.message_id, 20))
asyncio.create_task(_delete_message_delayed(bot, chat_id, sent_message.message_id, 60))
@@ -142,10 +141,9 @@ def register_handlers(bot: AsyncTeleBot):
response += f"{medal} {user_display} — {karma_points} кармы\n"
- sent_message = await bot.reply_to(message, response, parse_mode='HTML')
+ sent_message = await bot.reply_to(message, response)
# Удаляем команду через 20 секунд и ответ через 60 секунд
- import asyncio
asyncio.create_task(_delete_message_delayed(bot, chat_id, message.message_id, 20))
asyncio.create_task(_delete_message_delayed(bot, chat_id, sent_message.message_id, 60))
diff --git a/src/modules/mute.py b/src/modules/mute.py
index da72c01..0ba6551 100644
--- a/src/modules/mute.py
+++ b/src/modules/mute.py
@@ -33,7 +33,7 @@ async def mute_command(bot: AsyncTeleBot, message: Message, photo_path: str = No
# Определяем целевого пользователя
target_user = None
- # Отпределяем время
+ # Определяем время
time_arg = None
# Определяем причину
@@ -233,8 +233,11 @@ async def mute_command(bot: AsyncTeleBot, message: Message, photo_path: str = No
await delete_messages(bot, message, time_sleep=5, number_message=2)
return
- # Максимальное время мута - 30 дней (2592000 секунд)
- if mute_seconds > 2592000:
+ # Импортируем максимальное время мута
+ from config import MAX_MUTE_TIME
+
+ # Максимальное время мута - 30 дней
+ if mute_seconds > MAX_MUTE_TIME:
# Отправляем предупреждение
await bot.send_message(
diff --git a/src/modules/warn.py b/src/modules/warn.py
index 7b95d81..203cfdc 100644
--- a/src/modules/warn.py
+++ b/src/modules/warn.py
@@ -144,10 +144,10 @@ async def warn_command(bot: AsyncTeleBot, message: Message):
admin_id=message.from_user.id
)
- # Проверяем количество предупреждений
- ONE_WEEK = 604800 # 7 дней в секундах
- TWO_WEEKS = 1209600 # 14 дней в секундах
+ # Импортируем константы времени
+ from config import ONE_WEEK, TWO_WEEKS
+ # Проверяем количество предупреждений
warns_week = db.get_warnings_count(target_user.id, message.chat.id, ONE_WEEK)
warns_two_weeks = db.get_warnings_count(target_user.id, message.chat.id, TWO_WEEKS)