FishyFlip 1.4.19

There is a newer version of this package available.
See the version list below for details.
dotnet add package FishyFlip --version 1.4.19                
NuGet\Install-Package FishyFlip -Version 1.4.19                
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.4.19" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add FishyFlip --version 1.4.19                
#r "nuget: FishyFlip, 1.4.19"                
#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.4.19

// Install FishyFlip as a Cake Tool
#tool nuget:?package=FishyFlip&version=1.4.19                

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.
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 the success object, and throw an exception upon an error.
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 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 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 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.getHead
com.atproto.sync.getBlob
com.atproto.sync.getRepo
com.atproto.sync.notifyOfUpdate
com.atproto.sync.requestCrawl
com.atproto.sync.listBlobs
com.atproto.sync.getLatestCommit
com.atproto.sync.subscribeRepos
com.atproto.sync.getRecord
com.atproto.sync.listRepos
com.atproto.sync.getBlocks
com.atproto.sync.getCheckout

Actor

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

Feed

Endpoint Implemented
app.bsky.feed.getFeedGenerators
app.bsky.feed.getTimeline
app.bsky.feed.getFeedGenerator
app.bsky.feed.getAuthorFeed
app.bsky.feed.getLikes
app.bsky.feed.getPostThread
app.bsky.feed.getActorLikes
app.bsky.feed.getRepostedBy
app.bsky.feed.describeFeedGenerator
app.bsky.feed.searchPosts
app.bsky.feed.getPosts
app.bsky.feed.getFeed
app.bsky.feed.getFeedSkeleton
app.bsky.feed.getListFeed
app.bsky.feed.getSuggestedFeeds
app.bsky.feed.getActorFeeds

Graph

Endpoint Implemented
app.bsky.graph.getSuggestedFollowsByActor
app.bsky.graph.unmuteActorList
app.bsky.graph.getListBlocks
app.bsky.graph.muteActorList
app.bsky.graph.getLists
app.bsky.graph.getFollowers
app.bsky.graph.muteActor
app.bsky.graph.getMutes
app.bsky.graph.getListMutes
app.bsky.graph.getFollows
app.bsky.graph.getBlocks
app.bsky.graph.unmuteActor
app.bsky.graph.getList

Notification

Endpoint Implemented
app.bsky.notification.registerPush
app.bsky.notification.updateSeen
app.bsky.notification.listNotifications
app.bsky.notification.getUnreadCount

Server

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

Repo

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

Moderation

Endpoint Implemented
com.atproto.moderation.createReport

Labels

Endpoint Implemented
com.atproto.label.subscribeLabels ⚠️
com.atproto.label.queryLabels ⚠️

Identity

Endpoint Implemented
com.atproto.identity.updateHandle
com.atproto.identity.resolveHandle

Admin

Endpoint Implemented
com.atproto.admin.getRepo
com.atproto.admin.updateAccountEmail
com.atproto.admin.getAccountInfo
com.atproto.admin.getSubjectStatus
com.atproto.admin.queryModerationStatuses
com.atproto.admin.updateAccountHandle
com.atproto.admin.getInviteCodes
com.atproto.admin.enableAccountInvites
com.atproto.admin.disableAccountInvites
com.atproto.admin.disableInviteCodes
com.atproto.admin.updateSubjectStatus
com.atproto.admin.emitModerationEvent
com.atproto.admin.getModerationEvent
com.atproto.admin.getRecord
com.atproto.admin.queryModerationEvents
com.atproto.admin.sendEmail
com.atproto.admin.searchRepos
com.atproto.admin.getAccountInfos
com.atproto.admin.deleteAccount

Unspecced

Endpoint Implemented
app.bsky.unspecced.searchActorsSkeleton
app.bsky.unspecced.searchPostsSkeleton
app.bsky.unspecced.getPopularFeedGenerators
app.bsky.unspecced.getTimelineSkeleton

Temp

Endpoint Implemented
com.atproto.temp.transferAccount
com.atproto.temp.pushBlob
com.atproto.temp.importRepo
com.atproto.temp.fetchLabels

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.4 0 11/25/2024
3.1.0-alpha.3 38 11/24/2024
3.1.0-alpha.2 39 11/24/2024
3.1.0-alpha.1 39 11/23/2024
3.1.0-alpha.0 83 11/22/2024
2.2.0-alpha.71 43 11/22/2024
2.2.0-alpha.67 34 11/22/2024
2.2.0-alpha.64 65 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 318 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 611 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 829 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 511 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