Merge DNC into Master (#2034)

* Fix the issue where the user could not login if the plex account only allows email logins

* Fixed #2019

* Added Mass Email functionality (#2027)

* !wip

* !wip

* !qwip

* !wip

* Mass email is done

* Update README.md

* /bin/bash: wip: command not found
pull/2035/head
Jamie 6 years ago committed by GitHub
parent 0f9bbc80dc
commit dda467828c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

File diff suppressed because it is too large Load Diff

@ -70,10 +70,9 @@ We are planning to bring back these features in V3 but for now you can find a li
| Login page | Yes (brand new) | Yes |
| Custom Notification Messages | Yes | No |
| Sending newsletters | Planned | Yes |
| Send a Mass Email | Planned | Yes |
| Send a Mass Email | Yes | Yes |
| SickRage | Yes | Yes |
| CouchPotato | Yes | Yes |
| Watcher | Planned | Yes |
| DogNzb | Yes | No |
| Issues | Yes | Yes |
| Headphones | No (support dropped) | Yes |

@ -110,7 +110,8 @@ namespace Ombi.Core.Authentication
/// <returns></returns>
private async Task<bool> CheckPlexPasswordAsync(OmbiUser user, string password)
{
var result = await _plexApi.SignIn(new UserRequest { password = password, login = user.UserName });
var login = user.EmailLogin ? user.Email : user.UserName;
var result = await _plexApi.SignIn(new UserRequest { password = password, login = login });
if (result.user?.authentication_token != null)
{
return true;

@ -0,0 +1,10 @@
using System.Threading.Tasks;
using Ombi.Core.Models;
namespace Ombi.Core.Senders
{
public interface IMassEmailSender
{
Task<bool> SendMassEmail(MassEmailModel model);
}
}

@ -0,0 +1,40 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2018 Jamie Rees
// File: MassEmailModel.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System.Collections.Generic;
using Ombi.Store.Entities;
namespace Ombi.Core.Models
{
public class MassEmailModel
{
public string Subject { get; set; }
public string Body { get; set; }
public List<OmbiUser> Users { get; set; }
}
}

@ -0,0 +1,95 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2018 Jamie Rees
// File: MassEmailSender.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Ombi.Core.Authentication;
using Ombi.Core.Models;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Notifications;
using Ombi.Notifications.Models;
using Ombi.Settings.Settings.Models;
using Ombi.Settings.Settings.Models.Notifications;
using Ombi.Store.Entities;
namespace Ombi.Core.Senders
{
public class MassEmailSender : IMassEmailSender
{
public MassEmailSender(IEmailProvider emailProvider, ISettingsService<CustomizationSettings> custom, ISettingsService<EmailNotificationSettings> email,
ILogger<MassEmailSender> log, OmbiUserManager manager)
{
_email = emailProvider;
_customizationService = custom;
_emailService = email;
_log = log;
_userManager = manager;
}
private readonly IEmailProvider _email;
private readonly ISettingsService<CustomizationSettings> _customizationService;
private readonly ISettingsService<EmailNotificationSettings> _emailService;
private readonly ILogger<MassEmailSender> _log;
private readonly OmbiUserManager _userManager;
public async Task<bool> SendMassEmail(MassEmailModel model)
{
var customization = await _customizationService.GetSettingsAsync();
var email = await _emailService.GetSettingsAsync();
var messagesSent = new List<Task>();
foreach (var user in model.Users)
{
var fullUser = await _userManager.Users.FirstOrDefaultAsync(x => x.Id == user.Id);
if (!fullUser.Email.HasValue())
{
_log.LogInformation("User {0} has no email, cannot send mass email to this user", fullUser.UserName);
continue;
}
var resolver = new NotificationMessageResolver();
var curlys = new NotificationMessageCurlys();
curlys.Setup(fullUser, customization);
var template = new NotificationTemplates() { Message = model.Body, Subject = model.Subject };
var content = resolver.ParseMessage(template, curlys);
var msg = new NotificationMessage
{
Message = content.Message,
To = fullUser.Email,
Subject = content.Subject
};
messagesSent.Add(_email.SendAdHoc(msg, email));
_log.LogInformation("Sent mass email to user {0} @ {1}", fullUser.UserName, fullUser.Email);
}
await Task.WhenAll(messagesSent);
return true;
}
}
}

@ -80,6 +80,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<IRuleEvaluator, RuleEvaluator>();
services.AddTransient<IMovieSender, MovieSender>();
services.AddTransient<ITvSender, TvSender>();
services.AddTransient<IMassEmailSender, MassEmailSender>();
}
public static void RegisterHttp(this IServiceCollection services)
{

@ -81,11 +81,14 @@ namespace Ombi.Notifications
IssueStatus = opts.Substitutes.TryGetValue("IssueStatus", out val) ? val : string.Empty;
IssueSubject = opts.Substitutes.TryGetValue("IssueSubject", out val) ? val : string.Empty;
NewIssueComment = opts.Substitutes.TryGetValue("NewIssueComment", out val) ? val : string.Empty;
IssueUser = opts.Substitutes.TryGetValue("IssueUser", out val) ? val : string.Empty;
RequestedUser = opts.Substitutes.TryGetValue("IssueUser", out val) ? val : string.Empty;
}
// User Defined
public string RequestedUser { get; set; }
public string UserName => RequestedUser;
public string IssueUser => RequestedUser;
public string Title { get; set; }
public string RequestedDate { get; set; }
public string Type { get; set; }
@ -102,7 +105,6 @@ namespace Ombi.Notifications
public string IssueStatus { get; set; }
public string IssueSubject { get; set; }
public string NewIssueComment { get; set; }
public string IssueUser { get; set; }
// System Defined
private string LongDate => DateTime.Now.ToString("D");
@ -134,6 +136,7 @@ namespace Ombi.Notifications
{nameof(IssueSubject),IssueSubject},
{nameof(NewIssueComment),NewIssueComment},
{nameof(IssueUser),IssueUser},
{nameof(UserName),UserName},
};
}
}

@ -227,11 +227,21 @@ namespace Ombi.Schedule.Jobs.Plex
var existingImdb = false;
var existingMovieDbId = false;
var existingTvDbId = false;
existingImdb = await Repo.GetAll().AnyAsync(x => x.ImdbId == item.ImdbId && x.Type == PlexMediaTypeEntity.Show);
existingMovieDbId = await Repo.GetAll().AnyAsync(x => x.TheMovieDbId == item.TheMovieDbId && x.Type == PlexMediaTypeEntity.Show);
existingTvDbId = await Repo.GetAll().AnyAsync(x => x.TvDbId == item.TvDbId && x.Type == PlexMediaTypeEntity.Show);
if (item.ImdbId.HasValue())
{
existingImdb = await Repo.GetAll().AnyAsync(x =>
x.ImdbId == item.ImdbId && x.Type == PlexMediaTypeEntity.Show);
}
if (item.TheMovieDbId.HasValue())
{
existingMovieDbId = await Repo.GetAll().AnyAsync(x =>
x.TheMovieDbId == item.TheMovieDbId && x.Type == PlexMediaTypeEntity.Show);
}
if (item.TvDbId.HasValue())
{
existingTvDbId = await Repo.GetAll().AnyAsync(x =>
x.TvDbId == item.TvDbId && x.Type == PlexMediaTypeEntity.Show);
}
if (existingImdb || existingTvDbId || existingMovieDbId)
{
// We already have it!

@ -32,5 +32,8 @@ namespace Ombi.Store.Entities
[NotMapped]
public string UserAlias => string.IsNullOrEmpty(Alias) ? UserName : Alias;
[NotMapped]
public bool EmailLogin { get; set; }
}
}

@ -49,3 +49,14 @@ export interface IMobileUsersViewModel {
username: string;
devices: number;
}
export interface IMassEmailUserModel {
user: IUser;
selected: boolean;
}
export interface IMassEmailModel {
subject: string;
body: string;
users: IUser[];
}

@ -12,3 +12,4 @@ export * from "./status.service";
export * from "./job.service";
export * from "./issues.service";
export * from "./mobile.service";
export * from "./notificationMessage.service";

@ -0,0 +1,19 @@
import { PlatformLocation } from "@angular/common";
import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs/Rx";
import { IMassEmailModel } from "./../interfaces";
import { ServiceHelpers } from "./service.helpers";
@Injectable()
export class NotificationMessageService extends ServiceHelpers {
constructor(http: HttpClient, public platformLocation: PlatformLocation) {
super(http, "/api/v1/notifications/", platformLocation);
}
public sendMassEmail(model: IMassEmailModel): Observable<boolean> {
return this.http.post<boolean>(`${this.url}massemail/`, JSON.stringify(model) ,{headers: this.headers});
}
}

@ -0,0 +1,50 @@
<settings-menu></settings-menu>
<wiki [url]="'https://github.com/tidusjar/Ombi/wiki/Mass-Email'"></wiki>
<fieldset>
<legend>Mass Email</legend>
<div class="col-md-6">
<div class="form-group">
<input type="text" class="form-control form-control-custom " id="subject" name="subject" placeholder="Subject" [(ngModel)]="subject" [ngClass]="{'form-error': missingSubject}">
<small *ngIf="missingSubject" class="error-text">Hey! We need a subject!</small>
</div>
<div class="form-group" >
<textarea rows="10" type="text" class="form-control-custom form-control " id="themeContent" name="themeContent" [(ngModel)]="message"></textarea>
</div>
<div class="form-group">
<label for="logo" class="control-label">Message Preview</label>
<br/>
<small>May appear differently on email clients</small>
<hr/>
<div [innerHTML]="message"></div>
</div>
<div class="form-group">
<div>
<button type="submit" id="save" (click)="send()" class="btn btn-primary-outline">Send</button>
</div>
</div>
</div>
<div class="col-md-6">
<!--Users Section-->
<label class="control-label">Recipients</label>
<div class="form-group">
<div class="checkbox">
<input type="checkbox" id="all" (click)="selectAllUsers()">
<label for="all">Select All</label>
</div>
</div>
<div class="form-group" *ngFor="let u of users">
<div class="checkbox">
<input type="checkbox" id="{{u.user.id}}" [(ngModel)]="u.selected" (click)="selectSingleUser(u)">
<label for="{{u.user.id}}">{{u.user.userName}}</label>
</div>
</div>
</div>
</fieldset>

@ -0,0 +1,75 @@
import { Component, OnInit } from "@angular/core";
import { IMassEmailModel, IMassEmailUserModel } from "../../interfaces";
import { IdentityService, NotificationMessageService, NotificationService, SettingsService } from "../../services";
@Component({
templateUrl: "./massemail.component.html",
})
export class MassEmailComponent implements OnInit {
public users: IMassEmailUserModel[] = [];
public message: string;
public subject: string;
public missingSubject = false;
public emailEnabled: boolean;
constructor(private readonly notification: NotificationService,
private readonly identityService: IdentityService,
private readonly notificationMessageService: NotificationMessageService,
private readonly settingsService: SettingsService) {
}
public ngOnInit(): void {
this.identityService.getUsers().subscribe(x => {
x.forEach(u => {
this.users.push({
user: u,
selected: false,
});
});
});
this.settingsService.getEmailSettingsEnabled().subscribe(x => this.emailEnabled = x);
}
public selectAllUsers() {
this.users.forEach(u => u.selected = !u.selected);
}
public selectSingleUser(user: IMassEmailUserModel) {
user.selected = !user.selected;
}
public send() {
if(!this.subject) {
this.missingSubject = true;
return;
}
if(!this.emailEnabled) {
this.notification.error("You have not yet setup your email notifications, do that first!");
return;
}
this.missingSubject = false;
// Where(x => x.selected).Select(x => x.user)
const selectedUsers = this.users.filter(u => {
return u.selected;
}).map(u => u.user);
if(selectedUsers.length <=0) {
this.notification.error("You need to select at least one user to send the email");
return;
}
const model = <IMassEmailModel>{
users: selectedUsers,
subject: this.subject,
body: this.message,
};
this.notification.info("Sending","Sending mass email... Please wait");
this.notificationMessageService.sendMassEmail(model).subscribe(x => {
this.notification.success("We have sent the mass email to the users selected!");
});
}
}

@ -7,7 +7,8 @@ import { ClipboardModule } from "ngx-clipboard/dist";
import { AuthGuard } from "../auth/auth.guard";
import { AuthService } from "../auth/auth.service";
import { CouchPotatoService, EmbyService, IssuesService, JobService, MobileService, PlexService, RadarrService, SonarrService, TesterService, ValidationService } from "../services";
import { CouchPotatoService, EmbyService, IssuesService, JobService, MobileService, NotificationMessageService, PlexService, RadarrService,
SonarrService, TesterService, ValidationService } from "../services";
import { PipeModule } from "../pipes/pipe.module";
import { AboutComponent } from "./about/about.component";
@ -19,6 +20,7 @@ import { EmbyComponent } from "./emby/emby.component";
import { IssuesComponent } from "./issues/issues.component";
import { JobsComponent } from "./jobs/jobs.component";
import { LandingPageComponent } from "./landingpage/landingpage.component";
import { MassEmailComponent } from "./massemail/massemail.component";
import { DiscordComponent } from "./notifications/discord.component";
import { EmailNotificationComponent } from "./notifications/emailnotification.component";
import { MattermostComponent } from "./notifications/mattermost.component";
@ -66,6 +68,7 @@ const routes: Routes = [
{ path: "Issues", component: IssuesComponent, canActivate: [AuthGuard] },
{ path: "Authentication", component: AuthenticationComponent, canActivate: [AuthGuard] },
{ path: "Mobile", component: MobileComponent, canActivate: [AuthGuard] },
{ path: "MassEmail", component: MassEmailComponent, canActivate: [AuthGuard] },
];
@NgModule({
@ -114,6 +117,7 @@ const routes: Routes = [
IssuesComponent,
AuthenticationComponent,
MobileComponent,
MassEmailComponent,
],
exports: [
RouterModule,
@ -131,6 +135,7 @@ const routes: Routes = [
PlexService,
EmbyService,
MobileService,
NotificationMessageService,
],
})

@ -55,6 +55,7 @@
</a>
<ul class="dropdown-menu">
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Email']">Email</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/MassEmail']">Mass Email</a></li>
<!--<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Newsletter']">Newsletter</a></li>-->
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Discord']">Discord</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Slack']">Slack</a></li>

@ -0,0 +1,55 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2018 Jamie Rees
// File: NotificationsController.cs
// Created By: Jamie Rees
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// ************************************************************************/
#endregion
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Ombi.Attributes;
using Ombi.Core.Models;
using Ombi.Core.Senders;
namespace Ombi.Controllers
{
[Admin]
[ApiV1]
[Produces("application/json")]
public class NotificationsController : Controller
{
public NotificationsController(IMassEmailSender sender)
{
_sender = sender;
}
private readonly IMassEmailSender _sender;
[HttpPost("massemail")]
public async Task<bool> SendMassEmail([FromBody]MassEmailModel model)
{
return await _sender.SendMassEmail(model);
}
}
}

@ -58,6 +58,8 @@ namespace Ombi.Controllers
{
return new UnauthorizedResult();
}
user.EmailLogin = true;
}
// Verify Password

Loading…
Cancel
Save