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
1 change: 1 addition & 0 deletions src/Directory.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<RootNamespace>Devlooped.WhatsApp</RootNamespace>
<UserSecretsId>41fc668e-a410-48d4-9884-c2937478d9e1</UserSecretsId>
<PackageLicenseExpression>AGPL-3.0-or-later WITH Universal-FOSS-exception-1.0</PackageLicenseExpression>
<ImplicitUsings>true</ImplicitUsings>
</PropertyGroup>

</Project>
34 changes: 23 additions & 11 deletions src/Sample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,29 @@
using Devlooped.WhatsApp;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

var builder = FunctionsApplication.CreateBuilder(args);
var options = new JsonSerializerOptions(JsonSerializerDefaults.General)
builder.ConfigureFunctionsWebApplication();

#if DEBUG
builder.Environment.EnvironmentName = "Development";
builder.Configuration.AddUserSecrets<Program>();
#endif
builder.Services.AddSingleton(new JsonSerializerOptions(JsonSerializerDefaults.General)
{
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
Converters =
{
new JsonStringEnumConverter()
},
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
WriteIndented = true
};

builder.ConfigureFunctionsWebApplication();
builder.Configuration.AddUserSecrets<Program>();
});

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

Expand Down Expand Up @@ -52,24 +57,31 @@
}
else if (message is InteractiveMessage interactive)
{
logger.LogWarning("👤 User chose button {Button} ({Title})", interactive.Button.Id, interactive.Button.Title);
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("☑️ New message status: {Status}", status.Status);
logger.LogInformation("☑️ status: {Status}", status.Status);
return;
}
else if (message is ContentMessage content)
{
await client.ReactAsync(message, "🧠");
await client.ReactAsync(content, "🧠");
// simulate some hard work at hand, like doing some LLM-stuff :)
//await Task.Delay(2000);
await client.ReplyAsync(message, $"☑️ Got your {content.Content.Type}:\r\n{JsonSerializer.Serialize(content, options)}");
await client.ReplyAsync(content, $"☑️ Got your {content.Content.Type}:\r\n{JsonSerializer.Serialize(content, options)}");
}
else if (message is UnsupportedMessage unsupported)
{
await client.ReactAsync(message, "⚠️");
logger.LogWarning("⚠️ {Message}", unsupported);
return;
}
});
Expand Down
2 changes: 0 additions & 2 deletions src/Sample/Sample.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
<TargetFramework>net8.0</TargetFramework>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
Expand Down
38 changes: 38 additions & 0 deletions src/Tests/Content/WhatsApp/Reaction.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"object": "whatsapp_business_account",
"entry": [
{
"id": "123456789012345",
"changes": [
{
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "5491234567890",
"phone_number_id": "987654321098765"
},
"contacts": [
{
"profile": { "name": "RandomName" },
"wa_id": "5499876543210"
}
],
"messages": [
{
"from": "5499876543210",
"id": "wamid.HBgNMTIzNDU2Nzg5MDEyMzQ1MhUCABEYEkY5QzQxNDNBQjgyRkVENEIzMQA=",
"timestamp": "1744229999",
"type": "reaction",
"reaction": {
"message_id": "wamid.HBgNMTIzNDU2Nzg5MDEyMzQ1MhUCABEYEkY5QzQxNDNBQjgyRkVENEIzMQA=",
"emoji": "😊"
}
}
]
},
"field": "messages"
}
]
}
]
}
37 changes: 37 additions & 0 deletions src/Tests/Content/WhatsApp/StatusDelivered.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"object": "whatsapp_business_account",
"entry": [
{
"id": "918273645102347",
"changes": [
{
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "1209384756109",
"phone_number_id": "739102584617293"
},
"statuses": [
{
"id": "wamid.HBgMMTIwMzQ1Njc4OTAxNxUCABEYEjZFNzI5QzFDNkE5RDg3MjBBNwA=",
"status": "sent",
"timestamp": "1829471036",
"recipient_id": "1203456789012",
"conversation": {
"id": "4a7b9c2d8e5f0136pqr890xy12mn3op",
"origin": { "type": "utility" }
},
"pricing": {
"billable": false,
"pricing_model": "NBP",
"category": "utility"
}
}
]
},
"field": "messages"
}
]
}
]
}
4 changes: 4 additions & 0 deletions src/Tests/Content/WhatsApp/Text.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
],
"messages": [
{
"context": {
"from": "12025550123",
"id": "wamid.HBgNNTQ5MTE1OTL4ODI4MhUCBBEYEjUxNDI3NkMzRkI1ODVCRTgwOAA="
},
"from": "12029874563",
"id": "wamid.HBgNMTIwMjk4NzQ1NjM1NhUCABIYFjQ5RjE4QzJEMzU2ODk3QTJFMUY3RDEyMjNBNkI5QwA==",
"timestamp": "1678902345",
Expand Down
2 changes: 1 addition & 1 deletion src/Tests/WhatsAppClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public async Task SendsMessageAsync()
{
var (configuration, client) = Initialize();

await client.SendAync(configuration["SendFrom"]!, configuration["SendTo"]!, "Hi there!");
await client.SendAsync(configuration["SendFrom"]!, configuration["SendTo"]!, "Hi there!");
}

[SecretsFact("Meta:VerifyToken", "SendFrom", "SendTo")]
Expand Down
81 changes: 41 additions & 40 deletions src/Tests/WhatsAppModelTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Metadata;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
Expand All @@ -13,48 +14,28 @@ namespace Devlooped.WhatsApp;
public class WhatsAppModelTests(ITestOutputHelper output)
{
[Theory]
[InlineData(
"""
{
"object": "whatsapp_business_account",
"entry": [
{
"id": "554372691093163",
"changes": [
{
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "5491123960774",
"phone_number_id": "524718744066632"
},
"contacts": [
{
"profile": { "name": "Kzu" },
"wa_id": "5491159278282"
}
],
"messages": [
{
"from": "5491159278282",
"id": "wamid.HBgNNTQ5MTE1OTI3ODI4MhUCABIYFjNFQjBFOEFGODAzMEI4RTI3NzczNjkA",
"timestamp": "1744062742",
"text": { "body": "hello!" },
"type": "text"
}
]
},
"field": "messages"
}
]
}
]
}
""")]
public async Task DeserializePayload(string json)
[InlineData(nameof(ContentType.Audio), "927483105672819", "wamid.XYZRandomString123ABC456DEF789GHI==")]
[InlineData(nameof(ContentType.Contact), "927481035162874", "wamid.HBgNNDcyODkwMTIzNDU2NhUCABIYFjE4QTlDMzU2MkJDOTg3RUY2NDg5RTFEMTIzQzVFRAA==")]
[InlineData(nameof(ContentType.Document), "813947205126374", "wamid.HBgNMTIwMjU1NTk4NzY1NhUCABIYFjE4QTlDMzU2MkJDOTg3RUY2NDg5RTFEMTIzQzVFRAA==")]
[InlineData(nameof(ContentType.Image), "813927405162784", "wamid.HBgNMTIwMjU1NTk4NzY1NhUCABIYFjE4QTlDMzU2MkJDOTg3RUY2NDg5RTFEMTIzQzVFRAA==")]
[InlineData(nameof(ContentType.Location), "813920475601234", "wamid.HBgNMTIwMjk4NzQ1NjM1NhUCABIYFjE5RDhGMzQ2NEJDOTg3RUY2NDg5RTFEMTIzQzVFRAA==")]
[InlineData(nameof(ContentType.Text), "813920475102346", "wamid.HBgNMTIwMjk4NzQ1NjM1NhUCABIYFjQ5RjE4QzJEMzU2ODk3QTJFMUY3RDEyMjNBNkI5QwA==", "wamid.HBgNNTQ5MTE1OTL4ODI4MhUCBBEYEjUxNDI3NkMzRkI1ODVCRTgwOAA=")]
[InlineData(nameof(ContentType.Video), "813927405162374", "wamid.HBgNMTIwMjU1NTk4NzY1NhUCABIYFjE4QTlDMzU2MkJDOTg3RUY2NDg5RTFEMTIzQzVFRAA==")]
[InlineData(nameof(MessageType.Unsupported), "837625914708254", "wamid.HBgNNTQ5MzcyNjEwNDg1OVUCABIYFjJCRDM5RTg0QkY3OEQxMjM2RkE0QjcA")]
[InlineData(nameof(MessageType.Error), "729104583621947", "wamid.XYZgMDEyMzQ1Njc4OTA5MRUCABEYEjU5NkM3ODlFQjAxMjM0NTY7OA==")]
[InlineData(nameof(MessageType.Interactive), "123456789012345", "wamid.RandomMessageID", "wamid.RandomContextID")]
[InlineData(nameof(MessageType.Reaction), "123456789012345", "wamid.HBgNMTIzNDU2Nzg5MDEyMzQ1MhUCABEYEkY5QzQxNDNBQjgyRkVENEIzMQA=", "wamid.HBgNMTIzNDU2Nzg5MDEyMzQ1MhUCABEYEkY5QzQxNDNBQjgyRkVENEIzMQA=")]
// For consistency, status message ID == status context ID.
[InlineData(nameof(MessageType.Status), "987654321098765", "wamid.HBgNNTQ5OTg3NjU0MzIxMDlUCABEYEkLMNVzNDU2Nzg5MAA=", "wamid.HBgNNTQ5OTg3NjU0MzIxMDlUCABEYEkLMNVzNDU2Nzg5MAA=")]
public async Task DeserializeMessage(string type, string notification, string id, string? context = default)
{
var json = await File.ReadAllTextAsync($"Content/WhatsApp/{type}.json");
var message = await Message.DeserializeAsync(json);

Assert.NotNull(message);
Assert.Equal(notification, message.NotificationId);
Assert.Equal(id, message.Id);
Assert.Equal(context, message.Context);
Assert.NotNull(message.To);
Assert.NotNull(message.From);
}
Expand All @@ -67,14 +48,15 @@ public async Task DeserializePayload(string json)
[InlineData(ContentType.Location)]
[InlineData(ContentType.Text)]
[InlineData(ContentType.Video)]
public async Task DeserializePolymorphic(ContentType type)
public async Task DeserializeContent(ContentType type)
{
var json = await File.ReadAllTextAsync($"Content/WhatsApp/{type}.json");
var message = await Message.DeserializeAsync(json);

var content = Assert.IsType<ContentMessage>(message);

Assert.NotNull(message);
Assert.NotNull(message.NotificationId);
Assert.NotNull(message.To);
Assert.NotNull(message.From);
Assert.NotNull(content.Content);
Expand All @@ -90,6 +72,7 @@ public async Task DeserializeErrorStatus()
var error = Assert.IsType<ErrorMessage>(message);

Assert.NotNull(message);
Assert.NotNull(message.NotificationId);
Assert.NotNull(message.To);
Assert.NotNull(message.From);
Assert.NotNull(error.Error);
Expand All @@ -105,6 +88,7 @@ public async Task DeserializeStatus()
var status = Assert.IsType<StatusMessage>(message);

Assert.NotNull(message);
Assert.NotNull(message.NotificationId);
Assert.NotNull(message.To);
Assert.NotNull(message.From);
Assert.Equal(Status.Delivered, status.Status);
Expand All @@ -119,6 +103,7 @@ public async Task DeserializeInteractive()
var interactive = Assert.IsType<InteractiveMessage>(message);

Assert.NotNull(message);
Assert.NotNull(message.NotificationId);
Assert.NotNull(message.To);
Assert.NotNull(message.From);
Assert.Equal("btn_yes", interactive.Button.Id);
Expand All @@ -134,7 +119,23 @@ public async Task DeserializeUnsupported()
var unsupported = Assert.IsType<UnsupportedMessage>(message);

Assert.NotNull(message);
Assert.NotNull(message.NotificationId);
Assert.NotNull(message.To);
Assert.NotNull(message.From);
}

[Fact]
public async Task DeserializeReaction()
{
var json = await File.ReadAllTextAsync($"Content/WhatsApp/Reaction.json");
var message = await Message.DeserializeAsync(json);

var reaction = Assert.IsType<ReactionMessage>(message);

Assert.NotNull(message);
Assert.NotNull(message.NotificationId);
Assert.NotNull(message.To);
Assert.NotNull(message.From);
Assert.Equal("😊", reaction.Emoji);
}
}
Loading