diff --git a/src/Ombi.Core/Models/UI/MobileNotificationsViewModel.cs b/src/Ombi.Core/Models/UI/MobileNotificationsViewModel.cs new file mode 100644 index 000000000..b5f8969e9 --- /dev/null +++ b/src/Ombi.Core/Models/UI/MobileNotificationsViewModel.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using Ombi.Settings.Settings.Models.Notifications; +using Ombi.Store.Entities; + +namespace Ombi.Core.Models.UI +{ + public class MobileNotificationsViewModel : MobileNotificationSettings + { + /// + /// Gets or sets the notification templates. + /// + /// + /// The notification templates. + /// + public List NotificationTemplates { get; set; } + } +} diff --git a/src/Ombi.Mapping/Profiles/SettingsProfile.cs b/src/Ombi.Mapping/Profiles/SettingsProfile.cs index 8ff2200ba..62232ee19 100644 --- a/src/Ombi.Mapping/Profiles/SettingsProfile.cs +++ b/src/Ombi.Mapping/Profiles/SettingsProfile.cs @@ -17,6 +17,7 @@ namespace Ombi.Mapping.Profiles CreateMap().ReverseMap(); CreateMap().ReverseMap(); CreateMap().ReverseMap(); + CreateMap().ReverseMap(); } } } \ No newline at end of file diff --git a/src/Ombi.Notifications/Agents/EmailNotification.cs b/src/Ombi.Notifications/Agents/EmailNotification.cs index 8cec9f164..52dfb47ac 100644 --- a/src/Ombi.Notifications/Agents/EmailNotification.cs +++ b/src/Ombi.Notifications/Agents/EmailNotification.cs @@ -120,8 +120,10 @@ namespace Ombi.Notifications.Agents var plaintext = await LoadPlainTextMessage(NotificationType.IssueResolved, model, settings); message.Other.Add("PlainTextBody", plaintext); - // Issues should be sent to admin - message.To = settings.AdminEmail; + // Issues resolved should be sent to the user + message.To = model.RequestType == RequestType.Movie + ? MovieRequest.RequestedUser.Email + : TvRequest.RequestedUser.Email; await Send(message, settings); } diff --git a/src/Ombi.Notifications/Agents/MobileNotification.cs b/src/Ombi.Notifications/Agents/MobileNotification.cs index 5074226c8..56a58d8b8 100644 --- a/src/Ombi.Notifications/Agents/MobileNotification.cs +++ b/src/Ombi.Notifications/Agents/MobileNotification.cs @@ -39,7 +39,7 @@ namespace Ombi.Notifications.Agents protected override bool ValidateConfiguration(MobileNotificationSettings settings) { - return false; + return true; } protected override async Task NewRequest(NotificationOptions model, MobileNotificationSettings settings) @@ -56,9 +56,7 @@ namespace Ombi.Notifications.Agents }; // Get admin devices - var adminUsers = (await _userManager.GetUsersInRoleAsync(OmbiRoles.Admin)).Select(x => x.Id).ToList(); - var notificationUsers = _notifications.GetAll().Include(x => x.User).Where(x => adminUsers.Contains(x.UserId)); - var playerIds = await notificationUsers.Select(x => x.PlayerId).ToListAsync(); + var playerIds = await GetAdmins(NotificationType.NewRequest); await Send(playerIds, notification, settings); } @@ -74,8 +72,10 @@ namespace Ombi.Notifications.Agents { Message = parsed.Message, }; - notification.Other.Add("image", parsed.Image); - await Send(notification, settings); + + // Get admin devices + var playerIds = await GetAdmins(NotificationType.Issue); + await Send(playerIds, notification, settings); } protected override async Task IssueResolved(NotificationOptions model, MobileNotificationSettings settings) @@ -90,34 +90,36 @@ namespace Ombi.Notifications.Agents { Message = parsed.Message, }; - notification.Other.Add("image", parsed.Image); - await Send(notification, settings); + + // Send to user + var playerIds = GetUsers(model, NotificationType.IssueResolved); + + await Send(playerIds, notification, settings); } + protected override async Task AddedToRequestQueue(NotificationOptions model, MobileNotificationSettings settings) { - var user = string.Empty; - var title = string.Empty; - var image = string.Empty; + string user; + string title; if (model.RequestType == RequestType.Movie) { user = MovieRequest.RequestedUser.UserAlias; title = MovieRequest.Title; - image = MovieRequest.PosterPath; } else { user = TvRequest.RequestedUser.UserAlias; title = TvRequest.ParentRequest.Title; - image = TvRequest.ParentRequest.PosterPath; } var message = $"Hello! The user '{user}' has requested {title} but it could not be added. This has been added into the requests queue and will keep retrying"; var notification = new NotificationMessage { Message = message }; - notification.Other.Add("image", image); - await Send(notification, settings); + // Get admin devices + var playerIds = await GetAdmins(NotificationType.Test); + await Send(playerIds, notification, settings); } protected override async Task RequestDeclined(NotificationOptions model, MobileNotificationSettings settings) @@ -132,8 +134,10 @@ namespace Ombi.Notifications.Agents { Message = parsed.Message, }; - notification.Other.Add("image", parsed.Image); - await Send(notification, settings); + + // Send to user + var playerIds = GetUsers(model, NotificationType.RequestDeclined); + await Send(playerIds, notification, settings); } protected override async Task RequestApproved(NotificationOptions model, MobileNotificationSettings settings) @@ -149,8 +153,9 @@ namespace Ombi.Notifications.Agents Message = parsed.Message, }; - notification.Other.Add("image", parsed.Image); - await Send(notification, settings); + // Send to user + var playerIds = GetUsers(model, NotificationType.RequestApproved); + await Send(playerIds, notification, settings); } protected override async Task AvailableRequest(NotificationOptions model, MobileNotificationSettings settings) @@ -165,8 +170,9 @@ namespace Ombi.Notifications.Agents { Message = parsed.Message, }; - notification.Other.Add("image", parsed.Image); - await Send(notification, settings); + // Send to user + var playerIds = GetUsers(model, NotificationType.RequestAvailable); + await Send(playerIds, notification, settings); } protected override Task Send(NotificationMessage model, MobileNotificationSettings settings) { @@ -175,6 +181,10 @@ namespace Ombi.Notifications.Agents protected async Task Send(List playerIds, NotificationMessage model, MobileNotificationSettings settings) { + if (!playerIds.Any()) + { + return; + } var response = await _api.PushNotification(playerIds, model.Message); _logger.LogDebug("Sent message to {0} recipients with message id {1}", response.recipients, response.id); } @@ -186,7 +196,39 @@ namespace Ombi.Notifications.Agents { Message = message, }; - await Send(notification, settings); + // Send to user + var playerIds = await GetAdmins(NotificationType.RequestAvailable); + await Send(playerIds, notification, settings); + } + + private async Task> GetAdmins(NotificationType type) + { + var adminUsers = (await _userManager.GetUsersInRoleAsync(OmbiRoles.Admin)).Select(x => x.Id).ToList(); + var notificationUsers = _notifications.GetAll().Include(x => x.User).Where(x => adminUsers.Contains(x.UserId)); + var playerIds = await notificationUsers.Select(x => x.PlayerId).ToListAsync(); + if (!playerIds.Any()) + { + _logger.LogInformation( + $"there are no admins to send a notification for {type}, for agent {NotificationAgent.Mobile}"); + return null; + } + return playerIds; + } + private List GetUsers(NotificationOptions model, NotificationType type) + { + var notificationIds = model.RequestType == RequestType.Movie + ? MovieRequest.RequestedUser.NotificationUserIds + : TvRequest.RequestedUser.NotificationUserIds; + if (!notificationIds.Any()) + { + _logger.LogInformation( + $"there are no admins to send a notification for {type}, for agent {NotificationAgent.Mobile}"); + return null; + } + var playerIds = notificationIds.Select(x => x.PlayerId).ToList(); + return playerIds; } + + } } \ No newline at end of file diff --git a/src/Ombi.Store/Repository/Requests/MovieRequestRepository.cs b/src/Ombi.Store/Repository/Requests/MovieRequestRepository.cs index 2c2cd05f0..19a89a835 100644 --- a/src/Ombi.Store/Repository/Requests/MovieRequestRepository.cs +++ b/src/Ombi.Store/Repository/Requests/MovieRequestRepository.cs @@ -44,6 +44,7 @@ namespace Ombi.Store.Repository.Requests { return Db.MovieRequests .Include(x => x.RequestedUser) + .ThenInclude(x => x.NotificationUserIds) .AsQueryable(); } diff --git a/src/Ombi/ClientApp/app/interfaces/INotificationSettings.ts b/src/Ombi/ClientApp/app/interfaces/INotificationSettings.ts index a635c4e40..37fb1bc7a 100644 --- a/src/Ombi/ClientApp/app/interfaces/INotificationSettings.ts +++ b/src/Ombi/ClientApp/app/interfaces/INotificationSettings.ts @@ -87,3 +87,7 @@ export interface IMattermostNotifcationSettings extends INotificationSettings { iconUrl: string; notificationTemplates: INotificationTemplates[]; } + +export interface IMobileNotifcationSettings extends INotificationSettings { + notificationTemplates: INotificationTemplates[]; +} diff --git a/src/Ombi/ClientApp/app/interfaces/IUser.ts b/src/Ombi/ClientApp/app/interfaces/IUser.ts index 8cad85fb0..454bd8457 100644 --- a/src/Ombi/ClientApp/app/interfaces/IUser.ts +++ b/src/Ombi/ClientApp/app/interfaces/IUser.ts @@ -44,3 +44,8 @@ export interface IResetPasswordToken { token: string; password: string; } + +export interface IMobileUsersViewModel { + username: string; + devices: number; +} diff --git a/src/Ombi/ClientApp/app/services/index.ts b/src/Ombi/ClientApp/app/services/index.ts index 2ebe5415c..b5d7372a6 100644 --- a/src/Ombi/ClientApp/app/services/index.ts +++ b/src/Ombi/ClientApp/app/services/index.ts @@ -11,3 +11,4 @@ export * from "./settings.service"; export * from "./status.service"; export * from "./job.service"; export * from "./issues.service"; +export * from "./mobile.service"; diff --git a/src/Ombi/ClientApp/app/services/mobile.service.ts b/src/Ombi/ClientApp/app/services/mobile.service.ts new file mode 100644 index 000000000..04d2a6b36 --- /dev/null +++ b/src/Ombi/ClientApp/app/services/mobile.service.ts @@ -0,0 +1,18 @@ +import { PlatformLocation } from "@angular/common"; +import { Injectable } from "@angular/core"; + +import { HttpClient } from "@angular/common/http"; +import { Observable } from "rxjs/Rx"; + +import { IMobileUsersViewModel } from "./../interfaces"; +import { ServiceHelpers } from "./service.helpers"; + +@Injectable() +export class MobileService extends ServiceHelpers { + constructor(http: HttpClient, public platformLocation: PlatformLocation) { + super(http, "/api/v1/mobile/", platformLocation); + } + public getUserDeviceList(): Observable { + return this.http.get(`${this.url}notification/`, {headers: this.headers}); + } +} diff --git a/src/Ombi/ClientApp/app/services/settings.service.ts b/src/Ombi/ClientApp/app/services/settings.service.ts index f1aa7c1d2..059df61e8 100644 --- a/src/Ombi/ClientApp/app/services/settings.service.ts +++ b/src/Ombi/ClientApp/app/services/settings.service.ts @@ -16,6 +16,7 @@ import { IJobSettings, ILandingPageSettings, IMattermostNotifcationSettings, + IMobileNotifcationSettings, IOmbiSettings, IPlexSettings, IPushbulletNotificationSettings, @@ -139,14 +140,15 @@ export class SettingsService extends ServiceHelpers { return this.http.get(`${this.url}/notifications/mattermost`, {headers: this.headers}); } + public saveMattermostNotificationSettings(settings: IMattermostNotifcationSettings): Observable { + return this.http.post(`${this.url}/notifications/mattermost`, JSON.stringify(settings), {headers: this.headers}); + } + public saveDiscordNotificationSettings(settings: IDiscordNotifcationSettings): Observable { return this.http .post(`${this.url}/notifications/discord`, JSON.stringify(settings), {headers: this.headers}); } - public saveMattermostNotificationSettings(settings: IMattermostNotifcationSettings): Observable { - return this.http.post(`${this.url}/notifications/mattermost`, JSON.stringify(settings), {headers: this.headers}); - } public getPushbulletNotificationSettings(): Observable { return this.http.get(`${this.url}/notifications/pushbullet`, {headers: this.headers}); } @@ -172,6 +174,14 @@ export class SettingsService extends ServiceHelpers { .post(`${this.url}/notifications/slack`, JSON.stringify(settings), {headers: this.headers}); } + public getMobileNotificationSettings(): Observable { + return this.http.get(`${this.url}/notifications/mobile`, {headers: this.headers}); + } + + public saveMobileNotificationSettings(settings: IMobileNotifcationSettings): Observable { + return this.http.post(`${this.url}/notifications/mobile`, JSON.stringify(settings), {headers: this.headers}); + } + public getUpdateSettings(): Observable { return this.http.get(`${this.url}/update`, {headers: this.headers}); } diff --git a/src/Ombi/ClientApp/app/settings/notifications/mobile.component.html b/src/Ombi/ClientApp/app/settings/notifications/mobile.component.html new file mode 100644 index 000000000..ae771291a --- /dev/null +++ b/src/Ombi/ClientApp/app/settings/notifications/mobile.component.html @@ -0,0 +1,39 @@ + + +
+
+ Mobile Notifications +
+
+ +
+ {{user.username}} - {{user.devices}} +
+ + + +
+
+ +
+
+ + + +
+
+ +
+
+
+
+ + +
+ +
+
+
\ No newline at end of file diff --git a/src/Ombi/ClientApp/app/settings/notifications/mobile.component.ts b/src/Ombi/ClientApp/app/settings/notifications/mobile.component.ts new file mode 100644 index 000000000..4c78f462b --- /dev/null +++ b/src/Ombi/ClientApp/app/settings/notifications/mobile.component.ts @@ -0,0 +1,70 @@ +import { Component, OnInit } from "@angular/core"; +import { FormBuilder, FormGroup } from "@angular/forms"; + +import { IMobileNotifcationSettings, IMobileUsersViewModel, INotificationTemplates, NotificationType } from "../../interfaces"; +import { TesterService } from "../../services"; +import { NotificationService } from "../../services"; +import { MobileService, SettingsService } from "../../services"; + +@Component({ + templateUrl: "./mobile.component.html", +}) +export class MobileComponent implements OnInit { + + public NotificationType = NotificationType; + public templates: INotificationTemplates[]; + public form: FormGroup; + public userList: IMobileUsersViewModel[]; + + constructor(private settingsService: SettingsService, + private notificationService: NotificationService, + private fb: FormBuilder, + private testerService: TesterService, + private mobileService: MobileService) { } + + public ngOnInit() { + this.settingsService.getMobileNotificationSettings().subscribe(x => { + this.templates = x.notificationTemplates; + + this.form = this.fb.group({ + }); + }); + + this.mobileService.getUserDeviceList().subscribe(x => this.userList = x); + } + + public onSubmit(form: FormGroup) { + if (form.invalid) { + this.notificationService.error("Please check your entered values"); + return; + } + + const settings = form.value; + settings.notificationTemplates = this.templates; + + this.settingsService.saveMobileNotificationSettings(settings).subscribe(x => { + if (x) { + this.notificationService.success("Successfully saved the Mobile settings"); + } else { + this.notificationService.success("There was an error when saving the Mobile settings"); + } + }); + + } + + public test(form: FormGroup) { + if (form.invalid) { + this.notificationService.error("Please check your entered values"); + return; + } + + this.testerService.discordTest(form.value).subscribe(x => { + if (x) { + this.notificationService.success("Successfully sent a Mobile message, please check the admin mobile device"); + } else { + this.notificationService.error("There was an error when sending the Mobile message. Please check your settings"); + } + }); + + } +} diff --git a/src/Ombi/ClientApp/app/settings/settings.module.ts b/src/Ombi/ClientApp/app/settings/settings.module.ts index b7cdf88f6..4a3da70e8 100644 --- a/src/Ombi/ClientApp/app/settings/settings.module.ts +++ b/src/Ombi/ClientApp/app/settings/settings.module.ts @@ -7,7 +7,7 @@ import { ClipboardModule } from "ngx-clipboard/dist"; import { AuthGuard } from "../auth/auth.guard"; import { AuthService } from "../auth/auth.service"; -import { CouchPotatoService, EmbyService, IssuesService, JobService, PlexService, RadarrService, SonarrService, TesterService, ValidationService } from "../services"; +import { CouchPotatoService, EmbyService, IssuesService, JobService, MobileService, PlexService, RadarrService, SonarrService, TesterService, ValidationService } from "../services"; import { PipeModule } from "../pipes/pipe.module"; import { AboutComponent } from "./about/about.component"; @@ -22,6 +22,7 @@ import { LandingPageComponent } from "./landingpage/landingpage.component"; import { DiscordComponent } from "./notifications/discord.component"; import { EmailNotificationComponent } from "./notifications/emailnotification.component"; import { MattermostComponent } from "./notifications/mattermost.component"; +import { MobileComponent } from "./notifications/mobile.component"; import { NotificationTemplate } from "./notifications/notificationtemplate.component"; import { PushbulletComponent } from "./notifications/pushbullet.component"; import { PushoverComponent } from "./notifications/pushover.component"; @@ -64,6 +65,7 @@ const routes: Routes = [ { path: "SickRage", component: SickRageComponent, canActivate: [AuthGuard] }, { path: "Issues", component: IssuesComponent, canActivate: [AuthGuard] }, { path: "Authentication", component: AuthenticationComponent, canActivate: [AuthGuard] }, + { path: "Mobile", component: MobileComponent, canActivate: [AuthGuard] }, ]; @NgModule({ @@ -111,6 +113,7 @@ const routes: Routes = [ TelegramComponent, IssuesComponent, AuthenticationComponent, + MobileComponent, ], exports: [ RouterModule, @@ -127,6 +130,7 @@ const routes: Routes = [ IssuesService, PlexService, EmbyService, + MobileService, ], }) diff --git a/src/Ombi/Controllers/MobileController.cs b/src/Ombi/Controllers/MobileController.cs index 94d9d27a1..95703351c 100644 --- a/src/Ombi/Controllers/MobileController.cs +++ b/src/Ombi/Controllers/MobileController.cs @@ -1,8 +1,11 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using Ombi.Attributes; using Ombi.Core.Authentication; using Ombi.Helpers; using Ombi.Models; @@ -26,6 +29,7 @@ namespace Ombi.Controllers private readonly OmbiUserManager _userManager; [HttpPost("Notification")] + [ApiExplorerSettings(IgnoreApi = true)] public async Task AddNotitficationId([FromBody] NotificationIdBody body) { if (body?.PlayerId.HasValue() ?? false) @@ -50,5 +54,25 @@ namespace Ombi.Controllers } return BadRequest(); } + + [HttpGet("Notification")] + [ApiExplorerSettings(IgnoreApi = true)] + [Admin] + public IEnumerable GetRegisteredMobileUsers() + { + var users = _userManager.Users.Include(x => x.NotificationUserIds).Where(x => x.NotificationUserIds.Any()); + + var vm = new List(); + + foreach (var u in users) + { + vm.Add(new MobileUsersViewModel + { + Username = u.UserAlias, + Devices = u.NotificationUserIds.Count + }); + } + return vm; + } } } \ No newline at end of file diff --git a/src/Ombi/Controllers/SettingsController.cs b/src/Ombi/Controllers/SettingsController.cs index d1522e361..c68a648c8 100644 --- a/src/Ombi/Controllers/SettingsController.cs +++ b/src/Ombi/Controllers/SettingsController.cs @@ -765,6 +765,40 @@ namespace Ombi.Controllers return model; } + /// + /// Saves the Mobile notification settings. + /// + /// The model. + /// + [HttpPost("notifications/mobile")] + public async Task MobileNotificationSettings([FromBody] MobileNotificationsViewModel model) + { + // Save the email settings + var settings = Mapper.Map(model); + var result = await Save(settings); + + // Save the templates + await TemplateRepository.UpdateRange(model.NotificationTemplates); + + return result; + } + + /// + /// Gets the Mobile Notification Settings. + /// + /// + [HttpGet("notifications/mobile")] + public async Task MobileNotificationSettings() + { + var settings = await Get(); + var model = Mapper.Map(settings); + + // Lookup to see if we have any templates saved + model.NotificationTemplates = await BuildTemplates(NotificationAgent.Mobile); + + return model; + } + private async Task> BuildTemplates(NotificationAgent agent) { var templates = await TemplateRepository.GetAllTemplates(agent); diff --git a/src/Ombi/Models/MobileUsersViewModel.cs b/src/Ombi/Models/MobileUsersViewModel.cs new file mode 100644 index 000000000..e7d99569f --- /dev/null +++ b/src/Ombi/Models/MobileUsersViewModel.cs @@ -0,0 +1,8 @@ +namespace Ombi.Models +{ + public class MobileUsersViewModel + { + public string Username { get; set; } + public int Devices { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi/gulpfile.js b/src/Ombi/gulpfile.js index 5db74ca9e..ae7472871 100644 --- a/src/Ombi/gulpfile.js +++ b/src/Ombi/gulpfile.js @@ -5,6 +5,7 @@ const run = require('gulp-run'); const runSequence = require('run-sequence'); const del = require('del'); const path = require('path'); +const fs = require('fs'); const outputDir = './wwwroot/dist'; @@ -23,26 +24,56 @@ function getEnvOptions() { } } -gulp.task('vendor', function () { - return run('webpack --config webpack.config.vendor.ts' + getEnvOptions()).exec(); + +function webpack(vendor) { + return run(`webpack --config webpack.config${vendor ? '.vendor' : ''}.ts${getEnvOptions()}`).exec(); +} + +gulp.task('vendor', () => { + let build = false; + const vendorPath = path.join(outputDir, "vendor.js"); + const vendorExists = fs.existsSync(vendorPath); + if (vendorExists) { + const vendorStat = fs.statSync(vendorPath); + const packageStat = fs.statSync("package.json"); + const vendorConfigStat = fs.statSync("webpack.config.vendor.ts"); + if (packageStat.mtime > vendorStat.mtime) { + build = true; + } + if (vendorConfigStat.mtime > vendorStat.mtime) { + build = true; + } + } else { + build = true; + } + if (build) { + return webpack(true); + } }); -gulp.task('main', function () { - return run('webpack --config webpack.config.ts' + getEnvOptions()).exec(); + +gulp.task('vendor_force', () => { + return webpack(true); +}) + +gulp.task('main', () => { + return webpack() }); -gulp.task('prod_var', function () { +gulp.task('prod_var', () => { global.prod = true; }) -gulp.task('analyse_var', function () { +gulp.task('analyse_var', () => { global.analyse = true; }) -gulp.task('clean', function() { - del.sync(outputDir, { force: true }); +gulp.task('clean', () => { + del.sync(outputDir, { force: true }); }); + +gulp.task('lint', () => run("npm run lint").exec()); gulp.task('build', callback => runSequence('vendor', 'main', callback)); gulp.task('analyse', callback => runSequence('analyse_var', 'build')); gulp.task('full', callback => runSequence('clean', 'build'));