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
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>

<!-- NuGet -->
<Version>5.9.3</Version>
<Version>5.9.4</Version>
<AssemblyVersion>5.9.0</AssemblyVersion>
<FileVersion>5.9.0</FileVersion>
<Authors>Jon Sagara</Authors>
Expand Down
2 changes: 1 addition & 1 deletion src/Sagara.Core.ConsoleRunner/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
//await googleChatSvc.SendMessageAsync(
// webhookUrl: jonWebhookUrl,
// bodyMarkdown: "Hello, Jon!",
// mentionUsers: [new GoogleWorkspaceUser(Email: "jon@example.com")]);
// mentionUsers: [new GoogleWorkspaceUser(Id: "123456789", Email: "jon@example.com")]);

//// Send a multi-line text-only message.
//await googleChatSvc.SendMessageAsync(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,28 @@ public void AddGoogleChatService_ResolvesGoogleChatService()

Assert.NotNull(service);
}

[Fact]
public void AddGoogleChatService_NoConfigureOptions_DefaultsToMentionById()
{
var services = new ServiceCollection();
services.AddGoogleChatService();

using var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<GoogleChatServiceOptions>();

Assert.Equal(GoogleChatMentionStyle.Id, options.MentionStyle);
}

[Fact]
public void AddGoogleChatService_ConfigureOptions_AppliesConfiguredMentionStyle()
{
var services = new ServiceCollection();
services.AddGoogleChatService(options => options.MentionStyle = GoogleChatMentionStyle.Email);

using var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<GoogleChatServiceOptions>();

Assert.Equal(GoogleChatMentionStyle.Email, options.MentionStyle);
}
}
27 changes: 23 additions & 4 deletions src/Sagara.Core.Google.Tests/Chat/GoogleChatServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,34 @@ await service.SendMessageAsync(
}

[Fact]
public async Task SendMessageAsync_MentionUsers_AppendsMentionChipsToText()
public async Task SendMessageAsync_MentionUsers_DefaultIdStyle_AppendsMentionChipsToText()
{
var handler = new CapturingHttpMessageHandler(HttpStatusCode.OK);
var service = CreateService(handler);

await service.SendMessageAsync(
WebhookUrl,
"hello",
mentionUsers: [new GoogleWorkspaceUser("jon@example.com"), new GoogleWorkspaceUser("jane@example.com")],
mentionUsers: [new GoogleWorkspaceUser(Id: "111", Email: "jon@example.com"), new GoogleWorkspaceUser(Id: "222", Email: "jane@example.com")],
cancellationToken: TestContext.Current.CancellationToken);

var json = await handler.GetRequestJsonAsync();
var text = json.GetProperty("text").GetString();

Assert.Contains("""<chat-user data-user="users/111">""", text, StringComparison.Ordinal);
Assert.Contains("""<chat-user data-user="users/222">""", text, StringComparison.Ordinal);
}

[Fact]
public async Task SendMessageAsync_MentionUsers_EmailStyle_AppendsMentionChipsToText()
{
var handler = new CapturingHttpMessageHandler(HttpStatusCode.OK);
var service = CreateService(handler, options: new GoogleChatServiceOptions { MentionStyle = GoogleChatMentionStyle.Email });

await service.SendMessageAsync(
WebhookUrl,
"hello",
mentionUsers: [new GoogleWorkspaceUser(Id: "111", Email: "jon@example.com"), new GoogleWorkspaceUser(Id: "222", Email: "jane@example.com")],
cancellationToken: TestContext.Current.CancellationToken);

var json = await handler.GetRequestJsonAsync();
Expand Down Expand Up @@ -181,8 +200,8 @@ await Assert.ThrowsAsync<ArgumentException>(

}

private static GoogleChatService CreateService(HttpMessageHandler handler, ILogger<GoogleChatService>? logger = null)
=> new(new HttpClient(handler), logger ?? NullLogger<GoogleChatService>.Instance);
private static GoogleChatService CreateService(HttpMessageHandler handler, ILogger<GoogleChatService>? logger = null, GoogleChatServiceOptions? options = null)
=> new(new HttpClient(handler), logger ?? NullLogger<GoogleChatService>.Instance, options ?? new GoogleChatServiceOptions());

private sealed class RecordingLogger<T> : ILogger<T>
{
Expand Down
17 changes: 17 additions & 0 deletions src/Sagara.Core.Google/Chat/GoogleChatMentionStyle.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Sagara.Core.Google.Chat;

/// <summary>
/// Specifies how a <see cref="GoogleWorkspaceUser"/> is referenced when mentioned in a chat message.
/// </summary>
public enum GoogleChatMentionStyle
{
/// <summary>
/// Mention users by their Google Workspace user ID.
/// </summary>
Id,

/// <summary>
/// Mention users by their email address.
/// </summary>
Email,
}
20 changes: 16 additions & 4 deletions src/Sagara.Core.Google/Chat/GoogleChatService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ public sealed class GoogleChatService

private readonly HttpClient _httpClient;
private readonly ILogger<GoogleChatService> _logger;
private readonly GoogleChatServiceOptions _options;

public GoogleChatService(HttpClient httpClient, ILogger<GoogleChatService> logger)
public GoogleChatService(HttpClient httpClient, ILogger<GoogleChatService> logger, GoogleChatServiceOptions options)
{
_httpClient = httpClient;
_logger = logger;
_options = options;
}


Expand Down Expand Up @@ -309,7 +311,7 @@ card.AlertLevel is null &&
}
}

private static ChatMessagePayload BuildPayload(
private ChatMessagePayload BuildPayload(
string? bodyMarkdown,
bool mentionAllUsers,
IReadOnlyCollection<GoogleWorkspaceUser>? mentionUsers,
Expand All @@ -329,7 +331,7 @@ private static ChatMessagePayload BuildPayload(
};
}

private static string BuildText(string? bodyMarkdown, bool mentionAllUsers, IReadOnlyCollection<GoogleWorkspaceUser>? mentionUsers)
private string BuildText(string? bodyMarkdown, bool mentionAllUsers, IReadOnlyCollection<GoogleWorkspaceUser>? mentionUsers)
{
var text = new StringBuilder(bodyMarkdown);

Expand All @@ -341,12 +343,22 @@ private static string BuildText(string? bodyMarkdown, bool mentionAllUsers, IRea
else if (mentionUsers is { Count: > 0 })
{
text.Append("\n\n");
text.AppendJoin(' ', mentionUsers.Select(user => $"<chat-user data-email=\"{WebUtility.HtmlEncode(user.Email)}\">"));
text.AppendJoin(' ', mentionUsers.Select(BuildUserMention));
}

return text.ToString();
}

private string BuildUserMention(GoogleWorkspaceUser user)
{
return _options.MentionStyle switch
{
GoogleChatMentionStyle.Id => $"<chat-user data-user=\"users/{WebUtility.HtmlEncode(user.Id)}\">",
GoogleChatMentionStyle.Email => $"<chat-user data-email=\"{WebUtility.HtmlEncode(user.Email)}\">",
_ => throw new InvalidOperationException($"Unsupported {nameof(GoogleChatMentionStyle)}: {_options.MentionStyle}."),
};
}

/// <summary>
/// Replaces newlines with &lt;br&gt; tags everywhere in <paramref name="markdown"/> except where doing so would
/// break Markdown block structure that Google's parser relies on real newlines to recognize. Rather than
Expand Down
14 changes: 12 additions & 2 deletions src/Sagara.Core.Google/Chat/GoogleChatServiceExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,24 @@ public static class GoogleChatServiceExtensions
{
/// <summary>
/// Registers a <see cref="GoogleChatService"/>. The webhook URL is supplied per call to one of
/// <see cref="GoogleChatService"/>'s SendMessageAsync overloads, not at registration time, so
/// <see cref="GoogleChatService"/>'s SendMessageAsync overloads, not at registration time, so
/// a single registered instance can send to any number of Google Chat spaces.
/// </summary>
/// <param name="services">The DI services collection to add to.</param>
public static IServiceCollection AddGoogleChatService(this IServiceCollection services)
/// <param name="configureOptions">
/// An optional callback to configure <see cref="GoogleChatServiceOptions"/>. If not specified, the
/// default options are used, which mention users by <see cref="GoogleChatMentionStyle.Id"/>.
/// </param>
public static IServiceCollection AddGoogleChatService(
this IServiceCollection services,
Action<GoogleChatServiceOptions>? configureOptions = null)
{
Check.ThrowIfNull(services);

var options = new GoogleChatServiceOptions();
configureOptions?.Invoke(options);

services.AddSingleton(options);
services.AddHttpClient<GoogleChatService>();

return services;
Expand Down
13 changes: 13 additions & 0 deletions src/Sagara.Core.Google/Chat/GoogleChatServiceOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Sagara.Core.Google.Chat;

/// <summary>
/// Configuration options for <see cref="GoogleChatService"/>.
/// </summary>
public sealed class GoogleChatServiceOptions
{
/// <summary>
/// How to reference a <see cref="GoogleWorkspaceUser"/> when mentioning them in a chat message.
/// Defaults to <see cref="GoogleChatMentionStyle.Id"/>.
/// </summary>
public GoogleChatMentionStyle MentionStyle { get; set; } = GoogleChatMentionStyle.Id;
}
3 changes: 2 additions & 1 deletion src/Sagara.Core.Google/Chat/GoogleWorkspaceUser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ namespace Sagara.Core.Google.Chat;
/// <summary>
/// A Google Workspace user to mention in a chat message.
/// </summary>
/// <param name="Id">The Google Workspace user's unique ID.</param>
/// <param name="Email">The Google Workspace user's email address.</param>
public sealed record GoogleWorkspaceUser(string Email);
public sealed record GoogleWorkspaceUser(string Id, string Email);