~tsileo/microblog.pub-devel

microblog.pub: quote RT and proper mf2 formatting for reposts and attachments. v1 PROPOSED

Ash McAllan: 1
 quote RT and proper mf2 formatting for reposts and attachments.

 7 files changed, 79 insertions(+), 11 deletions(-)
#891737 running .build.yml
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~tsileo/microblog.pub-devel/patches/37150/mbox | git am -3
Learn more about email & git

[PATCH microblog.pub] quote RT and proper mf2 formatting for reposts and attachments. Export this patch

---
 app/admin.py                 |  2 ++
 app/ap_object.py             |  6 ++++++
 app/boxes.py                 | 29 +++++++++++++++++++++++++++++
 app/main.py                  | 14 ++++++++++++++
 app/templates/admin_new.html |  1 +
 app/templates/index.html     |  7 ++-----
 app/templates/utils.html     | 31 +++++++++++++++++++++++++------
 7 files changed, 79 insertions(+), 11 deletions(-)

diff --git a/app/admin.py b/app/admin.py
index e837c46..484aadf 100644
--- a/app/admin.py
+++ b/app/admin.py
@@ -1105,6 +1105,7 @@ async def admin_actions_new(
    in_reply_to: str | None = Form(None),
    content_warning: str | None = Form(None),
    is_sensitive: bool = Form(False),
    is_quote: bool = Form(False),
    visibility: str = Form(),
    poll_type: str | None = Form(None),
    name: str | None = Form(None),
@@ -1163,6 +1164,7 @@ async def admin_actions_new(
        poll_answers=poll_answers,
        poll_duration_in_minutes=poll_duration_in_minutes,
        name=name,
        is_quote=is_quote
    )
    return RedirectResponse(
        request.url_for("outbox_by_public_id", public_id=public_id),
diff --git a/app/ap_object.py b/app/ap_object.py
index 52061b8..cbd878e 100644
--- a/app/ap_object.py
+++ b/app/ap_object.py
@@ -212,6 +212,12 @@ class Object:
    def in_reply_to(self) -> str | None:
        return self.ap_object.get("inReplyTo")

    @property
    def quote_url(self) -> str | None:
        for tag in ap.as_list(self.ap_object.get("tag", [])):
            if tag["type"] == "Link" and tag["name"][:3] == "RE:" and "href" in tag:
                return tag["href"]

    @property
    def is_in_reply_to_from_inbox(self) -> bool | None:
        if not self.in_reply_to:
diff --git a/app/boxes.py b/app/boxes.py
index 19a92f8..d675098 100644
--- a/app/boxes.py
+++ b/app/boxes.py
@@ -561,6 +561,14 @@ async def send_self_destruct(db_session: AsyncSession) -> None:

    await db_session.commit()

def quote_tag(url):
    return {
        "type": "Link",
        "mediaType": "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
        "name": "RE: "+url,
        "href": url,
    }


async def send_create(
    db_session: AsyncSession,
@@ -575,6 +583,7 @@ async def send_create(
    poll_answers: list[str] | None = None,
    poll_duration_in_minutes: int | None = None,
    name: str | None = None,
    is_quote: bool = False,
) -> str:
    note_id = allocate_outbox_id()
    published = now().replace(microsecond=0).isoformat().replace("+00:00", "Z")
@@ -601,6 +610,10 @@ async def send_create(
            context = in_reply_to_object.ap_context
            conversation = in_reply_to_object.ap_context

    if is_quote:
        tags.append(quote_tag(in_reply_to))
        in_reply_to = None

    for (upload, filename, alt_text) in uploads:
        attachments.append(upload_to_attachment(upload, filename, alt_text))

@@ -2714,3 +2727,19 @@ async def get_replies_tree(
    )
    root_node.children = _get_reply_node_children(root_node, nodes_by_in_reply_to)
    return root_node

async def get_quote(db_session,anybox_object):
    logger.info("calling get_quote for "+anybox_object.ap_id)
    if "tag" not in anybox_object.ap_object:
        logger.info("no tags")
        return
    for tag in anybox_object.ap_object["tag"]:
        if tag["type"] == "Link" and tag["name"][:3] == "RE:":
            logger.info("looking for "+tag["href"])
            try_quote = await get_anybox_object_by_ap_id(db_session,tag["href"])
            if try_quote:
                tag["content"] = try_quote
                await get_quote(db_session, tag["content"])
            else:
                logger.info("Couldnt find object "+tag["href"])

diff --git a/app/main.py b/app/main.py
index d0063dc..0f1c9c2 100644
--- a/app/main.py
+++ b/app/main.py
@@ -327,6 +327,9 @@ async def index(
    )
    outbox_objects = outbox_objects_result.unique().all()

    for outbox_object in outbox_objects:
        await boxes.get_quote(db_session, outbox_object)

    return await templates.render_template(
        db_session,
        request,
@@ -770,6 +773,10 @@ async def outbox_by_public_id(
            f"{BASE_URL}/articles/{public_id[:7]}/{maybe_object.slug}",
            status_code=301,
        )
    await boxes.get_quote(db_session, maybe_object)

    if "quote_url" not in maybe_object.ap_object:
        maybe_object.conversation = None

    replies_tree = await boxes.get_replies_tree(
        db_session,
@@ -929,6 +936,13 @@ async def outbox_activity_by_public_id(
    if not maybe_object:
        raise HTTPException(status_code=404)

    quoteUrl = None
    for tag in maybe_object.tags:
        if tag["type"] == "Link" and tag["name"][:3] == "RE:":
            quoteUrl = tag["url"]
    if quoteUrl != None:
        maybe_object.ap_object["quoteUrl"] = quoteUrl

    await _check_outbox_object_acl(request, db_session, maybe_object, httpsig_info)

    return ActivityPubResponse(ap.wrap_object(maybe_object.ap_object))
diff --git a/app/templates/admin_new.html b/app/templates/admin_new.html
index ac3b3db..f2c7f60 100644
--- a/app/templates/admin_new.html
+++ b/app/templates/admin_new.html
@@ -79,6 +79,7 @@
    </p>
    <p>
        <input type="checkbox" name="is_sensitive" id="is_sensitive"> <label for="is_sensitive">Mark attachment(s) as sensitive</label>
        <input type="checkbox" name="is_quote" id="is_quote"> <label for="is_quote">Quote original post in reply</label>
    </p>
    <input type="hidden" name="in_reply_to" value="{{ request.query_params.in_reply_to }}">
    <p>
diff --git a/app/templates/index.html b/app/templates/index.html
index a6915a8..f7d0e0c 100644
--- a/app/templates/index.html
+++ b/app/templates/index.html
@@ -26,12 +26,9 @@
    <div class="h-feed">
    <data class="p-name" value="{{ local_actor.display_name}}'s notes"></data>
    {% for outbox_object in objects %}
    {% if outbox_object.ap_type in ["Note", "Article", "Video", "Question"] %}
    {% if outbox_object.ap_type in ["Note", "Article", "Video", "Question","Announce"] %}
    {{ utils.display_object(outbox_object) }}
    {% elif outbox_object.ap_type == "Announce" %}
    <div class="shared-header"><strong>{{ utils.display_tiny_actor_icon(local_actor) }} {{ local_actor.display_name | clean_html(local_actor) | safe  }}</strong> shared <span title="{{ outbox_object.ap_published_at.isoformat() }}">{{ outbox_object.ap_published_at | timeago }}</span></div>
    {{ utils.display_object(outbox_object.relates_to_anybox_object) }}
    {% endif %}
	{% endif %}
    {% endfor %}
    </div>

diff --git a/app/templates/utils.html b/app/templates/utils.html
index 2cf460a..9945d4c 100644
--- a/app/templates/utils.html
+++ b/app/templates/utils.html
@@ -247,7 +247,7 @@

{% macro display_tiny_actor_icon(actor) %}
{% block display_tiny_actor_icon scoped %}
    <img class="tiny-actor-icon" src="{{ actor.resized_icon_url }}" alt="{{ actor.display_name }}'s avatar">
    <img class="tiny-actor-icon" src="{{ actor.resized_icon_url }}">
{% endblock %}
{% endmacro %}

@@ -425,13 +425,13 @@
    {% if attachment.type == "Image" or (attachment | has_media_type("image")) %}
        {% if attachment.url not in object.inlined_images %}
        <a class="media-link" href="{{ attachment.proxied_url }}" target="_blank">
            <img src="{{ attachment.resized_url or attachment.proxied_url }}"{% if attachment.name %} title="{{ attachment.name }}" alt="{{ attachment.name }}"{% endif %} class="attachment">
            <img src="{{ attachment.resized_url or attachment.proxied_url }}"{% if attachment.name %} title="{{ attachment.name }}" alt="{{ attachment.name }}"{% endif %} class="attachment u-photo">
        </a>
        {% endif %}
    {% elif attachment.type == "Video" or (attachment | has_media_type("video")) %}
    <video controls preload="metadata"  src="{{ attachment.url | media_proxy_url }}"{% if attachment.name %} title="{{ attachment.name }}"{% endif %}></video>
    <video controls preload="metadata"  src="{{ attachment.url | media_proxy_url }}"{% if attachment.name %} title="{{ attachment.name }}"{% endif %} class="attachment u-video"></video>
    {% elif attachment.type == "Audio" or (attachment | has_media_type("audio")) %}
    <audio controls preload="metadata"  src="{{ attachment.url | media_proxy_url }}"{% if attachment.name%} title="{{ attachment.name }}"{% endif %} class="attachment"></audio>
    <audio controls preload="metadata"  src="{{ attachment.url | media_proxy_url }}"{% if attachment.name%} title="{{ attachment.name }}"{% endif %} class="attachment u-audio"></audio>
    {% elif attachment.type == "Link" %}
    <a href="{{ attachment.url }}" class="attachment">{{ attachment.url | truncate(64, True) }}</a> ({{ attachment.mimetype}})
    {% else %}
@@ -499,9 +499,28 @@
{% macro display_object(object, likes=[], shares=[], webmentions=[], expanded=False, actors_metadata={}, is_object_page=False, is_h_entry=True) %}
{% block display_object scoped %}
{% set is_article_mode = object.is_from_outbox and object.ap_type == "Article" and is_object_page %}
{% if object.ap_type in ["Note", "Article", "Video", "Page", "Question", "Event"] %}

{% if object.ap_type == "Announce" %}
	<div class="{% if is_h_entry %}h-entry{% endif %}" id="{{ object.permalink_id }}">
		<div class="shared-header"><strong><a class="p-author h-card" h-ref="{{ local_actor.url }}">{{ display_tiny_actor_icon(local_actor) }} {{ local_actor.display_name | clean_html(local_actor) | safe  }}</a></strong> shared <span title="{{ object.ap_published_at.isoformat() }}">{{ object.ap_published_at | timeago }}</span></div>
		<div class="h-cite u-repost-of">
			{{ display_object(object.relates_to_anybox_object,is_h_entry=False) }}
		</div>
	</div>
{% elif object.ap_type in ["Note", "Article", "Video", "Page", "Question", "Event"] %}
<div class="ap-object {% if expanded %}ap-object-expanded {% endif %}{% if is_h_entry %}h-entry{% endif %}" id="{{ object.permalink_id }}">


{% if object.quote_url%}
<div class="h-cite u-repost-of qrt">
	{% for tag in object.tags %}
	{% if tag.type == "Link" and tag.content%}
	{{ display_object(tag.content) }}
	{% endif %}
	{% endfor %}
</div>
{% endif %}

    {% if is_article_mode %}
    <data class="h-card">
        <data class="u-photo" value="{{ local_actor.icon_url }}"></data>
@@ -814,8 +833,8 @@
    </div>
    {% endif %}


</div>
{% endif %}

{% endblock %}
{% endmacro %}
-- 
2.25.1
Hey!

Thank you for the patch.

About quoting support, I think I mentioned once I have a branch in progress to add support for it.
It will be a bit different from your approach, as I am planning to add a new foreign key in DB to reference the quoted object (it will prevent making HTTP requests each time we need to display it).

But I like the mf2 improvements, here is what I pushed based on yours: https://git.sr.ht/~tsileo/microblog.pub/commit/578581b4dcf1847cbfa74f4b14522e93337a025a

Let me know if I missed something, but I tried to run the result through a mf2 parser, and I think it looks correct.

Thank you!