FishyFlip 1.1.62-alpha
See the version list below for details.
dotnet add package FishyFlip --version 1.1.62-alpha
NuGet\Install-Package FishyFlip -Version 1.1.62-alpha
<PackageReference Include="FishyFlip" Version="1.1.62-alpha" />
paket add FishyFlip --version 1.1.62-alpha
#r "nuget: FishyFlip, 1.1.62-alpha"
// Install FishyFlip as a Cake Addin #addin nuget:?package=FishyFlip&version=1.1.62-alpha&prerelease // Install FishyFlip as a Cake Tool #tool nuget:?package=FishyFlip&version=1.1.62-alpha&prerelease
FishyFlip - a .NET ATProtocol/Bluesky Library
FishyFlip is an implementation of ATProtocol for .NET, forked from bluesky-net.
It is currently under construction.
For a Blazor WASM demo, check out https://drasticactions.github.io/FishyFlip
Third-Party Libraries
FishyFlip
- Forked from bluesky-net.
- CBOR
- net-ipfs-core
- OneOf
bskycli
How To Use
- Use
ATProtocolBuilder
to build a new instance ofATProtocol
// Include a ILogger if you want additional logging from the base library.
var debugLog = new DebugLoggerProvider();
var atProtocolBuilder = new ATProtocolBuilder()
.EnableAutoRenewSession(true)
// Set the instance URL for the PDS you wish to connect to.
// Defaults to bsky.social.
.WithInstanceUrl(new Uri("https://drasticactions.ninja"))
.WithLogger(debugLog.CreateLogger("FishyFlipDebug"));
var atProtocol = atProtocolBuilder.Build();
- Once created, you can now access unauthenticated APIs. For example, to get a list of posts from a user...
// Calls com.atproto.repo.listRecords for da-admin.drasticactions.ninja.
// ATHandle and ATDid are identifiers and can be used for most endpoints,
// such as for ListRecord points like below.
var listRecords = await atProtocol.Repo.ListPostAsync(ATHandle.Create("da-admin.drasticactions.ninja"));
// Each endpoint returns a Result<T>.
// This was originally taken from bluesky-net, which itself took it from OneOf.
// This is a pattern match object which can either be the "Success" object,
// or an "Error" object. The "Error" object will always be the type of "Error" and always be from the Bluesky API.
// This would be where you would handle things like authentication errors and the like.
listRecords.Switch(
success => {
foreach(var post in success!.Records)
{
// Prints the CID and ATURI of each post.
Console.WriteLine($"CID: {post.Cid} Uri: {post.Uri}");
// Value is `ATRecord`, a base type.
// We can check if it's a Post and get its true value.
if (post.Value is Post atPost)
{
Console.WriteLine(atPost.Text);
}
}
},
error =>
{
Console.WriteLine($"Error: {error.StatusCode} {error.Detail}");
}
);
- Instead of pattern matching, you can also use
.HandleResult()
to return thesuccess
object, and throw an exception upon anerror
.
var listRecords = (await atProtocol.Repo.ListPostAsync(ATHandle.Create("da-admin.drasticactions.ninja"))).HandleResult();
- To log in, we need to create a session. This is applied to all
ATProtocol
calls once applied. If you need to create calls from a non-auth user session, create a newATProtocol
or destroy the existing session.
// While this accepts normal passwords, you should ask users
// to create an app password from their accounts to use it instead.
Result<Session> result = await atProtocol.Server.CreateSessionAsync(userName, password, CancellationToken.None);
result.Switch(
success =>
{
// Contains the session information and tokens used internally.
Console.WriteLine($"Session: {success.Did}");
},
error =>
{
Console.WriteLine($"Error: {error.StatusCode} {error.Detail}");
}
);
// Creates a text post of "Hello, World!" to the signed in users account.
var postResult = await atProtocol.Repo.CreatePostAsync("Hello, World!");
postResult.Switch(
success =>
{
// Contains the ATUri and CID.
Console.WriteLine($"Post: {success.Uri} {success.Cid}");
},
error =>
{
Console.WriteLine($"Error: {error.StatusCode} {error.Detail}");
}
);
- To upload an image, you need to first upload it as a blob, and then attach it to a post. You can also embed links in text by setting a "Link" Facet.
var stream = File.OpenRead("path/to/image.png");
var content = new StreamContent(stream);
content.Headers.ContentLength = stream.Length;
// Bluesky uses the content type header for setting the blob type.
// As of this writing, it does not verify what kind of blob gets uploaded.
// But you should be careful about setting generic types or using the wrong one.
// If you do not set a type, it will return an error.
content.Headers.ContentType = new MediaTypeHeaderValue("image/png");
var blobResult = await atProtocol.Repo.UploadBlobAsync(content);
await blobResult.SwitchAsync(
async success =>
{
// Blob is uploaded.
Console.WriteLine($"Blob: {success.Blob.Type}");
// Converts the blob to an image.
Image? image = success.Blob.ToImage();
var prompt = "Hello, Image! Link Goes Here!";
// To insert a link, we need to find the start and end of the link text.
// This is done as a "ByteSlice."
int promptStart = prompt.IndexOf("Link Goes Here!", StringComparison.InvariantCulture);
int promptEnd = promptStart + Encoding.Default.GetBytes("Link Goes Here!").Length;
var index = new FacetIndex(promptStart, promptEnd);
var link = FacetFeature.CreateLink("https://drasticactions.dev");
var facet = new Facet(index, link);
// Create a post with the image and the link.
var postResult = await atProtocol.Repo.CreatePostAsync(prompt, new[] { facet }, new ImagesEmbed(image, "Optional Alt Text, you should have your users set this when possible"));
},
async error =>
{
Console.WriteLine($"Error: {error.StatusCode} {error.Detail}");
}
);
You should then see your image and link.
- You can access the "Firehose" by using
SubscribeRepos
. This can be seen in theFishyFlip.Firehose
sample. SubscribeRepos uses Websockets to connect to a given instead and get messages whenever a new one is posted. Messages need to be handled outside of the general WebSocket stream; if anything blocks the stream from returning messages, you may see errors from the protocol saying your connection is too slow.
var debugLog = new DebugLoggerProvider();
var atProtocolBuilder = new ATWebSocketProtocolBuilder()
// Defaults to bsky.network.
.WithInstanceUrl(new Uri("https://drasticactions.ninja"))
.WithLogger(debugLog.CreateLogger("FishyFlipDebug"));
var atProtocol = atProtocolBuilder.Build();
atProtocol.OnSubscribedRepoMessage += (sender, args) =>
{
Task.Run(() => HandleMessageAsync(args.Message)).FireAndForgetSafeAsync();
};
await atProtocol.StartSubscribeReposAsync();
var key = Console.ReadKey();
await atProtocol.StopSubscriptionAsync();
async Task HandleMessageAsync(SubscribeRepoMessage message)
{
if (message.Commit is null)
{
return;
}
var orgId = message.Commit.Repo;
if (orgId is null)
{
return;
}
if (message.Record is not null)
{
Console.WriteLine($"Record: {message.Record.Type}");
}
}
Sync
endpoints generally encode their output as IPFS Car files. Here, we can process them as they are streaming so instead of needing to download a whole file to process it, we can do it as it is downloading. This is done by using theOnCarDecoded
delegate.
var debugLog = new DebugLoggerProvider();
var atProtocolBuilder = new ATProtocolBuilder()
.EnableAutoRenewSession(true)
.WithInstanceUrl(new Uri("https://drasticactions.ninja"))
.WithLogger(debugLog.CreateLogger("FishyFlipDebug"));
var atProtocol = atProtocolBuilder.Build();
var checkoutResult = await atProtocol.Sync.GetCheckoutAsync(ATDid.Create("did:plc:yhgc5rlqhoezrx6fbawajxlh"), HandleProgressStatus);
async void HandleProgressStatus(CarProgressStatusEvent e)
{
var cid = e.Cid;
var bytes = e.Bytes;
var test = CBORObject.DecodeFromBytes(bytes);
var record = ATRecord.FromCBORObject(test);
// Prints the type of the record.
Console.WriteLine(record?.Type);
}
For more samples, check the apps
, samples
, and website
directory.
Endpoints
As a general rule of thumb, com.atproto
endpoints (such as com.atproto.sync
) do not require authentication, where app.bsky
ones do.
❌ - Not Implemented ⚠️ - Partial support, untested ✅ - Should be "working"
Sync
Actor
Endpoint | Implemented |
---|---|
app.bsky.actor.getProfile | ✅ |
app.bsky.actor.getProfiles | ✅ |
app.bsky.actor.getSuggestions | ✅ |
app.bsky.actor.searchActors | ✅ |
app.bsky.actor.searchActorsTypeahead | ✅ |
Feed
Graph
Endpoint | Implemented |
---|---|
app.bsky.graph.getBlocks | ✅ |
app.bsky.graph.getFollowers | ✅ |
app.bsky.graph.getFollows | ✅ |
app.bsky.graph.getMutes | ✅ |
app.bsky.graph.muteActor | ✅ |
app.bsky.graph.unmuteActor | ✅ |
Notification
Endpoint | Implemented |
---|---|
app.bsky.notification.getUnreadCount | ✅ |
app.bsky.notification.listNotifications | ✅ |
app.bsky.notification.updateSeen | ✅ |
Server
Repo
Moderation
Endpoint | Implemented |
---|---|
com.atproto.moderation.createReport | ✅ |
Labels
Endpoint | Implemented |
---|---|
com.atproto.label.queryLabels | ❌ |
com.atproto.label.subscribeLabels | ❌ |
Identity
Endpoint | Implemented |
---|---|
com.atproto.identity.resolveHandle | ✅ |
com.atproto.identity.updateHandle | ✅ |
Admin
Why "FishyFlip?"
"FishyFlip" is a reference to the Your Kickstarter Sucks episode of the same name.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net7.0 is compatible. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
-
net7.0
- IpfsShipyard.Ipfs.Core (>= 0.0.5)
- Microsoft.Extensions.Logging.Abstractions (>= 7.0.1)
- PeterO.Cbor (>= 4.5.2)
- System.IdentityModel.Tokens.Jwt (>= 7.0.2)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on FishyFlip:
Package | Downloads |
---|---|
WhiteWindLib
Access WhiteWind through .NET. |
GitHub repositories (1)
Showing the top 1 popular GitHub repositories that depend on FishyFlip:
Repository | Stars |
---|---|
FritzAndFriends/TagzApp
An application that discovers content on social media for hashtags
|
Version | Downloads | Last updated |
---|---|---|
3.1.0-alpha.2 | 0 | 11/24/2024 |
3.1.0-alpha.1 | 24 | 11/23/2024 |
3.1.0-alpha.0 | 73 | 11/22/2024 |
2.2.0-alpha.71 | 37 | 11/22/2024 |
2.2.0-alpha.67 | 34 | 11/22/2024 |
2.2.0-alpha.64 | 58 | 11/21/2024 |
2.2.0-alpha.57 | 35 | 11/21/2024 |
2.2.0-alpha.55 | 30 | 11/20/2024 |
2.2.0-alpha.50 | 38 | 11/19/2024 |
2.2.0-alpha.38 | 55 | 11/17/2024 |
2.2.0-alpha.37 | 37 | 11/17/2024 |
2.2.0-alpha.11 | 36 | 11/17/2024 |
2.2.0-alpha.9 | 36 | 11/17/2024 |
2.2.0-alpha.6 | 41 | 11/13/2024 |
2.2.0-alpha.4 | 65 | 11/11/2024 |
2.2.0-alpha.2 | 83 | 11/5/2024 |
2.1.1 | 295 | 11/5/2024 |
2.1.0 | 103 | 11/4/2024 |
2.1.0-alpha.23 | 64 | 11/1/2024 |
2.1.0-alpha.22 | 47 | 10/31/2024 |
2.1.0-alpha.21 | 36 | 10/31/2024 |
2.1.0-alpha.20 | 47 | 10/29/2024 |
2.1.0-alpha.19 | 67 | 10/28/2024 |
2.0.0 | 251 | 10/19/2024 |
2.0.0-alpha.53 | 62 | 10/13/2024 |
2.0.0-alpha.45 | 66 | 9/27/2024 |
1.9.0-alpha.38 | 68 | 9/13/2024 |
1.8.80 | 230 | 9/12/2024 |
1.8.78 | 120 | 9/8/2024 |
1.8.39-alpha | 93 | 6/2/2024 |
1.7.56 | 607 | 3/18/2024 |
1.7.43-alpha | 202 | 2/24/2024 |
1.7.31-alpha | 210 | 2/14/2024 |
1.7.12-alpha | 243 | 2/8/2024 |
1.6.16 | 826 | 2/7/2024 |
1.5.25 | 299 | 1/17/2024 |
1.4.19 | 303 | 1/15/2024 |
1.4.16 | 281 | 1/15/2024 |
1.3.11 | 290 | 1/9/2024 |
1.2.1 | 700 | 11/22/2023 |
1.1.62-alpha | 333 | 11/6/2023 |
1.1.59-alpha | 343 | 11/6/2023 |
1.1.54-alpha | 349 | 10/16/2023 |
1.1.52-alpha | 350 | 10/13/2023 |
1.1.49-alpha | 510 | 9/22/2023 |
1.1.45-alpha | 431 | 8/9/2023 |
1.1.35-alpha | 542 | 7/28/2023 |
1.1.33-alpha | 372 | 7/26/2023 |
1.1.18-alpha | 342 | 7/15/2023 |