Compare commits
No commits in common. "5643d8fb4a503aabc6d754d45c968c0e75d895cd" and "c68c9e0774a951ce00f583b19239bfbfa5f9a9f9" have entirely different histories.
5643d8fb4a
...
c68c9e0774
8 changed files with 0 additions and 2896 deletions
|
|
@ -1,21 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
Скрипт для создания новых таблиц в БД (включая weekly_digest_inbox)
|
|
||||||
"""
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
||||||
sys.path.insert(0, str(BASE_DIR))
|
|
||||||
|
|
||||||
from database.db import init_db
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
print("🔄 Запуск инициализации новых таблиц в базе данных...")
|
|
||||||
await init_db()
|
|
||||||
print("✅ Инициализация успешно завершена!")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(main())
|
|
||||||
|
|
@ -829,20 +829,3 @@ class Speedtest(Base):
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<Speedtest #{self.id} DL={self.download} UP={self.upload} Server={self.server_name}>"
|
return f"<Speedtest #{self.id} DL={self.download} UP={self.upload} Server={self.server_name}>"
|
||||||
|
|
||||||
|
|
||||||
class WeeklyDigestInbox(Base):
|
|
||||||
"""Личный инбокс для еженедельного дайджеста"""
|
|
||||||
__tablename__ = 'weekly_digest_inbox'
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
||||||
user_id = Column(BigInteger, nullable=False)
|
|
||||||
message_text = Column(Text, nullable=True)
|
|
||||||
url = Column(Text, nullable=True)
|
|
||||||
media_type = Column(String(50), nullable=True)
|
|
||||||
created_at = Column(DateTime, default=datetime.utcnow)
|
|
||||||
status = Column(String(50), default='pending') # 'pending', 'sent'
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<WeeklyDigestInbox #{self.id} user={self.user_id} status={self.status}>"
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -23,5 +23,3 @@ from .achievements import router as achievements_router
|
||||||
from .thanks import router as thanks_router
|
from .thanks import router as thanks_router
|
||||||
from .events import router as events_router
|
from .events import router as events_router
|
||||||
from .initiative_group import router as initiative_group_router
|
from .initiative_group import router as initiative_group_router
|
||||||
from .personal_inbox import router as personal_inbox_router
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,126 +0,0 @@
|
||||||
import logging
|
|
||||||
import re
|
|
||||||
from aiogram import Router, F
|
|
||||||
from aiogram.types import Message
|
|
||||||
from aiogram.filters import Command
|
|
||||||
from services.personal_inbox_service import PersonalInboxService
|
|
||||||
import config
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
router = Router()
|
|
||||||
|
|
||||||
# Регулярка для поиска URL
|
|
||||||
URL_REGEX = re.compile(r'(https?://[^\s]+)')
|
|
||||||
|
|
||||||
@router.message(F.chat.type == 'private', Command('digest'))
|
|
||||||
@router.message(F.chat.type == 'private', Command('digest_send'))
|
|
||||||
async def cmd_send_digest(message: Message):
|
|
||||||
"""Ручной запуск отправки дайджеста"""
|
|
||||||
if message.from_user.id != config.ADMIN_USER_ID:
|
|
||||||
return
|
|
||||||
|
|
||||||
await message.answer("🔄 <b>Собираю еженедельный дайджест...</b>", parse_mode='HTML')
|
|
||||||
success = await PersonalInboxService.send_weekly_digest(message.bot)
|
|
||||||
if not success:
|
|
||||||
await message.answer("ℹ️ Нет активных (неотправленных) записей в вашем инбоксе.")
|
|
||||||
|
|
||||||
@router.message(F.chat.type == 'private', Command('digest_list'))
|
|
||||||
async def cmd_list_digest(message: Message):
|
|
||||||
"""Показать список текущих записей в инбоксе"""
|
|
||||||
if message.from_user.id != config.ADMIN_USER_ID:
|
|
||||||
return
|
|
||||||
|
|
||||||
items = await PersonalInboxService.get_pending_items(config.ADMIN_USER_ID)
|
|
||||||
if not items:
|
|
||||||
await message.answer("📥 Ваш инбокс дайджеста пуст.")
|
|
||||||
return
|
|
||||||
|
|
||||||
lines = [f"📥 <b>Текущие записи в инбоксе ({len(items)}):</b>\n"]
|
|
||||||
for idx, item in enumerate(items, 1):
|
|
||||||
date_str = item.created_at.strftime('%d.%m %H:%M')
|
|
||||||
line = f"<b>{idx}.</b> [{date_str}] "
|
|
||||||
if item.url:
|
|
||||||
line += f'<a href="{item.url}">Ссылка</a>'
|
|
||||||
else:
|
|
||||||
line += "Текст"
|
|
||||||
if item.message_text:
|
|
||||||
# Обрезаем длинный текст
|
|
||||||
short_text = item.message_text[:60] + "..." if len(item.message_text) > 60 else item.message_text
|
|
||||||
line += f": <i>{short_text}</i>"
|
|
||||||
lines.append(line)
|
|
||||||
|
|
||||||
lines.append("\n✍️ <i>Для ручной отправки дайджеста введите /digest. Для очистки - /digest_clear.</i>")
|
|
||||||
await message.answer("\n".join(lines), parse_mode='HTML', disable_web_page_preview=True)
|
|
||||||
|
|
||||||
@router.message(F.chat.type == 'private', Command('digest_clear'))
|
|
||||||
async def cmd_clear_digest(message: Message):
|
|
||||||
"""Очистить инбокс"""
|
|
||||||
if message.from_user.id != config.ADMIN_USER_ID:
|
|
||||||
return
|
|
||||||
|
|
||||||
success = await PersonalInboxService.clear_pending_items(config.ADMIN_USER_ID)
|
|
||||||
if success:
|
|
||||||
await message.answer("🧹 Ваш инбокс успешно очищен (все записи помечены как отправленные).")
|
|
||||||
else:
|
|
||||||
await message.answer("❌ Произошла ошибка при очистке инбокса.")
|
|
||||||
|
|
||||||
@router.message(F.chat.type == 'private')
|
|
||||||
async def handle_private_inbox(message: Message):
|
|
||||||
"""Обработчик всех входящих личных сообщений от админа для сохранения в инбокс"""
|
|
||||||
if message.from_user.id != config.ADMIN_USER_ID:
|
|
||||||
# Для других пользователей пропускаем или выдаем стандартный ответ
|
|
||||||
return
|
|
||||||
|
|
||||||
# Игнорируем команды, которые могли проскочить
|
|
||||||
if message.text and message.text.startswith('/'):
|
|
||||||
return
|
|
||||||
|
|
||||||
# Собираем текст
|
|
||||||
text = message.text or message.caption or ""
|
|
||||||
|
|
||||||
# Пытаемся вытащить URL из сущностей aiogram
|
|
||||||
urls = []
|
|
||||||
entities = message.entities or message.caption_entities or []
|
|
||||||
for entity in entities:
|
|
||||||
if entity.type == 'url':
|
|
||||||
urls.append(text[entity.offset:entity.offset+entity.length])
|
|
||||||
elif entity.type == 'text_link':
|
|
||||||
urls.append(entity.url)
|
|
||||||
|
|
||||||
# Если aiogram не нашел сущностей, ищем регуляркой
|
|
||||||
if not urls:
|
|
||||||
found = URL_REGEX.findall(text)
|
|
||||||
if found:
|
|
||||||
urls.extend(found)
|
|
||||||
|
|
||||||
# Определяем тип медиа
|
|
||||||
media_type = 'text'
|
|
||||||
if message.photo:
|
|
||||||
media_type = 'photo'
|
|
||||||
elif message.video:
|
|
||||||
media_type = 'video'
|
|
||||||
elif message.document:
|
|
||||||
media_type = 'document'
|
|
||||||
|
|
||||||
primary_url = urls[0] if urls else None
|
|
||||||
|
|
||||||
# Добавляем в инбокс
|
|
||||||
success = await PersonalInboxService.add_to_inbox(
|
|
||||||
user_id=config.ADMIN_USER_ID,
|
|
||||||
text=text,
|
|
||||||
url=primary_url,
|
|
||||||
media_type=media_type
|
|
||||||
)
|
|
||||||
|
|
||||||
if success:
|
|
||||||
items = await PersonalInboxService.get_pending_items(config.ADMIN_USER_ID)
|
|
||||||
count = len(items)
|
|
||||||
|
|
||||||
reply_msg = "📥 <b>Добавлено в инбокс дайджеста!</b>"
|
|
||||||
if primary_url:
|
|
||||||
reply_msg += f"\n🔗 Распознана ссылка: <code>{primary_url}</code>"
|
|
||||||
reply_msg += f"\n\n📂 Всего записей в очереди: <b>{count}</b>"
|
|
||||||
|
|
||||||
await message.answer(reply_msg, parse_mode='HTML', disable_web_page_preview=True)
|
|
||||||
else:
|
|
||||||
await message.answer("❌ Произошла ошибка при сохранении сообщения в инбокс.")
|
|
||||||
3
main.py
3
main.py
|
|
@ -79,11 +79,8 @@ async def main():
|
||||||
|
|
||||||
from handlers.smart_broadcast import router as smart_broadcast_router
|
from handlers.smart_broadcast import router as smart_broadcast_router
|
||||||
from handlers.digest import router as digest_router
|
from handlers.digest import router as digest_router
|
||||||
from handlers.personal_inbox import router as personal_inbox_router
|
|
||||||
dp.include_router(smart_broadcast_router)
|
dp.include_router(smart_broadcast_router)
|
||||||
dp.include_router(digest_router)
|
dp.include_router(digest_router)
|
||||||
dp.include_router(personal_inbox_router)
|
|
||||||
|
|
||||||
|
|
||||||
dp.startup.register(on_startup)
|
dp.startup.register(on_startup)
|
||||||
logger.info('🤖 Бот запускается...')
|
logger.info('🤖 Бот запускается...')
|
||||||
|
|
|
||||||
|
|
@ -1,162 +0,0 @@
|
||||||
import logging
|
|
||||||
from datetime import datetime
|
|
||||||
from sqlalchemy import select, update
|
|
||||||
from database.db import AsyncSessionLocal
|
|
||||||
from database.models import WeeklyDigestInbox
|
|
||||||
import google.generativeai as genai
|
|
||||||
import os
|
|
||||||
import config
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
class PersonalInboxService:
|
|
||||||
@staticmethod
|
|
||||||
async def add_to_inbox(user_id: int, text: str, url: str = None, media_type: str = 'text') -> bool:
|
|
||||||
"""Добавить запись в личный инбокс"""
|
|
||||||
try:
|
|
||||||
async with AsyncSessionLocal() as session:
|
|
||||||
item = WeeklyDigestInbox(
|
|
||||||
user_id=user_id,
|
|
||||||
message_text=text,
|
|
||||||
url=url,
|
|
||||||
media_type=media_type,
|
|
||||||
created_at=datetime.utcnow(),
|
|
||||||
status='pending'
|
|
||||||
)
|
|
||||||
session.add(item)
|
|
||||||
await session.commit()
|
|
||||||
logger.info(f"Добавлена запись в инбокс для пользователя {user_id}: {text[:30]}... [URL: {url}]")
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка при добавлении в инбокс: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def get_pending_items(user_id: int):
|
|
||||||
"""Получить все необработанные записи пользователя"""
|
|
||||||
try:
|
|
||||||
async with AsyncSessionLocal() as session:
|
|
||||||
stmt = select(WeeklyDigestInbox).where(
|
|
||||||
WeeklyDigestInbox.user_id == user_id,
|
|
||||||
WeeklyDigestInbox.status == 'pending'
|
|
||||||
).order_by(WeeklyDigestInbox.created_at.asc())
|
|
||||||
result = await session.execute(stmt)
|
|
||||||
return result.scalars().all()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка при получении записей из инбокса: {e}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def clear_pending_items(user_id: int) -> bool:
|
|
||||||
"""Очистить (пометить отправленными) все записи пользователя"""
|
|
||||||
try:
|
|
||||||
async with AsyncSessionLocal() as session:
|
|
||||||
stmt = update(WeeklyDigestInbox).where(
|
|
||||||
WeeklyDigestInbox.user_id == user_id,
|
|
||||||
WeeklyDigestInbox.status == 'pending'
|
|
||||||
).values(status='sent')
|
|
||||||
await session.execute(stmt)
|
|
||||||
await session.commit()
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка при очистке инбокса: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def generate_weekly_digest(items) -> str:
|
|
||||||
"""Сгенерировать сводный дайджест с помощью Gemini API"""
|
|
||||||
api_key = os.environ.get("GOOGLE_API_KEY")
|
|
||||||
if not api_key:
|
|
||||||
logger.warning("GOOGLE_API_KEY не задан. Генерация дайджеста через ИИ невозможна.")
|
|
||||||
return PersonalInboxService._format_fallback_digest(items)
|
|
||||||
|
|
||||||
try:
|
|
||||||
genai.configure(api_key=api_key)
|
|
||||||
model = genai.GenerativeModel('gemini-1.5-flash')
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка инициализации Gemini: {e}")
|
|
||||||
return PersonalInboxService._format_fallback_digest(items)
|
|
||||||
|
|
||||||
# Подготовка списка для промпта
|
|
||||||
items_list = []
|
|
||||||
for idx, item in enumerate(items, 1):
|
|
||||||
date_str = item.created_at.strftime('%d.%m.%Y %H:%M')
|
|
||||||
content = f"Запись #{idx} [Дата: {date_str}, Тип: {item.media_type}]:\n"
|
|
||||||
if item.message_text:
|
|
||||||
content += f"Текст/Описание: {item.message_text}\n"
|
|
||||||
if item.url:
|
|
||||||
content += f"Ссылка: {item.url}\n"
|
|
||||||
items_list.append(content)
|
|
||||||
|
|
||||||
items_text = "\n---\n".join(items_list)
|
|
||||||
|
|
||||||
prompt = f"""
|
|
||||||
Ты - умный персональный ИИ-ассистент Александра. Твоя задача - составить еженедельную субботнюю сводку (дайджест) по материалам, которые он прислал тебе за неделю.
|
|
||||||
|
|
||||||
Список присланных материалов:
|
|
||||||
{items_text}
|
|
||||||
|
|
||||||
ПРАВИЛА ОФОРМЛЕНИЯ СВОДКИ (ОЧЕНЬ ВАЖНО):
|
|
||||||
1. Отвечай строго на РУССКОМ языке.
|
|
||||||
2. Используй красивую HTML-разметку, подходящую для Telegram (используй теги <b>, <i>, <code>, <a>). Не используй markdown-символы типа ** или *.
|
|
||||||
3. Раздели материалы по логическим категориям, используя эмодзи. Подходящие категории:
|
|
||||||
- 📚 <b>Выучить / Изучить!</b> (для обучающих материалов, статей, документации, гайдов)
|
|
||||||
- 👀 <b>Проверить / Тестировать!</b> (для технических инструментов, репозиториев github, настроек серверов, команд)
|
|
||||||
- 📤 <b>Отправить / Переслать!</b> (если Александр прямо написал переслать кому-то, или это интересная новость для других)
|
|
||||||
- 📝 <b>Заметки и Идеи</b> (для текстовых заметок без ссылок, мыслей, набросков)
|
|
||||||
4. Для каждого пункта сделай ОЧЕНЬ краткую выжимку (буквально одно предложение) и укажи дату добавления.
|
|
||||||
5. Если есть ссылка (URL), ОБЯЗАТЕЛЬНО сделай её кликабельной в формате <a href="URL">Название/Источник</a>.
|
|
||||||
6. В конце добавь краткое бодрое пожелание отличных выходных.
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = model.generate_content(prompt)
|
|
||||||
return response.text.strip()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка генерации дайджеста в Gemini: {e}")
|
|
||||||
return PersonalInboxService._format_fallback_digest(items)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _format_fallback_digest(items) -> str:
|
|
||||||
"""Форматирование резервного дайджеста, если Gemini недоступен"""
|
|
||||||
lines = ["📅 <b>ЕЖЕНЕДЕЛЬНАЯ СВОДКА (Резервная копия)</b>\n"]
|
|
||||||
for idx, item in enumerate(items, 1):
|
|
||||||
date_str = item.created_at.strftime('%d.%m.%Y')
|
|
||||||
line = f"<b>{idx}.</b> "
|
|
||||||
if item.url:
|
|
||||||
line += f'<a href="{item.url}">Ссылка</a>'
|
|
||||||
else:
|
|
||||||
line += "Заметка"
|
|
||||||
line += f" ({date_str}):\n"
|
|
||||||
if item.message_text:
|
|
||||||
line += f"<i>{item.message_text[:100]}</i>"
|
|
||||||
lines.append(line)
|
|
||||||
lines.append("\n💡 <i>Модуль ИИ временно недоступен. Выше представлен простой список ваших ссылок. Приятных выходных!</i>")
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def send_weekly_digest(bot) -> bool:
|
|
||||||
"""Собрать и отправить еженедельный дайджест администратору"""
|
|
||||||
user_id = config.ADMIN_USER_ID
|
|
||||||
items = await PersonalInboxService.get_pending_items(user_id)
|
|
||||||
if not items:
|
|
||||||
logger.info("Нет записей в инбоксе для формирования дайджеста.")
|
|
||||||
return False
|
|
||||||
|
|
||||||
logger.info(f"Формирование еженедельного дайджеста из {len(items)} записей для {user_id}...")
|
|
||||||
digest_text = await PersonalInboxService.generate_weekly_digest(items)
|
|
||||||
|
|
||||||
try:
|
|
||||||
await bot.send_message(
|
|
||||||
chat_id=user_id,
|
|
||||||
text=digest_text,
|
|
||||||
parse_mode='HTML',
|
|
||||||
disable_web_page_preview=True
|
|
||||||
)
|
|
||||||
# Помечаем отправленными
|
|
||||||
await PersonalInboxService.clear_pending_items(user_id)
|
|
||||||
logger.info("Еженедельный дайджест успешно отправлен и записи очищены.")
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка при отправке дайджеста: {e}")
|
|
||||||
return False
|
|
||||||
|
|
@ -160,16 +160,6 @@ class Scheduler:
|
||||||
replace_existing=True
|
replace_existing=True
|
||||||
)
|
)
|
||||||
logger.info('✅ Добавлена задача прогноза погоды в чат по четвергам')
|
logger.info('✅ Добавлена задача прогноза погоды в чат по четвергам')
|
||||||
|
|
||||||
# Еженедельный дайджест Александра (суббота в 08:00)
|
|
||||||
self.scheduler.add_job(
|
|
||||||
self.send_personal_digest,
|
|
||||||
CronTrigger.from_crontab('0 8 * * 6'), # Каждую субботу в 08:00
|
|
||||||
id='personal_digest',
|
|
||||||
name='Еженедельный дайджест Александра',
|
|
||||||
replace_existing=True
|
|
||||||
)
|
|
||||||
logger.info('✅ Добавлена задача еженедельного дайджеста Александра (суббота 08:00)')
|
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""Остановка планировщика"""
|
"""Остановка планировщика"""
|
||||||
|
|
@ -575,13 +565,3 @@ class Scheduler:
|
||||||
logger.info("✅ Thursday weather forecast broadcasted to chat")
|
logger.info("✅ Thursday weather forecast broadcasted to chat")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in thursday_chat_weather: {e}")
|
logger.error(f"Error in thursday_chat_weather: {e}")
|
||||||
|
|
||||||
async def send_personal_digest(self):
|
|
||||||
"""Еженедельный дайджест для администратора (суббота в 08:00)"""
|
|
||||||
try:
|
|
||||||
from services.personal_inbox_service import PersonalInboxService
|
|
||||||
await PersonalInboxService.send_weekly_digest(self.bot)
|
|
||||||
logger.info('✅ Личный еженедельный дайджест отправлен')
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f'Ошибка отправки личного дайджеста: {e}')
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue