feat: add message reliability foundation
All checks were successful
CI / test (push) Successful in 23s
All checks were successful
CI / test (push) Successful in 23s
- implement idempotent message creation via client_message_id - add persistent delivered/read receipts - expose /messages/status and wire websocket receipt events - update web client to send client ids and auto-ack delivered/read
This commit is contained in:
@@ -24,6 +24,7 @@ class SendMessagePayload(BaseModel):
|
||||
type: MessageType = MessageType.TEXT
|
||||
text: str | None = Field(default=None, max_length=4096)
|
||||
temp_id: str | None = None
|
||||
client_message_id: str | None = Field(default=None, min_length=8, max_length=64)
|
||||
|
||||
|
||||
class ChatEventPayload(BaseModel):
|
||||
|
||||
@@ -9,8 +9,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.chats.repository import list_user_chat_ids
|
||||
from app.chats.service import ensure_chat_membership
|
||||
from app.messages.schemas import MessageCreateRequest, MessageRead
|
||||
from app.messages.service import create_chat_message
|
||||
from app.messages.schemas import MessageCreateRequest, MessageRead, MessageStatusUpdateRequest
|
||||
from app.messages.service import create_chat_message, mark_message_status
|
||||
from app.realtime.models import ConnectionContext
|
||||
from app.realtime.presence import mark_user_offline, mark_user_online
|
||||
from app.realtime.repository import RedisRealtimeRepository
|
||||
@@ -81,9 +81,19 @@ class RealtimeGateway:
|
||||
message = await create_chat_message(
|
||||
db,
|
||||
sender_id=user_id,
|
||||
payload=MessageCreateRequest(chat_id=payload.chat_id, type=payload.type, text=payload.text),
|
||||
payload=MessageCreateRequest(
|
||||
chat_id=payload.chat_id,
|
||||
type=payload.type,
|
||||
text=payload.text,
|
||||
client_message_id=payload.client_message_id or payload.temp_id,
|
||||
),
|
||||
)
|
||||
await self.publish_message_created(
|
||||
message=message,
|
||||
sender_id=user_id,
|
||||
temp_id=payload.temp_id,
|
||||
client_message_id=payload.client_message_id,
|
||||
)
|
||||
await self.publish_message_created(message=message, sender_id=user_id, temp_id=payload.temp_id)
|
||||
|
||||
async def handle_typing_event(self, db: AsyncSession, user_id: int, payload: ChatEventPayload, event: str) -> None:
|
||||
await ensure_chat_membership(db, chat_id=payload.chat_id, user_id=user_id)
|
||||
@@ -99,8 +109,12 @@ class RealtimeGateway:
|
||||
user_id: int,
|
||||
payload: MessageStatusPayload,
|
||||
event: str,
|
||||
) -> None:
|
||||
await ensure_chat_membership(db, chat_id=payload.chat_id, user_id=user_id)
|
||||
) -> dict[str, int]:
|
||||
receipt_state = await mark_message_status(
|
||||
db,
|
||||
user_id=user_id,
|
||||
payload=MessageStatusUpdateRequest(chat_id=payload.chat_id, message_id=payload.message_id, status=event),
|
||||
)
|
||||
await self._publish_chat_event(
|
||||
payload.chat_id,
|
||||
event=event,
|
||||
@@ -108,8 +122,11 @@ class RealtimeGateway:
|
||||
"chat_id": payload.chat_id,
|
||||
"message_id": payload.message_id,
|
||||
"user_id": user_id,
|
||||
"last_delivered_message_id": receipt_state["last_delivered_message_id"],
|
||||
"last_read_message_id": receipt_state["last_read_message_id"],
|
||||
},
|
||||
)
|
||||
return receipt_state
|
||||
|
||||
async def load_user_chat_ids(self, db: AsyncSession, user_id: int) -> list[int]:
|
||||
return await list_user_chat_ids(db, user_id=user_id)
|
||||
@@ -139,7 +156,14 @@ class RealtimeGateway:
|
||||
return
|
||||
await self._handle_redis_event(f"chat:{chat_id}", event_payload)
|
||||
|
||||
async def publish_message_created(self, *, message, sender_id: int, temp_id: str | None = None) -> None:
|
||||
async def publish_message_created(
|
||||
self,
|
||||
*,
|
||||
message,
|
||||
sender_id: int,
|
||||
temp_id: str | None = None,
|
||||
client_message_id: str | None = None,
|
||||
) -> None:
|
||||
message_data = MessageRead.model_validate(message).model_dump(mode="json")
|
||||
await self._publish_chat_event(
|
||||
message.chat_id,
|
||||
@@ -148,6 +172,7 @@ class RealtimeGateway:
|
||||
"chat_id": message.chat_id,
|
||||
"message": message_data,
|
||||
"temp_id": temp_id,
|
||||
"client_message_id": client_message_id,
|
||||
"sender_id": sender_id,
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user