fix: resolve owner through Avatar model in takeover service

- Fix C1: check_takeover_enabled now uses owner_huihui_id to query Avatar first
- Fix C2: execute_takeover resolves owner_id via Avatar model (Authorization has no owner_id)
- Fix I1: add test for None case (no Avatar found)
- Fix I2: add test verifying filter arguments
- Fix I3: implement process_delayed_queue to scan and dispatch expired messages
This commit is contained in:
stefanfeng
2026-08-07 16:55:25 +08:00
parent 190ec48d9c
commit dbd668e451
2 changed files with 282 additions and 65 deletions
@@ -7,7 +7,7 @@ from typing import Optional
import httpx
from sqlalchemy.orm import Session
from models import Authorization
from models import Avatar, Authorization
from services.boxim_client import BoxIMClient
logger = logging.getLogger(__name__)
@@ -33,15 +33,18 @@ class TakeoverService:
self, owner_huihui_id: str, from_user_id: str
) -> Optional[Authorization]:
"""Check whether takeover is enabled for the given target user."""
avatar = self.db.query(Avatar).filter(Avatar.owner_id == owner_huihui_id).first()
if not avatar:
return None
auth = (
self.db.query(Authorization)
.filter(Authorization.avatar_id == avatar.id)
.filter(Authorization.target_id == from_user_id)
.filter(Authorization.takeover_enabled == True)
.first()
)
if auth and auth.takeover_enabled:
return auth
return None
return auth if auth and auth.takeover_enabled else None
async def generate_reply(self, avatar_id: str, message: str) -> str:
"""Call the avatar chat endpoint to generate a reply."""
@@ -63,7 +66,13 @@ class TakeoverService:
async def execute_takeover(self, auth: Authorization, message: dict) -> bool:
"""Execute takeover: generate a reply and send it as the owner via IM."""
try:
owner_huihui_id = auth.owner_id if hasattr(auth, "owner_id") else ""
# Resolve owner through Avatar model
avatar = self.db.query(Avatar).filter(Avatar.id == auth.avatar_id).first()
if not avatar:
logger.warning(f"Avatar not found: {auth.avatar_id}")
return False
owner_huihui_id = avatar.owner_id
credentials = await self.boxim.get_credentials(owner_huihui_id)
if not credentials:
logger.warning(f"Cannot obtain IM credentials for owner: {owner_huihui_id}")
@@ -91,12 +100,15 @@ class TakeoverService:
if not self.redis:
logger.warning("Redis not configured, degrading to immediate takeover")
return
avatar = self.db.query(Avatar).filter(Avatar.id == auth.avatar_id).first()
owner_huihui_id = avatar.owner_id if avatar else ""
key = f"takeover:delayed:{auth.target_id}:{message.get('msg_id', '')}"
value = json.dumps({
"avatar_id": auth.avatar_id,
"from_accid": message.get("from_accid", ""),
"content": message.get("content", ""),
"owner_huihui_id": auth.owner_id if hasattr(auth, "owner_id") else "",
"owner_huihui_id": owner_huihui_id,
})
self.redis.setex(key, auth.takeover_delay_seconds + 10, value)
logger.info(f"Message enqueued to delayed queue: {key}")
@@ -104,8 +116,31 @@ class TakeoverService:
async def process_delayed_queue(self):
"""Process expired messages from the delayed queue.
Redis handles expiration automatically; this is a placeholder
for a future scheduled worker that scans and dispatches.
Scans Redis keys matching the takeover:delayed: pattern and dispatches
each to execute_takeover after resolving the Authorization.
"""
if not self.redis:
return
try:
pattern = "takeover:delayed:*"
keys = self.redis.keys(pattern)
for key in keys:
raw = self.redis.get(key)
if not raw:
continue
data = json.loads(raw)
auth = (
self.db.query(Authorization)
.filter(Authorization.target_id == key.split(":")[2])
.first()
)
if auth:
message = {
"msg_id": key.split(":")[-1],
"from_accid": data.get("from_accid", ""),
"content": data.get("content", ""),
}
await self.execute_takeover(auth, message)
self.redis.delete(key)
except Exception as e:
logger.error(f"Failed to process delayed queue: {e}")