diff --git a/ci-build.yaml b/ci-build.yaml index fbaa7c03c..066b15016 100644 --- a/ci-build.yaml +++ b/ci-build.yaml @@ -9,7 +9,7 @@ trigger: branches: include: - - feature/* + - feature/v4 exclude: - develop - master diff --git a/src/Ombi.Api.Twilio/IWhatsAppApi.cs b/src/Ombi.Api.Twilio/IWhatsAppApi.cs new file mode 100644 index 000000000..490aaa8fc --- /dev/null +++ b/src/Ombi.Api.Twilio/IWhatsAppApi.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Ombi.Api.Twilio +{ + public interface IWhatsAppApi + { + Task SendMessage(WhatsAppModel message, string accountSid, string authToken); + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Twilio/Ombi.Api.Twilio.csproj b/src/Ombi.Api.Twilio/Ombi.Api.Twilio.csproj new file mode 100644 index 000000000..d55f9680a --- /dev/null +++ b/src/Ombi.Api.Twilio/Ombi.Api.Twilio.csproj @@ -0,0 +1,11 @@ + + + + netstandard2.0 + + + + + + + diff --git a/src/Ombi.Api.Twilio/WhatsAppApi.cs b/src/Ombi.Api.Twilio/WhatsAppApi.cs new file mode 100644 index 000000000..5290a02ec --- /dev/null +++ b/src/Ombi.Api.Twilio/WhatsAppApi.cs @@ -0,0 +1,24 @@ +using System; +using System.Threading.Tasks; +using Twilio; +using Twilio.Rest.Api.V2010.Account; +using Twilio.Types; + +namespace Ombi.Api.Twilio +{ + public class WhatsAppApi : IWhatsAppApi + { + public async Task SendMessage(WhatsAppModel message, string accountSid, string authToken) + { + TwilioClient.Init(accountSid, authToken); + + var response =await MessageResource.CreateAsync( + body: message.Message, + from: new PhoneNumber($"whatsapp:{message.From}"), + to: new PhoneNumber($"whatsapp:{message.To}") + ); + + return response.Sid; + } + } +} diff --git a/src/Ombi.Api.Twilio/WhatsAppModel.cs b/src/Ombi.Api.Twilio/WhatsAppModel.cs new file mode 100644 index 000000000..e7f4e5c21 --- /dev/null +++ b/src/Ombi.Api.Twilio/WhatsAppModel.cs @@ -0,0 +1,9 @@ +namespace Ombi.Api.Twilio +{ + public class WhatsAppModel + { + public string Message { get; set; } + public string To { get; set; } + public string From { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Core/Models/UI/TwilioSettingsViewModel.cs b/src/Ombi.Core/Models/UI/TwilioSettingsViewModel.cs new file mode 100644 index 000000000..5e19535ad --- /dev/null +++ b/src/Ombi.Core/Models/UI/TwilioSettingsViewModel.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using Ombi.Settings.Settings.Models.Notifications; +using Ombi.Store.Entities; + +namespace Ombi.Core.Models.UI +{ + /// + /// The view model for the notification settings page + /// + /// + public class TwilioSettingsViewModel + { + public int Id { get; set; } + public WhatsAppSettingsViewModel WhatsAppSettings { get; set; } = new WhatsAppSettingsViewModel(); + } + + public class WhatsAppSettingsViewModel : WhatsAppSettings + { + /// + /// Gets or sets the notification templates. + /// + /// + /// The notification templates. + /// + public List NotificationTemplates { get; set; } + } +} diff --git a/src/Ombi.DependencyInjection/IocExtensions.cs b/src/Ombi.DependencyInjection/IocExtensions.cs index dd7dcd3b8..66e0a05e6 100644 --- a/src/Ombi.DependencyInjection/IocExtensions.cs +++ b/src/Ombi.DependencyInjection/IocExtensions.cs @@ -64,6 +64,7 @@ using Ombi.Schedule.Processor; using Ombi.Store.Entities; using Quartz.Spi; using Ombi.Api.MusicBrainz; +using Ombi.Api.Twilio; namespace Ombi.DependencyInjection { @@ -147,6 +148,7 @@ namespace Ombi.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); } public static void RegisterStore(this IServiceCollection services) { diff --git a/src/Ombi.DependencyInjection/Ombi.DependencyInjection.csproj b/src/Ombi.DependencyInjection/Ombi.DependencyInjection.csproj index 59a37d6aa..636e96a3c 100644 --- a/src/Ombi.DependencyInjection/Ombi.DependencyInjection.csproj +++ b/src/Ombi.DependencyInjection/Ombi.DependencyInjection.csproj @@ -37,6 +37,7 @@ + diff --git a/src/Ombi.Helpers/LoggingEvents.cs b/src/Ombi.Helpers/LoggingEvents.cs index 0723800ab..7c4bd73f7 100644 --- a/src/Ombi.Helpers/LoggingEvents.cs +++ b/src/Ombi.Helpers/LoggingEvents.cs @@ -33,6 +33,7 @@ namespace Ombi.Helpers public static EventId PushoverNotification => new EventId(4005); public static EventId TelegramNotifcation => new EventId(4006); public static EventId GotifyNotification => new EventId(4007); + public static EventId WhatsApp => new EventId(4008); public static EventId TvSender => new EventId(5000); public static EventId SonarrSender => new EventId(5001); diff --git a/src/Ombi.Helpers/NotificationAgent.cs b/src/Ombi.Helpers/NotificationAgent.cs index 18f28105a..78fe529c0 100644 --- a/src/Ombi.Helpers/NotificationAgent.cs +++ b/src/Ombi.Helpers/NotificationAgent.cs @@ -11,5 +11,6 @@ Mattermost = 6, Mobile = 7, Gotify = 8, + WhatsApp = 9 } } \ No newline at end of file diff --git a/src/Ombi.Mapping/Profiles/SettingsProfile.cs b/src/Ombi.Mapping/Profiles/SettingsProfile.cs index f460ce78b..f336db5f9 100644 --- a/src/Ombi.Mapping/Profiles/SettingsProfile.cs +++ b/src/Ombi.Mapping/Profiles/SettingsProfile.cs @@ -20,6 +20,8 @@ namespace Ombi.Mapping.Profiles CreateMap().ReverseMap(); CreateMap().ReverseMap(); CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); } } } \ No newline at end of file diff --git a/src/Ombi.Notifications/Agents/WhatsAppNotification.cs b/src/Ombi.Notifications/Agents/WhatsAppNotification.cs new file mode 100644 index 000000000..860acc8d0 --- /dev/null +++ b/src/Ombi.Notifications/Agents/WhatsAppNotification.cs @@ -0,0 +1,125 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +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; +using Ombi.Api.Twilio; + +namespace Ombi.Notifications.Agents +{ + public class WhatsAppNotification : BaseNotification + { + public WhatsAppNotification(IWhatsAppApi api, ISettingsService sn, ILogger log, + INotificationTemplatesRepository r, IMovieRequestRepository m, + ITvRequestRepository t, ISettingsService s + , IRepository sub, IMusicRequestRepository music, + IRepository userPref) : base(sn, r, m, t,s,log, sub, music, userPref) + { + Api = api; + Logger = log; + } + + public override string NotificationName => "WhatsAppNotification"; + + private IWhatsAppApi Api { get; } + private ILogger Logger { get; } + + protected override bool ValidateConfiguration(TwilioSettings settings) + { + if (!settings.WhatsAppSettings?.Enabled ?? false) + { + return false; + } + return !settings.WhatsAppSettings.AccountSid.IsNullOrEmpty() && !settings.WhatsAppSettings.AuthToken.IsNullOrEmpty() && !settings.WhatsAppSettings.From.IsNullOrEmpty(); + } + + protected override async Task NewRequest(NotificationOptions model, TwilioSettings settings) + { + await Run(model, settings, NotificationType.NewRequest); + } + + protected override async Task NewIssue(NotificationOptions model, TwilioSettings settings) + { + await Run(model, settings, NotificationType.Issue); + } + + protected override async Task IssueComment(NotificationOptions model, TwilioSettings settings) + { + await Run(model, settings, NotificationType.IssueComment); + } + + protected override async Task IssueResolved(NotificationOptions model, TwilioSettings settings) + { + await Run(model, settings, NotificationType.IssueResolved); + } + + protected override async Task AddedToRequestQueue(NotificationOptions model, TwilioSettings settings) + { + await Run(model, settings, NotificationType.ItemAddedToFaultQueue); + } + + protected override async Task RequestDeclined(NotificationOptions model, TwilioSettings settings) + { + await Run(model, settings, NotificationType.RequestDeclined); + } + + protected override async Task RequestApproved(NotificationOptions model, TwilioSettings settings) + { + await Run(model, settings, NotificationType.RequestApproved); + } + + protected override async Task AvailableRequest(NotificationOptions model, TwilioSettings settings) + { + await Run(model, settings, NotificationType.RequestAvailable); + } + + protected override async Task Send(NotificationMessage model, TwilioSettings settings) + { + try + { + var whatsApp = new WhatsAppModel + { + Message = model.Message, + From = settings.WhatsAppSettings.From, + To = ""// TODO + }; + await Api.SendMessage(whatsApp, settings.WhatsAppSettings.AccountSid, settings.WhatsAppSettings.AuthToken); + } + catch (Exception e) + { + Logger.LogError(LoggingEvents.WhatsApp, e, "Failed to send WhatsApp Notification"); + } + } + + protected override async Task Test(NotificationOptions model, TwilioSettings 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, TwilioSettings settings, NotificationType type) + { + var parsed = await LoadTemplate(NotificationAgent.WhatsApp, type, model); + if (parsed.Disabled) + { + Logger.LogInformation($"Template {type} is disabled for {NotificationAgent.WhatsApp}"); + return; + } + var notification = new NotificationMessage + { + Message = parsed.Message, + }; + await Send(notification, settings); + } + } +} diff --git a/src/Ombi.Notifications/Ombi.Notifications.csproj b/src/Ombi.Notifications/Ombi.Notifications.csproj index 3015c150d..c613f5b61 100644 --- a/src/Ombi.Notifications/Ombi.Notifications.csproj +++ b/src/Ombi.Notifications/Ombi.Notifications.csproj @@ -22,6 +22,7 @@ + diff --git a/src/Ombi.Settings/Settings/Models/Notifications/TwilioSettings.cs b/src/Ombi.Settings/Settings/Models/Notifications/TwilioSettings.cs new file mode 100644 index 000000000..d4b0d69ba --- /dev/null +++ b/src/Ombi.Settings/Settings/Models/Notifications/TwilioSettings.cs @@ -0,0 +1,15 @@ +namespace Ombi.Settings.Settings.Models.Notifications +{ + public class TwilioSettings : Settings + { + public WhatsAppSettings WhatsAppSettings { get; set; } + } + + public class WhatsAppSettings + { + public bool Enabled { get; set; } + public string From { get; set; } + public string AccountSid { get; set; } + public string AuthToken { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.sln b/src/Ombi.sln index d557516c9..78d3898ac 100644 --- a/src/Ombi.sln +++ b/src/Ombi.sln @@ -108,7 +108,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Hubs", "Ombi.Hubs\Ombi EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Api.GroupMe", "Ombi.Api.GroupMe\Ombi.Api.GroupMe.csproj", "{9266403C-B04D-4C0F-AC39-82F12C781949}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ombi.Api.MusicBrainz", "Ombi.Api.MusicBrainz\Ombi.Api.MusicBrainz.csproj", "{C5C1769B-4197-4410-A160-0EEF39EDDC98}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Api.MusicBrainz", "Ombi.Api.MusicBrainz\Ombi.Api.MusicBrainz.csproj", "{C5C1769B-4197-4410-A160-0EEF39EDDC98}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ombi.Api.Twilio", "Ombi.Api.Twilio\Ombi.Api.Twilio.csproj", "{34E5DD1A-6A90-448B-9E71-64D1ACD6C1A3}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -292,6 +294,10 @@ Global {C5C1769B-4197-4410-A160-0EEF39EDDC98}.Debug|Any CPU.Build.0 = Debug|Any CPU {C5C1769B-4197-4410-A160-0EEF39EDDC98}.Release|Any CPU.ActiveCfg = Release|Any CPU {C5C1769B-4197-4410-A160-0EEF39EDDC98}.Release|Any CPU.Build.0 = Release|Any CPU + {34E5DD1A-6A90-448B-9E71-64D1ACD6C1A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {34E5DD1A-6A90-448B-9E71-64D1ACD6C1A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {34E5DD1A-6A90-448B-9E71-64D1ACD6C1A3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {34E5DD1A-6A90-448B-9E71-64D1ACD6C1A3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -334,6 +340,7 @@ Global {27111E7C-748E-4996-BD71-2117027C6460} = {6F42AB98-9196-44C4-B888-D5E409F415A1} {9266403C-B04D-4C0F-AC39-82F12C781949} = {9293CA11-360A-4C20-A674-B9E794431BF5} {C5C1769B-4197-4410-A160-0EEF39EDDC98} = {9293CA11-360A-4C20-A674-B9E794431BF5} + {34E5DD1A-6A90-448B-9E71-64D1ACD6C1A3} = {9293CA11-360A-4C20-A674-B9E794431BF5} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {192E9BF8-00B4-45E4-BCCC-4C215725C869} diff --git a/src/Ombi/ClientApp/src/app/interfaces/INotificationSettings.ts b/src/Ombi/ClientApp/src/app/interfaces/INotificationSettings.ts index 5472a6c7c..a6c14ae3b 100644 --- a/src/Ombi/ClientApp/src/app/interfaces/INotificationSettings.ts +++ b/src/Ombi/ClientApp/src/app/interfaces/INotificationSettings.ts @@ -27,11 +27,16 @@ export interface INotificationTemplates { } export enum NotificationAgent { - Email, - Discord, - Pushbullet, - Pushover, - Telegram, + Email = 0, + Discord = 1, + Pushbullet = 2, + Pushover = 3, + Telegram = 4, + Slack = 5, + Mattermost = 6, + Mobile = 7, + Gotify = 8, + WhatsApp = 9 } export enum NotificationType { @@ -47,6 +52,7 @@ export enum NotificationType { IssueResolved = 9, IssueComment = 10, Newsletter = 11, + WhatsApp = 12, } export interface IDiscordNotifcationSettings extends INotificationSettings { @@ -85,6 +91,18 @@ export interface IPushbulletNotificationSettings extends INotificationSettings { channelTag: string; } +export interface ITwilioSettings extends ISettings { + whatsAppSettings: IWhatsAppSettings; +} + +export interface IWhatsAppSettings { + enabled: number; + from: string; + accountSid: string; + authToken: string; + notificationTemplates: INotificationTemplates[]; +} + export interface IPushoverNotificationSettings extends INotificationSettings { accessToken: string; notificationTemplates: INotificationTemplates[]; diff --git a/src/Ombi/ClientApp/src/app/interfaces/IUser.ts b/src/Ombi/ClientApp/src/app/interfaces/IUser.ts index 0816ad42e..9b0500891 100644 --- a/src/Ombi/ClientApp/src/app/interfaces/IUser.ts +++ b/src/Ombi/ClientApp/src/app/interfaces/IUser.ts @@ -91,6 +91,7 @@ export interface INotificationPreferences { } export enum INotificationAgent { + Email = 0, Discord = 1, Pushbullet = 2, @@ -99,4 +100,6 @@ export enum INotificationAgent { Slack = 5, Mattermost = 6, Mobile = 7, + Gotify = 8, + WhatsApp = 9 } diff --git a/src/Ombi/ClientApp/src/app/media-details/components/movie/panels/movie-admin-panel/movie-admin-panel.component.html b/src/Ombi/ClientApp/src/app/media-details/components/movie/panels/movie-admin-panel/movie-admin-panel.component.html index 13fc86050..e40815036 100644 --- a/src/Ombi/ClientApp/src/app/media-details/components/movie/panels/movie-admin-panel/movie-admin-panel.component.html +++ b/src/Ombi/ClientApp/src/app/media-details/components/movie/panels/movie-admin-panel/movie-admin-panel.component.html @@ -1,3 +1,3 @@ -
+
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/requests-list/components/movies-grid/movies-grid.component.html b/src/Ombi/ClientApp/src/app/requests-list/components/movies-grid/movies-grid.component.html index 7255e42e9..7c91e9900 100644 --- a/src/Ombi/ClientApp/src/app/requests-list/components/movies-grid/movies-grid.component.html +++ b/src/Ombi/ClientApp/src/app/requests-list/components/movies-grid/movies-grid.component.html @@ -41,11 +41,11 @@ {{element.requestStatus | translate}} - + - + - + diff --git a/src/Ombi/ClientApp/src/app/requests-list/components/movies-grid/movies-grid.component.ts b/src/Ombi/ClientApp/src/app/requests-list/components/movies-grid/movies-grid.component.ts index e8e4cbbf8..513558fa0 100644 --- a/src/Ombi/ClientApp/src/app/requests-list/components/movies-grid/movies-grid.component.ts +++ b/src/Ombi/ClientApp/src/app/requests-list/components/movies-grid/movies-grid.component.ts @@ -1,10 +1,11 @@ -import { Component, AfterViewInit, ViewChild, EventEmitter, Output } from "@angular/core"; +import { Component, AfterViewInit, ViewChild, EventEmitter, Output, ChangeDetectorRef } from "@angular/core"; import { IMovieRequests, IRequestsViewModel } from "../../../interfaces"; import { MatPaginator, MatSort } from "@angular/material"; import { merge, Observable, of as observableOf } from 'rxjs'; import { catchError, map, startWith, switchMap } from 'rxjs/operators'; import { RequestServiceV2 } from "../../../services/requestV2.service"; +import { AuthService } from "../../../auth/auth.service"; @Component({ templateUrl: "./movies-grid.component.html", @@ -18,13 +19,15 @@ export class MoviesGridComponent implements AfterViewInit { public displayedColumns: string[] = ['requestedUser.requestedBy', 'title', 'requestedDate', 'status', 'requestStatus', 'actions']; public gridCount: string = "15"; public showUnavailableRequests: boolean; - - @Output() public onOpenOptions = new EventEmitter<{request: any, filter: any}>(); + public isAdmin: boolean; + + @Output() public onOpenOptions = new EventEmitter<{ request: any, filter: any, onChange: any }>(); @ViewChild(MatPaginator, { static: false }) paginator: MatPaginator; @ViewChild(MatSort, { static: false }) sort: MatSort; - constructor(private requestService: RequestServiceV2) { + constructor(private requestService: RequestServiceV2, private ref: ChangeDetectorRef, + private auth: AuthService) { } @@ -34,6 +37,8 @@ export class MoviesGridComponent implements AfterViewInit { // this.dataSource = results.collection; // this.resultsLength = results.total; + this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser"); + // If the user changes the sort order, reset back to the first page. this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0); @@ -69,10 +74,16 @@ export class MoviesGridComponent implements AfterViewInit { } public openOptions(request: IMovieRequests) { - const filter = () => { this.dataSource = this.dataSource.filter((req) => { + const filter = () => { + this.dataSource = this.dataSource.filter((req) => { return req.id !== request.id; - })}; + }) + }; + + const onChange = () => { + this.ref.detectChanges(); + }; - this.onOpenOptions.emit({request: request, filter: filter}); + this.onOpenOptions.emit({ request: request, filter: filter, onChange: onChange }); } } diff --git a/src/Ombi/ClientApp/src/app/requests-list/components/options/request-options.component.html b/src/Ombi/ClientApp/src/app/requests-list/components/options/request-options.component.html index 9c98e376f..a22c5a2ef 100644 --- a/src/Ombi/ClientApp/src/app/requests-list/components/options/request-options.component.html +++ b/src/Ombi/ClientApp/src/app/requests-list/components/options/request-options.component.html @@ -2,4 +2,7 @@ Delete Request + + Approve Request + \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/requests-list/components/options/request-options.component.ts b/src/Ombi/ClientApp/src/app/requests-list/components/options/request-options.component.ts index 565b8678b..153942810 100644 --- a/src/Ombi/ClientApp/src/app/requests-list/components/options/request-options.component.ts +++ b/src/Ombi/ClientApp/src/app/requests-list/components/options/request-options.component.ts @@ -1,7 +1,8 @@ -import {Component, Inject} from '@angular/core'; -import {MAT_BOTTOM_SHEET_DATA, MatBottomSheetRef} from '@angular/material/bottom-sheet'; +import { Component, Inject } from '@angular/core'; +import { MAT_BOTTOM_SHEET_DATA, MatBottomSheetRef } from '@angular/material/bottom-sheet'; import { RequestService } from '../../../services'; import { RequestType } from '../../../interfaces'; +import { UpdateType } from '../../models/UpdateType'; @Component({ selector: 'request-options', @@ -9,17 +10,29 @@ import { RequestType } from '../../../interfaces'; }) export class RequestOptionsComponent { constructor(@Inject(MAT_BOTTOM_SHEET_DATA) public data: any, - private requestService: RequestService, private bottomSheetRef: MatBottomSheetRef) { } + private requestService: RequestService, private bottomSheetRef: MatBottomSheetRef) { } public async delete() { if (this.data.type === RequestType.movie) { - await this.requestService.removeMovieRequestAsync(this.data.id); + await this.requestService.removeMovieRequestAsync(this.data.id); } - if(this.data.type === RequestType.tvShow) { - await this.requestService.deleteChild(this.data.id).toPromise(); + if (this.data.type === RequestType.tvShow) { + await this.requestService.deleteChild(this.data.id).toPromise(); } - this.bottomSheetRef.dismiss(true); + this.bottomSheetRef.dismiss({type: UpdateType.Delete}); + return; + } + + public async approve() { + if (this.data.type === RequestType.movie) { + await this.requestService.approveMovie({id: this.data.id}).toPromise(); + } + if (this.data.type === RequestType.tvShow) { + await this.requestService.approveChild({id: this.data.id}).toPromise(); + } + + this.bottomSheetRef.dismiss({type: UpdateType.Approve}); return; } } \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/requests-list/components/requests-list.component.ts b/src/Ombi/ClientApp/src/app/requests-list/components/requests-list.component.ts index 7c7c4e30d..8b3b35f90 100644 --- a/src/Ombi/ClientApp/src/app/requests-list/components/requests-list.component.ts +++ b/src/Ombi/ClientApp/src/app/requests-list/components/requests-list.component.ts @@ -1,6 +1,8 @@ -import { Component } from "@angular/core"; +import { Component, ViewChild } from "@angular/core"; import { MatBottomSheet } from "@angular/material"; import { RequestOptionsComponent } from "./options/request-options.component"; +import { UpdateType } from "../models/UpdateType"; +import { MoviesGridComponent } from "./movies-grid/movies-grid.component"; @Component({ templateUrl: "./requests-list.component.html", @@ -9,15 +11,24 @@ import { RequestOptionsComponent } from "./options/request-options.component"; export class RequestsListComponent { constructor(private bottomSheet: MatBottomSheet) { } - - public onOpenOptions(event: {request: any, filter: any}) { - const ref = this.bottomSheet.open(RequestOptionsComponent, { data: { id: event.request.id, type: event.request.requestType } }); + + public onOpenOptions(event: { request: any, filter: any, onChange: any }) { + const ref = this.bottomSheet.open(RequestOptionsComponent, { data: { id: event.request.id, type: event.request.requestType, canApprove: event.request.canApprove } }); ref.afterDismissed().subscribe((result) => { - if (!result) { + if(!result) { + return; + } + if (result.type == UpdateType.Delete) { + event.filter(); + return; + } + if (result.type == UpdateType.Approve) { + // Need to do this here, as the status is calculated on the server + event.request.requestStatus = 'Common.ProcessingRequest'; + event.onChange(); return; } - event.filter(); }); } } diff --git a/src/Ombi/ClientApp/src/app/requests-list/components/tv-grid/tv-grid.component.html b/src/Ombi/ClientApp/src/app/requests-list/components/tv-grid/tv-grid.component.html index 5d93bb84f..343b9f255 100644 --- a/src/Ombi/ClientApp/src/app/requests-list/components/tv-grid/tv-grid.component.html +++ b/src/Ombi/ClientApp/src/app/requests-list/components/tv-grid/tv-grid.component.html @@ -1,18 +1,18 @@
- + - - 10 - 15 - 30 - 100 - - + + 10 + 15 + 30 + 100 + + - +
@@ -21,44 +21,42 @@ - + - - - - - - - - - + + + + - - - - + + + + + + + + + @@ -67,4 +65,4 @@
Requested By Requested By {{element.requestedUser.userAlias}} Status - {{element.parentRequest.status}} - Requested Date - {{element.requestedDate | amLocal | amDateFormat: 'LL'}} - Status + {{element.parentRequest.status}} + Request Status -
{{'Common.ProcessingRequest' | translate}}
-
{{'Common.PendingApproval' | - translate}} -
-
{{'Common.NotRequested' | - translate}} -
-
Requested Date + {{element.requestedDate | amLocal | amDateFormat: 'LL'}} + Request Status +
{{'Common.ProcessingRequest' | translate}}
+
{{'Common.PendingApproval' |translate}}
+
{{'Common.Available' | translate}}
+ +
- - + +
-
+
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/requests-list/components/tv-grid/tv-grid.component.ts b/src/Ombi/ClientApp/src/app/requests-list/components/tv-grid/tv-grid.component.ts index efa68838b..78dd23c1a 100644 --- a/src/Ombi/ClientApp/src/app/requests-list/components/tv-grid/tv-grid.component.ts +++ b/src/Ombi/ClientApp/src/app/requests-list/components/tv-grid/tv-grid.component.ts @@ -1,10 +1,11 @@ -import { Component, AfterViewInit, ViewChild, Output, EventEmitter } from "@angular/core"; +import { Component, AfterViewInit, ViewChild, Output, EventEmitter, ChangeDetectorRef } from "@angular/core"; import { IRequestsViewModel, IChildRequests } from "../../../interfaces"; import { MatPaginator, MatSort } from "@angular/material"; import { merge, of as observableOf, Observable } from 'rxjs'; import { catchError, map, startWith, switchMap } from 'rxjs/operators'; import { RequestServiceV2 } from "../../../services/requestV2.service"; +import { AuthService } from "../../../auth/auth.service"; @Component({ templateUrl: "./tv-grid.component.html", @@ -18,18 +19,21 @@ export class TvGridComponent implements AfterViewInit { public displayedColumns: string[] = ['series', 'requestedBy', 'status', 'requestStatus', 'requestedDate','actions']; public gridCount: string = "15"; public showUnavailableRequests: boolean; + public isAdmin: boolean; - @Output() public onOpenOptions = new EventEmitter<{request: any, filter: any}>(); + @Output() public onOpenOptions = new EventEmitter<{request: any, filter: any, onChange: any}>(); @ViewChild(MatPaginator, {static: false}) paginator: MatPaginator; @ViewChild(MatSort, {static: false}) sort: MatSort; - constructor(private requestService: RequestServiceV2) { + constructor(private requestService: RequestServiceV2, private auth: AuthService, + private ref: ChangeDetectorRef) { } public async ngAfterViewInit() { + this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser"); // If the user changes the sort order, reset back to the first page. this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0); @@ -58,8 +62,12 @@ export class TvGridComponent implements AfterViewInit { const filter = () => { this.dataSource = this.dataSource.filter((req) => { return req.id !== request.id; })}; + + const onChange = () => { + this.ref.detectChanges(); + }; - this.onOpenOptions.emit({request: request, filter: filter}); + this.onOpenOptions.emit({request: request, filter: filter, onChange}); } private loadData(): Observable> { diff --git a/src/Ombi/ClientApp/src/app/requests-list/models/UpdateType.ts b/src/Ombi/ClientApp/src/app/requests-list/models/UpdateType.ts new file mode 100644 index 000000000..647c4601d --- /dev/null +++ b/src/Ombi/ClientApp/src/app/requests-list/models/UpdateType.ts @@ -0,0 +1,4 @@ +export enum UpdateType { + Delete, + Approve +} \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/services/applications/tester.service.ts b/src/Ombi/ClientApp/src/app/services/applications/tester.service.ts index 1b7c55821..6c1d9ff6c 100644 --- a/src/Ombi/ClientApp/src/app/services/applications/tester.service.ts +++ b/src/Ombi/ClientApp/src/app/services/applications/tester.service.ts @@ -24,6 +24,7 @@ import { ISlackNotificationSettings, ISonarrSettings, ITelegramNotifcationSettings, + IWhatsAppSettings, } from "../../interfaces"; @Injectable() @@ -52,6 +53,10 @@ export class TesterService extends ServiceHelpers { return this.http.post(`${this.url}mattermost`, JSON.stringify(settings), {headers: this.headers}); } + public whatsAppTest(settings: IWhatsAppSettings): Observable { + return this.http.post(`${this.url}whatsapp`, JSON.stringify(settings), {headers: this.headers}); + } + public slackTest(settings: ISlackNotificationSettings): Observable { return this.http.post(`${this.url}slack`, JSON.stringify(settings), {headers: this.headers}); } diff --git a/src/Ombi/ClientApp/src/app/services/settings.service.ts b/src/Ombi/ClientApp/src/app/services/settings.service.ts index fb50f3e89..4eec29081 100644 --- a/src/Ombi/ClientApp/src/app/services/settings.service.ts +++ b/src/Ombi/ClientApp/src/app/services/settings.service.ts @@ -36,6 +36,7 @@ import { IUpdateSettings, IUserManagementSettings, IVoteSettings, + ITwilioSettings, } from "../interfaces"; import { ServiceHelpers } from "./service.helpers"; @@ -254,6 +255,15 @@ export class SettingsService extends ServiceHelpers { .post(`${this.url}/notifications/telegram`, JSON.stringify(settings), {headers: this.headers}); } + public getTwilioSettings(): Observable { + return this.http.get(`${this.url}/notifications/twilio`, {headers: this.headers}); + } + + public saveTwilioSettings(settings: ITwilioSettings): Observable { + return this.http + .post(`${this.url}/notifications/twilio`, JSON.stringify(settings), {headers: this.headers}); + } + public getJobSettings(): Observable { return this.http.get(`${this.url}/jobs`, {headers: this.headers}); } diff --git a/src/Ombi/ClientApp/src/app/settings/notifications/notificationtemplate.component.html b/src/Ombi/ClientApp/src/app/settings/notifications/notificationtemplate.component.html index 1fd475a38..02551a345 100644 --- a/src/Ombi/ClientApp/src/app/settings/notifications/notificationtemplate.component.html +++ b/src/Ombi/ClientApp/src/app/settings/notifications/notificationtemplate.component.html @@ -1,36 +1,26 @@ - - + +

+ + + + + {{NotificationType[template.notificationType] | humanize}} + + - - - -
-
- -
-
- -
-
- -
- -
- -
-
+
+ Enable +
-
- -
- -
-
-
-
+ + + -
-
-
+ + + +
+ +
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/settings/notifications/twilio/twilio.component.html b/src/Ombi/ClientApp/src/app/settings/notifications/twilio/twilio.component.html new file mode 100644 index 000000000..9c9d0ea1e --- /dev/null +++ b/src/Ombi/ClientApp/src/app/settings/notifications/twilio/twilio.component.html @@ -0,0 +1,22 @@ + + +
+
+ Twilio + Below are the supported integrations with Twilio +
+ + + + + + + +
+
+ +
+
+
+
+
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/settings/notifications/twilio/twilio.component.ts b/src/Ombi/ClientApp/src/app/settings/notifications/twilio/twilio.component.ts new file mode 100644 index 000000000..4f2364107 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/settings/notifications/twilio/twilio.component.ts @@ -0,0 +1,55 @@ +import { Component, OnInit } from "@angular/core"; +import { FormBuilder, FormGroup, Validators } from "@angular/forms"; + +import { INotificationTemplates, ITwilioSettings, NotificationType } from "../../../interfaces"; +import { TesterService } from "../../../services"; +import { NotificationService } from "../../../services"; +import { SettingsService } from "../../../services"; + +@Component({ + templateUrl: "./twilio.component.html", +}) +export class TwilioComponent 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.getTwilioSettings().subscribe(x => { + this.templates = x.whatsAppSettings.notificationTemplates; + + this.form = this.fb.group({ + whatsAppSettings: this.fb.group({ + enabled: [x.whatsAppSettings.enabled], + accountSid: [x.whatsAppSettings.accountSid], + authToken: [x.whatsAppSettings.authToken], + from: [x.whatsAppSettings.from], + }), + }); + }); + } + + public onSubmit(form: FormGroup) { + if (form.invalid) { + this.notificationService.error("Please check your entered values"); + return; + } + + const settings = form.value; + settings.whatsAppSettings.notificationTemplates = this.templates; + + this.settingsService.saveTwilioSettings(settings).subscribe(x => { + if (x) { + this.notificationService.success("Successfully saved the Twilio settings"); + } else { + this.notificationService.success("There was an error when saving the Twilio settings"); + } + }); + + } +} diff --git a/src/Ombi/ClientApp/src/app/settings/notifications/twilio/whatsapp.component.html b/src/Ombi/ClientApp/src/app/settings/notifications/twilio/whatsapp.component.html new file mode 100644 index 000000000..9ce863df7 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/settings/notifications/twilio/whatsapp.component.html @@ -0,0 +1,37 @@ +
+
+
+
+
+ Enable +
+ +
+ + + +
+
+ + + +
+
+ + + +
+
+
+ +
+
+
+ +
+ +
+ +
+
+
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/settings/notifications/twilio/whatsapp.component.ts b/src/Ombi/ClientApp/src/app/settings/notifications/twilio/whatsapp.component.ts new file mode 100644 index 000000000..80222e426 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/settings/notifications/twilio/whatsapp.component.ts @@ -0,0 +1,37 @@ +import { Component, Input } from "@angular/core"; +import { FormGroup } from "@angular/forms"; +import { TesterService, NotificationService } from "../../../services"; +import { INotificationTemplates, NotificationType } from "../../../interfaces"; + + + +@Component({ + templateUrl: "./whatsapp.component.html", + selector: "app-whatsapp" +}) +export class WhatsAppComponent { + + public NotificationType = NotificationType; + @Input() public templates: INotificationTemplates[]; + @Input() public form: FormGroup; + + constructor(private testerService: TesterService, + private notificationService: NotificationService) { } + + + public test(form: FormGroup) { + if (form.invalid) { + this.notificationService.error("Please check your entered values"); + return; + } + + this.testerService.whatsAppTest(form.get("whatsAppSettings").value).subscribe(x => { + if (x) { + this.notificationService.success( "Successfully sent a WhatsApp message, please check the appropriate channel"); + } else { + this.notificationService.error("There was an error when sending the WhatsApp message. Please check your settings"); + } + }); + + } +} diff --git a/src/Ombi/ClientApp/src/app/settings/settings.module.ts b/src/Ombi/ClientApp/src/app/settings/settings.module.ts index e4be0c24e..bdf0596ac 100644 --- a/src/Ombi/ClientApp/src/app/settings/settings.module.ts +++ b/src/Ombi/ClientApp/src/app/settings/settings.module.ts @@ -55,6 +55,8 @@ import { MatMenuModule} from "@angular/material"; import { SharedModule } from "../shared/shared.module"; import { HubService } from "../services/hub.service"; import { LogsComponent } from "./logs/logs.component"; +import { TwilioComponent } from "./notifications/twilio/twilio.component"; +import { WhatsAppComponent } from "./notifications/twilio/whatsapp.component"; const routes: Routes = [ { path: "Ombi", component: OmbiComponent, canActivate: [AuthGuard] }, @@ -72,6 +74,7 @@ const routes: Routes = [ { path: "Pushbullet", component: PushbulletComponent, canActivate: [AuthGuard] }, { path: "Gotify", component: GotifyComponent, canActivate: [AuthGuard] }, { path: "Mattermost", component: MattermostComponent, canActivate: [AuthGuard] }, + { path: "Twilio", component: TwilioComponent, canActivate: [AuthGuard] }, { path: "UserManagement", component: UserManagementComponent, canActivate: [AuthGuard] }, { path: "Update", component: UpdateComponent, canActivate: [AuthGuard] }, { path: "CouchPotato", component: CouchPotatoComponent, canActivate: [AuthGuard] }, @@ -149,6 +152,8 @@ const routes: Routes = [ TheMovieDbComponent, FailedRequestsComponent, LogsComponent, + TwilioComponent, + WhatsAppComponent ], exports: [ RouterModule, diff --git a/src/Ombi/ClientApp/src/app/settings/settingsmenu.component.html b/src/Ombi/ClientApp/src/app/settings/settingsmenu.component.html index 41a5c5d66..21f01d010 100644 --- a/src/Ombi/ClientApp/src/app/settings/settingsmenu.component.html +++ b/src/Ombi/ClientApp/src/app/settings/settingsmenu.component.html @@ -2,62 +2,63 @@ - - - - - - - + + + + + + + - - + + - - - + + + - - - + + + - + - - - - - - - - - - - + + + + + + + + + + + + - - - - - + + + + +
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/settings/wiki.component.html b/src/Ombi/ClientApp/src/app/settings/wiki.component.html index 9f02b1354..c02b42d5b 100644 --- a/src/Ombi/ClientApp/src/app/settings/wiki.component.html +++ b/src/Ombi/ClientApp/src/app/settings/wiki.component.html @@ -1,5 +1,5 @@ -
-
+
+ -
+