336 lines
9.4 KiB
C#
336 lines
9.4 KiB
C#
using System.Net.Http.Headers;
|
|
using System.Net.Http.Json;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
|
|
namespace IronNotify.Client;
|
|
|
|
public class NotifyClient : IDisposable
|
|
{
|
|
private readonly HttpClient _httpClient;
|
|
private readonly NotifyClientOptions _options;
|
|
private readonly NotifyOfflineQueue? _offlineQueue;
|
|
private bool _disposed;
|
|
|
|
/// <summary>
|
|
/// Gets the offline queue for accessing queued items.
|
|
/// </summary>
|
|
public NotifyOfflineQueue? OfflineQueue => _offlineQueue;
|
|
|
|
public NotifyClient(NotifyClientOptions options)
|
|
{
|
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
|
|
|
if (string.IsNullOrEmpty(options.ApiKey))
|
|
throw new ArgumentException("API key is required", nameof(options));
|
|
|
|
_httpClient = new HttpClient
|
|
{
|
|
BaseAddress = new Uri(options.BaseUrl.TrimEnd('/') + "/")
|
|
};
|
|
|
|
_httpClient.DefaultRequestHeaders.Authorization =
|
|
new AuthenticationHeaderValue("Bearer", options.ApiKey);
|
|
_httpClient.DefaultRequestHeaders.Accept.Add(
|
|
new MediaTypeWithQualityHeaderValue("application/json"));
|
|
|
|
if (options.Timeout.HasValue)
|
|
_httpClient.Timeout = options.Timeout.Value;
|
|
|
|
// Initialize offline queue if enabled
|
|
if (options.EnableOfflineQueue)
|
|
{
|
|
_offlineQueue = new NotifyOfflineQueue(
|
|
SendQueuedEventAsync,
|
|
options.OfflineQueueDirectory,
|
|
options.MaxOfflineQueueSize,
|
|
enableAutoRetry: true);
|
|
}
|
|
}
|
|
|
|
private async Task<bool> SendQueuedEventAsync(QueuedEvent queuedEvent)
|
|
{
|
|
var result = await NotifyAsync(queuedEvent.Request, skipQueue: true);
|
|
return result.Success;
|
|
}
|
|
|
|
public NotifyClient(string apiKey, string? appSlug = null)
|
|
: this(new NotifyClientOptions { ApiKey = apiKey, DefaultAppSlug = appSlug })
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Send an event notification
|
|
/// </summary>
|
|
public Task<EventResult> NotifyAsync(NotifyEventRequest request, CancellationToken cancellationToken = default)
|
|
{
|
|
return NotifyAsync(request, skipQueue: false, cancellationToken);
|
|
}
|
|
|
|
private async Task<EventResult> NotifyAsync(NotifyEventRequest request, bool skipQueue, CancellationToken cancellationToken = default)
|
|
{
|
|
var apiRequest = new
|
|
{
|
|
appSlug = request.AppSlug ?? _options.DefaultAppSlug,
|
|
appId = request.AppId,
|
|
eventType = request.EventType,
|
|
severity = request.Severity.ToString(),
|
|
source = request.Source ?? _options.DefaultSource,
|
|
title = request.Title,
|
|
message = request.Message,
|
|
entityId = request.EntityId,
|
|
metadata = request.Metadata,
|
|
actions = request.Actions?.Select(a => new
|
|
{
|
|
actionId = a.ActionId,
|
|
label = a.Label,
|
|
webhookUrl = a.WebhookUrl
|
|
})
|
|
};
|
|
|
|
try
|
|
{
|
|
var response = await _httpClient.PostAsJsonAsync("api/v1/events", apiRequest, cancellationToken);
|
|
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
var result = await response.Content.ReadFromJsonAsync<EventResponse>(cancellationToken: cancellationToken);
|
|
return new EventResult
|
|
{
|
|
Success = true,
|
|
EventId = result?.Id,
|
|
Status = result?.Status
|
|
};
|
|
}
|
|
|
|
var error = await response.Content.ReadAsStringAsync(cancellationToken);
|
|
|
|
// Queue for retry if enabled and not already retrying
|
|
if (!skipQueue && _offlineQueue != null)
|
|
{
|
|
_offlineQueue.Enqueue(request);
|
|
}
|
|
|
|
return new EventResult
|
|
{
|
|
Success = false,
|
|
Error = error,
|
|
Queued = !skipQueue && _offlineQueue != null
|
|
};
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Queue for retry on network errors
|
|
if (!skipQueue && _offlineQueue != null)
|
|
{
|
|
_offlineQueue.Enqueue(request);
|
|
}
|
|
|
|
return new EventResult
|
|
{
|
|
Success = false,
|
|
Error = ex.Message,
|
|
Queued = !skipQueue && _offlineQueue != null
|
|
};
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Send a simple event notification
|
|
/// </summary>
|
|
public Task<EventResult> NotifyAsync(
|
|
string eventType,
|
|
string title,
|
|
Severity severity = Severity.Info,
|
|
string? message = null,
|
|
Dictionary<string, object>? metadata = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
return NotifyAsync(new NotifyEventRequest
|
|
{
|
|
EventType = eventType,
|
|
Title = title,
|
|
Severity = severity,
|
|
Message = message,
|
|
Metadata = metadata
|
|
}, cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a fluent event builder
|
|
/// </summary>
|
|
public EventBuilder Event(string eventType) => new(this, eventType);
|
|
|
|
public void Dispose()
|
|
{
|
|
if (!_disposed)
|
|
{
|
|
_offlineQueue?.Dispose();
|
|
_httpClient.Dispose();
|
|
_disposed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
public class NotifyClientOptions
|
|
{
|
|
public string ApiKey { get; set; } = string.Empty;
|
|
public string BaseUrl { get; set; } = "https://ironnotify.com";
|
|
public string? DefaultAppSlug { get; set; }
|
|
public string? DefaultSource { get; set; }
|
|
public TimeSpan? Timeout { get; set; }
|
|
|
|
/// <summary>
|
|
/// Enable offline queue for failed sends. Default is true.
|
|
/// </summary>
|
|
public bool EnableOfflineQueue { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// Directory to store offline queue. Defaults to LocalApplicationData/IronNotify/Queue.
|
|
/// </summary>
|
|
public string? OfflineQueueDirectory { get; set; }
|
|
|
|
/// <summary>
|
|
/// Maximum items to store in offline queue. Default: 500.
|
|
/// </summary>
|
|
public int MaxOfflineQueueSize { get; set; } = 500;
|
|
}
|
|
|
|
public class NotifyEventRequest
|
|
{
|
|
public Guid? AppId { get; set; }
|
|
public string? AppSlug { get; set; }
|
|
public string EventType { get; set; } = string.Empty;
|
|
public Severity Severity { get; set; } = Severity.Info;
|
|
public string? Source { get; set; }
|
|
public string Title { get; set; } = string.Empty;
|
|
public string? Message { get; set; }
|
|
public string? EntityId { get; set; }
|
|
public Dictionary<string, object>? Metadata { get; set; }
|
|
public List<EventAction>? Actions { get; set; }
|
|
}
|
|
|
|
public class EventAction
|
|
{
|
|
public string ActionId { get; set; } = string.Empty;
|
|
public string Label { get; set; } = string.Empty;
|
|
public string? WebhookUrl { get; set; }
|
|
}
|
|
|
|
public enum Severity
|
|
{
|
|
Info,
|
|
Warning,
|
|
High,
|
|
Critical
|
|
}
|
|
|
|
public class EventResult
|
|
{
|
|
public bool Success { get; set; }
|
|
public Guid? EventId { get; set; }
|
|
public string? Status { get; set; }
|
|
public string? Error { get; set; }
|
|
|
|
/// <summary>
|
|
/// Whether the event was queued for later retry (when offline queue is enabled).
|
|
/// </summary>
|
|
public bool Queued { get; set; }
|
|
}
|
|
|
|
internal class EventResponse
|
|
{
|
|
[JsonPropertyName("id")]
|
|
public Guid Id { get; set; }
|
|
|
|
[JsonPropertyName("status")]
|
|
public string Status { get; set; } = string.Empty;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fluent builder for creating events
|
|
/// </summary>
|
|
public class EventBuilder
|
|
{
|
|
private readonly NotifyClient _client;
|
|
private readonly NotifyEventRequest _request;
|
|
|
|
internal EventBuilder(NotifyClient client, string eventType)
|
|
{
|
|
_client = client;
|
|
_request = new NotifyEventRequest { EventType = eventType };
|
|
}
|
|
|
|
public EventBuilder WithSeverity(Severity severity)
|
|
{
|
|
_request.Severity = severity;
|
|
return this;
|
|
}
|
|
|
|
public EventBuilder WithTitle(string title)
|
|
{
|
|
_request.Title = title;
|
|
return this;
|
|
}
|
|
|
|
public EventBuilder WithMessage(string message)
|
|
{
|
|
_request.Message = message;
|
|
return this;
|
|
}
|
|
|
|
public EventBuilder WithSource(string source)
|
|
{
|
|
_request.Source = source;
|
|
return this;
|
|
}
|
|
|
|
public EventBuilder WithEntityId(string entityId)
|
|
{
|
|
_request.EntityId = entityId;
|
|
return this;
|
|
}
|
|
|
|
public EventBuilder WithApp(string slug)
|
|
{
|
|
_request.AppSlug = slug;
|
|
return this;
|
|
}
|
|
|
|
public EventBuilder WithApp(Guid appId)
|
|
{
|
|
_request.AppId = appId;
|
|
return this;
|
|
}
|
|
|
|
public EventBuilder WithMetadata(string key, object value)
|
|
{
|
|
_request.Metadata ??= new Dictionary<string, object>();
|
|
_request.Metadata[key] = value;
|
|
return this;
|
|
}
|
|
|
|
public EventBuilder WithMetadata(Dictionary<string, object> metadata)
|
|
{
|
|
_request.Metadata = metadata;
|
|
return this;
|
|
}
|
|
|
|
public EventBuilder WithAction(string actionId, string label, string? webhookUrl = null)
|
|
{
|
|
_request.Actions ??= new List<EventAction>();
|
|
_request.Actions.Add(new EventAction
|
|
{
|
|
ActionId = actionId,
|
|
Label = label,
|
|
WebhookUrl = webhookUrl
|
|
});
|
|
return this;
|
|
}
|
|
|
|
public Task<EventResult> SendAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
return _client.NotifyAsync(_request, cancellationToken);
|
|
}
|
|
}
|