From 1a88ce72598c02b166c5478d515067b9da14ccb8 Mon Sep 17 00:00:00 2001 From: Thomas Sileo Date: Thu, 23 Jun 2022 21:07:20 +0200 Subject: [PATCH] Attachments support for the outbox --- ...n.py => 714b4a5307c7_initial_migration.py} | 86 +++++++----- app/admin.py | 10 +- app/ap_object.py | 31 ++++- app/boxes.py | 20 ++- app/main.py | 122 ++++++++++++++++++ app/models.py | 77 ++++++++--- app/templates.py | 4 +- app/templates/utils.html | 7 +- app/uploads.py | 100 ++++++++++++++ poetry.lock | 95 +++++++++++++- pyproject.toml | 3 + 11 files changed, 497 insertions(+), 58 deletions(-) rename alembic/versions/{b122c3a69fc9_initial_migration.py => 714b4a5307c7_initial_migration.py} (72%) create mode 100644 app/uploads.py diff --git a/alembic/versions/b122c3a69fc9_initial_migration.py b/alembic/versions/714b4a5307c7_initial_migration.py similarity index 72% rename from alembic/versions/b122c3a69fc9_initial_migration.py rename to alembic/versions/714b4a5307c7_initial_migration.py index 1a590aa..ff6f866 100644 --- a/alembic/versions/b122c3a69fc9_initial_migration.py +++ b/alembic/versions/714b4a5307c7_initial_migration.py @@ -1,8 +1,8 @@ """Initial migration -Revision ID: b122c3a69fc9 +Revision ID: 714b4a5307c7 Revises: -Create Date: 2022-06-22 19:54:19.153320 +Create Date: 2022-06-23 18:42:56.009810 """ import sqlalchemy as sa @@ -10,7 +10,7 @@ import sqlalchemy as sa from alembic import op # revision identifiers, used by Alembic. -revision = 'b122c3a69fc9' +revision = '714b4a5307c7' down_revision = None branch_labels = None depends_on = None @@ -18,7 +18,7 @@ depends_on = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.create_table('actors', + op.create_table('actor', sa.Column('id', sa.Integer(), nullable=False), sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), @@ -28,9 +28,9 @@ def upgrade() -> None: sa.Column('handle', sa.String(), nullable=True), sa.PrimaryKeyConstraint('id') ) - op.create_index(op.f('ix_actors_ap_id'), 'actors', ['ap_id'], unique=True) - op.create_index(op.f('ix_actors_handle'), 'actors', ['handle'], unique=False) - op.create_index(op.f('ix_actors_id'), 'actors', ['id'], unique=False) + op.create_index(op.f('ix_actor_ap_id'), 'actor', ['ap_id'], unique=True) + op.create_index(op.f('ix_actor_handle'), 'actor', ['handle'], unique=False) + op.create_index(op.f('ix_actor_id'), 'actor', ['id'], unique=False) op.create_table('inbox', sa.Column('id', sa.Integer(), nullable=False), sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), @@ -54,7 +54,7 @@ def upgrade() -> None: sa.Column('is_bookmarked', sa.Boolean(), nullable=False), sa.Column('has_replies', sa.Boolean(), nullable=False), sa.Column('og_meta', sa.JSON(), nullable=True), - sa.ForeignKeyConstraint(['actor_id'], ['actors.id'], ), + sa.ForeignKeyConstraint(['actor_id'], ['actor.id'], ), sa.ForeignKeyConstraint(['relates_to_inbox_object_id'], ['inbox.id'], ), sa.ForeignKeyConstraint(['relates_to_outbox_object_id'], ['outbox.id'], ), sa.ForeignKeyConstraint(['undone_by_inbox_object_id'], ['inbox.id'], ), @@ -93,20 +93,33 @@ def upgrade() -> None: op.create_index(op.f('ix_outbox_ap_id'), 'outbox', ['ap_id'], unique=True) op.create_index(op.f('ix_outbox_id'), 'outbox', ['id'], unique=False) op.create_index(op.f('ix_outbox_public_id'), 'outbox', ['public_id'], unique=False) - op.create_table('followers', + op.create_table('upload', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('content_type', sa.String(), nullable=False), + sa.Column('content_hash', sa.String(), nullable=False), + sa.Column('has_thumbnail', sa.Boolean(), nullable=False), + sa.Column('blurhash', sa.String(), nullable=True), + sa.Column('width', sa.Integer(), nullable=True), + sa.Column('height', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('content_hash') + ) + op.create_index(op.f('ix_upload_id'), 'upload', ['id'], unique=False) + op.create_table('follower', sa.Column('id', sa.Integer(), nullable=False), sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), sa.Column('actor_id', sa.Integer(), nullable=False), sa.Column('inbox_object_id', sa.Integer(), nullable=False), sa.Column('ap_actor_id', sa.String(), nullable=False), - sa.ForeignKeyConstraint(['actor_id'], ['actors.id'], ), + sa.ForeignKeyConstraint(['actor_id'], ['actor.id'], ), sa.ForeignKeyConstraint(['inbox_object_id'], ['inbox.id'], ), sa.PrimaryKeyConstraint('id'), sa.UniqueConstraint('actor_id'), sa.UniqueConstraint('ap_actor_id') ) - op.create_index(op.f('ix_followers_id'), 'followers', ['id'], unique=False) + op.create_index(op.f('ix_follower_id'), 'follower', ['id'], unique=False) op.create_table('following', sa.Column('id', sa.Integer(), nullable=False), sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), @@ -114,7 +127,7 @@ def upgrade() -> None: sa.Column('actor_id', sa.Integer(), nullable=False), sa.Column('outbox_object_id', sa.Integer(), nullable=False), sa.Column('ap_actor_id', sa.String(), nullable=False), - sa.ForeignKeyConstraint(['actor_id'], ['actors.id'], ), + sa.ForeignKeyConstraint(['actor_id'], ['actor.id'], ), sa.ForeignKeyConstraint(['outbox_object_id'], ['outbox.id'], ), sa.PrimaryKeyConstraint('id'), sa.UniqueConstraint('actor_id'), @@ -129,13 +142,24 @@ def upgrade() -> None: sa.Column('actor_id', sa.Integer(), nullable=True), sa.Column('outbox_object_id', sa.Integer(), nullable=True), sa.Column('inbox_object_id', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['actor_id'], ['actors.id'], ), + sa.ForeignKeyConstraint(['actor_id'], ['actor.id'], ), sa.ForeignKeyConstraint(['inbox_object_id'], ['inbox.id'], ), sa.ForeignKeyConstraint(['outbox_object_id'], ['outbox.id'], ), sa.PrimaryKeyConstraint('id') ) op.create_index(op.f('ix_notifications_id'), 'notifications', ['id'], unique=False) - op.create_table('outgoing_activities', + op.create_table('outbox_object_attachment', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('filename', sa.String(), nullable=False), + sa.Column('outbox_object_id', sa.Integer(), nullable=False), + sa.Column('upload_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['outbox_object_id'], ['outbox.id'], ), + sa.ForeignKeyConstraint(['upload_id'], ['upload.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_outbox_object_attachment_id'), 'outbox_object_attachment', ['id'], unique=False) + op.create_table('outgoing_activity', sa.Column('id', sa.Integer(), nullable=False), sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), sa.Column('recipient', sa.String(), nullable=False), @@ -151,8 +175,8 @@ def upgrade() -> None: sa.ForeignKeyConstraint(['outbox_object_id'], ['outbox.id'], ), sa.PrimaryKeyConstraint('id') ) - op.create_index(op.f('ix_outgoing_activities_id'), 'outgoing_activities', ['id'], unique=False) - op.create_table('tagged_outbox_objects', + op.create_index(op.f('ix_outgoing_activity_id'), 'outgoing_activity', ['id'], unique=False) + op.create_table('tagged_outbox_object', sa.Column('id', sa.Integer(), nullable=False), sa.Column('outbox_object_id', sa.Integer(), nullable=False), sa.Column('tag', sa.String(), nullable=False), @@ -160,24 +184,28 @@ def upgrade() -> None: sa.PrimaryKeyConstraint('id'), sa.UniqueConstraint('outbox_object_id', 'tag', name='uix_tagged_object') ) - op.create_index(op.f('ix_tagged_outbox_objects_id'), 'tagged_outbox_objects', ['id'], unique=False) - op.create_index(op.f('ix_tagged_outbox_objects_tag'), 'tagged_outbox_objects', ['tag'], unique=False) + op.create_index(op.f('ix_tagged_outbox_object_id'), 'tagged_outbox_object', ['id'], unique=False) + op.create_index(op.f('ix_tagged_outbox_object_tag'), 'tagged_outbox_object', ['tag'], unique=False) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.drop_index(op.f('ix_tagged_outbox_objects_tag'), table_name='tagged_outbox_objects') - op.drop_index(op.f('ix_tagged_outbox_objects_id'), table_name='tagged_outbox_objects') - op.drop_table('tagged_outbox_objects') - op.drop_index(op.f('ix_outgoing_activities_id'), table_name='outgoing_activities') - op.drop_table('outgoing_activities') + op.drop_index(op.f('ix_tagged_outbox_object_tag'), table_name='tagged_outbox_object') + op.drop_index(op.f('ix_tagged_outbox_object_id'), table_name='tagged_outbox_object') + op.drop_table('tagged_outbox_object') + op.drop_index(op.f('ix_outgoing_activity_id'), table_name='outgoing_activity') + op.drop_table('outgoing_activity') + op.drop_index(op.f('ix_outbox_object_attachment_id'), table_name='outbox_object_attachment') + op.drop_table('outbox_object_attachment') op.drop_index(op.f('ix_notifications_id'), table_name='notifications') op.drop_table('notifications') op.drop_index(op.f('ix_following_id'), table_name='following') op.drop_table('following') - op.drop_index(op.f('ix_followers_id'), table_name='followers') - op.drop_table('followers') + op.drop_index(op.f('ix_follower_id'), table_name='follower') + op.drop_table('follower') + op.drop_index(op.f('ix_upload_id'), table_name='upload') + op.drop_table('upload') op.drop_index(op.f('ix_outbox_public_id'), table_name='outbox') op.drop_index(op.f('ix_outbox_id'), table_name='outbox') op.drop_index(op.f('ix_outbox_ap_id'), table_name='outbox') @@ -185,8 +213,8 @@ def downgrade() -> None: op.drop_index(op.f('ix_inbox_id'), table_name='inbox') op.drop_index(op.f('ix_inbox_ap_id'), table_name='inbox') op.drop_table('inbox') - op.drop_index(op.f('ix_actors_id'), table_name='actors') - op.drop_index(op.f('ix_actors_handle'), table_name='actors') - op.drop_index(op.f('ix_actors_ap_id'), table_name='actors') - op.drop_table('actors') + op.drop_index(op.f('ix_actor_id'), table_name='actor') + op.drop_index(op.f('ix_actor_handle'), table_name='actor') + op.drop_index(op.f('ix_actor_ap_id'), table_name='actor') + op.drop_table('actor') # ### end Alembic commands ### diff --git a/app/admin.py b/app/admin.py index 394a774..b2ef190 100644 --- a/app/admin.py +++ b/app/admin.py @@ -22,6 +22,7 @@ from app.config import verify_csrf_token from app.config import verify_password from app.database import get_db from app.lookup import lookup +from app.uploads import save_upload def user_session_or_redirect( @@ -231,7 +232,7 @@ def admin_actions_bookmark( @router.post("/actions/new") -async def admin_actions_new( +def admin_actions_new( request: Request, files: list[UploadFile], content: str = Form(), @@ -240,9 +241,12 @@ async def admin_actions_new( db: Session = Depends(get_db), ) -> RedirectResponse: # XXX: for some reason, no files restuls in an empty single file + uploads = [] if len(files) >= 1 and files[0].filename: - print("Got files") - public_id = boxes.send_create(db, content) + for f in files: + upload = save_upload(db, f) + uploads.append((upload, f.filename)) + public_id = boxes.send_create(db, source=content, uploads=uploads) return RedirectResponse( request.url_for("outbox_by_public_id", public_id=public_id), status_code=302, diff --git a/app/ap_object.py b/app/ap_object.py index 5c9c613..127320b 100644 --- a/app/ap_object.py +++ b/app/ap_object.py @@ -1,3 +1,4 @@ +import base64 import hashlib from datetime import datetime from typing import Any @@ -60,21 +61,35 @@ class Object: return self.ap_object.get("sensitive", False) @property - def attachments(self) -> list["Attachment"]: - attachments = [ - Attachment.parse_obj(obj) for obj in self.ap_object.get("attachment", []) - ] + def attachments_old(self) -> list["Attachment"]: + # TODO: set img_src with the proxy URL (proxy_url?) + attachments = [] + for obj in self.ap_object.get("attachment", []): + proxied_url = _proxied_url(obj["url"]) + attachments.append( + Attachment.parse_obj( + { + "proxiedUrl": proxied_url, + "resizedUrl": proxied_url + "/740" + if obj["mediaType"].startswith("image") + else None, + **obj, + } + ) + ) # Also add any video Link (for PeerTube compat) if self.ap_type == "Video": for link in ap.as_list(self.ap_object.get("url", [])): if (isinstance(link, dict)) and link.get("type") == "Link": if link.get("mediaType", "").startswith("video"): + proxied_url = _proxied_url(link["href"]) attachments.append( Attachment( type="Video", mediaType=link["mediaType"], url=link["href"], + proxiedUrl=proxied_url, ) ) break @@ -137,12 +152,20 @@ class BaseModel(pydantic.BaseModel): alias_generator = _to_camel +def _proxied_url(url: str) -> str: + return "/proxy/media/" + base64.urlsafe_b64encode(url.encode()).decode() + + class Attachment(BaseModel): type: str media_type: str name: str | None url: str + # Extra fields for the templates + proxied_url: str + resized_url: str | None = None + class RemoteObject(Object): def __init__(self, raw_object: ap.RawObject, actor: Actor | None = None): diff --git a/app/boxes.py b/app/boxes.py index ecd48bb..8df119d 100644 --- a/app/boxes.py +++ b/app/boxes.py @@ -22,6 +22,7 @@ from app.config import ID from app.database import now from app.process_outgoing_activities import new_outgoing_activity from app.source import markdownify +from app.uploads import upload_to_attachment def allocate_outbox_id() -> str: @@ -214,11 +215,20 @@ def send_undo(db: Session, ap_object_id: str) -> None: raise ValueError("Should never happen") -def send_create(db: Session, source: str) -> str: +def send_create( + db: Session, + source: str, + uploads: list[tuple[models.Upload, str]], +) -> str: note_id = allocate_outbox_id() published = now().replace(microsecond=0).isoformat().replace("+00:00", "Z") context = f"{ID}/contexts/" + uuid.uuid4().hex content, tags = markdownify(db, source) + attachments = [] + + for (upload, filename) in uploads: + attachments.append(upload_to_attachment(upload, filename)) + note = { "@context": ap.AS_CTX, "type": "Note", @@ -235,6 +245,7 @@ def send_create(db: Session, source: str) -> str: "summary": None, "inReplyTo": None, "sensitive": False, + "attachment": attachments, } outbox_object = save_outbox_object(db, note_id, note, source=source) if not outbox_object.id: @@ -247,6 +258,13 @@ def send_create(db: Session, source: str) -> str: outbox_object_id=outbox_object.id, ) db.add(tagged_object) + + for (upload, filename) in uploads: + outbox_object_attachment = models.OutboxObjectAttachment( + filename=filename, outbox_object_id=outbox_object.id, upload_id=upload.id + ) + db.add(outbox_object_attachment) + db.commit() recipients = _compute_recipients(db, note) diff --git a/app/main.py b/app/main.py index 41525b3..ebbb819 100644 --- a/app/main.py +++ b/app/main.py @@ -3,6 +3,7 @@ import os import sys import time from datetime import datetime +from io import BytesIO from typing import Any from typing import Type @@ -13,10 +14,12 @@ from fastapi import FastAPI from fastapi import Request from fastapi import Response from fastapi.exceptions import HTTPException +from fastapi.responses import FileResponse from fastapi.responses import PlainTextResponse from fastapi.responses import StreamingResponse from fastapi.staticfiles import StaticFiles from loguru import logger +from PIL import Image from sqlalchemy.orm import Session from sqlalchemy.orm import joinedload from starlette.background import BackgroundTask @@ -41,6 +44,7 @@ from app.config import USERNAME from app.config import is_activitypub_requested from app.database import get_db from app.templates import is_current_user_admin +from app.uploads import UPLOAD_DIR # TODO(ts): # @@ -113,6 +117,8 @@ async def add_security_headers(request: Request, call_next): response.headers["x-xss-protection"] = "1; mode=block" response.headers["x-frame-options"] = "SAMEORIGIN" # TODO(ts): disallow inline CSS? + if DEBUG: + return response response.headers["content-security-policy"] = ( "default-src 'self'" + " style-src 'self' 'unsafe-inline';" ) @@ -157,6 +163,11 @@ def index( outbox_objects = ( db.query(models.OutboxObject) + .options( + joinedload(models.OutboxObject.outbox_object_attachments).options( + joinedload(models.OutboxObjectAttachment.upload) + ) + ) .filter( models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC, models.OutboxObject.is_deleted.is_(False), @@ -367,6 +378,11 @@ def outbox_by_public_id( # TODO: ACL? maybe_object = ( db.query(models.OutboxObject) + .options( + joinedload(models.OutboxObject.outbox_object_attachments).options( + joinedload(models.OutboxObjectAttachment.upload) + ) + ) .filter( models.OutboxObject.public_id == public_id, # models.OutboxObject.is_deleted.is_(False), @@ -550,6 +566,112 @@ async def serve_proxy_media(request: Request, encoded_url: str) -> StreamingResp ) +@app.get("/proxy/media/{encoded_url}/{size}") +def serve_proxy_media_resized( + request: Request, + encoded_url: str, + size: int, +) -> PlainTextResponse: + if size not in {50, 740}: + raise ValueError("Unsupported size") + + # Decode the base64-encoded URL + url = base64.urlsafe_b64decode(encoded_url).decode() + # Request the URL (and filter request headers) + proxy_resp = httpx.get( + url, + headers=[ + (k, v) + for (k, v) in request.headers.raw + if k.lower() + not in [b"host", b"cookie", b"x-forwarded-for", b"x-real-ip", b"user-agent"] + ] + + [(b"user-agent", USER_AGENT.encode())], + ) + if proxy_resp.status_code != 200: + return PlainTextResponse( + proxy_resp.content, + status_code=proxy_resp.status_code, + ) + + # Filter the headers + proxy_resp_headers = { + k: v + for (k, v) in proxy_resp.headers.items() + if k.lower() + in [ + "content-type", + "etag", + "cache-control", + "expires", + "last-modified", + ] + } + + try: + out = BytesIO(proxy_resp.content) + i = Image.open(out) + i.thumbnail((size, size)) + resized_buf = BytesIO() + i.save(resized_buf, format=i.format) + resized_buf.seek(0) + return PlainTextResponse( + resized_buf.read(), + media_type=i.get_format_mimetype(), # type: ignore + headers=proxy_resp_headers, + ) + except Exception: + logger.exception(f"Failed to resize {url} on the fly") + return PlainTextResponse( + proxy_resp.content, + headers=proxy_resp_headers, + ) + + +@app.get("/attachments/{content_hash}/{filename}") +def serve_attachment( + content_hash: str, + filename: str, + db: Session = Depends(get_db), +): + upload = ( + db.query(models.Upload) + .filter( + models.Upload.content_hash == content_hash, + ) + .one_or_none() + ) + if not upload: + raise HTTPException(status_code=404) + + return FileResponse( + UPLOAD_DIR / content_hash, + media_type=upload.content_type, + ) + + +@app.get("/attachments/thumbnails/{content_hash}/{filename}") +def serve_attachment_thumbnail( + content_hash: str, + filename: str, + db: Session = Depends(get_db), +): + upload = ( + db.query(models.Upload) + .filter( + models.Upload.content_hash == content_hash, + ) + .one_or_none() + ) + if not upload or not upload.has_thumbnail: + raise HTTPException(status_code=404) + + return FileResponse( + UPLOAD_DIR / (content_hash + "_resized"), + media_type=upload.content_type, + ) + + @app.get("/robots.txt", response_class=PlainTextResponse) async def robots_file(): return """User-agent: * diff --git a/app/models.py b/app/models.py index c1a4ae4..3450565 100644 --- a/app/models.py +++ b/app/models.py @@ -17,13 +17,15 @@ from sqlalchemy.orm import relationship from app import activitypub as ap from app.actor import LOCAL_ACTOR from app.actor import Actor as BaseActor +from app.ap_object import Attachment from app.ap_object import Object as BaseObject +from app.config import BASE_URL from app.database import Base from app.database import now class Actor(Base, BaseActor): - __tablename__ = "actors" + __tablename__ = "actor" id = Column(Integer, primary_key=True, index=True) created_at = Column(DateTime(timezone=True), nullable=False, default=now) @@ -47,7 +49,7 @@ class InboxObject(Base, BaseObject): created_at = Column(DateTime(timezone=True), nullable=False, default=now) updated_at = Column(DateTime(timezone=True), nullable=False, default=now) - actor_id = Column(Integer, ForeignKey("actors.id"), nullable=False) + actor_id = Column(Integer, ForeignKey("actor.id"), nullable=False) actor: Mapped[Actor] = relationship(Actor, uselist=False) server = Column(String, nullable=False) @@ -166,15 +168,48 @@ class OutboxObject(Base, BaseObject): def actor(self) -> BaseActor: return LOCAL_ACTOR + outbox_object_attachments: Mapped[list["OutboxObjectAttachment"]] = relationship( + "OutboxObjectAttachment", uselist=True, backref="outbox_object" + ) + + @property + def attachments(self) -> list[Attachment]: + out = [] + for attachment in self.outbox_object_attachments: + url = ( + BASE_URL + + f"/attachments/{attachment.upload.content_hash}/{attachment.filename}" + ) + out.append( + Attachment.parse_obj( + { + "type": "Document", + "mediaType": attachment.upload.content_type, + "name": attachment.filename, + "url": url, + "proxiedUrl": url, + "resizedUrl": BASE_URL + + ( + "/attachments/thumbnails/" + f"{attachment.upload.content_hash}" + f"/{attachment.filename}" + ) + if attachment.upload.has_thumbnail + else None, + } + ) + ) + return out + class Follower(Base): - __tablename__ = "followers" + __tablename__ = "follower" id = Column(Integer, primary_key=True, index=True) created_at = Column(DateTime(timezone=True), nullable=False, default=now) updated_at = Column(DateTime(timezone=True), nullable=False, default=now) - actor_id = Column(Integer, ForeignKey("actors.id"), nullable=False, unique=True) + actor_id = Column(Integer, ForeignKey("actor.id"), nullable=False, unique=True) actor = relationship(Actor, uselist=False) inbox_object_id = Column(Integer, ForeignKey("inbox.id"), nullable=False) @@ -190,7 +225,7 @@ class Following(Base): created_at = Column(DateTime(timezone=True), nullable=False, default=now) updated_at = Column(DateTime(timezone=True), nullable=False, default=now) - actor_id = Column(Integer, ForeignKey("actors.id"), nullable=False, unique=True) + actor_id = Column(Integer, ForeignKey("actor.id"), nullable=False, unique=True) actor = relationship(Actor, uselist=False) outbox_object_id = Column(Integer, ForeignKey("outbox.id"), nullable=False) @@ -220,7 +255,7 @@ class Notification(Base): notification_type = Column(Enum(NotificationType), nullable=True) is_new = Column(Boolean, nullable=False, default=True) - actor_id = Column(Integer, ForeignKey("actors.id"), nullable=True) + actor_id = Column(Integer, ForeignKey("actor.id"), nullable=True) actor = relationship(Actor, uselist=False) outbox_object_id = Column(Integer, ForeignKey("outbox.id"), nullable=True) @@ -231,7 +266,7 @@ class Notification(Base): class OutgoingActivity(Base): - __tablename__ = "outgoing_activities" + __tablename__ = "outgoing_activity" id = Column(Integer, primary_key=True, index=True) created_at = Column(DateTime(timezone=True), nullable=False, default=now) @@ -253,7 +288,7 @@ class OutgoingActivity(Base): class TaggedOutboxObject(Base): - __tablename__ = "tagged_outbox_objects" + __tablename__ = "tagged_outbox_object" __table_args__ = ( UniqueConstraint("outbox_object_id", "tag", name="uix_tagged_object"), ) @@ -266,23 +301,35 @@ class TaggedOutboxObject(Base): tag = Column(String, nullable=False, index=True) -""" class Upload(Base): __tablename__ = "upload" - filename = Column(String, nullable=False) - filehash = Column(String, nullable=False) - filesize = Column(Integer, nullable=False) + id = Column(Integer, primary_key=True, index=True) + created_at = Column(DateTime(timezone=True), nullable=False, default=now) + + content_type: Mapped[str] = Column(String, nullable=False) + content_hash = Column(String, nullable=False, unique=True) + + has_thumbnail = Column(Boolean, nullable=False) + + # Only set for images + blurhash = Column(String, nullable=True) + width = Column(Integer, nullable=True) + height = Column(Integer, nullable=True) + + @property + def is_image(self) -> bool: + return self.content_type.startswith("image") class OutboxObjectAttachment(Base): __tablename__ = "outbox_object_attachment" id = Column(Integer, primary_key=True, index=True) + created_at = Column(DateTime(timezone=True), nullable=False, default=now) + filename = Column(String, nullable=False) outbox_object_id = Column(Integer, ForeignKey("outbox.id"), nullable=False) - outbox_object = relationship(OutboxObject, uselist=False) - upload_id = Column(Integer, ForeignKey("upload.id")) + upload_id = Column(Integer, ForeignKey("upload.id"), nullable=False) upload = relationship(Upload, uselist=False) -""" diff --git a/app/templates.py b/app/templates.py index a651d3b..f77f4aa 100644 --- a/app/templates.py +++ b/app/templates.py @@ -17,8 +17,8 @@ from app import models from app.actor import LOCAL_ACTOR from app.ap_object import Attachment from app.boxes import public_outbox_objects_count +from app.config import BASE_URL from app.config import DEBUG -from app.config import DOMAIN from app.config import VERSION from app.config import generate_csrf_token from app.config import session_serializer @@ -40,7 +40,7 @@ def _media_proxy_url(url: str | None) -> str: if not url: return "/static/nopic.png" - if url.startswith(DOMAIN): + if url.startswith(BASE_URL): return url encoded_url = base64.urlsafe_b64encode(url.encode()).decode() diff --git a/app/templates/utils.html b/app/templates/utils.html index dfb5f4a..1125143 100644 --- a/app/templates/utils.html +++ b/app/templates/utils.html @@ -57,7 +57,7 @@ {% set metadata = actors_metadata.get(actor.ap_id) %}
- +
{{ actor.name or actor.preferred_username }}
@@ -90,7 +90,7 @@ {% if object.ap_type in ["Note", "Article", "Video"] %}