It's version 0.4

This commit is contained in:
Niken
2025-10-19 14:28:41 +03:00
parent 772d3d5b83
commit 7b653d4dcc
32 changed files with 775 additions and 326 deletions
+2
View File
@@ -1,7 +1,9 @@
def register(dp, state, bot): def register(dp, state, bot):
from . import handlers from . import handlers
handlers.register_handlers(dp, state, bot) handlers.register_handlers(dp, state, bot)
def unregister(dp): def unregister(dp):
# Здесь можно удалить хендлеры, если нужно # Здесь можно удалить хендлеры, если нужно
dp.message_handlers.handlers.clear() dp.message_handlers.handlers.clear()
+57 -37
View File
@@ -12,8 +12,8 @@ import uuid
from config import Config from config import Config
# Настройка кодировки для всего приложения # Настройка кодировки для всего приложения
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8")
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -24,13 +24,15 @@ LIMIT = 2 * 1024 * 1024 * 1024 # 2 ГБ
async def safe_filename(name: str) -> str: async def safe_filename(name: str) -> str:
"""Создает безопасное имя файла""" """Создает безопасное имя файла"""
# Нормализуем Unicode символы # Нормализуем Unicode символы
normalized = unicodedata.normalize('NFKD', name) normalized = unicodedata.normalize("NFKD", name)
# Убираем акценты и специальные символы, оставляем только ASCII # Убираем акценты и специальные символы, оставляем только 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]}" 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""" """Получает информацию о видео через yt-dlp"""
try: try:
process = await asyncio.create_subprocess_exec( process = await asyncio.create_subprocess_exec(
'yt-dlp', "yt-dlp",
'--dump-json', "--dump-json",
'--no-playlist', "--no-playlist",
url, url,
stdout=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE stderr=asyncio.subprocess.PIPE,
) )
stdout, stderr = await process.communicate() stdout, stderr = await process.communicate()
if process.returncode == 0: if process.returncode == 0:
result = json.loads(stdout.decode('utf-8', errors='ignore')) result = json.loads(stdout.decode("utf-8", errors="ignore"))
logger.info(f"Информация о видео получена: {result.get('title', 'Unknown')}") logger.info(
f"Информация о видео получена: {result.get('title', 'Unknown')}"
)
return result return result
else: else:
error_msg = stderr.decode('utf-8', errors='ignore') error_msg = stderr.decode("utf-8", errors="ignore")
logger.warning(f"yt-dlp ошибка: {error_msg}") logger.warning(f"yt-dlp ошибка: {error_msg}")
except Exception as e: except Exception as e:
@@ -75,38 +79,54 @@ async def download_mp4_to_dropbox(url: str) -> tuple[str, dict]:
duration = 0 duration = 0
if video_info: if video_info:
title = await safe_filename(video_info.get('title', 'Unknown_Title')) title = await safe_filename(video_info.get("title", "Unknown_Title"))
uploader = await safe_filename(video_info.get('uploader', 'Unknown_Uploader')) uploader = await safe_filename(
duration = video_info.get('duration', 0) video_info.get("uploader", "Unknown_Uploader")
)
duration = video_info.get("duration", 0)
logger.info(f"Обработано видео: {title}") logger.info(f"Обработано видео: {title}")
# ОПТИМИЗИРОВАННЫЕ НАСТРОЙКИ ДЛЯ СКОРОСТИ # ОПТИМИЗИРОВАННЫЕ НАСТРОЙКИ ДЛЯ СКОРОСТИ
download_process = await asyncio.create_subprocess_exec( download_process = await asyncio.create_subprocess_exec(
'yt-dlp', "yt-dlp",
'-f', 'bestvideo[height<=720][filesize<800M]+bestaudio/best[height<=720][filesize<800M]', "-f",
'--no-playlist', "bestvideo[height<=720][filesize<800M]+bestaudio/best[height<=720][filesize<800M]",
'-o', output_template, "--no-playlist",
'--ignore-errors', "-o",
'--no-warnings', output_template,
'--format-sort', 'quality,res:720,size:800M', "--ignore-errors",
'--concurrent-fragments', '4', "--no-warnings",
"--format-sort",
"quality,res:720,size:800M",
"--concurrent-fragments",
"4",
url, url,
stdout=asyncio.subprocess.PIPE, 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: 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}") logger.error(f"Ошибка скачивания: {error_msg}")
raise Exception(f"Ошибка скачивания: {error_msg}") raise Exception(f"Ошибка скачивания: {error_msg}")
mp4_files = glob.glob(os.path.join(temp_dir, "*.mp4")) mp4_files = glob.glob(os.path.join(temp_dir, "*.mp4"))
if not mp4_files: if not mp4_files:
video_files = glob.glob(os.path.join(temp_dir, "*.*")) 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: if video_files:
mp4_files = [video_files[0]] 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) dbx = dropbox.Dropbox(DROPBOX_TOKEN)
dropbox_path = f"/{final_filename}" 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: with open(actual_file, "rb") as f:
file_data = f.read() file_data = f.read()
dbx.files_upload( dbx.files_upload(
file_data, file_data,
dropbox_path, dropbox_path,
mode=dropbox.files.WriteMode("overwrite") mode=dropbox.files.WriteMode("overwrite"),
) )
shared_link = dbx.sharing_create_shared_link_with_settings(dropbox_path) shared_link = dbx.sharing_create_shared_link_with_settings(dropbox_path)
link = shared_link.url.replace("?dl=0", "?dl=1") link = shared_link.url.replace("?dl=0", "?dl=1")
metadata = { metadata = {
'title': title, "title": title,
'uploader': uploader, "uploader": uploader,
'duration': duration, "duration": duration,
'filesize': size, "filesize": size,
'quality': 'optimized for speed' "quality": "optimized for speed",
} }
logger.info(f"Успешно загружено в Dropbox: {link}") logger.info(f"Успешно загружено в Dropbox: {link}")
@@ -154,5 +176,3 @@ async def download_mp4_to_dropbox(url: str) -> tuple[str, dict]:
except Exception as e: except Exception as e:
logger.error(f"Общая ошибка: {e}") logger.error(f"Общая ошибка: {e}")
raise e raise e
+8 -7
View File
@@ -2,18 +2,16 @@ import logging
from aiogram import Dispatcher, Bot from aiogram import Dispatcher, Bot
from aiogram.filters import Command from aiogram.filters import Command
from models.state import BotState from models.state import BotState
from utils.antispam import admin_required
from .dowmp4 import download_mp4_to_dropbox from .dowmp4 import download_mp4_to_dropbox
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def register_handlers(dp: Dispatcher, state: BotState, bot: Bot): def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
@dp.message(Command("dowmp4")) @dp.message(Command("dowmp4"))
#@admin_required(4) # @admin_required(4)
async def dowmp4_handler(message): async def dowmp4_handler(message):
"""Обработчик команды /dowmp4""" """Обработчик команды /dowmp4"""
try: try:
@@ -22,7 +20,9 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
await message.answer("Пожалуйста, укажите URL видео после команды /dowmp4") await message.answer("Пожалуйста, укажите URL видео после команды /dowmp4")
return return
processing_msg = await message.answer("⏳ Начинаю обработку видео... Это может занять несколько минут.") processing_msg = await message.answer(
"⏳ Начинаю обработку видео... Это может занять несколько минут."
)
try: try:
# Скачиваем и загружаем в Dropbox # Скачиваем и загружаем в Dropbox
@@ -44,12 +44,13 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
await message.answer( await message.answer(
f"✅ **Видео успешно обработано!**\n\n{caption}", f"✅ **Видео успешно обработано!**\n\n{caption}",
parse_mode="Markdown", parse_mode="Markdown",
disable_web_page_preview=True disable_web_page_preview=True,
) )
except ValueError as e: except ValueError as e:
await message.answer(f"❌ Ошибка: {str(e)}") await message.answer(f"❌ Ошибка: {str(e)}")
except Exception as e: except Exception as e:
logger.error(f"Ошибка при обработке /dowmp4: {e}", exc_info=True) logger.error(f"Ошибка при обработке /dowmp4: {e}", exc_info=True)
await message.answer("❌ Произошла ошибка при обработке видео. Попробуйте позже.") await message.answer(
"❌ Произошла ошибка при обработке видео. Попробуйте позже."
)
+9
View File
@@ -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()
+190
View File
@@ -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, # можно 1520
"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}")
+2
View File
@@ -1,7 +1,9 @@
def register(dp, state, bot): def register(dp, state, bot):
from . import handlers from . import handlers
handlers.register_handlers(dp, state, bot) handlers.register_handlers(dp, state, bot)
def unregister(dp): def unregister(dp):
# Здесь можно удалить хендлеры, если нужно # Здесь можно удалить хендлеры, если нужно
dp.message_handlers.handlers.clear() dp.message_handlers.handlers.clear()
+56 -40
View File
@@ -5,6 +5,7 @@ import logging
import glob import glob
import json import json
import requests import requests
from typing import Optional
from mutagen.easyid3 import EasyID3 from mutagen.easyid3 import EasyID3
from mutagen.id3 import ID3, APIC, error from mutagen.id3 import ID3, APIC, error
@@ -16,13 +17,14 @@ async def get_video_info(url: str) -> dict:
"""Получает информацию о видео через yt-dlp""" """Получает информацию о видео через yt-dlp"""
try: try:
process = await asyncio.create_subprocess_exec( process = await asyncio.create_subprocess_exec(
'yt-dlp', "yt-dlp",
'--dump-json', "--dump-json",
'--no-playlist', "--no-playlist",
'--cookies', '~/myfirstprogramm/addons/example_addon/cookies.txt', "--cookies",
"~/myfirstprogramm/addons/example_addon/cookies.txt",
url, url,
stdout=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE stderr=asyncio.subprocess.PIPE,
) )
stdout, stderr = await process.communicate() stdout, stderr = await process.communicate()
@@ -33,17 +35,19 @@ async def get_video_info(url: str) -> dict:
return None 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 тип""" """Скачивает обложку видео и возвращает данные и MIME тип"""
try: try:
response = requests.get(thumbnail_url, timeout=10) response = requests.get(thumbnail_url, timeout=10)
if response.status_code == 200: if response.status_code == 200:
if 'jpeg' in thumbnail_url or 'jpg' in thumbnail_url: if "jpeg" in thumbnail_url or "jpg" in thumbnail_url:
mime_type = 'image/jpeg' mime_type = "image/jpeg"
elif 'png' in thumbnail_url: elif "png" in thumbnail_url:
mime_type = 'image/png' mime_type = "image/png"
else: else:
mime_type = response.headers.get('Content-Type', 'image/jpeg') mime_type = response.headers.get("Content-Type", "image/jpeg")
return response.content, mime_type return response.content, mime_type
except Exception as e: except Exception as e:
logger.warning(f"Не удалось скачать обложку: {e}") logger.warning(f"Не удалось скачать обложку: {e}")
@@ -59,19 +63,19 @@ def apply_metadata(mp3_path: str, metadata: dict):
audio = EasyID3() audio = EasyID3()
audio.save(mp3_path) audio.save(mp3_path)
audio['title'] = metadata.get('title', 'Unknown Title') audio["title"] = metadata.get("title", "Unknown Title")
audio['artist'] = metadata.get('performer', 'Unknown Artist') audio["artist"] = metadata.get("performer", "Unknown Artist")
audio.save(mp3_path) audio.save(mp3_path)
if metadata.get('thumbnail_data'): if metadata.get("thumbnail_data"):
audio = ID3(mp3_path) audio = ID3(mp3_path)
audio.add( audio.add(
APIC( APIC(
encoding=3, encoding=3,
mime=metadata.get('thumbnail_mime', 'image/jpeg'), mime=metadata.get("thumbnail_mime", "image/jpeg"),
type=3, # front cover type=3, # front cover
desc='Cover', desc="Cover",
data=metadata['thumbnail_data'] data=metadata["thumbnail_data"],
) )
) )
audio.save(mp3_path) audio.save(mp3_path)
@@ -96,27 +100,32 @@ async def download_mp3_isolated(url: str) -> tuple[str, dict]:
duration = 0 duration = 0
if video_info: if video_info:
title = video_info.get('title', 'Unknown Title') title = video_info.get("title", "Unknown Title")
uploader = video_info.get('uploader', 'Unknown Artist') uploader = video_info.get("uploader", "Unknown Artist")
thumbnail_url = video_info.get('thumbnail') thumbnail_url = video_info.get("thumbnail")
if not thumbnail_url and video_info.get('thumbnails'): if not thumbnail_url and video_info.get("thumbnails"):
thumbnails = video_info.get('thumbnails', []) thumbnails = video_info.get("thumbnails", [])
if thumbnails: if thumbnails:
thumbnail_url = thumbnails[-1].get('url') thumbnail_url = thumbnails[-1].get("url")
duration = video_info.get('duration', 0) duration = video_info.get("duration", 0)
logger.info(f"Получена информация о видео: {title}") logger.info(f"Получена информация о видео: {title}")
process = await asyncio.create_subprocess_exec( process = await asyncio.create_subprocess_exec(
'yt-dlp', "yt-dlp",
'-x', '--audio-format', 'mp3', "-x",
'--cookies', '~/myfirstprogramm/addons/example_addon/cookies.txt', "--audio-format",
'--audio-quality', '320K', "mp3",
'--no-playlist', "--cookies",
'-o', output_template, "~/myfirstprogramm/addons/example_addon/cookies.txt",
'--ignore-errors', "--audio-quality",
"320K",
"--no-playlist",
"-o",
output_template,
"--ignore-errors",
url, url,
stdout=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE stderr=asyncio.subprocess.PIPE,
) )
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=300) 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: if mp3_files:
actual_file = mp3_files[0] actual_file = mp3_files[0]
if os.path.getsize(actual_file) > 1000: 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 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()) dst.write(src.read())
thumbnail_data, mime_type = None, None thumbnail_data, mime_type = None, None
if thumbnail_url: if thumbnail_url:
thumbnail_data, mime_type = await download_thumbnail(thumbnail_url) thumbnail_data, mime_type = await download_thumbnail(
thumbnail_url
)
if thumbnail_data: if thumbnail_data:
logger.info(f"Обложка скачана: {thumbnail_url}") logger.info(f"Обложка скачана: {thumbnail_url}")
metadata = { metadata = {
'title': title, "title": title,
'performer': uploader, "performer": uploader,
'duration': duration, "duration": duration,
'thumbnail_data': thumbnail_data, "thumbnail_data": thumbnail_data,
'thumbnail_mime': mime_type "thumbnail_mime": mime_type,
} }
# Прописываем теги в MP3 # Прописываем теги в MP3
+22 -15
View File
@@ -3,11 +3,14 @@ from aiogram.filters import Command
from models.state import BotState from models.state import BotState
from utils.antispam import admin_required from utils.antispam import admin_required
from logging import getLogger 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 from os import path, unlink
logger = getLogger(__name__) logger = getLogger(__name__)
def register_handlers(dp: Dispatcher, state: BotState, bot: Bot): def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
@dp.message(Command("dowmp3")) @dp.message(Command("dowmp3"))
@admin_required(5) @admin_required(5)
@@ -18,7 +21,9 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
return return
url = args[1] 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 минуты") status_msg = await message.reply("⏳ Скачиваю аудио... Это займет 1-2 минуты")
@@ -29,30 +34,32 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
if file_size < 1000: if file_size < 1000:
raise Exception("Файл слишком маленький") raise Exception("Файл слишком маленький")
await status_msg.edit_text(f"✅ Аудио готово! Отправляю...") await status_msg.edit_text("✅ Аудио готово! Отправляю...")
# Подготавливаем аудио файл # Подготавливаем аудио файл
audio_input = types.FSInputFile(filename) audio_input = types.FSInputFile(filename)
# Базовые параметры # Базовые параметры
send_params = { send_params = {
'audio': audio_input, "audio": audio_input,
'title': metadata['title'][:64], "title": metadata["title"][:64],
'performer': metadata['performer'][:64], "performer": metadata["performer"][:64],
'duration': int(metadata['duration']) if metadata['duration'] else None, "duration": int(metadata["duration"]) if metadata["duration"] else None,
'caption': f"🎵 {metadata['title']}\n👤 {metadata['performer']}" "caption": f"🎵 {metadata['title']}\n👤 {metadata['performer']}",
} }
# Добавляем обложку если есть # Добавляем обложку если есть
if metadata['thumbnail_data']: if metadata["thumbnail_data"]:
try: 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_filename = thumb_file.name
thumb_file.write(metadata['thumbnail_data']) thumb_file.write(metadata["thumbnail_data"])
# Используем FSInputFile для обложки # Используем FSInputFile для обложки
send_params['thumbnail'] = types.FSInputFile(thumb_filename) send_params["thumbnail"] = types.FSInputFile(thumb_filename)
logger.info("Обложка добавлена к сообщению") logger.info("Обложка добавлена к сообщению")
except Exception as e: except Exception as e:
@@ -62,7 +69,7 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
await message.answer_audio(**send_params) 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) unlink(thumb_filename)
await status_msg.delete() 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)}") await status_msg.edit_text(f"❌ Ошибка: {str(e)}")
logger.error(f"Ошибка при скачивании: {e}") logger.error(f"Ошибка при скачивании: {e}")
finally: finally:
if 'filename' in locals() and path.exists(filename): if "filename" in locals() and path.exists(filename):
try: try:
unlink(filename) unlink(filename)
except: except OSError:
pass pass
+9
View File
@@ -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()
+105
View File
@@ -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)
+2
View File
@@ -1,7 +1,9 @@
def register(dp, state, bot): def register(dp, state, bot):
from . import handlers from . import handlers
handlers.register_handlers(dp, state, bot) handlers.register_handlers(dp, state, bot)
def unregister(dp): def unregister(dp):
# Здесь можно удалить хендлеры, если нужно # Здесь можно удалить хендлеры, если нужно
dp.message_handlers.handlers.clear() dp.message_handlers.handlers.clear()
+3 -3
View File
@@ -5,10 +5,11 @@ from models.state import BotState
from config import Config from config import Config
import logging import logging
from utils.antispam import admin_required from utils.antispam import admin_required
from storage.message_storage import save_message # импортируем функцию from storage.message_storage import save_message # импортируем функцию
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def register_handlers(dp: Dispatcher, state: BotState, bot: Bot): def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
@dp.message(Command("hello")) @dp.message(Command("hello"))
@admin_required(1) @admin_required(1)
@@ -20,8 +21,7 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
try: try:
name = Config.Names.get(admin_id, "Админ") name = Config.Names.get(admin_id, "Админ")
msg = await bot.send_message( msg = await bot.send_message(
chat_id=admin_id, chat_id=admin_id, text=f"🤖 Я готов к работе, господин {name}!"
text=f"🤖 Я готов к работе, господин {name}!"
) )
# сохраняем сообщение, отправленное админу # сохраняем сообщение, отправленное админу
save_message(msg.chat.id, msg.message_id) save_message(msg.chat.id, msg.message_id)
+2
View File
@@ -1,7 +1,9 @@
def register(dp, state, bot): def register(dp, state, bot):
from . import handlers from . import handlers
handlers.register_handlers(dp, state, bot) handlers.register_handlers(dp, state, bot)
def unregister(dp): def unregister(dp):
# Здесь можно удалить хендлеры, если нужно # Здесь можно удалить хендлеры, если нужно
dp.message_handlers.handlers.clear() dp.message_handlers.handlers.clear()
+1
View File
@@ -9,6 +9,7 @@ API_URL = "http://127.0.0.1:7700/speak"
logger = getLogger(__name__) logger = getLogger(__name__)
def register_handlers(dp: Dispatcher, state: BotState, bot: Bot): def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
@dp.message(Command("id")) @dp.message(Command("id"))
@saving @saving
+1
View File
@@ -2,6 +2,7 @@ import importlib
import sys import sys
from pathlib import Path from pathlib import Path
class AddonManager: class AddonManager:
def __init__(self, dp, state, bot): def __init__(self, dp, state, bot):
self.dp = dp self.dp = dp
+2
View File
@@ -1,7 +1,9 @@
def register(dp, state, bot): def register(dp, state, bot):
from . import handlers from . import handlers
handlers.register_handlers(dp, state, bot) handlers.register_handlers(dp, state, bot)
def unregister(dp): def unregister(dp):
# Здесь можно удалить хендлеры, если нужно # Здесь можно удалить хендлеры, если нужно
dp.message_handlers.handlers.clear() dp.message_handlers.handlers.clear()
+23 -30
View File
@@ -1,6 +1,4 @@
from config import Config from config import Config
import aiohttp
from aiogram.types import BufferedInputFile
from utils.antispam import admin_required from utils.antispam import admin_required
from aiogram import Dispatcher, Bot from aiogram import Dispatcher, Bot
from aiogram.types import Message from aiogram.types import Message
@@ -11,22 +9,7 @@ from aiogram.types import PollAnswer
from storage.message_storage import save_message from storage.message_storage import save_message
logger = getLogger(__name__) 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): def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
@dp.message(Command("poll")) @dp.message(Command("poll"))
@@ -37,9 +20,12 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
poll_msg = await bot.send_poll( poll_msg = await bot.send_poll(
chat_id=chat_id, chat_id=chat_id,
question="Кто опоздает?", question="Кто опоздает?",
options=["Я", "Не знаю", "Наверное"], options=["Я", "Я очень сильно опоздаю", "Наверное"],
is_anonymous=False, 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) 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: except Exception as e:
logger.error(f"Ошибка при отправке в чат {chat_id}: {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() @dp.poll_answer()
async def handle_poll_answer(poll_answer: PollAnswer): async def handle_poll_answer(poll_answer: PollAnswer):
user = poll_answer.user 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 username = f"@{user.username}" if user.username else user.first_name
# всегда пишем в первый чат из Config.CHAT_IDS # всегда пишем в первый чат из Config.CHAT_IDS
# 6394047531
if option_ids and option_ids[0] == 0: if option_ids and option_ids[0] == 0:
msg = await bot.send_message( msg = await bot.send_message(
chat_id=6394047531, chat_id=6394047531, text=f"{username} опоздает"
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) save_message(msg.chat.id, msg.message_id)
else: else:
msg = await bot.send_message( msg = await bot.send_message(
chat_id=6394047531, chat_id=6394047531, text=f"{username} выбрал вариант {option_ids}"
text=f"{username} выбрал вариант {option_ids}"
) )
save_message(msg.chat.id, msg.message_id) save_message(msg.chat.id, msg.message_id)
+2
View File
@@ -1,7 +1,9 @@
def register(dp, state, bot): def register(dp, state, bot):
from . import handlers from . import handlers
handlers.register_handlers(dp, state, bot) handlers.register_handlers(dp, state, bot)
def unregister(dp): def unregister(dp):
# Здесь можно удалить хендлеры, если нужно # Здесь можно удалить хендлеры, если нужно
dp.message_handlers.handlers.clear() dp.message_handlers.handlers.clear()
+57 -16
View File
@@ -1,16 +1,22 @@
from config import Config from config import Config
import aiohttp import ssl
import certifi
from aiogram.types import BufferedInputFile 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 import Dispatcher, Bot
from aiogram.types import Message from aiogram.types import Message
from models.state import BotState from models.state import BotState
from aiogram.filters import Command from aiogram.filters import Command
from logging import getLogger from logging import getLogger
logger = getLogger(__name__) logger = getLogger(__name__)
API_URL = "http://127.0.0.1:7700/speak" 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): def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
@dp.message(Command("vadmin")) @dp.message(Command("vadmin"))
@admin_required(0) @admin_required(0)
@@ -21,11 +27,26 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
return return
phrase = parts[1] 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 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: if resp.status != 200:
await message.reply("Ошибка генерации аудио") error_text = await resp.text()
await message.reply(
f"Ошибка генерации аудио: {resp.status} {error_text}"
)
return return
audio_bytes = await resp.read() audio_bytes = await resp.read()
audio_file = BufferedInputFile(audio_bytes, filename="speech.wav") audio_file = BufferedInputFile(audio_bytes, filename="speech.wav")
@@ -43,8 +64,12 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
@admin_required(0) @admin_required(0)
async def admin(message: Message): async def admin(message: Message):
raw_text = message.text or message.caption raw_text = message.text or message.caption
if not raw_text and not (message.photo or message.document or message.audio or message.video): if not raw_text and not (
await message.reply("❌ Укажи текст или прикрепи файл/медиа: /admin <сообщение>") message.photo or message.document or message.audio or message.video
):
await message.reply(
"❌ Укажи текст или прикрепи файл/медиа: /admin <сообщение>"
)
return return
# Отрезаем саму команду (/admin) # Отрезаем саму команду (/admin)
@@ -60,15 +85,21 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
elif message.document: 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: 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: 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: else:
# Только текст # Только текст
@@ -85,8 +116,12 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
@admin_required(0) @admin_required(0)
async def id_admin(message: Message): async def id_admin(message: Message):
raw_text = message.text or message.caption raw_text = message.text or message.caption
if not raw_text and not (message.photo or message.document or message.audio or message.video): if not raw_text and not (
await message.reply("❌ Укажи ID чата и текст или прикрепи файл/медиа: /iadmin <chat_id> <сообщение>") message.photo or message.document or message.audio or message.video
):
await message.reply(
"❌ Укажи ID чата и текст или прикрепи файл/медиа: /iadmin <chat_id> <сообщение>"
)
return return
# Отрезаем саму команду (/iadmin) # Отрезаем саму команду (/iadmin)
@@ -111,19 +146,25 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
elif message.document: 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: 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: 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: 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}") logger.info(f"Сообщение отправлено в чат {chat_id}")
await message.answer("✅ Сообщение отправлено.") await message.answer("✅ Сообщение отправлено.")
+6 -4
View File
@@ -13,20 +13,22 @@ class TelegramBot:
def setup_handlers(self): 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) admin.register_handlers(self.dp, self.state, self.bot)
schedule.register_handlers(self.dp, self.state) schedule.register_handlers(self.dp, self.state)
#media.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) # common.register_handlers(self.dp, self.state, self.bot)
#add addons # add addons
self.addons.load("example_addon") self.addons.load("example_addon")
self.addons.load("id") self.addons.load("id")
self.addons.load("send_message") self.addons.load("send_message")
self.addons.load("poll") self.addons.load("poll")
self.addons.load("hello") self.addons.load("hello")
self.addons.load("draw")
self.addons.load("gpt")
async def start(self): async def start(self):
"""Запуск бота""" """Запуск бота"""
+9 -8
View File
@@ -2,6 +2,7 @@ import os
from dotenv import load_dotenv from dotenv import load_dotenv
from typing import Dict from typing import Dict
class Config: class Config:
# Загружаем .env # Загружаем .env
load_dotenv() load_dotenv()
@@ -9,19 +10,19 @@ class Config:
# API # API
API_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN") API_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
Token = os.getenv("ACCESS_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: if not API_TOKEN:
raise ValueError("❌ TELEGRAM_BOT_TOKEN не найден в переменных окружения!") raise ValueError("❌ TELEGRAM_BOT_TOKEN не найден в переменных окружения!")
# Admins (user_id: уровень) # Admins (user_id: уровень)
ADMINS: Dict[int, int] = { ADMINS: Dict[int, int] = {850906163: 0, 6394047531: 4, 1345058877: 3}
850906163: 0,
6394047531: 4
}
Names: Dict[int, str] = { Names: Dict[int, str] = {850906163: "Ляпич", 6394047531: "Прокопович"}
850906163: "Ляпич",
6394047531: "Прокопович"
}
# Chats # Chats
CHAT_IDS = [-1003038389942] CHAT_IDS = [-1003038389942]
+15 -10
View File
@@ -12,9 +12,6 @@ from utils.analytics import create_statistics_text
logger = getLogger(__name__) logger = getLogger(__name__)
def register_handlers(dp: Dispatcher, state: BotState, bot: Bot): def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
@dp.message(Command("log")) @dp.message(Command("log"))
@saving @saving
@@ -32,6 +29,7 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
async def send_status(message: Message): async def send_status(message: Message):
from utils.analytics import analyze_bot_logs from utils.analytics import analyze_bot_logs
from utils.mac_metrics import get_macbook_battery_level, get_process_usage from utils.mac_metrics import get_macbook_battery_level, get_process_usage
try: try:
stats = analyze_bot_logs(Config.LOG_FILE) stats = analyze_bot_logs(Config.LOG_FILE)
batt = await get_macbook_battery_level() 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"✅ Uptime: {stats.get('uptime_percentage', 0)}%\n"
f"⏱️ Слежка расписания: {'ВКЛ' if state.watcher_work else 'ВЫКЛ'}\n" f"⏱️ Слежка расписания: {'ВКЛ' if state.watcher_work else 'ВЫКЛ'}\n"
f"🔋 Уровень заряда: {batt}%\n" f"🔋 Уровень заряда: {batt}%\n"
f"🖥️ Загрузка цп: {usage["cpu_percent"]}\n" f"🖥️ Загрузка цп: {usage['cpu_percent']}\n"
f"🧠 Загрузка оперативки: {usage["rss_mb"]:.2f} MB\n" f"🧠 Загрузка оперативки: {usage['rss_mb']:.2f} MB\n"
) )
await message.answer(status_text) await message.answer(status_text)
except Exception as e: except Exception as e:
@@ -54,16 +52,21 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
@admin_required(1) @admin_required(1)
async def stat(message: Message): async def stat(message: Message):
from utils.analytics import analyze_bot_logs from utils.analytics import analyze_bot_logs
stats = analyze_bot_logs(Config.LOG_FILE) 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")) @dp.message(Command("del"))
@admin_required(1) @admin_required(1)
async def delete_all_messages(message: Message): async def delete_all_messages(message: Message):
messages = load_messages() messages = load_messages()
if not messages: if not messages:
sent = await message.answer("📭 Нет сохранённых сообщений для удаления.", sent = await message.answer(
reply_to_message_id=message.message_id) "📭 Нет сохранённых сообщений для удаления.",
reply_to_message_id=message.message_id,
)
save_message(sent.chat.id, sent.message_id) save_message(sent.chat.id, sent.message_id)
return return
@@ -76,8 +79,10 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
logger.warning(f"Не удалось удалить {msg_id} в чате {chat_id}: {e}") logger.warning(f"Не удалось удалить {msg_id} в чате {chat_id}: {e}")
clear_messages() clear_messages()
sent = await message.answer(f"✅ Удалено {deleted} сообщений (включая /rasp).", sent = await message.answer(
reply_to_message_id=message.message_id) f"✅ Удалено {deleted} сообщений (включая /rasp).",
reply_to_message_id=message.message_id,
)
save_message(sent.chat.id, sent.message_id) save_message(sent.chat.id, sent.message_id)
@dp.message(Command("power")) @dp.message(Command("power"))
+5 -3
View File
@@ -22,7 +22,7 @@ def register_handlers(dp: Dispatcher, state: BotState):
schedule_service = ScheduleService() schedule_service = ScheduleService()
text, url, day, month = await schedule_service.get_schedule(group, day_offset) 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) save_message(message.chat.id, msg.message_id)
@@ -38,14 +38,16 @@ def register_handlers(dp: Dispatcher, state: BotState):
day_offset = int(args[2]) if len(args) > 2 and args[2].isdigit() else 0 day_offset = int(args[2]) if len(args) > 2 and args[2].isdigit() else 0
schedule_service = ScheduleService() 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: if clip_png:
save_message(message.chat.id, message.message_id) save_message(message.chat.id, message.message_id)
msg = await message.answer_photo( msg = await message.answer_photo(
types.BufferedInputFile(clip_png, filename=f"{group}.png"), 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) save_message(message.chat.id, msg.message_id)
+4 -5
View File
@@ -8,15 +8,13 @@ basicConfig(
level=INFO, level=INFO,
format="%(asctime)s [%(levelname)s] %(message)s", format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S", datefmt="%Y-%m-%d %H:%M:%S",
handlers=[ handlers=[FileHandler(Config.LOG_FILE, encoding="utf-8"), StreamHandler()],
FileHandler(Config.LOG_FILE, encoding="utf-8"), force=True,
StreamHandler()
],
force=True
) )
logger = getLogger(__name__) logger = getLogger(__name__)
async def main(): async def main():
"""Основная функция запуска""" """Основная функция запуска"""
try: try:
@@ -28,5 +26,6 @@ async def main():
finally: finally:
logger.info("Бот остановлен") logger.info("Бот остановлен")
if __name__ == "__main__": if __name__ == "__main__":
run(main()) run(main())
+2
View File
@@ -2,9 +2,11 @@ from dataclasses import dataclass
from typing import Dict, Optional from typing import Dict, Optional
from asyncio import Task from asyncio import Task
@dataclass @dataclass
class BotState: class BotState:
"""Состояние бота""" """Состояние бота"""
last_chat_time: Dict[int, str] = None last_chat_time: Dict[int, str] = None
last_pinned: Dict[str, int] = None last_pinned: Dict[str, int] = None
watcher_work: bool = False watcher_work: bool = False
+20 -12
View File
@@ -12,7 +12,9 @@ logger = logging.getLogger(__name__)
class ScheduleService: class ScheduleService:
def __init__(self): 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]: def _make_url(self, day: int = 0) -> Tuple[str, int, int]:
"""Генерация URL для расписания""" """Генерация URL для расписания"""
@@ -24,7 +26,11 @@ class ScheduleService:
d += timedelta(days=1) d += timedelta(days=1)
return self.base_url.format(day=d.day, mouth=d.month), d.day, d.month return self.base_url.format(day=d.day, mouth=d.month), d.day, d.month
else: 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( async def get_schedule(
self, group: str, day_offset: int = 0 self, group: str, day_offset: int = 0
@@ -39,11 +45,13 @@ class ScheduleService:
connector = aiohttp.TCPConnector(ssl=ssl_context) connector = aiohttp.TCPConnector(ssl=ssl_context)
headers = { 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 # тут можно использовать 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: async with session.get(url) as resp:
raw_bytes = await resp.read() raw_bytes = await resp.read()
@@ -71,24 +79,24 @@ class ScheduleService:
else: else:
result = f"📅 Расписание для {day} числа:\n```\n" result = f"📅 Расписание для {day} числа:\n```\n"
for line in schedule_lines: for line in schedule_lines:
formatted = ( formatted = line.replace("¦", "").replace(" ", " ").strip()
line.replace("¦", "")
.replace(" ", " ")
.strip()
)
if formatted: if formatted:
result += f"{formatted}\n" result += f"{formatted}\n"
result += "```" result += "```"
return result, url, day, month 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) url, day, month = self._make_url(day_offset)
async with async_playwright() as p: async with async_playwright() as p:
browser = await p.chromium.launch(headless=True) 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() page = await context.new_page()
try: try:
@@ -108,7 +116,7 @@ class ScheduleService:
x=float(max(box["x"] - 0, 0)), x=float(max(box["x"] - 0, 0)),
y=float(max(box["y"] - 0, 0)), y=float(max(box["y"] - 0, 0)),
width=float(box["width"] + 150), 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 return await page.screenshot(clip=clip_rect), url, day, month
+36 -19
View File
@@ -44,10 +44,19 @@ class WatcherService:
"""Основной цикл слежки""" """Основной цикл слежки"""
while self.state.watcher_work: while self.state.watcher_work:
try: try:
await self._check_all_groups() find = await self._check_all_groups()
delay = randint(Config.WATCHER_BASE_DELAY, Config.WATCHER_BASE_DELAY + 100) if find:
logger.info(f"Следущая проверка через {delay}") # ничего не нашли → ждём
await asyncio.sleep(delay) 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: except asyncio.CancelledError:
break break
except Exception as e: except Exception as e:
@@ -63,33 +72,41 @@ class WatcherService:
target += timedelta(days=1) target += timedelta(days=1)
return target return target
async def _check_all_groups(self) -> bool:
async def _check_all_groups(self): """
"""Проверка всех групп на изменения""" Возвращает True, если НИ в одной группе не найдено расписание.
Возвращает False, если хотя бы в одной группе найдено расписание.
"""
day = self._get_target_day() day = self._get_target_day()
found_any = False
for group, chat_id in Config.GROUP_CHATS.items(): for group, chat_id in Config.GROUP_CHATS.items():
logger.info(f"Проверяем расписание для {group} на {day.strftime('%d.%m.%Y')}") logger.info(
await self._check_group_schedule(group, chat_id, day.day) 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): return not found_any # <-- вот так правильно
"""Проверка расписания для конкретной группы"""
text, url, data_day, data_month = await self.schedule_service.get_schedule(group, day)
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(): if text and "не найдено" not in text.lower():
msg = await self.bot.send_message( msg = await self.bot.send_message(
chat_id, chat_id,
f"Авто-расписание для {group} на {data_day:02d}.{data_month:02d}\n\n{text}", 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) await self.bot.pin_chat_message(
else: chat_id, msg.message_id, disable_notification=False
logger.warning(
f"Не удалось получить расписание для {group}, {data_day}, {data_month}, {url}"
) )
return return True
return False
#clip_hash = hashlib.md5(clip_png).hexdigest() # clip_hash = hashlib.md5(clip_png).hexdigest()
# Логика проверки изменений и отправки сообщений # Логика проверки изменений и отправки сообщений
# ... (ваша существующая логика) # ... (ваша существующая логика)
-4
View File
@@ -1,5 +1,4 @@
import sqlite3 import sqlite3
import os
DIR = "/Users/mac/myfirstprogramm/storage/message.db" DIR = "/Users/mac/myfirstprogramm/storage/message.db"
if __name__ == "__main__": if __name__ == "__main__":
@@ -19,8 +18,5 @@ if __name__ == "__main__":
db.close() db.close()
def get_db(): def get_db():
return sqlite3.connect(DIR) return sqlite3.connect(DIR)
+3 -4
View File
@@ -1,5 +1,6 @@
from .DB import get_db from .DB import get_db
def save_message(chat_id: int, message_id: int): def save_message(chat_id: int, message_id: int):
db = get_db() db = get_db()
cur = db.cursor() cur = db.cursor()
@@ -8,6 +9,7 @@ def save_message(chat_id: int, message_id: int):
cur.close() cur.close()
db.close() db.close()
def load_messages(): def load_messages():
db = get_db() db = get_db()
cur = db.cursor() cur = db.cursor()
@@ -17,6 +19,7 @@ def load_messages():
db.close() db.close()
return rows return rows
def clear_messages(): def clear_messages():
db = get_db() db = get_db()
cur = db.cursor() cur = db.cursor()
@@ -24,7 +27,3 @@ def clear_messages():
db.commit() db.commit()
cur.close() cur.close()
db.close() db.close()
+93 -87
View File
@@ -6,13 +6,12 @@ import tempfile
import json import json
def analyze_bot_logs(log_file_path="bot.log"): def analyze_bot_logs(log_file_path="bot.log"):
""" """
Анализирует логи бота и создает детальную статистику Анализирует логи бота и создает детальную статистику
""" """
try: 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() lines = log.readlines()
except FileNotFoundError: except FileNotFoundError:
return {"error": "Лог файл не найден"} return {"error": "Лог файл не найден"}
@@ -24,27 +23,27 @@ def analyze_bot_logs(log_file_path="bot.log"):
# Основные счетчики # Основные счетчики
stats = { stats = {
'total_lines': len(lines), "total_lines": len(lines),
'time_period': {}, "time_period": {},
'log_levels': Counter(), "log_levels": Counter(),
'activities': Counter(), "activities": Counter(),
'errors': Counter(), "errors": Counter(),
'warnings': Counter(), "warnings": Counter(),
'user_commands': Counter(), "user_commands": Counter(),
'groups': Counter(), "groups": Counter(),
'restarts': 0, "restarts": 0,
'schedule_checks': 0, "schedule_checks": 0,
'schedule_changes': 0, "schedule_changes": 0,
'schedule_failures': 0, "schedule_failures": 0,
'network_errors': 0, "network_errors": 0,
'browser_errors': 0, "browser_errors": 0,
'telegram_errors': Counter(), "telegram_errors": Counter(),
'performance': { "performance": {
'avg_handling_time': 0, "avg_handling_time": 0,
'fastest_handling': float('inf'), "fastest_handling": float("inf"),
'slowest_handling': 0, "slowest_handling": 0,
'handling_count': 0 "handling_count": 0,
} },
} }
# Временные метрики # Временные метрики
@@ -53,11 +52,11 @@ def analyze_bot_logs(log_file_path="bot.log"):
handling_times = [] handling_times = []
# Регулярные выражения для парсинга # Регулярные выражения для парсинга
timestamp_pattern = r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})' timestamp_pattern = r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})"
log_level_pattern = r'\[(INFO|WARNING|ERROR)\]' log_level_pattern = r"\[(INFO|WARNING|ERROR)\]"
handling_time_pattern = r'Duration (\d+) ms' handling_time_pattern = r"Duration (\d+) ms"
command_pattern = r'Команда /rasp от ([\d-]+), группа=([^,]+), дата=(\d+)' command_pattern = r"Команда /rasp от ([\d-]+), группа=([^,]+), дата=(\d+)"
schedule_pattern = r'Проверяем расписание для ([^ ]+) на (\d{2}\.\d{2}\.\d{4})' schedule_pattern = r"Проверяем расписание для ([^ ]+) на (\d{2}\.\d{2}\.\d{4})"
for line in lines: 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) level_match = re.search(log_level_pattern, line)
if level_match: if level_match:
level = level_match.group(1) level = level_match.group(1)
stats['log_levels'][level] += 1 stats["log_levels"][level] += 1
# Время обработки сообщений # Время обработки сообщений
time_match = re.search(handling_time_pattern, line) 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_time = int(time_match.group(1))
handling_times.append(handling_time) handling_times.append(handling_time)
stats['performance']['handling_count'] += 1 stats["performance"]["handling_count"] += 1
stats['performance']['slowest_handling'] = max( stats["performance"]["slowest_handling"] = max(
stats['performance']['slowest_handling'], handling_time stats["performance"]["slowest_handling"], handling_time
) )
stats['performance']['fastest_handling'] = min( stats["performance"]["fastest_handling"] = min(
stats['performance']['fastest_handling'], handling_time stats["performance"]["fastest_handling"], handling_time
) )
# Команды пользователей # Команды пользователей
cmd_match = re.search(command_pattern, line) cmd_match = re.search(command_pattern, line)
if cmd_match: if cmd_match:
user_id, group, date_offset = cmd_match.groups() user_id, group, date_offset = cmd_match.groups()
stats['user_commands'][group] += 1 stats["user_commands"][group] += 1
stats['groups'][group] += 1 stats["groups"][group] += 1
# Проверки расписания # Проверки расписания
if 'Проверяем расписание' in line: if "Проверяем расписание" in line:
stats['schedule_checks'] += 1 stats["schedule_checks"] += 1
sched_match = re.search(schedule_pattern, line) sched_match = re.search(schedule_pattern, line)
if sched_match: if sched_match:
group, date = sched_match.groups() group, date = sched_match.groups()
stats['groups'][group] += 1 stats["groups"][group] += 1
# Изменения расписания # Изменения расписания
if 'Изменения найдены' in line: if "Изменения найдены" in line:
stats['schedule_changes'] += 1 stats["schedule_changes"] += 1
# Ошибки расписания # Ошибки расписания
if 'Не удалось получить расписание' in line: if "Не удалось получить расписание" in line:
stats['schedule_failures'] += 1 stats["schedule_failures"] += 1
# Перезапуски бота # Перезапуски бота
if 'Бот запускается' in line: if "Бот запускается" in line:
stats['restarts'] += 1 stats["restarts"] += 1
# Сетевые ошибки # Сетевые ошибки
if 'Failed to fetch updates' in line: if "Failed to fetch updates" in line:
stats['network_errors'] += 1 stats["network_errors"] += 1
# Ошибки браузера # Ошибки браузера
if 'TargetClosedError' in line or 'BrowserContext.close' in line: if "TargetClosedError" in line or "BrowserContext.close" in line:
stats['browser_errors'] += 1 stats["browser_errors"] += 1
# Ошибки Telegram API # Ошибки Telegram API
if 'Telegram server says' in line: if "Telegram server says" in line:
error_msg = line.split('Telegram server says - ')[-1].split(':')[0] error_msg = line.split("Telegram server says - ")[-1].split(":")[0]
stats['telegram_errors'][error_msg] += 1 stats["telegram_errors"][error_msg] += 1
# Сбор ошибок и предупреждений # Сбор ошибок и предупреждений
if '[ERROR]' in line: if "[ERROR]" in line:
error_msg = line.split('[ERROR]')[-1].strip() error_msg = line.split("[ERROR]")[-1].strip()
stats['errors'][error_msg[:100]] += 1 stats["errors"][error_msg[:100]] += 1
if '[WARNING]' in line: if "[WARNING]" in line:
warning_msg = line.split('[WARNING]')[-1].strip() warning_msg = line.split("[WARNING]")[-1].strip()
stats['warnings'][warning_msg[:100]] += 1 stats["warnings"][warning_msg[:100]] += 1
# Расчет средней скорости обработки # Расчет средней скорости обработки
if handling_times: 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: if start_time and end_time:
stats['time_period'] = { stats["time_period"] = {
'start': start_time, "start": start_time,
'end': end_time, "end": end_time,
'duration_hours': calculate_duration_hours(start_time, end_time) "duration_hours": calculate_duration_hours(start_time, end_time),
} }
# Дополнительные метрики # Дополнительные метрики
stats['success_rate'] = calculate_success_rate(stats) stats["success_rate"] = calculate_success_rate(stats)
stats['uptime_percentage'] = calculate_uptime_percentage(stats) stats["uptime_percentage"] = calculate_uptime_percentage(stats)
stats['schedule_success_rate'] = calculate_schedule_success_rate(stats) stats["schedule_success_rate"] = calculate_schedule_success_rate(stats)
return stats return stats
@@ -159,7 +160,7 @@ def analyze_bot_logs(log_file_path="bot.log"):
def calculate_duration_hours(start_str, end_str): def calculate_duration_hours(start_str, end_str):
"""Вычисляет продолжительность в часах""" """Вычисляет продолжительность в часах"""
try: try:
fmt = '%Y-%m-%d %H:%M:%S' fmt = "%Y-%m-%d %H:%M:%S"
start = datetime.strptime(start_str, fmt) start = datetime.strptime(start_str, fmt)
end = datetime.strptime(end_str, fmt) end = datetime.strptime(end_str, fmt)
return round((end - start).total_seconds() / 3600, 2) 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): 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: if total_operations == 0:
return 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) return round(success_rate, 2)
def calculate_uptime_percentage(stats): 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 return 0
# Предполагаем, что каждый перезапуск занимает ~10 секунд # Предполагаем, что каждый перезапуск занимает ~10 секунд
restart_downtime = stats['restarts'] * 10 / 3600 restart_downtime = stats["restarts"] * 10 / 3600
total_hours = stats['time_period']['duration_hours'] total_hours = stats["time_period"]["duration_hours"]
uptime_hours = total_hours - restart_downtime uptime_hours = total_hours - restart_downtime
uptime_percentage = (uptime_hours / total_hours) * 100 uptime_percentage = (uptime_hours / total_hours) * 100
return round(uptime_percentage, 2) return round(uptime_percentage, 2)
@@ -190,17 +193,17 @@ def calculate_uptime_percentage(stats):
def calculate_schedule_success_rate(stats): def calculate_schedule_success_rate(stats):
"""Рассчитывает процент успешных проверок расписания""" """Рассчитывает процент успешных проверок расписания"""
total_checks = stats['schedule_checks'] total_checks = stats["schedule_checks"]
if total_checks == 0: if total_checks == 0:
return 0 return 0
successful_checks = total_checks - stats['schedule_failures'] successful_checks = total_checks - stats["schedule_failures"]
success_rate = (successful_checks / total_checks) * 100 success_rate = (successful_checks / total_checks) * 100
return round(success_rate, 2) return round(success_rate, 2)
def create_statistics_text(stats): def create_statistics_text(stats):
"""Создает текстовый отчет статистики с расширенными метриками""" """Создает текстовый отчет статистики с расширенными метриками"""
if 'error' in stats: if "error" in stats:
return f"❌ Ошибка анализа логов: {stats['error']}" return f"❌ Ошибка анализа логов: {stats['error']}"
text = "📊 СТАТИСТИКА РАБОТЫ БОТА\n" text = "📊 СТАТИСТИКА РАБОТЫ БОТА\n"
@@ -213,7 +216,9 @@ def create_statistics_text(stats):
text += f"• Строк в логе: {stats['total_lines']:,}\n\n" 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 median_time = statistics.median(handling_times) if handling_times else 0
text += "⚡ ПРОИЗВОДИТЕЛЬНОСТЬ:\n" text += "⚡ ПРОИЗВОДИТЕЛЬНОСТЬ:\n"
@@ -223,11 +228,11 @@ def create_statistics_text(stats):
text += f"• Успешных операций: {stats['success_rate']}%\n\n" text += f"• Успешных операций: {stats['success_rate']}%\n\n"
# Статус работы # Статус работы
duration = stats['time_period'].get('duration_hours', 0) duration = stats["time_period"].get("duration_hours", 0)
errors_total = sum(stats['errors'].values()) errors_total = sum(stats["errors"].values())
errors_per_hour = round(errors_total / duration, 2) if duration else 0 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 mtbf = round(duration / restarts, 2) if restarts else duration
text += "🔄 СТАТУС РАБОТЫ:\n" text += "🔄 СТАТУС РАБОТЫ:\n"
@@ -251,9 +256,9 @@ def create_statistics_text(stats):
text += f"• Браузера: {stats['browser_errors']}\n" text += f"• Браузера: {stats['browser_errors']}\n"
# Топ-3 ошибок # Топ-3 ошибок
if stats['errors']: if stats["errors"]:
text += "• Топ ошибок:\n" 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 += f" - {err} ({count})\n"
text += "\n" text += "\n"
@@ -262,9 +267,9 @@ def create_statistics_text(stats):
text += f"• Команд: {sum(stats['user_commands'].values())}\n" text += f"• Команд: {sum(stats['user_commands'].values())}\n"
text += f"• Групп: {len(stats['groups'])}\n" text += f"• Групп: {len(stats['groups'])}\n"
if stats['groups']: if stats["groups"]:
text += "• Топ групп:\n" 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" text += f" - {group}: {count}\n"
return text return text
@@ -272,12 +277,13 @@ def create_statistics_text(stats):
def create_statistics_file(stats): def create_statistics_file(stats):
"""Создает временный файл с полной статистикой""" """Создает временный файл с полной статистикой"""
if 'error' in stats: if "error" in stats:
return None return None
# Создаем временный файл # Создаем временный файл
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', with tempfile.NamedTemporaryFile(
suffix='.json', delete=False) as f: mode="w", encoding="utf-8", suffix=".json", delete=False
) as f:
json.dump(stats, f, ensure_ascii=False, indent=2, default=str) json.dump(stats, f, ensure_ascii=False, indent=2, default=str)
temp_filename = f.name temp_filename = f.name
+4 -3
View File
@@ -21,9 +21,6 @@ def is_chat_spam(chat_id: int, state: BotState) -> bool:
return False return False
from functools import wraps
from aiogram import types
def admin_required(need_level: int): def admin_required(need_level: int):
"""Декоратор для проверки прав администратора (0 = высший уровень)""" """Декоратор для проверки прав администратора (0 = высший уровень)"""
@@ -41,12 +38,16 @@ def admin_required(need_level: int):
return await func(message, *args, **kwargs) return await func(message, *args, **kwargs)
return wrapper return wrapper
return decorator return decorator
def saving(func): def saving(func):
"""Декоратор для сохранения входящего сообщения""" """Декоратор для сохранения входящего сообщения"""
@wraps(func) @wraps(func)
async def wrapper(message: types.Message, *args, **kwargs): async def wrapper(message: types.Message, *args, **kwargs):
save_message(message.chat.id, message.message_id) save_message(message.chat.id, message.message_id)
return await func(message, *args, **kwargs) return await func(message, *args, **kwargs)
return wrapper return wrapper
+15 -9
View File
@@ -1,11 +1,14 @@
import asyncio import asyncio
import os import os
async def get_macbook_battery_level(): async def get_macbook_battery_level():
process = await asyncio.create_subprocess_exec( process = await asyncio.create_subprocess_exec(
"pmset", "-g", "batt", "pmset",
"-g",
"batt",
stdout=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE stderr=asyncio.subprocess.PIPE,
) )
stdout, stderr = await process.communicate() stdout, stderr = await process.communicate()
if process.returncode != 0: if process.returncode != 0:
@@ -28,9 +31,13 @@ async def get_process_usage(pid=None):
pid = os.getpid() pid = os.getpid()
process = await asyncio.create_subprocess_exec( 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, stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE stderr=asyncio.subprocess.PIPE,
) )
stdout, stderr = await process.communicate() stdout, stderr = await process.communicate()
if process.returncode != 0: if process.returncode != 0:
@@ -47,19 +54,18 @@ async def get_process_usage(pid=None):
"command": comm, "command": comm,
"cpu_percent": float(cpu), "cpu_percent": float(cpu),
"mem_percent": float(mem_percent), "mem_percent": float(mem_percent),
"rss_mb": int(rss_kb) / 1024 # переводим КБ → МБ "rss_mb": int(rss_kb) / 1024, # переводим КБ → МБ
} }
async def main(): async def main():
battery = await get_macbook_battery_level() battery = await get_macbook_battery_level()
usage = await get_process_usage() usage = await get_process_usage()
print(f"🔋 Батарея: {battery}%") 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__": if __name__ == "__main__":