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