feat(notify): add FinlyticNotify push notification microservice and test suite
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FinlyticCore.Dtos;
|
||||
using FinlyticCore.Services;
|
||||
using FinlyticCore.Util;
|
||||
using FinlyticNotify.Settings;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace FinlyticNotify.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service interface for resolving a UserId GUID to a clean username for ntfy channel addressing.
|
||||
/// </summary>
|
||||
public interface IUserTradeResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves the clean username for a given UserId GUID via in-memory cache and FinlyticBackend MQTT RPC.
|
||||
/// </summary>
|
||||
/// <param name="userId">The unique ID of the user owning the trade.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The resolved username (e.g. "lars", "kleidukos", "admin") for ntfy channel addressing.</returns>
|
||||
Task<string> ResolveUsernameByUserIdAsync(Guid userId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="IUserTradeResolver"/> performing cached MQTT RPC calls to FinlyticBackend
|
||||
/// without any direct cross-database dependencies.
|
||||
/// </summary>
|
||||
public class UserTradeResolver : IUserTradeResolver
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly ILogger<UserTradeResolver> _logger;
|
||||
|
||||
private static readonly Regex InvalidChannelCharRegex = new("[^a-zA-Z0-9_-]", RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserTradeResolver"/> class.
|
||||
/// </summary>
|
||||
public UserTradeResolver(
|
||||
IServiceProvider serviceProvider,
|
||||
IMemoryCache cache,
|
||||
ISettingsService settingsService,
|
||||
IConfiguration configuration,
|
||||
ILogger<UserTradeResolver> logger)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_cache = cache;
|
||||
_settingsService = settingsService;
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<string> ResolveUsernameByUserIdAsync(Guid userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
string defaultUsername = await _settingsService.GetSettingAsync(NotifySettingKeys.NtfyDefaultUsername, cancellationToken);
|
||||
if (string.IsNullOrWhiteSpace(defaultUsername))
|
||||
{
|
||||
defaultUsername = _configuration.GetValue<string>("Ntfy:DefaultUsername") ?? "admin";
|
||||
}
|
||||
|
||||
if (userId == Guid.Empty)
|
||||
{
|
||||
return defaultUsername;
|
||||
}
|
||||
|
||||
string cacheKey = $"user_name_{userId}";
|
||||
if (_cache.TryGetValue(cacheKey, out string? cachedUsername) && !string.IsNullOrWhiteSpace(cachedUsername))
|
||||
{
|
||||
return cachedUsername;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var mqttClient = _serviceProvider.GetService<NotifyMqttClient>();
|
||||
if (mqttClient != null && mqttClient.IsConnected)
|
||||
{
|
||||
var username = await mqttClient.SendRpcRequestAsync<string, UserIdRequest>(
|
||||
MqttTopics.Channels.BackendGetUsername,
|
||||
new UserIdRequest(userId),
|
||||
TimeSpan.FromSeconds(2));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(username))
|
||||
{
|
||||
string cleanChannel = CleanUsername(username);
|
||||
_cache.Set(cacheKey, cleanChannel, TimeSpan.FromHours(1));
|
||||
return cleanChannel;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "[UserTradeResolver] Failed to resolve username from FinlyticBackend for UserId {UserId}. Falling back to default.", userId);
|
||||
}
|
||||
|
||||
return defaultUsername;
|
||||
}
|
||||
|
||||
private static string CleanUsername(string rawName)
|
||||
{
|
||||
var cleaned = rawName.Trim().ToLowerInvariant().Replace(" ", "_");
|
||||
cleaned = InvalidChannelCharRegex.Replace(cleaned, "");
|
||||
return string.IsNullOrWhiteSpace(cleaned) ? "admin" : cleaned;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user