Improve actor icons handling and admin

This commit is contained in:
Thomas Sileo 2022-06-25 08:23:28 +02:00
parent 951c74c40a
commit f66e3f3995
11 changed files with 172 additions and 25 deletions

View file

@ -7,6 +7,7 @@ from sqlalchemy.orm import Session
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from app import activitypub as ap from app import activitypub as ap
from app import media
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from app.models import Actor as ActorModel from app.models import Actor as ActorModel
@ -78,6 +79,20 @@ class Actor:
def public_key_id(self) -> str: def public_key_id(self) -> str:
return self.ap_actor["publicKey"]["id"] 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): class RemoteActor(Actor):
def __init__(self, ap_actor: ap.RawObject) -> None: def __init__(self, ap_actor: ap.RawObject) -> None:

View file

@ -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") @router.get("/notifications")
def get_notifications( def get_notifications(
request: Request, db: Session = Depends(get_db) request: Request, db: Session = Depends(get_db)

View file

@ -1,4 +1,3 @@
import base64
import hashlib import hashlib
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any
@ -11,6 +10,7 @@ from app import activitypub as ap
from app.actor import LOCAL_ACTOR from app.actor import LOCAL_ACTOR
from app.actor import Actor from app.actor import Actor
from app.actor import RemoteActor from app.actor import RemoteActor
from app.media import proxied_media_url
from app.utils import opengraph from app.utils import opengraph
@ -64,7 +64,7 @@ class Object:
def attachments(self) -> list["Attachment"]: def attachments(self) -> list["Attachment"]:
attachments = [] attachments = []
for obj in self.ap_object.get("attachment", []): for obj in self.ap_object.get("attachment", []):
proxied_url = _proxied_url(obj["url"]) proxied_url = proxied_media_url(obj["url"])
attachments.append( attachments.append(
Attachment.parse_obj( Attachment.parse_obj(
{ {
@ -82,7 +82,7 @@ class Object:
for link in ap.as_list(self.ap_object.get("url", [])): for link in ap.as_list(self.ap_object.get("url", [])):
if (isinstance(link, dict)) and link.get("type") == "Link": if (isinstance(link, dict)) and link.get("type") == "Link":
if link.get("mediaType", "").startswith("video"): if link.get("mediaType", "").startswith("video"):
proxied_url = _proxied_url(link["href"]) proxied_url = proxied_media_url(link["href"])
attachments.append( attachments.append(
Attachment( Attachment(
type="Video", type="Video",
@ -151,10 +151,6 @@ class BaseModel(pydantic.BaseModel):
alias_generator = _to_camel alias_generator = _to_camel
def _proxied_url(url: str) -> str:
return "/proxy/media/" + base64.urlsafe_b64encode(url.encode()).decode()
class Attachment(BaseModel): class Attachment(BaseModel):
type: str type: str
media_type: str media_type: str

View file

@ -52,6 +52,11 @@ from app.uploads import UPLOAD_DIR
# TODO(ts): # TODO(ts):
# #
# Next: # 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 # - show likes/announces counter for outbox activities
# - update actor support # - update actor support
# - replies support # - replies support

20
app/media.py Normal file
View file

@ -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}"

View file

@ -1,6 +1,7 @@
import enum import enum
from typing import Any from typing import Any
from typing import Optional from typing import Optional
from typing import Union
from sqlalchemy import JSON from sqlalchemy import JSON
from sqlalchemy import Boolean from sqlalchemy import Boolean
@ -104,6 +105,15 @@ class InboxObject(Base, BaseObject):
og_meta: Mapped[list[dict[str, Any]] | None] = Column(JSON, nullable=True) 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): class OutboxObject(Base, BaseObject):
__tablename__ = "outbox" __tablename__ = "outbox"
@ -202,6 +212,15 @@ class OutboxObject(Base, BaseObject):
) )
return out 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): class Follower(Base):
__tablename__ = "follower" __tablename__ = "follower"

View file

@ -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 %}

View file

@ -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 %}

View file

@ -4,23 +4,17 @@
{% for inbox_object in stream %} {% for inbox_object in stream %}
{% if inbox_object.ap_type == "Announce" %} {% if inbox_object.ap_type == "Announce" %}
{% if inbox_object.relates_to_inbox_object_id %} {{ utils.display_object(inbox_object.relates_to_anybox_object) }}
{{ utils.display_object(inbox_object.relates_to_inbox_object) }} {% elif inbox_object.ap_type in ["Article", "Note", "Video"] %}
{% else %}
{% endif %}
{% else %}
{{ utils.display_object(inbox_object) }} {{ utils.display_object(inbox_object) }}
{% if inbox_object.liked_via_outbox_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") }} {{ utils.admin_undo_button(inbox_object.liked_via_outbox_object_ap_id, "Unlike") }}
{% else %} {% else %}
{{ utils.admin_like_button(inbox_object.ap_id) }} {{ utils.admin_like_button(inbox_object.ap_id) }}
{% endif %} {% endif %}
{{ utils.admin_announce_button(inbox_object.ap_id) }} {{ utils.admin_announce_button(inbox_object.ap_id) }}
{% endif %}
{{ utils.admin_reply_button(inbox_object.ap_id) }} {{ utils.admin_reply_button(inbox_object.ap_id) }}
{% endif %}
{% endfor %} {% endfor %}
{% endblock %} {% endblock %}

View file

@ -25,6 +25,8 @@
<li>{{ admin_link("index", "Public") }}</li> <li>{{ admin_link("index", "Public") }}</li>
<li>{{ admin_link("admin_new", "New") }}</li> <li>{{ admin_link("admin_new", "New") }}</li>
<li>{{ admin_link("stream", "Stream") }}</li> <li>{{ admin_link("stream", "Stream") }}</li>
<li>{{ admin_link("admin_inbox", "Inbox") }}</li>
<li>{{ admin_link("admin_outbox", "Outbox") }}</li>
<li>{{ admin_link("get_notifications", "Notifications") }} {% if notifications_count %}({{ notifications_count }}){% endif %}</li> <li>{{ admin_link("get_notifications", "Notifications") }} {% if notifications_count %}({{ notifications_count }}){% endif %}</li>
<li>{{ admin_link("get_lookup", "Lookup") }}</li> <li>{{ admin_link("get_lookup", "Lookup") }}</li>
<li><a href="">Bookmarks</a></li> <li><a href="">Bookmarks</a></li>

View file

@ -64,7 +64,7 @@
{% set metadata = actors_metadata.get(actor.ap_id) %} {% set metadata = actors_metadata.get(actor.ap_id) %}
<div style="display: flex;column-gap: 20px;margin:20px 0 10px 0;" class="actor-box"> <div style="display: flex;column-gap: 20px;margin:20px 0 10px 0;" class="actor-box">
<div style="flex: 0 0 48px;"> <div style="flex: 0 0 48px;">
<img src="{{ actor.icon_url | media_proxy_url }}/50" style="max-width:45px;"> <img src="{{ actor.resized_icon_url }}" style="max-width:45px;">
</div> </div>
<a href="{{ actor.url }}" style=""> <a href="{{ actor.url }}" style="">
<div><strong>{{ actor.name or actor.preferred_username }}</strong></div> <div><strong>{{ actor.name or actor.preferred_username }}</strong></div>
@ -97,12 +97,12 @@
{% if object.ap_type in ["Note", "Article", "Video"] %} {% if object.ap_type in ["Note", "Article", "Video"] %}
<div class="activity-wrap" id="{{ object.permalink_id }}"> <div class="activity-wrap" id="{{ object.permalink_id }}">
<div class="activity-content"> <div class="activity-content">
<img src="{% if object.actor.icon_url %}{{ object.actor.icon_url | media_proxy_url }}/50{% else %}/static/nopic.png{% endif %}" alt="" class="actor-icon"> <img src="{{ object.actor.resized_icon_url }}" alt="" class="actor-icon">
<div class="activity-header"> <div class="activity-header">
<strong>{{ object.actor.name or object.actor.preferred_username }}</strong> <strong>{{ object.actor.name or object.actor.preferred_username }}</strong>
<span>{{ object.actor.handle }}</span> <span>{{ object.actor.handle }}</span>
<span class="activity-date" title="{{ object.ap_published_at.isoformat() }}"> <span class="activity-date" title="{{ object.ap_published_at.isoformat() }}">
{{ object.visibility }} {{ object.visibility.value }}
<a href="{{ object.url }}">{{ object.ap_published_at | timeago }}</a> <a href="{{ object.url }}">{{ object.ap_published_at | timeago }}</a>
</span> </span>
<div class="activity-main"> <div class="activity-main">