FEP-5624: Per-object reply control policies

source: fep/fep-5624.md at main - fep - Codeberg.org


---
authors: @Claire
status: DRAFT
dateReceived: 2022-08-23
---

FEP-5624: Per-object reply control policies

Summary

Sometimes, users may want to share an information or a story without inviting replies from outside their circles or from anyone at all. In particular, individuals may want to restrict who can reply to them in order to avoid “reply guys” or limit outright harassment, while instutions may want to disable replies on their posts to provide information without having to deal with a moderation burden.

This can be broken into an advisory part advertising what sets of actors are expected to be able to reply, and a collaborative verification process where third-parties check with the actor being replied to that the reply is indeed allowed.

Requirements

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this specification are to be interpreted as described in [RFC-2119].

In the remaining of this FEP, “distribution authority” (or “authority” for short) refers to an actor that controls the distribution and audience of replies. The purpose of this wording is to make this FEP applicable both for models where replies are first-class posts, and for “post and comments” models where comments only exist in the context of a post and the post author decides who gets to see the comments. In the absence of extensions, the “authority” is the author of the post being replied to.

Declaring a reply policy

In order to advertise who is allowed to reply to an object, an author MAY set the canReply (http://joinmastodon.org/ns#canReply) property on their objects. If set, this property MUST be an empty array or one or more actors or collections.

To ease implementation, collections SHOULD be restricted to one of the following:

  • as:Public, to indicate that anyone can reply
  • the authority’s followers collection (if defined)
  • the authority’s followed collection (if defined)

In addition, canReply SHOULD contain every actor mentioned in the original object.

Whenever one of these collections is used, the receiving end can easily know whether they are expected to be able to reply.

Example object

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    {
      "toot": "http://joinmastodon.org/ns#",
      "canReply": "toot:canReply"
    }
  ],
  "attributedTo": "https://example.com/users/1",
  "id": "https://example.com/users/1/statuses/1",
  "type": "Note",
  "content": "Hello world",
  "canReply": "https://www.w3.org/ns/activitystreams#Public"
}

Checking whether the user can reply and submitting the reply to the authority

When an object with canReply is set, it SHOULD be conveyed in human-readable form to the user if possible, for instance with something like “Only mentioned users can reply” or “Only people Authority follows and mentioned users can reply”.

The software SHOULD NOT offer the user to reply unless it is directly mentioned in the object’s tag attribute or listed in canReply (either directly or through a collection), or canReply contains a collection for which the recipient cannot efficiently check the membership of the would-be replier.

After locally verifying that the replier should be allowed to reply, the replier’s end SHOULD POST the Create activity for the reply to the authority’s inbox only, and consider the reply to be pending approval.

Receiving and accepting a reply

When receiving a reply to an object with a canReply property, the authority decides whether the reply is acceptable.

If the reply is considered acceptable, the authority MUST reply with an Accept activity with the object property set to the id of the reply object, and its inReplyTo property set to the object it is in reply to.

That Accept activity SHOULD be publicly dereferenceable and MUST be dereferenceable by all parties allowed to see the original post. It MUST NOT embed its object nor its inReplyTo as to avoid possible information leaks.

If the reply is considered unacceptable, the authority SHOULD reply with a Reject activity. This activity MAY be publicly accessible, but this is not a requirement.

Example Accept activity

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "actor": "https://example.com/users/1",
  "id": "https://example.com/reply_approvals/1",
  "type": "Accept",
  "object": "https://example.org/users/bob/statuses/3",
  "inReplyTo": "https://example.com/users/1/statuses/1"
}

Example Reject activity

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "actor": "https://example.com/users/1",
  "id": "https://example.com/reply_approvals/1",
  "type": "Reject",
  "object": "https://example.org/users/bob/statuses/3"
}

Receiving approval and distributing the reply

After sending the initial Create, the replier SHOULD wait for an Accept activity such as described above.

Once the Accept has been received, the replier SHOULD add a replyApproval (http://joinmastodon.org/ns#replyApproval) property to their reply object pointing to the Accept activity they received, and then MAY send a Create activity with the modified object to its intended audience.

If it instead receives a Reject, the reply SHOULD be immediately deleted and the replier MAY be notified.

Example reply object with replyApproval

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    {
      "toot": "http://joinmastodon.org/ns#",
      "canReply": "toot:canReply",
      "replyApproval": "toot:replyApproval"
    }
  ],
  "attributedTo": "https://example.com/users/1",
  "id": "https://example.org/users/bob/statuses/3",
  "type": "Note",
  "content": "@alice hello!",
  "inReplyTo": "https://example.com/users/1/statuses/1",
  "canReply": "https://www.w3.org/ns/activitystreams#Public",
  "replyApproval": "https://example.com/reply_approvals/1",
  "tag": {
    "type": "Mention",
    "href": "https://example.com/users/1"
  }
}

Verifying third-party replies

When processing a reply from a remote actor to a remote authority, a recipient SHOULD discard any reply that does not match any of the following conditions:

  • the object it is in reply to does not set a canReply property
  • the object it is in reply to has a canReply containing the as:Public collection
  • the author of the reply appears in a Mention object in the tag property of the object it is in reply to
  • the object it is in reply to sets a non-empty canReply, and replyApproval can be dereferenced and is a valid Accept activity

To be considered valid, the Accept activity referenced in replyApproval MUST satisfy the following properties:

  • its actor property is the authority
  • its authenticity can be asserted
  • its object property is the reply under consideration
  • its inReplyTo property matches that of the reply under consideration

In addition, if the reply is considered valid, but has no valid replyApproval despite the object it is in reply to having a canReply property, the recipient MAY hide the reply from certain views.

Revoking a previously-accepted reply

The authority may want to perform /a posteriori/ moderation of their replies.

To do this, the authority SHOULD send a Reject activity to the sender and the reply’s audience, with the reply URI as the object property. The object property MUST NOT be embedded, as to avoid possible information leaks.

The URI at which the previously-offered Accept was available should return HTTP 404 or redirect to the newly-issued Reject activity.

Handling a revocation

Upon receiving a Reject activity for a previously-accepted reply, third-parties SHOULD check that the Reject is valid and SHOULD delete or hide the revoked reply if it is.

To be considered valid, the Reject activity MUST satisfy the following properties:

  • its actor property is the authority
  • its authenticity can be asserted
  • its object property is the reply under consideration

Deployment considerations

Because it is unrealistic to expect all implementations and deployments to implement this proposal at the same time, deployment SHOULD be gradual, with verification of third-party replies only performed once the other steps are widely implemented. To encourage adoption without breaking compatibility altogether, implementations MAY want to hide non-validated replies from certain views (e.g. requiring a click to see “hidden replies”, or not showing the reply to non-followers).

Security considerations

By not adding a hash or copy of the reply in the Accept activity, malicious actors could exploit this in a split horizon setting, sending different versions of the same activity to different actors. This is, however, already a concern in pretty much all contexts in ActivityPub, and enshrining that information in the Accept activity would have many drawbacks:

  • significantly more complex implementation
  • inability to change the JSON-LD representation after the fact
  • possibly leaking private information if the Accept activity is publicly dereferenceable

Implementations

None so far.

References

Copyright

CC0 1.0 Universal (CC0 1.0) Public Domain Dedication

To the extent possible under law, the authors of this Fediverse Enhancement Proposal have waived all copyright and related or neighboring rights to this work.

2 Likes

Hello!

This is a discussion thread for the proposed FEP-5624: Per-object reply control policies. Please use this thread to discuss the proposed FEP and any potential problems or improvements that can be addressed.

Summary
Sometimes, users may want to share information or a story without inviting replies from outside their circles or from anyone at all. In particular, individuals may want to restrict who can reply to them in order to avoid “reply guys” or limit outright harassment, while institutions may want to disable replies on their posts to provide information without having to deal with a moderation burden.

This can be broken into an advisory part advertising what sets of actors are expected to be able to reply, and a collaborative verification process where third-parties check with the actor being replied to that the reply is indeed allowed.

1 Like

I mentioned this to @Claire before in a different channel (Discord?), but it might make sense to split this into a “base” FEP that defines the mechanism for approval by an authority, and then a second FEP that inherits from the “base” FEP to define reply controls. This way, you could have multiple FEPs that build off the “base” FEP to allow signaling authority and consent via a generic approval property rather than ending up with specific replyApproval as well as potential announceApproval, quoteApproval, mentionApproval, etc.

2 Likes

thanks my issue has been fixed.

Thank you @weex for making this discussion thread! I have been in about 10 discussions in the last week or so about this. @zzz@eldritch.cafe just mentioned this issue on the w3c github about reply control thats open since 2018 that has a lot of related discussion about this issue.

post update:
I noticed that an issue has already been opened the Wordpress ActivityPub Plugin repo to support this FEP

2 Likes

This seems fairly sensible, I will try to give it some thought shortly.

I’m in the midst of implementing this. The workflow has a bit of an issue, let’s see if I can describe it adequately.

Bob sends a comment to Alice’s post. Bob’s comment fits the canReply crtiteria and the comment is accepted. Alice’s server sends an Accept activity and posts the comment to her copy of the conversation. Bob’s server receives the Accept activity and adds the requisite replyApproval field before distributing to his own followers. All is working as designed.

Here’s the issue: Alice’s server is going to send out her copy of the post to her own followers, but it has no replyApproval. They are going to reject it. She will need to wait for the comment activity to be updated by Bob to include the replyApproval and resent.

Does Bob’s server also change the edited timestamp? This is after all an edit.

So basically every comment will become an edited comment and require three activities to be exchanged; including being delivered twice to the remote authority. This is unfortunate.

But back to Alice. She already has a copy of the message and finds it acceptable but cannot distribute it to her followers until Bob updates and returns it. Unless the specification permits Alice to distribute it as a relayed comment without requiring replyApproval to be set. We can currently do this by requiring LD-signatures on the relayed comment. The exact signature mechanism may change since that specification is a dead parrot; but the overall flow from Alice to her followers needs to be clarified.

So, “replyApproval is not required if the comment was sent/transmitted by the remote authority and the sender can be verified” or however you want to word that. This is basically how we are performing comment permission checks today. Otherwise, Alice’s server needs to wait for an update from Bob before it can update and distribute the comment as part of Alice’s conversation. If you add manual moderation to the equation, the length of time involved and the accumulated inefficiencies could become significant, since every edit will be forced to trigger a moderation review.

1 Like

That makes total sense to me, and I’ll likely amend the FEP to allow this flow.

So, I’m about to basically:

  • rename replyApproval to approval
  • make it explicit in “Receiving and accepting a reply” that the authority can forward the activity (“Additionally, the authority MAY forward an accepted reply according to its own rules.”)
  • adding to “Verifying third-party replies” that a reply relayed through the authority should be considered valid
  • write a new FEP that describes approval and the related flow without canReply. I am less sure about this because there are several parts that are context-dependent, e.g. inReplyTo in the Accept, and the exact rules for accepting replies without explicit approval

Does that make sense to you?

No problem at all with the first three. A bit uncertain about the last one, but apparently you are also.

My work on this is pretty far along so I hope the changes aren’t too dramatic, but that’s my problem for jumping in so early.

Cheers.

So far I’m not planning on making other changes, and the last item would not change anything to the protocol itself in practice, so the only changes so far are replyApproval being renamed to approval, and making explicit that the authority may forward the activity and that such a thing should be considered as an approval.

However, the FEP was designed with the replies-as-first-class-objects model where the authority is the post you immediately reply to, with the aim to have it applicable to the posts-and-comments model. But it seems it kinds of fall short of providing the latter, because nothing ensures that canReply carries over. I’m curious to have your thoughts on this, especially since if I remember correctly, your projects tend to fit the “posts-and-comments” model more.

canReply does not carry over in the posts and comments model. The sender (which may or may not be the author) of the initial post in a conversation (currently identified by context) is the authority and there can be no other. So it’s relatively easy for me to make this work. It will probably be a lot harder for you.

1 Like

starting to wonder if perhaps the “replies vs comments” model is so disjoint that it might actually make sense to make it explicitly disjoint

this would probably imply the following

  • using commentOn instead of or in addition to inReplyTo (as comments may be replies to other comments)
  • signaling approvesComments instead of or in addition to approvesReplies
  • signaling canComment instead of or in addition to canReply (both remain optional)
  • explicitly specifying that these comment-related properties MUST/SHOULD be carried over as-is?

with the following issues arising:

  • a generic approval might not make sense anymore, as you might need multiple approvals
    • how do you elegantly coordinate between multiple actors for their approval?
    • what happens if some actors approve, but not all?
    • do we split it back out into replyApproval commentApproval etc etc etc?
    • this was probably already a concern anyway
  • what happens if you are only “reply-aware” and not “comment-aware”
    • your post might then unintentionally strip comment metadata
    • reply-only impls will traverse the reply chain up as far as they can
    • comment-aware impls will have to traverse the reply chain up BUT if they hit an object that has commentOn then they should assume all downward replies are also comments on the same post?
      • or maybe they show replies to comments in a separate view? like viewing the comments will ONLY show objects with commentOn, and if commentOn gets dropped at any point, then you detach all further replies and put them behind a “show replies to this comment” kind of thing or possibly discard them (this would be an implementation detail)

things i’m still not clear about:

  • how do you signal authority for a different sender than author? given only an initial post in the conversation, what property do you use other than attributedTo?

how do you signal authority for a different sender than author?

replyTo

Shamelessly ripped off of E/SMTP because not every messaging problem is new or unique to ActivityPub.

When the "Reply-To:" field is present, it indicates the address(es) to which the author of the message suggests that replies be sent.

canReply does not carry over in the posts and comments model. The sender (which may or may not be the author) of the initial post in a conversation (currently identified by context) is the authority and there can be no other. So it’s relatively easy for me to make this work. It will probably be a lot harder for you.

Then that sounds like a significant departure from the proposal, unless you can’t reply to a reply, since the current version of the proposal instructs to check the canReply attribute on the object that the reply is inReplyTo.

replyTo seems fit for a different purpose, as it doesn’t imply authority. Also, as stated, it is a suggestion and not a requirement. Aside from that, I’m concerned that blindly copying such a field could lead to spamming an unrelated third party.

Basically, there should be some field that unambiguously signals authority over comments. It makes sense to make that be the attributedTo actor, as everywhere else, authority is derived from the author. But I can see the use in having a conversation be mediated by some third-party actor.

Given this, maybe there should be either a standalone authority property (that carries over?) – or perhaps we use context.attributedTo? And then we say that context carries over. (Which it probably should do, anyway, insofar as we want activities to be grouped together.) This would allow equally for context to point either to some Collection of activities representing the conversation, or otherwise point to the root object in the thread (if the authority lies with the root post’s author).

A commenter does not have any authority over the conversation. So if you get an approval from Bob to reply to his comment, it still isn’t going to end up as a branch of my conversation (and Bob will never see it) unless you get approval from me.

This implies that every level of threading adds another approval requirement. A random commenter can possibly get an approval from Bob, but what about Jennifer and Fred? And if they don’t all approve it, the approval will never get to me and the comment will not appear in my conversation.

I think asking for approval from every single parent author doesn’t make sense. At most, you’d only ask the immediate parent (in a reply-based system) and/or the root post/conversation authority (in a comment-based system).

That is, if

  • Alice makes a post and is comment-aware
  • Bob is comment-aware and comments on Alice’s post
  • Charlie is comment-aware and replies to Bob’s comment
  • Delta is comment-aware and replies to Charlie’s reply
  • Alice expects comment approval
  • Bob expects reply approval
  • Charlie expects reply approval

then Delta only has to get reply approval from Charlie and comment approval from Alice, at most. Bob is irrelevant, because it’s not Bob’s conversation.

Alice may also signal that someone else owns the conversation by making the root post have a context that is attributedTo someone else? In which case Delta would have to ask Charlie and also whoever owns the context/conversation.

If you get approval for a reply but not a comment then that could look like a “see more replies” link on the comment in comment-aware implementations (or dropped, it’s an implementation detail). If you get approval for a comment but not a reply then it should not be shown, or perhaps could be shown behind a link as above (still an implementation detail). If you get approval for both then it makes sense to render the post both as a reply and as a comment.

We currently indicate that the conversation falls under the Facebook model by using replyTo. If you want to use something else, fine. We’re using this today and have been for a year or three.

If present on the conversation root node, it just says “Replies to anything in this conversation go to this address - and nobody else”. There’s no need to overthink it, and we don’t have to use the SMTP definition precisely. Users almost never set reply-to in email these days. Their software does.

So for comment approvals - if replyTo is present on the root node, it’s using the Facebook model, and that address is who you ask for approval. If replyTo is an array, you only need to seek reply permission from the first one. Let’s keep it simple.

If there’s no replyTo at the root of the conversation, it is using the Twitter model. Ask the author of the inReplyTo. And if the author of the inReplyTo is an array, ask the first one.

Done.

Or we can make it incredibly complicated.

I am still worried about how to “trust” replyTo is valid. What prevents me from authoring an object and setting a replyTo for someone else completely unrelated in order to spam them?

Aside from that, I’m realizing we could just have a comments collection separately from a replies collection. Though there is still the problem that both replies and comments are optional properties, so perhaps a dedicated property for detecting comment support is still needed. Or we could move to require a comments collection MUST be exposed if comments are supported? Replies don’t have this issue because replies are always assumed to be supported (so replies collection is optional, and inReplyTo is tracked instead).

In effect, this is where we are in the discussion:

Use-case Replies only Comment-aware
Collection replies
(optional)
comments
(required)
Reference inReplyTo
(any object)
commentOn
(root object)
Approval request approvesReplies approvesComments
Approval hinting canReply
(optional)
canComment
(or existing commentPolicy)
(optional)
Approval proof replyApproval
(required if inReplyTo.approvesReplies)
commentApproval
(required if commentOn.approvesComments)
Approval authority inReplyTo.attributedTo commentOn.attributedTo
(or commentOn.replyTo? i still have concerns…)

It would still be nice if there was a way to unify the approval stuff behind some common interface… I’m not sure it’s a great idea to have n different types of approvesX and xApproval properties, but it may be unavoidable in certain cases.

Really, the outstanding issue here is dealing with the case where there are multiple authorities. And tangentially, detecting such authorities. Replies and comments are easy. Mentions, announces, quotes, etc. are all far more nebulous. Given that, it’s looking more likely that we’ll end up with multiple properties as an unavoidable outcome. (This is probably just the natural consequence of lacking a capability-based ecosystem…)