323 lines
16 KiB
C#
323 lines
16 KiB
C#
using System;
|
|
using System.Linq;
|
|
using System.Collections.Generic;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Logging;
|
|
using AyaNova.Models;
|
|
using AyaNova.Util;
|
|
|
|
namespace AyaNova.Biz
|
|
{
|
|
|
|
/// <summary>
|
|
/// Notification processor
|
|
/// turn notifyEvent records into notifications for in app and deliver smtp
|
|
///
|
|
/// </summary>
|
|
internal static class CoreJobNotify
|
|
{
|
|
private static bool NotifyIsRunning = false;
|
|
private static ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger("CoreJobNotify");
|
|
|
|
|
|
|
|
private static DateTime lastRun = DateTime.MinValue;
|
|
// private static TimeSpan DELETE_AFTER_AGE = new TimeSpan(90, 0, 0, 0);
|
|
// private static TimeSpan RUN_EVERY_INTERVAL = new TimeSpan(0, 2, 0);//once every 2 minutes minimum
|
|
private static DateTime lastNotifyHealthCheckSentLocal = DateTime.MinValue;
|
|
private static TimeSpan TS_24_HOURS = new TimeSpan(24, 0, 0);//used to ensure daily ops happen no more than that
|
|
|
|
#if (DEBUG)
|
|
private static TimeSpan DELETE_AFTER_AGE = new TimeSpan(0, 12, 0, 0);
|
|
private static TimeSpan RUN_EVERY_INTERVAL = new TimeSpan(0, 0, 20);//no more frequently than once every 20 seconds
|
|
#else
|
|
private static TimeSpan DELETE_AFTER_AGE = new TimeSpan(90, 0, 0, 0);
|
|
private static TimeSpan RUN_EVERY_INTERVAL = new TimeSpan(0, 2, 0);//no more frequently than once every 2 minutes
|
|
#endif
|
|
|
|
//temporary list to hold translations as required during delivery
|
|
private static Dictionary<long, List<NameIdItem>> _transCache = new Dictionary<long, List<NameIdItem>>();
|
|
private static Dictionary<long, long> _UserTranslationIdCache = new Dictionary<long, long>();
|
|
private static Dictionary<long, string> _ServerTheWordTranslations = new Dictionary<long, string>();
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// DoSweep
|
|
//
|
|
public static async Task DoWorkAsync()
|
|
{
|
|
log.LogTrace("Checking if Notify should run");
|
|
if (NotifyIsRunning)
|
|
{
|
|
log.LogTrace("Notify is running already exiting this cycle");
|
|
return;
|
|
}
|
|
//This will get triggered roughly every minute, but we don't want to deliver that frequently
|
|
if (DateTime.UtcNow - lastRun < RUN_EVERY_INTERVAL)
|
|
{
|
|
log.LogTrace($"Notify ran less than {RUN_EVERY_INTERVAL}, exiting this cycle");
|
|
return;
|
|
}
|
|
try
|
|
{
|
|
NotifyIsRunning = true;
|
|
log.LogTrace("Notify set to RUNNING state and starting now");
|
|
|
|
//NotifyHealthCheck processing
|
|
//Note this deliberately uses LOCAL time in effort to deliver the health check first thing in the morning for workers
|
|
//However if server is on UTC already then that's what is used, there is no adjustment
|
|
DateTime dtNowLocal = DateTime.Now;
|
|
if (dtNowLocal - lastNotifyHealthCheckSentLocal > TS_24_HOURS)
|
|
{
|
|
//are we in the 7th to 9th hour?
|
|
if (dtNowLocal.Hour > 6 && dtNowLocal.Hour < 10)
|
|
{
|
|
log.LogTrace("Notify health check submitted to subscribers");
|
|
await NotifyEventProcessor.AddGeneralNotifyEvent(NotifyEventType.NotifyHealthCheck, "OK","");
|
|
lastNotifyHealthCheckSentLocal = dtNowLocal;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
using (AyContext ct = AyaNova.Util.ServiceProviderProvider.DBContext)
|
|
{
|
|
//select all jobs with no deliver date or deliver date no longer in future
|
|
|
|
//IMPLEMENTATION NOTES: This code
|
|
// finds the events in NotifyEvent
|
|
// check subscription to determine if the age related ones are deliverable now
|
|
// check subscriptions for each delivery type, i.e. there might be both smtp and inapp separately for the same event so it needs to know that and deliver accordingly
|
|
// remove the notify event after it's processed
|
|
|
|
|
|
|
|
//Older notes:
|
|
//### PLAN if it's an smtp delivery that fails and it's to someone who can be delivered in app then it should send an inapp notification of
|
|
//delivery failure and still delete the smtp delivery
|
|
//If it's not possible to notify the person via in app of the failed smtp then perhaps it notifies OPS personnel and biz admin personnel
|
|
//NEW NOTIFICATION SUBSCRIPTION EVENT TYPE:
|
|
//OPERATIONS_PROBLEMS - backup, notifications, out of memory, what have you, anyone can subscribe to it regardless of rights
|
|
//this is just to let people know there is a problem
|
|
//todo: create message here if not already set?
|
|
//todo: generate and attach report here?
|
|
|
|
|
|
//All items have an event date, for non time delayed events it's just the moment it was created
|
|
//which will predate this moment now if it's pre-existing
|
|
var events = await ct.NotifyEvent.Include(z => z.NotifySubscription).ToListAsync();
|
|
log.LogDebug($"Found {events.Count} NotifyEvents to examine for potential delivery");
|
|
|
|
//cache translations
|
|
//Get all subscription unique userId's that aren't inapp deliveries
|
|
var usersNeedingTranslations = events.Where(z => z.NotifySubscription.DeliveryMethod != NotifyDeliveryMethod.App).Select(z => z.NotifySubscription.UserId).ToList().Distinct();
|
|
foreach (long userid in usersNeedingTranslations)
|
|
{
|
|
long transId = (await ct.UserOptions.SingleAsync(z => z.UserId == userid)).TranslationId;
|
|
_UserTranslationIdCache.Add(userid, transId);
|
|
await CacheNotifyEventTypeTranslations(transId);
|
|
}
|
|
//cache all translations of the word "Server" for server notifications
|
|
_ServerTheWordTranslations = await TranslationBiz.GetAllTranslationsForKey("Server");
|
|
|
|
//iterate and deliver
|
|
foreach (var notifyevent in events)
|
|
{
|
|
//TIME DELAYED AGED EVENT?
|
|
//when to time delay deliver formula:If sub.agevalue!= timespan.zero then deliver on =
|
|
//NotifyEvent "EventDate"+NotifySubscription.AgeValue timespan - NotifySubscription AdvanceNotice timespan > utcNow
|
|
//Is it time delayed?
|
|
if (notifyevent.NotifySubscription.AgeValue != TimeSpan.Zero)
|
|
{
|
|
var deliverAfter = notifyevent.EventDate + notifyevent.NotifySubscription.AgeValue - notifyevent.NotifySubscription.AdvanceNotice;
|
|
if (deliverAfter < DateTime.UtcNow)
|
|
{
|
|
if (notifyevent.NotifySubscription.DeliveryMethod == NotifyDeliveryMethod.App)
|
|
{
|
|
await DeliverInApp(notifyevent, ct);
|
|
}
|
|
if (notifyevent.NotifySubscription.DeliveryMethod == NotifyDeliveryMethod.SMTP)
|
|
{
|
|
await DeliverSMTP(notifyevent, ct);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//NORMAL IMMEDIATE DELIVERY EVENT
|
|
if (notifyevent.NotifySubscription.DeliveryMethod == NotifyDeliveryMethod.App)
|
|
{
|
|
await DeliverInApp(notifyevent, ct);
|
|
}
|
|
if (notifyevent.NotifySubscription.DeliveryMethod == NotifyDeliveryMethod.SMTP)
|
|
{
|
|
await DeliverSMTP(notifyevent, ct);
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.LogError(ex, $"Error processing notification event");
|
|
}
|
|
finally
|
|
{
|
|
log.LogTrace("Notify is done setting to not running state and tagging lastRun timestamp");
|
|
_UserTranslationIdCache.Clear();
|
|
_transCache.Clear();
|
|
lastRun = DateTime.UtcNow;
|
|
NotifyIsRunning = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
//cache any translations required for email notification
|
|
private static async Task CacheNotifyEventTypeTranslations(long translationId)
|
|
{
|
|
if (_transCache.ContainsKey(translationId))
|
|
{
|
|
return;
|
|
}
|
|
_transCache.Add(translationId, await AyaNova.Api.Controllers.EnumListController.GetEnumList("NotifyEventType", translationId));
|
|
}
|
|
|
|
//Used for subject of email and message deliveries
|
|
private static string GetTranslatedNotifyEventName(NotifyEventType net, long translationId)
|
|
{
|
|
return _transCache.First(z => z.Key == translationId).Value.First(z => z.Id == (int)net).Name;
|
|
}
|
|
|
|
private static async Task DeliverInApp(NotifyEvent ne, AyContext ct)
|
|
{
|
|
log.LogTrace($"DeliverInApp deliving notify event: {ne}");
|
|
await ct.Notification.AddAsync(new Notification() { UserId = ne.UserId, AyaType = ne.AyaType, ObjectId = ne.ObjectId, EventType = ne.EventType, NotifySubscriptionId = ne.NotifySubscriptionId, Message = ne.Message, Name = ne.Name });
|
|
ct.NotifyEvent.Remove(ne);
|
|
await ct.SaveChangesAsync();
|
|
}
|
|
|
|
|
|
|
|
private static async Task DeliverSMTP(NotifyEvent ne, AyContext ct)
|
|
{
|
|
log.LogTrace($"DeliverSMTP delivering notify event: {ne}");
|
|
if (string.IsNullOrWhiteSpace(ne.NotifySubscription.DeliveryAddress))
|
|
{
|
|
await NotifyEventProcessor.AddGeneralNotifyEvent(NotifyEventType.GeneralNotification, $"No email address is set in subscription to deliver email notification for this event:{ne}","Error", null, ne.UserId);
|
|
}
|
|
|
|
|
|
|
|
var transid = _UserTranslationIdCache[ne.NotifySubscription.UserId];
|
|
var name = ne.Name;
|
|
if (name == "~SERVER~")
|
|
{
|
|
name = _ServerTheWordTranslations.First(z => z.Key == transid).Value;
|
|
}
|
|
var subject = $"AY:{GetTranslatedNotifyEventName(ne.EventType, transid)}:{name}";
|
|
|
|
IMailer m = AyaNova.Util.ServiceProviderProvider.Mailer;
|
|
try
|
|
{
|
|
var body = "";
|
|
//NOTE: if need any other exemptions besides backup status make a separate static function "CanOpen(NotifyEventType)"
|
|
|
|
if (ne.ObjectId != 0 || ne.EventType == NotifyEventType.BackupStatus)
|
|
{
|
|
body = OpenObjectUrlBuilder(ne.AyaType, ne.ObjectId, ne.EventType) + "\n";
|
|
}
|
|
body += ne.Message;
|
|
|
|
if (!ServerGlobalOpsSettingsCache.Notify.SmtpDeliveryActive)
|
|
{
|
|
await NotifyEventProcessor.AddGeneralNotifyEvent(NotifyEventType.GeneralNotification, $"Email notifications are set to OFF at server, unable to send email notification for this event:{ne}","Error", null, ne.UserId);
|
|
log.LogInformation($"** WARNING: SMTP notification is currently set to Active=False; unable to deliver email notification, re-routed to in-app notification instead [UserId={ne.UserId}, Notify subscription={ne.NotifySubscriptionId}]. Change this setting or have users remove email delivery notifications if this is permanent **");
|
|
}
|
|
else
|
|
{
|
|
await m.SendEmailAsync(ne.NotifySubscription.DeliveryAddress, subject, body, ServerGlobalOpsSettingsCache.Notify);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await NotifyEventProcessor.AddOpsProblemEvent("SMTP Notification failed", ex);
|
|
await NotifyEventProcessor.AddGeneralNotifyEvent(NotifyEventType.GeneralNotification, $"An error prevented delivering the following notification via email. System operator users have been notified:{ne}","Error", null, ne.UserId);
|
|
}
|
|
finally
|
|
{
|
|
//remove event no matter what
|
|
ct.NotifyEvent.Remove(ne);
|
|
await ct.SaveChangesAsync();
|
|
}
|
|
}
|
|
|
|
//Called from ops notification settings to test smtp setup by delivering to address of choosing
|
|
public static async Task<string> TestSMTPDelivery(string toAddress)
|
|
{
|
|
//DO TEST DELIVERY HERE USING EXACT SAME SETTINGS AS FOR DeliverSMTP above
|
|
//todo: abstract out email sending to it's own class maybe or whatever method I choose supports the best
|
|
//https://jasonwatmore.com/post/2020/07/15/aspnet-core-3-send-emails-via-smtp-with-mailkit
|
|
//https://medium.com/@ffimnsr/sending-email-using-mailkit-in-asp-net-core-web-api-71b946380442
|
|
|
|
IMailer m = AyaNova.Util.ServiceProviderProvider.Mailer;
|
|
try
|
|
{
|
|
await m.SendEmailAsync(toAddress, "Test from Notification system", "This is a test to confirm notification system is working", ServerGlobalOpsSettingsCache.Notify);
|
|
return "ok";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await NotifyEventProcessor.AddOpsProblemEvent("SMTP (TEST) Notification failed", ex);
|
|
return ExceptionUtil.ExtractAllExceptionMessages(ex);
|
|
}
|
|
|
|
}
|
|
|
|
|
|
//Open object url
|
|
//### NOTE: If this is required anywhere else, move it to a central BizUtils class in Biz folder callable from here and there
|
|
private static string OpenObjectUrlBuilder(AyaType otype, long id, NotifyEventType net)
|
|
{
|
|
var ServerUrl = ServerGlobalOpsSettingsCache.Notify.AyaNovaServerURL;
|
|
if (string.IsNullOrWhiteSpace(ServerUrl))
|
|
{
|
|
NotifyEventProcessor.AddOpsProblemEvent("Notification system: The OPS Notification setting is empty for AyaNova Server URL. This prevents Notification system from linking events to openable objects.").Wait();
|
|
return "OPS ERROR NO SERVER URL CONFIGURED";
|
|
}
|
|
ServerUrl = ServerUrl.Trim().TrimEnd('/');
|
|
|
|
//HANDLE ITEMS WITHOUT TYPE OR ID
|
|
if (net == NotifyEventType.BackupStatus)
|
|
{
|
|
return $"{ServerUrl}/ops-backup";
|
|
}
|
|
|
|
|
|
//Might not have a type or id in which case nothing directly to open
|
|
if (otype == AyaType.NoType || id == 0)
|
|
{
|
|
return ServerUrl;
|
|
}
|
|
|
|
if (NotifyEventType.ObjectDeleted == net)
|
|
{
|
|
//goto event log for item
|
|
// path: "/history/:ayatype/:recordid/:userlog?",
|
|
return $"{ServerUrl}/history/{(int)otype}/{id}";
|
|
}
|
|
|
|
//default is to open the object in question directly
|
|
return $"{ServerUrl}/open/{(int)otype}/{id}";
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////////
|
|
|
|
}//eoc
|
|
|
|
|
|
}//eons
|
|
|