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.
- package/components/tryghost-adapter-cache-redis-5.35.1.tgz +0 -0
- package/components/{tryghost-adapter-manager-5.34.1.tgz → tryghost-adapter-manager-5.35.1.tgz} +0 -0
- package/components/{tryghost-api-framework-5.34.1.tgz → tryghost-api-framework-5.35.1.tgz} +0 -0
- package/components/tryghost-api-version-compatibility-service-5.35.1.tgz +0 -0
- package/components/tryghost-audience-feedback-5.35.1.tgz +0 -0
- package/components/{tryghost-bootstrap-socket-5.34.1.tgz → tryghost-bootstrap-socket-5.35.1.tgz} +0 -0
- package/components/tryghost-constants-5.35.1.tgz +0 -0
- package/components/tryghost-custom-theme-settings-service-5.35.1.tgz +0 -0
- package/components/tryghost-data-generator-5.35.1.tgz +0 -0
- package/components/{tryghost-domain-events-5.34.1.tgz → tryghost-domain-events-5.35.1.tgz} +0 -0
- package/components/tryghost-dynamic-routing-events-5.35.1.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.35.1.tgz +0 -0
- package/components/tryghost-email-analytics-service-5.35.1.tgz +0 -0
- package/components/tryghost-email-content-generator-5.35.1.tgz +0 -0
- package/components/tryghost-email-events-5.35.1.tgz +0 -0
- package/components/tryghost-email-service-5.35.1.tgz +0 -0
- package/components/{tryghost-email-suppression-list-5.34.1.tgz → tryghost-email-suppression-list-5.35.1.tgz} +0 -0
- package/components/{tryghost-express-dynamic-redirects-5.34.1.tgz → tryghost-express-dynamic-redirects-5.35.1.tgz} +0 -0
- package/components/tryghost-extract-api-key-5.35.1.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.35.1.tgz +0 -0
- package/components/{tryghost-i18n-5.34.1.tgz → tryghost-i18n-5.35.1.tgz} +0 -0
- package/components/{tryghost-importer-revue-5.34.1.tgz → tryghost-importer-revue-5.35.1.tgz} +0 -0
- package/components/tryghost-job-manager-5.35.1.tgz +0 -0
- package/components/tryghost-link-redirects-5.35.1.tgz +0 -0
- package/components/tryghost-link-replacer-5.35.1.tgz +0 -0
- package/components/tryghost-link-tracking-5.35.1.tgz +0 -0
- package/components/{tryghost-magic-link-5.34.1.tgz → tryghost-magic-link-5.35.1.tgz} +0 -0
- package/components/tryghost-mailgun-client-5.35.1.tgz +0 -0
- package/components/tryghost-member-attribution-5.35.1.tgz +0 -0
- package/components/{tryghost-member-events-5.34.1.tgz → tryghost-member-events-5.35.1.tgz} +0 -0
- package/components/{tryghost-members-api-5.34.1.tgz → tryghost-members-api-5.35.1.tgz} +0 -0
- package/components/tryghost-members-csv-5.35.1.tgz +0 -0
- package/components/{tryghost-members-events-service-5.34.1.tgz → tryghost-members-events-service-5.35.1.tgz} +0 -0
- package/components/{tryghost-members-importer-5.34.1.tgz → tryghost-members-importer-5.35.1.tgz} +0 -0
- package/components/tryghost-members-offers-5.35.1.tgz +0 -0
- package/components/tryghost-members-payments-5.35.1.tgz +0 -0
- package/components/tryghost-members-ssr-5.35.1.tgz +0 -0
- package/components/tryghost-members-stripe-service-5.35.1.tgz +0 -0
- package/components/tryghost-milestones-5.35.1.tgz +0 -0
- package/components/tryghost-minifier-5.35.1.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.35.1.tgz +0 -0
- package/components/{tryghost-mw-cache-control-5.34.1.tgz → tryghost-mw-cache-control-5.35.1.tgz} +0 -0
- package/components/{tryghost-mw-error-handler-5.34.1.tgz → tryghost-mw-error-handler-5.35.1.tgz} +0 -0
- package/components/tryghost-mw-session-from-token-5.35.1.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.35.1.tgz +0 -0
- package/components/tryghost-mw-vhost-5.35.1.tgz +0 -0
- package/components/{tryghost-oembed-service-5.34.1.tgz → tryghost-oembed-service-5.35.1.tgz} +0 -0
- package/components/tryghost-package-json-5.35.1.tgz +0 -0
- package/components/tryghost-public-resource-repository-5.35.1.tgz +0 -0
- package/components/tryghost-referrers-5.35.1.tgz +0 -0
- package/components/tryghost-security-5.35.1.tgz +0 -0
- package/components/tryghost-session-service-5.35.1.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.35.1.tgz +0 -0
- package/components/tryghost-slack-notifications-5.35.1.tgz +0 -0
- package/components/{tryghost-staff-service-5.34.1.tgz → tryghost-staff-service-5.35.1.tgz} +0 -0
- package/components/tryghost-stats-service-5.35.1.tgz +0 -0
- package/components/tryghost-tiers-5.35.1.tgz +0 -0
- package/components/tryghost-update-check-service-5.35.1.tgz +0 -0
- package/components/tryghost-verification-trigger-5.35.1.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-5.35.1.tgz +0 -0
- package/components/tryghost-webmentions-5.35.1.tgz +0 -0
- package/core/boot.js +27 -3
- package/core/built/admin/assets/{chunk.143.07f5af56ff872bb0e9e4.js → chunk.143.3e5760df9072a9463476.js} +5 -5
- package/core/built/admin/assets/{chunk.178.2e831ef9072743e38dd1.js → chunk.178.773722be9deba376bacb.js} +4 -4
- package/core/built/admin/assets/{chunk.616.181e1ad6c33f0bec7a65.js → chunk.502.c4afca88c98edad8b268.js} +1621 -1338
- package/core/built/admin/assets/{chunk.616.181e1ad6c33f0bec7a65.js.LICENSE.txt → chunk.502.c4afca88c98edad8b268.js.LICENSE.txt} +43 -0
- package/core/built/admin/assets/{ghost-558c1e319d6e025bfab2054bc0f7fe83.css → ghost-1d4b69d04b8a97e8a1bd83ed5bb20777.css} +1 -1
- package/core/built/admin/assets/{ghost-ad40d109653288e74a7cd922341fb33d.js → ghost-5f530fdaca961aa2ec5c339d5bb13443.js} +4353 -4323
- package/core/built/admin/assets/{ghost-dark-a15754df1f9070dc2525482ce22e2251.css → ghost-dark-633bf4628beead68e8a0d1fa668735ff.css} +1 -1
- package/core/built/admin/assets/{vendor-253d6527ca6353855164ef65f896f762.js → vendor-c4684647d4f5213e5dbb6763de430e7e.js} +2741 -2309
- package/core/built/admin/index.html +6 -6
- package/core/cli/generate-data.js +3 -1
- package/core/server/api/endpoints/emails.js +35 -0
- package/core/server/api/endpoints/images.js +13 -6
- package/core/server/api/endpoints/pages-public.js +3 -1
- package/core/server/api/endpoints/posts-public.js +3 -1
- package/core/server/api/endpoints/utils/serializers/output/emails.js +4 -0
- package/core/server/api/endpoints/utils/serializers/output/mappers/mentions.js +2 -1
- package/core/server/api/endpoints/utils/serializers/output/mappers/posts.js +11 -0
- package/core/server/data/migrations/versions/5.35/2023-02-13-06-24-add-mentions-verified-column.js +7 -0
- package/core/server/data/schema/schema.js +2 -1
- package/core/server/lib/request-external.js +1 -0
- package/core/server/models/mention.js +2 -1
- package/core/server/services/email-analytics/lib/queries.js +18 -3
- package/core/server/services/email-analytics/wrapper.js +34 -15
- package/core/server/services/email-service/wrapper.js +2 -1
- package/core/server/services/mega/post-email-serializer.js +2 -2
- package/core/server/services/mega/template.js +1 -1
- package/core/server/services/member-attribution/index.js +8 -4
- package/core/server/services/mentions/BookshelfMentionRepository.js +4 -2
- package/core/server/services/mentions/WebmentionRequest.js +20 -0
- package/core/server/services/mentions/service.js +4 -1
- package/core/server/services/mentions-jobs/index.js +1 -0
- package/core/server/services/mentions-jobs/job-service.js +48 -0
- package/core/server/services/{milestone-emails → milestones}/MilestoneQueries.js +0 -0
- package/core/server/services/{milestone-emails → milestones}/index.js +0 -0
- package/core/server/services/milestones/service.js +78 -0
- package/core/server/services/posts-public/index.js +1 -0
- package/core/server/services/posts-public/service.js +31 -0
- package/core/server/services/slack-notifications/index.js +1 -0
- package/core/server/services/slack-notifications/service.js +60 -0
- package/core/server/services/tags-public/service.js +5 -5
- package/core/server/services/websockets/index.js +1 -0
- package/core/server/services/websockets/service.js +37 -0
- package/core/server/web/api/endpoints/admin/routes.js +3 -0
- package/core/shared/config/defaults.json +2 -2
- package/core/shared/labs.js +4 -3
- package/package.json +121 -118
- package/yarn.lock +307 -102
- package/components/tryghost-adapter-cache-redis-5.34.1.tgz +0 -0
- package/components/tryghost-api-version-compatibility-service-5.34.1.tgz +0 -0
- package/components/tryghost-audience-feedback-5.34.1.tgz +0 -0
- package/components/tryghost-constants-5.34.1.tgz +0 -0
- package/components/tryghost-custom-theme-settings-service-5.34.1.tgz +0 -0
- package/components/tryghost-data-generator-5.34.1.tgz +0 -0
- package/components/tryghost-dynamic-routing-events-5.34.1.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.34.1.tgz +0 -0
- package/components/tryghost-email-analytics-service-5.34.1.tgz +0 -0
- package/components/tryghost-email-content-generator-5.34.1.tgz +0 -0
- package/components/tryghost-email-events-5.34.1.tgz +0 -0
- package/components/tryghost-email-service-5.34.1.tgz +0 -0
- package/components/tryghost-extract-api-key-5.34.1.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.34.1.tgz +0 -0
- package/components/tryghost-job-manager-5.34.1.tgz +0 -0
- package/components/tryghost-link-redirects-5.34.1.tgz +0 -0
- package/components/tryghost-link-replacer-5.34.1.tgz +0 -0
- package/components/tryghost-link-tracking-5.34.1.tgz +0 -0
- package/components/tryghost-mailgun-client-5.34.1.tgz +0 -0
- package/components/tryghost-member-attribution-5.34.1.tgz +0 -0
- package/components/tryghost-members-csv-5.34.1.tgz +0 -0
- package/components/tryghost-members-offers-5.34.1.tgz +0 -0
- package/components/tryghost-members-payments-5.34.1.tgz +0 -0
- package/components/tryghost-members-ssr-5.34.1.tgz +0 -0
- package/components/tryghost-members-stripe-service-5.34.1.tgz +0 -0
- package/components/tryghost-milestone-emails-5.34.1.tgz +0 -0
- package/components/tryghost-minifier-5.34.1.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.34.1.tgz +0 -0
- package/components/tryghost-mw-session-from-token-5.34.1.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.34.1.tgz +0 -0
- package/components/tryghost-mw-vhost-5.34.1.tgz +0 -0
- package/components/tryghost-package-json-5.34.1.tgz +0 -0
- package/components/tryghost-referrers-5.34.1.tgz +0 -0
- package/components/tryghost-security-5.34.1.tgz +0 -0
- package/components/tryghost-session-service-5.34.1.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.34.1.tgz +0 -0
- package/components/tryghost-stats-service-5.34.1.tgz +0 -0
- package/components/tryghost-tags-public-5.34.1.tgz +0 -0
- package/components/tryghost-tiers-5.34.1.tgz +0 -0
- package/components/tryghost-update-check-service-5.34.1.tgz +0 -0
- package/components/tryghost-verification-trigger-5.34.1.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-5.34.1.tgz +0 -0
- package/components/tryghost-webmentions-5.34.1.tgz +0 -0
- 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.
|
|
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-
|
|
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-
|
|
60
|
-
<script src="assets/chunk.
|
|
61
|
-
<script src="assets/chunk.143.
|
|
62
|
-
<script src="assets/ghost-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
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
|
|
39
|
+
return postsPublicService.api.browse(frame.options);
|
|
38
40
|
}
|
|
39
41
|
},
|
|
40
42
|
|
|
@@ -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
|
|
@@ -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)) {
|
|
@@ -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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
68
|
+
async fetchMissing({maxEvents} = {maxEvents: Infinity}) {
|
|
69
|
+
logging.info('[EmailAnalytics] Fetch missing started');
|
|
72
70
|
|
|
73
|
-
|
|
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
|
|
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.
|
|
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.
|
|
409
|
+
url = memberAttribution.outboundLinkTagger.addToUrl(url);
|
|
410
410
|
}
|
|
411
411
|
|
|
412
412
|
// Add link click tracking
|
|
@@ -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;
|
|
File without changes
|
|
File without changes
|
|
@@ -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');
|