From 7b653d4dcc92e5445b99fe5fb5b9c9d492f292f7 Mon Sep 17 00:00:00 2001 From: Niken Date: Sun, 19 Oct 2025 14:28:41 +0300 Subject: [PATCH] It's version 0.4 --- addons/dowloadmp4_to_youtube/__init__.py | 2 + addons/dowloadmp4_to_youtube/dowmp4.py | 94 +++++---- addons/dowloadmp4_to_youtube/handlers.py | 15 +- addons/draw/__init__.py | 9 + addons/draw/handlers.py | 190 ++++++++++++++++++ addons/example_addon/__init__.py | 2 + addons/example_addon/dowloadmp3_to_youtube.py | 96 +++++---- addons/example_addon/handlers.py | 39 ++-- addons/gpt/__init__.py | 9 + addons/gpt/handlers.py | 105 ++++++++++ addons/hello/__init__.py | 2 + addons/hello/handlers.py | 6 +- addons/id/__init__.py | 2 + addons/id/handlers.py | 3 +- addons/manager.py | 1 + addons/poll/__init__.py | 2 + addons/poll/handlers.py | 53 +++-- addons/send_message/__init__.py | 2 + addons/send_message/handlers.py | 73 +++++-- bot/core.py | 12 +- config.py | 17 +- handlers/admin.py | 25 ++- handlers/schedule.py | 10 +- main.py | 11 +- models/state.py | 4 +- services/schedule_service.py | 34 ++-- services/watcher_service.py | 57 ++++-- storage/DB.py | 4 - storage/message_storage.py | 7 +- utils/analytics.py | 182 +++++++++-------- utils/antispam.py | 9 +- utils/mac_metrics.py | 24 ++- 32 files changed, 775 insertions(+), 326 deletions(-) create mode 100644 addons/draw/__init__.py create mode 100644 addons/draw/handlers.py create mode 100644 addons/gpt/__init__.py create mode 100644 addons/gpt/handlers.py diff --git a/addons/dowloadmp4_to_youtube/__init__.py b/addons/dowloadmp4_to_youtube/__init__.py index 8a3fbb5..e0acb2f 100644 --- a/addons/dowloadmp4_to_youtube/__init__.py +++ b/addons/dowloadmp4_to_youtube/__init__.py @@ -1,7 +1,9 @@ def register(dp, state, bot): from . import handlers + handlers.register_handlers(dp, state, bot) + def unregister(dp): # Здесь можно удалить хендлеры, если нужно dp.message_handlers.handlers.clear() diff --git a/addons/dowloadmp4_to_youtube/dowmp4.py b/addons/dowloadmp4_to_youtube/dowmp4.py index 446ec96..ac60fa9 100644 --- a/addons/dowloadmp4_to_youtube/dowmp4.py +++ b/addons/dowloadmp4_to_youtube/dowmp4.py @@ -12,8 +12,8 @@ import uuid from config import Config # Настройка кодировки для всего приложения -sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') -sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8") +sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8") logger = logging.getLogger(__name__) @@ -24,13 +24,15 @@ LIMIT = 2 * 1024 * 1024 * 1024 # 2 ГБ async def safe_filename(name: str) -> str: """Создает безопасное имя файла""" # Нормализуем Unicode символы - normalized = unicodedata.normalize('NFKD', name) + normalized = unicodedata.normalize("NFKD", name) # Убираем акценты и специальные символы, оставляем только ASCII - ascii_name = normalized.encode('ascii', 'ignore').decode('ascii') + ascii_name = normalized.encode("ascii", "ignore").decode("ascii") # Заменяем проблемные символы - safe_name = "".join(c if c.isalnum() or c in (' ', '-', '_', '.') else '_' for c in ascii_name) + safe_name = "".join( + c if c.isalnum() or c in (" ", "-", "_", ".") else "_" for c in ascii_name + ) # Убираем множественные подчеркивания и обрезаем длину - safe_name = '_'.join(filter(None, safe_name.split('_'))) + safe_name = "_".join(filter(None, safe_name.split("_"))) return safe_name[:100] or f"video_{uuid.uuid4().hex[:8]}" @@ -38,21 +40,23 @@ async def get_video_info(url: str) -> dict: """Получает информацию о видео через yt-dlp""" try: process = await asyncio.create_subprocess_exec( - 'yt-dlp', - '--dump-json', - '--no-playlist', + "yt-dlp", + "--dump-json", + "--no-playlist", url, stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE + stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await process.communicate() if process.returncode == 0: - result = json.loads(stdout.decode('utf-8', errors='ignore')) - logger.info(f"Информация о видео получена: {result.get('title', 'Unknown')}") + result = json.loads(stdout.decode("utf-8", errors="ignore")) + logger.info( + f"Информация о видео получена: {result.get('title', 'Unknown')}" + ) return result else: - error_msg = stderr.decode('utf-8', errors='ignore') + error_msg = stderr.decode("utf-8", errors="ignore") logger.warning(f"yt-dlp ошибка: {error_msg}") except Exception as e: @@ -75,38 +79,54 @@ async def download_mp4_to_dropbox(url: str) -> tuple[str, dict]: duration = 0 if video_info: - title = await safe_filename(video_info.get('title', 'Unknown_Title')) - uploader = await safe_filename(video_info.get('uploader', 'Unknown_Uploader')) - duration = video_info.get('duration', 0) + title = await safe_filename(video_info.get("title", "Unknown_Title")) + uploader = await safe_filename( + video_info.get("uploader", "Unknown_Uploader") + ) + duration = video_info.get("duration", 0) logger.info(f"Обработано видео: {title}") # ОПТИМИЗИРОВАННЫЕ НАСТРОЙКИ ДЛЯ СКОРОСТИ download_process = await asyncio.create_subprocess_exec( - 'yt-dlp', - '-f', 'bestvideo[height<=720][filesize<800M]+bestaudio/best[height<=720][filesize<800M]', - '--no-playlist', - '-o', output_template, - '--ignore-errors', - '--no-warnings', - '--format-sort', 'quality,res:720,size:800M', - '--concurrent-fragments', '4', + "yt-dlp", + "-f", + "bestvideo[height<=720][filesize<800M]+bestaudio/best[height<=720][filesize<800M]", + "--no-playlist", + "-o", + output_template, + "--ignore-errors", + "--no-warnings", + "--format-sort", + "quality,res:720,size:800M", + "--concurrent-fragments", + "4", url, stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE + stderr=asyncio.subprocess.PIPE, ) - stdout, stderr = await asyncio.wait_for(download_process.communicate(), timeout=600) # Уменьшил таймаут + stdout, stderr = await asyncio.wait_for( + download_process.communicate(), timeout=600 + ) # Уменьшил таймаут # Остальной код без изменений... if download_process.returncode != 0: - error_msg = stderr.decode('utf-8', errors='ignore') if stderr else "Unknown error" + error_msg = ( + stderr.decode("utf-8", errors="ignore") + if stderr + else "Unknown error" + ) logger.error(f"Ошибка скачивания: {error_msg}") raise Exception(f"Ошибка скачивания: {error_msg}") mp4_files = glob.glob(os.path.join(temp_dir, "*.mp4")) if not mp4_files: video_files = glob.glob(os.path.join(temp_dir, "*.*")) - video_files = [f for f in video_files if f.lower().endswith(('.mp4', '.mkv', '.avi', '.mov', '.webm'))] + video_files = [ + f + for f in video_files + if f.lower().endswith((".mp4", ".mkv", ".avi", ".mov", ".webm")) + ] if video_files: mp4_files = [video_files[0]] @@ -123,25 +143,27 @@ async def download_mp4_to_dropbox(url: str) -> tuple[str, dict]: dbx = dropbox.Dropbox(DROPBOX_TOKEN) dropbox_path = f"/{final_filename}" - logger.info(f"Загружаем файл в Dropbox: {dropbox_path} (размер: {size / (1024 * 1024):.1f} MB)") + logger.info( + f"Загружаем файл в Dropbox: {dropbox_path} (размер: {size / (1024 * 1024):.1f} MB)" + ) with open(actual_file, "rb") as f: file_data = f.read() dbx.files_upload( file_data, dropbox_path, - mode=dropbox.files.WriteMode("overwrite") + mode=dropbox.files.WriteMode("overwrite"), ) shared_link = dbx.sharing_create_shared_link_with_settings(dropbox_path) link = shared_link.url.replace("?dl=0", "?dl=1") metadata = { - 'title': title, - 'uploader': uploader, - 'duration': duration, - 'filesize': size, - 'quality': 'optimized for speed' + "title": title, + "uploader": uploader, + "duration": duration, + "filesize": size, + "quality": "optimized for speed", } logger.info(f"Успешно загружено в Dropbox: {link}") @@ -154,5 +176,3 @@ async def download_mp4_to_dropbox(url: str) -> tuple[str, dict]: except Exception as e: logger.error(f"Общая ошибка: {e}") raise e - - diff --git a/addons/dowloadmp4_to_youtube/handlers.py b/addons/dowloadmp4_to_youtube/handlers.py index ff50b82..b01add4 100644 --- a/addons/dowloadmp4_to_youtube/handlers.py +++ b/addons/dowloadmp4_to_youtube/handlers.py @@ -2,18 +2,16 @@ import logging from aiogram import Dispatcher, Bot from aiogram.filters import Command from models.state import BotState -from utils.antispam import admin_required from .dowmp4 import download_mp4_to_dropbox - logger = logging.getLogger(__name__) def register_handlers(dp: Dispatcher, state: BotState, bot: Bot): @dp.message(Command("dowmp4")) - #@admin_required(4) + # @admin_required(4) async def dowmp4_handler(message): """Обработчик команды /dowmp4""" try: @@ -22,7 +20,9 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot): await message.answer("Пожалуйста, укажите URL видео после команды /dowmp4") return - processing_msg = await message.answer("⏳ Начинаю обработку видео... Это может занять несколько минут.") + processing_msg = await message.answer( + "⏳ Начинаю обработку видео... Это может занять несколько минут." + ) try: # Скачиваем и загружаем в Dropbox @@ -44,12 +44,13 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot): await message.answer( f"✅ **Видео успешно обработано!**\n\n{caption}", parse_mode="Markdown", - disable_web_page_preview=True + disable_web_page_preview=True, ) except ValueError as e: await message.answer(f"❌ Ошибка: {str(e)}") except Exception as e: logger.error(f"Ошибка при обработке /dowmp4: {e}", exc_info=True) - await message.answer("❌ Произошла ошибка при обработке видео. Попробуйте позже.") - + await message.answer( + "❌ Произошла ошибка при обработке видео. Попробуйте позже." + ) diff --git a/addons/draw/__init__.py b/addons/draw/__init__.py new file mode 100644 index 0000000..e0acb2f --- /dev/null +++ b/addons/draw/__init__.py @@ -0,0 +1,9 @@ +def register(dp, state, bot): + from . import handlers + + handlers.register_handlers(dp, state, bot) + + +def unregister(dp): + # Здесь можно удалить хендлеры, если нужно + dp.message_handlers.handlers.clear() diff --git a/addons/draw/handlers.py b/addons/draw/handlers.py new file mode 100644 index 0000000..fd41d8c --- /dev/null +++ b/addons/draw/handlers.py @@ -0,0 +1,190 @@ +import logging +import base64 +from io import BytesIO +import asyncio +import aiohttp + +from aiogram import Dispatcher, Bot +from aiogram.types import Message, FSInputFile, BufferedInputFile +from aiogram.filters import Command + +from models.state import BotState +from config import Config + +from storage.message_storage import save_message + +from transformers import pipeline + +from utils.antispam import saving + +logger = logging.getLogger(__name__) + +SD_URL = "http://192.168.31.95:7860/sdapi/v1/txt2img" + +# Загружаем пайплайн перевода один раз при старте (синхронный) +translator = pipeline("translation", model="Helsinki-NLP/opus-mt-ru-en") + + +async def translate_to_en(text: str) -> str: + try: + # выполняем перевод в отдельном потоке, чтобы не блокировать event loop + result = await asyncio.to_thread(translator, text, max_length=512) + return result[0]["translation_text"] + except Exception as e: + logger.error(f"Ошибка перевода: {e}") + return text + + +async def generate_img2img(prompt: str, init_image: BytesIO) -> BytesIO | None: + """ + Генерация изображения по методу img2img. + :param prompt: текстовый промт (уже переведённый на английский) + :param init_image: входное изображение в BytesIO + :return: BytesIO с результатом или None при ошибке + """ + try: + # кодируем входное изображение в base64 + init_image_base64 = base64.b64encode(init_image.getvalue()).decode("utf-8") + + payload = { + "init_images": [init_image_base64], + "prompt": prompt, + "negative_prompt": "blurry, low quality, bad anatomy, watermark, text, cropped", + "steps": 20, # можно 15–20 + "width": 1024, # лучше подставлять размеры исходного фото + "height": 1024, + "sampler_name": "Euler a", # мягкий и стабильный для img2img + "Schedule_type": "Karras", + "cfg_scale": 6, # чуть ниже, чем для txt2img + "seed": -1, + "denoising_strength": 0.8, # 0.3–0.5 для «сохранить стиль», 0.6–0.8 для «перерисовать» + "restore_faces": True, # если работаешь с людьми + "override_settings": { + "sd_model_checkpoint": "waiNSFWIllustrious_v150.safetensors" + }, + } + + async with aiohttp.ClientSession() as session: + async with session.post( + SD_URL.replace("txt2img", "img2img"), json=payload + ) as resp: + if resp.status != 200: + logger.error(f"Stable Diffusion img2img API error: {resp.status}") + return None + r = await resp.json() + image_base64 = r["images"][0] + return BytesIO(base64.b64decode(image_base64)) + + except Exception as e: + logger.error(f"Ошибка img2img: {e}") + return None + + +# sd_xl_base_1.0.safetensors +# waiNSFWIllustrious_v150.safetensors +async def generate_image(prompt: str) -> BytesIO | None: + payload = { + "prompt": prompt, + "negative_prompt": "blurry, low quality, bad anatomy, watermark, text, cropped", + "steps": 20, + "width": 1024, + "height": 1024, + "sampler_name": "Euler a", # сэмплер + "cfg_scale": 7, # насколько строго следовать промту + "seed": -1, # -1 = случайный сид + "batch_size": 1, # сколько картинок за раз + "n_iter": 1, # сколько раз повторить генерацию + "restore_faces": False, # восстановление лиц + "tiling": False, # тайлинг для текстур + "enable_hr": False, # highres fix (двухэтапная генерация) + "denoising_strength": 0.7, # сила денойзинга (актуально при enable_hr или img2img) + "hr_scale": 2, # во сколько раз увеличить при highres fix + "hr_upscaler": "Latent", # апскейлер для highres fix + "override_settings": { + "sd_model_checkpoint": "waiNSFWIllustrious_v150.safetensors" # выбор модели + }, + } + + try: + async with aiohttp.ClientSession() as session: + async with session.post(SD_URL, json=payload) as resp: + if resp.status != 200: + logger.error(f"Stable Diffusion API error: {resp.status}") + return None + r = await resp.json() + image_base64 = r["images"][0] + return BytesIO(base64.b64decode(image_base64)) + except Exception as e: + logger.error(f"Ошибка генерации изображения: {e}") + return None + + +def register_handlers(dp: Dispatcher, state: BotState, bot: Bot): + @dp.message(Command("draw")) + async def draw(message: Message): + save_message(message.chat.id, message.message_id) + if message.from_user.id in Config.BAN: + msg = await message.reply("Вы в бане") + save_message(msg.chat.id, msg.message_id) + else: + user_prompt = message.text.replace("/draw", "").strip() + if not user_prompt: + confirm_msg = await message.answer("❗ Укажи промт после команды /draw") + save_message(confirm_msg.chat.id, confirm_msg.message_id) + return + + en_prompt = await translate_to_en(user_prompt) + logger.info(f"Промт переведен: {user_prompt} -> {en_prompt}") + + img_bytes = await generate_image(en_prompt) + if img_bytes: + img_bytes.seek(0) + photo = BufferedInputFile(img_bytes.read(), filename="result.png") + msg = await bot.send_photo(chat_id=message.chat.id, photo=photo) + save_message(msg.chat.id, msg.message_id) + else: + error_msg = await message.answer("⚠️ Ошибка при генерации изображения.") + save_message(error_msg.chat.id, error_msg.message_id) + + @dp.message(Command("img2img")) + @saving + async def img2img_with_caption(message: Message, bot: Bot): + raw_caption = message.caption or "" + user_prompt = raw_caption.replace("/img2img", "").strip() + if not user_prompt: + await message.answer( + "❗ Укажи промт в подписи к фото после команды /img2img" + ) + return + + en_prompt = await translate_to_en(user_prompt) + logger.info(f"Промт для img2img переведен: {user_prompt} -> {en_prompt}") + + try: + if message.photo: + # Берём последнее (самое большое) фото + photo = message.photo[-1].file_id + + # Отправляем в SD API по file_id (как в iadmin) + # Здесь отличие: SD API требует base64, поэтому file_id нужно скачать + # Но логика построена как в iadmin — сначала берём file_id + file = await bot.get_file(photo) + file_bytes = await bot.download_file(file.file_path) + + img_bytes = await generate_img2img( + en_prompt, BytesIO(file_bytes.read()) + ) + if img_bytes: + img_bytes.seek(0) + photo = BufferedInputFile( + img_bytes.read(), filename="img2img_result.png" + ) + msg = await bot.send_photo(chat_id=message.chat.id, photo=photo) + else: + msg = await message.answer("⚠️ Ошибка при img2img генерации.") + + else: + msg = await message.answer("❗ Пришли фото с подписью /img2img <промт>") + save_message(msg.chat.id, msg.message_id) + except Exception as e: + await message.answer(f"⚠️ Ошибка: {e}") diff --git a/addons/example_addon/__init__.py b/addons/example_addon/__init__.py index 8a3fbb5..e0acb2f 100644 --- a/addons/example_addon/__init__.py +++ b/addons/example_addon/__init__.py @@ -1,7 +1,9 @@ def register(dp, state, bot): from . import handlers + handlers.register_handlers(dp, state, bot) + def unregister(dp): # Здесь можно удалить хендлеры, если нужно dp.message_handlers.handlers.clear() diff --git a/addons/example_addon/dowloadmp3_to_youtube.py b/addons/example_addon/dowloadmp3_to_youtube.py index 00d8b2d..78e3c3f 100644 --- a/addons/example_addon/dowloadmp3_to_youtube.py +++ b/addons/example_addon/dowloadmp3_to_youtube.py @@ -5,6 +5,7 @@ import logging import glob import json import requests +from typing import Optional from mutagen.easyid3 import EasyID3 from mutagen.id3 import ID3, APIC, error @@ -16,13 +17,14 @@ async def get_video_info(url: str) -> dict: """Получает информацию о видео через yt-dlp""" try: process = await asyncio.create_subprocess_exec( - 'yt-dlp', - '--dump-json', - '--no-playlist', - '--cookies', '~/myfirstprogramm/addons/example_addon/cookies.txt', + "yt-dlp", + "--dump-json", + "--no-playlist", + "--cookies", + "~/myfirstprogramm/addons/example_addon/cookies.txt", url, stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE + stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await process.communicate() @@ -33,17 +35,19 @@ async def get_video_info(url: str) -> dict: return None -async def download_thumbnail(thumbnail_url: str) -> tuple[bytes, str]: +async def download_thumbnail( + thumbnail_url: str, +) -> tuple[Optional[bytes], Optional[str]]: """Скачивает обложку видео и возвращает данные и MIME тип""" try: response = requests.get(thumbnail_url, timeout=10) if response.status_code == 200: - if 'jpeg' in thumbnail_url or 'jpg' in thumbnail_url: - mime_type = 'image/jpeg' - elif 'png' in thumbnail_url: - mime_type = 'image/png' + if "jpeg" in thumbnail_url or "jpg" in thumbnail_url: + mime_type = "image/jpeg" + elif "png" in thumbnail_url: + mime_type = "image/png" else: - mime_type = response.headers.get('Content-Type', 'image/jpeg') + mime_type = response.headers.get("Content-Type", "image/jpeg") return response.content, mime_type except Exception as e: logger.warning(f"Не удалось скачать обложку: {e}") @@ -59,19 +63,19 @@ def apply_metadata(mp3_path: str, metadata: dict): audio = EasyID3() audio.save(mp3_path) - audio['title'] = metadata.get('title', 'Unknown Title') - audio['artist'] = metadata.get('performer', 'Unknown Artist') + audio["title"] = metadata.get("title", "Unknown Title") + audio["artist"] = metadata.get("performer", "Unknown Artist") audio.save(mp3_path) - if metadata.get('thumbnail_data'): + if metadata.get("thumbnail_data"): audio = ID3(mp3_path) audio.add( APIC( encoding=3, - mime=metadata.get('thumbnail_mime', 'image/jpeg'), + mime=metadata.get("thumbnail_mime", "image/jpeg"), type=3, # front cover - desc='Cover', - data=metadata['thumbnail_data'] + desc="Cover", + data=metadata["thumbnail_data"], ) ) audio.save(mp3_path) @@ -96,27 +100,32 @@ async def download_mp3_isolated(url: str) -> tuple[str, dict]: duration = 0 if video_info: - title = video_info.get('title', 'Unknown Title') - uploader = video_info.get('uploader', 'Unknown Artist') - thumbnail_url = video_info.get('thumbnail') - if not thumbnail_url and video_info.get('thumbnails'): - thumbnails = video_info.get('thumbnails', []) + title = video_info.get("title", "Unknown Title") + uploader = video_info.get("uploader", "Unknown Artist") + thumbnail_url = video_info.get("thumbnail") + if not thumbnail_url and video_info.get("thumbnails"): + thumbnails = video_info.get("thumbnails", []) if thumbnails: - thumbnail_url = thumbnails[-1].get('url') - duration = video_info.get('duration', 0) + thumbnail_url = thumbnails[-1].get("url") + duration = video_info.get("duration", 0) logger.info(f"Получена информация о видео: {title}") process = await asyncio.create_subprocess_exec( - 'yt-dlp', - '-x', '--audio-format', 'mp3', - '--cookies', '~/myfirstprogramm/addons/example_addon/cookies.txt', - '--audio-quality', '320K', - '--no-playlist', - '-o', output_template, - '--ignore-errors', + "yt-dlp", + "-x", + "--audio-format", + "mp3", + "--cookies", + "~/myfirstprogramm/addons/example_addon/cookies.txt", + "--audio-quality", + "320K", + "--no-playlist", + "-o", + output_template, + "--ignore-errors", url, stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE + stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=300) @@ -125,24 +134,31 @@ async def download_mp3_isolated(url: str) -> tuple[str, dict]: if mp3_files: actual_file = mp3_files[0] if os.path.getsize(actual_file) > 1000: - with tempfile.NamedTemporaryFile(suffix='.mp3', delete=False) as final_file: + with tempfile.NamedTemporaryFile( + suffix=".mp3", delete=False + ) as final_file: final_filename = final_file.name - with open(actual_file, 'rb') as src, open(final_filename, 'wb') as dst: + with ( + open(actual_file, "rb") as src, + open(final_filename, "wb") as dst, + ): dst.write(src.read()) thumbnail_data, mime_type = None, None if thumbnail_url: - thumbnail_data, mime_type = await download_thumbnail(thumbnail_url) + thumbnail_data, mime_type = await download_thumbnail( + thumbnail_url + ) if thumbnail_data: logger.info(f"Обложка скачана: {thumbnail_url}") metadata = { - 'title': title, - 'performer': uploader, - 'duration': duration, - 'thumbnail_data': thumbnail_data, - 'thumbnail_mime': mime_type + "title": title, + "performer": uploader, + "duration": duration, + "thumbnail_data": thumbnail_data, + "thumbnail_mime": mime_type, } # Прописываем теги в MP3 diff --git a/addons/example_addon/handlers.py b/addons/example_addon/handlers.py index 4c227ed..c319224 100644 --- a/addons/example_addon/handlers.py +++ b/addons/example_addon/handlers.py @@ -3,11 +3,14 @@ from aiogram.filters import Command from models.state import BotState from utils.antispam import admin_required from logging import getLogger -from .dowloadmp3_to_youtube import * +from .dowloadmp3_to_youtube import download_mp3_isolated +import tempfile +import asyncio from os import path, unlink logger = getLogger(__name__) + def register_handlers(dp: Dispatcher, state: BotState, bot: Bot): @dp.message(Command("dowmp3")) @admin_required(5) @@ -18,7 +21,9 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot): return url = args[1] - logger.info(f"Получена команда /dowmp3 от user_id={message.from_user.id}, url={url}") + logger.info( + f"Получена команда /dowmp3 от user_id={message.from_user.id}, url={url}" + ) status_msg = await message.reply("⏳ Скачиваю аудио... Это займет 1-2 минуты") @@ -29,30 +34,32 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot): if file_size < 1000: raise Exception("Файл слишком маленький") - await status_msg.edit_text(f"✅ Аудио готово! Отправляю...") + await status_msg.edit_text("✅ Аудио готово! Отправляю...") # Подготавливаем аудио файл audio_input = types.FSInputFile(filename) # Базовые параметры send_params = { - 'audio': audio_input, - 'title': metadata['title'][:64], - 'performer': metadata['performer'][:64], - 'duration': int(metadata['duration']) if metadata['duration'] else None, - 'caption': f"🎵 {metadata['title']}\n👤 {metadata['performer']}" + "audio": audio_input, + "title": metadata["title"][:64], + "performer": metadata["performer"][:64], + "duration": int(metadata["duration"]) if metadata["duration"] else None, + "caption": f"🎵 {metadata['title']}\n👤 {metadata['performer']}", } # Добавляем обложку если есть - if metadata['thumbnail_data']: + if metadata["thumbnail_data"]: try: # Создаем временный файл для обложки - with tempfile.NamedTemporaryFile(suffix='.jpg', delete=False) as thumb_file: + with tempfile.NamedTemporaryFile( + suffix=".jpg", delete=False + ) as thumb_file: thumb_filename = thumb_file.name - thumb_file.write(metadata['thumbnail_data']) + thumb_file.write(metadata["thumbnail_data"]) # Используем FSInputFile для обложки - send_params['thumbnail'] = types.FSInputFile(thumb_filename) + send_params["thumbnail"] = types.FSInputFile(thumb_filename) logger.info("Обложка добавлена к сообщению") except Exception as e: @@ -62,7 +69,7 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot): await message.answer_audio(**send_params) # Удаляем временный файл обложки если создавали - if 'thumb_filename' in locals() and path.exists(thumb_filename): + if "thumb_filename" in locals() and path.exists(thumb_filename): unlink(thumb_filename) await status_msg.delete() @@ -74,8 +81,8 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot): await status_msg.edit_text(f"❌ Ошибка: {str(e)}") logger.error(f"Ошибка при скачивании: {e}") finally: - if 'filename' in locals() and path.exists(filename): + if "filename" in locals() and path.exists(filename): try: unlink(filename) - except: - pass \ No newline at end of file + except OSError: + pass diff --git a/addons/gpt/__init__.py b/addons/gpt/__init__.py new file mode 100644 index 0000000..e0acb2f --- /dev/null +++ b/addons/gpt/__init__.py @@ -0,0 +1,9 @@ +def register(dp, state, bot): + from . import handlers + + handlers.register_handlers(dp, state, bot) + + +def unregister(dp): + # Здесь можно удалить хендлеры, если нужно + dp.message_handlers.handlers.clear() diff --git a/addons/gpt/handlers.py b/addons/gpt/handlers.py new file mode 100644 index 0000000..b955ef5 --- /dev/null +++ b/addons/gpt/handlers.py @@ -0,0 +1,105 @@ +import base64 +import aiohttp +import logging +from aiogram import Dispatcher, Bot +from aiogram.types import Message +from utils.antispam import saving, save_message +from aiogram.filters import Command +from models.state import BotState + +logger = logging.getLogger(__name__) + +def register_handlers(dp: Dispatcher, state: BotState, bot: Bot): + chat_history = {} + MAX_HISTORY = 20 # храним последние 20 сообщений (user+assistant) + + @dp.message(Command("gpt")) + @saving + async def ask_gpt(message: Message): + chat_id = message.chat.id + if chat_id not in chat_history: + chat_history[chat_id] = [] + + content_blocks = [] + user_prompt = None + + # Текст после команды или caption + if message.text: + parts = message.text.split(maxsplit=1) + if len(parts) > 1: + user_prompt = parts[1] + if message.caption: + user_prompt = message.caption + + if user_prompt: + content_blocks.append({"type": "text", "text": user_prompt}) + + # Фото → base64 → image_url + if message.photo: + photo = message.photo[-1] + file = await bot.get_file(photo.file_id) + file_bytes = await bot.download_file(file.file_path) + image_b64 = base64.b64encode(file_bytes.read()).decode("utf-8") + content_blocks.append( + { + "type": "image_url", + "image_url": {"url": f"data:image/jpeg;base64,{image_b64}"}, + } + ) + + if not content_blocks: + await message.reply("❗ Укажи текст или прикрепи фото") + return + + url = "http://192.168.31.197:1234/v1/chat/completions" + + # Добавляем новое сообщение в историю + chat_history[chat_id].append({"role": "user", "content": content_blocks}) + + # Ограничиваем историю (оставляем последние MAX_HISTORY сообщений) + if len(chat_history[chat_id]) > MAX_HISTORY: + chat_history[chat_id] = chat_history[chat_id][-MAX_HISTORY:] + + payload = { + "model": "qwen/qwen3-vl-4b", + "messages": chat_history[ chat_id], + "temperature": 0.7, + "max_tokens": 4096, + "stream": False, + } + + try: + async with aiohttp.ClientSession() as session: + async with session.post(url, json=payload) as resp: + if resp.status != 200: + error_text = await resp.text() + await message.reply( + f"❌ Ошибка LM Studio: {resp.status} {error_text}" + ) + return + + data = await resp.json() + reply_text = data["choices"][0]["message"]["content"] + + # Сохраняем ответ ассистента в историю + chat_history[chat_id].append( + { + "role": "assistant", + "content": [{"type": "text", "text": reply_text}], + } + ) + + # Ограничиваем снова (чтобы не разрасталось) + if len(chat_history[chat_id]) > MAX_HISTORY: + chat_history[chat_id] = chat_history[chat_id][-MAX_HISTORY:] + + msg = await message.reply(f"🤖 Ответ:\n{reply_text}") + save_message(msg.chat.id, msg.message_id) + + except Exception as e: + logger.error(f"Ошибка при запросе к LM Studio: {e}") + await message.reply(f"❌ Ошибка при запросе к LM Studio: {e}") + + @dp.message(Command("clear")) + async def clear(message: Message): + chat_history.pop(message.chat.id, None) \ No newline at end of file diff --git a/addons/hello/__init__.py b/addons/hello/__init__.py index 8a3fbb5..e0acb2f 100644 --- a/addons/hello/__init__.py +++ b/addons/hello/__init__.py @@ -1,7 +1,9 @@ def register(dp, state, bot): from . import handlers + handlers.register_handlers(dp, state, bot) + def unregister(dp): # Здесь можно удалить хендлеры, если нужно dp.message_handlers.handlers.clear() diff --git a/addons/hello/handlers.py b/addons/hello/handlers.py index cb4150d..39f8cb2 100644 --- a/addons/hello/handlers.py +++ b/addons/hello/handlers.py @@ -5,10 +5,11 @@ from models.state import BotState from config import Config import logging from utils.antispam import admin_required -from storage.message_storage import save_message # импортируем функцию +from storage.message_storage import save_message # импортируем функцию logger = logging.getLogger(__name__) + def register_handlers(dp: Dispatcher, state: BotState, bot: Bot): @dp.message(Command("hello")) @admin_required(1) @@ -20,8 +21,7 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot): try: name = Config.Names.get(admin_id, "Админ") msg = await bot.send_message( - chat_id=admin_id, - text=f"🤖 Я готов к работе, господин {name}!" + chat_id=admin_id, text=f"🤖 Я готов к работе, господин {name}!" ) # сохраняем сообщение, отправленное админу save_message(msg.chat.id, msg.message_id) diff --git a/addons/id/__init__.py b/addons/id/__init__.py index 8a3fbb5..e0acb2f 100644 --- a/addons/id/__init__.py +++ b/addons/id/__init__.py @@ -1,7 +1,9 @@ def register(dp, state, bot): from . import handlers + handlers.register_handlers(dp, state, bot) + def unregister(dp): # Здесь можно удалить хендлеры, если нужно dp.message_handlers.handlers.clear() diff --git a/addons/id/handlers.py b/addons/id/handlers.py index 5c297dc..ed45d9f 100644 --- a/addons/id/handlers.py +++ b/addons/id/handlers.py @@ -9,9 +9,10 @@ API_URL = "http://127.0.0.1:7700/speak" logger = getLogger(__name__) + def register_handlers(dp: Dispatcher, state: BotState, bot: Bot): @dp.message(Command("id")) @saving async def id(message: Message): id = message.from_user.id - msg = await message.reply(str(id)) \ No newline at end of file + msg = await message.reply(str(id)) diff --git a/addons/manager.py b/addons/manager.py index 81c5b0e..904923d 100644 --- a/addons/manager.py +++ b/addons/manager.py @@ -2,6 +2,7 @@ import importlib import sys from pathlib import Path + class AddonManager: def __init__(self, dp, state, bot): self.dp = dp diff --git a/addons/poll/__init__.py b/addons/poll/__init__.py index 8a3fbb5..e0acb2f 100644 --- a/addons/poll/__init__.py +++ b/addons/poll/__init__.py @@ -1,7 +1,9 @@ def register(dp, state, bot): from . import handlers + handlers.register_handlers(dp, state, bot) + def unregister(dp): # Здесь можно удалить хендлеры, если нужно dp.message_handlers.handlers.clear() diff --git a/addons/poll/handlers.py b/addons/poll/handlers.py index 09c87b9..b9e3ee6 100644 --- a/addons/poll/handlers.py +++ b/addons/poll/handlers.py @@ -1,6 +1,4 @@ from config import Config -import aiohttp -from aiogram.types import BufferedInputFile from utils.antispam import admin_required from aiogram import Dispatcher, Bot from aiogram.types import Message @@ -11,22 +9,7 @@ from aiogram.types import PollAnswer from storage.message_storage import save_message logger = getLogger(__name__) -API_URL = "http://127.0.0.1:7700/speak" -from config import Config -import aiohttp -from aiogram.types import BufferedInputFile -from utils.antispam import admin_required -from aiogram import Dispatcher, Bot -from aiogram.types import Message -from models.state import BotState -from aiogram.filters import Command -from logging import getLogger -from aiogram.types import PollAnswer -from storage.message_storage import save_message - -logger = getLogger(__name__) -API_URL = "http://127.0.0.1:7700/speak" def register_handlers(dp: Dispatcher, state: BotState, bot: Bot): @dp.message(Command("poll")) @@ -37,9 +20,12 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot): poll_msg = await bot.send_poll( chat_id=chat_id, question="Кто опоздает?", - options=["Я", "Не знаю", "Наверное"], + options=["Я", "Я очень сильно опоздаю", "Наверное"], is_anonymous=False, - allows_multiple_answers=False + allows_multiple_answers=False, + ) + await bot.pin_chat_message( + chat_id, poll_msg.message_id, disable_notification=False ) # сохраняем сам опрос save_message(poll_msg.chat.id, poll_msg.message_id) @@ -48,9 +34,6 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot): except Exception as e: logger.error(f"Ошибка при отправке в чат {chat_id}: {e}") - confirm_msg = await message.answer("✅ Сообщение отправлено.") - save_message(confirm_msg.chat.id, confirm_msg.message_id) - @dp.poll_answer() async def handle_poll_answer(poll_answer: PollAnswer): user = poll_answer.user @@ -60,20 +43,30 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot): username = f"@{user.username}" if user.username else user.first_name # всегда пишем в первый чат из Config.CHAT_IDS + # 6394047531 if option_ids and option_ids[0] == 0: msg = await bot.send_message( - chat_id=6394047531, - text=f"{username} опоздает" + chat_id=6394047531, text=f"{username} опоздает" + ) + save_message(msg.chat.id, msg.message_id) + elif option_ids and option_ids[0] == 1: + msg = await bot.send_message( + chat_id=6394047531, text=f"{username} сильно опоздает" + ) + save_message(msg.chat.id, msg.message_id) + elif option_ids and option_ids[0] == 2: + msg = await bot.send_message( + chat_id=6394047531, text=f"{username} возможно опоздает" + ) + save_message(msg.chat.id, msg.message_id) + elif not option_ids: + msg = await bot.send_message( + chat_id=6394047531, text=f"{username} Отменил свой голос" ) save_message(msg.chat.id, msg.message_id) else: msg = await bot.send_message( - chat_id=6394047531, - text=f"{username} выбрал вариант {option_ids}" + chat_id=6394047531, text=f"{username} выбрал вариант {option_ids}" ) save_message(msg.chat.id, msg.message_id) - - - - diff --git a/addons/send_message/__init__.py b/addons/send_message/__init__.py index 8a3fbb5..e0acb2f 100644 --- a/addons/send_message/__init__.py +++ b/addons/send_message/__init__.py @@ -1,7 +1,9 @@ def register(dp, state, bot): from . import handlers + handlers.register_handlers(dp, state, bot) + def unregister(dp): # Здесь можно удалить хендлеры, если нужно dp.message_handlers.handlers.clear() diff --git a/addons/send_message/handlers.py b/addons/send_message/handlers.py index 494d6f0..b5a80b4 100644 --- a/addons/send_message/handlers.py +++ b/addons/send_message/handlers.py @@ -1,16 +1,22 @@ from config import Config -import aiohttp +import ssl +import certifi from aiogram.types import BufferedInputFile -from utils.antispam import admin_required +from storage.message_storage import save_message +from utils.antispam import admin_required, saving from aiogram import Dispatcher, Bot from aiogram.types import Message from models.state import BotState from aiogram.filters import Command from logging import getLogger + logger = getLogger(__name__) API_URL = "http://127.0.0.1:7700/speak" +ssl_context = ssl.create_default_context(cafile=certifi.where()) + + def register_handlers(dp: Dispatcher, state: BotState, bot: Bot): @dp.message(Command("vadmin")) @admin_required(0) @@ -21,11 +27,26 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot): return phrase = parts[1] - # Запрос к TTS API + # URL с параметрами модели и голоса + url = f"{Config.DEEPGRAM_TTS_URL}model=aura-2-andromeda-en" + + headers = { + "Authorization": f"Token {Config.DEEPGRAM_API_KEY}", + "Content-Type": "application/json", + } + + # В JSON только text + payload = {"text": phrase} + async with aiohttp.ClientSession() as session: - async with session.post(API_URL, json={"text": phrase}) as resp: + async with session.post( + url, headers=headers, json=payload, ssl=ssl_context + ) as resp: if resp.status != 200: - await message.reply("Ошибка генерации аудио") + error_text = await resp.text() + await message.reply( + f"Ошибка генерации аудио: {resp.status} {error_text}" + ) return audio_bytes = await resp.read() audio_file = BufferedInputFile(audio_bytes, filename="speech.wav") @@ -43,8 +64,12 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot): @admin_required(0) async def admin(message: Message): raw_text = message.text or message.caption - if not raw_text and not (message.photo or message.document or message.audio or message.video): - await message.reply("❌ Укажи текст или прикрепи файл/медиа: /admin <сообщение>") + if not raw_text and not ( + message.photo or message.document or message.audio or message.video + ): + await message.reply( + "❌ Укажи текст или прикрепи файл/медиа: /admin <сообщение>" + ) return # Отрезаем саму команду (/admin) @@ -60,15 +85,21 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot): elif message.document: # Документ - await bot.send_document(chat_id, message.document.file_id, caption=text_to_send) + await bot.send_document( + chat_id, message.document.file_id, caption=text_to_send + ) elif message.audio: # Аудио (музыка) - await bot.send_audio(chat_id, message.audio.file_id, caption=text_to_send) + await bot.send_audio( + chat_id, message.audio.file_id, caption=text_to_send + ) elif message.video: # Видео - await bot.send_video(chat_id, message.video.file_id, caption=text_to_send) + await bot.send_video( + chat_id, message.video.file_id, caption=text_to_send + ) else: # Только текст @@ -85,8 +116,12 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot): @admin_required(0) async def id_admin(message: Message): raw_text = message.text or message.caption - if not raw_text and not (message.photo or message.document or message.audio or message.video): - await message.reply("❌ Укажи ID чата и текст или прикрепи файл/медиа: /iadmin <сообщение>") + if not raw_text and not ( + message.photo or message.document or message.audio or message.video + ): + await message.reply( + "❌ Укажи ID чата и текст или прикрепи файл/медиа: /iadmin <сообщение>" + ) return # Отрезаем саму команду (/iadmin) @@ -111,19 +146,25 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot): elif message.document: # Документ - await bot.send_document(chat_id, message.document.file_id, caption=text_to_send) + await bot.send_document( + chat_id, message.document.file_id, caption=text_to_send + ) elif message.audio: # Аудио (музыка) - await bot.send_audio(chat_id, message.audio.file_id, caption=text_to_send) + await bot.send_audio( + chat_id, message.audio.file_id, caption=text_to_send + ) elif message.video: # Видео - await bot.send_video(chat_id, message.video.file_id, caption=text_to_send) + await bot.send_video( + chat_id, message.video.file_id, caption=text_to_send + ) else: # Только текст - await bot.send_message(chat_id, text_to_send) + await bot.send_message(chat_id, text_to_send, parse_mode="Markdown") logger.info(f"Сообщение отправлено в чат {chat_id}") await message.answer("✅ Сообщение отправлено.") diff --git a/bot/core.py b/bot/core.py index 57111b3..77d1414 100644 --- a/bot/core.py +++ b/bot/core.py @@ -13,22 +13,24 @@ class TelegramBot: def setup_handlers(self): """Регистрация всех обработчиков""" - from handlers import admin, schedule#, media, common + from handlers import admin, schedule # , media, common # Регистрируем обработчики из разных модулей admin.register_handlers(self.dp, self.state, self.bot) schedule.register_handlers(self.dp, self.state) - #media.register_handlers(self.dp, self.state, self.bot) - #common.register_handlers(self.dp, self.state, self.bot) + # media.register_handlers(self.dp, self.state, self.bot) + # common.register_handlers(self.dp, self.state, self.bot) - #add addons + # add addons self.addons.load("example_addon") self.addons.load("id") self.addons.load("send_message") self.addons.load("poll") self.addons.load("hello") + self.addons.load("draw") + self.addons.load("gpt") async def start(self): """Запуск бота""" self.setup_handlers() - await self.dp.start_polling(self.bot) \ No newline at end of file + await self.dp.start_polling(self.bot) diff --git a/config.py b/config.py index 3a02bc3..1275b1a 100644 --- a/config.py +++ b/config.py @@ -2,6 +2,7 @@ import os from dotenv import load_dotenv from typing import Dict + class Config: # Загружаем .env load_dotenv() @@ -9,19 +10,19 @@ class Config: # API API_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN") Token = os.getenv("ACCESS_TOKEN") + DEEPGRAM_API_KEY = os.getenv("DEEPGRAM_API_KEY") + DEEPGRAM_AGENT_URL = "https://api.deepgram.com/v1/agent/think" + DEEPGRAM_TTS_URL = "https://api.deepgram.com/v1/speak?" + # 5575756416 + BAN = [1] + if not API_TOKEN: raise ValueError("❌ TELEGRAM_BOT_TOKEN не найден в переменных окружения!") # Admins (user_id: уровень) - ADMINS: Dict[int, int] = { - 850906163: 0, - 6394047531: 4 - } + ADMINS: Dict[int, int] = {850906163: 0, 6394047531: 4, 1345058877: 3} - Names: Dict[int, str] = { - 850906163: "Ляпич", - 6394047531: "Прокопович" - } + Names: Dict[int, str] = {850906163: "Ляпич", 6394047531: "Прокопович"} # Chats CHAT_IDS = [-1003038389942] diff --git a/handlers/admin.py b/handlers/admin.py index 7ff408c..64b4f2c 100644 --- a/handlers/admin.py +++ b/handlers/admin.py @@ -12,9 +12,6 @@ from utils.analytics import create_statistics_text logger = getLogger(__name__) - - - def register_handlers(dp: Dispatcher, state: BotState, bot: Bot): @dp.message(Command("log")) @saving @@ -32,6 +29,7 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot): async def send_status(message: Message): from utils.analytics import analyze_bot_logs from utils.mac_metrics import get_macbook_battery_level, get_process_usage + try: stats = analyze_bot_logs(Config.LOG_FILE) batt = await get_macbook_battery_level() @@ -42,8 +40,8 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot): f"✅ Uptime: {stats.get('uptime_percentage', 0)}%\n" f"⏱️ Слежка расписания: {'ВКЛ' if state.watcher_work else 'ВЫКЛ'}\n" f"🔋 Уровень заряда: {batt}%\n" - f"🖥️ Загрузка цп: {usage["cpu_percent"]}\n" - f"🧠 Загрузка оперативки: {usage["rss_mb"]:.2f} MB\n" + f"🖥️ Загрузка цп: {usage['cpu_percent']}\n" + f"🧠 Загрузка оперативки: {usage['rss_mb']:.2f} MB\n" ) await message.answer(status_text) except Exception as e: @@ -54,16 +52,21 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot): @admin_required(1) async def stat(message: Message): from utils.analytics import analyze_bot_logs + stats = analyze_bot_logs(Config.LOG_FILE) - await message.answer(create_statistics_text(stats), reply_to_message_id=message.message_id) + await message.answer( + create_statistics_text(stats), reply_to_message_id=message.message_id + ) @dp.message(Command("del")) @admin_required(1) async def delete_all_messages(message: Message): messages = load_messages() if not messages: - sent = await message.answer("📭 Нет сохранённых сообщений для удаления.", - reply_to_message_id=message.message_id) + sent = await message.answer( + "📭 Нет сохранённых сообщений для удаления.", + reply_to_message_id=message.message_id, + ) save_message(sent.chat.id, sent.message_id) return @@ -76,8 +79,10 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot): logger.warning(f"Не удалось удалить {msg_id} в чате {chat_id}: {e}") clear_messages() - sent = await message.answer(f"✅ Удалено {deleted} сообщений (включая /rasp).", - reply_to_message_id=message.message_id) + sent = await message.answer( + f"✅ Удалено {deleted} сообщений (включая /rasp).", + reply_to_message_id=message.message_id, + ) save_message(sent.chat.id, sent.message_id) @dp.message(Command("power")) diff --git a/handlers/schedule.py b/handlers/schedule.py index 7697154..c0e1bc3 100644 --- a/handlers/schedule.py +++ b/handlers/schedule.py @@ -22,7 +22,7 @@ def register_handlers(dp: Dispatcher, state: BotState): schedule_service = ScheduleService() text, url, day, month = await schedule_service.get_schedule(group, day_offset) # Отправляем текст расписания - msg = await message.answer(text, parse_mode="Markdown") + msg = await message.answer(text, parse_mode="Markdownv2") save_message(message.chat.id, msg.message_id) @@ -38,17 +38,19 @@ def register_handlers(dp: Dispatcher, state: BotState): day_offset = int(args[2]) if len(args) > 2 and args[2].isdigit() else 0 schedule_service = ScheduleService() - clip_png, url, day, mouth = await schedule_service.get_pschedule(group, day_offset) + clip_png, url, day, mouth = await schedule_service.get_pschedule( + group, day_offset + ) if clip_png: save_message(message.chat.id, message.message_id) msg = await message.answer_photo( types.BufferedInputFile(clip_png, filename=f"{group}.png"), - caption=f"Расписание для {group} на {day}.{mouth:02d}" + caption=f"Расписание для {group} на {day}.{mouth:02d}", ) save_message(message.chat.id, msg.message_id) state.file_id_cache[group.lower()] = msg.photo[-1].file_id else: - await message.answer(f"Не удалось найти расписание для {group}") \ No newline at end of file + await message.answer(f"Не удалось найти расписание для {group}") diff --git a/main.py b/main.py index 33d32c4..2a794f1 100644 --- a/main.py +++ b/main.py @@ -8,15 +8,13 @@ basicConfig( level=INFO, format="%(asctime)s [%(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S", - handlers=[ - FileHandler(Config.LOG_FILE, encoding="utf-8"), - StreamHandler() - ], - force=True + handlers=[FileHandler(Config.LOG_FILE, encoding="utf-8"), StreamHandler()], + force=True, ) logger = getLogger(__name__) + async def main(): """Основная функция запуска""" try: @@ -28,5 +26,6 @@ async def main(): finally: logger.info("Бот остановлен") + if __name__ == "__main__": - run(main()) \ No newline at end of file + run(main()) diff --git a/models/state.py b/models/state.py index 5ef8a30..0e8b321 100644 --- a/models/state.py +++ b/models/state.py @@ -2,9 +2,11 @@ from dataclasses import dataclass from typing import Dict, Optional from asyncio import Task + @dataclass class BotState: """Состояние бота""" + last_chat_time: Dict[int, str] = None last_pinned: Dict[str, int] = None watcher_work: bool = False @@ -23,4 +25,4 @@ class BotState: if self.last_day is None: self.last_day = {} if self.last_clip_hash is None: - self.last_clip_hash = {} \ No newline at end of file + self.last_clip_hash = {} diff --git a/services/schedule_service.py b/services/schedule_service.py index f4ffc32..f39cc92 100644 --- a/services/schedule_service.py +++ b/services/schedule_service.py @@ -12,7 +12,9 @@ logger = logging.getLogger(__name__) class ScheduleService: def __init__(self): - self.base_url = "https://college.by/accounts/raspis/{mouth:02d}/{day:02d}-PODNAM.htm" + self.base_url = ( + "https://college.by/accounts/raspis/{mouth:02d}/{day:02d}-PODNAM.htm" + ) def _make_url(self, day: int = 0) -> Tuple[str, int, int]: """Генерация URL для расписания""" @@ -24,7 +26,11 @@ class ScheduleService: d += timedelta(days=1) return self.base_url.format(day=d.day, mouth=d.month), d.day, d.month else: - return self.base_url.format(day=int(day), mouth=d.month), int(day), int(d.month) + return ( + self.base_url.format(day=int(day), mouth=d.month), + int(day), + int(d.month), + ) async def get_schedule( self, group: str, day_offset: int = 0 @@ -39,11 +45,13 @@ class ScheduleService: connector = aiohttp.TCPConnector(ssl=ssl_context) headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36' + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36" } # тут можно использовать aiohttp + chardet/charset_normalizer - async with aiohttp.ClientSession(connector=connector, headers=headers) as session: + async with aiohttp.ClientSession( + connector=connector, headers=headers + ) as session: async with session.get(url) as resp: raw_bytes = await resp.read() @@ -71,24 +79,24 @@ class ScheduleService: else: result = f"📅 Расписание для {day} числа:\n```\n" for line in schedule_lines: - formatted = ( - line.replace("¦", "│") - .replace(" ", " ") - .strip() - ) + formatted = line.replace("¦", "│").replace(" ", " ").strip() if formatted: result += f"{formatted}\n" result += "```" return result, url, day, month - async def get_pschedule(self, group: str, day_offset: int = 0) -> Tuple[Optional[bytes], str, int, int]: + async def get_pschedule( + self, group: str, day_offset: int = 0 + ) -> Tuple[Optional[bytes], str, int, int]: """Получение скриншота расписания""" url, day, month = self._make_url(day_offset) async with async_playwright() as p: browser = await p.chromium.launch(headless=True) - context = await browser.new_context(viewport=ViewportSize(width=400, height=3000)) + context = await browser.new_context( + viewport=ViewportSize(width=400, height=3000) + ) page = await context.new_page() try: @@ -108,7 +116,7 @@ class ScheduleService: x=float(max(box["x"] - 0, 0)), y=float(max(box["y"] - 0, 0)), width=float(box["width"] + 150), - height=float(box["height"] + 100) + height=float(box["height"] + 100), ) return await page.screenshot(clip=clip_rect), url, day, month @@ -118,4 +126,4 @@ class ScheduleService: await context.close() await browser.close() - return None, url, day, month \ No newline at end of file + return None, url, day, month diff --git a/services/watcher_service.py b/services/watcher_service.py index 175bf06..7b098c3 100644 --- a/services/watcher_service.py +++ b/services/watcher_service.py @@ -44,10 +44,19 @@ class WatcherService: """Основной цикл слежки""" while self.state.watcher_work: try: - await self._check_all_groups() - delay = randint(Config.WATCHER_BASE_DELAY, Config.WATCHER_BASE_DELAY + 100) - logger.info(f"Следущая проверка через {delay}") - await asyncio.sleep(delay) + find = await self._check_all_groups() + if find: + # ничего не нашли → ждём + delay = randint( + Config.WATCHER_BASE_DELAY, Config.WATCHER_BASE_DELAY + 100 + ) + logger.info(f"Следующая проверка через {delay}") + await asyncio.sleep(delay) + else: + # нашли → останавливаемся + logger.info("Расписание найдено, останавливаем watcher") + self.state.watcher_work = False + break except asyncio.CancelledError: break except Exception as e: @@ -63,33 +72,41 @@ class WatcherService: target += timedelta(days=1) return target - - async def _check_all_groups(self): - """Проверка всех групп на изменения""" + async def _check_all_groups(self) -> bool: + """ + Возвращает True, если НИ в одной группе не найдено расписание. + Возвращает False, если хотя бы в одной группе найдено расписание. + """ day = self._get_target_day() + found_any = False for group, chat_id in Config.GROUP_CHATS.items(): - logger.info(f"Проверяем расписание для {group} на {day.strftime('%d.%m.%Y')}") - await self._check_group_schedule(group, chat_id, day.day) + logger.info( + f"Проверяем расписание для {group} на {day.strftime('%d.%m.%Y')}" + ) + found = await self._check_group_schedule(group, chat_id, day.day) + if found: + found_any = True - async def _check_group_schedule(self, group: str, chat_id: int, day: int): - """Проверка расписания для конкретной группы""" - text, url, data_day, data_month = await self.schedule_service.get_schedule(group, day) + return not found_any # <-- вот так правильно + async def _check_group_schedule(self, group: str, chat_id: int, day: int) -> bool: + text, url, data_day, data_month = await self.schedule_service.get_schedule( + group, day + ) if text and "не найдено" not in text.lower(): msg = await self.bot.send_message( chat_id, f"Авто-расписание для {group} на {data_day:02d}.{data_month:02d}\n\n{text}", - parse_mode="Markdown" + parse_mode="Markdown", ) - await self.bot.pin_chat_message(chat_id, msg.message_id, disable_notification=True) - else: - logger.warning( - f"Не удалось получить расписание для {group}, {data_day}, {data_month}, {url}" + await self.bot.pin_chat_message( + chat_id, msg.message_id, disable_notification=False ) - return + return True + return False - #clip_hash = hashlib.md5(clip_png).hexdigest() + # clip_hash = hashlib.md5(clip_png).hexdigest() # Логика проверки изменений и отправки сообщений - # ... (ваша существующая логика) \ No newline at end of file + # ... (ваша существующая логика) diff --git a/storage/DB.py b/storage/DB.py index 2373062..9facc23 100644 --- a/storage/DB.py +++ b/storage/DB.py @@ -1,5 +1,4 @@ import sqlite3 -import os DIR = "/Users/mac/myfirstprogramm/storage/message.db" if __name__ == "__main__": @@ -19,8 +18,5 @@ if __name__ == "__main__": db.close() - def get_db(): return sqlite3.connect(DIR) - - diff --git a/storage/message_storage.py b/storage/message_storage.py index 30910b1..0add373 100644 --- a/storage/message_storage.py +++ b/storage/message_storage.py @@ -1,5 +1,6 @@ from .DB import get_db + def save_message(chat_id: int, message_id: int): db = get_db() cur = db.cursor() @@ -8,6 +9,7 @@ def save_message(chat_id: int, message_id: int): cur.close() db.close() + def load_messages(): db = get_db() cur = db.cursor() @@ -17,6 +19,7 @@ def load_messages(): db.close() return rows + def clear_messages(): db = get_db() cur = db.cursor() @@ -24,7 +27,3 @@ def clear_messages(): db.commit() cur.close() db.close() - - - - diff --git a/utils/analytics.py b/utils/analytics.py index 9afe6a2..fc57e15 100644 --- a/utils/analytics.py +++ b/utils/analytics.py @@ -6,13 +6,12 @@ import tempfile import json - def analyze_bot_logs(log_file_path="bot.log"): """ Анализирует логи бота и создает детальную статистику """ try: - with open(log_file_path, 'r', encoding='utf-8') as log: + with open(log_file_path, "r", encoding="utf-8") as log: lines = log.readlines() except FileNotFoundError: return {"error": "Лог файл не найден"} @@ -24,27 +23,27 @@ def analyze_bot_logs(log_file_path="bot.log"): # Основные счетчики stats = { - 'total_lines': len(lines), - 'time_period': {}, - 'log_levels': Counter(), - 'activities': Counter(), - 'errors': Counter(), - 'warnings': Counter(), - 'user_commands': Counter(), - 'groups': Counter(), - 'restarts': 0, - 'schedule_checks': 0, - 'schedule_changes': 0, - 'schedule_failures': 0, - 'network_errors': 0, - 'browser_errors': 0, - 'telegram_errors': Counter(), - 'performance': { - 'avg_handling_time': 0, - 'fastest_handling': float('inf'), - 'slowest_handling': 0, - 'handling_count': 0 - } + "total_lines": len(lines), + "time_period": {}, + "log_levels": Counter(), + "activities": Counter(), + "errors": Counter(), + "warnings": Counter(), + "user_commands": Counter(), + "groups": Counter(), + "restarts": 0, + "schedule_checks": 0, + "schedule_changes": 0, + "schedule_failures": 0, + "network_errors": 0, + "browser_errors": 0, + "telegram_errors": Counter(), + "performance": { + "avg_handling_time": 0, + "fastest_handling": float("inf"), + "slowest_handling": 0, + "handling_count": 0, + }, } # Временные метрики @@ -53,11 +52,11 @@ def analyze_bot_logs(log_file_path="bot.log"): handling_times = [] # Регулярные выражения для парсинга - timestamp_pattern = r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})' - log_level_pattern = r'\[(INFO|WARNING|ERROR)\]' - handling_time_pattern = r'Duration (\d+) ms' - command_pattern = r'Команда /rasp от ([\d-]+), группа=([^,]+), дата=(\d+)' - schedule_pattern = r'Проверяем расписание для ([^ ]+) на (\d{2}\.\d{2}\.\d{4})' + timestamp_pattern = r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})" + log_level_pattern = r"\[(INFO|WARNING|ERROR)\]" + handling_time_pattern = r"Duration (\d+) ms" + command_pattern = r"Команда /rasp от ([\d-]+), группа=([^,]+), дата=(\d+)" + schedule_pattern = r"Проверяем расписание для ([^ ]+) на (\d{2}\.\d{2}\.\d{4})" for line in lines: # Извлекаем временную метку @@ -72,86 +71,88 @@ def analyze_bot_logs(log_file_path="bot.log"): level_match = re.search(log_level_pattern, line) if level_match: level = level_match.group(1) - stats['log_levels'][level] += 1 + stats["log_levels"][level] += 1 # Время обработки сообщений time_match = re.search(handling_time_pattern, line) - if time_match and 'is handled' in line: + if time_match and "is handled" in line: handling_time = int(time_match.group(1)) handling_times.append(handling_time) - stats['performance']['handling_count'] += 1 - stats['performance']['slowest_handling'] = max( - stats['performance']['slowest_handling'], handling_time + stats["performance"]["handling_count"] += 1 + stats["performance"]["slowest_handling"] = max( + stats["performance"]["slowest_handling"], handling_time ) - stats['performance']['fastest_handling'] = min( - stats['performance']['fastest_handling'], handling_time + stats["performance"]["fastest_handling"] = min( + stats["performance"]["fastest_handling"], handling_time ) # Команды пользователей cmd_match = re.search(command_pattern, line) if cmd_match: user_id, group, date_offset = cmd_match.groups() - stats['user_commands'][group] += 1 - stats['groups'][group] += 1 + stats["user_commands"][group] += 1 + stats["groups"][group] += 1 # Проверки расписания - if 'Проверяем расписание' in line: - stats['schedule_checks'] += 1 + if "Проверяем расписание" in line: + stats["schedule_checks"] += 1 sched_match = re.search(schedule_pattern, line) if sched_match: group, date = sched_match.groups() - stats['groups'][group] += 1 + stats["groups"][group] += 1 # Изменения расписания - if 'Изменения найдены' in line: - stats['schedule_changes'] += 1 + if "Изменения найдены" in line: + stats["schedule_changes"] += 1 # Ошибки расписания - if 'Не удалось получить расписание' in line: - stats['schedule_failures'] += 1 + if "Не удалось получить расписание" in line: + stats["schedule_failures"] += 1 # Перезапуски бота - if 'Бот запускается' in line: - stats['restarts'] += 1 + if "Бот запускается" in line: + stats["restarts"] += 1 # Сетевые ошибки - if 'Failed to fetch updates' in line: - stats['network_errors'] += 1 + if "Failed to fetch updates" in line: + stats["network_errors"] += 1 # Ошибки браузера - if 'TargetClosedError' in line or 'BrowserContext.close' in line: - stats['browser_errors'] += 1 + if "TargetClosedError" in line or "BrowserContext.close" in line: + stats["browser_errors"] += 1 # Ошибки Telegram API - if 'Telegram server says' in line: - error_msg = line.split('Telegram server says - ')[-1].split(':')[0] - stats['telegram_errors'][error_msg] += 1 + if "Telegram server says" in line: + error_msg = line.split("Telegram server says - ")[-1].split(":")[0] + stats["telegram_errors"][error_msg] += 1 # Сбор ошибок и предупреждений - if '[ERROR]' in line: - error_msg = line.split('[ERROR]')[-1].strip() - stats['errors'][error_msg[:100]] += 1 + if "[ERROR]" in line: + error_msg = line.split("[ERROR]")[-1].strip() + stats["errors"][error_msg[:100]] += 1 - if '[WARNING]' in line: - warning_msg = line.split('[WARNING]')[-1].strip() - stats['warnings'][warning_msg[:100]] += 1 + if "[WARNING]" in line: + warning_msg = line.split("[WARNING]")[-1].strip() + stats["warnings"][warning_msg[:100]] += 1 # Расчет средней скорости обработки if handling_times: - stats['performance']['avg_handling_time'] = sum(handling_times) / len(handling_times) + stats["performance"]["avg_handling_time"] = sum(handling_times) / len( + handling_times + ) # Период работы if start_time and end_time: - stats['time_period'] = { - 'start': start_time, - 'end': end_time, - 'duration_hours': calculate_duration_hours(start_time, end_time) + stats["time_period"] = { + "start": start_time, + "end": end_time, + "duration_hours": calculate_duration_hours(start_time, end_time), } # Дополнительные метрики - stats['success_rate'] = calculate_success_rate(stats) - stats['uptime_percentage'] = calculate_uptime_percentage(stats) - stats['schedule_success_rate'] = calculate_schedule_success_rate(stats) + stats["success_rate"] = calculate_success_rate(stats) + stats["uptime_percentage"] = calculate_uptime_percentage(stats) + stats["schedule_success_rate"] = calculate_schedule_success_rate(stats) return stats @@ -159,7 +160,7 @@ def analyze_bot_logs(log_file_path="bot.log"): def calculate_duration_hours(start_str, end_str): """Вычисляет продолжительность в часах""" try: - fmt = '%Y-%m-%d %H:%M:%S' + fmt = "%Y-%m-%d %H:%M:%S" start = datetime.strptime(start_str, fmt) end = datetime.strptime(end_str, fmt) return round((end - start).total_seconds() / 3600, 2) @@ -169,20 +170,22 @@ def calculate_duration_hours(start_str, end_str): def calculate_success_rate(stats): """Рассчитывает процент успешных операций""" - total_operations = stats['performance']['handling_count'] + sum(stats['errors'].values()) + total_operations = stats["performance"]["handling_count"] + sum( + stats["errors"].values() + ) if total_operations == 0: return 0 - success_rate = (stats['performance']['handling_count'] / total_operations) * 100 + success_rate = (stats["performance"]["handling_count"] / total_operations) * 100 return round(success_rate, 2) def calculate_uptime_percentage(stats): """Рассчитывает процент времени работы""" - if stats['time_period'].get('duration_hours', 0) == 0: + if stats["time_period"].get("duration_hours", 0) == 0: return 0 # Предполагаем, что каждый перезапуск занимает ~10 секунд - restart_downtime = stats['restarts'] * 10 / 3600 - total_hours = stats['time_period']['duration_hours'] + restart_downtime = stats["restarts"] * 10 / 3600 + total_hours = stats["time_period"]["duration_hours"] uptime_hours = total_hours - restart_downtime uptime_percentage = (uptime_hours / total_hours) * 100 return round(uptime_percentage, 2) @@ -190,17 +193,17 @@ def calculate_uptime_percentage(stats): def calculate_schedule_success_rate(stats): """Рассчитывает процент успешных проверок расписания""" - total_checks = stats['schedule_checks'] + total_checks = stats["schedule_checks"] if total_checks == 0: return 0 - successful_checks = total_checks - stats['schedule_failures'] + successful_checks = total_checks - stats["schedule_failures"] success_rate = (successful_checks / total_checks) * 100 return round(success_rate, 2) def create_statistics_text(stats): """Создает текстовый отчет статистики с расширенными метриками""" - if 'error' in stats: + if "error" in stats: return f"❌ Ошибка анализа логов: {stats['error']}" text = "📊 СТАТИСТИКА РАБОТЫ БОТА\n" @@ -213,7 +216,9 @@ def create_statistics_text(stats): text += f"• Строк в логе: {stats['total_lines']:,}\n\n" # Производительность - handling_times = stats.get("handling_times", []) # сохрани список в analyze_bot_logs + handling_times = stats.get( + "handling_times", [] + ) # сохрани список в analyze_bot_logs median_time = statistics.median(handling_times) if handling_times else 0 text += "⚡ ПРОИЗВОДИТЕЛЬНОСТЬ:\n" @@ -223,11 +228,11 @@ def create_statistics_text(stats): text += f"• Успешных операций: {stats['success_rate']}%\n\n" # Статус работы - duration = stats['time_period'].get('duration_hours', 0) - errors_total = sum(stats['errors'].values()) + duration = stats["time_period"].get("duration_hours", 0) + errors_total = sum(stats["errors"].values()) errors_per_hour = round(errors_total / duration, 2) if duration else 0 - restarts = stats['restarts'] + restarts = stats["restarts"] mtbf = round(duration / restarts, 2) if restarts else duration text += "🔄 СТАТУС РАБОТЫ:\n" @@ -251,9 +256,9 @@ def create_statistics_text(stats): text += f"• Браузера: {stats['browser_errors']}\n" # Топ-3 ошибок - if stats['errors']: + if stats["errors"]: text += "• Топ ошибок:\n" - for err, count in stats['errors'].most_common(3): + for err, count in stats["errors"].most_common(3): text += f" - {err} ({count})\n" text += "\n" @@ -262,9 +267,9 @@ def create_statistics_text(stats): text += f"• Команд: {sum(stats['user_commands'].values())}\n" text += f"• Групп: {len(stats['groups'])}\n" - if stats['groups']: + if stats["groups"]: text += "• Топ групп:\n" - for group, count in stats['groups'].most_common(3): + for group, count in stats["groups"].most_common(3): text += f" - {group}: {count}\n" return text @@ -272,13 +277,14 @@ def create_statistics_text(stats): def create_statistics_file(stats): """Создает временный файл с полной статистикой""" - if 'error' in stats: + if "error" in stats: return None # Создаем временный файл - with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', - suffix='.json', delete=False) as f: + with tempfile.NamedTemporaryFile( + mode="w", encoding="utf-8", suffix=".json", delete=False + ) as f: json.dump(stats, f, ensure_ascii=False, indent=2, default=str) temp_filename = f.name - return temp_filename \ No newline at end of file + return temp_filename diff --git a/utils/antispam.py b/utils/antispam.py index 8b57edb..fc6c525 100644 --- a/utils/antispam.py +++ b/utils/antispam.py @@ -21,9 +21,6 @@ def is_chat_spam(chat_id: int, state: BotState) -> bool: return False -from functools import wraps -from aiogram import types - def admin_required(need_level: int): """Декоратор для проверки прав администратора (0 = высший уровень)""" @@ -41,12 +38,16 @@ def admin_required(need_level: int): return await func(message, *args, **kwargs) return wrapper + return decorator + def saving(func): """Декоратор для сохранения входящего сообщения""" + @wraps(func) async def wrapper(message: types.Message, *args, **kwargs): save_message(message.chat.id, message.message_id) return await func(message, *args, **kwargs) - return wrapper \ No newline at end of file + + return wrapper diff --git a/utils/mac_metrics.py b/utils/mac_metrics.py index 453f738..b868320 100644 --- a/utils/mac_metrics.py +++ b/utils/mac_metrics.py @@ -1,11 +1,14 @@ import asyncio import os + async def get_macbook_battery_level(): process = await asyncio.create_subprocess_exec( - "pmset", "-g", "batt", + "pmset", + "-g", + "batt", stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE + stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await process.communicate() if process.returncode != 0: @@ -28,9 +31,13 @@ async def get_process_usage(pid=None): pid = os.getpid() process = await asyncio.create_subprocess_exec( - "ps", "-p", str(pid), "-o", "%cpu,%mem,rss,comm", + "ps", + "-p", + str(pid), + "-o", + "%cpu,%mem,rss,comm", stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE + stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await process.communicate() if process.returncode != 0: @@ -47,19 +54,18 @@ async def get_process_usage(pid=None): "command": comm, "cpu_percent": float(cpu), "mem_percent": float(mem_percent), - "rss_mb": int(rss_kb) / 1024 # переводим КБ → МБ + "rss_mb": int(rss_kb) / 1024, # переводим КБ → МБ } - - - async def main(): battery = await get_macbook_battery_level() usage = await get_process_usage() print(f"🔋 Батарея: {battery}%") - print(f"🖥 CPU: {usage['cpu_percent']}% | MEM: {usage['mem_percent']}% | RSS: {usage['rss_mb']:.2f} MB") + print( + f"🖥 CPU: {usage['cpu_percent']}% | MEM: {usage['mem_percent']}% | RSS: {usage['rss_mb']:.2f} MB" + ) if __name__ == "__main__":