2020-06-28 21:44:35 -05:00
|
|
|
using System;
|
2020-05-27 18:41:00 -05:00
|
|
|
using System.Collections.Generic;
|
|
|
|
using System.Linq;
|
|
|
|
using System.Threading.Tasks;
|
2020-11-17 13:46:23 +01:00
|
|
|
using BTCPayServer.Abstractions.Contracts;
|
2020-05-28 15:15:24 -05:00
|
|
|
using BTCPayServer.Data;
|
2020-06-17 11:26:21 +09:00
|
|
|
using Microsoft.EntityFrameworkCore;
|
2020-06-12 00:01:45 -05:00
|
|
|
using Microsoft.Extensions.Caching.Memory;
|
2020-05-27 18:41:00 -05:00
|
|
|
|
2020-06-15 01:22:09 -05:00
|
|
|
namespace BTCPayServer.Services.Notifications
|
2020-05-27 18:41:00 -05:00
|
|
|
{
|
2020-05-28 22:48:09 -05:00
|
|
|
public class NotificationManager
|
|
|
|
{
|
2020-06-12 00:01:45 -05:00
|
|
|
private readonly ApplicationDbContextFactory _factory;
|
2020-06-14 23:26:04 -05:00
|
|
|
private readonly IMemoryCache _memoryCache;
|
2020-06-24 10:23:16 +02:00
|
|
|
private readonly EventAggregator _eventAggregator;
|
2020-06-16 23:29:25 +09:00
|
|
|
private readonly Dictionary<string, INotificationHandler> _handlersByNotificationType;
|
2020-05-28 22:48:09 -05:00
|
|
|
|
2023-09-18 10:55:05 +09:00
|
|
|
public NotificationManager(ApplicationDbContextFactory factory,
|
2020-06-24 10:23:16 +02:00
|
|
|
IMemoryCache memoryCache, IEnumerable<INotificationHandler> handlers, EventAggregator eventAggregator)
|
2020-05-28 22:48:09 -05:00
|
|
|
{
|
2020-06-12 00:01:45 -05:00
|
|
|
_factory = factory;
|
2020-06-14 23:26:04 -05:00
|
|
|
_memoryCache = memoryCache;
|
2020-06-24 10:23:16 +02:00
|
|
|
_eventAggregator = eventAggregator;
|
2020-06-16 23:29:25 +09:00
|
|
|
_handlersByNotificationType = handlers.ToDictionary(h => h.NotificationType);
|
2020-05-28 22:48:09 -05:00
|
|
|
}
|
|
|
|
|
2023-09-18 10:55:05 +09:00
|
|
|
public async Task<(List<NotificationViewModel> Items, int? Count)> GetSummaryNotifications(string userId, bool cachedOnly)
|
2020-05-28 22:48:09 -05:00
|
|
|
{
|
2020-06-23 03:06:02 +02:00
|
|
|
var cacheKey = GetNotificationsCacheId(userId);
|
2023-09-18 10:55:05 +09:00
|
|
|
if (cachedOnly)
|
|
|
|
return _memoryCache.Get<(List<NotificationViewModel> Items, int? Count)>(cacheKey);
|
2020-12-11 15:11:08 +01:00
|
|
|
return await _memoryCache.GetOrCreateAsync(cacheKey, async entry =>
|
|
|
|
{
|
2023-09-18 10:55:05 +09:00
|
|
|
var res = await GetNotifications(new NotificationsQuery
|
2020-12-11 15:11:08 +01:00
|
|
|
{
|
2021-12-31 16:59:02 +09:00
|
|
|
Seen = false,
|
|
|
|
Skip = 0,
|
|
|
|
Take = 5,
|
|
|
|
UserId = userId
|
2020-12-11 15:11:08 +01:00
|
|
|
});
|
|
|
|
entry.Value = res;
|
|
|
|
return res;
|
|
|
|
});
|
2020-06-12 00:01:45 -05:00
|
|
|
}
|
2021-12-31 16:59:02 +09:00
|
|
|
|
2020-12-11 15:11:08 +01:00
|
|
|
public void InvalidateNotificationCache(params string[] userIds)
|
2020-06-23 03:06:02 +02:00
|
|
|
{
|
2020-12-11 15:11:08 +01:00
|
|
|
foreach (var userId in userIds)
|
|
|
|
{
|
|
|
|
_memoryCache.Remove(GetNotificationsCacheId(userId));
|
2021-12-31 16:59:02 +09:00
|
|
|
_eventAggregator.Publish(new UserNotificationsUpdatedEvent() { UserId = userId });
|
2020-12-11 15:11:08 +01:00
|
|
|
}
|
2020-06-23 03:06:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private static string GetNotificationsCacheId(string userId)
|
|
|
|
{
|
|
|
|
return $"notifications-{userId}";
|
|
|
|
}
|
2023-09-18 10:55:05 +09:00
|
|
|
public const int MaxUnseen = 100;
|
2022-05-02 16:35:28 +09:00
|
|
|
public async Task<(List<NotificationViewModel> Items, int? Count)> GetNotifications(NotificationsQuery query)
|
2020-12-11 15:11:08 +01:00
|
|
|
{
|
|
|
|
await using var dbContext = _factory.CreateContext();
|
|
|
|
|
|
|
|
var queryables = GetNotificationsQueryable(dbContext, query);
|
2022-05-02 16:35:28 +09:00
|
|
|
var items = (await queryables.withPaging.ToListAsync()).Select(ToViewModel).Where(model => model != null).ToList();
|
2024-07-02 09:55:54 +01:00
|
|
|
items = FilterNotifications(items, query);
|
2022-05-02 16:35:28 +09:00
|
|
|
int? count = null;
|
|
|
|
if (query.Seen is false)
|
|
|
|
{
|
|
|
|
// Unseen notifications aren't likely to be too huge, so count should be fast
|
|
|
|
count = await queryables.withoutPaging.CountAsync();
|
2023-09-18 10:55:05 +09:00
|
|
|
if (count >= MaxUnseen)
|
|
|
|
{
|
|
|
|
// If we have too much unseen notifications, we don't want to show the exact count
|
|
|
|
// because it would be too long to display, so we just show 99+
|
|
|
|
// Then cleanup a bit the database by removing the oldest notifications, as it would be expensive to fetch every time
|
|
|
|
if (count >= MaxUnseen + (MaxUnseen / 2))
|
|
|
|
{
|
|
|
|
nextBatch:
|
|
|
|
var seenToRemove = await queryables.withoutPaging.OrderByDescending(data => data.Created).Skip(MaxUnseen).Take(1000).ToListAsync();
|
|
|
|
if (seenToRemove.Count > 0)
|
|
|
|
{
|
|
|
|
foreach (var seen in seenToRemove)
|
|
|
|
{
|
|
|
|
seen.Seen = true;
|
|
|
|
}
|
|
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
goto nextBatch;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
count = MaxUnseen;
|
|
|
|
}
|
2022-05-02 16:35:28 +09:00
|
|
|
}
|
|
|
|
return (Items: items, Count: count);
|
2020-12-11 15:11:08 +01:00
|
|
|
}
|
|
|
|
|
2021-12-31 16:59:02 +09:00
|
|
|
private (IQueryable<NotificationData> withoutPaging, IQueryable<NotificationData> withPaging)
|
2020-12-11 15:11:08 +01:00
|
|
|
GetNotificationsQueryable(ApplicationDbContext dbContext, NotificationsQuery query)
|
2020-06-12 00:01:45 -05:00
|
|
|
{
|
2020-12-11 15:11:08 +01:00
|
|
|
var queryable = dbContext.Notifications.AsQueryable();
|
|
|
|
if (query.Ids?.Any() is true)
|
2020-06-10 18:55:31 -05:00
|
|
|
{
|
2020-12-11 15:11:08 +01:00
|
|
|
queryable = queryable.Where(data => query.Ids.Contains(data.Id));
|
|
|
|
}
|
2020-06-12 00:01:45 -05:00
|
|
|
|
2020-12-11 15:11:08 +01:00
|
|
|
if (!string.IsNullOrEmpty(query.UserId))
|
|
|
|
{
|
|
|
|
queryable = queryable.Where(data => data.ApplicationUserId == query.UserId);
|
2020-06-10 18:55:31 -05:00
|
|
|
}
|
|
|
|
|
2020-12-11 15:11:08 +01:00
|
|
|
if (query.Seen.HasValue)
|
|
|
|
{
|
|
|
|
queryable = queryable.Where(data => data.Seen == query.Seen);
|
|
|
|
}
|
|
|
|
|
|
|
|
queryable = queryable.OrderByDescending(a => a.Created);
|
|
|
|
|
|
|
|
var queryable2 = queryable;
|
|
|
|
if (query.Skip.HasValue)
|
|
|
|
{
|
|
|
|
queryable2 = queryable.Skip(query.Skip.Value);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (query.Take.HasValue)
|
|
|
|
{
|
|
|
|
queryable2 = queryable.Take(query.Take.Value);
|
|
|
|
}
|
|
|
|
|
|
|
|
return (queryable, queryable2);
|
2020-05-28 22:48:09 -05:00
|
|
|
}
|
2020-06-16 23:29:25 +09:00
|
|
|
|
2024-07-02 09:55:54 +01:00
|
|
|
private List<NotificationViewModel> FilterNotifications(List<NotificationViewModel> notifications, NotificationsQuery query)
|
|
|
|
{
|
|
|
|
if (!string.IsNullOrEmpty(query.SearchText))
|
|
|
|
{
|
|
|
|
notifications = notifications.Where(data => data.Body.Contains(query.SearchText)).ToList();
|
|
|
|
}
|
|
|
|
if (query.Type?.Length > 0)
|
|
|
|
{
|
|
|
|
if (query.Type?.Length > 0)
|
|
|
|
{
|
|
|
|
if (query.Type.Contains("userupdate"))
|
|
|
|
{
|
|
|
|
notifications = notifications.Where(n => n.Type.Equals("inviteaccepted", StringComparison.OrdinalIgnoreCase) ||
|
|
|
|
n.Type.Equals("userapproval", StringComparison.OrdinalIgnoreCase)).ToList();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
notifications = notifications.Where(n => query.Type.Contains(n.Type, StringComparer.OrdinalIgnoreCase)).ToList();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-07-10 17:12:22 +02:00
|
|
|
if (query.StoreIds?.Length > 0)
|
2024-07-02 09:55:54 +01:00
|
|
|
{
|
2024-07-10 17:12:22 +02:00
|
|
|
notifications = notifications.Where(n => !string.IsNullOrEmpty(n.StoreId) && query.StoreIds.Contains(n.StoreId, StringComparer.OrdinalIgnoreCase)).ToList();
|
2024-07-02 09:55:54 +01:00
|
|
|
}
|
|
|
|
return notifications;
|
|
|
|
}
|
|
|
|
|
2020-12-11 15:11:08 +01:00
|
|
|
public async Task<List<NotificationViewModel>> ToggleSeen(NotificationsQuery notificationsQuery, bool? setSeen)
|
|
|
|
{
|
|
|
|
await using var dbContext = _factory.CreateContext();
|
|
|
|
|
|
|
|
var queryables = GetNotificationsQueryable(dbContext, notificationsQuery);
|
|
|
|
var items = await queryables.withPaging.ToListAsync();
|
|
|
|
var userIds = items.Select(data => data.ApplicationUserId).Distinct();
|
|
|
|
foreach (var notificationData in items)
|
|
|
|
{
|
|
|
|
notificationData.Seen = setSeen.GetValueOrDefault(!notificationData.Seen);
|
|
|
|
}
|
|
|
|
|
|
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
InvalidateNotificationCache(userIds.ToArray());
|
2021-07-27 14:11:47 +02:00
|
|
|
return items.Select(ToViewModel).Where(model => model != null).ToList();
|
2020-12-11 15:11:08 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
public async Task Remove(NotificationsQuery notificationsQuery)
|
|
|
|
{
|
|
|
|
await using var dbContext = _factory.CreateContext();
|
|
|
|
|
|
|
|
var queryables = GetNotificationsQueryable(dbContext, notificationsQuery);
|
|
|
|
dbContext.RemoveRange(queryables.withPaging);
|
|
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
|
|
|
|
if (!string.IsNullOrEmpty(notificationsQuery.UserId))
|
|
|
|
InvalidateNotificationCache(notificationsQuery.UserId);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private NotificationViewModel ToViewModel(NotificationData data)
|
2020-06-16 23:29:25 +09:00
|
|
|
{
|
|
|
|
var handler = GetHandler(data.NotificationType);
|
2021-07-27 14:11:47 +02:00
|
|
|
if (handler is null)
|
|
|
|
return null;
|
2023-02-21 15:06:34 +09:00
|
|
|
var notification = data.HasTypedBlob(handler.NotificationBlobType).GetBlob();
|
2023-04-10 11:07:03 +09:00
|
|
|
var obj = new NotificationViewModel
|
|
|
|
{
|
|
|
|
Id = data.Id,
|
|
|
|
Type = data.NotificationType,
|
|
|
|
Created = data.Created,
|
|
|
|
Seen = data.Seen
|
|
|
|
};
|
|
|
|
handler.FillViewModel(notification, obj);
|
2020-06-16 23:29:25 +09:00
|
|
|
return obj;
|
|
|
|
}
|
|
|
|
|
|
|
|
public INotificationHandler GetHandler(string notificationId)
|
|
|
|
{
|
2021-07-27 14:11:47 +02:00
|
|
|
_handlersByNotificationType.TryGetValue(notificationId, out var h);
|
|
|
|
return h;
|
2020-06-16 23:29:25 +09:00
|
|
|
}
|
2020-05-28 22:48:09 -05:00
|
|
|
}
|
2020-12-11 15:11:08 +01:00
|
|
|
|
|
|
|
public class NotificationsQuery
|
|
|
|
{
|
|
|
|
public string[] Ids { get; set; }
|
|
|
|
public string UserId { get; set; }
|
|
|
|
public int? Skip { get; set; }
|
|
|
|
public int? Take { get; set; }
|
|
|
|
public bool? Seen { get; set; }
|
2024-07-02 09:55:54 +01:00
|
|
|
public string SearchText { get; set; }
|
|
|
|
public string[] Type { get; set; }
|
2024-07-10 17:12:22 +02:00
|
|
|
public string[] StoreIds { get; set; }
|
2020-12-11 15:11:08 +01:00
|
|
|
}
|
2020-05-27 18:41:00 -05:00
|
|
|
}
|