pull/3400/head
Jamie Rees 4 years ago
commit ac51126437

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

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

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

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

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

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

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

@ -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>();

@ -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" />

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

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

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

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

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

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

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

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

@ -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)

@ -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" />

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

@ -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;
} }
_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; foreach (var i in itemsForAvailbility)
movie.MarkedAsAvailable = DateTime.Now; {
item.RequestId = movie.Id; 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 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();
} }

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

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

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

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

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

@ -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)
{ {

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

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

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

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

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

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

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

@ -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,

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

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

@ -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",

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

Loading…
Cancel
Save