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):
from . import handlers
handlers.register_handlers(dp, state, bot)
def unregister(dp):
# Здесь можно удалить хендлеры, если нужно
dp.message_handlers.handlers.clear()
+57 -37
View File
@@ -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
+7 -6
View File
@@ -2,12 +2,10 @@ 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__)
@@ -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(
"❌ Произошла ошибка при обработке видео. Попробуйте позже."
)
+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):
from . import handlers
handlers.register_handlers(dp, state, bot)
def unregister(dp):
# Здесь можно удалить хендлеры, если нужно
dp.message_handlers.handlers.clear()
+56 -40
View File
@@ -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
+22 -15
View File
@@ -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:
except OSError:
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):
from . import handlers
handlers.register_handlers(dp, state, bot)
def unregister(dp):
# Здесь можно удалить хендлеры, если нужно
dp.message_handlers.handlers.clear()
+2 -2
View File
@@ -9,6 +9,7 @@ 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)
+2
View File
@@ -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()
+1
View File
@@ -9,6 +9,7 @@ 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
+1
View File
@@ -2,6 +2,7 @@ import importlib
import sys
from pathlib import Path
class AddonManager:
def __init__(self, dp, state, bot):
self.dp = dp
+2
View File
@@ -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()
+23 -30
View File
@@ -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)
+2
View File
@@ -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()
+57 -16
View File
@@ -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 <chat_id> <сообщение>")
if not raw_text and not (
message.photo or message.document or message.audio or message.video
):
await message.reply(
"❌ Укажи ID чата и текст или прикрепи файл/медиа: /iadmin <chat_id> <сообщение>"
)
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("✅ Сообщение отправлено.")
+2
View File
@@ -27,6 +27,8 @@ class TelegramBot:
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):
"""Запуск бота"""
+9 -8
View File
@@ -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]
+15 -10
View File
@@ -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"))
+5 -3
View File
@@ -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,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
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)
+4 -5
View File
@@ -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())
+2
View File
@@ -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
+20 -12
View File
@@ -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
+34 -17
View File
@@ -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}")
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,31 +72,39 @@ 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()
-4
View File
@@ -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)
+3 -4
View File
@@ -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()
+93 -87
View File
@@ -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,12 +277,13 @@ 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
+4 -3
View File
@@ -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
+15 -9
View File
@@ -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__":