Improved audience support and implement featured collection

This commit is contained in:
Thomas Sileo 2022-06-26 18:07:55 +02:00
parent ff8975acab
commit 4bf54c7040
16 changed files with 284 additions and 37 deletions

View file

@ -1,8 +1,8 @@
"""Initial migration """Initial migration
Revision ID: 714b4a5307c7 Revision ID: ba131b14c3a1
Revises: Revises:
Create Date: 2022-06-23 18:42:56.009810 Create Date: 2022-06-26 14:36:44.107422
""" """
import sqlalchemy as sa import sqlalchemy as sa
@ -10,7 +10,7 @@ import sqlalchemy as sa
from alembic import op from alembic import op
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = '714b4a5307c7' revision = 'ba131b14c3a1'
down_revision = None down_revision = None
branch_labels = None branch_labels = None
depends_on = None depends_on = None
@ -81,10 +81,13 @@ def upgrade() -> None:
sa.Column('replies_count', sa.Integer(), nullable=False), sa.Column('replies_count', sa.Integer(), nullable=False),
sa.Column('webmentions', sa.JSON(), nullable=True), sa.Column('webmentions', sa.JSON(), nullable=True),
sa.Column('og_meta', sa.JSON(), nullable=True), sa.Column('og_meta', sa.JSON(), nullable=True),
sa.Column('is_pinned', sa.Boolean(), nullable=False),
sa.Column('is_deleted', sa.Boolean(), nullable=False), sa.Column('is_deleted', sa.Boolean(), nullable=False),
sa.Column('relates_to_inbox_object_id', sa.Integer(), nullable=True), sa.Column('relates_to_inbox_object_id', sa.Integer(), nullable=True),
sa.Column('relates_to_outbox_object_id', sa.Integer(), nullable=True), sa.Column('relates_to_outbox_object_id', sa.Integer(), nullable=True),
sa.Column('relates_to_actor_id', sa.Integer(), nullable=True),
sa.Column('undone_by_outbox_object_id', sa.Integer(), nullable=True), sa.Column('undone_by_outbox_object_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['relates_to_actor_id'], ['actor.id'], ),
sa.ForeignKeyConstraint(['relates_to_inbox_object_id'], ['inbox.id'], ), sa.ForeignKeyConstraint(['relates_to_inbox_object_id'], ['inbox.id'], ),
sa.ForeignKeyConstraint(['relates_to_outbox_object_id'], ['outbox.id'], ), sa.ForeignKeyConstraint(['relates_to_outbox_object_id'], ['outbox.id'], ),
sa.ForeignKeyConstraint(['undone_by_outbox_object_id'], ['outbox.id'], ), sa.ForeignKeyConstraint(['undone_by_outbox_object_id'], ['outbox.id'], ),

View file

@ -1,6 +1,7 @@
import enum import enum
import json import json
import mimetypes import mimetypes
from typing import TYPE_CHECKING
from typing import Any from typing import Any
import httpx import httpx
@ -10,6 +11,9 @@ from app.config import AP_CONTENT_TYPE # noqa: F401
from app.httpsig import auth from app.httpsig import auth
from app.key import get_pubkey_as_pem from app.key import get_pubkey_as_pem
if TYPE_CHECKING:
from app.actor import Actor
RawObject = dict[str, Any] RawObject = dict[str, Any]
AS_CTX = "https://www.w3.org/ns/activitystreams" AS_CTX = "https://www.w3.org/ns/activitystreams"
AS_PUBLIC = "https://www.w3.org/ns/activitystreams#Public" AS_PUBLIC = "https://www.w3.org/ns/activitystreams#Public"
@ -24,8 +28,18 @@ class ObjectIsGoneError(Exception):
class VisibilityEnum(str, enum.Enum): class VisibilityEnum(str, enum.Enum):
PUBLIC = "public" PUBLIC = "public"
UNLISTED = "unlisted" UNLISTED = "unlisted"
FOLLOWERS_ONLY = "followers-only"
DIRECT = "direct" DIRECT = "direct"
@staticmethod
def get_display_name(key: "VisibilityEnum") -> str:
return {
VisibilityEnum.PUBLIC: "Public - sent to followers and visible on the homepage", # noqa: E501
VisibilityEnum.UNLISTED: "Unlisted - like public, but hidden from the homepage", # noqa: E501,
VisibilityEnum.FOLLOWERS_ONLY: "Followers only",
VisibilityEnum.DIRECT: "Direct - only visible for mentioned actors",
}[key]
MICROBLOGPUB = { MICROBLOGPUB = {
"@context": [ "@context": [
@ -70,7 +84,7 @@ ME = {
"id": config.ID, "id": config.ID,
"following": config.BASE_URL + "/following", "following": config.BASE_URL + "/following",
"followers": config.BASE_URL + "/followers", "followers": config.BASE_URL + "/followers",
# "featured": ID + "/featured", "featured": config.BASE_URL + "/featured",
"inbox": config.BASE_URL + "/inbox", "inbox": config.BASE_URL + "/inbox",
"outbox": config.BASE_URL + "/outbox", "outbox": config.BASE_URL + "/outbox",
"preferredUsername": config.USERNAME, "preferredUsername": config.USERNAME,
@ -198,13 +212,15 @@ def get_id(val: str | dict[str, Any]) -> str:
return val return val
def object_visibility(ap_activity: RawObject) -> VisibilityEnum: def object_visibility(ap_activity: RawObject, actor: "Actor") -> VisibilityEnum:
to = as_list(ap_activity.get("to", [])) to = as_list(ap_activity.get("to", []))
cc = as_list(ap_activity.get("cc", [])) cc = as_list(ap_activity.get("cc", []))
if AS_PUBLIC in to: if AS_PUBLIC in to:
return VisibilityEnum.PUBLIC return VisibilityEnum.PUBLIC
elif AS_PUBLIC in cc: elif AS_PUBLIC in cc:
return VisibilityEnum.UNLISTED return VisibilityEnum.UNLISTED
elif actor.followers_collection_id in to + cc:
return VisibilityEnum.FOLLOWERS_ONLY
else: else:
return VisibilityEnum.DIRECT return VisibilityEnum.DIRECT

View file

@ -97,6 +97,14 @@ class Actor:
else: else:
return "/static/nopic.png" return "/static/nopic.png"
@property
def tags(self) -> list[ap.RawObject]:
return self.ap_actor.get("tag", [])
@property
def followers_collection_id(self) -> str:
return self.ap_actor["followers"]
class RemoteActor(Actor): class RemoteActor(Actor):
def __init__(self, ap_actor: ap.RawObject) -> None: def __init__(self, ap_actor: ap.RawObject) -> None:

View file

@ -13,8 +13,10 @@ from app import activitypub as ap
from app import boxes from app import boxes
from app import models from app import models
from app import templates from app import templates
from app.actor import LOCAL_ACTOR
from app.actor import get_actors_metadata from app.actor import get_actors_metadata
from app.boxes import get_inbox_object_by_ap_id from app.boxes import get_inbox_object_by_ap_id
from app.boxes import get_outbox_object_by_ap_id
from app.boxes import send_follow from app.boxes import send_follow
from app.config import generate_csrf_token from app.config import generate_csrf_token
from app.config import session_serializer from app.config import session_serializer
@ -96,17 +98,32 @@ def admin_new(
in_reply_to: str | None = None, in_reply_to: str | None = None,
db: Session = Depends(get_db), db: Session = Depends(get_db),
) -> templates.TemplateResponse: ) -> templates.TemplateResponse:
content = ""
in_reply_to_object = None in_reply_to_object = None
if in_reply_to: if in_reply_to:
in_reply_to_object = boxes.get_anybox_object_by_ap_id(db, in_reply_to) in_reply_to_object = boxes.get_anybox_object_by_ap_id(db, in_reply_to)
# Add mentions to the initial note content
if not in_reply_to_object: if not in_reply_to_object:
raise ValueError(f"Unknown object {in_reply_to=}") raise ValueError(f"Unknown object {in_reply_to=}")
if in_reply_to_object.actor.ap_id != LOCAL_ACTOR.ap_id:
content += f"{in_reply_to_object.actor.handle} "
for tag in in_reply_to_object.tags:
if tag.get("type") == "Mention" and tag["name"] != LOCAL_ACTOR.handle:
content += f'{tag["name"]} '
return templates.render_template( return templates.render_template(
db, db,
request, request,
"admin_new.html", "admin_new.html",
{"in_reply_to_object": in_reply_to_object}, {
"in_reply_to_object": in_reply_to_object,
"content": content,
"visibility_enum": [
(v.name, ap.VisibilityEnum.get_display_name(v))
for v in ap.VisibilityEnum
],
},
) )
@ -194,24 +211,39 @@ def admin_inbox(
@router.get("/outbox") @router.get("/outbox")
def admin_outbox( def admin_outbox(
request: Request, request: Request, db: Session = Depends(get_db), filter_by: str | None = None
db: Session = Depends(get_db),
) -> templates.TemplateResponse: ) -> templates.TemplateResponse:
q = db.query(models.OutboxObject).filter(
models.OutboxObject.ap_type.not_in(["Accept"])
)
if filter_by:
q = q.filter(models.OutboxObject.ap_type == filter_by)
outbox = ( outbox = (
db.query(models.OutboxObject) q.options(
.options(
joinedload(models.OutboxObject.relates_to_inbox_object), joinedload(models.OutboxObject.relates_to_inbox_object),
joinedload(models.OutboxObject.relates_to_outbox_object), joinedload(models.OutboxObject.relates_to_outbox_object),
joinedload(models.OutboxObject.relates_to_actor),
) )
.order_by(models.OutboxObject.ap_published_at.desc()) .order_by(models.OutboxObject.ap_published_at.desc())
.limit(20) .limit(20)
.all() .all()
) )
actors_metadata = get_actors_metadata(
db,
[
outbox_object.relates_to_actor
for outbox_object in outbox
if outbox_object.relates_to_actor
],
)
return templates.render_template( return templates.render_template(
db, db,
request, request,
"admin_outbox.html", "admin_outbox.html",
{ {
"actors_metadata": actors_metadata,
"outbox": outbox, "outbox": outbox,
}, },
) )
@ -288,6 +320,7 @@ def admin_profile(
models.InboxObject.actor_id == actor.id, models.InboxObject.actor_id == actor.id,
models.InboxObject.ap_type.in_(["Note", "Article", "Video"]), models.InboxObject.ap_type.in_(["Note", "Article", "Video"]),
) )
.order_by(models.InboxObject.ap_published_at.desc())
.all() .all()
) )
@ -384,6 +417,38 @@ def admin_actions_unbookmark(
return RedirectResponse(redirect_url, status_code=302) return RedirectResponse(redirect_url, status_code=302)
@router.post("/actions/pin")
def admin_actions_pin(
request: Request,
ap_object_id: str = Form(),
redirect_url: str = Form(),
csrf_check: None = Depends(verify_csrf_token),
db: Session = Depends(get_db),
) -> RedirectResponse:
outbox_object = get_outbox_object_by_ap_id(db, ap_object_id)
if not outbox_object:
raise ValueError("Should never happen")
outbox_object.is_pinned = True
db.commit()
return RedirectResponse(redirect_url, status_code=302)
@router.post("/actions/unpin")
def admin_actions_unpin(
request: Request,
ap_object_id: str = Form(),
redirect_url: str = Form(),
csrf_check: None = Depends(verify_csrf_token),
db: Session = Depends(get_db),
) -> RedirectResponse:
outbox_object = get_outbox_object_by_ap_id(db, ap_object_id)
if not outbox_object:
raise ValueError("Should never happen")
outbox_object.is_pinned = False
db.commit()
return RedirectResponse(redirect_url, status_code=302)
@router.post("/actions/new") @router.post("/actions/new")
def admin_actions_new( def admin_actions_new(
request: Request, request: Request,
@ -391,6 +456,7 @@ def admin_actions_new(
content: str = Form(), content: str = Form(),
redirect_url: str = Form(), redirect_url: str = Form(),
in_reply_to: str | None = Form(None), in_reply_to: str | None = Form(None),
visibility: str = Form(),
csrf_check: None = Depends(verify_csrf_token), csrf_check: None = Depends(verify_csrf_token),
db: Session = Depends(get_db), db: Session = Depends(get_db),
) -> RedirectResponse: ) -> RedirectResponse:
@ -405,6 +471,7 @@ def admin_actions_new(
source=content, source=content,
uploads=uploads, uploads=uploads,
in_reply_to=in_reply_to or None, in_reply_to=in_reply_to or None,
visibility=ap.VisibilityEnum[visibility],
) )
return RedirectResponse( return RedirectResponse(
request.url_for("outbox_by_public_id", public_id=public_id), request.url_for("outbox_by_public_id", public_id=public_id),

View file

@ -58,7 +58,7 @@ class Object:
@property @property
def visibility(self) -> ap.VisibilityEnum: def visibility(self) -> ap.VisibilityEnum:
return ap.object_visibility(self.ap_object) return ap.object_visibility(self.ap_object, self.actor)
@property @property
def ap_context(self) -> str | None: def ap_context(self) -> str | None:
@ -68,6 +68,10 @@ class Object:
def sensitive(self) -> bool: def sensitive(self) -> bool:
return self.ap_object.get("sensitive", False) return self.ap_object.get("sensitive", False)
@property
def tags(self) -> list[ap.RawObject]:
return self.ap_object.get("tag", [])
@property @property
def attachments(self) -> list["Attachment"]: def attachments(self) -> list["Attachment"]:
attachments = [] attachments = []

View file

@ -43,6 +43,7 @@ def save_outbox_object(
raw_object: ap.RawObject, raw_object: ap.RawObject,
relates_to_inbox_object_id: int | None = None, relates_to_inbox_object_id: int | None = None,
relates_to_outbox_object_id: int | None = None, relates_to_outbox_object_id: int | None = None,
relates_to_actor_id: int | None = None,
source: str | None = None, source: str | None = None,
) -> models.OutboxObject: ) -> models.OutboxObject:
ra = RemoteObject(raw_object) ra = RemoteObject(raw_object)
@ -57,6 +58,7 @@ def save_outbox_object(
og_meta=ra.og_meta, og_meta=ra.og_meta,
relates_to_inbox_object_id=relates_to_inbox_object_id, relates_to_inbox_object_id=relates_to_inbox_object_id,
relates_to_outbox_object_id=relates_to_outbox_object_id, relates_to_outbox_object_id=relates_to_outbox_object_id,
relates_to_actor_id=relates_to_actor_id,
activity_object_ap_id=ra.activity_object_ap_id, activity_object_ap_id=ra.activity_object_ap_id,
is_hidden_from_homepage=True if ra.in_reply_to else False, is_hidden_from_homepage=True if ra.in_reply_to else False,
) )
@ -136,7 +138,9 @@ def send_follow(db: Session, ap_actor_id: str) -> None:
"object": ap_actor_id, "object": ap_actor_id,
} }
outbox_object = save_outbox_object(db, follow_id, follow) outbox_object = save_outbox_object(
db, follow_id, follow, relates_to_actor_id=actor.id
)
if not outbox_object.id: if not outbox_object.id:
raise ValueError("Should never happen") raise ValueError("Should never happen")
@ -224,6 +228,7 @@ def send_create(
source: str, source: str,
uploads: list[tuple[models.Upload, str]], uploads: list[tuple[models.Upload, str]],
in_reply_to: str | None, in_reply_to: str | None,
visibility: ap.VisibilityEnum,
) -> str: ) -> str:
note_id = allocate_outbox_id() note_id = allocate_outbox_id()
published = now().replace(microsecond=0).isoformat().replace("+00:00", "Z") published = now().replace(microsecond=0).isoformat().replace("+00:00", "Z")
@ -247,14 +252,33 @@ def send_create(
for (upload, filename) in uploads: for (upload, filename) in uploads:
attachments.append(upload_to_attachment(upload, filename)) attachments.append(upload_to_attachment(upload, filename))
mentioned_actors = [
mention["href"] for mention in tags if mention["type"] == "Mention"
]
to = []
cc = []
if visibility == ap.VisibilityEnum.PUBLIC:
to = [ap.AS_PUBLIC]
cc = [f"{BASE_URL}/followers"] + mentioned_actors
elif visibility == ap.VisibilityEnum.UNLISTED:
to = [f"{BASE_URL}/followers"]
cc = [ap.AS_PUBLIC] + mentioned_actors
elif visibility == ap.VisibilityEnum.FOLLOWERS_ONLY:
to = [f"{BASE_URL}/followers"]
cc = mentioned_actors
elif visibility == ap.VisibilityEnum.DIRECT:
to = mentioned_actors
cc = []
note = { note = {
"@context": ap.AS_CTX, "@context": ap.AS_CTX,
"type": "Note", "type": "Note",
"id": outbox_object_id(note_id), "id": outbox_object_id(note_id),
"attributedTo": ID, "attributedTo": ID,
"content": content, "content": content,
"to": [ap.AS_PUBLIC], "to": to,
"cc": [f"{BASE_URL}/followers"], "cc": cc,
"published": published, "published": published,
"context": context, "context": context,
"conversation": context, "conversation": context,

View file

@ -158,24 +158,30 @@ def index(
request: Request, request: Request,
db: Session = Depends(get_db), db: Session = Depends(get_db),
_: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker), _: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
page: int | None = None,
) -> templates.TemplateResponse | ActivityPubResponse: ) -> templates.TemplateResponse | ActivityPubResponse:
if is_activitypub_requested(request): if is_activitypub_requested(request):
return ActivityPubResponse(LOCAL_ACTOR.ap_actor) return ActivityPubResponse(LOCAL_ACTOR.ap_actor)
outbox_objects = ( page = page or 1
db.query(models.OutboxObject) q = db.query(models.OutboxObject).filter(
.options(
joinedload(models.OutboxObject.outbox_object_attachments).options(
joinedload(models.OutboxObjectAttachment.upload)
)
)
.filter(
models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC, models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC,
models.OutboxObject.is_deleted.is_(False), models.OutboxObject.is_deleted.is_(False),
models.OutboxObject.is_hidden_from_homepage.is_(False), models.OutboxObject.is_hidden_from_homepage.is_(False),
) )
total_count = q.count()
page_size = 2
page_offset = (page - 1) * page_size
outbox_objects = (
q.options(
joinedload(models.OutboxObject.outbox_object_attachments).options(
joinedload(models.OutboxObjectAttachment.upload)
)
)
.order_by(models.OutboxObject.ap_published_at.desc()) .order_by(models.OutboxObject.ap_published_at.desc())
.limit(20) .offset(page_offset)
.limit(page_size)
.all() .all()
) )
@ -183,7 +189,13 @@ def index(
db, db,
request, request,
"index.html", "index.html",
{"request": request, "objects": outbox_objects}, {
"request": request,
"objects": outbox_objects,
"current_page": page,
"has_next_page": page_offset + len(outbox_objects) < total_count,
"has_previous_page": page > 1,
},
) )
@ -369,6 +381,33 @@ def outbox(
) )
@app.get("/featured")
def featured(
db: Session = Depends(get_db),
_: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
) -> ActivityPubResponse:
outbox_objects = (
db.query(models.OutboxObject)
.filter(
models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC,
models.OutboxObject.is_deleted.is_(False),
models.OutboxObject.is_pinned.is_(True),
)
.order_by(models.OutboxObject.ap_published_at.desc())
.limit(5)
.all()
)
return ActivityPubResponse(
{
"@context": DEFAULT_CTX,
"id": f"{ID}/featured",
"type": "OrderedCollection",
"totalItems": len(outbox_objects),
"orderedItems": [ap.remove_context(a.ap_object) for a in outbox_objects],
}
)
@app.get("/o/{public_id}") @app.get("/o/{public_id}")
def outbox_by_public_id( def outbox_by_public_id(
public_id: str, public_id: str,
@ -499,7 +538,10 @@ def post_remote_follow(
@app.get("/.well-known/webfinger") @app.get("/.well-known/webfinger")
def wellknown_webfinger(resource: str) -> JSONResponse: def wellknown_webfinger(resource: str) -> JSONResponse:
"""Exposes/servers WebFinger data.""" """Exposes/servers WebFinger data."""
omg = f"acct:{USERNAME}@{DOMAIN}"
logger.info(f"{resource == omg}/{resource}/{omg}/{len(resource)}/{len(omg)}")
if resource not in [f"acct:{USERNAME}@{DOMAIN}", ID]: if resource not in [f"acct:{USERNAME}@{DOMAIN}", ID]:
logger.info(f"Got invalid req for {resource}")
raise HTTPException(status_code=404) raise HTTPException(status_code=404)
out = { out = {
@ -651,6 +693,8 @@ def serve_proxy_media_resized(
try: try:
out = BytesIO(proxy_resp.content) out = BytesIO(proxy_resp.content)
i = Image.open(out) i = Image.open(out)
if i.is_animated:
raise ValueError
i.thumbnail((size, size)) i.thumbnail((size, size))
resized_buf = BytesIO() resized_buf = BytesIO()
i.save(resized_buf, format=i.format) i.save(resized_buf, format=i.format)
@ -660,6 +704,11 @@ def serve_proxy_media_resized(
media_type=i.get_format_mimetype(), # type: ignore media_type=i.get_format_mimetype(), # type: ignore
headers=proxy_resp_headers, headers=proxy_resp_headers,
) )
except ValueError:
return PlainTextResponse(
proxy_resp.content,
headers=proxy_resp_headers,
)
except Exception: except Exception:
logger.exception(f"Failed to resize {url} on the fly") logger.exception(f"Failed to resize {url} on the fly")
return PlainTextResponse( return PlainTextResponse(

View file

@ -156,6 +156,9 @@ class OutboxObject(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)
# For the featured collection
is_pinned = Column(Boolean, nullable=False, default=False)
# Never actually delete from the outbox # Never actually delete from the outbox
is_deleted = Column(Boolean, nullable=False, default=False) is_deleted = Column(Boolean, nullable=False, default=False)
@ -181,6 +184,17 @@ class OutboxObject(Base, BaseObject):
remote_side=id, remote_side=id,
uselist=False, uselist=False,
) )
# For Follow activies
relates_to_actor_id = Column(
Integer,
ForeignKey("actor.id"),
nullable=True,
)
relates_to_actor: Mapped[Optional["Actor"]] = relationship(
"Actor",
foreign_keys=[relates_to_actor_id],
uselist=False,
)
undone_by_outbox_object_id = Column(Integer, ForeignKey("outbox.id"), nullable=True) undone_by_outbox_object_id = Column(Integer, ForeignKey("outbox.id"), nullable=True)

View file

@ -140,3 +140,6 @@ nav.flexbox {
float: right; float: right;
} }
} }
.custom-emoji {
max-width: 25px;
}

View file

@ -163,11 +163,14 @@ def _update_inline_imgs(content):
def _clean_html(html: str, note: Object) -> str: def _clean_html(html: str, note: Object) -> str:
try: try:
return bleach.clean( return _replace_custom_emojis(
_replace_custom_emojis(_update_inline_imgs(highlight(html)), note), bleach.clean(
_update_inline_imgs(highlight(html)),
tags=ALLOWED_TAGS, tags=ALLOWED_TAGS,
attributes=ALLOWED_ATTRIBUTES, attributes=ALLOWED_ATTRIBUTES,
strip=True, strip=True,
),
note,
) )
except Exception: except Exception:
raise raise
@ -197,7 +200,7 @@ def _pluralize(count: int, singular: str = "", plural: str = "s") -> str:
def _replace_custom_emojis(content: str, note: Object) -> str: def _replace_custom_emojis(content: str, note: Object) -> str:
idx = {} idx = {}
for tag in note.ap_object.get("tag", []): for tag in note.tags:
if tag.get("type") == "Emoji": if tag.get("type") == "Emoji":
try: try:
idx[tag["name"]] = proxied_media_url(tag["icon"]["url"]) idx[tag["name"]] = proxied_media_url(tag["icon"]["url"])

View file

@ -10,7 +10,14 @@
<form action="{{ request.url_for("admin_actions_new") }}" enctype="multipart/form-data" method="POST"> <form action="{{ request.url_for("admin_actions_new") }}" enctype="multipart/form-data" method="POST">
{{ utils.embed_csrf_token() }} {{ utils.embed_csrf_token() }}
{{ utils.embed_redirect_url() }} {{ utils.embed_redirect_url() }}
<textarea name="content" rows="10" cols="50" autofocus="autofocus" designMode="on" style="font-size:1.2em;width:95%;"></textarea> <p>
<select name="visibility">
{% for (k, v) in visibility_enum %}
<option value="{{ k }}">{{ v }}</option>
{% endfor %}
</select>
</p>
<textarea name="content" rows="10" cols="50" autofocus="autofocus" designMode="on" style="font-size:1.2em;width:95%;">{{ content }}</textarea>
<input type="hidden" name="in_reply_to" value="{{ request.query_params.in_reply_to }}"> <input type="hidden" name="in_reply_to" value="{{ request.query_params.in_reply_to }}">
<p> <p>
<input name="files" type="file" multiple> <input name="files" type="file" multiple>

View file

@ -2,10 +2,29 @@
{% extends "layout.html" %} {% extends "layout.html" %}
{% block content %} {% block content %}
<p>Filter by
{% for ap_type in ["Note", "Like", "Announce", "Follow"] %}
<a style="margin-right:12px;" href="{{ url_for("admin_outbox") }}?filter_by={{ ap_type }}">
{% if request.query_params.filter_by == ap_type %}
<strong>{{ ap_type }}</strong>
{% else %}
{{ ap_type }}
{% endif %}</a>
{% endfor %}.
{% if request.query_params.filter_by %}<a href="{{ url_for("admin_outbox") }}">Reset filter</a>{% endif %}</p>
</p>
{% for outbox_object in outbox %} {% for outbox_object in outbox %}
{% if outbox_object.ap_type == "Announce" %} {% if outbox_object.ap_type == "Announce" %}
<div class="actor-action">You shared</div>
{{ utils.display_object(outbox_object.relates_to_anybox_object) }} {{ utils.display_object(outbox_object.relates_to_anybox_object) }}
{% elif outbox_object.ap_type == "Like" %}
<div class="actor-action">You liked</div>
{{ utils.display_object(outbox_object.relates_to_anybox_object) }}
{% elif outbox_object.ap_type == "Follow" %}
<div class="actor-action">You followed</div>
{{ utils.display_actor(outbox_object.relates_to_actor, actors_metadata) }}
{% elif outbox_object.ap_type in ["Article", "Note", "Video"] %} {% elif outbox_object.ap_type in ["Article", "Note", "Video"] %}
{{ utils.display_object(outbox_object) }} {{ utils.display_object(outbox_object) }}
{% else %} {% else %}

View file

@ -24,6 +24,7 @@
<li>{{ header_link("index", "Notes") }}</li> <li>{{ header_link("index", "Notes") }}</li>
<li>{{ header_link("followers", "Followers") }} <span>{{ followers_count }}</span></li> <li>{{ header_link("followers", "Followers") }} <span>{{ followers_count }}</span></li>
<li>{{ header_link("following", "Following") }} <span>{{ following_count }}</span></li> <li>{{ header_link("following", "Following") }} <span>{{ following_count }}</span></li>
<li>{{ header_link("get_remote_follow", "Remote follow") }}</li>
</ul> </ul>
</nav> </nav>

View file

@ -7,6 +7,12 @@
{{ utils.display_object(outbox_object) }} {{ utils.display_object(outbox_object) }}
{% endfor %} {% endfor %}
{% if has_previous_page %}
<a href="{{ url_for("index") }}?page={{ current_page - 1 }}">Previous</a>
{% endif %}
{% if has_next_page %}
<a href="{{ url_for("index") }}?page={{ current_page + 1 }}">Next</a>
{% endif %}
{% endblock %} {% endblock %}

View file

@ -24,7 +24,6 @@
<li>Admin</li> <li>Admin</li>
<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("admin_inbox", "Inbox") }}/{{ admin_link("admin_outbox", "Outbox") }}</li> <li>{{ admin_link("admin_inbox", "Inbox") }}/{{ 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>

View file

@ -42,6 +42,24 @@
</form> </form>
{% endmacro %} {% endmacro %}
{% macro admin_pin_button(ap_object_id) %}
<form action="{{ request.url_for("admin_actions_pin") }}" method="POST">
{{ embed_csrf_token() }}
{{ embed_redirect_url() }}
<input type="hidden" name="ap_object_id" value="{{ ap_object_id }}">
<input type="submit" value="Pin">
</form>
{% endmacro %}
{% macro admin_unpin_button(ap_object_id) %}
<form action="{{ request.url_for("admin_actions_unpin") }}" method="POST">
{{ embed_csrf_token() }}
{{ embed_redirect_url() }}
<input type="hidden" name="ap_object_id" value="{{ ap_object_id }}">
<input type="submit" value="Unpin">
</form>
{% endmacro %}
{% macro admin_announce_button(ap_object_id) %} {% macro admin_announce_button(ap_object_id) %}
<form action="{{ request.url_for("admin_actions_announce") }}" method="POST"> <form action="{{ request.url_for("admin_actions_announce") }}" method="POST">
{{ embed_csrf_token() }} {{ embed_csrf_token() }}
@ -98,7 +116,7 @@
<img src="{{ actor.resized_icon_url }}" 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.display_name | clean_html(actor) | safe }}</strong></div>
<div>{{ actor.handle }}</div> <div>{{ actor.handle }}</div>
</a> </a>
</div> </div>
@ -156,7 +174,7 @@
<div class="activity-content"> <div class="activity-content">
<img src="{{ object.actor.resized_icon_url }}" 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.display_name }}</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.value }} {{ object.visibility.value }}
@ -206,8 +224,14 @@
<div class="bar-item"> <div class="bar-item">
{{ admin_reply_button(object.ap_id) }} {{ admin_reply_button(object.ap_id) }}
</div> </div>
<div class="bar-item">
{% if object.is_pinned %}
{{ admin_unpin_button(object.ap_id) }}
{% else %}
{{ admin_pin_button(object.ap_id) }}
{% endif %}
</div>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if object.is_from_inbox %} {% if object.is_from_inbox %}