168 lines
6.2 KiB
Python
168 lines
6.2 KiB
Python
"""Matching, chat, join, VIP — adapted from NomadCNA."""
|
|
|
|
from fastapi import APIRouter, Header, HTTPException, Query
|
|
|
|
from app.schemas import (
|
|
ChatMessage, ConversationItem, JoinMemberRequest, MatchProfile, MatchQuota,
|
|
SendMessageRequest, SwipeRequest, SwipeResponse, VipStatus,
|
|
)
|
|
from app.services.auth import get_user_by_token
|
|
from app.services import social_store
|
|
|
|
router = APIRouter(prefix="/social", tags=["social"])
|
|
|
|
|
|
def _token_user(authorization: str | None) -> dict:
|
|
if not authorization or not authorization.startswith("Bearer "):
|
|
raise HTTPException(401, "未登录")
|
|
user = get_user_by_token(authorization[7:])
|
|
if not user:
|
|
raise HTTPException(401, "未登录")
|
|
return user
|
|
|
|
|
|
@router.post("/join")
|
|
async def join_member(body: JoinMemberRequest, authorization: str | None = Header(None)):
|
|
user = _token_user(authorization)
|
|
profile = social_store.join_member(user["id"], user["name"], body.model_dump())
|
|
return {"success": True, "profile": _profile_payload(profile, user["id"])}
|
|
|
|
|
|
@router.get("/join/status")
|
|
async def join_status(authorization: str | None = Header(None)):
|
|
user = _token_user(authorization)
|
|
profile = social_store.get_public_profile(user["id"])
|
|
return {"complete": bool(profile), "profile": _profile_payload(profile, user["id"]) if profile else None}
|
|
|
|
|
|
def _profile_payload(profile: dict | None, user_id: str) -> dict:
|
|
if not profile:
|
|
return {}
|
|
return {
|
|
"id": profile.get("id") or f"user-{user_id}",
|
|
"userId": profile.get("userId") or user_id,
|
|
"name": profile.get("name") or "游民",
|
|
"location": profile.get("location") or "",
|
|
"citySlug": profile.get("citySlug") or "",
|
|
"gender": profile.get("gender") or "",
|
|
"single": profile.get("single") or "",
|
|
"bio": profile.get("bio") or "",
|
|
"photo": profile.get("photo") or "🧑💻",
|
|
"tags": profile.get("tags") or [],
|
|
"lookingFor": profile.get("lookingFor") or profile.get("looking_for") or [],
|
|
}
|
|
|
|
|
|
def _has_match_profile(user_id: str) -> bool:
|
|
return social_store.get_public_profile(user_id) is not None
|
|
|
|
|
|
@router.get("/matches/candidates", response_model=list[MatchProfile])
|
|
async def match_candidates(
|
|
authorization: str | None = Header(None),
|
|
intent: str = Query("friends"),
|
|
city: str = Query(""),
|
|
gender: str = Query(""),
|
|
single: str = Query(""),
|
|
exclude_swiped: bool = Query(True),
|
|
):
|
|
user = _token_user(authorization)
|
|
if not _has_match_profile(user["id"]):
|
|
# Soft-join so dating is usable after login without a stuck gate
|
|
social_store.join_member(
|
|
user["id"],
|
|
user.get("name") or "游民",
|
|
{"city": "全球", "lookingFor": [intent, "explore"], "bio": "刚加入 nomadro 匹配"},
|
|
)
|
|
return [MatchProfile(**p) for p in social_store.list_candidates(
|
|
user["id"], intent=intent, city=city, gender=gender, single=single, exclude_swiped=exclude_swiped
|
|
)]
|
|
|
|
|
|
@router.get("/matches/quota", response_model=MatchQuota)
|
|
async def match_quota(authorization: str | None = Header(None)):
|
|
user = _token_user(authorization)
|
|
return MatchQuota(**social_store.get_quota(user["id"]))
|
|
|
|
|
|
@router.post("/matches/swipes/undo")
|
|
async def undo_swipe(authorization: str | None = Header(None)):
|
|
user = _token_user(authorization)
|
|
res = social_store.undo_last_swipe(user["id"])
|
|
if not res.get("ok"):
|
|
raise HTTPException(400, "没有可撤销的滑动")
|
|
return res
|
|
|
|
|
|
@router.get("/matches/likes/received", response_model=list[MatchProfile])
|
|
async def match_likes_received(authorization: str | None = Header(None)):
|
|
user = _token_user(authorization)
|
|
return [MatchProfile(**p) for p in social_store.list_likes_received(user["id"])]
|
|
|
|
|
|
@router.get("/matches/likes", response_model=list[MatchProfile])
|
|
async def match_likes(authorization: str | None = Header(None)):
|
|
user = _token_user(authorization)
|
|
return [MatchProfile(**p) for p in social_store.list_likes(user["id"])]
|
|
|
|
|
|
@router.get("/matches/mutual")
|
|
async def mutual_matches(authorization: str | None = Header(None)):
|
|
user = _token_user(authorization)
|
|
return {"items": social_store.list_mutual(user["id"])}
|
|
|
|
|
|
@router.post("/matches/swipes", response_model=SwipeResponse)
|
|
async def record_swipe(body: SwipeRequest, authorization: str | None = Header(None)):
|
|
user = _token_user(authorization)
|
|
result = social_store.record_swipe(user["id"], body.profileId, body.action, body.intent)
|
|
if not result.get("ok"):
|
|
err = result.get("error")
|
|
if err == "quota_exceeded":
|
|
raise HTTPException(429, "今日滑动次数已用完")
|
|
if err == "vip_required":
|
|
raise HTTPException(403, "超级喜欢需要 VIP")
|
|
if err == "duplicate":
|
|
raise HTTPException(409, "已滑动过该用户")
|
|
raise HTTPException(400, err or "滑动失败")
|
|
return SwipeResponse(**result)
|
|
|
|
|
|
@router.get("/conversations", response_model=list[ConversationItem])
|
|
async def list_conversations(authorization: str | None = Header(None)):
|
|
user = _token_user(authorization)
|
|
items = social_store.list_conversations(user["id"])
|
|
return [ConversationItem(**c) for c in items]
|
|
|
|
|
|
@router.get("/conversations/{conv_id}")
|
|
async def get_conversation(conv_id: str, authorization: str | None = Header(None)):
|
|
user = _token_user(authorization)
|
|
conv = social_store.get_conversation(conv_id, user["id"])
|
|
if not conv:
|
|
raise HTTPException(404, "会话不存在")
|
|
return conv
|
|
|
|
|
|
@router.get("/conversations/{conv_id}/messages", response_model=list[ChatMessage])
|
|
async def get_messages(conv_id: str, authorization: str | None = Header(None), limit: int = 50):
|
|
user = _token_user(authorization)
|
|
return [ChatMessage(**m) for m in social_store.list_messages(conv_id, user["id"], limit)]
|
|
|
|
|
|
@router.post("/conversations/{conv_id}/messages", response_model=ChatMessage)
|
|
async def post_message(conv_id: str, body: SendMessageRequest, authorization: str | None = Header(None)):
|
|
user = _token_user(authorization)
|
|
msg = social_store.send_message(conv_id, user["id"], body.body)
|
|
if not msg:
|
|
raise HTTPException(400, "发送失败")
|
|
return ChatMessage(**msg)
|
|
|
|
|
|
@router.get("/vip/check", response_model=VipStatus)
|
|
async def vip_check(authorization: str | None = Header(None)):
|
|
user = _token_user(authorization)
|
|
vip = social_store.is_vip(user["id"])
|
|
exp = social_store.membership_expires_at(user["id"])
|
|
return VipStatus(vip=vip, expires_at=exp)
|