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
6 changes: 6 additions & 0 deletions WhatsApp.sln
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "src\Tests\Tests.cs
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample", "src\Sample\Sample.csproj", "{37A61B10-BE1C-476D-81E0-2D0BCEAF3EE7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeAnalysis", "src\CodeAnalysis\CodeAnalysis.csproj", "{63583965-B86B-485E-AACF-5C4E453B182E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -27,6 +29,10 @@ Global
{37A61B10-BE1C-476D-81E0-2D0BCEAF3EE7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{37A61B10-BE1C-476D-81E0-2D0BCEAF3EE7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{37A61B10-BE1C-476D-81E0-2D0BCEAF3EE7}.Release|Any CPU.Build.0 = Release|Any CPU
{63583965-B86B-485E-AACF-5C4E453B182E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{63583965-B86B-485E-AACF-5C4E453B182E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{63583965-B86B-485E-AACF-5C4E453B182E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{63583965-B86B-485E-AACF-5C4E453B182E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
6 changes: 3 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ builder.UseWhatsApp<IWhatsAppClient, ILogger<Program>>(async (client, logger, me
logger.LogInformation($"Got message type {message.Type}");
// Reply to an incoming content message, for example.
if (message is ContentMessage content)
await client.SendTextAync(message.To.Id, message.From.Number, $"Got your {content.}");
await client.ReplyAsync(message, $"☑️ Got your {content.Content.Type}");
}
```

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

Expand Down
21 changes: 21 additions & 0 deletions src/CodeAnalysis/CodeAnalysis.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<AssemblyName>Devlooped.WhatsApp.CodeAnalysis</AssemblyName>
<TargetFramework>netstandard2.0</TargetFramework>
<PackFolder>analyzers/dotnet/cs</PackFolder>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<IsRoslynComponent>true</IsRoslynComponent>
</PropertyGroup>

<ItemGroup>
<Compile Include="..\WhatsApp\IWhatsAppClient.cs" Link="IWhatsAppClient.cs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="NuGetizer" Version="1.2.4" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" Pack="false" />
<PackageReference Include="PolySharp" Version="1.15.0" PrivateAssets="all" />
</ItemGroup>

</Project>
8 changes: 8 additions & 0 deletions src/CodeAnalysis/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"profiles": {
"Roslyn": {
"commandName": "DebugRoslynComponent",
"targetProject": "..\\Tests\\Tests.csproj"
}
}
}
49 changes: 49 additions & 0 deletions src/CodeAnalysis/SendStringAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Devlooped.WhatsApp;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class SendStringAnalyzer : DiagnosticAnalyzer
{
public static DiagnosticDescriptor Rule { get; } = new(
id: "WA001",
title: "Invalid Payload Type",
messageFormat: $"The second parameter of '{nameof(IWhatsAppClient)}.{nameof(IWhatsAppClient.SendAsync)}' should not be a string.",
category: "Usage",
defaultSeverity: DiagnosticSeverity.Error,
description: "The payload parameter is serialized and sent as JSON over HTTP. Use an object instead.",
isEnabledByDefault: true);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);

public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(AnalyzeSyntax, SyntaxKind.InvocationExpression);
}

static void AnalyzeSyntax(SyntaxNodeAnalysisContext context)
{
var invocation = (InvocationExpressionSyntax)context.Node;
if (invocation.Expression is MemberAccessExpressionSyntax memberAccess &&
memberAccess.Name.Identifier.Text == nameof(IWhatsAppClient.SendAsync))
{
var methodSymbol = context.SemanticModel.GetSymbolInfo(memberAccess).Symbol as IMethodSymbol;
if (methodSymbol?.ContainingSymbol.Name == nameof(IWhatsAppClient) && invocation.ArgumentList.Arguments.Count == 2)
{
var secondArgument = invocation.ArgumentList.Arguments[1];
var argumentType = context.SemanticModel.GetTypeInfo(secondArgument.Expression).Type;
if (argumentType?.SpecialType == SpecialType.System_String)
{
var diagnostic = Diagnostic.Create(Rule, secondArgument.GetLocation());
context.ReportDiagnostic(diagnostic);
}
}
}
}
}
21 changes: 16 additions & 5 deletions src/Sample/Program.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
using Devlooped.WhatsApp;
using System.Text.Json;
using System.Text.Json.Serialization;
using Devlooped.WhatsApp;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

var builder = FunctionsApplication.CreateBuilder(args);
var options = new JsonSerializerOptions(JsonSerializerDefaults.General)
{
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
Converters =
{
new JsonStringEnumConverter()
},
WriteIndented = true
};

builder.ConfigureFunctionsWebApplication();
builder.Configuration.AddUserSecrets<Program>();
Expand All @@ -18,7 +29,7 @@
// Reengagement error, we need to invite the user.
if (error.Error.Code == 131047)
{
await client.SendAync(error.To.Id, new
await client.SendAsync(error.To.Id, new
{
messaging_product = "whatsapp",
to = error.From.Number,
Expand Down Expand Up @@ -51,10 +62,10 @@
}
else if (message is ContentMessage content)
{
await client.ReactAsync(from: message.To.Id, to: message.From.Number, message.Id, "🧠");
await client.ReactAsync(message, "🧠");
// simulate some hard work at hand, like doing some LLM-stuff :)
await Task.Delay(2000);
await client.SendTextAync(message.To.Id, message.From.Number, $"☑️ Got your {content.Type.ToString().ToLowerInvariant()}");
//await Task.Delay(2000);
await client.ReplyAsync(message, $"☑️ Got your {content.Content.Type}:\r\n{JsonSerializer.Serialize(content, options)}");
}
});

Expand Down
5 changes: 5 additions & 0 deletions src/Sample/host.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
"excludedTypes": "Request"
},
"enableLiveMetricsFilters": true
},
"logLevel": {
"Microsoft": "Warning",
"System": "Warning",
"Devlooped": "Trace"
}
}
}
20 changes: 20 additions & 0 deletions src/Tests/AnalyzerExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Devlooped.WhatsApp;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Testing;

namespace Devlooped.WhatsApp;

public static class AnalyzerExtensions
{
public static TTest WithWhatsApp<TTest>(this TTest test) where TTest : AnalyzerTest<DefaultVerifier>
{
test.SolutionTransforms.Add((solution, projectId)
=> solution
.GetProject(projectId)?
.AddMetadataReference(MetadataReference.CreateFromFile(typeof(IWhatsAppClient).Assembly.Location))
.Solution ?? solution);

return test;
}
}
45 changes: 45 additions & 0 deletions src/Tests/AnalyzerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
extern alias CodeAnalysis;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
using Analyzer = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<CodeAnalysis.Devlooped.WhatsApp.SendStringAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
using AnalyzerTest = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerTest<CodeAnalysis.Devlooped.WhatsApp.SendStringAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
using SendStringAnalyzer = CodeAnalysis.Devlooped.WhatsApp.SendStringAnalyzer;

namespace Devlooped.WhatsApp;

public class AnalyzerTests
{
[Fact]
public async Task InvalidSendTextAsync()
{
var test = new CSharpAnalyzerTest<SendStringAnalyzer, DefaultVerifier>
{
ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
TestCode =
$$"""
using System.Threading.Tasks;
using {{nameof(Devlooped)}}.{{nameof(Devlooped.WhatsApp)}};

public class Handler
{
public async Task Send({{nameof(IWhatsAppClient)}} client, string text)
{
await client.{{nameof(IWhatsAppClient.SendAsync)}}("1234", {|#0:text|});
}
}
"""
}.WithWhatsApp();

var expected = Analyzer.Diagnostic(SendStringAnalyzer.Rule).WithLocation(0);

test.ExpectedDiagnostics.Add(expected);

await test.RunAsync();
}
}
2 changes: 2 additions & 0 deletions src/Tests/Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing" Version="1.1.2" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.1" />
Expand All @@ -25,6 +26,7 @@

<ItemGroup>
<ProjectReference Include="..\WhatsApp\WhatsApp.csproj" />
<ProjectReference Include="..\CodeAnalysis\CodeAnalysis.csproj" OutputItemType="Analyzer" Aliases="CodeAnalysis" />
</ItemGroup>

<ItemGroup>
Expand Down
6 changes: 3 additions & 3 deletions src/Tests/WhatsAppClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public async Task ThrowsIfNoConfiguredNumberAsync()
VerifyToken = "asdf"
}, MockLogger.Create<WhatsAppClient>());

var ex = await Assert.ThrowsAsync<ArgumentException>(() => client.SendAync("1234", new { }));
var ex = await Assert.ThrowsAsync<ArgumentException>(() => client.SendAsync("1234", new { }));

Assert.Equal("from", ex.ParamName);
}
Expand All @@ -31,7 +31,7 @@ public async Task SendsMessageAsync()
{
var (configuration, client) = Initialize();

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

[SecretsFact("Meta:VerifyToken", "SendFrom", "SendTo")]
Expand All @@ -41,7 +41,7 @@ public async Task SendsButtonAsync()

// Send an interactive message with three buttons showcasing the payload/value
// being different than the button text
await client.SendAync(configuration["SendFrom"]!, new
await client.SendAsync(configuration["SendFrom"]!, new
{
messaging_product = "whatsapp",
recipient_type = "individual",
Expand Down
6 changes: 4 additions & 2 deletions src/WhatsApp/IWhatsAppClient.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.ComponentModel;
using System.Threading.Tasks;

namespace Devlooped.WhatsApp;

Expand All @@ -16,5 +17,6 @@ public interface IWhatsAppClient
/// <see cref="https://developers.facebook.com/docs/whatsapp/cloud-api/reference/messages"/>
/// <exception cref="ArgumentException">The number <paramref name="from"/> is not registered in <see cref="MetaOptions"/>.</exception>
/// <exception cref="HttpRequestException">The HTTP request failed. Exception message contains the error response body from WhatsApp.</exception>
Task SendAync(string from, object payload);
[Description(nameof(Devlooped) + nameof(WhatsApp) + nameof(IWhatsAppClient) + nameof(SendAsync))]
Task SendAsync(string from, object payload);
}
4 changes: 4 additions & 0 deletions src/WhatsApp/WhatsApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,8 @@
<PackageReference Include="System.Net.Http.Json" Version="8.0.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\CodeAnalysis\CodeAnalysis.csproj" ReferenceOutputAssembly="false" OutputItemType="Analyzer" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion src/WhatsApp/WhatsAppClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public static IWhatsAppClient Create(IHttpClientFactory httpFactory, MetaOptions
=> new WhatsAppClient(httpFactory, Options.Create(options), logger);

/// <inheritdoc />
public async Task SendAync(string from, object payload)
public async Task SendAsync(string from, object payload)
{
if (!options.Numbers.TryGetValue(from, out var token))
throw new ArgumentException($"The number '{from}' is not registered in the options.", nameof(from));
Expand Down
Loading