408 lines
19 KiB
C#
408 lines
19 KiB
C#
using System;
|
|
using System.Linq;
|
|
using System.Globalization;
|
|
using System.Text;
|
|
using System.Collections.Generic;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using AyaNova.Util;
|
|
using AyaNova.Models;
|
|
//using System.Diagnostics;
|
|
|
|
|
|
namespace AyaNova.Biz
|
|
{
|
|
|
|
internal static class NotifyEventHelper
|
|
{
|
|
private static ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger("NotifyEventProcessor");
|
|
|
|
|
|
///////////////////////////////////////////
|
|
// ENSURE USER HAS IN APP NOTIFICATION
|
|
//
|
|
//
|
|
public static async Task EnsureDefaultInAppUserNotificationSubscriptionExists(long userId, AyContext ct)
|
|
{
|
|
var defaultsub = await ct.NotifySubscription.FirstOrDefaultAsync(z => z.EventType == NotifyEventType.GeneralNotification && z.UserId == userId && z.DeliveryMethod == NotifyDeliveryMethod.App);
|
|
if (defaultsub == null)
|
|
{
|
|
//NOTE: agevalue and advanced notice settings here will ensure that direct in app notifications with a future delivery date ("deadman" switch deliveries) set in their
|
|
//notifyevent.eventdate will deliver on that date and not immediately to support all the things that are direct built in notifications for future dates
|
|
//such as for an overdue Review which doesn't have or need it's own notifyeventtype and subscription independently
|
|
|
|
//NEW NOTE: above makes not sense, I'm setting these back to timespan zero
|
|
defaultsub = new NotifySubscription()
|
|
{
|
|
UserId = userId,
|
|
EventType = NotifyEventType.GeneralNotification,
|
|
DeliveryMethod = NotifyDeliveryMethod.App,
|
|
AgeValue = TimeSpan.Zero,//new TimeSpan(0, 0, 1),
|
|
AdvanceNotice = TimeSpan.Zero//new TimeSpan(0, 0, 1)
|
|
};
|
|
await ct.NotifySubscription.AddAsync(defaultsub);
|
|
await ct.SaveChangesAsync();
|
|
}
|
|
return;
|
|
}
|
|
|
|
|
|
/////////////////////////////////////////
|
|
// PROCESS STANDARD EVENTS
|
|
//
|
|
//
|
|
public static async Task ProcessStandardObjectEvents(AyaEvent ayaEvent, ICoreBizObjectModel newObject, AyContext ct)
|
|
{
|
|
switch (ayaEvent)
|
|
{
|
|
case AyaEvent.Created:
|
|
await ProcessStandardObjectCreatedEvents(newObject, ct);
|
|
break;
|
|
case AyaEvent.Deleted:
|
|
await ProcessStandardObjectDeletedEvents(newObject, ct);
|
|
break;
|
|
case AyaEvent.Modified:
|
|
await ProcessStandardObjectModifiedEvents(newObject, ct);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/////////////////////////////////////////
|
|
// PROCESS STANDARD CREATE NOTIFICATION
|
|
//
|
|
//
|
|
public static async Task ProcessStandardObjectCreatedEvents(ICoreBizObjectModel newObject, AyContext ct)
|
|
{
|
|
//CREATED SUBSCRIPTIONS
|
|
{
|
|
var subs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.ObjectCreated && z.AyaType == newObject.AyaType).ToListAsync();
|
|
foreach (var sub in subs)
|
|
{
|
|
//not for inactive users
|
|
if (!await UserBiz.UserIsActive(sub.UserId)) continue;
|
|
if (ObjectHasAllSubscriptionTags(newObject.Tags, sub.Tags))
|
|
{
|
|
NotifyEvent n = new NotifyEvent()
|
|
{
|
|
EventType = NotifyEventType.ObjectCreated,
|
|
UserId = sub.UserId,
|
|
AyaType = newObject.AyaType,
|
|
ObjectId = newObject.Id,
|
|
NotifySubscriptionId = sub.Id,
|
|
Name = newObject.Name
|
|
};
|
|
await ct.NotifyEvent.AddAsync(n);
|
|
await ct.SaveChangesAsync();
|
|
log.LogDebug($"Added NotifyEvent: [{n.ToString()}]");
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
//AGE SUBSCRIPTIONS
|
|
{
|
|
var subs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.ObjectAge && z.AyaType == newObject.AyaType).ToListAsync();
|
|
foreach (var sub in subs)
|
|
{
|
|
//not for inactive users
|
|
if (!await UserBiz.UserIsActive(sub.UserId)) continue;
|
|
if (ObjectHasAllSubscriptionTags(newObject.Tags, sub.Tags))
|
|
{
|
|
//Note: age is set by advance notice which is consulted by CoreJobNotify in it's run so the deliver date is not required here only the reference EventDate to check for deliver
|
|
//ObjectAge is determined by subscription AgeValue in combo with the EventDate NotifyEvent parameter which together determines at what age from notifyevent.EventDate it's considered for the event to have officially occured
|
|
//However delivery is determined by sub.advancenotice so all three values play a part
|
|
//
|
|
NotifyEvent n = new NotifyEvent()
|
|
{
|
|
EventType = NotifyEventType.ObjectAge,
|
|
UserId = sub.UserId,
|
|
AyaType = newObject.AyaType,
|
|
ObjectId = newObject.Id,
|
|
NotifySubscriptionId = sub.Id,
|
|
Name = newObject.Name
|
|
};
|
|
await ct.NotifyEvent.AddAsync(n);
|
|
await ct.SaveChangesAsync();
|
|
log.LogDebug($"Added NotifyEvent: [{n.ToString()}]");
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
///////////////////////////////////////////////
|
|
// PROCESS STANDARD MODIFIED NOTIFICATION
|
|
//
|
|
//
|
|
public static async Task ProcessStandardObjectModifiedEvents(ICoreBizObjectModel newObject, AyContext ct)
|
|
{
|
|
{
|
|
var subs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.ObjectModified && z.AyaType == newObject.AyaType).ToListAsync();
|
|
foreach (var sub in subs)
|
|
{
|
|
//not for inactive users
|
|
if (!await UserBiz.UserIsActive(sub.UserId)) continue;
|
|
if (ObjectHasAllSubscriptionTags(newObject.Tags, sub.Tags))
|
|
{
|
|
NotifyEvent n = new NotifyEvent()
|
|
{
|
|
EventType = NotifyEventType.ObjectModified,
|
|
UserId = sub.UserId,
|
|
AyaType = newObject.AyaType,
|
|
ObjectId = newObject.Id,
|
|
NotifySubscriptionId = sub.Id,
|
|
Name = newObject.Name
|
|
};
|
|
await ct.NotifyEvent.AddAsync(n);
|
|
await ct.SaveChangesAsync();
|
|
log.LogDebug($"Added NotifyEvent: [{n.ToString()}]");
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
|
|
/////////////////////////////////////////
|
|
// PROCESS STANDARD DELETE NOTIFICATION
|
|
//
|
|
//
|
|
public static async Task ProcessStandardObjectDeletedEvents(ICoreBizObjectModel bizObject, AyContext ct)
|
|
{
|
|
// It's gone and shouldn't have any events left for it
|
|
await ClearPriorEventsForObject(ct, bizObject.AyaType, bizObject.Id);
|
|
|
|
|
|
//------------------------------------------
|
|
//ObjectDeleted notification
|
|
//
|
|
{
|
|
var subs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.ObjectDeleted && z.AyaType == bizObject.AyaType).ToListAsync();
|
|
foreach (var sub in subs)
|
|
{
|
|
//not for inactive users
|
|
if (!await UserBiz.UserIsActive(sub.UserId)) continue;
|
|
if (ObjectHasAllSubscriptionTags(bizObject.Tags, sub.Tags))
|
|
{
|
|
//TODO: On deliver should point to history event log record or take from there and insert into delivery message?
|
|
NotifyEvent n = new NotifyEvent()
|
|
{
|
|
EventType = NotifyEventType.ObjectDeleted,
|
|
UserId = sub.UserId,
|
|
AyaType = bizObject.AyaType,
|
|
ObjectId = bizObject.Id,
|
|
NotifySubscriptionId = sub.Id,
|
|
Name = bizObject.Name
|
|
};
|
|
await ct.NotifyEvent.AddAsync(n);
|
|
await ct.SaveChangesAsync();
|
|
log.LogDebug($"Added NotifyEvent: [{n.ToString()}]");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
//////////////////////////////////
|
|
// CLEAN OUT OLD EVENTS
|
|
//
|
|
//
|
|
//Any specific event created in objects biz code (not simply "Modified")
|
|
//should trigger this remove code prior to updates etc where it creates a new one
|
|
//particularly for future delivery ones but will catch the case of a quick double edit of an object that
|
|
//would alter what gets delivered in the notification and before it's sent out yet
|
|
public static async Task ClearPriorEventsForObject(AyContext ct, AyaType ayaType, long objectId, NotifyEventType eventType)
|
|
{
|
|
var eventsToDelete = await ct.NotifyEvent.Where(z => z.AyaType == ayaType && z.ObjectId == objectId && z.EventType == eventType).ToListAsync();
|
|
if (eventsToDelete.Count == 0) return;
|
|
ct.NotifyEvent.RemoveRange(eventsToDelete);
|
|
await ct.SaveChangesAsync();
|
|
}
|
|
|
|
public static async Task ClearPriorCustomerNotifyEventsForObject(AyContext ct, AyaType ayaType, long objectId, NotifyEventType eventType)
|
|
{
|
|
var eventsToDelete = await ct.CustomerNotifyEvent.Where(z => z.AyaType == ayaType && z.ObjectId == objectId && z.EventType == eventType).ToListAsync();
|
|
if (eventsToDelete.Count == 0) return;
|
|
ct.CustomerNotifyEvent.RemoveRange(eventsToDelete);
|
|
await ct.SaveChangesAsync();
|
|
}
|
|
|
|
//scorched earth one for outright delete of objects when you don't want any prior events left for it
|
|
//probably only ever used for the delete event, can't think of another one right now new years morning early 2021 a bit hungover but possibly there is :)
|
|
public static async Task ClearPriorEventsForObject(AyContext ct, AyaType ayaType, long objectId)
|
|
{
|
|
var eventsToDelete = await ct.NotifyEvent.Where(z => z.AyaType == ayaType && z.ObjectId == objectId).ToListAsync();
|
|
if (eventsToDelete.Count == 0) return;
|
|
ct.NotifyEvent.RemoveRange(eventsToDelete);
|
|
await ct.SaveChangesAsync();
|
|
}
|
|
|
|
|
|
//////////////////////////////////
|
|
// COMPARE TAGS COLLECTION
|
|
//
|
|
// A match here means *all* tags in the subscription are present in the object
|
|
//
|
|
public static bool ObjectHasAllSubscriptionTags(List<string> objectTags, List<string> subTags)
|
|
{
|
|
//no subscription tags? Then it always will match
|
|
if (subTags.Count == 0) return true;
|
|
//have sub tags but object has none? Then it's never going to match
|
|
if (objectTags.Count == 0) return false;
|
|
|
|
//not enought tags on object to match sub tags?
|
|
if (subTags.Count > objectTags.Count) return false;
|
|
|
|
//Do ALL the tags in the subscription exist in the object?
|
|
return subTags.All(z => objectTags.Any(x => x == z));
|
|
}
|
|
|
|
//////////////////////////////////
|
|
// COMPARE TAGS COLLECTION
|
|
//
|
|
// A match here means *all* tags are the same in both objects (don't have to be in same order)
|
|
//
|
|
public static bool TwoObjectsHaveSameTags(List<string> firstObjectTags, List<string> secondObjectTags)
|
|
{
|
|
//no tags on either side?
|
|
if (firstObjectTags.Count == 0 && secondObjectTags.Count == 0) return true;
|
|
|
|
//different counts will always mean not a match
|
|
if (firstObjectTags.Count != secondObjectTags.Count) return false;
|
|
|
|
//Do ALL the tags in the first object exist in the second object?
|
|
return firstObjectTags.All(z => secondObjectTags.Any(x => x == z));
|
|
}
|
|
|
|
/////////////////////////////////////////
|
|
// CREATE OPS PROBLEM EVENT
|
|
//
|
|
//
|
|
internal static async Task AddOpsProblemEvent(string message, Exception ex = null)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(message) && ex == null)
|
|
return;
|
|
|
|
//Log as a backup in case there is no one to notify and also for the record and support
|
|
if (ex != null)
|
|
{
|
|
//actually, if there is an exception it's already logged anyway so don't re-log it here, just makes dupes
|
|
// log.LogError(ex, $"Ops problem notification: \"{message}\"");
|
|
message += $"\nException error: {ExceptionUtil.ExtractAllExceptionMessages(ex)}";
|
|
}
|
|
else
|
|
log.LogWarning($"Ops problem notification: \"{message}\"");
|
|
|
|
await AddGeneralNotifyEvent(NotifyEventType.ServerOperationsProblem, message, "OPS");
|
|
}
|
|
|
|
|
|
/////////////////////////////////////////
|
|
// CREATE GENERAL NOTIFY EVENT
|
|
//
|
|
//
|
|
|
|
internal static async Task AddGeneralNotifyEvent(NotifyEventType eventType, string message, string name, Exception except = null, long userId = 0)
|
|
{
|
|
await AddGeneralNotifyEvent(AyaType.NoType, 0, eventType, message, name, except, userId);
|
|
}
|
|
internal static async Task AddGeneralNotifyEvent(AyaType atype, long objectid, NotifyEventType eventType, string message, string name, Exception except = null, long userId = 0)
|
|
{
|
|
|
|
//This handles general notification events not requiring a decision or tied to an object that are basically just a immediate message to the user
|
|
//e.g. ops problems, GeneralNotification, NotifyHealthCheck etc
|
|
//optional user id to send directly to them
|
|
log.LogDebug($"AddGeneralNotifyEvent processing: [type:{eventType}, userId:{userId}, message:{message}]");
|
|
#if (DEBUG)
|
|
switch (eventType)
|
|
{
|
|
case NotifyEventType.BackupStatus:
|
|
case NotifyEventType.GeneralNotification:
|
|
case NotifyEventType.NotifyHealthCheck://created by job processor itself
|
|
case NotifyEventType.ServerOperationsProblem:
|
|
case NotifyEventType.PMGenerationFailed:
|
|
break;
|
|
default://this will likely be a development error, not a production error so no need to log etc
|
|
throw (new System.NotSupportedException($"NotifyEventProcessor:AddGeneralNotifyEvent - Type of event {eventType} is unexpected and not supported"));
|
|
}
|
|
|
|
if (eventType != NotifyEventType.GeneralNotification && userId != 0)
|
|
{
|
|
throw (new System.NotSupportedException($"NotifyEventProcessor:AddGeneralNotifyEvent - event {eventType} was specified with user id {userId} which is unexpected and not supported"));
|
|
}
|
|
#endif
|
|
|
|
try
|
|
{
|
|
using (AyContext ct = AyaNova.Util.ServiceProviderProvider.DBContext)
|
|
{
|
|
|
|
//General notification goes to one specific user only
|
|
if (eventType == NotifyEventType.GeneralNotification)
|
|
{
|
|
if (userId == 0)
|
|
{
|
|
//this will likely be a development error, not a production error so no need to log etc
|
|
throw new System.ArgumentException("NotifyEventProcessor:AddGeneralNotifyEvent: GeneralNotification requires a user id but none was specified");
|
|
}
|
|
//not for inactive users
|
|
if (!await UserBiz.UserIsActive(userId)) return;
|
|
|
|
var UserName = await ct.User.AsNoTracking().Where(z => z.Id == userId).Select(z => z.Name).FirstOrDefaultAsync();
|
|
|
|
//if they don't have a regular inapp subscription create one now
|
|
await EnsureDefaultInAppUserNotificationSubscriptionExists(userId, ct);
|
|
|
|
if (string.IsNullOrWhiteSpace(name))
|
|
name = UserName;
|
|
|
|
var gensubs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.GeneralNotification && z.UserId == userId).ToListAsync();
|
|
foreach (var sub in gensubs)
|
|
{
|
|
NotifyEvent n = new NotifyEvent() { EventType = eventType, UserId = userId, Message = message, NotifySubscriptionId = sub.Id, Name = name, AyaType = atype, ObjectId = objectid };
|
|
await ct.NotifyEvent.AddAsync(n);
|
|
}
|
|
if (gensubs.Count > 0)
|
|
await ct.SaveChangesAsync();
|
|
return;
|
|
}
|
|
|
|
|
|
//check subscriptions for event and send accordingly to each user
|
|
var subs = await ct.NotifySubscription.Where(z => z.EventType == eventType).ToListAsync();
|
|
|
|
//append exception message if not null
|
|
if (except != null)
|
|
message += $"\nException error: {ExceptionUtil.ExtractAllExceptionMessages(except)}";
|
|
|
|
foreach (var sub in subs)
|
|
{
|
|
//not for inactive users
|
|
if (!await UserBiz.UserIsActive(sub.UserId)) continue;
|
|
//note flag ~SERVER~ means to client to substitute "Server" translation key text instead
|
|
NotifyEvent n = new NotifyEvent() { EventType = eventType, UserId = sub.UserId, Message = message, NotifySubscriptionId = sub.Id, Name = "~SERVER~", AyaType = atype, ObjectId = objectid };
|
|
await ct.NotifyEvent.AddAsync(n);
|
|
}
|
|
if (subs.Count > 0)
|
|
await ct.SaveChangesAsync();
|
|
}
|
|
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.LogError(ex, $"Error adding general notify event [type:{eventType}, userId:{userId}, message:{message}]");
|
|
DbUtil.HandleIfDatabaseUnavailableTypeException(ex);
|
|
}
|
|
|
|
}//eom
|
|
|
|
|
|
|
|
}//eoc
|
|
|
|
}//eons
|