From 43dbe854a60d31b0746808b2bf58a69cadbcf4c8 Mon Sep 17 00:00:00 2001 From: "Jamie.Rees" Date: Wed, 7 Jun 2017 16:28:17 +0100 Subject: [PATCH] Added Discord notification #865 --- src/Ombi.Api.Discord/DiscordApi.cs | 33 +++++ src/Ombi.Api.Discord/IDiscordApi.cs | 9 ++ .../Models/DiscordWebhookBody.cs | 8 ++ src/Ombi.Api.Discord/Ombi.Api.Discord.csproj | 11 ++ src/Ombi.Api.Radarr/RadarrApi.cs | 2 +- src/Ombi.Api/Api.cs | 31 ++++- src/Ombi.Api/Request.cs | 2 +- src/Ombi.DependencyInjection/IocExtensions.cs | 3 + src/Ombi.Helpers/LoggingEvents.cs | 5 + src/Ombi.Notification.Discord/Class1.cs | 131 ++++++++++++++++++ .../Ombi.Notification.Discord.csproj | 12 ++ .../EmailNotification.cs | 6 +- src/Ombi.Notifications/BaseNotification.cs | 2 +- .../Interfaces/INotificationService.cs | 4 - src/Ombi.Notifications/NotificationService.cs | 54 ++++++-- .../DiscordNotificationSettings.cs | 31 +++++ .../EmailNotificationSettings.cs | 4 +- src/Ombi.sln | 14 ++ 18 files changed, 336 insertions(+), 26 deletions(-) create mode 100644 src/Ombi.Api.Discord/DiscordApi.cs create mode 100644 src/Ombi.Api.Discord/IDiscordApi.cs create mode 100644 src/Ombi.Api.Discord/Models/DiscordWebhookBody.cs create mode 100644 src/Ombi.Api.Discord/Ombi.Api.Discord.csproj create mode 100644 src/Ombi.Notification.Discord/Class1.cs create mode 100644 src/Ombi.Notification.Discord/Ombi.Notification.Discord.csproj create mode 100644 src/Ombi.Settings/Settings/Models/Notifications/DiscordNotificationSettings.cs diff --git a/src/Ombi.Api.Discord/DiscordApi.cs b/src/Ombi.Api.Discord/DiscordApi.cs new file mode 100644 index 000000000..537bf15ef --- /dev/null +++ b/src/Ombi.Api.Discord/DiscordApi.cs @@ -0,0 +1,33 @@ +using System.Net.Http; +using System.Threading.Tasks; +using Ombi.Api.Discord.Models; + +namespace Ombi.Api.Discord +{ + public class DiscordApi : IDiscordApi + { + public DiscordApi() + { + Api = new Api(); + } + + private string Endpoint => "https://discordapp.com/api/"; //webhooks/270828242636636161/lLysOMhJ96AFO1kvev0bSqP-WCZxKUh1UwfubhIcLkpS0DtM3cg4Pgeraw3waoTXbZii + private Api Api { get; } + + public async Task SendMessage(string message, string webhookId, string webhookToken, string username = null) + { + var request = new Request(Endpoint, $"webhooks/{webhookId}/{webhookToken}", HttpMethod.Post); + + var body = new DiscordWebhookBody + { + content = message, + username = username + }; + request.AddJsonBody(body); + + request.AddHeader("Content-Type", "application/json"); + + await Api.Request(request); + } + } +} diff --git a/src/Ombi.Api.Discord/IDiscordApi.cs b/src/Ombi.Api.Discord/IDiscordApi.cs new file mode 100644 index 000000000..2a5375ee2 --- /dev/null +++ b/src/Ombi.Api.Discord/IDiscordApi.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Ombi.Api.Discord +{ + public interface IDiscordApi + { + Task SendMessage(string message, string webhookId, string webhookToken, string username = null); + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Discord/Models/DiscordWebhookBody.cs b/src/Ombi.Api.Discord/Models/DiscordWebhookBody.cs new file mode 100644 index 000000000..a80423a84 --- /dev/null +++ b/src/Ombi.Api.Discord/Models/DiscordWebhookBody.cs @@ -0,0 +1,8 @@ +namespace Ombi.Api.Discord.Models +{ + public class DiscordWebhookBody + { + public string content { get; set; } + public string username { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Discord/Ombi.Api.Discord.csproj b/src/Ombi.Api.Discord/Ombi.Api.Discord.csproj new file mode 100644 index 000000000..a8c3e7a4c --- /dev/null +++ b/src/Ombi.Api.Discord/Ombi.Api.Discord.csproj @@ -0,0 +1,11 @@ + + + + netstandard1.6 + + + + + + + \ No newline at end of file diff --git a/src/Ombi.Api.Radarr/RadarrApi.cs b/src/Ombi.Api.Radarr/RadarrApi.cs index e04c7c8bb..9ea5490cb 100644 --- a/src/Ombi.Api.Radarr/RadarrApi.cs +++ b/src/Ombi.Api.Radarr/RadarrApi.cs @@ -82,7 +82,7 @@ namespace Ombi.Api.Radarr try { - var response = await Api.Request(request); + var response = await Api.RequestContent(request); if (response.Contains("\"message\":")) { var error = JsonConvert.DeserializeObject(response); diff --git a/src/Ombi.Api/Api.cs b/src/Ombi.Api/Api.cs index 248257468..fcf688c26 100644 --- a/src/Ombi.Api/Api.cs +++ b/src/Ombi.Api/Api.cs @@ -60,7 +60,7 @@ namespace Ombi.Api } } - public async Task Request(Request request) + public async Task RequestContent(Request request) { using (var httpClient = new HttpClient()) { @@ -93,5 +93,34 @@ namespace Ombi.Api } } } + + public async Task Request(Request request) + { + using (var httpClient = new HttpClient()) + { + using (var httpRequestMessage = new HttpRequestMessage(request.HttpMethod, request.FullUri)) + { + // Add the Json Body + if (request.JsonBody != null) + { + httpRequestMessage.Content = new JsonContent(request.JsonBody); + } + + // Add headers + foreach (var header in request.Headers) + { + httpRequestMessage.Headers.Add(header.Key, header.Value); + + } + using (var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage)) + { + if (!httpResponseMessage.IsSuccessStatusCode) + { + // Logging + } + } + } + } + } } } diff --git a/src/Ombi.Api/Request.cs b/src/Ombi.Api/Request.cs index c9714f3c4..56ff100eb 100644 --- a/src/Ombi.Api/Request.cs +++ b/src/Ombi.Api/Request.cs @@ -50,7 +50,7 @@ namespace Ombi.Api public List> Headers { get; } = new List>(); public List> ContentHeaders { get; } = new List>(); - public object JsonBody { get; set; } + public object JsonBody { get; private set; } public bool IsValidUrl { diff --git a/src/Ombi.DependencyInjection/IocExtensions.cs b/src/Ombi.DependencyInjection/IocExtensions.cs index 15dc99cd2..f88d41e81 100644 --- a/src/Ombi.DependencyInjection/IocExtensions.cs +++ b/src/Ombi.DependencyInjection/IocExtensions.cs @@ -2,6 +2,8 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.DependencyInjection; + +using Ombi.Api.Discord; using Ombi.Api.Emby; using Ombi.Api.Plex; using Ombi.Api.Radarr; @@ -60,6 +62,7 @@ namespace Ombi.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); } public static void RegisterStore(this IServiceCollection services) diff --git a/src/Ombi.Helpers/LoggingEvents.cs b/src/Ombi.Helpers/LoggingEvents.cs index acab47918..525035c98 100644 --- a/src/Ombi.Helpers/LoggingEvents.cs +++ b/src/Ombi.Helpers/LoggingEvents.cs @@ -6,8 +6,13 @@ namespace Ombi.Helpers { public static EventId ApiException => new EventId(1000); public static EventId RadarrApiException => new EventId(1001); + public static EventId CacherException => new EventId(2000); public static EventId RadarrCacherException => new EventId(2001); + public static EventId MovieSender => new EventId(3000); + + public static EventId Notification => new EventId(4000); + public static EventId DiscordNotification => new EventId(4001); } } \ No newline at end of file diff --git a/src/Ombi.Notification.Discord/Class1.cs b/src/Ombi.Notification.Discord/Class1.cs new file mode 100644 index 000000000..aa693b363 --- /dev/null +++ b/src/Ombi.Notification.Discord/Class1.cs @@ -0,0 +1,131 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Ombi.Api.Discord; +using Ombi.Core.Settings; +using Ombi.Helpers; +using Ombi.Notifications; +using Ombi.Notifications.Models; +using Ombi.Settings.Settings.Models.Notifications; + +namespace Ombi.Notification.Discord +{ + public class DiscordNotification : BaseNotification + { + public DiscordNotification(IDiscordApi api, ISettingsService sn, ILogger log) : base(sn) + { + Api = api; + Logger = log; + } + + public override string NotificationName => "DiscordNotification"; + + private IDiscordApi Api { get; } + private ILogger Logger { get; } + + protected override bool ValidateConfiguration(DiscordNotificationSettings settings) + { + if (!settings.Enabled) + { + return false; + } + if (string.IsNullOrEmpty(settings.WebhookUrl)) + { + return false; + } + try + { + var a = settings.Token; + var b = settings.WebookId; + } + catch (IndexOutOfRangeException) + { + return false; + } + return true; + } + + protected override async Task NewRequest(NotificationModel model, DiscordNotificationSettings settings) + { + var message = $"{model.Title} has been requested by user: {model.User}"; + + var notification = new NotificationMessage + { + Message = message, + }; + await Send(notification, settings); + } + + protected override async Task Issue(NotificationModel model, DiscordNotificationSettings settings) + { + var message = $"A new issue: {model.Body} has been reported by user: {model.User} for the title: {model.Title}"; + var notification = new NotificationMessage + { + Message = message, + }; + await Send(notification, settings); + } + + protected override async Task AddedToRequestQueue(NotificationModel model, DiscordNotificationSettings settings) + { + var message = $"Hello! The user '{model.User}' has requested {model.Title} but it could not be added. This has been added into the requests queue and will keep retrying"; + var notification = new NotificationMessage + { + Message = message, + }; + await Send(notification, settings); + } + + protected override async Task RequestDeclined(NotificationModel model, DiscordNotificationSettings settings) + { + var message = $"Hello! Your request for {model.Title} has been declined, Sorry!"; + var notification = new NotificationMessage + { + Message = message, + }; + await Send(notification, settings); + } + + protected override async Task RequestApproved(NotificationModel model, DiscordNotificationSettings settings) + { + var message = $"Hello! The request for {model.Title} has now been approved!"; + var notification = new NotificationMessage + { + Message = message, + }; + await Send(notification, settings); + } + + protected override async Task AvailableRequest(NotificationModel model, DiscordNotificationSettings settings) + { + var message = $"Hello! The request for {model.Title} is now available!"; + var notification = new NotificationMessage + { + Message = message, + }; + await Send(notification, settings); + } + + protected override async Task Send(NotificationMessage model, DiscordNotificationSettings settings) + { + try + { + await Api.SendMessage(model.Message, settings.WebookId, settings.Token, settings.Username); + } + catch (Exception e) + { + Logger.LogError(LoggingEvents.DiscordNotification, e, "Failed to send Discord Notification"); + } + } + + protected override async Task Test(NotificationModel model, DiscordNotificationSettings settings) + { + var message = $"This is a test from Ombi, if you can see this then we have successfully pushed a notification!"; + var notification = new NotificationMessage + { + Message = message, + }; + await Send(notification, settings); + } + } +} diff --git a/src/Ombi.Notification.Discord/Ombi.Notification.Discord.csproj b/src/Ombi.Notification.Discord/Ombi.Notification.Discord.csproj new file mode 100644 index 000000000..ecacf8bcf --- /dev/null +++ b/src/Ombi.Notification.Discord/Ombi.Notification.Discord.csproj @@ -0,0 +1,12 @@ + + + + netstandard1.6 + + + + + + + + \ No newline at end of file diff --git a/src/Ombi.Notifications.Email/EmailNotification.cs b/src/Ombi.Notifications.Email/EmailNotification.cs index 9f633320c..f5986dc34 100644 --- a/src/Ombi.Notifications.Email/EmailNotification.cs +++ b/src/Ombi.Notifications.Email/EmailNotification.cs @@ -4,9 +4,9 @@ using MailKit.Net.Smtp; using MimeKit; using Ombi.Core.Models.Requests; using Ombi.Core.Settings; -using Ombi.Core.Settings.Models.Notifications; using Ombi.Notifications.Models; using Ombi.Notifications.Templates; +using Ombi.Settings.Settings.Models.Notifications; namespace Ombi.Notifications.Email { @@ -20,6 +20,10 @@ namespace Ombi.Notifications.Email protected override bool ValidateConfiguration(EmailNotificationSettings settings) { + if (!settings.Enabled) + { + return false; + } if (settings.Authentication) { if (string.IsNullOrEmpty(settings.EmailUsername) || string.IsNullOrEmpty(settings.EmailPassword)) diff --git a/src/Ombi.Notifications/BaseNotification.cs b/src/Ombi.Notifications/BaseNotification.cs index 86fd18bcf..fe81ac2c4 100644 --- a/src/Ombi.Notifications/BaseNotification.cs +++ b/src/Ombi.Notifications/BaseNotification.cs @@ -25,7 +25,7 @@ namespace Ombi.Notifications public async Task NotifyAsync(NotificationModel model, Settings.Settings.Models.Settings settings) { if (settings == null) await NotifyAsync(model); - + var notificationSettings = (T)settings; if (!ValidateConfiguration(notificationSettings)) diff --git a/src/Ombi.Notifications/Interfaces/INotificationService.cs b/src/Ombi.Notifications/Interfaces/INotificationService.cs index 9001b299a..35e239764 100644 --- a/src/Ombi.Notifications/Interfaces/INotificationService.cs +++ b/src/Ombi.Notifications/Interfaces/INotificationService.cs @@ -7,12 +7,8 @@ namespace Ombi.Notifications { public interface INotificationService { - ConcurrentDictionary Observers { get; } - Task Publish(NotificationModel model); Task Publish(NotificationModel model, Settings.Settings.Models.Settings settings); Task PublishTest(NotificationModel model, Settings.Settings.Models.Settings settings, INotification type); - void Subscribe(INotification notification); - void UnSubscribe(INotification notification); } } \ No newline at end of file diff --git a/src/Ombi.Notifications/NotificationService.cs b/src/Ombi.Notifications/NotificationService.cs index f73361108..f513f263a 100644 --- a/src/Ombi.Notifications/NotificationService.cs +++ b/src/Ombi.Notifications/NotificationService.cs @@ -1,15 +1,47 @@ using System; -using System.Collections.Concurrent; +using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Threading.Tasks; -using Ombi.Core.Settings.Models; +using Microsoft.Extensions.Logging; +using Ombi.Helpers; using Ombi.Notifications.Models; namespace Ombi.Notifications { public class NotificationService : INotificationService { - public ConcurrentDictionary Observers { get; } = new ConcurrentDictionary(); + public NotificationService(IServiceProvider provider, ILogger log) + { + Log = log; + NotificationAgents = new List(); + + var baseSearchType = typeof(BaseNotification<>).FullName; + + var ass = typeof(NotificationService).GetTypeInfo().Assembly; + + foreach (var ti in ass.DefinedTypes) + { + if (ti?.BaseType?.FullName == baseSearchType) + { + var type = ti?.AsType(); + var ctors = type.GetConstructors(); + var ctor = ctors.FirstOrDefault(); + + var services = new List(); + foreach (var param in ctor.GetParameters()) + { + services.Add(provider.GetService(param.ParameterType)); + } + + var item = Activator.CreateInstance(type, services.ToArray()); + NotificationAgents.Add((INotification)item); + } + } + } + + private List NotificationAgents { get; } + private ILogger Log { get; } /// /// Sends a notification to the user. This one is used in normal notification scenarios @@ -18,7 +50,7 @@ namespace Ombi.Notifications /// public async Task Publish(NotificationModel model) { - var notificationTasks = Observers.Values.Select(notification => NotifyAsync(notification, model)); + var notificationTasks = NotificationAgents.Select(notification => NotifyAsync(notification, model)); await Task.WhenAll(notificationTasks).ConfigureAwait(false); } @@ -31,21 +63,12 @@ namespace Ombi.Notifications /// public async Task Publish(NotificationModel model, Settings.Settings.Models.Settings settings) { - var notificationTasks = Observers.Values.Select(notification => NotifyAsync(notification, model, settings)); + var notificationTasks = NotificationAgents.Select(notification => NotifyAsync(notification, model, settings)); await Task.WhenAll(notificationTasks).ConfigureAwait(false); } - public void Subscribe(INotification notification) - { - Observers.TryAdd(notification.NotificationName, notification); - } - - public void UnSubscribe(INotification notification) - { - Observers.TryRemove(notification.NotificationName, out notification); - } - + private async Task NotifyAsync(INotification notification, NotificationModel model) { try @@ -54,6 +77,7 @@ namespace Ombi.Notifications } catch (Exception ex) { + Log.LogError(LoggingEvents.Notification, ex, "Failed to notify for notification: {@notification}", notification); } } diff --git a/src/Ombi.Settings/Settings/Models/Notifications/DiscordNotificationSettings.cs b/src/Ombi.Settings/Settings/Models/Notifications/DiscordNotificationSettings.cs new file mode 100644 index 000000000..3352d7153 --- /dev/null +++ b/src/Ombi.Settings/Settings/Models/Notifications/DiscordNotificationSettings.cs @@ -0,0 +1,31 @@ +using System; +using Newtonsoft.Json; + +namespace Ombi.Settings.Settings.Models.Notifications +{ + public class DiscordNotificationSettings : Settings + { + public bool Enabled { get; set; } + public string WebhookUrl { get; set; } + public string Username { get; set; } + + [JsonIgnore] + public string WebookId => SplitWebUrl(4); + + [JsonIgnore] + public string Token => SplitWebUrl(5); + + private string SplitWebUrl(int index) + { + if (!WebhookUrl.StartsWith("http", StringComparison.CurrentCulture)) + { + WebhookUrl = "https://" + WebhookUrl; + } + var split = WebhookUrl.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + + return split.Length < index + ? string.Empty + : split[index]; + } + } +} \ No newline at end of file diff --git a/src/Ombi.Settings/Settings/Models/Notifications/EmailNotificationSettings.cs b/src/Ombi.Settings/Settings/Models/Notifications/EmailNotificationSettings.cs index 331475d31..bdcc1dbd9 100644 --- a/src/Ombi.Settings/Settings/Models/Notifications/EmailNotificationSettings.cs +++ b/src/Ombi.Settings/Settings/Models/Notifications/EmailNotificationSettings.cs @@ -1,6 +1,6 @@ -namespace Ombi.Core.Settings.Models.Notifications +namespace Ombi.Settings.Settings.Models.Notifications { - public sealed class EmailNotificationSettings : Ombi.Settings.Settings.Models.Settings + public sealed class EmailNotificationSettings : Settings { public bool Enabled { get; set; } public string EmailHost { get; set; } diff --git a/src/Ombi.sln b/src/Ombi.sln index 8b7c88743..da95c8add 100644 --- a/src/Ombi.sln +++ b/src/Ombi.sln @@ -61,6 +61,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ombi.Core.Tests", "Ombi.Cor EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ombi.Api.Radarr", "Ombi.Api.Radarr\Ombi.Api.Radarr.csproj", "{94D04C1F-E35A-499C-B0A0-9FADEBDF8336}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ombi.Api.Discord", "Ombi.Api.Discord\Ombi.Api.Discord.csproj", "{5AF2B6D2-5CC6-49FE-928A-BA27AF52B194}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ombi.Notification.Discord", "Ombi.Notification.Discord\Ombi.Notification.Discord.csproj", "{D7B05CF2-E6B3-4BE5-8A02-6698CC0DCE9A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -147,6 +151,14 @@ Global {94D04C1F-E35A-499C-B0A0-9FADEBDF8336}.Debug|Any CPU.Build.0 = Debug|Any CPU {94D04C1F-E35A-499C-B0A0-9FADEBDF8336}.Release|Any CPU.ActiveCfg = Release|Any CPU {94D04C1F-E35A-499C-B0A0-9FADEBDF8336}.Release|Any CPU.Build.0 = Release|Any CPU + {5AF2B6D2-5CC6-49FE-928A-BA27AF52B194}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5AF2B6D2-5CC6-49FE-928A-BA27AF52B194}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5AF2B6D2-5CC6-49FE-928A-BA27AF52B194}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5AF2B6D2-5CC6-49FE-928A-BA27AF52B194}.Release|Any CPU.Build.0 = Release|Any CPU + {D7B05CF2-E6B3-4BE5-8A02-6698CC0DCE9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D7B05CF2-E6B3-4BE5-8A02-6698CC0DCE9A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D7B05CF2-E6B3-4BE5-8A02-6698CC0DCE9A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D7B05CF2-E6B3-4BE5-8A02-6698CC0DCE9A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -166,5 +178,7 @@ Global {3880375C-1A7E-4D75-96EC-63B954C42FEA} = {9293CA11-360A-4C20-A674-B9E794431BF5} {FC6A8F7C-9722-4AE4-960D-277ACB0E81CB} = {6F42AB98-9196-44C4-B888-D5E409F415A1} {94D04C1F-E35A-499C-B0A0-9FADEBDF8336} = {9293CA11-360A-4C20-A674-B9E794431BF5} + {5AF2B6D2-5CC6-49FE-928A-BA27AF52B194} = {9293CA11-360A-4C20-A674-B9E794431BF5} + {D7B05CF2-E6B3-4BE5-8A02-6698CC0DCE9A} = {EA30DD15-6280-4687-B370-2956EC2E54E5} EndGlobalSection EndGlobal