diff --git a/src/FishyFlip.Tests/AuthorizedTests.cs b/src/FishyFlip.Tests/AuthorizedTests.cs index 21c9bcd7..a402eb8f 100644 --- a/src/FishyFlip.Tests/AuthorizedTests.cs +++ b/src/FishyFlip.Tests/AuthorizedTests.cs @@ -328,10 +328,72 @@ public async Task CreatePostWithTagAsyncTest() } [Fact] - public async Task CreateFollowAsyncTest() + public async Task CreateAndRemoveListTest() { - // Did is for follow1.drasticactions.ninja. - var response = (await this.proto.Repo.CreateFollowAsync(ATDid.Create("did:plc:up76ybimufzledmmhbv25wse"))).HandleResult(); - Assert.True(response!.Cid is not null); + var randomName = Guid.NewGuid().ToString(); + var createList = (await this.proto.Repo.CreateCurateListAsync(randomName, "Test List", DateTime.UtcNow)).HandleResult(); + Assert.True(createList!.Cid is not null); + Assert.True(createList!.Uri is not null); + var repo = this.proto.SessionManager!.Session!.Did; + var removeList = (await this.proto.Repo.DeleteListAsync(repo, createList.Uri.Rkey)).HandleResult(); + Assert.True(removeList is not null); + } + + [Fact] + public async Task CreateAndRemovePostTest() + { + var randomName = Guid.NewGuid().ToString(); + var create = (await this.proto.Repo.CreatePostAsync(randomName)).HandleResult(); + Assert.True(create!.Cid is not null); + Assert.True(create!.Uri is not null); + var repo = this.proto.SessionManager!.Session!.Did; + var remove = (await this.proto.Repo.DeletePostAsync(repo, create.Uri.Rkey)).HandleResult(); + Assert.True(remove is not null); + } + + [Fact] + public async Task CreateAndRemoveLikeTest() + { + var randomName = Guid.NewGuid().ToString(); + var create = (await this.proto.Repo.CreatePostAsync(randomName)).HandleResult(); + Assert.True(create!.Cid is not null); + Assert.True(create!.Uri is not null); + + var like = (await this.proto.Repo.CreateLikeAsync(create.Cid, create.Uri)).HandleResult(); + Assert.True(like!.Cid is not null); + Assert.True(like!.Uri is not null); + + var repo = this.proto.SessionManager!.Session!.Did; + var removeLike = (await this.proto.Repo.DeleteLikeAsync(repo, like.Uri.Rkey)).HandleResult(); + Assert.True(removeLike is not null); + + var remove = (await this.proto.Repo.DeletePostAsync(repo, create.Uri.Rkey)).HandleResult(); + Assert.True(remove is not null); + } + + [Fact] + public async Task CreateAndRemoveFollowTest() + { + var follow1 = ATDid.Create("did:plc:up76ybimufzledmmhbv25wse"); + var follow = (await this.proto.Repo.CreateFollowAsync(follow1)).HandleResult(); + Assert.True(follow!.Cid is not null); + Assert.True(follow!.Uri is not null); + + var repo = this.proto.SessionManager!.Session!.Did; + var remove = (await this.proto.Repo.DeleteFollowAsync(repo, follow.Uri.Rkey)).HandleResult(); + Assert.True(remove is not null); + } + + [Fact] + public async Task CreateAndRemoveBlockTest() + { + var follow2 = ATDid.Create("did:plc:5x7vqoe5l3qbh3koqizdipst"); + var follow = (await this.proto.Repo.CreateBlockAsync(follow2)).HandleResult(); + Assert.True(follow!.Cid is not null); + Assert.True(follow!.Uri is not null); + + var repo = this.proto.SessionManager!.Session!.Did; + var remove = (await this.proto.Repo.DeleteBlockAsync(repo, follow.Uri.Rkey)).HandleResult(); + Assert.True(remove is not null); } } diff --git a/src/FishyFlip/ATProtoRepo.cs b/src/FishyFlip/ATProtoRepo.cs index aa6515be..11bc68ff 100644 --- a/src/FishyFlip/ATProtoRepo.cs +++ b/src/FishyFlip/ATProtoRepo.cs @@ -2,6 +2,9 @@ // Copyright (c) Drastic Actions. All rights reserved. // +using System.Threading; +using static FishyFlip.Constants; + namespace FishyFlip; /// @@ -104,6 +107,31 @@ public Task> CreateBlockAsync( this.Options.Logger); } + public Task> CreateCurateListAsync( + string name, + string description, + DateTime? createdAt = null, + string? rkey = null, + string? swapCommit = null, + CancellationToken cancellationToken = default) + { + var listRecord = new ListRecordInternal(name, description, ListReasons.CurateList); + + CreateListRecord record = new( + Constants.GraphTypes.List, + this.proto.SessionManager!.Session!.Did.ToString(), + listRecord); + + return + this.Client + .Post( + Constants.Urls.ATProtoRepo.CreateRecord, + this.Options.JsonSerializerOptions, + record, + cancellationToken, + this.Options.Logger); + } + public async Task> GetPostAsync(ATIdentifier repo, string rkey, Cid? cid = null, CancellationToken cancellationToken = default) => await this.GetRecordAsync(Constants.FeedType.Post, repo, rkey, cid, cancellationToken); @@ -172,16 +200,13 @@ public Task> DeletePostAsync( CancellationToken cancellationToken = default) => this.DeleteRecordAsync(Constants.FeedType.Post, repo, rkey, swapRecord, swapCommit, cancellationToken); - public async Task> DeleteRecordAsync(string collection, ATIdentifier repo, string rkey, Cid? swapRecord = null, Cid? swapCommit = null, CancellationToken cancellationToken = default) - { - DeleteRecord record = new( - collection, - this.proto.SessionManager!.Session!.Did.ToString()!, - rkey, - swapRecord, - swapCommit); - return await this.Client.Post(Constants.Urls.ATProtoRepo.DeleteRecord, this.Options.JsonSerializerOptions, cancellationToken, this.Options.Logger); - } + public Task> DeleteListAsync( + ATIdentifier repo, + string rkey, + Cid? swapRecord = null, + Cid? swapCommit = null, + CancellationToken cancellationToken = default) + => this.DeleteRecordAsync(Constants.GraphTypes.List, repo, rkey, swapRecord, swapCommit, cancellationToken); [Obsolete("Use ListFollowsAsync instead")] public Task> ListFollowAsync(ATIdentifier repo, int limit = 50, string? cursor = default, bool? reverse = default, CancellationToken cancellationToken = default) @@ -250,4 +275,15 @@ private Task> CreateRecord(T record, CancellationToken cancell cancellationToken, this.Options.Logger); } + + private async Task> DeleteRecordAsync(string collection, ATIdentifier repo, string rkey, Cid? swapRecord = null, Cid? swapCommit = null, CancellationToken cancellationToken = default) + { + DeleteRecord record = new( + collection, + this.proto.SessionManager!.Session!.Did.ToString()!, + rkey, + swapRecord, + swapCommit); + return await this.Client.Post(Constants.Urls.ATProtoRepo.DeleteRecord, this.Options.JsonSerializerOptions, record, cancellationToken, this.Options.Logger); + } } diff --git a/src/FishyFlip/Constants.cs b/src/FishyFlip/Constants.cs index ff4b41fd..9f39649c 100644 --- a/src/FishyFlip/Constants.cs +++ b/src/FishyFlip/Constants.cs @@ -166,6 +166,12 @@ public static class GraphTypes public const string Block = "app.bsky.graph.block"; } + public static class ListReasons + { + public const string ModList = "app.bsky.graph.defs#modlist"; + public const string CurateList = "app.bsky.graph.defs#curatelist"; + } + public static class FacetTypes { public const string Tag = "app.bsky.richtext.facet#tag"; @@ -203,6 +209,13 @@ public static class ModerationReasons public const string ReasonOther = "com.atproto.moderation.defs#reasonOther"; } + public static class ApplyWriteTypes + { + public const string Create = "com.atproto.repo.applyWrites#create"; + public const string Update = "com.atproto.repo.applyWrites#update"; + public const string Delete = "com.atproto.repo.applyWrites#delete"; + } + internal class HeaderNames { public const string UserAgent = "user-agent"; diff --git a/src/FishyFlip/Models/Internal/CreateListRecord.cs b/src/FishyFlip/Models/Internal/CreateListRecord.cs new file mode 100644 index 00000000..a6db8d10 --- /dev/null +++ b/src/FishyFlip/Models/Internal/CreateListRecord.cs @@ -0,0 +1,30 @@ +// +// Copyright (c) Drastic Actions. All rights reserved. +// + +using static FishyFlip.Constants; + +namespace FishyFlip.Models.Internal; + +internal record CreateListRecord(string Collection, string Repo, ListRecordInternal Record, string? Rkey = null, string? SwapCommit = null); + +internal class ListRecordInternal : ATRecord +{ + [JsonConstructor] + public ListRecordInternal(string name, string description, string purpose, DateTime? createdAt = default) + { + this.Name = name; + this.Description = description; + this.Type = GraphTypes.List; + this.CreatedAt = createdAt ?? DateTime.UtcNow; + this.Purpose = purpose; + } + + public DateTime CreatedAt { get; } + + public string Name { get; } + + public string Description { get; } + + public string Purpose { get; } +} \ No newline at end of file