feat: Initial commit - Task Reporter incident response system
Complete implementation of the production line incident response system (生產線異常即時反應系統) including: Backend (FastAPI): - User authentication with AD integration and session management - Chat room management (create, list, update, members, roles) - Real-time messaging via WebSocket (typing indicators, reactions) - File storage with MinIO (upload, download, image preview) Frontend (React + Vite): - Authentication flow with token management - Room list with filtering, search, and pagination - Real-time chat interface with WebSocket - File upload with drag-and-drop and image preview - Member management and room settings - Breadcrumb navigation - 53 unit tests (Vitest) Specifications: - authentication: AD auth, sessions, JWT tokens - chat-room: rooms, members, templates - realtime-messaging: WebSocket, messages, reactions - file-storage: MinIO integration, file management - frontend-core: React SPA structure 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
262
app/modules/realtime/schemas.py
Normal file
262
app/modules/realtime/schemas.py
Normal file
@@ -0,0 +1,262 @@
|
||||
"""Pydantic schemas for WebSocket messages and REST API"""
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, Dict, Any, List
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class MessageTypeEnum(str, Enum):
|
||||
"""Message type enumeration for validation"""
|
||||
TEXT = "text"
|
||||
IMAGE_REF = "image_ref"
|
||||
FILE_REF = "file_ref"
|
||||
SYSTEM = "system"
|
||||
INCIDENT_DATA = "incident_data"
|
||||
|
||||
|
||||
class WebSocketMessageType(str, Enum):
|
||||
"""WebSocket message type for protocol"""
|
||||
MESSAGE = "message"
|
||||
EDIT_MESSAGE = "edit_message"
|
||||
DELETE_MESSAGE = "delete_message"
|
||||
ADD_REACTION = "add_reaction"
|
||||
REMOVE_REACTION = "remove_reaction"
|
||||
TYPING = "typing"
|
||||
SYSTEM = "system"
|
||||
|
||||
|
||||
class SystemEventType(str, Enum):
|
||||
"""System event types"""
|
||||
USER_JOINED = "user_joined"
|
||||
USER_LEFT = "user_left"
|
||||
ROOM_STATUS_CHANGED = "room_status_changed"
|
||||
MEMBER_ADDED = "member_added"
|
||||
MEMBER_REMOVED = "member_removed"
|
||||
FILE_UPLOADED = "file_uploaded"
|
||||
FILE_DELETED = "file_deleted"
|
||||
|
||||
|
||||
# WebSocket Incoming Messages (from client)
|
||||
class WebSocketMessageIn(BaseModel):
|
||||
"""Incoming WebSocket message from client"""
|
||||
type: WebSocketMessageType
|
||||
content: Optional[str] = None
|
||||
message_type: Optional[MessageTypeEnum] = MessageTypeEnum.TEXT
|
||||
message_id: Optional[str] = None # For edit/delete/reaction operations
|
||||
emoji: Optional[str] = None # For reactions
|
||||
metadata: Optional[Dict[str, Any]] = None # For mentions, file refs, etc.
|
||||
|
||||
|
||||
class TextMessageIn(BaseModel):
|
||||
"""Text message input"""
|
||||
content: str = Field(..., min_length=1, max_length=10000)
|
||||
mentions: Optional[List[str]] = None
|
||||
|
||||
|
||||
class ImageRefMessageIn(BaseModel):
|
||||
"""Image reference message input"""
|
||||
content: str # Description
|
||||
file_id: str
|
||||
file_url: str
|
||||
|
||||
|
||||
class FileRefMessageIn(BaseModel):
|
||||
"""File reference message input"""
|
||||
content: str # Description
|
||||
file_id: str
|
||||
file_url: str
|
||||
file_name: str
|
||||
|
||||
|
||||
class IncidentDataMessageIn(BaseModel):
|
||||
"""Structured incident data message input"""
|
||||
content: Dict[str, Any] # Structured data (temperature, pressure, etc.)
|
||||
|
||||
|
||||
# WebSocket Outgoing Messages (to client)
|
||||
class MessageBroadcast(BaseModel):
|
||||
"""Message broadcast to all room members"""
|
||||
type: str = "message"
|
||||
message_id: str
|
||||
room_id: str
|
||||
sender_id: str
|
||||
content: str
|
||||
message_type: MessageTypeEnum
|
||||
metadata: Optional[Dict[str, Any]] = None
|
||||
created_at: datetime
|
||||
edited_at: Optional[datetime] = None
|
||||
deleted_at: Optional[datetime] = None
|
||||
sequence_number: int
|
||||
|
||||
|
||||
class SystemMessageBroadcast(BaseModel):
|
||||
"""System message broadcast"""
|
||||
type: str = "system"
|
||||
event: SystemEventType
|
||||
user_id: Optional[str] = None
|
||||
room_id: Optional[str] = None
|
||||
timestamp: datetime
|
||||
data: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class TypingBroadcast(BaseModel):
|
||||
"""Typing indicator broadcast"""
|
||||
type: str = "typing"
|
||||
room_id: str
|
||||
user_id: str
|
||||
is_typing: bool
|
||||
|
||||
|
||||
class MessageAck(BaseModel):
|
||||
"""Message acknowledgment"""
|
||||
type: str = "ack"
|
||||
message_id: str
|
||||
sequence_number: int
|
||||
timestamp: datetime
|
||||
|
||||
|
||||
class ErrorMessage(BaseModel):
|
||||
"""Error message"""
|
||||
type: str = "error"
|
||||
error: str
|
||||
code: str
|
||||
details: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
# REST API Schemas
|
||||
class MessageCreate(BaseModel):
|
||||
"""Create message via REST API"""
|
||||
content: str = Field(..., min_length=1, max_length=10000)
|
||||
message_type: MessageTypeEnum = MessageTypeEnum.TEXT
|
||||
metadata: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class MessageUpdate(BaseModel):
|
||||
"""Update message content"""
|
||||
content: str = Field(..., min_length=1, max_length=10000)
|
||||
|
||||
|
||||
class MessageResponse(BaseModel):
|
||||
"""Message response"""
|
||||
message_id: str
|
||||
room_id: str
|
||||
sender_id: str
|
||||
content: str
|
||||
message_type: MessageTypeEnum
|
||||
metadata: Optional[Dict[str, Any]] = Field(None, alias="message_metadata")
|
||||
created_at: datetime
|
||||
edited_at: Optional[datetime] = None
|
||||
deleted_at: Optional[datetime] = None
|
||||
sequence_number: int
|
||||
reaction_counts: Optional[Dict[str, int]] = None # emoji -> count
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
populate_by_name = True # Allow both 'metadata' and 'message_metadata'
|
||||
|
||||
|
||||
class MessageListResponse(BaseModel):
|
||||
"""Paginated message list response"""
|
||||
messages: List[MessageResponse]
|
||||
total: int
|
||||
limit: int
|
||||
offset: int
|
||||
has_more: bool
|
||||
|
||||
|
||||
class ReactionCreate(BaseModel):
|
||||
"""Add reaction to message"""
|
||||
emoji: str = Field(..., min_length=1, max_length=10)
|
||||
|
||||
|
||||
class ReactionResponse(BaseModel):
|
||||
"""Reaction response"""
|
||||
reaction_id: int
|
||||
message_id: str
|
||||
user_id: str
|
||||
emoji: str
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class ReactionSummary(BaseModel):
|
||||
"""Reaction summary for a message"""
|
||||
emoji: str
|
||||
count: int
|
||||
users: List[str] # List of user IDs who reacted
|
||||
|
||||
|
||||
class OnlineUser(BaseModel):
|
||||
"""Online user in a room"""
|
||||
user_id: str
|
||||
connected_at: datetime
|
||||
|
||||
|
||||
# File Upload WebSocket Schemas
|
||||
class FileUploadedBroadcast(BaseModel):
|
||||
"""Broadcast when a file is uploaded to a room"""
|
||||
type: str = "file_uploaded"
|
||||
file_id: str
|
||||
room_id: str
|
||||
uploader_id: str
|
||||
filename: str
|
||||
file_type: str # image, document, log
|
||||
file_size: int
|
||||
mime_type: str
|
||||
download_url: Optional[str] = None
|
||||
uploaded_at: datetime
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert to dictionary for WebSocket broadcast"""
|
||||
return {
|
||||
"type": self.type,
|
||||
"file_id": self.file_id,
|
||||
"room_id": self.room_id,
|
||||
"uploader_id": self.uploader_id,
|
||||
"filename": self.filename,
|
||||
"file_type": self.file_type,
|
||||
"file_size": self.file_size,
|
||||
"mime_type": self.mime_type,
|
||||
"download_url": self.download_url,
|
||||
"uploaded_at": self.uploaded_at.isoformat()
|
||||
}
|
||||
|
||||
|
||||
class FileUploadAck(BaseModel):
|
||||
"""Acknowledgment sent to uploader after successful upload"""
|
||||
type: str = "file_upload_ack"
|
||||
file_id: str
|
||||
status: str # success, error
|
||||
download_url: Optional[str] = None
|
||||
error_message: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert to dictionary for WebSocket message"""
|
||||
return {
|
||||
"type": self.type,
|
||||
"file_id": self.file_id,
|
||||
"status": self.status,
|
||||
"download_url": self.download_url,
|
||||
"error_message": self.error_message
|
||||
}
|
||||
|
||||
|
||||
class FileDeletedBroadcast(BaseModel):
|
||||
"""Broadcast when a file is deleted from a room"""
|
||||
type: str = "file_deleted"
|
||||
file_id: str
|
||||
room_id: str
|
||||
deleted_by: str
|
||||
deleted_at: datetime
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert to dictionary for WebSocket broadcast"""
|
||||
return {
|
||||
"type": self.type,
|
||||
"file_id": self.file_id,
|
||||
"room_id": self.room_id,
|
||||
"deleted_by": self.deleted_by,
|
||||
"deleted_at": self.deleted_at.isoformat()
|
||||
}
|
||||
Reference in New Issue
Block a user