Skip to content

Commit aa5fb17

Browse files
committed
Add first-class support for unsupported messages
This allows reacting to unsupported messages from the handler. We take the chance to vastly improve the JQ performace by leveraging polymorphic deserialization also for the top-level message itself, which also simplifies. Fixes #28
1 parent 0d8469d commit aa5fb17

File tree

11 files changed

+262
-161
lines changed

11 files changed

+262
-161
lines changed

src/Sample/Program.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@
6767
//await Task.Delay(2000);
6868
await client.ReplyAsync(message, $"☑️ Got your {content.Content.Type}:\r\n{JsonSerializer.Serialize(content, options)}");
6969
}
70+
else if (message is UnsupportedMessage unsupported)
71+
{
72+
await client.ReactAsync(message, "⚠️");
73+
return;
74+
}
7075
});
7176

7277
builder.Build().Run();
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"object": "whatsapp_business_account",
3+
"entry": [
4+
{
5+
"id": "837625914708254",
6+
"changes": [
7+
{
8+
"value": {
9+
"messaging_product": "whatsapp",
10+
"metadata": {
11+
"display_phone_number": "5492847619038",
12+
"phone_number_id": "781364925037481"
13+
},
14+
"contacts": [
15+
{
16+
"profile": { "name": "User745" },
17+
"wa_id": "5493726104859"
18+
}
19+
],
20+
"messages": [
21+
{
22+
"from": "5493726104859",
23+
"id": "wamid.HBgNNTQ5MzcyNjEwNDg1OVUCABIYFjJCRDM5RTg0QkY3OEQxMjM2RkE0QjcA",
24+
"timestamp": "1678945321",
25+
"errors": [
26+
{
27+
"code": 131051,
28+
"title": "Message type unknown",
29+
"message": "Message type unknown",
30+
"error_data": { "details": "Message type is currently not supported." }
31+
}
32+
],
33+
"type": "unsupported"
34+
}
35+
]
36+
},
37+
"field": "messages"
38+
}
39+
]
40+
}
41+
]
42+
}

src/Tests/WhatsAppModelTests.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,4 +124,17 @@ public async Task DeserializeInteractive()
124124
Assert.Equal("btn_yes", interactive.Button.Id);
125125
Assert.Equal("Yes", interactive.Button.Title);
126126
}
127+
128+
[Fact]
129+
public async Task DeserializeUnsupported()
130+
{
131+
var json = await File.ReadAllTextAsync($"Content/WhatsApp/Unsupported.json");
132+
var message = await Message.DeserializeAsync(json);
133+
134+
var unsupported = Assert.IsType<UnsupportedMessage>(message);
135+
136+
Assert.NotNull(message);
137+
Assert.NotNull(message.To);
138+
Assert.NotNull(message.From);
139+
}
127140
}

src/WhatsApp/Content.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public abstract record Content
3434
public record DocumentContent(string Id, string Name, string Mime, string Sha256) : Content
3535
{
3636
/// <inheritdoc/>
37+
[JsonIgnore]
3738
public override ContentType Type => ContentType.Document;
3839
}
3940

@@ -46,6 +47,7 @@ public record DocumentContent(string Id, string Name, string Mime, string Sha256
4647
public record ContactContent(string Name, string Surname, string[] Numbers) : Content
4748
{
4849
/// <inheritdoc/>
50+
[JsonIgnore]
4951
public override ContentType Type => ContentType.Contact;
5052
}
5153

@@ -56,6 +58,7 @@ public record ContactContent(string Name, string Surname, string[] Numbers) : Co
5658
public record TextContent(string Text) : Content
5759
{
5860
/// <inheritdoc/>
61+
[JsonIgnore]
5962
public override ContentType Type => ContentType.Text;
6063
}
6164

@@ -76,6 +79,7 @@ public record Location(double Latitude, double Longitude);
7679
public record LocationContent(Location Location, string? Address, string? Name, string? Url) : Content
7780
{
7881
/// <inheritdoc/>
82+
[JsonIgnore]
7983
public override ContentType Type => ContentType.Location;
8084
}
8185

@@ -96,6 +100,7 @@ public abstract record MediaContent(string Id, string Mime, string Sha256) : Con
96100
public record AudioContent(string Id, string Mime, string Sha256) : MediaContent(Id, Mime, Sha256)
97101
{
98102
/// <inheritdoc/>
103+
[JsonIgnore]
99104
public override ContentType Type => ContentType.Audio;
100105
}
101106

@@ -108,6 +113,7 @@ public record AudioContent(string Id, string Mime, string Sha256) : MediaContent
108113
public record ImageContent(string Id, string Mime, string Sha256) : MediaContent(Id, Mime, Sha256)
109114
{
110115
/// <inheritdoc/>
116+
[JsonIgnore]
111117
public override ContentType Type => ContentType.Image;
112118
}
113119

@@ -120,6 +126,7 @@ public record ImageContent(string Id, string Mime, string Sha256) : MediaContent
120126
public record VideoContent(string Id, string Mime, string Sha256) : MediaContent(Id, Mime, Sha256)
121127
{
122128
/// <inheritdoc/>
129+
[JsonIgnore]
123130
public override ContentType Type => ContentType.Video;
124131
}
125132

@@ -130,5 +137,6 @@ public record VideoContent(string Id, string Mime, string Sha256) : MediaContent
130137
public record UnknownContent(JsonElement Raw) : Content
131138
{
132139
/// <inheritdoc/>
140+
[JsonIgnore]
133141
public override ContentType Type => ContentType.Unknown;
134142
}

src/WhatsApp/ContentMessage.cs

Lines changed: 4 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
namespace Devlooped.WhatsApp;
1+
using System.Text.Json.Serialization;
2+
3+
namespace Devlooped.WhatsApp;
24

35
/// <summary>
46
/// A <see cref="Message"/> containing <see cref="Content"/>.
@@ -10,72 +12,7 @@
1012
/// <param name="Content">Message content.</param>
1113
public record ContentMessage(string Id, Service To, User From, long Timestamp, Content Content) : Message(Id, To, From, Timestamp)
1214
{
13-
/// <summary>
14-
/// A JQ query that transforms WhatsApp Cloud API JSON into polymorphic JSON for
15-
/// C# deserialization of a <see cref="ContentMessage"/>.
16-
/// </summary>
17-
public const string JQ =
18-
"""
19-
.entry[].changes[].value.metadata as $phone |
20-
.entry[].changes[].value.contacts[]? as $user |
21-
.entry[].changes[].value.messages[]? |
22-
select(. != null and .type != "interactive") |
23-
.type as $type |
24-
{
25-
id: .id,
26-
timestamp: .timestamp | tonumber,
27-
to: {
28-
id: $phone.phone_number_id,
29-
number: $phone.display_phone_number
30-
},
31-
from: {
32-
name: $user.profile.name,
33-
number: $user.wa_id
34-
},
35-
content: (
36-
if $type == "document" then {
37-
"$type": $type,
38-
id: .document.id,
39-
name: .document.filename,
40-
mime: .document.mime_type,
41-
sha256: .document.sha256
42-
}
43-
elif $type == "contacts" then {
44-
"$type": $type,
45-
name: .contacts[].name.first_name,
46-
surname: .contacts[].name.last_name,
47-
numbers: [.contacts[].phones[] |
48-
select(.wa_id? != null) | .wa_id]
49-
}
50-
elif $type == "text" then {
51-
"$type": $type,
52-
text: .text.body
53-
}
54-
elif $type == "location" then {
55-
"$type": $type,
56-
location: {
57-
latitude: .location.latitude,
58-
longitude: .location.longitude
59-
},
60-
address: .location.address,
61-
name: .location.name,
62-
url: .location.url
63-
}
64-
elif $type == "image" or $type == "video" or $type == "audio" then {
65-
"$type": $type,
66-
id: .[$type].id,
67-
mime: .[$type].mime_type,
68-
sha256: .[$type].sha256
69-
}
70-
else {
71-
"$type": "unknown",
72-
raw: .
73-
}
74-
end
75-
)
76-
}
77-
""";
78-
7915
/// <inheritdoc/>
16+
[JsonIgnore]
8017
public override MessageType Type => MessageType.Content;
8118
}

src/WhatsApp/ErrorMessage.cs

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
namespace Devlooped.WhatsApp;
1+
using System.Text.Json.Serialization;
2+
3+
namespace Devlooped.WhatsApp;
24

35
/// <summary>
46
/// A <see cref="Message"/> containing an <see cref="Error"/>.
@@ -10,34 +12,8 @@
1012
/// <param name="Error">The error.</param>
1113
public record ErrorMessage(string Id, Service To, User From, long Timestamp, Error Error) : Message(Id, To, From, Timestamp)
1214
{
13-
/// <summary>
14-
/// A JQ query that transforms WhatsApp Cloud API JSON into the serialization
15-
/// expected by <see cref="ErrorMessage"/>.
16-
/// </summary>
17-
public const string JQ =
18-
"""
19-
.entry[].changes[].value.metadata as $phone |
20-
.entry[].changes[].value.statuses[]? |
21-
select(. != null) |
22-
{
23-
id: .id,
24-
timestamp: .timestamp | tonumber,
25-
to: {
26-
id: $phone.phone_number_id,
27-
number: $phone.display_phone_number
28-
},
29-
from: {
30-
name: .recipient_id,
31-
number: .recipient_id
32-
},
33-
error: .errors[]? | {
34-
code: .code,
35-
message: (.error_data.details // .message),
36-
}
37-
}
38-
""";
39-
4015
/// <inheritdoc/>
16+
[JsonIgnore]
4117
public override MessageType Type => MessageType.Error;
4218
}
4319

src/WhatsApp/InteractiveMessage.cs

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
namespace Devlooped.WhatsApp;
1+
using System.Text.Json.Serialization;
2+
3+
namespace Devlooped.WhatsApp;
24

35
/// <summary>
46
/// A <see cref="Message"/> containing an interactive button reply.
@@ -10,32 +12,8 @@
1012
/// <param name="Button">The button selected by the user.</param>
1113
public record InteractiveMessage(string Id, Service To, User From, long Timestamp, Button Button) : Message(Id, To, From, Timestamp)
1214
{
13-
/// <summary>
14-
/// A JQ query that transforms WhatsApp Cloud API JSON into the serialization
15-
/// expected by <see cref="InteractiveMessage"/>.
16-
/// </summary>
17-
public const string JQ =
18-
"""
19-
.entry[].changes[].value.metadata as $phone |
20-
.entry[].changes[].value.contacts[]? as $user |
21-
.entry[].changes[].value.messages[]? |
22-
select(. != null and .type == "interactive") |
23-
{
24-
id: .id,
25-
timestamp: .timestamp | tonumber,
26-
to: {
27-
id: $phone.phone_number_id,
28-
number: $phone.display_phone_number
29-
},
30-
from: {
31-
name: $user.profile.name,
32-
number: $user.wa_id
33-
},
34-
button: .interactive.button_reply
35-
}
36-
""";
37-
3815
/// <inheritdoc/>
16+
[JsonIgnore]
3917
public override MessageType Type => MessageType.Interactive;
4018
}
4119

0 commit comments

Comments
 (0)