Most services — including social networks — offer APIs that allow third-party programs or tech-savvy users to perform actions that would normally be manual and tedious, all through code.
Let’s imagine I’m blocking people on BlueSky, and I want to know if another account has blocked the same people as I have. I would need to:
- Retrieve my own block list
- Retrieve the block list of the other account
- And for each name, check if it's present in both lists.
It’s not impossible to do — at least on BlueSky, since everything is public. But it’s a massive pain.
Another example: imagine I want to block everyone who liked a post (say, because it’s a post praising Kubernetes and I don’t like Kubernetes).
Here’s what I’d have to do:
- Find the post
- Retrieve the list of people who liked it
- Block them one by one
Let’s not lie to ourselves — that’s boring as hell to do!
Especially since not all services are created equal when it comes to APIs. Between Twitter’s ridiculous pricing (yes, I still call it Twitter — what are you gonna do, Elon?), LinkedIn’s extreme restrictions, or Yammer’s complete lack of documentation, we could write an entire article just about different API designs.
There are still a few good students when it comes to APIs, and BlueSky is one of them. Mainly because their web app is built on top of their own API — which definitely helps keep things clean.
But no matter how clean BlueSky’s API is, it’s still an API — a REST API, which means a web protocol. REST APIs are great: they can be used with pretty much any language (even Bash, which tells you a lot!)
Still, between handling async calls and HTTP response codes that are longer than your holiday shopping list, it can be... annoying to manage cleanly.
That’s where SDKs come in. SDKs are libraries for your language that make it easier to work with a service.
For BlueSky, you’ve got TypeScript and Python SDKs, for example (and honestly, I get the point of a Python SDK — but TypeScript? Web devs are already weird enough, they’re used to hitting REST APIs manually anyway).
But if you're coding in something else, like Java or C#, you’ll have no choice but to manage those REST calls yourself.
Fortunately, my fellow C# developers, no worries — we’re going to look at how to solve these problems in C#!
Hello World
First things first: we need a SDK!
Good news — there’s one available on NuGet called ATbluePandaSDK
, which lets you easily interact with BlueSky in C#.
To install it, just use your NuGet package manager or run this command:
dotnet add package ATbluePandaSDK --version 0.1.2
Before diving into a complex use case, let’s start with something simple and classic: a little Hello World.
Open up your project called ConsoleApp1
— because we’re not here to waste time naming things.
Here’s a small snippet to get started:
using ATPandaSDK;
using ATPandaSDK.Models.Auth;
Console.WriteLine("Hello, World!");
ATPClient client = new ATPClient();
AuthRequest authRequest = new AuthRequest("handle.bsky.social", "password");
BskyAuthUser authUser = client.Authenticate(authRequest);
client.CreatePost("Hello world from ATbluePandaSDK version 0.1.2");
If you run this code, you’ll see “Hello, World!” in your console.
But the real magic happens on your BlueSky account — you should now see something like this:
The code is short, but let’s break it down:
We start by creating a ATPClient
object. This is the core class of the SDK — everything you want to do on BlueSky goes through this client.
But to actually do things, we need to log in. For that, we create an AuthRequest
object with your handle and password.
“But to do things, we need to log in” — Well, not entirely true. Some endpoints don’t require authentication, but posting — and most things we’ll want to do — definitely does.
We then call the Authenticate
method on the ATPClient
, passing in our AuthRequest
. This returns a BskyAuthUser
object — that’s you, your identity on BlueSky. It contains key info like your DID, handle, and most importantly, your token — which you'll use to make further API calls without having to re-enter your password.
Never share this token with anyone!
Finally, we use CreatePost
to publish our message. It just takes the text you want to post.
Notice we didn’t explicitly pass in the
BskyAuthUser
. We could have, but theATPClient
keeps it in memory after authentication — so you can skip that in future calls.
Congrats! You just posted your first Hello World using the BlueSky C# SDK.
Feeling emotional? Hold on — this is just the beginning!
A Real Use Case
Alright, you’ve tested the first features of the SDK to make sure everything is working fine. Posting on BlueSky via your IDE is very cool, but it’s probably less user-friendly than doing it through the web app. So let’s go a bit further with our very first example: finding out which accounts are blocked by both you and another user.
What’s the point of this? I don’t know—maybe a future Tinder-like app will match people based on the percentage of users they’ve both blocked!
Let’s start by retrieving your own list of blocked users and printing their names:
using ATbluePandaSDK.Models.Account;
using ATPandaSDK;
using ATPandaSDK.Models.Auth;
ATPClient client = new ATPClient();
AuthRequest authRequest = new AuthRequest("handle.bsky.social", "password");
BskyAuthUser authUser = client.Authenticate(authRequest);
List<Block> listOfUsersBlocked = client.GetBlocks();
foreach (Block b in listOfUsersBlocked)
{
Console.WriteLine(b.displayName);
}
This code gives me the following output:
Of course, you’ll see your own blocked users, but don’t be shy about sharing your list—it’s public anyway!
Note: these are the people you personally blocked—those blocked through moderation lists won’t appear here!
Let’s walk through the code from the login step!
With our client, we call the GetBlocks
function, which returns a list of blocked users (tough luck for them!) — this function runs for the currently authenticated user, so just you, and only you!
We loop through the list, and among the attributes of the Block
object, we have the displayName
(there’s more info, but it’s not useful for now).
Is Block
a User
?
Yes, there's also a User
object that contains more information, but Block
has most of the user-related data, so you can easily convert a Block
to a User
if needed!
Next step: get the list of people blocked by someone else!
For this, we’ll need two more client functions:
using ATbluePandaSDK.Models.Account;
using ATPandaSDK;
using ATPandaSDK.Models.Auth;
ATPClient client = new ATPClient();
AuthRequest authRequest = new AuthRequest("handle.bsky.social", "password");
BskyAuthUser authUser = client.Authenticate(authRequest);
List<Block> listOfUsersBlocked = client.GetBlocks();
User actor = client.GetUserProfil("otherhandle.bsky.social");
List<Records> otherListOfUsersBlocked = client.GetAccountsBlocked(actor, cursor: true);
This time, we need an actor (which is a User
object).
There are many ways to get one—here we used the GetUserProfil
function, which retrieves full user info. You can pass either a DID or a handle as a parameter.
So now we have two lists: one of Block
(the users you blocked) and one of Record
(a bit of a catch-all term in BlueSky — but in our case, also blocked users!).
We won’t go into detail here about what a
Record
is—this post is already too long!
We now have two lists that we can easily cross-reference—thankfully, we’re using C# and not Bash!
using ATbluePandaSDK.Models;
using ATbluePandaSDK.Models.Account;
using ATPandaSDK;
using ATPandaSDK.Models.Auth;
ATPClient client = new ATPClient();
AuthRequest authRequest = new AuthRequest("handle.bsky.social", "password");
BskyAuthUser authUser = client.Authenticate(authRequest);
List<Block> listOfUsersBlocked = client.GetBlocks();
User actor = client.GetUserProfil("otherhandle.bsky.social");
List<Records> otherListOfUsersBlocked = client.GetAccountsBlocked(actor, cursor: true);
foreach (Block b in listOfUsersBlocked)
{
foreach (Records r in otherListOfUsersBlocked)
{
if (b.did.Equals(r.Value.Subject))
{
try
{
User userBlocked = client.GetUserProfil(b.did);
Console.WriteLine(userBlocked.DisplayName);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
}
Here’s my output:
As you can see, the list is shorter than before (which makes sense—we just did a kind of inner join
!). Again, your output will likely be different.
Be careful: the
GetAccountsBlocked
call can take a while if the user has a long block list.
You’ll notice that this function accepts acursor
argument set totrue
— this means multiple requests may be required to handle pagination, which can take time depending on how many pages there are.
Conclusion
Did you enjoy this blog post? Let me know by liking my hello world post! Now that you’ve got the SDK, it’s super easy:
using ATPandaSDK;
using ATPandaSDK.Models.Auth;
using ATPandaSDK.Models.Feed;
ATPClient client = new ATPClient();
AuthRequest authRequest = new AuthRequest("handle.bsky.social", "password");
BskyAuthUser authUser = client.Authenticate(authRequest);
BskyTimeline timeline = client.GetAuthorTimeline();
BskyThread helloWorld = client.GetPostThread("at://did:plc:6rujkdwb4mzzplqzccqmui2h/app.bsky.feed.post/3lojdtvoqb52g");
client.LikePost(helloWorld.thread.post);
And if you want to go further, many other features are already available in the SDK:
Like/unlike, follow/unfollow, block/unblock, mute/unmute, and full access to all your timelines.
Top comments (0)