Skip to content

Commit

Permalink
Start adding more Graph Endpoints for List
Browse files Browse the repository at this point in the history
  • Loading branch information
drasticactions committed Jan 15, 2024
1 parent 9f8922f commit c850674
Show file tree
Hide file tree
Showing 8 changed files with 225 additions and 13 deletions.
3 changes: 2 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
<ItemGroup>
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.2.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.435" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging.Debug" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Debug" Version="8.0.0" />
<PackageVersion Include="Sharprompt" Version="2.4.5" />
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="7.2.0" />
<PackageVersion Include="MimeTypes" Version="2.4.1" />
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions samples/FishyFlipSamplesApp/FishyFlipSamplesApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" />
<PackageReference Include="Sharprompt" />
</ItemGroup>

Expand Down
152 changes: 145 additions & 7 deletions samples/FishyFlipSamplesApp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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)
{
Expand All @@ -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"];

Expand All @@ -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;
Expand Down Expand Up @@ -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<string>("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<string>("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<string>("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);
Expand All @@ -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.");
Expand All @@ -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<ActorRecord?> GetProfileViaHandle(ATProtocol protocol)
{
var handle = Prompt.Input<string>("Handle", defaultValue: "drasticactions.dev", validators: new[] { Validators.Required() });
var handle = Prompt.Input<string>("Handle", defaultValue: "drasticactions.dev",
validators: new[] { Validators.Required() });
var profile = (await protocol.Identity.ResolveHandleAsync(ATHandle.Create(handle)!)).HandleResult();
return await GetProfileViaATDID(protocol, profile?.Did!);
}
Expand All @@ -132,14 +268,16 @@ public static class ProtocolValidators
{
public static Func<object, ValidationResult> 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;
};
}
}
54 changes: 54 additions & 0 deletions src/FishyFlip/BlueskyGraph.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,58 @@ public Task<Result<Success>> UnmuteActorAsync(ATDid did, CancellationToken cance
var muteRecord = new CreateMuteRecord(did);
return this.Client.Post<CreateMuteRecord, Success>(Constants.Urls.Bluesky.Graph.UnmuteActor, this.Options.JsonSerializerOptions, muteRecord, cancellationToken, this.Options.Logger);
}

public async Task<Result<ListViewRecord?>> 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<ListViewRecord>(url, this.Options.JsonSerializerOptions, cancellationToken, this.Options.Logger);
}

public async Task<Result<ListViewRecord?>> 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<ListViewRecord>(url, this.Options.JsonSerializerOptions, cancellationToken, this.Options.Logger);
}

public async Task<Result<ListViewRecord?>> 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<ListViewRecord>(url, this.Options.JsonSerializerOptions, cancellationToken, this.Options.Logger);
}

public async Task<Result<ListItemViewRecord?>> 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<ListItemViewRecord>(url, this.Options.JsonSerializerOptions, cancellationToken, this.Options.Logger);
}

public async Task<Result<SuggestionsRecord?>> GetSuggestedFollowsByActorAsync(ATIdentifier identifier, CancellationToken cancellationToken = default)
{
var url = Constants.Urls.Bluesky.Graph.GetSuggestedFollowsByActor + $"?actor={identifier}";
return await this.Client.Get<SuggestionsRecord>(url, this.Options.JsonSerializerOptions, cancellationToken, this.Options.Logger);
}
}
5 changes: 5 additions & 0 deletions src/FishyFlip/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
9 changes: 9 additions & 0 deletions src/FishyFlip/Models/ListView.cs
Original file line number Diff line number Diff line change
@@ -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);
3 changes: 3 additions & 0 deletions src/FishyFlip/Models/SuggestionsRecord.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace FishyFlip.Models;

public record SuggestionsRecord(ActorProfile[] Suggestions);

0 comments on commit c850674

Please sign in to comment.