A kind of initial commit

This commit is contained in:
heboba
2026-02-26 19:33:05 +00:00
commit 9ab125b1a6
37 changed files with 3053 additions and 0 deletions

217
app/bot/telegram_bot.py Normal file
View File

@@ -0,0 +1,217 @@
from __future__ import annotations
import contextlib
from telegram import Update
from telegram.ext import Application, CommandHandler, ContextTypes
from app.db.repositories import PlaylistRunRepository, SavedTrackRepository, UserRepository
from app.services.app_services import AppServices
class TelegramBotRunner:
def __init__(self, token: str, session_factory, services: AppServices, app_base_url: str) -> None:
self.token = token
self.session_factory = session_factory
self.services = services
self.app_base_url = app_base_url.rstrip("/")
self.application = Application.builder().token(token).build()
self._setup_handlers()
self._running = False
def _setup_handlers(self) -> None:
self.application.add_handler(CommandHandler("start", self.start))
self.application.add_handler(CommandHandler("help", self.help))
self.application.add_handler(CommandHandler("connect", self.connect))
self.application.add_handler(CommandHandler("status", self.status))
self.application.add_handler(CommandHandler("generate", self.generate))
self.application.add_handler(CommandHandler("latest", self.latest))
self.application.add_handler(CommandHandler("setsize", self.set_size))
self.application.add_handler(CommandHandler("setratio", self.set_ratio))
self.application.add_handler(CommandHandler("sync", self.sync_likes))
async def start_polling(self) -> None:
await self.application.initialize()
await self.application.start()
if self.application.updater is None:
raise RuntimeError("Telegram updater is not available")
await self.application.updater.start_polling(drop_pending_updates=False)
self._running = True
async def stop(self) -> None:
if not self._running:
return
with contextlib.suppress(Exception):
if self.application.updater:
await self.application.updater.stop()
with contextlib.suppress(Exception):
await self.application.stop()
with contextlib.suppress(Exception):
await self.application.shutdown()
self._running = False
async def send_message(self, chat_id: str, text: str) -> None:
await self.application.bot.send_message(chat_id=int(chat_id), text=text, disable_web_page_preview=False)
async def _ensure_user(self, update: Update):
chat = update.effective_chat
user = update.effective_user
if not chat:
return None
async with self.session_factory() as session:
repo = UserRepository(session)
db_user = await repo.get_or_create_by_chat(
chat_id=str(chat.id),
username=user.username if user else None,
)
await session.commit()
return db_user
async def start(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
await self._ensure_user(update)
await update.message.reply_text(
"Я бот для Spotify daily vibe playlist.\n"
"Команды: /connect /status /generate /latest /setsize 30 /setratio 0.8 /sync"
)
async def help(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
await update.message.reply_text(
"/connect - привязать Spotify\n"
"/status - статус аккаунта и последнего плейлиста\n"
"/generate - сгенерировать плейлист сейчас\n"
"/latest - показать ссылку на последний плейлист\n"
"/setsize N - размер плейлиста\n"
"/setratio X - доля новых треков (0.5..1.0)\n"
"/sync - обновить лайкнутые треки из Spotify"
)
async def connect(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
chat = update.effective_chat
user = update.effective_user
if not chat:
return
url = await self.services.auth.create_connect_url(str(chat.id), user.username if user else None)
await update.message.reply_text(f"Открой ссылку и авторизуй Spotify:\n{url}", disable_web_page_preview=True)
async def status(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
chat = update.effective_chat
if not chat:
return
async with self.session_factory() as session:
users = UserRepository(session)
runs = PlaylistRunRepository(session)
saved = SavedTrackRepository(session)
db_user = await users.get_by_chat_id(str(chat.id))
if not db_user:
await update.message.reply_text("Пользователь не найден. Напиши /start")
return
latest = await runs.latest_for_user(db_user.id)
saved_count = await saved.count_for_user(db_user.id)
connected = "yes" if db_user.spotify_refresh_token else "no"
text = (
f"Connected: {connected}\n"
f"Spotify user: {db_user.spotify_user_id or '-'}\n"
f"Liked tracks cached: {saved_count}\n"
f"Playlist size: {db_user.playlist_size}\n"
f"Min new ratio: {db_user.min_new_ratio:.2f}\n"
f"Last generated: {db_user.last_generated_date or '-'}"
)
if latest:
text += f"\nLast run: {latest.status}, tracks={latest.total_tracks}"
if latest.playlist_url:
text += f"\n{latest.playlist_url}"
await update.message.reply_text(text, disable_web_page_preview=False)
async def latest(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
chat = update.effective_chat
if not chat:
return
async with self.session_factory() as session:
users = UserRepository(session)
db_user = await users.get_by_chat_id(str(chat.id))
if not db_user or not db_user.latest_playlist_url:
await update.message.reply_text("Пока нет сгенерированного плейлиста.")
return
await update.message.reply_text(db_user.latest_playlist_url, disable_web_page_preview=False)
async def generate(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
chat = update.effective_chat
if not chat:
return
async with self.session_factory() as session:
users = UserRepository(session)
db_user = await users.get_by_chat_id(str(chat.id))
if not db_user:
await update.message.reply_text("Пользователь не найден. Напиши /start")
return
user_id = db_user.id
await update.message.reply_text("Генерирую плейлист, это может занять 20-60 секунд...")
outcome = await self.services.jobs.generate_for_user(user_id=user_id, force=True, notify=False)
if outcome.ok:
msg = outcome.message
if outcome.playlist_url:
msg += f"\n{outcome.playlist_url}"
await update.message.reply_text(msg, disable_web_page_preview=False)
else:
await update.message.reply_text(f"Ошибка: {outcome.message}")
async def set_size(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
chat = update.effective_chat
if not chat:
return
if not context.args:
await update.message.reply_text("Использование: /setsize 30")
return
try:
value = int(context.args[0])
if value < 5 or value > 100:
raise ValueError
except ValueError:
await update.message.reply_text("Размер должен быть числом от 5 до 100.")
return
async with self.session_factory() as session:
users = UserRepository(session)
db_user = await users.get_or_create_by_chat(str(chat.id), update.effective_user.username if update.effective_user else None)
db_user.playlist_size = value
await session.commit()
await update.message.reply_text(f"Размер плейлиста установлен: {value}")
async def set_ratio(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
chat = update.effective_chat
if not chat:
return
if not context.args:
await update.message.reply_text("Использование: /setratio 0.8")
return
try:
value = float(context.args[0])
if value < 0.5 or value > 1.0:
raise ValueError
except ValueError:
await update.message.reply_text("Значение должно быть от 0.5 до 1.0")
return
async with self.session_factory() as session:
users = UserRepository(session)
db_user = await users.get_or_create_by_chat(str(chat.id), update.effective_user.username if update.effective_user else None)
db_user.min_new_ratio = value
await session.commit()
await update.message.reply_text(f"Минимальная доля новых треков: {value:.2f}")
async def sync_likes(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
chat = update.effective_chat
if not chat:
return
async with self.session_factory() as session:
users = UserRepository(session)
db_user = await users.get_by_chat_id(str(chat.id))
if not db_user:
await update.message.reply_text("Пользователь не найден. Напиши /start")
return
if not db_user.spotify_refresh_token:
await update.message.reply_text("Сначала /connect")
return
access_token = await self.services.auth.ensure_valid_access_token(session, db_user)
await self.services.recommendation.sync_saved_tracks(session, db_user, access_token)
await session.commit()
saved_count = await SavedTrackRepository(session).count_for_user(db_user.id)
await update.message.reply_text(f"Лайкнутые треки обновлены: {saved_count}")