diff --git a/app/actor.py b/app/actor.py index c11bb9f..7291a6b 100644 --- a/app/actor.py +++ b/app/actor.py @@ -7,6 +7,7 @@ from sqlalchemy.orm import Session from sqlalchemy.orm import joinedload from app import activitypub as ap +from app import media if typing.TYPE_CHECKING: from app.models import Actor as ActorModel @@ -78,6 +79,20 @@ class Actor: def public_key_id(self) -> str: return self.ap_actor["publicKey"]["id"] + @property + def proxied_icon_url(self) -> str: + if self.icon_url: + return media.proxied_media_url(self.icon_url) + else: + return "/static/nopic.png" + + @property + def resized_icon_url(self) -> str: + if self.icon_url: + return media.resized_media_url(self.icon_url, 50) + else: + return "/static/nopic.png" + class RemoteActor(Actor): def __init__(self, ap_actor: ap.RawObject) -> None: diff --git a/app/admin.py b/app/admin.py index e95fba1..830b2c5 100644 --- a/app/admin.py +++ b/app/admin.py @@ -140,6 +140,56 @@ def stream( ) +@router.get("/inbox") +def admin_inbox( + request: Request, + db: Session = Depends(get_db), +) -> templates.TemplateResponse: + inbox = ( + db.query(models.InboxObject) + .options( + joinedload(models.InboxObject.relates_to_inbox_object), + joinedload(models.InboxObject.relates_to_outbox_object), + ) + .order_by(models.InboxObject.ap_published_at.desc()) + .limit(20) + .all() + ) + return templates.render_template( + db, + request, + "admin_inbox.html", + { + "inbox": inbox, + }, + ) + + +@router.get("/outbox") +def admin_outbox( + request: Request, + db: Session = Depends(get_db), +) -> templates.TemplateResponse: + outbox = ( + db.query(models.OutboxObject) + .options( + joinedload(models.OutboxObject.relates_to_inbox_object), + joinedload(models.OutboxObject.relates_to_outbox_object), + ) + .order_by(models.OutboxObject.ap_published_at.desc()) + .limit(20) + .all() + ) + return templates.render_template( + db, + request, + "admin_outbox.html", + { + "outbox": outbox, + }, + ) + + @router.get("/notifications") def get_notifications( request: Request, db: Session = Depends(get_db) diff --git a/app/ap_object.py b/app/ap_object.py index c779f01..0051867 100644 --- a/app/ap_object.py +++ b/app/ap_object.py @@ -1,4 +1,3 @@ -import base64 import hashlib from datetime import datetime from typing import Any @@ -11,6 +10,7 @@ from app import activitypub as ap from app.actor import LOCAL_ACTOR from app.actor import Actor from app.actor import RemoteActor +from app.media import proxied_media_url from app.utils import opengraph @@ -64,7 +64,7 @@ class Object: def attachments(self) -> list["Attachment"]: attachments = [] for obj in self.ap_object.get("attachment", []): - proxied_url = _proxied_url(obj["url"]) + proxied_url = proxied_media_url(obj["url"]) attachments.append( Attachment.parse_obj( { @@ -82,7 +82,7 @@ class Object: 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"]) + proxied_url = proxied_media_url(link["href"]) attachments.append( Attachment( type="Video", @@ -151,10 +151,6 @@ 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 diff --git a/app/main.py b/app/main.py index bda63b4..c594303 100644 --- a/app/main.py +++ b/app/main.py @@ -52,6 +52,11 @@ from app.uploads import UPLOAD_DIR # TODO(ts): # # Next: +# - inbox/outbox admin +# - no counters anymore? +# - allow to show tags in the menu +# - support update post with history +# - inbox/outbox in the admin (as in show every objects) # - show likes/announces counter for outbox activities # - update actor support # - replies support diff --git a/app/media.py b/app/media.py new file mode 100644 index 0000000..04339b1 --- /dev/null +++ b/app/media.py @@ -0,0 +1,20 @@ +import base64 + +from app.config import BASE_URL + +SUPPORTED_RESIZE = [50, 740] + + +def proxied_media_url(url: str) -> str: + if url.startswith(BASE_URL): + return url + + return "/proxy/media/" + base64.urlsafe_b64encode(url.encode()).decode() + + +def resized_media_url(url: str, size: int) -> str: + if size not in SUPPORTED_RESIZE: + raise ValueError(f"Unsupported resize {size}") + if url.startswith(BASE_URL): + return url + return proxied_media_url(url) + f"/{size}" diff --git a/app/models.py b/app/models.py index d2035c8..991f046 100644 --- a/app/models.py +++ b/app/models.py @@ -1,6 +1,7 @@ import enum from typing import Any from typing import Optional +from typing import Union from sqlalchemy import JSON from sqlalchemy import Boolean @@ -104,6 +105,15 @@ class InboxObject(Base, BaseObject): og_meta: Mapped[list[dict[str, Any]] | None] = Column(JSON, nullable=True) + @property + def relates_to_anybox_object(self) -> Union["InboxObject", "OutboxObject"] | None: + if self.relates_to_inbox_object_id: + return self.relates_to_inbox_object + elif self.relates_to_outbox_object_id: + return self.relates_to_outbox_object + else: + return None + class OutboxObject(Base, BaseObject): __tablename__ = "outbox" @@ -202,6 +212,15 @@ class OutboxObject(Base, BaseObject): ) return out + @property + def relates_to_anybox_object(self) -> Union["InboxObject", "OutboxObject"] | None: + if self.relates_to_inbox_object_id: + return self.relates_to_inbox_object + elif self.relates_to_outbox_object_id: + return self.relates_to_outbox_object + else: + return None + class Follower(Base): __tablename__ = "follower" diff --git a/app/templates/admin_inbox.html b/app/templates/admin_inbox.html new file mode 100644 index 0000000..575ee2f --- /dev/null +++ b/app/templates/admin_inbox.html @@ -0,0 +1,22 @@ +{%- import "utils.html" as utils with context -%} +{% extends "layout.html" %} +{% block content %} + +{% for inbox_object in inbox %} +{% if inbox_object.ap_type == "Announce" %} + {{ utils.display_object(inbox_object.relates_to_anybox_object) }} +{% elif inbox_object.ap_type in ["Article", "Note", "Video"] %} +{{ utils.display_object(inbox_object) }} + {% if inbox_object.liked_via_outbox_object_ap_id %} + {{ utils.admin_undo_button(inbox_object.liked_via_outbox_object_ap_id, "Unlike") }} + {% else %} + {{ utils.admin_like_button(inbox_object.ap_id) }} + {% endif %} + {{ utils.admin_announce_button(inbox_object.ap_id) }} + {{ utils.admin_reply_button(inbox_object.ap_id) }} +{% else %} + Implement {{ inbox_object.ap_type }} +{% endif %} +{% endfor %} + +{% endblock %} diff --git a/app/templates/admin_outbox.html b/app/templates/admin_outbox.html new file mode 100644 index 0000000..6154d2a --- /dev/null +++ b/app/templates/admin_outbox.html @@ -0,0 +1,24 @@ +{%- import "utils.html" as utils with context -%} +{% extends "layout.html" %} +{% block content %} + +{% for outbox_object in outbox %} + + {% if outbox_object.ap_type == "Announce" %} + {{ utils.display_object(outbox_object.relates_to_anybox_object) }} + {% elif outbox_object.ap_type in ["Article", "Note", "Video"] %} + {{ utils.display_object(outbox_object) }} + {% if outbox_object.liked_via_outbox_object_ap_id %} + {{ utils.admin_undo_button(outbox_object.liked_via_outbox_object_ap_id, "Unlike") }} + {% else %} + {{ utils.admin_like_button(outbox_object.ap_id) }} + {% endif %} + {{ utils.admin_announce_button(outbox_object.ap_id) }} + {{ utils.admin_reply_button(outbox_object.ap_id) }} +{% else %} + Implement {{ outbox_object.ap_type }} +{% endif %} + +{% endfor %} + +{% endblock %} diff --git a/app/templates/admin_stream.html b/app/templates/admin_stream.html index 233e0ed..22e066d 100644 --- a/app/templates/admin_stream.html +++ b/app/templates/admin_stream.html @@ -4,23 +4,17 @@ {% for inbox_object in stream %} {% if inbox_object.ap_type == "Announce" %} - {% if inbox_object.relates_to_inbox_object_id %} - {{ utils.display_object(inbox_object.relates_to_inbox_object) }} - {% else %} - - {% endif %} - -{% else %} + {{ utils.display_object(inbox_object.relates_to_anybox_object) }} +{% elif inbox_object.ap_type in ["Article", "Note", "Video"] %} {{ utils.display_object(inbox_object) }} -{% if inbox_object.liked_via_outbox_object_ap_id %} -{{ utils.admin_undo_button(inbox_object.liked_via_outbox_object_ap_id, "Unlike") }} -{% else %} -{{ utils.admin_like_button(inbox_object.ap_id) }} + {% if inbox_object.liked_via_outbox_object_ap_id %} + {{ utils.admin_undo_button(inbox_object.liked_via_outbox_object_ap_id, "Unlike") }} + {% else %} + {{ utils.admin_like_button(inbox_object.ap_id) }} + {% endif %} + {{ utils.admin_announce_button(inbox_object.ap_id) }} + {{ utils.admin_reply_button(inbox_object.ap_id) }} {% endif %} - -{{ utils.admin_announce_button(inbox_object.ap_id) }} -{% endif %} -{{ utils.admin_reply_button(inbox_object.ap_id) }} {% endfor %} {% endblock %} diff --git a/app/templates/layout.html b/app/templates/layout.html index e417639..6c58ff7 100644 --- a/app/templates/layout.html +++ b/app/templates/layout.html @@ -25,6 +25,8 @@