Merge branch 'feature/v4' of https://github.com/tidusjar/Ombi into feature/v4

pull/3895/head
Jamie Rees 6 years ago
commit 8a7ecd7955

@ -59,6 +59,7 @@ We integrate with the following applications:
Supported notifications: Supported notifications:
* SMTP Notifications (Email) * SMTP Notifications (Email)
* Discord * Discord
* Gotify
* Slack * Slack
* Pushbullet * Pushbullet
* Pushover * Pushover

@ -3,7 +3,7 @@
#addin "Cake.Gulp" #addin "Cake.Gulp"
#addin "SharpZipLib" #addin "SharpZipLib"
#addin nuget:?package=Cake.Compression&version=0.1.4 #addin nuget:?package=Cake.Compression&version=0.1.4
#addin "Cake.Incubator" #addin "Cake.Incubator&version=3.1.0"
#addin "Cake.Yarn" #addin "Cake.Yarn"
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////

@ -0,0 +1,36 @@
using System.Net.Http;
using System.Threading.Tasks;
namespace Ombi.Api.Gotify
{
public class GotifyApi : IGotifyApi
{
public GotifyApi(IApi api)
{
_api = api;
}
private readonly IApi _api;
public async Task PushAsync(string baseUrl, string accessToken, string subject, string body, sbyte priority)
{
var request = new Request("/message", baseUrl, HttpMethod.Post);
request.AddQueryString("token", accessToken);
request.AddHeader("Access-Token", accessToken);
request.ApplicationJsonContentType();
var jsonBody = new
{
message = body,
title = subject,
priority = priority
};
request.AddJsonBody(jsonBody);
await _api.Request(request);
}
}
}

@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace Ombi.Api.Gotify
{
public interface IGotifyApi
{
Task PushAsync(string endpoint, string accessToken, string subject, string body, sbyte priority);
}
}

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</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>

@ -4,6 +4,7 @@ using Moq;
using Ombi.Core.Rule.Rules.Request; using Ombi.Core.Rule.Rules.Request;
using Ombi.Store.Entities.Requests; using Ombi.Store.Entities.Requests;
using NUnit.Framework; using NUnit.Framework;
using Ombi.Core.Authentication;
using Ombi.Helpers; using Ombi.Helpers;
namespace Ombi.Core.Tests.Rule.Request namespace Ombi.Core.Tests.Rule.Request
@ -16,7 +17,7 @@ namespace Ombi.Core.Tests.Rule.Request
{ {
PrincipalMock = new Mock<IPrincipal>(); PrincipalMock = new Mock<IPrincipal>();
Rule = new AutoApproveRule(PrincipalMock.Object); Rule = new AutoApproveRule(PrincipalMock.Object, null);
} }

@ -3,6 +3,7 @@ using System.Threading.Tasks;
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
using Ombi.Core.Rule.Rules; using Ombi.Core.Rule.Rules;
using Ombi.Core.Rule.Rules.Request;
using Ombi.Helpers; using Ombi.Helpers;
using Ombi.Store.Entities.Requests; using Ombi.Store.Entities.Requests;
@ -15,7 +16,7 @@ namespace Ombi.Core.Tests.Rule.Request
{ {
PrincipalMock = new Mock<IPrincipal>(); PrincipalMock = new Mock<IPrincipal>();
Rule = new CanRequestRule(PrincipalMock.Object); Rule = new CanRequestRule(PrincipalMock.Object, null);
} }

@ -16,7 +16,7 @@ namespace Ombi.Core.Tests.Rule.Search
public void Setup() public void Setup()
{ {
ContextMock = new Mock<IEmbyContentRepository>(); ContextMock = new Mock<IEmbyContentRepository>();
Rule = new EmbyAvailabilityRule(ContextMock.Object); Rule = new EmbyAvailabilityRule(ContextMock.Object, null);
} }
private EmbyAvailabilityRule Rule { get; set; } private EmbyAvailabilityRule Rule { get; set; }

@ -32,14 +32,13 @@ namespace Ombi.Core.Engine
{ {
public TvRequestEngine(ITvMazeApi tvApi, IMovieDbApi movApi, IRequestServiceMain requestService, IPrincipal user, public TvRequestEngine(ITvMazeApi tvApi, IMovieDbApi movApi, IRequestServiceMain requestService, IPrincipal user,
INotificationHelper helper, IRuleEvaluator rule, OmbiUserManager manager, INotificationHelper helper, IRuleEvaluator rule, OmbiUserManager manager,
ITvSender sender, IAuditRepository audit, IRepository<RequestLog> rl, ISettingsService<OmbiSettings> settings, ICacheService cache, ITvSender sender, IRepository<RequestLog> rl, ISettingsService<OmbiSettings> settings, ICacheService cache,
IRepository<RequestSubscription> sub) : base(user, requestService, rule, manager, cache, settings, sub) IRepository<RequestSubscription> sub) : base(user, requestService, rule, manager, cache, settings, sub)
{ {
TvApi = tvApi; TvApi = tvApi;
MovieDbApi = movApi; MovieDbApi = movApi;
NotificationHelper = helper; NotificationHelper = helper;
TvSender = sender; TvSender = sender;
Audit = audit;
_requestLog = rl; _requestLog = rl;
} }
@ -47,7 +46,6 @@ namespace Ombi.Core.Engine
private ITvMazeApi TvApi { get; } private ITvMazeApi TvApi { get; }
private IMovieDbApi MovieDbApi { get; } private IMovieDbApi MovieDbApi { get; }
private ITvSender TvSender { get; } private ITvSender TvSender { get; }
private IAuditRepository Audit { get; }
private readonly IRepository<RequestLog> _requestLog; private readonly IRepository<RequestLog> _requestLog;
public async Task<RequestEngineResult> RequestTvShow(TvRequestViewModel tv) public async Task<RequestEngineResult> RequestTvShow(TvRequestViewModel tv)
@ -85,8 +83,6 @@ namespace Ombi.Core.Engine
} }
} }
await Audit.Record(AuditType.Added, AuditArea.TvRequest, $"Added Request {tvBuilder.ChildRequest.Title}", Username);
var existingRequest = await TvRepository.Get().FirstOrDefaultAsync(x => x.TvDbId == tv.TvDbId); var existingRequest = await TvRepository.Get().FirstOrDefaultAsync(x => x.TvDbId == tv.TvDbId);
if (existingRequest != null) if (existingRequest != null)
{ {
@ -408,7 +404,6 @@ namespace Ombi.Core.Engine
public async Task<TvRequests> UpdateTvRequest(TvRequests request) public async Task<TvRequests> UpdateTvRequest(TvRequests request)
{ {
await Audit.Record(AuditType.Updated, AuditArea.TvRequest, $"Updated Request {request.Title}", Username);
var allRequests = TvRepository.Get(); var allRequests = TvRepository.Get();
var results = await allRequests.FirstOrDefaultAsync(x => x.Id == request.Id); var results = await allRequests.FirstOrDefaultAsync(x => x.Id == request.Id);
@ -451,7 +446,6 @@ namespace Ombi.Core.Engine
if (request.Approved) if (request.Approved)
{ {
NotificationHelper.Notify(request, NotificationType.RequestApproved); NotificationHelper.Notify(request, NotificationType.RequestApproved);
await Audit.Record(AuditType.Approved, AuditArea.TvRequest, $"Approved Request {request.Title}", Username);
// Autosend // Autosend
await TvSender.Send(request); await TvSender.Send(request);
} }
@ -483,9 +477,7 @@ namespace Ombi.Core.Engine
public async Task<ChildRequests> UpdateChildRequest(ChildRequests request) public async Task<ChildRequests> UpdateChildRequest(ChildRequests request)
{ {
await Audit.Record(AuditType.Updated, AuditArea.TvRequest, $"Updated Request {request.Title}", Username); await TvRepository.UpdateChild(request);
await TvRepository.UpdateChild(request);
return request; return request;
} }
@ -503,16 +495,14 @@ namespace Ombi.Core.Engine
// Delete the parent // Delete the parent
TvRepository.Db.TvRequests.Remove(parent); TvRepository.Db.TvRequests.Remove(parent);
} }
await Audit.Record(AuditType.Deleted, AuditArea.TvRequest, $"Deleting Request {request.Title}", Username);
await TvRepository.Db.SaveChangesAsync(); await TvRepository.Db.SaveChangesAsync();
} }
public async Task RemoveTvRequest(int requestId) public async Task RemoveTvRequest(int requestId)
{ {
var request = await TvRepository.Get().FirstOrDefaultAsync(x => x.Id == requestId); var request = await TvRepository.Get().FirstOrDefaultAsync(x => x.Id == requestId);
await Audit.Record(AuditType.Deleted, AuditArea.TvRequest, $"Deleting Request {request.Title}", Username); await TvRepository.Delete(request);
await TvRepository.Delete(request);
} }
public async Task<bool> UserHasRequest(string userId) public async Task<bool> UserHasRequest(string userId)

@ -0,0 +1,23 @@

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="GotifyNotificationSettings" />
public class GotifyNotificationViewModel : GotifySettings
{
/// <summary>
/// Gets or sets the notification templates.
/// </summary>
/// <value>
/// The notification templates.
/// </value>
public List<NotificationTemplates> NotificationTemplates { get; set; }
}
}

@ -1,5 +1,7 @@
using System.Security.Principal; using System.Security.Principal;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Ombi.Core.Authentication;
using Ombi.Core.Models.Requests; using Ombi.Core.Models.Requests;
using Ombi.Core.Rule.Interfaces; using Ombi.Core.Rule.Interfaces;
using Ombi.Helpers; using Ombi.Helpers;
@ -10,28 +12,31 @@ namespace Ombi.Core.Rule.Rules.Request
{ {
public class AutoApproveRule : BaseRequestRule, IRules<BaseRequest> public class AutoApproveRule : BaseRequestRule, IRules<BaseRequest>
{ {
public AutoApproveRule(IPrincipal principal) public AutoApproveRule(IPrincipal principal, OmbiUserManager um)
{ {
User = principal; User = principal;
_manager = um;
} }
private IPrincipal User { get; } private IPrincipal User { get; }
private readonly OmbiUserManager _manager;
public Task<RuleResult> Execute(BaseRequest obj) public async Task<RuleResult> Execute(BaseRequest obj)
{ {
if (User.IsInRole(OmbiRoles.Admin)) var user = await _manager.Users.FirstOrDefaultAsync(x => x.UserName == User.Identity.Name);
if (await _manager.IsInRoleAsync(user, OmbiRoles.Admin))
{ {
obj.Approved = true; obj.Approved = true;
return Task.FromResult(Success()); return Success();
} }
if (obj.RequestType == RequestType.Movie && User.IsInRole(OmbiRoles.AutoApproveMovie)) if (obj.RequestType == RequestType.Movie && await _manager.IsInRoleAsync(user, OmbiRoles.AutoApproveMovie))
obj.Approved = true; obj.Approved = true;
if (obj.RequestType == RequestType.TvShow && User.IsInRole(OmbiRoles.AutoApproveTv)) if (obj.RequestType == RequestType.TvShow && await _manager.IsInRoleAsync(user, OmbiRoles.AutoApproveTv))
obj.Approved = true; obj.Approved = true;
if (obj.RequestType == RequestType.Album && User.IsInRole(OmbiRoles.AutoApproveMusic)) if (obj.RequestType == RequestType.Album && await _manager.IsInRoleAsync(user, OmbiRoles.AutoApproveMusic))
obj.Approved = true; obj.Approved = true;
return Task.FromResult(Success()); // We don't really care, we just don't set the obj to approve return Success(); // We don't really care, we just don't set the obj to approve
} }
} }
} }

@ -1,46 +1,52 @@
using Ombi.Store.Entities; using System.Security.Claims;
using System.Security.Principal; using System.Security.Principal;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Ombi.Core.Authentication;
using Ombi.Core.Rule.Interfaces; using Ombi.Core.Rule.Interfaces;
using Ombi.Helpers; using Ombi.Helpers;
using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests; using Ombi.Store.Entities.Requests;
namespace Ombi.Core.Rule.Rules namespace Ombi.Core.Rule.Rules.Request
{ {
public class CanRequestRule : BaseRequestRule, IRules<BaseRequest> public class CanRequestRule : BaseRequestRule, IRules<BaseRequest>
{ {
public CanRequestRule(IPrincipal principal) public CanRequestRule(IPrincipal principal, OmbiUserManager manager)
{ {
User = principal; User = principal;
_manager = manager;
} }
private IPrincipal User { get; } private IPrincipal User { get; }
private readonly OmbiUserManager _manager;
public Task<RuleResult> Execute(BaseRequest obj) public async Task<RuleResult> Execute(BaseRequest obj)
{ {
if (User.IsInRole(OmbiRoles.Admin)) var user = await _manager.Users.FirstOrDefaultAsync(x => x.UserName == User.Identity.Name);
return Task.FromResult(Success()); if (await _manager.IsInRoleAsync(user, OmbiRoles.Admin))
return Success();
if (obj.RequestType == RequestType.Movie) if (obj.RequestType == RequestType.Movie)
{ {
if (User.IsInRole(OmbiRoles.RequestMovie) || User.IsInRole(OmbiRoles.AutoApproveMovie)) if (await _manager.IsInRoleAsync(user, OmbiRoles.RequestMovie) || await _manager.IsInRoleAsync(user, OmbiRoles.AutoApproveMovie))
return Task.FromResult(Success()); return Success();
return Task.FromResult(Fail("You do not have permissions to Request a Movie")); return Fail("You do not have permissions to Request a Movie");
} }
if (obj.RequestType == RequestType.TvShow) if (obj.RequestType == RequestType.TvShow)
{ {
if (User.IsInRole(OmbiRoles.RequestTv) || User.IsInRole(OmbiRoles.AutoApproveTv)) if (await _manager.IsInRoleAsync(user, OmbiRoles.RequestTv) || await _manager.IsInRoleAsync(user, OmbiRoles.AutoApproveTv))
return Task.FromResult(Success()); return Success();
} }
if (obj.RequestType == RequestType.Album) if (obj.RequestType == RequestType.Album)
{ {
if (User.IsInRole(OmbiRoles.RequestMusic) || User.IsInRole(OmbiRoles.AutoApproveMusic)) if (await _manager.IsInRoleAsync(user, OmbiRoles.RequestMusic) || await _manager.IsInRoleAsync(user, OmbiRoles.AutoApproveMusic))
return Task.FromResult(Success()); return Success();
} }
return Task.FromResult(Fail("You do not have permissions to Request a TV Show")); return Fail("You do not have permissions to Request a TV Show");
} }
} }
} }

@ -3,6 +3,8 @@ using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Ombi.Core.Models.Search; using Ombi.Core.Models.Search;
using Ombi.Core.Rule.Interfaces; using Ombi.Core.Rule.Interfaces;
using Ombi.Core.Settings;
using Ombi.Core.Settings.Models.External;
using Ombi.Helpers; using Ombi.Helpers;
using Ombi.Store.Entities; using Ombi.Store.Entities;
using Ombi.Store.Repository; using Ombi.Store.Repository;
@ -11,12 +13,14 @@ namespace Ombi.Core.Rule.Rules.Search
{ {
public class EmbyAvailabilityRule : BaseSearchRule, IRules<SearchViewModel> public class EmbyAvailabilityRule : BaseSearchRule, IRules<SearchViewModel>
{ {
public EmbyAvailabilityRule(IEmbyContentRepository repo) public EmbyAvailabilityRule(IEmbyContentRepository repo, ISettingsService<EmbySettings> s)
{ {
EmbyContentRepository = repo; EmbyContentRepository = repo;
EmbySettings = s;
} }
private IEmbyContentRepository EmbyContentRepository { get; } private IEmbyContentRepository EmbyContentRepository { get; }
private ISettingsService<EmbySettings> EmbySettings { get; }
public async Task<RuleResult> Execute(SearchViewModel obj) public async Task<RuleResult> Execute(SearchViewModel obj)
{ {
@ -60,7 +64,16 @@ namespace Ombi.Core.Rule.Rules.Search
if (item != null) if (item != null)
{ {
obj.Available = true; obj.Available = true;
obj.EmbyUrl = item.Url; var s = await EmbySettings.GetSettingsAsync();
var server = s.Servers.FirstOrDefault(x => x.ServerHostname != null);
if ((server?.ServerHostname ?? string.Empty).HasValue())
{
obj.EmbyUrl = $"{server.ServerHostname}#!/itemdetails.html?id={item.EmbyId}";
}
else
{
obj.EmbyUrl = $"https://app.emby.media/#!/itemdetails.html?id={item.EmbyId}";
}
if (obj.Type == RequestType.TvShow) if (obj.Type == RequestType.TvShow)
{ {

@ -32,6 +32,7 @@ using Ombi.Api.CouchPotato;
using Ombi.Api.DogNzb; using Ombi.Api.DogNzb;
using Ombi.Api.FanartTv; using Ombi.Api.FanartTv;
using Ombi.Api.Github; using Ombi.Api.Github;
using Ombi.Api.Gotify;
using Ombi.Api.Lidarr; using Ombi.Api.Lidarr;
using Ombi.Api.Mattermost; using Ombi.Api.Mattermost;
using Ombi.Api.Notifications; using Ombi.Api.Notifications;
@ -128,6 +129,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<IOmbiService, OmbiService>(); services.AddTransient<IOmbiService, OmbiService>();
services.AddTransient<IFanartTvApi, FanartTvApi>(); services.AddTransient<IFanartTvApi, FanartTvApi>();
services.AddTransient<IPushoverApi, PushoverApi>(); services.AddTransient<IPushoverApi, PushoverApi>();
services.AddTransient<IGotifyApi, GotifyApi>();
services.AddTransient<IMattermostApi, MattermostApi>(); services.AddTransient<IMattermostApi, MattermostApi>();
services.AddTransient<ICouchPotatoApi, CouchPotatoApi>(); services.AddTransient<ICouchPotatoApi, CouchPotatoApi>();
services.AddTransient<IDogNzbApi, DogNzbApi>(); services.AddTransient<IDogNzbApi, DogNzbApi>();
@ -178,6 +180,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<ISlackNotification, SlackNotification>(); services.AddTransient<ISlackNotification, SlackNotification>();
services.AddTransient<IMattermostNotification, MattermostNotification>(); services.AddTransient<IMattermostNotification, MattermostNotification>();
services.AddTransient<IPushoverNotification, PushoverNotification>(); services.AddTransient<IPushoverNotification, PushoverNotification>();
services.AddTransient<IGotifyNotification, GotifyNotification>();
services.AddTransient<ITelegramNotification, TelegramNotification>(); services.AddTransient<ITelegramNotification, TelegramNotification>();
services.AddTransient<IMobileNotification, MobileNotification>(); services.AddTransient<IMobileNotification, MobileNotification>();
services.AddTransient<IChangeLogProcessor, ChangeLogProcessor>(); services.AddTransient<IChangeLogProcessor, ChangeLogProcessor>();

@ -32,6 +32,7 @@ namespace Ombi.Helpers
public static EventId MattermostNotification => new EventId(4004); public static EventId MattermostNotification => new EventId(4004);
public static EventId PushoverNotification => new EventId(4005); public static EventId PushoverNotification => new EventId(4005);
public static EventId TelegramNotifcation => new EventId(4006); public static EventId TelegramNotifcation => new EventId(4006);
public static EventId GotifyNotification => new EventId(4007);
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);

@ -10,5 +10,6 @@
Slack = 5, Slack = 5,
Mattermost = 6, Mattermost = 6,
Mobile = 7, Mobile = 7,
Gotify = 8,
} }
} }

@ -19,6 +19,7 @@ namespace Ombi.Mapping.Profiles
CreateMap<UpdateSettingsViewModel, UpdateSettings>().ReverseMap(); CreateMap<UpdateSettingsViewModel, UpdateSettings>().ReverseMap();
CreateMap<MobileNotificationsViewModel, MobileNotificationSettings>().ReverseMap(); CreateMap<MobileNotificationsViewModel, MobileNotificationSettings>().ReverseMap();
CreateMap<NewsletterNotificationViewModel, NewsletterSettings>().ReverseMap(); CreateMap<NewsletterNotificationViewModel, NewsletterSettings>().ReverseMap();
CreateMap<GotifyNotificationViewModel, GotifySettings>().ReverseMap();
} }
} }
} }

@ -0,0 +1,116 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Ombi.Api.Gotify;
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 GotifyNotification : BaseNotification<GotifySettings>, IGotifyNotification
{
public GotifyNotification(IGotifyApi api, ISettingsService<GotifySettings> sn, ILogger<GotifyNotification> 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 => "GotifyNotification";
private IGotifyApi Api { get; }
private ILogger<GotifyNotification> Logger { get; }
protected override bool ValidateConfiguration(GotifySettings settings)
{
return settings.Enabled && !string.IsNullOrEmpty(settings.BaseUrl) && !string.IsNullOrEmpty(settings.ApplicationToken);
}
protected override async Task NewRequest(NotificationOptions model, GotifySettings settings)
{
await Run(model, settings, NotificationType.NewRequest);
}
protected override async Task NewIssue(NotificationOptions model, GotifySettings settings)
{
await Run(model, settings, NotificationType.Issue);
}
protected override async Task IssueComment(NotificationOptions model, GotifySettings settings)
{
await Run(model, settings, NotificationType.IssueComment);
}
protected override async Task IssueResolved(NotificationOptions model, GotifySettings settings)
{
await Run(model, settings, NotificationType.IssueResolved);
}
protected override async Task AddedToRequestQueue(NotificationOptions model, GotifySettings settings)
{
await Run(model, settings, NotificationType.ItemAddedToFaultQueue);
}
protected override async Task RequestDeclined(NotificationOptions model, GotifySettings settings)
{
await Run(model, settings, NotificationType.RequestDeclined);
}
protected override async Task RequestApproved(NotificationOptions model, GotifySettings settings)
{
await Run(model, settings, NotificationType.RequestApproved);
}
protected override async Task AvailableRequest(NotificationOptions model, GotifySettings settings)
{
await Run(model, settings, NotificationType.RequestAvailable);
}
protected override async Task Send(NotificationMessage model, GotifySettings settings)
{
try
{
await Api.PushAsync(settings.BaseUrl, settings.ApplicationToken, model.Subject, model.Message, settings.Priority);
}
catch (Exception e)
{
Logger.LogError(LoggingEvents.GotifyNotification, e, "Failed to send Gotify notification");
}
}
protected override async Task Test(NotificationOptions model, GotifySettings settings)
{
var message = $"This is a test from Ombi, if you can see this then we have successfully pushed a notification!";
var notification = new NotificationMessage
{
Message = message,
};
await Send(notification, settings);
}
private async Task Run(NotificationOptions model, GotifySettings settings, NotificationType type)
{
var parsed = await LoadTemplate(NotificationAgent.Gotify, type, model);
if (parsed.Disabled)
{
Logger.LogInformation($"Template {type} is disabled for {NotificationAgent.Gotify}");
return;
}
var notification = new NotificationMessage
{
Message = parsed.Message,
};
await Send(notification, settings);
}
}
}

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

@ -4,6 +4,7 @@ using EnsureThat;
using MailKit.Net.Smtp; using MailKit.Net.Smtp;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using MimeKit; using MimeKit;
using MimeKit.Utils;
using Ombi.Core.Settings; using Ombi.Core.Settings;
using Ombi.Helpers; using Ombi.Helpers;
using Ombi.Notifications.Models; using Ombi.Notifications.Models;
@ -37,6 +38,15 @@ namespace Ombi.Notifications
var customization = await CustomizationSettings.GetSettingsAsync(); var customization = await CustomizationSettings.GetSettingsAsync();
var html = email.LoadTemplate(model.Subject, model.Message, null, customization.Logo); var html = email.LoadTemplate(model.Subject, model.Message, null, customization.Logo);
var messageId = MimeUtils.GenerateMessageId();
if (customization.ApplicationUrl.HasValue())
{
if (Uri.TryCreate(customization.ApplicationUrl, UriKind.RelativeOrAbsolute, out var url))
{
messageId = MimeUtils.GenerateMessageId(url.IdnHost);
}
}
var textBody = string.Empty; var textBody = string.Empty;
@ -50,7 +60,8 @@ namespace Ombi.Notifications
var message = new MimeMessage var message = new MimeMessage
{ {
Body = body.ToMessageBody(), Body = body.ToMessageBody(),
Subject = model.Subject Subject = model.Subject,
MessageId = messageId
}; };
message.From.Add(new MailboxAddress(string.IsNullOrEmpty(settings.SenderName) ? settings.SenderAddress : settings.SenderName, settings.SenderAddress)); message.From.Add(new MailboxAddress(string.IsNullOrEmpty(settings.SenderName) ? settings.SenderAddress : settings.SenderName, settings.SenderAddress));
message.To.Add(new MailboxAddress(model.To, model.To)); message.To.Add(new MailboxAddress(model.To, model.To));

@ -15,6 +15,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.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 @@
namespace Ombi.Settings.Settings.Models.Notifications
{
public class GotifySettings : Settings
{
public bool Enabled { get; set; }
public string BaseUrl { get; set; }
public string ApplicationToken { get; set; }
public sbyte Priority { get; set; } = 4;
}
}

@ -5,7 +5,7 @@ using System.Linq.Expressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query;
using Nito.AsyncEx; using Ombi.Helpers;
using Ombi.Store.Context; using Ombi.Store.Context;
using Ombi.Store.Entities; using Ombi.Store.Entities;
@ -20,7 +20,6 @@ namespace Ombi.Store.Repository
} }
public DbSet<T> _db { get; } public DbSet<T> _db { get; }
private readonly U _ctx; private readonly U _ctx;
private readonly AsyncLock _mutex = new AsyncLock();
public async Task<T> Find(object key) public async Task<T> Find(object key)
{ {
@ -32,7 +31,7 @@ namespace Ombi.Store.Repository
return _db.AsQueryable(); return _db.AsQueryable();
} }
public async Task<T> FirstOrDefaultAsync(Expression<Func<T,bool>> predicate) public async Task<T> FirstOrDefaultAsync(Expression<Func<T, bool>> predicate)
{ {
return await _db.FirstOrDefaultAsync(predicate); return await _db.FirstOrDefaultAsync(predicate);
} }
@ -82,14 +81,11 @@ namespace Ombi.Store.Repository
await _ctx.Database.ExecuteSqlCommandAsync(sql); await _ctx.Database.ExecuteSqlCommandAsync(sql);
} }
private async Task<int> InternalSaveChanges() protected async Task<int> InternalSaveChanges()
{ {
using (await _mutex.LockAsync()) return await _ctx.SaveChangesAsync();
{
return await _ctx.SaveChangesAsync();
}
} }
private bool _disposed; private bool _disposed;
// Protected implementation of Dispose pattern. // Protected implementation of Dispose pattern.
@ -102,7 +98,7 @@ namespace Ombi.Store.Repository
{ {
_ctx?.Dispose(); _ctx?.Dispose();
} }
_disposed = true; _disposed = true;
} }

@ -72,7 +72,7 @@ namespace Ombi.Store.Repository
public async Task Update(EmbyContent existingContent) public async Task Update(EmbyContent existingContent)
{ {
Db.EmbyContent.Update(existingContent); Db.EmbyContent.Update(existingContent);
await Db.SaveChangesAsync(); await InternalSaveChanges();
} }
public IQueryable<EmbyEpisode> GetAllEpisodes() public IQueryable<EmbyEpisode> GetAllEpisodes()
@ -83,7 +83,7 @@ namespace Ombi.Store.Repository
public async Task<EmbyEpisode> Add(EmbyEpisode content) public async Task<EmbyEpisode> Add(EmbyEpisode content)
{ {
await Db.EmbyEpisode.AddAsync(content); await Db.EmbyEpisode.AddAsync(content);
await Db.SaveChangesAsync(); await InternalSaveChanges();
return content; return content;
} }
public async Task<EmbyEpisode> GetEpisodeByEmbyId(string key) public async Task<EmbyEpisode> GetEpisodeByEmbyId(string key)
@ -94,12 +94,13 @@ namespace Ombi.Store.Repository
public async Task AddRange(IEnumerable<EmbyEpisode> content) public async Task AddRange(IEnumerable<EmbyEpisode> content)
{ {
Db.EmbyEpisode.AddRange(content); Db.EmbyEpisode.AddRange(content);
await Db.SaveChangesAsync(); await InternalSaveChanges();
} }
public void UpdateWithoutSave(EmbyContent existingContent) public void UpdateWithoutSave(EmbyContent existingContent)
{ {
Db.EmbyContent.Update(existingContent); Db.EmbyContent.Update(existingContent);
} }
} }
} }

@ -45,7 +45,7 @@ namespace Ombi.Store.Repository
Db.Attach(template); Db.Attach(template);
Db.Entry(template).State = EntityState.Modified; Db.Entry(template).State = EntityState.Modified;
} }
await Db.SaveChangesAsync(); await InternalSaveChanges();
} }
public async Task UpdateRange(IEnumerable<NotificationTemplates> templates) public async Task UpdateRange(IEnumerable<NotificationTemplates> templates)
@ -56,16 +56,21 @@ namespace Ombi.Store.Repository
Db.Attach(t); Db.Attach(t);
Db.Entry(t).State = EntityState.Modified; Db.Entry(t).State = EntityState.Modified;
} }
await Db.SaveChangesAsync(); await InternalSaveChanges();
} }
public async Task<NotificationTemplates> Insert(NotificationTemplates entity) public async Task<NotificationTemplates> Insert(NotificationTemplates entity)
{ {
var settings = await Db.NotificationTemplates.AddAsync(entity).ConfigureAwait(false); var settings = await Db.NotificationTemplates.AddAsync(entity).ConfigureAwait(false);
await Db.SaveChangesAsync().ConfigureAwait(false); await InternalSaveChanges().ConfigureAwait(false);
return settings.Entity; return settings.Entity;
} }
private async Task<int> InternalSaveChanges()
{
return await Db.SaveChangesAsync();
}
private bool _disposed; private bool _disposed;
// Protected implementation of Dispose pattern. // Protected implementation of Dispose pattern.
protected virtual void Dispose(bool disposing) protected virtual void Dispose(bool disposing)

@ -96,7 +96,7 @@ namespace Ombi.Store.Repository
public async Task Update(PlexServerContent existingContent) public async Task Update(PlexServerContent existingContent)
{ {
Db.PlexServerContent.Update(existingContent); Db.PlexServerContent.Update(existingContent);
await Db.SaveChangesAsync(); await InternalSaveChanges();
} }
public void UpdateWithoutSave(PlexServerContent existingContent) public void UpdateWithoutSave(PlexServerContent existingContent)
{ {
@ -106,7 +106,7 @@ namespace Ombi.Store.Repository
public async Task UpdateRange(IEnumerable<PlexServerContent> existingContent) public async Task UpdateRange(IEnumerable<PlexServerContent> existingContent)
{ {
Db.PlexServerContent.UpdateRange(existingContent); Db.PlexServerContent.UpdateRange(existingContent);
await Db.SaveChangesAsync(); await InternalSaveChanges();
} }
public IQueryable<PlexEpisode> GetAllEpisodes() public IQueryable<PlexEpisode> GetAllEpisodes()
@ -127,14 +127,14 @@ namespace Ombi.Store.Repository
public async Task<PlexEpisode> Add(PlexEpisode content) public async Task<PlexEpisode> Add(PlexEpisode content)
{ {
await Db.PlexEpisode.AddAsync(content); await Db.PlexEpisode.AddAsync(content);
await Db.SaveChangesAsync(); await InternalSaveChanges();
return content; return content;
} }
public async Task DeleteEpisode(PlexEpisode content) public async Task DeleteEpisode(PlexEpisode content)
{ {
Db.PlexEpisode.Remove(content); Db.PlexEpisode.Remove(content);
await Db.SaveChangesAsync(); await InternalSaveChanges();
} }
public async Task<PlexEpisode> GetEpisodeByKey(int key) public async Task<PlexEpisode> GetEpisodeByKey(int key)
@ -144,7 +144,7 @@ namespace Ombi.Store.Repository
public async Task AddRange(IEnumerable<PlexEpisode> content) public async Task AddRange(IEnumerable<PlexEpisode> content)
{ {
Db.PlexEpisode.AddRange(content); Db.PlexEpisode.AddRange(content);
await Db.SaveChangesAsync(); await InternalSaveChanges();
} }
} }
} }

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Ombi.Helpers;
using Ombi.Store.Context; using Ombi.Store.Context;
using Ombi.Store.Entities.Requests; using Ombi.Store.Entities.Requests;
@ -70,12 +71,12 @@ namespace Ombi.Store.Repository.Requests
Db.MovieRequests.Attach(request); Db.MovieRequests.Attach(request);
Db.Update(request); Db.Update(request);
} }
await Db.SaveChangesAsync(); await InternalSaveChanges();
} }
public async Task Save() public async Task Save()
{ {
await Db.SaveChangesAsync(); await InternalSaveChanges();
} }
} }
} }

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Ombi.Helpers;
using Ombi.Store.Context; using Ombi.Store.Context;
using Ombi.Store.Entities.Requests; using Ombi.Store.Entities.Requests;
@ -61,12 +62,12 @@ namespace Ombi.Store.Repository.Requests
Db.AlbumRequests.Attach(request); Db.AlbumRequests.Attach(request);
Db.Update(request); Db.Update(request);
} }
await Db.SaveChangesAsync(); await InternalSaveChanges();
} }
public async Task Save() public async Task Save()
{ {
await Db.SaveChangesAsync(); await InternalSaveChanges();
} }
} }
} }

@ -2,6 +2,7 @@
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Ombi.Helpers;
using Ombi.Store.Context; using Ombi.Store.Context;
using Ombi.Store.Entities.Requests; using Ombi.Store.Entities.Requests;
@ -101,20 +102,20 @@ namespace Ombi.Store.Repository.Requests
public async Task Save() public async Task Save()
{ {
await Db.SaveChangesAsync(); await InternalSaveChanges();
} }
public async Task<TvRequests> Add(TvRequests request) public async Task<TvRequests> Add(TvRequests request)
{ {
await Db.TvRequests.AddAsync(request); await Db.TvRequests.AddAsync(request);
await Db.SaveChangesAsync(); await InternalSaveChanges();
return request; return request;
} }
public async Task<ChildRequests> AddChild(ChildRequests request) public async Task<ChildRequests> AddChild(ChildRequests request)
{ {
await Db.ChildRequests.AddAsync(request); await Db.ChildRequests.AddAsync(request);
await Db.SaveChangesAsync(); await InternalSaveChanges();
return request; return request;
} }
@ -122,33 +123,38 @@ namespace Ombi.Store.Repository.Requests
public async Task Delete(TvRequests request) public async Task Delete(TvRequests request)
{ {
Db.TvRequests.Remove(request); Db.TvRequests.Remove(request);
await Db.SaveChangesAsync(); await InternalSaveChanges();
} }
public async Task DeleteChild(ChildRequests request) public async Task DeleteChild(ChildRequests request)
{ {
Db.ChildRequests.Remove(request); Db.ChildRequests.Remove(request);
await Db.SaveChangesAsync(); await InternalSaveChanges();
} }
public async Task DeleteChildRange(IEnumerable<ChildRequests> request) public async Task DeleteChildRange(IEnumerable<ChildRequests> request)
{ {
Db.ChildRequests.RemoveRange(request); Db.ChildRequests.RemoveRange(request);
await Db.SaveChangesAsync(); await InternalSaveChanges();
} }
public async Task Update(TvRequests request) public async Task Update(TvRequests request)
{ {
Db.Update(request); Db.Update(request);
await Db.SaveChangesAsync(); await InternalSaveChanges();
} }
public async Task UpdateChild(ChildRequests request) public async Task UpdateChild(ChildRequests request)
{ {
Db.Update(request); Db.Update(request);
await Db.SaveChangesAsync(); await InternalSaveChanges();
}
private async Task<int> InternalSaveChanges()
{
return await Db.SaveChangesAsync();
} }
} }
} }

@ -62,14 +62,14 @@ namespace Ombi.Store.Repository
{ {
//_cache.Remove(GetName(entity.SettingsName)); //_cache.Remove(GetName(entity.SettingsName));
Db.Settings.Remove(entity); Db.Settings.Remove(entity);
await Db.SaveChangesAsync(); await InternalSaveChanges();
} }
public async Task UpdateAsync(GlobalSettings entity) public async Task UpdateAsync(GlobalSettings entity)
{ {
//_cache.Remove(GetName(entity.SettingsName)); //_cache.Remove(GetName(entity.SettingsName));
Db.Update(entity); Db.Update(entity);
await Db.SaveChangesAsync(); await InternalSaveChanges();
} }
public void Delete(GlobalSettings entity) public void Delete(GlobalSettings entity)
@ -91,6 +91,11 @@ namespace Ombi.Store.Repository
return $"{entity}Json"; return $"{entity}Json";
} }
private async Task<int> InternalSaveChanges()
{
return await Db.SaveChangesAsync();
}
private bool _disposed; private bool _disposed;
protected virtual void Dispose(bool disposing) protected virtual void Dispose(bool disposing)
{ {

@ -4,6 +4,7 @@ using Ombi.Store.Entities;
using System; using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Ombi.Helpers;
namespace Ombi.Store.Repository namespace Ombi.Store.Repository
{ {
@ -19,12 +20,16 @@ namespace Ombi.Store.Repository
public async Task CreateToken(Tokens token) public async Task CreateToken(Tokens token)
{ {
await Db.Tokens.AddAsync(token); await Db.Tokens.AddAsync(token);
await Db.SaveChangesAsync(); await InternalSaveChanges();
} }
public IQueryable<Tokens> GetToken(string tokenId) public IQueryable<Tokens> GetToken(string tokenId)
{ {
return Db.Tokens.Where(x => x.Token == tokenId); return Db.Tokens.Where(x => x.Token == tokenId);
} }
private async Task<int> InternalSaveChanges()
{
return await Db.SaveChangesAsync();
}
} }
} }

@ -98,6 +98,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Api.Lidarr", "Ombi.Api
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Helpers.Tests", "Ombi.Helpers.Tests\Ombi.Helpers.Tests.csproj", "{CC8CEFCD-0CB6-45BB-845F-508BCAB5BDC3}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Helpers.Tests", "Ombi.Helpers.Tests\Ombi.Helpers.Tests.csproj", "{CC8CEFCD-0CB6-45BB-845F-508BCAB5BDC3}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Api.Gotify", "Ombi.Api.Gotify\Ombi.Api.Gotify.csproj", "{105EA346-766E-45B8-928B-DE6991DCB7EB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ombi.Settings.Tests", "Ombi.Settings.Tests\Ombi.Settings.Tests.csproj", "{F3969B69-3B07-4884-A7AB-0BAB8B84DF94}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ombi.Settings.Tests", "Ombi.Settings.Tests\Ombi.Settings.Tests.csproj", "{F3969B69-3B07-4884-A7AB-0BAB8B84DF94}"
EndProject EndProject
Global Global
@ -258,6 +260,10 @@ Global
{CC8CEFCD-0CB6-45BB-845F-508BCAB5BDC3}.Debug|Any CPU.Build.0 = Debug|Any CPU {CC8CEFCD-0CB6-45BB-845F-508BCAB5BDC3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CC8CEFCD-0CB6-45BB-845F-508BCAB5BDC3}.Release|Any CPU.ActiveCfg = Release|Any CPU {CC8CEFCD-0CB6-45BB-845F-508BCAB5BDC3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CC8CEFCD-0CB6-45BB-845F-508BCAB5BDC3}.Release|Any CPU.Build.0 = Release|Any CPU {CC8CEFCD-0CB6-45BB-845F-508BCAB5BDC3}.Release|Any CPU.Build.0 = Release|Any CPU
{105EA346-766E-45B8-928B-DE6991DCB7EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{105EA346-766E-45B8-928B-DE6991DCB7EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{105EA346-766E-45B8-928B-DE6991DCB7EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{105EA346-766E-45B8-928B-DE6991DCB7EB}.Release|Any CPU.Build.0 = Release|Any CPU
{F3969B69-3B07-4884-A7AB-0BAB8B84DF94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F3969B69-3B07-4884-A7AB-0BAB8B84DF94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F3969B69-3B07-4884-A7AB-0BAB8B84DF94}.Debug|Any CPU.Build.0 = Debug|Any CPU {F3969B69-3B07-4884-A7AB-0BAB8B84DF94}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F3969B69-3B07-4884-A7AB-0BAB8B84DF94}.Release|Any CPU.ActiveCfg = Release|Any CPU {F3969B69-3B07-4884-A7AB-0BAB8B84DF94}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -299,6 +305,7 @@ Global
{10D1FE9D-9124-42B7-B1E1-CEB99B832618} = {9293CA11-360A-4C20-A674-B9E794431BF5} {10D1FE9D-9124-42B7-B1E1-CEB99B832618} = {9293CA11-360A-4C20-A674-B9E794431BF5}
{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}
{F3969B69-3B07-4884-A7AB-0BAB8B84DF94} = {6F42AB98-9196-44C4-B888-D5E409F415A1} {F3969B69-3B07-4884-A7AB-0BAB8B84DF94} = {6F42AB98-9196-44C4-B888-D5E409F415A1}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution

@ -9,5 +9,6 @@
"typescript.tsdk": "node_modules\\typescript\\lib", "typescript.tsdk": "node_modules\\typescript\\lib",
"cSpell.words": [ "cSpell.words": [
"usermanagement" "usermanagement"
] ],
"discord.enabled": true
} }

@ -93,6 +93,14 @@ export interface IPushoverNotificationSettings extends INotificationSettings {
sound: string; sound: string;
} }
export interface IGotifyNotificationSettings extends INotificationSettings {
accessToken: string;
notificationTemplates: INotificationTemplates[];
baseUrl: string;
applicationToken: string;
priority: number;
}
export interface IMattermostNotifcationSettings extends INotificationSettings { export interface IMattermostNotifcationSettings extends INotificationSettings {
webhookUrl: string; webhookUrl: string;
username: string; username: string;

@ -80,7 +80,7 @@
<i class="fa fa-check"></i> {{ 'Common.Available' | translate }}</button> <i class="fa fa-check"></i> {{ 'Common.Available' | translate }}</button>
</div> </div>
<div *ngIf="!result.fullyAvailable"> <div *ngIf="!result.fullyAvailable">
<div *ngIf="result.requested || result.approved; then requestedBtn else notRequestedBtn"></div> <div *ngIf="result.requested || result.approved || result.monitored; then requestedBtn else notRequestedBtn"></div>
<ng-template #requestedBtn> <ng-template #requestedBtn>
<button style="text-align: right" class="btn btn-primary-outline disabled" [disabled]> <button style="text-align: right" class="btn btn-primary-outline disabled" [disabled]>
<i class="fa fa-check"></i> {{ 'Common.Requested' | translate }}</button> <i class="fa fa-check"></i> {{ 'Common.Requested' | translate }}</button>

@ -11,6 +11,7 @@ import {
IDiscordNotifcationSettings, IDiscordNotifcationSettings,
IEmailNotificationSettings, IEmailNotificationSettings,
IEmbyServer, IEmbyServer,
IGotifyNotificationSettings,
ILidarrSettings, ILidarrSettings,
IMattermostNotifcationSettings, IMattermostNotifcationSettings,
IMobileNotificationTestSettings, IMobileNotificationTestSettings,
@ -40,7 +41,11 @@ export class TesterService extends ServiceHelpers {
} }
public pushoverTest(settings: IPushoverNotificationSettings): Observable<boolean> { public pushoverTest(settings: IPushoverNotificationSettings): Observable<boolean> {
return this.http.post<boolean>(`${this.url}pushover`, JSON.stringify(settings), {headers: this.headers}); return this.http.post<boolean>(`${this.url}pushover`, JSON.stringify(settings), { headers: this.headers });
}
public gotifyTest(settings: IGotifyNotificationSettings): Observable<boolean> {
return this.http.post<boolean>(`${this.url}gotify`, JSON.stringify(settings), { headers: this.headers });
} }
public mattermostTest(settings: IMattermostNotifcationSettings): Observable<boolean> { public mattermostTest(settings: IMattermostNotifcationSettings): Observable<boolean> {

@ -14,6 +14,7 @@ import {
IDogNzbSettings, IDogNzbSettings,
IEmailNotificationSettings, IEmailNotificationSettings,
IEmbySettings, IEmbySettings,
IGotifyNotificationSettings,
IIssueSettings, IIssueSettings,
IJobSettings, IJobSettings,
IJobSettingsViewModel, IJobSettingsViewModel,
@ -182,6 +183,14 @@ export class SettingsService extends ServiceHelpers {
.post<boolean>(`${this.url}/notifications/pushover`, JSON.stringify(settings), {headers: this.headers}); .post<boolean>(`${this.url}/notifications/pushover`, JSON.stringify(settings), {headers: this.headers});
} }
public getGotifyNotificationSettings(): Observable<IGotifyNotificationSettings> {
return this.http.get<IGotifyNotificationSettings>(`${this.url}/notifications/gotify`, { headers: this.headers });
}
public saveGotifyNotificationSettings(settings: IGotifyNotificationSettings): Observable<boolean> {
return this.http
.post<boolean>(`${this.url}/notifications/gotify`, 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});
} }

@ -0,0 +1,67 @@

<settings-menu></settings-menu>
<div *ngIf="form">
<fieldset>
<legend>Gotify 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="baseUrl" name="baseUrl" [ngClass]="{'form-error': form.get('baseUrl').hasError('required')}" formControlName="baseUrl" pTooltip="Enter the URL of your gotify server.">
<small *ngIf="form.get('baseUrl').hasError('required')" class="error-text">The Base URL is required</small>
</div>
<div class="form-group">
<label for="applicationToken" class="control-label">Application Token</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 Gotify.">
<small *ngIf="form.get('applicationToken').hasError('required')" class="error-text">The Application Token is required</small>
</div>
<div class="form-group">
<label for="priority" class="control-label">Priority</label>
<div>
<select class="form-control form-control-custom " id="priority" name="priority" formControlName="priority" pTooltip="The priority you want your gotify notifications sent as.">
<option value="4">Normal</option>
<option value="8">High</option>
<option value="2">Low</option>
<option value="0">Lowest</option>
</select>
</div>
</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>
<div class="col-md-6">
<notification-templates [templates]="templates" [showSubject]="false"></notification-templates>
</div>
</fieldset>
</div>

@ -0,0 +1,68 @@
import { Component, OnInit } from "@angular/core";
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { IGotifyNotificationSettings, INotificationTemplates, NotificationType } from "../../interfaces";
import { TesterService } from "../../services";
import { NotificationService } from "../../services";
import { SettingsService } from "../../services";
@Component({
templateUrl: "./gotify.component.html",
})
export class GotifyComponent 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.getGotifyNotificationSettings().subscribe(x => {
this.templates = x.notificationTemplates;
this.form = this.fb.group({
enabled: [x.enabled],
baseUrl: [x.baseUrl, [Validators.required]],
applicationToken: [x.applicationToken, [Validators.required]],
priority: [x.priority],
});
});
}
public onSubmit(form: FormGroup) {
if (form.invalid) {
this.notificationService.error("Please check your entered values");
return;
}
const settings = <IGotifyNotificationSettings> form.value;
settings.notificationTemplates = this.templates;
this.settingsService.saveGotifyNotificationSettings(settings).subscribe(x => {
if (x) {
this.notificationService.success("Successfully saved the Gotify settings");
} else {
this.notificationService.success("There was an error when saving the Gotify settings");
}
});
}
public test(form: FormGroup) {
if (form.invalid) {
this.notificationService.error("Please check your entered values");
return;
}
this.testerService.gotifyTest(form.value).subscribe(x => {
if (x) {
this.notificationService.success("Successfully sent a Gotify message");
} else {
this.notificationService.error("There was an error when sending the Gotify message. Please check your settings");
}
});
}
}

@ -27,6 +27,7 @@ import { LidarrComponent } from "./lidarr/lidarr.component";
import { MassEmailComponent } from "./massemail/massemail.component"; import { MassEmailComponent } from "./massemail/massemail.component";
import { DiscordComponent } from "./notifications/discord.component"; import { DiscordComponent } from "./notifications/discord.component";
import { EmailNotificationComponent } from "./notifications/emailnotification.component"; import { EmailNotificationComponent } from "./notifications/emailnotification.component";
import { GotifyComponent } from "./notifications/gotify.component";
import { MattermostComponent } from "./notifications/mattermost.component"; import { MattermostComponent } from "./notifications/mattermost.component";
import { MobileComponent } from "./notifications/mobile.component"; import { MobileComponent } from "./notifications/mobile.component";
import { NewsletterComponent } from "./notifications/newsletter.component"; import { NewsletterComponent } from "./notifications/newsletter.component";
@ -65,6 +66,7 @@ const routes: Routes = [
{ path: "Slack", component: SlackComponent, canActivate: [AuthGuard] }, { path: "Slack", component: SlackComponent, canActivate: [AuthGuard] },
{ 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: "Mattermost", component: MattermostComponent, canActivate: [AuthGuard] }, { path: "Mattermost", component: MattermostComponent, canActivate: [AuthGuard] },
{ path: "UserManagement", component: UserManagementComponent, canActivate: [AuthGuard] }, { path: "UserManagement", component: UserManagementComponent, canActivate: [AuthGuard] },
{ path: "Update", component: UpdateComponent, canActivate: [AuthGuard] }, { path: "Update", component: UpdateComponent, canActivate: [AuthGuard] },
@ -121,6 +123,7 @@ const routes: Routes = [
PushoverComponent, PushoverComponent,
MattermostComponent, MattermostComponent,
PushbulletComponent, PushbulletComponent,
GotifyComponent,
UserManagementComponent, UserManagementComponent,
UpdateComponent, UpdateComponent,
AboutComponent, AboutComponent,

@ -47,6 +47,7 @@
<button mat-menu-item [routerLink]="['/Settings/Pushover']">Pushover</button> <button mat-menu-item [routerLink]="['/Settings/Pushover']">Pushover</button>
<button mat-menu-item [routerLink]="['/Settings/Mattermost']">Mattermost</button> <button mat-menu-item [routerLink]="['/Settings/Mattermost']">Mattermost</button>
<button mat-menu-item [routerLink]="['/Settings/Telegram']">Telegram</button> <button mat-menu-item [routerLink]="['/Settings/Telegram']">Telegram</button>
<button mat-menu-item [routerLink]="['/Settings/Gotify']">Gotify</button>
</mat-menu> </mat-menu>
<button mat-button [matMenuTriggerFor]="systemMenu"><i class="fa fa-tachometer" aria-hidden="true"></i> System</button> <button mat-button [matMenuTriggerFor]="systemMenu"><i class="fa fa-tachometer" aria-hidden="true"></i> System</button>

@ -40,7 +40,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) ILidarrApi lidarrApi, IGotifyNotification gotifyNotification)
{ {
Service = service; Service = service;
DiscordNotification = notification; DiscordNotification = notification;
@ -61,6 +61,7 @@ namespace Ombi.Controllers.V1.External
Newsletter = newsletter; Newsletter = newsletter;
MobileNotification = mobileNotification; MobileNotification = mobileNotification;
LidarrApi = lidarrApi; LidarrApi = lidarrApi;
GotifyNotification = gotifyNotification;
} }
private INotificationService Service { get; } private INotificationService Service { get; }
@ -69,6 +70,7 @@ namespace Ombi.Controllers.V1.External
private IPushbulletNotification PushbulletNotification { get; } private IPushbulletNotification PushbulletNotification { get; }
private ISlackNotification SlackNotification { get; } private ISlackNotification SlackNotification { get; }
private IPushoverNotification PushoverNotification { get; } private IPushoverNotification PushoverNotification { get; }
private IGotifyNotification GotifyNotification { 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; }
@ -155,6 +157,30 @@ namespace Ombi.Controllers.V1.External
} }
/// <summary>
/// Sends a test message to Gotify using the provided settings
/// </summary>
/// <param name="settings">The settings.</param>
/// <returns></returns>
[HttpPost("gotify")]
public bool Gotify([FromBody] GotifySettings settings)
{
try
{
settings.Enabled = true;
GotifyNotification.NotifyAsync(
new NotificationOptions { NotificationType = NotificationType.Test, RequestId = -1 }, settings);
return true;
}
catch (Exception e)
{
Log.LogError(LoggingEvents.Api, e, "Could not test Gotify");
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>

@ -968,6 +968,40 @@ namespace Ombi.Controllers.V1
return model; return model;
} }
/// <summary>
/// Saves the gotify notification settings.
/// </summary>
/// <param name="model">The model.</param>
/// <returns></returns>
[HttpPost("notifications/gotify")]
public async Task<bool> GotifyNotificationSettings([FromBody] GotifyNotificationViewModel model)
{
// Save the email settings
var settings = Mapper.Map<GotifySettings>(model);
var result = await Save(settings);
// Save the templates
await TemplateRepository.UpdateRange(model.NotificationTemplates);
return result;
}
/// <summary>
/// Gets the gotify Notification Settings.
/// </summary>
/// <returns></returns>
[HttpGet("notifications/gotify")]
public async Task<GotifyNotificationViewModel> GotifyNotificationSettings()
{
var settings = await Get<GotifySettings>();
var model = Mapper.Map<GotifyNotificationViewModel>(settings);
// Lookup to see if we have any templates saved
model.NotificationTemplates = BuildTemplates(NotificationAgent.Gotify);
return model;
}
/// <summary> /// <summary>
/// Saves the Newsletter notification settings. /// Saves the Newsletter notification settings.
/// </summary> /// </summary>

@ -49,6 +49,7 @@ namespace Ombi
demoInstance.Demo = demo; demoInstance.Demo = demo;
instance.StoragePath = storagePath ?? string.Empty; instance.StoragePath = storagePath ?? string.Empty;
// Check if we need to migrate the settings // Check if we need to migrate the settings
DeleteSchedules();
CheckAndMigrate(); CheckAndMigrate();
var ctx = new SettingsContext(); var ctx = new SettingsContext();
var config = ctx.ApplicationConfigurations.ToList(); var config = ctx.ApplicationConfigurations.ToList();
@ -97,6 +98,20 @@ namespace Ombi
CreateWebHostBuilder(args).Build().Run(); CreateWebHostBuilder(args).Build().Run();
} }
private static void DeleteSchedules()
{
try
{
if (File.Exists("Schedules.db"))
{
File.Delete("Schedules.db");
}
}
catch (Exception)
{
}
}
/// <summary> /// <summary>
/// This is to remove the Settings from the Ombi.db to the "new" /// This is to remove the Settings from the Ombi.db to the "new"
/// OmbiSettings.db /// OmbiSettings.db
@ -115,7 +130,7 @@ namespace Ombi
try try
{ {
if (ombi.Settings.Any()) if (ombi.Settings.Any() && !settings.Settings.Any())
{ {
// OK migrate it! // OK migrate it!
var allSettings = ombi.Settings.ToList(); var allSettings = ombi.Settings.ToList();
@ -125,7 +140,7 @@ namespace Ombi
// Check for any application settings // Check for any application settings
if (ombi.ApplicationConfigurations.Any()) if (ombi.ApplicationConfigurations.Any() && !settings.ApplicationConfigurations.Any())
{ {
// OK migrate it! // OK migrate it!
var allSettings = ombi.ApplicationConfigurations.ToList(); var allSettings = ombi.ApplicationConfigurations.ToList();

Loading…
Cancel
Save