from __future__ import annotations from datetime import UTC, datetime import hashlib from app.database.lazer_user import BASE_INCLUDES, User, UserResp from app.database.team import Team, TeamMember, TeamRequest from app.dependencies.database import Database from app.dependencies.storage import get_storage_service from app.dependencies.user import get_client_user from app.models.notification import ( TeamApplicationAccept, TeamApplicationReject, TeamApplicationStore, ) from app.router.notification import server from app.storage.base import StorageService from app.utils import check_image from .router import router from fastapi import Depends, File, Form, HTTPException, Path, Request, Security from pydantic import BaseModel from sqlmodel import exists, select @router.post("/team", name="创建战队", response_model=Team) async def create_team( session: Database, storage: StorageService = Depends(get_storage_service), current_user: User = Security(get_client_user), flag: bytes = File(..., description="战队图标文件"), cover: bytes = File(..., description="战队头图文件"), name: str = Form(max_length=100, description="战队名称"), short_name: str = Form(max_length=10, description="战队缩写"), ): """创建战队。 flag 限制 240x120, 2MB; cover 限制 3000x2000, 10MB 支持的图片格式: PNG、JPEG、GIF """ user_id = current_user.id if (await current_user.awaitable_attrs.team_membership) is not None: raise HTTPException(status_code=403, detail="You are already in a team") is_existed = (await session.exec(select(exists()).where(Team.name == name))).first() if is_existed: raise HTTPException(status_code=409, detail="Name already exists") is_existed = (await session.exec(select(exists()).where(Team.short_name == short_name))).first() if is_existed: raise HTTPException(status_code=409, detail="Short name already exists") check_image(flag, 2 * 1024 * 1024, 240, 120) check_image(cover, 10 * 1024 * 1024, 3000, 2000) now = datetime.now(UTC) team = Team(name=name, short_name=short_name, leader_id=user_id, created_at=now) session.add(team) await session.commit() await session.refresh(team) filehash = hashlib.sha256(flag).hexdigest() storage_path = f"team_flag/{team.id}_{filehash}.png" if not await storage.is_exists(storage_path): await storage.write_file(storage_path, flag) team.flag_url = await storage.get_file_url(storage_path) filehash = hashlib.sha256(cover).hexdigest() storage_path = f"team_cover/{team.id}_{filehash}.png" if not await storage.is_exists(storage_path): await storage.write_file(storage_path, cover) team.cover_url = await storage.get_file_url(storage_path) team_member = TeamMember(user_id=user_id, team_id=team.id, joined_at=now) session.add(team_member) await session.commit() await session.refresh(team) return team @router.patch("/team/{team_id}", name="修改战队", response_model=Team) async def update_team( team_id: int, session: Database, storage: StorageService = Depends(get_storage_service), current_user: User = Security(get_client_user), flag: bytes | None = File(default=None, description="战队图标文件"), cover: bytes | None = File(default=None, description="战队头图文件"), name: str | None = Form(default=None, max_length=100, description="战队名称"), short_name: str | None = Form(default=None, max_length=10, description="战队缩写"), leader_id: int | None = Form(default=None, description="战队队长 ID"), ): """修改战队。 flag 限制 240x120, 2MB; cover 限制 3000x2000, 10MB 支持的图片格式: PNG、JPEG、GIF """ team = await session.get(Team, team_id) user_id = current_user.id if not team: raise HTTPException(status_code=404, detail="Team not found") if team.leader_id != user_id: raise HTTPException(status_code=403, detail="You are not the team leader") is_existed = (await session.exec(select(exists()).where(Team.name == name))).first() if is_existed: raise HTTPException(status_code=409, detail="Name already exists") is_existed = (await session.exec(select(exists()).where(Team.short_name == short_name))).first() if is_existed: raise HTTPException(status_code=409, detail="Short name already exists") if flag: check_image(flag, 2 * 1024 * 1024, 240, 120) filehash = hashlib.sha256(flag).hexdigest() storage_path = f"team_flag/{team.id}_{filehash}.png" if not await storage.is_exists(storage_path): await storage.write_file(storage_path, flag) team.flag_url = await storage.get_file_url(storage_path) if cover: check_image(cover, 10 * 1024 * 1024, 3000, 2000) filehash = hashlib.sha256(cover).hexdigest() storage_path = f"team_cover/{team.id}_{filehash}.png" if not await storage.is_exists(storage_path): await storage.write_file(storage_path, cover) team.cover_url = await storage.get_file_url(storage_path) if leader_id is not None: if not (await session.exec(select(exists()).where(User.id == leader_id))).first(): raise HTTPException(status_code=404, detail="Leader not found") if not ( await session.exec(select(TeamMember).where(TeamMember.user_id == leader_id, TeamMember.team_id == team.id)) ).first(): raise HTTPException(status_code=404, detail="Leader is not a member of the team") team.leader_id = leader_id await session.commit() await session.refresh(team) return team @router.delete("/team/{team_id}", name="删除战队", status_code=204) async def delete_team( session: Database, team_id: int = Path(..., description="战队 ID"), current_user: User = Security(get_client_user), ): team = await session.get(Team, team_id) if not team: raise HTTPException(status_code=404, detail="Team not found") if team.leader_id != current_user.id: raise HTTPException(status_code=403, detail="You are not the team leader") team_members = await session.exec(select(TeamMember).where(TeamMember.team_id == team_id)) for member in team_members: await session.delete(member) await session.delete(team) await session.commit() class TeamQueryResp(BaseModel): team: Team members: list[UserResp] @router.get("/team/{team_id}", name="查询战队", response_model=TeamQueryResp) async def get_team( session: Database, team_id: int = Path(..., description="战队 ID"), ): members = (await session.exec(select(TeamMember).where(TeamMember.team_id == team_id))).all() return TeamQueryResp( team=members[0].team, members=[await UserResp.from_db(m.user, session, include=BASE_INCLUDES) for m in members], ) @router.post("/team/{team_id}/request", name="请求加入战队", status_code=204) async def request_join_team( session: Database, team_id: int = Path(..., description="战队 ID"), current_user: User = Security(get_client_user), ): team = await session.get(Team, team_id) if not team: raise HTTPException(status_code=404, detail="Team not found") if (await current_user.awaitable_attrs.team_membership) is not None: raise HTTPException(status_code=403, detail="You are already in a team") if ( await session.exec( select(exists()).where(TeamRequest.team_id == team_id, TeamRequest.user_id == current_user.id) ) ).first(): raise HTTPException(status_code=409, detail="Join request already exists") team_request = TeamRequest(user_id=current_user.id, team_id=team_id, requested_at=datetime.now(UTC)) session.add(team_request) await session.commit() await session.refresh(team_request) await server.new_private_notification(TeamApplicationStore.init(team_request)) @router.post("/team/{team_id}/{user_id}/request", name="接受加入请求", status_code=204) @router.delete("/team/{team_id}/{user_id}/request", name="拒绝加入请求", status_code=204) async def handle_request( req: Request, session: Database, team_id: int = Path(..., description="战队 ID"), user_id: int = Path(..., description="用户 ID"), current_user: User = Security(get_client_user), ): team = await session.get(Team, team_id) if not team: raise HTTPException(status_code=404, detail="Team not found") if team.leader_id != current_user.id: raise HTTPException(status_code=403, detail="You are not the team leader") team_request = ( await session.exec(select(TeamRequest).where(TeamRequest.team_id == team_id, TeamRequest.user_id == user_id)) ).first() if not team_request: raise HTTPException(status_code=404, detail="Join request not found") user = await session.get(User, user_id) if not user: raise HTTPException(status_code=404, detail="User not found") if req.method == "POST": if (await session.exec(select(exists()).where(TeamMember.user_id == user_id))).first(): raise HTTPException(status_code=409, detail="User is already a member of the team") session.add(TeamMember(user_id=user_id, team_id=team_id, joined_at=datetime.now(UTC))) await server.new_private_notification(TeamApplicationAccept.init(team_request)) else: await server.new_private_notification(TeamApplicationReject.init(team_request)) await session.delete(team_request) await session.commit() @router.delete("/team/{team_id}/{user_id}", name="踢出成员 / 退出战队", status_code=204) async def kick_member( session: Database, team_id: int = Path(..., description="战队 ID"), user_id: int = Path(..., description="用户 ID"), current_user: User = Security(get_client_user), ): team = await session.get(Team, team_id) if not team: raise HTTPException(status_code=404, detail="Team not found") if team.leader_id != current_user.id and user_id != current_user.id: raise HTTPException(status_code=403, detail="You are not the team leader") team_member = ( await session.exec(select(TeamMember).where(TeamMember.team_id == team_id, TeamMember.user_id == user_id)) ).first() if not team_member: raise HTTPException(status_code=404, detail="User is not a member of the team") if team.leader_id == current_user.id: raise HTTPException(status_code=403, detail="You cannot leave because you are the team leader") await session.delete(team_member) await session.commit()