8b17c17137
Implements direct-message (DM) chat send and history endpoints:
- Migration 0011_add_dm_last_read.py (last_read_at on conversation_participants)
- Models updated in backend/app/models/chat.py
- Router in backend/app/routers/chat_dm.py:
- POST /api/chat/dm/{user_id}/messages (send DM, filtered via TextFilter, rate-limited via Redis)
- GET /api/chat/dm/{user_id}/messages (paginated history, updates last_read_at)
- GET /api/chat/dm/conversations (conversations list, last message, unread count)
- Blocks enforced in both directions (cannot send to/from blocked users)
- Self-messaging prevented, peer exist check, auth required
- Integration tests in backend/tests/test_chat_dm.py
Independently re-verified: 14/14 new DM chat tests pass,
272/272 full backend regression suite passes with zero regressions.
37 lines
1.3 KiB
Python
37 lines
1.3 KiB
Python
"""add last_read_at to conversation_participants (DM unread tracking)
|
|
|
|
Revision ID: 0011_add_dm_last_read
|
|
Revises: 0010_add_profile_tables
|
|
Create Date: 2026-09-15 00:00:00.000000
|
|
|
|
app/routers/chat_dm.py (DM send/list/conversations endpoints). Unread-count
|
|
tracking is implemented as a last-read timestamp per participant: whenever a
|
|
user fetches a conversation's message history, their
|
|
``conversation_participants`` row's ``last_read_at`` is bumped to "now", and
|
|
the conversation-list endpoint counts messages with
|
|
``created_at > last_read_at`` (or all messages if never read) as unread for
|
|
that participant. Nullable/no default so existing rows (none yet in
|
|
practice, but consistent with "never read") read as "everything unread".
|
|
"""
|
|
from typing import Sequence, Union
|
|
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
|
|
# revision identifiers, used by Alembic.
|
|
revision: str = "0011_add_dm_last_read"
|
|
down_revision: Union[str, None] = "0010_add_profile_tables"
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
depends_on: Union[str, Sequence[str], None] = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
op.add_column(
|
|
"conversation_participants",
|
|
sa.Column("last_read_at", sa.DateTime(timezone=True), nullable=True),
|
|
)
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.drop_column("conversation_participants", "last_read_at")
|