danirod.es

Antes que nada, lo siento

Mi investigación sobre cómo se programa un feed de Bluesky

📁

Esto no es un sustituto para la «maravillosa» experiencia que resulta de leer la documentación oficial de ATProto y Bluesky, solamente quiero dejar anotado lo que he aprendido para poder revisarlo en el futuro


¿Cómo sabe Bluesky cuál es el algoritmo que proporciona la inteligencia de un feed?

Bueno, la respuesta parece ser que un feed de Bluesky no es más que un servicio HTTP con el que Bluesky interactúa usando la API RPC con los endpoints definidos en el protocolo ATProto.

Entonces, «crear» un feed es exponer a través de un servicio web esos endpoints y prepararlos para que Bluesky pueda lanzarle peticiones cuando una persona consulta el feed.


¿Y cómo sabe Bluesky cuál es el servicio web que debe usar?

Cuando «creas» un feed, es decir, cuando lo haces público para que salga en tu perfil y para que «exista» para Bluesky, hay que darle un parámetro que codifica el hostname. De modo que cuando alguien intenta consultar tu feed, Bluesky usa el hostname para saber a qué servidor hacer las peticiones HTTP que devuelven los datos del feed y así «usar tu algoritmo».

Ejemplo: el feed Linux está creado por @mackuba.eu. El DID de esta cuenta es did:plc:oio4hkxaop4ao4wz2pp3f4cr. Si uso el endpoint RPC com.atproto.repo.listRecords para pedirle todos los app.bsky.feed.generator creados por esta cuenta, el array JSON incluye el feed Linux.

Petición RPC al endpoint com.atproto.repo.listRecords en el PDS bsky.social
https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=did:plc:oio4hkxaop4ao4wz2pp3f4cr&collection=app.bsky.feed.generator

El campo did de este record tiene como valor did:web:blue.mackuba.eu. Eso significa que el feed está asociado con el hostname blue.mackuba.eu y que cuando quieras consultar el feed Linux, Bluesky le tiene que tirar las peticiones a https://blue.mackuba.eu.

La API de un feed

Leyendo la documentación del generador que he consultado, un servicio web que sirva feeds de Bluesky debe implementar tres métodos:

  • /.well-known/did.json: este tiene que validar que, efectivamente, ese servicio web es el correcto, para evitar impersonaciones.
  • /xrpc/app.bsky.feed.describeFeedGenerator: este es el que devuelve información de los feeds que se sirven desde ese servicio.
  • /xrpc/app.bsky.feed.getFeedSkeleton: este es el que devuelve el contenido del feed, para que los posts se puedan ver. Va paginado.

ericvolp12/go-bsky-feed-generator es un generador de feeds hecho por @jaz.bsky.social que está programado en Go. De aquí es de donde he sacado esta información. Todavía no lo he clonado para ver si es tan sencillo como tomar la plantilla, programar los algoritmos, y dejar que el proyecto levante el servidor web.

did.json

El endpoint de DID parece confirmar que estamos en la ubicación correcta. Imagino que validar el feed quiere decir verificar que no estás usando otro hostname diferente o que alguien se ha equivocado al poner la URL del servidor. No tengo ni idea del contexto de este endpoint.

Petición al endpoint did.json de blue.mackuba.eu.
https://blue.mackuba.eu/.well-known/did.json

app.bsky.feed.describeFeedGenerator

El endpoint app.bsky.feed.describeFeedGenerator devuelve la lista de feeds servidos desde ese servicio web. La documentación con los tipos del objeto devuelto están en la API de Bluesky.

Petición RPC al endpoint app.bsky.feed.describeFeedGenerator de blue.mackuba.eu.
https://blue.mackuba.eu/xrpc/app.bsky.feed.describeFeedGenerator

app.bsky.feed.getFeedSkeleton

El endpoint app.bsky.feed.getFeedSkeleton es el que devuelve los datos de un feed. Si le pido sin más que me hable, da un error HTTP 500, aunque supongo que esto depende del servidor.

Petición RPC al endpoint app.bsky.feed.getFeedSkeleton de blue.mackuba.eu.
https://blue.mackuba.eu/xrpc/app.bsky.feed.getFeedSkeleton

Eso es porque según la documentación de ese endpoint, le tengo que poner como queryparam feed para indicarle el record del feed que quiero que me devuelva. El feed tiene que estar en formato URL de protocolo ATProto, es decir, at://[did]/app.bsky.feed.generator/[slug], que para el feed «Linux» es at://did:plc:oio4hkxaop4ao4wz2pp3f4cr/app.bsky.feed.generator/linux, siendo el DID de la cuenta de mackuba. En cuanto hago eso, el feed empieza a hablar.

Petición RPC al endpoint app.bsky.feed.getFeedSkeleton de blue.mackuba.eu con un feed como parámetro.
https://blue.mackuba.eu/xrpc/app.bsky.feed.getFeedSkeleton?feed=at://did:plc:oio4hkxaop4ao4wz2pp3f4cr/app.bsky.feed.generator/linux

El formato de un feed tiene el siguiente tipo (voy a usar una interfaz de TypeScript por simplicidad):

interface Feed {
  cursor: string;
  feed: Array<{
    post: string;
  }>;
}

Uno de los campos de retorno es cursor, que es el que permite paginar el feed. Es un paginador un poco complejo porque como el feed se puede actualizar en tiempo real a medida que se va paginando si se agregan o se borran posts, se recomienda que lleve también un timestamp para asegurarse que se sincroniza bien.

El otro es feed, que devuelve un array de objetos con los IDs de cada post. El feed no porta el contenido de los posts, solamente sus IDs externos. Tú devuelves la URL en protocolo ATProto de un post, y ya se ocupa Bluesky de recuperar por separado el contenido de cada post a partir de la propia API del PDS.

Voy a hacer la prueba tratando de resolver desde el PDS https://bsky.social uno de los posts del feed, en este caso, at://did:plc:gxt7mot2ujgovitv6j4eo7n4/app.bsky.feed.post/3lbyn6inlds22. Puedo sacarlo mediante el endpoint RPC app.bsky.feed.getPosts. Como este endpoint requiere autenticación, voy a usar https://public.api.bsky.app, que permite acceso anónimo, para no tener que aprender ahora a crear tokens.

Petición RPC a app.bsky.feed.getPosts para este endpoint y este PDS.
https://public.api.bsky.app/xrpc/app.bsky.feed.getPosts?uris=at://did:plc:gxt7mot2ujgovitv6j4eo7n4/app.bsky.feed.post/3lbyn6inlds22

Estrategias para fabricar un algoritmo

Si quiero programar un feed estático, puedo devolver hardcodeado el array con las URLs ATProto de los posts que quiero que porte. Sin embargo, imagino que la gracia está en prestar atención a JetStream o al firehose de Bluesky directamente, que es el websocket que te trae en tiempo real los posts a medida que se publican. Para mirar a la cara a este websocket sin usar comandos de consola se pueden usar las siguientes herramientas web:

Cuando encuentre un post en el websocket que concuerde con el criterio de mi algoritmo, puedo anotar su ID para poder devolverlo más adelante cuando se pidan datos del feed. Se recomienda también descartar los posts más antiguos para no devolver un feed muy grande.

Siguientes puntos

La siguiente pregunta que me queda por hacer es cómo funciona la autenticación, porque algunos feeds son anónimos ya que solo tienen que devolver datos del firehose público. Sin embargo, si quiero filtrar para que muestre información relacionada con mi cuenta (por ejemplo, posts de la gente que sigo o posts de la gente que me sigue), supongo que el feed necesita una forma de saber quién soy. Esto es algo que dejo para investigar otro día.


(¿Cómo dar me gusta o repostear?)

Puedes dar repost o like a esta publicación desde el fediverso. Pon la URL del artículo en el buscador de tu instancia de fediverso (por ejemplo, Mastodon), y haz una búsqueda. Lo normal es que tu instancia haga un descubrimiento del post que hay en la URL y aparezca como primer resultado.