24 дня индивеба: ActivityPub

От Тима: привет! Сегодня в календаре — гостевой пост про ActivityPub. Это важная часть истории про альтернативы корпоративным социальным сетям, которая развивалась параллельно стандартам Индивеба Microformats и Webmention.

Мне хочется, чтобы ActivityPub был упомянут в календаре, но мне не хватает знаний из первых рук про всю эту экосистему — поэтому я попросил Гришу написать пост про ActivityPub. ActivityPub-экосистема подходит к федеративным социальным сетям с несколько другой стороны, но не буду спойлерить.


Если кратко: ActivityPub — это самый популярный протокол федерации между серверами социальных сетей, микроблогов и прочих подобных вещей.

По своей концепции ActivityPub очень похож на SMTP. Есть независимые сервера, на каждом сервере есть свои пользователи, у каждого пользователя есть инбокс. В инбоксы кладут «активити» — действия других пользователей, которые так или иначе касаются получателя. Совершают эти действия «акторы» — пользователи, группы, боты, и так далее. Например, если Вася с сервера mastodon.xyz подписан на Петю с сервера example.social, и Петя создаёт новый пост, Петин сервер положит этот пост Васе в инбокс на его сервере, и он появится в Васиной ленте.

Вся совокупность серверов социальных сетей, которые федерируются друг с другом, называется fediverse — соединение слов federation и universe.

Все объекты представлены в (весьма наркоманском) формате JSON-LD и передаются по HTTPS, но пока что представьте себе, что это обычный JSON с ключом @context. Вот пример объекта (актора) пользователя, обратите внимание на ключ inbox:


{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://w3id.org/security/v1"
  ],
  "id": "https://example.social/users/petya",
  "type": "Person",
  "url": "https://example.social/@petya",
  "preferredUsername": "petya",
  "name": "Петя",
  "inbox": "https://example.social/users/petya/inbox"
  "outbox": "https://example.social/users/petya/outbox",
  "followers": "https://example.social/users/petya/followers",
  "following": "https://example.social/users/petya/following",
  "summary": "<p>Тот самый мазохист, который упомянут в спецификации LD-signatures<\/p>",
  "endpoints": {
    "sharedInbox": "https://example.social/sharedInbox"
  },
  "icon": {
    "type": "Image",
    "url": "https://example.social/uploads/avatars/79b12c2b44826342fa0fe07d9662010d.jpg"
  },
  "publicKey": {
    "owner": "https://example.social/users/petya",
    "publicKeyPem": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n",
    "id": "https://example.social/users/petya#main-key"
  },
}

Вот создание (активити типа Create) поста (объекта типа Note) пользователем (актором типа Person):


{
  "@context":[
    "https://www.w3.org/ns/activitystreams",
    {"sensitive":"as:sensitive"}
  ],
  "id":"https://example.social/posts/2851/activity",
  "type":"Create",
  "actor":"https://example.social/users/petya",
  "to":["https://www.w3.org/ns/activitystreams#Public"],
  "cc":["https://example.social/users/petya/followers"],
  "published":"2019-12-15T16:28:35Z",
  "object":{
    "id":"https://example.social/posts/2851",
    "type":"Note",
    "to":["https://www.w3.org/ns/activitystreams#Public"],
    "cc":["https://example.social/users/petya/followers"],
    "attributedTo":"https://example.social/users/petya",
    "published":"2019-12-15T16:28:35Z",
    "sensitive":false,
    "content":"<p>Я сегодня видел няшного котика!<\/p>",
    "url":"https://example.social/posts/2851"
  }
}

Получить ActivityPub-объект для любого пользователя или поста можно по адресу этого пользователя или поста, добавив в запрос заголовок Accept: application/ld+json, например:

$ curl -H "Accept: application/ld+json" https://mastodon.social/users/grishka

Результат:


{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://w3id.org/security/v1",
    {
      "manuallyApprovesFollowers":"as:manuallyApprovesFollowers",
      "toot":"http://joinmastodon.org/ns#",
      "featured":{
        "@id":"toot:featured",
        "@type":"@id"
      },
      "alsoKnownAs":{
        "@id":"as:alsoKnownAs",
        "@type":"@id"
      },
      "movedTo":{
        "@id":"as:movedTo",
        "@type":"@id"
      },
      "schema":"http://schema.org#",
      "PropertyValue":"schema:PropertyValue",
      "value":"schema:value",
      "IdentityProof":"toot:IdentityProof",
      "discoverable":"toot:discoverable",
      "focalPoint":{
        "@container":"@list",
        "@id":"toot:focalPoint"
      }
    }
  ],
  "id":"https://mastodon.social/users/grishka",
  "type":"Person",
  "following":"https://mastodon.social/users/grishka/following",
  "followers":"https://mastodon.social/users/grishka/followers",
  "inbox":"https://mastodon.social/users/grishka/inbox",
  "outbox":"https://mastodon.social/users/grishka/outbox",
  "featured":"https://mastodon.social/users/grishka/collections/featured",
  "preferredUsername":"grishka",
  "name":"Гришка",
  "summary":"\u003cp\u003eГришка, теперь децентрализованный!\u003c/p\u003e",
  "url":"https://mastodon.social/@grishka",
  "manuallyApprovesFollowers":false,
  "discoverable":true,
  "publicKey":{
    "id":"https://mastodon.social/users/grishka#main-key",
    "owner":"https://mastodon.social/users/grishka",
    "publicKeyPem":"-----BEGIN PUBLIC KEY-----\n[redacted for brevity]\n-----END PUBLIC KEY-----\n"
    },
  "tag":[],
  "attachment":[
    {
      "type":"PropertyValue",
      "name":"VK",
      "value":"\u003ca href=\"https://vk.com/grishka\" rel=\"me nofollow noopener noreferrer\" target=\"_blank\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"\"\u003evk.com/grishka\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e"
    },
    {
      "type":"PropertyValue",
      "name":"Сайт/блог",
      "value":"\u003ca href=\"https://grishka.me\" rel=\"me nofollow noopener noreferrer\" target=\"_blank\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"\"\u003egrishka.me\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e"
    }
  ],
  "endpoints":{
    "sharedInbox":"https://mastodon.social/inbox"
  },
  "icon":{
    "type":"Image",
    "mediaType":"image/jpeg",
    "url":"https://files.mastodon.social/accounts/avatars/000/909/172/original/c8f4511dede32ad7.jpg"
  }
}

Сам по себе ActivityPub слишком сложный и многогранный, чтобы быть полностью описанным в одной статье адекватной длины. Если появилось любопытство, хочется больше подробностей и потыкать в настоящие сервера, я бы советовал начать с этих двух статей в блоге Mastodon.

Существующие реализации

На данный момент есть уже достаточно много различного серверного софта, поддерживающего ActivityPub:

Всё это в изрядной степени работает друг с другом. То есть, я могу подписаться на пользователя Pixelfed со своего акканта в Mastodon, и смотреть на его фотографии котов в своей ленте. Как тебе такое, Илон Маск Марк Цукерберг?!

Мой опыт

Мне, как человеку, полжизни прожившему и 5 лет проработавшему ВКонтакте, очень хотелось аналогичной социальной сети, но со стеной без Mail.ru Group и без централизации со всеми её последствиями. Посмотрел я такой на это всё, и решил сделать свою, потому что Friendica слишком далека от того, что я хочу, и назвал её Smithereen. Да, этот Smithereen.

Пишу на джаве. Для федерации, само собой, использую ActivityPub. С мастодоном уже работает, но до чего-то презентабельного там далековато, так что ссылок пока не будет. Но могу описать сложности, с которыми я столкнулся:

  • JSON-LD — наркоманский формат. Это как XML со схемой и неймспейсами, только JSON. Для нормального участия в fediverse надо уметь его корректно парсить, переводя всё полученное в понятный тебе вид. Есть спецификация, в которой по шагам описаны алгоритмы преобразования, но лучше поберегите свою психику и не читайте её без необходимости.
  • Подписывание запросов. По-хорошему, каждый запрос должен быть подписан приватным ключом актора два раза: HTTP-подписью и LD-подписью. Первая генерируется и проверяется просто, но запросы, подписанные только HTTP-подписью, нельзя проксировать, т.к. подписываемые заголовки включают в себя Host, делая подпись зависимой от хоста назначения запроса. Вторая — это очень больно, ибо предполагает многочисленные неочевидные манипуляции с JSON-LD, но запросы с ней проксировать можно, нужно и полезно. Подробно о моих страданиях с LD-подписями можно почитать вот здесь.
  • Первопроходчество. Его много. Всё существующее так или иначе похоже на твиттер, концепции друзей ни у кого нет, сообществ тоже ни у кого нет, даже стен ни у кого нет. И это я пока не начал думать про фотоальбомы и прочие видеозаписи. Но сам по себе протокол достаточно открытый для интерпретации и расширения, приходится много всего придумывать.

Что почитать по теме

День 15: WebSub · День 17