"Follow"/"Accept" Object Identifiers

I’m implementing the Follow/Accept/Reject activity types in my server-to-server application. I’m trying to understand what is expected (by spec and by convention) for the "id"s of these activities.

When I receive a “Follow” activity from mastodon, it has an ID like “http://localhost:3000/562d0ad7-79ac-4c45-98f6-ceff5fbe583e”. However, this ID is not dereferenceable.

But the ActivityPub spec of course reads:

All Objects in [ActivityStreams] should have unique global identifiers. ActivityPub extends this requirement; all objects distributed by the ActivityPub protocol MUST have unique global identifiers, unless they are intentionally transient (short lived activities that are not intended to be able to be looked up, such as some kinds of chat messages or game notifications). These identifiers must fall into one of the following groups:

  1. Publicly dereferencable URIs, such as HTTPS URIs, with their authority belonging to that of their originating server. (Publicly facing content SHOULD use HTTPS URIs).
  2. An ID explicitly specified as the JSON null object, which implies an anonymous object (a part of its parent context)

So, a Follow is probably an “intentionally transient” activity, which means it doesn’t need an identifier? Certainly “2” can’t apply, since there is no parent context.

In any case, ignoring Mastodon for a moment, if we were to accept the language of the spec, would

"@context":"https://www.w3.org/ns/activitystreams",
"type":"Follow",
"actor":"http://localhost:3000/users/technical",
"object":"https://7902-75-164-4-199.ngrok-free.app/@helloworld"
}

be a valid Follow? If so, the response “Accept”/“Reject” would then have to inline the object:

{
  "@context":"https://www.w3.org/ns/activitystreams",
  "type":"Accept",
  "actor":"https://7902-75-164-4-199.ngrok-free.app/@helloworld",
  "object" : {
    "@context":"https://www.w3.org/ns/activitystreams",
    "id": null, 
    "type":"Follow",
    "actor":"http://localhost:3000/users/technical",
    "object":"https://7902-75-164-4-199.ngrok-free.app/@helloworld"
  }
}

Where the ID is now set to null since there is now a parent context? Or without an ID again?

Alternatively, should Follow IDs just be non-transient and dereferencable? Accept too?

I think most people would be happier if Follow IDs are dereferencable. IIRC, Mastodon ids were originally as you proposed (no “id” property anywhere), but unique IDs were added because people kept complaining about them. Unique but non dereferencable IDs was the compromise solution

1 Like

Alright, that makes some sense. At least for Follows. I’m less certain it makes sense for Accept/Reject. These are definitely more transient.

But it does seem like at least Mastodon doesn’t mind if your Accept doesn’t have an ID. I’ll try and poke around the other major implementations. I’ve been meaning to set up a “pasture”…

And of course, I just noticed in section 7, it’s implied that the “object” param of Accept/Reject is optional. These can be pretty lightweight!

Err, never mind.

If the object of an Accept received to an inbox is a Follow activity previously sent by the receiver, the server SHOULD add the actor to the receiver’s Following Collection.

So these are my thoughts, as I’ve recently started reworking the follow logic for bovine. Hopefully it’s the final try.

I assume validation of incoming activities that I consider sane. So if a message is HTTP signed by Alice and it has embedded an object O attributed to Bob, one should refetch object O. In my opinion this is an obvious consideration, as Alice is not the authority on Bob’s stuff.

The above has the following consequence:

  1. Follow Activities MUST have ids. Otherwise the Accept is impossible.
  2. If one doesn’t want to assume that Follow activities are HTTP fetchable, one should store them. Similarly, for Accept Follow if one wants to allow a Unfo Accept Follow.

Unfortunately, the ActivityPub specification doesn’t make these considerations. This is one of the “requires hard work” changes a fork of ActivityPub should make. The entire Follow Activity part of the ActivityPub is convoluted enough, that I cannot claim to understand it, or draw any conclusions from it on how people might implement it.

1 Like

I’m not sure it is. The AP Primer seems to say an id is not required for activities like Follow.

Heuristics for identifying activities uniquely

  • Follow: Typically, an actor can only follow one other actor one time. So, the actor id and object id should uniquely identify the Follow activity. If there have been multiple Follow/Undo Follow pairs, the most recent Follow is probably the one being referred to.
    Reference: ActivityPub/Primer/Referring to activities - W3C Wiki

There are no examples for Follow, but they may have something like this in mind:

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Accept",
  "actor": "https://server.test/followed",
  "to": "https://server.test/follower",
  "object": {
    "type": "Follow",
    "actor": "https://server.test/follower",
    "object": "https://server.test/followed"
  }
}

It’s clear which following relationship is being accepted, even without the activity identifiers. I think this also avoids the need to store Follow requests (“transient” or otherwise).


It would be radical and would almost certainly not interoperate with any current AP implementation, but with some assumptions (actor = message signer, to = inbox owner , normative AS2 content type so @context is not needed), one could even minimize the Accept(Follow) message to:

{
  "type": "Accept",
  "object": { "type": "Follow" }
}
1 Like

this does go back to the discussions around the “shape” of certain activities. in particular, the only ID that’s “useful” is the top-level one. that way, you can refer to it and say something like “activity xyz was successful” or otherwise key off of it.

another discussion this goes back to is the one around idempotence of certain activities, and the split between activities as a resource vs. activities as a procedure call. Follow is one of those activities that honestly is more procedural than it is intended to be a resource. this becomes even more apparent when you compare to how subscriptions work in other systems, e.g. WebSub, where you generally declare a topic of interest + a callback to receive future notifications. From Publish–subscribe pattern - Wikipedia :

n software architecture, publish–subscribe is a messaging pattern where publishers categorize messages into classes that are received by subscribers. This is contrasted to the typical messaging pattern model where publishers send messages directly to subscribers.

of course, activitypub is built on the messaging pattern more than it is built on pubsub. see also Observer pattern - Wikipedia which is closer to what’s going on.

ActivityPub does not make any restrictions on which activities are legal to send, so this is left to developers. If people send me activities, which I consider illegal, it is clear that they will not interoperate. This is then a feature not a bug.

1 Like

This is one of those cases where Primer doesn’t reflect best practices. Referring to Follow activity by its ID is simple, clear and supported by most (if not all) existing implementations. However, the Primer says:

Consumers should make an effort to process activities without an id if those activities can be identified uniquely by other properties.

Even though there’s no reason to make such effort. This leads to “N different ways of doing the same thing” type of situations, and we already have enough of that in Fediverse. I expressed my concerns in a related issue but the Primer wasn’t updated: Undo activity without an ID on the object activity · Issue #384 · w3c/activitypub · GitHub.

Only IDs are required, implementers don’t need to store whole activities.

1 Like

the reason is because it allows you to differentiate a generic Accept from an Accept Follow. if you have an activity like this:

id: https://example.com/sample-accept
actor: <someone>
type: Accept
object: https://example.com/something

well, now you have to figure out what /something is. so i guess you’d better have stored all Follow ids forever, and then you have to do a query against all of those to see if it matches, and in fact you actually do have to store the activity, at least to know who issued the Follow and who was being followed. specifically, you want object.actor, object.type == Follow, and object.object. and you’re finding this by the object.id. so this is pretty much the entire activity. whether you store the JSON payload or you store the fields in a database, you’re still storing the whole activity.

so, given that you need to discriminate by shape anyway, why not make it the primary way of doing it? really, what you probably want to do is always embed the Follow within the Accept/Reject. the id isn’t really needed unless you need to refer to the Follow later… and you usually don’t. perhaps you might need to do so in cases where you want to prove that you once followed someone or that someone once followed you? perhaps you want to store metadata about the follow relationship, such as when it was made, or if it came with some message included? but these are not currently common use cases for Follow activities. if they were, then i would say that indeed, the Follow should have an id and should be dereferenceable somehow.

2 Likes

Yes, this is what I do, and I also store Like and Announce IDs to process Undo activities. This is not a big deal.

In my view protocol simplicity is more important, and the situation where object can be an ID, or a full Follow activity, or an incomplete copy of it without an ID, is not a good one.

This is a good point. But as far as I know, Accept(Follow) is currently the only real use case for Accept activity. Another use case was suggested in FEP-5264 discussion, and yet another one was proposed in FEP-0837, however these are not widely implemented. Are there other examples?

It seems to me that best practices in this area have not been established yet. Discrimination by shape is not the only solution, and shouldn’t be promoted as such. There are at least two alternatives:

  • Discriminate by URL structure. Implementers may use https://social.example/follow-requests/<id> for Follow activities and https://social.example/offers/<id> for Offer activities.
  • Discriminate by additional properties in Accept activity. These properties may depend on the type of the original activity, and we can also define a new generic property such as objectType.

All of them have downsides, but I’m slightly in favor of the latter approach and decided to rely on additional properties in my FEP-0837 implementation where application needs to distinguish between Accept(Offer) and Accept(Follow) activities.

To followup, Mastodon almost supports this. I was able to use a message like the one below to Accept a Follow.

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Accept",
  "object": {
    "type": "Follow",
    "actor": "https://server.example/tester"
  }
}

The @context is required because Mastodon uses it as a magic JSON value to identify an AP message (rather than using the HTTP Content Type header). The object.actor property is required instead of using the non-shared inbox URI to determine the target account.

For a Follow, I was able to use something like:

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Follow",
  "to": "https://server2.example/users/followed"
  "object": "https://server2.example/users/followed"
}

Note there are no identifiers and no top-level actor in either case. (I think the to isn’t required by Mastodon, but my server implementation needs it for outbox delivery targeting.)

The minimal version of the Follow message (not supported by Mastodon) with the same assumptions as I described previously would be:

{  "type": "Follow" }

(@context inferred by Content-Type header, to and object inferred by non-shared target inbox URI).

1 Like

I consider the Primer to be the best practice here and I agree with n on their assessment