Skip to content

Commit acb3005

Browse files
committed
Fix typo in SendAsync, add ReplyAsync and message-based overloads
In most use cases, we'd have a Message from the user at hand. Therefore, it significantly simplifies things if we can just Reply/React/MarkRead/Send back to the sender of the message, so we add extension method overloads. Also, since attempting to use SendAsync(number, "hello") is an obvious mistake that's too easy to make (the payload should be an object matching expected payloads from WhatsApp API), we add an analyzer that flags this as a compilation error so it can never happen. Ideally, we'd like to exclude a string from being acceptable as an `object`, but this is the next best thing :).
1 parent 74989ae commit acb3005

15 files changed

+259
-18
lines changed

WhatsApp.sln

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "src\Tests\Tests.cs
99
EndProject
1010
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample", "src\Sample\Sample.csproj", "{37A61B10-BE1C-476D-81E0-2D0BCEAF3EE7}"
1111
EndProject
12+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeAnalysis", "src\CodeAnalysis\CodeAnalysis.csproj", "{63583965-B86B-485E-AACF-5C4E453B182E}"
13+
EndProject
1214
Global
1315
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1416
Debug|Any CPU = Debug|Any CPU
@@ -27,6 +29,10 @@ Global
2729
{37A61B10-BE1C-476D-81E0-2D0BCEAF3EE7}.Debug|Any CPU.Build.0 = Debug|Any CPU
2830
{37A61B10-BE1C-476D-81E0-2D0BCEAF3EE7}.Release|Any CPU.ActiveCfg = Release|Any CPU
2931
{37A61B10-BE1C-476D-81E0-2D0BCEAF3EE7}.Release|Any CPU.Build.0 = Release|Any CPU
32+
{63583965-B86B-485E-AACF-5C4E453B182E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
33+
{63583965-B86B-485E-AACF-5C4E453B182E}.Debug|Any CPU.Build.0 = Debug|Any CPU
34+
{63583965-B86B-485E-AACF-5C4E453B182E}.Release|Any CPU.ActiveCfg = Release|Any CPU
35+
{63583965-B86B-485E-AACF-5C4E453B182E}.Release|Any CPU.Build.0 = Release|Any CPU
3036
EndGlobalSection
3137
GlobalSection(SolutionProperties) = preSolution
3238
HideSolutionNode = FALSE

readme.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ builder.UseWhatsApp<IWhatsAppClient, ILogger<Program>>(async (client, logger, me
5858
logger.LogInformation($"Got message type {message.Type}");
5959
// Reply to an incoming content message, for example.
6060
if (message is ContentMessage content)
61-
await client.SendTextAync(message.To.Id, message.From.Number, $"Got your {content.}");
61+
await client.ReplyAsync(message, $"☑️ Got your {content.Content.Type}");
6262
}
6363
```
6464

@@ -86,10 +86,10 @@ common scenarios, such as reacting to a message and replying with plain text:
8686
```csharp
8787
if (message is ContentMessage content)
8888
{
89-
await client.ReactAsync(from: message.To.Id, to: message.From.Number, message.Id, "🧠");
89+
await client.ReactAsync(message, "🧠");
9090
// simulate some hard work at hand, like doing some LLM-stuff :)
9191
await Task.Delay(2000);
92-
await client.SendTextAync(message.To.Id, message.From.Number, $"☑️ Processed your {content.Type}");
92+
await client.ReplyAsync(message, $"☑️ Got your {content.Content.Type}");
9393
}
9494
```
9595

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<AssemblyName>Devlooped.WhatsApp.CodeAnalysis</AssemblyName>
5+
<TargetFramework>netstandard2.0</TargetFramework>
6+
<PackFolder>analyzers/dotnet/cs</PackFolder>
7+
<GenerateDocumentationFile>false</GenerateDocumentationFile>
8+
<IsRoslynComponent>true</IsRoslynComponent>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<Compile Include="..\WhatsApp\IWhatsAppClient.cs" Link="IWhatsAppClient.cs" />
13+
</ItemGroup>
14+
15+
<ItemGroup>
16+
<PackageReference Include="NuGetizer" Version="1.2.4" />
17+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" Pack="false" />
18+
<PackageReference Include="PolySharp" Version="1.15.0" PrivateAssets="all" />
19+
</ItemGroup>
20+
21+
</Project>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"profiles": {
3+
"Roslyn": {
4+
"commandName": "DebugRoslynComponent",
5+
"targetProject": "..\\Tests\\Tests.csproj"
6+
}
7+
}
8+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using System.Collections.Immutable;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.CSharp;
4+
using Microsoft.CodeAnalysis.CSharp.Syntax;
5+
using Microsoft.CodeAnalysis.Diagnostics;
6+
7+
namespace Devlooped.WhatsApp;
8+
9+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
10+
public class SendStringAnalyzer : DiagnosticAnalyzer
11+
{
12+
public static DiagnosticDescriptor Rule { get; } = new(
13+
id: "WA001",
14+
title: "Invalid Payload Type",
15+
messageFormat: $"The second parameter of '{nameof(IWhatsAppClient)}.{nameof(IWhatsAppClient.SendAsync)}' should not be a string.",
16+
category: "Usage",
17+
defaultSeverity: DiagnosticSeverity.Error,
18+
description: "The payload parameter is serialized and sent as JSON over HTTP. Use an object instead.",
19+
isEnabledByDefault: true);
20+
21+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
22+
23+
public override void Initialize(AnalysisContext context)
24+
{
25+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
26+
context.EnableConcurrentExecution();
27+
context.RegisterSyntaxNodeAction(AnalyzeSyntax, SyntaxKind.InvocationExpression);
28+
}
29+
30+
static void AnalyzeSyntax(SyntaxNodeAnalysisContext context)
31+
{
32+
var invocation = (InvocationExpressionSyntax)context.Node;
33+
if (invocation.Expression is MemberAccessExpressionSyntax memberAccess &&
34+
memberAccess.Name.Identifier.Text == nameof(IWhatsAppClient.SendAsync))
35+
{
36+
var methodSymbol = context.SemanticModel.GetSymbolInfo(memberAccess).Symbol as IMethodSymbol;
37+
if (methodSymbol?.ContainingSymbol.Name == nameof(IWhatsAppClient) && invocation.ArgumentList.Arguments.Count == 2)
38+
{
39+
var secondArgument = invocation.ArgumentList.Arguments[1];
40+
var argumentType = context.SemanticModel.GetTypeInfo(secondArgument.Expression).Type;
41+
if (argumentType?.SpecialType == SpecialType.System_String)
42+
{
43+
var diagnostic = Diagnostic.Create(Rule, secondArgument.GetLocation());
44+
context.ReportDiagnostic(diagnostic);
45+
}
46+
}
47+
}
48+
}
49+
}

src/Sample/Program.cs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
1-
using Devlooped.WhatsApp;
1+
using System.Text.Json;
2+
using System.Text.Json.Serialization;
3+
using Devlooped.WhatsApp;
24
using Microsoft.Azure.Functions.Worker.Builder;
35
using Microsoft.Extensions.Configuration;
46
using Microsoft.Extensions.Hosting;
57
using Microsoft.Extensions.Logging;
68

79
var builder = FunctionsApplication.CreateBuilder(args);
10+
var options = new JsonSerializerOptions(JsonSerializerDefaults.General)
11+
{
12+
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
13+
Converters =
14+
{
15+
new JsonStringEnumConverter()
16+
},
17+
WriteIndented = true
18+
};
819

920
builder.ConfigureFunctionsWebApplication();
1021
builder.Configuration.AddUserSecrets<Program>();
@@ -18,7 +29,7 @@
1829
// Reengagement error, we need to invite the user.
1930
if (error.Error.Code == 131047)
2031
{
21-
await client.SendAync(error.To.Id, new
32+
await client.SendAsync(error.To.Id, new
2233
{
2334
messaging_product = "whatsapp",
2435
to = error.From.Number,
@@ -51,10 +62,10 @@
5162
}
5263
else if (message is ContentMessage content)
5364
{
54-
await client.ReactAsync(from: message.To.Id, to: message.From.Number, message.Id, "🧠");
65+
await client.ReactAsync(message, "🧠");
5566
// simulate some hard work at hand, like doing some LLM-stuff :)
56-
await Task.Delay(2000);
57-
await client.SendTextAync(message.To.Id, message.From.Number, $"☑️ Got your {content.Type.ToString().ToLowerInvariant()}");
67+
//await Task.Delay(2000);
68+
await client.ReplyAsync(message, $"☑️ Got your {content.Content.Type}:\r\n{JsonSerializer.Serialize(content, options)}");
5869
}
5970
});
6071

src/Sample/host.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
"excludedTypes": "Request"
1313
},
1414
"enableLiveMetricsFilters": true
15+
},
16+
"logLevel": {
17+
"Microsoft": "Warning",
18+
"System": "Warning",
19+
"Devlooped": "Trace"
1520
}
1621
}
1722
}

src/Tests/AnalyzerExtensions.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using Devlooped.WhatsApp;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.Host;
4+
using Microsoft.CodeAnalysis.Testing;
5+
6+
namespace Devlooped.WhatsApp;
7+
8+
public static class AnalyzerExtensions
9+
{
10+
public static TTest WithWhatsApp<TTest>(this TTest test) where TTest : AnalyzerTest<DefaultVerifier>
11+
{
12+
test.SolutionTransforms.Add((solution, projectId)
13+
=> solution
14+
.GetProject(projectId)?
15+
.AddMetadataReference(MetadataReference.CreateFromFile(typeof(IWhatsAppClient).Assembly.Location))
16+
.Solution ?? solution);
17+
18+
return test;
19+
}
20+
}

src/Tests/AnalyzerTests.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
extern alias CodeAnalysis;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Text;
6+
using System.Threading.Tasks;
7+
using Microsoft.CodeAnalysis;
8+
using Microsoft.CodeAnalysis.CSharp.Testing;
9+
using Microsoft.CodeAnalysis.Testing;
10+
using Analyzer = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<CodeAnalysis.Devlooped.WhatsApp.SendStringAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
11+
using AnalyzerTest = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerTest<CodeAnalysis.Devlooped.WhatsApp.SendStringAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
12+
using SendStringAnalyzer = CodeAnalysis.Devlooped.WhatsApp.SendStringAnalyzer;
13+
14+
namespace Devlooped.WhatsApp;
15+
16+
public class AnalyzerTests
17+
{
18+
[Fact]
19+
public async Task InvalidSendTextAsync()
20+
{
21+
var test = new CSharpAnalyzerTest<SendStringAnalyzer, DefaultVerifier>
22+
{
23+
ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
24+
TestCode =
25+
$$"""
26+
using System.Threading.Tasks;
27+
using {{nameof(Devlooped)}}.{{nameof(Devlooped.WhatsApp)}};
28+
29+
public class Handler
30+
{
31+
public async Task Send({{nameof(IWhatsAppClient)}} client, string text)
32+
{
33+
await client.{{nameof(IWhatsAppClient.SendAsync)}}("1234", {|#0:text|});
34+
}
35+
}
36+
"""
37+
}.WithWhatsApp();
38+
39+
var expected = Analyzer.Diagnostic(SendStringAnalyzer.Rule).WithLocation(0);
40+
41+
test.ExpectedDiagnostics.Add(expected);
42+
43+
await test.RunAsync();
44+
}
45+
}

src/Tests/Tests.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
<ItemGroup>
1313
<PackageReference Include="coverlet.collector" Version="6.0.4" />
14+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing" Version="1.1.2" />
1415
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
1516
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
1617
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.1" />
@@ -25,6 +26,7 @@
2526

2627
<ItemGroup>
2728
<ProjectReference Include="..\WhatsApp\WhatsApp.csproj" />
29+
<ProjectReference Include="..\CodeAnalysis\CodeAnalysis.csproj" OutputItemType="Analyzer" Aliases="CodeAnalysis" />
2830
</ItemGroup>
2931

3032
<ItemGroup>

0 commit comments

Comments
 (0)