169 lines
5.8 KiB
Python
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)
|