diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..f6310b7f --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,33 @@ +name: Tests +on: + push: + branches: [ "main", release-*, develop ] + pull_request: + branches: [ "main", release-*, develop ] + workflow_dispatch: +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of commits + submodules: 'true' + + - name: Setup .NET Core + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.x + + - name: Run FishyFlip.Tests + run: dotnet test src/FishyFlip.Tests/FishyFlip.Tests.csproj -- --report-trx --results-directory ../../dotnet-test-results + + - name: Run WhiteWindLib.Tests + run: dotnet test src/WhiteWindLib.Tests/WhiteWindLib.Tests.csproj -- --report-trx --results-directory ../../dotnet-test-results + + - name: Upload Test Results + uses: actions/upload-artifact@v4 + with: + name: dotnet-test-results + path: dotnet-test-results + if: ${{ always() }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1603c7eb..f10c293e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. +dotnet-test-results/ whitewindlib.runsettings fishyflip.runsettings external/Drastic.FlipperKit/build/ diff --git a/Directory.Packages.props b/Directory.Packages.props index 8f1190c1..b241c556 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -26,7 +26,7 @@ - + @@ -34,7 +34,10 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/samples/Jetstream/Jetstream.csproj b/samples/Jetstream/Jetstream.csproj new file mode 100644 index 00000000..f138398d --- /dev/null +++ b/samples/Jetstream/Jetstream.csproj @@ -0,0 +1,18 @@ + + + + + + + + Exe + net8.0 + enable + enable + true + + + + + + diff --git a/samples/Jetstream/Jetstream.sln b/samples/Jetstream/Jetstream.sln new file mode 100644 index 00000000..a3cce401 --- /dev/null +++ b/samples/Jetstream/Jetstream.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FishyFlip", "..\..\src\FishyFlip\FishyFlip.csproj", "{48260865-FB6C-4F37-B80B-4CF1F8B52E66}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jetstream", "Jetstream.csproj", "{D80F3939-6001-4EFE-B574-26771F9B7523}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {48260865-FB6C-4F37-B80B-4CF1F8B52E66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {48260865-FB6C-4F37-B80B-4CF1F8B52E66}.Debug|Any CPU.Build.0 = Debug|Any CPU + {48260865-FB6C-4F37-B80B-4CF1F8B52E66}.Release|Any CPU.ActiveCfg = Release|Any CPU + {48260865-FB6C-4F37-B80B-4CF1F8B52E66}.Release|Any CPU.Build.0 = Release|Any CPU + {D80F3939-6001-4EFE-B574-26771F9B7523}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D80F3939-6001-4EFE-B574-26771F9B7523}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D80F3939-6001-4EFE-B574-26771F9B7523}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D80F3939-6001-4EFE-B574-26771F9B7523}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/samples/Jetstream/Program.cs b/samples/Jetstream/Program.cs new file mode 100644 index 00000000..78c0bf40 --- /dev/null +++ b/samples/Jetstream/Program.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) Drastic Actions. All rights reserved. +// + +using FishyFlip; +using Microsoft.Extensions.Logging.Debug; + +Console.WriteLine("Hello, Jetstream!"); + +var debugLog = new DebugLoggerProvider(); + +// You can set a custom url with WithInstanceUrl +var jetstreamBuilder = new ATJetStreamBuilder() + .WithLogger(debugLog.CreateLogger("FishyFlipDebug")); +var atWebProtocol = jetstreamBuilder.Build(); + +atWebProtocol.OnConnectionUpdated += (sender, args) => +{ + Console.WriteLine($"Connection Updated: {args.State}"); +}; + +atWebProtocol.OnRecordReceived += (sender, args) => +{ + Console.WriteLine($"Record Received: {args.Record.Type}"); +}; + +await atWebProtocol.ConnectAsync(); + +var key = Console.ReadKey(); + +await atWebProtocol.CloseAsync(); \ No newline at end of file diff --git a/samples/PasswordAuth/PasswordAuth.csproj b/samples/PasswordAuth/PasswordAuth.csproj new file mode 100644 index 00000000..17797bf1 --- /dev/null +++ b/samples/PasswordAuth/PasswordAuth.csproj @@ -0,0 +1,23 @@ + + + + Exe + net8.0 + enable + enable + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/samples/PasswordAuth/PasswordAuth.sln b/samples/PasswordAuth/PasswordAuth.sln new file mode 100644 index 00000000..18de094a --- /dev/null +++ b/samples/PasswordAuth/PasswordAuth.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PasswordAuth", "PasswordAuth.csproj", "{CA58096E-487C-4C4B-B72F-FC5A48AD6A4B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FishyFlip", "..\..\src\FishyFlip\FishyFlip.csproj", "{7E7D4B52-3C34-430A-AA04-E3E77852F052}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {CA58096E-487C-4C4B-B72F-FC5A48AD6A4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CA58096E-487C-4C4B-B72F-FC5A48AD6A4B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CA58096E-487C-4C4B-B72F-FC5A48AD6A4B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CA58096E-487C-4C4B-B72F-FC5A48AD6A4B}.Release|Any CPU.Build.0 = Release|Any CPU + {7E7D4B52-3C34-430A-AA04-E3E77852F052}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7E7D4B52-3C34-430A-AA04-E3E77852F052}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7E7D4B52-3C34-430A-AA04-E3E77852F052}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7E7D4B52-3C34-430A-AA04-E3E77852F052}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/samples/PasswordAuth/Program.cs b/samples/PasswordAuth/Program.cs new file mode 100644 index 00000000..346d8bee --- /dev/null +++ b/samples/PasswordAuth/Program.cs @@ -0,0 +1,43 @@ +using ConsoleAppFramework; +using FishyFlip; +using Microsoft.Extensions.Logging.Debug; + +var app = ConsoleApp.Create(); +app.Add(); +app.Run(args); + +/// +/// App Commands. +/// +#pragma warning disable SA1649 // File name should match first type name +public class AppCommands +#pragma warning restore SA1649 // File name should match first type name +{ + /// + /// Authenticate with the ATProtocol using a password. + /// + /// The user's identifier. + /// The user's password. + /// Cancellation Token. + /// Task. + [Command("authenticate")] + public async Task AuthenticateAsync([Argument] string identifier, [Argument] string password, CancellationToken cancellationToken = default) + { + var protocol = new ATProtocolBuilder() + .WithLogger(new DebugLoggerProvider().CreateLogger("FishyFlip")) + .Build(); + + var session = await protocol.AuthenticateWithPasswordAsync(identifier, password, cancellationToken); + if (session is null) + { + Console.WriteLine("Failed to authenticate."); + return; + } + + Console.WriteLine("Authenticated."); + Console.WriteLine($"Session Did: {session.Did}"); + Console.WriteLine($"Session Email: {session.Email}"); + Console.WriteLine($"Session Handle: {session.Handle}"); + Console.WriteLine($"Session Token: {session.AccessJwt}"); + } +} \ No newline at end of file diff --git a/src/FishyFlip.Tests/AuthorizedTests.cs b/src/FishyFlip.Auth.Tests/AuthorizedTests.cs similarity index 77% rename from src/FishyFlip.Tests/AuthorizedTests.cs rename to src/FishyFlip.Auth.Tests/AuthorizedTests.cs index 7c71e286..1f88ebe1 100644 --- a/src/FishyFlip.Tests/AuthorizedTests.cs +++ b/src/FishyFlip.Auth.Tests/AuthorizedTests.cs @@ -12,19 +12,6 @@ namespace FishyFlip.Tests; public class AuthorizedTests { static ATProtocol proto; - static string handle; - static string handle_2; - static string did; - static string did_2; - static string post_thread; - static string quote_post; - static string quote_post_2; - static string feed_generator; - static string follow_did; - static string block_did; - static string media_post; - static string images_post; - static string external_post; public AuthorizedTests() { @@ -33,28 +20,16 @@ public AuthorizedTests() [ClassInitialize] public static void ClassInitialize(TestContext context) { - feed_generator = (string?)context.Properties["BLUESKY_TEST_FEED_GENERATOR"] ?? throw new ArgumentNullException(); - follow_did = (string?)context.Properties["BLUESKY_TEST_FOLLOW_DID"] ?? throw new ArgumentNullException(); - block_did = (string?)context.Properties["BLUESKY_TEST_BLOCK_DID"] ?? throw new ArgumentNullException(); - media_post = (string?)context.Properties["BLUESKY_TEST_MEDIA_POST"] ?? throw new ArgumentNullException(); - images_post = (string?)context.Properties["BLUESKY_TEST_IMAGES_POST"] ?? throw new ArgumentNullException(); - external_post = (string?)context.Properties["BLUESKY_TEST_EXTERNAL_POST"] ?? throw new ArgumentNullException(); - handle = (string?)context.Properties["BLUESKY_TEST_HANDLE"] ?? throw new ArgumentNullException(); - handle_2 = (string?)context.Properties["BLUESKY_TEST_HANDLE_2"] ?? throw new ArgumentNullException(); - did = (string?)context.Properties["BLUESKY_TEST_DID"] ?? throw new ArgumentNullException(); - did_2 = (string?)context.Properties["BLUESKY_TEST_DID_2"] ?? throw new ArgumentNullException(); - post_thread = (string?)context.Properties["BLUESKY_TEST_POST_THREAD"] ?? throw new ArgumentNullException(); - quote_post = (string?)context.Properties["BLUESKY_TEST_QUOTE_POST"] ?? throw new ArgumentNullException(); - quote_post_2 = (string?)context.Properties["BLUESKY_TEST_QUOTE_POST_2"] ?? throw new ArgumentNullException(); + string handle = (string?)context.Properties["BLUESKY_TEST_HANDLE"] ?? throw new ArgumentNullException(); string password = (string?)context.Properties["BLUESKY_TEST_PASSWORD"] ?? throw new ArgumentNullException(); - string instance = (string?)context.Properties["BLUESKY_INSTANCE_URL"] ?? throw new ArgumentNullException(); + string instance = "https://bsky.social"; var debugLog = new DebugLoggerProvider(); var atProtocolBuilder = new ATProtocolBuilder() .EnableAutoRenewSession(false) .WithInstanceUrl(new Uri(instance)) .WithLogger(debugLog.CreateLogger("FishyFlipTests")); AuthorizedTests.proto = atProtocolBuilder.Build(); - AuthorizedTests.proto.AuthenticateWithPasswordAsync(AuthorizedTests.handle, password).Wait(); + AuthorizedTests.proto.AuthenticateWithPasswordAsync(handle, password).Wait(); } [TestMethod] @@ -73,9 +48,10 @@ public async Task GetPopularFeedGeneratorsAsync() } [TestMethod] - public async Task GetFeedAsyncTest() + [DataRow("at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot")] + public async Task GetFeedAsyncTest(string feedGenerator) { - var atUri = ATUri.Create(AuthorizedTests.feed_generator); + var atUri = ATUri.Create(feedGenerator); var result = await AuthorizedTests.proto.Feed.GetFeedAsync(atUri); result.Switch( success => @@ -89,9 +65,10 @@ public async Task GetFeedAsyncTest() } [TestMethod] - public async Task GetFeedGeneratorAsyncTest() + [DataRow("at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot")] + public async Task GetFeedGeneratorAsyncTest(string feedGenerator) { - var atUri = ATUri.Create(AuthorizedTests.feed_generator); + var atUri = ATUri.Create(feedGenerator); var result = await AuthorizedTests.proto.Feed.GetFeedGeneratorAsync(atUri); result.Switch( success => @@ -105,13 +82,14 @@ public async Task GetFeedGeneratorAsyncTest() } [TestMethod] - public async Task GetProfileAsyncTest() + [DataRow("drasticactions.xn--q9jyb4c")] + public async Task GetProfileAsyncTest(string handle1) { - var result = await AuthorizedTests.proto.Actor.GetProfileAsync(ATHandle.Create(AuthorizedTests.handle) ?? throw new ArgumentNullException(nameof(AuthorizedTests.handle))); + var result = await AuthorizedTests.proto.Actor.GetProfileAsync(ATHandle.Create(handle1) ?? throw new ArgumentNullException(nameof(handle1))); result.Switch( success => { - Assert.AreEqual(success!.Handle, AuthorizedTests.handle); + Assert.AreEqual(success!.Handle, handle1); }, failed => { @@ -120,14 +98,15 @@ public async Task GetProfileAsyncTest() } [TestMethod] - public async Task GetProfilesAsyncWithHandlesTest() + [DataRow("drasticactions.xn--q9jyb4c", "peepthisbot.bsky.social")] + public async Task GetProfilesAsyncWithHandlesTest(string handle1, string handle2) { - var result = await AuthorizedTests.proto.Actor.GetProfilesAsync(new[] { ATHandle.Create(AuthorizedTests.handle), ATHandle.Create(AuthorizedTests.handle_2) }); + var result = await AuthorizedTests.proto.Actor.GetProfilesAsync(new[] { ATHandle.Create(handle1), ATHandle.Create(handle2) }); result.Switch( success => { - Assert.AreEqual(AuthorizedTests.handle, success!.Profiles[0]!.Handle.ToString()); - Assert.AreEqual(AuthorizedTests.handle_2.ToString(), success!.Profiles[1]!.Handle.ToString()); + Assert.AreEqual(handle1, success!.Profiles[0]!.Handle.ToString()); + Assert.AreEqual(handle2, success!.Profiles[1]!.Handle.ToString()); }, failed => { @@ -136,10 +115,11 @@ public async Task GetProfilesAsyncWithHandlesTest() } [TestMethod] - public async Task GetProfilesAsyncWithDidTest() + [DataRow("did:plc:nrfz3bngz57p7g7yg6pbkyqr", "did:plc:okblbaji7rz243bluudjlgxt")] + public async Task GetProfilesAsyncWithDidTest(string did1, string did2) { - var test1did = ATDid.Create(AuthorizedTests.did); - var test2did = ATDid.Create(AuthorizedTests.did_2); + var test1did = ATDid.Create(did1); + var test2did = ATDid.Create(did2); var result = await AuthorizedTests.proto.Actor.GetProfilesAsync(new[] { test1did, test2did }); result.Switch( success => @@ -154,10 +134,11 @@ public async Task GetProfilesAsyncWithDidTest() } [TestMethod] - public async Task GetPostsAsyncTest() + [DataRow("at://did:plc:okblbaji7rz243bluudjlgxt/app.bsky.feed.post/3knxmjdxlpl2r", "at://did:plc:okblbaji7rz243bluudjlgxt/app.bsky.feed.post/3knxdo7r2cj2m")] + public async Task GetPostsAsyncTest(string quotePost, string quotePost2) { - var postUri = ATUri.Create(AuthorizedTests.quote_post); - var postUri2 = ATUri.Create(AuthorizedTests.quote_post_2); + var postUri = ATUri.Create(quotePost); + var postUri2 = ATUri.Create(quotePost2); var postThreadResult = await AuthorizedTests.proto.Feed.GetPostsAsync(new[] { postUri, postUri2 }); postThreadResult.Switch( success => @@ -172,9 +153,10 @@ public async Task GetPostsAsyncTest() } [TestMethod] - public async Task GetPostThreadAsyncTest() + [DataRow("at://did:plc:okblbaji7rz243bluudjlgxt/app.bsky.feed.post/3l5bialwzz52f")] + public async Task GetPostThreadAsyncTest(string postThread) { - var postUri = ATUri.Create(AuthorizedTests.post_thread); + var postUri = ATUri.Create(postThread); var postThreadResult = await AuthorizedTests.proto.Feed.GetPostThreadAsync(postUri); postThreadResult.Switch( success => @@ -182,21 +164,22 @@ public async Task GetPostThreadAsyncTest() Assert.AreEqual(postUri.ToString(), success!.Thread.Post!.Uri.ToString()); }, failed => - { - Assert.Fail($"{failed.StatusCode}: {failed.Detail}"); - }); + { + Assert.Fail($"{failed.StatusCode}: {failed.Detail}"); + }); } [TestMethod] - public async Task GetQuotePostThreadAsyncTest() + [DataRow("", "")] + public async Task GetQuotePostThreadAsyncTest(string quotePost, string quotePost2) { - var postUri = ATUri.Create(AuthorizedTests.quote_post); + var postUri = ATUri.Create(quotePost); var postThreadResult = await AuthorizedTests.proto.Feed.GetPostThreadAsync(postUri); postThreadResult.Switch( success => { Assert.AreEqual(postUri.ToString(), success!.Thread.Post!.Uri.ToString()); - Assert.AreEqual(AuthorizedTests.quote_post_2, ((RecordViewEmbed)success!.Thread!.Post!.Embed!)!.Post.Uri.ToString()); + Assert.AreEqual(quotePost2, ((RecordViewEmbed)success!.Thread!.Post!.Embed!)!.Post.Uri.ToString()); }, failed => { @@ -205,9 +188,10 @@ public async Task GetQuotePostThreadAsyncTest() } [TestMethod] - public async Task GetExternalPostThreadAsyncTest() + [DataRow("")] + public async Task GetExternalPostThreadAsyncTest(string externalPost) { - var postUri = ATUri.Create(AuthorizedTests.external_post); + var postUri = ATUri.Create(externalPost); var postThreadResult = await AuthorizedTests.proto.Feed.GetPostThreadAsync(postUri); postThreadResult.Switch( success => @@ -221,9 +205,10 @@ public async Task GetExternalPostThreadAsyncTest() } [TestMethod] - public async Task GetImagesPostThreadAsyncTest() + [DataRow("at://did:plc:okblbaji7rz243bluudjlgxt/app.bsky.feed.post/3l46tcntvgy2a")] + public async Task GetImagesPostThreadAsyncTest(string imagesPost) { - var postUri = ATUri.Create(AuthorizedTests.images_post); + var postUri = ATUri.Create(imagesPost); var postThreadResult = await AuthorizedTests.proto.Feed.GetPostThreadAsync(postUri); postThreadResult.Switch( success => @@ -237,9 +222,10 @@ public async Task GetImagesPostThreadAsyncTest() } [TestMethod] - public async Task GetRecordWithMediaPostThreadAsyncTest() + [DataRow("at://did:plc:okblbaji7rz243bluudjlgxt/app.bsky.feed.post/3l46xtosyvf2y")] + public async Task GetRecordWithMediaPostThreadAsyncTest(string mediaPost) { - var postUri = ATUri.Create(AuthorizedTests.media_post); + var postUri = ATUri.Create(mediaPost); var postThreadResult = await AuthorizedTests.proto.Feed.GetPostThreadAsync(postUri); postThreadResult.Switch( success => @@ -253,9 +239,10 @@ public async Task GetRecordWithMediaPostThreadAsyncTest() } [TestMethod] - public async Task GetRepliesPostThreadAsyncTest() + [DataRow("")] + public async Task GetRepliesPostThreadAsyncTest(string postThread) { - var postUri = ATUri.Create(AuthorizedTests.post_thread); + var postUri = ATUri.Create(postThread); var postThreadResult = await AuthorizedTests.proto.Feed.GetPostThreadAsync(postUri); postThreadResult.Switch( success => @@ -360,7 +347,8 @@ public async Task CreatePostWithTagAsyncTest() } [TestMethod] - public async Task CreateAndRemoveListTest() + [DataRow("did:plc:nrfz3bngz57p7g7yg6pbkyqr")] + public async Task CreateAndRemoveListTest(string followDid) { var randomName = Guid.NewGuid().ToString(); var createList = (await AuthorizedTests.proto.Repo.CreateCurateListAsync(randomName, "Test List", DateTime.UtcNow)).HandleResult(); @@ -372,7 +360,7 @@ public async Task CreateAndRemoveListTest() Assert.IsTrue(lists is not null); Assert.IsTrue(lists!.Lists.Count() > 0); - var follow1 = ATDid.Create(AuthorizedTests.follow_did); + var follow1 = ATDid.Create(followDid); var follow = (await AuthorizedTests.proto.Repo.CreateListItemAsync(follow1, createList.Uri)).HandleResult(); Assert.IsTrue(follow!.Cid is not null); Assert.IsTrue(follow!.Uri is not null); @@ -441,9 +429,10 @@ public async Task CreateAndRemoveLikeTest() } [TestMethod] - public async Task CreateAndRemoveFollowTest() + [DataRow("did:plc:nrfz3bngz57p7g7yg6pbkyqr")] + public async Task CreateAndRemoveFollowTest(string followDid) { - var follow1 = ATDid.Create(AuthorizedTests.follow_did); + var follow1 = ATDid.Create(followDid); var follow = (await AuthorizedTests.proto.Repo.CreateFollowAsync(follow1)).HandleResult(); Assert.IsTrue(follow!.Cid is not null); Assert.IsTrue(follow!.Uri is not null); @@ -453,9 +442,10 @@ public async Task CreateAndRemoveFollowTest() } [TestMethod] - public async Task CreateAndRemoveBlockTest() + [DataRow("did:plc:nrfz3bngz57p7g7yg6pbkyqr")] + public async Task CreateAndRemoveBlockTest(string blockDid) { - var follow2 = ATDid.Create(AuthorizedTests.block_did); + var follow2 = ATDid.Create(blockDid); var follow = (await AuthorizedTests.proto.Repo.CreateBlockAsync(follow2)).HandleResult(); Assert.IsTrue(follow!.Cid is not null); Assert.IsTrue(follow!.Uri is not null); @@ -488,4 +478,4 @@ public async Task CreatePostWithThreadGate() Assert.IsNotNull(threadGate.Uri); Assert.IsNotNull(threadGate.Cid); } -} +} \ No newline at end of file diff --git a/src/FishyFlip.Auth.Tests/FishyFlip.Auth.Tests.csproj b/src/FishyFlip.Auth.Tests/FishyFlip.Auth.Tests.csproj new file mode 100644 index 00000000..6c6a77bc --- /dev/null +++ b/src/FishyFlip.Auth.Tests/FishyFlip.Auth.Tests.csproj @@ -0,0 +1,24 @@ + + + + net8.0 + enable + enable + false + true + true + + + + $(MSBuildProjectDirectory)\fishyflip.runsettings + + + + + + + + + + + diff --git a/src/FishyFlip.Auth.Tests/GlobalUsings.cs b/src/FishyFlip.Auth.Tests/GlobalUsings.cs new file mode 100644 index 00000000..d6b21ee2 --- /dev/null +++ b/src/FishyFlip.Auth.Tests/GlobalUsings.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) Drastic Actions. All rights reserved. +// + +global using System.IdentityModel.Tokens.Jwt; +global using System.Net.Http.Headers; +global using System.Net.WebSockets; +global using System.Text; +global using System.Text.Encodings; +global using System.Text.Json; +global using System.Text.Json.Serialization; +global using System.Text.RegularExpressions; +global using System.Timers; +global using FishyFlip.Events; +global using FishyFlip.Models; +global using FishyFlip.Models.Internal; +global using FishyFlip.Tools; +global using FishyFlip.Tools.Cbor; +global using FishyFlip.Tools.Json; +global using Ipfs; +global using Microsoft.Extensions.Logging; +global using Microsoft.IdentityModel.Tokens; +global using Microsoft.Testing; +global using Microsoft.VisualStudio.TestTools.UnitTesting; +global using PeterO.Cbor; diff --git a/src/FishyFlip.Auth.Tests/README.md b/src/FishyFlip.Auth.Tests/README.md new file mode 100644 index 00000000..11300bfe --- /dev/null +++ b/src/FishyFlip.Auth.Tests/README.md @@ -0,0 +1,3 @@ +# FishyFlip.Tests + +To run the tests, create a copy of `fishyflip.runsettings.sample` and rename it `fishyflip.runsettings`. Then, fill in the handle name and password fields. \ No newline at end of file diff --git a/src/FishyFlip.Auth.Tests/Samples.cs b/src/FishyFlip.Auth.Tests/Samples.cs new file mode 100644 index 00000000..8d0f122b --- /dev/null +++ b/src/FishyFlip.Auth.Tests/Samples.cs @@ -0,0 +1,10 @@ +// +// Copyright (c) Drastic Actions. All rights reserved. +// + +namespace FishyFlip.Tests; + +internal static class Samples +{ + public static string Base64Image => @"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"; +} diff --git a/src/FishyFlip.Auth.Tests/fishyflip.runsettings.sample b/src/FishyFlip.Auth.Tests/fishyflip.runsettings.sample new file mode 100644 index 00000000..403fc127 --- /dev/null +++ b/src/FishyFlip.Auth.Tests/fishyflip.runsettings.sample @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/FishyFlip.Tests/AnonymousTests.cs b/src/FishyFlip.Tests/AnonymousTests.cs index 7727de40..5d97913e 100644 --- a/src/FishyFlip.Tests/AnonymousTests.cs +++ b/src/FishyFlip.Tests/AnonymousTests.cs @@ -3,6 +3,7 @@ // using FishyFlip.Models; +using FishyFlip.Tools; using Microsoft.Extensions.Logging.Debug; using Microsoft.VisualStudio.TestTools.UnitTesting; using static FishyFlip.Tools.CarDecoder; @@ -13,27 +14,11 @@ namespace FishyFlip.Tests; public class AnonymousTests { static ATProtocol proto; - static string handle; - static string did; - static string media_post; - static string images_post; - static string external_post; - static string quote_post; - static string quote_post_2; - static string post_thread; [ClassInitialize] public static void ClassInitialize(TestContext context) { - did = (string?)context.Properties["BLUESKY_TEST_DID"] ?? throw new ArgumentNullException(); - images_post = (string?)context.Properties["BLUESKY_TEST_IMAGES_POST"] ?? throw new ArgumentNullException(); - media_post = (string?)context.Properties["BLUESKY_TEST_MEDIA_POST"] ?? throw new ArgumentNullException(); - images_post = (string?)context.Properties["BLUESKY_TEST_IMAGES_POST"] ?? throw new ArgumentNullException(); - external_post = (string?)context.Properties["BLUESKY_TEST_EXTERNAL_POST"] ?? throw new ArgumentNullException(); - post_thread = (string?)context.Properties["BLUESKY_TEST_POST_THREAD"] ?? throw new ArgumentNullException(); - quote_post = (string?)context.Properties["BLUESKY_TEST_QUOTE_POST"] ?? throw new ArgumentNullException(); - quote_post_2 = (string?)context.Properties["BLUESKY_TEST_QUOTE_POST_2"] ?? throw new ArgumentNullException(); - string instance = (string?)context.Properties["BLUESKY_INSTANCE_URL"] ?? throw new ArgumentNullException(); + string instance = "https://bsky.social"; var debugLog = new DebugLoggerProvider(); var atProtocolBuilder = new ATProtocolBuilder() .EnableAutoRenewSession(false) @@ -43,50 +28,78 @@ public static void ClassInitialize(TestContext context) } [TestMethod] - public async Task GetPostRecordTest() + [DataRow("at://did:plc:okblbaji7rz243bluudjlgxt/app.bsky.feed.post/3kv25q4gqbk2y", "")] + [DataRow("at://did:plc:okblbaji7rz243bluudjlgxt/app.bsky.feed.post/3knxcq7bwwo2j", Constants.EmbedTypes.Record)] + [DataRow("at://did:plc:okblbaji7rz243bluudjlgxt/app.bsky.feed.post/3l46sr63j7r2m", Constants.EmbedTypes.External)] + [DataRow("at://did:plc:okblbaji7rz243bluudjlgxt/app.bsky.feed.post/3kv25q57gcs2k", Constants.EmbedTypes.Images)] + [DataRow("at://did:plc:okblbaji7rz243bluudjlgxt/app.bsky.feed.post/3l46xtosyvf2y", Constants.EmbedTypes.Video)] + public async Task TestPostAsync(string atUri, string embedType) { - var postUri = ATUri.Create(post_thread); + var postUri = ATUri.Create(atUri); var post = await AnonymousTests.proto.Repo.GetPostAsync(postUri.Did!, postUri.Rkey); post.Switch( success => { Assert.AreEqual(postUri.ToString(), success!.Uri!.ToString()); - }, - failed => - { - Assert.Fail($"{failed.StatusCode}: {failed.Detail}"); - }); - } + Assert.IsNotNull(success.Value); + Assert.AreEqual(success.Value.Type, Constants.FeedType.Post); - [TestMethod] - public async Task GetQuotePostRecordTest() - { - var postUri = ATUri.Create(quote_post); - var post = await AnonymousTests.proto.Repo.GetPostAsync(postUri.Did!, postUri.Rkey); - post.Switch( - success => - { - Assert.AreEqual(postUri.ToString().ToString(), success!.Uri!.ToString()); - Assert.IsTrue(success.Value?.Embed is not null); - Assert.AreEqual(Constants.EmbedTypes.Record, success.Value?.Embed?.Type); - }, - failed => - { - Assert.Fail($"{failed.StatusCode}: {failed.Detail}"); - }); - } + if (!string.IsNullOrEmpty(embedType)) + { + Assert.IsNotNull(success.Value.Embed); + Assert.AreEqual(success.Value.Embed.Type, embedType); + switch (success.Value.Embed.Type) + { + case Constants.EmbedTypes.Record: + var recordEmbed = (RecordEmbed)success.Value.Embed; + Assert.IsNotNull(recordEmbed); + Assert.IsNotNull(recordEmbed.Record); + Assert.IsNotNull(recordEmbed.Record.Cid); + Assert.IsNotNull(recordEmbed.Record.Uri); + break; + case Constants.EmbedTypes.External: + var externalEmbed = (ExternalEmbed)success.Value.Embed; + Assert.IsNotNull(externalEmbed); + Assert.IsNotNull(externalEmbed.External); + var external = externalEmbed.External; + Assert.IsTrue(!string.IsNullOrEmpty(external.Description)); + Assert.IsTrue(!string.IsNullOrEmpty(external.Title)); + Assert.IsTrue(!string.IsNullOrEmpty(external.Uri)); + Assert.IsNotNull(external.Thumb); + Assert.IsTrue(!string.IsNullOrEmpty(external.Thumb.MimeType)); + Assert.IsTrue(!string.IsNullOrEmpty(external.Thumb.Type)); + Assert.IsNotNull(external.Thumb.Ref); + Assert.IsNotNull(external.Thumb.Ref.Link); + break; + case Constants.EmbedTypes.Images: + var imagesEmbed = (ImagesEmbed)success.Value.Embed; + Assert.IsNotNull(imagesEmbed); + Assert.IsNotNull(imagesEmbed.Images); + foreach (var image in imagesEmbed.Images) + { + Assert.IsNotNull(image); + image.Image.ThrowIfNull(); + image.Image?.Ref.ThrowIfNull(); + Assert.IsTrue(!string.IsNullOrEmpty(image.Image?.MimeType)); + } - [TestMethod] - public async Task GetExternalPostRecordTest() - { - var postUri = ATUri.Create(external_post); - var post = await AnonymousTests.proto.Repo.GetPostAsync(postUri.Did!, postUri.Rkey); - post.Switch( - success => - { - Assert.AreEqual(postUri.ToString(), success!.Uri!.ToString()); - Assert.IsTrue(success.Value?.Embed is not null); - Assert.AreEqual(Constants.EmbedTypes.External, success.Value?.Embed?.Type); + break; + case Constants.EmbedTypes.Video: + var videoEmbed = (VideoEmbed)success.Value.Embed; + Assert.IsNotNull(videoEmbed); + Assert.IsNotNull(videoEmbed.Video); + videoEmbed.Video?.Ref.ThrowIfNull(); + Assert.IsTrue(!string.IsNullOrEmpty(videoEmbed.Video?.MimeType)); + Assert.IsTrue(!string.IsNullOrEmpty(videoEmbed.Video?.Type)); + Assert.IsNotNull(videoEmbed.AspectRatio); + Assert.IsTrue(videoEmbed.AspectRatio.Width > 0); + Assert.IsTrue(videoEmbed.AspectRatio.Height > 0); + break; + default: + Assert.Fail("Type not listed for test."); + break; + } + } }, failed => { @@ -95,43 +108,8 @@ public async Task GetExternalPostRecordTest() } [TestMethod] - public async Task GetImagesPostRecordTest() - { - var postUri = ATUri.Create(images_post); - var post = await AnonymousTests.proto.Repo.GetPostAsync(postUri.Did!, postUri.Rkey); - post.Switch( - success => - { - Assert.IsTrue(success.Value?.Embed is not null); - Assert.AreEqual(Constants.EmbedTypes.Images, success.Value?.Embed.Type); - }, - failed => - { - Assert.Fail($"{failed.StatusCode}: {failed.Detail}"); - }); - } - - [TestMethod] - [Ignore("Can't create MediaWithPostRecord. Not sure if that's broken in Bluesky.")] - public async Task GetRecordWithMediaPostRecordTest() - { - var postUri = ATUri.Create(media_post); - var post = await AnonymousTests.proto.Repo.GetPostAsync(postUri.Did!, postUri.Rkey); - post.Switch( - success => - { - Assert.AreEqual(postUri.ToString(), success!.Uri!.ToString()); - Assert.IsTrue(success.Value?.Embed is not null); - Assert.AreEqual(Constants.EmbedTypes.RecordWithMedia, success.Value?.Embed.Type); - }, - failed => - { - Assert.Fail($"{failed.StatusCode}: {failed.Detail}"); - }); - } - - [TestMethod] - public async Task DescribeRepoTest() + [DataRow("did:plc:okblbaji7rz243bluudjlgxt")] + public async Task DescribeRepoTest(string did) { var repo = ATDid.Create(did); var describe = (await AnonymousTests.proto.Repo.DescribeRepoAsync(repo)).HandleResult(); @@ -140,11 +118,4 @@ public async Task DescribeRepoTest() Assert.IsTrue(describe.Did is not null); Assert.AreEqual(describe.Did!.ToString(), repo.ToString()); } - - private static void HandleProgressStatus(CarProgressStatusEvent e) - { - var cid = e.Cid; - var bytes = e.Bytes; - var test = CBORObject.DecodeFromBytes(bytes); - } } \ No newline at end of file diff --git a/src/FishyFlip.Tests/FishyFlip.Tests.csproj b/src/FishyFlip.Tests/FishyFlip.Tests.csproj index 6c6a77bc..4006061f 100644 --- a/src/FishyFlip.Tests/FishyFlip.Tests.csproj +++ b/src/FishyFlip.Tests/FishyFlip.Tests.csproj @@ -9,10 +9,6 @@ true - - $(MSBuildProjectDirectory)\fishyflip.runsettings - - diff --git a/src/FishyFlip.Tests/README.md b/src/FishyFlip.Tests/README.md new file mode 100644 index 00000000..11300bfe --- /dev/null +++ b/src/FishyFlip.Tests/README.md @@ -0,0 +1,3 @@ +# FishyFlip.Tests + +To run the tests, create a copy of `fishyflip.runsettings.sample` and rename it `fishyflip.runsettings`. Then, fill in the handle name and password fields. \ No newline at end of file diff --git a/src/FishyFlip.Tests/fishyflip.runsettings.sample b/src/FishyFlip.Tests/fishyflip.runsettings.sample deleted file mode 100644 index dbad3cb3..00000000 --- a/src/FishyFlip.Tests/fishyflip.runsettings.sample +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/FishyFlip.sln b/src/FishyFlip.sln index f07a828b..43663599 100644 --- a/src/FishyFlip.sln +++ b/src/FishyFlip.sln @@ -11,6 +11,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WhiteWindLib", "WhiteWindLi EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WhiteWindLib.Tests", "WhiteWindLib.Tests\WhiteWindLib.Tests.csproj", "{2E024F2D-D298-4F7C-9644-63C417824128}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FishyFlip.Auth.Tests", "FishyFlip.Auth.Tests\FishyFlip.Auth.Tests.csproj", "{262AB344-8C76-4243-ADBA-01A478A1C10C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WhiteWindLib.Auth.Tests", "WhiteWindLib.Auth.Tests\WhiteWindLib.Auth.Tests.csproj", "{39CDE0DA-3AA1-4713-9B0C-C7EF61534243}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -33,6 +37,14 @@ Global {2E024F2D-D298-4F7C-9644-63C417824128}.Debug|Any CPU.Build.0 = Debug|Any CPU {2E024F2D-D298-4F7C-9644-63C417824128}.Release|Any CPU.ActiveCfg = Release|Any CPU {2E024F2D-D298-4F7C-9644-63C417824128}.Release|Any CPU.Build.0 = Release|Any CPU + {262AB344-8C76-4243-ADBA-01A478A1C10C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {262AB344-8C76-4243-ADBA-01A478A1C10C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {262AB344-8C76-4243-ADBA-01A478A1C10C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {262AB344-8C76-4243-ADBA-01A478A1C10C}.Release|Any CPU.Build.0 = Release|Any CPU + {39CDE0DA-3AA1-4713-9B0C-C7EF61534243}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {39CDE0DA-3AA1-4713-9B0C-C7EF61534243}.Debug|Any CPU.Build.0 = Debug|Any CPU + {39CDE0DA-3AA1-4713-9B0C-C7EF61534243}.Release|Any CPU.ActiveCfg = Release|Any CPU + {39CDE0DA-3AA1-4713-9B0C-C7EF61534243}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/FishyFlip/ATJetStream.cs b/src/FishyFlip/ATJetStream.cs new file mode 100644 index 00000000..249ce669 --- /dev/null +++ b/src/FishyFlip/ATJetStream.cs @@ -0,0 +1,251 @@ +// +// Copyright (c) Drastic Actions. All rights reserved. +// + +namespace FishyFlip; + +/// +/// AT JetStream. +/// +public sealed class ATJetStream : IDisposable +{ + private const int ReceiveBufferSize = 32768; + private readonly JsonSerializerOptions jsonSerializerOptions; + private readonly SourceGenerationContext sourceGenerationContext; + private ClientWebSocket client; + private bool disposedValue; + private ILogger? logger; + private Uri instanceUri; + + /// + /// Initializes a new instance of the class. + /// + /// . + public ATJetStream(ATJetStreamOptions options) + { + this.logger = options.Logger; + this.instanceUri = options.Url; + this.client = new ClientWebSocket(); + this.jsonSerializerOptions = new JsonSerializerOptions() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull | JsonIgnoreCondition.WhenWritingDefault, + Converters = + { + new AtUriJsonConverter(), + new AtHandlerJsonConverter(), + new AtDidJsonConverter(), + new EmbedConverter(), + new ATRecordJsonConverter(), + new ATCidConverter(), + new ATWebSocketCommitTypeConverter(), + new ATWebSocketEventConverter(), + }, + }; + this.sourceGenerationContext = new SourceGenerationContext(this.jsonSerializerOptions); + } + + /// + /// On Connection Updated. + /// + public event EventHandler? OnConnectionUpdated; + + /// + /// On Raw Message Received. + /// + public event EventHandler? OnRawMessageReceived; + + /// + /// On AT WebSocket Record Received. + /// + public event EventHandler? OnRecordReceived; + + /// + void IDisposable.Dispose() + { + this.Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + /// + /// Connect to the JetStream instance via a WebSocket connection. + /// + /// List of collection namespaces (ex. app.bsky.feed.post) you want to receive. Defaults to all. + /// List of User ATDids to filter for. Defaults to All Repos. + /// A unix microseconds timestamp cursor to begin playback from. Set the value from a previous value to start stream from this point. Defaults to live tail. + /// CancellationToken. + /// A representing the asynchronous operation. + public Task ConnectAsync(string[]? wantedCollections = default, string[]? wantedDids = default, long cursor = 0, CancellationToken? token = default) + { + var subscribe = "/subscribe?"; + if (wantedCollections is not null && wantedCollections.Length > 0) + { + foreach (var collection in wantedCollections) + { + subscribe += $"wantedCollections={collection}&"; + } + } + + if (wantedDids is not null && wantedDids.Length > 0) + { + foreach (var did in wantedDids) + { + subscribe += $"wantedDids={did}&"; + } + } + + if (cursor > 0) + { + subscribe += $"cursor={cursor}&"; + } + + if (subscribe.EndsWith("&")) + { + subscribe = subscribe.Substring(0, subscribe.Length - 1); + } + + this.logger?.LogInformation($"WSS: Connecting to {this.instanceUri}{subscribe}"); + + return this.ConnectAsync(subscribe, token); + } + + /// + /// Connect to the JetStream instance via a WebSocket connection. + /// + /// Connection string. + /// CancellationToken. + /// A representing the asynchronous operation. + public async Task ConnectAsync(string connection, CancellationToken? token = default) + { + if (this.client.State == WebSocketState.Open) + { + return; + } + + if (this.client.State == WebSocketState.Aborted || this.client.State == WebSocketState.Closed) + { + this.client = new ClientWebSocket(); + } + + var endToken = token ?? CancellationToken.None; + await this.client.ConnectAsync(new Uri($"wss://{this.instanceUri.Host}{connection}"), endToken); + this.logger?.LogInformation($"WSS: Connected to {this.instanceUri}"); + this.ReceiveMessages(this.client, endToken).FireAndForgetSafeAsync(this.logger); + this.OnConnectionUpdated?.Invoke(this, new SubscriptionConnectionStatusEventArgs(this.client.State)); + } + + /// + /// Close the existing WebSocket connection. + /// + /// Status for the shutdown. Defaults to . + /// Reason for the shutdown. + /// CancellationToken. + /// A representing the asynchronous operation. + public async Task CloseAsync(WebSocketCloseStatus status = WebSocketCloseStatus.NormalClosure, string disconnectReason = "Client disconnecting", CancellationToken? token = default) + { + var endToken = token ?? CancellationToken.None; + this.logger?.LogInformation($"WSS: Disconnecting"); + try + { + await this.client.CloseAsync(status, disconnectReason, endToken); + } + catch (Exception ex) + { + this.logger?.LogError(ex, "Failed to Close WebSocket connection."); + } + + this.OnConnectionUpdated?.Invoke(this, new SubscriptionConnectionStatusEventArgs(this.client.State)); + } + + /// + /// Dispose. + /// + public void Dispose() + { + this.Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + private async Task ReceiveMessages(ClientWebSocket webSocket, CancellationToken token) + { + byte[] receiveBuffer = new byte[ReceiveBufferSize]; + while (webSocket.State == WebSocketState.Open) + { + try + { +#if NETSTANDARD + var result = + await webSocket.ReceiveAsync(new ArraySegment(receiveBuffer), token); + if (result is not { MessageType: WebSocketMessageType.Text, EndOfMessage: true }) + { + continue; + } + + byte[] newArray = new byte[result.Count]; + Array.Copy(receiveBuffer, 0, newArray, 0, result.Count); +#else + var result = + await webSocket.ReceiveAsync(new Memory(receiveBuffer), token); + if (result is not { MessageType: WebSocketMessageType.Text, EndOfMessage: true }) + { + continue; + } + + // Convert result to string + byte[] newArray = new byte[result.Count]; + Array.Copy(receiveBuffer, 0, newArray, 0, result.Count); +#endif + var message = Encoding.UTF8.GetString(newArray); + this.OnRawMessageReceived?.Invoke(this, new JetStreamRawMessageEventArgs(message)); + Task.Run(() => this.HandleMessage(message)).FireAndForgetSafeAsync(this.logger); + } + catch (OperationCanceledException) + { + this.logger?.LogDebug("WSS: Operation Canceled."); + } + catch (Exception e) + { + this.logger?.LogError(e, "WSS: ATError receiving message."); + } + } + + this.OnConnectionUpdated?.Invoke(this, new SubscriptionConnectionStatusEventArgs(webSocket.State)); + } + + private void HandleMessage(string json) + { + if (string.IsNullOrEmpty(json)) + { + this.logger?.LogDebug("WSS: Empty message received."); + return; + } + + var atWebSocketRecord = JsonSerializer.Deserialize(json, this.sourceGenerationContext.ATWebSocketRecord); + if (atWebSocketRecord is null) + { + this.logger?.LogError("WSS: Failed to deserialize ATWebSocketRecord."); + this.logger?.LogError(json); + return; + } + + this.OnRecordReceived?.Invoke(this, new JetStreamATWebSocketRecordEventArgs(atWebSocketRecord)); + } + + /// + /// Dispose. + /// + /// Is Disposing. + private void Dispose(bool disposing) + { + if (!this.disposedValue) + { + if (disposing) + { + this.client.Dispose(); + } + + this.disposedValue = true; + } + } +} \ No newline at end of file diff --git a/src/FishyFlip/ATJetStreamBuilder.cs b/src/FishyFlip/ATJetStreamBuilder.cs new file mode 100644 index 00000000..ad3955e2 --- /dev/null +++ b/src/FishyFlip/ATJetStreamBuilder.cs @@ -0,0 +1,72 @@ +// +// Copyright (c) Drastic Actions. All rights reserved. +// + +namespace FishyFlip; + +/// +/// AT JetStream Builder. +/// +public class ATJetStreamBuilder +{ + private readonly ATJetStreamOptions atProtocolOptions; + + /// + /// Initializes a new instance of the class. + /// + public ATJetStreamBuilder() + { + this.atProtocolOptions = new ATJetStreamOptions(); + } + + /// + /// Initializes a new instance of the class. + /// + /// ATJetStreamOptions. + public ATJetStreamBuilder(ATJetStreamOptions options) + { + this.atProtocolOptions = options; + } + + /// + /// Set the instance url to connect to. + /// + /// Instance Url. + /// . + public ATJetStreamBuilder WithInstanceUrl(Uri url) + { + this.atProtocolOptions.Url = url; + return this; + } + + /// + /// Adds a logger. + /// + /// Logger. + /// . + public ATJetStreamBuilder WithLogger(ILogger? logger) + { + this.atProtocolOptions.Logger = logger; + return this; + } + + /// + /// Returns the ATWebSocketProtocolOptions. + /// + /// ATJetStreamBuilder. + public ATJetStreamOptions BuildOptions() + { + return this.atProtocolOptions; + } + + /// + /// Builds the Protocol. + /// + /// The build with these configs. + public ATJetStream Build() + { + var options = this.BuildOptions(); + + return new ATJetStream(options); + } +} \ No newline at end of file diff --git a/src/FishyFlip/ATJetStreamOptions.cs b/src/FishyFlip/ATJetStreamOptions.cs new file mode 100644 index 00000000..a14288cc --- /dev/null +++ b/src/FishyFlip/ATJetStreamOptions.cs @@ -0,0 +1,29 @@ +// +// Copyright (c) Drastic Actions. All rights reserved. +// + +namespace FishyFlip; + +/// +/// AT JetStream Options. +/// +public class ATJetStreamOptions +{ + /// + /// Initializes a new instance of the class. + /// + public ATJetStreamOptions() + { + this.Url = new Uri("https://jetstream.atproto.tools"); + } + + /// + /// Gets the instance Url. + /// + public Uri Url { get; internal set; } + + /// + /// Gets the logger. + /// + public ILogger? Logger { get; internal set; } +} \ No newline at end of file diff --git a/src/FishyFlip/ATProtoServer.cs b/src/FishyFlip/ATProtoServer.cs index 87ba7bbb..7f240cbb 100644 --- a/src/FishyFlip/ATProtoServer.cs +++ b/src/FishyFlip/ATProtoServer.cs @@ -31,6 +31,7 @@ internal ATProtoServer(ATProtocol proto) /// The password of the user. /// Optional. A CancellationToken that can be used to cancel the operation. /// A Task that represents the asynchronous operation. The task result contains a Result object with the session details, or null if the session could not be created. + [Obsolete("No longer automatically authenticates password sessions. To authenticate with a password and create a session, use ATProtocol.AuthenticateWithPasswordAsync")] public async Task> CreateSessionAsync(string identifier, string password, CancellationToken cancellationToken = default) { Result result = diff --git a/src/FishyFlip/ATProtocol.cs b/src/FishyFlip/ATProtocol.cs index d2097cf2..850848bf 100644 --- a/src/FishyFlip/ATProtocol.cs +++ b/src/FishyFlip/ATProtocol.cs @@ -129,6 +129,21 @@ public ATProtocol(ATProtocolOptions options) /// public AuthSession? OAuthSession => this.sessionManager is OAuth2SessionManager oAuth2SessionManager ? oAuth2SessionManager.OAuthSession : null; + /// + /// Gets the current PasswordSession session, if any is active. + /// + public AuthSession? PasswordSession => this.sessionManager is PasswordSessionManager passwordSessionManager ? passwordSessionManager.PasswordSession : null; + + /// + /// Gets the current AuthSession. + /// + public AuthSession? AuthSession => this.sessionManager switch + { + PasswordSessionManager passwordSessionManager => passwordSessionManager.PasswordSession, + OAuth2SessionManager oAuth2SessionManager => oAuth2SessionManager.OAuthSession, + _ => null, + }; + /// /// Gets or sets the internal session manager. /// diff --git a/src/FishyFlip/ATProtocolOptions.cs b/src/FishyFlip/ATProtocolOptions.cs index e92120cf..4688ca4f 100644 --- a/src/FishyFlip/ATProtocolOptions.cs +++ b/src/FishyFlip/ATProtocolOptions.cs @@ -2,6 +2,8 @@ // Copyright (c) Drastic Actions. All rights reserved. // +using System.Net; + namespace FishyFlip; /// @@ -86,7 +88,7 @@ public ATProtocolOptions(IReadOnlyList? customEmbedConver /// . internal HttpClient GenerateHttpClient(HttpMessageHandler? handler = default) { - var httpClient = new HttpClient(handler ?? new HttpClientHandler { MaxRequestContentBufferSize = int.MaxValue }); + var httpClient = new HttpClient(handler ?? new HttpClientHandler { MaxRequestContentBufferSize = int.MaxValue, AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, }); httpClient.DefaultRequestHeaders.Add(Constants.HeaderNames.UserAgent, this.UserAgent); httpClient.DefaultRequestHeaders.Add("Accept", Constants.AcceptedMediaType); httpClient.BaseAddress = this.Url; diff --git a/src/FishyFlip/BlueskyFeed.cs b/src/FishyFlip/BlueskyFeed.cs index a65a91b1..b1909535 100644 --- a/src/FishyFlip/BlueskyFeed.cs +++ b/src/FishyFlip/BlueskyFeed.cs @@ -28,20 +28,27 @@ internal BlueskyFeed(ATProtocol proto) /// Asynchronously retrieves the thread of a post. /// /// The URI of the post whose thread is to be retrieved. - /// Optional. The depth of the thread. Default is 0. + /// Optional. The depth of the thread. Default is 6. + /// How many levels of parent (and grandparent, etc) post to include. Default is 80. /// Optional. A CancellationToken that can be used to cancel the operation. /// A Task that represents the asynchronous operation. The task result contains a Result object with the thread of the post, or null if the thread could not be retrieved. public async Task> GetPostThreadAsync( ATUri uri, - int depth = 0, + int depth = 6, + int parentHeight = 80, CancellationToken cancellationToken = default) { string url = $"{Constants.Urls.Bluesky.Feed.GetPostThread}?uri={uri}"; - if (depth > 0) + if (depth != 6) { url += $"&depth={depth}"; } + if (parentHeight != 80) + { + url += $"&parentHeight={parentHeight}"; + } + Multiple result = await this.Client.Get(url, this.Options.SourceGenerationContext.ThreadPostViewFeed, this.Options.JsonSerializerOptions, cancellationToken, this.Options.Logger); return result .Match>( diff --git a/src/FishyFlip/Events/JetStreamATWebSocketRecordEventArgs.cs b/src/FishyFlip/Events/JetStreamATWebSocketRecordEventArgs.cs new file mode 100644 index 00000000..be92d92f --- /dev/null +++ b/src/FishyFlip/Events/JetStreamATWebSocketRecordEventArgs.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) Drastic Actions. All rights reserved. +// + +namespace FishyFlip.Events; + +/// +/// JetStream AT WebSocket Record Event Args. +/// +public class JetStreamATWebSocketRecordEventArgs : EventArgs +{ + /// + /// Initializes a new instance of the class. + /// + /// . + public JetStreamATWebSocketRecordEventArgs(ATWebSocketRecord record) + { + this.Record = record; + } + + /// + /// Gets the AT WebSocket Record. + /// + public ATWebSocketRecord Record { get; } +} \ No newline at end of file diff --git a/src/FishyFlip/Events/JetStreamRawMessageEventArgs.cs b/src/FishyFlip/Events/JetStreamRawMessageEventArgs.cs new file mode 100644 index 00000000..2754049b --- /dev/null +++ b/src/FishyFlip/Events/JetStreamRawMessageEventArgs.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) Drastic Actions. All rights reserved. +// + +namespace FishyFlip.Events; + +/// +/// JetStream Raw Message Event Args. +/// +public class JetStreamRawMessageEventArgs : EventArgs +{ + /// + /// Initializes a new instance of the class. + /// + /// Raw Message JSON. + public JetStreamRawMessageEventArgs(string messageJson) + { + this.MessageJson = messageJson; + } + + /// + /// Gets the Message JSON. + /// + public string MessageJson { get; } +} \ No newline at end of file diff --git a/src/FishyFlip/ISessionManager.cs b/src/FishyFlip/ISessionManager.cs index 99847566..12ed0c66 100644 --- a/src/FishyFlip/ISessionManager.cs +++ b/src/FishyFlip/ISessionManager.cs @@ -39,10 +39,4 @@ internal interface ISessionManager : IDisposable /// Cancellation Token. /// Task. public Task RefreshSessionAsync(CancellationToken cancellationToken = default); - - /// - /// Set the current session. - /// - /// Session. - public void SetSession(Session session); } diff --git a/src/FishyFlip/Models/ATWebSocketCommit.cs b/src/FishyFlip/Models/ATWebSocketCommit.cs new file mode 100644 index 00000000..ef20cd2c --- /dev/null +++ b/src/FishyFlip/Models/ATWebSocketCommit.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) Drastic Actions. All rights reserved. +// + +namespace FishyFlip.Models; + +/// +/// AT WebSocket Commit. +/// +public class ATWebSocketCommit +{ + /// + /// Initializes a new instance of the class. + /// + /// The revision identifier. + /// The type of the WebSocket commit. + /// The collection name. + /// The record key. + /// The record associated with the commit. + /// The CID associated with the commit. + public ATWebSocketCommit(string? rev, ATWebSocketCommitType type, string? collection, string? rKey, ATRecord? record, ATCid? cid) + { + this.Rev = rev; + this.Type = type; + this.Collection = collection; + this.RKey = rKey; + this.Record = record; + this.Cid = cid; + } + + /// + /// Gets the revision identifier. + /// + public string? Rev { get; } + + /// + /// Gets the type of the WebSocket commit. + /// + public ATWebSocketCommitType Type { get; } + + /// + /// Gets the collection name. + /// + public string? Collection { get; } + + /// + /// Gets the record key. + /// + public string? RKey { get; } + + /// + /// Gets the record associated with the commit. + /// + public ATRecord? Record { get; } + + /// + /// Gets the CID associated with the commit. + /// + public ATCid? Cid { get; } +} \ No newline at end of file diff --git a/src/FishyFlip/Models/ATWebSocketCommitType.cs b/src/FishyFlip/Models/ATWebSocketCommitType.cs new file mode 100644 index 00000000..8ed3a8aa --- /dev/null +++ b/src/FishyFlip/Models/ATWebSocketCommitType.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) Drastic Actions. All rights reserved. +// + +namespace FishyFlip.Models; + +/// +/// AT WebSocket Commit Type. +/// +public enum ATWebSocketCommitType +{ + /// + /// Unknown. + /// + Unknown, + + /// + /// Create. + /// + Create, + + /// + /// Update. + /// + Update, + + /// + /// Delete. + /// + Delete, +} \ No newline at end of file diff --git a/src/FishyFlip/Models/ATWebSocketEvent.cs b/src/FishyFlip/Models/ATWebSocketEvent.cs new file mode 100644 index 00000000..c3225d65 --- /dev/null +++ b/src/FishyFlip/Models/ATWebSocketEvent.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) Drastic Actions. All rights reserved. +// + +namespace FishyFlip.Models; + +/// +/// AT WebSocket Event. +/// +public enum ATWebSocketEvent +{ + /// + /// Unknown. + /// + Unknown, + + /// + /// Commit. + /// + Commit, + + /// + /// Account. + /// + Account, + + /// + /// Identity. + /// + Identity, +} \ No newline at end of file diff --git a/src/FishyFlip/Models/ATWebSocketRecord.cs b/src/FishyFlip/Models/ATWebSocketRecord.cs new file mode 100644 index 00000000..db1d51fe --- /dev/null +++ b/src/FishyFlip/Models/ATWebSocketRecord.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) Drastic Actions. All rights reserved. +// + +namespace FishyFlip.Models; + +/// +/// AT WebSocket Record. +/// +public class ATWebSocketRecord +{ + /// + /// Initializes a new instance of the class. + /// + /// Type. + /// Did. + /// Commit. + /// Identity. + /// Account. + [JsonConstructor] + public ATWebSocketRecord(ATWebSocketEvent type, ATDid? did, ATWebSocketCommit? commit, ActorIdentity? identity, ActorAccount? account) + { + this.Type = type; + this.Did = did; + this.Commit = commit; + this.Identity = identity; + this.Account = account; + } + + /// + /// Gets the Type. + /// + public ATWebSocketEvent Type { get; } + + /// + /// Gets the Commit. + /// + public ATWebSocketCommit? Commit { get; } + + /// + /// Gets the Did. + /// + public ATDid? Did { get; } + + /// + /// Gets the Identity. + /// + public ActorIdentity? Identity { get; } + + /// + /// Gets the Account. + /// + public ActorAccount? Account { get; } + + /// + /// Gets or sets the time. + /// + [JsonPropertyName("time_us")] + public long? TimeUs { get; set; } +} \ No newline at end of file diff --git a/src/FishyFlip/Models/ActorAccount.cs b/src/FishyFlip/Models/ActorAccount.cs new file mode 100644 index 00000000..aa803260 --- /dev/null +++ b/src/FishyFlip/Models/ActorAccount.cs @@ -0,0 +1,14 @@ +// +// Copyright (c) Drastic Actions. All rights reserved. +// + +namespace FishyFlip.Models; + +/// +/// Represents an actor account. +/// +/// Indicates whether the actor account is active. +/// The decentralized identifier of the actor. +/// The sequence number associated with the actor. +/// The timestamp associated with the actor. +public record ActorAccount(bool Active, ATDid? Did, double Seq, DateTime? Time); \ No newline at end of file diff --git a/src/FishyFlip/Models/ActorIdentity.cs b/src/FishyFlip/Models/ActorIdentity.cs new file mode 100644 index 00000000..2f7e892f --- /dev/null +++ b/src/FishyFlip/Models/ActorIdentity.cs @@ -0,0 +1,14 @@ +// +// Copyright (c) Drastic Actions. All rights reserved. +// + +namespace FishyFlip.Models; + +/// +/// Represents the identity of an actor. +/// +/// The decentralized identifier of the actor. +/// The handle of the actor. +/// The sequence number associated with the actor. +/// The timestamp associated with the actor. +public record ActorIdentity(ATDid? Did, string? Handle, double Seq, DateTime? Time); \ No newline at end of file diff --git a/src/FishyFlip/Models/RecordWithMediaEmbed.cs b/src/FishyFlip/Models/RecordWithMediaEmbed.cs index ce2cd532..5563931a 100644 --- a/src/FishyFlip/Models/RecordWithMediaEmbed.cs +++ b/src/FishyFlip/Models/RecordWithMediaEmbed.cs @@ -13,12 +13,12 @@ public class RecordWithMediaEmbed : Embed /// Initializes a new instance of the class. /// /// The record to be embedded. Can be null. - /// The images to be embedded. Can be null. + /// The media to be embedded. Can be null. [JsonConstructor] - public RecordWithMediaEmbed(RecordEmbed? record, ImagesEmbed? images) + public RecordWithMediaEmbed(RecordEmbed? record, Embed? embed) { this.Record = record; - this.Images = images; + this.Embed = embed; this.Type = Constants.EmbedTypes.RecordWithMedia; } @@ -35,7 +35,7 @@ public RecordWithMediaEmbed(CBORObject record, CBORObject media) switch (type) { case Constants.EmbedTypes.Images: - this.Images = new ImagesEmbed(media["images"]); + this.Embed = new ImagesEmbed(media["images"]); break; } } @@ -48,5 +48,5 @@ public RecordWithMediaEmbed(CBORObject record, CBORObject media) /// /// Gets the images to be embedded. /// - public ImagesEmbed? Images { get; } + public Embed? Embed { get; } } diff --git a/src/FishyFlip/Models/RecordWithMediaViewEmbed.cs b/src/FishyFlip/Models/RecordWithMediaViewEmbed.cs index 71dbf227..5fdb2397 100644 --- a/src/FishyFlip/Models/RecordWithMediaViewEmbed.cs +++ b/src/FishyFlip/Models/RecordWithMediaViewEmbed.cs @@ -19,12 +19,12 @@ public class RecordWithMediaViewEmbed : Embed /// Initializes a new instance of the class. /// /// The record view embed. - /// The image view embed. + /// The media embed. [JsonConstructor] - public RecordWithMediaViewEmbed(RecordViewEmbed? record, ImageViewEmbed? images) + public RecordWithMediaViewEmbed(RecordViewEmbed? record, Embed? embed) { this.Record = record; - this.Images = images; + this.Embed = embed; this.Type = Constants.EmbedTypes.RecordWithMedia; } @@ -36,5 +36,5 @@ public RecordWithMediaViewEmbed(RecordViewEmbed? record, ImageViewEmbed? images) /// /// Gets the image view embed. /// - public ImageViewEmbed? Images { get; } + public Embed? Embed { get; } } \ No newline at end of file diff --git a/src/FishyFlip/OAuth2SessionManager.cs b/src/FishyFlip/OAuth2SessionManager.cs index af5765ea..932fd210 100644 --- a/src/FishyFlip/OAuth2SessionManager.cs +++ b/src/FishyFlip/OAuth2SessionManager.cs @@ -188,7 +188,18 @@ public Task RefreshSessionAsync(CancellationToken cancellationToken = default) => this.RefreshTokenAsync(cancellationToken); /// - public void SetSession(Session session) + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + this.Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + /// + /// Set the session. + /// + /// Session. + internal void SetSession(Session session) { if (this.protocol.Options.UseServiceEndpointUponLogin) { @@ -218,14 +229,6 @@ public void SetSession(Session session) this.session = session; } - /// - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - this.Dispose(disposing: true); - GC.SuppressFinalize(this); - } - /// /// Refresh Token. /// diff --git a/src/FishyFlip/PasswordSessionManager.cs b/src/FishyFlip/PasswordSessionManager.cs index 05b3bac9..5e40e3c2 100644 --- a/src/FishyFlip/PasswordSessionManager.cs +++ b/src/FishyFlip/PasswordSessionManager.cs @@ -38,6 +38,7 @@ internal class PasswordSessionManager : ISessionManager private System.Timers.Timer? timer; private int refreshing; private ILogger? logger; + private AuthSession? authSession; /// /// Initializes a new instance of the class. @@ -79,6 +80,11 @@ public PasswordSessionManager(ATProtocol protocol, Session session) /// public HttpClient Client => this.client; + /// + /// Gets the password Auth Session. + /// + public AuthSession? PasswordSession => this.authSession; + /// public Task RefreshSessionAsync(CancellationToken cancellationToken = default) => this.RefreshTokenAsync(cancellationToken); @@ -93,11 +99,16 @@ public Task RefreshSessionAsync(CancellationToken cancellationToken = default) /// The password of the user. /// Optional. A CancellationToken that can be used to cancel the operation. /// A Task that represents the asynchronous operation. The task result contains a Result object with the session details, or null if the session could not be created. - public async Task CreateSessionAsync(string identifier, string password, CancellationToken cancellationToken = default) + internal async Task CreateSessionAsync(string identifier, string password, CancellationToken cancellationToken = default) { - var session = (await this.protocol.Server.CreateSessionAsync(identifier, password, cancellationToken)).HandleResult(); - if (session is not null) +#pragma warning disable CS0618 + var sessionResult = await this.protocol.Server.CreateSessionAsync(identifier, password, cancellationToken); +#pragma warning restore CS0618 + Session? resultSession = null; + sessionResult.Switch( + session => { + resultSession = session; if (this.protocol.Options.UseServiceEndpointUponLogin) { var logger = this.protocol.Options.Logger; @@ -124,23 +135,29 @@ public Task RefreshSessionAsync(CancellationToken cancellationToken = default) } this.SetSession(session); - } + }, + e => this.logger?.LogError(e.ToString(), e)); - return session; + return resultSession; } /// /// Sets the given session. /// /// . - public void SetSession(Session session) + internal void SetSession(Session session) { this.session = session; this.UpdateBearerToken(session); this.logger?.LogDebug($"Session set, {session.Did}"); - this.SessionUpdated?.Invoke(this, new SessionUpdatedEventArgs(new AuthSession(session), this.protocol.Options.Url)); + lock (this) + { + this.authSession = new AuthSession(session); + } + + this.SessionUpdated?.Invoke(this, new SessionUpdatedEventArgs(this.authSession, this.protocol.Options.Url)); if (!this.protocol.Options.AutoRenewSession) { diff --git a/src/FishyFlip/SourceGenerationContext.cs b/src/FishyFlip/SourceGenerationContext.cs index 09019460..bf8d838b 100644 --- a/src/FishyFlip/SourceGenerationContext.cs +++ b/src/FishyFlip/SourceGenerationContext.cs @@ -208,6 +208,12 @@ namespace FishyFlip; [JsonSerializable(typeof(Dictionary))] [JsonSerializable(typeof(OidcClientOptions))] [JsonSerializable(typeof(DPoPProofPayload))] +[JsonSerializable(typeof(ATWebSocketEvent))] +[JsonSerializable(typeof(ATWebSocketCommit))] +[JsonSerializable(typeof(ATWebSocketCommitType))] +[JsonSerializable(typeof(ATWebSocketRecord))] +[JsonSerializable(typeof(ActorRecord))] +[JsonSerializable(typeof(ActorIdentity))] [JsonSerializable(typeof(Microsoft.IdentityModel.Tokens.JsonWebKey), TypeInfoPropertyName = nameof(Microsoft.IdentityModel.Tokens.JsonWebKey) + "_A")] internal partial class SourceGenerationContext : JsonSerializerContext { diff --git a/src/FishyFlip/Tools/Json/ATWebSocketCommitTypeConverter.cs b/src/FishyFlip/Tools/Json/ATWebSocketCommitTypeConverter.cs new file mode 100644 index 00000000..da60151a --- /dev/null +++ b/src/FishyFlip/Tools/Json/ATWebSocketCommitTypeConverter.cs @@ -0,0 +1,39 @@ +// +// Copyright (c) Drastic Actions. All rights reserved. +// + +namespace FishyFlip.Tools.Json; + +/// +/// AT WebSocket Event Converter. +/// +public class ATWebSocketCommitTypeConverter : JsonConverter +{ + /// + public override ATWebSocketCommitType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + string? value = reader.GetString(); + if (value is null) + { + return ATWebSocketCommitType.Unknown; + } + + switch (value) + { + case "u": + return ATWebSocketCommitType.Update; + case "c": + return ATWebSocketCommitType.Create; + case "d": + return ATWebSocketCommitType.Delete; + default: + return ATWebSocketCommitType.Unknown; + } + } + + /// + public override void Write(Utf8JsonWriter writer, ATWebSocketCommitType value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString().ToLower()); + } +} \ No newline at end of file diff --git a/src/FishyFlip/Tools/Json/ATWebSocketEventConverter.cs b/src/FishyFlip/Tools/Json/ATWebSocketEventConverter.cs new file mode 100644 index 00000000..635bc2d6 --- /dev/null +++ b/src/FishyFlip/Tools/Json/ATWebSocketEventConverter.cs @@ -0,0 +1,39 @@ +// +// Copyright (c) Drastic Actions. All rights reserved. +// + +namespace FishyFlip.Tools.Json; + +/// +/// AT WebSocket Event Converter. +/// +public class ATWebSocketEventConverter : JsonConverter +{ + /// + public override ATWebSocketEvent Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + string? value = reader.GetString(); + if (value is null) + { + return ATWebSocketEvent.Unknown; + } + + switch (value) + { + case "com": + return ATWebSocketEvent.Commit; + case "acc": + return ATWebSocketEvent.Account; + case "id": + return ATWebSocketEvent.Identity; + default: + return ATWebSocketEvent.Unknown; + } + } + + /// + public override void Write(Utf8JsonWriter writer, ATWebSocketEvent value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString().ToLower()); + } +} \ No newline at end of file diff --git a/src/FishyFlip/Tools/Json/EmbedConverter.cs b/src/FishyFlip/Tools/Json/EmbedConverter.cs index cc70457b..29024fca 100644 --- a/src/FishyFlip/Tools/Json/EmbedConverter.cs +++ b/src/FishyFlip/Tools/Json/EmbedConverter.cs @@ -94,7 +94,7 @@ public EmbedConverter(IReadOnlyList? converters = default break; case Constants.EmbedTypes.RecordWithMediaView: RecordViewEmbed? record1 = null; - ImageViewEmbed? media1 = null; + Embed? media1 = null; if (doc.RootElement.TryGetProperty("record", out var recordVal2)) { @@ -111,6 +111,9 @@ public EmbedConverter(IReadOnlyList? converters = default case Constants.EmbedTypes.ImageView: media1 = JsonSerializer.Deserialize(mediaVal2.GetRawText(), ((SourceGenerationContext)options.TypeInfoResolver!).ImageViewEmbed); break; + case Constants.EmbedTypes.VideoView: + media1 = JsonSerializer.Deserialize(mediaVal2.GetRawText(), ((SourceGenerationContext)options.TypeInfoResolver!).VideoViewEmbed); + break; } } } @@ -118,7 +121,7 @@ public EmbedConverter(IReadOnlyList? converters = default return new RecordWithMediaViewEmbed(record1, media1); case Constants.EmbedTypes.RecordWithMedia: RecordEmbed? record = null; - ImagesEmbed? media = null; + Embed? media = null; if (doc.RootElement.TryGetProperty("record", out var recordVal)) { @@ -135,6 +138,9 @@ record = JsonSerializer.Deserialize(recordVal.GetRawText(), ((Sourc case Constants.EmbedTypes.Images: media = JsonSerializer.Deserialize(mediaVal.GetRawText(), ((SourceGenerationContext)options.TypeInfoResolver!).ImagesEmbed); break; + case Constants.EmbedTypes.VideoView: + media = JsonSerializer.Deserialize(mediaVal.GetRawText(), ((SourceGenerationContext)options.TypeInfoResolver!).VideoViewEmbed); + break; } } } diff --git a/src/FishyFlip/UnauthenticatedSessionManager.cs b/src/FishyFlip/UnauthenticatedSessionManager.cs index 3b2a29cc..6ae4a973 100644 --- a/src/FishyFlip/UnauthenticatedSessionManager.cs +++ b/src/FishyFlip/UnauthenticatedSessionManager.cs @@ -45,13 +45,6 @@ public Task RefreshSessionAsync(CancellationToken cancellationToken = default) return Task.CompletedTask; } - /// - public void SetSession(Session session) - { - this.logger?.LogWarning("Unauthenticated session manager, session not set."); - this.SessionUpdated?.Invoke(this, new SessionUpdatedEventArgs(new AuthSession(session), null)); - } - /// public void Dispose() { @@ -60,6 +53,16 @@ public void Dispose() GC.SuppressFinalize(this); } + /// + /// Set the session. + /// + /// Session. + internal void SetSession(Session session) + { + this.logger?.LogWarning("Unauthenticated session manager, session not set."); + this.SessionUpdated?.Invoke(this, new SessionUpdatedEventArgs(new AuthSession(session), null)); + } + /// /// Dispose. /// diff --git a/src/WhiteWindLib.Tests/AuthorizedTests.cs b/src/WhiteWindLib.Auth.Tests/AuthorizedTests.cs similarity index 88% rename from src/WhiteWindLib.Tests/AuthorizedTests.cs rename to src/WhiteWindLib.Auth.Tests/AuthorizedTests.cs index f3169bae..789f6515 100644 --- a/src/WhiteWindLib.Tests/AuthorizedTests.cs +++ b/src/WhiteWindLib.Auth.Tests/AuthorizedTests.cs @@ -14,14 +14,14 @@ public static void ClassInitialize(TestContext context) { handle = (string?)context.Properties["BLUESKY_TEST_HANDLE"] ?? throw new ArgumentNullException(); string password = (string?)context.Properties["BLUESKY_TEST_PASSWORD"] ?? throw new ArgumentNullException(); - string instance = (string?)context.Properties["BLUESKY_INSTANCE_URL"] ?? throw new ArgumentNullException(); + string instance = "https://bsky.social"; var debugLog = new DebugLoggerProvider(); var atProtocolBuilder = new ATProtocolBuilder() .EnableAutoRenewSession(false) .WithInstanceUrl(new Uri(instance)) .WithLogger(debugLog.CreateLogger("FishyFlipTests")); AuthorizedTests.proto = atProtocolBuilder.Build(); - AuthorizedTests.proto.Server.CreateSessionAsync(AuthorizedTests.handle, password).Wait(); + AuthorizedTests.proto.AuthenticateWithPasswordAsync(AuthorizedTests.handle, password).Wait(); AuthorizedTests.blog = new WhiteWindBlog(AuthorizedTests.proto); } diff --git a/src/WhiteWindLib.Auth.Tests/GlobalUsings.cs b/src/WhiteWindLib.Auth.Tests/GlobalUsings.cs new file mode 100644 index 00000000..e9eab795 --- /dev/null +++ b/src/WhiteWindLib.Auth.Tests/GlobalUsings.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) Drastic Actions. All rights reserved. +// + +global using System.IdentityModel.Tokens.Jwt; +global using System.Net.Http.Headers; +global using System.Net.WebSockets; +global using System.Text; +global using System.Text.Encodings; +global using System.Text.Json; +global using System.Text.Json.Serialization; +global using System.Text.RegularExpressions; +global using System.Timers; +global using FishyFlip; +global using FishyFlip.Events; +global using FishyFlip.Models; +global using FishyFlip.Models.Internal; +global using FishyFlip.Tools; +global using FishyFlip.Tools.Cbor; +global using FishyFlip.Tools.Json; +global using Ipfs; +global using Microsoft.Extensions.Logging; +global using Microsoft.IdentityModel.Tokens; +global using Microsoft.Testing; +global using Microsoft.VisualStudio.TestTools.UnitTesting; +global using PeterO.Cbor; +global using WhiteWindLib; \ No newline at end of file diff --git a/src/WhiteWindLib.Auth.Tests/README.md b/src/WhiteWindLib.Auth.Tests/README.md new file mode 100644 index 00000000..72fc6b82 --- /dev/null +++ b/src/WhiteWindLib.Auth.Tests/README.md @@ -0,0 +1,3 @@ +# WhiteWindLib.Tests + +To run the tests, create a copy of `whitewindlib.runsettings.sample` and rename it `whitewindlib.runsettings`. Then, fill in the handle name and password fields. \ No newline at end of file diff --git a/src/WhiteWindLib.Auth.Tests/WhiteWindLib.Auth.Tests.csproj b/src/WhiteWindLib.Auth.Tests/WhiteWindLib.Auth.Tests.csproj new file mode 100644 index 00000000..ddc3997a --- /dev/null +++ b/src/WhiteWindLib.Auth.Tests/WhiteWindLib.Auth.Tests.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + enable + enable + false + true + true + + + + $(MSBuildProjectDirectory)\whitewindlib.runsettings + + + + + + + + + + + + diff --git a/src/WhiteWindLib.Tests/whitewindlib.runsettings.sample b/src/WhiteWindLib.Auth.Tests/whitewindlib.runsettings.sample similarity index 57% rename from src/WhiteWindLib.Tests/whitewindlib.runsettings.sample rename to src/WhiteWindLib.Auth.Tests/whitewindlib.runsettings.sample index 7bf7ce0a..9f3d8e08 100644 --- a/src/WhiteWindLib.Tests/whitewindlib.runsettings.sample +++ b/src/WhiteWindLib.Auth.Tests/whitewindlib.runsettings.sample @@ -2,9 +2,6 @@ - - - \ No newline at end of file diff --git a/src/WhiteWindLib.Tests/AnonymousTests.cs b/src/WhiteWindLib.Tests/AnonymousTests.cs index d2fe80d2..39d01aa0 100644 --- a/src/WhiteWindLib.Tests/AnonymousTests.cs +++ b/src/WhiteWindLib.Tests/AnonymousTests.cs @@ -13,19 +13,13 @@ namespace WhiteWindLib.Tests; [TestClass] public class AnonymousTests { - static string did; - - static string aturi; - static ATProtocol proto; static WhiteWindBlog blog; [ClassInitialize] public static void ClassInitialize(TestContext context) { - did = (string?)context.Properties["BLUESKY_TEST_DID"] ?? throw new ArgumentNullException(); - aturi = (string?)context.Properties["BLUESKY_TEST_ATURI"] ?? throw new ArgumentNullException(); - string instance = (string?)context.Properties["BLUESKY_INSTANCE_URL"] ?? throw new ArgumentNullException(); + string instance = "https://bsky.social"; var debugLog = new DebugLoggerProvider(); var atProtocolBuilder = new ATProtocolBuilder() .EnableAutoRenewSession(false) @@ -36,9 +30,10 @@ public static void ClassInitialize(TestContext context) } [TestMethod] - public async Task GetAuthorPostsTest() + [DataRow("did:plc:fzkpgpjj7nki7r5rhtmgzrez")] + public async Task GetAuthorPostsTest(string didString) { - var did = ATDid.Create(AnonymousTests.did); + var did = ATDid.Create(didString); var (result, error) = await blog.GetAuthorEntriesAsync(did!); Assert.IsNull(error); Assert.IsNotNull(result); @@ -46,9 +41,10 @@ public async Task GetAuthorPostsTest() } [TestMethod] - public async Task GetAuthorPostTest() + [DataRow("at://did:plc:fzkpgpjj7nki7r5rhtmgzrez/com.whtwnd.blog.entry/3kudrxp52ps2a")] + public async Task GetAuthorPostTest(string atDid) { - var postUri = ATUri.Create(AnonymousTests.aturi); + var postUri = ATUri.Create(atDid); var (result, error) = await blog.GetEntryAsync(postUri.Did!, postUri.Rkey); Assert.IsNull(error); Assert.IsNotNull(result); diff --git a/src/WhiteWindLib.Tests/README.md b/src/WhiteWindLib.Tests/README.md new file mode 100644 index 00000000..72fc6b82 --- /dev/null +++ b/src/WhiteWindLib.Tests/README.md @@ -0,0 +1,3 @@ +# WhiteWindLib.Tests + +To run the tests, create a copy of `whitewindlib.runsettings.sample` and rename it `whitewindlib.runsettings`. Then, fill in the handle name and password fields. \ No newline at end of file diff --git a/src/WhiteWindLib.Tests/WhiteWindLib.Tests.csproj b/src/WhiteWindLib.Tests/WhiteWindLib.Tests.csproj index ddc3997a..497f116a 100644 --- a/src/WhiteWindLib.Tests/WhiteWindLib.Tests.csproj +++ b/src/WhiteWindLib.Tests/WhiteWindLib.Tests.csproj @@ -9,10 +9,6 @@ true - - $(MSBuildProjectDirectory)\whitewindlib.runsettings - - diff --git a/website/docs/logging-in.md b/website/docs/logging-in.md index 4663b664..d559a0b0 100644 --- a/website/docs/logging-in.md +++ b/website/docs/logging-in.md @@ -1,24 +1,62 @@ # Logging In -- 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. +- There are two methods for logging in: OAuth and App Passwords. App Passwords were the original method for authentication, with OAuth being its replacement. However, the ATProtocol OAuth implementation is still being worked on and not totally final. If building a new application with authentication in mind, you may wish to design with OAuth for the future, but use app passwords today. + +- To log in with an App Password, you can call `AuthenticateWithPasswordAsync` ```csharp -// While this accepts normal passwords, you should ask users -// to create an app password from their accounts to use it instead. -Result 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}"); - } -); +var protocol = new ATProtocolBuilder() + .WithLogger(new DebugLoggerProvider().CreateLogger("FishyFlip")) + .Build(); + +var session = await protocol.AuthenticateWithPasswordAsync(identifier, password, cancellationToken); +if (session is null) +{ + Console.WriteLine("Failed to authenticate."); + return; +} + +Console.WriteLine("Authenticated."); +Console.WriteLine($"Session Did: {session.Did}"); +Console.WriteLine($"Session Email: {session.Email}"); +Console.WriteLine($"Session Handle: {session.Handle}"); +Console.WriteLine($"Session Token: {session.AccessJwt}"); ``` -- Instead of pattern matching, you can also use `.HandleResult()` to return the `success` object, and throw an exception upon an `error`. +- OAuth authentication is more complex. There is a full example showing a [local user authentication session](https://github.com/drasticactions/BSkyOAuthTokenGenerator/tree/main/src/BSkyOAuthTokenGenerator) but in short, you must: + - Starting the session with `protocol.GenerateOAuth2AuthenticationUrlAsync` + - Sending the user to a web browser to log in + - Handling the callback with the return URI, + - Sending that URI to `protocol.AuthenticateWithOAuth2CallbackAsync` to generate the session. + +```csharp +var scopeList = scopes.Split(',').Select(n => n.Trim()).ToArray(); +if (scopeList.Length == 0) +{ + consoleLog.LogError("Invalid Scopes"); + return; +} +var protocol = this.GenerateProtocol(iUrl); +consoleLog.Log($"Starting OAuth2 Authentication for {instanceUrl}"); +var url = await protocol.GenerateOAuth2AuthenticationUrlAsync(clientId, "http://127.0.0.1", scopeList, instanceUrl.ToString(), cancellationToken); +consoleLog.Log($"Login URL: {url}"); +consoleLog.Log("Please login and copy the URL of the page you are redirected to."); +var redirectUrl = Console.ReadLine(); +if (string.IsNullOrEmpty(redirectUrl)) +{ + consoleLog.LogError("Invalid redirect URL"); + return; +} + +consoleLog.Log($"Got redirect url, finishing OAuth2 Authentication on {instanceUrl}"); +var session = await protocol.AuthenticateWithOAuth2CallbackAsync(redirectUrl, cancellationToken); + +if (session is null) +{ + consoleLog.LogError("Failed to authenticate, session is null"); + return; +} + +consoleLog.Log($"Authenticated as {session.Did}"); +```