forked from forks/microblog.pub
Improved audience support and implement featured collection
This commit is contained in:
parent
ff8975acab
commit
4bf54c7040
16 changed files with 284 additions and 37 deletions
|
@ -1,8 +1,8 @@
|
|||
"""Initial migration
|
||||
|
||||
Revision ID: 714b4a5307c7
|
||||
Revision ID: ba131b14c3a1
|
||||
Revises:
|
||||
Create Date: 2022-06-23 18:42:56.009810
|
||||
Create Date: 2022-06-26 14:36:44.107422
|
||||
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
|
@ -10,7 +10,7 @@ import sqlalchemy as sa
|
|||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '714b4a5307c7'
|
||||
revision = 'ba131b14c3a1'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
@ -81,10 +81,13 @@ def upgrade() -> None:
|
|||
sa.Column('replies_count', sa.Integer(), nullable=False),
|
||||
sa.Column('webmentions', 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('relates_to_inbox_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.ForeignKeyConstraint(['relates_to_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_outbox_object_id'], ['outbox.id'], ),
|
|
@ -1,6 +1,7 @@
|
|||
import enum
|
||||
import json
|
||||
import mimetypes
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
@ -10,6 +11,9 @@ from app.config import AP_CONTENT_TYPE # noqa: F401
|
|||
from app.httpsig import auth
|
||||
from app.key import get_pubkey_as_pem
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.actor import Actor
|
||||
|
||||
RawObject = dict[str, Any]
|
||||
AS_CTX = "https://www.w3.org/ns/activitystreams"
|
||||
AS_PUBLIC = "https://www.w3.org/ns/activitystreams#Public"
|
||||
|
@ -24,8 +28,18 @@ class ObjectIsGoneError(Exception):
|
|||
class VisibilityEnum(str, enum.Enum):
|
||||
PUBLIC = "public"
|
||||
UNLISTED = "unlisted"
|
||||
FOLLOWERS_ONLY = "followers-only"
|
||||
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 = {
|
||||
"@context": [
|
||||
|
@ -70,7 +84,7 @@ ME = {
|
|||
"id": config.ID,
|
||||
"following": config.BASE_URL + "/following",
|
||||
"followers": config.BASE_URL + "/followers",
|
||||
# "featured": ID + "/featured",
|
||||
"featured": config.BASE_URL + "/featured",
|
||||
"inbox": config.BASE_URL + "/inbox",
|
||||
"outbox": config.BASE_URL + "/outbox",
|
||||
"preferredUsername": config.USERNAME,
|
||||
|
@ -198,13 +212,15 @@ def get_id(val: str | dict[str, Any]) -> str:
|
|||
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", []))
|
||||
cc = as_list(ap_activity.get("cc", []))
|
||||
if AS_PUBLIC in to:
|
||||
return VisibilityEnum.PUBLIC
|
||||
elif AS_PUBLIC in cc:
|
||||
return VisibilityEnum.UNLISTED
|
||||
elif actor.followers_collection_id in to + cc:
|
||||
return VisibilityEnum.FOLLOWERS_ONLY
|
||||
else:
|
||||
return VisibilityEnum.DIRECT
|
||||
|
||||
|
|
|
@ -97,6 +97,14 @@ class Actor:
|
|||
else:
|
||||
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):
|
||||
def __init__(self, ap_actor: ap.RawObject) -> None:
|
||||
|
|
77
app/admin.py
77
app/admin.py
|
@ -13,8 +13,10 @@ from app import activitypub as ap
|
|||
from app import boxes
|
||||
from app import models
|
||||
from app import templates
|
||||
from app.actor import LOCAL_ACTOR
|
||||
from app.actor import get_actors_metadata
|
||||
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.config import generate_csrf_token
|
||||
from app.config import session_serializer
|
||||
|
@ -96,17 +98,32 @@ def admin_new(
|
|||
in_reply_to: str | None = None,
|
||||
db: Session = Depends(get_db),
|
||||
) -> templates.TemplateResponse:
|
||||
content = ""
|
||||
in_reply_to_object = None
|
||||
if 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:
|
||||
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(
|
||||
db,
|
||||
request,
|
||||
"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")
|
||||
def admin_outbox(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
request: Request, db: Session = Depends(get_db), filter_by: str | None = None
|
||||
) -> 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 = (
|
||||
db.query(models.OutboxObject)
|
||||
.options(
|
||||
q.options(
|
||||
joinedload(models.OutboxObject.relates_to_inbox_object),
|
||||
joinedload(models.OutboxObject.relates_to_outbox_object),
|
||||
joinedload(models.OutboxObject.relates_to_actor),
|
||||
)
|
||||
.order_by(models.OutboxObject.ap_published_at.desc())
|
||||
.limit(20)
|
||||
.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(
|
||||
db,
|
||||
request,
|
||||
"admin_outbox.html",
|
||||
{
|
||||
"actors_metadata": actors_metadata,
|
||||
"outbox": outbox,
|
||||
},
|
||||
)
|
||||
|
@ -288,6 +320,7 @@ def admin_profile(
|
|||
models.InboxObject.actor_id == actor.id,
|
||||
models.InboxObject.ap_type.in_(["Note", "Article", "Video"]),
|
||||
)
|
||||
.order_by(models.InboxObject.ap_published_at.desc())
|
||||
.all()
|
||||
)
|
||||
|
||||
|
@ -384,6 +417,38 @@ def admin_actions_unbookmark(
|
|||
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")
|
||||
def admin_actions_new(
|
||||
request: Request,
|
||||
|
@ -391,6 +456,7 @@ def admin_actions_new(
|
|||
content: str = Form(),
|
||||
redirect_url: str = Form(),
|
||||
in_reply_to: str | None = Form(None),
|
||||
visibility: str = Form(),
|
||||
csrf_check: None = Depends(verify_csrf_token),
|
||||
db: Session = Depends(get_db),
|
||||
) -> RedirectResponse:
|
||||
|
@ -405,6 +471,7 @@ def admin_actions_new(
|
|||
source=content,
|
||||
uploads=uploads,
|
||||
in_reply_to=in_reply_to or None,
|
||||
visibility=ap.VisibilityEnum[visibility],
|
||||
)
|
||||
return RedirectResponse(
|
||||
request.url_for("outbox_by_public_id", public_id=public_id),
|
||||
|
|
|
@ -58,7 +58,7 @@ class Object:
|
|||
|
||||
@property
|
||||
def visibility(self) -> ap.VisibilityEnum:
|
||||
return ap.object_visibility(self.ap_object)
|
||||
return ap.object_visibility(self.ap_object, self.actor)
|
||||
|
||||
@property
|
||||
def ap_context(self) -> str | None:
|
||||
|
@ -68,6 +68,10 @@ class Object:
|
|||
def sensitive(self) -> bool:
|
||||
return self.ap_object.get("sensitive", False)
|
||||
|
||||
@property
|
||||
def tags(self) -> list[ap.RawObject]:
|
||||
return self.ap_object.get("tag", [])
|
||||
|
||||
@property
|
||||
def attachments(self) -> list["Attachment"]:
|
||||
attachments = []
|
||||
|
|
30
app/boxes.py
30
app/boxes.py
|
@ -43,6 +43,7 @@ def save_outbox_object(
|
|||
raw_object: ap.RawObject,
|
||||
relates_to_inbox_object_id: int | None = None,
|
||||
relates_to_outbox_object_id: int | None = None,
|
||||
relates_to_actor_id: int | None = None,
|
||||
source: str | None = None,
|
||||
) -> models.OutboxObject:
|
||||
ra = RemoteObject(raw_object)
|
||||
|
@ -57,6 +58,7 @@ def save_outbox_object(
|
|||
og_meta=ra.og_meta,
|
||||
relates_to_inbox_object_id=relates_to_inbox_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,
|
||||
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,
|
||||
}
|
||||
|
||||
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:
|
||||
raise ValueError("Should never happen")
|
||||
|
||||
|
@ -224,6 +228,7 @@ def send_create(
|
|||
source: str,
|
||||
uploads: list[tuple[models.Upload, str]],
|
||||
in_reply_to: str | None,
|
||||
visibility: ap.VisibilityEnum,
|
||||
) -> str:
|
||||
note_id = allocate_outbox_id()
|
||||
published = now().replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
||||
|
@ -247,14 +252,33 @@ def send_create(
|
|||
for (upload, filename) in uploads:
|
||||
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 = {
|
||||
"@context": ap.AS_CTX,
|
||||
"type": "Note",
|
||||
"id": outbox_object_id(note_id),
|
||||
"attributedTo": ID,
|
||||
"content": content,
|
||||
"to": [ap.AS_PUBLIC],
|
||||
"cc": [f"{BASE_URL}/followers"],
|
||||
"to": to,
|
||||
"cc": cc,
|
||||
"published": published,
|
||||
"context": context,
|
||||
"conversation": context,
|
||||
|
|
69
app/main.py
69
app/main.py
|
@ -158,24 +158,30 @@ def index(
|
|||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
_: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
|
||||
page: int | None = None,
|
||||
) -> templates.TemplateResponse | ActivityPubResponse:
|
||||
if is_activitypub_requested(request):
|
||||
return ActivityPubResponse(LOCAL_ACTOR.ap_actor)
|
||||
|
||||
outbox_objects = (
|
||||
db.query(models.OutboxObject)
|
||||
.options(
|
||||
joinedload(models.OutboxObject.outbox_object_attachments).options(
|
||||
joinedload(models.OutboxObjectAttachment.upload)
|
||||
)
|
||||
)
|
||||
.filter(
|
||||
page = page or 1
|
||||
q = db.query(models.OutboxObject).filter(
|
||||
models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC,
|
||||
models.OutboxObject.is_deleted.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())
|
||||
.limit(20)
|
||||
.offset(page_offset)
|
||||
.limit(page_size)
|
||||
.all()
|
||||
)
|
||||
|
||||
|
@ -183,7 +189,13 @@ def index(
|
|||
db,
|
||||
request,
|
||||
"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}")
|
||||
def outbox_by_public_id(
|
||||
public_id: str,
|
||||
|
@ -499,7 +538,10 @@ def post_remote_follow(
|
|||
@app.get("/.well-known/webfinger")
|
||||
def wellknown_webfinger(resource: str) -> JSONResponse:
|
||||
"""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]:
|
||||
logger.info(f"Got invalid req for {resource}")
|
||||
raise HTTPException(status_code=404)
|
||||
|
||||
out = {
|
||||
|
@ -651,6 +693,8 @@ def serve_proxy_media_resized(
|
|||
try:
|
||||
out = BytesIO(proxy_resp.content)
|
||||
i = Image.open(out)
|
||||
if i.is_animated:
|
||||
raise ValueError
|
||||
i.thumbnail((size, size))
|
||||
resized_buf = BytesIO()
|
||||
i.save(resized_buf, format=i.format)
|
||||
|
@ -660,6 +704,11 @@ def serve_proxy_media_resized(
|
|||
media_type=i.get_format_mimetype(), # type: ignore
|
||||
headers=proxy_resp_headers,
|
||||
)
|
||||
except ValueError:
|
||||
return PlainTextResponse(
|
||||
proxy_resp.content,
|
||||
headers=proxy_resp_headers,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception(f"Failed to resize {url} on the fly")
|
||||
return PlainTextResponse(
|
||||
|
|
|
@ -156,6 +156,9 @@ class OutboxObject(Base, BaseObject):
|
|||
|
||||
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
|
||||
is_deleted = Column(Boolean, nullable=False, default=False)
|
||||
|
||||
|
@ -181,6 +184,17 @@ class OutboxObject(Base, BaseObject):
|
|||
remote_side=id,
|
||||
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)
|
||||
|
||||
|
|
|
@ -140,3 +140,6 @@ nav.flexbox {
|
|||
float: right;
|
||||
}
|
||||
}
|
||||
.custom-emoji {
|
||||
max-width: 25px;
|
||||
}
|
||||
|
|
|
@ -163,11 +163,14 @@ def _update_inline_imgs(content):
|
|||
|
||||
def _clean_html(html: str, note: Object) -> str:
|
||||
try:
|
||||
return bleach.clean(
|
||||
_replace_custom_emojis(_update_inline_imgs(highlight(html)), note),
|
||||
return _replace_custom_emojis(
|
||||
bleach.clean(
|
||||
_update_inline_imgs(highlight(html)),
|
||||
tags=ALLOWED_TAGS,
|
||||
attributes=ALLOWED_ATTRIBUTES,
|
||||
strip=True,
|
||||
),
|
||||
note,
|
||||
)
|
||||
except Exception:
|
||||
raise
|
||||
|
@ -197,7 +200,7 @@ def _pluralize(count: int, singular: str = "", plural: str = "s") -> str:
|
|||
|
||||
def _replace_custom_emojis(content: str, note: Object) -> str:
|
||||
idx = {}
|
||||
for tag in note.ap_object.get("tag", []):
|
||||
for tag in note.tags:
|
||||
if tag.get("type") == "Emoji":
|
||||
try:
|
||||
idx[tag["name"]] = proxied_media_url(tag["icon"]["url"])
|
||||
|
|
|
@ -10,7 +10,14 @@
|
|||
<form action="{{ request.url_for("admin_actions_new") }}" enctype="multipart/form-data" method="POST">
|
||||
{{ utils.embed_csrf_token() }}
|
||||
{{ 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 }}">
|
||||
<p>
|
||||
<input name="files" type="file" multiple>
|
||||
|
|
|
@ -2,10 +2,29 @@
|
|||
{% extends "layout.html" %}
|
||||
{% 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 %}
|
||||
|
||||
{% if outbox_object.ap_type == "Announce" %}
|
||||
<div class="actor-action">You shared</div>
|
||||
{{ 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"] %}
|
||||
{{ utils.display_object(outbox_object) }}
|
||||
{% else %}
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
<li>{{ header_link("index", "Notes") }}</li>
|
||||
<li>{{ header_link("followers", "Followers") }} <span>{{ followers_count }}</span></li>
|
||||
<li>{{ header_link("following", "Following") }} <span>{{ following_count }}</span></li>
|
||||
<li>{{ header_link("get_remote_follow", "Remote follow") }}</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
|
|
|
@ -7,6 +7,12 @@
|
|||
{{ utils.display_object(outbox_object) }}
|
||||
{% 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 %}
|
||||
|
|
|
@ -24,7 +24,6 @@
|
|||
<li>Admin</li>
|
||||
<li>{{ admin_link("index", "Public") }}</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("get_notifications", "Notifications") }} {% if notifications_count %}({{ notifications_count }}){% endif %}</li>
|
||||
<li>{{ admin_link("get_lookup", "Lookup") }}</li>
|
||||
|
|
|
@ -42,6 +42,24 @@
|
|||
</form>
|
||||
{% 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) %}
|
||||
<form action="{{ request.url_for("admin_actions_announce") }}" method="POST">
|
||||
{{ embed_csrf_token() }}
|
||||
|
@ -98,7 +116,7 @@
|
|||
<img src="{{ actor.resized_icon_url }}" style="max-width:45px;">
|
||||
</div>
|
||||
<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>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -156,7 +174,7 @@
|
|||
<div class="activity-content">
|
||||
<img src="{{ object.actor.resized_icon_url }}" alt="" class="actor-icon">
|
||||
<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 class="activity-date" title="{{ object.ap_published_at.isoformat() }}">
|
||||
{{ object.visibility.value }}
|
||||
|
@ -206,8 +224,14 @@
|
|||
<div class="bar-item">
|
||||
{{ admin_reply_button(object.ap_id) }}
|
||||
</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 %}
|
||||
|
||||
{% if object.is_from_inbox %}
|
||||
|
|
Loading…
Reference in a new issue