FEP-5624: Per-object reply control policies

Yeah, so that’s a clear departure from the current proposal.

The proposal as it stands would only check the approval of the person being immediately replied to. So the scope would potentially widen with replies, but that could indeed be an issue.

The proposal was aiming to support both models without fully specifying them, but it is clear it is not currently adequate.

A third-party actor being the authority also makes sense for groups.

Another thing that is under discussion is quoted posts (twitter’s quote-retweets). I think an approval mechanism would be very important for such a feature, and would also be slightly easier because we don’t have to care that much about a “Facebook” post-and-comments model VS a “Twitter” replies-as-first-class-objects model. I think it’s a good opportunity to see if common approval mechanisms can effectively get abstracted and if splitting the FEP really is a good idea after all.

@macgirvin do your projects handle quote-posts? And if so, do you have an approval mechanism for them, and while we’re at it, do they behave differently for “root posts” and “comments”?

We support all of the different quoted posts mechanisms that I’m aware of (around ten of them). They do not require approval and there is no difference between root nodes and comments.

I’ve made this proposal work for our needs with a small variation for the different conversation models. If that’s unacceptable and we’re asked to double/triple the network traffic of the entire fediverse just so we can 403 strangers’ comments on our posts (which we did already), it might be time to re-evaluate.

i’m not sure if the traffic increase can be avoided, as we would fundamentally be asking for permission for every activity. perhaps some way to ask for or receive future permission would help out, but i’m not sure what that would look like. you’d obviously want support for revocation at least.

alternatively, we could change the whole distribution model so that all activities necessarily flow through the authority. i think that’s what you keep bringing up replyTo as one approach to this. meaning that, you SHOULD NOT any longer send activities to your own followers; you SHOULD send them only to the authority, who will respond back with something you can forward* to your followers. (*forward how?)

  • of course, the devil is in the details, as you cannot prevent someone from sending an activity to their own followers, nor prevent their followers from accepting it for processing. this would necessarily happen if an implementation didn’t understand replyTo
  • and i feel like it bears repeating every time it comes up, i’m still not sure how to prevent replyTo being used to spam an unrelated third party.
  • and also, when should replyTo be used? only when inReplyTo references that object? or can it cover more use-cases / does the authority extend to other references?
  • finally, the “multiple authority” case is still unhandled. although i guess you could make replyTo an array and then forward each response you get

After discussion with the community, we will not be implementing approvals on quoted posts.

of course, the devil is in the details, as you cannot prevent someone from sending an activity to their own followers, nor prevent their followers from accepting it for processing. this would necessarily happen if an implementation didn’t understand replyTo

We live with this today. You can’t stop anybody from doing anything. This model has been tried and proven in the fediverse since long before Mastodon existed. The only hard incompatibilities between these messaging models are where you send comments. We’ve got workarounds for nearly all the other quirks, so let’s just specify where to send comments. I’m open to other suggestions, but having two nearly identical protocols with different names for comments and other objects isn’t my idea of an elegant solution. Any kind of a boolean which says “we use this model” works for me. We did this originally with an “inheritAudience” flag or something like that and replaced it with replyTo because that more accurately fit what we were trying to accomplish, which was simply to direct comments to the actor with authority over these matters.

We can’t force anybody to do anything, but we can get along with those who are interested in getting along, whoever that might be.

and i feel like it bears repeating every time it comes up, i’m still not sure how to prevent replyTo being used to spam an unrelated third party.

Comment permissions.

and also, when should replyTo be used?

For conversational projects, always. Commenters do not and cannot know the exact recipients of a conversation, which could include recipients on other networks they aren’t aware of and in our universe might involve specifying addresses not to send comments to; and cc’ing nomadic identities that ActivityPub doesn’t know about or that other projects intentionally ignore. So they should always send their comments to the only actor that does.

finally, the “multiple authority” case is still unhandled. although i guess you could make replyTo an array and then forward each response you get

You should send the comment to everybody in the array. For now, we’re only seeking approvals from the first one.

Remember we don’t actually need this FEP for anything at all. We can just send a 4xx like we always have and be done with it. I’m happy to simply standardise on the syntax for canReply, even though it doesn’t cover all of our needs either. We also provide time-based and protocol based comment restrictions and a few other variants. But canReply covers 99% of our needs, which is dealing with random strangers commenting in our family-friendly spaces with random kiddie porn and penis pics and of course the day to day spam and harassment which afflicts other fediverse projects.

the reason i explored a replies vs comments split above was not simply a boolean difference. there are some big (possibly intractable, possibly reconcilable) differences between the two models. if anything, “comment control” is far easier than “reply control”. almost trivially so, in fact. this is because the authority is constant in comment-based systems; authority cascades down through the whole conversation.

arguably you could implement a comment system with replies and inReplyTo (and just add replyTo and maybe a boolean like you said, or maybe replyTo implies a comment system?), but there would be some expectations broken

long exploration
  • an object’s replies collection implies only direct children, that is, objects with inReplyTo set to the immediate parent object. a proposed/hypothetical comments collection would contain all descendants, even indirect ones.
    • you wouldn’t be able to support both replies and comments as separate concepts. you can reply to a comment, or you can have a top-level comment just like a top-level post.
    • how do you page replies across multiple dimensions? just chronologically (or reverse chrono)? comments at least are a flat context…
  • traversing replies is done recursively. traversing comments is done via a single collection.
    • consuming implementations that are only reply-aware and not comment-aware could end up confused or doing a lot of unnecessary requests due to not being aware that recursion isn’t needed. even with a recursion depth of n=1, you would be recursing against all descendants instead of just immediate descendants.
  • a reply is self-sufficient; a comment is not.
    • arguably an implementation detail? you could show orphan comments in an actor’s outbox or not.
  • a reply’s parent context is immediately knowable. a comment’s context requires resolving all the way to the root (in the absence of some other property pointing to the root object).
  • crucially: replies can be addressed to anyone. comments should/must be addressed to the conversation owner / authority in order to be valid/validated.
    • you are allowed to drop addressing to someone up-thread and still have a valid reply. if you drop the replyTo or ignore it, you will no longer have a valid comment.

so you’d end up needing the following to be adopted and understood:

  • replyTo = the authority
    • replyTo should be copied as-is?
    • how do other third-party observer implementations know that this is valid or not? it seems like they would blindly copy replyTo for their “comments”, leading to a potential amplification attack if someone just sets your actor as the replyTo. you would receive a lot of unrelated replies that don’t immediately appear to be invalid. in SMTP the use of Reply-To is similarly problematic if an email client just follows it without a second thought.
    • it also complicates UX because you no longer know exactly where your reply will be delivered.
      • replyTo may be dropped by an unaware implementation
      • replyTo may be recognized, but the author may wish to break the comment chain intentionally and send a reply to some participants not including the root authority.
  • ??? = the root object (commentOn or perhaps something more general that could be applied to reply-only systems)
  • some way to tell if the replies collection contains immediate children or all descendants
  • some way to tell if any given object is a reply or a comment
    • (presence of replyTo may be enough… or not)
  • some way to handle when a comment chain breaks but a reply chain continues (i.e. in cases where replyTo is dropped and/or addressing has changed)
    • some way to handle commenting on a reply-chain? (i.e. don’t set replyTo in such cases)

in summary:

replies comments
context nested flat
class first-class (self-sufficient) second-class (dependent)
scope immediate parent root object
addressing anyone only replyTo
authority isolated inherited

i don’t really see an easy way to reconcile these differences easily or elegantly with just a boolean. you could maybe discount class as an implementation detail and just serve orphaned comments as replies, but the other four considerations are pretty significant. addressing and context in particular.


example flow:

  1. alice makes a post while comment-aware (id = a:1, replyTo = alice)
  2. bob replies to alice’s post while comment-aware (id = b:2, inReplyTo = alice:post1, replyTo = alice, to = alice)
  3. charlie replies to bob’s post while comment-unaware, and mentions delta (id = c:3, inReplyTo = bob:post2, replyTo was dropped, to = bob+alice+delta)

how can alice or bob reply to charlie’s post while both are comment-aware?

furthermore: say bob approves charlie’s reply (as bob sees it as a valid reply) but alice doesn’t (since it’s an invalid comment).

  • does delta consider charlie’s post as valid or not? whose approval does delta check for?
  • does it depend entirely on whether delta is comment-aware as well?
  • does each user now have to consider whether each other participant is comment-aware or not? how can the user know if their post/reply was considered valid after delivery?
    • if you send out accept/reject to signal side effect processing or approval, then charlie may receive an accept from bob and a reject from alice, but charlie has no idea whether their post/reply was shown to delta or not.

4a. let’s say delta is comment-unaware and replies to alice’s post. delta’s post is orphaned because alice doesn’t see it as a valid comment.

4b. let’s say delta is comment-aware and replies (d:4) to alice’s post. delta also replies (d:5) to bob’s post separately. the conversation thread now looks like this:

  • a:1
    • b:2
      • c:3
      • d:5
    • d:4

if you ask alice, alice sees it like this:

  • a:1
    • b:2
      • d:5
    • d:4

alice serves the following comments collection via replies:

  • b2
  • d4
  • d5 (inReplyTo: b2)

say edgar (comment-unaware) decides to check the conversation from alice’s perspective by fetching a1 replies. edgar decides to recurse once (depth n=1). edgar has to make 3 requests (one useless call for d5) instead of 2 (only for b2 and d4). additionally, edgar has to deduplicate now because fetching the replies for b2 returned c3 (new) and d5 (already known).

say the conversation actually stretches much deeper. alice is aware of a comment that’s 15 levels deep, and returns it in the replies collection (as well as all ancestors). edgar has a recursion limit of n=3 and only expects to see replies up to 3 levels deep. however, edgar already has encountered a chain that’s 15 levels deep before recursing even once! this breaks some ui/ux assumptions edgar had made – edgar expects that replies only contains immediate children, so asking for a context of 3 levels should return a context of 3 levels. but it hasn’t.

summary 2

these problems all arose because alice decided comments and replies are the same thing.

i think it’s important that replies and comments should not be confused; replies should contain immediate replies, and comments should be served via a different collection.

additionally, comments exist only within the comments collection of the root post. the question of whether a comment is valid or not depends entirely on whether the comment authority decides to serve it via the comments collection or not. put another way, comments are like direct messages that the authority decides to publish. note that your object may be both a comment and a reply.


back to the fep

in particular, this FEP for reply controls assumes a replies-only worldview. it was written with that assumption in mind, but i would like to see it be applicable to the comment model as well. although, it seems like the comment-aware implementations may wish to pursue a different distribution path in general.

  • a “reply” would obtain reply approval and then send it off to their followers, anyone mentioned, the person being replied to, etc (alternatively: send it first, obtain approval, then send an Update)

  • a “comment” would be sent only to the comment authority; being forwarded by the comment authority carries implicit approval. (i’m not entirely sure how the authority forwards to the commenter’s followers, or the specifics of how an activity is rewritten to address more actors than only the comment authority. i suppose dual-classing as both a reply and a comment can help.)

  • a “reply to a reply” would be no different than a “reply”

  • a “comment replying to a comment” would be the hard case – you have to consider whether to obtain reply approval or not. the comment part is the same (with implicit approval). this also varies depending on whether you are comment-aware or reply-control-aware (or both), and whether you are dual-classing.

  • a “reply replying to a comment” is also a challenge, but less so – you could obtain reply approval, but the comment chain would be broken by virtue of not being comment-aware.

summary again i guess

comment:

  • send only to comment authority
  • comment authority will forward it or not

reply (current state of FEP):

  • check approvesReplies = true
    • optionally if canReply is present it advertises hints on whether your reply will be accepted
  • send pending reply to reply authority (or send out unapproved reply)
  • obtain Accept and add replyApproval
  • send out modified reply to followers (or Update the previously-unapproved reply)

unsolved issues:

  • signal that the conversation is owned by someone else (a la replyTo but still allowing arbitrary addressing)
    • how to know if this is a valid claim? i could just claim a random person is the authority and get a random signature. or i could amplify-attack someone by getting others to ask them for approval
  • potential for multiple authorities / multiple approvals (an object may be in reply to multiple other objects, or other types of approval/controls may be proposed)

What we’re trying to do is educate charlie’s software devs so he will instead automatically send his comment to the appropriate place for the target conversation. That is what I meant by “getting along”. Because as pointed out here, each model has certain advantages. The replies-based model provides greater reach. Great for public threads and influencers. The comments-based model provides better audience control for protected/restricted conversations.

What we discovered is that supporting both is a win-win. Maybe somebody else will also discover this. It’s not like I’m trying to keep it secret.

1 Like

i agree that supporting both is good! and indeed, the comment model does provide better control… which is why this FEP aims to provide a similar level of control for replies. there’s just the contention around how to safely define who the authority is. i think that replyTo gets us halfway there, but it breaks the reply model as currently defined:

this “appropriate place” and “target conversation” is not immediately known in a reply model. the “appropriate place” is arbitrary. you can usually assume your followers, anyone mentioned, anyone replied to, anyone in to/cc/audience… (“inbox forwarding” also calls out object and targetas potentially relevant.)

essentially we need properties to point to the “conversation” and optionally the “conversation authority” if it’s not the conversation author. these properties should carry over if present. if not present, then we can assume a reply instead of a comment or dual-class reply/comment.

  • for “conversation” it may be suitable to equate this with context… i am not 100% sure. on the surface it looks like it would work, as context is intended for grouping related objects or activities. but i hesitate because of the “intentionally vague” definition, which has led to conflicting interpretations. Yuforium in particular has used context for pointing to an array of Topic actors. a strange use to be sure, perhaps better fit for tag, and it depends on an extension, but it’s worth mentioning here as an example of divergent context usage in the wild. but also, i am not entirely sure that defining a conversation extension is justified (and conversation itself has divergent extension usage as well).

  • for “conversation authority” we might say it is equal to context.attributedTo, which may not be the same as whatever the root object’s attributedTo is.

    • also tangentially it would be nice to have an unambiguous way to point to the root object – it is not guaranteed to be context.items[0] or context.first.items[0] or context.orderedItems[0] or context.first.orderedItems[0] or context.last.orderedItems[end] (although it might very well be one of those five).

      • something like commentOn but i’d prefer a property name that doesn’t imply a comment model and is instead more generic, and maybe also doesn’t imply approval or an approval flow. although maybe it’s sufficient to use commentOn as you generally only care about the root object in cases where you are sending a comment (though not always)? maybe root or replyRoot? thread? probably more thought needs to go into figuring out which qualities we care about here…

summary

so basically maybe

  • context always implies a “conversation” (or conversational context)
  • which should be copied over as-is to preserve the “conversation” (conversational context)
  • and it should resolve to an Object or Collection with attributedTo at minimum (pointing to the “conversation” owner)?
  • if present, it implies you can send comments and to where – or does it not imply that?
  • if context is an array then you can resolve multiple conversations that you can address your comment to?
  • if context is dropped along the way – i suppose it implies a broken comment chain and a fallback to replies-only threading.
  • caveat: we may need a different property for threading/grouping purposes in non-comment situations?
  • also could use an unambiguous way to point to the root object in a chain of replies… or what is being commented on (unsure how to define the scope of this property)

this would probably end up in its own FEP and potentially the comments collection. comment approval probably doesn’t need to be covered by this FEP because the comment distribution method has implicit approval, but something like canComment/commentPolicy should probably be formalized as well outside of this FEP.

alternative ideas include making inReplyTo an array with the first item being the immediate parent and optionally additional items being ancestors. but this would imply needing approval from all reply authorities?

hopefully i haven’t missed anything important

I guess the big difference here is that we never ever import a post if we can’t import the root of the conversation because it is required by our model. This is defined on the ActivityPub side as the ancestor which has no ‘inReplyTo’.

Under ActivityPub we’re forced to traverse the tree backward from the current point until we find it. Under Nomad, conversation fetches returns the entire conversation (Collection), including all other branches of the tree. We emulated this functionality in ActivityPub using ‘context’ to return the conversation.

The sticky point seems to be that there are multiple conflicting uses of ‘context’ under ActivityPub as well as no defined mechanism to fetch the entire conversation. So maybe we call it [ “context”, “nomad:conversation” ] to indicate that this context represents the current conversation and not something else. It could also be used to detect that the conversation model is in use and fetch the conversation. I used nomad here just so as not to confuse it with ostatus:conversation, which we only recently deprecated. I’d be happy to call it something else completely, but this is just a trial idea.

If context is dropped along the way, or traversing the tree doesn’t find an ancestor with no ‘inReplyTo’, we’re done with it and toss it. We see it as a rogue comment, as parentless threads do not exist in our universe.

I guess in your universe, one might process them anyway.

I’m not ignoring your other points - I’m trying to focus on the important bits, which seem to mostly revolve around foolproof identification of conversation-model entities. TBH I’ve never had a problem with this, but I agree that it may not be as easy or foolproof as I originally suggested. Perhaps used alongside nomad:replyTo in order to immediately answer the question of where to send the comment without requiring you to fetch the conversation to find it. Incidentally we also copy the replyTo field over to new activities in the same container in the same manner as context (and the old ostatus:conversation). We probably don’t need to - because we only ever check replyTo on the root node, but this might be helpful for replies-model projects which allow parentless conversations.

1 Like

right, that’s what i’d like to avoid. traversing upwards may result in broken links or links which you are not authorized to see. if it’s relevant, there should be a way to get back to the root. i suppose it’s up to each author to decide if it’s relevant to advertise this root or not.

i generally support using context for fetching the conversation from the root, as you describe in Nomad, but the problem is that

  • context is not guaranteed to always resolve in the wild
  • context is not guaranteed to be a Collection
  • if it is a collection, context may contain Object or Activity, so you have to be prepared to possibly unwrap activities or deal with nonwrapped objects
  • collections are not strictly or consistently ordered except for OrderedCollection which MUST be reverse-chronological and not forward-chronological (see Stricter specifications for pagination of Collections and OrderedCollections for more discussion)

ideally, the conversation (regardless of which property it is referenced by) would always be a strictly ordered (forward chronological?) Collection where the first item (or last item, if OrderedCollection) is the root object or activity. unfortunately in practice there are five different possible paths to check depending on whether the context collection is paged or not, unordered or ordered or Ordered. so you end up having to deal with a lot of conditional checks and cases instead of just using one property and being done with it.

it could be served via both context and nomad:conversation but that doesn’t really solve the issue unless you define nomad:conversation with stricter Collection ordering. it would be better to use context and have some other consistent way of fetching the “root object” from a given context.

this sounds a bit ridiculous at first but perhaps context.context could work? “the context for the context”.

  • alternatively, maybe something like context.generator, but that’s a bit less semantic (as it seems intended to point to the application that generated the activity or object, but it doesn’t have to be).

  • we could also break the spec and say context.origin (plucking the origin out of the Activity properties and grafting it onto Object`), but this seems strictly worse.

also tangentially, for C2S I suppose you’d have to do something like

  1. Create a Collection
  2. Create an Object with context pointing to that Collection
  3. Add the Object to that Collection
  4. Update the Collection so that context points to the Object you just created

with the caveat that a remote server might receive the Create and try to reply before step 4 is completed. so that’s a possible race condition in automated systems, but hopefully not likely to occur.

so i suppose that could work like so:

  1. if context is present and it is a (root) Object, stop and return
  2. if context is present and it is a Collection, try to resolve context.context
  3. if you fail, try to traverse the inReplyTo chain upwards
  4. if you fail, discard the comment (or treat it as a standalone reply)

in a comment world, the conversation authority might send you an Add Object to the context collection, or the root authority might send you an Add Object to a comments collection, and then you could reprocess the object?


ok back to the fep again

I suppose we’re back to the question of dealing with an external observer trying to validate whether a dual-class comment-and-reply was approved… so far, i’ve been sort of punting it and assuming “if you care about comment approval on a dual-class object, just ask the conversation authority for their view of the conversation”. that’s probably not sufficient, though, so maybe a commentApproval/commentOn is still justified?

at least in a comment-only world using nomad:replyTo your comment will be a DM and will not be visible to your own followers unless forwarded later, right?

long example the case i'm particularly confused by is like so:
  • alice has a post
id: alice.com/posts/1
attributedTo: alice.com
content: "comment below with your thoughts about cheese"
context: alice.com/contexts/1
  • bob makes a dual-class reply+comment:
id: bob.com/posts/2
attributedTo: bob.com
content: "i love cheese"
inReplyTo: alice.com/posts/1  # this makes it a reply
context: alice.com/contexts/1  # this makes it a comment?
audience: alice.com, alice.com/followers, bob.com/followers
  • alice forwards bob’s post to alice’s followers, carrying implicit approval:
id: alice.com/activities/fe538744-110d-44bf-8f4e-efbeee5fc498
actor: alice.com
type: Add
object: bob.com/posts/2  # this should be fetchable by alice's followers or otherwise inlined with a proof
target: alice.com/contexts/1
to: alice.com/followers, bob.com
  • bob’s followers need to validate either or both reply-approval and comment-approval
    • if bob somehow forwards alice’s activity, it carries implicit approval for the comment, but it might be out-of-date as alice could remove it from the conversation and bob might not forward this removal
    • if bob doesn’t forward alice’s activity, then bob needs to attach approval via some property

reply approval

in a reply-approval world, alice sends bob an Accept and bob sends an Update to bob’s followers:

id: alice.com/accepts/bob.com/posts/2
to: bob.com
actor: alice.com
type: Accept
object: bob.com/posts/2
inReplyTo: alice.com/posts/1  # currently in the fep
approvalCondition:  # currently not in the fep, but we should be able to explicitly specify the conditions for approval
  # basically saying "i approve this reply on these conditions"
  # e.g. that `inReplyTo` has this value,
  # that `content` has this exact hash,
  # and so on.
  # (idk what vocab to use for this though or what it should look like)
id: bob.com/posts/2/history/2
to: bob.com/followers
actor: bob.com
type: Update
object:
  id: bob.com/posts/2
  content: "i love cheese"
  inReplyTo: alice.com/posts/1
  context: alice.com/contexts/1
  audience: alice.com, alice.com/followers, bob.com/followers
  replyApproval: alice.com/accepts/bob.com/posts/2  # added by the update
  • bob’s followers validate like so:
replyApproval.actor == inReplyTo.attributedTo
replyApproval.type == Accept
replyApproval.object == id
replyApproval.inReplyTo == inReplyTo  # current FEP state -- ideally would be replaced by explicit approvalCondition

comment approval

in a comment-approval world, either

  • bob’s followers check context for the whole conversation (which is bad for validating a single comment)
  • bob’s followers check commentApproval which is defined similarly to replyApproval
  • bob’s followers check a generic approval or conversationApproval or whatever unity we can achieve between the two models
  • bob’s followers must also follow alice, as alice controls the entire conversation

taking a step back: bob’s followers still need to decide whose authority to respect.

or, in other words: bob’s followers need to decide whether to even respect bob’s post as a standalone reply if it exists in a specific conversational context that doesn’t belong to bob.

in effect, does the conversational “comment model” override the standalone “reply model”?

another attempt at reply-vs-comment and a bit of a breakthrough

there is also a possibility we haven’t considered yet – what if bob changes the context? in effect, anyone replying to bob would be part of bob’s conversation now. or part of no conversation at all.

in fact, maybe we can define the difference between “reply model” and “comment model” like so:

  • comment model: the context is inherited (copied as-is from the parent, whether immediate parent or root parent)
  • reply model: the context is replaced by the author’s own context or removed entirely

and we could extend it such that “approval” is tied to that conversational context. no context, no approval needed. context present? approval needed.

we could derive the following algorithm for dealing with conversational context, that applies to both replies and comments:

  • when making a root post that doesn’t require approval, do not set context
  • when making a root post that requires approval, set context = some Collection
    • (in effect, this sets the authority via context.attributedTo)
    • (context is a Collection and context.context should point to the root object in the conversation)
  • when replying to a post that has context set:
    • copy the context of the immediate parent (inReplyTo.context) to turn your reply into a comment on the same conversation as the immediate parent
    • copy the context of the root object (context.context.context (yes i know)) to turn your reply into a comment on the root object
    • set the context to your own Collection to declare your post as a standalone reply that requires reply approval
    • drop the context to declare your post as a standalone reply that doesn’t require reply approval
  • when replying to a post that has no context:
    • leave the context off to declare your post as a standalone reply that doesn’t require approval
    • set the context to your own id or Collection to declare your post as a standalone reply that requires reply approval

this gets us surprisingly far with pretty good coverage of the stated aims, with the following caveats:

  • you have to explicitly choose between controlling your own replies vs. giving up control to another conversation authority. if you participate in someone else’s conversation, you don’t get to control who replies to you unless you opt out of that conversation and make your own.
  • you cannot easily find the root object for any reply thread; you can only find the root object for a conversation
    • note that as a consequence, a root object for a conversation MAY have inReplyTo set; this is just metadata, though, and should not be used for threading

bonus:

  • this could maybe help with managing group posts in a way that doesn’t strictly depend on FEP-400e. the conversation authority would be the group, and context inheritance would be strictly enforced (or else the group won’t add your post to the conversation). replace target on objects with context.

  • this could also be used with something similar to “quote tweet”/“quote post” and the desire to control who can “quote” you. in effect, a “quote post” is just a reply that changes the context to be part of its own conversation, turning inReplyTo into just metadata. such objects should be rendered as new top-level posts, with the replied-to post being rendered as a preview above (or possibly just a link, if it has a url). effectively this collapses “quote approval” and “reply approval” into the same problem space.

    • caveat: we now need to define “quote approval” in much the same way that the current “reply approval” is defined in the current FEP: with replyApproval (or is it now quoteApproval?) having to be obtained from inReplyTo.attributedTo if inReplyTo.approvesReplies (or possibly inReplyTo.approvesQuotes). or, put another way, i guess what we need to do is disambiguate between in-context replies and out-of-context replies. disallowing “quote tweets”/“quote posts” is really in effect just disallowing out-of-context replies.
  • following a conversation should be easier because you can literally Follow a context if it is an actor. or maybe we define protocol-level expectations that if you send a Follow of the context to context.attributedTo then that should be supported. anyway i’m punting this point over to Unresolved issues surrounding Follow activities because there’s far more discussion there

ui/ux and related implications

  • if context is present and dereferenceable, this should be shown to users before they reply as a hint that they are participating in someone else’s conversation, and their reply may not be accepted
  • the user should know which conversation they are participating in, or at least, who owns it
  • mention-based addressing may no longer suffice. or, put another way, participating in a conversation implies that your post will be delivered to someone whose mention you are not allowed to drop

My comment wasn’t to tell you what you should or should not do, but point out that you departed from the proposal (which is still a draft) in a way you haven’t mentioned before: you’re not just doing something specific in the wiggling room allowed by the current proposal, but changing how the approval is checked (by not checking from inReplyTo). If that’s what you have to do for to work with the post-and-comments model, then it means my proposal falls short, because I wanted it to accommodate both models.

That I am a bit confused about: I would think the concerns and protocol implications of quoted posts are similar to that of replies, so I am not sure why there would be value in restricting replies but not quote-posts.

Afaik, at least Mastodon and Pleroma currently carry over context on replies to provide a quick way to tell that two posts are somewhere in the same thread, but to my knowledge, both of them are exclusively using a reply-as-first-class-object model, so I wouldn’t consider that to be enough to decide on the reply model. But if we can decide something is using a post-and-comments model, then context being either the root post or a collection of all the known posts sounds fine to me.

Note that the post-and-comment conversation model is not necessarily just about reply control, but also about having the original author control the audience of the replies (that is, comments to a circle post are visible to members of that circle, not a different audience). This has wildly different UX considerations though, so I’m not sure we want to tie reply control and audience control together (that being said, in the post-and-comments model where the root post’s author has total control over the replies’ audience, they implicitly have control over who can reply or not anyway, so the whole approval part of the FEP is far less important).

I would think the concerns and protocol implications of quoted posts are similar to that of replies, so I am not sure why there would be value in restricting replies but not quote-posts.

For me, this all boils down to what you can control and what you simply cannot.

Replies/comments to my posts are going to end up on my public facing home page and be sent to all my friends. So you can create them all you want, but I get to moderate and possibly reject them from being published on my site and forwarded to my audience, my friends. I can’t stop you from sharing your rejected comment with your own audience (as Mastodon currently does by default - regardless of my rejection). I can only control you attempting to share it with me or passing it along to my friends.

This is the only advantage I can see in this proposal, because I will finally be able to “advise” Mastodon folks not to share the comment I rejected with their friends and keep this from happening automatically on every comment. I still can’t stop them from doing it. I can only advise.

The difference with quotedPosts, is that you are basically talking about me behind my back, and this is physically impossible for me to prevent. It is your conversation and your audience. You can notify me out of courtesy (and we do this), but it’s outside my ability to control what you do with my posts once they leave my site.

Imagine if I need to ask Donald Trump to quote one of his election-stealing tweets with some critique and corrections and share it with my friends. How do you think this would have played out if Twitter required approval of quote posts in the 2020 US election and CNN wanted to publish a fact check?

For this reason I will not support approvals on quoted shares and at least for me, it is non-negotiable.

My comment wasn’t to tell you what you should or should not do, but point out that you departed from the proposal (which is still a draft) in a way you haven’t mentioned before: you’re not just doing something specific in the wiggling room allowed by the current proposal, but changing how the approval is checked (by not checking from inReplyTo). If that’s what you have to do for to work with the post-and-comments model, then it means my proposal falls short, because I wanted it to accommodate both models.

OK, so it fell short. I tried to figure out a way to make the basic mechanism work and then see about changing the proposal accordingly - once I figured out if it could work at all, and after any major bugs had been ironed it. I’m still in the exploratory phase - even though I do have working code. I was treating it as a work in progress based on your comments that changes were still being made. It still keeps momentum moving forward and I did my best to share what I was doing and provide progress updates. It was either that or just say “it’s unworkable” and reject it completely. If you prefer I do that, I guess that’s kind of where we’re at now.

1 Like

this is why i think we shouldn’t check from inReplyTo.attributedTo in all cases. the solution to “replies vs comments” is to have a different abstraction at a different layer – that of the conversation.

put another way: there are two separate but very similar concerns here:

  1. controlling who participates in a conversation
  2. controlling who links to you via inReplyTo

the FEP in its current state does indeed fall short for the first concern, since it currently “hardcodes” checking the inReplyTo. whether this is an acceptable outcome or not depends on the goal we are trying to achieve. that’s enough to satisfy the second concern, but the second concern is the less useful of the two in a future where every implementation should understand what a “conversation” is – in that future, a conversation is not defined purely/only by the reply chain, but instead by some conversational authority that determines which posts are allowed in that conversational context and which ones are not. (and we’re punting the problem of multiple-authority down the road.)


on linking by reference

once again i think this comes down to the two similar-but-distinct concerns we have: controlling a conversation vs controlling a reference. the former has implicit authority by what you choose to publish on your own site. the latter has ambient authority by what others choose to publish on their own site.

what the current FEP tries to do is override the ambient authority of references, and replace it with a token representing explicit authority derived from the author of the linked resource. its only relevance to the “conversation” is in assuming that a mere reference is enough to establish a conversation. the end result is the same, of course – an advisory signal to “please don’t show this object as valid”.

in that way, the current FEP is less about controlling replies (i.e. the conversation) and more about controlling links (i.e. the reference). it is more akin to “mention control” or “link control” and is going to be very difficult to implement anything useful other than what amounts to saying “i approve this message”. the question is, of course, why a third-party observer should care whether you approve of it or not – if you own the conversation, then it would make sense to exercise control over the authenticity of its record. but if you don’t have a concept of a “conversation” at all, then you expect others to also understand that inReplyTo is somehow special. you end up with a pseudo conversational context that is owned by no one, and thus cannot be controlled. unless you hardcode authority to the immediate parent. and why should the immediate parent have authority, again? because we expect inReplyTo to be doing “double duty” and also representing some shadow conversation? maybe this is fine, but it’s awkward.


the “conversation”, as seen by mastodon/pleroma/etc

we wouldn’t have to. audience control could be done by setting audience and then addressing that. collections owned by the original author would depend on that actor to perform inbox forwarding.

this again goes back to separate concerns:

  • whether the post exists on its own (as a “first-class” object)
  • whether to show the post in a thread/conversation (validity or “approval”)

the former is an implementation detail. the latter requires a knowable authority.

you can have replies exist as first-class objects but still require approval from a conversation authority, as long as you set the conversation authority to yourself – you either claim authority over immediate replies (in effect starting a new conversational context that you control), or you defer to existing authority (in effect participating in the current conversation similarly to a “comment”), or you claim null authority (the current default, no conversation). in each of these cases, the signalling of conversational context exists in parallel to how you construct the thread. you’re free to construct based on inReplyTo still – but you are at least aware that a larger conversation might exist and that someone might own it and maybe moderate it.

side note, for the sake of C2S i decided against having context being the root post, although for monolithic impls this doesn’t matter – you would just assign context as a recursive reference on the root post, as ID generation is done at the same time as authoring the activity. (not so for C2S – ids are generated after the activity is defined, so you’d need an Update for either the post or for the context. it’s maybe easier to update the context than it is to update the post, and you’d have to update the context anyway to set the id of the root post, unless your server auto-generated a context for each activity, which i’m not sure is a good idea.)

there’s also the consideration that a conversational authority may not be the same as the author of the root post in a chain, so it makes sense to have a separate context object representing the conversation.

but mainly, the use of context to signal a conversation doesn’t have to be a “comments model only” thing. it can fit the reply model too. i wouldn’t make the use of context contingent on “if” comments are in use. i would think of it as a higher-level abstraction that works regardless of whether you have replies or comments, and in fact, it is an abstraction that clearly denotes authority.

if there is a concern to be raised here, it is not in tying reply control to audience control, but rather, tying reply control to threading in general. which doesn’t have to be the case, either… what you “lose” by having variable context and not simply copying it over blindly, is that you no longer have “a quick way to tell that two posts are somewhere in the same thread”. but in most cases, the thread and the conversation are going to be the same. i do agree that there should be consideration of what happens when you willingly change the context though – as described above, it seems to me like this is effectively turning your post into a quote of sorts. based purely on context alone, it is a different thread; based purely on inReplyTo alone, you are still in the same thread. but context may be null, so you can’t thread purely on context alone. or can you?

(tangentially: mastodon still uses ostatus:conversation while pleroma uses context for this “thread identifier” that doesn’t resolve to anything. you could keep ostatus:conversation for this “quick way to tell that two posts are somewhere in the same thread” that it is currently used for, if it would still be useful.)

all possible cases

i think you would maybe have to enforce that context resolves to something? a context that does not resolve is a context that cannot be used to signal ownership, authority, or control. at minimum, you need to resolve context.attributedTo as an actor (or multiple actors, if multiple authority is ever tackled). it would be useful to also resolve context.context to either the immediate parent or the root post in the chain. or you could have the approval-based properties live on the context object?

but aside from that you have the following breakdown:

  1. there is no resolvable context. there are no controls.
  • you do not set a context on your reply. you are signaling no control.

    • implementations may allow your reply to exist as a first-class object, or they may discard it.
  • you set a context on your reply. you are signaling control over immediate replies (and any descendants that inherit this context).

    • implementations may allow your reply to exist as a first-class object, or they may discard it.
    • implementations may show your reply in a different thread.
    • implementations may choose to render the inReplyTo as metadata above your new thread.
  1. there is a resolvable context. this implies controls.
  • you inherit the context for your reply. you are signaling participation in the same conversation. you gain approval from the context owner, who may be the immediate parent, the root parent, or some other moderating actor.

    • implementations should verify your approval came from the context.attributedTo. if it does not validate, implementations may discard your reply or otherwise mark it unapproved.
  • you set your own context for your reply. you are signaling control over immediate replies (and any descendants that inherit this context). the previous context owner no longer has control over this subthread, since it is effectively a new conversation.

    • implementations may allow your reply to exist as a first-class object, or they may discard it.
    • implementations may(/should?) show your reply in a different thread.
    • implementations may choose to render the inReplyTo as metadata above your new thread.
  • you set null context for your reply. you are signaling no control over replies or descendants. the previous owner no longer has control over this subthread, since it is effectively a new conversation.

    • implementations may allow your reply to exist as a first-class object, or they may discard it.
    • implementations may(/should?) show your reply in a different thread.
    • implementations may choose to render the inReplyTo as metadata above your new thread.

implementation and ux

we can make some observations based on the above:

  • whether the reply exists as a first-class object is a largely separate concern and an implementation detail.
  • the additional considerations are largely only relevant when changing the context
    • therefore, it might make sense to consider if the context of the immediate parent matches the context of the object; implementations may wish to add additional checks or limitations on context-switching, although they probably don’t have to. this is only really relevant for determining if the inReplyTo should be rendered above a given post or not (and whether that chain should recurse upwards to form a thread).

the implementation could look something like this for mastodon:

  • if reply controls are enabled, set a context
  • if a context is present and resolvable, copy it unless you want to set your own reply controls
    • expose this choice to the end user via API / client UI.

basically, users should be aware of when they are participating in someone else’s conversation, and they should be aware that they are giving up control over replies by doing so.

there is also a UX choice to be made about whether having per-post reply controls is even desirable or good UX within the same thread… imagine a back-and-forth between two people who both declare they approve replies. a third party can choose to make a post inReplyTo a favorable authority while actually replying to someone else who would otherwise reject their posts – that is, variable authority implies disjoint and conflicting authority. following the current FEP, if someone makes an approved reply but doesn’t add their own reply controls, they become a trojan horse for other people to reply through them and bypass approval entirely.

(worth noting that twitter treats both reply-controls and circle posts as an implicit comment-model when it comes to authority and approval – these controls only exist for the root post. so twitter doesn’t have these issues. this may imply it is a good idea to do the same in mastodon.)

Is it neccessary to request approval for every reply?
I think in many cases it would make more sense for authority to grant permission once. The representation of permission can be signed by the authority, so replier may simply add the signed representation to Note object under the replyApproval key.

This will reduce the number of network requests required to create and verify replies. In the best case the process would be the same as today, where replier simply sends Create() activity.

Permissions can be revocable. For example, there could be an expiration date after which recipient will be required to check permission validity (by re-fetching permission object from the authority).

i think the current state of talks is to have an Accept activity for each activity, and this gets used as the replyApproval for the third-party observer to verify, but beyond that, there is no specified mechanism for how replies get approved logically. it may be manual, it may be automatic based on some criteria (or not). you could totally have an application feature where replies from certain people get automatically approved, and from anyone else it goes to a sort of “reply request” UI similar to follow requests. you could add or remove people to the “auto-approve” list as you pleased.

aside from that, the reason it’s a per-object approval instead of a per-actor capability is because you may wish to handle those two things separately – if the capability for replying gets revoked, then this should not affect previously-approved objects. you may also hand out a capability for replying, but still require manual approval for actually using that capability.

  • there is also the question of how would you even be able to use capabilities in such a way? i don’t have a full understanding of ocap literature, but i’m not sure capabilities would allow discriminating who can reply if the id is known to everyone, as anyone can just… use the existing known reference? my rough understanding at this point in time is that object capabilities work on a reference level: when creating an object, you pass in references to whatever capabilities it can invoke. having a reference allows you to use it. and we’ve already established that you necessarily have the reference, in the form of the id property. so capability invocation is the wrong tool for this kind of external verification job. the more appropriate pattern is to use a bearer token, where having a valid token authorizes the action.

Even in cases where per-object approval is required the replier could include a full representation of permission to reduce network load. In the current version of FEP-5624 that would mean inclusion of Accept activity:

{
  ...
  "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": {
    "@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",
    "proof": {
      // here goes integrity proof 
    }
  }

(taken from fep/fep-5624.md at main - fep - Codeberg.org)

I have a very limited understanding of capabilities. Does Accept activity in the above example count as capability?
What I imagined is some new object type that will be able to describe permissions. For example, this permission object represents per-thread reply permission:

{
  "id": "https://example.com/reply_approvals/1",
  "type": "Permission",
  "actor": "https://example.com/users/1",
  "inReplyTo": "https://example.com/users/1/statuses/1",
  "proof": {
    // here goes integrity proof 
  }
}

This pattern can be used in other situations: permission to send follow request, permission to mention. I think the creation of new object type is justified because there might be cases where permission is acquired without interaction between actors (so there’s no Accept activity).

well, sure, you could inline/embed the approval after receiving it. but the way it gets invalidated after-the-fact currently is to stop serving the Accept. so having a signed proof included and attaching the Accept activity would mean that it can’t be revoked later… unless you rotate your public key, i think? which has other implications. i think it’s possible to use different keys (i.e. multiple keys per actor) but this is inherently more complicated. regardless, this should be fine / irrelevant for delivered activities; it mostly only matters for fetching something after some time.

put another way: yes, you can wait to receive the Accept, then embed it in your activity, then do “delayed delivery” to everyone else. but this has complications if you are using a generic activitypub server – a generic server will delivery the activity to everyone addressed, so you’ll have to get clever with the way you serialize your activity. it would end up being addressed to the approver, and to no one else. it would show up as a DM since there are no other recipients (and there is no way to signal other recipients without actually delivering to them).

sort of. a capability is “what you can do”, a token of authority that lets you “do something”. for object capabilities, you create an object and pass in references only to what you want it to use, effectively encapsulating the access rights within the object. this reference becomes a capability method.

on the web, we might consider a capability url, a capability token, and a capability proof:

  • a capability url is a url that grants authorization by simply calling it. you invoke the capability by HTTP GET/POST to that url or whatever. example: an “unlisted” google doc or youtube video
  • a capability token is some token that grants authorization by providing it as a parameter to a method call. example: i guess oauth2 bearer tokens count?
  • a capability proof is some verifiable claim that grants authorization by being valid. example: idk look at zcap Authorization Capabilities for Linked Data v0.3 – a proof with a proofPurpose of capabilityInvocation links to some capability document, the cryptographic key that created it, and a signature value to be verified against the creator key. i guess http-sigs also count, but those aren’t generally forwardable unless you replay the entire original HTTP request somehow

if we really wanted to do capability delegation, this would basically be analogous to giving someone a capability that they can use to sign their own proofs. again, it’s certainly something we can do, if we wanted to. but it has different properties. like we said before, with per-actor delegation, if the capability is revoked, then objects will no longer verify as valid when we fetch them later, unless we get a new delegated capability and update all of our old proofs. there’s also the property of an invoked capability a la zcap being harder to verify and validate – at minimum, you have to validate the signature on both the invocation and on the capability delegation, and this scales linearly with every step in the capability chain. speaking of which, you can delegate any capability you have to anyone else, without any input from the original controller. so if you trust the wrong person, you might go to sleep and then wake up to a bunch of nasty replies that have propagated widely because they had a valid proof and valid capability chain! compare this to per-object approval, where the authority resides with the original controller at all times, the previously validated public objects remain valid as long as the original controller wants them to be, and validation is as easy as checking a few properties for equality, and automatic approval of one actor cannot be delegated to other, possibly limitless unknown actors.

not sure this makes sense unless the security properties and verification mechanism are defined more completely. unfortunately zcap-ld isn’t really ready yet.

i think these are kind of the wrong way to think about things. you might consider having someone’s inbox to be a form of capability url. but a Follow can be sent by anyone at any time and you’re not required to accept or reject it – you can just let it sit there forever, or discard it, or whatever you want. the problem with “mention control” is that, not only does it quickly explode in complexity due to the ability to mention multiple people (do you seek approval from all of these people? how do you encode or serialize their approval? what happens if some people approve but some don’t?)… but also a mention isn’t actually supposed to be anything more than a link. it’s a mastodon-ism that a Mention is required in order to generate a notification.

1 Like

Emedded Accept can’t be revoked unless there’s an expiration date – another reason why I was thinking about introducing new object type. If replyApproval contains expired permission object, recipient should fetch it by ID to see if it is still valid.

My post was in part inspired by reading zcap spec… Perhaps something simpler and less powerful has a better chance of being adopted?

I want to have an inbox that can reject Note with mention if it doesn’t carry a valid permission object. This is an anti-spam measure: I expect that sooner or later spammers will start to mass-send activities that generate notifications, and currently the only solution we have is shared blocklists.

maybe revocation is not exactly the right word, but wouldn’t you invalidate all existing proofs that reference your key if you change the key? this doesn’t have anything to do with object types as far as i can see.

again, do you reject if any Mention is missing some signature? this seems like a good way to break receipt of any mentions unless every single implementation rolls out the exact same scheme as you. and i can’t see any value in having some mentions verified, but some mentions not. and furthermore, why do we even add special handling to Mentions at all? you can already mass-send activities that spam lots of people using to/cc/audience.

this is why i said that “mention control” is a red herring. a Mention should be basically no different than a Link. <a href="https://trwnh.com">trwnh</a> is a “mention” in HTML, and you can use the Webmention protocol to notify and aggregate such mentions.’

you may more generally consider the problem of “link control”. basically, unless the link has some special qualification, it doesn’t make any sense to ask for approval unless you explicitly want to signal that you obtained prior consent from the controlling authority.

the reason we are trying to do this for replies is because replies have a semantic quality of being part of some larger conversation, and we don’t want to bottleneck ourselves on only getting this conversation from the conversation authority. having the “reply approval” allows you to distribute updates to someone else’s conversation.

this pattern doesn’t work for mentions because mentions don’t have those semantics – they’re basically just regular links. the semantic of a mention is that the “name” (inner content) in some way identifies the “href” (the hyperlinked reference). so in the HTML example from before, trwnh is a Mention of https://trwnh.com. this statement is in no way dependent on the authority of https://trwnh.com; it’s more like a nickname in your address book. in plain english, you could equivalently say “every time i mention trwnh, I am talking about the subject of the resource https://trwnh.com.” in fact, you can mention someone without even addressing them or sending them the document that mentions them! you don’t have to let someone know that you mentioned them. this only fails if you tie mentions and addressing together (which i think you shouldn’t necessarily do).

for the purposes of “anti-spam”, you can only depend on your own internal policies. you may consider dropping any post that is addressed to too many people, like pleroma’s “hellthread mrf”. or, you may consider dropping any activity addressed to you that didn’t come from someone in a collection you maintain, like only allowing messages from people you follow. or, instead of dropping messages, you may decide to process them successfully but otherwise suppress notifications. it’s entirely up to you.

in any case, the scope of this FEP is specifically about the inReplyTo metadata field, and giving third-party observers a way to construct conversations with some kind of authority considerations. coincidentally, this is why we/i talked so much about conversations above – the abstraction of the “conversation” allows us to specify an authority who controls that conversation. you can consider a similar problem that FEP-400e tried to solve but with less particulars: how do you know an object is part of a collection without having that collection?

The key doesn’t change, instead the whole “permission object” expires. This can be compared to JSON Web Token which also can have expiration time, or Verifiable Credential. Actually, VCs seems to be exactly what I was looking for.

Maybe I’ll accept it but won’t generate notification

I understand that mentions are just Links, but they do generate notifications and this behavior is very common in Fediverse. My point is that any activity that can generate notifications can be used for spamming.

Currently the only effective anti-spam policy is blocklist. If the message can carry a credential (capability, token), the space of possible policies vastly increases, because credential can come from an external system. In other words, this enables interoperability with other identity and reputation systems.

I meant that some implementations like Pleroma can do blind key rotation, which would mean that any earlier signatures would no longer validate.

Generating notifications is entirely an app policy. Spam is generally a function of deliverability. The way to combat spam is to reduce deliverability. We can reduce deliverability by using capability-based security to prevent delivery in the first place, or we can use policies to drop activities after delivery but before receipt. examples:

  • we can choose to hand out our inbox URI only to trusted contacts. we might even have multiple inboxes, like a public inbox and a private inbox, and different inboxes can have different policies.
  • we can require some shared secret to be used as a bearer token. activities posted without a valid token might fail or be deprioritized or greylisted.
  • we might have a server-side filtering policy that checks incoming activities for certain properties, such as who the actor is, or what the content includes, or whether certain logical conditions are met.

anyway, this is a bit off-topic so maybe we could make a new thread for further discussion of anti-spam policies, if you wish?

2 Likes