mirror of
https://github.com/Ombi-app/Ombi.git
synced 2025-07-31 12:00:06 -07:00
Added the ability to unsubscribe from the newsletter #2137
This commit is contained in:
parent
043eb08aa5
commit
ac3f1941bc
11 changed files with 162 additions and 22 deletions
|
@ -2,6 +2,6 @@
|
||||||
{
|
{
|
||||||
public interface INewsletterTemplate
|
public interface INewsletterTemplate
|
||||||
{
|
{
|
||||||
string LoadTemplate(string subject, string intro, string tableHtml, string logo);
|
string LoadTemplate(string subject, string intro, string tableHtml, string logo, string unsubscribeLink);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -13,7 +13,7 @@ namespace Ombi.Notifications.Templates
|
||||||
if (string.IsNullOrEmpty(_templateLocation))
|
if (string.IsNullOrEmpty(_templateLocation))
|
||||||
{
|
{
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
_templateLocation = Path.Combine(Directory.GetCurrentDirectory(), "bin", "Debug", "netcoreapp3.0", "Templates", "NewsletterTemplate.html");
|
_templateLocation = Path.Combine(Directory.GetCurrentDirectory(), "bin", "Debug", "net5.0", "Templates", "NewsletterTemplate.html");
|
||||||
#else
|
#else
|
||||||
_templateLocation = Path.Combine(Directory.GetCurrentDirectory(), "Templates", "NewsletterTemplate.html");
|
_templateLocation = Path.Combine(Directory.GetCurrentDirectory(), "Templates", "NewsletterTemplate.html");
|
||||||
#endif
|
#endif
|
||||||
|
@ -29,9 +29,10 @@ namespace Ombi.Notifications.Templates
|
||||||
private const string Logo = "{@LOGO}";
|
private const string Logo = "{@LOGO}";
|
||||||
private const string TableLocation = "{@RECENTLYADDED}";
|
private const string TableLocation = "{@RECENTLYADDED}";
|
||||||
private const string IntroText = "{@INTRO}";
|
private const string IntroText = "{@INTRO}";
|
||||||
|
private const string Unsubscribe = "{@UNSUBSCRIBE}";
|
||||||
|
|
||||||
|
|
||||||
public string LoadTemplate(string subject, string intro, string tableHtml, string logo)
|
public string LoadTemplate(string subject, string intro, string tableHtml, string logo, string unsubscribeLink)
|
||||||
{
|
{
|
||||||
var sb = new StringBuilder(File.ReadAllText(TemplateLocation));
|
var sb = new StringBuilder(File.ReadAllText(TemplateLocation));
|
||||||
sb.Replace(SubjectKey, subject);
|
sb.Replace(SubjectKey, subject);
|
||||||
|
@ -39,6 +40,7 @@ namespace Ombi.Notifications.Templates
|
||||||
sb.Replace(IntroText, intro);
|
sb.Replace(IntroText, intro);
|
||||||
sb.Replace(DateKey, DateTime.Now.ToString("f"));
|
sb.Replace(DateKey, DateTime.Now.ToString("f"));
|
||||||
sb.Replace(Logo, string.IsNullOrEmpty(logo) ? OmbiLogo : logo);
|
sb.Replace(Logo, string.IsNullOrEmpty(logo) ? OmbiLogo : logo);
|
||||||
|
sb.Replace(Unsubscribe, string.IsNullOrEmpty(unsubscribeLink) ? string.Empty : unsubscribeLink);
|
||||||
|
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
|
|
|
@ -451,6 +451,11 @@
|
||||||
<div class="footer" style="clear: both; Margin-top: 10px; text-align: center; width: 100%;">
|
<div class="footer" style="clear: both; Margin-top: 10px; text-align: center; width: 100%;">
|
||||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="content-block powered-by" valign="top" align="center" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; color: #999999; font-size: 12px; text-align: center;">
|
||||||
|
<a href="{@UNSUBSCRIBE}" style="font-weight: 400; font-size: 12px; text-align: center; color: #ff761b;">Unsubscribe</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="content-block powered-by" valign="top" align="center" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; color: #999999; font-size: 12px; text-align: center;">
|
<td class="content-block powered-by" valign="top" align="center" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; color: #999999; font-size: 12px; text-align: center;">
|
||||||
Powered by <a href="https://github.com/Ombi-app/Ombi" style="font-weight: 400; font-size: 12px; text-align: center; text-decoration: none; color: #ff761b;">Ombi</a>
|
Powered by <a href="https://github.com/Ombi-app/Ombi" style="font-weight: 400; font-size: 12px; text-align: center; text-decoration: none; color: #ff761b;">Ombi</a>
|
||||||
|
|
35
src/Ombi.Schedule.Tests/NewsletterUnsubscribeTests.cs
Normal file
35
src/Ombi.Schedule.Tests/NewsletterUnsubscribeTests.cs
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
using NUnit.Framework;
|
||||||
|
using Ombi.Schedule.Jobs.Ombi;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Ombi.Schedule.Tests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class NewsletterUnsubscribeTests
|
||||||
|
{
|
||||||
|
[TestCaseSource(nameof(Data))]
|
||||||
|
public string GenerateUnsubscribeLinkTest(string appUrl, string id)
|
||||||
|
{
|
||||||
|
return NewsletterJob.GenerateUnsubscribeLink(appUrl, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<TestCaseData> Data
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
yield return new TestCaseData("https://google.com/", "1").Returns("https://google.com:443/unsubscribe/1").SetName("Fully Qualified");
|
||||||
|
yield return new TestCaseData("https://google.com", "1").Returns("https://google.com:443/unsubscribe/1").SetName("Missing Slash");
|
||||||
|
yield return new TestCaseData("google.com", "1").Returns("http://google.com:80/unsubscribe/1").SetName("Missing scheme");
|
||||||
|
yield return new TestCaseData("ombi.google.com", "1").Returns("http://ombi.google.com:80/unsubscribe/1").SetName("Sub domain missing scheme");
|
||||||
|
yield return new TestCaseData("https://ombi.google.com", "1").Returns("https://ombi.google.com:443/unsubscribe/1").SetName("Sub domain");
|
||||||
|
yield return new TestCaseData("https://ombi.google.com/", "1").Returns("https://ombi.google.com:443/unsubscribe/1").SetName("Sub domain with slash");
|
||||||
|
yield return new TestCaseData("https://google.com/ombi/", "1").Returns("https://google.com:443/ombi/unsubscribe/1").SetName("RP");
|
||||||
|
yield return new TestCaseData("https://google.com/ombi", "1").Returns("https://google.com:443/ombi/unsubscribe/1").SetName("RP missing slash");
|
||||||
|
yield return new TestCaseData("https://google.com:3577", "1").Returns("https://google.com:3577/unsubscribe/1").SetName("Port");
|
||||||
|
yield return new TestCaseData("https://google.com:3577/", "1").Returns("https://google.com:3577/unsubscribe/1").SetName("Port With Slash");
|
||||||
|
yield return new TestCaseData("", "1").Returns(string.Empty).SetName("Missing App URL empty");
|
||||||
|
yield return new TestCaseData(null, "1").Returns(string.Empty).SetName("Missing App URL null");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -228,32 +228,33 @@ namespace Ombi.Schedule.Jobs.Ombi
|
||||||
var messageContent = ParseTemplate(template, customization);
|
var messageContent = ParseTemplate(template, customization);
|
||||||
var email = new NewsletterTemplate();
|
var email = new NewsletterTemplate();
|
||||||
|
|
||||||
var html = email.LoadTemplate(messageContent.Subject, messageContent.Message, body, customization.Logo);
|
|
||||||
|
|
||||||
var bodyBuilder = new BodyBuilder
|
|
||||||
{
|
|
||||||
HtmlBody = html,
|
|
||||||
};
|
|
||||||
|
|
||||||
var message = new MimeMessage
|
|
||||||
{
|
|
||||||
Body = bodyBuilder.ToMessageBody(),
|
|
||||||
Subject = messageContent.Subject
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var user in users)
|
foreach (var user in users)
|
||||||
{
|
{
|
||||||
|
var url = GenerateUnsubscribeLink(customization.ApplicationUrl, user.Id);
|
||||||
|
var html = email.LoadTemplate(messageContent.Subject, messageContent.Message, body, customization.Logo, url);
|
||||||
|
|
||||||
|
var bodyBuilder = new BodyBuilder
|
||||||
|
{
|
||||||
|
HtmlBody = html,
|
||||||
|
};
|
||||||
|
|
||||||
|
var message = new MimeMessage
|
||||||
|
{
|
||||||
|
Body = bodyBuilder.ToMessageBody(),
|
||||||
|
Subject = messageContent.Subject
|
||||||
|
};
|
||||||
|
|
||||||
// Get the users to send it to
|
// Get the users to send it to
|
||||||
if (user.Email.IsNullOrEmpty())
|
if (user.Email.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// BCC the messages
|
// Send the message to the user
|
||||||
message.Bcc.Add(new MailboxAddress(user.Email.Trim(), user.Email.Trim()));
|
message.To.Add(new MailboxAddress(user.Email.Trim(), user.Email.Trim()));
|
||||||
}
|
|
||||||
|
|
||||||
// Send the email
|
// Send the email
|
||||||
await _email.Send(message, emailSettings);
|
await _email.Send(message, emailSettings);
|
||||||
|
}
|
||||||
|
|
||||||
// Now add all of this to the Recently Added log
|
// Now add all of this to the Recently Added log
|
||||||
var recentlyAddedLog = new HashSet<RecentlyAddedLog>();
|
var recentlyAddedLog = new HashSet<RecentlyAddedLog>();
|
||||||
|
@ -346,11 +347,14 @@ namespace Ombi.Schedule.Jobs.Ombi
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var unsubscribeLink = GenerateUnsubscribeLink(customization.ApplicationUrl, a.Id);
|
||||||
|
|
||||||
var messageContent = ParseTemplate(template, customization);
|
var messageContent = ParseTemplate(template, customization);
|
||||||
|
|
||||||
var email = new NewsletterTemplate();
|
var email = new NewsletterTemplate();
|
||||||
|
|
||||||
var html = email.LoadTemplate(messageContent.Subject, messageContent.Message, body, customization.Logo);
|
var html = email.LoadTemplate(messageContent.Subject, messageContent.Message, body, customization.Logo, unsubscribeLink);
|
||||||
|
|
||||||
await _email.Send(
|
await _email.Send(
|
||||||
new NotificationMessage { Message = html, Subject = messageContent.Subject, To = a.Email },
|
new NotificationMessage { Message = html, Subject = messageContent.Subject, To = a.Email },
|
||||||
|
@ -371,6 +375,21 @@ namespace Ombi.Schedule.Jobs.Ombi
|
||||||
.SendAsync(NotificationHub.NotificationEvent, "Newsletter Finished");
|
.SendAsync(NotificationHub.NotificationEvent, "Newsletter Finished");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string GenerateUnsubscribeLink(string applicationUrl, string id)
|
||||||
|
{
|
||||||
|
if (!applicationUrl.HasValue())
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!applicationUrl.EndsWith('/'))
|
||||||
|
{
|
||||||
|
applicationUrl += '/';
|
||||||
|
}
|
||||||
|
var b = new UriBuilder($"{applicationUrl}unsubscribe/{id}");
|
||||||
|
return b.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<HashSet<PlexServerContent>> GetMoviesWithoutId(HashSet<int> addedMovieLogIds, HashSet<PlexServerContent> needsMovieDbPlex)
|
private async Task<HashSet<PlexServerContent>> GetMoviesWithoutId(HashSet<int> addedMovieLogIds, HashSet<PlexServerContent> needsMovieDbPlex)
|
||||||
{
|
{
|
||||||
foreach (var movie in needsMovieDbPlex)
|
foreach (var movie in needsMovieDbPlex)
|
||||||
|
|
|
@ -91,6 +91,7 @@ const routes: Routes = [
|
||||||
{ loadChildren: () => import("./vote/vote.module").then(m => m.VoteModule), path: "vote" },
|
{ loadChildren: () => import("./vote/vote.module").then(m => m.VoteModule), path: "vote" },
|
||||||
{ loadChildren: () => import("./media-details/media-details.module").then(m => m.MediaDetailsModule), path: "details" },
|
{ loadChildren: () => import("./media-details/media-details.module").then(m => m.MediaDetailsModule), path: "details" },
|
||||||
{ loadChildren: () => import("./user-preferences/user-preferences.module").then(m => m.UserPreferencesModule), path: "user-preferences" },
|
{ loadChildren: () => import("./user-preferences/user-preferences.module").then(m => m.UserPreferencesModule), path: "user-preferences" },
|
||||||
|
{ loadChildren: () => import("./unsubscribe/unsubscribe.module").then(m => m.UnsubscribeModule), path: "unsubscribe" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -95,4 +95,8 @@ export class IdentityService extends ServiceHelpers {
|
||||||
public updateStreamingCountry(code: string): Observable<null> {
|
public updateStreamingCountry(code: string): Observable<null> {
|
||||||
return this.http.post<any>(`${this.url}streamingcountry`, {code: code}, {headers: this.headers});
|
return this.http.post<any>(`${this.url}streamingcountry`, {code: code}, {headers: this.headers});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public unsubscribeNewsletter(userId: string): Observable<any>{
|
||||||
|
return this.http.get<any>(`${this.url}newsletter/unsubscribe/${userId}`, {headers: this.headers});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
<div class="wizard-background">
|
||||||
|
<h2 style="color:white">Unsubscribed!</h2>
|
||||||
|
</div>
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { Component, OnInit } from "@angular/core";
|
||||||
|
import { IdentityService, SettingsService } from "../../../services";
|
||||||
|
|
||||||
|
import { ActivatedRoute } from "@angular/router";
|
||||||
|
import { AuthService } from "../../../auth/auth.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
templateUrl: "./unsubscribe-confirm.component.html",
|
||||||
|
})
|
||||||
|
export class UnsubscribeConfirmComponent implements OnInit {
|
||||||
|
|
||||||
|
private userId: string;
|
||||||
|
|
||||||
|
constructor(private authService: AuthService,
|
||||||
|
private readonly identityService: IdentityService,
|
||||||
|
private readonly settingsService: SettingsService,
|
||||||
|
private route: ActivatedRoute) {
|
||||||
|
this.route.params.subscribe(async (params: any) => {
|
||||||
|
if (typeof params.id === 'string' || params.id instanceof String) {
|
||||||
|
this.userId = params.id;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ngOnInit() {
|
||||||
|
this.identityService.unsubscribeNewsletter(this.userId).subscribe()
|
||||||
|
}
|
||||||
|
}
|
27
src/Ombi/ClientApp/src/app/unsubscribe/unsubscribe.module.ts
Normal file
27
src/Ombi/ClientApp/src/app/unsubscribe/unsubscribe.module.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||||
|
import { RouterModule, Routes } from "@angular/router";
|
||||||
|
|
||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { NgModule } from "@angular/core";
|
||||||
|
import { SharedModule } from "../shared/shared.module";
|
||||||
|
import { UnsubscribeConfirmComponent } from "./components/confirm-component/unsubscribe-confirm.component";
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{ path: ":id", component: UnsubscribeConfirmComponent},
|
||||||
|
];
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
SharedModule,
|
||||||
|
RouterModule.forChild(routes),
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
UnsubscribeConfirmComponent,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
],
|
||||||
|
|
||||||
|
})
|
||||||
|
export class UnsubscribeModule { }
|
|
@ -1032,6 +1032,22 @@ namespace Ombi.Controllers.V1
|
||||||
return Json(true);
|
return Json(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[HttpGet("newsletter/unsubscribe/{userId}")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> UnsubscribeUser(string userId)
|
||||||
|
{
|
||||||
|
// lookup user
|
||||||
|
var user = await UserManager.Users.FirstOrDefaultAsync(x => x.Id == userId);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
await UserManager.RemoveFromRoleAsync(user, OmbiRoles.ReceivesNewsletter);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<List<IdentityResult>> AddRoles(IEnumerable<ClaimCheckboxes> roles, OmbiUser ombiUser)
|
private async Task<List<IdentityResult>> AddRoles(IEnumerable<ClaimCheckboxes> roles, OmbiUser ombiUser)
|
||||||
{
|
{
|
||||||
var roleResult = new List<IdentityResult>();
|
var roleResult = new List<IdentityResult>();
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue