NotificationService.cs 6.03 KB
Newer Older
Dmytro Bogatov's avatar
Dmytro Bogatov committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
using System;
using StatusMonitor.Shared.Extensions;
using StatusMonitor.Shared.Models;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using System.Linq;
using Microsoft.Extensions.Configuration;
using Microsoft.EntityFrameworkCore;
using StatusMonitor.Shared.Models.Entities;
using MailKit.Net.Smtp;
using MimeKit;
using MailKit.Security;
using System.Security.Cryptography.X509Certificates;
using System.Net.Security;
using System.Collections.Generic;
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("test")]

namespace StatusMonitor.Shared.Services
{
	/// <summary>
	/// Service used to schedule and send notifications
	/// </summary>
	public interface INotificationService
	{
		/// <summary>
		/// Generates a notification with given message of given severity and puts it in a queue
		/// </summary>
		/// <param name="message">Notification body</param>
		/// <param name="severity">Notification severity</param>
		/// <returns>Newly created notification</returns>
		Task<Notification> ScheduleNotificationAsync(string message, NotificationSeverity severity);

		/// <summary>
		/// Traverses queues of notifications and sends them out if necessary
		/// </summary>
		/// <returns>True if notification queue has been flushed and false otherwise</returns>
		Task<bool> ProcessNotificationQueueAsync();
	}

	public class NotificationService : INotificationService
	{
		private readonly ILogger<NotificationService> _logger;
		private readonly IConfiguration _config;
		private readonly IDataContext _context;
		private readonly IEmailService _email;
		private readonly ISlackService _slack;

		public NotificationService(
			ILogger<NotificationService> logger,
			IConfiguration config,
			IDataContext context,
			IEmailService email,
			ISlackService slack
		)
		{
			_logger = logger;
			_config = config;
			_context = context;
			_email = email;
			_slack = slack;
		}

		public async Task<bool> ProcessNotificationQueueAsync()
		{
			var send = Enum
				.GetValues(typeof(NotificationSeverity))
				.Cast<object>()
				.Select(async severity => await CheckIfNeedToSendAsync((NotificationSeverity)severity))
				.Select(task => task.Result)
				.Aggregate((self, next) => self || next);

			if (send && await _context.Notifications.AnyAsync(ntf => !ntf.IsSent))
			{
				var notifications = await _context
					.Notifications
					.Where(ntf => !ntf.IsSent)
					.OrderBy(ntf => ntf.DateCreated)
					.ToListAsync();

Dmytro Bogatov's avatar
Dmytro Bogatov committed
82
				var message = await ComposeMessageAsync(notifications);
Dmytro Bogatov's avatar
Dmytro Bogatov committed
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150

				await _email.SendEmailAsync(
					new string[] { _config["Secrets:Email:ToEmail"] },
					"Status site notifications",
					message
				);

				await _slack.SendMessageAsync(message);

				notifications
					.ForEach(ntf =>
					{
						ntf.DateSent = DateTime.UtcNow;
						ntf.IsSent = true;
					});
				await _context.SaveChangesAsync();

				return true;
			}
			else
			{
				return false;
			}
		}

		public async Task<Notification> ScheduleNotificationAsync(string message, NotificationSeverity severity)
		{
			var notification = await _context
				.Notifications
				.AddAsync(new Notification
				{
					Message = message,
					Severity = severity
				});

			await _context.SaveChangesAsync();

			return notification.Entity;
		}

		/// <summary>
		/// Verifies if notifications of given severity need to be sent considering the configuration
		/// </summary>
		/// <param name="severity">Severity for which to check</param>
		/// <returns>True if it is time to send this severity and false otherwise</returns>
		internal async Task<bool> CheckIfNeedToSendAsync(NotificationSeverity severity)
		{
			if (await _context.Notifications.AnyAsync(ntf => ntf.IsSent && ntf.Severity == severity))
			{
				var interval = new TimeSpan(
					0,
					0,
					Convert.ToInt32(_config[$"ServiceManager:NotificationService:Frequencies:{severity.ToString()}"])
				);

				var lastSentDate = (await _context
					.Notifications
					.Where(ntf => ntf.IsSent && ntf.Severity == severity)
					.OrderByDescending(ntf => ntf.DateSent)
					.FirstAsync())
					.DateSent;

				return lastSentDate < DateTime.UtcNow - interval;
			}

			return true;
		}

Dmytro Bogatov's avatar
Dmytro Bogatov committed
151 152 153 154 155 156
		internal async Task<string> GenerateUnresolvedDiscrepanciesNoteAsync() {
			var unresolved = 	await _context.Discrepancies.Where(d => !d.Resolved).CountAsync();

			return
				unresolved == 0 ?
				"There are no outstanding issues. Well done." :
Dmytro Bogatov's avatar
Fix.  
Dmytro Bogatov committed
157
				$"There {(unresolved == 1 ? "is" : "are")} still outstanding {unresolved} issue{(unresolved == 1 ? "" : "s")}. See admin panel."
Dmytro Bogatov's avatar
Dmytro Bogatov committed
158 159 160 161
			;

		}

Dmytro Bogatov's avatar
Dmytro Bogatov committed
162 163 164 165 166
		/// <summary>
		/// Generate a message containing all given notifications grouped by severities
		/// </summary>
		/// <param name="notifications">List of notifications to include in the message</param>
		/// <returns>The composed message</returns>
Dmytro Bogatov's avatar
Dmytro Bogatov committed
167 168 169 170
		internal async Task<string> ComposeMessageAsync(IEnumerable<Notification> notifications) =>
			_config["ServiceManager:NotificationService:Verbosity"] == "normal" ?
				$@"
					Dear recipient,
Dmytro Bogatov's avatar
Dmytro Bogatov committed
171

Dmytro Bogatov's avatar
Dmytro Bogatov committed
172
					Following are the notification messages from Status Site.
Dmytro Bogatov's avatar
Dmytro Bogatov committed
173

Dmytro Bogatov's avatar
Dmytro Bogatov committed
174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213
					{
						notifications
							.GroupBy(ntf => ntf.Severity)
							.Select(			
								(value, key) => $@"Severity {(NotificationSeverity)value.Key}:{Environment.NewLine}			
									{
										value
											.Select(ntf => $"[{ntf.DateCreated.ToStringUsingTimeZone(_config["ServiceManager:NotificationService:TimeZone"])}] {ntf.Message}")
											.Aggregate((self, next) => $"{self}{Environment.NewLine}{next}")
									}
								"
							)
							.Aggregate((self, next) => $"{self}{Environment.NewLine}{next}")
					}

					{await GenerateUnresolvedDiscrepanciesNoteAsync()}

					Always yours, 
					Notificator
				"
				.Replace("\t", "")
			: 
				$@"
					{
						notifications
							.Select(
								ntf => $@"[{
									ntf.
										DateCreated.
										ToStringUsingTimeZone(
											_config["ServiceManager:NotificationService:TimeZone"]
										)
								}] {ntf.Message}"
							)
							.Aggregate((self, next) => $"{self}{Environment.NewLine}{next}")
					}
					{await GenerateUnresolvedDiscrepanciesNoteAsync()}
					"
					.Replace("\t", "")
			;
Dmytro Bogatov's avatar
Dmytro Bogatov committed
214 215
	}
}