Files
raven/server/AyaNova/generator/CoreJobNotify.cs
2020-07-22 15:30:56 +00:00

202 lines
10 KiB
C#

using System;
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
////////////////////////////////////////////////////////////////////////////////////////////////
// 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.LogTrace($"Found {events.Count} NotifyEvents to examine for potential delivery");
//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");
lastRun = DateTime.UtcNow;
NotifyIsRunning = false;
}
}
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 deliving notify event: {ne}");
//todo: //Open question: what to do with failed deliveries?
//we dont' want them piling up but we don't want to just dump them do we?
//it should be only mail ones that fail, not app ones, there's no way for an app delivery to fail as it's just put in a table
//### 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
}
//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)
{
return ExceptionUtil.ExtractAllExceptionMessages(ex);
}
}
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons