ghost 5.18.0 → 5.19.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/README.md +1 -1
- package/components/tryghost-adapter-manager-5.19.0.tgz +0 -0
- package/components/{tryghost-api-framework-5.18.0.tgz → tryghost-api-framework-5.19.0.tgz} +0 -0
- package/components/tryghost-api-version-compatibility-service-5.19.0.tgz +0 -0
- package/components/tryghost-audience-feedback-5.19.0.tgz +0 -0
- package/components/tryghost-bootstrap-socket-5.19.0.tgz +0 -0
- package/components/tryghost-constants-5.19.0.tgz +0 -0
- package/components/{tryghost-custom-theme-settings-service-5.18.0.tgz → tryghost-custom-theme-settings-service-5.19.0.tgz} +0 -0
- package/components/tryghost-domain-events-5.19.0.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.19.0.tgz +0 -0
- package/components/tryghost-email-analytics-service-5.19.0.tgz +0 -0
- package/components/tryghost-email-content-generator-5.19.0.tgz +0 -0
- package/components/tryghost-express-dynamic-redirects-5.19.0.tgz +0 -0
- package/components/tryghost-extract-api-key-5.19.0.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.19.0.tgz +0 -0
- package/components/{tryghost-job-manager-5.18.0.tgz → tryghost-job-manager-5.19.0.tgz} +0 -0
- package/components/tryghost-link-redirects-5.19.0.tgz +0 -0
- package/components/tryghost-link-replacer-5.19.0.tgz +0 -0
- package/components/tryghost-link-tracking-5.19.0.tgz +0 -0
- package/components/{tryghost-magic-link-5.18.0.tgz → tryghost-magic-link-5.19.0.tgz} +0 -0
- package/components/tryghost-mailgun-client-5.19.0.tgz +0 -0
- package/components/tryghost-member-analytics-service-5.19.0.tgz +0 -0
- package/components/tryghost-member-attribution-5.19.0.tgz +0 -0
- package/components/tryghost-member-events-5.19.0.tgz +0 -0
- package/components/tryghost-members-analytics-ingress-5.19.0.tgz +0 -0
- package/components/tryghost-members-api-5.19.0.tgz +0 -0
- package/components/tryghost-members-csv-5.19.0.tgz +0 -0
- package/components/tryghost-members-events-service-5.19.0.tgz +0 -0
- package/components/tryghost-members-importer-5.19.0.tgz +0 -0
- package/components/tryghost-members-offers-5.19.0.tgz +0 -0
- package/components/tryghost-members-payments-5.19.0.tgz +0 -0
- package/components/tryghost-members-ssr-5.19.0.tgz +0 -0
- package/components/tryghost-members-stripe-service-5.19.0.tgz +0 -0
- package/components/tryghost-minifier-5.19.0.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.19.0.tgz +0 -0
- package/components/tryghost-mw-cache-control-5.19.0.tgz +0 -0
- package/components/{tryghost-mw-error-handler-5.18.0.tgz → tryghost-mw-error-handler-5.19.0.tgz} +0 -0
- package/components/tryghost-mw-session-from-token-5.19.0.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.19.0.tgz +0 -0
- package/components/tryghost-mw-vhost-5.19.0.tgz +0 -0
- package/components/tryghost-oembed-service-5.19.0.tgz +0 -0
- package/components/tryghost-package-json-5.19.0.tgz +0 -0
- package/components/tryghost-referrers-5.19.0.tgz +0 -0
- package/components/tryghost-security-5.19.0.tgz +0 -0
- package/components/tryghost-session-service-5.19.0.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.19.0.tgz +0 -0
- package/components/tryghost-staff-service-5.19.0.tgz +0 -0
- package/components/tryghost-stats-service-5.19.0.tgz +0 -0
- package/components/tryghost-update-check-service-5.19.0.tgz +0 -0
- package/components/tryghost-verification-trigger-5.19.0.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-5.19.0.tgz +0 -0
- package/core/boot.js +3 -1
- package/core/built/admin/assets/{chunk.143.6d23a3157dae7a9c899d.js → chunk.143.eaf838fbf1470f018bf3.js} +6 -6
- package/core/built/admin/assets/{chunk.174.e997dfffceeaa0ce4636.js → chunk.174.3a133d51d9b45097c101.js} +31 -31
- package/core/built/admin/assets/{chunk.178.52a9ca26217a593eda67.js → chunk.178.44dae8a74f7f9d606e06.js} +4 -4
- package/core/built/admin/assets/{chunk.427.4483d5bbdaf2a65888ac.js → chunk.613.f1d519ad47e7f9024263.js} +40 -47
- package/core/built/admin/assets/{chunk.427.4483d5bbdaf2a65888ac.js.LICENSE.txt → chunk.613.f1d519ad47e7f9024263.js.LICENSE.txt} +0 -0
- package/core/built/admin/assets/{ghost-7e6e9479705e7e772bb5ea3b6476cd52.js → ghost-5ce6f5a730c83c91fc258b12c537ea35.js} +555 -559
- package/core/built/admin/assets/{ghost-ff0bee94743aa886ce35305a5b46fac3.css → ghost-982146a4ada3a5af1981d1919ae01d08.css} +1 -1
- package/core/built/admin/assets/{ghost-dark-a41f7645a406e0df78b7152f3f805e66.css → ghost-dark-41929e4857de411a23597a9de49a4e4f.css} +1 -1
- package/core/built/admin/assets/{vendor-4da5d2584fbe1442e25e4271a5513f1c.js → vendor-5c7d7063620bec13668c4370145cd4b4.js} +41 -34
- package/core/built/admin/index.html +6 -6
- package/core/frontend/helpers/t.js +12 -0
- package/core/frontend/helpers/tpl/content-cta.hbs +1 -1
- package/core/frontend/public/robots.txt +1 -0
- package/core/frontend/services/sitemap/handler.js +1 -1
- package/core/frontend/services/sitemap/index-generator.js +1 -3
- package/core/server/api/endpoints/feedback-members.js +23 -0
- package/core/server/api/endpoints/index.js +5 -1
- package/core/server/api/endpoints/utils/serializers/input/posts.js +6 -1
- package/core/server/api/endpoints/utils/serializers/output/mappers/posts.js +8 -0
- package/core/server/data/exporter/table-lists.js +3 -1
- package/core/server/data/importer/handlers/json.js +21 -23
- package/core/server/data/importer/importers/data/base.js +1 -1
- package/core/server/data/migrations/versions/4.0/05-add-members-subscribe-events-table.js +1 -1
- package/core/server/data/migrations/versions/4.0/06-populate-members-subscribe-events-table.js +1 -1
- package/core/server/data/migrations/versions/4.0/11-add-members-paid-subscription-events-table.js +1 -1
- package/core/server/data/migrations/versions/4.0/13-add-members-payment-events-table.js +1 -1
- package/core/server/data/migrations/versions/4.0/17-populate-members-status-events-table.js +1 -1
- package/core/server/data/migrations/versions/4.0/22-solve-orphaned-webhooks.js +1 -1
- package/core/server/data/migrations/versions/4.0/25-populate-members-paid-subscription-events-table.js +1 -1
- package/core/server/data/migrations/versions/4.11/02-add-email-verification-required-setting.js +1 -1
- package/core/server/data/migrations/versions/4.12/01-add-email-only-column-to-posts-meta-table.js +1 -1
- package/core/server/data/migrations/versions/4.3/03-add-default-product.js +1 -1
- package/core/server/data/migrations/versions/4.3/04-attach-members-to-product.js +1 -1
- package/core/server/data/migrations/versions/4.3/06-add-stripe-prices-table.js +2 -2
- package/core/server/data/migrations/versions/4.3/08-migrate-members-signup-setting.js +1 -1
- package/core/server/data/migrations/versions/4.33/2022-01-14-11-51-add-default-free-tier.js +1 -1
- package/core/server/data/migrations/versions/4.42/2022-03-21-17-17-add.js +2 -2
- package/core/server/data/migrations/versions/4.43/2022-03-28-19-26-recreate-newsletter-table.js +5 -5
- package/core/server/data/migrations/versions/4.46/2022-04-13-13-00-add-default-newsletter.js +1 -1
- package/core/server/data/migrations/versions/4.46/2022-04-20-08-39-map-subscribers-to-default-newsletter.js +1 -1
- package/core/server/data/migrations/versions/4.7/03-add-labs-setting.js +1 -1
- package/core/server/data/migrations/versions/4.8/03-add-default-product-portal-products.js +1 -1
- package/core/server/data/migrations/versions/4.8/04-migrate-show-newsletter-header-setting.js +1 -1
- package/core/server/data/migrations/versions/5.0/2022-05-06-13-22-add-frontend-integration.js +1 -1
- package/core/server/data/migrations/versions/5.17/2022-09-29-12-39-add-track-clicks-column-to-emails.js +2 -2
- package/core/server/data/migrations/versions/5.19/2022-09-02-20-25-add-columns-to-products-table.js +19 -0
- package/core/server/data/migrations/versions/5.19/2022-09-02-20-52-backfill-new-product-columns.js +37 -0
- package/core/server/data/migrations/versions/5.19/2022-10-10-06-58-add-subscriptions-table.js +19 -0
- package/core/server/data/migrations/versions/5.19/2022-10-10-10-05-add-members-feedback-table.js +10 -0
- package/core/server/data/migrations/versions/5.19/2022-10-11-10-38-add-feedback-enabled-column-to-newsletters.js +7 -0
- package/core/server/data/schema/commands.js +3 -3
- package/core/server/data/schema/fixtures/fixtures.json +4 -1
- package/core/server/data/schema/schema.js +90 -24
- package/core/server/data/schema/validator.js +1 -1
- package/core/server/models/base/bookshelf.js +3 -4
- package/core/server/models/base/plugins/data-manipulation.js +1 -1
- package/core/server/models/base/plugins/events.js +1 -1
- package/core/server/models/base/utils.js +1 -1
- package/core/server/models/member-feedback.js +22 -0
- package/core/server/models/newsletter.js +3 -2
- package/core/server/models/post.js +24 -0
- package/core/server/models/settings.js +1 -1
- package/core/server/models/user.js +1 -1
- package/core/server/services/audience-feedback/FeedbackRepository.js +67 -0
- package/core/server/services/audience-feedback/index.js +33 -0
- package/core/server/services/bulk-email/bulk-email-processor.js +7 -1
- package/core/server/services/mail/GhostMailer.js +17 -1
- package/core/server/services/mega/feedback-buttons.js +69 -0
- package/core/server/services/mega/mega.js +1 -1
- package/core/server/services/mega/post-email-serializer.js +24 -4
- package/core/server/services/mega/template.js +3 -0
- package/core/server/services/members/middleware.js +40 -0
- package/core/server/services/notifications/notifications.js +1 -1
- package/core/server/services/settings/settings-service.js +1 -1
- package/core/server/services/themes/storage.js +1 -1
- package/core/server/web/members/app.js +12 -0
- package/core/shared/labs.js +4 -2
- package/package.json +110 -110
- package/yarn.lock +496 -432
- package/components/tryghost-adapter-manager-5.18.0.tgz +0 -0
- package/components/tryghost-api-version-compatibility-service-5.18.0.tgz +0 -0
- package/components/tryghost-bootstrap-socket-5.18.0.tgz +0 -0
- package/components/tryghost-constants-5.18.0.tgz +0 -0
- package/components/tryghost-domain-events-5.18.0.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.18.0.tgz +0 -0
- package/components/tryghost-email-analytics-service-5.18.0.tgz +0 -0
- package/components/tryghost-email-content-generator-5.18.0.tgz +0 -0
- package/components/tryghost-express-dynamic-redirects-5.18.0.tgz +0 -0
- package/components/tryghost-extract-api-key-5.18.0.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.18.0.tgz +0 -0
- package/components/tryghost-link-redirects-5.18.0.tgz +0 -0
- package/components/tryghost-link-replacer-5.18.0.tgz +0 -0
- package/components/tryghost-link-tracking-5.18.0.tgz +0 -0
- package/components/tryghost-mailgun-client-5.18.0.tgz +0 -0
- package/components/tryghost-member-analytics-service-5.18.0.tgz +0 -0
- package/components/tryghost-member-attribution-5.18.0.tgz +0 -0
- package/components/tryghost-member-events-5.18.0.tgz +0 -0
- package/components/tryghost-members-analytics-ingress-5.18.0.tgz +0 -0
- package/components/tryghost-members-api-5.18.0.tgz +0 -0
- package/components/tryghost-members-csv-5.18.0.tgz +0 -0
- package/components/tryghost-members-events-service-5.18.0.tgz +0 -0
- package/components/tryghost-members-importer-5.18.0.tgz +0 -0
- package/components/tryghost-members-offers-5.18.0.tgz +0 -0
- package/components/tryghost-members-payments-5.18.0.tgz +0 -0
- package/components/tryghost-members-ssr-5.18.0.tgz +0 -0
- package/components/tryghost-members-stripe-service-5.18.0.tgz +0 -0
- package/components/tryghost-minifier-5.18.0.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.18.0.tgz +0 -0
- package/components/tryghost-mw-cache-control-5.18.0.tgz +0 -0
- package/components/tryghost-mw-session-from-token-5.18.0.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.18.0.tgz +0 -0
- package/components/tryghost-mw-vhost-5.18.0.tgz +0 -0
- package/components/tryghost-oembed-service-5.18.0.tgz +0 -0
- package/components/tryghost-package-json-5.18.0.tgz +0 -0
- package/components/tryghost-referrers-5.18.0.tgz +0 -0
- package/components/tryghost-security-5.18.0.tgz +0 -0
- package/components/tryghost-session-service-5.18.0.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.18.0.tgz +0 -0
- package/components/tryghost-staff-service-5.18.0.tgz +0 -0
- package/components/tryghost-stats-service-5.18.0.tgz +0 -0
- package/components/tryghost-update-check-service-5.18.0.tgz +0 -0
- package/components/tryghost-verification-trigger-5.18.0.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-5.18.0.tgz +0 -0
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
const _ = require('lodash');
|
|
2
2
|
const bookshelf = require('bookshelf');
|
|
3
|
-
const ObjectId = require('bson-objectid');
|
|
3
|
+
const ObjectId = require('bson-objectid').default;
|
|
4
4
|
const plugins = require('@tryghost/bookshelf-plugins');
|
|
5
|
-
const Promise = require('bluebird');
|
|
6
5
|
|
|
7
6
|
const db = require('../../data/db');
|
|
8
7
|
|
|
@@ -84,13 +83,13 @@ ghostBookshelf.plugin('bookshelf-relations', {
|
|
|
84
83
|
return Promise.resolve();
|
|
85
84
|
}
|
|
86
85
|
|
|
87
|
-
return Promise.
|
|
86
|
+
return Promise.all(targets.models.map((target, index) => {
|
|
88
87
|
queryOptions.query.where[existing.relatedData.otherKey] = target.id;
|
|
89
88
|
|
|
90
89
|
return existing.updatePivot({
|
|
91
90
|
sort_order: index
|
|
92
91
|
}, _.extend({}, options, queryOptions));
|
|
93
|
-
});
|
|
92
|
+
}));
|
|
94
93
|
},
|
|
95
94
|
beforeRelationCreation: function onCreatingRelation(model, data) {
|
|
96
95
|
data.id = ObjectId().toHexString();
|
|
@@ -78,7 +78,7 @@ module.exports = function (Bookshelf) {
|
|
|
78
78
|
_.each(attrs, function each(value, key) {
|
|
79
79
|
const tableDef = schema.tables[self.tableName];
|
|
80
80
|
const columnDef = tableDef ? tableDef[key] : null;
|
|
81
|
-
if (columnDef
|
|
81
|
+
if (columnDef?.type === 'boolean') {
|
|
82
82
|
attrs[key] = value ? true : false;
|
|
83
83
|
}
|
|
84
84
|
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const errors = require('@tryghost/errors');
|
|
2
|
+
const ghostBookshelf = require('./base');
|
|
3
|
+
|
|
4
|
+
const MemberFeedback = ghostBookshelf.Model.extend({
|
|
5
|
+
tableName: 'members_feedback',
|
|
6
|
+
|
|
7
|
+
post() {
|
|
8
|
+
return this.belongsTo('Post', 'post_id', 'id');
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
member() {
|
|
12
|
+
return this.belongsTo('Member', 'member_id', 'id');
|
|
13
|
+
}
|
|
14
|
+
}, {
|
|
15
|
+
async destroy() {
|
|
16
|
+
throw new errors.IncorrectUsageError({message: 'Cannot destroy MemberFeedback'});
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
module.exports = {
|
|
21
|
+
MemberFeedback: ghostBookshelf.model('MemberFeedback', MemberFeedback)
|
|
22
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const ghostBookshelf = require('./base');
|
|
2
|
-
const ObjectID = require('bson-objectid');
|
|
2
|
+
const ObjectID = require('bson-objectid').default;
|
|
3
3
|
const uuid = require('uuid');
|
|
4
4
|
const urlUtils = require('../../shared/url-utils');
|
|
5
5
|
|
|
@@ -21,7 +21,8 @@ const Newsletter = ghostBookshelf.Model.extend({
|
|
|
21
21
|
show_badge: true,
|
|
22
22
|
show_header_icon: true,
|
|
23
23
|
show_header_title: true,
|
|
24
|
-
show_header_name: true
|
|
24
|
+
show_header_name: true,
|
|
25
|
+
feedback_enabled: false
|
|
25
26
|
};
|
|
26
27
|
},
|
|
27
28
|
|
|
@@ -1354,6 +1354,30 @@ Post = ghostBookshelf.Model.extend({
|
|
|
1354
1354
|
.whereRaw('posts.id = redirects.post_id')
|
|
1355
1355
|
.as('count__clicks');
|
|
1356
1356
|
});
|
|
1357
|
+
},
|
|
1358
|
+
sentiment(modelOrCollection) {
|
|
1359
|
+
modelOrCollection.query('columns', 'posts.*', (qb) => {
|
|
1360
|
+
qb.select(qb.client.raw('ROUND(AVG(score) * 100)'))
|
|
1361
|
+
.from('members_feedback')
|
|
1362
|
+
.whereRaw('posts.id = members_feedback.post_id')
|
|
1363
|
+
.as('count__sentiment');
|
|
1364
|
+
});
|
|
1365
|
+
},
|
|
1366
|
+
negative_feedback(modelOrCollection) {
|
|
1367
|
+
modelOrCollection.query('columns', 'posts.*', (qb) => {
|
|
1368
|
+
qb.count('*')
|
|
1369
|
+
.from('members_feedback')
|
|
1370
|
+
.whereRaw('posts.id = members_feedback.post_id AND members_feedback.score = 0')
|
|
1371
|
+
.as('count__positive_feedback');
|
|
1372
|
+
});
|
|
1373
|
+
},
|
|
1374
|
+
positive_feedback(modelOrCollection) {
|
|
1375
|
+
modelOrCollection.query('columns', 'posts.*', (qb) => {
|
|
1376
|
+
qb.sum('score')
|
|
1377
|
+
.from('members_feedback')
|
|
1378
|
+
.whereRaw('posts.id = members_feedback.post_id')
|
|
1379
|
+
.as('count__positive_feedback');
|
|
1380
|
+
});
|
|
1357
1381
|
}
|
|
1358
1382
|
};
|
|
1359
1383
|
}
|
|
@@ -3,7 +3,7 @@ const _ = require('lodash');
|
|
|
3
3
|
const uuid = require('uuid');
|
|
4
4
|
const crypto = require('crypto');
|
|
5
5
|
const keypair = require('keypair');
|
|
6
|
-
const ObjectID = require('bson-objectid');
|
|
6
|
+
const ObjectID = require('bson-objectid').default;
|
|
7
7
|
const ghostBookshelf = require('./base');
|
|
8
8
|
const tpl = require('@tryghost/tpl');
|
|
9
9
|
const errors = require('@tryghost/errors');
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const _ = require('lodash');
|
|
2
2
|
const Promise = require('bluebird');
|
|
3
3
|
const validator = require('@tryghost/validator');
|
|
4
|
-
const ObjectId = require('bson-objectid');
|
|
4
|
+
const ObjectId = require('bson-objectid').default;
|
|
5
5
|
const ghostBookshelf = require('./base');
|
|
6
6
|
const baseUtils = require('./base/utils');
|
|
7
7
|
const limitService = require('../services/limits');
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
module.exports = class FeedbackRepository {
|
|
2
|
+
/** @type {object} */
|
|
3
|
+
#Member;
|
|
4
|
+
|
|
5
|
+
/** @type {object} */
|
|
6
|
+
#Post;
|
|
7
|
+
|
|
8
|
+
/** @type {object} */
|
|
9
|
+
#MemberFeedback;
|
|
10
|
+
|
|
11
|
+
/** @type {typeof Object} */
|
|
12
|
+
#Feedback;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {object} deps
|
|
16
|
+
* @param {object} deps.Member Bookshelf Model
|
|
17
|
+
* @param {object} deps.Post Bookshelf Model
|
|
18
|
+
* @param {object} deps.MemberFeedback Bookshelf Model
|
|
19
|
+
* @param {object} deps.Feedback Feedback object
|
|
20
|
+
*/
|
|
21
|
+
constructor(deps) {
|
|
22
|
+
this.#Member = deps.Member;
|
|
23
|
+
this.#Post = deps.Post;
|
|
24
|
+
this.#MemberFeedback = deps.MemberFeedback;
|
|
25
|
+
this.#Feedback = deps.Feedback;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async add(feedback) {
|
|
29
|
+
await this.#MemberFeedback.add({
|
|
30
|
+
id: feedback.id.toHexString(),
|
|
31
|
+
member_id: feedback.memberId.toHexString(),
|
|
32
|
+
post_id: feedback.postId.toHexString(),
|
|
33
|
+
score: feedback.score
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async edit(feedback) {
|
|
38
|
+
await this.#MemberFeedback.edit({
|
|
39
|
+
score: feedback.score
|
|
40
|
+
}, {
|
|
41
|
+
id: feedback.id.toHexString()
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async get(postId, memberId) {
|
|
46
|
+
const model = await this.#MemberFeedback.findOne({member_id: memberId, post_id: postId}, {require: false});
|
|
47
|
+
|
|
48
|
+
if (!model) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return new this.#Feedback({
|
|
53
|
+
id: model.id,
|
|
54
|
+
memberId: model.get('member_id'),
|
|
55
|
+
postId: model.get('post_id'),
|
|
56
|
+
score: model.get('score')
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async getMemberByUuid(uuid) {
|
|
61
|
+
return await this.#Member.findOne({uuid});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async getPostById(id) {
|
|
65
|
+
return await this.#Post.findOne({id});
|
|
66
|
+
}
|
|
67
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const urlUtils = require('../../../shared/url-utils');
|
|
2
|
+
const FeedbackRepository = require('./FeedbackRepository');
|
|
3
|
+
|
|
4
|
+
class AudienceFeedbackServiceWrapper {
|
|
5
|
+
async init() {
|
|
6
|
+
if (this.service) {
|
|
7
|
+
// Already done
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Wire up all the dependencies
|
|
12
|
+
const models = require('../../models');
|
|
13
|
+
|
|
14
|
+
const {AudienceFeedbackService, AudienceFeedbackController, Feedback} = require('@tryghost/audience-feedback');
|
|
15
|
+
|
|
16
|
+
this.repository = new FeedbackRepository({
|
|
17
|
+
Member: models.Member,
|
|
18
|
+
MemberFeedback: models.MemberFeedback,
|
|
19
|
+
Feedback,
|
|
20
|
+
Post: models.Post
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Expose the service
|
|
24
|
+
this.service = new AudienceFeedbackService({
|
|
25
|
+
config: {
|
|
26
|
+
baseURL: new URL(urlUtils.urlFor('home', true))
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
this.controller = new AudienceFeedbackController({repository: this.repository});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = new AudienceFeedbackServiceWrapper();
|
|
@@ -11,6 +11,7 @@ const debug = require('@tryghost/debug')('mega');
|
|
|
11
11
|
const postEmailSerializer = require('../mega/post-email-serializer');
|
|
12
12
|
const configService = require('../../../shared/config');
|
|
13
13
|
const settingsCache = require('../../../shared/settings-cache');
|
|
14
|
+
const labs = require('../../../shared/labs');
|
|
14
15
|
|
|
15
16
|
const messages = {
|
|
16
17
|
error: 'The email service received an error from mailgun and was unable to send.'
|
|
@@ -208,7 +209,7 @@ module.exports = {
|
|
|
208
209
|
|
|
209
210
|
/**
|
|
210
211
|
* @param {Email-like} emailData - The email to send, must be a POJO so emailModel.toJSON() before calling if needed
|
|
211
|
-
* @param {[
|
|
212
|
+
* @param {EmailRecipient[]} recipients - The recipients to send the email to with their associated data
|
|
212
213
|
* @param {string?} memberSegment - The member segment of the recipients
|
|
213
214
|
* @returns {Promise<Object>} - {providerId: 'xxx'}
|
|
214
215
|
*/
|
|
@@ -234,6 +235,11 @@ module.exports = {
|
|
|
234
235
|
unsubscribe_url: postEmailSerializer.createUnsubscribeUrl(recipient.member_uuid, {newsletterUuid})
|
|
235
236
|
};
|
|
236
237
|
|
|
238
|
+
if (labs.isSet('audienceFeedback')) {
|
|
239
|
+
// create unique urls for every recipient (for example, for feedback buttons)
|
|
240
|
+
emailData = postEmailSerializer.createUserLinks(emailData, recipient.member_uuid);
|
|
241
|
+
}
|
|
242
|
+
|
|
237
243
|
// computed properties on recipients - TODO: better way of handling these
|
|
238
244
|
recipient.member_first_name = (recipient.member_name || '').split(' ')[0];
|
|
239
245
|
|
|
@@ -7,6 +7,7 @@ const errors = require('@tryghost/errors');
|
|
|
7
7
|
const tpl = require('@tryghost/tpl');
|
|
8
8
|
const settingsCache = require('../../../shared/settings-cache');
|
|
9
9
|
const urlUtils = require('../../../shared/url-utils');
|
|
10
|
+
const metrics = require('@tryghost/metrics');
|
|
10
11
|
const messages = {
|
|
11
12
|
title: 'Ghost at {domain}',
|
|
12
13
|
checkEmailConfigInstructions: 'Please see {url} for instructions on configuring email.',
|
|
@@ -83,7 +84,8 @@ module.exports = class GhostMailer {
|
|
|
83
84
|
const options = config.get('mail') && _.clone(config.get('mail').options) || {};
|
|
84
85
|
|
|
85
86
|
this.state = {
|
|
86
|
-
usingDirect: transport === 'direct'
|
|
87
|
+
usingDirect: transport === 'direct',
|
|
88
|
+
usingMailgun: transport === 'mailgun'
|
|
87
89
|
};
|
|
88
90
|
this.transport = nodemailer(transport, options);
|
|
89
91
|
}
|
|
@@ -121,10 +123,24 @@ module.exports = class GhostMailer {
|
|
|
121
123
|
}
|
|
122
124
|
|
|
123
125
|
async sendMail(message) {
|
|
126
|
+
const startTime = Date.now();
|
|
124
127
|
try {
|
|
125
128
|
const response = await this.transport.sendMail(message);
|
|
129
|
+
if (this.state.usingMailgun) {
|
|
130
|
+
metrics.metric('mailgun-send-transactional-mail', {
|
|
131
|
+
value: Date.now() - startTime,
|
|
132
|
+
statusCode: 200
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
126
136
|
return response;
|
|
127
137
|
} catch (err) {
|
|
138
|
+
if (this.state.usingMailgun) {
|
|
139
|
+
metrics.metric('mailgun-send-transactional-mail', {
|
|
140
|
+
value: Date.now() - startTime,
|
|
141
|
+
statusCode: err.status
|
|
142
|
+
});
|
|
143
|
+
}
|
|
128
144
|
throw createMailError({
|
|
129
145
|
message: tpl(messages.reason, {reason: err.message || err}),
|
|
130
146
|
err
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const {Color} = require('@tryghost/color-utils');
|
|
2
|
+
const audienceFeedback = require('../audience-feedback');
|
|
3
|
+
|
|
4
|
+
const templateStrings = {
|
|
5
|
+
like: '%{feedback_button_like}%',
|
|
6
|
+
dislike: '%{feedback_button_dislike}%'
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const generateLinks = (postId, uuid, html) => {
|
|
10
|
+
const positiveLink = audienceFeedback.service.buildLink(
|
|
11
|
+
uuid,
|
|
12
|
+
postId,
|
|
13
|
+
1
|
|
14
|
+
);
|
|
15
|
+
const negativeLink = audienceFeedback.service.buildLink(
|
|
16
|
+
uuid,
|
|
17
|
+
postId,
|
|
18
|
+
0
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
html = html.replace(templateStrings.like, positiveLink.href);
|
|
22
|
+
html = html.replace(templateStrings.dislike, negativeLink.href);
|
|
23
|
+
|
|
24
|
+
return html;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const getTemplate = (accentColor) => {
|
|
28
|
+
const likeButtonHtml = getButtonHtml(templateStrings.like, 'More like this', accentColor);
|
|
29
|
+
const dislikeButtonHtml = getButtonHtml(templateStrings.dislike, 'Less like this', accentColor);
|
|
30
|
+
|
|
31
|
+
return (`
|
|
32
|
+
<tr>
|
|
33
|
+
<td dir="ltr" width="100%" style="background-color: #ffffff; text-align: center; padding: 40px 4px; border-bottom: 1px solid #e5eff5" align="center">
|
|
34
|
+
<h3 style="text-align: center; margin-bottom: 22px; font-size: 17px; letter-spacing: -0.2px; margin-top: 0 !important;">What did you think of this post?</h3>
|
|
35
|
+
<table role="presentation" border="0" cellpadding="0" cellspacing="0" style="margin: auto; width: auto !important;">
|
|
36
|
+
<tr>
|
|
37
|
+
${likeButtonHtml}
|
|
38
|
+
${dislikeButtonHtml}
|
|
39
|
+
</tr>
|
|
40
|
+
</table>
|
|
41
|
+
</td>
|
|
42
|
+
</tr>
|
|
43
|
+
`);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function getButtonHtml(href, buttonText, accentColor) {
|
|
47
|
+
const color = new Color(accentColor);
|
|
48
|
+
const bgColor = `${accentColor}10`;
|
|
49
|
+
const textColor = color.darken(0.6).hex();
|
|
50
|
+
|
|
51
|
+
return (`
|
|
52
|
+
<td dir="ltr" valign="top" align="center" style="font-family: inherit; font-size: 14px; text-align: center;" nowrap>
|
|
53
|
+
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="width: auto !important;">
|
|
54
|
+
<tr>
|
|
55
|
+
<td style="padding: 0 6px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';">
|
|
56
|
+
<a href=${href} style="background-color: ${bgColor}; color: ${textColor}; border-radius: 22px; font-family: inherit; padding: 12px 20px; border: none; font-size: 14px; font-weight: bold; line-height: 100%; text-decoration: none; display: block;">
|
|
57
|
+
${buttonText}
|
|
58
|
+
</a>
|
|
59
|
+
</td>
|
|
60
|
+
</tr>
|
|
61
|
+
</table>
|
|
62
|
+
</td>
|
|
63
|
+
`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = {
|
|
67
|
+
generateLinks,
|
|
68
|
+
getTemplate
|
|
69
|
+
};
|
|
@@ -3,7 +3,7 @@ const Promise = require('bluebird');
|
|
|
3
3
|
const debug = require('@tryghost/debug')('mega');
|
|
4
4
|
const tpl = require('@tryghost/tpl');
|
|
5
5
|
const moment = require('moment');
|
|
6
|
-
const ObjectID = require('bson-objectid');
|
|
6
|
+
const ObjectID = require('bson-objectid').default;
|
|
7
7
|
const errors = require('@tryghost/errors');
|
|
8
8
|
const logging = require('@tryghost/logging');
|
|
9
9
|
const settingsCache = require('../../../shared/settings-cache');
|
|
@@ -16,11 +16,12 @@ const urlService = require('../../services/url');
|
|
|
16
16
|
const linkReplacer = require('@tryghost/link-replacer');
|
|
17
17
|
const linkTracking = require('../link-tracking');
|
|
18
18
|
const memberAttribution = require('../member-attribution');
|
|
19
|
+
const feedbackButtons = require('./feedback-buttons');
|
|
19
20
|
|
|
20
21
|
const ALLOWED_REPLACEMENTS = ['first_name', 'uuid'];
|
|
21
22
|
|
|
22
23
|
const PostEmailSerializer = {
|
|
23
|
-
|
|
24
|
+
|
|
24
25
|
// Format a full html document ready for email by inlining CSS, adjusting links,
|
|
25
26
|
// and performing any client-specific fixes
|
|
26
27
|
formatHtmlForEmail(html) {
|
|
@@ -107,6 +108,23 @@ const PostEmailSerializer = {
|
|
|
107
108
|
return signupUrl.href;
|
|
108
109
|
},
|
|
109
110
|
|
|
111
|
+
/**
|
|
112
|
+
* createUserLinks
|
|
113
|
+
*
|
|
114
|
+
* Generate personalised links for each user
|
|
115
|
+
*
|
|
116
|
+
* @param {string} memberUuid member uuid
|
|
117
|
+
* @param {Object} email
|
|
118
|
+
*/
|
|
119
|
+
createUserLinks(email, memberUuid) {
|
|
120
|
+
const result = {...email};
|
|
121
|
+
|
|
122
|
+
result.html = feedbackButtons.generateLinks(result.post.id, memberUuid, result.html);
|
|
123
|
+
result.plaintext = htmlToPlaintext.email(result.html);
|
|
124
|
+
|
|
125
|
+
return result;
|
|
126
|
+
},
|
|
127
|
+
|
|
110
128
|
// NOTE: serialization is needed to make sure we do post transformations such as image URL transformation from relative to absolute
|
|
111
129
|
async serializePostModel(model) {
|
|
112
130
|
// fetch mobiledoc rather than html and plaintext so we can render email-specific contents
|
|
@@ -206,6 +224,7 @@ const PostEmailSerializer = {
|
|
|
206
224
|
titleAlignment: newsletter.get('title_alignment'),
|
|
207
225
|
bodyFontCategory: newsletter.get('body_font_category'),
|
|
208
226
|
showBadge: newsletter.get('show_badge'),
|
|
227
|
+
feedbackEnabled: newsletter.get('feedback_enabled'),
|
|
209
228
|
footerContent: newsletter.get('footer_content'),
|
|
210
229
|
showHeaderName: newsletter.get('show_header_name'),
|
|
211
230
|
accentColor,
|
|
@@ -335,7 +354,7 @@ const PostEmailSerializer = {
|
|
|
335
354
|
plaintext: post.plaintext
|
|
336
355
|
};
|
|
337
356
|
|
|
338
|
-
/**
|
|
357
|
+
/**
|
|
339
358
|
* If a part of the email is members-only and the post is paid-only, add a paywall:
|
|
340
359
|
* - Just before sending the email, we'll hide the paywall or paid content depending on the member segment it is sent to.
|
|
341
360
|
* - We already need to do URL-replacement on the HTML here
|
|
@@ -369,7 +388,7 @@ const PostEmailSerializer = {
|
|
|
369
388
|
|
|
370
389
|
// Add link click tracking
|
|
371
390
|
url = await linkTracking.service.addTrackingToUrl(url, post, '--uuid--');
|
|
372
|
-
|
|
391
|
+
|
|
373
392
|
// We need to convert to a string at this point, because we need invalid string characters in the URL
|
|
374
393
|
const str = url.toString().replace(/--uuid--/g, '%%{uuid}%%');
|
|
375
394
|
return str;
|
|
@@ -490,7 +509,7 @@ const PostEmailSerializer = {
|
|
|
490
509
|
});
|
|
491
510
|
|
|
492
511
|
result.html = this.formatHtmlForEmail($.html());
|
|
493
|
-
result.plaintext = htmlToPlaintext.email(result.html);
|
|
512
|
+
result.plaintext = htmlToPlaintext.email(result.html);
|
|
494
513
|
delete result.post;
|
|
495
514
|
|
|
496
515
|
return result;
|
|
@@ -501,6 +520,7 @@ module.exports = {
|
|
|
501
520
|
serialize: PostEmailSerializer.serialize.bind(PostEmailSerializer),
|
|
502
521
|
createUnsubscribeUrl: PostEmailSerializer.createUnsubscribeUrl.bind(PostEmailSerializer),
|
|
503
522
|
createPostSignupUrl: PostEmailSerializer.createPostSignupUrl.bind(PostEmailSerializer),
|
|
523
|
+
createUserLinks: PostEmailSerializer.createUserLinks.bind(PostEmailSerializer),
|
|
504
524
|
renderEmailForSegment: PostEmailSerializer.renderEmailForSegment.bind(PostEmailSerializer),
|
|
505
525
|
parseReplacements: PostEmailSerializer.parseReplacements.bind(PostEmailSerializer),
|
|
506
526
|
// Export for tests
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const {escapeHtml: escape} = require('@tryghost/string');
|
|
2
|
+
const feedbackButtons = require('./feedback-buttons');
|
|
2
3
|
|
|
3
4
|
/* eslint indent: warn, no-irregular-whitespace: warn */
|
|
4
5
|
const iff = (cond, yes, no) => (cond ? yes : no);
|
|
@@ -1265,6 +1266,8 @@ ${ templateSettings.showBadge ? `
|
|
|
1265
1266
|
|
|
1266
1267
|
<!-- END MAIN CONTENT AREA -->
|
|
1267
1268
|
|
|
1269
|
+
${iff(templateSettings.feedbackEnabled, feedbackButtons.getTemplate(templateSettings.accentColor), '')}
|
|
1270
|
+
|
|
1268
1271
|
<tr>
|
|
1269
1272
|
<td class="wrapper" align="center">
|
|
1270
1273
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="padding-top: 40px; padding-bottom: 30px;">
|
|
@@ -5,6 +5,13 @@ const models = require('../../models');
|
|
|
5
5
|
const urlUtils = require('../../../shared/url-utils');
|
|
6
6
|
const spamPrevention = require('../../web/shared/middleware/api/spam-prevention');
|
|
7
7
|
const {formattedMemberResponse} = require('./utils');
|
|
8
|
+
const errors = require('@tryghost/errors');
|
|
9
|
+
const tpl = require('@tryghost/tpl');
|
|
10
|
+
|
|
11
|
+
const messages = {
|
|
12
|
+
missingUuid: 'Missing uuid.',
|
|
13
|
+
invalidUuid: 'Invalid uuid.'
|
|
14
|
+
};
|
|
8
15
|
|
|
9
16
|
// @TODO: This piece of middleware actually belongs to the frontend, not to the member app
|
|
10
17
|
// Need to figure a way to separate these things (e.g. frontend actually talks to members API)
|
|
@@ -20,6 +27,38 @@ const loadMemberSession = async function (req, res, next) {
|
|
|
20
27
|
}
|
|
21
28
|
};
|
|
22
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Require member authentication, and make it possible to authenticate via uuid.
|
|
32
|
+
* You can chain this after loadMemberSession to make it possible to authetnicate via both the uuid and the session.
|
|
33
|
+
*/
|
|
34
|
+
const authMemberByUuid = async function (req, res, next) {
|
|
35
|
+
try {
|
|
36
|
+
const uuid = req.query.uuid;
|
|
37
|
+
if (!uuid) {
|
|
38
|
+
if (res.locals.member && req.member) {
|
|
39
|
+
// Already authenticated via session
|
|
40
|
+
return next();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
throw new errors.UnauthorizedError({
|
|
44
|
+
messsage: tpl(messages.missingUuid)
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const member = await membersService.api.memberBREADService.read({uuid});
|
|
49
|
+
if (!member) {
|
|
50
|
+
throw new errors.UnauthorizedError({
|
|
51
|
+
message: tpl(messages.invalidUuid)
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
Object.assign(req, {member});
|
|
55
|
+
res.locals.member = req.member;
|
|
56
|
+
next();
|
|
57
|
+
} catch (err) {
|
|
58
|
+
next(err);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
23
62
|
const getIdentityToken = async function (req, res) {
|
|
24
63
|
try {
|
|
25
64
|
const token = await membersService.ssr.getIdentityTokenForMemberFromSession(req, res);
|
|
@@ -216,6 +255,7 @@ const createSessionFromMagicLink = async function (req, res, next) {
|
|
|
216
255
|
// Set req.member & res.locals.member if a cookie is set
|
|
217
256
|
module.exports = {
|
|
218
257
|
loadMemberSession,
|
|
258
|
+
authMemberByUuid,
|
|
219
259
|
createSessionFromMagicLink,
|
|
220
260
|
getIdentityToken,
|
|
221
261
|
getMemberNewsletters,
|
|
@@ -5,7 +5,7 @@ const _ = require('lodash');
|
|
|
5
5
|
const errors = require('@tryghost/errors');
|
|
6
6
|
const ghostVersion = require('@tryghost/version');
|
|
7
7
|
const tpl = require('@tryghost/tpl');
|
|
8
|
-
const ObjectId = require('bson-objectid');
|
|
8
|
+
const ObjectId = require('bson-objectid').default;
|
|
9
9
|
|
|
10
10
|
const messages = {
|
|
11
11
|
noPermissionToDismissNotif: 'You do not have permission to dismiss this notification.',
|
|
@@ -13,7 +13,7 @@ const mail = require('../mail');
|
|
|
13
13
|
const SingleUseTokenProvider = require('../members/SingleUseTokenProvider');
|
|
14
14
|
const urlUtils = require('../../../shared/url-utils');
|
|
15
15
|
|
|
16
|
-
const ObjectId = require('bson-objectid');
|
|
16
|
+
const ObjectId = require('bson-objectid').default;
|
|
17
17
|
const settingsHelpers = require('../settings-helpers');
|
|
18
18
|
|
|
19
19
|
const MAGIC_LINK_TOKEN_VALIDITY = 24 * 60 * 60 * 1000;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const debug = require('@tryghost/debug')('themes');
|
|
2
2
|
const fs = require('fs-extra');
|
|
3
|
-
const ObjectID = require('bson-objectid');
|
|
3
|
+
const ObjectID = require('bson-objectid').default;
|
|
4
4
|
|
|
5
5
|
const tpl = require('@tryghost/tpl');
|
|
6
6
|
const logging = require('@tryghost/logging');
|
|
@@ -10,6 +10,8 @@ const shared = require('../shared');
|
|
|
10
10
|
const labs = require('../../../shared/labs');
|
|
11
11
|
const errorHandler = require('@tryghost/mw-error-handler');
|
|
12
12
|
const config = require('../../../shared/config');
|
|
13
|
+
const {http} = require('@tryghost/api-framework');
|
|
14
|
+
const api = require('../../api').endpoints;
|
|
13
15
|
|
|
14
16
|
const commentRouter = require('../comments');
|
|
15
17
|
|
|
@@ -65,6 +67,16 @@ module.exports = function setupMembersApp() {
|
|
|
65
67
|
// Comments
|
|
66
68
|
membersApp.use('/api/comments', commentRouter());
|
|
67
69
|
|
|
70
|
+
// Feedback
|
|
71
|
+
membersApp.post(
|
|
72
|
+
'/api/feedback',
|
|
73
|
+
labs.enabledMiddleware('audienceFeedback'),
|
|
74
|
+
bodyParser.json({limit: '50mb'}),
|
|
75
|
+
middleware.loadMemberSession,
|
|
76
|
+
middleware.authMemberByUuid,
|
|
77
|
+
http(api.feedbackMembers.add)
|
|
78
|
+
);
|
|
79
|
+
|
|
68
80
|
// API error handling
|
|
69
81
|
membersApp.use('/api', errorHandler.resourceNotFound);
|
|
70
82
|
membersApp.use('/api', errorHandler.handleJSONResponse(sentry));
|
package/core/shared/labs.js
CHANGED
|
@@ -26,15 +26,17 @@ const GA_FEATURES = [
|
|
|
26
26
|
// input for the "labs" setting value
|
|
27
27
|
const BETA_FEATURES = [
|
|
28
28
|
'activitypub',
|
|
29
|
+
'sourceAttribution',
|
|
29
30
|
'memberAttribution'
|
|
30
31
|
];
|
|
31
32
|
|
|
32
33
|
const ALPHA_FEATURES = [
|
|
33
34
|
'urlCache',
|
|
34
35
|
'beforeAfterCard',
|
|
35
|
-
'sourceAttribution',
|
|
36
36
|
'lexicalEditor',
|
|
37
|
-
'exploreApp'
|
|
37
|
+
'exploreApp',
|
|
38
|
+
'audienceFeedback',
|
|
39
|
+
'fixNewsletterLinks'
|
|
38
40
|
];
|
|
39
41
|
|
|
40
42
|
module.exports.GA_KEYS = [...GA_FEATURES];
|