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
|
"""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'], ),
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
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 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),
|
||||||
|
|
|
@ -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 = []
|
||||||
|
|
30
app/boxes.py
30
app/boxes.py
|
@ -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,
|
||||||
|
|
67
app/main.py
67
app/main.py
|
@ -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)
|
||||||
|
|
||||||
|
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 = (
|
outbox_objects = (
|
||||||
db.query(models.OutboxObject)
|
q.options(
|
||||||
.options(
|
|
||||||
joinedload(models.OutboxObject.outbox_object_attachments).options(
|
joinedload(models.OutboxObject.outbox_object_attachments).options(
|
||||||
joinedload(models.OutboxObjectAttachment.upload)
|
joinedload(models.OutboxObjectAttachment.upload)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.filter(
|
|
||||||
models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC,
|
|
||||||
models.OutboxObject.is_deleted.is_(False),
|
|
||||||
models.OutboxObject.is_hidden_from_homepage.is_(False),
|
|
||||||
)
|
|
||||||
.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(
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -140,3 +140,6 @@ nav.flexbox {
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.custom-emoji {
|
||||||
|
max-width: 25px;
|
||||||
|
}
|
||||||
|
|
|
@ -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(
|
||||||
tags=ALLOWED_TAGS,
|
_update_inline_imgs(highlight(html)),
|
||||||
attributes=ALLOWED_ATTRIBUTES,
|
tags=ALLOWED_TAGS,
|
||||||
strip=True,
|
attributes=ALLOWED_ATTRIBUTES,
|
||||||
|
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"])
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -2,15 +2,34 @@
|
||||||
{% 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 %}
|
||||||
Implement {{ outbox_object.ap_type }}
|
Implement {{ outbox_object.ap_type }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
Loading…
Reference in a new issue