This commit is contained in:
tidusjar 2019-03-27 10:41:20 +00:00
commit 8febad53b0
47 changed files with 632 additions and 139 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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>();
@ -140,28 +142,28 @@ namespace Ombi.DependencyInjection
} }
public static void RegisterStore(this IServiceCollection services) { public static void RegisterStore(this IServiceCollection services) {
services.AddEntityFrameworkSqlite().AddDbContext<OmbiContext>(); services.AddDbContext<OmbiContext>();
services.AddEntityFrameworkSqlite().AddDbContext<SettingsContext>(); services.AddDbContext<SettingsContext>();
services.AddEntityFrameworkSqlite().AddDbContext<ExternalContext>(); services.AddDbContext<ExternalContext>();
services.AddScoped<IOmbiContext, OmbiContext>(); // https://docs.microsoft.com/en-us/aspnet/core/data/entity-framework-6 services.AddScoped<IOmbiContext, OmbiContext>(); // https://docs.microsoft.com/en-us/aspnet/core/data/entity-framework-6
services.AddScoped<ISettingsContext, SettingsContext>(); // https://docs.microsoft.com/en-us/aspnet/core/data/entity-framework-6 services.AddScoped<ISettingsContext, SettingsContext>(); // https://docs.microsoft.com/en-us/aspnet/core/data/entity-framework-6
services.AddScoped<IExternalContext, ExternalContext>(); // https://docs.microsoft.com/en-us/aspnet/core/data/entity-framework-6 services.AddScoped<IExternalContext, ExternalContext>(); // https://docs.microsoft.com/en-us/aspnet/core/data/entity-framework-6
services.AddTransient<ISettingsRepository, SettingsJsonRepository>(); services.AddScoped<ISettingsRepository, SettingsJsonRepository>();
services.AddTransient<ISettingsResolver, SettingsResolver>(); services.AddScoped<ISettingsResolver, SettingsResolver>();
services.AddTransient<IPlexContentRepository, PlexServerContentRepository>(); services.AddScoped<IPlexContentRepository, PlexServerContentRepository>();
services.AddTransient<IEmbyContentRepository, EmbyContentRepository>(); services.AddScoped<IEmbyContentRepository, EmbyContentRepository>();
services.AddTransient<INotificationTemplatesRepository, NotificationTemplatesRepository>(); services.AddScoped<INotificationTemplatesRepository, NotificationTemplatesRepository>();
services.AddTransient<ITvRequestRepository, TvRequestRepository>(); services.AddScoped<ITvRequestRepository, TvRequestRepository>();
services.AddTransient<IMovieRequestRepository, MovieRequestRepository>(); services.AddScoped<IMovieRequestRepository, MovieRequestRepository>();
services.AddTransient<IMusicRequestRepository, MusicRequestRepository>(); services.AddScoped<IMusicRequestRepository, MusicRequestRepository>();
services.AddTransient<IAuditRepository, AuditRepository>(); services.AddScoped<IAuditRepository, AuditRepository>();
services.AddTransient<IApplicationConfigRepository, ApplicationConfigRepository>(); services.AddScoped<IApplicationConfigRepository, ApplicationConfigRepository>();
services.AddTransient<ITokenRepository, TokenRepository>(); services.AddScoped<ITokenRepository, TokenRepository>();
services.AddTransient(typeof(ISettingsService<>), typeof(SettingsService<>)); services.AddScoped(typeof(ISettingsService<>), typeof(SettingsService<>));
services.AddTransient(typeof(IRepository<>), typeof(Repository<>)); services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
services.AddTransient(typeof(IExternalRepository<>), typeof(ExternalRepository<>)); services.AddScoped(typeof(IExternalRepository<>), typeof(ExternalRepository<>));
} }
public static void RegisterServices(this IServiceCollection services) public static void RegisterServices(this IServiceCollection services)
{ {
@ -169,7 +171,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<INotificationService, NotificationService>(); services.AddTransient<INotificationService, NotificationService>();
services.AddTransient<IEmailProvider, GenericEmailProvider>(); services.AddTransient<IEmailProvider, GenericEmailProvider>();
services.AddTransient<INotificationHelper, NotificationHelper>(); services.AddTransient<INotificationHelper, NotificationHelper>();
services.AddTransient<ICacheService, CacheService>(); services.AddSingleton<ICacheService, CacheService>();
services.AddTransient<IDiscordNotification, DiscordNotification>(); services.AddTransient<IDiscordNotification, DiscordNotification>();
services.AddTransient<IEmailNotification, EmailNotification>(); services.AddTransient<IEmailNotification, EmailNotification>();
@ -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>();

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -17,6 +17,7 @@ namespace Ombi.Store.Context
{ {
if (_created) return; if (_created) return;
_created = true; _created = true;
Database.SetCommandTimeout(60); Database.SetCommandTimeout(60);
Database.Migrate(); Database.Migrate();

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -61,9 +61,6 @@ namespace Ombi
// This method gets called by the runtime. Use this method to add services to the container. // This method gets called by the runtime. Use this method to add services to the container.
public IServiceProvider ConfigureServices(IServiceCollection services) public IServiceProvider ConfigureServices(IServiceCollection services)
{ {
// Add framework services.
services.AddDbContext<OmbiContext>();
services.AddIdentity<OmbiUser, IdentityRole>() services.AddIdentity<OmbiUser, IdentityRole>()
.AddEntityFrameworkStores<OmbiContext>() .AddEntityFrameworkStores<OmbiContext>()
.AddDefaultTokenProviders() .AddDefaultTokenProviders()

View file

@ -1766,9 +1766,9 @@
} }
}, },
"bootstrap": { "bootstrap": {
"version": "3.4.0", "version": "3.4.1",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-3.4.0.tgz", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-3.4.1.tgz",
"integrity": "sha512-F1yTDO9OHKH0xjl03DsOe8Nu1OWBIeAugGMhy3UTIYDdbbIPesQIhCEbj+HEr6wqlwweGAlP8F3OBC6kEuhFuw==" "integrity": "sha512-yN5oZVmRCwe5aKwzRj6736nSmKDX7pLYwsXiCj/EYmo16hODaBiT4En5btW/jhBF/seV+XMx3aYwukYC3A49DA=="
}, },
"bootswatch": { "bootswatch": {
"version": "3.4.0", "version": "3.4.0",
@ -4190,8 +4190,7 @@
}, },
"ansi-regex": { "ansi-regex": {
"version": "2.1.1", "version": "2.1.1",
"bundled": true, "bundled": true
"optional": true
}, },
"aproba": { "aproba": {
"version": "1.2.0", "version": "1.2.0",
@ -4209,13 +4208,11 @@
}, },
"balanced-match": { "balanced-match": {
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true
"optional": true
}, },
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@ -4228,18 +4225,15 @@
}, },
"code-point-at": { "code-point-at": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true
"optional": true
}, },
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"bundled": true, "bundled": true
"optional": true
}, },
"console-control-strings": { "console-control-strings": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true
"optional": true
}, },
"core-util-is": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
@ -4342,8 +4336,7 @@
}, },
"inherits": { "inherits": {
"version": "2.0.3", "version": "2.0.3",
"bundled": true, "bundled": true
"optional": true
}, },
"ini": { "ini": {
"version": "1.3.5", "version": "1.3.5",
@ -4353,7 +4346,6 @@
"is-fullwidth-code-point": { "is-fullwidth-code-point": {
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"number-is-nan": "^1.0.0" "number-is-nan": "^1.0.0"
} }
@ -4366,20 +4358,17 @@
"minimatch": { "minimatch": {
"version": "3.0.4", "version": "3.0.4",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }
}, },
"minimist": { "minimist": {
"version": "0.0.8", "version": "0.0.8",
"bundled": true, "bundled": true
"optional": true
}, },
"minipass": { "minipass": {
"version": "2.3.5", "version": "2.3.5",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"safe-buffer": "^5.1.2", "safe-buffer": "^5.1.2",
"yallist": "^3.0.0" "yallist": "^3.0.0"
@ -4396,7 +4385,6 @@
"mkdirp": { "mkdirp": {
"version": "0.5.1", "version": "0.5.1",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"minimist": "0.0.8" "minimist": "0.0.8"
} }
@ -4469,8 +4457,7 @@
}, },
"number-is-nan": { "number-is-nan": {
"version": "1.0.1", "version": "1.0.1",
"bundled": true, "bundled": true
"optional": true
}, },
"object-assign": { "object-assign": {
"version": "4.1.1", "version": "4.1.1",
@ -4480,7 +4467,6 @@
"once": { "once": {
"version": "1.4.0", "version": "1.4.0",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"wrappy": "1" "wrappy": "1"
} }
@ -4556,8 +4542,7 @@
}, },
"safe-buffer": { "safe-buffer": {
"version": "5.1.2", "version": "5.1.2",
"bundled": true, "bundled": true
"optional": true
}, },
"safer-buffer": { "safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
@ -4587,7 +4572,6 @@
"string-width": { "string-width": {
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"code-point-at": "^1.0.0", "code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0", "is-fullwidth-code-point": "^1.0.0",
@ -4605,7 +4589,6 @@
"strip-ansi": { "strip-ansi": {
"version": "3.0.1", "version": "3.0.1",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"ansi-regex": "^2.0.0" "ansi-regex": "^2.0.0"
} }
@ -4644,13 +4627,11 @@
}, },
"wrappy": { "wrappy": {
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true
"optional": true
}, },
"yallist": { "yallist": {
"version": "3.0.3", "version": "3.0.3",
"bundled": true, "bundled": true
"optional": true
} }
} }
}, },