FishyFlip 1.1.18-alpha

This is a prerelease version of FishyFlip.
There is a newer version of this package available.
See the version list below for details.
dotnet add package FishyFlip --version 1.1.18-alpha                
NuGet\Install-Package FishyFlip -Version 1.1.18-alpha                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="FishyFlip" Version="1.1.18-alpha" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add FishyFlip --version 1.1.18-alpha                
#r "nuget: FishyFlip, 1.1.18-alpha"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install FishyFlip as a Cake Addin
#addin nuget:?package=FishyFlip&version=1.1.18-alpha&prerelease

// Install FishyFlip as a Cake Tool
#tool nuget:?package=FishyFlip&version=1.1.18-alpha&prerelease                

FishyFlip - a .NET ATProtocol/Bluesky Library

NuGet Version License

FishyFlip Logo

1444070256569233

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

bskycli

How To Use

  • Use ATProtocolBuilder to build a new instance of ATProtocol
// 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.
// You can get around this by using `.AsT0` to ignore the error object, but I would handle it where possible.
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}");
    }
);
  • 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 new ATProtocol 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.

Post Sample

  • You can access the "Firehose" by using SubscribeRepos. This can be seen in the FishyFlip.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 ATProtocolBuilder()
    .EnableAutoRenewSession(true)
    .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 the OnCarDecoded 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

Endpoint Implemented
com.atproto.sync.getBlob
com.atproto.sync.getBlocks
com.atproto.sync.getCheckout
com.atproto.sync.getCommitPath
com.atproto.sync.getHead
com.atproto.sync.getRecord
com.atproto.sync.getRepo
com.atproto.sync.listBlobs
com.atproto.sync.listRepos
com.atproto.sync.notifyOfUpdate ⚠️
com.atproto.sync.requestCrawl ⚠️
com.atproto.sync.subscribeRepos

Actor

Endpoint Implemented
app.bsky.actor.getProfile
app.bsky.actor.getProfiles
app.bsky.actor.getSuggestions
app.bsky.actor.searchActors
app.bsky.actor.searchActorsTypeahead

Feed

Endpoint Implemented
app.bsky.feed.getAuthorFeed
app.bsky.feed.getLikes
app.bsky.feed.getPostThread
app.bsky.feed.getPosts
app.bsky.feed.getRepostedBy
app.bsky.feed.getTimeline
app.bsky.feed.getFeedSkeleton

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

Endpoint Implemented
com.atproto.server.createAccount
com.atproto.server.createAppPassword
com.atproto.server.createInviteCode
com.atproto.server.createInviteCodes
com.atproto.server.createSession
com.atproto.server.deleteAccount
com.atproto.server.deleteSession
com.atproto.server.describeServer
com.atproto.server.getAccountInviteCodes
com.atproto.server.getSession
com.atproto.server.listAppPasswords
com.atproto.server.refreshSession
com.atproto.server.requestAccountDelete
com.atproto.server.requestPasswordReset
com.atproto.server.resetPassword
com.atproto.server.revokeAppPassword

Repo

Endpoint Implemented
com.atproto.repo.applyWrites
com.atproto.repo.createRecord
com.atproto.repo.deleteRecord
com.atproto.repo.describeRepo
com.atproto.repo.getRecord
com.atproto.repo.listRecords
com.atproto.repo.putRecord
com.atproto.repo.uploadBlob

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

Endpoint Implemented
com.atproto.admin.disableInviteCodes
com.atproto.admin.getInviteCodes
com.atproto.admin.getModerationAction
com.atproto.admin.getModerationActions
com.atproto.admin.getModerationReport
com.atproto.admin.getModerationReports
com.atproto.admin.getRecord
com.atproto.admin.getRepo
com.atproto.admin.resolveModerationReports
com.atproto.admin.reverseModerationAction
com.atproto.admin.searchRepos
com.atproto.admin.takeModerationAction
com.atproto.admin.updateAccountEmail
com.atproto.admin.updateAccountHandle

Why "FishyFlip?"

"FishyFlip" is a reference to the Your Kickstarter Sucks episode of the same name.

Discord Image

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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