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)