Files

114 lines
4.2 KiB
C#

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;
}
}