diff --git a/src/SampleApp/Sample/ProcessHandler.cs b/src/SampleApp/Sample/ProcessHandler.cs index d982232..ad3b184 100644 --- a/src/SampleApp/Sample/ProcessHandler.cs +++ b/src/SampleApp/Sample/ProcessHandler.cs @@ -42,9 +42,14 @@ public async IAsyncEnumerable HandleAsync(IEnumerable messag } else if (message is ContentMessage content) { - yield return content.React("🧠"); + yield return content.React("🧠") + .WithConsoleText(":brain::fire:"); - yield return content.Reply("Spinning my digital neurons..."); + yield return content.Reply("Spinning my digital neurons...") + // Showcases how to use CLI-specific text + .WithConsoleText("[lime]Spinning[/] my :desktop_computer: :brain:..."); + + // Showcases restoring the typing indicator after a reply yield return content.Typing(); // simulate some hard work at hand, like doing some LLM-stuff :) @@ -60,6 +65,9 @@ public async IAsyncEnumerable HandleAsync(IEnumerable messag new Button("btn_good", "👍"), new Button("btn_bad", "👎")); + yield return content.Reply("[grey][italic]This is for the CLI only.[/][/] [link=https://github.com/devlooped/WhatsApp]WhatsApp Lib[/]") + .ForConsoleOnly(); + yield return content.React("✅"); } else if (message is UnsupportedMessage unsupported) diff --git a/src/WhatsApp/CompositeService.cs b/src/WhatsApp/CompositeService.cs index 362023b..ded6167 100644 --- a/src/WhatsApp/CompositeService.cs +++ b/src/WhatsApp/CompositeService.cs @@ -3,6 +3,12 @@ /// /// Allows responding to console and WhatsApp simultaneously. /// +/// +/// This combined service is only ever created whenever a WhatsApp message is sent +/// within the context window of a console-driven conversation. It allows complementing +/// the CLI thread by sending messages that can only be sent via WhatsApp, such as +/// media, documents or contacts. +/// record CompositeService : Service { public CompositeService(Service primary, Service secondary) diff --git a/src/WhatsApp/MessageExtensions.cs b/src/WhatsApp/MessageExtensions.cs index 71f4391..2e6fb55 100644 --- a/src/WhatsApp/MessageExtensions.cs +++ b/src/WhatsApp/MessageExtensions.cs @@ -37,6 +37,27 @@ public bool ConsoleOnly } } + extension(T response) where T : Response + { + /// + /// Marks the response for console use only by setting the ConsoleOnly property to . + /// + public T ForConsoleOnly() + { + response.ConsoleOnly = true; + return response; + } + + /// + /// Sets the console alternate text for the response, in case the response is intended to be sent to the CLI. + /// + public T WithConsoleText(string text) + { + response.ConsoleText = text; + return response; + } + } + /// /// Creates a reaction response for the user message. /// diff --git a/src/WhatsApp/ReactionResponse.cs b/src/WhatsApp/ReactionResponse.cs index 900b38d..7494ba1 100644 --- a/src/WhatsApp/ReactionResponse.cs +++ b/src/WhatsApp/ReactionResponse.cs @@ -11,6 +11,8 @@ /// The emoji representing the reaction to the message. public record ReactionResponse(string ServiceId, string UserNumber, string Context, string? ConversationId, string Emoji) : Response(ServiceId, UserNumber, Context, ConversationId) { + // If this variable is not null, it means the originating message came from WhatsApp + // and there's an ongoing conversation with the CLI simultaneously. readonly CompositeService? service; internal ReactionResponse(Service service, string userNumber, string context, string? conversationId, string emoji) @@ -23,8 +25,13 @@ internal ReactionResponse(Service service, string userNumber, string context, st if (service != null) await client.ReactAsync(service.Secondary.Id, UserNumber, Context, this.ConsoleText ?? Emoji, cancellationToken); + // If service is null, it's either a WhatsApp regular without CLI, or it's pure CLI. + // In both cases, we need to send the reaction to the WhatsApp service. + // The other case is if we did get a composite service, but the message is not intended for console only. if (service == null || this.ConsoleOnly != true) - await client.ReactAsync(ServiceId, UserNumber, Context, Emoji); + await client.ReactAsync(ServiceId, UserNumber, Context, + // Automatically pick the CLI version of the text if sending to the CLI + ServiceId.IsCLI() ? this.ConsoleText ?? Emoji : Emoji); return Ulid.NewUlid().ToString(); } diff --git a/src/WhatsApp/Service.cs b/src/WhatsApp/Service.cs index b697d97..2fe8047 100644 --- a/src/WhatsApp/Service.cs +++ b/src/WhatsApp/Service.cs @@ -5,4 +5,13 @@ /// /// The identifier for the number in WhatsApp Manager. /// The phone number. -public record Service(string Id, string Number); \ No newline at end of file +public record Service(string Id, string Number); + +static class ServiceExtensions +{ + /// + /// Checks whether the given service number ID is a CLI local endpoint. + /// + public static bool IsCLI(this string serviceId) + => serviceId.StartsWith("http://localhost", StringComparison.OrdinalIgnoreCase); +} \ No newline at end of file diff --git a/src/WhatsApp/TextResponse.cs b/src/WhatsApp/TextResponse.cs index df5e182..3c35fd0 100644 --- a/src/WhatsApp/TextResponse.cs +++ b/src/WhatsApp/TextResponse.cs @@ -13,6 +13,8 @@ /// An optional second button to include in the response for user interaction. public record TextResponse(string ServiceId, string UserNumber, string Context, string? ConversationId, string Text, Button? Button1 = default, Button? Button2 = default) : Response(ServiceId, UserNumber, Context, ConversationId) { + // If this variable is not null, it means the originating message came from WhatsApp + // and there's an ongoing conversation with the CLI simultaneously. readonly CompositeService? service; internal TextResponse(Service service, string userNumber, string context, string? conversationId, string text, Button? button1 = default, Button? button2 = default) @@ -26,8 +28,13 @@ internal TextResponse(Service service, string userNumber, string context, string if (service != null) id = await SendReplyAsync(client, service.Secondary.Id, this.ConsoleText ?? Text, cancellation); + // If service is null, it's either a WhatsApp regular without CLI, or it's pure CLI. + // In both cases, we need to send the reaction to the WhatsApp service. + // The other case is if we did get a composite service, but the message is not intended for console only. if (service == null || this.ConsoleOnly != true) - return await SendReplyAsync(client, ServiceId, Text, cancellation); + return await SendReplyAsync(client, ServiceId, + // Automatically pick the CLI version of the text if sending to the CLI + ServiceId.IsCLI() ? this.ConsoleText ?? Text : Text, cancellation); return id; } diff --git a/src/WhatsApp/WhatsAppClient.cs b/src/WhatsApp/WhatsAppClient.cs index 42ac4ba..ed1eeb8 100644 --- a/src/WhatsApp/WhatsAppClient.cs +++ b/src/WhatsApp/WhatsAppClient.cs @@ -56,8 +56,7 @@ public HttpClient CreateHttp(string numberId) if (!options.Numbers.TryGetValue(numberId, out var token)) { // Try to reply to the debug console - if (numberId.StartsWith("http://localhost", StringComparison.OrdinalIgnoreCase) && - Uri.TryCreate(numberId, UriKind.Absolute, out var uri)) + if (numberId.IsCLI() && Uri.TryCreate(numberId, UriKind.Absolute, out var uri)) { using var httpClient = httpFactory.CreateClient();