From e4c102b65f8d6e6af48580d8ddc46aa84b0dcc57 Mon Sep 17 00:00:00 2001 From: Tim Miller Date: Thu, 23 May 2024 15:57:25 +0900 Subject: [PATCH] More Chat APIs --- src/FishyFlip/BlueskyChat.cs | 105 ++++++++++++++---- src/FishyFlip/Constants.cs | 5 + src/FishyFlip/Models/DeletedMessageView.cs | 50 +++++++++ src/FishyFlip/Models/Internal/UpdateRead.cs | 11 ++ src/FishyFlip/Models/LeaveConvoResponse.cs | 12 ++ src/FishyFlip/Models/LogBeginConvo.cs | 35 ++++++ src/FishyFlip/Models/LogCreateMessage.cs | 42 +++++++ src/FishyFlip/Models/LogDeleteMessage.cs | 42 +++++++ src/FishyFlip/Models/LogLeaveConvo.cs | 35 ++++++ src/FishyFlip/Models/LogResponse.cs | 12 ++ src/FishyFlip/SourceGenerationContext.cs | 8 ++ .../Tools/Json/ATRecordJsonConverter.cs | 30 +++++ 12 files changed, 364 insertions(+), 23 deletions(-) create mode 100644 src/FishyFlip/Models/DeletedMessageView.cs create mode 100644 src/FishyFlip/Models/Internal/UpdateRead.cs create mode 100644 src/FishyFlip/Models/LeaveConvoResponse.cs create mode 100644 src/FishyFlip/Models/LogBeginConvo.cs create mode 100644 src/FishyFlip/Models/LogCreateMessage.cs create mode 100644 src/FishyFlip/Models/LogDeleteMessage.cs create mode 100644 src/FishyFlip/Models/LogLeaveConvo.cs create mode 100644 src/FishyFlip/Models/LogResponse.cs diff --git a/src/FishyFlip/BlueskyChat.cs b/src/FishyFlip/BlueskyChat.cs index a66654ce..2a2aac1b 100644 --- a/src/FishyFlip/BlueskyChat.cs +++ b/src/FishyFlip/BlueskyChat.cs @@ -9,7 +9,8 @@ namespace FishyFlip; /// public sealed class BlueskyChat { - private ATProtocol proto; + private readonly ATProtocol proto; + private readonly Dictionary chatHeaders; /// /// Initializes a new instance of the class. @@ -18,6 +19,10 @@ public sealed class BlueskyChat internal BlueskyChat(ATProtocol proto) { this.proto = proto; + this.chatHeaders = new Dictionary + { + { Constants.ATProtoProxy.Proxy, Constants.ATProtoProxy.BskyChat }, + }; } private ATProtocolOptions Options => this.proto.Options; @@ -46,12 +51,7 @@ internal BlueskyChat(ATProtocol proto) url += $"&limit={limit}"; } - var headers = new Dictionary - { - { Constants.ATProtoProxy.Proxy, Constants.ATProtoProxy.BskyChat }, - }; - - return this.Client.Get(url, this.Options.SourceGenerationContext.ConversationMessages, this.Options.JsonSerializerOptions, cancellationToken, this.Options.Logger, headers); + return this.Client.Get(url, this.Options.SourceGenerationContext.ConversationMessages, this.Options.JsonSerializerOptions, cancellationToken, this.Options.Logger, this.chatHeaders); } /// @@ -70,12 +70,7 @@ internal BlueskyChat(ATProtocol proto) url += $"&cursor={cursor}"; } - var headers = new Dictionary - { - { Constants.ATProtoProxy.Proxy, Constants.ATProtoProxy.BskyChat }, - }; - - return this.Client.Get(url, this.Options.SourceGenerationContext.ConversationList, this.Options.JsonSerializerOptions, cancellationToken, this.Options.Logger, headers); + return this.Client.Get(url, this.Options.SourceGenerationContext.ConversationList, this.Options.JsonSerializerOptions, cancellationToken, this.Options.Logger, this.chatHeaders); } /// @@ -87,12 +82,20 @@ internal BlueskyChat(ATProtocol proto) public Task> GetConversationAsync(string convoId, CancellationToken cancellationToken = default) { var url = $"{Constants.Urls.Bluesky.Chat.Convo.GetConvo}?convoId={convoId}"; - var headers = new Dictionary - { - { Constants.ATProtoProxy.Proxy, Constants.ATProtoProxy.BskyChat }, - }; + return this.Client.Get(url, this.Options.SourceGenerationContext.ConversationView, this.Options.JsonSerializerOptions, cancellationToken, this.Options.Logger, this.chatHeaders); + } - return this.Client.Get(url, this.Options.SourceGenerationContext.ConversationView, this.Options.JsonSerializerOptions, cancellationToken, this.Options.Logger, headers); + /// + /// Retrieves a conversation for members asynchronously. + /// + /// The ATDid of the member of the conversation. + /// An optional token to cancel the asynchronous operation. + /// A task that represents the asynchronous operation. The task result contains a that encapsulates the result of the operation. + public Task> GetConversationForMembersAsync(ATDid[] members, CancellationToken cancellationToken = default) + { + var commaSeparatedMembers = string.Join(",", members.Select(x => x.ToString())); + var url = $"{Constants.Urls.Bluesky.Chat.Convo.GetConvoForMembers}?members={commaSeparatedMembers}"; + return this.Client.Get(url, this.Options.SourceGenerationContext.ConversationView, this.Options.JsonSerializerOptions, cancellationToken, this.Options.Logger, this.chatHeaders); } /// @@ -106,11 +109,67 @@ public Task> SendMessageAsync(string convoId, string message { var url = $"{Constants.Urls.Bluesky.Chat.Convo.SendMessage}"; var createMessage = new CreateMessage(convoId, new CreateMessageMessage(message)); - var headers = new Dictionary - { - { Constants.ATProtoProxy.Proxy, Constants.ATProtoProxy.BskyChat }, - }; - return this.Client.Post(url, this.Options.SourceGenerationContext.CreateMessage, this.Options.SourceGenerationContext.MessageView, this.Options.JsonSerializerOptions, createMessage, cancellationToken, this.Options.Logger, headers); + return this.Client.Post(url, this.Options.SourceGenerationContext.CreateMessage, this.Options.SourceGenerationContext.MessageView, this.Options.JsonSerializerOptions, createMessage, cancellationToken, this.Options.Logger, this.chatHeaders); + } + + /// + /// Update the read status of a conversation asynchronously. + /// + /// The unique identifier of the conversation. + /// An optional token to cancel the asynchronous operation. + /// A task that represents the asynchronous operation. The task result contains a that encapsulates the result of the operation. + public Task> UpdateReadAsync(string convoId, CancellationToken cancellationToken = default) + { + var updateRead = new UpdateRead(convoId); + return this.Client.Post(Constants.Urls.Bluesky.Chat.Convo.UpdateRead, this.Options.SourceGenerationContext.UpdateRead, this.Options.SourceGenerationContext.ConversationView, this.Options.JsonSerializerOptions, updateRead, cancellationToken, this.Options.Logger, this.chatHeaders); + } + + /// + /// Mute a conversation asynchronously. + /// + /// The unique identifier of the conversation. + /// An optional token to cancel the asynchronous operation. + /// A task that represents the asynchronous operation. The task result contains a that encapsulates the result of the operation. + public Task> MuteConvoAsync(string convoId, CancellationToken cancellationToken = default) + { + var updateRead = new UpdateRead(convoId); + return this.Client.Post(Constants.Urls.Bluesky.Chat.Convo.MuteConvo, this.Options.SourceGenerationContext.UpdateRead, this.Options.SourceGenerationContext.ConversationView, this.Options.JsonSerializerOptions, updateRead, cancellationToken, this.Options.Logger, this.chatHeaders); + } + + /// + /// Unmute a conversation asynchronously. + /// + /// The unique identifier of the conversation. + /// An optional token to cancel the asynchronous operation. + /// A task that represents the asynchronous operation. The task result contains a that encapsulates the result of the operation. + public Task> UnmuteConvoAsync(string convoId, CancellationToken cancellationToken = default) + { + var updateRead = new UpdateRead(convoId); + return this.Client.Post(Constants.Urls.Bluesky.Chat.Convo.UnmuteConvo, this.Options.SourceGenerationContext.UpdateRead, this.Options.SourceGenerationContext.ConversationView, this.Options.JsonSerializerOptions, updateRead, cancellationToken, this.Options.Logger, this.chatHeaders); + } + + /// + /// Retrieves the log asynchronously. + /// + /// The cursor for pagination in the log. + /// An optional token to cancel the asynchronous operation. + /// A task that represents the asynchronous operation. The task result contains a that encapsulates the result of the operation. + public Task> GetLogAsync(string cursor, CancellationToken cancellationToken = default) + { + var url = $"{Constants.Urls.Bluesky.Chat.Convo.GetLog}?cursor={cursor}"; + return this.Client.Get(url, this.Options.SourceGenerationContext.LogResponse, this.Options.JsonSerializerOptions, cancellationToken, this.Options.Logger, this.chatHeaders); + } + + /// + /// Leaves a conversation asynchronously. + /// + /// The unique identifier of the conversation. + /// An optional token to cancel the asynchronous operation. + /// A task that represents the asynchronous operation. The task result contains a that encapsulates the result of the operation. + public Task> LeaveConvoAsync(string convoId, CancellationToken cancellationToken = default) + { + var updateRead = new UpdateRead(convoId); + return this.Client.Post(Constants.Urls.Bluesky.Chat.Convo.LeaveConvo, this.Options.SourceGenerationContext.UpdateRead, this.Options.SourceGenerationContext.LeaveConvoResponse, this.Options.JsonSerializerOptions, updateRead, cancellationToken, this.Options.Logger, this.chatHeaders); } } \ No newline at end of file diff --git a/src/FishyFlip/Constants.cs b/src/FishyFlip/Constants.cs index d11f8464..d86452de 100644 --- a/src/FishyFlip/Constants.cs +++ b/src/FishyFlip/Constants.cs @@ -286,6 +286,11 @@ public static class ActorTypes public static class ConversationTypes { public const string MessageView = "chat.bsky.convo.defs#messageView"; + public const string DeletedMessageView = "chat.bsky.convo.defs#deletedMessageView"; + public const string LogBeginConvo = "chat.bsky.convo.defs#logBeginConvo"; + public const string LogLeaveConvo = "chat.bsky.convo.defs#logLeaveConvo"; + public const string LogCreateMessage = "chat.bsky.convo.defs#logCreateMessage"; + public const string LogDeleteMessage = "chat.bsky.convo.defs#logDeleteMessage"; } public static class DeclarationTypes diff --git a/src/FishyFlip/Models/DeletedMessageView.cs b/src/FishyFlip/Models/DeletedMessageView.cs new file mode 100644 index 00000000..11d41cd3 --- /dev/null +++ b/src/FishyFlip/Models/DeletedMessageView.cs @@ -0,0 +1,50 @@ +// +// Copyright (c) Drastic Actions. All rights reserved. +// + +namespace FishyFlip.Models; + +/// +/// Represents a view of a message in a chat conversation. +/// +public class DeletedMessageView : ATRecord +{ + /// + /// Initializes a new instance of the class. + /// + /// The unique identifier of the message. + /// The revision of the message. + /// The sender of the message. + /// The date and time when the message was sent. + /// The type of the message. If not provided, defaults to . + [JsonConstructor] + public DeletedMessageView(string id, string rev, ChatSender sender, DateTime sentAt, string? type = default) + : base(type) + { + this.Id = id; + this.Rev = rev; + this.Sender = sender; + this.SentAt = sentAt; + this.Type = type ?? Constants.ConversationTypes.DeletedMessageView; + } + + /// + /// Gets the unique identifier of the message. + /// + public string Id { get; } + + /// + /// Gets the revision of the message. + /// + public string Rev { get; } + + /// + /// Gets the sender of the message. + /// + public ChatSender Sender { get; } + + /// + /// Gets the date and time when the message was sent. + /// + public DateTime SentAt { get; } +} \ No newline at end of file diff --git a/src/FishyFlip/Models/Internal/UpdateRead.cs b/src/FishyFlip/Models/Internal/UpdateRead.cs new file mode 100644 index 00000000..3ac9fcdf --- /dev/null +++ b/src/FishyFlip/Models/Internal/UpdateRead.cs @@ -0,0 +1,11 @@ +// +// Copyright (c) Drastic Actions. All rights reserved. +// + +namespace FishyFlip.Models.Internal; + +/// +/// Update Conversation Read. +/// +/// Conversation Id. +public record UpdateRead(string ConvoId); \ No newline at end of file diff --git a/src/FishyFlip/Models/LeaveConvoResponse.cs b/src/FishyFlip/Models/LeaveConvoResponse.cs new file mode 100644 index 00000000..48b6caf2 --- /dev/null +++ b/src/FishyFlip/Models/LeaveConvoResponse.cs @@ -0,0 +1,12 @@ +// +// Copyright (c) Drastic Actions. All rights reserved. +// + +namespace FishyFlip.Models; + +/// +/// Response to leaving a conversation. +/// +/// Conversation id. +/// Rev Id. +public record LeaveConvoResponse(string ConvoId, string Rev); \ No newline at end of file diff --git a/src/FishyFlip/Models/LogBeginConvo.cs b/src/FishyFlip/Models/LogBeginConvo.cs new file mode 100644 index 00000000..36a392a2 --- /dev/null +++ b/src/FishyFlip/Models/LogBeginConvo.cs @@ -0,0 +1,35 @@ +// +// Copyright (c) Drastic Actions. All rights reserved. +// + +namespace FishyFlip.Models; + +/// +/// Represents a log begin conversation message. +/// +public class LogBeginConvo : ATRecord +{ + /// + /// Initializes a new instance of the class. + /// + /// The conversation ID. + /// The revision. + /// The type of the record. Optional. + public LogBeginConvo(string convoId, string rev, string? type = default) + : base(type) + { + this.ConvoId = convoId; + this.Rev = rev; + this.Type = type ?? Constants.ConversationTypes.LogBeginConvo; + } + + /// + /// Gets the conversation ID. + /// + public string ConvoId { get; } + + /// + /// Gets the revision. + /// + public string Rev { get; } +} \ No newline at end of file diff --git a/src/FishyFlip/Models/LogCreateMessage.cs b/src/FishyFlip/Models/LogCreateMessage.cs new file mode 100644 index 00000000..1374316b --- /dev/null +++ b/src/FishyFlip/Models/LogCreateMessage.cs @@ -0,0 +1,42 @@ +// +// Copyright (c) Drastic Actions. All rights reserved. +// + +namespace FishyFlip.Models; + +/// +/// Represents a log creation message. +/// +public class LogCreateMessage : ATRecord +{ + /// + /// Initializes a new instance of the class. + /// + /// The conversation ID. + /// The message view. + /// The revision. + /// The type of the record. Optional. + public LogCreateMessage(string convoId, MessageView message, string rev, string? type = default) + : base(type) + { + this.ConvoId = convoId; + this.Message = message; + this.Rev = rev; + this.Type = type ?? Constants.ConversationTypes.LogCreateMessage; + } + + /// + /// Gets the conversation ID. + /// + public string ConvoId { get; } + + /// + /// Gets the message view. + /// + public MessageView Message { get; } + + /// + /// Gets the revision. + /// + public string Rev { get; } +} \ No newline at end of file diff --git a/src/FishyFlip/Models/LogDeleteMessage.cs b/src/FishyFlip/Models/LogDeleteMessage.cs new file mode 100644 index 00000000..a84b4274 --- /dev/null +++ b/src/FishyFlip/Models/LogDeleteMessage.cs @@ -0,0 +1,42 @@ +// +// Copyright (c) Drastic Actions. All rights reserved. +// + +namespace FishyFlip.Models; + +/// +/// Represents a log delete message. +/// +public class LogDeleteMessage : ATRecord +{ + /// + /// Initializes a new instance of the class. + /// + /// The conversation ID. + /// The message view. + /// The revision. + /// The type of the record. Optional. + public LogDeleteMessage(string convoId, DeletedMessageView message, string rev, string? type = default) + : base(type) + { + this.ConvoId = convoId; + this.Message = message; + this.Rev = rev; + this.Type = type ?? Constants.ConversationTypes.LogDeleteMessage; + } + + /// + /// Gets the conversation ID. + /// + public string ConvoId { get; } + + /// + /// Gets the message view. + /// + public DeletedMessageView Message { get; } + + /// + /// Gets the revision. + /// + public string Rev { get; } +} \ No newline at end of file diff --git a/src/FishyFlip/Models/LogLeaveConvo.cs b/src/FishyFlip/Models/LogLeaveConvo.cs new file mode 100644 index 00000000..1bfcc0d1 --- /dev/null +++ b/src/FishyFlip/Models/LogLeaveConvo.cs @@ -0,0 +1,35 @@ +// +// Copyright (c) Drastic Actions. All rights reserved. +// + +namespace FishyFlip.Models; + +/// +/// Represents a log leave conversation message. +/// +public class LogLeaveConvo : ATRecord +{ + /// + /// Initializes a new instance of the class. + /// + /// The conversation ID. + /// The revision. + /// The type of the record. Optional. + public LogLeaveConvo(string convoId, string rev, string? type = default) + : base(type) + { + this.ConvoId = convoId; + this.Rev = rev; + this.Type = type ?? Constants.ConversationTypes.LogLeaveConvo; + } + + /// + /// Gets the conversation ID. + /// + public string ConvoId { get; } + + /// + /// Gets the revision. + /// + public string Rev { get; } +} \ No newline at end of file diff --git a/src/FishyFlip/Models/LogResponse.cs b/src/FishyFlip/Models/LogResponse.cs new file mode 100644 index 00000000..409221fc --- /dev/null +++ b/src/FishyFlip/Models/LogResponse.cs @@ -0,0 +1,12 @@ +// +// Copyright (c) Drastic Actions. All rights reserved. +// + +namespace FishyFlip.Models; + +/// +/// Response to a log request. +/// +/// A cursor that can be used to paginate through logs. +/// Logs. +public record LogResponse(string Cursor, ATRecord[] Logs); \ No newline at end of file diff --git a/src/FishyFlip/SourceGenerationContext.cs b/src/FishyFlip/SourceGenerationContext.cs index 5655e998..db8828d8 100644 --- a/src/FishyFlip/SourceGenerationContext.cs +++ b/src/FishyFlip/SourceGenerationContext.cs @@ -184,6 +184,14 @@ namespace FishyFlip; [JsonSerializable(typeof(Conversation))] [JsonSerializable(typeof(ConversationView))] [JsonSerializable(typeof(ConversationList))] +[JsonSerializable(typeof(UpdateRead))] +[JsonSerializable(typeof(LogCreateMessage))] +[JsonSerializable(typeof(LogDeleteMessage))] +[JsonSerializable(typeof(LogResponse))] +[JsonSerializable(typeof(LeaveConvoResponse))] +[JsonSerializable(typeof(LogLeaveConvo))] +[JsonSerializable(typeof(LogBeginConvo))] +[JsonSerializable(typeof(DeletedMessageView))] internal partial class SourceGenerationContext : JsonSerializerContext { } \ No newline at end of file diff --git a/src/FishyFlip/Tools/Json/ATRecordJsonConverter.cs b/src/FishyFlip/Tools/Json/ATRecordJsonConverter.cs index 60b2b750..cc61022f 100644 --- a/src/FishyFlip/Tools/Json/ATRecordJsonConverter.cs +++ b/src/FishyFlip/Tools/Json/ATRecordJsonConverter.cs @@ -38,6 +38,18 @@ public class ATRecordJsonConverter : JsonConverter return JsonSerializer.Deserialize(doc.RootElement.GetRawText(), ((SourceGenerationContext)options.TypeInfoResolver!).SavedFeedsPref); case Constants.WhiteWindTypes.Entry: return JsonSerializer.Deserialize(doc.RootElement.GetRawText(), ((SourceGenerationContext)options.TypeInfoResolver!).Entry); + case Constants.ConversationTypes.LogCreateMessage: + return JsonSerializer.Deserialize(doc.RootElement.GetRawText(), ((SourceGenerationContext)options.TypeInfoResolver!).LogCreateMessage); + case Constants.ConversationTypes.LogDeleteMessage: + return JsonSerializer.Deserialize(doc.RootElement.GetRawText(), ((SourceGenerationContext)options.TypeInfoResolver!).LogDeleteMessage); + case Constants.ConversationTypes.LogLeaveConvo: + return JsonSerializer.Deserialize(doc.RootElement.GetRawText(), ((SourceGenerationContext)options.TypeInfoResolver!).LogLeaveConvo); + case Constants.ConversationTypes.LogBeginConvo: + return JsonSerializer.Deserialize(doc.RootElement.GetRawText(), ((SourceGenerationContext)options.TypeInfoResolver!).LogBeginConvo); + case Constants.ConversationTypes.MessageView: + return JsonSerializer.Deserialize(doc.RootElement.GetRawText(), ((SourceGenerationContext)options.TypeInfoResolver!).MessageView); + case Constants.ConversationTypes.DeletedMessageView: + return JsonSerializer.Deserialize(doc.RootElement.GetRawText(), ((SourceGenerationContext)options.TypeInfoResolver!).DeletedMessageView); default: #if DEBUG System.Diagnostics.Debugger.Break(); @@ -82,6 +94,24 @@ public override void Write(Utf8JsonWriter writer, ATRecord value, JsonSerializer case Constants.WhiteWindTypes.Entry: writer.WriteRawValue(JsonSerializer.SerializeToUtf8Bytes((Models.WhiteWind.Entry)value, ((SourceGenerationContext)options.TypeInfoResolver!).Entry)); break; + case Constants.ConversationTypes.MessageView: + writer.WriteRawValue(JsonSerializer.SerializeToUtf8Bytes((MessageView)value, ((SourceGenerationContext)options.TypeInfoResolver!).MessageView)); + break; + case Constants.ConversationTypes.DeletedMessageView: + writer.WriteRawValue(JsonSerializer.SerializeToUtf8Bytes((DeletedMessageView)value, ((SourceGenerationContext)options.TypeInfoResolver!).DeletedMessageView)); + break; + case Constants.ConversationTypes.LogCreateMessage: + writer.WriteRawValue(JsonSerializer.SerializeToUtf8Bytes((LogCreateMessage)value, ((SourceGenerationContext)options.TypeInfoResolver!).LogCreateMessage)); + break; + case Constants.ConversationTypes.LogDeleteMessage: + writer.WriteRawValue(JsonSerializer.SerializeToUtf8Bytes((LogDeleteMessage)value, ((SourceGenerationContext)options.TypeInfoResolver!).LogDeleteMessage)); + break; + case Constants.ConversationTypes.LogLeaveConvo: + writer.WriteRawValue(JsonSerializer.SerializeToUtf8Bytes((LogLeaveConvo)value, ((SourceGenerationContext)options.TypeInfoResolver!).LogLeaveConvo)); + break; + case Constants.ConversationTypes.LogBeginConvo: + writer.WriteRawValue(JsonSerializer.SerializeToUtf8Bytes((LogBeginConvo)value, ((SourceGenerationContext)options.TypeInfoResolver!).LogBeginConvo)); + break; default: #if DEBUG System.Diagnostics.Debugger.Break();