ghost 5.3.1 → 5.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/tryghost-custom-theme-settings-service-0.0.0.tgz +0 -0
- package/components/tryghost-domain-events-0.0.0.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-0.0.0.tgz +0 -0
- package/components/tryghost-email-analytics-service-0.0.0.tgz +0 -0
- package/components/tryghost-express-dynamic-redirects-0.0.0.tgz +0 -0
- package/components/tryghost-magic-link-0.0.0.tgz +0 -0
- package/components/tryghost-member-analytics-service-0.0.0.tgz +0 -0
- package/components/tryghost-member-events-0.0.0.tgz +0 -0
- package/components/tryghost-members-analytics-ingress-0.0.0.tgz +0 -0
- package/components/tryghost-members-api-0.0.0.tgz +0 -0
- package/components/tryghost-members-csv-0.0.0.tgz +0 -0
- package/components/tryghost-members-events-service-0.0.0.tgz +0 -0
- package/components/tryghost-members-importer-0.0.0.tgz +0 -0
- package/components/tryghost-members-offers-0.0.0.tgz +0 -0
- package/components/tryghost-members-payments-0.0.0.tgz +0 -0
- package/components/tryghost-members-ssr-0.0.0.tgz +0 -0
- package/components/tryghost-members-stripe-service-0.0.0.tgz +0 -0
- package/components/tryghost-verification-trigger-0.0.0.tgz +0 -0
- package/content/themes/casper/assets/built/global.css +1 -1
- package/content/themes/casper/assets/built/global.css.map +1 -1
- package/content/themes/casper/assets/built/screen.css +1 -1
- package/content/themes/casper/assets/built/screen.css.map +1 -1
- package/content/themes/casper/assets/css/screen.css +31 -8
- package/content/themes/casper/default.hbs +8 -5
- package/content/themes/casper/gulpfile.js +1 -1
- package/content/themes/casper/package.json +9 -9
- package/content/themes/casper/yarn.lock +1154 -1249
- package/core/boot.js +5 -0
- package/core/built/assets/{chunk.3.dc389a0f93cb5fabd695.js → chunk.3.550552fbc71864fb9738.js} +20 -20
- package/core/built/assets/fonts/Inter.ttf +0 -0
- package/core/built/assets/ghost-dark-5c2a961b35311d7298136e02289d98b2.css +1 -0
- package/core/built/assets/ghost.min-a89d10b3b58c1a5ebaca68cef93a404c.css +1 -0
- package/core/built/assets/{ghost.min-f4bba3a2a5ef256b82641345505d4f0f.js → ghost.min-c75f224decd20f9538179d7564cd2ab4.js} +3025 -2883
- package/core/built/assets/icons/event-comment.svg +3 -0
- package/core/built/assets/{vendor.min-4076498ccd6c8412365f43b156084ed8.js → vendor.min-cf3af99dca0c71937669305afb3686a1.js} +6122 -3197
- package/core/frontend/helpers/comments.js +22 -10
- package/core/frontend/helpers/ghost_head.js +22 -4
- package/core/frontend/helpers/total_members.js +17 -0
- package/core/frontend/helpers/total_paid_members.js +16 -0
- package/core/frontend/utils/frontend-apps.js +33 -0
- package/core/frontend/utils/member-count.js +50 -0
- package/core/frontend/web/middleware/cors.js +2 -1
- package/core/server/api/endpoints/comments-comments.js +50 -32
- package/core/server/api/endpoints/offers-public.js +2 -2
- package/core/server/api/endpoints/offers.js +8 -8
- package/core/server/api/endpoints/settings.js +62 -30
- package/core/server/api/endpoints/utils/serializers/input/settings.js +1 -0
- package/core/server/api/endpoints/utils/serializers/output/config.js +2 -1
- package/core/server/api/endpoints/utils/serializers/output/index.js +0 -4
- package/core/server/api/endpoints/utils/serializers/output/mappers/activity-feed-events.js +17 -0
- package/core/server/api/endpoints/utils/serializers/output/mappers/comments.js +18 -0
- package/core/server/api/endpoints/utils/serializers/output/mappers/index.js +2 -0
- package/core/server/api/endpoints/utils/serializers/output/mappers/offers.js +28 -0
- package/core/server/api/endpoints/utils/serializers/output/members.js +12 -1
- package/core/server/api/endpoints/utils/serializers/output/settings.js +2 -1
- package/core/server/api/endpoints/utils/validators/input/settings.js +22 -2
- package/core/server/data/exporter/table-lists.js +2 -1
- package/core/server/data/migrations/versions/5.5/2022-07-18-14-29-add-comment-reporting-permissions.js +10 -0
- package/core/server/data/migrations/versions/5.5/2022-07-18-14-31-drop-reports-reason.js +3 -0
- package/core/server/data/migrations/versions/5.5/2022-07-18-14-32-drop-nullable-member-id-from-likes.js +4 -0
- package/core/server/data/migrations/versions/5.5/2022-07-18-14-33-fix-comments-on-delete-foreign-keys.js +119 -0
- package/core/server/data/migrations/versions/5.5/2022-07-21-08-56-add-jobs-table.js +11 -0
- package/core/server/data/schema/commands.js +7 -2
- package/core/server/data/schema/fixtures/fixtures.json +5 -0
- package/core/server/data/schema/schema.js +12 -4
- package/core/server/ghost-server.js +0 -22
- package/core/server/models/comment-report.js +34 -0
- package/core/server/models/comment.js +8 -7
- package/core/server/models/job.js +9 -0
- package/core/server/models/tag.js +4 -0
- package/core/server/services/comments/email-templates/new-comment-reply.hbs +2 -2
- package/core/server/services/comments/email-templates/new-comment-reply.txt.js +7 -8
- package/core/server/services/comments/email-templates/new-comment.hbs +2 -2
- package/core/server/services/comments/email-templates/new-comment.txt.js +7 -6
- package/core/server/services/comments/email-templates/report.hbs +199 -0
- package/core/server/services/comments/email-templates/report.txt.js +16 -0
- package/core/server/services/comments/emails.js +57 -1
- package/core/server/services/comments/service.js +194 -2
- package/core/server/services/jobs/job-service.js +24 -1
- package/core/server/services/mail/GhostMailer.js +1 -0
- package/core/server/services/members/SingleUseTokenProvider.js +3 -3
- package/core/server/services/members/api.js +2 -1
- package/core/server/services/members/config.js +4 -1
- package/core/server/services/members/middleware.js +14 -2
- package/core/server/services/members/settings.js +4 -90
- package/core/server/services/public-config/config.js +2 -1
- package/core/server/services/settings/emails/verify-email.js +166 -0
- package/core/server/services/settings/settings-bread-service.js +170 -4
- package/core/server/services/settings/settings-service.js +9 -1
- package/core/server/services/stripe/service.js +9 -1
- package/core/server/services/webhooks/serialize.js +5 -0
- package/core/server/web/admin/views/default-prod.html +4 -4
- package/core/server/web/admin/views/default.html +4 -4
- package/core/server/web/api/endpoints/admin/routes.js +6 -0
- package/core/server/web/api/endpoints/content/routes.js +2 -1
- package/core/server/web/api/middleware/cors.js +2 -1
- package/core/server/web/api/testmode/jobs/graceful-job.js +2 -2
- package/core/server/web/api/testmode/routes.js +14 -0
- package/core/server/web/comments/routes.js +2 -0
- package/core/server/web/members/app.js +2 -4
- package/core/shared/config/defaults.json +15 -7
- package/core/shared/config/env/config.testing.json +3 -2
- package/package.json +75 -60
- package/yarn.lock +1812 -1832
- package/core/built/assets/ghost-dark-9e5d1f0dfae41232e5e34e4d0df53ae0.css +0 -1
- package/core/built/assets/ghost.min-e7cfbd1800f8e99b9158f74f1e39cd76.css +0 -1
- package/core/server/api/endpoints/utils/serializers/output/offers.js +0 -16
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta name="viewport" content="width=device-width">
|
|
5
|
+
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
|
6
|
+
<title>🚩 A comment has been reported on {{postTitle}}</title>
|
|
7
|
+
<style>
|
|
8
|
+
/* -------------------------------------
|
|
9
|
+
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
|
10
|
+
------------------------------------- */
|
|
11
|
+
@media only screen and (max-width: 620px) {
|
|
12
|
+
table[class=body] h1 {
|
|
13
|
+
font-size: 28px !important;
|
|
14
|
+
margin-bottom: 10px !important;
|
|
15
|
+
}
|
|
16
|
+
table[class=body] p,
|
|
17
|
+
table[class=body] ul,
|
|
18
|
+
table[class=body] ol,
|
|
19
|
+
table[class=body] td,
|
|
20
|
+
table[class=body] span,
|
|
21
|
+
table[class=body] a {
|
|
22
|
+
font-size: 16px !important;
|
|
23
|
+
}
|
|
24
|
+
table[class=body] .wrapper,
|
|
25
|
+
table[class=body] .article {
|
|
26
|
+
padding: 10px !important;
|
|
27
|
+
}
|
|
28
|
+
table[class=body] .content {
|
|
29
|
+
padding: 0 !important;
|
|
30
|
+
}
|
|
31
|
+
table[class=body] .container {
|
|
32
|
+
padding: 0 !important;
|
|
33
|
+
width: 100% !important;
|
|
34
|
+
}
|
|
35
|
+
table[class=body] .main {
|
|
36
|
+
border-left-width: 0 !important;
|
|
37
|
+
border-radius: 0 !important;
|
|
38
|
+
border-right-width: 0 !important;
|
|
39
|
+
}
|
|
40
|
+
table[class=body] .btn table {
|
|
41
|
+
width: 100% !important;
|
|
42
|
+
}
|
|
43
|
+
table[class=body] .btn a {
|
|
44
|
+
width: 100% !important;
|
|
45
|
+
}
|
|
46
|
+
table[class=body] .img-responsive {
|
|
47
|
+
height: auto !important;
|
|
48
|
+
max-width: 100% !important;
|
|
49
|
+
width: auto !important;
|
|
50
|
+
}
|
|
51
|
+
table[class=body] p[class=small],
|
|
52
|
+
table[class=body] a[class=small] {
|
|
53
|
+
font-size: 11px !important;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/* -------------------------------------
|
|
57
|
+
PRESERVE THESE STYLES IN THE HEAD
|
|
58
|
+
------------------------------------- */
|
|
59
|
+
@media all {
|
|
60
|
+
.ExternalClass {
|
|
61
|
+
width: 100%;
|
|
62
|
+
}
|
|
63
|
+
.ExternalClass,
|
|
64
|
+
.ExternalClass p,
|
|
65
|
+
.ExternalClass span,
|
|
66
|
+
.ExternalClass font,
|
|
67
|
+
.ExternalClass td,
|
|
68
|
+
.ExternalClass div {
|
|
69
|
+
line-height: 100%;
|
|
70
|
+
}
|
|
71
|
+
.recipient-link a {
|
|
72
|
+
color: inherit !important;
|
|
73
|
+
font-family: inherit !important;
|
|
74
|
+
font-size: inherit !important;
|
|
75
|
+
font-weight: inherit !important;
|
|
76
|
+
line-height: inherit !important;
|
|
77
|
+
text-decoration: none !important;
|
|
78
|
+
}
|
|
79
|
+
#MessageViewBody a {
|
|
80
|
+
color: inherit;
|
|
81
|
+
text-decoration: none;
|
|
82
|
+
font-size: inherit;
|
|
83
|
+
font-family: inherit;
|
|
84
|
+
font-weight: inherit;
|
|
85
|
+
line-height: inherit;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
hr {
|
|
89
|
+
border-width: 0;
|
|
90
|
+
height: 0;
|
|
91
|
+
margin-top: 34px;
|
|
92
|
+
margin-bottom: 34px;
|
|
93
|
+
border-bottom-width: 1px;
|
|
94
|
+
border-bottom-color: #EEF5F8;
|
|
95
|
+
}
|
|
96
|
+
a {
|
|
97
|
+
color: #3A464C;
|
|
98
|
+
}
|
|
99
|
+
</style>
|
|
100
|
+
</head>
|
|
101
|
+
<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%;">
|
|
102
|
+
<table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
|
|
103
|
+
<tr>
|
|
104
|
+
<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>
|
|
105
|
+
<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;">
|
|
106
|
+
<div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 600px; padding: 30px 20px;">
|
|
107
|
+
|
|
108
|
+
<!-- START CENTERED CONTAINER -->
|
|
109
|
+
<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;">{{reporterName}} ({{reporterEmail}}) reported a comment on your post</span>
|
|
110
|
+
<table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 8px;">
|
|
111
|
+
|
|
112
|
+
<!-- START MAIN CONTENT AREA -->
|
|
113
|
+
<tr>
|
|
114
|
+
<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;">
|
|
115
|
+
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
|
|
116
|
+
<tr>
|
|
117
|
+
<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;">
|
|
118
|
+
<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>
|
|
119
|
+
<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;">{{reporter}} has reported the comment below on <a href="{{postUrl}}" target="_blank">{{postTitle}}</a>. This comment will remain visible until you choose to remove it, which can be done directly on the post.</p>
|
|
120
|
+
|
|
121
|
+
<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; background: #F9F9FA; border-radius: 7px;">
|
|
122
|
+
<tbody>
|
|
123
|
+
<tr>
|
|
124
|
+
<td align="left" style="padding: 16px;">
|
|
125
|
+
<table border="0" cellpadding="0" cellspacing="0">
|
|
126
|
+
<tr>
|
|
127
|
+
<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; padding-right: 8px;">
|
|
128
|
+
<table border="0" cellpadding="0" cellspacing="0" width="44" height="44" style="width: 44px; height: 44px; background-color: #15171A; border-radius: 999px;">
|
|
129
|
+
<tr>
|
|
130
|
+
<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: 18px; color: #FFFFFF; text-align: center; font-weight: 500; height: 44px; width: 44px;">{{memberInitials}}</td>
|
|
131
|
+
</tr>
|
|
132
|
+
</table>
|
|
133
|
+
</td>
|
|
134
|
+
<td style="padding-right: 8px;">
|
|
135
|
+
<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; padding-right: 8px; padding: 0; margin: 0; color: #15171A; font-weight: 600;">{{memberName}}</p>
|
|
136
|
+
<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: 13px; padding-right: 8px; padding: 0; margin: 0; color: #95A1AD;">{{#if memberBio}}{{memberBio}} • {{/if}}{{commentDate}}</p>
|
|
137
|
+
</td>
|
|
138
|
+
</tr>
|
|
139
|
+
</table>
|
|
140
|
+
</td>
|
|
141
|
+
</tr>
|
|
142
|
+
<tr>
|
|
143
|
+
<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-top: 0; padding-right: 16px; padding-bottom: 16px; padding-left: 16px; color: #15171A;">
|
|
144
|
+
{{{commentHtml}}}
|
|
145
|
+
</td>
|
|
146
|
+
</tr>
|
|
147
|
+
</tbody>
|
|
148
|
+
</table>
|
|
149
|
+
|
|
150
|
+
<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;">
|
|
151
|
+
<tbody>
|
|
152
|
+
<tr>
|
|
153
|
+
<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-top: 32px; padding-bottom: 12px;">
|
|
154
|
+
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;">
|
|
155
|
+
<tbody>
|
|
156
|
+
<tr>
|
|
157
|
+
<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="{{postUrl}}#ghost-comments-root" 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}};">View comment</a> </td>
|
|
158
|
+
</tr>
|
|
159
|
+
</tbody>
|
|
160
|
+
</table>
|
|
161
|
+
</td>
|
|
162
|
+
</tr>
|
|
163
|
+
</tbody>
|
|
164
|
+
</table>
|
|
165
|
+
<hr/>
|
|
166
|
+
<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>
|
|
167
|
+
<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;">{{postUrl}}#ghost-comments-root</p>
|
|
168
|
+
</td>
|
|
169
|
+
</tr>
|
|
170
|
+
|
|
171
|
+
<!-- START FOOTER -->
|
|
172
|
+
<tr>
|
|
173
|
+
<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;">
|
|
174
|
+
<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:{{toEmail}}" style="text-decoration: underline; color: #738A94; font-size: 11px;">{{toEmail}}</a></p>
|
|
175
|
+
</td>
|
|
176
|
+
</tr>
|
|
177
|
+
<tr>
|
|
178
|
+
<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">
|
|
179
|
+
<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;"><a class="small" href="{{staffUrl}}" style="text-decoration: underline; color: #738A94; font-size: 11px;">Manage your email preferences</a></p>
|
|
180
|
+
</td>
|
|
181
|
+
</tr>
|
|
182
|
+
|
|
183
|
+
<!-- END FOOTER -->
|
|
184
|
+
</table>
|
|
185
|
+
</td>
|
|
186
|
+
</tr>
|
|
187
|
+
|
|
188
|
+
<!-- END MAIN CONTENT AREA -->
|
|
189
|
+
</table>
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
<!-- END CENTERED CONTAINER -->
|
|
193
|
+
</div>
|
|
194
|
+
</td>
|
|
195
|
+
<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>
|
|
196
|
+
</tr>
|
|
197
|
+
</table>
|
|
198
|
+
</body>
|
|
199
|
+
</html>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module.exports = function (data) {
|
|
2
|
+
// Be careful when you indent the email, because whitespaces are visible in emails!
|
|
3
|
+
return `Hey there,
|
|
4
|
+
|
|
5
|
+
${data.reporter} has reported the comment below on ${data.postTitle}. This comment will remain visible until you choose to remove it, which can be done directly on the post.
|
|
6
|
+
|
|
7
|
+
${data.memberName} (${data.memberEmail}):
|
|
8
|
+
${data.commentText}
|
|
9
|
+
|
|
10
|
+
${data.postUrl}#ghost-comments-root
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
Sent to ${data.toEmail} from ${data.siteDomain}.
|
|
15
|
+
You can manage your notification preferences at ${data.staffUrl}.`;
|
|
16
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const {promises: fs} = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const moment = require('moment');
|
|
4
|
+
const htmlToPlaintext = require('../../../shared/html-to-plaintext');
|
|
4
5
|
|
|
5
6
|
class CommentsServiceEmails {
|
|
6
7
|
constructor({config, logging, models, mailer, settingsCache, urlService, urlUtils}) {
|
|
@@ -48,7 +49,7 @@ class CommentsServiceEmails {
|
|
|
48
49
|
|
|
49
50
|
const {html, text} = await this.renderEmailTemplate('new-comment', templateData);
|
|
50
51
|
|
|
51
|
-
this.sendMail({
|
|
52
|
+
await this.sendMail({
|
|
52
53
|
to,
|
|
53
54
|
subject,
|
|
54
55
|
html,
|
|
@@ -65,6 +66,11 @@ class CommentsServiceEmails {
|
|
|
65
66
|
return;
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
// Don't send a notification if you reply to your own comment
|
|
70
|
+
if (parentMember.id === reply.get('member_id')) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
68
74
|
const to = parentMember.get('email');
|
|
69
75
|
const subject = '💬 You have a new reply on one of your comments';
|
|
70
76
|
|
|
@@ -100,6 +106,56 @@ class CommentsServiceEmails {
|
|
|
100
106
|
});
|
|
101
107
|
}
|
|
102
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Send an email to notify the owner of the site that a comment has been reported by a member
|
|
111
|
+
* @param {*} comment The comment model that has been reported
|
|
112
|
+
* @param {*} reporter The member object who reported this comment
|
|
113
|
+
*/
|
|
114
|
+
async notifiyReport(comment, reporter) {
|
|
115
|
+
const post = await this.models.Post.findOne({id: comment.get('post_id')}, {withRelated: ['authors']});
|
|
116
|
+
const member = await this.models.Member.findOne({id: comment.get('member_id')});
|
|
117
|
+
const owner = await this.models.User.getOwnerUser();
|
|
118
|
+
|
|
119
|
+
// For now we only send the report to the owner
|
|
120
|
+
const to = owner.get('email');
|
|
121
|
+
const subject = '🚩 A comment has been reported on your post';
|
|
122
|
+
|
|
123
|
+
const memberName = member.get('name') || 'Anonymous';
|
|
124
|
+
|
|
125
|
+
const templateData = {
|
|
126
|
+
siteTitle: this.settingsCache.get('title'),
|
|
127
|
+
siteUrl: this.urlUtils.getSiteUrl(),
|
|
128
|
+
siteDomain: this.siteDomain,
|
|
129
|
+
postTitle: post.get('title'),
|
|
130
|
+
postUrl: this.urlService.getUrlByResourceId(post.get('id'), {absolute: true}),
|
|
131
|
+
commentHtml: comment.get('html'),
|
|
132
|
+
commentText: htmlToPlaintext.email(comment.get('html')),
|
|
133
|
+
commentDate: moment(comment.get('created_at')).tz(this.settingsCache.get('timezone')).format('D MMM YYYY'),
|
|
134
|
+
|
|
135
|
+
reporterName: reporter.name,
|
|
136
|
+
reporterEmail: reporter.email,
|
|
137
|
+
reporter: reporter.name ? `${reporter.name} (${reporter.email})` : reporter.email,
|
|
138
|
+
|
|
139
|
+
memberName: memberName,
|
|
140
|
+
memberEmail: member.get('email'),
|
|
141
|
+
memberBio: member.get('bio'),
|
|
142
|
+
memberInitials: this.extractInitials(memberName),
|
|
143
|
+
accentColor: this.settingsCache.get('accent_color'),
|
|
144
|
+
fromEmail: this.notificationFromAddress,
|
|
145
|
+
toEmail: to,
|
|
146
|
+
staffUrl: `${this.urlUtils.getAdminUrl()}ghost/#/settings/staff/${owner.get('slug')}`
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const {html, text} = await this.renderEmailTemplate('report', templateData);
|
|
150
|
+
|
|
151
|
+
await this.sendMail({
|
|
152
|
+
to,
|
|
153
|
+
subject,
|
|
154
|
+
html,
|
|
155
|
+
text
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
103
159
|
// Utils
|
|
104
160
|
|
|
105
161
|
get siteDomain() {
|
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
const tpl = require('@tryghost/tpl');
|
|
2
|
+
const errors = require('@tryghost/errors');
|
|
3
|
+
const {MemberCommentEvent} = require('@tryghost/member-events');
|
|
4
|
+
const DomainEvents = require('@tryghost/domain-events');
|
|
5
|
+
|
|
6
|
+
const messages = {
|
|
7
|
+
commentNotFound: 'Comment could not be found',
|
|
8
|
+
memberNotFound: 'Unable to find member',
|
|
9
|
+
likeNotFound: 'Unable to find like',
|
|
10
|
+
alreadyLiked: 'This comment was liked already',
|
|
11
|
+
replyToReply: 'Can not reply to a reply'
|
|
12
|
+
};
|
|
13
|
+
|
|
1
14
|
class CommentsService {
|
|
2
15
|
constructor({config, logging, models, mailer, settingsCache, urlService, urlUtils}) {
|
|
3
16
|
this.config = config;
|
|
@@ -13,11 +26,190 @@ class CommentsService {
|
|
|
13
26
|
}
|
|
14
27
|
|
|
15
28
|
async sendNewCommentNotifications(comment) {
|
|
16
|
-
this.emails.notifyPostAuthors(comment);
|
|
29
|
+
await this.emails.notifyPostAuthors(comment);
|
|
17
30
|
|
|
18
31
|
if (comment.get('parent_id')) {
|
|
19
|
-
this.emails.notifyParentCommentAuthor(comment);
|
|
32
|
+
await this.emails.notifyParentCommentAuthor(comment);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async reportComment(commentId, reporter) {
|
|
37
|
+
const comment = await this.models.Comment.findOne({id: commentId}, {require: true});
|
|
38
|
+
|
|
39
|
+
// Check if this reporter already reported this comment (then don't send an email)?
|
|
40
|
+
const existing = await this.models.CommentReport.findOne({
|
|
41
|
+
comment_id: comment.id,
|
|
42
|
+
member_id: reporter.id
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (existing) {
|
|
46
|
+
// Ignore silently for now
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Save report model
|
|
51
|
+
await this.models.CommentReport.add({
|
|
52
|
+
comment_id: comment.id,
|
|
53
|
+
member_id: reporter.id
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
await this.emails.notifiyReport(comment, reporter);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {string} id - The ID of the Comment to get
|
|
61
|
+
* @param {any} options
|
|
62
|
+
*/
|
|
63
|
+
async getCommentByID(id, options) {
|
|
64
|
+
const model = await this.models.Comment.findOne({id}, options);
|
|
65
|
+
|
|
66
|
+
if (!model) {
|
|
67
|
+
throw new errors.NotFoundError({
|
|
68
|
+
messages: tpl(messages.commentNotFound)
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return model;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @param {string} post - The ID of the Post to comment on
|
|
77
|
+
* @param {string} member - The ID of the Member to comment as
|
|
78
|
+
* @param {string} comment - The HTML content of the Comment
|
|
79
|
+
* @param {any} options
|
|
80
|
+
*/
|
|
81
|
+
async commentOnPost(post, member, comment, options) {
|
|
82
|
+
await this.models.Member.findOne({
|
|
83
|
+
id: member
|
|
84
|
+
}, {
|
|
85
|
+
require: true,
|
|
86
|
+
...options
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const model = await this.models.Comment.add({
|
|
90
|
+
post_id: post,
|
|
91
|
+
member_id: member,
|
|
92
|
+
parent_id: null,
|
|
93
|
+
html: comment,
|
|
94
|
+
status: 'published'
|
|
95
|
+
}, options);
|
|
96
|
+
|
|
97
|
+
if (!options.context.internal) {
|
|
98
|
+
await this.sendNewCommentNotifications(model);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
DomainEvents.dispatch(MemberCommentEvent.create({
|
|
102
|
+
memberId: member,
|
|
103
|
+
postId: post,
|
|
104
|
+
commentId: model.id
|
|
105
|
+
}));
|
|
106
|
+
|
|
107
|
+
return model;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {string} parent - The ID of the Comment to reply to
|
|
112
|
+
* @param {string} member - The ID of the Member to comment as
|
|
113
|
+
* @param {string} comment - The HTML content of the Comment
|
|
114
|
+
* @param {any} options
|
|
115
|
+
*/
|
|
116
|
+
async replyToComment(parent, member, comment, options) {
|
|
117
|
+
await this.models.Member.findOne({
|
|
118
|
+
id: member
|
|
119
|
+
}, {
|
|
120
|
+
require: true,
|
|
121
|
+
...options
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const parentComment = await this.getCommentByID(parent, options);
|
|
125
|
+
if (!parentComment) {
|
|
126
|
+
throw new errors.BadRequestError({
|
|
127
|
+
message: tpl(messages.commentNotFound)
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (parentComment.get('parent_id') !== null) {
|
|
132
|
+
throw new errors.BadRequestError({
|
|
133
|
+
message: tpl(messages.replyToReply)
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const model = await this.models.Comment.add({
|
|
138
|
+
post_id: parentComment.get('post_id'),
|
|
139
|
+
member_id: member,
|
|
140
|
+
parent_id: parentComment.id,
|
|
141
|
+
html: comment,
|
|
142
|
+
status: 'published'
|
|
143
|
+
}, options);
|
|
144
|
+
|
|
145
|
+
if (!options.context.internal) {
|
|
146
|
+
await this.sendNewCommentNotifications(model);
|
|
20
147
|
}
|
|
148
|
+
|
|
149
|
+
DomainEvents.dispatch(MemberCommentEvent.create({
|
|
150
|
+
memberId: member,
|
|
151
|
+
postId: parentComment.get('post_id'),
|
|
152
|
+
commentId: model.id
|
|
153
|
+
}));
|
|
154
|
+
|
|
155
|
+
return model;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @param {string} id - The ID of the Comment to delete
|
|
160
|
+
* @param {string} member - The ID of the Member to delete as
|
|
161
|
+
* @param {any} options
|
|
162
|
+
*/
|
|
163
|
+
async deleteComment(id, member, options) {
|
|
164
|
+
const existingComment = await this.getCommentByID(id, options);
|
|
165
|
+
|
|
166
|
+
if (existingComment.get('member_id') !== member) {
|
|
167
|
+
throw new errors.NoPermissionError({
|
|
168
|
+
// todo fix message
|
|
169
|
+
message: tpl(messages.memberNotFound)
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const model = await this.models.Comment.edit({
|
|
174
|
+
status: 'deleted'
|
|
175
|
+
}, {
|
|
176
|
+
id,
|
|
177
|
+
require: true,
|
|
178
|
+
...options
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
return model;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* @param {string} id - The ID of the Comment to edit
|
|
186
|
+
* @param {string} member - The ID of the Member to edit as
|
|
187
|
+
* @param {string} comment - The new HTML content of the Comment
|
|
188
|
+
* @param {any} options
|
|
189
|
+
*/
|
|
190
|
+
async editCommentContent(id, member, comment, options) {
|
|
191
|
+
const existingComment = await this.getCommentByID(id, options);
|
|
192
|
+
|
|
193
|
+
if (!comment) {
|
|
194
|
+
return existingComment;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (existingComment.get('member_id') !== member) {
|
|
198
|
+
throw new errors.NoPermissionError({
|
|
199
|
+
// todo fix message
|
|
200
|
+
message: tpl(messages.memberNotFound)
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const model = await this.models.Comment.edit({
|
|
205
|
+
html: comment
|
|
206
|
+
}, {
|
|
207
|
+
id,
|
|
208
|
+
require: true,
|
|
209
|
+
...options
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
return model;
|
|
21
213
|
}
|
|
22
214
|
}
|
|
23
215
|
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
const JobManager = require('@tryghost/job-manager');
|
|
7
7
|
const logging = require('@tryghost/logging');
|
|
8
|
+
const models = require('../../models');
|
|
8
9
|
const sentry = require('../../../shared/sentry');
|
|
9
10
|
|
|
10
11
|
const errorHandler = (error, workerMeta) => {
|
|
@@ -17,6 +18,28 @@ const workerMessageHandler = ({name, message}) => {
|
|
|
17
18
|
logging.info(`Worker for job ${name} sent a message: ${message}`);
|
|
18
19
|
};
|
|
19
20
|
|
|
20
|
-
const
|
|
21
|
+
const initTestMode = () => {
|
|
22
|
+
// Output job queue length every 5 seconds
|
|
23
|
+
setInterval(() => {
|
|
24
|
+
logging.warn(`${jobManager.queue.length()} jobs in the queue. Idle: ${jobManager.queue.idle()}`);
|
|
25
|
+
|
|
26
|
+
const runningScheduledjobs = Object.keys(jobManager.bree.workers);
|
|
27
|
+
if (Object.keys(jobManager.bree.workers).length) {
|
|
28
|
+
logging.warn(`${Object.keys(jobManager.bree.workers).length} jobs running: ${runningScheduledjobs}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const scheduledJobs = Object.keys(jobManager.bree.intervals);
|
|
32
|
+
if (Object.keys(jobManager.bree.intervals).length) {
|
|
33
|
+
logging.warn(`${Object.keys(jobManager.bree.intervals).length} scheduled jobs: ${scheduledJobs}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (runningScheduledjobs.length === 0 && scheduledJobs.length === 0) {
|
|
37
|
+
logging.warn('No scheduled or running jobs');
|
|
38
|
+
}
|
|
39
|
+
}, 5000);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const jobManager = new JobManager({errorHandler, workerMessageHandler, JobModel: models.Job});
|
|
21
43
|
|
|
22
44
|
module.exports = jobManager;
|
|
45
|
+
module.exports.initTestMode = initTestMode;
|
|
@@ -95,6 +95,7 @@ module.exports = class GhostMailer {
|
|
|
95
95
|
* @param {string} message.html - email content
|
|
96
96
|
* @param {string} message.to - email recipient address
|
|
97
97
|
* @param {string} [message.from] - sender email address
|
|
98
|
+
* @param {string} [message.text] - text version of this message
|
|
98
99
|
* @param {boolean} [message.forceTextContent] - maps to generateTextFromHTML nodemailer option
|
|
99
100
|
* which is: "if set to true uses HTML to generate plain text body part from the HTML if the text is not defined"
|
|
100
101
|
* (ref: https://github.com/nodemailer/nodemailer/tree/da2f1d278f91b4262e940c0b37638e7027184b1d#e-mail-message-fields)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
-
const {
|
|
2
|
+
const {ValidationError} = require('@tryghost/errors');
|
|
3
3
|
|
|
4
4
|
class SingleUseTokenProvider {
|
|
5
5
|
/**
|
|
@@ -41,7 +41,7 @@ class SingleUseTokenProvider {
|
|
|
41
41
|
const model = await this.model.findOne({token});
|
|
42
42
|
|
|
43
43
|
if (!model) {
|
|
44
|
-
throw new
|
|
44
|
+
throw new ValidationError({
|
|
45
45
|
message: 'Invalid token provided'
|
|
46
46
|
});
|
|
47
47
|
}
|
|
@@ -51,7 +51,7 @@ class SingleUseTokenProvider {
|
|
|
51
51
|
const tokenLifetimeMilliseconds = Date.now() - createdAtEpoch;
|
|
52
52
|
|
|
53
53
|
if (tokenLifetimeMilliseconds > this.validity) {
|
|
54
|
-
throw new
|
|
54
|
+
throw new ValidationError({
|
|
55
55
|
message: 'Token expired'
|
|
56
56
|
});
|
|
57
57
|
}
|
|
@@ -189,7 +189,8 @@ function createApiInstance(config) {
|
|
|
189
189
|
StripeProduct: models.StripeProduct,
|
|
190
190
|
StripePrice: models.StripePrice,
|
|
191
191
|
Product: models.Product,
|
|
192
|
-
Settings: models.Settings
|
|
192
|
+
Settings: models.Settings,
|
|
193
|
+
Comment: models.Comment
|
|
193
194
|
},
|
|
194
195
|
stripeAPIService: stripeService.api,
|
|
195
196
|
offersAPI: offersService.api,
|
|
@@ -154,11 +154,14 @@ class MembersConfigProvider {
|
|
|
154
154
|
};
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
getSigninURL(token, type) {
|
|
157
|
+
getSigninURL(token, type, referrer) {
|
|
158
158
|
const siteUrl = this._urlUtils.urlFor({relativeUrl: '/members/'}, true);
|
|
159
159
|
const signinURL = new URL(siteUrl);
|
|
160
160
|
signinURL.searchParams.set('token', token);
|
|
161
161
|
signinURL.searchParams.set('action', type);
|
|
162
|
+
if (referrer) {
|
|
163
|
+
signinURL.searchParams.set('r', referrer);
|
|
164
|
+
}
|
|
162
165
|
return signinURL.toString();
|
|
163
166
|
}
|
|
164
167
|
}
|
|
@@ -143,8 +143,8 @@ const createSessionFromMagicLink = async function (req, res, next) {
|
|
|
143
143
|
// req.query is a plain object, copy it to a URLSearchParams object so we can call toString()
|
|
144
144
|
const searchParams = new URLSearchParams('');
|
|
145
145
|
Object.keys(req.query).forEach((param) => {
|
|
146
|
-
// don't copy the token
|
|
147
|
-
if (param !== 'token') {
|
|
146
|
+
// don't copy the "token" or "r" params
|
|
147
|
+
if (param !== 'token' && param !== 'r') {
|
|
148
148
|
searchParams.set(param, req.query[param]);
|
|
149
149
|
}
|
|
150
150
|
});
|
|
@@ -182,6 +182,18 @@ const createSessionFromMagicLink = async function (req, res, next) {
|
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
if (action === 'signin') {
|
|
186
|
+
const referrer = req.query.r;
|
|
187
|
+
const siteUrl = urlUtils.getSiteUrl();
|
|
188
|
+
|
|
189
|
+
if (referrer && referrer.startsWith(siteUrl)) {
|
|
190
|
+
const redirectUrl = new URL(referrer);
|
|
191
|
+
redirectUrl.searchParams.set('success', true);
|
|
192
|
+
redirectUrl.searchParams.set('action', 'signin');
|
|
193
|
+
return res.redirect(redirectUrl.pathname + redirectUrl.search);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
185
197
|
// Do a standard 302 redirect to the homepage, with success=true
|
|
186
198
|
searchParams.set('success', true);
|
|
187
199
|
res.redirect(`${urlUtils.getSubdir()}/?${searchParams.toString()}`);
|