265 lines
8.8 KiB
C#
265 lines
8.8 KiB
C#
using System.Text;
|
|
using System.Text.Json;
|
|
using DysonNetwork.Sphere.Connection;
|
|
using EFCore.BulkExtensions;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using NodaTime;
|
|
|
|
namespace DysonNetwork.Sphere.Account;
|
|
|
|
public class NotificationService(
|
|
AppDatabase db,
|
|
WebSocketService ws,
|
|
ILogger<NotificationService> logger,
|
|
IHttpClientFactory httpFactory,
|
|
IConfiguration config)
|
|
{
|
|
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
|
|
|
|
public async Task<NotificationPushSubscription> SubscribePushNotification(
|
|
Account account,
|
|
NotificationPushProvider provider,
|
|
string deviceId,
|
|
string deviceToken
|
|
)
|
|
{
|
|
var existingSubscription = await db.NotificationPushSubscriptions
|
|
.Where(s => s.AccountId == account.Id)
|
|
.Where(s => s.DeviceId == deviceId || s.DeviceToken == deviceToken)
|
|
.FirstOrDefaultAsync();
|
|
|
|
if (existingSubscription != null)
|
|
{
|
|
// 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();
|
|
return existingSubscription;
|
|
}
|
|
|
|
var subscription = new NotificationPushSubscription
|
|
{
|
|
DeviceId = deviceId,
|
|
DeviceToken = deviceToken,
|
|
Provider = provider,
|
|
AccountId = account.Id,
|
|
};
|
|
|
|
db.NotificationPushSubscriptions.Add(subscription);
|
|
await db.SaveChangesAsync();
|
|
|
|
return subscription;
|
|
}
|
|
|
|
public async Task<Notification> SendNotification(
|
|
Account account,
|
|
string topic,
|
|
string? title = null,
|
|
string? subtitle = null,
|
|
string? content = null,
|
|
Dictionary<string, object>? meta = null,
|
|
bool isSilent = false
|
|
)
|
|
{
|
|
if (title is null && subtitle is null && content is null)
|
|
{
|
|
throw new ArgumentException("Unable to send notification that completely empty.");
|
|
}
|
|
|
|
var notification = new Notification
|
|
{
|
|
Topic = topic,
|
|
Title = title,
|
|
Subtitle = subtitle,
|
|
Content = content,
|
|
Meta = meta,
|
|
AccountId = account.Id,
|
|
};
|
|
|
|
db.Add(notification);
|
|
await db.SaveChangesAsync();
|
|
|
|
if (!isSilent) _ = DeliveryNotification(notification);
|
|
|
|
return notification;
|
|
}
|
|
|
|
public async Task DeliveryNotification(Notification notification)
|
|
{
|
|
ws.SendPacketToAccount(notification.AccountId, new WebSocketPacket
|
|
{
|
|
Type = "notifications.new",
|
|
Data = notification
|
|
});
|
|
|
|
// Pushing the notification
|
|
var subscribers = await db.NotificationPushSubscriptions
|
|
.Where(s => s.AccountId == notification.AccountId)
|
|
.ToListAsync();
|
|
|
|
await _PushNotification(notification, subscribers);
|
|
}
|
|
|
|
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 BroadcastNotification(Notification notification, bool save = false)
|
|
{
|
|
if (save)
|
|
{
|
|
var accounts = await db.Accounts.ToListAsync();
|
|
var notifications = accounts.Select(x =>
|
|
{
|
|
var newNotification = new Notification
|
|
{
|
|
Topic = notification.Topic,
|
|
Title = notification.Title,
|
|
Subtitle = notification.Subtitle,
|
|
Content = notification.Content,
|
|
Meta = notification.Meta,
|
|
Priority = notification.Priority,
|
|
Account = x,
|
|
AccountId = x.Id
|
|
};
|
|
return newNotification;
|
|
}).ToList();
|
|
await db.BulkInsertAsync(notifications);
|
|
}
|
|
|
|
var subscribers = await db.NotificationPushSubscriptions
|
|
.ToListAsync();
|
|
await _PushNotification(notification, subscribers);
|
|
}
|
|
|
|
public async Task SendNotificationBatch(Notification notification, List<Account> accounts, bool save = false)
|
|
{
|
|
if (save)
|
|
{
|
|
var notifications = accounts.Select(x =>
|
|
{
|
|
var newNotification = new Notification
|
|
{
|
|
Topic = notification.Topic,
|
|
Title = notification.Title,
|
|
Subtitle = notification.Subtitle,
|
|
Content = notification.Content,
|
|
Meta = notification.Meta,
|
|
Priority = notification.Priority,
|
|
Account = x,
|
|
AccountId = x.Id
|
|
};
|
|
return newNotification;
|
|
}).ToList();
|
|
await db.BulkInsertAsync(notifications);
|
|
}
|
|
|
|
var accountsId = accounts.Select(x => x.Id).ToList();
|
|
var subscribers = await db.NotificationPushSubscriptions
|
|
.Where(s => accountsId.Contains(s.AccountId))
|
|
.ToListAsync();
|
|
await _PushNotification(notification, subscribers);
|
|
}
|
|
|
|
private List<Dictionary<string, object>> _BuildNotificationPayload(Notification notification,
|
|
IEnumerable<NotificationPushSubscription> subscriptions)
|
|
{
|
|
var subDict = subscriptions
|
|
.GroupBy(x => x.Provider)
|
|
.ToDictionary(x => x.Key, x => x.ToList());
|
|
|
|
var notifications = subDict.Select(value =>
|
|
{
|
|
int platformCode = value.Key switch
|
|
{
|
|
NotificationPushProvider.Google => 1,
|
|
NotificationPushProvider.Apple => 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<string, object> _BuildNotificationPayload(Notification notification, int platformCode,
|
|
IEnumerable<string> deviceTokens)
|
|
{
|
|
var alertDict = new Dictionary<string, object>();
|
|
var dict = new Dictionary<string, object>
|
|
{
|
|
["notif_id"] = notification.Id.ToString(),
|
|
["apns_id"] = notification.Id.ToString(),
|
|
["topic"] = _notifyTopic,
|
|
["category"] = notification.Topic,
|
|
["tokens"] = deviceTokens,
|
|
["alert"] = new Dictionary<string, object>(),
|
|
["data"] = new Dictionary<string, object>
|
|
{
|
|
["d_topic"] = notification.Topic,
|
|
["meta"] = notification.Meta ?? new Dictionary<string, object>(),
|
|
},
|
|
["mutable_content"] = true,
|
|
};
|
|
|
|
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["sound"] = new Dictionary<string, object> { ["name"] = "default" };
|
|
}
|
|
|
|
dict["platform"] = platformCode;
|
|
dict["alert"] = alertDict;
|
|
|
|
return dict;
|
|
}
|
|
|
|
private async Task _PushNotification(Notification notification,
|
|
IEnumerable<NotificationPushSubscription> subscriptions)
|
|
{
|
|
var requestDict = new Dictionary<string, object>
|
|
{
|
|
["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();
|
|
}
|
|
} |