"""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)