From fc63a76eb2b0014c0c4a6891b28dddc035db8e84 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 20 Jul 2025 02:11:33 +0800 Subject: [PATCH] :recycle: To use CorePush to no longer depends on gorush --- .../RegistryProxyConfigProvider.cs | 2 +- .../DysonNetwork.Pusher.csproj | 1 + .../Notification/PushService.cs | 269 +++++++++++------- DysonNetwork.Pusher/Program.cs | 1 + .../Startup/ServiceCollectionExtensions.cs | 11 + DysonNetwork.Pusher/appsettings.json | 12 +- 6 files changed, 183 insertions(+), 113 deletions(-) diff --git a/DysonNetwork.Gateway/RegistryProxyConfigProvider.cs b/DysonNetwork.Gateway/RegistryProxyConfigProvider.cs index ef64b04..bdd5e36 100644 --- a/DysonNetwork.Gateway/RegistryProxyConfigProvider.cs +++ b/DysonNetwork.Gateway/RegistryProxyConfigProvider.cs @@ -91,7 +91,7 @@ public class RegistryProxyConfigProvider : IProxyConfigProvider, IDisposable { { "destination1", new DestinationConfig { Address = serviceUrl } } }, - HttpRequest = new ForwarderRequestConfig() + HttpRequest = new ForwarderRequestConfig { ActivityTimeout = directRoute.IsWebsocket ? TimeSpan.FromHours(24) : TimeSpan.FromMinutes(2) } diff --git a/DysonNetwork.Pusher/DysonNetwork.Pusher.csproj b/DysonNetwork.Pusher/DysonNetwork.Pusher.csproj index 4e1b9d7..49445bb 100644 --- a/DysonNetwork.Pusher/DysonNetwork.Pusher.csproj +++ b/DysonNetwork.Pusher/DysonNetwork.Pusher.csproj @@ -8,6 +8,7 @@ + diff --git a/DysonNetwork.Pusher/Notification/PushService.cs b/DysonNetwork.Pusher/Notification/PushService.cs index 0d605e9..6ac612e 100644 --- a/DysonNetwork.Pusher/Notification/PushService.cs +++ b/DysonNetwork.Pusher/Notification/PushService.cs @@ -1,5 +1,6 @@ -using System.Text; -using System.Text.Json; +using CorePush.Apple; +using CorePush.Firebase; +using DysonNetwork.Pusher.Connection; using DysonNetwork.Shared.Proto; using EFCore.BulkExtensions; using Microsoft.EntityFrameworkCore; @@ -7,14 +8,55 @@ using NodaTime; namespace DysonNetwork.Pusher.Notification; -public class PushService(IConfiguration config, AppDatabase db, IHttpClientFactory httpFactory) +public class PushService { - private readonly string _notifyTopic = config["Notifications:Topic"]!; - private readonly Uri _notifyEndpoint = new(config["Notifications:Endpoint"]!); + private readonly AppDatabase _db; + private readonly WebSocketService _ws; + private readonly ILogger _logger; + private readonly FirebaseSender? _fcm; + private readonly ApnSender? _apns; + private readonly string? _apnsTopic; + + public PushService( + IConfiguration config, + AppDatabase db, + WebSocketService ws, + IHttpClientFactory httpFactory, + ILogger logger + ) + { + var cfgSection = config.GetSection("Notifications:Push"); + + // Set up Firebase Cloud Messaging + var fcmConfig = cfgSection.GetValue("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("Apple:PrivateKey"); + if (apnsKeyPath != null && File.Exists(apnsKeyPath)) + { + _apns = new ApnSender(new ApnSettings + { + P8PrivateKey = File.ReadAllText(apnsKeyPath), + P8PrivateKeyId = cfgSection.GetValue("Apple:PrivateKeyId"), + TeamId = cfgSection.GetValue("Apple:TeamId"), + AppBundleIdentifier = cfgSection.GetValue("Apple:BundleIdentifier"), + ServerType = cfgSection.GetValue("Production") + ? ApnServerType.Production + : ApnServerType.Development + }, httpFactory.CreateClient()); + _apnsTopic = cfgSection.GetValue("Apple:BundleIdentifier"); + } + + _db = db; + _ws = ws; + _logger = logger; + } public async Task UnsubscribeDevice(string deviceId) { - await db.PushSubscriptions + await _db.PushSubscriptions .Where(s => s.DeviceId == deviceId) .ExecuteDeleteAsync(); } @@ -27,41 +69,40 @@ public class PushService(IConfiguration config, AppDatabase db, IHttpClientFacto ) { var now = SystemClock.Instance.GetCurrentInstant(); - - // First check if a matching subscription exists var accountId = Guid.Parse(account.Id!); - var existingSubscription = await db.PushSubscriptions + + // 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 is not null) + if (existingSubscription != null) { - // Update the existing subscription directly in the database - await db.PushSubscriptions - .Where(s => s.Id == existingSubscription.Id) - .ExecuteUpdateAsync(setters => setters - .SetProperty(s => s.DeviceId, deviceId) - .SetProperty(s => s.DeviceToken, deviceToken) - .SetProperty(s => s.UpdatedAt, now)); - - // Return the updated subscription + // Update existing subscription existingSubscription.DeviceId = deviceId; existingSubscription.DeviceToken = deviceToken; + existingSubscription.Provider = provider; existingSubscription.UpdatedAt = now; + + _db.Update(existingSubscription); + await _db.SaveChangesAsync(); return existingSubscription; } + // Create new subscription var subscription = new PushSubscription { DeviceId = deviceId, DeviceToken = deviceToken, Provider = provider, AccountId = accountId, + CreatedAt = now, + UpdatedAt = now }; - db.PushSubscriptions.Add(subscription); - await db.SaveChangesAsync(); + _db.PushSubscriptions.Add(subscription); + await _db.SaveChangesAsync(); return subscription; } @@ -94,8 +135,8 @@ public class PushService(IConfiguration config, AppDatabase db, IHttpClientFacto if (save) { - db.Add(notification); - await db.SaveChangesAsync(); + _db.Add(notification); + await _db.SaveChangesAsync(); } if (!isSilent) _ = DeliveryNotification(notification); @@ -104,7 +145,7 @@ public class PushService(IConfiguration config, AppDatabase db, IHttpClientFacto public async Task DeliveryNotification(Notification notification) { // Pushing the notification - var subscribers = await db.PushSubscriptions + var subscribers = await _db.PushSubscriptions .Where(s => s.AccountId == notification.AccountId) .ToListAsync(); @@ -117,7 +158,7 @@ public class PushService(IConfiguration config, AppDatabase db, IHttpClientFacto 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) ); @@ -141,105 +182,113 @@ public class PushService(IConfiguration config, AppDatabase db, IHttpClientFacto }; return newNotification; }).ToList(); - await db.BulkInsertAsync(notifications); + await _db.BulkInsertAsync(notifications); } - var subscribers = await db.PushSubscriptions + var subscribers = await _db.PushSubscriptions .Where(s => accounts.Contains(s.AccountId)) .ToListAsync(); await _PushNotification(notification, subscribers); } - private List> _BuildNotificationPayload(Notification notification, - IEnumerable subscriptions) - { - var subDict = subscriptions - .GroupBy(x => x.Provider) - .ToDictionary(x => x.Key, x => x.ToList()); - - var notifications = subDict.Select(value => - { - var platformCode = value.Key switch - { - PushProvider.Apple => 1, - PushProvider.Google => 2, - _ => throw new InvalidOperationException($"Unknown push provider: {value.Key}") - }; - - var tokens = value.Value.Select(x => x.DeviceToken).ToList(); - return _BuildNotificationPayload(notification, platformCode, tokens); - }).ToList(); - - return notifications.ToList(); - } - - private Dictionary _BuildNotificationPayload(Pusher.Notification.Notification notification, - int platformCode, - IEnumerable deviceTokens) - { - var alertDict = new Dictionary(); - var dict = new Dictionary - { - ["notif_id"] = notification.Id.ToString(), - ["apns_id"] = notification.Id.ToString(), - ["topic"] = _notifyTopic, - ["tokens"] = deviceTokens, - ["data"] = new Dictionary - { - ["type"] = notification.Topic, - ["meta"] = notification.Meta ?? new Dictionary(), - }, - ["mutable_content"] = true, - ["priority"] = notification.Priority >= 5 ? "high" : "normal", - }; - - if (!string.IsNullOrWhiteSpace(notification.Title)) - { - dict["title"] = notification.Title; - alertDict["title"] = notification.Title; - } - - 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["name"] = "default"; - - dict["platform"] = platformCode; - dict["alert"] = alertDict; - - return dict; - } - private async Task _PushNotification( Notification notification, IEnumerable subscriptions ) { - var subList = subscriptions.ToList(); - if (subList.Count == 0) return; + var tasks = subscriptions + .Select(subscription => _PushSingleNotification(notification, subscription)) + .ToList(); - var requestDict = new Dictionary + await Task.WhenAll(tasks); + } + + private async Task _PushSingleNotification(Notification notification, PushSubscription subscription) + { + try { - ["notifications"] = _BuildNotificationPayload(notification, subList) - }; + _logger.LogDebug( + $"Pushing notification {notification.Topic} #{notification.Id} to device #{subscription.DeviceId}"); - var client = httpFactory.CreateClient(); - client.BaseAddress = _notifyEndpoint; - var request = await client.PostAsync("/push", new StringContent( - JsonSerializer.Serialize(requestDict), - Encoding.UTF8, - "application/json" - )); - request.EnsureSuccessStatusCode(); + 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(); + } + + 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; + + case PushProvider.Apple: + if (_apns == null) + throw new InvalidOperationException("Apple Push Notification 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"] = _apnsTopic, + ["aps"] = new Dictionary + { + ["alert"] = alertDict, + ["sound"] = notification.Priority >= 5 ? "default" : null, + ["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($"Push provider not supported: {subscription.Provider}"); + } + } + catch (Exception ex) + { + _logger.LogError(ex, + $"Failed to push notification #{notification.Id} to device {subscription.DeviceId}. {ex.Message}"); + throw new Exception($"Failed to send notification to {subscription.Provider}: {ex.Message}", ex); + } + + _logger.LogInformation( + $"Successfully pushed notification #{notification.Id} to device {subscription.DeviceId}"); } } \ No newline at end of file diff --git a/DysonNetwork.Pusher/Program.cs b/DysonNetwork.Pusher/Program.cs index 1694200..57c5174 100644 --- a/DysonNetwork.Pusher/Program.cs +++ b/DysonNetwork.Pusher/Program.cs @@ -23,6 +23,7 @@ builder.Services.AddAppFlushHandlers(); // Add business services builder.Services.AddAppBusinessServices(); +builder.Services.AddPushServices(builder.Configuration); // Add scheduled jobs builder.Services.AddAppScheduledJobs(); diff --git a/DysonNetwork.Pusher/Startup/ServiceCollectionExtensions.cs b/DysonNetwork.Pusher/Startup/ServiceCollectionExtensions.cs index 1064dbf..b24311f 100644 --- a/DysonNetwork.Pusher/Startup/ServiceCollectionExtensions.cs +++ b/DysonNetwork.Pusher/Startup/ServiceCollectionExtensions.cs @@ -1,5 +1,7 @@ using System.Text.Json; using System.Threading.RateLimiting; +using CorePush.Apple; +using CorePush.Firebase; using DysonNetwork.Pusher.Connection; using DysonNetwork.Pusher.Email; using DysonNetwork.Pusher.Notification; @@ -137,4 +139,13 @@ public static class ServiceCollectionExtensions return services; } + + public static void AddPushServices(this IServiceCollection services, IConfiguration configuration) + { + services.Configure(configuration.GetSection("PushNotify:Apple")); + services.AddHttpClient(); + + services.Configure(configuration.GetSection("PushNotify:Firebase")); + services.AddHttpClient(); + } } \ No newline at end of file diff --git a/DysonNetwork.Pusher/appsettings.json b/DysonNetwork.Pusher/appsettings.json index 59ec98b..c0ecae8 100644 --- a/DysonNetwork.Pusher/appsettings.json +++ b/DysonNetwork.Pusher/appsettings.json @@ -14,8 +14,16 @@ "Etcd": "etcd.orb.local:2379" }, "Notifications": { - "Topic": "dev.solsynth.solian", - "Endpoint": "http://localhost:8088" + "Push": { + "Production": true, + "Google": "./Keys/Solian.json", + "Apple": { + "PrivateKey": "./Keys/Solian.p8", + "PrivateKeyId": "4US4KSX4W6", + "TeamId": "W7HPZ53V6B", + "BundleIdentifier": "dev.solsynth.solian" + } + } }, "Email": { "Server": "smtp4dev.orb.local",