This commit is contained in:
Jamie Rees 2020-02-18 22:03:54 +00:00
commit ac51126437
39 changed files with 529 additions and 29 deletions

View file

@ -74,6 +74,7 @@ Supported notifications:
* Pushover * Pushover
* Mattermost * Mattermost
* Telegram * Telegram
* Webhook
### The difference between Version 3 and 2 ### The difference between Version 3 and 2

View file

@ -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<string, string> parameters);
}
}

View file

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<AssemblyVersion>3.0.0.0</AssemblyVersion>
<FileVersion>3.0.0.0</FileVersion>
<Version></Version>
<PackageVersion></PackageVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Ombi.Api\Ombi.Api.csproj" />
</ItemGroup>
</Project>

View file

@ -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<string, string> 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);
}
}
}

View file

@ -7,6 +7,8 @@ namespace Ombi.Core.Models.Search
{ {
public int Id { get; set; } public int Id { get; set; }
public bool Approved { get; set; } public bool Approved { get; set; }
public bool? Denied { get; set; }
public string DeniedReason { get; set; }
public bool Requested { get; set; } public bool Requested { get; set; }
public int RequestId { get; set; } public int RequestId { get; set; }
public bool Available { get; set; } public bool Available { get; set; }

View file

@ -0,0 +1,15 @@

using System.Collections.Generic;
using Ombi.Settings.Settings.Models.Notifications;
using Ombi.Store.Entities;
namespace Ombi.Core.Models.UI
{
/// <summary>
/// The view model for the notification settings page
/// </summary>
/// <seealso cref="WebhookSettings" />
public class WebhookNotificationViewModel : WebhookSettings
{
}
}

View file

@ -34,6 +34,8 @@ namespace Ombi.Core.Rule.Rules.Search
obj.Requested = true; obj.Requested = true;
obj.RequestId = movieRequests.Id; obj.RequestId = movieRequests.Id;
obj.Approved = movieRequests.Approved; obj.Approved = movieRequests.Approved;
obj.Denied = movieRequests.Denied ?? false;
obj.DeniedReason = movieRequests.DeniedReason;
obj.Available = movieRequests.Available; obj.Available = movieRequests.Available;
return Success(); return Success();
@ -49,6 +51,7 @@ namespace Ombi.Core.Rule.Rules.Search
request.RequestId = tvRequests.Id; request.RequestId = tvRequests.Id;
request.Requested = true; request.Requested = true;
request.Approved = tvRequests.ChildRequests.Any(x => x.Approved); 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... // Let's modify the seasonsrequested to reflect what we have requested...
foreach (var season in request.SeasonRequests) 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? if (albumRequest != null) // Do we already have a request for this?
{ {
obj.Requested = true; obj.Requested = true;
obj.RequestId = albumRequest.Id; obj.RequestId = albumRequest.Id;
obj.Denied = albumRequest.Denied;
obj.DeniedReason = albumRequest.DeniedReason;
obj.Approved = albumRequest.Approved; obj.Approved = albumRequest.Approved;
obj.Available = albumRequest.Available; obj.Available = albumRequest.Available;

View file

@ -34,6 +34,7 @@ using Ombi.Api.FanartTv;
using Ombi.Api.Github; using Ombi.Api.Github;
using Ombi.Api.Gotify; using Ombi.Api.Gotify;
using Ombi.Api.GroupMe; using Ombi.Api.GroupMe;
using Ombi.Api.Webhook;
using Ombi.Api.Lidarr; using Ombi.Api.Lidarr;
using Ombi.Api.Mattermost; using Ombi.Api.Mattermost;
using Ombi.Api.Notifications; using Ombi.Api.Notifications;
@ -137,6 +138,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<IFanartTvApi, FanartTvApi>(); services.AddTransient<IFanartTvApi, FanartTvApi>();
services.AddTransient<IPushoverApi, PushoverApi>(); services.AddTransient<IPushoverApi, PushoverApi>();
services.AddTransient<IGotifyApi, GotifyApi>(); services.AddTransient<IGotifyApi, GotifyApi>();
services.AddTransient<IWebhookApi, WebhookApi>();
services.AddTransient<IMattermostApi, MattermostApi>(); services.AddTransient<IMattermostApi, MattermostApi>();
services.AddTransient<ICouchPotatoApi, CouchPotatoApi>(); services.AddTransient<ICouchPotatoApi, CouchPotatoApi>();
services.AddTransient<IDogNzbApi, DogNzbApi>(); services.AddTransient<IDogNzbApi, DogNzbApi>();
@ -192,6 +194,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<IMattermostNotification, MattermostNotification>(); services.AddTransient<IMattermostNotification, MattermostNotification>();
services.AddTransient<IPushoverNotification, PushoverNotification>(); services.AddTransient<IPushoverNotification, PushoverNotification>();
services.AddTransient<IGotifyNotification, GotifyNotification>(); services.AddTransient<IGotifyNotification, GotifyNotification>();
services.AddTransient<IWebhookNotification, WebhookNotification>();
services.AddTransient<ITelegramNotification, TelegramNotification>(); services.AddTransient<ITelegramNotification, TelegramNotification>();
services.AddTransient<IMobileNotification, MobileNotification>(); services.AddTransient<IMobileNotification, MobileNotification>();
services.AddTransient<IChangeLogProcessor, ChangeLogProcessor>(); services.AddTransient<IChangeLogProcessor, ChangeLogProcessor>();

View file

@ -38,6 +38,7 @@
<ProjectReference Include="..\Ombi.Api.Trakt\Ombi.Api.Trakt.csproj" /> <ProjectReference Include="..\Ombi.Api.Trakt\Ombi.Api.Trakt.csproj" />
<ProjectReference Include="..\Ombi.Api.TvMaze\Ombi.Api.TvMaze.csproj" /> <ProjectReference Include="..\Ombi.Api.TvMaze\Ombi.Api.TvMaze.csproj" />
<ProjectReference Include="..\Ombi.Api.Twilio\Ombi.Api.Twilio.csproj" /> <ProjectReference Include="..\Ombi.Api.Twilio\Ombi.Api.Twilio.csproj" />
<ProjectReference Include="..\Ombi.Api.Webhook\Ombi.Api.Webhook.csproj" />
<ProjectReference Include="..\Ombi.Core\Ombi.Core.csproj" /> <ProjectReference Include="..\Ombi.Core\Ombi.Core.csproj" />
<ProjectReference Include="..\Ombi.Notifications\Ombi.Notifications.csproj" /> <ProjectReference Include="..\Ombi.Notifications\Ombi.Notifications.csproj" />
<ProjectReference Include="..\Ombi.Schedule\Ombi.Schedule.csproj" /> <ProjectReference Include="..\Ombi.Schedule\Ombi.Schedule.csproj" />

View file

@ -34,6 +34,7 @@ namespace Ombi.Helpers
public static EventId TelegramNotifcation => new EventId(4006); public static EventId TelegramNotifcation => new EventId(4006);
public static EventId GotifyNotification => new EventId(4007); public static EventId GotifyNotification => new EventId(4007);
public static EventId WhatsApp => new EventId(4008); public static EventId WhatsApp => new EventId(4008);
public static EventId WebhookNotification => new EventId(4009);
public static EventId TvSender => new EventId(5000); public static EventId TvSender => new EventId(5000);
public static EventId SonarrSender => new EventId(5001); public static EventId SonarrSender => new EventId(5001);

View file

@ -11,6 +11,7 @@
Mattermost = 6, Mattermost = 6,
Mobile = 7, Mobile = 7,
Gotify = 8, Gotify = 8,
WhatsApp = 9 Webhook = 9,
WhatsApp = 10
} }
} }

View file

@ -22,6 +22,7 @@ namespace Ombi.Mapping.Profiles
CreateMap<GotifyNotificationViewModel, GotifySettings>().ReverseMap(); CreateMap<GotifyNotificationViewModel, GotifySettings>().ReverseMap();
CreateMap<WhatsAppSettingsViewModel, WhatsAppSettings>().ReverseMap(); CreateMap<WhatsAppSettingsViewModel, WhatsAppSettings>().ReverseMap();
CreateMap<TwilioSettingsViewModel, TwilioSettings>().ReverseMap(); CreateMap<TwilioSettingsViewModel, TwilioSettings>().ReverseMap();
CreateMap<WebhookNotificationViewModel, WebhookSettings>().ReverseMap();
} }
} }
} }

View file

@ -0,0 +1,6 @@
namespace Ombi.Notifications.Agents
{
public interface IWebhookNotification : INotification
{
}
}

View file

@ -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<WebhookSettings>, IWebhookNotification
{
public WebhookNotification(IWebhookApi api, ISettingsService<WebhookSettings> sn, ILogger<WebhookNotification> log, INotificationTemplatesRepository r, IMovieRequestRepository m, ITvRequestRepository t,
ISettingsService<CustomizationSettings> s, IRepository<RequestSubscription> sub, IMusicRequestRepository music,
IRepository<UserNotificationPreferences> 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<WebhookNotification> 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);
}
}
}

View file

@ -9,5 +9,6 @@ namespace Ombi.Notifications.Models
public string To { get; set; } public string To { get; set; }
public Dictionary<string, string> Other { get; set; } = new Dictionary<string, string>(); public Dictionary<string, string> Other { get; set; } = new Dictionary<string, string>();
public IDictionary<string, string> Data { get; set; }
} }
} }

View file

@ -1,4 +1,6 @@
namespace Ombi.Notifications using System.Collections.Generic;
namespace Ombi.Notifications
{ {
public class NotificationMessageContent public class NotificationMessageContent
{ {
@ -6,5 +8,6 @@
public string Subject { get; set; } public string Subject { get; set; }
public string Message { get; set; } public string Message { get; set; }
public string Image { get; set; } public string Image { get; set; }
public IReadOnlyDictionary<string, string> Data { get; set; }
} }
} }

View file

@ -17,7 +17,9 @@ namespace Ombi.Notifications
public void Setup(NotificationOptions opts, FullBaseRequest req, CustomizationSettings s, UserNotificationPreferences pref) public void Setup(NotificationOptions opts, FullBaseRequest req, CustomizationSettings s, UserNotificationPreferences pref)
{ {
LoadIssues(opts); LoadIssues(opts);
RequestId = req.Id.ToString();
RequestId = req?.Id.ToString();
string title; string title;
if (req == null) if (req == null)
{ {
@ -68,7 +70,8 @@ namespace Ombi.Notifications
{ {
LoadIssues(opts); LoadIssues(opts);
RequestId = req.Id.ToString(); RequestId = req?.Id.ToString();
string title; string title;
if (req == null) if (req == null)
{ {
@ -218,6 +221,7 @@ namespace Ombi.Notifications
} }
// User Defined // User Defined
public string RequestId { get; set; }
public string RequestedUser { get; set; } public string RequestedUser { get; set; }
public string UserName { get; set; } public string UserName { get; set; }
public string IssueUser => UserName; public string IssueUser => UserName;
@ -241,7 +245,6 @@ namespace Ombi.Notifications
public string UserPreference { get; set; } public string UserPreference { get; set; }
public string DenyReason { get; set; } public string DenyReason { get; set; }
public string AvailableDate { get; set; } public string AvailableDate { get; set; }
public string RequestId { get; set; }
// System Defined // System Defined
private string LongDate => DateTime.Now.ToString("D"); private string LongDate => DateTime.Now.ToString("D");
@ -251,6 +254,7 @@ namespace Ombi.Notifications
public Dictionary<string, string> Curlys => new Dictionary<string, string> public Dictionary<string, string> Curlys => new Dictionary<string, string>
{ {
{nameof(RequestId), RequestId },
{nameof(RequestedUser), RequestedUser }, {nameof(RequestedUser), RequestedUser },
{nameof(Title), Title }, {nameof(Title), Title },
{nameof(RequestedDate), RequestedDate }, {nameof(RequestedDate), RequestedDate },

View file

@ -47,7 +47,7 @@ namespace Ombi.Notifications
body = ReplaceFields(bodyFields, parameters, body); body = ReplaceFields(bodyFields, parameters, body);
subject = ReplaceFields(subjectFields, parameters, subject); 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<string> ProcessConditions(IEnumerable<string> conditionalFields, IReadOnlyDictionary<string, string> parameters) public IEnumerable<string> ProcessConditions(IEnumerable<string> conditionalFields, IReadOnlyDictionary<string, string> parameters)

View file

@ -16,6 +16,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Ombi.Api.Discord\Ombi.Api.Discord.csproj" /> <ProjectReference Include="..\Ombi.Api.Discord\Ombi.Api.Discord.csproj" />
<ProjectReference Include="..\Ombi.Api.Gotify\Ombi.Api.Gotify.csproj" /> <ProjectReference Include="..\Ombi.Api.Gotify\Ombi.Api.Gotify.csproj" />
<ProjectReference Include="..\Ombi.Api.Webhook\Ombi.Api.Webhook.csproj" />
<ProjectReference Include="..\Ombi.Api.Mattermost\Ombi.Api.Mattermost.csproj" /> <ProjectReference Include="..\Ombi.Api.Mattermost\Ombi.Api.Mattermost.csproj" />
<ProjectReference Include="..\Ombi.Api.Notifications\Ombi.Api.Notifications.csproj" /> <ProjectReference Include="..\Ombi.Api.Notifications\Ombi.Api.Notifications.csproj" />
<ProjectReference Include="..\Ombi.Api.Pushbullet\Ombi.Api.Pushbullet.csproj" /> <ProjectReference Include="..\Ombi.Api.Pushbullet\Ombi.Api.Pushbullet.csproj" />

View file

@ -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; }
}
}

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
@ -8,6 +9,7 @@ using Ombi.Core;
using Ombi.Helpers; using Ombi.Helpers;
using Ombi.Hubs; using Ombi.Hubs;
using Ombi.Notifications.Models; using Ombi.Notifications.Models;
using Ombi.Schedule.Jobs.Plex.Models;
using Ombi.Store.Entities; using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests; using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository; using Ombi.Store.Repository;
@ -60,13 +62,13 @@ namespace Ombi.Schedule.Jobs.Plex
private Task ProcessTv() private Task ProcessTv()
{ {
var tv = _tvRepo.GetChild().Where(x => !x.Available); var tv = _tvRepo.GetChild().Where(x => !x.Available).AsNoTracking();
return ProcessTv(tv); return ProcessTv(tv);
} }
private async Task ProcessTv(IQueryable<ChildRequests> tv) private async Task ProcessTv(IQueryable<ChildRequests> tv)
{ {
var plexEpisodes = _repo.GetAllEpisodes().Include(x => x.Series); var plexEpisodes = _repo.GetAllEpisodes().Include(x => x.Series).AsNoTracking();
foreach (var child in tv) foreach (var child in tv)
{ {
@ -108,6 +110,7 @@ namespace Ombi.Schedule.Jobs.Plex
} }
var availableEpisode = new List<AvailabilityModel>();
foreach (var season in child.SeasonRequests) foreach (var season in child.SeasonRequests)
{ {
foreach (var episode in season.Episodes) foreach (var episode in season.Episodes)
@ -122,20 +125,28 @@ namespace Ombi.Schedule.Jobs.Plex
if (foundEp != null) if (foundEp != null)
{ {
availableEpisode.Add(new AvailabilityModel
{
Id = episode.Id
});
episode.Available = true; 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 // 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)); var allAvailable = child.SeasonRequests.All(x => x.Episodes.All(c => c.Available));
if (allAvailable) if (allAvailable)
{ {
_log.LogInformation("[PAC] - Child request {0} is now available, sending notification", $"{child.Title} - {child.Id}"); _log.LogInformation("[PAC] - Child request {0} is now available, sending notification", $"{child.Title} - {child.Id}");
// We have ful-fulled this request! // We have ful-fulled this request!
child.Available = true; await _tvRepo.MarkChildAsAvailable(child.Id);
child.MarkedAsAvailable = DateTime.Now;
await _notificationService.Notify(new NotificationOptions await _notificationService.Notify(new NotificationOptions
{ {
DateTime = DateTime.Now, DateTime = DateTime.Now,
@ -153,10 +164,16 @@ namespace Ombi.Schedule.Jobs.Plex
private async Task ProcessMovies() private async Task ProcessMovies()
{ {
// Get all non available // 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<AvailabilityModel>();
foreach (var movie in movies) foreach (var movie in movies)
{ {
if (movie.Available)
{
return;
}
PlexServerContent item = null; PlexServerContent item = null;
if (movie.ImdbId.HasValue()) if (movie.ImdbId.HasValue())
{ {
@ -174,24 +191,29 @@ namespace Ombi.Schedule.Jobs.Plex
// We don't yet have this // We don't yet have this
continue; continue;
} }
movie.Available = true;
movie.MarkedAsAvailable = DateTime.Now;
item.RequestId = movie.Id;
_log.LogInformation("[PAC] - Movie request {0} is now available, sending notification", $"{movie.Title} - {movie.Id}"); _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
});
}
foreach (var i in itemsForAvailbility)
{
await _movieRepo.MarkAsAvailable(i.Id);
await _notificationService.Notify(new NotificationOptions await _notificationService.Notify(new NotificationOptions
{ {
DateTime = DateTime.Now, DateTime = DateTime.Now,
NotificationType = NotificationType.RequestAvailable, NotificationType = NotificationType.RequestAvailable,
RequestId = movie.Id, RequestId = i.Id,
RequestType = RequestType.Movie, RequestType = RequestType.Movie,
Recipient = movie.RequestedUser != null ? movie.RequestedUser.Email : string.Empty Recipient = i.RequestedUser
}); });
} }
await _movieRepo.Save();
await _repo.SaveChangesAsync(); await _repo.SaveChangesAsync();
} }

View file

@ -235,4 +235,4 @@ namespace Ombi.Schedule.Jobs.Plex
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
} }
} }

View file

@ -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; }
}
}

View file

@ -7,6 +7,7 @@ using Microsoft.EntityFrameworkCore;
using Ombi.Helpers; using Ombi.Helpers;
using Ombi.Store.Entities; using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests; using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository.Requests;
namespace Ombi.Store.Context namespace Ombi.Store.Context
{ {
@ -35,6 +36,7 @@ namespace Ombi.Store.Context
public DbSet<AlbumRequest> AlbumRequests { get; set; } public DbSet<AlbumRequest> AlbumRequests { get; set; }
public DbSet<TvRequests> TvRequests { get; set; } public DbSet<TvRequests> TvRequests { get; set; }
public DbSet<ChildRequests> ChildRequests { get; set; } public DbSet<ChildRequests> ChildRequests { get; set; }
public DbSet<EpisodeRequests> EpisodeRequests { get; set; }
public DbSet<Issues> Issues { get; set; } public DbSet<Issues> Issues { get; set; }
public DbSet<IssueCategory> IssueCategories { get; set; } public DbSet<IssueCategory> IssueCategories { get; set; }

View file

@ -10,6 +10,7 @@ namespace Ombi.Store.Repository.Requests
MovieRequests GetRequest(int theMovieDbId); MovieRequests GetRequest(int theMovieDbId);
Task Update(MovieRequests request); Task Update(MovieRequests request);
Task Save(); Task Save();
Task MarkAsAvailable(int id);
IQueryable<MovieRequests> GetWithUser(); IQueryable<MovieRequests> GetWithUser();
IQueryable<MovieRequests> GetWithUser(string userId); IQueryable<MovieRequests> GetWithUser(string userId);
IQueryable<MovieRequests> GetAll(string userId); IQueryable<MovieRequests> GetAll(string userId);

View file

@ -23,6 +23,8 @@ namespace Ombi.Store.Repository.Requests
Task UpdateChild(ChildRequests request); Task UpdateChild(ChildRequests request);
IQueryable<ChildRequests> GetChild(); IQueryable<ChildRequests> GetChild();
IQueryable<ChildRequests> GetChild(string userId); IQueryable<ChildRequests> GetChild(string userId);
Task MarkEpisodeAsAvailable(int id);
Task MarkChildAsAvailable(int id);
Task Save(); Task Save();
Task DeleteChildRange(IEnumerable<ChildRequests> request); Task DeleteChildRange(IEnumerable<ChildRequests> request);
} }

View file

@ -54,6 +54,14 @@ namespace Ombi.Store.Repository.Requests
.AsQueryable(); .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<MovieRequests> GetWithUser(string userId) public IQueryable<MovieRequests> GetWithUser(string userId)
{ {

View file

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -100,6 +101,23 @@ namespace Ombi.Store.Repository.Requests
.AsQueryable(); .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() public async Task Save()
{ {
await InternalSaveChanges(); await InternalSaveChanges();
@ -128,10 +146,10 @@ namespace Ombi.Store.Repository.Requests
public async Task Update(TvRequests request) public async Task Update(TvRequests request)
{ {
Db.Update(request); Db.Update(request);
await InternalSaveChanges(); await InternalSaveChanges();
} }
public async Task UpdateChild(ChildRequests request) public async Task UpdateChild(ChildRequests request)
{ {
Db.Update(request); Db.Update(request);

View file

@ -112,6 +112,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Api.MusicBrainz", "Omb
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ombi.Api.Twilio", "Ombi.Api.Twilio\Ombi.Api.Twilio.csproj", "{34E5DD1A-6A90-448B-9E71-64D1ACD6C1A3}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ombi.Api.Twilio", "Ombi.Api.Twilio\Ombi.Api.Twilio.csproj", "{34E5DD1A-6A90-448B-9E71-64D1ACD6C1A3}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ombi.Api.Webhook", "Ombi.Api.Webhook\Ombi.Api.Webhook.csproj", "{E2186FDA-D827-4781-8663-130AC382F12C}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{34E5DD1A-6A90-448B-9E71-64D1ACD6C1A3}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -336,6 +342,7 @@ Global
{4FA21A20-92F4-462C-B929-2C517A88CC56} = {9293CA11-360A-4C20-A674-B9E794431BF5} {4FA21A20-92F4-462C-B929-2C517A88CC56} = {9293CA11-360A-4C20-A674-B9E794431BF5}
{CC8CEFCD-0CB6-45BB-845F-508BCAB5BDC3} = {6F42AB98-9196-44C4-B888-D5E409F415A1} {CC8CEFCD-0CB6-45BB-845F-508BCAB5BDC3} = {6F42AB98-9196-44C4-B888-D5E409F415A1}
{105EA346-766E-45B8-928B-DE6991DCB7EB} = {9293CA11-360A-4C20-A674-B9E794431BF5} {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} {F3969B69-3B07-4884-A7AB-0BAB8B84DF94} = {6F42AB98-9196-44C4-B888-D5E409F415A1}
{27111E7C-748E-4996-BD71-2117027C6460} = {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} {9266403C-B04D-4C0F-AC39-82F12C781949} = {9293CA11-360A-4C20-A674-B9E794431BF5}

View file

@ -0,0 +1,49 @@

<settings-menu></settings-menu>
<div *ngIf="form">
<fieldset>
<legend>Webhook Notifications</legend>
<div class="col-md-6">
<form novalidate [formGroup]="form" (ngSubmit)="onSubmit(form)">
<div class="form-group">
<div class="checkbox">
<input type="checkbox" id="enable" formControlName="enabled">
<label for="enable">Enabled</label>
</div>
</div>
<div class="form-group">
<label for="baseUrl" class="control-label">Base URL</label>
<input type="text" class="form-control form-control-custom " id="webhookUrl" name="webhookUrl" [ngClass]="{'form-error': form.get('webhookUrl').hasError('required')}" formControlName="webhookUrl" pTooltip="Enter the URL of your webhook server.">
<small *ngIf="form.get('webhookUrl').hasError('required')" class="error-text">The Webhook URL is required</small>
</div>
<div class="form-group">
<label for="applicationToken" class="control-label">Application Token
<i class="fa fa-question-circle" pTooltip="Optional authentication token. Will be sent as 'Access-Token' header."></i>
</label>
<input type="text" class="form-control form-control-custom " id="applicationToken" name="applicationToken" [ngClass]="{'form-error': form.get('applicationToken').hasError('required')}" formControlName="applicationToken" pTooltip="Enter your Application token from Webhook.">
<small *ngIf="form.get('applicationToken').hasError('required')" class="error-text">The Application Token is required</small>
</div>
<div class="form-group">
<div>
<button [disabled]="form.invalid" type="button" (click)="test(form)" class="btn btn-primary-outline">
Test
<div id="spinner"></div>
</button>
</div>
</div>
<div class="form-group">
<div>
<button [disabled]="form.invalid" type="submit" id="save" class="btn btn-primary-outline">Submit</button>
</div>
</div>
</form>
</div>
</fieldset>
</div>

View file

@ -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 = <IWebhookNotificationSettings> 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");
}
});
}
}

View file

@ -119,6 +119,11 @@ export interface IGotifyNotificationSettings extends INotificationSettings {
priority: number; priority: number;
} }
export interface IWebhookNotificationSettings extends INotificationSettings {
webhookUrl: string;
applicationToken: string;
}
export interface IMattermostNotifcationSettings extends INotificationSettings { export interface IMattermostNotifcationSettings extends INotificationSettings {
webhookUrl: string; webhookUrl: string;
username: string; username: string;

View file

@ -24,6 +24,7 @@ import {
ISlackNotificationSettings, ISlackNotificationSettings,
ISonarrSettings, ISonarrSettings,
ITelegramNotifcationSettings, ITelegramNotifcationSettings,
IWebhookNotificationSettings,
IWhatsAppSettings, IWhatsAppSettings,
} from "../../interfaces"; } from "../../interfaces";
@ -49,6 +50,10 @@ export class TesterService extends ServiceHelpers {
return this.http.post<boolean>(`${this.url}gotify`, JSON.stringify(settings), { headers: this.headers }); return this.http.post<boolean>(`${this.url}gotify`, JSON.stringify(settings), { headers: this.headers });
} }
public webhookTest(settings: IWebhookNotificationSettings): Observable<boolean> {
return this.http.post<boolean>(`${this.url}webhook`, JSON.stringify(settings), { headers: this.headers });
}
public mattermostTest(settings: IMattermostNotifcationSettings): Observable<boolean> { public mattermostTest(settings: IMattermostNotifcationSettings): Observable<boolean> {
return this.http.post<boolean>(`${this.url}mattermost`, JSON.stringify(settings), {headers: this.headers}); return this.http.post<boolean>(`${this.url}mattermost`, JSON.stringify(settings), {headers: this.headers});
} }

View file

@ -37,6 +37,7 @@ import {
IUserManagementSettings, IUserManagementSettings,
IVoteSettings, IVoteSettings,
ITwilioSettings, ITwilioSettings,
IWebhookNotificationSettings,
} from "../interfaces"; } from "../interfaces";
import { ServiceHelpers } from "./service.helpers"; import { ServiceHelpers } from "./service.helpers";
@ -193,6 +194,14 @@ export class SettingsService extends ServiceHelpers {
.post<boolean>(`${this.url}/notifications/gotify`, JSON.stringify(settings), { headers: this.headers }); .post<boolean>(`${this.url}/notifications/gotify`, JSON.stringify(settings), { headers: this.headers });
} }
public getWebhookNotificationSettings(): Observable<IWebhookNotificationSettings> {
return this.http.get<IWebhookNotificationSettings>(`${this.url}/notifications/webhook`, { headers: this.headers });
}
public saveWebhookNotificationSettings(settings: IWebhookNotificationSettings): Observable<boolean> {
return this.http
.post<boolean>(`${this.url}/notifications/webhook`, JSON.stringify(settings), { headers: this.headers });
}
public getSlackNotificationSettings(): Observable<ISlackNotificationSettings> { public getSlackNotificationSettings(): Observable<ISlackNotificationSettings> {
return this.http.get<ISlackNotificationSettings>(`${this.url}/notifications/slack`, {headers: this.headers}); return this.http.get<ISlackNotificationSettings>(`${this.url}/notifications/slack`, {headers: this.headers});
} }

View file

@ -37,6 +37,7 @@ import { PushbulletComponent } from "./notifications/pushbullet.component";
import { PushoverComponent } from "./notifications/pushover.component"; import { PushoverComponent } from "./notifications/pushover.component";
import { SlackComponent } from "./notifications/slack.component"; import { SlackComponent } from "./notifications/slack.component";
import { TelegramComponent } from "./notifications/telegram.component"; import { TelegramComponent } from "./notifications/telegram.component";
import { WebhookComponent } from "./notifications/webhook.component";
import { OmbiComponent } from "./ombi/ombi.component"; import { OmbiComponent } from "./ombi/ombi.component";
import { PlexComponent } from "./plex/plex.component"; import { PlexComponent } from "./plex/plex.component";
import { RadarrComponent } from "./radarr/radarr.component"; import { RadarrComponent } from "./radarr/radarr.component";
@ -73,6 +74,7 @@ const routes: Routes = [
{ path: "Pushover", component: PushoverComponent, canActivate: [AuthGuard] }, { path: "Pushover", component: PushoverComponent, canActivate: [AuthGuard] },
{ path: "Pushbullet", component: PushbulletComponent, canActivate: [AuthGuard] }, { path: "Pushbullet", component: PushbulletComponent, canActivate: [AuthGuard] },
{ path: "Gotify", component: GotifyComponent, canActivate: [AuthGuard] }, { path: "Gotify", component: GotifyComponent, canActivate: [AuthGuard] },
{ path: "Webhook", component: WebhookComponent, canActivate: [AuthGuard] },
{ path: "Mattermost", component: MattermostComponent, canActivate: [AuthGuard] }, { path: "Mattermost", component: MattermostComponent, canActivate: [AuthGuard] },
{ path: "Twilio", component: TwilioComponent, canActivate: [AuthGuard] }, { path: "Twilio", component: TwilioComponent, canActivate: [AuthGuard] },
{ path: "UserManagement", component: UserManagementComponent, canActivate: [AuthGuard] }, { path: "UserManagement", component: UserManagementComponent, canActivate: [AuthGuard] },
@ -134,6 +136,7 @@ const routes: Routes = [
MattermostComponent, MattermostComponent,
PushbulletComponent, PushbulletComponent,
GotifyComponent, GotifyComponent,
WebhookComponent,
UserManagementComponent, UserManagementComponent,
UpdateComponent, UpdateComponent,
AboutComponent, AboutComponent,

View file

@ -44,7 +44,7 @@ namespace Ombi.Controllers.V1.External
IPushbulletNotification pushbullet, ISlackNotification slack, IPushoverNotification po, IMattermostNotification mm, IPushbulletNotification pushbullet, ISlackNotification slack, IPushoverNotification po, IMattermostNotification mm,
IPlexApi plex, IEmbyApi emby, IRadarrApi radarr, ISonarrApi sonarr, ILogger<TesterController> log, IEmailProvider provider, IPlexApi plex, IEmbyApi emby, IRadarrApi radarr, ISonarrApi sonarr, ILogger<TesterController> log, IEmailProvider provider,
ICouchPotatoApi cpApi, ITelegramNotification telegram, ISickRageApi srApi, INewsletterJob newsletter, IMobileNotification mobileNotification, 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; Service = service;
DiscordNotification = notification; DiscordNotification = notification;
@ -68,6 +68,7 @@ namespace Ombi.Controllers.V1.External
GotifyNotification = gotifyNotification; GotifyNotification = gotifyNotification;
WhatsAppApi = whatsAppApi; WhatsAppApi = whatsAppApi;
UserManager = um; UserManager = um;
WebhookNotification = webhookNotification;
} }
private INotificationService Service { get; } private INotificationService Service { get; }
@ -77,6 +78,7 @@ namespace Ombi.Controllers.V1.External
private ISlackNotification SlackNotification { get; } private ISlackNotification SlackNotification { get; }
private IPushoverNotification PushoverNotification { get; } private IPushoverNotification PushoverNotification { get; }
private IGotifyNotification GotifyNotification { get; } private IGotifyNotification GotifyNotification { get; }
private IWebhookNotification WebhookNotification { get; }
private IMattermostNotification MattermostNotification { get; } private IMattermostNotification MattermostNotification { get; }
private IPlexApi PlexApi { get; } private IPlexApi PlexApi { get; }
private IRadarrApi RadarrApi { get; } private IRadarrApi RadarrApi { get; }
@ -188,6 +190,30 @@ namespace Ombi.Controllers.V1.External
} }
/// <summary>
/// Sends a test message to configured webhook using the provided settings
/// </summary>
/// <param name="settings">The settings.</param>
/// <returns></returns>
[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;
}
}
/// <summary> /// <summary>
/// Sends a test message to mattermost using the provided settings /// Sends a test message to mattermost using the provided settings
/// </summary> /// </summary>

View file

@ -1069,6 +1069,33 @@ namespace Ombi.Controllers.V1
return model; return model;
} }
/// <summary>
/// Saves the webhook notification settings.
/// </summary>
/// <param name="model">The model.</param>
/// <returns></returns>
[HttpPost("notifications/webhook")]
public async Task<bool> WebhookNotificationSettings([FromBody] WebhookNotificationViewModel model)
{
var settings = Mapper.Map<WebhookSettings>(model);
var result = await Save(settings);
return result;
}
/// <summary>
/// Gets the webhook notification settings.
/// </summary>
/// <returns></returns>
[HttpGet("notifications/webhook")]
public async Task<WebhookNotificationViewModel> WebhookNotificationSettings()
{
var settings = await Get<WebhookSettings>();
var model = Mapper.Map<WebhookNotificationViewModel>(settings);
return model;
}
/// <summary> /// <summary>
/// Saves the Newsletter notification settings. /// Saves the Newsletter notification settings.
/// </summary> /// </summary>

View file

@ -75,7 +75,7 @@
"RequestAdded": "Kérés sikeresen leadva erre: {{title}}", "RequestAdded": "Kérés sikeresen leadva erre: {{title}}",
"Similar": "Hasonló", "Similar": "Hasonló",
"Refine": "Finomítás", "Refine": "Finomítás",
"SearchBarPlaceholder": "Type Here to Search", "SearchBarPlaceholder": "A kereséshez írj be valamit",
"Movies": { "Movies": {
"PopularMovies": "Népszerű filmek", "PopularMovies": "Népszerű filmek",
"UpcomingMovies": "Közelgő filmek", "UpcomingMovies": "Közelgő filmek",

View file

@ -3,7 +3,7 @@
"SignInButton": "Войти", "SignInButton": "Войти",
"UsernamePlaceholder": "Имя пользователя", "UsernamePlaceholder": "Имя пользователя",
"PasswordPlaceholder": "Пароль", "PasswordPlaceholder": "Пароль",
"RememberMe": "Запомнить Меня", "RememberMe": "Запомнить меня",
"ForgottenPassword": "Забыли пароль?", "ForgottenPassword": "Забыли пароль?",
"Errors": { "Errors": {
"IncorrectCredentials": "Неверное имя пользователя или пароль" "IncorrectCredentials": "Неверное имя пользователя или пароль"