合入家庭共享与设备消息能力
This commit is contained in:
208
talkingq-url/banban/service/family.py
Normal file
208
talkingq-url/banban/service/family.py
Normal file
@@ -0,0 +1,208 @@
|
||||
from collections.abc import Mapping
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
from banban.dao.family import (
|
||||
FAMILY_ROLE_OWNER,
|
||||
INVITE_STATUS_PENDING,
|
||||
MAX_FAMILY_MEMBERS,
|
||||
FamilyDAO,
|
||||
)
|
||||
from services.database_service_base import DatabaseServiceBase
|
||||
|
||||
|
||||
class FamilyService(DatabaseServiceBase):
|
||||
def __init__(self):
|
||||
super().__init__(service_name="family_service")
|
||||
|
||||
async def ensure_device_access(self, *, device_id: str, user_id: int) -> Mapping:
|
||||
db_session = await self.get_session()
|
||||
try:
|
||||
dao = FamilyDAO(db_session)
|
||||
binding = await dao.get_binding_for_access(device_id=device_id, user_id=user_id)
|
||||
if binding is None:
|
||||
raise HTTPException(status_code=404, detail="device not found")
|
||||
return binding
|
||||
finally:
|
||||
await db_session.close()
|
||||
|
||||
async def ensure_device_owner(self, *, device_id: str, user_id: int) -> Mapping:
|
||||
db_session = await self.get_session()
|
||||
try:
|
||||
dao = FamilyDAO(db_session)
|
||||
binding = await dao.get_binding_for_owner(device_id=device_id, user_id=user_id)
|
||||
if binding is None:
|
||||
raise HTTPException(status_code=403, detail="only owner can manage family")
|
||||
await dao.ensure_owner_member(device_id=device_id)
|
||||
await db_session.commit()
|
||||
return binding
|
||||
finally:
|
||||
await db_session.close()
|
||||
|
||||
async def has_child_access(self, *, child_id: int, user_id: int) -> bool:
|
||||
db_session = await self.get_session()
|
||||
try:
|
||||
dao = FamilyDAO(db_session)
|
||||
return await dao.has_child_access(child_id=child_id, user_id=user_id)
|
||||
finally:
|
||||
await db_session.close()
|
||||
|
||||
async def get_child_for_access(self, *, child_id: int, user_id: int) -> Mapping | None:
|
||||
db_session = await self.get_session()
|
||||
try:
|
||||
dao = FamilyDAO(db_session)
|
||||
return await dao.get_child_for_access(child_id=child_id, user_id=user_id)
|
||||
finally:
|
||||
await db_session.close()
|
||||
|
||||
async def list_children_for_access(self, *, user_id: int, limit: int, cursor: int | None) -> tuple[list[Mapping], bool]:
|
||||
db_session = await self.get_session()
|
||||
try:
|
||||
dao = FamilyDAO(db_session)
|
||||
rows = await dao.list_children_for_access(user_id=user_id, limit=limit, cursor=cursor)
|
||||
has_more = len(rows) > limit
|
||||
return rows[:limit], has_more
|
||||
finally:
|
||||
await db_session.close()
|
||||
|
||||
async def list_members(self, *, device_id: str, user_id: int) -> list[Mapping]:
|
||||
db_session = await self.get_session()
|
||||
try:
|
||||
dao = FamilyDAO(db_session)
|
||||
await dao.ensure_owner_member(device_id=device_id)
|
||||
rows = await dao.list_members(device_id=device_id, user_id=user_id)
|
||||
if not rows:
|
||||
raise HTTPException(status_code=404, detail="device not found")
|
||||
await db_session.commit()
|
||||
return rows
|
||||
finally:
|
||||
await db_session.close()
|
||||
|
||||
async def create_invitation(self, *, device_id: str, user_id: int) -> Mapping:
|
||||
db_session = await self.get_session()
|
||||
try:
|
||||
dao = FamilyDAO(db_session)
|
||||
binding = await dao.get_binding_for_owner(device_id=device_id, user_id=user_id)
|
||||
if binding is None:
|
||||
raise HTTPException(status_code=403, detail="only owner can invite family members")
|
||||
|
||||
await dao.ensure_owner_member(device_id=device_id)
|
||||
member_count = await dao.count_active_members(device_id=device_id)
|
||||
if member_count >= MAX_FAMILY_MEMBERS:
|
||||
raise HTTPException(status_code=409, detail="family member limit reached")
|
||||
|
||||
invite_token, expires_at = await dao.create_invitation(device_id=device_id, owner_user_id=user_id)
|
||||
await db_session.commit()
|
||||
return {
|
||||
"invite_token": invite_token,
|
||||
"device_id": device_id,
|
||||
"expires_at": expires_at,
|
||||
}
|
||||
except Exception:
|
||||
await db_session.rollback()
|
||||
raise
|
||||
finally:
|
||||
await db_session.close()
|
||||
|
||||
async def get_invitation(self, *, invite_token: str) -> Mapping:
|
||||
db_session = await self.get_session()
|
||||
try:
|
||||
dao = FamilyDAO(db_session)
|
||||
invitation = await dao.get_invitation(invite_token=invite_token)
|
||||
if invitation is None:
|
||||
raise HTTPException(status_code=404, detail="family invitation not found")
|
||||
|
||||
status = int(invitation["status"])
|
||||
if status == INVITE_STATUS_PENDING and datetime.utcnow() > invitation["expires_at"]:
|
||||
await dao.mark_invitation_expired(invitation_id=int(invitation["id"]))
|
||||
await db_session.commit()
|
||||
invitation = await dao.get_invitation(invite_token=invite_token)
|
||||
if invitation is None:
|
||||
raise HTTPException(status_code=404, detail="family invitation not found")
|
||||
|
||||
return invitation
|
||||
finally:
|
||||
await db_session.close()
|
||||
|
||||
async def accept_invitation(self, *, invite_token: str, user_id: int) -> Mapping:
|
||||
db_session = await self.get_session()
|
||||
try:
|
||||
dao = FamilyDAO(db_session)
|
||||
invitation = await dao.get_invitation(invite_token=invite_token)
|
||||
if invitation is None:
|
||||
raise HTTPException(status_code=404, detail="family invitation not found")
|
||||
|
||||
if int(invitation["status"]) != INVITE_STATUS_PENDING:
|
||||
raise HTTPException(status_code=409, detail="family invitation is not available")
|
||||
if datetime.utcnow() > invitation["expires_at"]:
|
||||
await dao.mark_invitation_expired(invitation_id=int(invitation["id"]))
|
||||
await db_session.commit()
|
||||
raise HTTPException(status_code=410, detail="family invitation expired")
|
||||
|
||||
await dao.ensure_owner_member(device_id=str(invitation["device_id"]))
|
||||
existing_access = await dao.get_binding_for_access(device_id=str(invitation["device_id"]), user_id=user_id)
|
||||
if existing_access is None:
|
||||
member_count = await dao.count_active_members(device_id=str(invitation["device_id"]))
|
||||
if member_count >= MAX_FAMILY_MEMBERS:
|
||||
raise HTTPException(status_code=409, detail="family member limit reached")
|
||||
|
||||
await dao.accept_invitation(invitation=invitation, user_id=user_id)
|
||||
await db_session.commit()
|
||||
return {
|
||||
"device_id": str(invitation["device_id"]),
|
||||
"child_id": invitation.get("child_id"),
|
||||
"child_name": invitation.get("child_name"),
|
||||
}
|
||||
except Exception:
|
||||
await db_session.rollback()
|
||||
raise
|
||||
finally:
|
||||
await db_session.close()
|
||||
|
||||
async def remove_member(self, *, device_id: str, member_user_id: int, user_id: int) -> bool:
|
||||
db_session = await self.get_session()
|
||||
try:
|
||||
dao = FamilyDAO(db_session)
|
||||
removed = await dao.remove_member(
|
||||
device_id=device_id,
|
||||
member_user_id=member_user_id,
|
||||
removed_by_user_id=user_id,
|
||||
)
|
||||
if not removed:
|
||||
owner_binding = await dao.get_binding_for_owner(device_id=device_id, user_id=user_id)
|
||||
if owner_binding is None:
|
||||
raise HTTPException(status_code=403, detail="only owner can remove family members")
|
||||
if int(owner_binding["owner_user_id"]) == member_user_id:
|
||||
raise HTTPException(status_code=400, detail="owner cannot be removed")
|
||||
raise HTTPException(status_code=404, detail="family member not found")
|
||||
await db_session.commit()
|
||||
return True
|
||||
except Exception:
|
||||
await db_session.rollback()
|
||||
raise
|
||||
finally:
|
||||
await db_session.close()
|
||||
|
||||
async def leave_family(self, *, device_id: str, user_id: int) -> bool:
|
||||
db_session = await self.get_session()
|
||||
try:
|
||||
dao = FamilyDAO(db_session)
|
||||
left = await dao.leave_family(device_id=device_id, user_id=user_id)
|
||||
if not left:
|
||||
binding = await dao.get_binding_for_access(device_id=device_id, user_id=user_id)
|
||||
if binding is None:
|
||||
raise HTTPException(status_code=404, detail="device not found")
|
||||
if int(binding["owner_user_id"]) == user_id:
|
||||
raise HTTPException(status_code=400, detail="owner cannot leave family")
|
||||
raise HTTPException(status_code=404, detail="family member not found")
|
||||
await db_session.commit()
|
||||
return True
|
||||
except Exception:
|
||||
await db_session.rollback()
|
||||
raise
|
||||
finally:
|
||||
await db_session.close()
|
||||
|
||||
|
||||
family_service = FamilyService()
|
||||
Reference in New Issue
Block a user