ghost 5.15.0 → 5.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/tryghost-adapter-manager-5.16.0.tgz +0 -0
- package/components/{tryghost-api-framework-5.15.0.tgz → tryghost-api-framework-5.16.0.tgz} +0 -0
- package/components/{tryghost-api-version-compatibility-service-5.15.0.tgz → tryghost-api-version-compatibility-service-5.16.0.tgz} +0 -0
- package/components/tryghost-bootstrap-socket-5.16.0.tgz +0 -0
- package/components/{tryghost-constants-5.15.0.tgz → tryghost-constants-5.16.0.tgz} +0 -0
- package/components/tryghost-custom-theme-settings-service-5.16.0.tgz +0 -0
- package/components/{tryghost-domain-events-5.15.0.tgz → tryghost-domain-events-5.16.0.tgz} +0 -0
- package/components/{tryghost-email-analytics-provider-mailgun-5.15.0.tgz → tryghost-email-analytics-provider-mailgun-5.16.0.tgz} +0 -0
- package/components/{tryghost-email-analytics-service-5.15.0.tgz → tryghost-email-analytics-service-5.16.0.tgz} +0 -0
- package/components/{tryghost-email-content-generator-5.15.0.tgz → tryghost-email-content-generator-5.16.0.tgz} +0 -0
- package/components/{tryghost-express-dynamic-redirects-5.15.0.tgz → tryghost-express-dynamic-redirects-5.16.0.tgz} +0 -0
- package/components/tryghost-extract-api-key-5.16.0.tgz +0 -0
- package/components/{tryghost-html-to-plaintext-5.15.0.tgz → tryghost-html-to-plaintext-5.16.0.tgz} +0 -0
- package/components/{tryghost-job-manager-5.15.0.tgz → tryghost-job-manager-5.16.0.tgz} +0 -0
- package/components/tryghost-link-redirects-5.16.0.tgz +0 -0
- package/components/tryghost-link-replacer-5.16.0.tgz +0 -0
- package/components/tryghost-link-tracking-5.16.0.tgz +0 -0
- package/components/tryghost-magic-link-5.16.0.tgz +0 -0
- package/components/tryghost-mailgun-client-5.16.0.tgz +0 -0
- package/components/{tryghost-member-analytics-service-5.15.0.tgz → tryghost-member-analytics-service-5.16.0.tgz} +0 -0
- package/components/tryghost-member-attribution-5.16.0.tgz +0 -0
- package/components/tryghost-member-events-5.16.0.tgz +0 -0
- package/components/{tryghost-members-analytics-ingress-5.15.0.tgz → tryghost-members-analytics-ingress-5.16.0.tgz} +0 -0
- package/components/tryghost-members-api-5.16.0.tgz +0 -0
- package/components/{tryghost-members-csv-5.15.0.tgz → tryghost-members-csv-5.16.0.tgz} +0 -0
- package/components/tryghost-members-events-service-5.16.0.tgz +0 -0
- package/components/{tryghost-members-importer-5.15.0.tgz → tryghost-members-importer-5.16.0.tgz} +0 -0
- package/components/{tryghost-members-offers-5.15.0.tgz → tryghost-members-offers-5.16.0.tgz} +0 -0
- package/components/{tryghost-members-payments-5.15.0.tgz → tryghost-members-payments-5.16.0.tgz} +0 -0
- package/components/{tryghost-members-ssr-5.15.0.tgz → tryghost-members-ssr-5.16.0.tgz} +0 -0
- package/components/tryghost-members-stripe-service-5.16.0.tgz +0 -0
- package/components/tryghost-minifier-5.16.0.tgz +0 -0
- package/components/{tryghost-mw-api-version-mismatch-5.15.0.tgz → tryghost-mw-api-version-mismatch-5.16.0.tgz} +0 -0
- package/components/{tryghost-mw-cache-control-5.15.0.tgz → tryghost-mw-cache-control-5.16.0.tgz} +0 -0
- package/components/{tryghost-mw-error-handler-5.15.0.tgz → tryghost-mw-error-handler-5.16.0.tgz} +0 -0
- package/components/tryghost-mw-session-from-token-5.16.0.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.16.0.tgz +0 -0
- package/components/tryghost-mw-vhost-5.16.0.tgz +0 -0
- package/components/{tryghost-oembed-service-5.15.0.tgz → tryghost-oembed-service-5.16.0.tgz} +0 -0
- package/components/{tryghost-package-json-5.15.0.tgz → tryghost-package-json-5.16.0.tgz} +0 -0
- package/components/tryghost-referrers-5.16.0.tgz +0 -0
- package/components/tryghost-security-5.16.0.tgz +0 -0
- package/components/tryghost-session-service-5.16.0.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.16.0.tgz +0 -0
- package/components/{tryghost-staff-service-5.15.0.tgz → tryghost-staff-service-5.16.0.tgz} +0 -0
- package/components/tryghost-stats-service-5.16.0.tgz +0 -0
- package/components/tryghost-update-check-service-5.16.0.tgz +0 -0
- package/components/{tryghost-verification-trigger-5.15.0.tgz → tryghost-verification-trigger-5.16.0.tgz} +0 -0
- package/components/{tryghost-version-notifications-data-service-5.15.0.tgz → tryghost-version-notifications-data-service-5.16.0.tgz} +0 -0
- package/core/boot.js +2 -4
- package/core/built/admin/assets/{chunk.143.558b9943af7b15f189ae.js → chunk.143.a281d460e6059cd0210a.js} +6 -6
- package/core/built/admin/assets/{chunk.178.a9f6ddaea01e2bc76235.js → chunk.178.68eca2346b6f343991e7.js} +4 -4
- package/core/built/admin/assets/{chunk.579.dc11bf8dda5cf4406708.js → chunk.579.d14c3688558f34afeb3e.js} +8462 -7944
- package/core/built/admin/assets/{chunk.579.dc11bf8dda5cf4406708.js.LICENSE.txt → chunk.579.d14c3688558f34afeb3e.js.LICENSE.txt} +45 -0
- package/core/built/admin/assets/ghost-6491d134c450ca676911ea17e16cd7d4.css +1 -0
- package/core/built/admin/assets/ghost-dark-297ab2fcf4cadd1c950b84089a38c5e2.css +1 -0
- package/core/built/admin/assets/{ghost-4b1b550e34300f5f4774a261aac29557.js → ghost-f2bf99b26aee662cf37fe59f87b1ceb5.js} +259 -194
- package/core/built/admin/assets/img/marketing/analytics-1-aa2d72c4e7347a3cb5666d07916b92aa.jpg +0 -0
- package/core/built/admin/assets/img/marketing/analytics-2-389d53f80041ff98111cce79facf66b8.jpg +0 -0
- package/core/built/admin/assets/{vendor-271c32988ab16ba175a9bfa2acb2887a.js → vendor-b2375e2f383cbc3fd73340c4b656c993.js} +48 -42
- package/core/built/admin/index.html +6 -6
- package/core/frontend/helpers/search.js +1 -15
- package/core/frontend/src/member-attribution/member-attribution.js +38 -4
- package/core/server/api/endpoints/index.js +4 -0
- package/core/server/api/endpoints/links.js +25 -0
- package/core/server/api/endpoints/posts.js +2 -1
- package/core/server/api/endpoints/stats.js +24 -0
- package/core/server/api/endpoints/utils/serializers/input/posts.js +6 -1
- package/core/server/api/endpoints/utils/serializers/output/mappers/activity-feed-events.js +51 -0
- package/core/server/api/endpoints/utils/serializers/output/mappers/comments.js +10 -1
- package/core/server/api/endpoints/utils/serializers/output/mappers/posts.js +1 -1
- package/core/server/data/exporter/table-lists.js +3 -1
- package/core/server/data/migrations/versions/5.16/2022-09-19-09-04-add-link-redirects-table.js +10 -0
- package/core/server/data/migrations/versions/5.16/2022-09-19-09-05-add-members-link-click-events-table.js +8 -0
- package/core/server/data/migrations/versions/5.16/2022-09-19-17-44-add-referrer-columns-to-member-events-table.js +21 -0
- package/core/server/data/migrations/versions/5.16/2022-09-19-17-44-add-referrer-columns-to-subscription-events-table.js +21 -0
- package/core/server/data/schema/schema.js +21 -1
- package/core/server/models/link-redirect.js +65 -0
- package/core/server/models/member-link-click-event.js +26 -0
- package/core/server/models/post.js +21 -5
- package/core/server/services/bulk-email/bulk-email-processor.js +7 -5
- package/core/server/services/link-redirection/LinkRedirectRepository.js +88 -0
- package/core/server/services/link-redirection/index.js +9 -11
- package/core/server/services/link-tracking/LinkClickRepository.js +69 -0
- package/core/server/services/link-tracking/PostLinkRepository.js +62 -0
- package/core/server/services/link-tracking/index.js +48 -0
- package/core/server/services/mega/post-email-serializer.js +28 -2
- package/core/server/services/member-attribution/index.js +12 -5
- package/core/server/services/members/api.js +1 -0
- package/core/server/web/admin/app.js +8 -2
- package/core/server/web/api/endpoints/admin/routes.js +5 -0
- package/core/shared/config/defaults.json +5 -5
- package/package.json +109 -108
- package/yarn.lock +535 -318
- package/components/tryghost-adapter-manager-5.15.0.tgz +0 -0
- package/components/tryghost-bootstrap-socket-5.15.0.tgz +0 -0
- package/components/tryghost-custom-theme-settings-service-5.15.0.tgz +0 -0
- package/components/tryghost-extract-api-key-5.15.0.tgz +0 -0
- package/components/tryghost-link-redirects-5.15.0.tgz +0 -0
- package/components/tryghost-link-replacement-5.15.0.tgz +0 -0
- package/components/tryghost-link-tracking-5.15.0.tgz +0 -0
- package/components/tryghost-magic-link-5.15.0.tgz +0 -0
- package/components/tryghost-mailgun-client-5.15.0.tgz +0 -0
- package/components/tryghost-member-attribution-5.15.0.tgz +0 -0
- package/components/tryghost-member-events-5.15.0.tgz +0 -0
- package/components/tryghost-members-api-5.15.0.tgz +0 -0
- package/components/tryghost-members-events-service-5.15.0.tgz +0 -0
- package/components/tryghost-members-stripe-service-5.15.0.tgz +0 -0
- package/components/tryghost-minifier-5.15.0.tgz +0 -0
- package/components/tryghost-mw-session-from-token-5.15.0.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.15.0.tgz +0 -0
- package/components/tryghost-mw-vhost-5.15.0.tgz +0 -0
- package/components/tryghost-security-5.15.0.tgz +0 -0
- package/components/tryghost-session-service-5.15.0.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.15.0.tgz +0 -0
- package/components/tryghost-update-check-service-5.15.0.tgz +0 -0
- package/core/built/admin/assets/ghost-c933adafb359b75ea1577365ce252e76.css +0 -1
- package/core/built/admin/assets/ghost-dark-04981c84bf590e0fae0a8e83e018190f.css +0 -1
- package/core/server/services/link-click-tracking/index.js +0 -25
- package/core/server/services/link-replacement/index.js +0 -24
|
@@ -6,6 +6,7 @@ const localUtils = require('../../index');
|
|
|
6
6
|
const mobiledoc = require('../../../../../lib/mobiledoc');
|
|
7
7
|
const postsMetaSchema = require('../../../../../data/schema').tables.posts_meta;
|
|
8
8
|
const clean = require('./utils/clean');
|
|
9
|
+
const labs = require('../../../../../../shared/labs');
|
|
9
10
|
|
|
10
11
|
function removeSourceFormats(frame) {
|
|
11
12
|
if (frame.options.formats?.includes('mobiledoc') || frame.options.formats?.includes('lexical')) {
|
|
@@ -24,7 +25,11 @@ function defaultRelations(frame) {
|
|
|
24
25
|
return false;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
if (labs.isSet('emailClicks')) {
|
|
29
|
+
frame.options.withRelated = ['tags', 'authors', 'authors.roles', 'email', 'tiers', 'newsletter', 'count.signups', 'count.conversions', 'count.clicks'];
|
|
30
|
+
} else {
|
|
31
|
+
frame.options.withRelated = ['tags', 'authors', 'authors.roles', 'email', 'tiers', 'newsletter', 'count.signups', 'count.conversions'];
|
|
32
|
+
}
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
function setDefaultOrder(frame) {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
const mapComment = require('./comments');
|
|
2
|
+
const url = require('../utils/url');
|
|
3
|
+
const _ = require('lodash');
|
|
2
4
|
|
|
3
5
|
const commentEventMapper = (json, frame) => {
|
|
4
6
|
return {
|
|
@@ -7,10 +9,59 @@ const commentEventMapper = (json, frame) => {
|
|
|
7
9
|
};
|
|
8
10
|
};
|
|
9
11
|
|
|
12
|
+
const clickEventMapper = (json, frame) => {
|
|
13
|
+
const memberFields = [
|
|
14
|
+
'id',
|
|
15
|
+
'uuid',
|
|
16
|
+
'name',
|
|
17
|
+
'email',
|
|
18
|
+
'avatar_image'
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const linkFields = [
|
|
22
|
+
'from',
|
|
23
|
+
'to'
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const postFields = [
|
|
27
|
+
'id',
|
|
28
|
+
'uuid',
|
|
29
|
+
'title',
|
|
30
|
+
'url'
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const data = json.data;
|
|
34
|
+
const response = {};
|
|
35
|
+
|
|
36
|
+
if (data.link && data.link.post) {
|
|
37
|
+
// We could use the post mapper here, but we need less field + don't need al the async behavior support
|
|
38
|
+
url.forPost(data.link.post.id, data.link.post, frame);
|
|
39
|
+
response.post = _.pick(data.link.post, postFields);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (data.link) {
|
|
43
|
+
response.link = _.pick(data.link, linkFields);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (data.member) {
|
|
47
|
+
response.member = _.pick(data.member, memberFields);
|
|
48
|
+
} else {
|
|
49
|
+
response.member = null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
...json,
|
|
54
|
+
data: response
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
|
|
10
58
|
const activityFeedMapper = (event, frame) => {
|
|
11
59
|
if (event.type === 'comment_event') {
|
|
12
60
|
return commentEventMapper(event, frame);
|
|
13
61
|
}
|
|
62
|
+
if (event.type === 'click_event') {
|
|
63
|
+
return clickEventMapper(event, frame);
|
|
64
|
+
}
|
|
14
65
|
return event;
|
|
15
66
|
};
|
|
16
67
|
|
|
@@ -18,6 +18,15 @@ const memberFields = [
|
|
|
18
18
|
'avatar_image'
|
|
19
19
|
];
|
|
20
20
|
|
|
21
|
+
const memberFieldsAdmin = [
|
|
22
|
+
'id',
|
|
23
|
+
'uuid',
|
|
24
|
+
'name',
|
|
25
|
+
'email',
|
|
26
|
+
'expertise',
|
|
27
|
+
'avatar_image'
|
|
28
|
+
];
|
|
29
|
+
|
|
21
30
|
const postFields = [
|
|
22
31
|
'id',
|
|
23
32
|
'uuid',
|
|
@@ -36,7 +45,7 @@ const commentMapper = (model, frame) => {
|
|
|
36
45
|
const response = _.pick(jsonModel, commentFields);
|
|
37
46
|
|
|
38
47
|
if (jsonModel.member) {
|
|
39
|
-
response.member = _.pick(jsonModel.member, memberFields);
|
|
48
|
+
response.member = _.pick(jsonModel.member, utils.isMembersAPI(frame) ? memberFields : memberFieldsAdmin);
|
|
40
49
|
} else {
|
|
41
50
|
response.member = null;
|
|
42
51
|
}
|
package/core/server/data/migrations/versions/5.16/2022-09-19-09-04-add-link-redirects-table.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const {addTable} = require('../../utils');
|
|
2
|
+
|
|
3
|
+
module.exports = addTable('link_redirects', {
|
|
4
|
+
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
5
|
+
from: {type: 'string', maxlength: 2000, nullable: false},
|
|
6
|
+
to: {type: 'string', maxlength: 2000, nullable: false},
|
|
7
|
+
post_id: {type: 'string', maxlength: 24, nullable: true, unique: false, references: 'posts.id', setNullDelete: true},
|
|
8
|
+
created_at: {type: 'dateTime', nullable: false},
|
|
9
|
+
updated_at: {type: 'dateTime', nullable: true}
|
|
10
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const {addTable} = require('../../utils');
|
|
2
|
+
|
|
3
|
+
module.exports = addTable('members_link_click_events', {
|
|
4
|
+
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
5
|
+
member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
|
|
6
|
+
link_id: {type: 'string', maxlength: 24, nullable: false, references: 'link_redirects.id', cascadeDelete: true},
|
|
7
|
+
created_at: {type: 'dateTime', nullable: false}
|
|
8
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const {createAddColumnMigration, combineNonTransactionalMigrations} = require('../../utils');
|
|
2
|
+
|
|
3
|
+
module.exports = combineNonTransactionalMigrations(
|
|
4
|
+
createAddColumnMigration('members_created_events', 'referrer_source', {
|
|
5
|
+
type: 'string',
|
|
6
|
+
maxlength: 191,
|
|
7
|
+
nullable: true
|
|
8
|
+
}),
|
|
9
|
+
|
|
10
|
+
createAddColumnMigration('members_created_events', 'referrer_medium', {
|
|
11
|
+
type: 'string',
|
|
12
|
+
maxlength: 191,
|
|
13
|
+
nullable: true
|
|
14
|
+
}),
|
|
15
|
+
|
|
16
|
+
createAddColumnMigration('members_created_events', 'referrer_url', {
|
|
17
|
+
type: 'string',
|
|
18
|
+
maxlength: 2000,
|
|
19
|
+
nullable: true
|
|
20
|
+
})
|
|
21
|
+
);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const {createAddColumnMigration, combineNonTransactionalMigrations} = require('../../utils');
|
|
2
|
+
|
|
3
|
+
module.exports = combineNonTransactionalMigrations(
|
|
4
|
+
createAddColumnMigration('members_subscription_created_events', 'referrer_source', {
|
|
5
|
+
type: 'string',
|
|
6
|
+
maxlength: 191,
|
|
7
|
+
nullable: true
|
|
8
|
+
}),
|
|
9
|
+
|
|
10
|
+
createAddColumnMigration('members_subscription_created_events', 'referrer_medium', {
|
|
11
|
+
type: 'string',
|
|
12
|
+
maxlength: 191,
|
|
13
|
+
nullable: true
|
|
14
|
+
}),
|
|
15
|
+
|
|
16
|
+
createAddColumnMigration('members_subscription_created_events', 'referrer_url', {
|
|
17
|
+
type: 'string',
|
|
18
|
+
maxlength: 2000,
|
|
19
|
+
nullable: true
|
|
20
|
+
})
|
|
21
|
+
);
|
|
@@ -497,6 +497,9 @@ module.exports = {
|
|
|
497
497
|
}
|
|
498
498
|
},
|
|
499
499
|
attribution_url: {type: 'string', maxlength: 2000, nullable: true},
|
|
500
|
+
referrer_source: {type: 'string', maxlength: 191, nullable: true},
|
|
501
|
+
referrer_medium: {type: 'string', maxlength: 191, nullable: true},
|
|
502
|
+
referrer_url: {type: 'string', maxlength: 2000, nullable: true},
|
|
500
503
|
source: {
|
|
501
504
|
type: 'string', maxlength: 50, nullable: false, validations: {
|
|
502
505
|
isIn: [['member', 'import', 'system', 'api', 'admin']]
|
|
@@ -634,7 +637,10 @@ module.exports = {
|
|
|
634
637
|
isIn: [['url', 'post', 'page', 'author', 'tag']]
|
|
635
638
|
}
|
|
636
639
|
},
|
|
637
|
-
attribution_url: {type: 'string', maxlength: 2000, nullable: true}
|
|
640
|
+
attribution_url: {type: 'string', maxlength: 2000, nullable: true},
|
|
641
|
+
referrer_source: {type: 'string', maxlength: 191, nullable: true},
|
|
642
|
+
referrer_medium: {type: 'string', maxlength: 191, nullable: true},
|
|
643
|
+
referrer_url: {type: 'string', maxlength: 2000, nullable: true}
|
|
638
644
|
},
|
|
639
645
|
offer_redemptions: {
|
|
640
646
|
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
@@ -829,5 +835,19 @@ module.exports = {
|
|
|
829
835
|
finished_at: {type: 'dateTime', nullable: true},
|
|
830
836
|
created_at: {type: 'dateTime', nullable: false},
|
|
831
837
|
updated_at: {type: 'dateTime', nullable: true}
|
|
838
|
+
},
|
|
839
|
+
link_redirects: {
|
|
840
|
+
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
841
|
+
from: {type: 'string', maxlength: 2000, nullable: false},
|
|
842
|
+
to: {type: 'string', maxlength: 2000, nullable: false},
|
|
843
|
+
post_id: {type: 'string', maxlength: 24, nullable: true, unique: false, references: 'posts.id', setNullDelete: true},
|
|
844
|
+
created_at: {type: 'dateTime', nullable: false},
|
|
845
|
+
updated_at: {type: 'dateTime', nullable: true}
|
|
846
|
+
},
|
|
847
|
+
members_link_click_events: {
|
|
848
|
+
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
849
|
+
member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
|
|
850
|
+
link_id: {type: 'string', maxlength: 24, nullable: false, references: 'link_redirects.id', cascadeDelete: true},
|
|
851
|
+
created_at: {type: 'dateTime', nullable: false}
|
|
832
852
|
}
|
|
833
853
|
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const ghostBookshelf = require('./base');
|
|
2
|
+
const urlUtils = require('../../shared/url-utils');
|
|
3
|
+
|
|
4
|
+
const LinkRedirect = ghostBookshelf.Model.extend({
|
|
5
|
+
tableName: 'link_redirects',
|
|
6
|
+
|
|
7
|
+
post() {
|
|
8
|
+
return this.belongsTo('Post', 'post_id');
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
formatOnWrite(attrs) {
|
|
12
|
+
if (attrs.to) {
|
|
13
|
+
attrs.to = urlUtils.absoluteToTransformReady(attrs.to);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return attrs;
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
parse() {
|
|
20
|
+
const attrs = ghostBookshelf.Model.prototype.parse.apply(this, arguments);
|
|
21
|
+
|
|
22
|
+
if (attrs.to) {
|
|
23
|
+
attrs.to = urlUtils.transformReadyToAbsolute(attrs.to);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return attrs;
|
|
27
|
+
}
|
|
28
|
+
}, {
|
|
29
|
+
orderDefaultRaw(options) {
|
|
30
|
+
if (options.withRelated && options.withRelated.includes('count.clicks')) {
|
|
31
|
+
return '`count__clicks` DESC, `to` DESC';
|
|
32
|
+
}
|
|
33
|
+
return '`to` DESC';
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
permittedOptions(methodName) {
|
|
37
|
+
let options = ghostBookshelf.Model.permittedOptions.call(this, methodName);
|
|
38
|
+
const validOptions = {
|
|
39
|
+
findAll: ['filter', 'columns', 'withRelated']
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
if (validOptions[methodName]) {
|
|
43
|
+
options = options.concat(validOptions[methodName]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return options;
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
countRelations() {
|
|
50
|
+
return {
|
|
51
|
+
clicks(modelOrCollection) {
|
|
52
|
+
modelOrCollection.query('columns', 'link_redirects.*', (qb) => {
|
|
53
|
+
qb.countDistinct('members_link_click_events.member_id')
|
|
54
|
+
.from('members_link_click_events')
|
|
55
|
+
.whereRaw('link_redirects.id = members_link_click_events.link_id')
|
|
56
|
+
.as('count__clicks');
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
module.exports = {
|
|
64
|
+
LinkRedirect: ghostBookshelf.model('LinkRedirect', LinkRedirect)
|
|
65
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const errors = require('@tryghost/errors');
|
|
2
|
+
const ghostBookshelf = require('./base');
|
|
3
|
+
|
|
4
|
+
const MemberLinkClickEvent = ghostBookshelf.Model.extend({
|
|
5
|
+
tableName: 'members_link_click_events',
|
|
6
|
+
|
|
7
|
+
link() {
|
|
8
|
+
return this.belongsTo('LinkRedirect', 'link_id');
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
member() {
|
|
12
|
+
return this.belongsTo('Member', 'member_id', 'id');
|
|
13
|
+
}
|
|
14
|
+
}, {
|
|
15
|
+
async edit() {
|
|
16
|
+
throw new errors.IncorrectUsageError({message: 'Cannot edit MemberLinkClickEvent'});
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
async destroy() {
|
|
20
|
+
throw new errors.IncorrectUsageError({message: 'Cannot destroy MemberLinkClickEvent'});
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
MemberLinkClickEvent: ghostBookshelf.model('MemberLinkClickEvent', MemberLinkClickEvent)
|
|
26
|
+
};
|
|
@@ -26,7 +26,11 @@ const messages = {
|
|
|
26
26
|
expectedPublishedAtInFuture: 'Date must be at least {cannotScheduleAPostBeforeInMinutes} minutes in the future.',
|
|
27
27
|
untitled: '(Untitled)',
|
|
28
28
|
notEnoughPermission: 'You do not have permission to perform this action',
|
|
29
|
-
invalidNewsletter: 'The newsletter parameter doesn\'t match any active newsletter.'
|
|
29
|
+
invalidNewsletter: 'The newsletter parameter doesn\'t match any active newsletter.',
|
|
30
|
+
invalidMobiledocStructure: 'Invalid mobiledoc structure.',
|
|
31
|
+
invalidMobiledocStructureHelp: 'https://ghost.org/docs/publishing/',
|
|
32
|
+
invalidLexicalStructure: 'Invalid lexical structure.',
|
|
33
|
+
invalidLexicalStructureHelp: 'https://ghost.org/docs/publishing/'
|
|
30
34
|
};
|
|
31
35
|
|
|
32
36
|
const MOBILEDOC_REVISIONS_COUNT = 10;
|
|
@@ -130,6 +134,7 @@ Post = ghostBookshelf.Model.extend({
|
|
|
130
134
|
// transform URLs from __GHOST_URL__ to absolute
|
|
131
135
|
[
|
|
132
136
|
'mobiledoc',
|
|
137
|
+
'lexical',
|
|
133
138
|
'html',
|
|
134
139
|
'plaintext',
|
|
135
140
|
'custom_excerpt',
|
|
@@ -158,6 +163,7 @@ Post = ghostBookshelf.Model.extend({
|
|
|
158
163
|
cardTransformers: mobiledocLib.cards
|
|
159
164
|
}
|
|
160
165
|
},
|
|
166
|
+
lexical: 'lexicalToTransformReady',
|
|
161
167
|
html: 'htmlToTransformReady',
|
|
162
168
|
plaintext: 'plaintextToTransformReady',
|
|
163
169
|
custom_excerpt: 'htmlToTransformReady',
|
|
@@ -623,7 +629,7 @@ Post = ghostBookshelf.Model.extend({
|
|
|
623
629
|
this.set('html', mobiledocLib.mobiledocHtmlRenderer.render(JSON.parse(this.get('mobiledoc'))));
|
|
624
630
|
} catch (err) {
|
|
625
631
|
throw new errors.ValidationError({
|
|
626
|
-
message:
|
|
632
|
+
message: tpl(messages.invalidMobiledocStructure),
|
|
627
633
|
help: 'https://ghost.org/docs/publishing/'
|
|
628
634
|
});
|
|
629
635
|
}
|
|
@@ -644,9 +650,10 @@ Post = ghostBookshelf.Model.extend({
|
|
|
644
650
|
this.set('html', lexicalLib.lexicalHtmlRenderer.render(this.get('lexical')));
|
|
645
651
|
} catch (err) {
|
|
646
652
|
throw new errors.ValidationError({
|
|
647
|
-
message:
|
|
648
|
-
|
|
649
|
-
property: 'lexical'
|
|
653
|
+
message: tpl(messages.invalidLexicalStructure),
|
|
654
|
+
context: err.message,
|
|
655
|
+
property: 'lexical',
|
|
656
|
+
help: tpl(messages.invalidLexicalStructureHelp)
|
|
650
657
|
});
|
|
651
658
|
}
|
|
652
659
|
}
|
|
@@ -1338,6 +1345,15 @@ Post = ghostBookshelf.Model.extend({
|
|
|
1338
1345
|
.whereRaw('posts.id = members_subscription_created_events.attribution_id')
|
|
1339
1346
|
.as('count__conversions');
|
|
1340
1347
|
});
|
|
1348
|
+
},
|
|
1349
|
+
clicks(modelOrCollection) {
|
|
1350
|
+
modelOrCollection.query('columns', 'posts.*', (qb) => {
|
|
1351
|
+
qb.countDistinct('members_link_click_events.member_id')
|
|
1352
|
+
.from('members_link_click_events')
|
|
1353
|
+
.join('link_redirects', 'members_link_click_events.link_id', 'link_redirects.id')
|
|
1354
|
+
.whereRaw('posts.id = link_redirects.post_id')
|
|
1355
|
+
.as('count__clicks');
|
|
1356
|
+
});
|
|
1341
1357
|
}
|
|
1342
1358
|
};
|
|
1343
1359
|
}
|
|
@@ -13,7 +13,7 @@ const configService = require('../../../shared/config');
|
|
|
13
13
|
const settingsCache = require('../../../shared/settings-cache');
|
|
14
14
|
|
|
15
15
|
const messages = {
|
|
16
|
-
error: 'The email service was unable to send
|
|
16
|
+
error: 'The email service received an error from mailgun and was unable to send.'
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
const mailgunClient = new MailgunClient({config: configService, settings: settingsCache});
|
|
@@ -251,11 +251,13 @@ module.exports = {
|
|
|
251
251
|
const response = await mailgunClient.send(emailData, recipientData, replacements);
|
|
252
252
|
debug(`sent message (${Date.now() - startTime}ms)`);
|
|
253
253
|
return response;
|
|
254
|
-
} catch (
|
|
255
|
-
// REF: possible mailgun errors https://documentation.mailgun.com/en/latest/api-intro.html#errors
|
|
254
|
+
} catch (err) {
|
|
256
255
|
let ghostError = new errors.EmailError({
|
|
257
|
-
err
|
|
258
|
-
|
|
256
|
+
err,
|
|
257
|
+
message: tpl(messages.error),
|
|
258
|
+
context: `Mailgun Error ${err.error.status}: ${err.error.details}`,
|
|
259
|
+
// REF: possible mailgun errors https://documentation.mailgun.com/en/latest/api-intro.html#errors
|
|
260
|
+
help: `https://ghost.org/docs/newsletters/#bulk-email-configuration`,
|
|
259
261
|
code: 'BULK_EMAIL_SEND_FAILED'
|
|
260
262
|
});
|
|
261
263
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const LinkRedirect = require('@tryghost/link-redirects').LinkRedirect;
|
|
2
|
+
const ObjectID = require('bson-objectid').default;
|
|
3
|
+
|
|
4
|
+
module.exports = class LinkRedirectRepository {
|
|
5
|
+
/** @type {Object} */
|
|
6
|
+
#LinkRedirect;
|
|
7
|
+
/** @type {Object} */
|
|
8
|
+
#urlUtils;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {object} deps
|
|
12
|
+
* @param {object} deps.LinkRedirect Bookshelf Model
|
|
13
|
+
* @param {object} deps.urlUtils
|
|
14
|
+
*/
|
|
15
|
+
constructor(deps) {
|
|
16
|
+
this.#LinkRedirect = deps.LinkRedirect;
|
|
17
|
+
this.#urlUtils = deps.urlUtils;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {InstanceType<LinkRedirect>} linkRedirect
|
|
22
|
+
* @returns {Promise<void>}
|
|
23
|
+
*/
|
|
24
|
+
async save(linkRedirect) {
|
|
25
|
+
const model = await this.#LinkRedirect.add({
|
|
26
|
+
// Only store the parthname (no support for variable query strings)
|
|
27
|
+
from: this.stripSubdirectoryFromPath(linkRedirect.from.pathname),
|
|
28
|
+
to: linkRedirect.to.href
|
|
29
|
+
}, {});
|
|
30
|
+
|
|
31
|
+
linkRedirect.link_id = ObjectID.createFromHexString(model.id);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#trimLeadingSlash(url) {
|
|
35
|
+
return url.replace(/^\//, '');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fromModel(model) {
|
|
39
|
+
return new LinkRedirect({
|
|
40
|
+
id: model.id,
|
|
41
|
+
from: new URL(this.#trimLeadingSlash(model.get('from')), this.#urlUtils.urlFor('home', true)),
|
|
42
|
+
to: new URL(model.get('to'))
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async getAll(options) {
|
|
47
|
+
const collection = await this.#LinkRedirect.findAll(options);
|
|
48
|
+
|
|
49
|
+
const result = [];
|
|
50
|
+
|
|
51
|
+
for (const model of collection.models) {
|
|
52
|
+
result.push(this.fromModel(model));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
*
|
|
60
|
+
* @param {URL} url
|
|
61
|
+
* @returns {Promise<InstanceType<LinkRedirect>|undefined>} linkRedirect
|
|
62
|
+
*/
|
|
63
|
+
async getByURL(url) {
|
|
64
|
+
// Strip subdirectory from path
|
|
65
|
+
const from = this.stripSubdirectoryFromPath(url.pathname);
|
|
66
|
+
|
|
67
|
+
const linkRedirect = await this.#LinkRedirect.findOne({
|
|
68
|
+
from
|
|
69
|
+
}, {});
|
|
70
|
+
|
|
71
|
+
if (linkRedirect) {
|
|
72
|
+
return this.fromModel(linkRedirect);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Convert root relative URLs to subdirectory relative URLs
|
|
78
|
+
*/
|
|
79
|
+
stripSubdirectoryFromPath(path) {
|
|
80
|
+
// Bit weird, but only way to do it with the urlUtils atm
|
|
81
|
+
|
|
82
|
+
// First convert path to an absolute path
|
|
83
|
+
const absolute = this.#urlUtils.relativeToAbsolute(path);
|
|
84
|
+
|
|
85
|
+
// Then convert it to a relative path, but without subdirectory
|
|
86
|
+
return this.#urlUtils.absoluteToRelative(absolute, {withoutSubdirectory: true});
|
|
87
|
+
}
|
|
88
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const urlUtils = require('../../../shared/url-utils');
|
|
2
|
+
const LinkRedirectRepository = require('./LinkRedirectRepository');
|
|
2
3
|
|
|
3
4
|
class LinkRedirectsServiceWrapper {
|
|
4
5
|
async init() {
|
|
@@ -8,21 +9,18 @@ class LinkRedirectsServiceWrapper {
|
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
// Wire up all the dependencies
|
|
12
|
+
const models = require('../../models');
|
|
13
|
+
|
|
11
14
|
const {LinkRedirectsService} = require('@tryghost/link-redirects');
|
|
12
15
|
|
|
13
|
-
|
|
16
|
+
this.linkRedirectRepository = new LinkRedirectRepository({
|
|
17
|
+
LinkRedirect: models.LinkRedirect,
|
|
18
|
+
urlUtils
|
|
19
|
+
});
|
|
20
|
+
|
|
14
21
|
// Expose the service
|
|
15
22
|
this.service = new LinkRedirectsService({
|
|
16
|
-
linkRedirectRepository:
|
|
17
|
-
async save(linkRedirect) {
|
|
18
|
-
store.push(linkRedirect);
|
|
19
|
-
},
|
|
20
|
-
async getByURL(url) {
|
|
21
|
-
return store.find((link) => {
|
|
22
|
-
return link.from.pathname === url.pathname;
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
},
|
|
23
|
+
linkRedirectRepository: this.linkRedirectRepository,
|
|
26
24
|
config: {
|
|
27
25
|
baseURL: new URL(urlUtils.getSiteUrl())
|
|
28
26
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const {LinkClick} = require('@tryghost/link-tracking');
|
|
2
|
+
const ObjectID = require('bson-objectid').default;
|
|
3
|
+
|
|
4
|
+
module.exports = class LinkClickRepository {
|
|
5
|
+
/** @type {Object} */
|
|
6
|
+
#MemberLinkClickEventModel;
|
|
7
|
+
|
|
8
|
+
/** @type {Object} */
|
|
9
|
+
#MemberLinkClickEvent;
|
|
10
|
+
|
|
11
|
+
/** @type {object} */
|
|
12
|
+
#Member;
|
|
13
|
+
|
|
14
|
+
/** @type {object} */
|
|
15
|
+
#DomainEvents;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {object} deps
|
|
19
|
+
* @param {object} deps.MemberLinkClickEventModel Bookshelf Model
|
|
20
|
+
* @param {object} deps.Member Bookshelf Model
|
|
21
|
+
* @param {object} deps.MemberLinkClickEvent Event
|
|
22
|
+
* @param {object} deps.DomainEvents
|
|
23
|
+
*/
|
|
24
|
+
constructor(deps) {
|
|
25
|
+
this.#MemberLinkClickEventModel = deps.MemberLinkClickEventModel;
|
|
26
|
+
this.#Member = deps.Member;
|
|
27
|
+
this.#MemberLinkClickEvent = deps.MemberLinkClickEvent;
|
|
28
|
+
this.#DomainEvents = deps.DomainEvents;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async getAll(options) {
|
|
32
|
+
const collection = await this.#MemberLinkClickEventModel.findAll(options);
|
|
33
|
+
|
|
34
|
+
const result = [];
|
|
35
|
+
|
|
36
|
+
for (const model of collection.models) {
|
|
37
|
+
const member = await this.#Member.findOne({id: model.get('member_id')});
|
|
38
|
+
result.push(new LinkClick({
|
|
39
|
+
link_id: model.get('link_id'),
|
|
40
|
+
member_uuid: member.get('uuid')
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {LinkClick} linkClick
|
|
49
|
+
* @returns {Promise<void>}
|
|
50
|
+
*/
|
|
51
|
+
async save(linkClick) {
|
|
52
|
+
// Convert uuid to id
|
|
53
|
+
const member = await this.#Member.findOne({uuid: linkClick.member_uuid});
|
|
54
|
+
if (!member) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const model = await this.#MemberLinkClickEventModel.add({
|
|
59
|
+
// Only store the parthname (no support for variable query strings)
|
|
60
|
+
link_id: linkClick.link_id.toHexString(),
|
|
61
|
+
member_id: member.id
|
|
62
|
+
}, {});
|
|
63
|
+
|
|
64
|
+
linkClick.event_id = ObjectID.createFromHexString(model.id);
|
|
65
|
+
|
|
66
|
+
// Dispatch event
|
|
67
|
+
this.#DomainEvents.dispatch(this.#MemberLinkClickEvent.create({memberId: member.id, memberLastSeenAt: member.get('last_seen_at'), linkId: linkClick.link_id.toHexString()}, new Date()));
|
|
68
|
+
}
|
|
69
|
+
};
|