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 Sockeye.Util; using Sockeye.Models; //using System.Diagnostics; namespace Sockeye.Biz { internal static class NotifyEventHelper { private static ILogger log = Sockeye.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(SockEvent ayaEvent, ICoreBizObjectModel newObject, AyContext ct) { switch (ayaEvent) { case SockEvent.Created: await ProcessStandardObjectCreatedEvents(newObject, ct); break; case SockEvent.Deleted: await ProcessStandardObjectDeletedEvents(newObject, ct); break; case SockEvent.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.SockType == newObject.SType).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, SockType = newObject.SType, 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.SockType == newObject.SType).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, SockType = newObject.SType, 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.SockType == newObject.SType).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, SockType = newObject.SType, 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.SType, bizObject.Id); //------------------------------------------ //ObjectDeleted notification // { var subs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.ObjectDeleted && z.SockType == bizObject.SType).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, SockType = bizObject.SType, 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, SockType sockType, long objectId, NotifyEventType eventType) { var eventsToDelete = await ct.NotifyEvent.Where(z => z.SockType == sockType && 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, SockType sockType, long objectId, NotifyEventType eventType) { var eventsToDelete = await ct.CustomerNotifyEvent.Where(z => z.SockType == sockType && 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, SockType sockType, long objectId) { var eventsToDelete = await ct.NotifyEvent.Where(z => z.SockType == sockType && 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 objectTags, List 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 firstObjectTags, List 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(SockType.NoType, 0, eventType, message, name, except, userId); } internal static async Task AddGeneralNotifyEvent(SockType sockType, 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: 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 = Sockeye.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, SockType = sockType, 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~", SockType = sockType, 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