Last active
June 2, 2025 08:24
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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