Glossaire

Comment fonctionne un générateur de feeds Bluesky, du lexicon au feed en direct

Taras Shynkarenko
Taras Shynkarenko
Mis à jour : 10 min de lecture
Comment fonctionne un générateur de feeds Bluesky, du lexicon au feed en directComment fonctionne un générateur de feeds Bluesky, du lexicon au feed en direct

TL;DR, Réponse Rapide

10 min de lecture

Un générateur de feeds Bluesky est un service web qui implémente une seule requête de l'AT Protocol, app.bsky.feed.getFeedSkeleton, et renvoie une liste d'AT-URIs de publications sans aucun contenu dedans. L'AppView de Bluesky hydrate ces URIs en vraies publications avant même que le client les voie. Le feed lui-même est déclaré par un enregistrement app.bsky.feed.generator placé dans le repo de son auteur, qui n'exige que did, displayName et createdAt, et qui pointe vers le DID du service. Le service prouve qu'il possède ce DID avec un .well-known/did.json portant une entrée de service de type BskyFeedGenerator. Les requêtes arrivent avec un JWT signé par la clé de signature du repo de la personne qui interroge, et limit est plafonné à 100.

Qu'est-ce qu'un générateur de feeds Bluesky ?

Chaque feed personnalisé de l'app Bluesky est servi par un générateur de feeds Bluesky, un simple service HTTPS qui répond à une seule requête et ne renvoie rien d'autre qu'une liste d'adresses de publications. Il ne détient aucun texte de publication, aucun avatar et aucun compteur de likes. Il classe, et c'est tout.

Le kit de démarrage officiel le dit sans détour : « the server receives a request from a user's server and returns a list of post URIs with some optional metadata attached. Those posts are then hydrated into full views by the requesting server and sent back to the client. »

Ce partage constitue toute la conception. Votre service décide quelles publications et dans quel ordre. L'infrastructure de Bluesky transforme les adresses en publications affichables.

Quel lexicon un générateur de feeds implémente-t-il ?

Un seul : app.bsky.feed.getFeedSkeleton. Le lexicon le décrit comme « Get a skeleton of a feed provided by a feed generator. Auth is optional, depending on provider requirements, and provides the DID of the requester. Implemented by Feed Generator Service. »

Quatre lexicons entourent ce flux, et savoir quel côté implémente lequel vous épargne une journée de débogage :

LexiconTypeImplémenté parRôle
app.bsky.feed.generatorrecordle repo de l'auteur du feedDéclare que le feed existe et nomme le DID du service
app.bsky.feed.getFeedSkeletonqueryvotre générateur de feedsRenvoie la liste classée des URIs de publications
app.bsky.feed.describeFeedGeneratorqueryvotre générateur de feedsÉnumère les URIs de feed que ce service dessert
app.bsky.feed.getFeedqueryl'AppView de BlueskyRenvoie le feed hydraté que le client affiche réellement

describeFeedGenerator n'est explicitement pas une méthode de l'AppView : « Does not require auth; implemented by Feed Generator services (not App View). » L'appeler sur l'AppView renvoie {"error":"MethodNotImplemented","message":"Method Not Implemented"}, alors que le même appel sur le service Discover de Bluesky à discover.bsky.app renvoie toutes les URIs de feed que ce service héberge.

Que met-on dans l'enregistrement app.bsky.feed.generator ?

C'est l'enregistrement qui fait apparaître le feed dans l'app, et il vit dans votre propre repo plutôt que sur votre serveur. Le lexicon le présente comme un « Record declaring of the existence of a feed generator, and containing metadata about it. The record can exist in any repository. »

ChampTypeObligatoireContrainte
didstring, format: didouiLe DID du service qui dessert le feed
displayNamestringouimaxGraphemes: 24, maxLength: 240
createdAtstring, format: datetimeoui
descriptionstringnonmaxGraphemes: 300, maxLength: 3000
descriptionFacetsarraynonentrées app.bsky.richtext.facet
avatarblobnonaccept: image/png, image/jpeg, maxSize: 1000000
acceptsInteractionsbooleannonInscrit le feed à app.bsky.feed.sendInteractions
contentModestringnoncontentModeUnspecified ou contentModeVideo

Trois champs sont obligatoires et aucun n'est l'algorithme. Notez le plafond de 24 graphèmes sur displayName, assez court pour que la plupart des noms de feed soient tronqués avant que vous le remarquiez, et le plafond de 1 000 000 d'octets sur l'avatar. La distinction entre graphème et octet derrière ces nombres est celle-là même qui fait trébucher sur les offsets d'octets des facets Bluesky.

contentMode est le seul champ qui change le rendu. app.bsky.feed.defs#contentModeVideo est un token qui « Declares the feed generator returns posts containing app.bsky.embed.video embeds, » et bascule le client vers le lecteur vidéo plein écran.

Le rkey de l'enregistrement devient l'identifiant public du feed. Le feed Discover de Bluesky lui-même s'adresse comme at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot, où le DID appartient au compte bsky.app et whats-hot est la clé de l'enregistrement.

Comment Bluesky trouve-t-il mon service ?

Par un document DID portant une entrée de service de type BskyFeedGenerator. L'enregistrement donne un DID à Bluesky. Résoudre ce DID donne une URL à Bluesky.

Le kit de démarrage met cela en place sous forme de did:web, si bien que votre propre domaine sert le document à /.well-known/did.json. Le service Discover de Bluesky en production fait pareil, et vous pouvez le récupérer :

{
  "id": "did:web:discover.bsky.app",
  "service": [
    {
      "id": "#bsky_fg",
      "type": "BskyFeedGenerator",
      "serviceEndpoint": "https://discover.bsky.app"
    }
  ]
}

L'id #bsky_fg et le type BskyFeedGenerator sont deux chaînes figées. Trompez-vous sur l'une ou l'autre et le feed ne résout vers rien.

Deux contraintes d'hébergement viennent du README et non d'une spécification. « Your feed will need to be accessible at the value supplied to the FEEDGEN_HOSTNAME environment variable, » et « The service must be set up to respond to HTTPS queries over port 443. » Le HTTP simple n'est pas une option, un port non standard non plus.

Le kit de démarrage signale aussi un piège de migration à peser avant de choisir un domaine : vous voudrez peut-être un did:plc plutôt qu'un did:web « if you expect this Feed Generator to be long-standing and possibly migrating domains. » Un did:web, c'est votre nom d'hôte. Changez d'hébergeur et tout enregistrement publié qui pointe dessus casse. Un did:plc survit au déménagement.

AdaptlyPost
AdaptlyPost

Essai gratuit de 7 jours

Analyses multiplateforme

Boîte sociale

Assistant IA

Une baie de serveurs, illustrant le service HTTPS basique qui répond à une requête getFeedSkeleton par une liste d'URI de publications.

Que renvoie getFeedSkeleton ?

Un curseur et un tableau d'entrées app.bsky.feed.defs#skeletonFeedPost, chacune étant une AT-URI plus un contexte facultatif. Voici une vraie réponse du service Discover de Bluesky, réduite à deux éléments :

{
  "cursor": "eyJvIjoiMjAyNi0wOS0xMVQxMDo0OTowNi42NTI4NDk4NzVaIiwibiI6IjIwMjYtMDktMTFUMjI6NDk6MDYuNjUyODQ5ODc1WiIsInNzIjoiMjAyNi0wOS0xMVQyMjo0OTowNi42NTI4NDk4NzVaIiwibHUiOiIyMDI2LTA5LTExVDIyOjQ5OjA2LjY1NjYyMzMwM1oiLCJwZyI6MSwibSI6IkFRQUFBQUFBQUFBSkFBQUFPakFBQUFJQUFBRHNWZ0FBa1ZjQUFCZ0FBQUFhQUFBQUtMTTNrUT09In0=",
  "feed": [
    {
      "feedContext": "th-nature_animals_pets",
      "post": "at://did:plc:mpvivcve3y2fdlw7pmq7niwz/app.bsky.feed.post/3mvb4r2qh4k2r"
    },
    {
      "feedContext": "th-science_research",
      "post": "at://did:plc:fb57h2fswm7s2qqzxflizlk7/app.bsky.feed.post/3mvasir35ec2e"
    }
  ]
}

Seul post est obligatoire dans chaque entrée. feedContext est facultatif et plafonné à maxLength: 2000, décrit dans le lexicon comme « Context that will be passed through to client and may be passed to feed generator back alongside interactions. » Bluesky s'en sert pour des étiquettes de sujet, comme le montre la chaîne th-nature_animals_pets ci-dessus. La réponse peut aussi porter reqId, un « Unique identifier per request, » plafonné à 100 caractères.

Le paramètre limit a un plancher et un plafond documentés : minimum: 1, maximum: 100, default: 50, et l'AppView les applique. Demander 101 renvoie :

{ "error": "InvalidRequest", "message": "Invalid app.bsky.feed.getFeed params: integer too big (maximum 100, got 101)" }

La pagination repose sur un curseur opaque que votre service invente. Le kit de démarrage est précis sur la forme attendue : « We strongly encourage that the cursor be unique per feed item to prevent unexpected behavior in pagination. We recommend, for instance, a compound cursor with a timestamp + a CID. »

Comment la requête est-elle authentifiée ?

Par un JWT signé avec la clé de signature du repo de la personne qui interroge, et le kit de démarrage détaille l'en-tête comme la charge utile :

const header = {
  type: 'JWT',
  alg: 'ES256K', // (key algorithm) - in this case secp256k1
};
const payload = {
  iss: 'did:example:alice', // (issuer) the requesting user's DID
  aud: 'did:example:feedGenerator', // (audience) the DID of the Feed Generator
  exp: 1683643619, // (expiration) unix timestamp in seconds
};

Le vérifier est facultatif, et la consigne dépend de ce que fait votre feed : « If you are creating a generic feed that does not differ for different users, you do not need to check auth. But if a user's state (such as follows or likes) is taken into account, we strongly encourage you to validate their auth token. » Un feed qui se personnalise selon les abonnements sans contrôler l'iss est un feed que n'importe qui peut interroger au nom de n'importe qui.

Le service Discover de Bluesky répond aux requêtes getFeedSkeleton non authentifiées par un vrai squelette, ce qui est un moyen peu coûteux de tester votre compréhension de l'endpoint.

Un développeur codant sur un ordinateur portable, le genre de travail derrière l'abonnement au firehose et le filtrage des publications pour un feed.

Le feed doit-il vérifier le JWT ?
Même feed pour tout le monde
  • Pas de follows, pas de likes, pas d'état propre à l'utilisateur
  • La vérification d'auth est facultative
Classe par follows ou likes
  • Le classement lit le DID de iss
  • Sans vérification, n'importe qui peut interroger en se faisant passer pour n'importe qui
Un feed qui classe seulement par engagement peut sauter la vérification du JWT ; un feed qui lit les follows ou les likes d'un utilisateur ne le peut pas.

D'où viennent les publications ?

Du firehose, dans la quasi-totalité des implémentations réelles : « For most use cases, we recommend subscribing to the firehose at com.atproto.sync.subscribeRepos. This websocket will send you every record that is published on the network. »

Comme un générateur de feeds ne sert jamais de contenu, il peut jeter presque tout. « Unless your algorithm is intended to provide posts you missed or something similar, you can likely garbage collect any data that is older than 48 hours. »

Les trois formes esquissées par le kit de démarrage couvrent la plupart des feeds que les gens construisent. Un feed des tendances compte les likes par publication et renvoie les publications récentes au-dessus d'un seuil. Un feed de communauté compile une liste de DIDs et renvoie tout ce que ces comptes publient. Un feed thématique passe le texte des publications dans un détecteur de mots-clés ou un modèle. Le firehose côtoie les autres limites de débit de l'API Bluesky dans lesquelles vous allez travailler.

Que laissent-ils indéfini dans les docs ?

Plusieurs choses, et ces trous sont l'endroit où les générateurs de feeds cassent en production plutôt qu'en test.

L'erreur déclarée ne correspond pas à l'erreur livrée. getFeedSkeleton comme getFeed déclarent exactement une erreur dans leurs lexicons, UnknownFeed. Demandez à l'AppView un feed qui n'existe pas et vous obtenez {"error":"InvalidRequest","message":"could not find feed"}. Demandez-le au service Discover de Bluesky et vous obtenez {"message":"no such feed: \"nope-not-real\""}, sans aucun champ error. Deux formes en production, et aucune ne porte le nom documenté.

Le kit de démarrage est plus vieux que le lexicon qu'il décrit. Sa section sur les métadonnées du squelette dit « for now, the only defined reason is a repost, but this is open to extension. » Le skeletonFeedPost actuel a une union reason à deux membres, #skeletonReasonRepost et #skeletonReasonPin. Développez contre le type Reason du README et vous ignorerez que l'épinglage existe.

AdaptlyPost
AdaptlyPost

Essai gratuit de 7 jours

Analyses multiplateforme

Boîte sociale

Assistant IA

L'histoire de l'hydratation a bougé elle aussi. Le README dit « In the future, the PDS will hydrate the feed with the help of an App View, but for now, the PDS handles hydration itself, » tandis que le lexicon de getFeed dit désormais platement « Implemented by App View. »

Aucune limite n'est publiée sur le squelette lui-même. limit plafonne une requête à 100 éléments, mais le tableau feed de la réponse ne porte aucun maxLength, et rien ne dit ce qui se passe si un générateur en renvoie plus que demandé. getFeedGenerator renvoie isOnline et isValid, décrits comme le fait que le service « has been online recently, or else seems to be inactive » et qu'il « is compatible with the record declaration. » Ni « recently » ni « compatible » ne sont définis nulle part.

Comment publier le feed ?

En écrivant l'enregistrement avec com.atproto.repo.putRecord dans votre propre repo. Le script de publication du kit de démarrage fait exactement cela, avec collection réglé sur app.bsky.feed.generator, le rkey réglé sur le nom court qui devient l'URL du feed, et did réglé sur did:web:$FEEDGEN_HOSTNAME sauf si vous avez fourni votre propre DID de service. Changer le nom, la description ou l'avatar, c'est le même appel avec le même rkey. Il n'y a pas de file d'approbation.

Rien de tout cela ne remplace la publication ordinaire. Un générateur de feeds décide de ce qu'un lecteur voit ; faire arriver vos propres publications sur le réseau reste l'affaire de programmer des publications Bluesky via une écriture d'enregistrement normale, ce que AdaptlyPost gère aux côtés des autres réseaux où il publie, l'engagement atterrissant ensuite dans les analytics Bluesky.

Foire aux questions

Ai-je besoin d'un serveur pour faire tourner un générateur de feeds Bluesky ?

Oui. L'enregistrement du feed vit dans votre repo, mais getFeedSkeleton doit être servi par un service que vous hébergez, en HTTPS sur le port 443, au nom d'hôte vers lequel pointe votre document DID. Il n'existe aucun moyen de définir un feed personnalisé entièrement à l'intérieur de Bluesky.

Un générateur de feeds peut-il voir le contenu des publications ?

Seulement ce qu'il collecte lui-même, en général depuis le firehose. Le squelette qu'il renvoie ne porte que des AT-URIs, et l'hydratation se fait côté Bluesky après que votre service a répondu.

Quelle est la différence entre getFeed et getFeedSkeleton ?

getFeedSkeleton est le vôtre et renvoie des entrées skeletonFeedPost, c'est-à-dire de simples URIs. getFeed est celui de l'AppView et renvoie des entrées feedViewPost, c'est-à-dire des publications complètes avec profils d'auteur et compteurs. Les clients appellent getFeed. Votre service, jamais.

Comment savoir si mon feed est cassé ?

Appelez app.bsky.feed.getFeedGenerator avec l'AT-URI de votre feed. Il renvoie isOnline et isValid à côté de la vue du feed. Bluesky ne publie pas depuis combien de temps un service doit avoir répondu pour compter comme en ligne, traitez donc un false comme une invitation à consulter vos propres logs.

Puis-je faire payer un feed ou restreindre qui le voit ?

Le protocole vous donne le point d'accroche et aucune facturation. L'authentification est facultative sur getFeedSkeleton, et lorsqu'elle est présente le JWT porte le DID du demandeur dans le claim iss, filtrer sur une liste de DIDs est donc simple. Le paiement, et la façon dont on annonce son refus à quelqu'un, sont entièrement laissés à votre service.

Comment mesurer la performance d'un feed ?

Bluesky publie un seul chiffre sur un feed, le likeCount de sa generatorView. Tout le reste de la performance du feed, c'est à vous de le journaliser, à partir des requêtes qui arrivent sur votre propre service. Ce manque est exactement celui traité dans Bluesky a-t-il des analytics.

Que se passe-t-il si un client demande plus de 100 éléments à un feed ?

L'AppView impose elle-même ce plafond. Demander un limit de 101 à getFeed renvoie une erreur InvalidRequest indiquant que l'entier est trop grand, avec un maximum de 100. Le paramètre limit a un minimum de 1, un maximum de 100 et une valeur par défaut de 50, donc un générateur de feeds n'a jamais à construire ce rejet lui-même.

Pourquoi le starter kit recommande-t-il did:plc plutôt que did:web pour certains générateurs de feeds ?

Un did:web est littéralement ton nom de domaine, donc changer de domaine casse tous les enregistrements publiés qui pointent vers lui. Un did:plc survit à un déménagement, c'est pourquoi le starter kit le conseille pour tout feed censé durer ou risquant de migrer.

Comment construire le curseur de pagination d'un générateur de feeds ?

Le lexicon déclare cursor comme une simple chaîne sans contrainte, et le starter kit qualifie la valeur d'opaque et entièrement laissée au choix du générateur. Le starter kit recommande un curseur composé, un horodatage associé à un CID, et insiste sur le fait qu'il doit être unique par élément du feed pour éviter les bugs de pagination.

Que fait acceptsInteractions dans l'enregistrement app.bsky.feed.generator ?

Mettre acceptsInteractions à true inscrit le feed dans app.bsky.feed.sendInteractions, ce qui permet aux clients de signaler avec quelles publications un utilisateur a interagi. Le champ est optionnel et booléen, rien dans la doc ne l'exige pour qu'un feed fonctionne.

Cet article vous a-t-il été utile ?

Dites-nous ce que vous en pensez !

Nous voir plus souvent sur Google

Un clic définit AdaptlyPost comme source préférée. Nos articles remontent alors dans vos À la une, en mode IA et dans les aperçus IA.

Avant de partir...

AdaptlyPost

AdaptlyPost

Planifiez vos contenus sur toutes les plateformes

Gérez tous vos comptes de réseaux sociaux en un seul endroit avec AdaptlyPost.

Analyses multiplateforme

Boîte sociale

Assistant IA

Termes connexes du glossaire

Articles Connexes