Add the ability to delete a message (#53)

Message.deleted_at has existed since the initial schema but was never
wired up -- no WS envelope, no permission check, no frontend concept of
it at all. Soft delete, author-only (mirrors the existing edit
permission exactly): content and any attached image/file are cleared
and the underlying MessageImage/MessageFile row and stored file are
actually removed, not just detached, so the message becomes a "This
message was deleted" tombstone with nothing left to recover through a
stale attachment URL. A deleted message can no longer be edited or
reacted to.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-28 16:35:00 -06:00
co-authored by Claude Sonnet 5
parent 157f1e30ac
commit ef615e1ef4
13 changed files with 477 additions and 8 deletions
@@ -0,0 +1,39 @@
"""allow deleted messages to clear content and attachments
Revision ID: e2fc4d65f93e
Revises: f3f255da9c96
Create Date: 2026-08-28 16:20:02.114630
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'e2fc4d65f93e'
down_revision: Union[str, Sequence[str], None] = 'f3f255da9c96'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
op.drop_constraint('messages_content_or_attachment_required', 'messages', type_='check')
op.create_check_constraint(
'messages_content_or_attachment_required',
'messages',
'content IS NOT NULL OR image_id IS NOT NULL OR file_id IS NOT NULL '
'OR deleted_at IS NOT NULL',
)
def downgrade() -> None:
"""Downgrade schema."""
op.drop_constraint('messages_content_or_attachment_required', 'messages', type_='check')
op.create_check_constraint(
'messages_content_or_attachment_required',
'messages',
'content IS NOT NULL OR image_id IS NOT NULL OR file_id IS NOT NULL',
)