Files
Finlytic/FinlyticNotify/Services/NtfyClient.cs
T

189 lines
7.4 KiB
C#

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;
/// <summary>
/// Model representing a notification payload to be dispatched via ntfy.
/// </summary>
public record NtfyNotification(
string Topic,
string Title,
string Message,
int Priority = 3,
List<string>? Tags = null,
string? ClickUrl = null
);
/// <summary>
/// Client interface for sending push notifications to an ntfy instance.
/// </summary>
public interface INtfyClient
{
/// <summary>
/// Sends a notification to the specified ntfy topic.
/// </summary>
/// <param name="notification">The notification payload.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>True if successfully delivered, false otherwise.</returns>
Task<bool> SendNotificationAsync(NtfyNotification notification, CancellationToken cancellationToken = default);
}
/// <summary>
/// High-performance HTTP client for dispatching push notifications to self-hosted ntfy server via JSON payload.
/// </summary>
public class NtfyClient : INtfyClient
{
private readonly HttpClient _httpClient;
private readonly ISettingsService? _settingsService;
private readonly IConfiguration _configuration;
private readonly IFinlyticLogger<NtfyClient>? _finlyticLogger;
private readonly ILogger<NtfyClient> _logger;
private static readonly JsonSerializerOptions JsonOptions = new()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
/// <summary>
/// Initializes a new instance of the <see cref="NtfyClient"/> class.
/// </summary>
public NtfyClient(
HttpClient httpClient,
IConfiguration configuration,
ILogger<NtfyClient> logger,
ISettingsService? settingsService = null,
IFinlyticLogger<NtfyClient>? finlyticLogger = null)
{
_httpClient = httpClient;
_configuration = configuration;
_logger = logger;
_settingsService = settingsService;
_finlyticLogger = finlyticLogger;
}
/// <inheritdoc />
public async Task<bool> 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<string>("Ntfy:BaseUrl") ?? NotifySettingKeys.NtfyBaseUrl.DefaultValue;
}
}
else
{
baseUrl = _configuration.GetValue<string>("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<string, object?>
{
["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<string>("Ntfy:AuthToken") : token;
authUser = string.IsNullOrWhiteSpace(authUser) ? _configuration.GetValue<string>("Ntfy:Username") : authUser;
authPass = string.IsNullOrWhiteSpace(authPass) ? _configuration.GetValue<string>("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;
}
}
}