💥 Rename Pusher to Ring
This commit is contained in:
21
DysonNetwork.Ring/Notification/Notification.cs
Normal file
21
DysonNetwork.Ring/Notification/Notification.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Ring.Notification;
|
||||
|
||||
public class Notification : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
[MaxLength(1024)] public string Topic { get; set; } = null!;
|
||||
[MaxLength(1024)] public string? Title { get; set; }
|
||||
[MaxLength(2048)] public string? Subtitle { get; set; }
|
||||
[MaxLength(4096)] public string? Content { get; set; }
|
||||
[Column(TypeName = "jsonb")] public Dictionary<string, object?> Meta { get; set; } = new();
|
||||
public int Priority { get; set; } = 10;
|
||||
public Instant? ViewedAt { get; set; }
|
||||
|
||||
public Guid AccountId { get; set; }
|
||||
}
|
||||
|
163
DysonNetwork.Ring/Notification/NotificationController.cs
Normal file
163
DysonNetwork.Ring/Notification/NotificationController.cs
Normal file
@@ -0,0 +1,163 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Shared.Auth;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Ring.Notification;
|
||||
|
||||
[ApiController]
|
||||
[Route("/api/notifications")]
|
||||
public class NotificationController(
|
||||
AppDatabase db,
|
||||
PushService nty
|
||||
) : ControllerBase
|
||||
{
|
||||
[HttpGet("count")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<int>> CountUnreadNotifications()
|
||||
{
|
||||
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
|
||||
if (currentUserValue is not Account currentUser) return Unauthorized();
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
var count = await db.Notifications
|
||||
.Where(s => s.AccountId == accountId && s.ViewedAt == null)
|
||||
.CountAsync();
|
||||
return Ok(count);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<List<Notification>>> ListNotifications(
|
||||
[FromQuery] int offset = 0,
|
||||
// The page size set to 5 is to avoid the client pulled the notification
|
||||
// but didn't render it in the screen-viewable region.
|
||||
[FromQuery] int take = 8
|
||||
)
|
||||
{
|
||||
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
|
||||
if (currentUserValue is not Account currentUser) return Unauthorized();
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
var totalCount = await db.Notifications
|
||||
.Where(s => s.AccountId == accountId)
|
||||
.CountAsync();
|
||||
var notifications = await db.Notifications
|
||||
.Where(s => s.AccountId == accountId)
|
||||
.OrderByDescending(e => e.CreatedAt)
|
||||
.Skip(offset)
|
||||
.Take(take)
|
||||
.ToListAsync();
|
||||
|
||||
Response.Headers["X-Total"] = totalCount.ToString();
|
||||
await nty.MarkNotificationsViewed(notifications.ToList());
|
||||
|
||||
return Ok(notifications);
|
||||
}
|
||||
|
||||
[HttpPost("all/read")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult> MarkAllNotificationsViewed()
|
||||
{
|
||||
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
|
||||
if (currentUserValue is not Account currentUser) return Unauthorized();
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
await nty.MarkAllNotificationsViewed(accountId);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
public class PushNotificationSubscribeRequest
|
||||
{
|
||||
[MaxLength(4096)] public string DeviceToken { get; set; } = null!;
|
||||
public PushProvider Provider { get; set; }
|
||||
}
|
||||
|
||||
[HttpPut("subscription")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<PushSubscription>>
|
||||
SubscribeToPushNotification(
|
||||
[FromBody] PushNotificationSubscribeRequest request
|
||||
)
|
||||
{
|
||||
HttpContext.Items.TryGetValue("CurrentSession", out var currentSessionValue);
|
||||
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
|
||||
var currentUser = currentUserValue as Account;
|
||||
if (currentUser == null) return Unauthorized();
|
||||
var currentSession = currentSessionValue as AuthSession;
|
||||
if (currentSession == null) return Unauthorized();
|
||||
|
||||
var result =
|
||||
await nty.SubscribeDevice(
|
||||
currentSession.Challenge.DeviceId,
|
||||
request.DeviceToken,
|
||||
request.Provider,
|
||||
currentUser
|
||||
);
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpDelete("subscription")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<int>> UnsubscribeFromPushNotification()
|
||||
{
|
||||
HttpContext.Items.TryGetValue("CurrentSession", out var currentSessionValue);
|
||||
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
|
||||
var currentUser = currentUserValue as Account;
|
||||
if (currentUser == null) return Unauthorized();
|
||||
var currentSession = currentSessionValue as AuthSession;
|
||||
if (currentSession == null) return Unauthorized();
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
var affectedRows = await db.PushSubscriptions
|
||||
.Where(s =>
|
||||
s.AccountId == accountId &&
|
||||
s.DeviceId == currentSession.Challenge.DeviceId
|
||||
).ExecuteDeleteAsync();
|
||||
return Ok(affectedRows);
|
||||
}
|
||||
|
||||
public class NotificationRequest
|
||||
{
|
||||
[Required][MaxLength(1024)] public string Topic { get; set; } = null!;
|
||||
[Required][MaxLength(1024)] public string Title { get; set; } = null!;
|
||||
[MaxLength(2048)] public string? Subtitle { get; set; }
|
||||
[Required][MaxLength(4096)] public string Content { get; set; } = null!;
|
||||
public Dictionary<string, object?>? Meta { get; set; }
|
||||
public int Priority { get; set; } = 10;
|
||||
}
|
||||
|
||||
public class NotificationWithAimRequest : NotificationRequest
|
||||
{
|
||||
[Required] public List<Guid> AccountId { get; set; } = null!;
|
||||
}
|
||||
|
||||
[HttpPost("send")]
|
||||
[Authorize]
|
||||
[RequiredPermission("global", "notifications.send")]
|
||||
public async Task<ActionResult> SendNotification(
|
||||
[FromBody] NotificationWithAimRequest request,
|
||||
[FromQuery] bool save = false
|
||||
)
|
||||
{
|
||||
await nty.SendNotificationBatch(
|
||||
new Notification
|
||||
{
|
||||
CreatedAt = SystemClock.Instance.GetCurrentInstant(),
|
||||
UpdatedAt = SystemClock.Instance.GetCurrentInstant(),
|
||||
Topic = request.Topic,
|
||||
Title = request.Title,
|
||||
Subtitle = request.Subtitle,
|
||||
Content = request.Content,
|
||||
Meta = request.Meta ?? [],
|
||||
},
|
||||
request.AccountId,
|
||||
save
|
||||
);
|
||||
return Ok();
|
||||
}
|
||||
}
|
27
DysonNetwork.Ring/Notification/NotificationFlushHandler.cs
Normal file
27
DysonNetwork.Ring/Notification/NotificationFlushHandler.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using DysonNetwork.Shared.Cache;
|
||||
using EFCore.BulkExtensions;
|
||||
using NodaTime;
|
||||
using Quartz;
|
||||
|
||||
namespace DysonNetwork.Ring.Notification;
|
||||
|
||||
public class NotificationFlushHandler(AppDatabase db) : IFlushHandler<Notification>
|
||||
{
|
||||
public async Task FlushAsync(IReadOnlyList<Notification> items)
|
||||
{
|
||||
await db.BulkInsertAsync(items.Select(x =>
|
||||
{
|
||||
x.CreatedAt = SystemClock.Instance.GetCurrentInstant();
|
||||
x.UpdatedAt = x.CreatedAt;
|
||||
return x;
|
||||
}), config => config.ConflictOption = ConflictOption.Ignore);
|
||||
}
|
||||
}
|
||||
|
||||
public class NotificationFlushJob(FlushBufferService fbs, NotificationFlushHandler hdl) : IJob
|
||||
{
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
await fbs.FlushAsync(hdl);
|
||||
}
|
||||
}
|
385
DysonNetwork.Ring/Notification/PushService.cs
Normal file
385
DysonNetwork.Ring/Notification/PushService.cs
Normal file
@@ -0,0 +1,385 @@
|
||||
using CorePush.Apple;
|
||||
using CorePush.Firebase;
|
||||
using DysonNetwork.Ring.Connection;
|
||||
using DysonNetwork.Ring.Services;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
using WebSocketPacket = DysonNetwork.Ring.Connection.WebSocketPacket;
|
||||
|
||||
namespace DysonNetwork.Ring.Notification;
|
||||
|
||||
public class PushService
|
||||
{
|
||||
private readonly AppDatabase _db;
|
||||
private readonly WebSocketService _ws;
|
||||
private readonly QueueService _queueService;
|
||||
private readonly ILogger<PushService> _logger;
|
||||
private readonly FirebaseSender? _fcm;
|
||||
private readonly ApnSender? _apns;
|
||||
private readonly string? _apnsTopic;
|
||||
|
||||
public PushService(
|
||||
IConfiguration config,
|
||||
AppDatabase db,
|
||||
WebSocketService ws,
|
||||
QueueService queueService,
|
||||
IHttpClientFactory httpFactory,
|
||||
ILogger<PushService> logger
|
||||
)
|
||||
{
|
||||
var cfgSection = config.GetSection("Notifications:Push");
|
||||
|
||||
// Set up Firebase Cloud Messaging
|
||||
var fcmConfig = cfgSection.GetValue<string>("Google");
|
||||
if (fcmConfig != null && File.Exists(fcmConfig))
|
||||
_fcm = new FirebaseSender(File.ReadAllText(fcmConfig), httpFactory.CreateClient());
|
||||
|
||||
// Set up Apple Push Notification Service
|
||||
var apnsKeyPath = cfgSection.GetValue<string>("Apple:PrivateKey");
|
||||
if (apnsKeyPath != null && File.Exists(apnsKeyPath))
|
||||
{
|
||||
_apns = new ApnSender(new ApnSettings
|
||||
{
|
||||
P8PrivateKey = File.ReadAllText(apnsKeyPath),
|
||||
P8PrivateKeyId = cfgSection.GetValue<string>("Apple:PrivateKeyId"),
|
||||
TeamId = cfgSection.GetValue<string>("Apple:TeamId"),
|
||||
AppBundleIdentifier = cfgSection.GetValue<string>("Apple:BundleIdentifier"),
|
||||
ServerType = cfgSection.GetValue<bool>("Production")
|
||||
? ApnServerType.Production
|
||||
: ApnServerType.Development
|
||||
}, httpFactory.CreateClient());
|
||||
_apnsTopic = cfgSection.GetValue<string>("Apple:BundleIdentifier");
|
||||
}
|
||||
|
||||
_db = db;
|
||||
_ws = ws;
|
||||
_queueService = queueService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task UnsubscribeDevice(string deviceId)
|
||||
{
|
||||
await _db.PushSubscriptions
|
||||
.Where(s => s.DeviceId == deviceId)
|
||||
.ExecuteDeleteAsync();
|
||||
}
|
||||
|
||||
public async Task<PushSubscription> SubscribeDevice(
|
||||
string deviceId,
|
||||
string deviceToken,
|
||||
PushProvider provider,
|
||||
Account account
|
||||
)
|
||||
{
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
var accountId = Guid.Parse(account.Id!);
|
||||
|
||||
// Check for existing subscription with same device ID or token
|
||||
var existingSubscription = await _db.PushSubscriptions
|
||||
.Where(s => s.AccountId == accountId)
|
||||
.Where(s => s.DeviceId == deviceId || s.DeviceToken == deviceToken)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (existingSubscription != null)
|
||||
{
|
||||
existingSubscription.DeviceId = deviceId;
|
||||
existingSubscription.DeviceToken = deviceToken;
|
||||
existingSubscription.Provider = provider;
|
||||
existingSubscription.UpdatedAt = now;
|
||||
|
||||
_db.Update(existingSubscription);
|
||||
await _db.SaveChangesAsync();
|
||||
return existingSubscription;
|
||||
}
|
||||
|
||||
var subscription = new PushSubscription
|
||||
{
|
||||
DeviceId = deviceId,
|
||||
DeviceToken = deviceToken,
|
||||
Provider = provider,
|
||||
AccountId = accountId,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now
|
||||
};
|
||||
|
||||
_db.PushSubscriptions.Add(subscription);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return subscription;
|
||||
}
|
||||
|
||||
public async Task SendNotification(Account account,
|
||||
string topic,
|
||||
string? title = null,
|
||||
string? subtitle = null,
|
||||
string? content = null,
|
||||
Dictionary<string, object?>? meta = null,
|
||||
string? actionUri = null,
|
||||
bool isSilent = false,
|
||||
bool save = true)
|
||||
{
|
||||
meta ??= [];
|
||||
if (title is null && subtitle is null && content is null)
|
||||
throw new ArgumentException("Unable to send notification that is completely empty.");
|
||||
|
||||
if (actionUri is not null) meta["action_uri"] = actionUri;
|
||||
|
||||
var accountId = Guid.Parse(account.Id!);
|
||||
var notification = new Notification
|
||||
{
|
||||
Topic = topic,
|
||||
Title = title,
|
||||
Subtitle = subtitle,
|
||||
Content = content,
|
||||
Meta = meta,
|
||||
AccountId = accountId,
|
||||
};
|
||||
|
||||
if (save)
|
||||
{
|
||||
_db.Notifications.Add(notification);
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
if (!isSilent)
|
||||
_ = _queueService.EnqueuePushNotification(notification, accountId, save);
|
||||
}
|
||||
|
||||
public async Task DeliverPushNotification(Notification notification, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ws.SendPacketToAccount(notification.AccountId.ToString(), new WebSocketPacket()
|
||||
{
|
||||
Type = "notifications.new",
|
||||
Data = notification,
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Delivering push notification: {NotificationTopic} with meta {NotificationMeta}",
|
||||
notification.Topic,
|
||||
notification.Meta
|
||||
);
|
||||
|
||||
// Get all push subscriptions for the account
|
||||
var subscriptions = await _db.PushSubscriptions
|
||||
.Where(s => s.AccountId == notification.AccountId)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (subscriptions.Count == 0)
|
||||
{
|
||||
_logger.LogInformation("No push subscriptions found for account {AccountId}", notification.AccountId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Send push notifications
|
||||
var tasks = new List<Task>();
|
||||
foreach (var subscription in subscriptions)
|
||||
{
|
||||
try
|
||||
{
|
||||
tasks.Add(SendPushNotificationAsync(subscription, notification));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error sending push notification to {DeviceId}", subscription.DeviceId);
|
||||
}
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error in DeliverPushNotification");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task MarkNotificationsViewed(ICollection<Notification> notifications)
|
||||
{
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
var id = notifications.Where(n => n.ViewedAt == null).Select(n => n.Id).ToList();
|
||||
if (id.Count == 0) return;
|
||||
|
||||
await _db.Notifications
|
||||
.Where(n => id.Contains(n.Id))
|
||||
.ExecuteUpdateAsync(s => s.SetProperty(n => n.ViewedAt, now));
|
||||
}
|
||||
|
||||
public async Task MarkAllNotificationsViewed(Guid accountId)
|
||||
{
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
await _db.Notifications
|
||||
.Where(n => n.AccountId == accountId)
|
||||
.Where(n => n.ViewedAt == null)
|
||||
.ExecuteUpdateAsync(s => s.SetProperty(n => n.ViewedAt, now));
|
||||
}
|
||||
|
||||
public async Task SendNotificationBatch(Notification notification, List<Guid> accounts, bool save = false)
|
||||
{
|
||||
if (save)
|
||||
{
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
var notifications = accounts.Select(accountId => new Notification
|
||||
{
|
||||
Topic = notification.Topic,
|
||||
Title = notification.Title,
|
||||
Subtitle = notification.Subtitle,
|
||||
Content = notification.Content,
|
||||
Meta = notification.Meta,
|
||||
Priority = notification.Priority,
|
||||
AccountId = accountId,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now
|
||||
}).ToList();
|
||||
|
||||
if (notifications.Count != 0)
|
||||
{
|
||||
await _db.Notifications.AddRangeAsync(notifications);
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Delivering notification in batch: {NotificationTopic} #{NotificationId} with meta {NotificationMeta}",
|
||||
notification.Topic,
|
||||
notification.Id,
|
||||
notification.Meta
|
||||
);
|
||||
|
||||
// WS first
|
||||
foreach (var account in accounts)
|
||||
{
|
||||
notification.AccountId = account; // keep original behavior
|
||||
_ws.SendPacketToAccount(account.ToString(), new Connection.WebSocketPacket
|
||||
{
|
||||
Type = "notifications.new",
|
||||
Data = notification
|
||||
});
|
||||
}
|
||||
|
||||
await DeliverPushNotification(notification);
|
||||
}
|
||||
|
||||
private async Task SendPushNotificationAsync(PushSubscription subscription, Notification notification)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogDebug(
|
||||
$"Pushing notification {notification.Topic} #{notification.Id} to device #{subscription.DeviceId}");
|
||||
|
||||
switch (subscription.Provider)
|
||||
{
|
||||
case PushProvider.Google:
|
||||
if (_fcm == null)
|
||||
throw new InvalidOperationException("Firebase Cloud Messaging is not initialized.");
|
||||
|
||||
var body = string.Empty;
|
||||
if (!string.IsNullOrEmpty(notification.Subtitle) || !string.IsNullOrEmpty(notification.Content))
|
||||
{
|
||||
body = string.Join("\n",
|
||||
notification.Subtitle ?? string.Empty,
|
||||
notification.Content ?? string.Empty
|
||||
).Trim();
|
||||
}
|
||||
|
||||
var fcmResult = await _fcm.SendAsync(new Dictionary<string, object>
|
||||
{
|
||||
["message"] = new Dictionary<string, object>
|
||||
{
|
||||
["token"] = subscription.DeviceToken,
|
||||
["notification"] = new Dictionary<string, object>
|
||||
{
|
||||
["title"] = notification.Title ?? string.Empty,
|
||||
["body"] = body
|
||||
},
|
||||
// You can re-enable data payloads if needed.
|
||||
// ["data"] = new Dictionary<string, object>
|
||||
// {
|
||||
// ["Id"] = notification.Id,
|
||||
// ["Topic"] = notification.Topic,
|
||||
// ["Meta"] = notification.Meta
|
||||
// }
|
||||
}
|
||||
});
|
||||
|
||||
if (fcmResult.Error != null)
|
||||
throw new Exception($"Notification pushed failed ({fcmResult.StatusCode}) {fcmResult.Error}");
|
||||
break;
|
||||
|
||||
case PushProvider.Apple:
|
||||
if (_apns == null)
|
||||
throw new InvalidOperationException("Apple Push Notification Service is not initialized.");
|
||||
|
||||
var alertDict = new Dictionary<string, object>();
|
||||
if (!string.IsNullOrEmpty(notification.Title))
|
||||
alertDict["title"] = notification.Title;
|
||||
if (!string.IsNullOrEmpty(notification.Subtitle))
|
||||
alertDict["subtitle"] = notification.Subtitle;
|
||||
if (!string.IsNullOrEmpty(notification.Content))
|
||||
alertDict["body"] = notification.Content;
|
||||
|
||||
var payload = new Dictionary<string, object?>
|
||||
{
|
||||
["topic"] = _apnsTopic,
|
||||
["type"] = notification.Topic,
|
||||
["aps"] = new Dictionary<string, object?>
|
||||
{
|
||||
["alert"] = alertDict,
|
||||
["sound"] = notification.Priority >= 5 ? "default" : null,
|
||||
["mutable-content"] = 1
|
||||
},
|
||||
["meta"] = notification.Meta
|
||||
};
|
||||
|
||||
var apnResult = await _apns.SendAsync(
|
||||
payload,
|
||||
deviceToken: subscription.DeviceToken,
|
||||
apnsId: notification.Id.ToString(),
|
||||
apnsPriority: notification.Priority,
|
||||
apnPushType: ApnPushType.Alert
|
||||
);
|
||||
if (apnResult.Error != null)
|
||||
throw new Exception($"Notification pushed failed ({apnResult.StatusCode}) {apnResult.Error}");
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new InvalidOperationException($"Push provider not supported: {subscription.Provider}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex,
|
||||
$"Failed to push notification #{notification.Id} to device {subscription.DeviceId}. {ex.Message}");
|
||||
// Swallow here to keep worker alive; upstream is fire-and-forget.
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
$"Successfully pushed notification #{notification.Id} to device {subscription.DeviceId} provider {subscription.Provider}");
|
||||
}
|
||||
|
||||
public async Task SaveNotification(Notification notification)
|
||||
{
|
||||
_db.Notifications.Add(notification);
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task SaveNotification(Notification notification, List<Guid> accounts)
|
||||
{
|
||||
_db.Notifications.AddRange(accounts.Select(a => new Notification
|
||||
{
|
||||
AccountId = a,
|
||||
Topic = notification.Topic,
|
||||
Content = notification.Content,
|
||||
Title = notification.Title,
|
||||
Subtitle = notification.Subtitle,
|
||||
Meta = notification.Meta,
|
||||
Priority = notification.Priority,
|
||||
CreatedAt = notification.CreatedAt,
|
||||
UpdatedAt = notification.UpdatedAt,
|
||||
}));
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
}
|
25
DysonNetwork.Ring/Notification/PushSubscription.cs
Normal file
25
DysonNetwork.Ring/Notification/PushSubscription.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Ring.Notification;
|
||||
|
||||
public enum PushProvider
|
||||
{
|
||||
Apple,
|
||||
Google
|
||||
}
|
||||
|
||||
[Index(nameof(AccountId), nameof(DeviceId), nameof(DeletedAt), IsUnique = true)]
|
||||
public class PushSubscription : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public Guid AccountId { get; set; }
|
||||
[MaxLength(8192)] public string DeviceId { get; set; } = null!;
|
||||
[MaxLength(8192)] public string DeviceToken { get; set; } = null!;
|
||||
public PushProvider Provider { get; set; }
|
||||
|
||||
public int CountDelivered { get; set; }
|
||||
public Instant? LastUsedAt { get; set; }
|
||||
}
|
Reference in New Issue
Block a user