From 321594c8901527c0d456b95f90ded41c4b750f3e Mon Sep 17 00:00:00 2001 From: Muzifs Date: Tue, 8 Jul 2025 15:06:51 +0300 Subject: [PATCH] initial project version --- .env.example | 1 + .gitignore | 6 +++ README.md | 39 ++++++++++++++++++ requirements.txt | 19 +++++++++ src/config.py | 9 +++++ src/database.py | 66 ++++++++++++++++++++++++++++++ src/logger.py | 58 ++++++++++++++++++++++++++ src/main.py | 90 +++++++++++++++++++++++++++++++++++++++++ src/modules/__init__.py | 0 src/modules/start.py | 16 ++++++++ 10 files changed, 304 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 requirements.txt create mode 100644 src/config.py create mode 100644 src/database.py create mode 100644 src/logger.py create mode 100644 src/main.py create mode 100644 src/modules/__init__.py create mode 100644 src/modules/start.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6df6c2a --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +BOT_TOKEN = "..." # Токен бота получать у @BotFather \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..737990b --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.venv/ +.env +__pycache__/ +.idea/ +bot.log +users.db \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7fb0bff --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +
+

LGBot

+

Бот-модератор для @linux_gaming_ru

+
+ +## Список дел + +- [X] Команда /start +- [ ] Команда /help +- [ ] Команда /mute (или мут) +- [ ] Команда /ban (или бан) +- [ ] Фильтрация сообщений + - [ ] Удаление сообщений с матом + - [ ] Удаление рекламы + +### Установка зависимостей (через pyenv) + +```sh +pyenv install 3.11.0 + +~/.pyenv/versions/3.11.0/bin/python3 -m venv .venv + +source .venv/bin/activate + +pip install -r requirements.txt + +``` + +### Настройка + +Создатите файл **.env** и внесите в него ваш токен, который вы получили у @BotFather. + +### Запуск + +```sh +python src/main.py +``` + +> Используется Python 3.11.0 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8b5c218 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,19 @@ +aiohappyeyeballs==2.6.1 +aiohttp==3.12.13 +aiosignal==1.4.0 +asyncio==3.4.3 +attrs==25.3.0 +certifi==2025.6.15 +charset-normalizer==3.4.2 +dotenv==0.9.9 +frozenlist==1.7.0 +idna==3.10 +multidict==6.6.3 +propcache==0.3.2 +pysqlite3==0.5.4 +pyTelegramBotAPI==4.27.0 +python-dotenv==1.1.1 +requests==2.32.4 +typing_extensions==4.14.1 +urllib3==2.5.0 +yarl==1.20.1 diff --git a/src/config.py b/src/config.py new file mode 100644 index 0000000..9292fce --- /dev/null +++ b/src/config.py @@ -0,0 +1,9 @@ +# Директория, где хранятся модули +MODULES_DIR = "modules" + +# Название файла db sqlite +DATABASE_NAME = "users.db" + +# Текст для команд +MESSAGE_FOR_START = "Бот-модератор для чата @linux_gaming_ru" +MESSAGE_FOR_HELP = "пусто" \ No newline at end of file diff --git a/src/database.py b/src/database.py new file mode 100644 index 0000000..d19957e --- /dev/null +++ b/src/database.py @@ -0,0 +1,66 @@ +import sqlite3 +import os +from typing import Optional, Tuple + +import logging + +from config import DATABASE_NAME + +logger = logging.getLogger(__name__) # Получаем логгер для текущего модуля + +class Database: # Инициализация класса + def __init__(self, db_name: str = DATABASE_NAME): + self.db_name = db_name + self._init_db() + + # Инициализирует базу данных и создает таблицу, если она не существует + def _init_db(self): + with self._get_connection() as connect: + cursor = connect.cursor() + cursor.execute(''' + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY, + nickname TEXT, + tag TEXT + ) + ''') + connect.commit() + + # Возвращает соединение с базой данных + def _get_connection(self): + return sqlite3.connect(self.db_name) + + # Добавляет нового пользователя или обновляет существующего + def add_or_update_user(self, user_id: int, nickname: Optional[str], tag: Optional[str]): + with self._get_connection() as connect: + cursor = connect.cursor() + + # Проверяем существование пользователя + cursor.execute('SELECT 1 FROM users WHERE id = ?', (user_id,)) + exists = cursor.fetchone() + + if exists: # Обновляем существующую запись + cursor.execute(''' + UPDATE users + SET nickname = ?, tag = ? + WHERE id = ? + ''', (nickname, tag, user_id)) + + else: # Добавляем нового пользователя + cursor.execute(''' + INSERT INTO users (id, nickname, tag) + VALUES (?, ?, ?) + ''', (user_id, nickname, tag)) + logger.info(f"Новый пользователь ({user_id})") + + connect.commit() + + # Возвращает информацию о пользователе + def get_user(self, user_id: int) -> Optional[Tuple]: + with self._get_connection() as connect: + cursor = connect.cursor() + cursor.execute('''SELECT id, nickname, tag FROM users WHERE id = ?''', (user_id,)) + return cursor.fetchone() + +# Создаем экземпляр базы данных для импорта в других модулях +db = Database() \ No newline at end of file diff --git a/src/logger.py b/src/logger.py new file mode 100644 index 0000000..cd9e537 --- /dev/null +++ b/src/logger.py @@ -0,0 +1,58 @@ +import logging +import time +import os + +class ColoredFormatter(logging.Formatter): # Цветные логи (для терминала) + LEVEL_COLORS = { + logging.INFO: '\033[92m', + logging.WARNING: '\033[93m', + logging.ERROR: '\033[91m', + logging.CRITICAL: '\033[91m' + } + + LEVEL_NAMES = { + logging.INFO: "I", + logging.WARNING: "W", + logging.ERROR: "E", + logging.CRITICAL: "C" + } + + def format(self, record): + local_time = time.localtime(record.created) + time_str = time.strftime("%H:%M:%S", local_time) + date_str = time.strftime("%d-%m-%Y", local_time) + level_name = self.LEVEL_NAMES.get(record.levelno, record.levelname) + message = f"[{time_str}] [{date_str}] [{level_name}] {record.getMessage()}" + color = self.LEVEL_COLORS.get(record.levelno, "") + return f"{color}{message}\033[0m" if color else message + +class UncoloredFormatter(logging.Formatter): # Бесцветные логи (для bot.log) + def format(self, record): + local_time = time.localtime(record.created) + time_str = time.strftime("%H:%M:%S", local_time) + date_str = time.strftime("%d-%m-%Y", local_time) + level_name = ColoredFormatter.LEVEL_NAMES.get( + record.levelno, + record.levelname + ) + return f"[{time_str}] [{date_str}] [{level_name}] {record.getMessage()}" + +def setup_logging(): # Инициализирует систему логирования + + # Создаем корневой логгер + logger = logging.getLogger() + logger.setLevel(logging.INFO) + + # Проверяем, не настроен ли логгер ранее + if not logger.hasHandlers(): + console_handler = logging.StreamHandler() + console_handler.setFormatter(ColoredFormatter()) + + # Сохраняем логи в файл + file_handler = logging.FileHandler("bot.log", encoding='utf-8') + file_handler.setFormatter(UncoloredFormatter()) + + logger.addHandler(console_handler) + logger.addHandler(file_handler) + + return logger \ No newline at end of file diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..dee6bc6 --- /dev/null +++ b/src/main.py @@ -0,0 +1,90 @@ +from telebot.async_telebot import AsyncTeleBot +from telebot.asyncio_handler_backends import BaseMiddleware +import asyncio + +import os +import sys +import importlib +from dotenv import load_dotenv + +import logging +from logger import setup_logging + +from database import db + +from config import MODULES_DIR + +load_dotenv() # Загружаем токен бота из .env +bot = AsyncTeleBot(os.getenv("BOT_TOKEN"), parse_mode="html") + +logger = logging.getLogger(__name__) # Получаем логгер для текущего модуля + +class UserUpdateMiddleware(BaseMiddleware): + def __init__(self, db): + super().__init__() + # message - все обычные сообщения + # chat_member - события изменения статуса участников чата + self.update_types = ['message', 'chat_member'] + self.db = db + + async def pre_process(self, message, data): + + # Обработка пользователей, отправившие сообщение + if message.content_type == 'text': + self.db.add_or_update_user( + user_id=message.from_user.id, + nickname=message.from_user.first_name, + tag=message.from_user.username + ) + + # Обработка новых участников группы + elif message.content_type == 'new_chat_members': + for new_member in message.new_chat_members: + self.db.add_or_update_user( + user_id=new_member.id, + nickname=new_member.first_name, + tag=new_member.username + ) + return data + + async def post_process(self, message, data, exception): + pass + +# Регистрируем middleware +bot.setup_middleware(UserUpdateMiddleware(db)) + +async def load_modules(): # Загружает все модули из директории /modules + + setup_logging() # Инициализация логирования + + loaded_count = 0 # Переменная для подсчёта модулей + modules_path = os.path.join(os.path.dirname(__file__), MODULES_DIR) # Импортируем относительный путь проекта + + for filename in os.listdir(modules_path): + if filename.endswith(".py") and filename != "__init__.py": + module_name = filename[:-3] # Убираем расширение .py + try: + module = importlib.import_module(f"{MODULES_DIR}.{module_name}") + if hasattr(module, "register_handlers"): + module.register_handlers(bot) + loaded_count += 1 + logger.info(f"Модуль {module_name} успешно загружен") + else: + logger.warning(f"Модуль {module_name} не содержит функцию register_handlers") + except Exception as e: + logger.error(f"Ошибка при загрузке модуля {module_name}: {str(e)}") + logger.info(f"Загружено модулей: {loaded_count} шт. Бот запущен.") + +async def main(): + os.system('clear') # Очищаем терминал + try: + await load_modules() # Проверяем и загружаем модули + await bot.infinity_polling() # Запускаем бота + except (KeyboardInterrupt, asyncio.CancelledError): + logger.info("Бот остановлен.") + except Exception as e: + logger.critical(f"Критическая ошибка: {str(e)}") + sys.exit(1) + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/src/modules/__init__.py b/src/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/start.py b/src/modules/start.py new file mode 100644 index 0000000..dd63f8a --- /dev/null +++ b/src/modules/start.py @@ -0,0 +1,16 @@ +from telebot.async_telebot import AsyncTeleBot +import logging + +from config import MESSAGE_FOR_START + +logger = logging.getLogger(__name__) # Получаем логгер для текущего модуля + +def register_handlers(bot: AsyncTeleBot): # Регистрирует все обработчики команд + + @bot.message_handler(commands=['start']) # Обработчик команды /start + async def start_command(message): + try: + logger.info(f"Команда START ({message.from_user.id})") + await bot.send_message(message.chat.id, MESSAGE_FOR_START) + except Exception as e: + logger.error(f"Команда START ({message.from_user.id}) {str(e)}") \ No newline at end of file