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; /// /// Service interface for resolving a UserId GUID to a clean username for ntfy channel addressing. /// public interface IUserTradeResolver { /// /// Resolves the clean username for a given UserId GUID via in-memory cache and FinlyticBackend MQTT RPC. /// /// The unique ID of the user owning the trade. /// Cancellation token. /// The resolved username (e.g. "lars", "kleidukos", "admin") for ntfy channel addressing. Task ResolveUsernameByUserIdAsync(Guid userId, CancellationToken cancellationToken = default); } /// /// Implementation of performing cached MQTT RPC calls to FinlyticBackend /// without any direct cross-database dependencies. /// public class UserTradeResolver : IUserTradeResolver { private readonly IServiceProvider _serviceProvider; private readonly IMemoryCache _cache; private readonly ISettingsService _settingsService; private readonly IConfiguration _configuration; private readonly ILogger _logger; private static readonly Regex InvalidChannelCharRegex = new("[^a-zA-Z0-9_-]", RegexOptions.Compiled); /// /// Initializes a new instance of the class. /// public UserTradeResolver( IServiceProvider serviceProvider, IMemoryCache cache, ISettingsService settingsService, IConfiguration configuration, ILogger logger) { _serviceProvider = serviceProvider; _cache = cache; _settingsService = settingsService; _configuration = configuration; _logger = logger; } /// public async Task ResolveUsernameByUserIdAsync(Guid userId, CancellationToken cancellationToken = default) { string defaultUsername = await _settingsService.GetSettingAsync(NotifySettingKeys.NtfyDefaultUsername, cancellationToken); if (string.IsNullOrWhiteSpace(defaultUsername)) { defaultUsername = _configuration.GetValue("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(); if (mqttClient != null && mqttClient.IsConnected) { var username = await mqttClient.SendRpcRequestAsync( 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; } }