2  Looking Up the Trending Topics

2.1 Problem

You want to keep track of the trending/“hot” topics on Bluesky over a period of time.

2.2 Solution

Use methods in app.bsky.feed with the “What’s Hot” Bluesky-network-wide feed.

2.3 Discussion

For the moment, Bluesky purports to believe you own your own attention. This means you choose how you engage with content on Bluesky, and can use the built-in algorithmic feeds provided by Bluesky, pick from published algorithmic feeds created by folks you trust, or build your own.

The URL for the “What’s Hot” feed gives us some information we can use to access it programmatically:

https://bsky.app/profile/did:plc:z72i7hdynmk6r22z27h6tvur/feed/whats-hot
  • did:plc:z72i7hdynmk6r22z27h6tvur is the actor
  • whats-hot is the short name of the feed

We’ll need a full at:// URI to be able to get access to it, so let’s look for it using the API:

library(reticulate)

atproto <- import("atproto")

client <- atproto$Client()

profile <- client$login(Sys.getenv("BSKY_USER"), Sys.getenv("BSKY_PASS"))

bsky_feeds <- client$bsky$feed$get_actor_feeds(list(actor = "did:plc:z72i7hdynmk6r22z27h6tvur"))

sort(names(bsky_feeds$feeds[[1]]))
 [1] "avatar"            "cid"               "creator"          
 [4] "description"       "descriptionFacets" "did"              
 [7] "displayName"       "indexedAt"         "likeCount"        
[10] "uri"               "viewer"           
data.frame(
  display_name = sapply(bsky_feeds$feeds, \(.x) .x$displayName),
  uri = sapply(bsky_feeds$feeds, \(.x) .x$uri)
)
          display_name
1              Mutuals
2      Best of Follows
3 Popular With Friends
4         Bluesky Team
5   What's Hot Classic
6           What's Hot
                                                                            uri
1         at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/mutuals
2 at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/best-of-follows
3    at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/with-friends
4       at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/bsky-team
5     at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/hot-classic
6       at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot

We can use the “What’s Hot” URI to get a list of the items in that feed:

client$bsky$feed$get_feed(
  list(
    feed = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot"
  )
) -> hot_feed

I’ve been glossing over the cursor slot in most of the return values since this is just a cookbook to get folks started. For any method that supports cursor-based pagination, you can add it as a parameter to the list. For example, this:

client$bsky$feed$get_feed(
  list(
    feed = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot",
    cursor = hot_feed$cursor
  )
)

will get the next 50 (the current default page size) items in the feed.

Inside hot_feed has a feed slot which is a list of posts. Each post is a large, nested structure which we’ll look at via the Python object pretty-printed view of one example record:

FeedViewPost(
  post=PostView(
    author=ProfileViewBasic(
      did='did:plc:xydmw4rhlnb4mrz5eyw3z6ak',
      handle='yesitscolin.bsky.social',
      avatar='https://cdn.bsky.social/imgproxy/_XZvSpVnwSOoZYun4CrxOERXmtD0FgOTz9ffXqbUnmE/rs:fill:1000:1000:1:0/plain/bafkreidluvoz62jftcf6iqsjjo7x46s237jbr6kkluecsallz6oci2k33q@jpeg',
      displayName='Colin',
      labels=[],
      viewer=ViewerState(
        blockedBy=False,
        blocking=None,
        followedBy=None,
        following=None,
        muted=False,
        mutedByList=None,
        _type='app.bsky.actor.defs#viewerState'
      ),
      _type='app.bsky.actor.defs#profileViewBasic')
    ,
    cid='bafyreigrcsumojr4k2cy7alnloeyxm464ab4exdt2orcfluvjzpe5t5uu4',
    indexedAt='2023-07-08T13:16:24.879Z',
    record=Main(
      createdAt='2023-07-08T13:16:24.760Z',
      text='Shoutout to everyone who still cuts these up before throwing them away because they told us all when we were kids that fish get stuck in them and die…',
      embed=Main(
        images=[
          Image(
            alt='A hand holding a plastic rings drink container',
            image=<atproto.xrpc_client.models.blob_ref.BlobRef object at 0x168b58890>,
            _type='app.bsky.embed.images#image'
        )],
        _type='app.bsky.embed.images'),
      entities=None,
      facets=None,
      langs=['en'],
      reply=None,
      _type='app.bsky.feed.post'
    ),
    uri='at://did:plc:xydmw4rhlnb4mrz5eyw3z6ak/app.bsky.feed.post/3jzzabaprgx26',
    embed=View(
      images=[ViewImage(
        alt='A hand holding a plastic rings drink container',
        fullsize='https://cdn.bsky.social/imgproxy/LWNCHRKfUz-hYAc5f9xwtL7f7BhzMgtEVdXX8c6EBIY/rs:fit:2000:2000:1:0/plain/bafkreihrnrzbyypa3cmsk2bgruci7hsssjnv3l7dlv5x257utpzg7twcim@jpeg',
        thumb='https://cdn.bsky.social/imgproxy/BAD839Z-NtgH_PHGUbkaJOlq2E8KeXJbPfCC8xDVmNY/rs:fit:1000:1000:1:0/plain/bafkreihrnrzbyypa3cmsk2bgruci7hsssjnv3l7dlv5x257utpzg7twcim@jpeg',
        _type='app.bsky.embed.images#viewImage'
      )],
      _type='app.bsky.embed.images#view'
    ),
    labels=[],
    likeCount=555,
    replyCount=76,
    repostCount=42,
    viewer=ViewerState(
      like=None,
      repost=None,
      _type='app.bsky.feed.defs#viewerState'
    ),
    _type='app.bsky.feed.defs#postView'),
  reason=None,
  reply=None,
  _type='app.bsky.feed.defs#feedViewPost'
)

You can pick and choose what components from each nested structure you want and then do something similar to what I suggested in the Twitter recipes to save the data locally.

2.4 See Also