From b9a886d5dc0873cf163dd49432e2273ad6e6ab54 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Thu, 17 Mar 2016 13:32:58 +0000 Subject: [PATCH] Added first implimentation of the Notification Service #8 Added tests to cover the notification service --- .../EmailNotificationSettings.cs | 1 + .../NotificationServiceTests.cs | 116 ++++++++++++++++++ .../PlexRequests.Services.Tests.csproj | 1 + .../Notification/EmailMessageNotification.cs | 108 ++++++++++++++++ .../Notification/INotification.cs | 46 +++++++ .../Notification/NotificationService.cs | 78 ++++++++++++ .../PlexRequests.Services.csproj | 3 + PlexRequests.UI/Bootstrapper.cs | 18 ++- PlexRequests.UI/Modules/AdminModule.cs | 8 +- PlexRequests.UI/Modules/SearchModule.cs | 20 ++- .../Views/Admin/EmailNotifications.cshtml | 14 +++ PlexRequests.UI/Views/Admin/_Sidebar.cshtml | 4 +- README.md | 3 + 13 files changed, 410 insertions(+), 10 deletions(-) create mode 100644 PlexRequests.Services.Tests/NotificationServiceTests.cs create mode 100644 PlexRequests.Services/Notification/EmailMessageNotification.cs create mode 100644 PlexRequests.Services/Notification/INotification.cs create mode 100644 PlexRequests.Services/Notification/NotificationService.cs diff --git a/PlexRequests.Core/SettingModels/EmailNotificationSettings.cs b/PlexRequests.Core/SettingModels/EmailNotificationSettings.cs index 3c81d83cf..e725b0f0c 100644 --- a/PlexRequests.Core/SettingModels/EmailNotificationSettings.cs +++ b/PlexRequests.Core/SettingModels/EmailNotificationSettings.cs @@ -4,6 +4,7 @@ { public string EmailHost { get; set; } public int EmailPort { get; set; } + public bool Ssl { get; set; } public string RecipientEmail { get; set; } public string EmailUsername { get; set; } public string EmailPassword { get; set; } diff --git a/PlexRequests.Services.Tests/NotificationServiceTests.cs b/PlexRequests.Services.Tests/NotificationServiceTests.cs new file mode 100644 index 000000000..ce5bf23ae --- /dev/null +++ b/PlexRequests.Services.Tests/NotificationServiceTests.cs @@ -0,0 +1,116 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: NotificationServiceTests.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion +using Moq; + +using NUnit.Framework; + +using PlexRequests.Services.Notification; + +namespace PlexRequests.Services.Tests +{ + [TestFixture] + public class NotificationServiceTests + { + + [Test] + public void SubscribeNewNotifier() + { + var notificationMock = new Mock(); + notificationMock.SetupGet(x => x.NotificationName).Returns("Notification1"); + NotificationService.Subscribe(notificationMock.Object); + + Assert.That(NotificationService.Observers["Notification1"], Is.Not.Null); + Assert.That(NotificationService.Observers.Count, Is.EqualTo(1)); + } + + [Test] + public void SubscribeExistingNotifier() + { + var notificationMock1 = new Mock(); + var notificationMock2 = new Mock(); + notificationMock1.SetupGet(x => x.NotificationName).Returns("Notification1"); + notificationMock2.SetupGet(x => x.NotificationName).Returns("Notification1"); + NotificationService.Subscribe(notificationMock1.Object); + + Assert.That(NotificationService.Observers["Notification1"], Is.Not.Null); + Assert.That(NotificationService.Observers.Count, Is.EqualTo(1)); + + NotificationService.Subscribe(notificationMock2.Object); + + Assert.That(NotificationService.Observers["Notification1"], Is.Not.Null); + Assert.That(NotificationService.Observers.Count, Is.EqualTo(1)); + } + + [Test] + public void UnSubscribeMissingNotifier() + { + var notificationMock = new Mock(); + notificationMock.SetupGet(x => x.NotificationName).Returns("Notification1"); + NotificationService.UnSubscribe(notificationMock.Object); + + Assert.That(NotificationService.Observers.Count, Is.EqualTo(0)); + } + + [Test] + public void UnSubscribeNotifier() + { + var notificationMock = new Mock(); + notificationMock.SetupGet(x => x.NotificationName).Returns("Notification1"); + NotificationService.Subscribe(notificationMock.Object); + Assert.That(NotificationService.Observers.Count, Is.EqualTo(1)); + + NotificationService.UnSubscribe(notificationMock.Object); + Assert.That(NotificationService.Observers.Count, Is.EqualTo(0)); + } + + [Test] + public void PublishWithNoObservers() + { + Assert.DoesNotThrow( + () => + { NotificationService.Publish(string.Empty, string.Empty); }); + } + + [Test] + public void PublishAllNotifiers() + { + var notificationMock1 = new Mock(); + var notificationMock2 = new Mock(); + notificationMock1.SetupGet(x => x.NotificationName).Returns("Notification1"); + notificationMock2.SetupGet(x => x.NotificationName).Returns("Notification2"); + NotificationService.Subscribe(notificationMock1.Object); + NotificationService.Subscribe(notificationMock2.Object); + + Assert.That(NotificationService.Observers.Count, Is.EqualTo(2)); + + NotificationService.Publish("a","b"); + + notificationMock1.Verify(x => x.Notify("a","b"), Times.Once); + notificationMock2.Verify(x => x.Notify("a","b"), Times.Once); + } + } +} \ No newline at end of file diff --git a/PlexRequests.Services.Tests/PlexRequests.Services.Tests.csproj b/PlexRequests.Services.Tests/PlexRequests.Services.Tests.csproj index 175b4f0f8..5fc159440 100644 --- a/PlexRequests.Services.Tests/PlexRequests.Services.Tests.csproj +++ b/PlexRequests.Services.Tests/PlexRequests.Services.Tests.csproj @@ -55,6 +55,7 @@ + diff --git a/PlexRequests.Services/Notification/EmailMessageNotification.cs b/PlexRequests.Services/Notification/EmailMessageNotification.cs new file mode 100644 index 000000000..2c5c84158 --- /dev/null +++ b/PlexRequests.Services/Notification/EmailMessageNotification.cs @@ -0,0 +1,108 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: EmailMessageNotification.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion +using System; +using System.Net; +using System.Net.Mail; + +using NLog; + +using PlexRequests.Core; +using PlexRequests.Core.SettingModels; + +namespace PlexRequests.Services.Notification +{ + public class EmailMessageNotification : INotification + { + public EmailMessageNotification(ISettingsService settings) + { + EmailNotificationSettings = settings; + } + + private static Logger Log = LogManager.GetCurrentClassLogger(); + private ISettingsService EmailNotificationSettings { get; } + public string NotificationName => "EmailMessageNotification"; + public bool Notify(string title, string requester) + { + var configuration = GetConfiguration(); + if (!ValidateConfiguration(configuration)) + { + return false; + } + + var message = new MailMessage + { + IsBodyHtml = true, + To = { new MailAddress(configuration.RecipientEmail) }, + Body = $"User {requester} has requested {title}!", + From = new MailAddress(configuration.EmailUsername), + Subject = $"New Request for {title}!" + }; + + try + { + using (var smtp = new SmtpClient(configuration.EmailHost, configuration.EmailPort)) + { + smtp.Credentials = new NetworkCredential(configuration.EmailUsername, configuration.EmailPassword); + smtp.EnableSsl = configuration.Ssl; + smtp.Send(message); + return true; + } + } + catch (SmtpException smtp) + { + Log.Fatal(smtp); + } + catch (Exception e) + { + Log.Fatal(e); + } + return false; + } + + private EmailNotificationSettings GetConfiguration() + { + var settings = EmailNotificationSettings.GetSettings(); + return settings; + } + + private bool ValidateConfiguration(EmailNotificationSettings settings) + { + if (!settings.Enabled) + { + return false; + } + if (string.IsNullOrEmpty(settings.EmailHost) || string.IsNullOrEmpty(settings.EmailUsername) + || string.IsNullOrEmpty(settings.EmailPassword) || string.IsNullOrEmpty(settings.RecipientEmail) + || string.IsNullOrEmpty(settings.EmailPort.ToString())) + { + return false; + } + + return true; + } + } +} \ No newline at end of file diff --git a/PlexRequests.Services/Notification/INotification.cs b/PlexRequests.Services/Notification/INotification.cs new file mode 100644 index 000000000..6b85bd178 --- /dev/null +++ b/PlexRequests.Services/Notification/INotification.cs @@ -0,0 +1,46 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: INotification.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion +namespace PlexRequests.Services.Notification +{ + public interface INotification + { + /// + /// Gets the name of the notification. + /// + /// + /// The name of the notification. + /// + string NotificationName { get; } + /// + /// Notifies the specified title. + /// + /// The title. + /// The requester. + /// + bool Notify(string title, string requester); + } +} \ No newline at end of file diff --git a/PlexRequests.Services/Notification/NotificationService.cs b/PlexRequests.Services/Notification/NotificationService.cs new file mode 100644 index 000000000..4afba12bf --- /dev/null +++ b/PlexRequests.Services/Notification/NotificationService.cs @@ -0,0 +1,78 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: NotificationService.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion +using System.Collections.Generic; +using System.Threading; + +namespace PlexRequests.Services.Notification +{ + public static class NotificationService + { + public static Dictionary Observers { get; } + + static NotificationService() + { + Observers = new Dictionary(); + } + + public static void Publish(string title, string requester) + { + foreach (var observer in Observers) + { + var notification = observer.Value; + + new Thread(() => + { + Thread.CurrentThread.IsBackground = true; + notification.Notify(title, requester); + }).Start(); + } + } + + public static void Subscribe(INotification notification) + { + INotification notificationValue; + if (Observers.TryGetValue(notification.NotificationName, out notificationValue)) + { + // Observer already exists + return; + } + + Observers[notification.NotificationName] = notification; + } + + public static void UnSubscribe(INotification notification) + { + INotification notificationValue; + if (!Observers.TryGetValue(notification.NotificationName, out notificationValue)) + { + // Observer doesn't exists + return; + } + Observers.Remove(notification.NotificationName); + } + } +} \ No newline at end of file diff --git a/PlexRequests.Services/PlexRequests.Services.csproj b/PlexRequests.Services/PlexRequests.Services.csproj index 3a18c5a04..ef5bc886b 100644 --- a/PlexRequests.Services/PlexRequests.Services.csproj +++ b/PlexRequests.Services/PlexRequests.Services.csproj @@ -77,6 +77,9 @@ + + + diff --git a/PlexRequests.UI/Bootstrapper.cs b/PlexRequests.UI/Bootstrapper.cs index 372117541..c6cc21598 100644 --- a/PlexRequests.UI/Bootstrapper.cs +++ b/PlexRequests.UI/Bootstrapper.cs @@ -44,6 +44,7 @@ using PlexRequests.Core.SettingModels; using PlexRequests.Helpers; using PlexRequests.Services; using PlexRequests.Services.Interfaces; +using PlexRequests.Services.Notification; using PlexRequests.Store; using PlexRequests.Store.Repository; using PlexRequests.UI.Jobs; @@ -86,19 +87,20 @@ namespace PlexRequests.UI container.Register(); + + SubscribeAllObservers(container); base.ConfigureRequestContainer(container, context); } protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines) { - TaskManager.TaskFactory = new Jobs.PlexTaskFactory(); + TaskManager.TaskFactory = new PlexTaskFactory(); TaskManager.Initialize(new PlexRegistry()); CookieBasedSessions.Enable(pipelines, CryptographyConfiguration.Default); StaticConfiguration.DisableErrorTraces = false; - base.ApplicationStartup(container, pipelines); // Enable forms auth @@ -109,8 +111,20 @@ namespace PlexRequests.UI }; FormsAuthentication.Enable(pipelines, formsAuthConfiguration); + } + protected override DiagnosticsConfiguration DiagnosticsConfiguration => new DiagnosticsConfiguration { Password = @"password" }; + + private void SubscribeAllObservers(TinyIoCContainer container) + { + var emailSettingsService = container.Resolve>(); + var emailSettings = emailSettingsService.GetSettings(); + if (emailSettings.Enabled) + { + NotificationService.Subscribe(new EmailMessageNotification(emailSettingsService)); + } + } } } \ No newline at end of file diff --git a/PlexRequests.UI/Modules/AdminModule.cs b/PlexRequests.UI/Modules/AdminModule.cs index 69a543708..457922db2 100644 --- a/PlexRequests.UI/Modules/AdminModule.cs +++ b/PlexRequests.UI/Modules/AdminModule.cs @@ -40,6 +40,7 @@ using PlexRequests.Api.Interfaces; using PlexRequests.Core; using PlexRequests.Core.SettingModels; using PlexRequests.Helpers; +using PlexRequests.Services.Notification; using PlexRequests.UI.Helpers; using PlexRequests.UI.Models; @@ -302,8 +303,13 @@ namespace PlexRequests.UI.Modules Log.Trace(settings.DumpJson()); var result = EmailService.SaveSettings(settings); + + NotificationService.Subscribe(new EmailMessageNotification(EmailService)); + Log.Info("Saved email settings, result: {0}", result); - return Context.GetRedirect("~/admin/emailnotification"); + return Response.AsJson(result + ? new JsonResponseModel { Result = true, Message = "Successfully Updated the Settings for Email Notifications!" } + : new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." }); } private Negotiator Status() diff --git a/PlexRequests.UI/Modules/SearchModule.cs b/PlexRequests.UI/Modules/SearchModule.cs index cf9c81531..e96a5e753 100644 --- a/PlexRequests.UI/Modules/SearchModule.cs +++ b/PlexRequests.UI/Modules/SearchModule.cs @@ -37,6 +37,7 @@ using PlexRequests.Core; using PlexRequests.Core.SettingModels; using PlexRequests.Helpers; using PlexRequests.Services.Interfaces; +using PlexRequests.Services.Notification; using PlexRequests.Store; using PlexRequests.UI.Models; @@ -235,7 +236,8 @@ namespace PlexRequests.UI.Modules { Log.Debug("Adding movie to database requests"); var id = RequestService.AddRequest(movieId, model); - //BackgroundJob.Enqueue(() => Checker.CheckAndUpdate(model.Title, (int)id)); + + NotificationService.Publish(model.Title, model.RequestedBy); return Response.AsJson(new JsonResponseModel { Result = true }); } @@ -291,7 +293,6 @@ namespace PlexRequests.UI.Modules LatestTv = latest }; - RequestService.AddRequest(showId, model); var settings = PrService.GetSettings(); if (!settings.RequireApproval) @@ -302,11 +303,20 @@ namespace PlexRequests.UI.Modules var result = SonarrApi.AddSeries(model.ProviderId, model.Title, qualityProfile, sonarrSettings.SeasonFolders, sonarrSettings.RootPath, model.LatestTv, sonarrSettings.ApiKey, sonarrSettings.FullUri); - Log.Info("Added series {0} to Sonarr, Result: {1}", model.Title, result); - Log.Trace("Model sent to Sonarr: "); - Log.Trace(model.DumpJson()); + if (result != null) + { + model.Approved = true; + Log.Debug("Adding tv to database requests (No approval required)"); + RequestService.AddRequest(showId, model); + + return Response.AsJson(new JsonResponseModel { Result = true }); + } + return Response.AsJson(new JsonResponseModel { Result = false, Message = "Something went wrong adding the movie to CouchPotato! Please check your settings." }); } + RequestService.AddRequest(showId, model); + NotificationService.Publish(model.Title, model.RequestedBy); + return Response.AsJson(new { Result = true }); } private string GetTvDbAuthToken(TheTvDbApi api) diff --git a/PlexRequests.UI/Views/Admin/EmailNotifications.cshtml b/PlexRequests.UI/Views/Admin/EmailNotifications.cshtml index 405a96e25..6e0543aea 100644 --- a/PlexRequests.UI/Views/Admin/EmailNotifications.cshtml +++ b/PlexRequests.UI/Views/Admin/EmailNotifications.cshtml @@ -29,6 +29,20 @@ +
+
+ +
+
diff --git a/PlexRequests.UI/Views/Admin/_Sidebar.cshtml b/PlexRequests.UI/Views/Admin/_Sidebar.cshtml index c2b594da8..88726ee09 100644 --- a/PlexRequests.UI/Views/Admin/_Sidebar.cshtml +++ b/PlexRequests.UI/Views/Admin/_Sidebar.cshtml @@ -46,14 +46,14 @@ } @*Sickbeard Settings*@ - @*@if (Context.Request.Path == "/admin/emailnotification") + @if (Context.Request.Path == "/admin/emailnotification") { Email Notifications } else { Email Notifications - }*@ + } @if (Context.Request.Path == "/admin/status") { diff --git a/README.md b/README.md index 000cac51c..4ee857e4d 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,9 @@ [![Gitter](https://badges.gitter.im/tidusjar/PlexRequest.NET.svg)](https://gitter.im/tidusjar/PlexRequests.Net?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![Build status](https://ci.appveyor.com/api/projects/status/hgj8j6lcea7j0yhn?svg=true)](https://ci.appveyor.com/project/tidusjar/requestplex) [![Linux Status](https://travis-ci.org/tidusjar/PlexRequests.Net.svg)](https://travis-ci.org/tidusjar/PlexRequests.Net) +[![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/tidusjar/plexrequests.net.svg)](http://isitmaintained.com/project/tidusjar/plexrequests.net "Average time to resolve an issue") +[![Percentage of issues still open](http://isitmaintained.com/badge/open/tidusjar/plexrequests.net.svg)](http://isitmaintained.com/project/tidusjar/plexrequests.net "Percentage of issues still open") +[![Github All Releases](https://img.shields.io/github/downloads/tidusjar/PlexRequests.net/total.svg)]() This is based off [Plex Requests by lokenx](https://github.com/lokenx/plexrequests-meteor) so big props to that guy! I wanted to write a similar application in .Net!