Files
modbison/backend/alembic/versions/0011_add_dm_last_read.py
daddy 8b17c17137 Add DM chat send/list endpoints: filter, rate limit, conversations (US-4)
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.
2026-09-15 16:56:13 +00:00

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")