Skip to content

Commit

Permalink
Start adding chat protocol
Browse files Browse the repository at this point in the history
  • Loading branch information
drasticactions committed May 22, 2024
1 parent cab3a05 commit b30889c
Show file tree
Hide file tree
Showing 14 changed files with 354 additions and 15 deletions.
5 changes: 5 additions & 0 deletions src/FishyFlip/ATProtocol.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@ public ATProtocol(ATProtocolOptions options)
/// </summary>
public PlcDirectory PlcDirectory => new(this);

/// <summary>
/// Gets the ATProto Chat Protocol.
/// </summary>
public BlueskyChat Chat => new(this);

/// <summary>
/// Gets a value indicating whether the subscription is active.
/// </summary>
Expand Down
116 changes: 116 additions & 0 deletions src/FishyFlip/BlueskyChat.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// <copyright file="BlueskyChat.cs" company="Drastic Actions">
// Copyright (c) Drastic Actions. All rights reserved.
// </copyright>

namespace FishyFlip;

/// <summary>
/// Bluesky Chat.
/// </summary>
public sealed class BlueskyChat
{
private ATProtocol proto;

/// <summary>
/// Initializes a new instance of the <see cref="BlueskyChat"/> class.
/// </summary>
/// <param name="proto"><see cref="ATProtocol"/>.</param>
internal BlueskyChat(ATProtocol proto)
{
this.proto = proto;
}

private ATProtocolOptions Options => this.proto.Options;

private HttpClient Client => this.proto.Client;

/// <summary>
/// Retrieves the messages of a conversation asynchronously.
/// </summary>
/// <param name="convoId">The unique identifier of the conversation.</param>
/// <param name="cursor">The cursor for pagination in the conversation.</param>
/// <param name="limit">Limit of conversations to get, defaults to 60.</param>
/// <param name="cancellationToken">An optional token to cancel the asynchronous operation.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a <see cref="Result{ConversationMessages?}"/> that encapsulates the result of the operation.</returns>
public Task<Result<ConversationMessages?>> GetConversationMessagesAsync(string convoId, string? cursor = default, int limit = 60, CancellationToken cancellationToken = default)
{
var url = $"{Constants.Urls.Bluesky.Chat.Convo.GetMessages}?convoId={convoId}";

if (!string.IsNullOrWhiteSpace(cursor))
{
url += $"&cursor={cursor}";
}

if (limit > 0)
{
url += $"&limit={limit}";
}

var headers = new Dictionary<string, string>
{
{ Constants.ATProtoProxy.Proxy, Constants.ATProtoProxy.BskyChat },
};

return this.Client.Get<ConversationMessages>(url, this.Options.SourceGenerationContext.ConversationMessages, this.Options.JsonSerializerOptions, cancellationToken, this.Options.Logger, headers);
}

/// <summary>
/// Retrieves a list of conversations asynchronously.
/// </summary>
/// <param name="cursor">An optional string that represents the cursor position in the list. Default is an empty string.</param>
/// <param name="limit">Limit of conversations to get, defaults to 60.</param>
/// <param name="cancellationToken">An optional token to cancel the asynchronous operation.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a <see cref="Result{ConversationList?}"/> that encapsulates the result of the operation.</returns>
public Task<Result<ConversationList?>> GetConversationsAsync(string cursor = "", int limit = 60, CancellationToken cancellationToken = default)
{
var url = $"{Constants.Urls.Bluesky.Chat.Convo.ListConvos}?limit={limit}";

if (!string.IsNullOrWhiteSpace(cursor))
{
url += $"&cursor={cursor}";
}

var headers = new Dictionary<string, string>
{
{ Constants.ATProtoProxy.Proxy, Constants.ATProtoProxy.BskyChat },
};

return this.Client.Get<ConversationList>(url, this.Options.SourceGenerationContext.ConversationList, this.Options.JsonSerializerOptions, cancellationToken, this.Options.Logger, headers);
}

/// <summary>
/// Retrieves a conversation asynchronously.
/// </summary>
/// <param name="convoId">The unique identifier of the conversation.</param>
/// <param name="cancellationToken">An optional token to cancel the asynchronous operation.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a <see cref="Result{Conversation?}"/> that encapsulates the result of the operation.</returns>
public Task<Result<ConversationView?>> GetConversationAsync(string convoId, CancellationToken cancellationToken = default)
{
var url = $"{Constants.Urls.Bluesky.Chat.Convo.GetConvo}?convoId={convoId}";
var headers = new Dictionary<string, string>
{
{ Constants.ATProtoProxy.Proxy, Constants.ATProtoProxy.BskyChat },
};

return this.Client.Get<ConversationView>(url, this.Options.SourceGenerationContext.ConversationView, this.Options.JsonSerializerOptions, cancellationToken, this.Options.Logger, headers);
}

/// <summary>
/// Sends a message asynchronously in a conversation.
/// </summary>
/// <param name="convoId">The unique identifier of the conversation.</param>
/// <param name="message">The message to be sent.</param>
/// <param name="cancellationToken">An optional token to cancel the asynchronous operation.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a <see cref="Result{MessageView}"/> that encapsulates the result of the operation.</returns>
public Task<Result<MessageView>> SendMessageAsync(string convoId, string message, CancellationToken cancellationToken = default)
{
var url = $"{Constants.Urls.Bluesky.Chat.Convo.SendMessage}";
var createMessage = new CreateMessage(convoId, new CreateMessageMessage(message));
var headers = new Dictionary<string, string>
{
{ Constants.ATProtoProxy.Proxy, Constants.ATProtoProxy.BskyChat },
};

return this.Client.Post<CreateMessage, MessageView>(url, this.Options.SourceGenerationContext.CreateMessage, this.Options.SourceGenerationContext.MessageView, this.Options.JsonSerializerOptions, createMessage, cancellationToken, this.Options.Logger, headers);
}
}
23 changes: 23 additions & 0 deletions src/FishyFlip/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,29 @@ public static class ActorTypes
public const string SavedFeedsPref = "app.bsky.actor.defs#savedFeedsPref";
}

public static class ConversationTypes
{
public const string MessageView = "chat.bsky.convo.defs#messageView";
}

public static class DeclarationTypes
{
public const string AllowIncomingPref = "chat.bsky.actor.declaration";
}

public static class ATProtoProxy
{
public const string Proxy = "atproto-proxy";
public const string BskyChat = "did:web:api.bsky.chat#bsky_chat";
}

public static class AllowIncomingTypes
{
public const string Following = "following";
public const string All = "all";
public const string None = "none";
}

public static class GraphTypes
{
public const string ListItem = "app.bsky.graph.listitem";
Expand Down
29 changes: 29 additions & 0 deletions src/FishyFlip/Models/AllowIncomingPref.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// <copyright file="AllowIncomingPref.cs" company="Drastic Actions">
// Copyright (c) Drastic Actions. All rights reserved.
// </copyright>

namespace FishyFlip.Models;

/// <summary>
/// Adult Content Preference.
/// </summary>
public class AllowIncomingPref : ATRecord
{
/// <summary>
/// Initializes a new instance of the <see cref="AllowIncomingPref"/> class.
/// </summary>
/// <param name="allowIncoming">Type of chat messages to allow.</param>
/// <param name="type">ATRecord Type.</param>
[JsonConstructor]
public AllowIncomingPref(string allowIncoming, string? type = default)
: base(type)
{
this.AllowIncoming = allowIncoming;
this.Type = type ?? Constants.DeclarationTypes.AllowIncomingPref;
}

/// <summary>
/// Gets a value the type of chat messages to allow.
/// </summary>
public string AllowIncoming { get; }
}
11 changes: 11 additions & 0 deletions src/FishyFlip/Models/ChatSender.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// <copyright file="ChatSender.cs" company="Drastic Actions">
// Copyright (c) Drastic Actions. All rights reserved.
// </copyright>

namespace FishyFlip.Models;

/// <summary>
/// Chat Sender.
/// </summary>
/// <param name="Did">ATDid.</param>
public record ChatSender(ATDid Did);
10 changes: 10 additions & 0 deletions src/FishyFlip/Models/Conversation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// <copyright file="Conversation.cs" company="Drastic Actions">
// Copyright (c) Drastic Actions. All rights reserved.
// </copyright>

namespace FishyFlip.Models;

/// <summary>
/// Bluesky Conversation.
/// </summary>
public record Conversation(string Id, string Rev, int UnreadCount, bool Muted);
12 changes: 12 additions & 0 deletions src/FishyFlip/Models/ConversationList.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// <copyright file="ConversationList.cs" company="Drastic Actions">
// Copyright (c) Drastic Actions. All rights reserved.
// </copyright>

namespace FishyFlip.Models;

/// <summary>
/// Represents a list of conversations.
/// </summary>
/// <param name="Convos">An array of Conversation objects.</param>
/// <param name="Cursor">An optional string that represents the cursor position in the list. Default is an empty string.</param>
public record ConversationList(Conversation[] Convos, string Cursor = "");
12 changes: 12 additions & 0 deletions src/FishyFlip/Models/ConversationMessages.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// <copyright file="ConversationMessages.cs" company="Drastic Actions">
// Copyright (c) Drastic Actions. All rights reserved.
// </copyright>

namespace FishyFlip.Models;

/// <summary>
/// Represents a collection of messages in a conversation.
/// </summary>
/// <param name="Messages">An array of <see cref="MessageView"/> instances representing the messages in the conversation.</param>
/// <param name="Cursor">A string representing the cursor for pagination in the conversation.</param>
public record ConversationMessages(MessageView[] Messages, string Cursor);
11 changes: 11 additions & 0 deletions src/FishyFlip/Models/ConversationView.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// <copyright file="ConversationView.cs" company="Drastic Actions">
// Copyright (c) Drastic Actions. All rights reserved.
// </copyright>

namespace FishyFlip.Models;

/// <summary>
/// The Conversation View.
/// </summary>
/// <param name="Convo">The conversation.</param>
public record ConversationView(Conversation Convo);
12 changes: 12 additions & 0 deletions src/FishyFlip/Models/Internal/CreateMessage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// <copyright file="CreateMessage.cs" company="Drastic Actions">
// Copyright (c) Drastic Actions. All rights reserved.
// </copyright>

namespace FishyFlip.Models.Internal;

/// <summary>
/// Create Message.
/// </summary>
/// <param name="ConvoId">Conversation Id.</param>
/// <param name="Message">Message.</param>
internal record CreateMessage(string ConvoId, CreateMessageMessage Message);
11 changes: 11 additions & 0 deletions src/FishyFlip/Models/Internal/CreateMessageMessage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// <copyright file="CreateMessageMessage.cs" company="Drastic Actions">
// Copyright (c) Drastic Actions. All rights reserved.
// </copyright>

namespace FishyFlip.Models.Internal;

/// <summary>
/// Create Message, with the message.
/// </summary>
/// <param name="Text">Text of message.</param>
internal record CreateMessageMessage(string Text);
57 changes: 57 additions & 0 deletions src/FishyFlip/Models/MessageView.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// <copyright file="MessageView.cs" company="Drastic Actions">
// Copyright (c) Drastic Actions. All rights reserved.
// </copyright>

namespace FishyFlip.Models;

/// <summary>
/// Represents a view of a message in a chat conversation.
/// </summary>
public class MessageView : ATRecord
{
/// <summary>
/// Initializes a new instance of the <see cref="MessageView"/> class.
/// </summary>
/// <param name="id">The unique identifier of the message.</param>
/// <param name="rev">The revision of the message.</param>
/// <param name="sender">The sender of the message.</param>
/// <param name="text">The text content of the message.</param>
/// <param name="sentAt">The date and time when the message was sent.</param>
/// <param name="type">The type of the message. If not provided, defaults to <see cref="Constants.ConversationTypes.MessageView"/>.</param>
[JsonConstructor]
public MessageView(string id, string rev, ChatSender sender, string text, DateTime sentAt, string? type = default)
: base(type)
{
this.Id = id;
this.Rev = rev;
this.Sender = sender;
this.Text = text;
this.SentAt = sentAt;
this.Type = type ?? Constants.ConversationTypes.MessageView;
}

/// <summary>
/// Gets the unique identifier of the message.
/// </summary>
public string Id { get; }

/// <summary>
/// Gets the revision of the message.
/// </summary>
public string Rev { get; }

/// <summary>
/// Gets the sender of the message.
/// </summary>
public ChatSender Sender { get; }

/// <summary>
/// Gets the text content of the message.
/// </summary>
public string Text { get; }

/// <summary>
/// Gets the date and time when the message was sent.
/// </summary>
public DateTime SentAt { get; }
}
8 changes: 8 additions & 0 deletions src/FishyFlip/SourceGenerationContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,14 @@ namespace FishyFlip;
[JsonSerializable(typeof(ActorPreferences))]
[JsonSerializable(typeof(TagSuggestion))]
[JsonSerializable(typeof(TagSuggestions))]
[JsonSerializable(typeof(MessageView))]
[JsonSerializable(typeof(ChatSender))]
[JsonSerializable(typeof(CreateMessage))]
[JsonSerializable(typeof(CreateMessageMessage))]
[JsonSerializable(typeof(ConversationMessages))]
[JsonSerializable(typeof(Conversation))]
[JsonSerializable(typeof(ConversationView))]
[JsonSerializable(typeof(ConversationList))]
internal partial class SourceGenerationContext : JsonSerializerContext
{
}
Loading

0 comments on commit b30889c

Please sign in to comment.