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

pull/3400/head
Jamie Rees 4 years ago
commit 0e7cbe5c6b

@ -9,7 +9,7 @@
trigger: trigger:
branches: branches:
include: include:
- feature/* - feature/v4
exclude: exclude:
- develop - develop
- master - master

@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace Ombi.Api.Twilio
{
public interface IWhatsAppApi
{
Task<string> SendMessage(WhatsAppModel message, string accountSid, string authToken);
}
}

@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Twilio" Version="5.37.2" />
</ItemGroup>
</Project>

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

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

@ -0,0 +1,27 @@
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="TwilioSettingsViewModel" />
public class TwilioSettingsViewModel
{
public int Id { get; set; }
public WhatsAppSettingsViewModel WhatsAppSettings { get; set; } = new WhatsAppSettingsViewModel();
}
public class WhatsAppSettingsViewModel : WhatsAppSettings
{
/// <summary>
/// Gets or sets the notification templates.
/// </summary>
/// <value>
/// The notification templates.
/// </value>
public List<NotificationTemplates> NotificationTemplates { get; set; }
}
}

@ -64,6 +64,7 @@ using Ombi.Schedule.Processor;
using Ombi.Store.Entities; using Ombi.Store.Entities;
using Quartz.Spi; using Quartz.Spi;
using Ombi.Api.MusicBrainz; using Ombi.Api.MusicBrainz;
using Ombi.Api.Twilio;
namespace Ombi.DependencyInjection namespace Ombi.DependencyInjection
{ {
@ -147,6 +148,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<ILidarrApi, LidarrApi>(); services.AddTransient<ILidarrApi, LidarrApi>();
services.AddTransient<IGroupMeApi, GroupMeApi>(); services.AddTransient<IGroupMeApi, GroupMeApi>();
services.AddTransient<IMusicBrainzApi, MusicBrainzApi>(); services.AddTransient<IMusicBrainzApi, MusicBrainzApi>();
services.AddTransient<IWhatsAppApi, WhatsAppApi>();
} }
public static void RegisterStore(this IServiceCollection services) { public static void RegisterStore(this IServiceCollection services) {

@ -37,6 +37,7 @@
<ProjectReference Include="..\Ombi.Api.Telegram\Ombi.Api.Telegram.csproj" /> <ProjectReference Include="..\Ombi.Api.Telegram\Ombi.Api.Telegram.csproj" />
<ProjectReference Include="..\Ombi.Api.Trakt\Ombi.Api.Trakt.csproj" /> <ProjectReference Include="..\Ombi.Api.Trakt\Ombi.Api.Trakt.csproj" />
<ProjectReference Include="..\Ombi.Api.TvMaze\Ombi.Api.TvMaze.csproj" /> <ProjectReference Include="..\Ombi.Api.TvMaze\Ombi.Api.TvMaze.csproj" />
<ProjectReference Include="..\Ombi.Api.Twilio\Ombi.Api.Twilio.csproj" />
<ProjectReference Include="..\Ombi.Core\Ombi.Core.csproj" /> <ProjectReference Include="..\Ombi.Core\Ombi.Core.csproj" />
<ProjectReference Include="..\Ombi.Notifications\Ombi.Notifications.csproj" /> <ProjectReference Include="..\Ombi.Notifications\Ombi.Notifications.csproj" />
<ProjectReference Include="..\Ombi.Schedule\Ombi.Schedule.csproj" /> <ProjectReference Include="..\Ombi.Schedule\Ombi.Schedule.csproj" />

@ -33,6 +33,7 @@ namespace Ombi.Helpers
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 GotifyNotification => new EventId(4007);
public static EventId WhatsApp => new EventId(4008);
public static EventId TvSender => new EventId(5000); public static EventId TvSender => new EventId(5000);
public static EventId SonarrSender => new EventId(5001); public static EventId SonarrSender => new EventId(5001);

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

@ -20,6 +20,8 @@ namespace Ombi.Mapping.Profiles
CreateMap<MobileNotificationsViewModel, MobileNotificationSettings>().ReverseMap(); CreateMap<MobileNotificationsViewModel, MobileNotificationSettings>().ReverseMap();
CreateMap<NewsletterNotificationViewModel, NewsletterSettings>().ReverseMap(); CreateMap<NewsletterNotificationViewModel, NewsletterSettings>().ReverseMap();
CreateMap<GotifyNotificationViewModel, GotifySettings>().ReverseMap(); CreateMap<GotifyNotificationViewModel, GotifySettings>().ReverseMap();
CreateMap<WhatsAppSettingsViewModel, WhatsAppSettings>().ReverseMap();
CreateMap<TwilioSettingsViewModel, TwilioSettings>().ReverseMap();
} }
} }
} }

@ -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<TwilioSettings>
{
public WhatsAppNotification(IWhatsAppApi api, ISettingsService<TwilioSettings> sn, ILogger<WhatsAppNotification> 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 => "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);
}
}
}

@ -22,6 +22,7 @@
<ProjectReference Include="..\Ombi.Api.Pushover\Ombi.Api.Pushover.csproj" /> <ProjectReference Include="..\Ombi.Api.Pushover\Ombi.Api.Pushover.csproj" />
<ProjectReference Include="..\Ombi.Api.Slack\Ombi.Api.Slack.csproj" /> <ProjectReference Include="..\Ombi.Api.Slack\Ombi.Api.Slack.csproj" />
<ProjectReference Include="..\Ombi.Api.Telegram\Ombi.Api.Telegram.csproj" /> <ProjectReference Include="..\Ombi.Api.Telegram\Ombi.Api.Telegram.csproj" />
<ProjectReference Include="..\Ombi.Api.Twilio\Ombi.Api.Twilio.csproj" />
<ProjectReference Include="..\Ombi.Notifications.Templates\Ombi.Notifications.Templates.csproj" /> <ProjectReference Include="..\Ombi.Notifications.Templates\Ombi.Notifications.Templates.csproj" />
<ProjectReference Include="..\Ombi.Settings\Ombi.Settings.csproj" /> <ProjectReference Include="..\Ombi.Settings\Ombi.Settings.csproj" />
<ProjectReference Include="..\Ombi.Store\Ombi.Store.csproj" /> <ProjectReference Include="..\Ombi.Store\Ombi.Store.csproj" />

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

@ -108,7 +108,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Hubs", "Ombi.Hubs\Ombi
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Api.GroupMe", "Ombi.Api.GroupMe\Ombi.Api.GroupMe.csproj", "{9266403C-B04D-4C0F-AC39-82F12C781949}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Api.GroupMe", "Ombi.Api.GroupMe\Ombi.Api.GroupMe.csproj", "{9266403C-B04D-4C0F-AC39-82F12C781949}"
EndProject 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 EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution 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}.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.ActiveCfg = Release|Any CPU
{C5C1769B-4197-4410-A160-0EEF39EDDC98}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -334,6 +340,7 @@ Global
{27111E7C-748E-4996-BD71-2117027C6460} = {6F42AB98-9196-44C4-B888-D5E409F415A1} {27111E7C-748E-4996-BD71-2117027C6460} = {6F42AB98-9196-44C4-B888-D5E409F415A1}
{9266403C-B04D-4C0F-AC39-82F12C781949} = {9293CA11-360A-4C20-A674-B9E794431BF5} {9266403C-B04D-4C0F-AC39-82F12C781949} = {9293CA11-360A-4C20-A674-B9E794431BF5}
{C5C1769B-4197-4410-A160-0EEF39EDDC98} = {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 EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {192E9BF8-00B4-45E4-BCCC-4C215725C869} SolutionGuid = {192E9BF8-00B4-45E4-BCCC-4C215725C869}

@ -27,11 +27,16 @@ export interface INotificationTemplates {
} }
export enum NotificationAgent { export enum NotificationAgent {
Email, Email = 0,
Discord, Discord = 1,
Pushbullet, Pushbullet = 2,
Pushover, Pushover = 3,
Telegram, Telegram = 4,
Slack = 5,
Mattermost = 6,
Mobile = 7,
Gotify = 8,
WhatsApp = 9
} }
export enum NotificationType { export enum NotificationType {
@ -47,6 +52,7 @@ export enum NotificationType {
IssueResolved = 9, IssueResolved = 9,
IssueComment = 10, IssueComment = 10,
Newsletter = 11, Newsletter = 11,
WhatsApp = 12,
} }
export interface IDiscordNotifcationSettings extends INotificationSettings { export interface IDiscordNotifcationSettings extends INotificationSettings {
@ -85,6 +91,18 @@ export interface IPushbulletNotificationSettings extends INotificationSettings {
channelTag: string; 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 { export interface IPushoverNotificationSettings extends INotificationSettings {
accessToken: string; accessToken: string;
notificationTemplates: INotificationTemplates[]; notificationTemplates: INotificationTemplates[];

@ -91,6 +91,7 @@ export interface INotificationPreferences {
} }
export enum INotificationAgent { export enum INotificationAgent {
Email = 0, Email = 0,
Discord = 1, Discord = 1,
Pushbullet = 2, Pushbullet = 2,
@ -99,4 +100,6 @@ export enum INotificationAgent {
Slack = 5, Slack = 5,
Mattermost = 6, Mattermost = 6,
Mobile = 7, Mobile = 7,
Gotify = 8,
WhatsApp = 9
} }

@ -1,3 +1,3 @@
<div *ngIf="movie && radarrEnabled"> <div *ngIf="movie && radarrEnabled" class="text-center">
<button mat-raised-button color="warn" class="text-center" (click)="openAdvancedOptions();">Advanced Options</button> <button mat-raised-button color="warn" class="text-center" (click)="openAdvancedOptions();">Advanced Options</button>
</div> </div>

@ -41,11 +41,11 @@
<td mat-cell *matCellDef="let element"> {{element.requestStatus | translate}} </td> <td mat-cell *matCellDef="let element"> {{element.requestStatus | translate}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions" >
<th mat-header-cell *matHeaderCellDef> </th> <th mat-header-cell *matHeaderCellDef> </th>
<td mat-cell *matCellDef="let element"> <td mat-cell *matCellDef="let element" >
<button mat-raised-button color="accent" [routerLink]="'/details/movie/' + element.theMovieDbId">Details</button> <button mat-raised-button color="accent" [routerLink]="'/details/movie/' + element.theMovieDbId">Details</button>
<button mat-raised-button color="warn" (click)="openOptions(element)">Options</button> <button mat-raised-button color="warn" (click)="openOptions(element)" *ngIf="isAdmin">Options</button>
</td> </td>
</ng-container> </ng-container>

@ -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 { IMovieRequests, IRequestsViewModel } from "../../../interfaces";
import { MatPaginator, MatSort } from "@angular/material"; import { MatPaginator, MatSort } from "@angular/material";
import { merge, Observable, of as observableOf } from 'rxjs'; import { merge, Observable, of as observableOf } from 'rxjs';
import { catchError, map, startWith, switchMap } from 'rxjs/operators'; import { catchError, map, startWith, switchMap } from 'rxjs/operators';
import { RequestServiceV2 } from "../../../services/requestV2.service"; import { RequestServiceV2 } from "../../../services/requestV2.service";
import { AuthService } from "../../../auth/auth.service";
@Component({ @Component({
templateUrl: "./movies-grid.component.html", 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 displayedColumns: string[] = ['requestedUser.requestedBy', 'title', 'requestedDate', 'status', 'requestStatus', 'actions'];
public gridCount: string = "15"; public gridCount: string = "15";
public showUnavailableRequests: boolean; 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(MatPaginator, { static: false }) paginator: MatPaginator;
@ViewChild(MatSort, { static: false }) sort: MatSort; @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.dataSource = results.collection;
// this.resultsLength = results.total; // 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. // If the user changes the sort order, reset back to the first page.
this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0); this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0);
@ -69,10 +74,16 @@ export class MoviesGridComponent implements AfterViewInit {
} }
public openOptions(request: IMovieRequests) { 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; 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 });
} }
} }

@ -2,4 +2,7 @@
<a (click)="delete()" mat-list-item> <a (click)="delete()" mat-list-item>
<span mat-line>Delete Request</span> <span mat-line>Delete Request</span>
</a> </a>
<a *ngIf="data.canApprove" (click)="approve()" mat-list-item>
<span mat-line>Approve Request</span>
</a>
</mat-nav-list> </mat-nav-list>

@ -1,7 +1,8 @@
import {Component, Inject} from '@angular/core'; import { Component, Inject } from '@angular/core';
import {MAT_BOTTOM_SHEET_DATA, MatBottomSheetRef} from '@angular/material/bottom-sheet'; import { MAT_BOTTOM_SHEET_DATA, MatBottomSheetRef } from '@angular/material/bottom-sheet';
import { RequestService } from '../../../services'; import { RequestService } from '../../../services';
import { RequestType } from '../../../interfaces'; import { RequestType } from '../../../interfaces';
import { UpdateType } from '../../models/UpdateType';
@Component({ @Component({
selector: 'request-options', selector: 'request-options',
@ -9,17 +10,29 @@ import { RequestType } from '../../../interfaces';
}) })
export class RequestOptionsComponent { export class RequestOptionsComponent {
constructor(@Inject(MAT_BOTTOM_SHEET_DATA) public data: any, constructor(@Inject(MAT_BOTTOM_SHEET_DATA) public data: any,
private requestService: RequestService, private bottomSheetRef: MatBottomSheetRef<RequestOptionsComponent>) { } private requestService: RequestService, private bottomSheetRef: MatBottomSheetRef<RequestOptionsComponent>) { }
public async delete() { public async delete() {
if (this.data.type === RequestType.movie) { 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) { if (this.data.type === RequestType.tvShow) {
await this.requestService.deleteChild(this.data.id).toPromise(); 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; return;
} }
} }

@ -1,6 +1,8 @@
import { Component } from "@angular/core"; import { Component, ViewChild } from "@angular/core";
import { MatBottomSheet } from "@angular/material"; import { MatBottomSheet } from "@angular/material";
import { RequestOptionsComponent } from "./options/request-options.component"; import { RequestOptionsComponent } from "./options/request-options.component";
import { UpdateType } from "../models/UpdateType";
import { MoviesGridComponent } from "./movies-grid/movies-grid.component";
@Component({ @Component({
templateUrl: "./requests-list.component.html", templateUrl: "./requests-list.component.html",
@ -9,15 +11,24 @@ import { RequestOptionsComponent } from "./options/request-options.component";
export class RequestsListComponent { export class RequestsListComponent {
constructor(private bottomSheet: MatBottomSheet) { } constructor(private bottomSheet: MatBottomSheet) { }
public onOpenOptions(event: {request: any, filter: any}) { public onOpenOptions(event: { request: any, filter: any, onChange: any }) {
const ref = this.bottomSheet.open(RequestOptionsComponent, { data: { id: event.request.id, type: event.request.requestType } }); const ref = this.bottomSheet.open(RequestOptionsComponent, { data: { id: event.request.id, type: event.request.requestType, canApprove: event.request.canApprove } });
ref.afterDismissed().subscribe((result) => { 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; return;
} }
event.filter();
}); });
} }
} }

@ -1,18 +1,18 @@
<div class="mat-elevation-z8"> <div class="mat-elevation-z8">
<grid-spinner [loading]="isLoadingResults"></grid-spinner> <grid-spinner [loading]="isLoadingResults"></grid-spinner>
<mat-form-field> <mat-form-field>
<mat-select placeholder="Requests to Display" [(value)]="gridCount" (selectionChange)="ngAfterViewInit()"> <mat-select placeholder="Requests to Display" [(value)]="gridCount" (selectionChange)="ngAfterViewInit()">
<mat-option value="10">10</mat-option> <mat-option value="10">10</mat-option>
<mat-option value="15">15</mat-option> <mat-option value="15">15</mat-option>
<mat-option value="30">30</mat-option> <mat-option value="30">30</mat-option>
<mat-option value="100">100</mat-option> <mat-option value="100">100</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<table mat-table [dataSource]="dataSource" class="table" matSort matSortActive="title" <table mat-table [dataSource]="dataSource" class="table" matSort matSortActive="title" matSortDisableClear
matSortDisableClear matSortDirection="desc"> matSortDirection="desc">
<ng-container matColumnDef="series"> <ng-container matColumnDef="series">
@ -21,44 +21,42 @@
</ng-container> </ng-container>
<ng-container matColumnDef="requestedBy"> <ng-container matColumnDef="requestedBy">
<th mat-header-cell *matHeaderCellDef > Requested By </th> <th mat-header-cell *matHeaderCellDef> Requested By </th>
<td mat-cell *matCellDef="let element"> {{element.requestedUser.userAlias}} </td> <td mat-cell *matCellDef="let element"> {{element.requestedUser.userAlias}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef > Status </th>
<td mat-cell *matCellDef="let element">
{{element.parentRequest.status}}
</td>
</ng-container>
<ng-container matColumnDef="requestedDate"> <ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef mat-sort-header disableClear> Requested Date </th> <th mat-header-cell *matHeaderCellDef> Status </th>
<td mat-cell *matCellDef="let element"> <td mat-cell *matCellDef="let element">
{{element.requestedDate | amLocal | amDateFormat: 'LL'}} {{element.parentRequest.status}}
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="requestStatus"> <ng-container matColumnDef="requestedDate">
<th mat-header-cell *matHeaderCellDef > Request Status </th> <th mat-header-cell *matHeaderCellDef mat-sort-header disableClear> Requested Date </th>
<td mat-cell *matCellDef="let element"> <td mat-cell *matCellDef="let element">
<div *ngIf="element.approved && !element.available">{{'Common.ProcessingRequest' | translate}}</div> {{element.requestedDate | amLocal | amDateFormat: 'LL'}}
<div *ngIf="element.requested && !element.approved && !element.available">{{'Common.PendingApproval' | </td>
translate}} </ng-container>
</div>
<div *ngIf="!element.requested && !element.available && !element.approved">{{'Common.NotRequested' | <ng-container matColumnDef="requestStatus">
translate}} <th mat-header-cell *matHeaderCellDef> Request Status </th>
</div> <td mat-cell *matCellDef="let element">
</td> <div *ngIf="element.approved && !element.available">{{'Common.ProcessingRequest' | translate}}</div>
</ng-container> <div *ngIf="!element.approved && !element.available">{{'Common.PendingApproval' |translate}}</div>
<div *ngIf="element.available">{{'Common.Available' | translate}}</div>
</td>
</ng-container>
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> </th> <th mat-header-cell *matHeaderCellDef> </th>
<td mat-cell *matCellDef="let element"> <td mat-cell *matCellDef="let element">
<button mat-raised-button color="accent" [routerLink]="'/details/tv/' + element.parentRequest.tvDbId">Details</button> <button mat-raised-button color="accent"
<button mat-raised-button color="warn" (click)="openOptions(element)">Options</button> [routerLink]="'/details/tv/' + element.parentRequest.tvDbId">Details</button>
<button mat-raised-button color="warn" (click)="openOptions(element)" *ngIf="isAdmin">Options</button>
</td> </td>
</ng-container> </ng-container>
@ -67,4 +65,4 @@
</table> </table>
<mat-paginator [length]="resultsLength" [pageSize]="gridCount"></mat-paginator> <mat-paginator [length]="resultsLength" [pageSize]="gridCount"></mat-paginator>
</div> </div>

@ -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 { IRequestsViewModel, IChildRequests } from "../../../interfaces";
import { MatPaginator, MatSort } from "@angular/material"; import { MatPaginator, MatSort } from "@angular/material";
import { merge, of as observableOf, Observable } from 'rxjs'; import { merge, of as observableOf, Observable } from 'rxjs';
import { catchError, map, startWith, switchMap } from 'rxjs/operators'; import { catchError, map, startWith, switchMap } from 'rxjs/operators';
import { RequestServiceV2 } from "../../../services/requestV2.service"; import { RequestServiceV2 } from "../../../services/requestV2.service";
import { AuthService } from "../../../auth/auth.service";
@Component({ @Component({
templateUrl: "./tv-grid.component.html", templateUrl: "./tv-grid.component.html",
@ -18,18 +19,21 @@ export class TvGridComponent implements AfterViewInit {
public displayedColumns: string[] = ['series', 'requestedBy', 'status', 'requestStatus', 'requestedDate','actions']; public displayedColumns: string[] = ['series', 'requestedBy', 'status', 'requestStatus', 'requestedDate','actions'];
public gridCount: string = "15"; public gridCount: string = "15";
public showUnavailableRequests: boolean; 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(MatPaginator, {static: false}) paginator: MatPaginator;
@ViewChild(MatSort, {static: false}) sort: MatSort; @ViewChild(MatSort, {static: false}) sort: MatSort;
constructor(private requestService: RequestServiceV2) { constructor(private requestService: RequestServiceV2, private auth: AuthService,
private ref: ChangeDetectorRef) {
} }
public async ngAfterViewInit() { 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. // If the user changes the sort order, reset back to the first page.
this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0); 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) => { const filter = () => { this.dataSource = this.dataSource.filter((req) => {
return req.id !== request.id; 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<IRequestsViewModel<IChildRequests>> { private loadData(): Observable<IRequestsViewModel<IChildRequests>> {

@ -0,0 +1,4 @@
export enum UpdateType {
Delete,
Approve
}

@ -24,6 +24,7 @@ import {
ISlackNotificationSettings, ISlackNotificationSettings,
ISonarrSettings, ISonarrSettings,
ITelegramNotifcationSettings, ITelegramNotifcationSettings,
IWhatsAppSettings,
} from "../../interfaces"; } from "../../interfaces";
@Injectable() @Injectable()
@ -52,6 +53,10 @@ export class TesterService extends ServiceHelpers {
return this.http.post<boolean>(`${this.url}mattermost`, JSON.stringify(settings), {headers: this.headers}); return this.http.post<boolean>(`${this.url}mattermost`, JSON.stringify(settings), {headers: this.headers});
} }
public whatsAppTest(settings: IWhatsAppSettings): Observable<boolean> {
return this.http.post<boolean>(`${this.url}whatsapp`, JSON.stringify(settings), {headers: this.headers});
}
public slackTest(settings: ISlackNotificationSettings): Observable<boolean> { public slackTest(settings: ISlackNotificationSettings): Observable<boolean> {
return this.http.post<boolean>(`${this.url}slack`, JSON.stringify(settings), {headers: this.headers}); return this.http.post<boolean>(`${this.url}slack`, JSON.stringify(settings), {headers: this.headers});
} }

@ -36,6 +36,7 @@ import {
IUpdateSettings, IUpdateSettings,
IUserManagementSettings, IUserManagementSettings,
IVoteSettings, IVoteSettings,
ITwilioSettings,
} from "../interfaces"; } from "../interfaces";
import { ServiceHelpers } from "./service.helpers"; import { ServiceHelpers } from "./service.helpers";
@ -254,6 +255,15 @@ export class SettingsService extends ServiceHelpers {
.post<boolean>(`${this.url}/notifications/telegram`, JSON.stringify(settings), {headers: this.headers}); .post<boolean>(`${this.url}/notifications/telegram`, JSON.stringify(settings), {headers: this.headers});
} }
public getTwilioSettings(): Observable<ITwilioSettings> {
return this.http.get<ITwilioSettings>(`${this.url}/notifications/twilio`, {headers: this.headers});
}
public saveTwilioSettings(settings: ITwilioSettings): Observable<boolean> {
return this.http
.post<boolean>(`${this.url}/notifications/twilio`, JSON.stringify(settings), {headers: this.headers});
}
public getJobSettings(): Observable<IJobSettings> { public getJobSettings(): Observable<IJobSettings> {
return this.http.get<IJobSettings>(`${this.url}/jobs`, {headers: this.headers}); return this.http.get<IJobSettings>(`${this.url}/jobs`, {headers: this.headers});
} }

@ -1,36 +1,26 @@
 <wiki [url]="'https://github.com/tidusjar/Ombi/wiki/Notification-Template-Variables'" [text]="'Notification Variables'">
<wiki [url]="'https://github.com/tidusjar/Ombi/wiki/Notification-Template-Variables'" [text]="'Notification Variables'"></wiki> </wiki>
<br><br> <br><br>
<mat-accordion>
<mat-expansion-panel *ngFor="let template of templates">
<mat-expansion-panel-header>
<mat-panel-title>
{{NotificationType[template.notificationType] | humanize}}
</mat-panel-title>
</mat-expansion-panel-header>
<ngb-accordion [closeOthers]="true" activeIds="0-header"> <div>
<ngb-panel *ngFor="let template of templates" id="{{template.notificationType}}" title="{{NotificationType[template.notificationType] | humanize}}"> <mat-slide-toggle [(ngModel)]="template.enabled">Enable</mat-slide-toggle>
<ng-template ngbPanelContent> </div>
<div class="panel panel-default a">
<div class="panel-body">
<div class="form-group">
<div class="checkbox">
<input type="checkbox" id="enabled" [(ngModel)]="template.enabled" ng-checked="template.enabled"><label for="enabled">Enable</label>
</div>
</div>
<div class="form-group" *ngIf="showSubject">
<label class="control-label">Subject</label>
<div>
<input type="text" class="form-control form-control-custom" [(ngModel)]="template.subject" value="{{template.subject}}">
</div>
</div>
<div class="form-group"> <mat-form-field *ngIf="showSubject">
<label class="control-label">Message</label> <input matInput placeholder="Subject" [(ngModel)]="template.subject">
<div> </mat-form-field>
<textarea type="text" class="form-control form-control-custom" [(ngModel)]="template.message" value="{{template.message}}"></textarea>
</div>
</div>
</div>
</div>
</ng-template> <mat-form-field>
</ngb-panel> <textarea matInput placeholder="Message" [(ngModel)]="template.message"></textarea>
</ngb-accordion> </mat-form-field>
</mat-expansion-panel>
</mat-accordion>

@ -0,0 +1,22 @@
<settings-menu>
</settings-menu>
<div *ngIf="form" class="container">
<fieldset>
<legend>Twilio</legend>
<span>Below are the supported integrations with Twilio</span>
<form novalidate [formGroup]="form" (ngSubmit)="onSubmit(form)">
<mat-tab-group>
<mat-tab label="WhatsApp">
<app-whatsapp [form]="form" [templates]="templates"></app-whatsapp>
</mat-tab>
</mat-tab-group>
<div class=" md-form-field ">
<div>
<button mat-raised-button type="submit " color="primary" [disabled]="form.invalid ">Submit</button>
</div>
</div>
</form>
</fieldset>
</div>

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

@ -0,0 +1,37 @@
<div class="container" style="padding-top: 3%;">
<div [formGroup]="form" class="col">
<div formGroupName="whatsAppSettings" class="row">
<div class="col">
<div>
<mat-slide-toggle formControlName="enabled">Enable</mat-slide-toggle>
</div>
<div class="md-form-field">
<mat-form-field>
<input matInput placeholder="From Number" formControlName="from" matTooltip="The mobile number that the WhatsApp message is from, with the international prefix">
</mat-form-field>
</div>
<div class="md-form-field">
<mat-form-field>
<input matInput placeholder="Account SID" formControlName="accountSid" matTooltip="The Account SID that you can find from the Programmable SMS Dashboard">
</mat-form-field>
</div>
<div class="md-form-field">
<mat-form-field>
<input matInput placeholder="Authentication Token" formControlName="authToken" matTooltip="The Auth Token that you can find from the Programmable SMS Dashboard">
</mat-form-field>
</div>
<div class="md-form-field">
<div>
<button mat-raised-button type="button" color="accent" (click)="test(form)">Test</button>
</div>
</div>
</div>
<div class="col">
<notification-templates [templates]="templates" [showSubject]="false"></notification-templates>
</div>
</div>
</div>
</div>

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

@ -55,6 +55,8 @@ import { MatMenuModule} from "@angular/material";
import { SharedModule } from "../shared/shared.module"; import { SharedModule } from "../shared/shared.module";
import { HubService } from "../services/hub.service"; import { HubService } from "../services/hub.service";
import { LogsComponent } from "./logs/logs.component"; import { LogsComponent } from "./logs/logs.component";
import { TwilioComponent } from "./notifications/twilio/twilio.component";
import { WhatsAppComponent } from "./notifications/twilio/whatsapp.component";
const routes: Routes = [ const routes: Routes = [
{ path: "Ombi", component: OmbiComponent, canActivate: [AuthGuard] }, { path: "Ombi", component: OmbiComponent, canActivate: [AuthGuard] },
@ -72,6 +74,7 @@ const routes: Routes = [
{ path: "Pushbullet", component: PushbulletComponent, canActivate: [AuthGuard] }, { path: "Pushbullet", component: PushbulletComponent, canActivate: [AuthGuard] },
{ path: "Gotify", component: GotifyComponent, canActivate: [AuthGuard] }, { path: "Gotify", component: GotifyComponent, canActivate: [AuthGuard] },
{ path: "Mattermost", component: MattermostComponent, canActivate: [AuthGuard] }, { path: "Mattermost", component: MattermostComponent, canActivate: [AuthGuard] },
{ path: "Twilio", component: TwilioComponent, 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] },
{ path: "CouchPotato", component: CouchPotatoComponent, canActivate: [AuthGuard] }, { path: "CouchPotato", component: CouchPotatoComponent, canActivate: [AuthGuard] },
@ -149,6 +152,8 @@ const routes: Routes = [
TheMovieDbComponent, TheMovieDbComponent,
FailedRequestsComponent, FailedRequestsComponent,
LogsComponent, LogsComponent,
TwilioComponent,
WhatsAppComponent
], ],
exports: [ exports: [
RouterModule, RouterModule,

@ -2,62 +2,63 @@
<button mat-button [matMenuTriggerFor]="configurationmenu"><i class="fa fa-cogs" aria-hidden="true"></i> Configuration</button> <button mat-button [matMenuTriggerFor]="configurationmenu"><i class="fa fa-cogs" aria-hidden="true"></i> Configuration</button>
<mat-menu #configurationmenu="matMenu"> <mat-menu #configurationmenu="matMenu">
<button mat-menu-item [routerLink]="['/Settings/Customization']">Customization</button> <button mat-menu-item [routerLink]="['/Settings/Customization']">Customization</button>
<button mat-menu-item [routerLink]="['/Settings/LandingPage']">Landing Page</button> <button mat-menu-item [routerLink]="['/Settings/LandingPage']">Landing Page</button>
<button mat-menu-item [routerLink]="['/Settings/Issues']">Issues</button> <button mat-menu-item [routerLink]="['/Settings/Issues']">Issues</button>
<button mat-menu-item [routerLink]="['/Settings/UserManagement']">User Management</button> <button mat-menu-item [routerLink]="['/Settings/UserManagement']">User Management</button>
<button mat-menu-item [routerLink]="['/Settings/Authentication']">Authentication</button> <button mat-menu-item [routerLink]="['/Settings/Authentication']">Authentication</button>
<button mat-menu-item [routerLink]="['/Settings/Vote']">Vote</button> <button mat-menu-item [routerLink]="['/Settings/Vote']">Vote</button>
<button mat-menu-item [routerLink]="['/Settings/TheMovieDb']">The Movie Database</button> <button mat-menu-item [routerLink]="['/Settings/TheMovieDb']">The Movie Database</button>
</mat-menu> </mat-menu>
<button mat-button [matMenuTriggerFor]="mediaservermenu"><i class="fa fa-server" aria-hidden="true"></i> Media Server</button> <button mat-button [matMenuTriggerFor]="mediaservermenu"><i class="fa fa-server" aria-hidden="true"></i> Media Server</button>
<mat-menu #mediaservermenu="matMenu"> <mat-menu #mediaservermenu="matMenu">
<button mat-menu-item [routerLink]="['/Settings/Plex']">Plex</button> <button mat-menu-item [routerLink]="['/Settings/Plex']">Plex</button>
<button mat-menu-item [routerLink]="['/Settings/Emby']">Emby/Jellyfin</button> <button mat-menu-item [routerLink]="['/Settings/Emby']">Emby/Jellyfin</button>
</mat-menu> </mat-menu>
<button mat-button [matMenuTriggerFor]="tvmenu"><i class="fa fa-television" aria-hidden="true"></i> TV</button> <button mat-button [matMenuTriggerFor]="tvmenu"><i class="fa fa-television" aria-hidden="true"></i> TV</button>
<mat-menu #tvmenu="matMenu"> <mat-menu #tvmenu="matMenu">
<button mat-menu-item [routerLink]="['/Settings/Sonarr']">Sonarr</button> <button mat-menu-item [routerLink]="['/Settings/Sonarr']">Sonarr</button>
<button mat-menu-item [routerLink]="['/Settings/DogNzb']">DogNzb</button> <button mat-menu-item [routerLink]="['/Settings/DogNzb']">DogNzb</button>
<button mat-menu-item [routerLink]="['/Settings/SickRage']">SickRage</button> <button mat-menu-item [routerLink]="['/Settings/SickRage']">SickRage</button>
</mat-menu> </mat-menu>
<button mat-button [matMenuTriggerFor]="movieMenu"><i class="fa fa-film" aria-hidden="true"></i> Movies</button> <button mat-button [matMenuTriggerFor]="movieMenu"><i class="fa fa-film" aria-hidden="true"></i> Movies</button>
<mat-menu #movieMenu="matMenu"> <mat-menu #movieMenu="matMenu">
<button mat-menu-item [routerLink]="['/Settings/CouchPotato']">CouchPotato</button> <button mat-menu-item [routerLink]="['/Settings/CouchPotato']">CouchPotato</button>
<button mat-menu-item [routerLink]="['/Settings/DogNzb']">DogNzb</button> <button mat-menu-item [routerLink]="['/Settings/DogNzb']">DogNzb</button>
<button mat-menu-item [routerLink]="['/Settings/Radarr']">Radarr</button> <button mat-menu-item [routerLink]="['/Settings/Radarr']">Radarr</button>
</mat-menu> </mat-menu>
<button mat-button [matMenuTriggerFor]="musicMenu"><i class="fa fa-music" aria-hidden="true"></i> Music</button> <button mat-button [matMenuTriggerFor]="musicMenu"><i class="fa fa-music" aria-hidden="true"></i> Music</button>
<mat-menu #musicMenu="matMenu"> <mat-menu #musicMenu="matMenu">
<button mat-menu-item [routerLink]="['/Settings/Lidarr']">Lidarr</button> <button mat-menu-item [routerLink]="['/Settings/Lidarr']">Lidarr</button>
</mat-menu> </mat-menu>
<button mat-button [matMenuTriggerFor]="notificationMenu"><i class="fa fa-bell-o" aria-hidden="true"></i> Notifications</button> <button mat-button [matMenuTriggerFor]="notificationMenu"><i class="fa fa-bell-o" aria-hidden="true"></i> Notifications</button>
<mat-menu #notificationMenu="matMenu"> <mat-menu #notificationMenu="matMenu">
<button mat-menu-item [routerLink]="['/Settings/Mobile']">Mobile</button> <button mat-menu-item [routerLink]="['/Settings/Mobile']">Mobile</button>
<button mat-menu-item [routerLink]="['/Settings/Email']">Email</button> <button mat-menu-item [routerLink]="['/Settings/Email']">Email</button>
<button mat-menu-item [routerLink]="['/Settings/MassEmail']">MassEmail</button> <button mat-menu-item [routerLink]="['/Settings/MassEmail']">MassEmail</button>
<button mat-menu-item [routerLink]="['/Settings/Newsletter']">Newsletter</button> <button mat-menu-item [routerLink]="['/Settings/Newsletter']">Newsletter</button>
<button mat-menu-item [routerLink]="['/Settings/Discord']">Discord</button> <button mat-menu-item [routerLink]="['/Settings/Discord']">Discord</button>
<button mat-menu-item [routerLink]="['/Settings/Slack']">Slack</button> <button mat-menu-item [routerLink]="['/Settings/Slack']">Slack</button>
<button mat-menu-item [routerLink]="['/Settings/Pushbullet']">Pushbullet</button> <button mat-menu-item [routerLink]="['/Settings/Pushbullet']">Pushbullet</button>
<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> <button mat-menu-item [routerLink]="['/Settings/Gotify']">Gotify</button>
<button mat-menu-item [routerLink]="['/Settings/Twilio']">Twilio</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>
<mat-menu #systemMenu="matMenu"> <mat-menu #systemMenu="matMenu">
<button mat-menu-item [routerLink]="['/Settings/About']">About</button> <button mat-menu-item [routerLink]="['/Settings/About']">About</button>
<button mat-menu-item [routerLink]="['/Settings/FailedRequests']">Failed Requests</button> <button mat-menu-item [routerLink]="['/Settings/FailedRequests']">Failed Requests</button>
<button mat-menu-item [routerLink]="['/Settings/Update']">Update</button> <button mat-menu-item [routerLink]="['/Settings/Update']">Update</button>
<button mat-menu-item [routerLink]="['/Settings/Jobs']">Scheduled Tasks</button> <button mat-menu-item [routerLink]="['/Settings/Jobs']">Scheduled Tasks</button>
<button mat-menu-item [routerLink]="['/Settings/Logs']">Logs</button> <button mat-menu-item [routerLink]="['/Settings/Logs']">Logs</button>
</mat-menu> </mat-menu>
<hr/> <hr/>

@ -1,5 +1,5 @@
<div class="col-md-12"> <div class="col">
<div *ngIf="!text" class="col-md-1 offset-md-11"> <div *ngIf="!text" class="col-md-4 ml-auto ">
<a href="{{url}}" target="_blank"> <a href="{{url}}" target="_blank">
<button mat-raised-button color="accent"> <button mat-raised-button color="accent">
<span>Wiki</span> <span>Wiki</span>
@ -7,7 +7,7 @@
</a> </a>
</div> </div>
<div *ngIf="text" class="col-md-1 offset-md-9"> <div *ngIf="text" class="col-md-4 ml-auto ">
<a href="{{url}}" target="_blank"> <a href="{{url}}" target="_blank">
<button mat-raised-button color="accent"> <button mat-raised-button color="accent">
<span>{{text}}</span> <span>{{text}}</span>

@ -2,6 +2,7 @@
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Ombi.Api.CouchPotato; using Ombi.Api.CouchPotato;
using Ombi.Api.Emby; using Ombi.Api.Emby;
@ -10,7 +11,9 @@ using Ombi.Api.Plex;
using Ombi.Api.Radarr; using Ombi.Api.Radarr;
using Ombi.Api.SickRage; using Ombi.Api.SickRage;
using Ombi.Api.Sonarr; using Ombi.Api.Sonarr;
using Ombi.Api.Twilio;
using Ombi.Attributes; using Ombi.Attributes;
using Ombi.Core.Authentication;
using Ombi.Core.Models.UI; using Ombi.Core.Models.UI;
using Ombi.Core.Notifications; using Ombi.Core.Notifications;
using Ombi.Core.Settings.Models.External; using Ombi.Core.Settings.Models.External;
@ -22,6 +25,7 @@ using Ombi.Notifications.Models;
using Ombi.Schedule.Jobs.Ombi; using Ombi.Schedule.Jobs.Ombi;
using Ombi.Settings.Settings.Models.External; using Ombi.Settings.Settings.Models.External;
using Ombi.Settings.Settings.Models.Notifications; using Ombi.Settings.Settings.Models.Notifications;
using Ombi.Store.Entities;
namespace Ombi.Controllers.V1.External namespace Ombi.Controllers.V1.External
{ {
@ -40,7 +44,7 @@ namespace Ombi.Controllers.V1.External
IPushbulletNotification pushbullet, ISlackNotification slack, IPushoverNotification po, IMattermostNotification mm, IPushbulletNotification pushbullet, ISlackNotification slack, IPushoverNotification po, IMattermostNotification mm,
IPlexApi plex, IEmbyApi emby, IRadarrApi radarr, ISonarrApi sonarr, ILogger<TesterController> log, IEmailProvider provider, IPlexApi plex, IEmbyApi emby, IRadarrApi radarr, ISonarrApi sonarr, ILogger<TesterController> log, IEmailProvider provider,
ICouchPotatoApi cpApi, ITelegramNotification telegram, ISickRageApi srApi, INewsletterJob newsletter, IMobileNotification mobileNotification, ICouchPotatoApi cpApi, ITelegramNotification telegram, ISickRageApi srApi, INewsletterJob newsletter, IMobileNotification mobileNotification,
ILidarrApi lidarrApi, IGotifyNotification gotifyNotification) ILidarrApi lidarrApi, IGotifyNotification gotifyNotification, IWhatsAppApi whatsAppApi, OmbiUserManager um)
{ {
Service = service; Service = service;
DiscordNotification = notification; DiscordNotification = notification;
@ -62,6 +66,8 @@ namespace Ombi.Controllers.V1.External
MobileNotification = mobileNotification; MobileNotification = mobileNotification;
LidarrApi = lidarrApi; LidarrApi = lidarrApi;
GotifyNotification = gotifyNotification; GotifyNotification = gotifyNotification;
WhatsAppApi = whatsAppApi;
UserManager = um;
} }
private INotificationService Service { get; } private INotificationService Service { get; }
@ -84,7 +90,8 @@ namespace Ombi.Controllers.V1.External
private INewsletterJob Newsletter { get; } private INewsletterJob Newsletter { get; }
private IMobileNotification MobileNotification { get; } private IMobileNotification MobileNotification { get; }
private ILidarrApi LidarrApi { get; } private ILidarrApi LidarrApi { get; }
private IWhatsAppApi WhatsAppApi { get; }
private OmbiUserManager UserManager {get;}
/// <summary> /// <summary>
/// Sends a test message to discord using the provided settings /// Sends a test message to discord using the provided settings
@ -459,5 +466,35 @@ namespace Ombi.Controllers.V1.External
return false; return false;
} }
} }
[HttpPost("whatsapp")]
public async Task<bool> WhatsAppTest([FromBody] WhatsAppSettingsViewModel settings)
{
try
{
var user = await UserManager.Users.Include(x => x.UserNotificationPreferences).FirstOrDefaultAsync(x => x.UserName == HttpContext.User.Identity.Name);
var status = await WhatsAppApi.SendMessage(new WhatsAppModel {
From = settings.From,
Message = "This is a test from Ombi!",
To = user.UserNotificationPreferences.FirstOrDefault(x => x.Agent == NotificationAgent.WhatsApp).Value
}, settings.AccountSid, settings.AuthToken);
if (status.HasValue())
{
return true;
}
else
{
return false;
}
}
catch (Exception e)
{
Log.LogError(LoggingEvents.Api, e, "Could not test Lidarr");
return false;
}
}
} }
} }

@ -963,6 +963,44 @@ namespace Ombi.Controllers.V1
return model; return model;
} }
/// <summary>
/// Gets the Twilio Notification Settings.
/// </summary>
/// <returns></returns>
[HttpGet("notifications/twilio")]
public async Task<TwilioSettingsViewModel> TwilioNotificationSettings()
{
var settings = await Get<TwilioSettings>();
var model = Mapper.Map<TwilioSettingsViewModel>(settings);
// Lookup to see if we have any templates saved
if(model.WhatsAppSettings == null)
{
model.WhatsAppSettings = new WhatsAppSettingsViewModel();
}
model.WhatsAppSettings.NotificationTemplates = BuildTemplates(NotificationAgent.WhatsApp);
return model;
}
/// <summary>
/// Saves the Mattermost notification settings.
/// </summary>
/// <param name="model">The model.</param>
/// <returns></returns>
[HttpPost("notifications/twilio")]
public async Task<bool> TwilioNotificationSettings([FromBody] TwilioSettingsViewModel model)
{
// Save the email settings
var settings = Mapper.Map<TwilioSettings>(model);
var result = await Save(settings);
// Save the templates
await TemplateRepository.UpdateRange(model.WhatsAppSettings.NotificationTemplates);
return result;
}
/// <summary> /// <summary>
/// Saves the Mobile notification settings. /// Saves the Mobile notification settings.
/// </summary> /// </summary>

@ -81,6 +81,7 @@
<ProjectReference Include="..\Ombi.Api.Github\Ombi.Api.Github.csproj" /> <ProjectReference Include="..\Ombi.Api.Github\Ombi.Api.Github.csproj" />
<ProjectReference Include="..\Ombi.Api.Lidarr\Ombi.Api.Lidarr.csproj" /> <ProjectReference Include="..\Ombi.Api.Lidarr\Ombi.Api.Lidarr.csproj" />
<ProjectReference Include="..\Ombi.Api.SickRage\Ombi.Api.SickRage.csproj" /> <ProjectReference Include="..\Ombi.Api.SickRage\Ombi.Api.SickRage.csproj" />
<ProjectReference Include="..\Ombi.Api.Twilio\Ombi.Api.Twilio.csproj" />
<ProjectReference Include="..\Ombi.Core\Ombi.Core.csproj" /> <ProjectReference Include="..\Ombi.Core\Ombi.Core.csproj" />
<ProjectReference Include="..\Ombi.DependencyInjection\Ombi.DependencyInjection.csproj" /> <ProjectReference Include="..\Ombi.DependencyInjection\Ombi.DependencyInjection.csproj" />
<ProjectReference Include="..\Ombi.Hubs\Ombi.Hubs.csproj" /> <ProjectReference Include="..\Ombi.Hubs\Ombi.Hubs.csproj" />

@ -2,12 +2,10 @@
using AutoMapper.EquivalencyExpression; using AutoMapper.EquivalencyExpression;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.StaticFiles; using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -24,7 +22,6 @@ using Ombi.Store.Context;
using Ombi.Store.Entities; using Ombi.Store.Entities;
using Ombi.Store.Repository; using Ombi.Store.Repository;
using Serilog; using Serilog;
using SQLitePCL;
using System; using System;
using System.IO; using System.IO;
using Microsoft.AspNetCore.StaticFiles.Infrastructure; using Microsoft.AspNetCore.StaticFiles.Infrastructure;

Loading…
Cancel
Save