Files
TGLotteryBot/backend/app/api/routers/chats.py
T
Stardream eace48732d
Build and Push Docker Images / build (push) Successful in 17s
Initial release
2026-06-02 12:02:02 +10:00

169 lines
5.8 KiB
Python

import logging
import time
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import Response
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_current_user
from app.api.schemas import ChatInfo
from app.core.database import get_db
from app.models.managed_chat import ManagedChat
from app.models.user import User
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/chats", tags=["chats"])
# chat_id → (bytes, content_type, expires_at)
_chat_avatar_cache: dict[int, tuple[bytes, str, float]] = {}
@router.get("", response_model=list[ChatInfo])
async def list_accessible_chats(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> list[ChatInfo]:
"""Groups: any member may use. Channels: admins/creators only."""
from app.bot.router import bot
from aiogram.exceptions import TelegramAPIError
result = await db.execute(
select(ManagedChat).where(ManagedChat.is_active == True)
)
chats = result.scalars().all()
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError
_unverifiable = (
"user_id_invalid",
"have no rights",
"not enough rights",
"administrators only",
)
accessible: list[ChatInfo] = []
for chat in chats:
try:
member = await bot.get_chat_member(chat.id, current_user.id)
status = member.status
if chat.chat_type == "channel":
allowed = status in ("administrator", "creator")
else:
allowed = status in ("member", "administrator", "creator", "restricted")
except TelegramBadRequest as e:
msg = str(e).lower()
# Can't verify due to anonymous/privacy settings - include the chat
allowed = any(p in msg for p in _unverifiable)
if allowed:
logger.debug("Chat %s membership unverifiable for user %s (%s) - including", chat.id, current_user.id, e)
except TelegramForbiddenError:
continue
except TelegramAPIError:
continue
if allowed:
accessible.append(ChatInfo(
id=chat.id,
title=chat.title,
username=chat.username,
chat_type=chat.chat_type,
))
return accessible
@router.get("/{chat_id}/link")
async def get_chat_link(
chat_id: int,
db: AsyncSession = Depends(get_db),
_current_user: User = Depends(get_current_user),
) -> dict:
"""Return the best navigation URL for a chat (username > last_message_id > /1 fallback)."""
result = await db.execute(select(ManagedChat).where(ManagedChat.id == chat_id))
managed = result.scalar_one_or_none()
if not managed:
raise HTTPException(404, "Chat not found")
if managed.username:
return {"url": f"https://t.me/{managed.username}"}
channel_id = str(abs(chat_id))[3:] # strip -100 prefix
return {"url": f"https://t.me/c/{channel_id}/{managed.last_message_id or 1}"}
@router.get("/{chat_id}/invite")
async def get_chat_invite_link(
chat_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> dict:
"""Return a persistent invite link for a chat.
Reading an already-stored link: any authenticated user associated with a
lottery that requires this chat (owner or participant).
Generating a new link: only the chat's admin/creator.
"""
result = await db.execute(select(ManagedChat).where(ManagedChat.id == chat_id))
managed = result.scalar_one_or_none()
if not managed:
raise HTTPException(404, "Chat not found")
# Already-stored link: any authenticated user may read it.
# The admin intentionally generated it for participants to use.
if managed.invite_link:
return {"url": managed.invite_link}
# No link stored yet - only an admin/creator may trigger generation
from app.bot.router import bot
from aiogram.exceptions import TelegramAPIError, TelegramBadRequest
is_admin = False
try:
member = await bot.get_chat_member(chat_id, current_user.id)
is_admin = member.status in ("administrator", "creator")
except TelegramBadRequest as e:
msg = str(e).lower()
is_admin = any(p in msg for p in ("administrators only", "have no rights", "not enough rights"))
except TelegramAPIError:
pass
if not is_admin:
raise HTTPException(403, "Not authorized")
url: str | None = None
error: str | None = None
try:
link = await bot.create_chat_invite_link(chat_id)
url = link.invite_link
except Exception as e:
error = str(e)
logger.warning("create_chat_invite_link failed for chat %s: %s", chat_id, e)
if url:
managed.invite_link = url
await db.commit()
return {"url": url, "error": error}
@router.get("/{chat_id}/avatar")
async def chat_avatar(chat_id: int) -> Response:
from app.core.config import settings
ttl = settings.avatar_cache_ttl
cached = _chat_avatar_cache.get(chat_id)
if cached and time.monotonic() < cached[2]:
return Response(content=cached[0], media_type=cached[1],
headers={"Cache-Control": f"public, max-age={ttl}"})
from app.bot.router import bot
try:
chat = await bot.get_chat(chat_id)
if not chat.photo:
return Response(status_code=404)
data = await bot.download(chat.photo.small_file_id)
content = data.read()
content_type = "image/jpeg"
_chat_avatar_cache[chat_id] = (content, content_type, time.monotonic() + ttl)
return Response(content=content, media_type=content_type,
headers={"Cache-Control": f"public, max-age={ttl}"})
except Exception:
return Response(status_code=404)