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.
Files changed (175) hide show
  1. package/README.md +1 -1
  2. package/components/tryghost-adapter-manager-5.19.0.tgz +0 -0
  3. package/components/{tryghost-api-framework-5.18.0.tgz → tryghost-api-framework-5.19.0.tgz} +0 -0
  4. package/components/tryghost-api-version-compatibility-service-5.19.0.tgz +0 -0
  5. package/components/tryghost-audience-feedback-5.19.0.tgz +0 -0
  6. package/components/tryghost-bootstrap-socket-5.19.0.tgz +0 -0
  7. package/components/tryghost-constants-5.19.0.tgz +0 -0
  8. package/components/{tryghost-custom-theme-settings-service-5.18.0.tgz → tryghost-custom-theme-settings-service-5.19.0.tgz} +0 -0
  9. package/components/tryghost-domain-events-5.19.0.tgz +0 -0
  10. package/components/tryghost-email-analytics-provider-mailgun-5.19.0.tgz +0 -0
  11. package/components/tryghost-email-analytics-service-5.19.0.tgz +0 -0
  12. package/components/tryghost-email-content-generator-5.19.0.tgz +0 -0
  13. package/components/tryghost-express-dynamic-redirects-5.19.0.tgz +0 -0
  14. package/components/tryghost-extract-api-key-5.19.0.tgz +0 -0
  15. package/components/tryghost-html-to-plaintext-5.19.0.tgz +0 -0
  16. package/components/{tryghost-job-manager-5.18.0.tgz → tryghost-job-manager-5.19.0.tgz} +0 -0
  17. package/components/tryghost-link-redirects-5.19.0.tgz +0 -0
  18. package/components/tryghost-link-replacer-5.19.0.tgz +0 -0
  19. package/components/tryghost-link-tracking-5.19.0.tgz +0 -0
  20. package/components/{tryghost-magic-link-5.18.0.tgz → tryghost-magic-link-5.19.0.tgz} +0 -0
  21. package/components/tryghost-mailgun-client-5.19.0.tgz +0 -0
  22. package/components/tryghost-member-analytics-service-5.19.0.tgz +0 -0
  23. package/components/tryghost-member-attribution-5.19.0.tgz +0 -0
  24. package/components/tryghost-member-events-5.19.0.tgz +0 -0
  25. package/components/tryghost-members-analytics-ingress-5.19.0.tgz +0 -0
  26. package/components/tryghost-members-api-5.19.0.tgz +0 -0
  27. package/components/tryghost-members-csv-5.19.0.tgz +0 -0
  28. package/components/tryghost-members-events-service-5.19.0.tgz +0 -0
  29. package/components/tryghost-members-importer-5.19.0.tgz +0 -0
  30. package/components/tryghost-members-offers-5.19.0.tgz +0 -0
  31. package/components/tryghost-members-payments-5.19.0.tgz +0 -0
  32. package/components/tryghost-members-ssr-5.19.0.tgz +0 -0
  33. package/components/tryghost-members-stripe-service-5.19.0.tgz +0 -0
  34. package/components/tryghost-minifier-5.19.0.tgz +0 -0
  35. package/components/tryghost-mw-api-version-mismatch-5.19.0.tgz +0 -0
  36. package/components/tryghost-mw-cache-control-5.19.0.tgz +0 -0
  37. package/components/{tryghost-mw-error-handler-5.18.0.tgz → tryghost-mw-error-handler-5.19.0.tgz} +0 -0
  38. package/components/tryghost-mw-session-from-token-5.19.0.tgz +0 -0
  39. package/components/tryghost-mw-update-user-last-seen-5.19.0.tgz +0 -0
  40. package/components/tryghost-mw-vhost-5.19.0.tgz +0 -0
  41. package/components/tryghost-oembed-service-5.19.0.tgz +0 -0
  42. package/components/tryghost-package-json-5.19.0.tgz +0 -0
  43. package/components/tryghost-referrers-5.19.0.tgz +0 -0
  44. package/components/tryghost-security-5.19.0.tgz +0 -0
  45. package/components/tryghost-session-service-5.19.0.tgz +0 -0
  46. package/components/tryghost-settings-path-manager-5.19.0.tgz +0 -0
  47. package/components/tryghost-staff-service-5.19.0.tgz +0 -0
  48. package/components/tryghost-stats-service-5.19.0.tgz +0 -0
  49. package/components/tryghost-update-check-service-5.19.0.tgz +0 -0
  50. package/components/tryghost-verification-trigger-5.19.0.tgz +0 -0
  51. package/components/tryghost-version-notifications-data-service-5.19.0.tgz +0 -0
  52. package/core/boot.js +3 -1
  53. package/core/built/admin/assets/{chunk.143.6d23a3157dae7a9c899d.js → chunk.143.eaf838fbf1470f018bf3.js} +6 -6
  54. package/core/built/admin/assets/{chunk.174.e997dfffceeaa0ce4636.js → chunk.174.3a133d51d9b45097c101.js} +31 -31
  55. package/core/built/admin/assets/{chunk.178.52a9ca26217a593eda67.js → chunk.178.44dae8a74f7f9d606e06.js} +4 -4
  56. package/core/built/admin/assets/{chunk.427.4483d5bbdaf2a65888ac.js → chunk.613.f1d519ad47e7f9024263.js} +40 -47
  57. package/core/built/admin/assets/{chunk.427.4483d5bbdaf2a65888ac.js.LICENSE.txt → chunk.613.f1d519ad47e7f9024263.js.LICENSE.txt} +0 -0
  58. package/core/built/admin/assets/{ghost-7e6e9479705e7e772bb5ea3b6476cd52.js → ghost-5ce6f5a730c83c91fc258b12c537ea35.js} +555 -559
  59. package/core/built/admin/assets/{ghost-ff0bee94743aa886ce35305a5b46fac3.css → ghost-982146a4ada3a5af1981d1919ae01d08.css} +1 -1
  60. package/core/built/admin/assets/{ghost-dark-a41f7645a406e0df78b7152f3f805e66.css → ghost-dark-41929e4857de411a23597a9de49a4e4f.css} +1 -1
  61. package/core/built/admin/assets/{vendor-4da5d2584fbe1442e25e4271a5513f1c.js → vendor-5c7d7063620bec13668c4370145cd4b4.js} +41 -34
  62. package/core/built/admin/index.html +6 -6
  63. package/core/frontend/helpers/t.js +12 -0
  64. package/core/frontend/helpers/tpl/content-cta.hbs +1 -1
  65. package/core/frontend/public/robots.txt +1 -0
  66. package/core/frontend/services/sitemap/handler.js +1 -1
  67. package/core/frontend/services/sitemap/index-generator.js +1 -3
  68. package/core/server/api/endpoints/feedback-members.js +23 -0
  69. package/core/server/api/endpoints/index.js +5 -1
  70. package/core/server/api/endpoints/utils/serializers/input/posts.js +6 -1
  71. package/core/server/api/endpoints/utils/serializers/output/mappers/posts.js +8 -0
  72. package/core/server/data/exporter/table-lists.js +3 -1
  73. package/core/server/data/importer/handlers/json.js +21 -23
  74. package/core/server/data/importer/importers/data/base.js +1 -1
  75. package/core/server/data/migrations/versions/4.0/05-add-members-subscribe-events-table.js +1 -1
  76. package/core/server/data/migrations/versions/4.0/06-populate-members-subscribe-events-table.js +1 -1
  77. package/core/server/data/migrations/versions/4.0/11-add-members-paid-subscription-events-table.js +1 -1
  78. package/core/server/data/migrations/versions/4.0/13-add-members-payment-events-table.js +1 -1
  79. package/core/server/data/migrations/versions/4.0/17-populate-members-status-events-table.js +1 -1
  80. package/core/server/data/migrations/versions/4.0/22-solve-orphaned-webhooks.js +1 -1
  81. package/core/server/data/migrations/versions/4.0/25-populate-members-paid-subscription-events-table.js +1 -1
  82. package/core/server/data/migrations/versions/4.11/02-add-email-verification-required-setting.js +1 -1
  83. package/core/server/data/migrations/versions/4.12/01-add-email-only-column-to-posts-meta-table.js +1 -1
  84. package/core/server/data/migrations/versions/4.3/03-add-default-product.js +1 -1
  85. package/core/server/data/migrations/versions/4.3/04-attach-members-to-product.js +1 -1
  86. package/core/server/data/migrations/versions/4.3/06-add-stripe-prices-table.js +2 -2
  87. package/core/server/data/migrations/versions/4.3/08-migrate-members-signup-setting.js +1 -1
  88. package/core/server/data/migrations/versions/4.33/2022-01-14-11-51-add-default-free-tier.js +1 -1
  89. package/core/server/data/migrations/versions/4.42/2022-03-21-17-17-add.js +2 -2
  90. package/core/server/data/migrations/versions/4.43/2022-03-28-19-26-recreate-newsletter-table.js +5 -5
  91. package/core/server/data/migrations/versions/4.46/2022-04-13-13-00-add-default-newsletter.js +1 -1
  92. package/core/server/data/migrations/versions/4.46/2022-04-20-08-39-map-subscribers-to-default-newsletter.js +1 -1
  93. package/core/server/data/migrations/versions/4.7/03-add-labs-setting.js +1 -1
  94. package/core/server/data/migrations/versions/4.8/03-add-default-product-portal-products.js +1 -1
  95. package/core/server/data/migrations/versions/4.8/04-migrate-show-newsletter-header-setting.js +1 -1
  96. package/core/server/data/migrations/versions/5.0/2022-05-06-13-22-add-frontend-integration.js +1 -1
  97. package/core/server/data/migrations/versions/5.17/2022-09-29-12-39-add-track-clicks-column-to-emails.js +2 -2
  98. package/core/server/data/migrations/versions/5.19/2022-09-02-20-25-add-columns-to-products-table.js +19 -0
  99. package/core/server/data/migrations/versions/5.19/2022-09-02-20-52-backfill-new-product-columns.js +37 -0
  100. package/core/server/data/migrations/versions/5.19/2022-10-10-06-58-add-subscriptions-table.js +19 -0
  101. package/core/server/data/migrations/versions/5.19/2022-10-10-10-05-add-members-feedback-table.js +10 -0
  102. package/core/server/data/migrations/versions/5.19/2022-10-11-10-38-add-feedback-enabled-column-to-newsletters.js +7 -0
  103. package/core/server/data/schema/commands.js +3 -3
  104. package/core/server/data/schema/fixtures/fixtures.json +4 -1
  105. package/core/server/data/schema/schema.js +90 -24
  106. package/core/server/data/schema/validator.js +1 -1
  107. package/core/server/models/base/bookshelf.js +3 -4
  108. package/core/server/models/base/plugins/data-manipulation.js +1 -1
  109. package/core/server/models/base/plugins/events.js +1 -1
  110. package/core/server/models/base/utils.js +1 -1
  111. package/core/server/models/member-feedback.js +22 -0
  112. package/core/server/models/newsletter.js +3 -2
  113. package/core/server/models/post.js +24 -0
  114. package/core/server/models/settings.js +1 -1
  115. package/core/server/models/user.js +1 -1
  116. package/core/server/services/audience-feedback/FeedbackRepository.js +67 -0
  117. package/core/server/services/audience-feedback/index.js +33 -0
  118. package/core/server/services/bulk-email/bulk-email-processor.js +7 -1
  119. package/core/server/services/mail/GhostMailer.js +17 -1
  120. package/core/server/services/mega/feedback-buttons.js +69 -0
  121. package/core/server/services/mega/mega.js +1 -1
  122. package/core/server/services/mega/post-email-serializer.js +24 -4
  123. package/core/server/services/mega/template.js +3 -0
  124. package/core/server/services/members/middleware.js +40 -0
  125. package/core/server/services/notifications/notifications.js +1 -1
  126. package/core/server/services/settings/settings-service.js +1 -1
  127. package/core/server/services/themes/storage.js +1 -1
  128. package/core/server/web/members/app.js +12 -0
  129. package/core/shared/labs.js +4 -2
  130. package/package.json +110 -110
  131. package/yarn.lock +496 -432
  132. package/components/tryghost-adapter-manager-5.18.0.tgz +0 -0
  133. package/components/tryghost-api-version-compatibility-service-5.18.0.tgz +0 -0
  134. package/components/tryghost-bootstrap-socket-5.18.0.tgz +0 -0
  135. package/components/tryghost-constants-5.18.0.tgz +0 -0
  136. package/components/tryghost-domain-events-5.18.0.tgz +0 -0
  137. package/components/tryghost-email-analytics-provider-mailgun-5.18.0.tgz +0 -0
  138. package/components/tryghost-email-analytics-service-5.18.0.tgz +0 -0
  139. package/components/tryghost-email-content-generator-5.18.0.tgz +0 -0
  140. package/components/tryghost-express-dynamic-redirects-5.18.0.tgz +0 -0
  141. package/components/tryghost-extract-api-key-5.18.0.tgz +0 -0
  142. package/components/tryghost-html-to-plaintext-5.18.0.tgz +0 -0
  143. package/components/tryghost-link-redirects-5.18.0.tgz +0 -0
  144. package/components/tryghost-link-replacer-5.18.0.tgz +0 -0
  145. package/components/tryghost-link-tracking-5.18.0.tgz +0 -0
  146. package/components/tryghost-mailgun-client-5.18.0.tgz +0 -0
  147. package/components/tryghost-member-analytics-service-5.18.0.tgz +0 -0
  148. package/components/tryghost-member-attribution-5.18.0.tgz +0 -0
  149. package/components/tryghost-member-events-5.18.0.tgz +0 -0
  150. package/components/tryghost-members-analytics-ingress-5.18.0.tgz +0 -0
  151. package/components/tryghost-members-api-5.18.0.tgz +0 -0
  152. package/components/tryghost-members-csv-5.18.0.tgz +0 -0
  153. package/components/tryghost-members-events-service-5.18.0.tgz +0 -0
  154. package/components/tryghost-members-importer-5.18.0.tgz +0 -0
  155. package/components/tryghost-members-offers-5.18.0.tgz +0 -0
  156. package/components/tryghost-members-payments-5.18.0.tgz +0 -0
  157. package/components/tryghost-members-ssr-5.18.0.tgz +0 -0
  158. package/components/tryghost-members-stripe-service-5.18.0.tgz +0 -0
  159. package/components/tryghost-minifier-5.18.0.tgz +0 -0
  160. package/components/tryghost-mw-api-version-mismatch-5.18.0.tgz +0 -0
  161. package/components/tryghost-mw-cache-control-5.18.0.tgz +0 -0
  162. package/components/tryghost-mw-session-from-token-5.18.0.tgz +0 -0
  163. package/components/tryghost-mw-update-user-last-seen-5.18.0.tgz +0 -0
  164. package/components/tryghost-mw-vhost-5.18.0.tgz +0 -0
  165. package/components/tryghost-oembed-service-5.18.0.tgz +0 -0
  166. package/components/tryghost-package-json-5.18.0.tgz +0 -0
  167. package/components/tryghost-referrers-5.18.0.tgz +0 -0
  168. package/components/tryghost-security-5.18.0.tgz +0 -0
  169. package/components/tryghost-session-service-5.18.0.tgz +0 -0
  170. package/components/tryghost-settings-path-manager-5.18.0.tgz +0 -0
  171. package/components/tryghost-staff-service-5.18.0.tgz +0 -0
  172. package/components/tryghost-stats-service-5.18.0.tgz +0 -0
  173. package/components/tryghost-update-check-service-5.18.0.tgz +0 -0
  174. package/components/tryghost-verification-trigger-5.18.0.tgz +0 -0
  175. 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.each(targets.models, function (target, index) {
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 && (columnDef.type === 'bool' || columnDef.type === 'boolean')) {
81
+ if (columnDef?.type === 'boolean') {
82
82
  attrs[key] = value ? true : false;
83
83
  }
84
84
  });
@@ -1,6 +1,6 @@
1
1
  const _ = require('lodash');
2
2
  const debug = require('@tryghost/debug')('models:base:model-events');
3
- const ObjectId = require('bson-objectid');
3
+ const ObjectId = require('bson-objectid').default;
4
4
 
5
5
  const schema = require('../../../data/schema');
6
6
 
@@ -5,7 +5,7 @@
5
5
  const _ = require('lodash');
6
6
 
7
7
  const Promise = require('bluebird');
8
- const ObjectId = require('bson-objectid');
8
+ const ObjectId = require('bson-objectid').default;
9
9
  const errors = require('@tryghost/errors');
10
10
 
11
11
  /**
@@ -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 {[EmailRecipient]} recipients - The recipients to send the email to with their associated data
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));
@@ -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];