mirror of
https://git.sr.ht/~tsileo/microblog.pub
synced 2024-12-22 13:14:28 +00:00
Improve actor icons handling and admin
This commit is contained in:
parent
951c74c40a
commit
f66e3f3995
11 changed files with 172 additions and 25 deletions
15
app/actor.py
15
app/actor.py
|
@ -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:
|
||||||
|
|
50
app/admin.py
50
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")
|
@router.get("/notifications")
|
||||||
def get_notifications(
|
def get_notifications(
|
||||||
request: Request, db: Session = Depends(get_db)
|
request: Request, db: Session = Depends(get_db)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
20
app/media.py
Normal 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}"
|
|
@ -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"
|
||||||
|
|
22
app/templates/admin_inbox.html
Normal file
22
app/templates/admin_inbox.html
Normal 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 %}
|
24
app/templates/admin_outbox.html
Normal file
24
app/templates/admin_outbox.html
Normal 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 %}
|
|
@ -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 %}
|
||||||
|
{{ utils.admin_announce_button(inbox_object.ap_id) }}
|
||||||
|
{{ utils.admin_reply_button(inbox_object.ap_id) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{{ utils.admin_announce_button(inbox_object.ap_id) }}
|
|
||||||
{% endif %}
|
|
||||||
{{ utils.admin_reply_button(inbox_object.ap_id) }}
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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">
|
||||||
|
|
Loading…
Reference in a new issue