mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-26 12:21:36 +03:00
Added WIP author email notifications for new comments
refs https://github.com/TryGhost/Team/issues/1664
This commit is contained in:
parent
25c0b75426
commit
1b4f8f0c95
@ -271,6 +271,7 @@ async function initServices({config}) {
|
|||||||
const limits = require('./server/services/limits');
|
const limits = require('./server/services/limits');
|
||||||
const apiVersionCompatibility = require('./server/services/api-version-compatibility');
|
const apiVersionCompatibility = require('./server/services/api-version-compatibility');
|
||||||
const scheduling = require('./server/adapters/scheduling');
|
const scheduling = require('./server/adapters/scheduling');
|
||||||
|
const comments = require('./server/services/comments');
|
||||||
|
|
||||||
const urlUtils = require('./shared/url-utils');
|
const urlUtils = require('./shared/url-utils');
|
||||||
|
|
||||||
@ -293,7 +294,8 @@ async function initServices({config}) {
|
|||||||
apiVersionCompatibility.init(),
|
apiVersionCompatibility.init(),
|
||||||
scheduling.init({
|
scheduling.init({
|
||||||
apiUrl: urlUtils.urlFor('api', {type: 'admin'}, true)
|
apiUrl: urlUtils.urlFor('api', {type: 'admin'}, true)
|
||||||
})
|
}),
|
||||||
|
comments.init()
|
||||||
]);
|
]);
|
||||||
debug('End: Services');
|
debug('End: Services');
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ const ghostBookshelf = require('./base');
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const errors = require('@tryghost/errors');
|
const errors = require('@tryghost/errors');
|
||||||
const tpl = require('@tryghost/tpl');
|
const tpl = require('@tryghost/tpl');
|
||||||
|
const commentsService = require('../services/comments');
|
||||||
|
|
||||||
const messages = {
|
const messages = {
|
||||||
commentNotFound: 'Comment could not be found',
|
commentNotFound: 'Comment could not be found',
|
||||||
@ -38,6 +39,10 @@ const Comment = ghostBookshelf.Model.extend({
|
|||||||
onCreated: function onCreated(model, options) {
|
onCreated: function onCreated(model, options) {
|
||||||
ghostBookshelf.Model.prototype.onCreated.apply(this, arguments);
|
ghostBookshelf.Model.prototype.onCreated.apply(this, arguments);
|
||||||
|
|
||||||
|
if (!options.context.internal) {
|
||||||
|
commentsService.api.sendNewCommentNotifications(model);
|
||||||
|
}
|
||||||
|
|
||||||
model.emitChange('added', options);
|
model.emitChange('added', options);
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
|
172
core/server/services/comments/emails/new-comment.hbs
Normal file
172
core/server/services/comments/emails/new-comment.hbs
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width">
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||||
|
<title>🔑 Secure sign in link for {{siteTitle}}</title>
|
||||||
|
<style>
|
||||||
|
/* -------------------------------------
|
||||||
|
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||||
|
------------------------------------- */
|
||||||
|
@media only screen and (max-width: 620px) {
|
||||||
|
table[class=body] h1 {
|
||||||
|
font-size: 28px !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] .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] p[class=small],
|
||||||
|
table[class=body] a[class=small] {
|
||||||
|
font-size: 11px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* -------------------------------------
|
||||||
|
PRESERVE THESE STYLES IN THE HEAD
|
||||||
|
------------------------------------- */
|
||||||
|
@media all {
|
||||||
|
.ExternalClass {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.ExternalClass,
|
||||||
|
.ExternalClass p,
|
||||||
|
.ExternalClass span,
|
||||||
|
.ExternalClass font,
|
||||||
|
.ExternalClass td,
|
||||||
|
.ExternalClass div {
|
||||||
|
line-height: 100%;
|
||||||
|
}
|
||||||
|
.recipient-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;
|
||||||
|
}
|
||||||
|
#MessageViewBody a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: inherit;
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hr {
|
||||||
|
border-width: 0;
|
||||||
|
height: 0;
|
||||||
|
margin-top: 34px;
|
||||||
|
margin-bottom: 34px;
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
border-bottom-color: #EEF5F8;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #3A464C;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body style="background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.5em; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
|
||||||
|
<tr>
|
||||||
|
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;"> </td>
|
||||||
|
<td class="container" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; display: block; Margin: 0 auto; max-width: 540px; padding: 10px; width: 540px;">
|
||||||
|
<div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 600px; padding: 30px 20px;">
|
||||||
|
|
||||||
|
<!-- START CENTERED 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;">Welcome back to {{siteTitle}}!</span>
|
||||||
|
<table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 8px;">
|
||||||
|
|
||||||
|
<!-- START MAIN CONTENT AREA -->
|
||||||
|
<tr>
|
||||||
|
<td class="wrapper" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; box-sizing: border-box;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
|
||||||
|
<tr>
|
||||||
|
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">
|
||||||
|
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 20px; color: #15212A; font-weight: bold; line-height: 25px; margin: 0; margin-bottom: 15px;">Hey there,</p>
|
||||||
|
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 32px;">Welcome back! Use this link to securely sign in to your {{siteTitle}} account:</p>
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; vertical-align: top; padding-bottom: 35px;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; vertical-align: top; background-color: {{accentColor}}; border-radius: 5px; text-align: center;"> <a href="{{url}}" target="_blank" style="display: inline-block; color: #ffffff; background-color: {{accentColor}}; border: solid 1px {{accentColor}}; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 16px; font-weight: normal; margin: 0; padding: 9px 22px 10px; border-color: {{accentColor}};">Sign in to {{siteTitle}}</a> </td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; line-height: 25px; margin: 0; margin-bottom: 25px;">For your security, the link will expire in 24 hours time.</p>
|
||||||
|
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; line-height: 25px; margin: 0; margin-bottom: 30px;">See you soon!</p>
|
||||||
|
<hr/>
|
||||||
|
<p style="word-break: break-all; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 5px;">You can also copy & paste this URL into your browser:</p>
|
||||||
|
<p style="word-break: break-all; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; line-height: 25px; margin-top:0; color: #3A464C;">{{url}}</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- START FOOTER -->
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; padding-top: 80px;">
|
||||||
|
<p class="small" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; line-height: 18px; font-size: 11px; color: #738A94; font-weight: normal; margin: 0; margin-bottom: 2px;">If you did not make this request, you can safely ignore this email.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; padding-top: 2px;">
|
||||||
|
<p class="small" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; line-height: 18px; font-size: 11px; color: #738A94; font-weight: normal; margin: 0; margin-bottom: 2px;">This message was sent from <a class="small" href="{{siteUrl}}" style="text-decoration: underline; color: #738A94; font-size: 11px;">{{siteDomain}}</a> to <a class="small" href="mailto:{{fromEmail}}" style="text-decoration: underline; color: #738A94; font-size: 11px;">{{fromEmail}}</a></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- END FOOTER -->
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- END MAIN CONTENT AREA -->
|
||||||
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- END CENTERED CONTAINER -->
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;"> </td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -1,10 +1,20 @@
|
|||||||
const CommentsService = require('./service');
|
|
||||||
const models = require('../models');
|
|
||||||
|
|
||||||
class CommentsServiceWrapper {
|
class CommentsServiceWrapper {
|
||||||
init() {
|
init() {
|
||||||
|
const CommentsService = require('./service');
|
||||||
|
|
||||||
|
const config = require('../../../shared/config');
|
||||||
|
const models = require('../../models');
|
||||||
|
const {GhostMailer} = require('../mail');
|
||||||
|
const mailer = new GhostMailer();
|
||||||
|
const settingsCache = require('../../../shared/settings-cache');
|
||||||
|
const urlUtils = require('../../../shared/url-utils');
|
||||||
|
|
||||||
this.api = new CommentsService({
|
this.api = new CommentsService({
|
||||||
models
|
config,
|
||||||
|
models,
|
||||||
|
mailer,
|
||||||
|
settingsCache,
|
||||||
|
urlUtils
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,96 @@
|
|||||||
|
const {promises: fs} = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
class CommentsService {
|
class CommentsService {
|
||||||
constructor({models}) {
|
constructor({config, models, mailer, settingsCache, urlUtils}) {
|
||||||
|
this.config = config;
|
||||||
this.models = models;
|
this.models = models;
|
||||||
|
this.mailer = mailer;
|
||||||
|
this.settingsCache = settingsCache;
|
||||||
|
this.urlUtils = urlUtils;
|
||||||
|
|
||||||
|
this.Handlebars = require('handlebars');
|
||||||
|
}
|
||||||
|
|
||||||
|
get siteDomain() {
|
||||||
|
return this.urlUtils.getSiteUrl()
|
||||||
|
.match(new RegExp('^https?://([^/:?#]+)(?:[/:?#]|$)', 'i'));
|
||||||
|
}
|
||||||
|
|
||||||
|
get membersAddress() {
|
||||||
|
// TODO: get from address of default newsletter?
|
||||||
|
return `noreply@${this.siteDomain}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: duplicated from services/members/config - exrtact to settings?
|
||||||
|
get supportAddress() {
|
||||||
|
const supportAddress = this.settingsCache.get('members_support_address') || 'noreply';
|
||||||
|
|
||||||
|
// Any fromAddress without domain uses site domain, like default setting `noreply`
|
||||||
|
if (supportAddress.indexOf('@') < 0) {
|
||||||
|
return `${supportAddress}@${this.siteDomain}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return supportAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
get notificationFromAddress() {
|
||||||
|
return this.supportAddress || this.membersAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendNewCommentNotifications(comment) {
|
||||||
|
this.notifyPostAuthor(comment);
|
||||||
|
|
||||||
|
if (comment.get('parent_id')) {
|
||||||
|
this.notifyParentCommentAuthor(comment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async notifyPostAuthor(comment) {
|
||||||
|
const post = await this.models.Post.findOne({id: comment.get('post_id')}, {withRelated: ['authors']});
|
||||||
|
|
||||||
|
for (const author of post.related('authors')) {
|
||||||
|
if (!author.get('comment_notifications')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const from = this.notificationFromAddress;
|
||||||
|
const to = author.get('email');
|
||||||
|
const subject = 'You have a new comment on one of your posts';
|
||||||
|
|
||||||
|
const templateData = {
|
||||||
|
siteTitle: this.settingsCache.get('title'),
|
||||||
|
siteUrl: this.urlUtils.getSiteUrl(),
|
||||||
|
siteDomain: this.siteDomain,
|
||||||
|
accentColor: this.settingsCache.get('accent_color'),
|
||||||
|
fromEmail: from
|
||||||
|
};
|
||||||
|
|
||||||
|
const html = await this.renderEmailTemplate('new-comment', templateData);
|
||||||
|
|
||||||
|
this.mailer.send({
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
subject,
|
||||||
|
html,
|
||||||
|
forceTextContent: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async notifyParentCommentAuthor(comment) {
|
||||||
|
const parent = await this.models.Comment.findOne({id: comment.get('parent_id')});
|
||||||
|
|
||||||
|
if (parent && parent.get('status') === 'published') {
|
||||||
|
// do the things
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async renderEmailTemplate(name, data) {
|
||||||
|
const templateSource = await fs.readFile(path.join(__dirname, './emails/', `${name}.hbs`), 'utf8');
|
||||||
|
const template = this.Handlebars.compile(Buffer.from(templateSource).toString());
|
||||||
|
|
||||||
|
return template(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user