В 2007 году Тим Бернерс-Ли написал эссе The Giant Global Graph. Вот цитата из него:
Раздаются крики души... о моей дружбе, о той связи с другим человеком, которая должна выйти за рамки документов и сайтов... Тогда любой другой сайт или программа сможет использовать эту информацию.
Эти слова кажутся особенно уместными сейчас, когда X разослал письма с требованием прекратить деятельность проекту Nitter. Nitter был простым фронтендом для X, позволявшим просматривать твиты без авторизации. Даже такое минимальное проксирование публичных страниц оказалось достаточным поводом для угрозы судебным иском.
API Twitter образца 2007 года был по-настоящему открытым — благодаря этому тысячи разработчиков бесплатно создавали клиенты, инструменты и аналитические сервисы. Что случилось потом? Почему доступ закрыли? Всё просто: сеть выиграла. Разработчики перестали быть активом, и API постепенно закрывался — рейт-лимиты, платные тарифы, обязательная авторизация, затем технические блокировки обходных путей, и наконец письма от юристов. Meta прошла тот же сценарий десятилетием раньше, и сейчас уже трудно вспомнить, что API Facebook или Instagram вообще стоило того, чтобы на нём что-то строить.
Именно поэтому Брюстер Кейл, основатель Internet Archive, уже больше десяти лет призывает запереть веб в открытом состоянии.
Nitter начинал с использования официальных API X. Когда их закрыли, проект перешёл на чтение публичных веб-страниц. А теперь, когда закрывать больше нечего, требование — убрать сам исходный код. Программа, которая отображает публичные посты, трактуется как средство обхода защиты по законам о компьютерных преступлениях.
Это проблема огороженных садов. Она не изменится сама по себе, и единственный выход — начать заново.
Хорошая новость в том, что atproto продолжает расти, activitypub остаётся устойчивым, а сообщество полно сторонников и разработчиков открытого социального веба. Поскольку работа ведётся именно над atproto, дальше речь пойдёт о нём.
Взаимодействие через SELECT *
SELECT * FROM internet.blogposts
Проблема огороженных садов сводится к простому вопросу: как выполнить SELECT * FROM internet?
Для тех, кто никогда не писал код для баз данных: SELECT * FROM users — это способ запросить у базы данных всю информацию о пользователях. Получив её, можно фильтровать, сортировать и джойнить с чем угодно ещё.
Веб исторически устроен иначе. Веб — это несколько десятков компаний, каждая со своим картотечным шкафом и своим секретарём на входе. Секретарь выдаст по одному файлу за раз, только те файлы, названия которых вы знаете, с той скоростью, с какой ему удобно читать, и до тех пор, пока это разрешает его начальник.
Nitter был лёгким ридером для X, который прекрасно работал ровно до того момента, пока X не отключил доступ, от которого он зависел. Любой API — тот самый "секретарь" — это бизнес-решение, которое пока просто не отменили.
Но проблема не только в непостоянстве доступа. Даже постоянный, бесплатный, щедрый по рейт-лимитам API всё равно был бы недостаточен. Приложениям нужен гораздо более содержательный доступ, чем может дать API.
- Можно задавать только те вопросы, ответы на которые кто-то заранее предусмотрел. API — это фиксированное меню. Он даёт
getPosts(user)иgetFollowers(user). Если продукту нужны "посты людей, на которых подписаны мои подписчики, отсортированные по частоте цитирования" — такого эндпоинта нет и никогда не будет, потому что никто в этой компании не строит продукт под ваши задачи. - Даже правильные вопросы возвращаются в неудобной форме. Подписчики приходят порциями по 100. Аккаунт с двумя миллионами подписчиков — это 20 000 запросов. При разумном рейт-лимите ответ на один вопрос об одном пользователе занимает часы — так что всё интерактивное, всё, что должно ощущаться мгновенным, отпадает ещё до старта.
- Нельзя джойнить между "шкафами". Самые интересные вопросы почти всегда межсервисные: посты одного человека сопоставить с фотографиями другого и отзывами из третьего сервиса. Два секретаря в двух разных зданиях не могут свериться друг с другом — и вы тоже не можете.
- Нельзя индексировать данные, которых у вас нет. Поиск, ранжирование, рекомендации, ленты, инструменты модерации — всё это строится на индексах поверх всего корпуса данных, устроенных под конкретные вопросы вашего продукта. Индекс через замочную скважину не построить.
Чтобы реально построить сервис, нужен весь датасет целиком, а не срез через него; нужно получать его в реальном времени по мере изменений, а не опрашивать; нужно индексировать его так, как требует продукт; нужно уметь записывать обратно в него; и всё это должно быть гарантировано так, чтобы квартальные приоритеты одной-единственной компании не могли это отозвать.
Десктопные приложения решают эту задачу через общую файловую систему. Интернет-приложения файлами не пользуются — они используют базы данных. Значит, нужно делиться базой данных.
Как пользователь, не хочется быть запертым внутри приложения не больше, чем в багажнике машины. Хочется настоящего свободного рынка.
Отсюда вытекает ещё один набор требований.
- Постоянство идентичности.
- Присутствие в сети и связи строятся вокруг идентичности. Она должна пережить приложение, в котором была создана.
- Экспорт живых (а не мёртвых) данных между сервисами.
- Экспорт архива твитов бесполезен как способ миграции аккаунта, потому что данные не существуют изолированно.
- Если данные больше не операбельны — то есть с ними нельзя производить дальнейшие действия участникам сети, — это статичный архив, бесполезный для другого приложения.
- Такой архив разве что можно распечатать и просто смотреть на него.
Чтобы данные оставались операбельными за пределами исходного сервиса, базой данных нужно делиться.
Все эти вопросы — открытый доступ к данным, миграция аккаунтов, живой поток активности сети — как раз то, для чего спроектирован atproto.
Как atproto реализует SELECT * FROM internet
Как поделиться базой данных? Никак — вместо одной базы создаётся множество. Строится целая сеть персональных серверов данных (PDS), с которыми взаимодействуют приложения.
Как обрабатывать сложные запросы SELECT * от приложений к персональным серверам данных? Никак — данные реплицируются в логи. Каждое приложение агрегирует копии данных и запрашивает их локально.
Как приложения записывают в эти базы данных? А вот тут — да, записывают напрямую. Приложения отправляют записи на PDS, который в свою очередь реплицирует их обратно другим приложениям.
Последний пункт — это ядро всей идеи atproto: цикл записи и приёма (write/ingest loop). Почти в каждом приложении на atproto есть код примерно такого вида:
// write
pds.putRecord(post)
// ingest
onPut(‘app.bsky.feed.post’, evt => {
mydb.put(‘posts’, {...})
})
Вместо ожидания, пока событие приёма вернётся по сети, можно использовать "короткое замыкание", чтобы база данных приложения обновлялась быстрее. Ответ 200 OK от PDS служит транзакционным разрешением на продолжение.
Более эффективный паттерн выглядит так:
// write
pds.putRecord(post)
mydb.put(‘posts’, {...}) // ← optimistic
// ingest
onPut(‘app.bsky.feed.post’, evt => {
mydb.put(‘posts’, {...})
})
Работает ли это?
Да. Сеть существует. Она живая, публичная, и её всю можно читать прямо сейчас — с обычного ноутбука, не спрашивая ни у кого разрешения. Именно так сейчас работают Bluesky, Tangled, Leaflet и множество других сервисов.
Вот немного статистики на момент написания:
- 46,1 млн аккаунтов в atproto
- 24,5 млрд записей
- 3,15 млрд из них — посты
- 17,4 млрд из них — лайки
- 500–1000 событий записи в секунду
- Более 5000 персональных серверов данных
- И достаточно приложений, чтобы собрать из них полноценный магазин приложений
Подключиться к этим данным сейчас проще некуда — благодаря новому сервису Jetstream.
import { Jetstream, isCreate } from '@bsky/jetstream';
import { app } from '@bsky/sdk/lexicons';
const jetstream = new Jetstream('https://jetstream.us-east.bsky.network');
const collections = [app.bsky.graph.follow, app.bsky.feed.repost, app.bsky.feed.post];
for await (const event of jetstream.live({ collections })) {
if (isCreate(event, app.bsky.graph.follow)) {
console.log(`🌱 ${event.did} follows ${event.commit.record.subject}`);
} else if (isCreate(event, app.bsky.feed.repost)) {
console.log(`♻️ ${event.did} reposts ${event.commit.record.subject.uri}`);
} else if (isCreate(event, app.bsky.feed.post) && event.commit.record.reply) {
console.log(`💭 ${event.did} replies ${event.commit.record.reply.parent.uri}`);
}
}
Быстрее всего попробовать самому можно здесь.
Хватит получать cease-and-desist письма. Лучше SELECT * FROM internet.blogposts
И да, если нужны именно блог-посты на atproto, стоит обратить внимание на standard.site.