forked from forks/microblog.pub
Improved replies support
This commit is contained in:
parent
7293160b6f
commit
baceb6be6c
11 changed files with 67 additions and 17 deletions
17
app/admin.py
17
app/admin.py
|
@ -93,13 +93,20 @@ def get_lookup(
|
||||||
def admin_new(
|
def admin_new(
|
||||||
request: Request,
|
request: Request,
|
||||||
query: str | None = None,
|
query: str | None = None,
|
||||||
|
in_reply_to: str | None = None,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
) -> templates.TemplateResponse:
|
) -> templates.TemplateResponse:
|
||||||
|
in_reply_to_object = None
|
||||||
|
if in_reply_to:
|
||||||
|
in_reply_to_object = boxes.get_anybox_object_by_ap_id(db, in_reply_to)
|
||||||
|
if not in_reply_to_object:
|
||||||
|
raise ValueError(f"Unknown object {in_reply_to=}")
|
||||||
|
|
||||||
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},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -237,6 +244,7 @@ def admin_actions_new(
|
||||||
files: list[UploadFile],
|
files: list[UploadFile],
|
||||||
content: str = Form(),
|
content: str = Form(),
|
||||||
redirect_url: str = Form(),
|
redirect_url: str = Form(),
|
||||||
|
in_reply_to: str | None = 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:
|
||||||
|
@ -246,7 +254,12 @@ def admin_actions_new(
|
||||||
for f in files:
|
for f in files:
|
||||||
upload = save_upload(db, f)
|
upload = save_upload(db, f)
|
||||||
uploads.append((upload, f.filename))
|
uploads.append((upload, f.filename))
|
||||||
public_id = boxes.send_create(db, source=content, uploads=uploads)
|
public_id = boxes.send_create(
|
||||||
|
db,
|
||||||
|
source=content,
|
||||||
|
uploads=uploads,
|
||||||
|
in_reply_to=in_reply_to or None,
|
||||||
|
)
|
||||||
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),
|
||||||
status_code=302,
|
status_code=302,
|
||||||
|
|
|
@ -54,15 +54,14 @@ class Object:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def context(self) -> str | None:
|
def context(self) -> str | None:
|
||||||
return self.ap_object.get("context")
|
return self.ap_object.get("context") or self.ap_object.get("conversation")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sensitive(self) -> bool:
|
def sensitive(self) -> bool:
|
||||||
return self.ap_object.get("sensitive", False)
|
return self.ap_object.get("sensitive", False)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def attachments_old(self) -> list["Attachment"]:
|
def attachments(self) -> list["Attachment"]:
|
||||||
# TODO: set img_src with the proxy URL (proxy_url?)
|
|
||||||
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_url(obj["url"])
|
||||||
|
|
30
app/boxes.py
30
app/boxes.py
|
@ -20,10 +20,12 @@ from app.ap_object import RemoteObject
|
||||||
from app.config import BASE_URL
|
from app.config import BASE_URL
|
||||||
from app.config import ID
|
from app.config import ID
|
||||||
from app.database import now
|
from app.database import now
|
||||||
from app.process_outgoing_activities import new_outgoing_activity
|
from app.outgoing_activities import new_outgoing_activity
|
||||||
from app.source import markdownify
|
from app.source import markdownify
|
||||||
from app.uploads import upload_to_attachment
|
from app.uploads import upload_to_attachment
|
||||||
|
|
||||||
|
AnyboxObject = models.InboxObject | models.OutboxObject
|
||||||
|
|
||||||
|
|
||||||
def allocate_outbox_id() -> str:
|
def allocate_outbox_id() -> str:
|
||||||
return uuid.uuid4().hex
|
return uuid.uuid4().hex
|
||||||
|
@ -219,6 +221,7 @@ def send_create(
|
||||||
db: Session,
|
db: Session,
|
||||||
source: str,
|
source: str,
|
||||||
uploads: list[tuple[models.Upload, str]],
|
uploads: list[tuple[models.Upload, str]],
|
||||||
|
in_reply_to: str | None,
|
||||||
) -> 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")
|
||||||
|
@ -226,6 +229,14 @@ def send_create(
|
||||||
content, tags = markdownify(db, source)
|
content, tags = markdownify(db, source)
|
||||||
attachments = []
|
attachments = []
|
||||||
|
|
||||||
|
if in_reply_to:
|
||||||
|
in_reply_to_object = get_anybox_object_by_ap_id(db, in_reply_to)
|
||||||
|
if not in_reply_to_object:
|
||||||
|
raise ValueError(f"Invalid in reply to {in_reply_to=}")
|
||||||
|
if not in_reply_to_object.context:
|
||||||
|
raise ValueError("Object has no context")
|
||||||
|
context = in_reply_to_object.context
|
||||||
|
|
||||||
for (upload, filename) in uploads:
|
for (upload, filename) in uploads:
|
||||||
attachments.append(upload_to_attachment(upload, filename))
|
attachments.append(upload_to_attachment(upload, filename))
|
||||||
|
|
||||||
|
@ -243,7 +254,7 @@ def send_create(
|
||||||
"url": outbox_object_id(note_id),
|
"url": outbox_object_id(note_id),
|
||||||
"tag": tags,
|
"tag": tags,
|
||||||
"summary": None,
|
"summary": None,
|
||||||
"inReplyTo": None,
|
"inReplyTo": in_reply_to,
|
||||||
"sensitive": False,
|
"sensitive": False,
|
||||||
"attachment": attachments,
|
"attachment": attachments,
|
||||||
}
|
}
|
||||||
|
@ -331,6 +342,13 @@ def get_outbox_object_by_ap_id(db: Session, ap_id: str) -> models.OutboxObject |
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_anybox_object_by_ap_id(db: Session, ap_id: str) -> AnyboxObject | None:
|
||||||
|
if ap_id.startswith(BASE_URL):
|
||||||
|
return get_outbox_object_by_ap_id(db, ap_id)
|
||||||
|
else:
|
||||||
|
return get_inbox_object_by_ap_id(db, ap_id)
|
||||||
|
|
||||||
|
|
||||||
def _handle_delete_activity(
|
def _handle_delete_activity(
|
||||||
db: Session,
|
db: Session,
|
||||||
from_actor: models.Actor,
|
from_actor: models.Actor,
|
||||||
|
@ -538,14 +556,18 @@ def save_to_inbox(db: Session, raw_object: ap.RawObject) -> None:
|
||||||
else None,
|
else None,
|
||||||
activity_object_ap_id=ra.activity_object_ap_id,
|
activity_object_ap_id=ra.activity_object_ap_id,
|
||||||
# Hide replies from the stream
|
# Hide replies from the stream
|
||||||
is_hidden_from_stream=True if ra.in_reply_to else False,
|
is_hidden_from_stream=(
|
||||||
|
True
|
||||||
|
if (ra.in_reply_to and not ra.in_reply_to.startswith(BASE_URL))
|
||||||
|
else False
|
||||||
|
), # TODO: handle mentions
|
||||||
)
|
)
|
||||||
|
|
||||||
db.add(inbox_object)
|
db.add(inbox_object)
|
||||||
db.flush()
|
db.flush()
|
||||||
db.refresh(inbox_object)
|
db.refresh(inbox_object)
|
||||||
|
|
||||||
if ra.ap_type == "Create":
|
if ra.ap_type == "Note": # TODO: handle create better
|
||||||
_handle_create_activity(db, actor, inbox_object)
|
_handle_create_activity(db, actor, inbox_object)
|
||||||
elif ra.ap_type == "Update":
|
elif ra.ap_type == "Update":
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -63,6 +63,7 @@ class InboxObject(Base, BaseObject):
|
||||||
ap_published_at = Column(DateTime(timezone=True), nullable=False)
|
ap_published_at = Column(DateTime(timezone=True), nullable=False)
|
||||||
ap_object: Mapped[ap.RawObject] = Column(JSON, nullable=False)
|
ap_object: Mapped[ap.RawObject] = Column(JSON, nullable=False)
|
||||||
|
|
||||||
|
# Only set for activities
|
||||||
activity_object_ap_id = Column(String, nullable=True)
|
activity_object_ap_id = Column(String, nullable=True)
|
||||||
|
|
||||||
visibility = Column(Enum(ap.VisibilityEnum), nullable=False)
|
visibility = Column(Enum(ap.VisibilityEnum), nullable=False)
|
||||||
|
@ -242,8 +243,6 @@ class NotificationType(str, enum.Enum):
|
||||||
UNDO_LIKE = "undo_like"
|
UNDO_LIKE = "undo_like"
|
||||||
ANNOUNCE = "announce"
|
ANNOUNCE = "announce"
|
||||||
UNDO_ANNOUNCE = "undo_announce"
|
UNDO_ANNOUNCE = "undo_announce"
|
||||||
|
|
||||||
# TODO:
|
|
||||||
MENTION = "mention"
|
MENTION = "mention"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,16 @@
|
||||||
{% extends "layout.html" %}
|
{% extends "layout.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
In reply to:
|
||||||
|
{% if in_reply_to_object %}
|
||||||
|
{{ utils.display_object(in_reply_to_object) }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<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>
|
<textarea name="content" rows="10" cols="50" autofocus="autofocus" designMode="on" style="font-size:1.2em;width:95%;"></textarea>
|
||||||
|
<input type="hidden" name="in_reply_to" value="{{ request.query_params.in_reply_to }}">
|
||||||
<input name="files" type="file" multiple>
|
<input name="files" type="file" multiple>
|
||||||
<input type="submit" value="Publish">
|
<input type="submit" value="Publish">
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
|
|
||||||
{{ utils.admin_announce_button(inbox_object.ap_id) }}
|
{{ utils.admin_announce_button(inbox_object.ap_id) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{{ utils.admin_reply_button(inbox_object.ap_id) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -15,8 +15,8 @@
|
||||||
{% if is_admin %}
|
{% if is_admin %}
|
||||||
<div id="admin">
|
<div id="admin">
|
||||||
{% macro admin_link(url, text) %}
|
{% macro admin_link(url, text) %}
|
||||||
{% set url_for = request.url_for(url) %}
|
{% set url_for = request.app.router.url_path_for(url) %}
|
||||||
<a href="{{ url_for }}" {% if request.url == url_for %}class="active"{% endif %}>{{ text }}</a>
|
<a href="{{ url_for }}" {% if request.url.path == url_for %}class="active"{% endif %}>{{ text }}</a>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
<div style="margin-bottom:30px;">
|
<div style="margin-bottom:30px;">
|
||||||
<nav class="flexbox">
|
<nav class="flexbox">
|
||||||
|
|
|
@ -52,6 +52,13 @@
|
||||||
</form>
|
</form>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro admin_reply_button(ap_object_id) %}
|
||||||
|
<form action="/admin/new" method="GET">
|
||||||
|
<input type="hidden" name="in_reply_to" value="{{ ap_object_id }}">
|
||||||
|
<button type="submit">Reply</button>
|
||||||
|
</form>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro display_actor(actor, actors_metadata) %}
|
{% macro display_actor(actor, actors_metadata) %}
|
||||||
{{ actors_metadata }}
|
{{ actors_metadata }}
|
||||||
{% set metadata = actors_metadata.get(actor.ap_id) %}
|
{% set metadata = actors_metadata.get(actor.ap_id) %}
|
||||||
|
@ -95,6 +102,7 @@
|
||||||
<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 }}
|
||||||
<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">
|
||||||
|
|
4
tasks.py
4
tasks.py
|
@ -51,7 +51,9 @@ def uvicorn(ctx):
|
||||||
@task
|
@task
|
||||||
def process_outgoing_activities(ctx):
|
def process_outgoing_activities(ctx):
|
||||||
# type: (Context) -> None
|
# type: (Context) -> None
|
||||||
run("poetry run python app/process_outgoing_activities.py", pty=True, echo=True)
|
from app.outgoing_activities import loop
|
||||||
|
|
||||||
|
loop()
|
||||||
|
|
||||||
|
|
||||||
@task
|
@task
|
||||||
|
|
|
@ -8,9 +8,9 @@ from app import models
|
||||||
from app.actor import LOCAL_ACTOR
|
from app.actor import LOCAL_ACTOR
|
||||||
from app.ap_object import RemoteObject
|
from app.ap_object import RemoteObject
|
||||||
from app.database import Session
|
from app.database import Session
|
||||||
from app.process_outgoing_activities import _MAX_RETRIES
|
from app.outgoing_activities import _MAX_RETRIES
|
||||||
from app.process_outgoing_activities import new_outgoing_activity
|
from app.outgoing_activities import new_outgoing_activity
|
||||||
from app.process_outgoing_activities import process_next_outgoing_activity
|
from app.outgoing_activities import process_next_outgoing_activity
|
||||||
from tests import factories
|
from tests import factories
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue