Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
sdk-generator-smoke / sdk-smoke (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
157 lines
5.9 KiB
C#
157 lines
5.9 KiB
C#
using System.Text.Json;
|
|
using Microsoft.Extensions.Logging;
|
|
using StellaOps.Notify.Models;
|
|
|
|
namespace StellaOps.Notifier.Worker.Channels;
|
|
|
|
/// <summary>
|
|
/// Channel adapter for in-app inbox notifications.
|
|
/// Stores notifications in the database for users to retrieve via API or WebSocket.
|
|
/// </summary>
|
|
public sealed class InAppInboxChannelAdapter : INotifyChannelAdapter
|
|
{
|
|
private readonly IInAppInboxStore _inboxStore;
|
|
private readonly ILogger<InAppInboxChannelAdapter> _logger;
|
|
|
|
public InAppInboxChannelAdapter(IInAppInboxStore inboxStore, ILogger<InAppInboxChannelAdapter> logger)
|
|
{
|
|
_inboxStore = inboxStore ?? throw new ArgumentNullException(nameof(inboxStore));
|
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
}
|
|
|
|
public NotifyChannelType ChannelType => NotifyChannelType.InAppInbox;
|
|
|
|
public async Task<ChannelDispatchResult> SendAsync(
|
|
NotifyChannel channel,
|
|
NotifyDeliveryRendered rendered,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(channel);
|
|
ArgumentNullException.ThrowIfNull(rendered);
|
|
|
|
var userId = rendered.Target;
|
|
if (string.IsNullOrWhiteSpace(userId))
|
|
{
|
|
// Try to get from channel config
|
|
userId = channel.Config?.Target;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(userId))
|
|
{
|
|
return ChannelDispatchResult.Fail("Target user ID not specified", shouldRetry: false);
|
|
}
|
|
|
|
var tenantId = channel.Config?.Properties.GetValueOrDefault("tenantId") ?? channel.TenantId;
|
|
|
|
var messageId = Guid.NewGuid().ToString("N");
|
|
var inboxMessage = new InAppInboxMessage
|
|
{
|
|
MessageId = messageId,
|
|
TenantId = tenantId,
|
|
UserId = userId,
|
|
Title = rendered.Title ?? "Notification",
|
|
Body = rendered.Body ?? string.Empty,
|
|
Summary = rendered.Summary,
|
|
Category = channel.Config?.Properties.GetValueOrDefault("category") ?? "general",
|
|
Priority = DeterminePriority(rendered),
|
|
Metadata = null,
|
|
CreatedAt = DateTimeOffset.UtcNow,
|
|
ExpiresAt = DetermineExpiry(channel),
|
|
SourceChannel = channel.ChannelId,
|
|
DeliveryId = messageId
|
|
};
|
|
|
|
try
|
|
{
|
|
await _inboxStore.StoreAsync(inboxMessage, cancellationToken).ConfigureAwait(false);
|
|
|
|
_logger.LogInformation(
|
|
"In-app inbox message stored for user {UserId}. MessageId: {MessageId}",
|
|
userId,
|
|
inboxMessage.MessageId);
|
|
|
|
return ChannelDispatchResult.Ok();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to store in-app inbox message for user {UserId}", userId);
|
|
return ChannelDispatchResult.Fail(ex.Message, shouldRetry: true);
|
|
}
|
|
}
|
|
|
|
private static InAppInboxPriority DeterminePriority(NotifyDeliveryRendered rendered)
|
|
{
|
|
if (rendered.Title?.Contains("critical", StringComparison.OrdinalIgnoreCase) == true ||
|
|
rendered.Title?.Contains("urgent", StringComparison.OrdinalIgnoreCase) == true)
|
|
return InAppInboxPriority.Critical;
|
|
|
|
if (rendered.Title?.Contains("error", StringComparison.OrdinalIgnoreCase) == true ||
|
|
rendered.Title?.Contains("important", StringComparison.OrdinalIgnoreCase) == true)
|
|
return InAppInboxPriority.High;
|
|
|
|
if (rendered.Title?.Contains("warning", StringComparison.OrdinalIgnoreCase) == true)
|
|
return InAppInboxPriority.Normal;
|
|
|
|
return InAppInboxPriority.Low;
|
|
}
|
|
|
|
private static DateTimeOffset? DetermineExpiry(NotifyChannel channel)
|
|
{
|
|
var ttlStr = channel.Config?.Properties.GetValueOrDefault("ttl");
|
|
if (!string.IsNullOrEmpty(ttlStr) && int.TryParse(ttlStr, out var ttlHours))
|
|
{
|
|
return DateTimeOffset.UtcNow.AddHours(ttlHours);
|
|
}
|
|
|
|
// Default 30 day expiry
|
|
return DateTimeOffset.UtcNow.AddDays(30);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Storage interface for in-app inbox messages.
|
|
/// </summary>
|
|
public interface IInAppInboxStore
|
|
{
|
|
Task StoreAsync(InAppInboxMessage message, CancellationToken cancellationToken = default);
|
|
Task<IReadOnlyList<InAppInboxMessage>> GetForUserAsync(string tenantId, string userId, int limit = 50, CancellationToken cancellationToken = default);
|
|
Task<InAppInboxMessage?> GetAsync(string tenantId, string messageId, CancellationToken cancellationToken = default);
|
|
Task MarkReadAsync(string tenantId, string messageId, CancellationToken cancellationToken = default);
|
|
Task MarkAllReadAsync(string tenantId, string userId, CancellationToken cancellationToken = default);
|
|
Task DeleteAsync(string tenantId, string messageId, CancellationToken cancellationToken = default);
|
|
Task<int> GetUnreadCountAsync(string tenantId, string userId, CancellationToken cancellationToken = default);
|
|
}
|
|
|
|
/// <summary>
|
|
/// In-app inbox message model.
|
|
/// </summary>
|
|
public sealed record InAppInboxMessage
|
|
{
|
|
public required string MessageId { get; init; }
|
|
public required string TenantId { get; init; }
|
|
public required string UserId { get; init; }
|
|
public required string Title { get; init; }
|
|
public required string Body { get; init; }
|
|
public string? Summary { get; init; }
|
|
public required string Category { get; init; }
|
|
public InAppInboxPriority Priority { get; init; }
|
|
public IReadOnlyDictionary<string, string>? Metadata { get; init; }
|
|
public DateTimeOffset CreatedAt { get; init; }
|
|
public DateTimeOffset? ExpiresAt { get; init; }
|
|
public DateTimeOffset? ReadAt { get; set; }
|
|
public bool IsRead => ReadAt.HasValue;
|
|
public string? SourceChannel { get; init; }
|
|
public string? DeliveryId { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Priority levels for in-app inbox messages.
|
|
/// </summary>
|
|
public enum InAppInboxPriority
|
|
{
|
|
Low,
|
|
Normal,
|
|
High,
|
|
Critical
|
|
}
|