From a78e92a23a41bbc4cc4d72c6c27b627c69e0d49b Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 1 Jun 2025 01:04:20 +0800 Subject: [PATCH] :recycle: Refactor the notification service to use gorush as push service --- .../Account/ActionLogService.cs | 2 +- DysonNetwork.Sphere/Account/MagicSpell.cs | 2 +- .../Account/NotificationService.cs | 266 +++++++----------- DysonNetwork.Sphere/Auth/Auth.cs | 2 +- DysonNetwork.Sphere/Chat/ChatController.cs | 2 +- .../Chat/ChatRoomController.cs | 6 +- .../Connection/Handlers/MessageReadHandler.cs | 2 - .../Handlers/MessageTypingHandler.cs | 2 +- .../Connection/WebSocketController.cs | 3 +- .../Connection/WebSocketPacket.cs | 2 +- .../DysonNetwork.Sphere.csproj | 1 - .../Permission/PermissionService.cs | 4 +- DysonNetwork.Sphere/Sticker/StickerService.cs | 5 - DysonNetwork.Sphere/appsettings.json | 13 +- 14 files changed, 121 insertions(+), 191 deletions(-) diff --git a/DysonNetwork.Sphere/Account/ActionLogService.cs b/DysonNetwork.Sphere/Account/ActionLogService.cs index e3a8cf5..0b963f2 100644 --- a/DysonNetwork.Sphere/Account/ActionLogService.cs +++ b/DysonNetwork.Sphere/Account/ActionLogService.cs @@ -5,7 +5,7 @@ using DysonNetwork.Sphere.Storage.Handlers; namespace DysonNetwork.Sphere.Account; -public class ActionLogService(AppDatabase db, GeoIpService geo, FlushBufferService fbs) +public class ActionLogService(GeoIpService geo, FlushBufferService fbs) { public void CreateActionLog(Guid accountId, string action, Dictionary meta) { diff --git a/DysonNetwork.Sphere/Account/MagicSpell.cs b/DysonNetwork.Sphere/Account/MagicSpell.cs index 72ff781..674809c 100644 --- a/DysonNetwork.Sphere/Account/MagicSpell.cs +++ b/DysonNetwork.Sphere/Account/MagicSpell.cs @@ -23,7 +23,7 @@ public class MagicSpell : ModelBase public MagicSpellType Type { get; set; } public Instant? ExpiresAt { get; set; } public Instant? AffectedAt { get; set; } - [Column(TypeName = "jsonb")] public Dictionary Meta { get; set; } + [Column(TypeName = "jsonb")] public Dictionary Meta { get; set; } = new(); public Guid? AccountId { get; set; } public Account? Account { get; set; } diff --git a/DysonNetwork.Sphere/Account/NotificationService.cs b/DysonNetwork.Sphere/Account/NotificationService.cs index cb73c8b..deb0a30 100644 --- a/DysonNetwork.Sphere/Account/NotificationService.cs +++ b/DysonNetwork.Sphere/Account/NotificationService.cs @@ -1,5 +1,5 @@ -using CorePush.Apple; -using CorePush.Firebase; +using System.Text; +using System.Text.Json; using DysonNetwork.Sphere.Connection; using EFCore.BulkExtensions; using Microsoft.EntityFrameworkCore; @@ -7,46 +7,15 @@ using NodaTime; namespace DysonNetwork.Sphere.Account; -public class NotificationService +public class NotificationService( + AppDatabase db, + WebSocketService ws, + ILogger logger, + IHttpClientFactory httpFactory, + IConfiguration config) { - private readonly AppDatabase _db; - private readonly WebSocketService _ws; - private readonly ILogger _logger; - private readonly FirebaseSender? _fcm; - private readonly ApnSender? _apns; - - public NotificationService( - AppDatabase db, - WebSocketService ws, - IConfiguration cfg, - IHttpClientFactory clientFactory, - ILogger logger - ) - { - _db = db; - _ws = ws; - _logger = logger; - - var cfgSection = cfg.GetSection("Notifications:Push"); - - // Set up the firebase push notification - var fcmConfig = cfgSection.GetValue("Google"); - if (fcmConfig != null) - _fcm = new FirebaseSender(File.ReadAllText(fcmConfig), clientFactory.CreateClient()); - // Set up the apple push notification service - var apnsCert = cfgSection.GetValue("Apple:PrivateKey"); - if (apnsCert != null) - _apns = new ApnSender(new ApnSettings - { - P8PrivateKey = File.ReadAllText(apnsCert), - P8PrivateKeyId = cfgSection.GetValue("Apple:PrivateKeyId"), - TeamId = cfgSection.GetValue("Apple:TeamId"), - AppBundleIdentifier = cfgSection.GetValue("Apple:BundleIdentifier"), - ServerType = cfgSection.GetValue("Production") - ? ApnServerType.Production - : ApnServerType.Development - }, clientFactory.CreateClient()); - } + private readonly string _notifyTopic = config["Notifications:Topic"]!; + private readonly Uri _notifyEndpoint = new(config["Notifications:Endpoint"]!); // TODO remove all push notification with this device id when this device is logged out @@ -57,7 +26,7 @@ public class NotificationService string deviceToken ) { - var existingSubscription = await _db.NotificationPushSubscriptions + var existingSubscription = await db.NotificationPushSubscriptions .Where(s => s.AccountId == account.Id) .Where(s => s.DeviceId == deviceId || s.DeviceToken == deviceToken) .FirstOrDefaultAsync(); @@ -67,8 +36,8 @@ public class NotificationService // Reset these audit fields to renew the lifecycle of this device token existingSubscription.CreatedAt = Instant.FromDateTimeUtc(DateTime.UtcNow); existingSubscription.UpdatedAt = Instant.FromDateTimeUtc(DateTime.UtcNow); - _db.Update(existingSubscription); - await _db.SaveChangesAsync(); + db.Update(existingSubscription); + await db.SaveChangesAsync(); return existingSubscription; } @@ -80,8 +49,8 @@ public class NotificationService AccountId = account.Id, }; - _db.NotificationPushSubscriptions.Add(subscription); - await _db.SaveChangesAsync(); + db.NotificationPushSubscriptions.Add(subscription); + await db.SaveChangesAsync(); return subscription; } @@ -111,8 +80,8 @@ public class NotificationService AccountId = account.Id, }; - _db.Add(notification); - await _db.SaveChangesAsync(); + db.Add(notification); + await db.SaveChangesAsync(); if (!isSilent) _ = DeliveryNotification(notification); @@ -121,22 +90,18 @@ public class NotificationService public async Task DeliveryNotification(Notification notification) { - _ws.SendPacketToAccount(notification.AccountId, new WebSocketPacket + ws.SendPacketToAccount(notification.AccountId, new WebSocketPacket { Type = "notifications.new", Data = notification }); // Pushing the notification - var subscribers = await _db.NotificationPushSubscriptions + var subscribers = await db.NotificationPushSubscriptions .Where(s => s.AccountId == notification.AccountId) .ToListAsync(); - var tasks = subscribers - .Select(subscriber => _PushSingleNotification(notification, subscriber)) - .ToList(); - - await Task.WhenAll(tasks); + await _PushNotification(notification, subscribers); } public async Task MarkNotificationsViewed(ICollection notifications) @@ -145,7 +110,7 @@ public class NotificationService var id = notifications.Where(n => n.ViewedAt == null).Select(n => n.Id).ToList(); if (id.Count == 0) return; - await _db.Notifications + await db.Notifications .Where(n => id.Contains(n.Id)) .ExecuteUpdateAsync(s => s.SetProperty(n => n.ViewedAt, now) ); @@ -155,7 +120,7 @@ public class NotificationService { if (save) { - var accounts = await _db.Accounts.ToListAsync(); + var accounts = await db.Accounts.ToListAsync(); var notifications = accounts.Select(x => { var newNotification = new Notification @@ -171,19 +136,12 @@ public class NotificationService }; return newNotification; }).ToList(); - await _db.BulkInsertAsync(notifications); + await db.BulkInsertAsync(notifications); } - var subscribers = await _db.NotificationPushSubscriptions + var subscribers = await db.NotificationPushSubscriptions .ToListAsync(); - var tasks = new List(); - foreach (var subscriber in subscribers) - { - notification.AccountId = subscriber.AccountId; - tasks.Add(_PushSingleNotification(notification, subscriber)); - } - - await Task.WhenAll(tasks); + await _PushNotification(notification, subscribers); } public async Task SendNotificationBatch(Notification notification, List accounts, bool save = false) @@ -205,113 +163,103 @@ public class NotificationService }; return newNotification; }).ToList(); - await _db.BulkInsertAsync(notifications); + await db.BulkInsertAsync(notifications); } var accountsId = accounts.Select(x => x.Id).ToList(); - var subscribers = await _db.NotificationPushSubscriptions + var subscribers = await db.NotificationPushSubscriptions .Where(s => accountsId.Contains(s.AccountId)) .ToListAsync(); - var tasks = new List(); - foreach (var subscriber in subscribers) - { - notification.AccountId = subscriber.AccountId; - tasks.Add(_PushSingleNotification(notification, subscriber)); - } - - await Task.WhenAll(tasks); + await _PushNotification(notification, subscribers); } - private async Task _PushSingleNotification(Notification notification, NotificationPushSubscription subscription) + private List> _BuildNotificationPayload(Notification notification, + IEnumerable subscriptions) { - try + var subDict = subscriptions + .GroupBy(x => x.Provider) + .ToDictionary(x => x.Key, x => x.ToList()); + + var notifications = subDict.Select(value => { - _logger.LogDebug( - $"Pushing notification {notification.Topic} #{notification.Id} to device #{subscription.DeviceId}"); - - var body = string.Empty; - switch (subscription.Provider) + int platformCode = value.Key switch { - case NotificationPushProvider.Google: - if (_fcm == null) - throw new InvalidOperationException("The firebase cloud messaging is not initialized."); + NotificationPushProvider.Google => 1, + NotificationPushProvider.Apple => 2, + _ => throw new InvalidOperationException($"Unknown push provider: {value.Key}") + }; - if (!string.IsNullOrEmpty(notification.Subtitle) || !string.IsNullOrEmpty(notification.Content)) - { - body = string.Join("\n", - notification.Subtitle ?? string.Empty, - notification.Content ?? string.Empty).Trim(); - } + var tokens = value.Value.Select(x => x.DeviceToken).ToList(); + return _BuildNotificationPayload(notification, platformCode, tokens); + }).ToList(); - await _fcm.SendAsync(new Dictionary - { - ["message"] = new Dictionary - { - ["token"] = subscription.DeviceToken, - ["notification"] = new Dictionary - { - ["title"] = notification.Title ?? string.Empty, - ["body"] = body - }, - ["data"] = new Dictionary - { - ["id"] = notification.Id, - ["topic"] = notification.Topic, - ["meta"] = notification.Meta ?? new Dictionary() - } - } - }); - break; + return notifications.ToList(); + } - case NotificationPushProvider.Apple: - if (_apns == null) - throw new InvalidOperationException("The apple notification push service is not initialized."); - - var alertDict = new Dictionary(); - - 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 - { - ["topic"] = notification.Topic, - ["aps"] = new Dictionary - { - ["alert"] = alertDict - }, - ["sound"] = (notification.Priority > 0 ? "default" : null) ?? string.Empty, - ["mutable-content"] = 1, - ["meta"] = notification.Meta ?? new Dictionary() - }; - - await _apns.SendAsync( - payload, - deviceToken: subscription.DeviceToken, - apnsId: notification.Id.ToString(), - apnsPriority: notification.Priority, - apnPushType: ApnPushType.Alert - ); - break; - - default: - throw new InvalidOperationException($"Provider not supported: {subscription.Provider}"); - } - } - catch (Exception ex) + private Dictionary _BuildNotificationPayload(Notification notification, int platformCode, + IEnumerable deviceTokens) + { + var alertDict = new Dictionary(); + var dict = new Dictionary { - // Log the exception - // Consider implementing a retry mechanism - // Rethrow or handle as needed - _logger.LogError( - $"Failed to push notification #{notification.Id} to device... {ex.Message} {ex.StackTrace}"); - throw new Exception($"Failed to send notification to {subscription.Provider}: {ex.Message}", ex); + ["notif_id"] = notification.Id.ToString(), + ["apns_id"] = notification.Id.ToString(), + ["topic"] = _notifyTopic, + ["category"] = notification.Topic, + ["tokens"] = deviceTokens, + ["alert"] = new Dictionary(), + ["data"] = new Dictionary + { + ["d_topic"] = notification.Topic, + ["meta"] = notification.Meta ?? new Dictionary(), + }, + ["mutable_content"] = true, + }; + + if (!string.IsNullOrWhiteSpace(notification.Title)) + { + dict["title"] = notification.Title; + alertDict["title"] = notification.Title; } - _logger.LogInformation( - $"Pushed notification #{notification.Id} to device #{subscription.DeviceId}"); + if (!string.IsNullOrWhiteSpace(notification.Content)) + { + dict["message"] = notification.Content; + alertDict["body"] = notification.Content; + } + + if (!string.IsNullOrWhiteSpace(notification.Subtitle)) + { + dict["message"] = $"{notification.Subtitle}\n{dict["message"]}"; + alertDict["subtitle"] = notification.Subtitle; + } + + if (notification.Priority >= 5) + { + dict["sound"] = new Dictionary { ["name"] = "default" }; + } + + dict["platform"] = platformCode; + dict["alert"] = alertDict; + + return dict; + } + + private async Task _PushNotification(Notification notification, + IEnumerable subscriptions) + { + var requestDict = new Dictionary + { + ["notifications"] = _BuildNotificationPayload(notification, subscriptions) + }; + + var client = httpFactory.CreateClient(); + client.BaseAddress = _notifyEndpoint; + var request = await client.PostAsync("/api/push", new StringContent( + JsonSerializer.Serialize(requestDict), + Encoding.UTF8, + "application/json" + )); + request.EnsureSuccessStatusCode(); } } \ No newline at end of file diff --git a/DysonNetwork.Sphere/Auth/Auth.cs b/DysonNetwork.Sphere/Auth/Auth.cs index 806d94a..064378e 100644 --- a/DysonNetwork.Sphere/Auth/Auth.cs +++ b/DysonNetwork.Sphere/Auth/Auth.cs @@ -178,7 +178,7 @@ public class DysonTokenAuthHandler( { return new TokenInfo { - Token = queryToken, + Token = queryToken.ToString(), Type = TokenType.AuthKey }; } diff --git a/DysonNetwork.Sphere/Chat/ChatController.cs b/DysonNetwork.Sphere/Chat/ChatController.cs index bc612fc..ef4a5ab 100644 --- a/DysonNetwork.Sphere/Chat/ChatController.cs +++ b/DysonNetwork.Sphere/Chat/ChatController.cs @@ -12,7 +12,7 @@ namespace DysonNetwork.Sphere.Chat; [ApiController] [Route("/chat")] -public partial class ChatController(AppDatabase db, ChatService cs, FileService fs) : ControllerBase +public partial class ChatController(AppDatabase db, ChatService cs) : ControllerBase { public class MarkMessageReadRequest { diff --git a/DysonNetwork.Sphere/Chat/ChatRoomController.cs b/DysonNetwork.Sphere/Chat/ChatRoomController.cs index 03a10e7..df5a704 100644 --- a/DysonNetwork.Sphere/Chat/ChatRoomController.cs +++ b/DysonNetwork.Sphere/Chat/ChatRoomController.cs @@ -484,7 +484,7 @@ public class ChatRoomController( member.JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow); db.Update(member); await db.SaveChangesAsync(); - crs.PurgeRoomMembersCache(roomId); + _ = crs.PurgeRoomMembersCache(roomId); als.CreateActionLogFromRequest( ActionLogType.ChatroomJoin, @@ -607,7 +607,7 @@ public class ChatRoomController( member.LeaveAt = SystemClock.Instance.GetCurrentInstant(); await db.SaveChangesAsync(); - crs.PurgeRoomMembersCache(roomId); + _ = crs.PurgeRoomMembersCache(roomId); als.CreateActionLogFromRequest( ActionLogType.ChatroomKick, @@ -649,7 +649,7 @@ public class ChatRoomController( db.ChatMembers.Add(newMember); await db.SaveChangesAsync(); - crs.PurgeRoomMembersCache(roomId); + _ = crs.PurgeRoomMembersCache(roomId); als.CreateActionLogFromRequest( ActionLogType.ChatroomJoin, diff --git a/DysonNetwork.Sphere/Connection/Handlers/MessageReadHandler.cs b/DysonNetwork.Sphere/Connection/Handlers/MessageReadHandler.cs index 4b5742b..c0841e9 100644 --- a/DysonNetwork.Sphere/Connection/Handlers/MessageReadHandler.cs +++ b/DysonNetwork.Sphere/Connection/Handlers/MessageReadHandler.cs @@ -9,8 +9,6 @@ using SystemClock = NodaTime.SystemClock; namespace DysonNetwork.Sphere.Connection.Handlers; public class MessageReadHandler( - AppDatabase db, - ICacheService cache, ChatRoomService crs, FlushBufferService buffer ) diff --git a/DysonNetwork.Sphere/Connection/Handlers/MessageTypingHandler.cs b/DysonNetwork.Sphere/Connection/Handlers/MessageTypingHandler.cs index 4018e53..e3bf75e 100644 --- a/DysonNetwork.Sphere/Connection/Handlers/MessageTypingHandler.cs +++ b/DysonNetwork.Sphere/Connection/Handlers/MessageTypingHandler.cs @@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore; namespace DysonNetwork.Sphere.Connection.Handlers; -public class MessageTypingHandler(AppDatabase db, ChatRoomService crs, ICacheService cache) : IWebSocketPacketHandler +public class MessageTypingHandler(ChatRoomService crs) : IWebSocketPacketHandler { public string PacketType => "messages.typing"; diff --git a/DysonNetwork.Sphere/Connection/WebSocketController.cs b/DysonNetwork.Sphere/Connection/WebSocketController.cs index 62ccb1a..3bf61cf 100644 --- a/DysonNetwork.Sphere/Connection/WebSocketController.cs +++ b/DysonNetwork.Sphere/Connection/WebSocketController.cs @@ -91,8 +91,7 @@ public class WebSocketController(WebSocketService ws, ILogger ); var packet = WebSocketPacket.FromBytes(buffer[..receiveResult.Count]); - if (packet is null) continue; - ws.HandlePacket(currentUser, connectionKey.DeviceId, packet, webSocket); + _ = ws.HandlePacket(currentUser, connectionKey.DeviceId, packet, webSocket); } } catch (OperationCanceledException) diff --git a/DysonNetwork.Sphere/Connection/WebSocketPacket.cs b/DysonNetwork.Sphere/Connection/WebSocketPacket.cs index 03fe54e..745a961 100644 --- a/DysonNetwork.Sphere/Connection/WebSocketPacket.cs +++ b/DysonNetwork.Sphere/Connection/WebSocketPacket.cs @@ -14,7 +14,7 @@ public class WebSocketPacketType public class WebSocketPacket { public string Type { get; set; } = null!; - public object Data { get; set; } + public object Data { get; set; } = null!; public string? ErrorMessage { get; set; } /// diff --git a/DysonNetwork.Sphere/DysonNetwork.Sphere.csproj b/DysonNetwork.Sphere/DysonNetwork.Sphere.csproj index 53bf0a7..32d73d4 100644 --- a/DysonNetwork.Sphere/DysonNetwork.Sphere.csproj +++ b/DysonNetwork.Sphere/DysonNetwork.Sphere.csproj @@ -18,7 +18,6 @@ - diff --git a/DysonNetwork.Sphere/Permission/PermissionService.cs b/DysonNetwork.Sphere/Permission/PermissionService.cs index 041b2c7..2c39dc0 100644 --- a/DysonNetwork.Sphere/Permission/PermissionService.cs +++ b/DysonNetwork.Sphere/Permission/PermissionService.cs @@ -7,8 +7,8 @@ namespace DysonNetwork.Sphere.Permission; public class PermissionService( AppDatabase db, - ICacheService cache, - ILogger logger) + ICacheService cache +) { private static readonly TimeSpan CacheExpiration = TimeSpan.FromMinutes(1); diff --git a/DysonNetwork.Sphere/Sticker/StickerService.cs b/DysonNetwork.Sphere/Sticker/StickerService.cs index abae50f..8883b02 100644 --- a/DysonNetwork.Sphere/Sticker/StickerService.cs +++ b/DysonNetwork.Sphere/Sticker/StickerService.cs @@ -1,10 +1,5 @@ using DysonNetwork.Sphere.Storage; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Caching.Memory; -using System; -using System.Linq; -using System.Threading.Tasks; namespace DysonNetwork.Sphere.Sticker; diff --git a/DysonNetwork.Sphere/appsettings.json b/DysonNetwork.Sphere/appsettings.json index 242c8b2..6923269 100644 --- a/DysonNetwork.Sphere/appsettings.json +++ b/DysonNetwork.Sphere/appsettings.json @@ -31,7 +31,6 @@ "StorePath": "Uploads" }, "Storage": { - "BaseUrl": "http://localhost:5071", "PreferredRemote": "cloudflare", "Remote": [ { @@ -53,16 +52,8 @@ "ApiSecret": "0x4AAAAAABCDUWABiJQweqlB7tYq-IqIm8U" }, "Notifications": { - "Push": { - "Production": true, - "Google": "./Keys/Solian.json", - "Apple": { - "PrivateKey": "./Keys/Solian.p8", - "PrivateKeyId": "4US4KSX4W6", - "TeamId": "W7HPZ53V6B", - "BundleIdentifier": "dev.solsynth.solian" - } - } + "Topic": "dev.solsynth.solian", + "Endpoint": "http://localhost:8088" }, "Email": { "Server": "smtp4dev.orb.local",