using System; using System.Collections.Generic; using System.Net.Http; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using FinlyticCore.Services; using FinlyticNotify.Settings; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace FinlyticNotify.Services; /// /// Model representing a notification payload to be dispatched via ntfy. /// public record NtfyNotification( string Topic, string Title, string Message, int Priority = 3, List? Tags = null, string? ClickUrl = null ); /// /// Client interface for sending push notifications to an ntfy instance. /// public interface INtfyClient { /// /// Sends a notification to the specified ntfy topic. /// /// The notification payload. /// Cancellation token. /// True if successfully delivered, false otherwise. Task SendNotificationAsync(NtfyNotification notification, CancellationToken cancellationToken = default); } /// /// High-performance HTTP client for dispatching push notifications to self-hosted ntfy server via JSON payload. /// public class NtfyClient : INtfyClient { private readonly HttpClient _httpClient; private readonly ISettingsService? _settingsService; private readonly IConfiguration _configuration; private readonly IFinlyticLogger? _finlyticLogger; private readonly ILogger _logger; private static readonly JsonSerializerOptions JsonOptions = new() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; /// /// Initializes a new instance of the class. /// public NtfyClient( HttpClient httpClient, IConfiguration configuration, ILogger logger, ISettingsService? settingsService = null, IFinlyticLogger? finlyticLogger = null) { _httpClient = httpClient; _configuration = configuration; _logger = logger; _settingsService = settingsService; _finlyticLogger = finlyticLogger; } /// public async Task SendNotificationAsync(NtfyNotification notification, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(notification.Topic)) { _logger.LogWarning("[NtfyClient] Aborting send: Topic is empty."); return false; } string baseUrl = "http://localhost:8080"; if (_settingsService != null) { try { baseUrl = await _settingsService.GetSettingAsync(NotifySettingKeys.NtfyBaseUrl, cancellationToken); } catch { baseUrl = _configuration.GetValue("Ntfy:BaseUrl") ?? NotifySettingKeys.NtfyBaseUrl.DefaultValue; } } else { baseUrl = _configuration.GetValue("Ntfy:BaseUrl") ?? NotifySettingKeys.NtfyBaseUrl.DefaultValue; } baseUrl = baseUrl.TrimEnd('/'); // Build native ntfy JSON payload (preserves full UTF-8 Unicode, Emojis, and Markdown without HTTP header ASCII constraints) var payload = new Dictionary { ["topic"] = notification.Topic.TrimStart('/'), ["title"] = notification.Title, ["message"] = notification.Message, ["priority"] = Math.Clamp(notification.Priority, 1, 5), ["tags"] = notification.Tags, ["click"] = notification.ClickUrl, ["markdown"] = true }; string json = JsonSerializer.Serialize(payload, JsonOptions); string? token = null; string? authUser = null; string? authPass = null; if (_settingsService != null) { try { token = await _settingsService.GetSettingAsync(NotifySettingKeys.NtfyAuthToken, cancellationToken); authUser = await _settingsService.GetSettingAsync(NotifySettingKeys.NtfyUsername, cancellationToken); authPass = await _settingsService.GetSettingAsync(NotifySettingKeys.NtfyPassword, cancellationToken); } catch { } } token = string.IsNullOrWhiteSpace(token) ? _configuration.GetValue("Ntfy:AuthToken") : token; authUser = string.IsNullOrWhiteSpace(authUser) ? _configuration.GetValue("Ntfy:Username") : authUser; authPass = string.IsNullOrWhiteSpace(authPass) ? _configuration.GetValue("Ntfy:Password") : authPass; try { using var request = new HttpRequestMessage(HttpMethod.Post, baseUrl); request.Content = new StringContent(json, Encoding.UTF8, "application/json"); if (!string.IsNullOrWhiteSpace(token)) { request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token.Trim()); } else if (!string.IsNullOrWhiteSpace(authUser) && !string.IsNullOrWhiteSpace(authPass)) { var authBytes = Encoding.UTF8.GetBytes($"{authUser}:{authPass}"); request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(authBytes)); } var response = await _httpClient.SendAsync(request, cancellationToken); if (response.IsSuccessStatusCode) { _logger.LogInformation("[NtfyClient] Push notification successfully delivered to {TargetUrl} (Topic: {Topic})", baseUrl, notification.Topic); if (_finlyticLogger != null) { await _finlyticLogger.LogInfoAsync(NotifySettingKeys.NotificationDeliveryChannel, "[NtfyDelivery] Push notification successfully delivered to topic '{Topic}' (HTTP {StatusCode})", notification.Topic, (int)response.StatusCode); } return true; } string errorBody = await response.Content.ReadAsStringAsync(cancellationToken); _logger.LogWarning("[NtfyClient] Failed to send notification to {TargetUrl} for topic {Topic}. HTTP {Status}: {Body}", baseUrl, notification.Topic, (int)response.StatusCode, errorBody); if (_finlyticLogger != null) { await _finlyticLogger.LogWarningAsync(NotifySettingKeys.NotificationDeliveryChannel, "[NtfyDelivery] Failed to send push notification to topic '{Topic}' (HTTP {StatusCode})", notification.Topic, (int)response.StatusCode); } return false; } catch (Exception ex) { _logger.LogError(ex, "[NtfyClient] Unexpected error sending push notification to {TargetUrl} for topic {Topic}", baseUrl, notification.Topic); if (_finlyticLogger != null) { await _finlyticLogger.LogErrorAsync(NotifySettingKeys.NotificationDeliveryChannel, ex, "[NtfyDelivery] Error sending push notification to topic '{Topic}'", notification.Topic); } return false; } } }