Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/SampleApp/Sample/ProcessHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,14 @@ public async IAsyncEnumerable<Response> HandleAsync(IEnumerable<IMessage> 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 :)
Expand All @@ -60,6 +65,9 @@ public async IAsyncEnumerable<Response> HandleAsync(IEnumerable<IMessage> 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)
Expand Down
6 changes: 6 additions & 0 deletions src/WhatsApp/CompositeService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
/// <summary>
/// Allows responding to console and WhatsApp simultaneously.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
record CompositeService : Service
{
public CompositeService(Service primary, Service secondary)
Expand Down
21 changes: 21 additions & 0 deletions src/WhatsApp/MessageExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,27 @@ public bool ConsoleOnly
}
}

extension<T>(T response) where T : Response
{
/// <summary>
/// Marks the response for console use only by setting the <c>ConsoleOnly</c> property to <see langword="true"/>.
/// </summary>
public T ForConsoleOnly()
{
response.ConsoleOnly = true;
return response;
}

/// <summary>
/// Sets the console alternate text for the response, in case the response is intended to be sent to the CLI.
/// </summary>
public T WithConsoleText(string text)
{
response.ConsoleText = text;
return response;
}
}

/// <summary>
/// Creates a reaction response for the user message.
/// </summary>
Expand Down
9 changes: 8 additions & 1 deletion src/WhatsApp/ReactionResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
/// <param name="Emoji">The emoji representing the reaction to the message.</param>
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)
Expand All @@ -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();
}
Expand Down
11 changes: 10 additions & 1 deletion src/WhatsApp/Service.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,13 @@
/// </summary>
/// <param name="Id">The identifier for the number in WhatsApp Manager.</param>
/// <param name="Number">The phone number.</param>
public record Service(string Id, string Number);
public record Service(string Id, string Number);

static class ServiceExtensions
{
/// <summary>
/// Checks whether the given service number ID is a CLI local endpoint.
/// </summary>
public static bool IsCLI(this string serviceId)
=> serviceId.StartsWith("http://localhost", StringComparison.OrdinalIgnoreCase);
}
9 changes: 8 additions & 1 deletion src/WhatsApp/TextResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
/// <param name="Button2">An optional second button to include in the response for user interaction.</param>
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)
Expand All @@ -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;
}
Expand Down
3 changes: 1 addition & 2 deletions src/WhatsApp/WhatsAppClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down