feat(team): support team
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from . import avatar, cover, oauth, relationship, username # noqa: F401
|
||||
from . import avatar, cover, oauth, relationship, team, username # noqa: F401
|
||||
from .router import router as private_router
|
||||
|
||||
__all__ = [
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from io import BytesIO
|
||||
|
||||
from app.database.lazer_user import User
|
||||
from app.dependencies.database import Database
|
||||
from app.dependencies.storage import get_storage_service
|
||||
from app.dependencies.user import get_client_user
|
||||
from app.storage.base import StorageService
|
||||
from app.utils import check_image
|
||||
|
||||
from .router import router
|
||||
|
||||
from fastapi import Depends, File, HTTPException, Security
|
||||
from PIL import Image
|
||||
from fastapi import Depends, File, Security
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -39,20 +38,7 @@ async def upload_avatar(
|
||||
"""
|
||||
|
||||
# check file
|
||||
if len(content) > 5 * 1024 * 1024: # 5MB limit
|
||||
raise HTTPException(status_code=400, detail="File size exceeds 5MB limit")
|
||||
elif len(content) == 0:
|
||||
raise HTTPException(status_code=400, detail="File cannot be empty")
|
||||
try:
|
||||
with Image.open(BytesIO(content)) as img:
|
||||
if img.format not in ["PNG", "JPEG", "GIF"]:
|
||||
raise HTTPException(status_code=400, detail="Invalid image format")
|
||||
if img.size[0] > 256 or img.size[1] > 256:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Image size exceeds 256x256 pixels"
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f"Error processing image: {e}")
|
||||
check_image(content, 5 * 1024 * 1024, 256, 256)
|
||||
|
||||
filehash = hashlib.sha256(content).hexdigest()
|
||||
storage_path = f"avatars/{current_user.id}_{filehash}.png"
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from io import BytesIO
|
||||
|
||||
from app.database.lazer_user import User, UserProfileCover
|
||||
from app.dependencies.database import Database
|
||||
from app.dependencies.storage import get_storage_service
|
||||
from app.dependencies.user import get_client_user
|
||||
from app.storage.base import StorageService
|
||||
from app.utils import check_image
|
||||
|
||||
from .router import router
|
||||
|
||||
from fastapi import Depends, File, HTTPException, Security
|
||||
from PIL import Image
|
||||
from fastapi import Depends, File, Security
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -39,20 +38,7 @@ async def upload_cover(
|
||||
"""
|
||||
|
||||
# check file
|
||||
if len(content) > 10 * 1024 * 1024: # 10MB limit
|
||||
raise HTTPException(status_code=400, detail="File size exceeds 10MB limit")
|
||||
elif len(content) == 0:
|
||||
raise HTTPException(status_code=400, detail="File cannot be empty")
|
||||
try:
|
||||
with Image.open(BytesIO(content)) as img:
|
||||
if img.format not in ["PNG", "JPEG", "GIF"]:
|
||||
raise HTTPException(status_code=400, detail="Invalid image format")
|
||||
if img.size[0] > 3000 or img.size[1] > 2000:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Image size exceeds 3000x2000 pixels"
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f"Error processing image: {e}")
|
||||
check_image(content, 10 * 1024 * 1024, 3000, 2000)
|
||||
|
||||
filehash = hashlib.sha256(content).hexdigest()
|
||||
storage_path = f"cover/{current_user.id}_{filehash}.png"
|
||||
|
||||
314
app/router/private/team.py
Normal file
314
app/router/private/team.py
Normal file
@@ -0,0 +1,314 @@
|
||||
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, get_redis
|
||||
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, Request, Security
|
||||
from pydantic import BaseModel
|
||||
from redis.asyncio import Redis
|
||||
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
|
||||
assert 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
|
||||
assert 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,
|
||||
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,
|
||||
):
|
||||
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,
|
||||
current_user: User = Security(get_client_user),
|
||||
redis: Redis = Depends(get_redis),
|
||||
):
|
||||
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,
|
||||
user_id: int,
|
||||
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,
|
||||
user_id: int,
|
||||
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()
|
||||
Reference in New Issue
Block a user