Add generic file attachments to chat messages

Messages can now carry an arbitrary file (MessageFile), parallel to the
existing MessageImage feature rather than a refactor of it. Files serve
with Content-Disposition: attachment to force a download and prevent an
uploaded HTML/SVG from executing same-origin. No content-type allowlist,
same 8MB cap as images for now (a separate size-limit redesign is tracked
as its own issue).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 08:02:55 -06:00
co-authored by Claude Sonnet 5
parent 89be497fdb
commit c78d7454b6
21 changed files with 697 additions and 55 deletions
+5 -3
View File
@@ -11,8 +11,8 @@ class Message(Base):
__tablename__ = "messages"
__table_args__ = (
CheckConstraint(
"content IS NOT NULL OR image_id IS NOT NULL",
name="messages_content_or_image_required",
"content IS NOT NULL OR image_id IS NOT NULL OR file_id IS NOT NULL",
name="messages_content_or_attachment_required",
),
)
@@ -21,9 +21,10 @@ class Message(Base):
user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"), nullable=False)
# Nullable since Phase "image uploads": a message can be an image with
# no caption. The CheckConstraint above still requires at least one of
# content/image_id.
# content/image_id/file_id.
content: Mapped[str | None] = mapped_column(Text)
image_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("message_images.id"))
file_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("message_files.id"))
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), index=True, nullable=False
)
@@ -32,3 +33,4 @@ class Message(Base):
user = relationship("User")
image = relationship("MessageImage")
file = relationship("MessageFile")