Skip to content

Instantly share code, notes, and snippets.

@jermdavis
Last active June 2, 2025 08:24
Show Gist options
  • Save jermdavis/bc04cca430696e80e354c962b3aefa47 to your computer and use it in GitHub Desktop.
Save jermdavis/bc04cca430696e80e354c962b3aefa47 to your computer and use it in GitHub Desktop.
Some hacky code that shows how to post an image attachment to a BlueSky post via the API
using System.Net.Http;
using System.Threading.Tasks;
using System.Net.Http.Json;
using System.Text.Json.Nodes;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Text.Json.Serialization;
internal class Program
{
async Task Main()
{
using var bsp = new BlueSkyPoster("","");
var token = await bsp.Authenticate();
token.Dump("token");
using var imgStream = File.OpenRead(@"C:\temp\test.jpg");
var blob = await bsp.UploadBlob(imgStream, "image/jpeg");
await bsp.Post("Test message", blob, "Test alt tag");
}
}
public class BlobRef
{
[JsonPropertyName("$link")]
public string Link { get; set; } = string.Empty;
}
public class BlobWrapper
{
[JsonPropertyName("blob")]
public Blob? Blob { get; set; }
}
public class Blob
{
[JsonPropertyName("$type")]
public string Type { get; set; } = string.Empty;
[JsonPropertyName("ref")]
public BlobRef? Ref { get; set; }
[JsonPropertyName("mimeType")]
public string MimeType { get; set; } = string.Empty;
[JsonPropertyName("size")]
public int Size { get; set; }
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;
}
}
public class Embed
{
[JsonPropertyName("$type")]
public string Type { get; set; } = "app.bsky.embed.images";
[JsonPropertyName("images")]
public ImageWrapper[] Images { get; set; } = Array.Empty<ImageWrapper>();
public static Embed Generate(Blob blob, string alt)
{
var e = new Embed();
e.Images = new ImageWrapper[] {
new ImageWrapper() {
Alt = alt,
Image = blob
}
};
return e;
}
}
public class ImageWrapper
{
[JsonPropertyName("alt")]
public string Alt { get; set; } = string.Empty;
[JsonPropertyName("image")]
public Blob? Image { get; set; }
}
public class PostData
{
[JsonPropertyName("repo")]
public string Repo { get; set; } = string.Empty;
[JsonPropertyName("collection")]
public string Collection { get; set; } = string.Empty;
[JsonPropertyName("record")]
public PostRecordData? Record { get; set; }
}
public class PostRecordData
{
[JsonPropertyName("text")]
public string Text { get; set; } = string.Empty;
[JsonPropertyName("createdAt")]
public DateTime CreatedAt { get; set; }
[JsonPropertyName("embed")]
public Embed? Embed { get; set; }
}
public class BlueSkyPoster : IDisposable
{
private string usr = "";
private string pwd = "";
private bool hasAuthenticated = false;
private HttpClient _client = new();
public BlueSkyPoster(string user, string password)
{
usr = user;
pwd = password;
}
public void Dispose()
{
_client.Dispose();
}
public async Task<string> Authenticate()
{
var authContent = JsonContent.Create(new {
identifier = usr,
password = pwd
});
var authDataResp = await _client.PostAsync("https://bsky.social/xrpc/com.atproto.server.createSession", authContent);
if (!authDataResp.IsSuccessStatusCode)
{
throw new ApplicationException($"Auth post failed: Result {authDataResp.StatusCode} - {await authDataResp.Content.ReadAsStringAsync()}");
}
var authJson = await authDataResp.Content.ReadAsStringAsync();
authJson.Dump("json");
var authData = JsonNode.Parse(authJson);
var jwt = authData["accessJwt"]?.GetValue<string>();
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
hasAuthenticated = true;
return jwt;
}
public async Task<Blob> UploadBlob(Stream data, string format)
{
if (hasAuthenticated == false)
{
throw new ApplicationException("Not authenticated.");
}
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);
if (!resp.IsSuccessStatusCode)
{
throw new ApplicationException($"Blob post failed: Result {resp.StatusCode} - {await resp.Content.ReadAsStringAsync()}");
}
var storageData = await resp.Content.ReadAsStringAsync();
storageData.Dump("blob");
var storageNode = JsonObject.Parse(storageData);
return Blob.Deserialize(storageNode);
}
public async Task<bool> Post(string message, Blob? blob, string? altText)
{
if (hasAuthenticated == false)
{
throw new ApplicationException("Not authenticated.");
}
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);
}
var postContent = JsonContent.Create(post);
var postResponse = await _client.PostAsync("https://bsky.social/xrpc/com.atproto.repo.createRecord", postContent);
if (!postResponse.IsSuccessStatusCode)
{
Console.WriteLine($"Post result {postResponse.StatusCode} - {await postResponse.Content.ReadAsStringAsync()}");
}
return postResponse.IsSuccessStatusCode;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment