I was tinkering with some some code that could post to BlueSky recently, and it took me a couple of goes to make the process of submitting a message with an image work. So in case it's of any use to anyone else, here's one way it can work:
There are four things to do:
Sounds easy, huh? All the code for this is posted in a gist, and the rest of this post is a broad explanation of what's happening.
This first bit isn't code. You don't use the account password to post via the API - you use an App Password instead. To get one of those you need to log in to the UI, and click the "Settings" cog from the left menu and then "Privacy and security" to find the App Passwords option:
That page will list any app passwords you've created, and let you add new ones:
Clicking the "Add" button here will let you create a new App Password entry by supplying a name:
And then it will give you a random password to use:
Note that you have to copy this and save it, as there is no way to view this again once you close this dialog. (Also, I deleted this key before posting this - sorry hacker type people)
Also, note that when you come to authenticate, the username is your account name plus the server it's hosted on. So if your name is
@mybotaccount
then the username you need to authenticate is
mybotaccount.bsky.social
.
The rest of the calls require a JWT passed in their request headers, so the first API call is to get that token. The code needs to pass the username and the app password wrapped up in some simple json.
var usr = "yourusername.bsky.social"; var pwd = "aaaa-1111-bbbb-2222"; var authContent = JsonContent.Create(new { identifier = usr, password = pwd });
Sending the request will require an
HttpClient
object to interact with BlueSky's API. You should avoid creating and destroying these per request, so you likely want to create this centrally and reuse it for the rest of the operations:
private HttpClient _client = new();
(That's an
IDisposable
object, so make sure it's disposed once it's no longer needed) But with that in place, the json content prepared above can be posted to the relevant endpoint:
var authDataResp = await _client.PostAsync("https://bsky.social/xrpc/com.atproto.server.createSession", authContent);
If that request fails, it's likely indicating the credentials are no good - a condition which will need handling instead of proceeding. But if it succeeds then the response is some json that includes the token you need and some other data. So that needs parsing to retrieve the token:
var authJson = await authDataResp.Content.ReadAsStringAsync(); var authData = JsonNode.Parse(authJson); var jwt = authData["accessJwt"]?.GetValue<string>();
And the easiest way to handle that token is to add the
Bearer
header as a default to the client - so it's sent automatically for the remaining requests:
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
To attach an image to your post, you need to upload it and get back a data structure that describes the stored image blob. The image is supplied as a raw data stream in the body of another post, which has to specify the MIME type for the image format as a header. (As well as the token set up previously)
var imageData = new StreamContent(data); imageData.Headers.Add("Content-Type", format); var resp = await _client.PostAsync("https://bsky.social/xrpc/com.atproto.repo.uploadBlob", imageData);
The
data
variable to pass as the
StreamContent
object is the stream returned by a call like
File.OpenRead(@"C:\temp\your-test-image.jpg")
.
If this succeeds, the response from this is a chunk of json which describes the storage of the data - which will be required later.
{ "blob": { "$type": "blob", "ref": { "$link": "baqkreieuv2ih2jk3ritnfarlmjq6psv2weifchape76bju6geadjoryktu" }, "mimeType": "image/jpeg", "size": 452753 } }
That can be read from the response and parsed into a POCO to reuse later:
var storageData = await resp.Content.ReadAsStringAsync(); var storageNode = JsonObject.Parse(storageData); var blob = Blob.Deserialize(storageNode);
The POCO includes a static method to do that deserialisation, just to make the example a bit prettier: (There is probably a prettier way to do this - but I was hacking about...)
public static Blob Deserialize(JsonNode n) { var json = n.ToString(); var b = System.Text.Json.JsonSerializer.Deserialize<BlobWrapper>(n); if (b == null || b.Blob == null) { throw new ArgumentException("Deserialising blob data returned a null"); } return b.Blob; }
Posting a message with an image requires building up a json structure that respresents the message and (optionally) the attached image:
var post = new PostData { Repo = usr, Collection = "app.bsky.feed.post", Record = new PostRecordData { Text = message, CreatedAt = DateTime.Now } }; if (blob != null) { altText = altText ?? string.Empty; post.Record.Embed = Embed.Generate(blob, altText); }
So it creates the basic data with the
message
we want to send, and then adds in
Blob
from earlier as an embed if we have one.
And then that structure can get posted to the API:
var postContent = JsonContent.Create(post); var postResponse = await _client.PostAsync("https://bsky.social/xrpc/com.atproto.repo.createRecord", postContent);
As before the code can check the
.IsSuccessStatusCode
property of the response to verify the message got accepted correctly.
And with that, your message should appear in the user's feed.
↑ Back to top