Files
Code_of_Conquest/api/app/api/chat.py

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)