321 lines
11 KiB
Python
321 lines
11 KiB
Python
"""
|
|
Chat API Endpoints.
|
|
|
|
Provides REST API for accessing player-NPC conversation history,
|
|
searching messages, and managing chat data.
|
|
"""
|
|
|
|
from flask import Blueprint, request
|
|
from datetime import datetime, timezone
|
|
|
|
from app.utils.auth import require_auth, get_current_user
|
|
from app.services.chat_message_service import (
|
|
get_chat_message_service,
|
|
ChatMessageNotFound,
|
|
ChatMessagePermissionDenied
|
|
)
|
|
from app.services.npc_loader import get_npc_loader
|
|
from app.models.chat_message import MessageContext
|
|
from app.utils.response import (
|
|
success_response,
|
|
error_response,
|
|
not_found_response,
|
|
validation_error_response
|
|
)
|
|
from app.utils.logging import get_logger
|
|
|
|
logger = get_logger(__file__)
|
|
|
|
chat_bp = Blueprint('chat', __name__)
|
|
|
|
|
|
@chat_bp.route('/api/v1/characters/<character_id>/chats', methods=['GET'])
|
|
@require_auth
|
|
def get_conversations_summary(character_id: str):
|
|
"""
|
|
Get summary of all NPC conversations for a character.
|
|
|
|
Returns list of NPCs the character has talked to, with message counts,
|
|
last message timestamp, and preview of most recent exchange.
|
|
|
|
Path params:
|
|
character_id: Character ID
|
|
|
|
Query params:
|
|
None
|
|
|
|
Returns:
|
|
JSON response with list of conversation summaries
|
|
"""
|
|
try:
|
|
user = get_current_user()
|
|
|
|
# Get conversation summaries
|
|
chat_service = get_chat_message_service()
|
|
summaries = chat_service.get_all_conversations_summary(character_id, user.id)
|
|
|
|
# Convert to dict for JSON response
|
|
conversations = [summary.to_dict() for summary in summaries]
|
|
|
|
logger.info("Retrieved conversation summaries",
|
|
user_id=user.id,
|
|
character_id=character_id,
|
|
count=len(conversations))
|
|
|
|
return success_response({"conversations": conversations})
|
|
|
|
except ChatMessagePermissionDenied as e:
|
|
logger.warning("Permission denied getting conversations",
|
|
user_id=user.id if 'user' in locals() else None,
|
|
character_id=character_id)
|
|
return error_response(str(e), 403)
|
|
except Exception as e:
|
|
logger.error("Failed to get conversations summary",
|
|
character_id=character_id,
|
|
error=str(e))
|
|
return error_response(f"Failed to retrieve conversations: {str(e)}", 500)
|
|
|
|
|
|
@chat_bp.route('/api/v1/characters/<character_id>/chats/<npc_id>', methods=['GET'])
|
|
@require_auth
|
|
def get_conversation_history(character_id: str, npc_id: str):
|
|
"""
|
|
Get full conversation history between character and specific NPC.
|
|
|
|
Returns paginated list of messages ordered by timestamp DESC (most recent first).
|
|
|
|
Path params:
|
|
character_id: Character ID
|
|
npc_id: NPC ID
|
|
|
|
Query params:
|
|
limit: Maximum messages to return (default 50, max 100)
|
|
offset: Number of messages to skip (default 0)
|
|
|
|
Returns:
|
|
JSON response with messages and pagination info
|
|
"""
|
|
try:
|
|
user = get_current_user()
|
|
|
|
# Get query parameters
|
|
limit = request.args.get('limit', 50, type=int)
|
|
offset = request.args.get('offset', 0, type=int)
|
|
|
|
# Validate parameters
|
|
if limit < 1:
|
|
return validation_error_response("limit must be at least 1")
|
|
if offset < 0:
|
|
return validation_error_response("offset must be at least 0")
|
|
|
|
# Get conversation history
|
|
chat_service = get_chat_message_service()
|
|
messages = chat_service.get_conversation_history(
|
|
character_id=character_id,
|
|
user_id=user.id,
|
|
npc_id=npc_id,
|
|
limit=limit,
|
|
offset=offset
|
|
)
|
|
|
|
# Get NPC name if available
|
|
npc_loader = get_npc_loader()
|
|
npc = npc_loader.load_npc(npc_id)
|
|
npc_name = npc.name if npc else npc_id.replace('_', ' ').title()
|
|
|
|
# Convert messages to dict
|
|
message_dicts = [msg.to_dict() for msg in messages]
|
|
|
|
result = {
|
|
"npc_id": npc_id,
|
|
"npc_name": npc_name,
|
|
"total_messages": len(messages), # Count in this batch
|
|
"messages": message_dicts,
|
|
"pagination": {
|
|
"limit": limit,
|
|
"offset": offset,
|
|
"has_more": len(messages) == limit # If we got a full batch, there might be more
|
|
}
|
|
}
|
|
|
|
logger.info("Retrieved conversation history",
|
|
user_id=user.id,
|
|
character_id=character_id,
|
|
npc_id=npc_id,
|
|
count=len(messages))
|
|
|
|
return success_response(result)
|
|
|
|
except ChatMessagePermissionDenied as e:
|
|
logger.warning("Permission denied getting conversation",
|
|
user_id=user.id if 'user' in locals() else None,
|
|
character_id=character_id,
|
|
npc_id=npc_id)
|
|
return error_response(str(e), 403)
|
|
except Exception as e:
|
|
logger.error("Failed to get conversation history",
|
|
character_id=character_id,
|
|
npc_id=npc_id,
|
|
error=str(e))
|
|
return error_response(f"Failed to retrieve conversation: {str(e)}", 500)
|
|
|
|
|
|
@chat_bp.route('/api/v1/characters/<character_id>/chats/search', methods=['GET'])
|
|
@require_auth
|
|
def search_messages(character_id: str):
|
|
"""
|
|
Search messages by text with optional filters.
|
|
|
|
Query params:
|
|
q (required): Search text to find in player_message and npc_response
|
|
npc_id (optional): Filter by specific NPC
|
|
context (optional): Filter by message context (dialogue, quest_offered, shop, etc.)
|
|
date_from (optional): Start date ISO format (e.g., 2025-11-25T00:00:00Z)
|
|
date_to (optional): End date ISO format
|
|
limit (optional): Maximum messages to return (default 50, max 100)
|
|
offset (optional): Number of messages to skip (default 0)
|
|
|
|
Returns:
|
|
JSON response with matching messages
|
|
"""
|
|
try:
|
|
user = get_current_user()
|
|
|
|
# Get query parameters
|
|
search_text = request.args.get('q')
|
|
npc_id = request.args.get('npc_id')
|
|
context_str = request.args.get('context')
|
|
date_from = request.args.get('date_from')
|
|
date_to = request.args.get('date_to')
|
|
limit = request.args.get('limit', 50, type=int)
|
|
offset = request.args.get('offset', 0, type=int)
|
|
|
|
# Validate required parameters
|
|
if not search_text:
|
|
return validation_error_response("q (search text) is required")
|
|
|
|
# Validate optional parameters
|
|
if limit < 1:
|
|
return validation_error_response("limit must be at least 1")
|
|
if offset < 0:
|
|
return validation_error_response("offset must be at least 0")
|
|
|
|
# Parse context enum if provided
|
|
context = None
|
|
if context_str:
|
|
try:
|
|
context = MessageContext(context_str)
|
|
except ValueError:
|
|
valid_contexts = [c.value for c in MessageContext]
|
|
return validation_error_response(
|
|
f"Invalid context. Must be one of: {', '.join(valid_contexts)}"
|
|
)
|
|
|
|
# Search messages
|
|
chat_service = get_chat_message_service()
|
|
messages = chat_service.search_messages(
|
|
character_id=character_id,
|
|
user_id=user.id,
|
|
search_text=search_text,
|
|
npc_id=npc_id,
|
|
context=context,
|
|
date_from=date_from,
|
|
date_to=date_to,
|
|
limit=limit,
|
|
offset=offset
|
|
)
|
|
|
|
# Convert messages to dict
|
|
message_dicts = [msg.to_dict() for msg in messages]
|
|
|
|
result = {
|
|
"search_text": search_text,
|
|
"filters": {
|
|
"npc_id": npc_id,
|
|
"context": context_str,
|
|
"date_from": date_from,
|
|
"date_to": date_to
|
|
},
|
|
"total_results": len(messages),
|
|
"messages": message_dicts,
|
|
"pagination": {
|
|
"limit": limit,
|
|
"offset": offset,
|
|
"has_more": len(messages) == limit
|
|
}
|
|
}
|
|
|
|
logger.info("Search completed",
|
|
user_id=user.id,
|
|
character_id=character_id,
|
|
search_text=search_text,
|
|
results=len(messages))
|
|
|
|
return success_response(result)
|
|
|
|
except ChatMessagePermissionDenied as e:
|
|
logger.warning("Permission denied searching messages",
|
|
user_id=user.id if 'user' in locals() else None,
|
|
character_id=character_id)
|
|
return error_response(str(e), 403)
|
|
except Exception as e:
|
|
logger.error("Failed to search messages",
|
|
character_id=character_id,
|
|
error=str(e))
|
|
return error_response(f"Search failed: {str(e)}", 500)
|
|
|
|
|
|
@chat_bp.route('/api/v1/characters/<character_id>/chats/<message_id>', methods=['DELETE'])
|
|
@require_auth
|
|
def delete_message(character_id: str, message_id: str):
|
|
"""
|
|
Soft delete a message (sets is_deleted=True).
|
|
|
|
Used for privacy/moderation. Message remains in database but filtered from queries.
|
|
|
|
Path params:
|
|
character_id: Character ID (for ownership validation)
|
|
message_id: Message ID to delete
|
|
|
|
Returns:
|
|
JSON response with success confirmation
|
|
"""
|
|
try:
|
|
user = get_current_user()
|
|
|
|
# Soft delete message
|
|
chat_service = get_chat_message_service()
|
|
success = chat_service.soft_delete_message(
|
|
message_id=message_id,
|
|
character_id=character_id,
|
|
user_id=user.id
|
|
)
|
|
|
|
logger.info("Message deleted",
|
|
user_id=user.id,
|
|
character_id=character_id,
|
|
message_id=message_id)
|
|
|
|
return success_response({
|
|
"message_id": message_id,
|
|
"deleted": success
|
|
})
|
|
|
|
except ChatMessageNotFound as e:
|
|
logger.warning("Message not found for deletion",
|
|
message_id=message_id,
|
|
character_id=character_id)
|
|
return not_found_response(str(e))
|
|
except ChatMessagePermissionDenied as e:
|
|
logger.warning("Permission denied deleting message",
|
|
user_id=user.id if 'user' in locals() else None,
|
|
character_id=character_id,
|
|
message_id=message_id)
|
|
return error_response(str(e), 403)
|
|
except Exception as e:
|
|
logger.error("Failed to delete message",
|
|
message_id=message_id,
|
|
character_id=character_id,
|
|
error=str(e))
|
|
return error_response(f"Delete failed: {str(e)}", 500)
|