Representing things in a VKontakte/Facebook-style social network?

So far, all of the ActivityPub implementations I’ve seen are about microblogging – which means unilateral following, only own posts in own profile, and no concept of friends. I’m making one that’s different from that and modeled off early, pre-public-pages VKontakte (Facebook probably was like that too as some point). So, I have some questions.

  1. How do I best represent friend requests? An Offer activity, with a Relationship as the object, as the spec suggests? For compatibility purposes, I interpret a mutual following as a friendship, so it already works with existing microblogging software.
  2. How do I best represent the friend list of a user? Do I just introduce my own JSON-LD namespace and add a property “friends” there, that works the same way as “followers” and “following”? Or is it supposed to be an ordered collection of Relationships rather than (links to) Persons?
  3. My users have “walls” where anyone can post. Only own posts appear in feeds. How do I address a someone else’s post on someone’s wall? So far I use “blablabla#Public” as “to” and the wall owner’s ID as the only element of “cc”. This post is public but isn’t addressed to anyone’s followers, so seems logical I guess?
  4. Reposts aka the Announce activity – that’s kinda limited for what I ideally want it to be. VK allows reposting with an optional comment and a media attachment. How could this be represented in ActivityPub, preferably such that it works nicely with the existing implementations? Since Objects have all the needed fields, namely “content” and “attachments”, is it going to be okay if I pretend my Announce is a post with its own content and attachments? But then how do I refer to it in comment objects (replies)?
1 Like

[2019-10-28 16:16:16+0000] Gregory via SocialHub:

So far, all of the ActivityPub implementations I’ve seen are about microblogging – which means unilateral following, only own posts in own profile, and no concept of friends. I’m making one that’s different from that and modeled off early, pre-public-pages VKontakte (Facebook probably was like that too as some point). So, I have some questions.

Well, this is quite because most of the Fediverse are Twitter/GnuSocial clones and Friends are a bit weird in a networking point of view tbh, but Friendica (2008-present IIRC) -which I’m going to assume you’re not aware of- seems pretty much facebook-like and started to federate over ActivityPub earlier this year.

  1. How do I best represent friend requests? An Offer activity, with a Relationship as the object, as the spec suggests? For compatibility purposes, I interpret a mutual following as a friendship, so it already works with existing microblogging software.

I think mutuals would be best, that’s also what Friendica does.

  1. How do I best represent the friend list of a user? Do I just introduce my own JSON-LD namespace and add a property “friends” there, that works the same way as “followers” and “following”? Or is it supposed to be an ordered collection of Relationships rather than (links to) Persons?

I think your first one is the way to go, and IMHO I would call it “mutuals” instead of “friends” as it’s more transparent on what it actually represents.

  1. My users have “walls” where anyone can post. Only own posts appear in feeds. How do I address a someone else’s post on someone’s wall? So far I use “blablabla#Public” as “to” and the wall owner’s ID as the only element of “cc”. This post is public but isn’t addressed to anyone’s followers, so seems logical I guess?

ActivityPub actually doesn’t says that your feed can only contain your own posts, relays are one example of ActivityPub actors that have no content. And I’m not sure I understood the other part but it seems okay (we often use as:Public as an abbreviation btw).

  1. Reposts aka the Announce activity – that’s kinda limited for what I ideally want it to be. VK allows reposting with an optional comment and a media attachment. How could this be represented in ActivityPub, preferably such that it works nicely with the existing implementations? Since Objects have all the needed fields, namely “content” and “attachments”, is it going to be okay if I pretend my Announce is a post with its own content and attachments? But then how do I refer to it in comment objects (replies)?

Not sure if this is supported in any implementations (Mastodon and Pleroma via MastodonAPI doesn’t), but this is also something that friendica allows so I guess they might have figured it out already.

Thanks! I’ve heard of Friendica, but I assumed it doesn’t support ActivityPub. Upon closer inspection it somewhat does, but it seems as though it mainly supports it for the purpose of compatibility with microblogging software like Mastodon and Pleroma, not exposing all of its features. And it’s not even really spec-compliant either – I followed my account by sending it a Follow activity, it appeared as a “mutual following” in Friendica but I didn’t receive an Accept. It also doesn’t allow me to post on that foreign user’s wall.

So I guess at this point I can make up whatever works best for my features and is as compatible with Mastodon/Pleroma/Misskey/etc as possible ¯\_(ツ)_/¯

My thoughts/opinions:

For interoperability, Person A sending a Follow to Person B, and having Person B send back an Accept{Follow} or Reject{Follow}. These two steps would be cognizant choices by the user. Then on their behalf, have your B server do a reverse-Follow upon sending an Accept{Follow}, and when A’s server gets the Accept{Follow} and then B’s Follow, auto-Accept it. Only then they’ll be mutually friends.

If your application strictly follows the paradigm above, only users in both followers and following will be “friends” (mutual).

You may have to (ab)use the Announce in a way that isn’t currently being used:

{
  "type": "Announce",
  "object": "https://example.com/other/shared/post",
  "content": "My commentary"
}

Note that this will only allow text commentary, not other media (the Range of the content property is xsd:string or rdf:langString.

to be more precise, the content-type of content, by default, is HTML, so there’s no problem with using rich media there

For interoperability, Person A sending a Follow to Person B, and having Person B send back an Accept{Follow} or Reject{Follow} . These two steps would be cognizant choices by the user. Then on their behalf, have your B server do a reverse- Follow upon sending an Accept{Follow} , and when A’s server gets the Accept{Follow} and then B’s Follow , auto- Accept it. Only then they’ll be mutually friends.

I think I wasn’t clear enough about how I want this to work, sorry. Person A sends Person B a friend request, which has a side effect of A following B. If B declines the friend request, this only means that they don’t want to follow them back (and this will have an effect on the visibility of friends-only content if I decide to implement that), A still follows B. The only way for B to prevent A from following them is to block them. So following is still non-consensual like in microblogs, and friend requests are just an addition to that. VK worked like that for ages so I’m quite used to this. Facebook is probably different.

This is best illustrated with this screenshot:
56

You may have to (ab)use the Announce in a way that isn’t currently being used:

But since I then still end up with an activity rather than a Note, how do I have replies to that? I’d like each repost to have a completely separate comment (reply) thread, which requires that each repost be an object like Note rather than an activity.

I’d post a screenshot of that as well, but apparently I’m a “new user” and am not allowed to have more than one image per post.

grishka

    October 29

For interoperability, Person A sending a Follow to Person B, and having Person B send back an Accept{Follow} or Reject{Follow} . These two steps would be cognizant choices by the user. Then on their behalf, have your B server do a reverse- Follow upon sending an Accept{Follow} , and when A’s server gets the Accept{Follow} and then B’s Follow , auto- Accept it. Only then they’ll be mutually friends.

I think I wasn’t clear enough about how I want this to work, sorry. Person A sends Person B a friend request, which has a side effect of A following B.

No, you misunderstand. A only follows B upon receiving an Accept{Follow}. Never when sending a Follow. (Edit to add: this is a SHOULD requirement in ActivityPub but not following this rule will violate federation expectations, result in weird interoperability issues in delivering content, and, I believe, result in a lot of very unhappy users demanding to boycott your software)

The scheme I outlined above ensures 2 follows and two Accept{Follows}, one for each person, are sent, without requiring 2 actions from each user, only 2 actions total, and they either end in a state where both follow each other, or neither one follows the other. Plus it interoperates correctly out of the box.

Your app will need to handle the case where other not-your-software federation results in a single sided follow. It will need to display it in a UI somehow, but luckily in either case I think it ends in the simple state of notifying your user that it is waiting for the other party to accept your follow.

In either case, you’ll always be able to tell who is mutual because their name will be present in both the followers and following properties.

Yeah, I understood, and that’s exactly what I don’t want. I’ll have to handle Mastodon’s manuallyApprovesFollowers as a special case anyway, and my software isn’t going to itself have that option – you’ll always be able to follow anyone.

1 Like

How do I best represent friend requests?

The majority of the existing network is based on Follow, which is primary behavior in a subscription-based network like Twitter or Mastodon.

However, you can create Relationship objects, if you wish: https://www.w3.org/TR/activitystreams-vocabulary/#connections – but you will probably be one of the first/only software implementations to handle it this way, at least at the moment. No software currently uses Relationship (mutual friendship) or Offer Relationship (friend request) in the way described. Also, another limitation of Relationship is that you must specify relationship using an external vocabulary, such as http://vocab.org/relationship/ or some other schema.

Some things you will have to consider:

  • How would you model a friendship where one person has “unfollowed” the other, as Facebook lets you do? Do you just handle that with your own internal muting system?
  • Are you interested in modeling cases where two people “follow” each other for their public posts, but are not necessarily “friends” when you attempt to consider access control or audience targeting?
  • To what extent do you want compatibility with the pubsub network that is the current fediverse? Are you going to be implementing more complex features that require managing collections of contacts, or can you get by collapsing everything to “follower” and “following”?

How do I best represent the friend list of a user?

Well, in extension of the previous question: you can either rely on the existing follower/following, or you can create a custom Collection. You probably don’t have to introduce your own JSON-LD namespace, as several existing vocabularies can cover that for you. As stated above, http://vocab.org/relationship/ is used in examples. (It would be better to not make unnecessary vocabularies where existing ones are good enough.) But you would probably have to define a property within your namespace for disambiguation of what exactly your Collection represents.

Consider the example of toot:featured from Mastodon, which points to a Collection of “featured” Objects, and is used to represent pinned statuses. You could certainly introduce a friends property within your namespace, if you expect remote sites to make use of it instead of followers/following. It would be up to you to define whether your property refers to a Collection of Relationship or a Collection of Person.

How do I address someone else’s post on someone’s wall?

This depends on how you model a “wall”. In ActivityPub, there is only inbox and outbox to be used for direct communications, like email. This is going to be pretty much up to what you think makes the most sense. Suppose you wanted to model a Group, that has members and a wall that only members can post to, but anyone can follow. You could say that the Group will check any activities received in its inbox and republish anything addressed to the Group (if it came from a member) to its outbox. That would be the simplest currently – you would just have to add support for Join/Leave Group to do your access control.

In terms of Person, there is technically a Profile that describes another Object – you can choose to implement this, but you will once again be one of the first/only implementations using these semantics, as the current fediverse has semantically collapsed “profiles” to mean simply “the outbox of an Actor”. You can also develop some heuristics based on the content of to and cc, but this is already pretty confusing even in the existing fediverse, so I would advise against going down that path, but it is technically possible to address your post to someone’s followers instead of to that person.

How to represent VK reposting with an optional comment/attachment?

You can just extend the existing Announce semantics by adding a content or attachment field, yeah, but existing implementations will ignore those properties and interpret it as a direct boost of the Object that is the target of the Announce, which may be unintended by you.

One alternative that might make more semantic sense is to instead send a Note with Note attached, as this will be interpreted as a Note in existing implementations and thus fallback to showing your added content instead of the referenced post.

I think you might also want to consider, instead of using attachment, you might want to tagthat post instead, as tag is possibly more semantically accurate. tag is for performing actions on content, e.g. “find name given in content and wrap it in <a> with the href given”. Or for toot:Emoji as custom emoji, “find name in content and embed the given icon in-place.” You could generalize this to say that if an Object is found in tag array, then it should be embedded in the post somehow.

If A and B are friends and A unfollows/unfriends B, they both disappear from each other’s friend lists and B becomes a follower of A. So “friend” is effectively just a term for “mutual follow”. My current implementation already does that – you will be friends with a Mastodon user if you follow each other (Mastodon would have no idea of course).

No. I might add friends-only posts, that’s it.

I can! As I said, I already can be friends with Mastodon users by mutually following. I added supportsFriendRequests: true to the metadata section in my nodeinfo to provide a semblance of capability negotiation but I don’t use it just yet as I haven’t yet started trying to federate friend requests.

That’s how I do it now:

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    {
      "sensitive": "as:sensitive"
    }
  ],
  "id": "https://localhost:4567/grishka/activitypub/outbox?max_id=0",
  "type": "OrderedCollectionPage",
  "next": "https://localhost:4567/grishka/activitypub/outbox?max_id=1",
  "partOf": "https://localhost:4567/grishka/activitypub/outbox", /* <- wall owner is @grishka */
  "totalItems": 12,
  "prev": "https://localhost:4567/grishka/activitypub/outbox?min_id=15",
  "orderedItems": [
    {
      "cc": [
        "https://localhost:4567/grishka"
      ],
      "actor": "https://localhost:4567/first.real.user",
      "id": "https://localhost:4567/grishka/posts/15/activityCreate",
      "published": "2019-10-29T22:06:19Z",
      "to": [
        "https://www.w3.org/ns/activitystreams#Public"
      ],
      "type": "Create",
      "object": {
        "cc": [
          "https://localhost:4567/grishka" /* <- not addressed to anyone's followers */
        ],
        "id": "https://localhost:4567/grishka/posts/15",
        "attributedTo": "https://localhost:4567/first.real.user", /* <- attributed to another user */
        "published": "2019-10-29T22:06:19Z",
        "to": [
          "https://www.w3.org/ns/activitystreams#Public" /* <- but still publicly accessible */
        ],
        "sensitive": false,
        "type": "Note",
        "content": "This is someone else&#39;s post.",
        "url": "https://localhost:4567/grishka/posts/15"
      }
    },
    {
      "cc": [
        "https://localhost:4567/grishka/activitypub/followers"
      ],
      "actor": "https://localhost:4567/grishka",
      "id": "https://localhost:4567/grishka/posts/14/activityCreate",
      "published": "2019-10-29T22:05:46Z",
      "to": [
        "https://www.w3.org/ns/activitystreams#Public"
      ],
      "type": "Create",
      "object": {
        "cc": [
          "https://localhost:4567/grishka/activitypub/followers"
        ],
        "id": "https://localhost:4567/grishka/posts/14",
        "attributedTo": "https://localhost:4567/grishka",
        "published": "2019-10-29T22:05:46Z",
        "to": [
          "https://www.w3.org/ns/activitystreams#Public"
        ],
        "sensitive": false,
        "type": "Note",
        "content": "This is my own post on my own wall.",
        "url": "https://localhost:4567/grishka/posts/14"
      }
    },
    /* ...more posts... */
  ]
}

No idea whether anything existing likes this because I wasn’t able to find anything that would allow me to see my outbox. Mastodon, for example, doesn’t use the outbox when you view a federated profile through an app but rather just shows everything the instance already knows about that user – so, only posts I sent to that instance and only followers from that instance.

I’m now more inclined towards doing a Twitter-esque thing where you can either “retweet” or “retweet with quote”. The second one is basically what I want, and Twitter itself represents it as a tweet with an invisible link to another tweet. The only downside is that Mastodon doesn’t display those nicely – it shows the link as-is. But other than that, this seems perfect: it is still a repost, it has a proper ID so it could have its own comment thread, there could be as much text and media as in a regular post, it works with existing software, and I can even add a hidden mention (or will addressing it be enough?) so the original poster receives a notification.

Note with a Note attached is a great idea too, but something tells me it won’t play nicely with anything existing.

Sorry to return to the same post I replied to earlier. I hadn’t had a chance to fully sit and understand the consequences of what was said.

In the ActivityStreams world, a Reject{Follow} is a pretty clear indicator that B doesnt want A to follow them. So don’t do it. Existing users on the Fediverse expect this.

It’s even a MUST in the spec:

If the object of a Reject received to an inbox is a Follow activity previously sent by the receiver, this means the recipient did not approve the Follow request. The server MUST NOT add the actor to the receiver’s Following Collection.

This is damaging to the ecosystem as a whole, violates expectations of users and developers. You’re forcing people to opt-out of your application by applying denylists, which could then become allowlist-only, which becomes a barrier to the open federative nature of the ecosystem.

Please, don’t do this approach.

It’s always been consensual on the Fediverse. By convention and history, there’s always either been:

  • Accept{Follow} always automatically sent as a reply.
  • Let the user decide whether to Accept{Follow} or Reject{Follow}. You don’t need manuallyApprovesFollowers, that’s just to let other instances know “oh I need to display a ‘manually approves followers’ tag on this federated profile”.

The current visibility systems are already a mess to users between original content and Announcing, but users are savvy and look out for each other, and have (and will) complain if it is changed to the detriment of the ecosystem. I’m not sure that is a fire you want to be fighting, and I want to strongly discourage you from ignoring Accept{Follow} and Reject{Follow}. But, you know your needs best, and if indeed you think it is in your best interest to continue down this path, I will go ahead and warn you that you will need to have really good answers lined up for users and devs of other Fediverse software who really want this convention/norm protected.

I can go ahead and tell you now, having the reason “I want it to be like <big social media company’s product>” is not going to go over well.

Edit: If you think I’m still misunderstanding you, it is because I am trying to interpret words like “decline” and “non-consensual” in an ActivityPub context, and it’s unclear to me whether you intend it that way or are instead using some specific language specific to your application / UI, in which case I really don’t understand what these terms mean in terms of actions/data in the ActivityPub/ActivityStreams space.

I wasn’t going to ignore them. I already send Accept{Follow} automatically and I expect it from the other server when I send Follow. The only thing I was saying is that my own implementation will always automatically send Accept{Follow} in response to a Follow without the option of manually approving followers because manually approving followers makes little sense for what I’m making.

Friend requests are an optional addition to following. The “I now follow you and ask you to follow me back” sort of thing that doesn’t break any of the already existing concepts.

Sorry, English isn’t my native language.

These are not the same action. I am asking if you have thought about how you will handle cases where two people are “friends” but one does not receive the activities of the other. But also, you will need to make sure that reciprocity/bidirectionality is implied, if you are interested in that – since manuallyRequiresApproval is not enough to guarantee that side effects will be processed on other servers (you can’t request a follow-back), and since the “mutual follower” case might change at any time without implying a break in both directions.


In response to other things, though:

  • If you add “friends-only posts, that’s it”, you will still have to consider whether you want that to be at-delivery or retroactive.
  • I’m not sure what supportsFriendsRequests is doing for you, exactly – if you’re depending on mutual-follows then “friends” is dynamically calculated.
  • It seems your “wall” is just your outbox – how do you differentiate between user’s own posts and other users’ posts on that “wall”? Do you interpret an Announce of another person’s post to be a post made to the wall? Are you publishing activities attributed to other actors within your own outbox?

This is a matter of display, not semantics. You can display information however you want to, and you can build your UI features however you want to, but on the backend you want to make sure your semantics are solid.

Nothing can be done about that. Mastodon does not (and will not) support “boost with quote” styled posts, by design. You also cannot assume that the “quote” will generate a notification, so this seems like a UX minefield as well. If it semantically collapses into pasting a link, then perhaps that’s all it needs to be? The inline preview aspect of it is just an extension of how the post is displayed. This is why I suggested tag.

These are not the same action. I am asking if you have thought about how you will handle cases where two people are “friends” but one does not receive the activities of the other. But also, you will need to make sure that reciprocity/bidirectionality is implied

this also feels like overcomplication to me?

I haven’t been able to track all of the back and forth on this thread, but stepping back to the original question: your friends are the list of people you are following who are also following you. no more, no less.

I don’t see any reason to maintain a collection of them (unless it makes audience targeting easier for you, but I don’t think it will), and I certainly don’t see the need for any sort of Offer or Relationship shenanigans

It’s how I’m going to be determining whether the server on the other end knows about, and is interested in, friend requests. If it does, I’ll send an Offer{Relationship} right after receiving Accept{Follow}. If it does not, I’ll just send Follow and stop right there, which is exactly what existing implementations do. My UI will also reflect that.

I use attributedTo in Note objects, as well as actor in activities, which must match.

You can’t repost something to someone else’s wall, only to your own.

Yes. Is this not a good thing to do? I can’t think of other ways of doing that in a semantically correct way.

The link is just so that Mastodon displays it in a way that makes sense rather than simply a text toot that doesn’t have the context that it’s supposed to have. Of course I’m still able to parse it and display it the way I want. tag will work for me.

One more important case where the friend list, and it being a dedicated collection exposed via ActivityPub, is useful: displaying common friends. It’s very useful in this style of social network.

i feel like if we’re going to have a dedicated friends property then it can’t be as simple as just calculating mutual Follow status. that would save you one computational step, sure, but it’s got exactly the same issues as checking follower/following on both ends. assuming you have actor A and actor B, fundamentally you need the following:

  • for following to be published on A while containing B, and followers published on B while containing A
  • for following to be published on B while containing A, and followers published on A while containing B

the issues you might run into is that an activitypub server might not make the contents of follower or following public. as that would essentially be the same as leaking your address book. consider mastodon’s “hide your network” feature, or pleroma’s “don’t show following/follower (count)” settings. even facebook has access control functionality on the friends list, e.g. only making the friends list visible to friends, or to friends of friends, etc… in such a case, the server knows this info but requires authorization before returning that info. thus, the “friend” relationship should be explicitly defined and not implictly derived.

Sorry for bumping an old topic, but I’m now at the stage where I’m going to be federating with myself.

I do need federated friend lists because many things in this style of social network revolve around them:

  • Mutual friends are a big one. I want to be able to display mutual friends for any profile you could open. It also makes sense to do that for friend requests. In both these cases the list of mutual friends helps understand the social context. To do this, I need to have a complete friend (mutual following) list of the other user and then intersect them. I could derive that by intersecting following and followers, and I’ll have to do that for Mastodon and such.
  • In groups and events, I’d like to show which friends are there. Doesn’t require any federation trickery as pertains to friends, but, btw, how do I represent the group member list?
  • Same for likes. People tend to be curious and want to know who of their friends liked something.

I’m going to send friend requests (semantically “I ask you to follow me back requests”) as Offer{Relationship} where relationship type is http://purl.org/vocab/relationship/friendOf. I’ll only send that to compatible servers and the UI will change based on this compatibility. A server is compatible if it contains friendRequests in metadata.capabilities in its nodeinfo. I can’t think of a better way of capability negotiation and there’s clearly some needed.

Flow between compatible servers:

  1. A sends B two activities: Follow and Offer{Relationship}
  2. B sends A Accept{Follow} immediately because I don’t implement follower approval
  3. The user on B sees the friend request and accepts it
    3.1. If the user rejects the request, B sends Reject{Offer{Relationship}} and the process ends here, with an unilateral follow
  4. B sends A Accept{Offer{Relationship}} and Follow
  5. A sends B Accept{Follow}
  6. Users are now friends

If the other server doesn’t support friend requests, the flow is exactly like it is between the existing microblogging servers. Follow, then Accept{Follow}, and that’s it.

I’m going to also add a friends collection to my actors, where each object would be like this:

{
    "type": "Relationship",
    "relationship": "http://purl.org/vocab/relationship/friendOf",
    "object": "https://friends.grishka.me/users/1",
    "subject": "https://friends.grishka.me/users/2"
}

Great to hear you’re making progress! Some thoughts:

If you’re going to have to do this anyway, why not just do it for all servers? what benefit do you gain from federating the extra data?

I think the replies property makes the most sense here—have all of the responses (Accept/Reject/Tentative) marked as inReplyTo the base Event, and then show them in the replies collection.

ActivityPub defines a likes collection as an extension on top of ActivityStreams, using that seems like it would make the most sense. https://www.w3.org/TR/activitypub/#likes

Why not just have an (extension) supportsMutualFollowRequests or supportsFriendRequests property on the actor object? ActivityPub is never going to define a “server-based” capability negotiation mechanism like the one you mention because “servers” are not a concept in the ActivityPub spec, only actors (and the endpoints associated with actors).

Why not just Offer { Follow } the inverse following relation? That seems to be the most straightforward way to represent “I’m asking you to follow me back”. Something like:

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "summary": "nightpool sent you a friend request",
  "type": "Offer",
  "actor": "https:​//cybre.space/users/nightpool",
  "target": "https://friends.grishka.me/users/1",
  "object": {
    "type": "Follow",
    "actor": "https://friends.grishka.me/users/1",
    "object": "https:​//cybre.space/users/nightpool"
  }
}

It’s an optimization. Popular users tend to have many followers, on the order of thousands, but not so many friends, on the order of hundreds. Fetching the follower and following lists only to throw away most of the data is a waste of resources. On the other hand, since I already store a flag whether a relationship is mutual, returning friend lists is trivial.

But what if a user joined a group themselves without an invitation, by sending a Join activity?

Again, it’s an optimization. It’d make sense to put it into the actor object if it was different across actors. But it’s not a property of an actor, rather it’s a property of a particular server software. It doesn’t vary across actors on the same server.

This makes sense and is a better fit for my concept of a friendship. I do have to pick one to send, but I should probably support receiving both in the future since the spec actually suggests using Offer{Relationship} for friend requests.

2 Likes