Initial release
Build and Push Docker Images / build (push) Successful in 17s

This commit is contained in:
Stardream
2026-06-02 10:58:24 +10:00
commit eace48732d
88 changed files with 11140 additions and 0 deletions
+168
View File
@@ -0,0 +1,168 @@
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)