Timelines and Feeds
Reading your timeline
To get the timeline for a logged in account you call agent.GetTimeline().
Warning
Treat data received from Bluesky, AT Protocol services, and streams such as Jetstream and the firehose as untrusted. This includes post and message text, profile and
list metadata, labels, error details such as AtErrorDetail, and raw JSON. Before displaying it, encode it for the output context (for example, HTML-encode it for a web page). Before using
a URL in a link or other active context, check its scheme against an allow-list of the schemes your application expects, such as https; a
well-formed URL can still use an active scheme such as javascript:, which can lead to cross-site scripting.
Validate other values before using them in active contexts.
The sample code on this page is for illustration. It does not sanitize or otherwise validate the input it uses; apply the validation and encoding appropriate for your application.
AtProtoHttpResult<Timeline> timelineResult = await agent.GetTimeline();
if (timelineResult.Succeeded && timelineResult.Result.Count != 0)
{
foreach (FeedViewPost timelineView in timelineResult.Result)
{
if (timelineView.Post.Record is Post postRecord && !string.IsNullOrEmpty(postRecord.Text))
{
Console.WriteLine(postRecord.Text);
}
Console.WriteLine($" From {@timelineView.Post.Author} {GetLabels(timelineView.Post.Author)}");
Console.WriteLine($" Posted at: {timelineView.Post.Record.CreatedAt.ToLocalTime():G}");
Console.Write($" {timelineView.Post.LikeCount} like{(timelineView.Post.LikeCount != 1 ? "s" : "")} ");
Console.WriteLine($"{timelineView.Post.RepostCount} repost{(timelineView.Post.RepostCount != 1 ? "s" : "")}.");
Console.WriteLine($" AtUri: {timelineView.Post.Uri}");
Console.WriteLine($" Cid: {timelineView.Post.Cid}");
if (timelineView.Reason is ReasonRepost repost)
{
Console.WriteLine($" ♲ Reposted by {repost.By}");
}
if (timelineView.Reply is not null)
{
// Display the parent of this post, as it was in reply to something.
}
}
}
Before attempting to iterate through the timeline the code above first checks if the API call was successful, if so the result from the API call
will contain a collection of FeedViewPosts.
Each FeedViewPost (defined in the lexicon for Feeds has a
Post property, and optional Reply and Reason properties. The Post property contains information about the post, such as its author,
the time it was created, etc., and a Record property that is typically a PostRecord, a type of record that has a Text property that contains any
text for that Post.
The code above shows iterating through Feed, retrieving each individual FeedView and writing some information from the FeedView.Post to the console.
The Timeline sample also contains code which pages through results. When browsing timelines and feeds you will need to understand how AtProto implements pagination with cursors.
Reading feeds
A timeline is a well known feed. A feed is a view created by a feed generator over collections of posts, the criteria for which the feed controls.
A feed is referenced by its at:// uri, loaded with GetFeed() rather than GetTimeline() and then paginated in exactly the same way.
AtUri feedUri = new("at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot");
const int pageSize = 25;
const int maxPagesToIterate = 10;
var getFeedResult = await agent.GetFeed(feedUri, limit: pageSize);
if (getFeedResult.Succeeded && getFeedResult.Result.Count != 0)
{
int page = 1;
do
{
// Display the feed data
getFeedResult = await agent.GetFeed(feedUri, limit: pageSize, cursor: getFeedResult.Result.Cursor);
page++;
} while (getFeedResult.Succeeded &&
!string.IsNullOrEmpty(getFeedResult.Result.Cursor) &&
page < maxPagesToIterate);
}
Tip
Some feeds, like the Discover feed use a specialized cursor, rather than the more typical timestamp. This can grow with each page, to the point when it is too large to send back to the server, hence the code above, taken from the Feed sample limiting the do while loop not just until data runs out but also to a maximum number of pages.
Searching
Feeds aren't just lists of posts, SearchPosts() returns its results as a feed.
const int pageSize = 25;
const int maxPagesToIterate = 10;
var searchResult = await agent.SearchPosts("#beans", limit: pageSize);
if (searchResult.Succeeded && searchResult.Result.Count != 0)
{
int page = 1;
do
{
// Display the feed data
searchResult = await agent.SearchPosts("#beans", limit: pageSize, cursor: searchResult.Result.Cursor);
page++;
} while (searchResult.Succeeded &&
!string.IsNullOrEmpty(searchResult.Result.Cursor) &&
page < maxPagesToIterate);
}
SearchActors also returns a feed of results. GetActorLikes returns a feed of likes for an actor, GetLikes returns a feed of likes for an individual post,
and so on.
You can find these APIs and more in the HTTP reference, each API has its equivalent BlueskyAgent method.