Repost/share with quote a.k.a. attach someone else's post to your own post

I thought I was going to be the first one to make these but as it turns out, Misskey already has this functionality, but it’s implemented in a weird way. Here’s the test post I made: https://misskey.io/notes/86wfb5uayv

And here’s it as an AP object:

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://w3id.org/security/v1",
    {
      "Hashtag": "as:Hashtag"
    }
  ],
  "id": "https://misskey.io/notes/86wfb5uayv",
  "type": "Note",
  "attributedTo": "https://misskey.io/users/81qo6ts4l7",
  "summary": null,
  "content": "<p><span>TIL that some fediverse software already allows reposts with quotes.<br><br>RE: </span><a href=\"https://friends.grishka.me/posts/12082\">https://friends.grishka.me/posts/12082</a></p>",
  "_misskey_content": "TIL that some fediverse software already allows reposts with quotes.",
  "_misskey_quote": "https://friends.grishka.me/posts/12082",
  "quoteUrl": "https://friends.grishka.me/posts/12082",
  "published": "2020-05-04T19:10:39.442Z",
  "to": [
    "https://www.w3.org/ns/activitystreams#Public"
  ],
  "cc": [
    "https://misskey.io/users/81qo6ts4l7/followers"
  ],
  "inReplyTo": null,
  "attachment": [],
  "sensitive": false,
  "tag": []
}

As you see, it does two things:

  • There’s the URL for the quoted post, but for some reason it’s in two different fields. Of course the _misskey ones are completely custom, but I can’t seem to figure out where quoteUrl comes from and what it’s used by — it’s not in the ActivityStreams vocabulary.
  • There’s also this URL in the content, presumably for backwards compatibility with Mastodon, Pleroma etc.

Anyway, let’s figure out the best way to standardize these reposts.

I would suggest to send two activities: Announce a post and then Create a reply to that post. That way we don’t need any custom fields or actions. This pair ensures that all references/quotes in the Reply will be correctly understood by recipients: they will see exactly the same post as the “reposting with a quote” Actor sees.
Client app (including Web site) seeing this pair of posts can show it as it wish…

I sat down to translate tumblr posts into an ActivityPub format a little while ago, and i settled on doing “reblog with note” this way as well, although for ease of client consumption, i combined both types into one activity:

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": ["Create", "Announce"],
  "actor": "https://example.com/users/nightpool",
  "id": "https://example.com/activity/my-reblog",
  "object": {
    "type": "Note",
    "id": "https://example.com/note/my-reblog"
    "content": "this is my reply note!",
    "inReplyTo": {
      "id": "https://example.com/note/first-in-trail",
      "type": "Note",
      "content": "the first note!",
      "attributedTo": "https://example.com/user/original-user"
    },
   },
   "origin": "https://example.com/activity/reblogged-from"
  }

This would translate into this UI:

How would software tell the difference between repost with quote as one action and announce/boost + reply as two distinct actions? What if there’s a network error and only one of the activities makes it to the other side?

This is going to break many implementations, including mine :thinking:
What does Mastodon do when it receives such an activity?

it shouldn’t break any spec compliant implementations, any JSON-LD value can be an array, unless it’s marked as a functional property.

I believe Mastodon currently parses it as a Create, taking the first value, but I’m writing the C2S side first so I haven’t tested it extensively yet.

I see, actually you are thinking about some kind of a transaction, a composition of two activities sent in one network message.
Interesting. But we should find out a general way to do this, and not inventing a new format for any new combination. E.g. simply send a collection of two (or more…) activities as one message…

Yeah well, technically it’s valid JSON-LD and it would, of course, work with my implementation of JSON-LD processing algorithms, but further down the line I map ActivityPub types to Java classes for convenience and deserialize the whole thing into an object of that class. This can’t possibly work with a statically-typed language.

I disagree. Go-fed’s approach was explicitly designed to handle use cases like this, where things can be of multiple types.

Actually, most statically-typed languages let values have multiple types. In Java, you could do this by having an object that implements both the Create and Announce interfaces.

Of course you can. I can as well map [Create, Announce] to an internal activity type like Repost which extends Create. My point was that all possible type combinations have to be known at compile time.

But then what use it is if you’re able to parse an activity but still don’t know what to do with it? I’m not making much sense with this. Sorry. Quarantine is taking its toll.

In Java, I might do something like this to access the different values:

class Activity implements Create, Announce {
    Optional<Create> asCreate {
        if (this.typeIncludes('Create')) {
            return Optional.some(this);
        } else {
            return Optional.none();
        }
    }

    Optional<Announce> asAnnounce {
        if (this.typeIncludes('Announce')) {
            return Optional.some(this);
        } else {
            return Optional.none();
        }
    }
};

class Inbox {
    void handleActivity(Activity activity) {
        activity.asCreate().ifPresent(create => {
            create.getNote().doSomething();
        });

        activity.asAnnounce().ifPresent(announce => {
            announce.getObject().doSomething();
        });
    }
}

But, honestly, I probably wouldn’t bother with the different interfaces, considering that the only types in ActivityStreams that affect the properties objects are allowed to have are Link, Object, and Activity.

Seems a bit weird, to be honest. The object of the Announce is supposed to be the announced object, but here it is the newly created object, so i don’t think it works out too well, semantically.

Why not just add a “content” field to the “Announce”? Seems rather straightforward.

Then implementations like Mastodon would display these as simple retoots boosts, losing the additional content. The way Misskey does it is actually rather smart in that Mastodon displays everything that’s in there, and Misskey itself strips the link and replaces it with its own richer UI.

image

the first implementation I had did use a content field on Announce, basically duplicating all of the properties a Note can have, but it felt like it was muddying the distinction between Objects and Activities, it led to a ton of special case processing and a lot of code like (attributedTo || actor) where I had to handle the complexities of processing both types of object. ultimately, I think using inReplyTo and Announce makes a lot more sense when considering the actual difference between reblog-with-note and reply-to-thread—the expected audience and context of the reply

So ultimately I decided on the Announced object here actually being your own post, which I think works well. in more thread-based forms of microblogging this would translate to replying to a thread and then boosting your own reply into your timeline, which is basically exactly what you’re doing semantically.

Well, that’s a hack. We can not keep adding compatibility hacks for every basic feature.

To elaborate a bit, Mastodon is not an abandoned project, they should be expected to code support for or accept contributions for new, sensible features.

2 Likes

How would one determine if this is a “reblog with note” or a reply when having just the object? For example if someone I don’t follow posts a “reblog with note” and then someone I do follow replies to it (putting the object uri into inReplyTo as every implementation does now), my server will fetch the object and assume it’s a reply, since it doesn’t have the activity type context

1 Like

I thought about this approach another time, and it looks like adding your “quote / notes” (“content” in terms of ActivityPub) directly to the “Announce” (Activity) object is the best way to implement “Repost with a quote”:

  1. “content” (like “name” and “summary”) in the Announce (activity) object is not a hack, but normal property. E.g. Activity Vocabulary spec shows summary (which also could serve as a comment to announced note!) in the “Announce” activity example https://www.w3.org/TR/activitystreams-vocabulary/#dfn-announce

  2. Semantically “announcing” that “quoted” object (and not OUR own notes on that) IS the activity that is supposed to be used here. From spec: “Indicates that the actor is calling the target 's attention the object.” - And we really mean that! We call attention and make our comment / quote…

We are NOT “replying” to that “someone else’s post”. Instead, we create new discussion context, for the new audience.

  1. Both our comments/quotes (in the Announce activity’s name, summary and content properties) AND the “someone else’s post” are contained in ONE activity / message sent over the network / between systems. This is really important in order not to lose context of our “quotes” AND simply for a target audience to be able to see that “someone else’s post”.

?!