diff --git a/README.md b/README.md index e8e269e7f..e9415432d 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ Supported notifications: * Pushover * Mattermost * Telegram +* Webhook ### The difference between Version 3 and 2 diff --git a/src/Ombi.Api.Webhook/IWebhookApi.cs b/src/Ombi.Api.Webhook/IWebhookApi.cs new file mode 100644 index 000000000..6a22a8b02 --- /dev/null +++ b/src/Ombi.Api.Webhook/IWebhookApi.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Ombi.Api.Webhook +{ + public interface IWebhookApi + { + Task PushAsync(string endpoint, string accessToken, IDictionary parameters); + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Webhook/Ombi.Api.Webhook.csproj b/src/Ombi.Api.Webhook/Ombi.Api.Webhook.csproj new file mode 100644 index 000000000..84f215437 --- /dev/null +++ b/src/Ombi.Api.Webhook/Ombi.Api.Webhook.csproj @@ -0,0 +1,15 @@ + + + + netstandard2.1 + 3.0.0.0 + 3.0.0.0 + + + + + + + + + \ No newline at end of file diff --git a/src/Ombi.Api.Webhook/WebhookApi.cs b/src/Ombi.Api.Webhook/WebhookApi.cs new file mode 100644 index 000000000..8b6b35ca0 --- /dev/null +++ b/src/Ombi.Api.Webhook/WebhookApi.cs @@ -0,0 +1,40 @@ +using Newtonsoft.Json.Serialization; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Ombi.Api.Webhook +{ + public class WebhookApi : IWebhookApi + { + private static readonly CamelCasePropertyNamesContractResolver _nameResolver = new CamelCasePropertyNamesContractResolver(); + + public WebhookApi(IApi api) + { + _api = api; + } + + private readonly IApi _api; + + public async Task PushAsync(string baseUrl, string accessToken, IDictionary parameters) + { + var request = new Request("/", baseUrl, HttpMethod.Post); + + if (!string.IsNullOrWhiteSpace(accessToken)) + { + request.AddHeader("Access-Token", accessToken); + } + + var body = parameters.ToDictionary( + x => _nameResolver.GetResolvedPropertyName(x.Key), + x => x.Value + ); + + request.ApplicationJsonContentType(); + request.AddJsonBody(body); + + await _api.Request(request); + } + } +} diff --git a/src/Ombi.Core/Models/Search/SearchViewModel.cs b/src/Ombi.Core/Models/Search/SearchViewModel.cs index c59b5284d..2f951c97e 100644 --- a/src/Ombi.Core/Models/Search/SearchViewModel.cs +++ b/src/Ombi.Core/Models/Search/SearchViewModel.cs @@ -7,6 +7,8 @@ namespace Ombi.Core.Models.Search { public int Id { get; set; } public bool Approved { get; set; } + public bool? Denied { get; set; } + public string DeniedReason { get; set; } public bool Requested { get; set; } public int RequestId { get; set; } public bool Available { get; set; } diff --git a/src/Ombi.Core/Models/UI/WebhookNotificationViewModel.cs b/src/Ombi.Core/Models/UI/WebhookNotificationViewModel.cs new file mode 100644 index 000000000..533de1d6b --- /dev/null +++ b/src/Ombi.Core/Models/UI/WebhookNotificationViewModel.cs @@ -0,0 +1,15 @@ + +using System.Collections.Generic; +using Ombi.Settings.Settings.Models.Notifications; +using Ombi.Store.Entities; + +namespace Ombi.Core.Models.UI +{ + /// + /// The view model for the notification settings page + /// + /// + public class WebhookNotificationViewModel : WebhookSettings + { + } +} diff --git a/src/Ombi.Core/Rule/Rules/Search/ExistingRule.cs b/src/Ombi.Core/Rule/Rules/Search/ExistingRule.cs index 853ad399c..45e74c3af 100644 --- a/src/Ombi.Core/Rule/Rules/Search/ExistingRule.cs +++ b/src/Ombi.Core/Rule/Rules/Search/ExistingRule.cs @@ -34,6 +34,8 @@ namespace Ombi.Core.Rule.Rules.Search obj.Requested = true; obj.RequestId = movieRequests.Id; obj.Approved = movieRequests.Approved; + obj.Denied = movieRequests.Denied ?? false; + obj.DeniedReason = movieRequests.DeniedReason; obj.Available = movieRequests.Available; return Success(); @@ -49,6 +51,7 @@ namespace Ombi.Core.Rule.Rules.Search request.RequestId = tvRequests.Id; request.Requested = true; request.Approved = tvRequests.ChildRequests.Any(x => x.Approved); + request.Denied = tvRequests.ChildRequests.Any(x => x.Denied ?? false); // Let's modify the seasonsrequested to reflect what we have requested... foreach (var season in request.SeasonRequests) @@ -96,7 +99,9 @@ namespace Ombi.Core.Rule.Rules.Search if (albumRequest != null) // Do we already have a request for this? { obj.Requested = true; - obj.RequestId = albumRequest.Id; + obj.RequestId = albumRequest.Id; + obj.Denied = albumRequest.Denied; + obj.DeniedReason = albumRequest.DeniedReason; obj.Approved = albumRequest.Approved; obj.Available = albumRequest.Available; diff --git a/src/Ombi.DependencyInjection/IocExtensions.cs b/src/Ombi.DependencyInjection/IocExtensions.cs index 66e0a05e6..1a36093ec 100644 --- a/src/Ombi.DependencyInjection/IocExtensions.cs +++ b/src/Ombi.DependencyInjection/IocExtensions.cs @@ -34,6 +34,7 @@ using Ombi.Api.FanartTv; using Ombi.Api.Github; using Ombi.Api.Gotify; using Ombi.Api.GroupMe; +using Ombi.Api.Webhook; using Ombi.Api.Lidarr; using Ombi.Api.Mattermost; using Ombi.Api.Notifications; @@ -137,6 +138,7 @@ namespace Ombi.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -192,6 +194,7 @@ namespace Ombi.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/src/Ombi.DependencyInjection/Ombi.DependencyInjection.csproj b/src/Ombi.DependencyInjection/Ombi.DependencyInjection.csproj index 636e96a3c..a7aedbfa6 100644 --- a/src/Ombi.DependencyInjection/Ombi.DependencyInjection.csproj +++ b/src/Ombi.DependencyInjection/Ombi.DependencyInjection.csproj @@ -38,6 +38,7 @@ + diff --git a/src/Ombi.Helpers/LoggingEvents.cs b/src/Ombi.Helpers/LoggingEvents.cs index 7c4bd73f7..a7c61d9d2 100644 --- a/src/Ombi.Helpers/LoggingEvents.cs +++ b/src/Ombi.Helpers/LoggingEvents.cs @@ -34,6 +34,7 @@ namespace Ombi.Helpers public static EventId TelegramNotifcation => new EventId(4006); public static EventId GotifyNotification => new EventId(4007); public static EventId WhatsApp => new EventId(4008); + public static EventId WebhookNotification => new EventId(4009); public static EventId TvSender => new EventId(5000); public static EventId SonarrSender => new EventId(5001); diff --git a/src/Ombi.Helpers/NotificationAgent.cs b/src/Ombi.Helpers/NotificationAgent.cs index 78fe529c0..9b5cc93fc 100644 --- a/src/Ombi.Helpers/NotificationAgent.cs +++ b/src/Ombi.Helpers/NotificationAgent.cs @@ -11,6 +11,7 @@ Mattermost = 6, Mobile = 7, Gotify = 8, - WhatsApp = 9 + Webhook = 9, + WhatsApp = 10 } } \ No newline at end of file diff --git a/src/Ombi.Mapping/Profiles/SettingsProfile.cs b/src/Ombi.Mapping/Profiles/SettingsProfile.cs index f336db5f9..3c27fff7e 100644 --- a/src/Ombi.Mapping/Profiles/SettingsProfile.cs +++ b/src/Ombi.Mapping/Profiles/SettingsProfile.cs @@ -22,6 +22,7 @@ namespace Ombi.Mapping.Profiles CreateMap().ReverseMap(); CreateMap().ReverseMap(); CreateMap().ReverseMap(); + CreateMap().ReverseMap(); } } } \ No newline at end of file diff --git a/src/Ombi.Notifications/Agents/Interfaces/IWebhookNotification.cs b/src/Ombi.Notifications/Agents/Interfaces/IWebhookNotification.cs new file mode 100644 index 000000000..303229878 --- /dev/null +++ b/src/Ombi.Notifications/Agents/Interfaces/IWebhookNotification.cs @@ -0,0 +1,6 @@ +namespace Ombi.Notifications.Agents +{ + public interface IWebhookNotification : INotification + { + } +} \ No newline at end of file diff --git a/src/Ombi.Notifications/Agents/WebhookNotification.cs b/src/Ombi.Notifications/Agents/WebhookNotification.cs new file mode 100644 index 000000000..2339b6abf --- /dev/null +++ b/src/Ombi.Notifications/Agents/WebhookNotification.cs @@ -0,0 +1,123 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Ombi.Api.Webhook; +using Ombi.Core.Settings; +using Ombi.Helpers; +using Ombi.Notifications.Models; +using Ombi.Settings.Settings.Models; +using Ombi.Settings.Settings.Models.Notifications; +using Ombi.Store.Entities; +using Ombi.Store.Repository; +using Ombi.Store.Repository.Requests; + +namespace Ombi.Notifications.Agents +{ + public class WebhookNotification : BaseNotification, IWebhookNotification + { + public WebhookNotification(IWebhookApi api, ISettingsService sn, ILogger log, INotificationTemplatesRepository r, IMovieRequestRepository m, ITvRequestRepository t, + ISettingsService s, IRepository sub, IMusicRequestRepository music, + IRepository userPref) : base(sn, r, m, t, s, log, sub, music, userPref) + { + Api = api; + Logger = log; + } + + public override string NotificationName => "WebhookNotification"; + + private IWebhookApi Api { get; } + private ILogger Logger { get; } + + protected override bool ValidateConfiguration(WebhookSettings settings) + { + return settings.Enabled && !string.IsNullOrEmpty(settings.WebhookUrl); + } + + protected override async Task NewRequest(NotificationOptions model, WebhookSettings settings) + { + await Run(model, settings, NotificationType.NewRequest); + } + + + protected override async Task NewIssue(NotificationOptions model, WebhookSettings settings) + { + await Run(model, settings, NotificationType.Issue); + } + + protected override async Task IssueComment(NotificationOptions model, WebhookSettings settings) + { + await Run(model, settings, NotificationType.IssueComment); + } + + protected override async Task IssueResolved(NotificationOptions model, WebhookSettings settings) + { + await Run(model, settings, NotificationType.IssueResolved); + } + + protected override async Task AddedToRequestQueue(NotificationOptions model, WebhookSettings settings) + { + await Run(model, settings, NotificationType.ItemAddedToFaultQueue); + } + + protected override async Task RequestDeclined(NotificationOptions model, WebhookSettings settings) + { + await Run(model, settings, NotificationType.RequestDeclined); + } + + protected override async Task RequestApproved(NotificationOptions model, WebhookSettings settings) + { + await Run(model, settings, NotificationType.RequestApproved); + } + + protected override async Task AvailableRequest(NotificationOptions model, WebhookSettings settings) + { + await Run(model, settings, NotificationType.RequestAvailable); + } + + protected override async Task Send(NotificationMessage model, WebhookSettings settings) + { + try + { + await Api.PushAsync(settings.WebhookUrl, settings.ApplicationToken, model.Data); + } + catch (Exception e) + { + Logger.LogError(LoggingEvents.WebhookNotification, e, "Failed to send webhook notification"); + } + } + + protected override async Task Test(NotificationOptions model, WebhookSettings settings) + { + var c = new NotificationMessageCurlys(); + + var testData = c.Curlys.ToDictionary(x => x.Key, x => x.Value); + testData[nameof(NotificationType)] = NotificationType.Test.ToString(); + var notification = new NotificationMessage + { + Data = testData, + }; + + await Send(notification, settings); + } + + private async Task Run(NotificationOptions model, WebhookSettings settings, NotificationType type) + { + var parsed = await LoadTemplate(NotificationAgent.Webhook, type, model); + if (parsed.Disabled) + { + Logger.LogInformation($"Template {type} is disabled for {NotificationAgent.Webhook}"); + return; + } + + var notificationData = parsed.Data.ToDictionary(x => x.Key, x => x.Value); + notificationData[nameof(NotificationType)] = type.ToString(); + var notification = new NotificationMessage + { + Data = notificationData, + }; + + await Send(notification, settings); + } + } +} diff --git a/src/Ombi.Notifications/Models/NotificationMessage.cs b/src/Ombi.Notifications/Models/NotificationMessage.cs index f14604d3f..7ffd48e63 100644 --- a/src/Ombi.Notifications/Models/NotificationMessage.cs +++ b/src/Ombi.Notifications/Models/NotificationMessage.cs @@ -9,5 +9,6 @@ namespace Ombi.Notifications.Models public string To { get; set; } public Dictionary Other { get; set; } = new Dictionary(); + public IDictionary Data { get; set; } } } \ No newline at end of file diff --git a/src/Ombi.Notifications/NotificationMessageContent.cs b/src/Ombi.Notifications/NotificationMessageContent.cs index 37f7504e9..901b3bcb2 100644 --- a/src/Ombi.Notifications/NotificationMessageContent.cs +++ b/src/Ombi.Notifications/NotificationMessageContent.cs @@ -1,4 +1,6 @@ -namespace Ombi.Notifications +using System.Collections.Generic; + +namespace Ombi.Notifications { public class NotificationMessageContent { @@ -6,5 +8,6 @@ public string Subject { get; set; } public string Message { get; set; } public string Image { get; set; } + public IReadOnlyDictionary Data { get; set; } } } \ No newline at end of file diff --git a/src/Ombi.Notifications/NotificationMessageCurlys.cs b/src/Ombi.Notifications/NotificationMessageCurlys.cs index bfa8213bc..def74ea79 100644 --- a/src/Ombi.Notifications/NotificationMessageCurlys.cs +++ b/src/Ombi.Notifications/NotificationMessageCurlys.cs @@ -17,7 +17,9 @@ namespace Ombi.Notifications public void Setup(NotificationOptions opts, FullBaseRequest req, CustomizationSettings s, UserNotificationPreferences pref) { LoadIssues(opts); - RequestId = req.Id.ToString(); + + RequestId = req?.Id.ToString(); + string title; if (req == null) { @@ -68,7 +70,8 @@ namespace Ombi.Notifications { LoadIssues(opts); - RequestId = req.Id.ToString(); + RequestId = req?.Id.ToString(); + string title; if (req == null) { @@ -218,6 +221,7 @@ namespace Ombi.Notifications } // User Defined + public string RequestId { get; set; } public string RequestedUser { get; set; } public string UserName { get; set; } public string IssueUser => UserName; @@ -241,7 +245,6 @@ namespace Ombi.Notifications public string UserPreference { get; set; } public string DenyReason { get; set; } public string AvailableDate { get; set; } - public string RequestId { get; set; } // System Defined private string LongDate => DateTime.Now.ToString("D"); @@ -251,6 +254,7 @@ namespace Ombi.Notifications public Dictionary Curlys => new Dictionary { + {nameof(RequestId), RequestId }, {nameof(RequestedUser), RequestedUser }, {nameof(Title), Title }, {nameof(RequestedDate), RequestedDate }, diff --git a/src/Ombi.Notifications/NotificationMessageResolver.cs b/src/Ombi.Notifications/NotificationMessageResolver.cs index 451ef1b55..fe6102eda 100644 --- a/src/Ombi.Notifications/NotificationMessageResolver.cs +++ b/src/Ombi.Notifications/NotificationMessageResolver.cs @@ -47,7 +47,7 @@ namespace Ombi.Notifications body = ReplaceFields(bodyFields, parameters, body); subject = ReplaceFields(subjectFields, parameters, subject); - return new NotificationMessageContent { Message = body ?? string.Empty, Subject = subject ?? string.Empty}; + return new NotificationMessageContent { Message = body ?? string.Empty, Subject = subject ?? string.Empty, Data = parameters }; } public IEnumerable ProcessConditions(IEnumerable conditionalFields, IReadOnlyDictionary parameters) diff --git a/src/Ombi.Notifications/Ombi.Notifications.csproj b/src/Ombi.Notifications/Ombi.Notifications.csproj index c613f5b61..4ab81a8b4 100644 --- a/src/Ombi.Notifications/Ombi.Notifications.csproj +++ b/src/Ombi.Notifications/Ombi.Notifications.csproj @@ -16,6 +16,7 @@ + diff --git a/src/Ombi.Schedule/Jobs/Plex/Models/AvailabilityModel.cs b/src/Ombi.Schedule/Jobs/Plex/Models/AvailabilityModel.cs new file mode 100644 index 000000000..44d018404 --- /dev/null +++ b/src/Ombi.Schedule/Jobs/Plex/Models/AvailabilityModel.cs @@ -0,0 +1,10 @@ +using Ombi.Store.Entities; + +namespace Ombi.Schedule.Jobs.Plex.Models +{ + public class AvailabilityModel + { + public int Id { get; set; } + public string RequestedUser { get; set; } + } +} diff --git a/src/Ombi.Schedule/Jobs/Plex/PlexAvailabilityChecker.cs b/src/Ombi.Schedule/Jobs/Plex/PlexAvailabilityChecker.cs index 4052b173c..de2abafb4 100644 --- a/src/Ombi.Schedule/Jobs/Plex/PlexAvailabilityChecker.cs +++ b/src/Ombi.Schedule/Jobs/Plex/PlexAvailabilityChecker.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR; @@ -8,6 +9,7 @@ using Ombi.Core; using Ombi.Helpers; using Ombi.Hubs; using Ombi.Notifications.Models; +using Ombi.Schedule.Jobs.Plex.Models; using Ombi.Store.Entities; using Ombi.Store.Entities.Requests; using Ombi.Store.Repository; @@ -60,13 +62,13 @@ namespace Ombi.Schedule.Jobs.Plex private Task ProcessTv() { - var tv = _tvRepo.GetChild().Where(x => !x.Available); + var tv = _tvRepo.GetChild().Where(x => !x.Available).AsNoTracking(); return ProcessTv(tv); } private async Task ProcessTv(IQueryable tv) { - var plexEpisodes = _repo.GetAllEpisodes().Include(x => x.Series); + var plexEpisodes = _repo.GetAllEpisodes().Include(x => x.Series).AsNoTracking(); foreach (var child in tv) { @@ -108,6 +110,7 @@ namespace Ombi.Schedule.Jobs.Plex } + var availableEpisode = new List(); foreach (var season in child.SeasonRequests) { foreach (var episode in season.Episodes) @@ -122,20 +125,28 @@ namespace Ombi.Schedule.Jobs.Plex if (foundEp != null) { + availableEpisode.Add(new AvailabilityModel + { + Id = episode.Id + }); episode.Available = true; } } } + //TODO Partial avilability notifications here + foreach(var c in availableEpisode) + { + await _tvRepo.MarkEpisodeAsAvailable(c.Id); + } + // Check to see if all of the episodes in all seasons are available for this request var allAvailable = child.SeasonRequests.All(x => x.Episodes.All(c => c.Available)); if (allAvailable) { _log.LogInformation("[PAC] - Child request {0} is now available, sending notification", $"{child.Title} - {child.Id}"); // We have ful-fulled this request! - child.Available = true; - child.MarkedAsAvailable = DateTime.Now; - + await _tvRepo.MarkChildAsAvailable(child.Id); await _notificationService.Notify(new NotificationOptions { DateTime = DateTime.Now, @@ -153,10 +164,16 @@ namespace Ombi.Schedule.Jobs.Plex private async Task ProcessMovies() { // Get all non available - var movies = _movieRepo.GetAll().Include(x => x.RequestedUser).Where(x => !x.Available); + var movies = _movieRepo.GetAll().Include(x => x.RequestedUser).Where(x => !x.Available).AsNoTracking(); + var itemsForAvailbility = new List(); foreach (var movie in movies) { + if (movie.Available) + { + return; + } + PlexServerContent item = null; if (movie.ImdbId.HasValue()) { @@ -174,24 +191,29 @@ namespace Ombi.Schedule.Jobs.Plex // We don't yet have this continue; } + + _log.LogInformation("[PAC] - Movie request {0} is now available, sending notification", $"{movie.Title} - {movie.Id}"); + itemsForAvailbility.Add(new AvailabilityModel + { + Id = movie.Id, + RequestedUser = movie.RequestedUser != null ? movie.RequestedUser.Email : string.Empty + }); + } - movie.Available = true; - movie.MarkedAsAvailable = DateTime.Now; - item.RequestId = movie.Id; + foreach (var i in itemsForAvailbility) + { + await _movieRepo.MarkAsAvailable(i.Id); - _log.LogInformation("[PAC] - Movie request {0} is now available, sending notification", $"{movie.Title} - {movie.Id}"); await _notificationService.Notify(new NotificationOptions { DateTime = DateTime.Now, NotificationType = NotificationType.RequestAvailable, - RequestId = movie.Id, + RequestId = i.Id, RequestType = RequestType.Movie, - Recipient = movie.RequestedUser != null ? movie.RequestedUser.Email : string.Empty + Recipient = i.RequestedUser }); - } - await _movieRepo.Save(); await _repo.SaveChangesAsync(); } diff --git a/src/Ombi.Schedule/Jobs/Plex/PlexEpisodeSync.cs b/src/Ombi.Schedule/Jobs/Plex/PlexEpisodeSync.cs index e75ca1b4b..90a6706c2 100644 --- a/src/Ombi.Schedule/Jobs/Plex/PlexEpisodeSync.cs +++ b/src/Ombi.Schedule/Jobs/Plex/PlexEpisodeSync.cs @@ -235,4 +235,4 @@ namespace Ombi.Schedule.Jobs.Plex GC.SuppressFinalize(this); } } -} \ No newline at end of file +} diff --git a/src/Ombi.Settings/Settings/Models/Notifications/WebhookSettings.cs b/src/Ombi.Settings/Settings/Models/Notifications/WebhookSettings.cs new file mode 100644 index 000000000..1c304e84c --- /dev/null +++ b/src/Ombi.Settings/Settings/Models/Notifications/WebhookSettings.cs @@ -0,0 +1,9 @@ +namespace Ombi.Settings.Settings.Models.Notifications +{ + public class WebhookSettings : Settings + { + public bool Enabled { get; set; } + public string WebhookUrl { get; set; } + public string ApplicationToken { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Store/Context/OmbiContext.cs b/src/Ombi.Store/Context/OmbiContext.cs index 4694f1151..2b145c7f2 100644 --- a/src/Ombi.Store/Context/OmbiContext.cs +++ b/src/Ombi.Store/Context/OmbiContext.cs @@ -7,6 +7,7 @@ using Microsoft.EntityFrameworkCore; using Ombi.Helpers; using Ombi.Store.Entities; using Ombi.Store.Entities.Requests; +using Ombi.Store.Repository.Requests; namespace Ombi.Store.Context { @@ -35,6 +36,7 @@ namespace Ombi.Store.Context public DbSet AlbumRequests { get; set; } public DbSet TvRequests { get; set; } public DbSet ChildRequests { get; set; } + public DbSet EpisodeRequests { get; set; } public DbSet Issues { get; set; } public DbSet IssueCategories { get; set; } diff --git a/src/Ombi.Store/Repository/Requests/IMovieRequestRepository.cs b/src/Ombi.Store/Repository/Requests/IMovieRequestRepository.cs index 275937360..5c44b2d6c 100644 --- a/src/Ombi.Store/Repository/Requests/IMovieRequestRepository.cs +++ b/src/Ombi.Store/Repository/Requests/IMovieRequestRepository.cs @@ -10,6 +10,7 @@ namespace Ombi.Store.Repository.Requests MovieRequests GetRequest(int theMovieDbId); Task Update(MovieRequests request); Task Save(); + Task MarkAsAvailable(int id); IQueryable GetWithUser(); IQueryable GetWithUser(string userId); IQueryable GetAll(string userId); diff --git a/src/Ombi.Store/Repository/Requests/ITvRequestRepository.cs b/src/Ombi.Store/Repository/Requests/ITvRequestRepository.cs index 1c6e4d302..56fdb6fba 100644 --- a/src/Ombi.Store/Repository/Requests/ITvRequestRepository.cs +++ b/src/Ombi.Store/Repository/Requests/ITvRequestRepository.cs @@ -23,6 +23,8 @@ namespace Ombi.Store.Repository.Requests Task UpdateChild(ChildRequests request); IQueryable GetChild(); IQueryable GetChild(string userId); + Task MarkEpisodeAsAvailable(int id); + Task MarkChildAsAvailable(int id); Task Save(); Task DeleteChildRange(IEnumerable request); } diff --git a/src/Ombi.Store/Repository/Requests/MovieRequestRepository.cs b/src/Ombi.Store/Repository/Requests/MovieRequestRepository.cs index 2df4cac0f..bbf52c7f0 100644 --- a/src/Ombi.Store/Repository/Requests/MovieRequestRepository.cs +++ b/src/Ombi.Store/Repository/Requests/MovieRequestRepository.cs @@ -54,6 +54,14 @@ namespace Ombi.Store.Repository.Requests .AsQueryable(); } + public async Task MarkAsAvailable(int id) + { + var movieRequest = new MovieRequests{ Id = id, Available = true, MarkedAsAvailable = DateTime.UtcNow}; + var attached = Db.MovieRequests.Attach(movieRequest); + attached.Property(x => x.Available).IsModified = true; + attached.Property(x => x.MarkedAsAvailable).IsModified = true; + await Db.SaveChangesAsync(); + } public IQueryable GetWithUser(string userId) { diff --git a/src/Ombi.Store/Repository/Requests/TvRequestRepository.cs b/src/Ombi.Store/Repository/Requests/TvRequestRepository.cs index 8c6fc2461..985f464fd 100644 --- a/src/Ombi.Store/Repository/Requests/TvRequestRepository.cs +++ b/src/Ombi.Store/Repository/Requests/TvRequestRepository.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; @@ -100,6 +101,23 @@ namespace Ombi.Store.Repository.Requests .AsQueryable(); } + public async Task MarkChildAsAvailable(int id) + { + var request = new ChildRequests { Id = id, Available = true, MarkedAsAvailable = DateTime.UtcNow }; + var attached = Db.ChildRequests.Attach(request); + attached.Property(x => x.Available).IsModified = true; + attached.Property(x => x.MarkedAsAvailable).IsModified = true; + await Db.SaveChangesAsync(); + } + + public async Task MarkEpisodeAsAvailable(int id) + { + var request = new EpisodeRequests { Id = id, Available = true }; + var attached = Db.EpisodeRequests.Attach(request); + attached.Property(x => x.Available).IsModified = true; + await Db.SaveChangesAsync(); + } + public async Task Save() { await InternalSaveChanges(); @@ -128,10 +146,10 @@ namespace Ombi.Store.Repository.Requests public async Task Update(TvRequests request) { Db.Update(request); - + await InternalSaveChanges(); } - + public async Task UpdateChild(ChildRequests request) { Db.Update(request); diff --git a/src/Ombi.sln b/src/Ombi.sln index 78d3898ac..57d4962d9 100644 --- a/src/Ombi.sln +++ b/src/Ombi.sln @@ -112,6 +112,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Api.MusicBrainz", "Omb EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ombi.Api.Twilio", "Ombi.Api.Twilio\Ombi.Api.Twilio.csproj", "{34E5DD1A-6A90-448B-9E71-64D1ACD6C1A3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ombi.Api.Webhook", "Ombi.Api.Webhook\Ombi.Api.Webhook.csproj", "{E2186FDA-D827-4781-8663-130AC382F12C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -298,6 +300,10 @@ Global {34E5DD1A-6A90-448B-9E71-64D1ACD6C1A3}.Debug|Any CPU.Build.0 = Debug|Any CPU {34E5DD1A-6A90-448B-9E71-64D1ACD6C1A3}.Release|Any CPU.ActiveCfg = Release|Any CPU {34E5DD1A-6A90-448B-9E71-64D1ACD6C1A3}.Release|Any CPU.Build.0 = Release|Any CPU + {E2186FDA-D827-4781-8663-130AC382F12C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E2186FDA-D827-4781-8663-130AC382F12C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E2186FDA-D827-4781-8663-130AC382F12C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E2186FDA-D827-4781-8663-130AC382F12C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -336,6 +342,7 @@ Global {4FA21A20-92F4-462C-B929-2C517A88CC56} = {9293CA11-360A-4C20-A674-B9E794431BF5} {CC8CEFCD-0CB6-45BB-845F-508BCAB5BDC3} = {6F42AB98-9196-44C4-B888-D5E409F415A1} {105EA346-766E-45B8-928B-DE6991DCB7EB} = {9293CA11-360A-4C20-A674-B9E794431BF5} + {E2186FDA-D827-4781-8663-130AC382F12C} = {9293CA11-360A-4C20-A674-B9E794431BF5} {F3969B69-3B07-4884-A7AB-0BAB8B84DF94} = {6F42AB98-9196-44C4-B888-D5E409F415A1} {27111E7C-748E-4996-BD71-2117027C6460} = {6F42AB98-9196-44C4-B888-D5E409F415A1} {9266403C-B04D-4C0F-AC39-82F12C781949} = {9293CA11-360A-4C20-A674-B9E794431BF5} diff --git a/src/Ombi/ClientApp/app/settings/notifications/webhook.component.html b/src/Ombi/ClientApp/app/settings/notifications/webhook.component.html new file mode 100644 index 000000000..ed6eb43b9 --- /dev/null +++ b/src/Ombi/ClientApp/app/settings/notifications/webhook.component.html @@ -0,0 +1,49 @@ + + +
+
+ Webhook Notifications +
+
+ +
+
+ + +
+
+ +
+ + + + The Webhook URL is required +
+ +
+ + + + The Application Token is required +
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
\ No newline at end of file diff --git a/src/Ombi/ClientApp/app/settings/notifications/webhook.component.ts b/src/Ombi/ClientApp/app/settings/notifications/webhook.component.ts new file mode 100644 index 000000000..d410b2b44 --- /dev/null +++ b/src/Ombi/ClientApp/app/settings/notifications/webhook.component.ts @@ -0,0 +1,64 @@ +import { Component, OnInit } from "@angular/core"; +import { FormBuilder, FormGroup, Validators } from "@angular/forms"; + +import { INotificationTemplates, IWebhookNotificationSettings, NotificationType } from "../../interfaces"; +import { TesterService } from "../../services"; +import { NotificationService } from "../../services"; +import { SettingsService } from "../../services"; + +@Component({ + templateUrl: "./webhook.component.html", +}) +export class WebhookComponent implements OnInit { + public NotificationType = NotificationType; + public templates: INotificationTemplates[]; + public form: FormGroup; + + constructor(private settingsService: SettingsService, + private notificationService: NotificationService, + private fb: FormBuilder, + private testerService: TesterService) { } + + public ngOnInit() { + this.settingsService.getWebhookNotificationSettings().subscribe(x => { + this.form = this.fb.group({ + enabled: [x.enabled], + webhookUrl: [x.webhookUrl, [Validators.required]], + applicationToken: [x.applicationToken], + }); + }); + } + + public onSubmit(form: FormGroup) { + if (form.invalid) { + this.notificationService.error("Please check your entered values"); + return; + } + + const settings = form.value; + + this.settingsService.saveWebhookNotificationSettings(settings).subscribe(x => { + if (x) { + this.notificationService.success("Successfully saved the Webhook settings"); + } else { + this.notificationService.success("There was an error when saving the Webhook settings"); + } + }); + + } + + public test(form: FormGroup) { + if (form.invalid) { + this.notificationService.error("Please check your entered values"); + return; + } + + this.testerService.webhookTest(form.value).subscribe(x => { + if (x) { + this.notificationService.success("Successfully sent a Webhook message"); + } else { + this.notificationService.error("There was an error when sending the Webhook message. Please check your settings"); + } + }); + } +} diff --git a/src/Ombi/ClientApp/src/app/interfaces/INotificationSettings.ts b/src/Ombi/ClientApp/src/app/interfaces/INotificationSettings.ts index a6c14ae3b..6a96a55e1 100644 --- a/src/Ombi/ClientApp/src/app/interfaces/INotificationSettings.ts +++ b/src/Ombi/ClientApp/src/app/interfaces/INotificationSettings.ts @@ -119,6 +119,11 @@ export interface IGotifyNotificationSettings extends INotificationSettings { priority: number; } +export interface IWebhookNotificationSettings extends INotificationSettings { + webhookUrl: string; + applicationToken: string; +} + export interface IMattermostNotifcationSettings extends INotificationSettings { webhookUrl: string; username: string; diff --git a/src/Ombi/ClientApp/src/app/services/applications/tester.service.ts b/src/Ombi/ClientApp/src/app/services/applications/tester.service.ts index 6c1d9ff6c..ad663da4f 100644 --- a/src/Ombi/ClientApp/src/app/services/applications/tester.service.ts +++ b/src/Ombi/ClientApp/src/app/services/applications/tester.service.ts @@ -24,6 +24,7 @@ import { ISlackNotificationSettings, ISonarrSettings, ITelegramNotifcationSettings, + IWebhookNotificationSettings, IWhatsAppSettings, } from "../../interfaces"; @@ -49,6 +50,10 @@ export class TesterService extends ServiceHelpers { return this.http.post(`${this.url}gotify`, JSON.stringify(settings), { headers: this.headers }); } + public webhookTest(settings: IWebhookNotificationSettings): Observable { + return this.http.post(`${this.url}webhook`, JSON.stringify(settings), { headers: this.headers }); + } + public mattermostTest(settings: IMattermostNotifcationSettings): Observable { return this.http.post(`${this.url}mattermost`, JSON.stringify(settings), {headers: this.headers}); } diff --git a/src/Ombi/ClientApp/src/app/services/settings.service.ts b/src/Ombi/ClientApp/src/app/services/settings.service.ts index 4eec29081..2d32bbcba 100644 --- a/src/Ombi/ClientApp/src/app/services/settings.service.ts +++ b/src/Ombi/ClientApp/src/app/services/settings.service.ts @@ -37,6 +37,7 @@ import { IUserManagementSettings, IVoteSettings, ITwilioSettings, + IWebhookNotificationSettings, } from "../interfaces"; import { ServiceHelpers } from "./service.helpers"; @@ -193,6 +194,14 @@ export class SettingsService extends ServiceHelpers { .post(`${this.url}/notifications/gotify`, JSON.stringify(settings), { headers: this.headers }); } + public getWebhookNotificationSettings(): Observable { + return this.http.get(`${this.url}/notifications/webhook`, { headers: this.headers }); + } + public saveWebhookNotificationSettings(settings: IWebhookNotificationSettings): Observable { + return this.http + .post(`${this.url}/notifications/webhook`, JSON.stringify(settings), { headers: this.headers }); + } + public getSlackNotificationSettings(): Observable { return this.http.get(`${this.url}/notifications/slack`, {headers: this.headers}); } diff --git a/src/Ombi/ClientApp/src/app/settings/settings.module.ts b/src/Ombi/ClientApp/src/app/settings/settings.module.ts index bdf0596ac..55cc17015 100644 --- a/src/Ombi/ClientApp/src/app/settings/settings.module.ts +++ b/src/Ombi/ClientApp/src/app/settings/settings.module.ts @@ -37,6 +37,7 @@ import { PushbulletComponent } from "./notifications/pushbullet.component"; import { PushoverComponent } from "./notifications/pushover.component"; import { SlackComponent } from "./notifications/slack.component"; import { TelegramComponent } from "./notifications/telegram.component"; +import { WebhookComponent } from "./notifications/webhook.component"; import { OmbiComponent } from "./ombi/ombi.component"; import { PlexComponent } from "./plex/plex.component"; import { RadarrComponent } from "./radarr/radarr.component"; @@ -73,6 +74,7 @@ const routes: Routes = [ { path: "Pushover", component: PushoverComponent, canActivate: [AuthGuard] }, { path: "Pushbullet", component: PushbulletComponent, canActivate: [AuthGuard] }, { path: "Gotify", component: GotifyComponent, canActivate: [AuthGuard] }, + { path: "Webhook", component: WebhookComponent, canActivate: [AuthGuard] }, { path: "Mattermost", component: MattermostComponent, canActivate: [AuthGuard] }, { path: "Twilio", component: TwilioComponent, canActivate: [AuthGuard] }, { path: "UserManagement", component: UserManagementComponent, canActivate: [AuthGuard] }, @@ -134,6 +136,7 @@ const routes: Routes = [ MattermostComponent, PushbulletComponent, GotifyComponent, + WebhookComponent, UserManagementComponent, UpdateComponent, AboutComponent, diff --git a/src/Ombi/Controllers/V1/External/TesterController.cs b/src/Ombi/Controllers/V1/External/TesterController.cs index 969e619ea..0f4fdd69a 100644 --- a/src/Ombi/Controllers/V1/External/TesterController.cs +++ b/src/Ombi/Controllers/V1/External/TesterController.cs @@ -44,7 +44,7 @@ namespace Ombi.Controllers.V1.External IPushbulletNotification pushbullet, ISlackNotification slack, IPushoverNotification po, IMattermostNotification mm, IPlexApi plex, IEmbyApi emby, IRadarrApi radarr, ISonarrApi sonarr, ILogger log, IEmailProvider provider, ICouchPotatoApi cpApi, ITelegramNotification telegram, ISickRageApi srApi, INewsletterJob newsletter, IMobileNotification mobileNotification, - ILidarrApi lidarrApi, IGotifyNotification gotifyNotification, IWhatsAppApi whatsAppApi, OmbiUserManager um) + ILidarrApi lidarrApi, IGotifyNotification gotifyNotification, IWhatsAppApi whatsAppApi, OmbiUserManager um, IWebhookNotification webhookNotification) { Service = service; DiscordNotification = notification; @@ -68,6 +68,7 @@ namespace Ombi.Controllers.V1.External GotifyNotification = gotifyNotification; WhatsAppApi = whatsAppApi; UserManager = um; + WebhookNotification = webhookNotification; } private INotificationService Service { get; } @@ -77,6 +78,7 @@ namespace Ombi.Controllers.V1.External private ISlackNotification SlackNotification { get; } private IPushoverNotification PushoverNotification { get; } private IGotifyNotification GotifyNotification { get; } + private IWebhookNotification WebhookNotification { get; } private IMattermostNotification MattermostNotification { get; } private IPlexApi PlexApi { get; } private IRadarrApi RadarrApi { get; } @@ -188,6 +190,30 @@ namespace Ombi.Controllers.V1.External } + /// + /// Sends a test message to configured webhook using the provided settings + /// + /// The settings. + /// + [HttpPost("webhook")] + public bool Webhook([FromBody] WebhookSettings settings) + { + try + { + settings.Enabled = true; + WebhookNotification.NotifyAsync( + new NotificationOptions { NotificationType = NotificationType.Test, RequestId = -1 }, settings); + + return true; + } + catch (Exception e) + { + Log.LogError(LoggingEvents.Api, e, "Could not test your webhook"); + return false; + } + + } + /// /// Sends a test message to mattermost using the provided settings /// diff --git a/src/Ombi/Controllers/V1/SettingsController.cs b/src/Ombi/Controllers/V1/SettingsController.cs index ea65e63cb..d3a3a5c4c 100644 --- a/src/Ombi/Controllers/V1/SettingsController.cs +++ b/src/Ombi/Controllers/V1/SettingsController.cs @@ -1069,6 +1069,33 @@ namespace Ombi.Controllers.V1 return model; } + /// + /// Saves the webhook notification settings. + /// + /// The model. + /// + [HttpPost("notifications/webhook")] + public async Task WebhookNotificationSettings([FromBody] WebhookNotificationViewModel model) + { + var settings = Mapper.Map(model); + var result = await Save(settings); + + return result; + } + + /// + /// Gets the webhook notification settings. + /// + /// + [HttpGet("notifications/webhook")] + public async Task WebhookNotificationSettings() + { + var settings = await Get(); + var model = Mapper.Map(settings); + + return model; + } + /// /// Saves the Newsletter notification settings. /// diff --git a/src/Ombi/wwwroot/translations/hu.json b/src/Ombi/wwwroot/translations/hu.json index 1414f0ea0..050c21e64 100644 --- a/src/Ombi/wwwroot/translations/hu.json +++ b/src/Ombi/wwwroot/translations/hu.json @@ -75,7 +75,7 @@ "RequestAdded": "Kérés sikeresen leadva erre: {{title}}", "Similar": "Hasonló", "Refine": "Finomítás", - "SearchBarPlaceholder": "Type Here to Search", + "SearchBarPlaceholder": "A kereséshez írj be valamit", "Movies": { "PopularMovies": "Népszerű filmek", "UpcomingMovies": "Közelgő filmek", diff --git a/src/Ombi/wwwroot/translations/ru.json b/src/Ombi/wwwroot/translations/ru.json index 092689584..1657fdcff 100644 --- a/src/Ombi/wwwroot/translations/ru.json +++ b/src/Ombi/wwwroot/translations/ru.json @@ -3,7 +3,7 @@ "SignInButton": "Войти", "UsernamePlaceholder": "Имя пользователя", "PasswordPlaceholder": "Пароль", - "RememberMe": "Запомнить Меня", + "RememberMe": "Запомнить меня", "ForgottenPassword": "Забыли пароль?", "Errors": { "IncorrectCredentials": "Неверное имя пользователя или пароль"