ghost 5.34.1 → 5.35.1

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 (153) hide show
  1. package/components/tryghost-adapter-cache-redis-5.35.1.tgz +0 -0
  2. package/components/{tryghost-adapter-manager-5.34.1.tgz → tryghost-adapter-manager-5.35.1.tgz} +0 -0
  3. package/components/{tryghost-api-framework-5.34.1.tgz → tryghost-api-framework-5.35.1.tgz} +0 -0
  4. package/components/tryghost-api-version-compatibility-service-5.35.1.tgz +0 -0
  5. package/components/tryghost-audience-feedback-5.35.1.tgz +0 -0
  6. package/components/{tryghost-bootstrap-socket-5.34.1.tgz → tryghost-bootstrap-socket-5.35.1.tgz} +0 -0
  7. package/components/tryghost-constants-5.35.1.tgz +0 -0
  8. package/components/tryghost-custom-theme-settings-service-5.35.1.tgz +0 -0
  9. package/components/tryghost-data-generator-5.35.1.tgz +0 -0
  10. package/components/{tryghost-domain-events-5.34.1.tgz → tryghost-domain-events-5.35.1.tgz} +0 -0
  11. package/components/tryghost-dynamic-routing-events-5.35.1.tgz +0 -0
  12. package/components/tryghost-email-analytics-provider-mailgun-5.35.1.tgz +0 -0
  13. package/components/tryghost-email-analytics-service-5.35.1.tgz +0 -0
  14. package/components/tryghost-email-content-generator-5.35.1.tgz +0 -0
  15. package/components/tryghost-email-events-5.35.1.tgz +0 -0
  16. package/components/tryghost-email-service-5.35.1.tgz +0 -0
  17. package/components/{tryghost-email-suppression-list-5.34.1.tgz → tryghost-email-suppression-list-5.35.1.tgz} +0 -0
  18. package/components/{tryghost-express-dynamic-redirects-5.34.1.tgz → tryghost-express-dynamic-redirects-5.35.1.tgz} +0 -0
  19. package/components/tryghost-extract-api-key-5.35.1.tgz +0 -0
  20. package/components/tryghost-html-to-plaintext-5.35.1.tgz +0 -0
  21. package/components/{tryghost-i18n-5.34.1.tgz → tryghost-i18n-5.35.1.tgz} +0 -0
  22. package/components/{tryghost-importer-revue-5.34.1.tgz → tryghost-importer-revue-5.35.1.tgz} +0 -0
  23. package/components/tryghost-job-manager-5.35.1.tgz +0 -0
  24. package/components/tryghost-link-redirects-5.35.1.tgz +0 -0
  25. package/components/tryghost-link-replacer-5.35.1.tgz +0 -0
  26. package/components/tryghost-link-tracking-5.35.1.tgz +0 -0
  27. package/components/{tryghost-magic-link-5.34.1.tgz → tryghost-magic-link-5.35.1.tgz} +0 -0
  28. package/components/tryghost-mailgun-client-5.35.1.tgz +0 -0
  29. package/components/tryghost-member-attribution-5.35.1.tgz +0 -0
  30. package/components/{tryghost-member-events-5.34.1.tgz → tryghost-member-events-5.35.1.tgz} +0 -0
  31. package/components/{tryghost-members-api-5.34.1.tgz → tryghost-members-api-5.35.1.tgz} +0 -0
  32. package/components/tryghost-members-csv-5.35.1.tgz +0 -0
  33. package/components/{tryghost-members-events-service-5.34.1.tgz → tryghost-members-events-service-5.35.1.tgz} +0 -0
  34. package/components/{tryghost-members-importer-5.34.1.tgz → tryghost-members-importer-5.35.1.tgz} +0 -0
  35. package/components/tryghost-members-offers-5.35.1.tgz +0 -0
  36. package/components/tryghost-members-payments-5.35.1.tgz +0 -0
  37. package/components/tryghost-members-ssr-5.35.1.tgz +0 -0
  38. package/components/tryghost-members-stripe-service-5.35.1.tgz +0 -0
  39. package/components/tryghost-milestones-5.35.1.tgz +0 -0
  40. package/components/tryghost-minifier-5.35.1.tgz +0 -0
  41. package/components/tryghost-mw-api-version-mismatch-5.35.1.tgz +0 -0
  42. package/components/{tryghost-mw-cache-control-5.34.1.tgz → tryghost-mw-cache-control-5.35.1.tgz} +0 -0
  43. package/components/{tryghost-mw-error-handler-5.34.1.tgz → tryghost-mw-error-handler-5.35.1.tgz} +0 -0
  44. package/components/tryghost-mw-session-from-token-5.35.1.tgz +0 -0
  45. package/components/tryghost-mw-update-user-last-seen-5.35.1.tgz +0 -0
  46. package/components/tryghost-mw-vhost-5.35.1.tgz +0 -0
  47. package/components/{tryghost-oembed-service-5.34.1.tgz → tryghost-oembed-service-5.35.1.tgz} +0 -0
  48. package/components/tryghost-package-json-5.35.1.tgz +0 -0
  49. package/components/tryghost-public-resource-repository-5.35.1.tgz +0 -0
  50. package/components/tryghost-referrers-5.35.1.tgz +0 -0
  51. package/components/tryghost-security-5.35.1.tgz +0 -0
  52. package/components/tryghost-session-service-5.35.1.tgz +0 -0
  53. package/components/tryghost-settings-path-manager-5.35.1.tgz +0 -0
  54. package/components/tryghost-slack-notifications-5.35.1.tgz +0 -0
  55. package/components/{tryghost-staff-service-5.34.1.tgz → tryghost-staff-service-5.35.1.tgz} +0 -0
  56. package/components/tryghost-stats-service-5.35.1.tgz +0 -0
  57. package/components/tryghost-tiers-5.35.1.tgz +0 -0
  58. package/components/tryghost-update-check-service-5.35.1.tgz +0 -0
  59. package/components/tryghost-verification-trigger-5.35.1.tgz +0 -0
  60. package/components/tryghost-version-notifications-data-service-5.35.1.tgz +0 -0
  61. package/components/tryghost-webmentions-5.35.1.tgz +0 -0
  62. package/core/boot.js +27 -3
  63. package/core/built/admin/assets/{chunk.143.07f5af56ff872bb0e9e4.js → chunk.143.3e5760df9072a9463476.js} +5 -5
  64. package/core/built/admin/assets/{chunk.178.2e831ef9072743e38dd1.js → chunk.178.773722be9deba376bacb.js} +4 -4
  65. package/core/built/admin/assets/{chunk.616.181e1ad6c33f0bec7a65.js → chunk.502.c4afca88c98edad8b268.js} +1621 -1338
  66. package/core/built/admin/assets/{chunk.616.181e1ad6c33f0bec7a65.js.LICENSE.txt → chunk.502.c4afca88c98edad8b268.js.LICENSE.txt} +43 -0
  67. package/core/built/admin/assets/{ghost-558c1e319d6e025bfab2054bc0f7fe83.css → ghost-1d4b69d04b8a97e8a1bd83ed5bb20777.css} +1 -1
  68. package/core/built/admin/assets/{ghost-ad40d109653288e74a7cd922341fb33d.js → ghost-5f530fdaca961aa2ec5c339d5bb13443.js} +4353 -4323
  69. package/core/built/admin/assets/{ghost-dark-a15754df1f9070dc2525482ce22e2251.css → ghost-dark-633bf4628beead68e8a0d1fa668735ff.css} +1 -1
  70. package/core/built/admin/assets/{vendor-253d6527ca6353855164ef65f896f762.js → vendor-c4684647d4f5213e5dbb6763de430e7e.js} +2741 -2309
  71. package/core/built/admin/index.html +6 -6
  72. package/core/cli/generate-data.js +3 -1
  73. package/core/server/api/endpoints/emails.js +35 -0
  74. package/core/server/api/endpoints/images.js +13 -6
  75. package/core/server/api/endpoints/pages-public.js +3 -1
  76. package/core/server/api/endpoints/posts-public.js +3 -1
  77. package/core/server/api/endpoints/utils/serializers/output/emails.js +4 -0
  78. package/core/server/api/endpoints/utils/serializers/output/mappers/mentions.js +2 -1
  79. package/core/server/api/endpoints/utils/serializers/output/mappers/posts.js +11 -0
  80. package/core/server/data/migrations/versions/5.35/2023-02-13-06-24-add-mentions-verified-column.js +7 -0
  81. package/core/server/data/schema/schema.js +2 -1
  82. package/core/server/lib/request-external.js +1 -0
  83. package/core/server/models/mention.js +2 -1
  84. package/core/server/services/email-analytics/lib/queries.js +18 -3
  85. package/core/server/services/email-analytics/wrapper.js +34 -15
  86. package/core/server/services/email-service/wrapper.js +2 -1
  87. package/core/server/services/mega/post-email-serializer.js +2 -2
  88. package/core/server/services/mega/template.js +1 -1
  89. package/core/server/services/member-attribution/index.js +8 -4
  90. package/core/server/services/mentions/BookshelfMentionRepository.js +4 -2
  91. package/core/server/services/mentions/WebmentionRequest.js +20 -0
  92. package/core/server/services/mentions/service.js +4 -1
  93. package/core/server/services/mentions-jobs/index.js +1 -0
  94. package/core/server/services/mentions-jobs/job-service.js +48 -0
  95. package/core/server/services/{milestone-emails → milestones}/MilestoneQueries.js +0 -0
  96. package/core/server/services/{milestone-emails → milestones}/index.js +0 -0
  97. package/core/server/services/milestones/service.js +78 -0
  98. package/core/server/services/posts-public/index.js +1 -0
  99. package/core/server/services/posts-public/service.js +31 -0
  100. package/core/server/services/slack-notifications/index.js +1 -0
  101. package/core/server/services/slack-notifications/service.js +60 -0
  102. package/core/server/services/tags-public/service.js +5 -5
  103. package/core/server/services/websockets/index.js +1 -0
  104. package/core/server/services/websockets/service.js +37 -0
  105. package/core/server/web/api/endpoints/admin/routes.js +3 -0
  106. package/core/shared/config/defaults.json +2 -2
  107. package/core/shared/labs.js +4 -3
  108. package/package.json +121 -118
  109. package/yarn.lock +307 -102
  110. package/components/tryghost-adapter-cache-redis-5.34.1.tgz +0 -0
  111. package/components/tryghost-api-version-compatibility-service-5.34.1.tgz +0 -0
  112. package/components/tryghost-audience-feedback-5.34.1.tgz +0 -0
  113. package/components/tryghost-constants-5.34.1.tgz +0 -0
  114. package/components/tryghost-custom-theme-settings-service-5.34.1.tgz +0 -0
  115. package/components/tryghost-data-generator-5.34.1.tgz +0 -0
  116. package/components/tryghost-dynamic-routing-events-5.34.1.tgz +0 -0
  117. package/components/tryghost-email-analytics-provider-mailgun-5.34.1.tgz +0 -0
  118. package/components/tryghost-email-analytics-service-5.34.1.tgz +0 -0
  119. package/components/tryghost-email-content-generator-5.34.1.tgz +0 -0
  120. package/components/tryghost-email-events-5.34.1.tgz +0 -0
  121. package/components/tryghost-email-service-5.34.1.tgz +0 -0
  122. package/components/tryghost-extract-api-key-5.34.1.tgz +0 -0
  123. package/components/tryghost-html-to-plaintext-5.34.1.tgz +0 -0
  124. package/components/tryghost-job-manager-5.34.1.tgz +0 -0
  125. package/components/tryghost-link-redirects-5.34.1.tgz +0 -0
  126. package/components/tryghost-link-replacer-5.34.1.tgz +0 -0
  127. package/components/tryghost-link-tracking-5.34.1.tgz +0 -0
  128. package/components/tryghost-mailgun-client-5.34.1.tgz +0 -0
  129. package/components/tryghost-member-attribution-5.34.1.tgz +0 -0
  130. package/components/tryghost-members-csv-5.34.1.tgz +0 -0
  131. package/components/tryghost-members-offers-5.34.1.tgz +0 -0
  132. package/components/tryghost-members-payments-5.34.1.tgz +0 -0
  133. package/components/tryghost-members-ssr-5.34.1.tgz +0 -0
  134. package/components/tryghost-members-stripe-service-5.34.1.tgz +0 -0
  135. package/components/tryghost-milestone-emails-5.34.1.tgz +0 -0
  136. package/components/tryghost-minifier-5.34.1.tgz +0 -0
  137. package/components/tryghost-mw-api-version-mismatch-5.34.1.tgz +0 -0
  138. package/components/tryghost-mw-session-from-token-5.34.1.tgz +0 -0
  139. package/components/tryghost-mw-update-user-last-seen-5.34.1.tgz +0 -0
  140. package/components/tryghost-mw-vhost-5.34.1.tgz +0 -0
  141. package/components/tryghost-package-json-5.34.1.tgz +0 -0
  142. package/components/tryghost-referrers-5.34.1.tgz +0 -0
  143. package/components/tryghost-security-5.34.1.tgz +0 -0
  144. package/components/tryghost-session-service-5.34.1.tgz +0 -0
  145. package/components/tryghost-settings-path-manager-5.34.1.tgz +0 -0
  146. package/components/tryghost-stats-service-5.34.1.tgz +0 -0
  147. package/components/tryghost-tags-public-5.34.1.tgz +0 -0
  148. package/components/tryghost-tiers-5.34.1.tgz +0 -0
  149. package/components/tryghost-update-check-service-5.34.1.tgz +0 -0
  150. package/components/tryghost-verification-trigger-5.34.1.tgz +0 -0
  151. package/components/tryghost-version-notifications-data-service-5.34.1.tgz +0 -0
  152. package/components/tryghost-webmentions-5.34.1.tgz +0 -0
  153. package/core/server/services/milestone-emails/service.js +0 -58
@@ -8,7 +8,7 @@
8
8
  <title>Ghost Admin</title>
9
9
 
10
10
 
11
- <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%225.34%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%7D" />
11
+ <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%225.35%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22ember-websockets%22%3A%7B%22socketIO%22%3Atrue%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%7D" />
12
12
 
13
13
  <meta name="HandheldFriendly" content="True" />
14
14
  <meta name="MobileOptimized" content="320" />
@@ -37,7 +37,7 @@
37
37
  </style>
38
38
 
39
39
  <link integrity="" rel="stylesheet" href="assets/vendor-3e6947aa681f0fb82b193090e520dc73.css">
40
- <link integrity="" rel="stylesheet" href="assets/ghost-558c1e319d6e025bfab2054bc0f7fe83.css" title="light">
40
+ <link integrity="" rel="stylesheet" href="assets/ghost-1d4b69d04b8a97e8a1bd83ed5bb20777.css" title="light">
41
41
 
42
42
 
43
43
  </head>
@@ -56,9 +56,9 @@
56
56
 
57
57
  <div id="ember-basic-dropdown-wormhole"></div>
58
58
 
59
- <script src="assets/vendor-253d6527ca6353855164ef65f896f762.js"></script>
60
- <script src="assets/chunk.616.181e1ad6c33f0bec7a65.js"></script>
61
- <script src="assets/chunk.143.07f5af56ff872bb0e9e4.js"></script>
62
- <script src="assets/ghost-ad40d109653288e74a7cd922341fb33d.js"></script>
59
+ <script src="assets/vendor-c4684647d4f5213e5dbb6763de430e7e.js"></script>
60
+ <script src="assets/chunk.502.c4afca88c98edad8b268.js"></script>
61
+ <script src="assets/chunk.143.3e5760df9072a9463476.js"></script>
62
+ <script src="assets/ghost-5f530fdaca961aa2ec5c339d5bb13443.js"></script>
63
63
  </body>
64
64
  </html>
@@ -9,6 +9,7 @@ module.exports = class DataGeneratorCommand extends Command {
9
9
  this.argument('--scale', {type: 'string', defaultValue: 'small', desc: 'Scale of the data to generate. `small` for a quick run, `large` for more content'});
10
10
  this.argument('--single-table', {type: 'string', desc: 'Import a single table'});
11
11
  this.argument('--quantity', {type: 'number', desc: 'When importing a single table, the quantity to import'});
12
+ this.argument('--clear-database', {type: 'boolean', defaultValue: false, desc: 'Clear all entries in the database before importing'});
12
13
  }
13
14
 
14
15
  initializeContext(context) {
@@ -55,7 +56,8 @@ module.exports = class DataGeneratorCommand extends Command {
55
56
  debug: this.debug
56
57
  },
57
58
  modelQuantities,
58
- baseUrl: config.getSiteUrl()
59
+ baseUrl: config.getSiteUrl(),
60
+ clearDatabase: argv['clear-database']
59
61
  });
60
62
  try {
61
63
  if (argv['single-table']) {
@@ -4,6 +4,7 @@ const errors = require('@tryghost/errors');
4
4
  const megaService = require('../../services/mega');
5
5
  const emailService = require('../../services/email-service');
6
6
  const labs = require('../../../shared/labs');
7
+ const emailAnalytics = require('../../services/email-analytics');
7
8
 
8
9
  const messages = {
9
10
  emailNotFound: 'Email not found.',
@@ -140,5 +141,39 @@ module.exports = {
140
141
  const filter = `email_id:'${frame.data.id}'` + (frame.options.filter ? `+(${frame.options.filter})` : '');
141
142
  return await models.EmailRecipientFailure.findPage({...frame.options, filter});
142
143
  }
144
+ },
145
+
146
+ analyticsStatus: {
147
+ permissions: {
148
+ method: 'browse'
149
+ },
150
+ async query() {
151
+ return emailAnalytics.service.getStatus();
152
+ }
153
+ },
154
+
155
+ scheduleAnalytics: {
156
+ permissions: {
157
+ method: 'browse'
158
+ },
159
+ data: [
160
+ 'id'
161
+ ],
162
+ async query(frame) {
163
+ const model = await models.Email.findOne(frame.data, frame.options);
164
+ return emailAnalytics.service.schedule({
165
+ begin: model.get('created_at'),
166
+ end: new Date(Math.min(Date.now() - 60 * 60 * 1000, model.get('created_at').getTime() + 24 * 60 * 60 * 1000 * 7))
167
+ });
168
+ }
169
+ },
170
+
171
+ cancelScheduledAnalytics: {
172
+ permissions: {
173
+ method: 'browse'
174
+ },
175
+ async query() {
176
+ return emailAnalytics.service.cancelScheduled();
177
+ }
143
178
  }
144
179
  };
@@ -37,13 +37,20 @@ module.exports = {
37
37
  ...frame.file,
38
38
  path: out
39
39
  });
40
- const processedImagePath = store.urlToPath(processedImageUrl);
41
40
 
42
- // Get the path and name of the processed image
43
- // We want to store the original image on the same name + _o
44
- // So we need to wait for the first store to finish before generating the name of the original image
45
- const processedImageName = path.basename(processedImagePath);
46
- const processedImageDir = path.dirname(processedImagePath);
41
+ let processedImageName = path.basename(processedImageUrl);
42
+ let processedImageDir = undefined;
43
+
44
+ if (store.urlToPath) {
45
+ // Currently urlToPath is not part of StorageBase, so not all storage provider have implemented it
46
+ const processedImagePath = store.urlToPath(processedImageUrl);
47
+
48
+ // Get the path and name of the processed image
49
+ // We want to store the original image on the same name + _o
50
+ // So we need to wait for the first store to finish before generating the name of the original image
51
+ processedImageName = path.basename(processedImagePath);
52
+ processedImageDir = path.dirname(processedImagePath);
53
+ }
47
54
 
48
55
  // Store the original image
49
56
  await store.save({
@@ -1,6 +1,8 @@
1
1
  const tpl = require('@tryghost/tpl');
2
2
  const errors = require('@tryghost/errors');
3
3
  const models = require('../../models');
4
+ const postsPublicService = require('../../services/posts-public');
5
+
4
6
  const ALLOWED_INCLUDES = ['tags', 'authors', 'tiers'];
5
7
 
6
8
  const messages = {
@@ -34,7 +36,7 @@ module.exports = {
34
36
  },
35
37
  permissions: true,
36
38
  query(frame) {
37
- return models.Post.findPage(frame.options);
39
+ return postsPublicService.api.browse(frame.options);
38
40
  }
39
41
  },
40
42
 
@@ -1,6 +1,8 @@
1
1
  const models = require('../../models');
2
2
  const tpl = require('@tryghost/tpl');
3
3
  const errors = require('@tryghost/errors');
4
+ const postsPublicService = require('../../services/posts-public');
5
+
4
6
  const allowedIncludes = ['tags', 'authors', 'tiers', 'sentiment'];
5
7
 
6
8
  const messages = {
@@ -34,7 +36,7 @@ module.exports = {
34
36
  },
35
37
  permissions: true,
36
38
  query(frame) {
37
- return models.Post.findPage(frame.options);
39
+ return postsPublicService.api.browse(frame.options);
38
40
  }
39
41
  },
40
42
 
@@ -28,5 +28,9 @@ module.exports = {
28
28
  if (response.meta) {
29
29
  frame.response.meta = response.meta;
30
30
  }
31
+ },
32
+
33
+ analyticsStatus(response, apiConfig, frame) {
34
+ frame.response = response;
31
35
  }
32
36
  };
@@ -13,6 +13,7 @@ module.exports = (model) => {
13
13
  source_excerpt: json.sourceExcerpt,
14
14
  source_author: json.sourceAuthor,
15
15
  source_favicon: json.sourceFavicon,
16
- source_featured_image: json.sourceFeaturedImage
16
+ source_featured_image: json.sourceFeaturedImage,
17
+ verified: json.verified
17
18
  };
18
19
  };
@@ -18,6 +18,8 @@ const getPostServiceInstance = require('../../../../../../services/posts/posts-s
18
18
  const postsService = getPostServiceInstance();
19
19
 
20
20
  const commentsService = require('../../../../../../services/comments');
21
+ const memberAttribution = require('../../../../../../services/member-attribution');
22
+ const labs = require('../../../../../../../shared/labs');
21
23
 
22
24
  module.exports = async (model, frame, options = {}) => {
23
25
  const {tiers: tiersData} = options || {};
@@ -65,6 +67,15 @@ module.exports = async (model, frame, options = {}) => {
65
67
  } else {
66
68
  jsonModel.comments = false;
67
69
  }
70
+
71
+ // Add outbound link tags
72
+ if (labs.isSet('outboundLinkTagging')) {
73
+ // Only add it in the flag! Without the flag we only add it to emails.
74
+ if (jsonModel.html) {
75
+ // Only set if HTML was requested
76
+ jsonModel.html = await memberAttribution.outboundLinkTagger.addToHtml(jsonModel.html);
77
+ }
78
+ }
68
79
  }
69
80
 
70
81
  // Transforms post/page metadata to flat structure
@@ -0,0 +1,7 @@
1
+ const {createAddColumnMigration} = require('../../utils');
2
+
3
+ module.exports = createAddColumnMigration('mentions', 'verified', {
4
+ type: 'boolean',
5
+ nullable: false,
6
+ defaultTo: false
7
+ });
@@ -995,6 +995,7 @@ module.exports = {
995
995
  resource_type: {type: 'string', maxlength: 50, nullable: true},
996
996
  created_at: {type: 'dateTime', nullable: false},
997
997
  payload: {type: 'text', maxlength: 65535, nullable: true},
998
- deleted: {type: 'boolean', nullable: false, defaultTo: false}
998
+ deleted: {type: 'boolean', nullable: false, defaultTo: false},
999
+ verified: {type: 'boolean', nullable: false, defaultTo: false}
999
1000
  }
1000
1001
  };
@@ -42,6 +42,7 @@ const externalRequest = got.extend({
42
42
  headers: {
43
43
  'user-agent': 'Ghost(https://github.com/TryGhost/Ghost)'
44
44
  },
45
+ timeout: 10000, // default is no timeout
45
46
  hooks: {
46
47
  init: [(options) => {
47
48
  if (!options.hostname || !validator.isURL(options.hostname)) {
@@ -3,7 +3,8 @@ const ghostBookshelf = require('./base');
3
3
  const Mention = ghostBookshelf.Model.extend({
4
4
  tableName: 'mentions',
5
5
  defaults: {
6
- deleted: false
6
+ deleted: false,
7
+ verified: false
7
8
  },
8
9
  enforcedFilters() {
9
10
  return 'deleted:false';
@@ -15,9 +15,24 @@ module.exports = {
15
15
  const startDate = new Date();
16
16
 
17
17
  // three separate queries is much faster than using max/greatest (with coalesce to handle nulls) across columns
18
- const {maxDeliveredAt} = await db.knex('email_recipients').select(db.knex.raw('MAX(delivered_at) as maxDeliveredAt')).first() || {};
19
- const {maxOpenedAt} = await db.knex('email_recipients').select(db.knex.raw('MAX(opened_at) as maxOpenedAt')).first() || {};
20
- const {maxFailedAt} = await db.knex('email_recipients').select(db.knex.raw('MAX(failed_at) as maxFailedAt')).first() || {};
18
+ let {maxDeliveredAt} = await db.knex('email_recipients').select(db.knex.raw('MAX(delivered_at) as maxDeliveredAt')).first() || {};
19
+ let {maxOpenedAt} = await db.knex('email_recipients').select(db.knex.raw('MAX(opened_at) as maxOpenedAt')).first() || {};
20
+ let {maxFailedAt} = await db.knex('email_recipients').select(db.knex.raw('MAX(failed_at) as maxFailedAt')).first() || {};
21
+
22
+ if (maxDeliveredAt && !(maxDeliveredAt instanceof Date)) {
23
+ // SQLite returns a string instead of a Date
24
+ maxDeliveredAt = new Date(maxDeliveredAt);
25
+ }
26
+
27
+ if (maxOpenedAt && !(maxOpenedAt instanceof Date)) {
28
+ // SQLite returns a string instead of a Date
29
+ maxOpenedAt = new Date(maxOpenedAt);
30
+ }
31
+
32
+ if (maxFailedAt && !(maxFailedAt instanceof Date)) {
33
+ // SQLite returns a string instead of a Date
34
+ maxFailedAt = new Date(maxFailedAt);
35
+ }
21
36
 
22
37
  const lastSeenEventTimestamp = _.max([maxDeliveredAt, maxOpenedAt, maxFailedAt]);
23
38
  debug(`getLastSeenEventTimestamp: finished in ${Date.now() - startDate}ms`);
@@ -1,5 +1,4 @@
1
1
  const logging = require('@tryghost/logging');
2
- const debug = require('@tryghost/debug')('jobs:email-analytics:fetch-latest');
3
2
 
4
3
  class EmailAnalyticsServiceWrapper {
5
4
  init() {
@@ -55,22 +54,40 @@ class EmailAnalyticsServiceWrapper {
55
54
  });
56
55
  }
57
56
 
58
- async fetchLatest() {
57
+ async fetchLatest({maxEvents} = {maxEvents: Infinity}) {
58
+ logging.info('[EmailAnalytics] Fetch latest started');
59
+
59
60
  const fetchStartDate = new Date();
60
- debug('Starting email analytics fetch of latest events');
61
- const eventStats = await this.service.fetchLatest();
61
+ const totalEvents = await this.service.fetchLatest({maxEvents});
62
62
  const fetchEndDate = new Date();
63
- debug(`Finished fetching ${eventStats.totalEvents} analytics events in ${fetchEndDate.getTime() - fetchStartDate.getTime()}ms`);
64
63
 
65
- const aggregateStartDate = new Date();
66
- debug(`Starting email analytics aggregation for ${eventStats.emailIds.length} emails`);
67
- await this.service.aggregateStats(eventStats);
68
- const aggregateEndDate = new Date();
69
- debug(`Finished aggregating email analytics in ${aggregateEndDate.getTime() - aggregateStartDate.getTime()}ms`);
64
+ logging.info(`[EmailAnalytics] Fetched ${totalEvents} events and aggregated stats in ${fetchEndDate.getTime() - fetchStartDate.getTime()}ms (latest)`);
65
+ return totalEvents;
66
+ }
70
67
 
71
- logging.info(`Fetched ${eventStats.totalEvents} events and aggregated stats for ${eventStats.emailIds.length} emails in ${aggregateEndDate.getTime() - fetchStartDate.getTime()}ms`);
68
+ async fetchMissing({maxEvents} = {maxEvents: Infinity}) {
69
+ logging.info('[EmailAnalytics] Fetch missing started');
72
70
 
73
- return eventStats;
71
+ const fetchStartDate = new Date();
72
+ const totalEvents = await this.service.fetchMissing({maxEvents});
73
+ const fetchEndDate = new Date();
74
+
75
+ logging.info(`[EmailAnalytics] Fetched ${totalEvents} events and aggregated stats in ${fetchEndDate.getTime() - fetchStartDate.getTime()}ms (missing)`);
76
+ return totalEvents;
77
+ }
78
+
79
+ async fetchScheduled({maxEvents}) {
80
+ if (maxEvents < 300) {
81
+ return 0;
82
+ }
83
+ logging.info('[EmailAnalytics] Fetch scheduled started');
84
+
85
+ const fetchStartDate = new Date();
86
+ const totalEvents = await this.service.fetchScheduled({maxEvents});
87
+ const fetchEndDate = new Date();
88
+
89
+ logging.info(`[EmailAnalytics] Fetched ${totalEvents} events and aggregated stats in ${fetchEndDate.getTime() - fetchStartDate.getTime()}ms (scheduled)`);
90
+ return totalEvents;
74
91
  }
75
92
 
76
93
  async startFetch() {
@@ -80,12 +97,14 @@ class EmailAnalyticsServiceWrapper {
80
97
  }
81
98
  this.fetching = true;
82
99
 
83
- logging.info('Email analytics fetch started');
84
100
  try {
85
- const eventStats = await this.fetchLatest();
101
+ const c1 = await this.fetchLatest({maxEvents: Infinity});
102
+ const c2 = await this.fetchMissing({maxEvents: Infinity});
103
+
104
+ // Only fetch scheduled if we didn't fetch a lot of normal events
105
+ await this.fetchScheduled({maxEvents: 20000 - c1 - c2});
86
106
 
87
107
  this.fetching = false;
88
- return eventStats;
89
108
  } catch (e) {
90
109
  logging.error(e, 'Error while fetching email analytics');
91
110
 
@@ -67,7 +67,8 @@ class EmailServiceWrapper {
67
67
  linkReplacer,
68
68
  linkTracking,
69
69
  memberAttributionService: memberAttribution.service,
70
- audienceFeedbackService: audienceFeedback.service
70
+ audienceFeedbackService: audienceFeedback.service,
71
+ outboundLinkTagger: memberAttribution.outboundLinkTagger
71
72
  });
72
73
 
73
74
  const sendingService = new SendingService({
@@ -400,13 +400,13 @@ const PostEmailSerializer = {
400
400
 
401
401
  if (isSite) {
402
402
  // Add newsletter name as ref to the URL
403
- url = memberAttribution.service.addOutboundLinkTagging(url, newsletter);
403
+ url = memberAttribution.outboundLinkTagger.addToUrl(url, newsletter);
404
404
 
405
405
  // Only add post attribution to our own site (because external sites could/should not process this information)
406
406
  url = memberAttribution.service.addPostAttributionTracking(url, post);
407
407
  } else {
408
408
  // Add email source attribution without the newsletter name
409
- url = memberAttribution.service.addOutboundLinkTagging(url);
409
+ url = memberAttribution.outboundLinkTagger.addToUrl(url);
410
410
  }
411
411
 
412
412
  // Add link click tracking
@@ -1041,7 +1041,7 @@ a[data-flickr-embed] img {
1041
1041
  }
1042
1042
 
1043
1043
  table.body .kg-bookmark-card {
1044
- width: 90vw !important;
1044
+ width: 90vw;
1045
1045
  }
1046
1046
 
1047
1047
  table.body .kg-bookmark-thumbnail {
@@ -12,7 +12,7 @@ class MemberAttributionServiceWrapper {
12
12
 
13
13
  // Wire up all the dependencies
14
14
  const {
15
- MemberAttributionService, UrlTranslator, ReferrerTranslator, AttributionBuilder
15
+ MemberAttributionService, UrlTranslator, ReferrerTranslator, AttributionBuilder, OutboundLinkTagger
16
16
  } = require('@tryghost/member-attribution');
17
17
  const models = require('../../models');
18
18
 
@@ -33,6 +33,12 @@ class MemberAttributionServiceWrapper {
33
33
 
34
34
  this.attributionBuilder = new AttributionBuilder({urlTranslator, referrerTranslator});
35
35
 
36
+ this.outboundLinkTagger = new OutboundLinkTagger({
37
+ isEnabled: () => !labs.isSet('outboundLinkTagging') || !!settingsCache.get('outbound_link_tagging'),
38
+ getSiteTitle: () => settingsCache.get('title'),
39
+ urlUtils
40
+ });
41
+
36
42
  // Expose the service
37
43
  this.service = new MemberAttributionService({
38
44
  models: {
@@ -41,9 +47,7 @@ class MemberAttributionServiceWrapper {
41
47
  Integration: models.Integration
42
48
  },
43
49
  attributionBuilder: this.attributionBuilder,
44
- getTrackingEnabled: () => !!settingsCache.get('members_track_sources'),
45
- getOutboundLinkTaggingEnabled: () => !labs.isSet('outboundLinkTagging') || !!settingsCache.get('outbound_link_tagging'),
46
- getSiteTitle: () => settingsCache.get('title')
50
+ getTrackingEnabled: () => !!settingsCache.get('members_track_sources')
47
51
  });
48
52
  }
49
53
  }
@@ -54,7 +54,8 @@ module.exports = class BookshelfMentionRepository {
54
54
  sourceAuthor: model.get('source_author'),
55
55
  sourceExcerpt: model.get('source_excerpt'),
56
56
  sourceFavicon: model.get('source_favicon'),
57
- sourceFeaturedImage: model.get('source_featured_image')
57
+ sourceFeaturedImage: model.get('source_featured_image'),
58
+ verified: model.get('verified')
58
59
  });
59
60
  }
60
61
 
@@ -107,7 +108,8 @@ module.exports = class BookshelfMentionRepository {
107
108
  resource_id: mention.resourceId?.toHexString(),
108
109
  resource_type: mention.resourceId ? 'post' : null,
109
110
  payload: mention.payload ? JSON.stringify(mention.payload) : null,
110
- deleted: Mention.isDeleted(mention)
111
+ deleted: Mention.isDeleted(mention),
112
+ verified: mention.verified
111
113
  };
112
114
 
113
115
  const existing = await this.#MentionModel.findOne({id: data.id}, {require: false});
@@ -0,0 +1,20 @@
1
+ const logging = require('@tryghost/logging');
2
+ const oembedService = require('../oembed');
3
+
4
+ module.exports = class WebmentionRequest {
5
+ /**
6
+ * @param {URL} url
7
+ * @returns {Promise<{html: string}>}
8
+ */
9
+ async fetch(url) {
10
+ try {
11
+ const data = await oembedService.fetchPageHtml(url.href);
12
+ return {
13
+ html: data.body
14
+ };
15
+ } catch (err) {
16
+ logging.warn(err);
17
+ return null;
18
+ }
19
+ }
20
+ };
@@ -1,5 +1,6 @@
1
1
  const MentionController = require('./MentionController');
2
2
  const WebmentionMetadata = require('./WebmentionMetadata');
3
+ const WebmentionRequest = require('./WebmentionRequest');
3
4
  const {
4
5
  MentionsAPI,
5
6
  MentionSendingService,
@@ -16,7 +17,7 @@ const outputSerializerUrlUtil = require('../../../server/api/endpoints/utils/ser
16
17
  const urlService = require('../url');
17
18
  const settingsCache = require('../../../shared/settings-cache');
18
19
  const DomainEvents = require('@tryghost/domain-events');
19
- const jobsService = require('../jobs');
20
+ const jobsService = require('../mentions-jobs');
20
21
 
21
22
  function getPostUrl(post) {
22
23
  const jsonModel = {};
@@ -32,6 +33,7 @@ module.exports = {
32
33
  DomainEvents
33
34
  });
34
35
  const webmentionMetadata = new WebmentionMetadata();
36
+ const webmentionRequest = new WebmentionRequest();
35
37
  const discoveryService = new MentionDiscoveryService({externalRequest});
36
38
  const resourceService = new ResourceService({
37
39
  urlUtils,
@@ -47,6 +49,7 @@ module.exports = {
47
49
  const api = new MentionsAPI({
48
50
  repository,
49
51
  webmentionMetadata,
52
+ webmentionRequest,
50
53
  resourceService,
51
54
  routingService
52
55
  });
@@ -0,0 +1 @@
1
+ module.exports = require('./job-service');
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Minimal wrapper around our external lib
3
+ * Intended for passing any Ghost internals such as logging and config
4
+ */
5
+
6
+ const JobManager = require('@tryghost/job-manager');
7
+ const logging = require('@tryghost/logging');
8
+ const models = require('../../models');
9
+ const sentry = require('../../../shared/sentry');
10
+ const domainEvents = require('@tryghost/domain-events');
11
+
12
+ const errorHandler = (error, workerMeta) => {
13
+ logging.info(`Capturing error for worker during execution of job: ${workerMeta.name}`);
14
+ logging.error(error);
15
+ sentry.captureException(error);
16
+ };
17
+
18
+ const workerMessageHandler = ({name, message}) => {
19
+ if (typeof message === 'string') {
20
+ logging.info(`Worker for job ${name} sent a message: ${message}`);
21
+ }
22
+ };
23
+
24
+ const initTestMode = () => {
25
+ // Output job queue length every 5 seconds
26
+ setInterval(() => {
27
+ logging.warn(`${jobManager.queue.length()} jobs in the queue. Idle: ${jobManager.queue.idle()}`);
28
+
29
+ const runningScheduledjobs = Object.keys(jobManager.bree.workers);
30
+ if (Object.keys(jobManager.bree.workers).length) {
31
+ logging.warn(`${Object.keys(jobManager.bree.workers).length} jobs running: ${runningScheduledjobs}`);
32
+ }
33
+
34
+ const scheduledJobs = Object.keys(jobManager.bree.intervals);
35
+ if (Object.keys(jobManager.bree.intervals).length) {
36
+ logging.warn(`${Object.keys(jobManager.bree.intervals).length} scheduled jobs: ${scheduledJobs}`);
37
+ }
38
+
39
+ if (runningScheduledjobs.length === 0 && scheduledJobs.length === 0) {
40
+ logging.warn('No scheduled or running jobs');
41
+ }
42
+ }, 5000);
43
+ };
44
+
45
+ const jobManager = new JobManager({errorHandler, workerMessageHandler, JobModel: models.Job, domainEvents});
46
+
47
+ module.exports = jobManager;
48
+ module.exports.initTestMode = initTestMode;
@@ -0,0 +1,78 @@
1
+ const DomainEvents = require('@tryghost/domain-events');
2
+
3
+ const getStripeLiveEnabled = () => {
4
+ const settingsCache = require('../../../shared/settings-cache');
5
+ const stripeConnect = settingsCache.get('stripe_connect_publishable_key');
6
+ const stripeKey = settingsCache.get('stripe_publishable_key');
7
+
8
+ const stripeLiveRegex = /pk_live_/;
9
+
10
+ if (stripeConnect && stripeConnect.match(stripeLiveRegex)) {
11
+ return true;
12
+ } else if (stripeKey && stripeKey.match(stripeLiveRegex)) {
13
+ return true;
14
+ }
15
+
16
+ return false;
17
+ };
18
+
19
+ module.exports = {
20
+ /** @type {import('@tryghost/milestones/lib/MilestonesService')} */
21
+ api: null,
22
+
23
+ /**
24
+ * @returns {Promise<void>}
25
+ */
26
+ async init() {
27
+ if (!this.api) {
28
+ const db = require('../../data/db');
29
+ const MilestoneQueries = require('./MilestoneQueries');
30
+
31
+ const {
32
+ MilestonesService,
33
+ InMemoryMilestoneRepository
34
+ } = require('@tryghost/milestones');
35
+ const config = require('../../../shared/config');
36
+ const milestonesConfig = config.get('milestones');
37
+
38
+ const repository = new InMemoryMilestoneRepository({DomainEvents});
39
+ const queries = new MilestoneQueries({db});
40
+
41
+ this.api = new MilestonesService({
42
+ repository,
43
+ milestonesConfig, // avoid using getters and pass as JSON
44
+ queries
45
+ });
46
+ }
47
+ },
48
+
49
+ /**
50
+ * @returns {Promise<object>}
51
+ */
52
+ async run() {
53
+ const labs = require('../../../shared/labs');
54
+
55
+ if (labs.isSet('milestoneEmails')) {
56
+ const members = await this.api.checkMilestones('members');
57
+ let arr;
58
+ const stripeLiveEnabled = getStripeLiveEnabled();
59
+
60
+ if (stripeLiveEnabled) {
61
+ arr = await this.api.checkMilestones('arr');
62
+ }
63
+
64
+ return {
65
+ members,
66
+ arr
67
+ };
68
+ }
69
+ },
70
+
71
+ /**
72
+ * @returns {Promise<object>}
73
+ */
74
+ async initAndRun() {
75
+ await this.init();
76
+ return await this.run();
77
+ }
78
+ };
@@ -0,0 +1 @@
1
+ module.exports = require('./service');