diff --git a/Directory.Packages.props b/Directory.Packages.props index 4922d3a..04e561b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,9 +6,10 @@ + - + diff --git a/README.md b/README.md index 5d413e6..d9c9010 100644 --- a/README.md +++ b/README.md @@ -295,19 +295,19 @@ As a general rule of thumb, `com.atproto` endpoints (such as `com.atproto.sync`) | Endpoint | Implemented |----------|----------| -| [app.bsky.graph.getSuggestedFollowsByActor](https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/graph/getSuggestedFollowsByActor.json) | ❌ | +| [app.bsky.graph.getSuggestedFollowsByActor](https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/graph/getSuggestedFollowsByActor.json) | ✅ | | [app.bsky.graph.unmuteActorList](https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/graph/unmuteActorList.json) | ❌ | -| [app.bsky.graph.getListBlocks](https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/graph/getListBlocks.json) | ❌ | +| [app.bsky.graph.getListBlocks](https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/graph/getListBlocks.json) | ✅ | | [app.bsky.graph.muteActorList](https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/graph/muteActorList.json) | ❌ | -| [app.bsky.graph.getLists](https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/graph/getLists.json) | ❌ | +| [app.bsky.graph.getLists](https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/graph/getLists.json) | ✅ | | [app.bsky.graph.getFollowers](https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/graph/getFollowers.json) | ✅ | | [app.bsky.graph.muteActor](https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/graph/muteActor.json) | ✅ | | [app.bsky.graph.getMutes](https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/graph/getMutes.json) | ✅ | -| [app.bsky.graph.getListMutes](https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/graph/getListMutes.json) | ❌ | +| [app.bsky.graph.getListMutes](https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/graph/getListMutes.json) | ✅ | | [app.bsky.graph.getFollows](https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/graph/getFollows.json) | ✅ | | [app.bsky.graph.getBlocks](https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/graph/getBlocks.json) | ✅ | | [app.bsky.graph.unmuteActor](https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/graph/unmuteActor.json) | ✅ | -| [app.bsky.graph.getList](https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/graph/getList.json) | ❌ | +| [app.bsky.graph.getList](https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/graph/getList.json) | ✅ | ### Notification diff --git a/samples/FishyFlipSamplesApp/FishyFlipSamplesApp.csproj b/samples/FishyFlipSamplesApp/FishyFlipSamplesApp.csproj index 7d8ab1f..efd58b9 100644 --- a/samples/FishyFlipSamplesApp/FishyFlipSamplesApp.csproj +++ b/samples/FishyFlipSamplesApp/FishyFlipSamplesApp.csproj @@ -8,6 +8,8 @@ + + diff --git a/samples/FishyFlipSamplesApp/Program.cs b/samples/FishyFlipSamplesApp/Program.cs index cad5ad9..df9c60a 100644 --- a/samples/FishyFlipSamplesApp/Program.cs +++ b/samples/FishyFlipSamplesApp/Program.cs @@ -7,6 +7,7 @@ using FishyFlip.Tools; using Sharprompt; using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Logging; using static System.Runtime.InteropServices.JavaScript.JSType; Console.WriteLine("FishyFlipSamplesApp"); @@ -24,7 +25,17 @@ var authAsk = Prompt.Confirm("Do you want to authenticate?", defaultValue: false); -var atProtocol = builder.Build(); +// Add Debug Logger +var loggerFactory = LoggerFactory.Create( + builder => builder + .AddConsole() + .AddDebug() + .SetMinimumLevel(LogLevel.Debug) +); + +var atProtocol = builder + .WithLogger(loggerFactory.CreateLogger("FishyFlipSamplesApp")) + .Build(); if (authAsk) { @@ -38,7 +49,11 @@ } } -string[] authMenuChoices = ["Exit"]; +string[] authMenuChoices = +[ + "Exit", "Get List Blocks for Actor", "Get List Mutes for Actor", "Get Suggestion Follows", + "Get Lists Via ATIdentifier", "Get List Via ATUri" +]; string[] noAuthMenuChoices = ["Exit", "Get Profile Via AtDID", "Get Profile Via Handle", "Get Avatar for Profile"]; @@ -47,6 +62,27 @@ while (true) { var menuChoice = Prompt.Select("Menu", authMenuChoices); + switch (menuChoice) + { + case "Get List Blocks for Actor": + await GetListBlocksForActor(atProtocol); + break; + case "Get List Mutes for Actor": + await GetListMutesForActor(atProtocol); + break; + case "Get Suggestion Follows": + await GetSuggestionFollows(atProtocol); + break; + case "Get List Via ATUri": + await GetListViaATUri(atProtocol); + break; + case "Get Lists Via ATIdentifier": + await GetListsViaATIdent(atProtocol); + break; + case "Exit": + return; + } + if (menuChoice == "Exit") { break; @@ -75,6 +111,103 @@ } } +async Task GetListBlocksForActor(ATProtocol protocol) +{ + var blocks = (await protocol.Graph.GetListBlocksAsync()).HandleResult(); + if (blocks is null) + { + Console.WriteLine("No blocks found."); + return; + } + + foreach(var block in blocks.Lists) + { + Console.WriteLine(block.Name); + Console.WriteLine(block.Description); + Console.WriteLine(block.Purpose); + Console.WriteLine("-----"); + } +} + +async Task GetListMutesForActor(ATProtocol protocol) +{ + var blocks = (await protocol.Graph.GetListMutesAsync()).HandleResult(); + if (blocks is null) + { + Console.WriteLine("No mutes found."); + return; + } + + foreach(var block in blocks.Lists) + { + Console.WriteLine(block.Name); + Console.WriteLine(block.Description); + Console.WriteLine(block.Purpose); + Console.WriteLine("-----"); + } +} + +async Task GetSuggestionFollows(ATProtocol protocol) +{ + var handle = Prompt.Input("Handle", defaultValue: "drasticactions.dev", + validators: new[] { Validators.Required() }); + var profile = (await protocol.Identity.ResolveHandleAsync(ATHandle.Create(handle)!)).HandleResult(); + var suggestions = (await protocol.Graph.GetSuggestedFollowsByActorAsync(profile.Did)).HandleResult(); + if (suggestions is null) + { + Console.WriteLine("No suggestions found."); + return; + } + + foreach (var item in suggestions.Suggestions) + { + Console.WriteLine(item.Did); + Console.WriteLine(item.Handle); + Console.WriteLine("-----"); + } +} + +async Task GetListViaATUri(ATProtocol protocol) +{ + var uri = Prompt.Input("ATUri", + defaultValue: "at://did:plc:yhgc5rlqhoezrx6fbawajxlh/app.bsky.graph.list/3kiwyqwydde2x", + validators: new[] { Validators.Required() }); + var lists = (await protocol.Graph.GetListAsync(ATUri.Create(uri))).HandleResult(); + if (lists is null) + { + Console.WriteLine("No lists found."); + return; + } + + foreach (var item in lists.Items) + { + Console.WriteLine(item.Uri); + Console.WriteLine(item.Subject.Handle); + Console.WriteLine("-----"); + } +} + +async Task GetListsViaATIdent(ATProtocol protocol) +{ + var handle = Prompt.Input("Handle", defaultValue: "drasticactions.dev", + validators: new[] { Validators.Required() }); + var profile = (await protocol.Identity.ResolveHandleAsync(ATHandle.Create(handle)!)).HandleResult(); + var lists = (await protocol.Graph.GetListsAsync(profile.Did)).HandleResult(); + if (lists is null) + { + Console.WriteLine("No lists found."); + return; + } + + foreach (var item in lists.Lists) + { + Console.WriteLine(item.Name); + Console.WriteLine(item.Description); + Console.WriteLine(item.Purpose); + Console.WriteLine("-----"); + } +} + async Task GetAvatarForProfile(ATProtocol protocol) { var actorRecord = await GetProfileViaHandle(protocol); @@ -90,7 +223,8 @@ async Task GetAvatarForProfile(ATProtocol protocol) } // Once we have the profile record, we can get the image by using GetBlob, the actors ATDid, and the ImageRef link. - var avatar = (await protocol.Sync.GetBlobAsync(actorRecord.Uri.Did, actorRecord.Value.Avatar.Ref.Link)).HandleResult(); + var avatar = + (await protocol.Sync.GetBlobAsync(actorRecord.Uri.Did, actorRecord.Value.Avatar.Ref.Link)).HandleResult(); if (avatar is null) { Console.WriteLine("Could not get avatar."); @@ -102,13 +236,15 @@ async Task GetAvatarForProfile(ATProtocol protocol) Console.WriteLine("Avatar saved to disk."); // We can also call on the BlueSky instance to get the avatar via a URL - var imageUri = $"https://{protocol.Options.Url.Host}{Constants.Urls.ATProtoSync.GetBlob}?did={actorRecord.Uri.Did!}&cid={actorRecord.Value.Avatar.Ref.Link}"; + var imageUri = + $"https://{protocol.Options.Url.Host}{Constants.Urls.ATProtoSync.GetBlob}?did={actorRecord.Uri.Did!}&cid={actorRecord.Value.Avatar.Ref.Link}"; Console.WriteLine($"Avatar URL: {imageUri}"); } async Task GetProfileViaHandle(ATProtocol protocol) { - var handle = Prompt.Input("Handle", defaultValue: "drasticactions.dev", validators: new[] { Validators.Required() }); + var handle = Prompt.Input("Handle", defaultValue: "drasticactions.dev", + validators: new[] { Validators.Required() }); var profile = (await protocol.Identity.ResolveHandleAsync(ATHandle.Create(handle)!)).HandleResult(); return await GetProfileViaATDID(protocol, profile?.Did!); } @@ -132,14 +268,16 @@ public static class ProtocolValidators { public static Func IsATDid() { - return delegate (object input) + return delegate(object input) { if (input == null) { return new ValidationResult("ATDid is invalid."); } - return (input is string value && !ATDid.IsValid(value)) ? new ValidationResult("ATDid is invalid.") : ValidationResult.Success; + return (input is string value && !ATDid.IsValid(value)) + ? new ValidationResult("ATDid is invalid.") + : ValidationResult.Success; }; } } \ No newline at end of file diff --git a/src/FishyFlip/BlueskyGraph.cs b/src/FishyFlip/BlueskyGraph.cs index 826baf2..8420f15 100644 --- a/src/FishyFlip/BlueskyGraph.cs +++ b/src/FishyFlip/BlueskyGraph.cs @@ -97,4 +97,58 @@ public Task> UnmuteActorAsync(ATDid did, CancellationToken cance var muteRecord = new CreateMuteRecord(did); return this.Client.Post(Constants.Urls.Bluesky.Graph.UnmuteActor, this.Options.JsonSerializerOptions, muteRecord, cancellationToken, this.Options.Logger); } + + public async Task> GetListsAsync(ATIdentifier identifier, int limit = 50, string? cursor = default, CancellationToken cancellationToken = default) + { + var url = Constants.Urls.Bluesky.Graph.GetLists + $"?actor={identifier}&limit={limit}"; + + if (cursor is not null) + { + url += $"&cursor={cursor}"; + } + + return await this.Client.Get(url, this.Options.JsonSerializerOptions, cancellationToken, this.Options.Logger); + } + + public async Task> GetListMutesAsync(int limit = 50, string? cursor = default, CancellationToken cancellationToken = default) + { + var url = Constants.Urls.Bluesky.Graph.GetListMutes + $"?limit={limit}"; + + if (cursor is not null) + { + url += $"&cursor={cursor}"; + } + + return await this.Client.Get(url, this.Options.JsonSerializerOptions, cancellationToken, this.Options.Logger); + } + + public async Task> GetListBlocksAsync(int limit = 50, string? cursor = default, CancellationToken cancellationToken = default) + { + var url = Constants.Urls.Bluesky.Graph.GetListBlocks + $"?limit={limit}"; + + if (cursor is not null) + { + url += $"&cursor={cursor}"; + } + + return await this.Client.Get(url, this.Options.JsonSerializerOptions, cancellationToken, this.Options.Logger); + } + + public async Task> GetListAsync(ATUri list, int limit = 50, string? cursor = default, CancellationToken cancellationToken = default) + { + var url = Constants.Urls.Bluesky.Graph.GetList + $"?list={list}&limit={limit}"; + + if (cursor is not null) + { + url += $"&cursor={cursor}"; + } + + return await this.Client.Get(url, this.Options.JsonSerializerOptions, cancellationToken, this.Options.Logger); + } + + public async Task> GetSuggestedFollowsByActorAsync(ATIdentifier identifier, CancellationToken cancellationToken = default) + { + var url = Constants.Urls.Bluesky.Graph.GetSuggestedFollowsByActor + $"?actor={identifier}"; + return await this.Client.Get(url, this.Options.JsonSerializerOptions, cancellationToken, this.Options.Logger); + } } diff --git a/src/FishyFlip/Constants.cs b/src/FishyFlip/Constants.cs index 9d74534..e83f456 100644 --- a/src/FishyFlip/Constants.cs +++ b/src/FishyFlip/Constants.cs @@ -122,7 +122,12 @@ public static class Actor public static class Graph { + public const string GetSuggestedFollowsByActor = "/xrpc/app.bsky.graph.getSuggestedFollowsByActor"; public const string GetBlocks = "/xrpc/app.bsky.graph.getBlocks"; + public const string GetLists = "/xrpc/app.bsky.graph.getLists"; + public const string GetListMutes = "/xrpc/app.bsky.graph.getListMutes"; + public const string GetListBlocks = "/xrpc/app.bsky.graph.getListBlocks"; + public const string GetList = "/xrpc/app.bsky.graph.getList"; public const string GetFollowers = "/xrpc/app.bsky.graph.getFollowers"; public const string GetFollows = "/xrpc/app.bsky.graph.getFollows"; public const string GetMutes = "/xrpc/app.bsky.graph.getMutes"; diff --git a/src/FishyFlip/Models/ListView.cs b/src/FishyFlip/Models/ListView.cs new file mode 100644 index 0000000..2a4a6af --- /dev/null +++ b/src/FishyFlip/Models/ListView.cs @@ -0,0 +1,9 @@ +namespace FishyFlip.Models; + +public record ListView(ATUri Uri, Cid Cid, string Name, string Purpose, string Description, Facet[]? DescriptionFacets, ActorProfile Creator, Viewer Viewer, DateTime IndexedAt); + +public record ListViewRecord(ListView[] Lists, string? Cursor); + +public record ListItemView(ATUri Uri, ActorProfile Subject); + +public record ListItemViewRecord(ListItemView[] Items, string? Cursor); \ No newline at end of file diff --git a/src/FishyFlip/Models/SuggestionsRecord.cs b/src/FishyFlip/Models/SuggestionsRecord.cs new file mode 100644 index 0000000..e76389f --- /dev/null +++ b/src/FishyFlip/Models/SuggestionsRecord.cs @@ -0,0 +1,3 @@ +namespace FishyFlip.Models; + +public record SuggestionsRecord(ActorProfile[] Suggestions); \ No newline at end of file