ghost 5.8.3 → 5.9.2
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-adapter-manager-0.0.0.tgz +0 -0
- package/components/tryghost-api-framework-0.0.0.tgz +0 -0
- package/components/tryghost-bootstrap-socket-0.0.0.tgz +0 -0
- package/components/tryghost-custom-theme-settings-service-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-job-manager-0.0.0.tgz +0 -0
- package/components/tryghost-mailgun-client-0.0.0.tgz +0 -0
- package/components/tryghost-member-analytics-service-0.0.0.tgz +0 -0
- package/components/tryghost-members-api-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-minifier-0.0.0.tgz +0 -0
- package/components/tryghost-mw-cache-control-0.0.0.tgz +0 -0
- package/components/tryghost-mw-error-handler-0.0.0.tgz +0 -0
- package/components/tryghost-oembed-service-0.0.0.tgz +0 -0
- package/components/tryghost-package-json-0.0.0.tgz +0 -0
- package/components/tryghost-session-service-0.0.0.tgz +0 -0
- package/components/tryghost-settings-path-manager-0.0.0.tgz +0 -0
- package/components/tryghost-update-check-service-0.0.0.tgz +0 -0
- package/content/themes/casper/README.md +1 -1
- package/content/themes/casper/assets/built/portal.min.js +3 -0
- 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 +119 -37
- package/content/themes/casper/package.json +3 -3
- package/content/themes/casper/partials/post-card.hbs +3 -1
- package/content/themes/casper/post.hbs +7 -7
- package/content/themes/casper/yarn.lock +69 -68
- package/core/boot.js +2 -0
- package/core/built/admin/assets/{chunk.143.cf66fd97f99af03c697f.js → chunk.143.4e4590d80452422483d8.js} +5 -5
- package/core/built/admin/assets/{chunk.178.e95388dfcc564cdb1ecc.js → chunk.178.16d88956c09eb76205e0.js} +4 -4
- package/core/built/admin/assets/{chunk.351.cbc224ca65c14ef5322d.js → chunk.351.73f27952f867334a8228.js} +3 -3
- package/core/built/admin/assets/{chunk.351.cbc224ca65c14ef5322d.js.LICENSE.txt → chunk.351.73f27952f867334a8228.js.LICENSE.txt} +0 -0
- package/core/built/admin/assets/{ghost-b469423d0fbe5e40af17b560f7e3cead.css → ghost-0319628ec5dcb83e6968e99eaa629a75.css} +1 -1
- package/core/built/admin/assets/{ghost-dark-bcb6f4517a2dfe23a0a280632bfca00c.css → ghost-dark-eada0a988277e84b4c8ead995f912044.css} +1 -1
- package/core/built/admin/assets/{ghost-f1d63ad9698b7d38261df6384513c952.js → ghost-f144e03b578116a5d0ed41bceaf2bab8.js} +90 -88
- package/core/built/admin/index.html +5 -5
- package/core/frontend/helpers/comment_count.js +1 -15
- package/core/frontend/helpers/comments.js +4 -16
- package/core/frontend/helpers/ghost_head.js +1 -1
- package/core/frontend/services/routing/controllers/unsubscribe.js +3 -0
- package/core/server/api/endpoints/comments-members.js +23 -1
- package/core/server/api/endpoints/index.js +52 -52
- package/core/server/api/endpoints/oembed.js +1 -1
- package/core/server/api/endpoints/utils/serializers/input/comments.js +18 -0
- package/core/server/api/endpoints/utils/serializers/input/db.js +1 -1
- package/core/server/api/endpoints/utils/serializers/input/index.js +4 -0
- package/core/server/api/endpoints/utils/serializers/input/integrations.js +2 -2
- package/core/server/api/{shared → endpoints/utils}/serializers/input/utils/settings-filter-type-group-mapper.js +0 -0
- package/core/server/api/{shared → endpoints/utils}/serializers/input/utils/settings-key-group-mapper.js +0 -0
- package/core/server/api/{shared → endpoints/utils}/serializers/input/utils/settings-key-type-mapper.js +0 -0
- package/core/server/api/endpoints/utils/serializers/output/mappers/comments.js +12 -12
- package/core/server/api/endpoints/utils/serializers/output/tiers.js +1 -1
- package/core/server/api/index.js +0 -2
- package/core/server/data/importer/importers/data/settings.js +2 -2
- package/core/server/data/migrations/versions/5.9/2022-08-09-08-32-added-new-integration-type.js +24 -0
- package/core/server/data/schema/clients/mysql.js +0 -15
- package/core/server/data/schema/commands.js +0 -9
- package/core/server/data/schema/fixtures/fixtures.json +1 -1
- package/core/server/data/schema/schema.js +3 -3
- package/core/server/models/base/plugins/user-type.js +1 -9
- package/core/server/models/comment.js +96 -15
- package/core/server/models/label.js +14 -0
- package/core/server/models/newsletter.js +21 -0
- package/core/server/models/tag.js +20 -0
- package/core/server/models/user.js +20 -0
- package/core/server/services/auth/api-key/admin.js +1 -5
- package/core/server/services/auth/api-key/content.js +1 -5
- package/core/server/services/bulk-email/bulk-email-processor.js +19 -12
- package/core/server/services/bulk-email/index.js +1 -17
- package/core/server/services/comments/controller.js +9 -0
- package/core/server/services/comments/email-templates/new-comment-reply.hbs +3 -3
- package/core/server/services/comments/email-templates/new-comment-reply.txt.js +1 -1
- package/core/server/services/comments/email-templates/new-comment.hbs +2 -2
- package/core/server/services/comments/email-templates/report.hbs +2 -2
- package/core/server/services/comments/emails.js +2 -1
- package/core/server/services/comments/service.js +16 -3
- package/core/server/services/mega/post-email-serializer.js +11 -6
- package/core/server/services/permissions/can-this.js +154 -161
- package/core/server/services/permissions/parse-context.js +1 -8
- package/core/server/services/webhooks/serialize.js +3 -3
- package/core/server/web/api/endpoints/admin/routes.js +1 -1
- package/core/server/web/api/endpoints/content/routes.js +1 -1
- package/core/server/web/comments/routes.js +2 -1
- package/core/server/web/shared/middleware/index.js +1 -1
- package/core/shared/config/defaults.json +2 -2
- package/core/shared/labs.js +0 -1
- package/package.json +34 -37
- package/yarn.lock +272 -245
- package/core/server/api/README.md +0 -130
- package/core/server/api/shared/frame.js +0 -95
- package/core/server/api/shared/headers.js +0 -152
- package/core/server/api/shared/http.js +0 -127
- package/core/server/api/shared/index.js +0 -25
- package/core/server/api/shared/pipeline.js +0 -259
- package/core/server/api/shared/serializers/handle.js +0 -140
- package/core/server/api/shared/serializers/index.js +0 -13
- package/core/server/api/shared/serializers/input/all.js +0 -41
- package/core/server/api/shared/serializers/input/index.js +0 -5
- package/core/server/api/shared/serializers/output/index.js +0 -1
- package/core/server/api/shared/utils/index.js +0 -5
- package/core/server/api/shared/utils/options.js +0 -23
- package/core/server/api/shared/validators/handle.js +0 -68
- package/core/server/api/shared/validators/index.js +0 -9
- package/core/server/api/shared/validators/input/all.js +0 -213
- package/core/server/api/shared/validators/input/index.js +0 -5
- package/core/server/services/bulk-email/mailgun.js +0 -122
- package/core/server/services/oembed.js +0 -346
- package/core/server/web/shared/middleware/cache-control.js +0 -43
|
@@ -319,7 +319,7 @@ module.exports = {
|
|
|
319
319
|
maxlength: 50,
|
|
320
320
|
nullable: false,
|
|
321
321
|
defaultTo: 'custom',
|
|
322
|
-
validations: {isIn: [['internal', 'builtin', 'custom']]}
|
|
322
|
+
validations: {isIn: [['internal', 'builtin', 'custom', 'core']]}
|
|
323
323
|
},
|
|
324
324
|
name: {type: 'string', maxlength: 191, nullable: false},
|
|
325
325
|
slug: {type: 'string', maxlength: 191, nullable: false, unique: true},
|
|
@@ -440,9 +440,9 @@ module.exports = {
|
|
|
440
440
|
stripe_coupon_id: {type: 'string', maxlength: 255, nullable: true, unique: true},
|
|
441
441
|
interval: {type: 'string', maxlength: 50, nullable: false, validations: {isIn: [['month', 'year']]}},
|
|
442
442
|
currency: {type: 'string', maxlength: 50, nullable: true},
|
|
443
|
-
discount_type: {type: 'string', maxlength: 50, nullable: false, validations: {isIn: [['percent', 'amount']]}},
|
|
443
|
+
discount_type: {type: 'string', maxlength: 50, nullable: false, validations: {isIn: [['percent', 'amount', 'trial']]}},
|
|
444
444
|
discount_amount: {type: 'integer', nullable: false},
|
|
445
|
-
duration: {type: 'string', maxlength: 50, nullable: false},
|
|
445
|
+
duration: {type: 'string', maxlength: 50, nullable: false, validations: {isIn: [['trial', 'once', 'repeating', 'forever']]}},
|
|
446
446
|
duration_in_months: {type: 'integer', nullable: true},
|
|
447
447
|
portal_title: {type: 'string', maxlength: 191, nullable: true},
|
|
448
448
|
portal_description: {type: 'string', maxlength: 2000, nullable: true},
|
|
@@ -33,7 +33,7 @@ module.exports = function (Bookshelf) {
|
|
|
33
33
|
options = options || {};
|
|
34
34
|
options.context = options.context || {};
|
|
35
35
|
|
|
36
|
-
if (options.context.user
|
|
36
|
+
if (options.context.user) {
|
|
37
37
|
return options.context.user;
|
|
38
38
|
} else if (options.context.integration) {
|
|
39
39
|
/**
|
|
@@ -64,8 +64,6 @@ module.exports = function (Bookshelf) {
|
|
|
64
64
|
return Bookshelf.Model.internalUser;
|
|
65
65
|
} else if (this.get('id')) {
|
|
66
66
|
return this.get('id');
|
|
67
|
-
} else if (options.context.external) {
|
|
68
|
-
return Bookshelf.Model.externalUser;
|
|
69
67
|
} else {
|
|
70
68
|
throw new errors.NotFoundError({
|
|
71
69
|
message: tpl(messages.missingContext),
|
|
@@ -80,15 +78,9 @@ module.exports = function (Bookshelf) {
|
|
|
80
78
|
* context.user ? true : false (if context.user is 0 as number, this condition is false)
|
|
81
79
|
*/
|
|
82
80
|
internalUser: 1,
|
|
83
|
-
externalUser: 0,
|
|
84
81
|
|
|
85
82
|
isInternalUser: function isInternalUser(id) {
|
|
86
83
|
return id === Bookshelf.Model.internalUser || id === Bookshelf.Model.internalUser.toString();
|
|
87
|
-
},
|
|
88
|
-
|
|
89
|
-
isExternalUser: function isExternalUser(id) {
|
|
90
|
-
return id === Bookshelf.Model.externalUser || id === Bookshelf.Model.externalUser.toString();
|
|
91
84
|
}
|
|
92
|
-
|
|
93
85
|
});
|
|
94
86
|
};
|
|
@@ -8,8 +8,7 @@ const messages = {
|
|
|
8
8
|
emptyComment: 'The body of a comment cannot be empty',
|
|
9
9
|
commentNotFound: 'Comment could not be found',
|
|
10
10
|
notYourCommentToEdit: 'You may only edit your own comments',
|
|
11
|
-
notYourCommentToDestroy: 'You may only delete your own comments'
|
|
12
|
-
cannotEditDeletedComment: 'You may only edit published comments'
|
|
11
|
+
notYourCommentToDestroy: 'You may only delete your own comments'
|
|
13
12
|
};
|
|
14
13
|
|
|
15
14
|
/**
|
|
@@ -52,7 +51,10 @@ const Comment = ghostBookshelf.Model.extend({
|
|
|
52
51
|
},
|
|
53
52
|
|
|
54
53
|
replies() {
|
|
55
|
-
return this.hasMany('Comment', 'parent_id')
|
|
54
|
+
return this.hasMany('Comment', 'parent_id', 'id')
|
|
55
|
+
.query('orderBy', 'created_at', 'ASC')
|
|
56
|
+
// Note: this limit is not working
|
|
57
|
+
.query('limit', 3);
|
|
56
58
|
},
|
|
57
59
|
|
|
58
60
|
emitChange: function emitChange(event, options) {
|
|
@@ -63,12 +65,6 @@ const Comment = ghostBookshelf.Model.extend({
|
|
|
63
65
|
onSaving() {
|
|
64
66
|
ghostBookshelf.Model.prototype.onSaving.apply(this, arguments);
|
|
65
67
|
|
|
66
|
-
if (this.hasChanged('html') && this.get('status') !== 'published') {
|
|
67
|
-
throw new ValidationError({
|
|
68
|
-
message: tpl(messages.cannotEditDeletedComment)
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
68
|
if (this.hasChanged('html')) {
|
|
73
69
|
const sanitizeHtml = require('sanitize-html');
|
|
74
70
|
|
|
@@ -105,13 +101,16 @@ const Comment = ghostBookshelf.Model.extend({
|
|
|
105
101
|
},
|
|
106
102
|
|
|
107
103
|
enforcedFilters: function enforcedFilters(options) {
|
|
108
|
-
|
|
109
|
-
|
|
104
|
+
// Convenicence option to merge all filters with parent_id:null filter
|
|
105
|
+
if (options.parentId !== undefined) {
|
|
106
|
+
if (options.parentId === null) {
|
|
107
|
+
return 'parent_id:null';
|
|
108
|
+
}
|
|
109
|
+
return 'parent_id:' + options.parentId;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
return
|
|
112
|
+
return null;
|
|
113
113
|
}
|
|
114
|
-
|
|
115
114
|
}, {
|
|
116
115
|
destroy: function destroy(unfilteredOptions) {
|
|
117
116
|
let options = this.filterOptions(unfilteredOptions, 'destroy', {extraAllowedProperties: ['id']});
|
|
@@ -181,12 +180,94 @@ const Comment = ghostBookshelf.Model.extend({
|
|
|
181
180
|
*/
|
|
182
181
|
defaultRelations: function defaultRelations(methodName, options) {
|
|
183
182
|
// @todo: the default relations are not working for 'add' when we add it below
|
|
184
|
-
if (['findAll', 'findPage', 'edit', 'findOne'].indexOf(methodName) !== -1) {
|
|
183
|
+
if (['findAll', 'findPage', 'edit', 'findOne', 'destroy'].indexOf(methodName) !== -1) {
|
|
185
184
|
if (!options.withRelated || options.withRelated.length === 0) {
|
|
186
|
-
options.
|
|
185
|
+
if (options.parentId) {
|
|
186
|
+
// Do not include replies for replies
|
|
187
|
+
options.withRelated = [
|
|
188
|
+
// Relations
|
|
189
|
+
'member', 'count.likes', 'count.liked'
|
|
190
|
+
];
|
|
191
|
+
} else {
|
|
192
|
+
options.withRelated = [
|
|
193
|
+
// Relations
|
|
194
|
+
'member', 'count.replies', 'count.likes', 'count.liked',
|
|
195
|
+
// Replies (limited to 3)
|
|
196
|
+
'replies', 'replies.member' , 'replies.count.likes', 'replies.count.liked'
|
|
197
|
+
];
|
|
198
|
+
}
|
|
187
199
|
}
|
|
188
200
|
}
|
|
189
201
|
|
|
202
|
+
return options;
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
async findPage(options) {
|
|
206
|
+
const {withRelated} = this.defaultRelations('findPage', options);
|
|
207
|
+
|
|
208
|
+
const relationsToLoadIndividually = [
|
|
209
|
+
'replies',
|
|
210
|
+
'replies.member',
|
|
211
|
+
'replies.count.likes',
|
|
212
|
+
'replies.count.liked'
|
|
213
|
+
].filter(relation => withRelated.includes(relation));
|
|
214
|
+
|
|
215
|
+
const result = await ghostBookshelf.Model.findPage.call(this, options);
|
|
216
|
+
|
|
217
|
+
for (const model of result.data) {
|
|
218
|
+
await model.load(relationsToLoadIndividually, _.omit(options, 'withRelated'));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return result;
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
countRelations() {
|
|
225
|
+
return {
|
|
226
|
+
replies(modelOrCollection) {
|
|
227
|
+
modelOrCollection.query('columns', 'comments.*', (qb) => {
|
|
228
|
+
qb.count('replies.id')
|
|
229
|
+
.from('comments AS replies')
|
|
230
|
+
.whereRaw('replies.parent_id = comments.id')
|
|
231
|
+
.as('count__replies');
|
|
232
|
+
});
|
|
233
|
+
},
|
|
234
|
+
likes(modelOrCollection) {
|
|
235
|
+
modelOrCollection.query('columns', 'comments.*', (qb) => {
|
|
236
|
+
qb.count('comment_likes.id')
|
|
237
|
+
.from('comment_likes')
|
|
238
|
+
.whereRaw('comment_likes.comment_id = comments.id')
|
|
239
|
+
.as('count__likes');
|
|
240
|
+
});
|
|
241
|
+
},
|
|
242
|
+
liked(modelOrCollection, options) {
|
|
243
|
+
modelOrCollection.query('columns', 'comments.*', (qb) => {
|
|
244
|
+
if (options.context && options.context.member && options.context.member.id) {
|
|
245
|
+
qb.count('comment_likes.id')
|
|
246
|
+
.from('comment_likes')
|
|
247
|
+
.whereRaw('comment_likes.comment_id = comments.id')
|
|
248
|
+
.where('comment_likes.member_id', options.context.member.id)
|
|
249
|
+
.as('count__liked');
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Return zero
|
|
254
|
+
qb.select(ghostBookshelf.knex.raw('0')).as('count__liked');
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Returns an array of keys permitted in a method's `options` hash, depending on the current method.
|
|
262
|
+
* @param {String} methodName The name of the method to check valid options for.
|
|
263
|
+
* @return {Array} Keys allowed in the `options` hash of the model's method.
|
|
264
|
+
*/
|
|
265
|
+
permittedOptions: function permittedOptions(methodName) {
|
|
266
|
+
let options = ghostBookshelf.Model.permittedOptions.call(this, methodName);
|
|
267
|
+
|
|
268
|
+
// The comment model additionally supports having a parentId option
|
|
269
|
+
options.push('parentId');
|
|
270
|
+
|
|
190
271
|
return options;
|
|
191
272
|
}
|
|
192
273
|
});
|
|
@@ -107,6 +107,20 @@ Label = ghostBookshelf.Model.extend({
|
|
|
107
107
|
return options;
|
|
108
108
|
},
|
|
109
109
|
|
|
110
|
+
countRelations() {
|
|
111
|
+
return {
|
|
112
|
+
members(modelOrCollection) {
|
|
113
|
+
modelOrCollection.query('columns', 'labels.*', (qb) => {
|
|
114
|
+
qb.count('members.id')
|
|
115
|
+
.from('members')
|
|
116
|
+
.leftOuterJoin('members_labels', 'members.id', 'members_labels.member_id')
|
|
117
|
+
.whereRaw('members_labels.label_id = labels.id')
|
|
118
|
+
.as('count__members');
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
|
|
110
124
|
destroy: function destroy(unfilteredOptions) {
|
|
111
125
|
const options = this.filterOptions(unfilteredOptions, 'destroy', {extraAllowedProperties: ['id']});
|
|
112
126
|
options.withRelated = ['members'];
|
|
@@ -126,6 +126,27 @@ const Newsletter = ghostBookshelf.Model.extend({
|
|
|
126
126
|
return options;
|
|
127
127
|
},
|
|
128
128
|
|
|
129
|
+
countRelations() {
|
|
130
|
+
return {
|
|
131
|
+
posts(modelOrCollection) {
|
|
132
|
+
modelOrCollection.query('columns', 'newsletters.*', (qb) => {
|
|
133
|
+
qb.count('posts.id')
|
|
134
|
+
.from('posts')
|
|
135
|
+
.whereRaw('posts.newsletter_id = newsletters.id')
|
|
136
|
+
.as('count__posts');
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
members(modelOrCollection) {
|
|
140
|
+
modelOrCollection.query('columns', 'newsletters.*', (qb) => {
|
|
141
|
+
qb.count('members_newsletters.id')
|
|
142
|
+
.from('members_newsletters')
|
|
143
|
+
.whereRaw('members_newsletters.newsletter_id = newsletters.id')
|
|
144
|
+
.as('count__members');
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
},
|
|
149
|
+
|
|
129
150
|
orderDefaultRaw: function () {
|
|
130
151
|
return 'sort_order ASC, created_at ASC, id ASC';
|
|
131
152
|
},
|
|
@@ -182,6 +182,26 @@ Tag = ghostBookshelf.Model.extend({
|
|
|
182
182
|
return options;
|
|
183
183
|
},
|
|
184
184
|
|
|
185
|
+
countRelations() {
|
|
186
|
+
return {
|
|
187
|
+
posts(modelOrCollection, options) {
|
|
188
|
+
modelOrCollection.query('columns', 'tags.*', (qb) => {
|
|
189
|
+
qb.count('posts.id')
|
|
190
|
+
.from('posts')
|
|
191
|
+
.leftOuterJoin('posts_tags', 'posts.id', 'posts_tags.post_id')
|
|
192
|
+
.whereRaw('posts_tags.tag_id = tags.id')
|
|
193
|
+
.as('count__posts');
|
|
194
|
+
|
|
195
|
+
if (options.context && options.context.public) {
|
|
196
|
+
// @TODO use the filter behavior for posts
|
|
197
|
+
qb.andWhere('posts.type', '=', 'post');
|
|
198
|
+
qb.andWhere('posts.status', '=', 'published');
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
},
|
|
204
|
+
|
|
185
205
|
destroy: function destroy(unfilteredOptions) {
|
|
186
206
|
const options = this.filterOptions(unfilteredOptions, 'destroy', {extraAllowedProperties: ['id']});
|
|
187
207
|
options.withRelated = ['posts'];
|
|
@@ -426,6 +426,26 @@ User = ghostBookshelf.Model.extend({
|
|
|
426
426
|
return permittedOptionsToReturn;
|
|
427
427
|
},
|
|
428
428
|
|
|
429
|
+
countRelations() {
|
|
430
|
+
return {
|
|
431
|
+
posts(modelOrCollection, options) {
|
|
432
|
+
modelOrCollection.query('columns', 'users.*', (qb) => {
|
|
433
|
+
qb.count('posts.id')
|
|
434
|
+
.from('posts')
|
|
435
|
+
.join('posts_authors', 'posts.id', 'posts_authors.post_id')
|
|
436
|
+
.whereRaw('posts_authors.author_id = users.id')
|
|
437
|
+
.as('count__posts');
|
|
438
|
+
|
|
439
|
+
if (options.context && options.context.public) {
|
|
440
|
+
// @TODO use the filter behavior for posts
|
|
441
|
+
qb.andWhere('posts.type', '=', 'post');
|
|
442
|
+
qb.andWhere('posts.status', '=', 'published');
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
},
|
|
448
|
+
|
|
429
449
|
/**
|
|
430
450
|
* ### Find One
|
|
431
451
|
*
|
|
@@ -127,11 +127,7 @@ const authenticateWithToken = async (req, res, next, {token, JWT_OPTIONS}) => {
|
|
|
127
127
|
|
|
128
128
|
// CASE: blocking all non-internal: "custom" and "builtin" integration requests when the limit is reached
|
|
129
129
|
if (limitService.isLimited('customIntegrations')
|
|
130
|
-
&& (apiKey.relations.integration
|
|
131
|
-
&& !['internal'].includes(apiKey.relations.integration.get('type'))
|
|
132
|
-
&& !['ghost-explore'].includes(apiKey.relations.integration.get('slug'))
|
|
133
|
-
)
|
|
134
|
-
) {
|
|
130
|
+
&& (apiKey.relations.integration && !['internal', 'core'].includes(apiKey.relations.integration.get('type')))) {
|
|
135
131
|
// NOTE: using "checkWouldGoOverLimit" instead of "checkIsOverLimit" here because flag limits don't have
|
|
136
132
|
// a concept of measuring if the limit has been surpassed
|
|
137
133
|
await limitService.errorIfWouldGoOverLimit('customIntegrations');
|
|
@@ -43,11 +43,7 @@ const authenticateContentApiKey = async function authenticateContentApiKey(req,
|
|
|
43
43
|
|
|
44
44
|
// CASE: blocking all non-internal: "custom" and "builtin" integration requests when the limit is reached
|
|
45
45
|
if (limitService.isLimited('customIntegrations')
|
|
46
|
-
&& (apiKey.relations.integration
|
|
47
|
-
&& !['internal'].includes(apiKey.relations.integration.get('type'))
|
|
48
|
-
&& !['ghost-explore'].includes(apiKey.relations.integration.get('slug'))
|
|
49
|
-
)
|
|
50
|
-
) {
|
|
46
|
+
&& (apiKey.relations.integration && !['internal', 'core'].includes(apiKey.relations.integration.get('type')))) {
|
|
51
47
|
// NOTE: using "checkWouldGoOverLimit" instead of "checkIsOverLimit" here because flag limits don't have
|
|
52
48
|
// a concept of measuring if the limit has been surpassed
|
|
53
49
|
await limitService.errorIfWouldGoOverLimit('customIntegrations');
|
|
@@ -5,17 +5,19 @@ const errors = require('@tryghost/errors');
|
|
|
5
5
|
const tpl = require('@tryghost/tpl');
|
|
6
6
|
const logging = require('@tryghost/logging');
|
|
7
7
|
const models = require('../../models');
|
|
8
|
-
const
|
|
8
|
+
const MailgunClient = require('@tryghost/mailgun-client');
|
|
9
9
|
const sentry = require('../../../shared/sentry');
|
|
10
10
|
const labs = require('../../../shared/labs');
|
|
11
11
|
const debug = require('@tryghost/debug')('mega');
|
|
12
12
|
const postEmailSerializer = require('../mega/post-email-serializer');
|
|
13
|
+
const configService = require('../../../shared/config');
|
|
14
|
+
const settingsCache = require('../../../shared/settings-cache');
|
|
13
15
|
|
|
14
16
|
const messages = {
|
|
15
17
|
error: 'The email service was unable to send an email batch.'
|
|
16
18
|
};
|
|
17
19
|
|
|
18
|
-
const
|
|
20
|
+
const mailgunClient = new MailgunClient({config: configService, settings: settingsCache});
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* An object representing batch request result
|
|
@@ -64,7 +66,7 @@ class FailedBatch extends BatchResultBase {
|
|
|
64
66
|
*/
|
|
65
67
|
|
|
66
68
|
module.exports = {
|
|
67
|
-
BATCH_SIZE,
|
|
69
|
+
BATCH_SIZE: MailgunClient.BATCH_SIZE,
|
|
68
70
|
SuccessfulBatch,
|
|
69
71
|
FailedBatch,
|
|
70
72
|
|
|
@@ -211,11 +213,12 @@ module.exports = {
|
|
|
211
213
|
* @param {Email-like} emailData - The email to send, must be a POJO so emailModel.toJSON() before calling if needed
|
|
212
214
|
* @param {[EmailRecipient]} recipients - The recipients to send the email to with their associated data
|
|
213
215
|
* @param {string?} memberSegment - The member segment of the recipients
|
|
214
|
-
* @returns {Object} - {providerId: 'xxx'}
|
|
216
|
+
* @returns {Promise<Object>} - {providerId: 'xxx'}
|
|
215
217
|
*/
|
|
216
|
-
send(emailData, recipients, memberSegment) {
|
|
217
|
-
const
|
|
218
|
-
if (!
|
|
218
|
+
async send(emailData, recipients, memberSegment) {
|
|
219
|
+
const mailgunConfigured = mailgunClient.isConfigured();
|
|
220
|
+
if (!mailgunConfigured) {
|
|
221
|
+
logging.warn('Bulk email has not been configured');
|
|
219
222
|
return;
|
|
220
223
|
}
|
|
221
224
|
|
|
@@ -231,7 +234,7 @@ module.exports = {
|
|
|
231
234
|
// static data for every recipient
|
|
232
235
|
const data = {
|
|
233
236
|
unique_id: recipient.member_uuid,
|
|
234
|
-
unsubscribe_url: postEmailSerializer.createUnsubscribeUrl(recipient.member_uuid, newsletterUuid)
|
|
237
|
+
unsubscribe_url: postEmailSerializer.createUnsubscribeUrl(recipient.member_uuid, {newsletterUuid})
|
|
235
238
|
};
|
|
236
239
|
|
|
237
240
|
// computed properties on recipients - TODO: better way of handling these
|
|
@@ -247,10 +250,11 @@ module.exports = {
|
|
|
247
250
|
|
|
248
251
|
emailData = postEmailSerializer.renderEmailForSegment(emailData, memberSegment);
|
|
249
252
|
|
|
250
|
-
|
|
253
|
+
try {
|
|
254
|
+
const response = await mailgunClient.send(emailData, recipientData, replacements);
|
|
251
255
|
debug(`sent message (${Date.now() - startTime}ms)`);
|
|
252
256
|
return response;
|
|
253
|
-
}
|
|
257
|
+
} catch (error) {
|
|
254
258
|
// REF: possible mailgun errors https://documentation.mailgun.com/en/latest/api-intro.html#errors
|
|
255
259
|
let ghostError = new errors.EmailError({
|
|
256
260
|
err: error,
|
|
@@ -263,6 +267,9 @@ module.exports = {
|
|
|
263
267
|
|
|
264
268
|
debug(`failed to send message (${Date.now() - startTime}ms)`);
|
|
265
269
|
throw ghostError;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
// NOTE: for testing only!
|
|
274
|
+
_mailgunClient: mailgunClient
|
|
268
275
|
};
|
|
@@ -1,17 +1 @@
|
|
|
1
|
-
|
|
2
|
-
BATCH_SIZE,
|
|
3
|
-
SuccessfulBatch,
|
|
4
|
-
FailedBatch,
|
|
5
|
-
processEmail,
|
|
6
|
-
processEmailBatch,
|
|
7
|
-
send
|
|
8
|
-
} = require('./bulk-email-processor');
|
|
9
|
-
|
|
10
|
-
module.exports = {
|
|
11
|
-
BATCH_SIZE,
|
|
12
|
-
SuccessfulBatch,
|
|
13
|
-
FailedBatch,
|
|
14
|
-
processEmail,
|
|
15
|
-
processEmailBatch,
|
|
16
|
-
send
|
|
17
|
-
};
|
|
1
|
+
module.exports = require('./bulk-email-processor');
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const _ = require('lodash');
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* @typedef {import('../../api/shared/frame')} Frame
|
|
3
5
|
*/
|
|
@@ -26,6 +28,13 @@ module.exports = class CommentsController {
|
|
|
26
28
|
return this.service.getComments(frame.options);
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
/**
|
|
32
|
+
* @param {Frame} frame
|
|
33
|
+
*/
|
|
34
|
+
async replies(frame) {
|
|
35
|
+
return this.service.getReplies(frame.options.id, _.omit(frame.options, 'id'));
|
|
36
|
+
}
|
|
37
|
+
|
|
29
38
|
/**
|
|
30
39
|
* @param {Frame} frame
|
|
31
40
|
*/
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
border-bottom-color: #EEF5F8;
|
|
95
95
|
}
|
|
96
96
|
a {
|
|
97
|
-
color: #
|
|
97
|
+
color: #15212A;
|
|
98
98
|
}
|
|
99
99
|
</style>
|
|
100
100
|
</head>
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
<tr>
|
|
117
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
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;">Someone just replied to your comment on <a href="{{postUrl}}" target="_blank" style="font-weight: bold; text-decoration: underline;">{{postTitle}}</a>.</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;">Someone just replied to your comment on <a href="{{postUrl}}" target="_blank" style="font-weight: bold; text-decoration: underline; color: #15212A;">{{postTitle}}</a>.</p>
|
|
120
120
|
|
|
121
121
|
<table width="100" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; table-layout: fixed; width: 100%; min-width: 100%; box-sizing: border-box; background: #F9F9FA; border-radius: 7px;">
|
|
122
122
|
<tbody>
|
|
@@ -174,7 +174,7 @@
|
|
|
174
174
|
</tr>
|
|
175
175
|
<tr>
|
|
176
176
|
<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">
|
|
177
|
-
<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="{{profileUrl}}" style="text-decoration: underline; color: #738A94; font-size: 11px;">
|
|
177
|
+
<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="{{profileUrl}}" style="text-decoration: underline; color: #738A94; font-size: 11px;">Unsubscribe from comment reply notifications</a></p>
|
|
178
178
|
</td>
|
|
179
179
|
</tr>
|
|
180
180
|
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
border-bottom-color: #EEF5F8;
|
|
95
95
|
}
|
|
96
96
|
a {
|
|
97
|
-
color: #
|
|
97
|
+
color: #15212A;
|
|
98
98
|
}
|
|
99
99
|
</style>
|
|
100
100
|
</head>
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
<tr>
|
|
117
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
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;">Someone just left a comment on your post <a href="{{postUrl}}" target="_blank" style="font-weight: bold; text-decoration: underline;">{{postTitle}}</a>.</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;">Someone just left a comment on your post <a href="{{postUrl}}" target="_blank" style="font-weight: bold; text-decoration: underline; color: #15212A;">{{postTitle}}</a>.</p>
|
|
120
120
|
|
|
121
121
|
<table width="100" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; table-layout: fixed; width: 100%; min-width: 100%; box-sizing: border-box; background: #F9F9FA; border-radius: 7px;">
|
|
122
122
|
<tbody>
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
border-bottom-color: #EEF5F8;
|
|
95
95
|
}
|
|
96
96
|
a {
|
|
97
|
-
color: #
|
|
97
|
+
color: #15212A;
|
|
98
98
|
}
|
|
99
99
|
</style>
|
|
100
100
|
</head>
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
<tr>
|
|
117
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
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>
|
|
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" style="font-weight: bold; text-decoration: underline; color: #15212A;">{{postTitle}}</a>. This comment will remain visible until you choose to remove it, which can be done directly on the post.</p>
|
|
120
120
|
|
|
121
121
|
<table width="100" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; table-layout: fixed; width: 100%; min-width: 100%; box-sizing: border-box; background: #F9F9FA; border-radius: 7px;">
|
|
122
122
|
<tbody>
|
|
@@ -2,6 +2,7 @@ const {promises: fs} = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const moment = require('moment');
|
|
4
4
|
const htmlToPlaintext = require('@tryghost/html-to-plaintext');
|
|
5
|
+
const postEmailSerializer = require('../mega/post-email-serializer');
|
|
5
6
|
|
|
6
7
|
class CommentsServiceEmails {
|
|
7
8
|
constructor({config, logging, models, mailer, settingsCache, urlService, urlUtils}) {
|
|
@@ -93,7 +94,7 @@ class CommentsServiceEmails {
|
|
|
93
94
|
accentColor: this.settingsCache.get('accent_color'),
|
|
94
95
|
fromEmail: this.notificationFromAddress,
|
|
95
96
|
toEmail: to,
|
|
96
|
-
profileUrl:
|
|
97
|
+
profileUrl: postEmailSerializer.createUnsubscribeUrl(member.get('uuid'), {comments: true})
|
|
97
98
|
};
|
|
98
99
|
|
|
99
100
|
const {html, text} = await this.renderEmailTemplate('new-comment-reply', templateData);
|
|
@@ -115,7 +115,18 @@ class CommentsService {
|
|
|
115
115
|
*/
|
|
116
116
|
async getComments(options) {
|
|
117
117
|
this.checkEnabled();
|
|
118
|
-
const page = await this.models.Comment.findPage(options);
|
|
118
|
+
const page = await this.models.Comment.findPage({...options, parentId: null});
|
|
119
|
+
|
|
120
|
+
return page;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @param {string} id - The ID of the Comment to get replies from
|
|
125
|
+
* @param {any} options
|
|
126
|
+
*/
|
|
127
|
+
async getReplies(id, options) {
|
|
128
|
+
this.checkEnabled();
|
|
129
|
+
const page = await this.models.Comment.findPage({...options, parentId: id});
|
|
119
130
|
|
|
120
131
|
return page;
|
|
121
132
|
}
|
|
@@ -181,7 +192,8 @@ class CommentsService {
|
|
|
181
192
|
commentId: model.id
|
|
182
193
|
}));
|
|
183
194
|
|
|
184
|
-
|
|
195
|
+
// Instead of returning the model, fetch it again, so we have all the relations properly fetched
|
|
196
|
+
return await this.models.Comment.findOne({id: model.id}, {...options, require: true});
|
|
185
197
|
}
|
|
186
198
|
|
|
187
199
|
/**
|
|
@@ -240,7 +252,8 @@ class CommentsService {
|
|
|
240
252
|
commentId: model.id
|
|
241
253
|
}));
|
|
242
254
|
|
|
243
|
-
|
|
255
|
+
// Instead of returning the model, fetch it again, so we have all the relations properly fetched
|
|
256
|
+
return await this.models.Comment.findOne({id: model.id}, {...options, require: true});
|
|
244
257
|
}
|
|
245
258
|
|
|
246
259
|
/**
|