FEP-5624: Per-object reply control policies

This is a big departure from how the feature works on Twitter today, and it would be a significant step back in terms of usability. The most common usecase for this feature is something like: “I made this post for my 250 followers, it went viral, now I have hundreds of people replying to it every minute and I need them to STOP TALKING because I can’t respond to every stupid thing somebody decided to attach to my post.”

(Twitter actually has two features designed to address this use-case, both with their own pros and cons)

1 Like

I think it’s better to return to the original topic. To summarize, the idea is to use some kind of signed object with expiration date to represent approval. This object can be inlined to make verification possible without making request to authority server.
To make approval revocable, authority can make it short-lived (for example, approval may expire in 30 minutes). Expired approval can’t be verified offline, so authority can revoke it by returning 404 in the same way it can revoke approval in the form of Accept activity.

The main advantage, in my opinion, is the possibility of offline verification.

Yeah, there’s also “i am not accepting responses, don’t at me” and “i am okay with feedback but i want to manually review everything and only let through the thoughtful responses” or even “when i block someone, i don’t want them to show up under my posts, not just for me, but for other people too”

One-time approval is not really good enough to handle all these use-cases.

this should be possible if you have a signed object that you can embed, yes.

this is also possible but imo not needed? it’s like saying “you can distribute this, but only for a limited time”. it is possible to interpret it in such a way: “well, this object used to be approved, but that approval has expired, i don’t need to check what’s at the node id given because i already have that data”. the problem with embedding is one we’ve covered already, in that it is unclear whether the representation is partial or outdated, and there is no guarantee that the node is fetchable separately. a generic JSON-LD parser would not know to re-fetch an expired proof, i think?

Sorry I left this hanging for so long, I’ve been busy with a lot of stuff, but this conversation has been brought up again and I have some more thoughts to share.

On controlling quotes

I understand what you mean, but I think there’s still value in denying the convenience to people who may be abusive. And more important, things like Twitter allow listing quote-posts from the quoted post: such a feature may lend the quote-poster your audience, in a way similar to replies, so it makes sense to me that we’d have similar mechanisms for controlling who can quote-boost you, or at least be listed from your post.

Back to controlling replies

Back to controlling replies, Erin pointed out that Accept may be too generic and end up ambiguous or opening to cross-purpose attacks.

I tend to agree. I wanted to do something quite generic, but there is value in making something explicit and straightforward.

I’m still not satisfied with the state of the FEP regarding defining who is the authority that gets to decide whether a reply is allowed, but I think even if not ideal, having it the author of the post being immediately being replied to has the benefit of being extremely simple and still providing a lot of value.

I guess I can try to abstract inReplyTo occurrences to something that can be overridden by a subsequent FEP defining another reply model (such as by defining replyTo and context or conversation).

I think there may be a simpler approach to reply moderation—it wouldn’t provide full moderation of every incoming reply, but it would solve many common issues while being much easier to implement and understand.

Ignore Accepts and replyApproval and conversations, and just focus on one feature: sending a Reject activity with a reply as its object.

  • You can only Reject replies to your own posts (no concept of a larger conversation to track)
  • Reply Rejects are federated widely, like Deletes
  • A Reject disconnects a reply from a thread
  • You can configure your server to auto-reject new replies, or replies from certain users, as soon as it receives them
  • You can Undo a Reject to approve a reply that was mistakenly auto-rejected
  • canReply would still be supported as a hint to hide the reply button
  • You could also reject Likes and Announces, and they won’t appear in your “Likes and Boosts” lists
  • To avoid the problem of fediverse-wide reject spam, an object could instead have a rejections collection containing Rejects for all related replies/likes/announces

Sounds like Signaling side effects asynchronously by generalizing Accept/Reject maybe?

In any case: only post-facto rejection of replies doesn’t work for the use-cases described. specific comments below:

this vastly increases traffic that would be considered not only useless, but counterproductive. you are basically advertising the existence of a reply.

similarly: just don’t advertise any unwanted Likes or Announces.

this could work in theory but it would be a bad idea. if you’re going to check a collection at the origin, then just check the actual replies/likes/shares


aside 1: what is a “quote” and how does it relate here

if “quotes” were just replies that embedded the replied-to object, then “reply controls” would also be “quote controls”. i would argue there’s actually near-zero difference between a quote and a reply; a quote is just a reply with an embed. one only needs to look at how a “reply” is rendered in the IndieWeb space to see the similarity to “quotes”. See https://aaronparecki.com/replies for an example of this.

i don’t think it makes any sort of sense to try and prevent people from typing or pasting a hyperlink into the content; on that front, what we might seek to adopt countermeasures for would be to disable embeds or previews via OEmbed.

offtopic: consider a toot:quoteReply boolean that, if true, indicates that the inReplyTo object should be embedded above while rendering the current object. mastodon could then change/relax its policy of not showing replies in feeds, and insert such posts into the feeds of followers (or make it an option? idk, this is an implementation detail, and other implementations already show all replies anyway)

aside 2: about contexts and conversations

if you had a first-class concept of a “conversation” that isn’t just a reply-chain, then such a “quote” may or may not be part of the same discussion (copy the context), or it may be part of a new discussion (set your own context), or it may be not a part of any discussion (in the default global context of null). seeing as how it makes sense to have a concept of a “moderated conversation” or “authoritative context”, we derive the following consequential cases:

  • support reply stamps/proofs/approvals, proving that a reply was added to inReplyTo.replies
  • support context stamps/proofs/approvals, proving that an object was added to context
  • support both

aside 3: Accept or Add?

so far the discussion and proposal has mostly assumed an Accept activity would serve the purpose… and it very well might. but wouldn’t an Add do the same as well? in fact, an Add could be distributed to the followers of the person being replied to, as well as to the person replying; the person replying can then reference that Add activity as proof, or even inbox forward it to their followers.

for revocation, i don’t think this is actually any different either; you just stop serving the “proof” activity, regardless of its type. if anything, you can explicitly send a Remove to your followers, and possibly to the author of the removed reply. if they are a good actor, they might forward it to their followers. if they are a bad actor, they might harass you about it and get blocked/reported.

it would also probably address this concern:

if you send an Add to replies, then that’s unambiguous.

based on a discussion I had elsewhere: the generalization of this pattern is “how do we prove an object is part of a collection”, where the salient semantic “special collections” might be:

  • Object.inReplyTo.replies
  • Object.context
  • Announce.object.shares
  • Like.object.likes

in each of these cases, you essentially have only one side of a bidirectional claim. if you want to verify the other side of the claim, then the following will do:

  • if you have access to the collection or an authoritative representation of it: iterate over items/orderedItems until you either find the object in question, or until you’ve run out of items.
  • if you have access to the Add activity and can verify its provenance: check the object/target references match.

obviously the latter is much easier. the only point of contention is how to discover this Add, and how to verify its provenance.

  • for provenance, the current model of fedi allows an activity to be proven if it is delivered from origin, fetchable from origin, or signed for redistribution

it may not be possible to unify all such cases in one property, so the “best” we have currently is like so:

replyProof

for proof in replyProof:
  assert "Add" in proof.type
  assert proof.object.id == id
  assert proof.target.id == inReplyTo.replies.id

contextProof

for proof in contextProof:
  assert "Add" in proof.type
  assert proof.object.id == id
  assert proof.target.id == context.id

announceProof

for proof in announceProof:
  assert "Add" in proof.type
  assert proof.object.id == id
  assert proof.target.id == object.shares.id

likeProof

for proof in likeProof:
  assert "Add" in proof.type
  assert proof.object.id == id
  assert proof.target.id == object.likes.id

exact property names subject to change, of course.

I have actually suggested something along the lines of "NS:replyDisposition": "NS:Quote" or similar as a way of signalling quote posts in the past. I think there is a lot about this model which is good!

I dislike this approach because it requires indexing the URL of the reply collection in order to process efficiently. If I receive an Add with the target being an object’s reply collection, I have to identfy that the target is that special purpose collection.

I prefer a special purpose activity for this reason. We shouldn’t be scared of vocabulary extensions. In fact, I think it makes things easier to be able to just recognise the type rather than having to infer the behaviour from the URL.

If I could do a ground-up backwards incompatible redesign of AP, I would ideally un-reify things like inboxes, outboxes, reply collections, etc; which are properties of objects more than distinct objects

1 Like

is this any different than what is already done for followers collections and “followers-only” posts? it seems to me like “special purpose” collections are, well… special. isn’t it worth it? at least for context modeling a conversation, you would need to index that anyway, right? but then again, is it really required to efficiently track everything? the argument is stronger for context or replies, but you could make it for any collection, no? (for what it’s worth, my conception is that every collection should be indexed by id, just like every object. assuming you’re using a database at all, that is.)

sure, “we shouldn’t be scared of vocabulary extensions”, but we also don’t need to make unnecessary ones. there’s no need for a Reply activity when inReplyTo and replies exist. “Add to replies” should be a valid way to advertise that someone replied to your post. you could forward the original Create, but you shouldn’t have to. same with Like and Announce.

aside from that, i’m afraid i don’t understand your issue with inbox/outbox/etc collections – if i were starting greenfield and didn’t care about compatibility with mastodon et al, i would rely on them more, not less. more things should be collections. your profile’s stream of posts should be a separate collection than the outbox. this would allow hiding posts by doing a Remove instead of being forced to do a Delete. you could have a more natural paradigm of “unlisted” posts a la youtube. and so on.

The followers collection has special behaviour, yes; but in terms of delivery it’s not really any different from any other arbitrary collection. The fact that most servers don’t allow creation of or addressing to arbitrary collections is irrelevant to the protocol semantics here, if that makes sense?

Semantically it can be modeled that way, but similarly follow request semantics aside there’s no reason you can’t model following someone as {Add, object: personImFollowing, target: myFollowersCollection}; or a Like as {Add, object: objectImLiking, target: myLikesCollection}.

However, the AS2 spec makes these distinct for a reason

The distinction I’m making here is sort of between “objects” and “Objects”. If I could have a complete do-over of the protocol (and maybe RDF, but that’s an even bigger can of worms) I would imbue the protocol with an explicit sense of nesting - i.e. a way to refer specifically to a given user’s followers collection or a given post’s replies collection, etc.

Effectively there are objects in AP/AS2 which are “synthetic” - things like inboxes, outboxes, likes/liked, followers/following, replies, keys - in that they exist only in relation to a parent object, and I’d make that explicit.

Given we can’t do that, I think it makes more sense to define specialised activity types (Like AS2 itself does!) when working with the synthetic collections

(My initial thought was also {Add, object: the reply, target: the parent post's replies collection}, but I realised that the difficulty with this is that it forces implementations to index the replies collection specifically)


One other reason I prefer an explicit ApproveReply activity is that it makes future extensions less fraught - you don’t have to worry about how the semantics work with a generic Approve or Add activity, you don’t have to worry about cross-purpose attacks, etc

distinct in the original action, because they have side effects. there are no special side effects for a reply; a reply is just metadata. even so, i’m not talking about your own follows, likes, etc – i’m talking about making your followers aware of someone else’s activity. there are two ways this could be done:

  • a) forward the activity verbatim. the original actor needs to deal with provenance via signatures or making it fetchable from origin.
  • b) add the activity to one of the special collections. the receiving actor needs to be able to recognize from id that this is a special collection if they want to apply special semantics.

a slightly different option is to look at the object of the Add, ignoring the semantics of the Add itself if you don’t actually maintain collections as activitypub expects you to. if the object is an Activity, process it. if the object is an Object, wrap it in a Create and process it. this is a limited approach, sure, because to do any more than that you actually need to have a concept of collections. but this should get you far enough to detect other people’s replies/in-context posts, likes, announces.

it’s different in that with sharedInbox, the recipient server is expected to know or calculate the contents of the collection for itself. this is probably why arbitrary collection addressing hasn’t taken off yet; you have to give up on sharedInbox in order to do it.

We really could use an envelopedInbox or similar for this purpose, which carries “envelope recipients” information (ala SMTP); but that’s off topic

I’m going to agree with @trwnh here that it makes more sense to list things you accept than a list of things you reject. There’s also the fact that Delete propagation relies on third-parties relaying the messages for you, and that is not entirely reliable. This would be worse in the case where you’re explicitly relying on the collaboration of the server whose replies you want to reject.

That proposal, however, does save a round-trip when the reply is accepted.

I’m going to agree with @erincandescent that the more explicit the activity, the better it is. We already have some guesswork around existing Accept and Reject patterns to know what they are about, and it’s already a pain. I think it would be easier to implement (and maintain) support for new activities if it were something different and more explicit.

Updating the FEP

I finally pushed a PR updating the FEP to explicitly support the case where the authority directly forwards the implicitly approved replies.

The PR also changes the use of Accept and Reject to more explicit ApproveReply and RejectReply to avoid any possible ambiguity with other extensions.

Unfortunately, the main sticking point of defining the “distribution authority” is still not addressed.

On quote posts

As for quote posts, I guess they could be considered as replies. In any case, the more I think and gather feedback about them, the more I think we do need some kind of approval/rejection mechanism for them too. For instance, Twitter apparently stops embedding a post when the person quoting it has been blocked by the person being quoted, reverting to a simple link without any preview. From what I’ve been told, this has not always been the case, and that change contributed to greatly reduce dogpiling caused by that feature.

Unfortunately, in a federated setting, we cannot easily achieve the same thing. Revocable explicit approvals would greatly help, but would still largely rely on the good will of the previously-approved-but-now-blocked account’s server to redistribute the revocation.

1 Like

[Deleted PR reference, since the PR is now merged.]

Wouldn’t this break federation with forums and groups and Facebook-style platforms?

The typical permission model on a forum is that the forum administrator determines who can post and who cannot post in the forum. Someone who comments on a thread has no authority to override the administrator’s policies, so forums would not implement permissions on a per reply level. Doing so would break thread-based discussions.

It works the same with with private or semi-private groups, or Facebook-style platforms, except in that case it is typically the conversation starter that has authority over who replies. If the person who started the thread wants to turn off discussion, then no one can reply to any of the comments or replies. A person who replied to the original thread cannot override this decision. Comments and replies are turned off.

So, what happens when a forum or group or Facebook-style platform does not implement this proposal since that is not how permissions work on those platforms?

@WisTex all good questions. Think of it as – this FEP just expresses the data model of how to express the preferences. And then the who gets the say (forum administrator, or the original poster) depends on each usecase and instance and implementation.
Meaning, these are orthogonal, so as long as our data model is flexible enough to express the different options.

1 Like

my current position is that conversations should be modeled by context. FEP-7888 attempts to clarify this situation further. with respect to “control”, which is really authority, the context can be a collection explicitly managed by that authority. the normal distribution flow would look kind of like this:

  1. you send Create to the authority
  2. the authority sends an Add to all participants/observers, targeting some explicit collection representing the conversation. it makes sense to serve this collection via context.

the current fedi flow is more like this:

  1. you send Create to whoever you want
  2. there is no authority over replies. or at least, no one cares, since no one is checking the inReplyTo.replies collection for approval, they’re just implicitly reconstructing the reply tree based on what they received.

tangentially, this also leads to cases of “missing replies” or “ghost replies”, and this is intended to be solved by inbox forwarding, but it gets complicated when the person replying is blocked or otherwise not forwarded by the person they replied to – the direct recipients will still see what was sent to them directly. there is also the problem of inbox forwarding not working, either because the person being replied to doesn’t support it, or because the person receiving the forwarded message does not have a way to validate or verify it. proof can be established if the object is directly fetchable from the origin, or if it has some embedded signature, and the adoption of LD Signatures has several issues with it, not least of which is that it is based on an outdated and deprecated proposal from 2017. FEP-8b32 tries to offer a modern solution to this problem.

now, with applying controls to a “conversation”, you basically have two options:

  • prove that the object is in inReplyTo.replies, if you are using the reply-tree as an implicit collection
  • prove that the object is in the context, if the context is an explicit collection that can be resolved.

a complicating factor is that these proofs can be established separately. an object may be provably part of some context collection, but not stamped for approval by the author of the replied-to object. what do you do in this case?

another complicating factor: what if there are multiple inReplyTo or multiple context? do you allow an object to exist in multiple conversations, or be in response to multiple things? how would you handle the associated proofs in that case?

moving on: the nature of the proof is different depending on whether you are trying to verify an object is part of some replies or part of some context. with the context, you can browse the collection directly and you don’t need to prove that items in the collection are in the collection – that’s a tautology. but browsing the replies collection directly doesn’t show you the full conversation, so you usually need to find the root of the tree, and recurse downwards from there… and you will quite possibly encounter broken links along the way, or cases where the replies collection doesn’t exist or isn’t accessible to you, or cases where the inReplyTo isn’t accessible to you. it’s a lot more work, and a lot more fragile.

if you allow for proofs via Add activities, you end up with the following flow for the context as conversation model:

  1. send a Create to the authority
  2. the authority sends an Add to their audience, including you
  3. you take that Add and serve it via some property on the object (via an Update). this allows your audience to verify the context link without fetching and iterating over every single item.

versus for the reply model under this proposal currently:

  1. send a Create to your “distribution authority”, which is assumed to be the direct replied-to author.
  2. the person you replied to sends you an Add to replies.
  3. you send an Update to your audience, or otherwise a “delayed Create” (which I am extremely uneasy with, since it doesn’t play well with C2S), including that Add via some property on the object

to handle the case of closing submissions:

  • the context model simply stops sending out Adds.
  • the reply model has no explicit conversation, so you can only do this for subtrees.

and so, reiterating an issue with reply approvals as currently formulated: they are extremely inconsistent. you might not be able to reply to some post, so you just reply to some other post that lets you reply. every single new reply is a “trojan horse” for allowing unwanted further replies, which you have no control over because they’re not replying to you, they’re replying to someone else. there is no explicit conversation that you explicitly own. to get that, you’d have to switch to a context model.

2 Likes

You all are looking at it at a protocol level, and I am looking at it at a UI level. And I am thinking of the information I would need to display the post and its permissions correctly.

We do need context, but I think we actually need more than that to be truly flexible and to reflect the different types of systems that may be connected to ActivityPub.

So far, every platform I have seen has the concept of a top-level post. And every platform has the concept of a reply or comment (which really are the same thing). But not all platforms have the concept of a group, forum, or context.

Ideally, the following information is communicated with each and every post:

  • Post Content
  • Post Format (HTML, BBCode, Markdown, mixed markup, etc.)
  • Post Author
  • Forum or Group or Collection (if any)
  • In Reply To (the post or comment this is in reply to, if any)
  • Top Level Post (if known; should reference itself if this post is a top level post)
  • Permissions at the time of transmittal (who can reply, etc.; subject to change, obviously).
  • Authority (who has authority over the permissions; author’s post or top level post)

It would be really nice if we could be told what type of post this is. For example, is this a text post (like Mastodon or Hubzilla), or is this a Video (like PeerTube) or is this a picture or collection of pictures (like Pixelfed). We could tailor the UI for each type of post.

So I would add

  • Platform
  • Post Type (text, video, images, audio, etc.)

This data can be used to modify the User Interface for the person reading the post (like if a reply button appears). The information about the authority can be used to contact the authority to verify whether the permissions has changed.

You can call these fields whatever you want on the back end, but this is the data someone would need when designing a UI that displays a post and tells a user whether they can reply or not.

So we need more than context, in my opinion.

Back to the protocol, you would define the authority as either the author’s post, the top level post, or in some cases something else, like the context (forum, collection, group). You would need a top-level post field and a context field to know which permissions are authoritative and what to query to get the current permissions.

I could be wrong, but I’m fairly certain @trwnh meant @Context, not just “context”-- which is, importantly, an array of links to “context” documents (i.e. machine-readable vocabularies) that get merged into a “context” in the colloquial sense, i.e., “when this activity/post/action/etc is interpreted, the interpreter should know which exact definition was intended for each property”. So I think you two might be violently agreeing, because they said that the diversity of systems using ActivityPub need to encode their assumptions about “types of posts” into the context files their posts are anchored to! For example, the forum/collection/groupchat context should be encoded into one or more FEP extensions, such that an extra member or two (the FEP vocabulary extension) would be added to the @Context array of that needs that extension to be intelligible/smoothly UI-generable.

Per-object replies are one extension, but maybe groupchats or post-types are other extensions, and you might be onto something that none of this will be sorted until there are many more FEPs. This could also help clients who don’t want to, or can’t yet, implement the FEPs: they can simply choose (or let individual users choose) whether or not to stream content that takes advantage of “extension features” not available to them yet. This could also take the form of displaying content in a different color or otherwise apologizing-in-advance for possibly-bad UX when content comes from a system with features the client knows about but doesn’t support (it’s as easy as recognizing a @Context entry that corresponds to a final FEP the client doesn’t support).

In summary: let’s all get FEPs written and implemented for all the things! :muscle:

not really ^_^; i meant context as in https://www.w3.org/ns/activitystreams#context, a way to group related objects and activities explicitly, as opposed to inReplyTo, which is currently used by fediverse implementations to construct conversations implicitly. the use of context is “intentionally vague”, but with an “intended usage” that i attempt to demystify in FEP-7888. namely, i argue that it should be a collection that can be explicitly targeted by Add or Remove, and the owner of that collection is the conversation’s authority. it is possible to hint how you expect that conversation/collection to be rendered if you define some extension type like Wall or ForumThread or ChatRoom or whatever, but i think that the presentation actually isn’t that important. you could just as easily render a “forum thread” as if it were a “chat room”.

the @context in json-ld is just a way to map term to some IRI like https://example.com/term. you can’t (and shouldn’t) match against it, because there is no guarantee that you will share the same @context even if you share the same terms. anyone can create their own @context. what you can do is expand the json-ld to be unambiguous, then possibly compact or flatten it to make it easier to deal with in app logic. you might be able to iterate over all the properties and heuristically determine that some properties were dropped along the way.

at a UI level, there isn’t really any need for encoding “permissions” into how an object is rendered. if there is, you can make use of certain properties to signal that, for example, “this user manually approves any Follow activities and may not automatically send you an Accept Follow” is signaled by the use of as:manuallyApprovesFollowers = true (tangentially, this property should not be defined in the activitystreams namespace, but we all collectively pretend it is for historical reasons).

a “top level post” isn’t necessarily special; it just happens to be the root of the reply tree, and therefore offers you the best chance at covering the entire tree through recursion downwards.

a “reply” and a “comment” aren’t the same thing, and if they were, this conversation would be unnecessary. a “comment” is a child of some parent post and cannot exist without it. a “reply” is merely a reference, and continues to exist even if that reference fails to resolve.

it is true that not all platforms understand the concept of an explicitly moderated context, but that is “their problem” in a sense – this problem currently already exists.

  • content = content
  • format = mediaType (where HTML is the default, and often the only supported type)
  • author = attributedTo
  • forum/group/collection (if any) = context (not guaranteed, but this is the best fit. there are implementations that use audience or redefine target to do much the same thing, but not quite)
  • in reply to = inReplyTo
  • top level post = not currently defined. either you recurse upwards through inReplyTo (and you might fail to resolve some link along the way), or you find the first/oldest item in the context collection (which isn’t exactly the same thing, but that’s more an issue with the concept of a “top level post” not being relevant for this use case)
  • permissions = also not currently defined, but this FEP proposes http://joinmastodon.org/ns#canReply as a set of all actors that are expected to be able to reply, with support for including the following/followers collections in the set, as well as the Public magic collection. for conversations represented by a context, this area is open for a similar proposal (canParticipate? audience? something else?)
  • authority = indeterminate; you can actually have multiple authorities, and this is the biggest challenge with the current discussion. setting inReplyTo or context can be done at any time, but it doesn’t prove that the object actually ended up in inReplyTo.replies or in the context collection. you need to go and check the other side of that claim in order to establish a bidirectional claim and therefore verify the statement. and not only do you have to deal with verifying both context and inReplyTo separately, but you also eventually have to deal with cases where there may be multiple inReplyTo or multiple context, and the object might provably be only in some of those collections but not in all of them. now, maybe you’re browsing a certain collection directly so you don’t care about other collections. or maybe you don’t care about proving anything at all. but consumers need to make a decision on how to handle such cases. maybe you decide “i am only going to respect context, and only when it has a single link to a single collection, and only when that collection actually contains the object”. maybe you decide “i am only going to respect inReplyTo, and only when it has a single link to a single object, and i don’t care to verify that the claim goes both ways”. maybe you decide something else. this is open to interpretation.

in other words, the answer to the question

is that this is already broken.