189 lines
7.4 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|