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
8 changes: 4 additions & 4 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Create agents for WhatsApp using Azure Functions.
var builder = FunctionsApplication.CreateBuilder(args);
builder.ConfigureFunctionsWebApplication();

builder.UseWhatsApp<MyWhatsAppHandler>();
builder.Services.AddWhatsApp<MyWhatsAppHandler>();

builder.Build().Run();
```
Expand All @@ -25,7 +25,7 @@ Instead of providing an `IWhatsAppHandler` implementation, you can also
register an inline handler using minimal API style:

```csharp
builder.UseWhatsApp(message =>
builder.Services.AddWhatsApp(message =>
{
// MessageType: Content | Error | Interactive | Status
Console.WriteLine($"Got message type {message.Type}");
Expand Down Expand Up @@ -53,7 +53,7 @@ If the handler needs additional services, they can be provided directly
as generic parameters of the `UseWhatsApp` method, such as:

```csharp
builder.UseWhatsApp<IWhatsAppClient, ILogger<Program>>(async (client, logger, message) =>
builder.Services.AddWhatsApp<IWhatsAppClient, ILogger<Program>>(async (client, logger, message) =>
{
logger.LogInformation($"Got message type {message.Type}");
// Reply to an incoming content message, for example.
Expand All @@ -66,7 +66,7 @@ You can also specify the parameter types in the delegate itself and avoid the
generic parameters:

```csharp
builder.UseWhatsApp(async (IWhatsAppClient client, ILogger<Program> logger, Message message) =>
builder.Services.AddWhatsApp(async (IWhatsAppClient client, ILogger<Program> logger, Message message) =>
```

The provided `IWhatsAppClient` provides a very thin abstraction allowing you to send
Expand Down
121 changes: 55 additions & 66 deletions src/Sample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,80 +32,69 @@
WriteIndented = true
});

builder.UseWhatsApp<IWhatsAppClient, ILogger<Program>, JsonSerializerOptions>(async (client, logger, options, message, cancellation) =>
{
logger.LogInformation("💬 Received message: {Message}", message);

if (message is ErrorMessage error)
builder.Services
.AddWhatsApp<IWhatsAppClient, ILogger<Program>, JsonSerializerOptions>(async (client, logger, options, message, cancellation) =>
{
// Reengagement error, we need to invite the user.
if (error.Error.Code == 131047)
logger.LogInformation("💬 Received message: {Message}", message);

if (message is ErrorMessage error)
{
await client.SendAsync(error.To.Id, new
// Reengagement error, we need to invite the user.
if (error.Error.Code == 131047)
{
messaging_product = "whatsapp",
to = error.From.Number,
type = "template",
template = new
await client.SendAsync(error.To.Id, new
{
name = "reengagement",
language = new
messaging_product = "whatsapp",
to = error.From.Number,
type = "template",
template = new
{
code = "es_AR"
name = "reengagement",
language = new
{
code = "es_AR"
}
}
}
});
});
}
else
{
logger.LogWarning("⚠️ Unknown error message received: {Error}", message);
}
return;
}
else
else if (message is InteractiveMessage interactive)
{
logger.LogWarning("⚠️ Unknown error message received: {Error}", message);
logger.LogWarning("👤 chose {Button} ({Title})", interactive.Button.Id, interactive.Button.Title);
await client.ReplyAsync(interactive, $"👤 chose: {interactive.Button.Title} ({interactive.Button.Id})");
return;
}
return;
}
else if (message is InteractiveMessage interactive)
{
logger.LogWarning("👤 chose {Button} ({Title})", interactive.Button.Id, interactive.Button.Title);
await client.ReplyAsync(interactive, $"👤 chose: {interactive.Button.Title} ({interactive.Button.Id})");
return;
}
else if (message is ReactionMessage reaction)
{
logger.LogInformation("👤 reaction: {Reaction}", reaction.Emoji);
await client.ReplyAsync(reaction, $"👤 reaction: {reaction.Emoji}");
return;
}
else if (message is StatusMessage status)
{
logger.LogInformation("☑️ status: {Status}", status.Status);
return;
}
else if (message is ContentMessage content)
{
await client.ReactAsync(content, "🧠");
// simulate some hard work at hand, like doing some LLM-stuff :)
//await Task.Delay(2000);
await client.ReplyAsync(content, $"☑️ Got your {content.Content.Type}:\r\n{JsonSerializer.Serialize(content, options)}",
new Button("btn_good", "👍"),
new Button("btn_bad", "👎"));
}
else if (message is UnsupportedMessage unsupported)
{
logger.LogWarning("⚠️ {Message}", unsupported);
return;
}
});

builder.Services.AddMemoryCache();
builder.Services.AddDistributedAzureTableStorageCache(options =>
{
options.PartitionKey = "SampleCache";
options.TableName = "SampleCache";
options.CreateTableIfNotExists = true;
options.ConnectionString = builder.Configuration["AzureWebJobsStorage"];
});
builder.Services.AddHybridCache(options =>
{
options.DefaultEntryOptions = new() { Expiration = TimeSpan.FromDays(180) };
});
else if (message is ReactionMessage reaction)
{
logger.LogInformation("👤 reaction: {Reaction}", reaction.Emoji);
await client.ReplyAsync(reaction, $"👤 reaction: {reaction.Emoji}");
return;
}
else if (message is StatusMessage status)
{
logger.LogInformation("☑️ status: {Status}", status.Status);
return;
}
else if (message is ContentMessage content)
{
await client.ReactAsync(content, "🧠");
// simulate some hard work at hand, like doing some LLM-stuff :)
//await Task.Delay(2000);
await client.ReplyAsync(content, $"☑️ Got your {content.Content.Type}:\r\n{JsonSerializer.Serialize(content, options)}",
new Button("btn_good", "👍"),
new Button("btn_bad", "👎"));
}
else if (message is UnsupportedMessage unsupported)
{
logger.LogWarning("⚠️ {Message}", unsupported);
return;
}
})
.UseLogging();

builder.Build().Run();
1 change: 0 additions & 1 deletion src/Sample/Sample.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.0.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="2.0.1" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.0.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="9.5.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WhatsApp\WhatsApp.csproj" />
Expand Down
7 changes: 3 additions & 4 deletions src/Sample/TestFunction.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Caching.Hybrid;

namespace Devlooped.WhatsApp;

public class TestFunction(HybridCache cache)
public class TestFunction
{
[Function("test")]
public async Task<IActionResult> RunAsync([HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequest req)
public static IActionResult Run([HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequest req)
{
var value = await cache.GetOrCreateAsync("test", entry => ValueTask.FromResult(Guid.NewGuid().ToString()));
var value = Guid.NewGuid().ToString();

return new OkObjectResult("Running: " + value);
}
Expand Down
20 changes: 15 additions & 5 deletions src/Tests/PipelineTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,25 +38,35 @@ public async Task CanBuildLoggingPipeline()
{
var after = false;
var before = false;
var target = true;

var pipeline = new WhatsAppHandlerBuilder()
.Use((message, inner, cancellation) =>
var pipeline = new WhatsAppHandlerBuilder(
services => AnonymousWhatsAppHandler.Create(services, (messages, cancellation) =>
{
after = true;
Assert.True(before);
Assert.True(target);
target = true;
return Task.CompletedTask;
}))
.Use((message, inner, cancellation) =>
{
before = true;
Assert.False(after);
return inner.HandleAsync(message, cancellation);
})
.UseLogging(output.AsLoggerFactory())
.Use((message, inner, cancellation) =>
{
before = true;
Assert.False(after);
Assert.True(before);
after = true;
return inner.HandleAsync(message, cancellation);
})
.Build();

await pipeline.HandleAsync(new ReactionMessage("1234", service, user, 0, "🗽"));

Assert.True(before);
Assert.True(after);
Assert.True(target);
}
}
21 changes: 21 additions & 0 deletions src/WhatsApp/AnonymousDelegatingWhatsAppHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@

namespace Devlooped.WhatsApp;

/// <summary>
/// Represents a delegating handler that wraps an inner handler with implementation provided by a delegate.
/// </summary>
/// <remarks>
/// Initializes a new instance of the <see cref="AnonymousDelegatingChatClient"/> class.
/// </remarks>
/// <param name="innerHandler">The inner handler.</param>
/// <param name="handlerFunc">A delegate that provides the implementation for <see cref="HandleAsync"/></param>
class AnonymousDelegatingWhatsAppHandler(
IWhatsAppHandler innerHandler,
Func<IEnumerable<Message>, IWhatsAppHandler, CancellationToken, Task> handlerFunc) : DelegatingWhatsAppHandler(innerHandler)
{
/// <summary>The delegate to use as the implementation of <see cref="Handle"/>.</summary>
readonly Func<IEnumerable<Message>, IWhatsAppHandler, CancellationToken, Task> handlerFunc = Throw.IfNull(handlerFunc);

public override Task HandleAsync(IEnumerable<Message> messages, CancellationToken cancellation = default)
=> handlerFunc(messages, InnerHandler, cancellation);
}
112 changes: 97 additions & 15 deletions src/WhatsApp/AnonymousWhatsAppHandler.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,106 @@

namespace Devlooped.WhatsApp;
namespace Devlooped.WhatsApp;

/// <summary>
/// Represents a delegating handler that wraps an inner handler with implementation provided by a delegate.
/// A handler that wraps an inner handler with implementation provided by a delegate.
/// </summary>
class AnonymousWhatsAppHandler : DelegatingWhatsAppHandler
public class AnonymousWhatsAppHandler : IWhatsAppHandler
{
/// <summary>The delegate to use as the implementation of <see cref="Handle"/>.</summary>
readonly Func<IEnumerable<Message>, IWhatsAppHandler, CancellationToken, Task> handlerFunc;
readonly IServiceProvider services;
readonly Func<IServiceProvider, IEnumerable<Message>, CancellationToken, Task> handler;

AnonymousWhatsAppHandler(IServiceProvider services, Func<IServiceProvider, IEnumerable<Message>, CancellationToken, Task> handler)
=> (this.services, this.handler) = (Throw.IfNull(services), Throw.IfNull(handler));

AnonymousWhatsAppHandler(IServiceProvider services, Func<IEnumerable<Message>, CancellationToken, Task> handler)
: this(services, (_, messages, cancellation) => handler(messages, cancellation)) { }

/// <inheritdoc />
public Task HandleAsync(IEnumerable<Message> messages, CancellationToken cancellation = default)
=> handler(services, messages, cancellation);

/// <summary>
/// Creates a new instance of an <see cref="IWhatsAppHandler"/> with the specified service provider and message
/// handler.
/// </summary>
public static IWhatsAppHandler Create(IServiceProvider services, Func<IEnumerable<Message>, CancellationToken, Task> handler)
=> new AnonymousWhatsAppHandler(services, handler);

/// <summary>
/// Creates a new instance of an <see cref="IWhatsAppHandler"/> with the specified service provider and message
/// handler.
/// </summary>
public static IWhatsAppHandler Create(IServiceProvider services, Func<IServiceProvider, IEnumerable<Message>, CancellationToken, Task> handler)
=> new AnonymousWhatsAppHandler(services, handler);

/// <summary>
/// Creates a new instance of an <see cref="IWhatsAppHandler"/> using the specified service and message handler
/// function.
/// </summary>
public static IWhatsAppHandler Create<TService>(TService service, Func<TService, IEnumerable<Message>, CancellationToken, Task> handler)
=> new AnonymousWhatsAppHandler1<TService>(service, handler);

/// <summary>
/// Creates a new instance of a WhatsApp message handler that processes messages using the specified services and
/// handler function.
/// </summary>
public static IWhatsAppHandler Create<TService1, TService2>(TService1 service1, TService2 service2, Func<TService1, TService2, IEnumerable<Message>, CancellationToken, Task> handler)
=> new AnonymousWhatsAppHandler2<TService1, TService2>(service1, service2, handler);

/// <summary>
/// Initializes a new instance of the <see cref="AnonymousDelegatingChatClient"/> class.
/// Creates a new instance of a WhatsApp message handler that processes messages using the specified services and
/// handler function.
/// </summary>
/// <param name="innerHandler">The inner handler.</param>
/// <param name="handlerFunc">A delegate that provides the implementation for <see cref="HandleAsync"/></param>
public AnonymousWhatsAppHandler(
IWhatsAppHandler innerHandler,
Func<IEnumerable<Message>, IWhatsAppHandler, CancellationToken, Task> handlerFunc) : base(innerHandler)
=> this.handlerFunc = Throw.IfNull(handlerFunc);
public static IWhatsAppHandler Create<TService1, TService2, TService3>(TService1 service1, TService2 service2, TService3 service3, Func<TService1, TService2, TService3, IEnumerable<Message>, CancellationToken, Task> handler)
=> new AnonymousWhatsAppHandler3<TService1, TService2, TService3>(service1, service2, service3, handler);

/// <summary>
/// Creates a new instance of a WhatsApp message handler that processes messages using the specified services and
/// handler function.
/// </summary>
public static IWhatsAppHandler Create<TService1, TService2, TService3, TService4>(TService1 service1, TService2 service2, TService3 service3, TService4 service4, Func<TService1, TService2, TService3, TService4, IEnumerable<Message>, CancellationToken, Task> handler)
=> new AnonymousWhatsAppHandler4<TService1, TService2, TService3, TService4>(service1, service2, service3, service4, handler);

/// <summary>
/// Creates a new instance of a WhatsApp message handler that processes messages using the specified services and
/// handler function.
/// </summary>
public static IWhatsAppHandler Create<TService1, TService2, TService3, TService4, TService5>(TService1 service1, TService2 service2, TService3 service3, TService4 service4, TService5 service5, Func<TService1, TService2, TService3, TService4, TService5, IEnumerable<Message>, CancellationToken, Task> handler)
=> new AnonymousWhatsAppHandler5<TService1, TService2, TService3, TService4, TService5>(service1, service2, service3, service4, service5, handler);

/// <summary>
/// Creates a new instance of a WhatsApp message handler that processes messages using the specified services and
/// handler function.
/// </summary>
public static IWhatsAppHandler Create<TService1, TService2, TService3, TService4, TService5, TService6>(TService1 service1, TService2 service2, TService3 service3, TService4 service4, TService5 service5, TService6 service6, Func<TService1, TService2, TService3, TService4, TService5, TService6, IEnumerable<Message>, CancellationToken, Task> handler)
=> new AnonymousWhatsAppHandler6<TService1, TService2, TService3, TService4, TService5, TService6>(service1, service2, service3, service4, service5, service6, handler);

class AnonymousWhatsAppHandler1<TService>(TService service, Func<TService, IEnumerable<Message>, CancellationToken, Task> handler) : IWhatsAppHandler
{
public Task HandleAsync(IEnumerable<Message> messages, CancellationToken cancellation = default) => handler(service, messages, cancellation);
}

class AnonymousWhatsAppHandler2<TService1, TService2>(TService1 service1, TService2 service2, Func<TService1, TService2, IEnumerable<Message>, CancellationToken, Task> handler) : IWhatsAppHandler
{
public Task HandleAsync(IEnumerable<Message> messages, CancellationToken cancellation = default) => handler(service1, service2, messages, cancellation);
}

class AnonymousWhatsAppHandler3<TService1, TService2, TService3>(TService1 service1, TService2 service2, TService3 service3, Func<TService1, TService2, TService3, IEnumerable<Message>, CancellationToken, Task> handler) : IWhatsAppHandler
{
public Task HandleAsync(IEnumerable<Message> messages, CancellationToken cancellation = default) => handler(service1, service2, service3, messages, cancellation);
}

class AnonymousWhatsAppHandler4<TService1, TService2, TService3, TService4>(TService1 service1, TService2 service2, TService3 service3, TService4 service4, Func<TService1, TService2, TService3, TService4, IEnumerable<Message>, CancellationToken, Task> handler) : IWhatsAppHandler
{
public Task HandleAsync(IEnumerable<Message> messages, CancellationToken cancellation = default) => handler(service1, service2, service3, service4, messages, cancellation);
}

class AnonymousWhatsAppHandler5<TService1, TService2, TService3, TService4, TService5>(TService1 service1, TService2 service2, TService3 service3, TService4 service4, TService5 service5, Func<TService1, TService2, TService3, TService4, TService5, IEnumerable<Message>, CancellationToken, Task> handler) : IWhatsAppHandler
{
public Task HandleAsync(IEnumerable<Message> messages, CancellationToken cancellation = default) => handler(service1, service2, service3, service4, service5, messages, cancellation);
}

public override Task HandleAsync(IEnumerable<Message> messages, CancellationToken cancellation = default)
=> handlerFunc(messages, InnerHandler, cancellation);
class AnonymousWhatsAppHandler6<TService1, TService2, TService3, TService4, TService5, TService6>(TService1 service1, TService2 service2, TService3 service3, TService4 service4, TService5 service5, TService6 service6, Func<TService1, TService2, TService3, TService4, TService5, TService6, IEnumerable<Message>, CancellationToken, Task> handler) : IWhatsAppHandler
{
public Task HandleAsync(IEnumerable<Message> messages, CancellationToken cancellation = default) => handler(service1, service2, service3, service4, service5, service6, messages, cancellation);
}
}
Loading