omnifeed

bluesky

import "github.com/kinorai/omnifeed/internal/engine/bluesky"

Package bluesky implements the Bluesky engine. It reads the public AT Protocol AppView (public.api.bsky.app) and renders a bsky.app post URL as a flattened reply tree and a profile URL as that account’s recent posts (TOON) — mirroring the Reddit and Hacker News engines’ output shape, so nesting is reconstructable from parent_uri.

Like the Hacker News and GitHub engines it fetches its upstream DIRECTLY over HTTP rather than through crawl4ai: the AppView is a public, keyless JSON API, while bsky.app itself is a client-side SPA that a headless browser renders as a near-empty shell. It does require omnifeed to have outbound access to public.api.bsky.app.

SCOPE: post threads and author feeds only. app.bsky.feed.searchPosts — keyword search across the network — is NOT used: the AppView answers it with HTTP 403 to unauthenticated callers (its own lexicon warns the endpoint “may require authentication (eg, not public)”), while getPostThread, getAuthorFeed and getProfile on the very same host stay open. Topic discovery on Bluesky therefore still needs a signed-in client or web search; this engine covers reading a post or an account you already have a URL for.

Index

type Author

Author is the account that wrote a post, stripped to LLM-relevant fields.

type Author struct {
    Handle string `json:"handle" toon:"handle"`
    Name   string `json:"name,omitempty" toon:"name,omitempty"`
}

type Config

Config configures a Bluesky Engine.

type Config struct {
    Client  *httpx.Client
    Limiter httpx.Limiter
    APIBase string        // defaults to the public AppView; overridden in tests
    Timeout time.Duration // wall-clock budget per crawl; defaults to defaultTimeout
    Logger  *slog.Logger
}

type Engine

Engine implements domain.Engine for Bluesky URLs via the public AppView.

type Engine struct {
    // contains filtered or unexported fields
}

func New

func New(cfg Config) *Engine

New returns a Bluesky Engine configured per cfg.

func (*Engine) Crawl

func (e *Engine) Crawl(ctx context.Context, rawURL string, _ domain.EngineOptions) (domain.Document, error)

Crawl fetches the Bluesky thread or author feed behind rawURL from the public AppView and returns it encoded as TOON.

func (*Engine) Matches

func (*Engine) Matches(rawURL string) bool

Matches claims the two bsky.app URL shapes this engine renders: /profile/{actor}/post/{rkey} threads and /profile/{actor} author feeds. Anything else (/search, feed generators, starter packs) falls through to the generic fallback — see the package doc on why /search is not claimed.

func (*Engine) Name

func (*Engine) Name() string

Name returns the engine identifier (“bluesky”).

type Feed

Feed is an account’s recent posts.

type Feed struct {
    Actor string `json:"actor" toon:"actor"`
    Posts []Post `json:"posts" toon:"posts"`
}

type Post

Post is one Bluesky post rendered for an LLM. URI is the AT-URI, which is also the join key: a reply’s ParentURI names the post it answers.

type Post struct {
    URI       string `json:"uri" toon:"uri"`
    ParentURI string `json:"parent_uri,omitempty" toon:"parent_uri,omitempty"`
    Author    Author `json:"author" toon:"author"`
    Text      string `json:"text" toon:"text"`
    CreatedAt string `json:"created_at" toon:"created_at"`
    Replies   int    `json:"replies" toon:"replies"`
    Reposts   int    `json:"reposts" toon:"reposts"`
    Likes     int    `json:"likes" toon:"likes"`
    Quotes    int    `json:"quotes,omitempty" toon:"quotes,omitempty"`
    // Link is the URL of an external page the post embeds, when it has one.
    // Bluesky posts are short and often exist only to point somewhere else, so
    // dropping the embed would lose the post's whole payload.
    Link string `json:"link,omitempty" toon:"link,omitempty"`
}

type Thread

Thread groups a Bluesky post with its ancestors and its flattened reply tree.

type Thread struct {
    // Ancestors are the posts this one replies to, root first. A bsky.app URL
    // for a reply is indistinguishable from one for a root post, so without
    // these a linked reply would arrive with no sight of what it answers.
    Ancestors []Post `json:"ancestors,omitempty" toon:"ancestors,omitempty"`
    Post      Post   `json:"post" toon:"post"`
    Replies   []Post `json:"replies" toon:"replies"`
}

Generated by gomarkdoc