Compare commits

..

No commits in common. "5643d8fb4a503aabc6d754d45c968c0e75d895cd" and "c68c9e0774a951ce00f583b19239bfbfa5f9a9f9" have entirely different histories.

8 changed files with 0 additions and 2896 deletions

View file

@ -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())

View file

@ -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

View file

@ -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

View file

@ -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("❌ Произошла ошибка при сохранении сообщения в инбокс.")

View file

@ -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('🤖 Бот запускается...')

View file

@ -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

View file

@ -161,16 +161,6 @@ class Scheduler:
) )
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):
"""Остановка планировщика""" """Остановка планировщика"""
self.scheduler.shutdown() self.scheduler.shutdown()
@ -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}')