It's version 0.4
This commit is contained in:
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
"❌ Произошла ошибка при обработке видео. Попробуйте позже."
|
||||||
|
)
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
import logging
|
||||||
|
import base64
|
||||||
|
from io import BytesIO
|
||||||
|
import asyncio
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
from aiogram import Dispatcher, Bot
|
||||||
|
from aiogram.types import Message, FSInputFile, BufferedInputFile
|
||||||
|
from aiogram.filters import Command
|
||||||
|
|
||||||
|
from models.state import BotState
|
||||||
|
from config import Config
|
||||||
|
|
||||||
|
from storage.message_storage import save_message
|
||||||
|
|
||||||
|
from transformers import pipeline
|
||||||
|
|
||||||
|
from utils.antispam import saving
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SD_URL = "http://192.168.31.95:7860/sdapi/v1/txt2img"
|
||||||
|
|
||||||
|
# Загружаем пайплайн перевода один раз при старте (синхронный)
|
||||||
|
translator = pipeline("translation", model="Helsinki-NLP/opus-mt-ru-en")
|
||||||
|
|
||||||
|
|
||||||
|
async def translate_to_en(text: str) -> str:
|
||||||
|
try:
|
||||||
|
# выполняем перевод в отдельном потоке, чтобы не блокировать event loop
|
||||||
|
result = await asyncio.to_thread(translator, text, max_length=512)
|
||||||
|
return result[0]["translation_text"]
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка перевода: {e}")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
async def generate_img2img(prompt: str, init_image: BytesIO) -> BytesIO | None:
|
||||||
|
"""
|
||||||
|
Генерация изображения по методу img2img.
|
||||||
|
:param prompt: текстовый промт (уже переведённый на английский)
|
||||||
|
:param init_image: входное изображение в BytesIO
|
||||||
|
:return: BytesIO с результатом или None при ошибке
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# кодируем входное изображение в base64
|
||||||
|
init_image_base64 = base64.b64encode(init_image.getvalue()).decode("utf-8")
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"init_images": [init_image_base64],
|
||||||
|
"prompt": prompt,
|
||||||
|
"negative_prompt": "blurry, low quality, bad anatomy, watermark, text, cropped",
|
||||||
|
"steps": 20, # можно 15–20
|
||||||
|
"width": 1024, # лучше подставлять размеры исходного фото
|
||||||
|
"height": 1024,
|
||||||
|
"sampler_name": "Euler a", # мягкий и стабильный для img2img
|
||||||
|
"Schedule_type": "Karras",
|
||||||
|
"cfg_scale": 6, # чуть ниже, чем для txt2img
|
||||||
|
"seed": -1,
|
||||||
|
"denoising_strength": 0.8, # 0.3–0.5 для «сохранить стиль», 0.6–0.8 для «перерисовать»
|
||||||
|
"restore_faces": True, # если работаешь с людьми
|
||||||
|
"override_settings": {
|
||||||
|
"sd_model_checkpoint": "waiNSFWIllustrious_v150.safetensors"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.post(
|
||||||
|
SD_URL.replace("txt2img", "img2img"), json=payload
|
||||||
|
) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
logger.error(f"Stable Diffusion img2img API error: {resp.status}")
|
||||||
|
return None
|
||||||
|
r = await resp.json()
|
||||||
|
image_base64 = r["images"][0]
|
||||||
|
return BytesIO(base64.b64decode(image_base64))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка img2img: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# sd_xl_base_1.0.safetensors
|
||||||
|
# waiNSFWIllustrious_v150.safetensors
|
||||||
|
async def generate_image(prompt: str) -> BytesIO | None:
|
||||||
|
payload = {
|
||||||
|
"prompt": prompt,
|
||||||
|
"negative_prompt": "blurry, low quality, bad anatomy, watermark, text, cropped",
|
||||||
|
"steps": 20,
|
||||||
|
"width": 1024,
|
||||||
|
"height": 1024,
|
||||||
|
"sampler_name": "Euler a", # сэмплер
|
||||||
|
"cfg_scale": 7, # насколько строго следовать промту
|
||||||
|
"seed": -1, # -1 = случайный сид
|
||||||
|
"batch_size": 1, # сколько картинок за раз
|
||||||
|
"n_iter": 1, # сколько раз повторить генерацию
|
||||||
|
"restore_faces": False, # восстановление лиц
|
||||||
|
"tiling": False, # тайлинг для текстур
|
||||||
|
"enable_hr": False, # highres fix (двухэтапная генерация)
|
||||||
|
"denoising_strength": 0.7, # сила денойзинга (актуально при enable_hr или img2img)
|
||||||
|
"hr_scale": 2, # во сколько раз увеличить при highres fix
|
||||||
|
"hr_upscaler": "Latent", # апскейлер для highres fix
|
||||||
|
"override_settings": {
|
||||||
|
"sd_model_checkpoint": "waiNSFWIllustrious_v150.safetensors" # выбор модели
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.post(SD_URL, json=payload) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
logger.error(f"Stable Diffusion API error: {resp.status}")
|
||||||
|
return None
|
||||||
|
r = await resp.json()
|
||||||
|
image_base64 = r["images"][0]
|
||||||
|
return BytesIO(base64.b64decode(image_base64))
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка генерации изображения: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
|
||||||
|
@dp.message(Command("draw"))
|
||||||
|
async def draw(message: Message):
|
||||||
|
save_message(message.chat.id, message.message_id)
|
||||||
|
if message.from_user.id in Config.BAN:
|
||||||
|
msg = await message.reply("Вы в бане")
|
||||||
|
save_message(msg.chat.id, msg.message_id)
|
||||||
|
else:
|
||||||
|
user_prompt = message.text.replace("/draw", "").strip()
|
||||||
|
if not user_prompt:
|
||||||
|
confirm_msg = await message.answer("❗ Укажи промт после команды /draw")
|
||||||
|
save_message(confirm_msg.chat.id, confirm_msg.message_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
en_prompt = await translate_to_en(user_prompt)
|
||||||
|
logger.info(f"Промт переведен: {user_prompt} -> {en_prompt}")
|
||||||
|
|
||||||
|
img_bytes = await generate_image(en_prompt)
|
||||||
|
if img_bytes:
|
||||||
|
img_bytes.seek(0)
|
||||||
|
photo = BufferedInputFile(img_bytes.read(), filename="result.png")
|
||||||
|
msg = await bot.send_photo(chat_id=message.chat.id, photo=photo)
|
||||||
|
save_message(msg.chat.id, msg.message_id)
|
||||||
|
else:
|
||||||
|
error_msg = await message.answer("⚠️ Ошибка при генерации изображения.")
|
||||||
|
save_message(error_msg.chat.id, error_msg.message_id)
|
||||||
|
|
||||||
|
@dp.message(Command("img2img"))
|
||||||
|
@saving
|
||||||
|
async def img2img_with_caption(message: Message, bot: Bot):
|
||||||
|
raw_caption = message.caption or ""
|
||||||
|
user_prompt = raw_caption.replace("/img2img", "").strip()
|
||||||
|
if not user_prompt:
|
||||||
|
await message.answer(
|
||||||
|
"❗ Укажи промт в подписи к фото после команды /img2img"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
en_prompt = await translate_to_en(user_prompt)
|
||||||
|
logger.info(f"Промт для img2img переведен: {user_prompt} -> {en_prompt}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if message.photo:
|
||||||
|
# Берём последнее (самое большое) фото
|
||||||
|
photo = message.photo[-1].file_id
|
||||||
|
|
||||||
|
# Отправляем в SD API по file_id (как в iadmin)
|
||||||
|
# Здесь отличие: SD API требует base64, поэтому file_id нужно скачать
|
||||||
|
# Но логика построена как в iadmin — сначала берём file_id
|
||||||
|
file = await bot.get_file(photo)
|
||||||
|
file_bytes = await bot.download_file(file.file_path)
|
||||||
|
|
||||||
|
img_bytes = await generate_img2img(
|
||||||
|
en_prompt, BytesIO(file_bytes.read())
|
||||||
|
)
|
||||||
|
if img_bytes:
|
||||||
|
img_bytes.seek(0)
|
||||||
|
photo = BufferedInputFile(
|
||||||
|
img_bytes.read(), filename="img2img_result.png"
|
||||||
|
)
|
||||||
|
msg = await bot.send_photo(chat_id=message.chat.id, photo=photo)
|
||||||
|
else:
|
||||||
|
msg = await message.answer("⚠️ Ошибка при img2img генерации.")
|
||||||
|
|
||||||
|
else:
|
||||||
|
msg = await message.answer("❗ Пришли фото с подписью /img2img <промт>")
|
||||||
|
save_message(msg.chat.id, msg.message_id)
|
||||||
|
except Exception as e:
|
||||||
|
await message.answer(f"⚠️ Ошибка: {e}")
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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)
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ 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
|
||||||
async def id(message: Message):
|
async def id(message: Message):
|
||||||
id = message.from_user.id
|
id = message.from_user.id
|
||||||
msg = await message.reply(str(id))
|
msg = await message.reply(str(id))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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,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("✅ Сообщение отправлено.")
|
||||||
|
|||||||
+7
-5
@@ -13,22 +13,24 @@ 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):
|
||||||
"""Запуск бота"""
|
"""Запуск бота"""
|
||||||
self.setup_handlers()
|
self.setup_handlers()
|
||||||
await self.dp.start_polling(self.bot)
|
await self.dp.start_polling(self.bot)
|
||||||
|
|||||||
@@ -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
@@ -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"))
|
||||||
|
|||||||
@@ -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,17 +38,19 @@ def register_handlers(dp: Dispatcher, state: BotState):
|
|||||||
day_offset = int(args[2]) if len(args) > 2 and args[2].isdigit() else 0
|
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)
|
||||||
|
|
||||||
state.file_id_cache[group.lower()] = msg.photo[-1].file_id
|
state.file_id_cache[group.lower()] = msg.photo[-1].file_id
|
||||||
else:
|
else:
|
||||||
await message.answer(f"Не удалось найти расписание для {group}")
|
await message.answer(f"Не удалось найти расписание для {group}")
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
+3
-1
@@ -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
|
||||||
@@ -23,4 +25,4 @@ class BotState:
|
|||||||
if self.last_day is None:
|
if self.last_day is None:
|
||||||
self.last_day = {}
|
self.last_day = {}
|
||||||
if self.last_clip_hash is None:
|
if self.last_clip_hash is None:
|
||||||
self.last_clip_hash = {}
|
self.last_clip_hash = {}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -118,4 +126,4 @@ class ScheduleService:
|
|||||||
await context.close()
|
await context.close()
|
||||||
await browser.close()
|
await browser.close()
|
||||||
|
|
||||||
return None, url, day, month
|
return None, url, day, month
|
||||||
|
|||||||
+37
-20
@@ -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()
|
||||||
|
|
||||||
# Логика проверки изменений и отправки сообщений
|
# Логика проверки изменений и отправки сообщений
|
||||||
# ... (ваша существующая логика)
|
# ... (ваша существующая логика)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+94
-88
@@ -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,13 +277,14 @@ 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
|
||||||
|
|
||||||
return temp_filename
|
return temp_filename
|
||||||
|
|||||||
+5
-4
@@ -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
@@ -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__":
|
||||||
|
|||||||
Reference in New Issue
Block a user