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
16 changes: 15 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,21 @@ in production, the CLI endpoint is not available. Example:

![](https://raw.githubusercontent.com/devlooped/WhatsApp/main/assets/img/cli.png)

The console will automatically remember the last used WhatsApp endpoint, and uses YAML if possible
The console will automatically remember the last used WhatsApp endpoint, output format and simulated
user phone number.

```bash
Usage: whatsapp [OPTIONS]+
Options:
-u, --url WhatsApp functions endpoint
-n, --number=VALUE Your WhatsApp user phone number
-j, --json Format output as JSON
-t, --text Format output as text
-y, --yaml Format output as YAML
-?, -h, --help Display this help.
-v, --version Render tool version and updates.
```

to render the responses since it provides a more readable format than JSON.

<!-- #cli -->
Expand Down
1 change: 1 addition & 0 deletions src/Console/Console.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.1.0" />
<PackageReference Include="Mono.Options" Version="6.12.0.148" />
<PackageReference Include="YamlDotNet" Version="16.3.0" />
<PackageReference Include="Spectre.Console.Json" Version="0.50.0" />
<PackageReference Include="ThisAssembly.Git" Version="2.0.14" PrivateAssets="all" />
Expand Down
21 changes: 21 additions & 0 deletions src/Console/ConsoleOption.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Mono.Options;

namespace Devlooped.WhatsApp;

class ConsoleOption : OptionSet
{
public ConsoleOption() =>
Add("u|url", "WhatsApp functions endpoint", u => Endpoint = u)
.Add("n|number=", "Your WhatsApp user phone number", n => Number = ParseNumber(n))
.Add("j|json", "Format output as JSON", _ => Format = OutputFormat.Json)
.Add("t|text", "Format output as text", _ => Format = OutputFormat.Text)
.Add("y|yaml", "Format output as YAML", _ => Format = OutputFormat.Yaml);

public string? Endpoint { get; private set; }

public OutputFormat? Format { get; private set; }

public int? Number { get; private set; }

static int ParseNumber(string value) => int.Parse([.. value.Where(char.IsDigit)]);
Copy link

Copilot AI Jun 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider adding validation or using TryParse to handle cases where the input string might not contain any digits, avoiding potential runtime exceptions.

Suggested change
static int ParseNumber(string value) => int.Parse([.. value.Where(char.IsDigit)]);
static int? ParseNumber(string value)
{
var digits = new string(value.Where(char.IsDigit).ToArray());
if (string.IsNullOrEmpty(digits))
return null;
return int.TryParse(digits, out var result) ? result : null;
}

Copilot uses AI. Check for mistakes.
}
49 changes: 24 additions & 25 deletions src/Console/Interactive.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,45 +11,44 @@

namespace Devlooped.WhatsApp;

enum RenderMode
{
Yaml,
Json,
Text,
}

[Service]
class Interactive(IConfiguration configuration, IHttpClientFactory httpFactory) : IHostedService
{
readonly CancellationTokenSource cts = new();

string? serviceEndpoint = configuration["WhatsApp:Endpoint"];
string? service = configuration["whatsapp:endpoint"];
string? number = configuration["whatsapp:number"];
OutputFormat? format = Enum.TryParse<OutputFormat>(configuration["whatsApp:format"], true, out var value) ? value : null;
string? clientEndpoint;
HttpListener? listener;
RenderMode mode = RenderMode.Text;
bool needsNewline = true;

public Task StartAsync(CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(serviceEndpoint))
if (string.IsNullOrEmpty(service))
{
service = AnsiConsole.Ask("Enter WhatsApp functions endpoint", "http://localhost:4242/whatsappcli");
Config.Build(ConfigLevel.Global)
.SetString("whatsapp", "endpoint", service);
}
if (format == null)
{
serviceEndpoint = AnsiConsole.Ask("Enter WhatsApp functions endpoint", "http://localhost:4242/whatsappcli");
var choices = Enum.GetValues<MessageType>();
format = AnsiConsole.Prompt(
new SelectionPrompt<OutputFormat>()
.Title("Select output format")
.AddChoices([OutputFormat.Text, OutputFormat.Yaml, OutputFormat.Json]));

Config.Build(ConfigLevel.Global)
.SetString("WhatsApp", "Endpoint", serviceEndpoint);
.SetString("whatsapp", "format", format.ToString()!.ToLowerInvariant());
}
else if (!AnsiConsole.Confirm($"Use WhatsApp functions endpoint [link]{serviceEndpoint}[/]"))
if (number == null)
{
serviceEndpoint = AnsiConsole.Ask("Enter WhatsApp functions endpoint", "http://localhost:4242/whatsappcli");
number = AnsiConsole.Ask<long>("Enter WhatsApp user phone number", 987654321).ToString();
Config.Build(ConfigLevel.Global)
.SetString("WhatsApp", "Endpoint", serviceEndpoint);
.SetString("whatsapp", "number", number);
}

var choices = Enum.GetValues<MessageType>();
mode = AnsiConsole.Prompt(
new SelectionPrompt<RenderMode>()
.Title("Select render mode")
.AddChoices([RenderMode.Text, RenderMode.Yaml, RenderMode.Json]));

listener = new HttpListener();
// Attempt to grab the first free port we can find on localhost
while (true)
Expand Down Expand Up @@ -97,15 +96,15 @@ async Task InputListener()
var message = new ContentMessage(
Id: Ulid.NewUlid().ToString(),
Service: new Service(clientEndpoint!, "123456789"),
User: new User("Console", "987654321"),
User: new User("Console", number ?? "987654321"),
Timestamp: DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
Content: new TextContent(input)
);

using var httpClient = httpFactory.CreateClient("whatsapp");
var payload = JsonSerializer.Serialize(message, JsonContext.Default.Message);

var response = await httpClient.PostAsync(serviceEndpoint, new StringContent(payload, Encoding.UTF8, "application/json"));
var response = await httpClient.PostAsync(service, new StringContent(payload, Encoding.UTF8, "application/json"));
if (!response.IsSuccessStatusCode)
{
AnsiConsole.MarkupLine($"[red] Failed to send message.[/] [bold]Status Code:[/] {response.StatusCode}");
Expand Down Expand Up @@ -164,7 +163,7 @@ async Task RenderAsync(string json)
AnsiConsole.WriteLine();

// Try to parse the request body as a dictionary and render it as YAML
if (mode == RenderMode.Yaml &&
if (format == OutputFormat.Yaml &&
DictionaryConverter.Parse(json) is { } dictionary &&
DictionaryConverter.ToYaml(dictionary) is { Length: > 0 } payload)
{
Expand All @@ -175,7 +174,7 @@ async Task RenderAsync(string json)
return;
}

if (mode == RenderMode.Text)
if (format == OutputFormat.Text)
{
try
{
Expand Down
8 changes: 8 additions & 0 deletions src/Console/OutputFormat.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Devlooped.WhatsApp;

enum OutputFormat
{
Yaml,
Json,
Text,
}
49 changes: 34 additions & 15 deletions src/Console/Program.cs
Original file line number Diff line number Diff line change
@@ -1,38 +1,57 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using Devlooped.WhatsApp;
using DotNetConfig;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Spectre.Console;

// Some users reported not getting emoji on Windows, so we force UTF-8 encoding.
// This not great, but I couldn't find a better way to do it.
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
Console.InputEncoding = Console.OutputEncoding = Encoding.UTF8;

// Alias -? to -h for help
if (args.Contains("-?"))
args = [.. args.Select(x => x == "-?" ? "-h" : x)];
var debug = false;
var help = false;
var version = false;

if (args.Contains("--debug"))
var options = new ConsoleOption
{
{ "?|h|help", "Display this help.", h => help = h != null },
{ "d|debug", "Debug the WhatsApp CLI.", d => debug = d != null, true },
{ "v|version", "Render tool version and updates.", v => version = v != null },
};

options.Parse(args);

if (debug)
Debugger.Launch();
args = [.. args.Where(x => x != "--debug")];

if (help)
{
AnsiConsole.MarkupLine("Usage: [green]whatsapp[/] [grey][[OPTIONS]]+[/]");
AnsiConsole.WriteLine("Options:");
options.WriteOptionDescriptions(Console.Out);
return 0;
}

if (options.Endpoint != null || options.Number != null || options.Format != null)
{
var config = Config.Build(ConfigLevel.Global);
if (options.Endpoint != null)
config = config.SetString("whatsapp", "endpoint", options.Endpoint);
if (options.Number != null)
config = config.SetNumber("whatsapp", "number", (long)options.Number);
if (options.Format != null)
config = config.SetString("whatsapp", "format", options.Format.ToString()!.ToLowerInvariant());
}

var host = Host.CreateApplicationBuilder(args);
host.Logging.ClearProviders();

host.Configuration.AddInMemoryCollection(new Dictionary<string, string?>()
{
{ "Logging:LogLevel:Default", "Information" },
{ "Logging:LogLevel:Microsoft", "Warning" },
{ "Logging:LogLevel:Microsoft.Hosting", "Warning" },
{ "Logging:LogLevel:System.Net.Http", "Warning" },
{ "Logging:LogLevel:Polly", "Warning" },
});

host.Configuration.AddDotNetConfig();
host.Configuration.AddUserSecrets<Program>();

Expand All @@ -54,7 +73,7 @@

var app = host.Build();

if (args.Contains("--version"))
if (version)
{
app.ShowVersion();
await app.ShowUpdatesAsync();
Expand Down
2 changes: 1 addition & 1 deletion src/Console/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"profiles": {
"Console": {
"commandName": "Project",
"commandLineArgs": ""
"commandLineArgs": "-h"
}
}
}