mirror of
https://github.com/Ombi-app/Ombi.git
synced 2025-07-12 16:22:55 -07:00
notification improvements
This commit is contained in:
parent
5816ddef98
commit
9c789363f6
17 changed files with 712 additions and 160 deletions
|
@ -1,147 +1,147 @@
|
||||||
#region Copyright
|
//#region Copyright
|
||||||
// /************************************************************************
|
//// /************************************************************************
|
||||||
// Copyright (c) 2016 Jamie Rees
|
//// Copyright (c) 2016 Jamie Rees
|
||||||
// File: AuthenticationSettingsTests.cs
|
//// File: AuthenticationSettingsTests.cs
|
||||||
// Created By: Jamie Rees
|
//// Created By: Jamie Rees
|
||||||
//
|
////
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining
|
//// Permission is hereby granted, free of charge, to any person obtaining
|
||||||
// a copy of this software and associated documentation files (the
|
//// a copy of this software and associated documentation files (the
|
||||||
// "Software"), to deal in the Software without restriction, including
|
//// "Software"), to deal in the Software without restriction, including
|
||||||
// without limitation the rights to use, copy, modify, merge, publish,
|
//// without limitation the rights to use, copy, modify, merge, publish,
|
||||||
// distribute, sublicense, and/or sell copies of the Software, and to
|
//// distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
// permit persons to whom the Software is furnished to do so, subject to
|
//// permit persons to whom the Software is furnished to do so, subject to
|
||||||
// the following conditions:
|
//// the following conditions:
|
||||||
//
|
////
|
||||||
// The above copyright notice and this permission notice shall be
|
//// The above copyright notice and this permission notice shall be
|
||||||
// included in all copies or substantial portions of the Software.
|
//// included in all copies or substantial portions of the Software.
|
||||||
//
|
////
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
//// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
//// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
//// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
//// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
//// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
//// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
//// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
// ************************************************************************/
|
//// ************************************************************************/
|
||||||
#endregion
|
//#endregion
|
||||||
using System;
|
//using System;
|
||||||
using System.Collections.Generic;
|
//using System.Collections.Generic;
|
||||||
|
|
||||||
using NUnit.Framework;
|
//using NUnit.Framework;
|
||||||
|
|
||||||
using PlexRequests.Core.Models;
|
//using PlexRequests.Core.Models;
|
||||||
using PlexRequests.Core.Notification;
|
//using PlexRequests.Core.Notification;
|
||||||
using PlexRequests.Core.SettingModels;
|
//using PlexRequests.Core.SettingModels;
|
||||||
|
|
||||||
namespace PlexRequests.Core.Tests
|
//namespace PlexRequests.Core.Tests
|
||||||
{
|
//{
|
||||||
[TestFixture]
|
// [TestFixture]
|
||||||
public class NotificationMessageResolverTests
|
// public class NotificationMessageResolverTests
|
||||||
{
|
// {
|
||||||
[TestCaseSource(nameof(MessageBodyResolver))]
|
// [TestCaseSource(nameof(MessageBodyResolver))]
|
||||||
public string ResolveBody(string body, NotificationMessageCurlys param)
|
// public string ResolveBody(string body, NotificationMessageCurlys param)
|
||||||
{
|
// {
|
||||||
var n = new NotificationMessageResolver();
|
// var n = new NotificationMessageResolver();
|
||||||
var s = new NotificationSettings
|
// var s = new NotificationSettings
|
||||||
{
|
// {
|
||||||
Message = new List<Notification.NotificationMessage> { new Notification.NotificationMessage { NotificationType = NotificationType.NewRequest, Body = body } }
|
// Message = new List<Notification.NotificationMessage> { new Notification.NotificationMessage { NotificationType = NotificationType.NewRequest, Body = body } }
|
||||||
};
|
// };
|
||||||
|
|
||||||
var result = n.ParseMessage(s, NotificationType.NewRequest, param);
|
// var result = n.ParseMessage(s, NotificationType.NewRequest, param, TransportType.Email);
|
||||||
return result.Body;
|
// return result.Body;
|
||||||
}
|
// }
|
||||||
|
|
||||||
[TestCaseSource(nameof(MessageSubjectResolver))]
|
// [TestCaseSource(nameof(MessageSubjectResolver))]
|
||||||
public string ResolveSubject(string subject, NotificationMessageCurlys param)
|
// public string ResolveSubject(string subject, NotificationMessageCurlys param)
|
||||||
{
|
// {
|
||||||
var n = new NotificationMessageResolver();
|
// var n = new NotificationMessageResolver();
|
||||||
var s = new NotificationSettings
|
// var s = new NotificationSettings
|
||||||
{
|
// {
|
||||||
Message = new List<Notification.NotificationMessage> { new Notification.NotificationMessage { NotificationType = NotificationType.NewRequest, Subject = subject }}
|
// Message = new List<Notification.NotificationMessage> { new Notification.NotificationMessage { NotificationType = NotificationType.NewRequest, Subject = subject }}
|
||||||
};
|
// };
|
||||||
|
|
||||||
var result = n.ParseMessage(s, NotificationType.NewRequest, param);
|
// var result = n.ParseMessage(s, NotificationType.NewRequest, param, TransportType.Email);
|
||||||
return result.Subject;
|
// return result.Subject;
|
||||||
}
|
// }
|
||||||
|
|
||||||
private static IEnumerable<TestCaseData> MessageSubjectResolver
|
// private static IEnumerable<TestCaseData> MessageSubjectResolver
|
||||||
{
|
// {
|
||||||
get
|
// get
|
||||||
{
|
// {
|
||||||
yield return new TestCaseData(
|
// yield return new TestCaseData(
|
||||||
"{Username} has requested a {Type}",
|
// "{Username} has requested a {Type}",
|
||||||
new NotificationMessageCurlys("Jamie", "Finding Dory", DateTime.Now.ToString(), "Movie", string.Empty))
|
// new NotificationMessageCurlys("Jamie", "Finding Dory", DateTime.Now.ToString(), "Movie", string.Empty))
|
||||||
.Returns("Jamie has requested a Movie").SetName("Subject Curlys");
|
// .Returns("Jamie has requested a Movie").SetName("Subject Curlys");
|
||||||
|
|
||||||
yield return new TestCaseData(
|
// yield return new TestCaseData(
|
||||||
null,
|
// null,
|
||||||
new NotificationMessageCurlys("Jamie", "Finding Dory", DateTime.Now.ToString(), "Movie", string.Empty))
|
// new NotificationMessageCurlys("Jamie", "Finding Dory", DateTime.Now.ToString(), "Movie", string.Empty))
|
||||||
.Returns(string.Empty).SetName("Empty Subject");
|
// .Returns(string.Empty).SetName("Empty Subject");
|
||||||
|
|
||||||
yield return new TestCaseData(
|
// yield return new TestCaseData(
|
||||||
"New Request Incoming!",
|
// "New Request Incoming!",
|
||||||
new NotificationMessageCurlys("Jamie", "Finding Dory", DateTime.Now.ToString(), "Movie", string.Empty))
|
// new NotificationMessageCurlys("Jamie", "Finding Dory", DateTime.Now.ToString(), "Movie", string.Empty))
|
||||||
.Returns("New Request Incoming!").SetName("No curlys");
|
// .Returns("New Request Incoming!").SetName("No curlys");
|
||||||
|
|
||||||
yield return new TestCaseData(
|
// yield return new TestCaseData(
|
||||||
"%$R£%$£^%$&{Username}@{}:§",
|
// "%$R£%$£^%$&{Username}@{}:§",
|
||||||
new NotificationMessageCurlys("Jamie", "Finding Dory", DateTime.Now.ToString(), "Movie", string.Empty))
|
// new NotificationMessageCurlys("Jamie", "Finding Dory", DateTime.Now.ToString(), "Movie", string.Empty))
|
||||||
.Returns("%$R£%$£^%$&Jamie@{}:§").SetName("Special Chars");
|
// .Returns("%$R£%$£^%$&Jamie@{}:§").SetName("Special Chars");
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
private static IEnumerable<TestCaseData> MessageBodyResolver
|
// private static IEnumerable<TestCaseData> MessageBodyResolver
|
||||||
{
|
// {
|
||||||
get
|
// get
|
||||||
{
|
// {
|
||||||
yield return new TestCaseData(
|
// yield return new TestCaseData(
|
||||||
"There has been a new request from {Username}, Title: {Title} for {Type}",
|
// "There has been a new request from {Username}, Title: {Title} for {Type}",
|
||||||
new NotificationMessageCurlys("Jamie", "Finding Dory", DateTime.Now.ToString(), "Movie", string.Empty))
|
// new NotificationMessageCurlys("Jamie", "Finding Dory", DateTime.Now.ToString(), "Movie", string.Empty))
|
||||||
.Returns("There has been a new request from Jamie, Title: Finding Dory for Movie").SetName("FindingDory");
|
// .Returns("There has been a new request from Jamie, Title: Finding Dory for Movie").SetName("FindingDory");
|
||||||
|
|
||||||
yield return new TestCaseData(
|
// yield return new TestCaseData(
|
||||||
null,
|
// null,
|
||||||
new NotificationMessageCurlys(string.Empty, string.Empty, string.Empty, string.Empty, string.Empty))
|
// new NotificationMessageCurlys(string.Empty, string.Empty, string.Empty, string.Empty, string.Empty))
|
||||||
.Returns(string.Empty)
|
// .Returns(string.Empty)
|
||||||
.SetName("Empty Message");
|
// .SetName("Empty Message");
|
||||||
|
|
||||||
yield return new TestCaseData(
|
// yield return new TestCaseData(
|
||||||
"{{Wowwzer}} Damn}{{Username}}}}",
|
// "{{Wowwzer}} Damn}{{Username}}}}",
|
||||||
new NotificationMessageCurlys("HEY!", string.Empty, string.Empty, string.Empty, string.Empty))
|
// new NotificationMessageCurlys("HEY!", string.Empty, string.Empty, string.Empty, string.Empty))
|
||||||
.Returns("{{Wowwzer}} Damn}{HEY!}}}")
|
// .Returns("{{Wowwzer}} Damn}{HEY!}}}")
|
||||||
.SetName("Multiple Curlys");
|
// .SetName("Multiple Curlys");
|
||||||
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
// yield return new TestCaseData(
|
||||||
"This is a message with no curlys",
|
// "This is a message with no curlys",
|
||||||
new NotificationMessageCurlys("Jamie", "Finding Dory", DateTime.Now.ToString(), "Movie", string.Empty))
|
// new NotificationMessageCurlys("Jamie", "Finding Dory", DateTime.Now.ToString(), "Movie", string.Empty))
|
||||||
.Returns("This is a message with no curlys")
|
// .Returns("This is a message with no curlys")
|
||||||
.SetName("No Curlys");
|
// .SetName("No Curlys");
|
||||||
|
|
||||||
yield return new TestCaseData(
|
// yield return new TestCaseData(
|
||||||
new string(')', 5000),
|
// new string(')', 5000),
|
||||||
new NotificationMessageCurlys(string.Empty, string.Empty, string.Empty, string.Empty, string.Empty))
|
// new NotificationMessageCurlys(string.Empty, string.Empty, string.Empty, string.Empty, string.Empty))
|
||||||
.Returns(new string(')', 5000))
|
// .Returns(new string(')', 5000))
|
||||||
.SetName("Long String");
|
// .SetName("Long String");
|
||||||
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
// yield return new TestCaseData(
|
||||||
"This is a {Username} and {Username} Because {Issue}{Issue}",
|
// "This is a {Username} and {Username} Because {Issue}{Issue}",
|
||||||
new NotificationMessageCurlys("HEY!", string.Empty, string.Empty, string.Empty, "Bob"))
|
// new NotificationMessageCurlys("HEY!", string.Empty, string.Empty, string.Empty, "Bob"))
|
||||||
.Returns("This is a HEY! and HEY! Because BobBob")
|
// .Returns("This is a HEY! and HEY! Because BobBob")
|
||||||
.SetName("Double Curly");
|
// .SetName("Double Curly");
|
||||||
|
|
||||||
yield return new TestCaseData(
|
// yield return new TestCaseData(
|
||||||
"This is a {username} and {username} Because {Issue}{Issue}",
|
// "This is a {username} and {username} Because {Issue}{Issue}",
|
||||||
new NotificationMessageCurlys("HEY!", string.Empty, string.Empty, string.Empty, "Bob"))
|
// new NotificationMessageCurlys("HEY!", string.Empty, string.Empty, string.Empty, "Bob"))
|
||||||
.Returns("This is a {username} and {username} Because BobBob")
|
// .Returns("This is a {username} and {username} Because BobBob")
|
||||||
.SetName("Case sensitive");
|
// .SetName("Case sensitive");
|
||||||
|
|
||||||
yield return new TestCaseData(
|
// yield return new TestCaseData(
|
||||||
"{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}",
|
// "{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}{Date}",
|
||||||
new NotificationMessageCurlys("HEY!", string.Empty, "b", string.Empty, "Bob"))
|
// new NotificationMessageCurlys("HEY!", string.Empty, "b", string.Empty, "Bob"))
|
||||||
.Returns("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
|
// .Returns("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
|
||||||
.SetName("Lots of curlys");
|
// .SetName("Lots of curlys");
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
//}
|
|
@ -24,6 +24,8 @@
|
||||||
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
// ************************************************************************/
|
// ************************************************************************/
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
|
@ -50,16 +52,37 @@ namespace PlexRequests.Core.Notification
|
||||||
/// <param name="notification">The notification.</param>
|
/// <param name="notification">The notification.</param>
|
||||||
/// <param name="type">The type.</param>
|
/// <param name="type">The type.</param>
|
||||||
/// <param name="c">The c.</param>
|
/// <param name="c">The c.</param>
|
||||||
|
/// <param name="transportType">Type of the transport.</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public NotificationMessageContent ParseMessage<T>(T notification, NotificationType type, NotificationMessageCurlys c) where T : NotificationSettings
|
public NotificationMessageContent ParseMessage(NotificationSettingsV2 notification, NotificationType type, NotificationMessageCurlys c, TransportType transportType)
|
||||||
{
|
{
|
||||||
var content = notification.Message.FirstOrDefault(x => x.NotificationType == type);
|
IEnumerable<NotificationMessage> content = null;
|
||||||
|
switch (transportType)
|
||||||
|
{
|
||||||
|
case TransportType.Email:
|
||||||
|
content = notification.EmailNotification;
|
||||||
|
break;
|
||||||
|
case TransportType.Pushbullet:
|
||||||
|
content = notification.PushbulletNotification;
|
||||||
|
break;
|
||||||
|
case TransportType.Pushover:
|
||||||
|
content = notification.PushoverNotification;
|
||||||
|
break;
|
||||||
|
case TransportType.Slack:
|
||||||
|
content = notification.SlackNotification;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(transportType), transportType, null);
|
||||||
|
}
|
||||||
|
|
||||||
if (content == null)
|
if (content == null)
|
||||||
{
|
{
|
||||||
return new NotificationMessageContent();
|
return new NotificationMessageContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Resolve(content.Body, content.Subject, c.Curlys);
|
var message = content.FirstOrDefault(x => x.NotificationType == type) ?? new NotificationMessage();
|
||||||
|
|
||||||
|
return Resolve(message.Body, message.Subject, c.Curlys);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -78,7 +101,7 @@ namespace PlexRequests.Core.Notification
|
||||||
body = ReplaceFields(bodyFields, parameters, body);
|
body = ReplaceFields(bodyFields, parameters, body);
|
||||||
subject = ReplaceFields(subjectFields, parameters, subject);
|
subject = ReplaceFields(subjectFields, parameters, subject);
|
||||||
|
|
||||||
return new NotificationMessageContent { Body = body ?? string.Empty, Subject = subject ?? string.Empty };
|
return new NotificationMessageContent {Body = body ?? string.Empty, Subject = subject ?? string.Empty};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -123,7 +146,6 @@ namespace PlexRequests.Core.Notification
|
||||||
{
|
{
|
||||||
currentWord += c.ToString(); // Add the character onto the word.
|
currentWord += c.ToString(); // Add the character onto the word.
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return fields;
|
return fields;
|
||||||
|
|
|
@ -0,0 +1,189 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<title>Plex Requests .Net</title>
|
||||||
|
<style media="all" type="text/css">
|
||||||
|
@media all {
|
||||||
|
.btn-primary table td:hover {
|
||||||
|
background-color: #34495e !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary a:hover {
|
||||||
|
background-color: #34495e !important;
|
||||||
|
border-color: #34495e !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all {
|
||||||
|
.btn-secondary a:hover {
|
||||||
|
border-color: #34495e !important;
|
||||||
|
color: #34495e !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 620px) {
|
||||||
|
table[class=body] h1 {
|
||||||
|
font-size: 28px !important;
|
||||||
|
margin-bottom: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] h2 {
|
||||||
|
font-size: 22px !important;
|
||||||
|
margin-bottom: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] h3 {
|
||||||
|
font-size: 16px !important;
|
||||||
|
margin-bottom: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] p,
|
||||||
|
table[class=body] ul,
|
||||||
|
table[class=body] ol,
|
||||||
|
table[class=body] td,
|
||||||
|
table[class=body] span,
|
||||||
|
table[class=body] a {
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .wrapper,
|
||||||
|
table[class=body] .article {
|
||||||
|
padding: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .content {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .container {
|
||||||
|
padding: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .header {
|
||||||
|
margin-bottom: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .main {
|
||||||
|
border-left-width: 0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
border-right-width: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .btn table {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .btn a {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .img-responsive {
|
||||||
|
height: auto !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
width: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .alert td {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
padding: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .span-2,
|
||||||
|
table[class=body] .span-3 {
|
||||||
|
max-width: none !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .receipt {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all {
|
||||||
|
.ExternalClass {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExternalClass,
|
||||||
|
.ExternalClass p,
|
||||||
|
.ExternalClass span,
|
||||||
|
.ExternalClass font,
|
||||||
|
.ExternalClass td,
|
||||||
|
.ExternalClass div {
|
||||||
|
line-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.apple-link a {
|
||||||
|
color: inherit !important;
|
||||||
|
font-family: inherit !important;
|
||||||
|
font-size: inherit !important;
|
||||||
|
font-weight: inherit !important;
|
||||||
|
line-height: inherit !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="" style="font-family: sans-serif; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.4; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; background-color: #f6f6f6; margin: 0; padding: 0;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background-color: #f6f6f6;" width="100%" bgcolor="#f6f6f6">
|
||||||
|
<tr>
|
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top"> </td>
|
||||||
|
<td class="container" style="font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; Margin: 0 auto !important; max-width: 580px; padding: 10px; width: 580px;" width="580" valign="top">
|
||||||
|
<div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 580px; padding: 10px;">
|
||||||
|
|
||||||
|
<!-- START CENTERED WHITE CONTAINER -->
|
||||||
|
<span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">This is preheader text. Some clients will show this text as a preview.</span>
|
||||||
|
<table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #fff; border-radius: 3px;" width="100%">
|
||||||
|
|
||||||
|
<!-- START MAIN CONTENT AREA -->
|
||||||
|
<tr>
|
||||||
|
<td class="wrapper" style="font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 20px;" valign="top">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" width="100%">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<img src="http://i.imgur.com/s4nswSA.png?" width="400px" text-align="center" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top">
|
||||||
|
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Hi there!</p>
|
||||||
|
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{@SUBJECT}</p>
|
||||||
|
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{@BODY}</p>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<img src="{@IMGSRC}" width="400px" text-align="center" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- END MAIN CONTENT AREA -->
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- START FOOTER -->
|
||||||
|
<div class="footer" style="clear: both; padding-top: 10px; text-align: center; width: 100%;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" width="100%">
|
||||||
|
<tr>
|
||||||
|
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; padding-top: 10px; padding-bottom: 10px; font-size: 12px; color: #999999; text-align: center;" valign="top" align="center">
|
||||||
|
Powered by <a href="https://github.com/tidusjar/PlexRequests.Net" style="color: #999999; font-size: 12px; text-align: center; text-decoration: underline;">Plex Requests .Net</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- END FOOTER -->
|
||||||
|
<!-- END CENTERED WHITE CONTAINER -->
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top"> </td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,70 @@
|
||||||
|
#region Copyright
|
||||||
|
// /************************************************************************
|
||||||
|
// Copyright (c) 2016 Jamie Rees
|
||||||
|
// File: EmailBasicTemplate.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;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
using NLog;
|
||||||
|
using PlexRequests.Core.Models;
|
||||||
|
using PlexRequests.Core.SettingModels;
|
||||||
|
|
||||||
|
namespace PlexRequests.Core.Notification.Templates
|
||||||
|
{
|
||||||
|
public class EmailBasicTemplate : IEmailBasicTemplate
|
||||||
|
{
|
||||||
|
public string TemplateLocation => Path.Combine(Path.GetDirectoryName(Application.ExecutablePath) ?? string.Empty, "Notification", "Templates", "BasicRequestTemplate.html");
|
||||||
|
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
|
||||||
|
|
||||||
|
private const string SubjectKey = "{@SUBJECT}";
|
||||||
|
private const string BodyKey = "{@BODY}";
|
||||||
|
private const string ImgSrc = "{@IMGSRC}";
|
||||||
|
private const string DateKey = "{@DATENOW}";
|
||||||
|
|
||||||
|
public string LoadTemplate(string subject, string body, string imgSrc)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder(File.ReadAllText(TemplateLocation));
|
||||||
|
sb.Replace(SubjectKey, subject);
|
||||||
|
sb.Replace(BodyKey, body);
|
||||||
|
sb.Replace(ImgSrc, imgSrc);
|
||||||
|
sb.Replace(DateKey, DateTime.Now.ToString("f"));
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.Error(e);
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
#region Copyright
|
||||||
|
// /************************************************************************
|
||||||
|
// Copyright (c) 2016 Jamie Rees
|
||||||
|
// File: IEmailBasicTemplate.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;
|
||||||
|
|
||||||
|
namespace PlexRequests.Core.Notification.Templates
|
||||||
|
{
|
||||||
|
public interface IEmailBasicTemplate
|
||||||
|
{
|
||||||
|
string LoadTemplate(string subject, string body, string imgSrc);
|
||||||
|
string TemplateLocation { get; }
|
||||||
|
}
|
||||||
|
}
|
36
PlexRequests.Core/Notification/TransportType.cs
Normal file
36
PlexRequests.Core/Notification/TransportType.cs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
#region Copyright
|
||||||
|
// /************************************************************************
|
||||||
|
// Copyright (c) 2016 Jamie Rees
|
||||||
|
// File: TransportType.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
|
||||||
|
namespace PlexRequests.Core.Notification
|
||||||
|
{
|
||||||
|
public enum TransportType
|
||||||
|
{
|
||||||
|
Email,
|
||||||
|
Pushbullet,
|
||||||
|
Pushover,
|
||||||
|
Slack
|
||||||
|
}
|
||||||
|
}
|
|
@ -44,6 +44,7 @@
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="System" />
|
<Reference Include="System" />
|
||||||
<Reference Include="System.Core" />
|
<Reference Include="System.Core" />
|
||||||
|
<Reference Include="System.Windows.Forms" />
|
||||||
<Reference Include="System.Xml.Linq" />
|
<Reference Include="System.Xml.Linq" />
|
||||||
<Reference Include="System.Data.DataSetExtensions" />
|
<Reference Include="System.Data.DataSetExtensions" />
|
||||||
<Reference Include="Microsoft.CSharp" />
|
<Reference Include="Microsoft.CSharp" />
|
||||||
|
@ -81,11 +82,15 @@
|
||||||
<Compile Include="Models\NotificationType.cs" />
|
<Compile Include="Models\NotificationType.cs" />
|
||||||
<Compile Include="Models\StatusModel.cs" />
|
<Compile Include="Models\StatusModel.cs" />
|
||||||
<Compile Include="Models\UserProperties.cs" />
|
<Compile Include="Models\UserProperties.cs" />
|
||||||
|
<Compile Include="Notification\Templates\EmailBasicTemplate.cs" />
|
||||||
|
<Compile Include="Notification\Templates\IEmailBasicTemplate.cs" />
|
||||||
|
<Compile Include="Notification\TransportType.cs" />
|
||||||
<Compile Include="SettingModels\AuthenticationSettings.cs" />
|
<Compile Include="SettingModels\AuthenticationSettings.cs" />
|
||||||
<Compile Include="SettingModels\ExternalSettings.cs" />
|
<Compile Include="SettingModels\ExternalSettings.cs" />
|
||||||
<Compile Include="SettingModels\HeadphonesSettings.cs" />
|
<Compile Include="SettingModels\HeadphonesSettings.cs" />
|
||||||
<Compile Include="SettingModels\LandingPageSettings.cs" />
|
<Compile Include="SettingModels\LandingPageSettings.cs" />
|
||||||
<Compile Include="SettingModels\NotificationSettings.cs" />
|
<Compile Include="SettingModels\NotificationSettings.cs" />
|
||||||
|
<Compile Include="SettingModels\NotificationSettingsV2.cs" />
|
||||||
<Compile Include="SettingModels\RequestSettings.cs" />
|
<Compile Include="SettingModels\RequestSettings.cs" />
|
||||||
<Compile Include="SettingModels\ScheduledJobsSettings.cs" />
|
<Compile Include="SettingModels\ScheduledJobsSettings.cs" />
|
||||||
<Compile Include="SettingModels\SlackNotificationSettings.cs" />
|
<Compile Include="SettingModels\SlackNotificationSettings.cs" />
|
||||||
|
@ -134,7 +139,11 @@
|
||||||
<Name>PlexRequests.Store</Name>
|
<Name>PlexRequests.Store</Name>
|
||||||
</ProjectReference>
|
</ProjectReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup />
|
<ItemGroup>
|
||||||
|
<Content Include="Notification\Templates\BasicRequestTemplate.html">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||||
Other similar extension points exist, see Microsoft.Common.targets.
|
Other similar extension points exist, see Microsoft.Common.targets.
|
||||||
|
|
|
@ -34,7 +34,7 @@ namespace PlexRequests.Core.SettingModels
|
||||||
public string EmailSender { get; set; }
|
public string EmailSender { get; set; }
|
||||||
public string EmailUsername { get; set; }
|
public string EmailUsername { get; set; }
|
||||||
public bool Enabled { get; set; }
|
public bool Enabled { get; set; }
|
||||||
public bool Authentication { get; set; }
|
public bool Authentication { get; set; }
|
||||||
public bool EnableUserEmailNotifications { get; set; }
|
public bool EnableUserEmailNotifications { get; set; }
|
||||||
public string RecipientEmail { get; set; }
|
public string RecipientEmail { get; set; }
|
||||||
}
|
}
|
||||||
|
|
62
PlexRequests.Core/SettingModels/NotificationSettingsV2.cs
Normal file
62
PlexRequests.Core/SettingModels/NotificationSettingsV2.cs
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
#region Copyright
|
||||||
|
// /************************************************************************
|
||||||
|
// Copyright (c) 2016 Jamie Rees
|
||||||
|
// File: NotificationSettingsV2.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 PlexRequests.Core.Models;
|
||||||
|
using PlexRequests.Core.Notification;
|
||||||
|
|
||||||
|
namespace PlexRequests.Core.SettingModels
|
||||||
|
{
|
||||||
|
public class NotificationSettingsV2 : Settings
|
||||||
|
{
|
||||||
|
public NotificationSettingsV2()
|
||||||
|
{
|
||||||
|
EmailNotification = new List<NotificationMessage>
|
||||||
|
{
|
||||||
|
new NotificationMessage
|
||||||
|
{
|
||||||
|
Body = "BODY",
|
||||||
|
NotificationType = NotificationType.NewRequest,
|
||||||
|
Subject = "SUB"
|
||||||
|
},
|
||||||
|
new NotificationMessage
|
||||||
|
{
|
||||||
|
NotificationType = NotificationType.Issue,
|
||||||
|
Body = "issue",
|
||||||
|
Subject = "issuesub"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
SlackNotification = new List<NotificationMessage>();
|
||||||
|
PushoverNotification = new List<NotificationMessage>();
|
||||||
|
PushbulletNotification = new List<NotificationMessage>();
|
||||||
|
}
|
||||||
|
public List<NotificationMessage> EmailNotification { get; set; }
|
||||||
|
public List<NotificationMessage> SlackNotification { get; set; }
|
||||||
|
public List<NotificationMessage> PushbulletNotification { get; set; }
|
||||||
|
public List<NotificationMessage> PushoverNotification { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,18 +25,13 @@
|
||||||
// ************************************************************************/
|
// ************************************************************************/
|
||||||
#endregion
|
#endregion
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using MailKit.Security;
|
|
||||||
|
|
||||||
using MimeKit;
|
using MimeKit;
|
||||||
using NLog;
|
using NLog;
|
||||||
|
|
||||||
using PlexRequests.Core;
|
using PlexRequests.Core;
|
||||||
using PlexRequests.Core.Models;
|
using PlexRequests.Core.Models;
|
||||||
|
using PlexRequests.Core.Notification.Templates;
|
||||||
using PlexRequests.Core.SettingModels;
|
using PlexRequests.Core.SettingModels;
|
||||||
using PlexRequests.Services.Interfaces;
|
using PlexRequests.Services.Interfaces;
|
||||||
using SmtpClient = MailKit.Net.Smtp.SmtpClient;
|
using SmtpClient = MailKit.Net.Smtp.SmtpClient;
|
||||||
|
@ -109,7 +104,7 @@ namespace PlexRequests.Services.Notification
|
||||||
if (string.IsNullOrEmpty(settings.EmailUsername) || string.IsNullOrEmpty(settings.EmailPassword))
|
if (string.IsNullOrEmpty(settings.EmailUsername) || string.IsNullOrEmpty(settings.EmailPassword))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (string.IsNullOrEmpty(settings.EmailHost) || string.IsNullOrEmpty(settings.RecipientEmail) || string.IsNullOrEmpty(settings.EmailPort.ToString()))
|
if (string.IsNullOrEmpty(settings.EmailHost) || string.IsNullOrEmpty(settings.RecipientEmail) || string.IsNullOrEmpty(settings.EmailPort.ToString()))
|
||||||
{
|
{
|
||||||
|
@ -129,13 +124,16 @@ namespace PlexRequests.Services.Notification
|
||||||
|
|
||||||
private async Task EmailNewRequest(NotificationModel model, EmailNotificationSettings settings)
|
private async Task EmailNewRequest(NotificationModel model, EmailNotificationSettings settings)
|
||||||
{
|
{
|
||||||
//var r = new NotificationMessageCurlys(model.User, model.Title, DateTime.Now.ToString(), model.RequestType.ToString(), string.Empty);
|
var email = new EmailBasicTemplate();
|
||||||
//var resolver = new NotificationMessageResolver();
|
var html = email.LoadTemplate(
|
||||||
//var bodyResult = resolver.ParseMessage(settings, NotificationType.NewRequest, r);
|
$"Plex Requests: New {model.RequestType.GetString()?.ToLower()} request for {model.Title}!",
|
||||||
|
$"Hello! The user '{model.User}' has requested the {model.RequestType.GetString()?.ToLower()} '{model.Title}'! Please log in to approve this request. Request Date: {model.DateTime.ToString("f")}",
|
||||||
|
model.ImgSrc);
|
||||||
|
var body = new BodyBuilder { HtmlBody = html, };
|
||||||
|
|
||||||
var message = new MimeMessage
|
var message = new MimeMessage
|
||||||
{
|
{
|
||||||
Body = new TextPart("plain") { Text = $"Hello! The user '{model.User}' has requested the {model.RequestType.GetString()?.ToLower()} '{model.Title}'! Please log in to approve this request. Request Date: {model.DateTime.ToString("f")}" },
|
Body = body.ToMessageBody(),
|
||||||
Subject = $"Plex Requests: New {model.RequestType.GetString()?.ToLower()} request for {model.Title}!"
|
Subject = $"Plex Requests: New {model.RequestType.GetString()?.ToLower()} request for {model.Title}!"
|
||||||
};
|
};
|
||||||
message.From.Add(new MailboxAddress(settings.EmailSender, settings.EmailSender));
|
message.From.Add(new MailboxAddress(settings.EmailSender, settings.EmailSender));
|
||||||
|
@ -147,9 +145,16 @@ namespace PlexRequests.Services.Notification
|
||||||
|
|
||||||
private async Task EmailIssue(NotificationModel model, EmailNotificationSettings settings)
|
private async Task EmailIssue(NotificationModel model, EmailNotificationSettings settings)
|
||||||
{
|
{
|
||||||
|
var email = new EmailBasicTemplate();
|
||||||
|
var html = email.LoadTemplate(
|
||||||
|
$"Plex Requests: New issue for {model.Title}!",
|
||||||
|
$"Hello! The user '{model.User}' has reported a new issue {model.Body} for the title {model.Title}!",
|
||||||
|
model.ImgSrc);
|
||||||
|
var body = new BodyBuilder { HtmlBody = html, };
|
||||||
|
|
||||||
var message = new MimeMessage
|
var message = new MimeMessage
|
||||||
{
|
{
|
||||||
Body = new TextPart("plain") { Text = $"Hello! The user '{model.User}' has reported a new issue {model.Body} for the title {model.Title}!" },
|
Body = body.ToMessageBody(),
|
||||||
Subject = $"Plex Requests: New issue for {model.Title}!"
|
Subject = $"Plex Requests: New issue for {model.Title}!"
|
||||||
};
|
};
|
||||||
message.From.Add(new MailboxAddress(settings.EmailSender, settings.EmailSender));
|
message.From.Add(new MailboxAddress(settings.EmailSender, settings.EmailSender));
|
||||||
|
@ -165,10 +170,16 @@ namespace PlexRequests.Services.Notification
|
||||||
{
|
{
|
||||||
await Task.FromResult(false);
|
await Task.FromResult(false);
|
||||||
}
|
}
|
||||||
|
var email = new EmailBasicTemplate();
|
||||||
|
var html = email.LoadTemplate(
|
||||||
|
$"Plex Requests: {model.Title} is now available!",
|
||||||
|
$"Hello! You requested {model.Title} on PlexRequests! This is now available on Plex! :)",
|
||||||
|
model.ImgSrc);
|
||||||
|
var body = new BodyBuilder { HtmlBody = html, };
|
||||||
|
|
||||||
var message = new MimeMessage
|
var message = new MimeMessage
|
||||||
{
|
{
|
||||||
Body = new TextPart("plain") { Text = $"Hello! You requested {model.Title} on PlexRequests! This is now available on Plex! :)" },
|
Body = body.ToMessageBody(),
|
||||||
Subject = $"Plex Requests: {model.Title} is now available!"
|
Subject = $"Plex Requests: {model.Title} is now available!"
|
||||||
};
|
};
|
||||||
message.From.Add(new MailboxAddress(settings.EmailSender, settings.EmailSender));
|
message.From.Add(new MailboxAddress(settings.EmailSender, settings.EmailSender));
|
||||||
|
@ -206,10 +217,15 @@ namespace PlexRequests.Services.Notification
|
||||||
|
|
||||||
private async Task EmailTest(NotificationModel model, EmailNotificationSettings settings)
|
private async Task EmailTest(NotificationModel model, EmailNotificationSettings settings)
|
||||||
{
|
{
|
||||||
|
var email = new EmailBasicTemplate();
|
||||||
|
var html = email.LoadTemplate(
|
||||||
|
"Test Message",
|
||||||
|
"This is just a test! Success!",
|
||||||
|
model.ImgSrc);
|
||||||
|
var body = new BodyBuilder { HtmlBody = html, };
|
||||||
var message = new MimeMessage
|
var message = new MimeMessage
|
||||||
{
|
{
|
||||||
Body = new TextPart("plain") { Text = "This is just a test! Success!" },
|
Body = body.ToMessageBody()
|
||||||
Subject = "Plex Requests: Test Message!",
|
|
||||||
};
|
};
|
||||||
message.From.Add(new MailboxAddress(settings.EmailSender, settings.EmailSender));
|
message.From.Add(new MailboxAddress(settings.EmailSender, settings.EmailSender));
|
||||||
message.To.Add(new MailboxAddress(settings.RecipientEmail, settings.RecipientEmail));
|
message.To.Add(new MailboxAddress(settings.RecipientEmail, settings.RecipientEmail));
|
||||||
|
|
|
@ -75,7 +75,7 @@ namespace PlexRequests.Services.Notification
|
||||||
if (user.Equals(adminUsername, StringComparison.CurrentCultureIgnoreCase))
|
if (user.Equals(adminUsername, StringComparison.CurrentCultureIgnoreCase))
|
||||||
{
|
{
|
||||||
Log.Info("This user is the Plex server owner");
|
Log.Info("This user is the Plex server owner");
|
||||||
await PublishUserNotification(userAccount.Username, userAccount.Email, model.Title);
|
await PublishUserNotification(userAccount.Username, userAccount.Email, model.Title, model.PosterPath);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,7 +88,7 @@ namespace PlexRequests.Services.Notification
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Info("Sending notification to: {0} at: {1}, for title: {2}", email.Username, email.Email, model.Title);
|
Log.Info("Sending notification to: {0} at: {1}, for title: {2}", email.Username, email.Email, model.Title);
|
||||||
await PublishUserNotification(email.Username, email.Email, model.Title);
|
await PublishUserNotification(email.Username, email.Email, model.Title, model.PosterPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -117,7 +117,7 @@ namespace PlexRequests.Services.Notification
|
||||||
if (user.Equals(adminUsername, StringComparison.CurrentCultureIgnoreCase))
|
if (user.Equals(adminUsername, StringComparison.CurrentCultureIgnoreCase))
|
||||||
{
|
{
|
||||||
Log.Info("This user is the Plex server owner");
|
Log.Info("This user is the Plex server owner");
|
||||||
await PublishUserNotification(userAccount.Username, userAccount.Email, model.Title);
|
await PublishUserNotification(userAccount.Username, userAccount.Email, model.Title, model.PosterPath);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,7 +130,7 @@ namespace PlexRequests.Services.Notification
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Info("Sending notification to: {0} at: {1}, for title: {2}", email.Username, email.Email, model.Title);
|
Log.Info("Sending notification to: {0} at: {1}, for title: {2}", email.Username, email.Email, model.Title);
|
||||||
await PublishUserNotification(email.Username, email.Email, model.Title);
|
await PublishUserNotification(email.Username, email.Email, model.Title, model.PosterPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
|
@ -139,14 +139,15 @@ namespace PlexRequests.Services.Notification
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task PublishUserNotification(string username, string email, string title)
|
private async Task PublishUserNotification(string username, string email, string title, string img)
|
||||||
{
|
{
|
||||||
var notificationModel = new NotificationModel
|
var notificationModel = new NotificationModel
|
||||||
{
|
{
|
||||||
User = username,
|
User = username,
|
||||||
UserEmail = email,
|
UserEmail = email,
|
||||||
NotificationType = NotificationType.RequestAvailable,
|
NotificationType = NotificationType.RequestAvailable,
|
||||||
Title = title
|
Title = title,
|
||||||
|
ImgSrc = img
|
||||||
};
|
};
|
||||||
|
|
||||||
// Send the notification to the user.
|
// Send the notification to the user.
|
||||||
|
|
|
@ -40,5 +40,6 @@ namespace PlexRequests.Services.Notification
|
||||||
public string User { get; set; }
|
public string User { get; set; }
|
||||||
public string UserEmail { get; set; }
|
public string UserEmail { get; set; }
|
||||||
public RequestType RequestType { get; set; }
|
public RequestType RequestType { get; set; }
|
||||||
|
public string ImgSrc { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -80,6 +80,7 @@ namespace PlexRequests.UI.Tests
|
||||||
private Mock<ISettingsService<LandingPageSettings>> LandingPageSettings { get; set; }
|
private Mock<ISettingsService<LandingPageSettings>> LandingPageSettings { get; set; }
|
||||||
private Mock<ISlackApi> SlackApi { get; set; }
|
private Mock<ISlackApi> SlackApi { get; set; }
|
||||||
private Mock<IAnalytics> Analytics { get; set; }
|
private Mock<IAnalytics> Analytics { get; set; }
|
||||||
|
private Mock<ISettingsService<NotificationSettingsV2>> NotifyV2 { get; set; }
|
||||||
|
|
||||||
private ConfigurableBootstrapper Bootstrapper { get; set; }
|
private ConfigurableBootstrapper Bootstrapper { get; set; }
|
||||||
|
|
||||||
|
@ -120,6 +121,7 @@ namespace PlexRequests.UI.Tests
|
||||||
ScheduledJobsSettingsMock = new Mock<ISettingsService<ScheduledJobsSettings>>();
|
ScheduledJobsSettingsMock = new Mock<ISettingsService<ScheduledJobsSettings>>();
|
||||||
RecorderMock = new Mock<IJobRecord>();
|
RecorderMock = new Mock<IJobRecord>();
|
||||||
Analytics = new Mock<IAnalytics>();
|
Analytics = new Mock<IAnalytics>();
|
||||||
|
NotifyV2= new Mock<ISettingsService<NotificationSettingsV2>>();
|
||||||
|
|
||||||
|
|
||||||
Bootstrapper = new ConfigurableBootstrapper(with =>
|
Bootstrapper = new ConfigurableBootstrapper(with =>
|
||||||
|
@ -140,6 +142,7 @@ namespace PlexRequests.UI.Tests
|
||||||
with.Dependency(LogRepo.Object);
|
with.Dependency(LogRepo.Object);
|
||||||
with.Dependency(PushoverSettings.Object);
|
with.Dependency(PushoverSettings.Object);
|
||||||
with.Dependency(PushoverApi.Object);
|
with.Dependency(PushoverApi.Object);
|
||||||
|
with.Dependency(NotifyV2.Object);
|
||||||
with.Dependency(NotificationService.Object);
|
with.Dependency(NotificationService.Object);
|
||||||
with.Dependency(Analytics.Object);
|
with.Dependency(Analytics.Object);
|
||||||
with.Dependency(HeadphonesSettings.Object);
|
with.Dependency(HeadphonesSettings.Object);
|
||||||
|
|
|
@ -94,6 +94,7 @@ namespace PlexRequests.UI.Modules
|
||||||
private ISlackApi SlackApi { get; }
|
private ISlackApi SlackApi { get; }
|
||||||
private IJobRecord JobRecorder { get; }
|
private IJobRecord JobRecorder { get; }
|
||||||
private IAnalytics Analytics { get; }
|
private IAnalytics Analytics { get; }
|
||||||
|
private ISettingsService<NotificationSettingsV2> NotifySettings { get; }
|
||||||
|
|
||||||
private static Logger Log = LogManager.GetCurrentClassLogger();
|
private static Logger Log = LogManager.GetCurrentClassLogger();
|
||||||
public AdminModule(ISettingsService<PlexRequestSettings> prService,
|
public AdminModule(ISettingsService<PlexRequestSettings> prService,
|
||||||
|
@ -116,7 +117,8 @@ namespace PlexRequests.UI.Modules
|
||||||
ISettingsService<LogSettings> logs,
|
ISettingsService<LogSettings> logs,
|
||||||
ICacheProvider cache, ISettingsService<SlackNotificationSettings> slackSettings,
|
ICacheProvider cache, ISettingsService<SlackNotificationSettings> slackSettings,
|
||||||
ISlackApi slackApi, ISettingsService<LandingPageSettings> lp,
|
ISlackApi slackApi, ISettingsService<LandingPageSettings> lp,
|
||||||
ISettingsService<ScheduledJobsSettings> scheduler, IJobRecord rec, IAnalytics analytics) : base("admin", prService)
|
ISettingsService<ScheduledJobsSettings> scheduler, IJobRecord rec, IAnalytics analytics,
|
||||||
|
ISettingsService<NotificationSettingsV2> notifyService) : base("admin", prService)
|
||||||
{
|
{
|
||||||
PrService = prService;
|
PrService = prService;
|
||||||
CpService = cpService;
|
CpService = cpService;
|
||||||
|
@ -143,6 +145,7 @@ namespace PlexRequests.UI.Modules
|
||||||
ScheduledJobSettings = scheduler;
|
ScheduledJobSettings = scheduler;
|
||||||
JobRecorder = rec;
|
JobRecorder = rec;
|
||||||
Analytics = analytics;
|
Analytics = analytics;
|
||||||
|
NotifySettings = notifyService;
|
||||||
|
|
||||||
this.RequiresClaims(UserClaims.Admin);
|
this.RequiresClaims(UserClaims.Admin);
|
||||||
|
|
||||||
|
@ -210,6 +213,9 @@ namespace PlexRequests.UI.Modules
|
||||||
Post["/scheduledjobs", true] = async (x, ct) => await SaveScheduledJobs();
|
Post["/scheduledjobs", true] = async (x, ct) => await SaveScheduledJobs();
|
||||||
|
|
||||||
Post["/clearlogs", true] = async (x, ct) => await ClearLogs();
|
Post["/clearlogs", true] = async (x, ct) => await ClearLogs();
|
||||||
|
|
||||||
|
Get["/notificationsettings", true] = async (x, ct) => await NotificationSettings();
|
||||||
|
Post["/notificationsettings", true] = async (x, ct) => await SaveNotificationSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Negotiator> Authentication()
|
private async Task<Negotiator> Authentication()
|
||||||
|
@ -489,7 +495,8 @@ namespace PlexRequests.UI.Modules
|
||||||
var notificationModel = new NotificationModel
|
var notificationModel = new NotificationModel
|
||||||
{
|
{
|
||||||
NotificationType = NotificationType.Test,
|
NotificationType = NotificationType.Test,
|
||||||
DateTime = DateTime.Now
|
DateTime = DateTime.Now,
|
||||||
|
ImgSrc = "http://3.bp.blogspot.com/-EFM-XoKoZ0o/UznF567wCRI/AAAAAAAAALM/6ut7MCF2LrU/s1600/xkcd.png"
|
||||||
};
|
};
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@ -966,5 +973,17 @@ namespace PlexRequests.UI.Modules
|
||||||
return Response.AsJson(new JsonResponseModel { Result = false, Message = e.Message });
|
return Response.AsJson(new JsonResponseModel { Result = false, Message = e.Message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<Negotiator> NotificationSettings()
|
||||||
|
{
|
||||||
|
var s = await NotifySettings.GetSettingsAsync();
|
||||||
|
return View["NotificationSettings", s];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Negotiator> SaveNotificationSettings()
|
||||||
|
{
|
||||||
|
var model = this.Bind<NotificationSettingsV2>();
|
||||||
|
return View["NotificationSettings", model];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -707,6 +707,9 @@
|
||||||
<Content Include="Views\UserWizard\Index.cshtml">
|
<Content Include="Views\UserWizard\Index.cshtml">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
|
<Content Include="Views\Admin\NotificationSettings.cshtml">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
<None Include="Web.Debug.config">
|
<None Include="Web.Debug.config">
|
||||||
<DependentUpon>web.config</DependentUpon>
|
<DependentUpon>web.config</DependentUpon>
|
||||||
</None>
|
</None>
|
||||||
|
|
84
PlexRequests.UI/Views/Admin/NotificationSettings.cshtml
Normal file
84
PlexRequests.UI/Views/Admin/NotificationSettings.cshtml
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
@using System.Linq
|
||||||
|
@using PlexRequests.Core.Models
|
||||||
|
@using PlexRequests.UI.Helpers
|
||||||
|
@inherits Nancy.ViewEngines.Razor.NancyRazorViewBase<PlexRequests.Core.SettingModels.NotificationSettingsV2>
|
||||||
|
@Html.Partial("_Sidebar")
|
||||||
|
|
||||||
|
<div class="col-sm-8 col-sm-push-1">
|
||||||
|
<form class="form-horizontal" method="POST" id="mainForm">
|
||||||
|
<fieldset>
|
||||||
|
<legend>Notification Settings</legend>
|
||||||
|
|
||||||
|
<!--Accordion Item-->
|
||||||
|
<div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading" role="tab" id="0headingOne">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<a role="button" data-toggle="collapse" data-parent="#accordion" href="#0collapseOne" aria-controls="0collapseOne">
|
||||||
|
New Request
|
||||||
|
</a>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div id="0collapseOne" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="0headingOne">
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="EmailNotification[0].Subject" class="control-label">Subject</label>
|
||||||
|
<div>
|
||||||
|
<input type="text" class="form-control form-control-custom " id="EmailNotification[0].Subject" name="EmailNotification0.Subject" value="@(Model.EmailNotification[0].Subject)">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="EmailNotification[0].Body" class="control-label">Body</label>
|
||||||
|
<div>
|
||||||
|
<input type="text" class="form-control form-control-custom " id="EmailNotification[0].Body" name="EmailNotification0.Body" value="@(Model.EmailNotification[0].Body)">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div>
|
||||||
|
<button id="save" type="submit" class="btn btn-primary-outline">Submit</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(function () {
|
||||||
|
|
||||||
|
|
||||||
|
var base = '@Html.GetBaseUrl()';
|
||||||
|
$('#save').click(function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
var $form = $("#mainForm");
|
||||||
|
var data = $form.serialize();
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
type: $form.prop("method"),
|
||||||
|
data: data,
|
||||||
|
url: $form.prop("action"),
|
||||||
|
dataType: "json",
|
||||||
|
success: function (response) {
|
||||||
|
if (response.result === true) {
|
||||||
|
generateNotify(response.message, "success");
|
||||||
|
} else {
|
||||||
|
generateNotify(response.message, "warning");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function (e) {
|
||||||
|
console.log(e);
|
||||||
|
generateNotify("Something went wrong!", "danger");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -1,7 +1,7 @@
|
||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio 14
|
# Visual Studio 14
|
||||||
VisualStudioVersion = 14.0.25420.1
|
VisualStudioVersion = 14.0.25123.0
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlexRequests.UI", "PlexRequests.UI\PlexRequests.UI.csproj", "{68F5F5F3-B8BB-4911-875F-6F00AAE04EA6}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlexRequests.UI", "PlexRequests.UI\PlexRequests.UI.csproj", "{68F5F5F3-B8BB-4911-875F-6F00AAE04EA6}"
|
||||||
EndProject
|
EndProject
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue