Posting
Creating a post
Let's start off by creating a simple post with the CreatePost()
method.
var postResult = await agent.Post("Hello world!");
if (postResult.Succeeded)
{
Console.WriteLine("Post created");
Console.WriteLine($" Post AT URI: {postResult.Result.StrongReference.Uri}");
Console.WriteLine($" Post CID: {postResult.Result.StrongReference.Cid}");
}
The result from creating a post contains. amongst other things, a strong reference to the new record. This StrongReference
consists of an
at:// uri and a Content Identifier (CID).
An AT URI is a way to reference individual records in a specific repository (every Bluesky user has their own repository).
A CID is a way to identify the contents of a record using a fingerprint hash.
The AT URI, or a record's complete StrongReference
are used as a parameters in methods which deal with existing Bluesky records, for example,
liking or deleting a post.
Setting the language on a post
Setting the post's language helps custom feeds or other services filter and parse posts. You can set the posts language or languages using the language argument:
await agent.Post("G'day world!", language: "en-au");
Or if you have multiple languages
await agent.Post("สวัสดีชาวโลก!\nHello World!"", languages: new string[] {"th", "en-US"});
Setting the creation date on a post
You can also set a specific create date and time on a post by using the createdAt
parameter.
await agent.Post("Hello world from the past.",
createdAt: new DateTimeOffset(new DateTime(1900, 1, 1)));
If you don't provide createdAt
the current date and time will be used.
Understanding the results from a post call
The Post()
method creates a record in your Bluesky repo and returns anAtProtoHttpResult<CreateRecordResult>
This encapsulates the HTTP status code returned by the Bluesky API, the result of the operation,
if the operation was successful, any error messages the API returned, and information on the current rate limits applied to you,
which can be useful for making sure you don't flood the servers and get locked by a rate limiter.
To check if the call was successful you can check the Succeeded
property of the HttpResult
, which will be true
if the operation succeeded.
If its false, the StatusCode
property will contain the HTTP status code returned by the Bluesky API, and the AtErrorDetail
property will contain any
error information the API returned.
var postResult = await agent.Post("Hello world!");
if (postResult.Succeeded)
{
// The post was created successfully.
// postResult.Result contains the CreateRecordResponse returned by the API.
Console.WriteLine($" Post AT URI: {postResult.Result.StrongReference.Uri}");
}
else
{
Console.WriteLine($"{postResult.StatusCode} occurred when creating the post.");
Console.WriteLine($"Error details: {postResult.AtErrorDetail}");
}
Deleting a post
"Hello world" isn't exactly the most engaging post, so now is a good time to look at how to delete posts.
To delete a post you can use a post's AT URI, or a post's strong reference, pass it to DeletePost()
and now the post is gone.
For example, to delete the post you just made using the first code snippet above you would pass the an AT URI returned as part of the strong reference
you got from creating the post, or the strong reference itself.
var deleteResult = await agent.DeletePost(postResult.Result.StrongReference.Uri);
if (!deleteResult.Succeeded)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"{deleteResult.StatusCode} occurred when deleting the post.");
}
Replying to a post
To reply to a post, again, you need a post's StrongReference
, which you pass into ReplyTo()
.
// Create a test post we will reply to.
var createPostResult =
await agent.Post("Another test post, this time to check replying.");
// Reply to the post we just created
var replyCreatePostResult =
await agent.ReplyTo(createPostResult.Result.StrongReference, "This is a reply.");
// Reply to the reply using the reply's StrongReference
var replyToReplyStrongReference =
await agent.ReplyTo(replyCreatePostResult.StrongReference, "This is a reply to the reply.");
Replying to a post creates a new record, and it may not surprise you to see that the ReplyTo()
methods returns an HttpResult<CreateRecordResult>
just like creating a post does.
Liking, reposting and quote posting posts
To like a post you need its StrongReference
, which you then pass to agent.Like()
.
var likeResult = await agent.Like(postStrongReference);
To unlike a post call delete like with the AT-URI of the post to unlike.
var undoResult = await agent.DeleteLike(postUri);
Reposting works in just the same way.
var repostResult = await agent.Repost(postStrongReference);
var undoRepostResult = await agent.DeleteRepost(postUri);
Quoting a post requires both the post strong reference, and the text you the quote post to contain.
Deleting a post quoting another post is like deleting a regular post, you call DeletePost
with the AT-URI of the quote post that was created;
var quoteResult = await agent.Quote(postStrongReference, "This is a quote of a post.");
var deleteResult = await agent.DeleteQuote(quoteResult.Result!);
Getting your relationships with a post
You can see if you have liked or reposted by examining the view of the post you get from a feed. If you're not dealing with feeds you can get a PostView
by calling GetPostView()
with a StrongReference
to the post. A PostView
contains an optional Viewer
property, which is present should you have
reposted, liked, pinned or muted the post, or if the post author has disabled replies to, or embedding of the post.
If you liked a post then its PostView.Viewer.Like
property will contain an AT Uri to your own like record, which you can use to unlike.
If you reposted the post then PostView.Viewer.Repost
will contain the AT Uri of your repost record, which you can use to delete the repost record.
Making rich posts
Rich Posts, in Bluesky parlance, are posts which have facets. Facets are post features, three of which are currently supported, links, mentions, and hashtags.
Facet auto-detection
The majority of the Post()
APIs will try to detect links, mentions and hashtags automatically, although you can disable this by setting the
extractFacets
parameter to false
. For example:
var postResult = await agent.Post("Hello #beans");
This will result in a hashtag of beans being added to the post. Detection works for hashtags, @ mentions and for uris which begin with either https:// or http://.
The only Post()
method that doesn't auto-detect and extract facets is Post(PostBuilder, CancellationToken)
as the PostBuilder
class allows you to
specifically add facets as you build your post, see Building facets with a PostBuilder.
Tip
You can write your own facet extractor if the default one doesn't work exactly as you want, by implementing IFacetExtractor
.
IFacetExtractor
has a single method, ExtractFacets(string, CancellationToken)
that you need to implement.
This is an async
method, as creating a mention facet requires you to resolve the detected handle to a DID.
To replace the default extractor set the FacetExtractor
property on the instance of BlueskyAgentOptions
that you pass into the BlueskyAgent
constructor.
Building facets with a PostBuilder
While you can rely on auto-detection, or create facets manually, and attach them to a PostRecord
and call down into the lower levels of the library to create a post record another option is available, a PostBuilder
.
You can use the PostBuilder
class to create facets, each facet has its own class which you can add to the PostBuilder
.
Each of these classes a parameter specific to the facet type, DIDs for mentions, strings for hashtags and URIs for links. They also have a text parameter, the text in a post you want the facet to apply to.
PostBuilder
works much like a StringBuilder
does, you create an instance of it, and build your post bit by bit, adding/appending to the PostBuilder
until you're ready to create a post from it, which you do by calling agent.Post()
with the PostBuilder
.
Mentions
To mention someone in a post you must know their DID, which you can get by resolving their handle.
Then create a Mention
instance and add it to your PostBuilder
, then finally call agent.Post()
with your PostBuilder.
string userToTagHandle = "userHandle.test";
var userToTagDid = await agent.ResolveHandle(userToTagHandle);
if (did is null)
{
// handle did not resolve to a did, react accordindly.
}
var builder = new PostBuilder("Hello ") + new Mention(userToTagDid, $"@{userToTagHandle}");
var mentionPostResult = await agent.Post(builder);
One thing of note: the text doesn't have to match the "@handle" format, that's just convention.
Links to external web sites
For links to web sites you create a new instance of a Link
:
Uri uriToLinkTo = new("https://bsky.app/");
var builder = new PostBuilder("Click me ") + new Link(uriToLinkTo, uriToLinkTo.ToString());
var linkPostResult = await agent.Post(builder);
Again you can choose whatever text you want, so you could generate links in posts where the linked part of the post is just textual rather than a URI:
Uri uriToLinkTo = new("https://bsky.app/");
var builder = new PostBuilder("Click me to ") + new Link(uriToLinkTo, "visit Bluesky");
var linkPostResult = await agent.Post(builder);
HashTags
To insert a hashtag you create a new Hashtag
instance:
PostBuilder hashtagBuilder = new PostBuilder("This will have a hashtag. ") + new Hashtag("test");
var hashtagPostResult = await agent.Post(hashtagBuilder);
Tip
The HashTag
does not begin with the # character. If you include a hash character you end up with a double hashed tag.
Of course, you can chain everything together:
Tip
If you chain multiple HashTags together with Append
they will be posted without a separator between them. You might want to append them like this.
postBuilder.Append(" ");
postBuilder.Append(new HashTag(hashtag));
string userToTagHandle = "userHandle.test";
var userToTagDid = await agent.ResolveHandle(userToTagHandle);
var postBuilder = new("Hey ");
postBuilder.Append(new Mention(userToTagDid, $"@{userToTagHandle}"));
postBuilder.Append(" why not try some delicious ");
var shroudedLink = new Link("https://www.heinz.com/en-GB/products/05000157152886-baked-beanz", "beans");
postBuilder.Append(shroudedLink);
postBuilder.Append("? ");
postBuilder.Append("\nRead more: ");
var link = new Link("https://en.wikipedia.org/wiki/Heinz_Baked_Beans");
postBuilder.Append(' ');
postBuilder.Append(link);
postBuilder.Append('.');
var hashTag = new HashTag("beans");
postBuilder.Append(hashTag);
var facetedCreatePostResult =
await agent.Post(postBuilder, cancellationToken: cancellationToken);
Caution
Do not concatenate facets with other facets or strings, for example:
postBuilder.Append(" " + new Link("https://en.wikipedia.org/wiki/Heinz_Baked_Beans"));
C# will call ToString()
on the `Link`` as it is being appended to a string and your post will look something like this:
Link { Text = Read More, Uri = https://en.wikipedia.org/wiki/Heinz_Baked_Bean }
Separate your PostBuilder
append calls into individual statements:
postBuilder.Append(" ")
postBuilder.Append(new Link("https://en.wikipedia.org/wiki/Heinz_Baked_Bean", "Read More"));
Posting with images
Creating a post with one or more images is a two step process, upload the image as a blob, then create a post with a reference to the newly created blob.
To upload an image you need the image as a byte array, which you can get by copying a stream to a memory stream, for example:
byte[] imageAsBytes;
using (FileStream fs = File.OpenRead(pathToImage))
using (MemoryStream ms = new())
{
fs.CopyTo(ms);
imageAsBytes = ms.ToArray();
}
Once you have your byte array upload it using UploadImage
:
var imageUploadResult = await agent.UploadImage(
imageAsBytes,
"image/jpg",
"The Bluesky Logo",
new AspectRatio(1000, 1000),
cancellationToken: cancellationToken);
There is no validation done on the MIME type by Bluesky when uploading a blob, it is up to you to choose the correct one.
If you upload a blob but don't use it in a post it will get deleted within an unspecified amount of time.
If the call to UploadImage
didn't error you can pass its result to agent.Post()
.
if (imageUploadResult.Succeeded)
{
var createPostResult = await agent.Post(
"Hello world with an image.",
imageUploadResult.Result,
cancellationToken: cancellationToken);
}
If you have multiple images you can pass an ICollection<EmbeddedImage>
into agent.Post()
You can, of course, add images to a PostBuilder
:
PostBuilder postBuilder = new("A reply with an image.");
var imageUploadResult = await agent.UploadImage(
imageAsBytes,
"image/jpg",
"The Bluesky Logo",
new AspectRatio(1000, 1000),
cancellationToken: cancellationToken);
if (imageUploadResult.Succeeded)
{
postBuilder +=
new EmbeddedImage(replyImageBlobLink.Result!, "Image alttext", new AspectRatio(1000, 1000));
}
Self-labelling your posts
Bluesky allows you to self label a post, classifying the media the post contains. You can label a post to indicate it contains one, or more, of the following classifications:
- Porn, which puts a warning on images and can only be clicked through if the user is 18+ and has enabled adult content,
- Sexual, which behaves like porn but is meant to handle less intense sexual content,
- Graphic-Media, which behaves like porn but is for violence / gore and
- Nudity which puts a warning on images but isn’t 18+ and defaults to ignore.
You can classify a post by passing in an instance of PostSelfLabels
to any of the agent.Post()
methods, with the properties set to indicate your content classification,
or into a PostBuilder
via the constructor, or via the SetSelfLabels
method.
var labels = new PostSelfLabels
{
Porn = true,
GraphicMedia = true,
Nudity = true,
SexualContent = true
};
var postResult = await agent.Post("Naughty bean content", labels : labels, cancellationToken: cancellationToken);
var postBuilder = new PostBuilder("Naughty bean content");
postBuilder.SetSelfLabels(labels);
var builderPostResult = await agent.Post(postBuilder, cancellationToken: cancellationToken);
Embedding an external link (Open Graph cards)
Open Graph is a standard that allows web pages to become a rich object in a social graph. Open Graph metadata allows you to embed a rich link card in a Bluesky post, which will look something like this:
To embed an external link with a card create an instance of EmbeddedExternal
then attach it to a PostBuilder
with the Embed()
method.
var embeddedExternal = new(pageUri, title, description, thumbnailBlob);
var postBuilder = new PostBuilder("Embedded record test");
postBuilder.EmbedRecord(embeddedExternal);
var postResult = await agent.Post(postBuilder, cancellationToken: cancellationToken);
If you don't want to use a PostBuilder
you can use the appropriate Post()
method
var postResult = agent.Post(externalCard: embeddedExternal, cancellationToken: cancellationToken);
You can use libraries like OpenGraph.net or X.Web.MetaExtractor to retrieve Open Graph properties from which you can construct a card. For example, using OpenGraph.Net
Uri pageUri = new ("https://en.wikipedia.org/wiki/Baked_beans");
OpenGraph graph = await OpenGraph.ParseUrlAsync(pageUri, cancellationToken: cancellationToken);
string? title = graph.Title;
string? description = graph.Description;
// Check to see if there's a different URI specified in the graph metadata.
if (graph.Url is not null)
{
pageUri = graph.Url;
}
The Embedded Card sample shows how to use OpenGraph.Net to extract the metadata, and to retrieve a preview image and use it, if the metadata has an image property.
Posts with an embedded card don't need any post text.