ghost 5.30.0 → 5.31.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.30.0.tgz → tryghost-adapter-manager-5.31.0.tgz} +0 -0
- package/components/{tryghost-api-framework-5.30.0.tgz → tryghost-api-framework-5.31.0.tgz} +0 -0
- package/components/{tryghost-api-version-compatibility-service-5.30.0.tgz → tryghost-api-version-compatibility-service-5.31.0.tgz} +0 -0
- package/components/{tryghost-audience-feedback-5.30.0.tgz → tryghost-audience-feedback-5.31.0.tgz} +0 -0
- package/components/{tryghost-bootstrap-socket-5.30.0.tgz → tryghost-bootstrap-socket-5.31.0.tgz} +0 -0
- package/components/{tryghost-constants-5.30.0.tgz → tryghost-constants-5.31.0.tgz} +0 -0
- package/components/{tryghost-custom-theme-settings-service-5.30.0.tgz → tryghost-custom-theme-settings-service-5.31.0.tgz} +0 -0
- package/components/{tryghost-data-generator-5.30.0.tgz → tryghost-data-generator-5.31.0.tgz} +0 -0
- package/components/tryghost-domain-events-5.31.0.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.31.0.tgz +0 -0
- package/components/{tryghost-email-analytics-service-5.30.0.tgz → tryghost-email-analytics-service-5.31.0.tgz} +0 -0
- package/components/{tryghost-email-content-generator-5.30.0.tgz → tryghost-email-content-generator-5.31.0.tgz} +0 -0
- package/components/{tryghost-email-events-5.30.0.tgz → tryghost-email-events-5.31.0.tgz} +0 -0
- package/components/tryghost-email-service-5.31.0.tgz +0 -0
- package/components/{tryghost-email-suppression-list-5.30.0.tgz → tryghost-email-suppression-list-5.31.0.tgz} +0 -0
- package/components/{tryghost-express-dynamic-redirects-5.30.0.tgz → tryghost-express-dynamic-redirects-5.31.0.tgz} +0 -0
- package/components/tryghost-extract-api-key-5.31.0.tgz +0 -0
- package/components/{tryghost-html-to-plaintext-5.30.0.tgz → tryghost-html-to-plaintext-5.31.0.tgz} +0 -0
- package/components/tryghost-i18n-5.31.0.tgz +0 -0
- package/components/tryghost-importer-revue-5.31.0.tgz +0 -0
- package/components/{tryghost-job-manager-5.30.0.tgz → tryghost-job-manager-5.31.0.tgz} +0 -0
- package/components/{tryghost-link-redirects-5.30.0.tgz → tryghost-link-redirects-5.31.0.tgz} +0 -0
- package/components/tryghost-link-replacer-5.31.0.tgz +0 -0
- package/components/{tryghost-link-tracking-5.30.0.tgz → tryghost-link-tracking-5.31.0.tgz} +0 -0
- package/components/{tryghost-magic-link-5.30.0.tgz → tryghost-magic-link-5.31.0.tgz} +0 -0
- package/components/{tryghost-mailgun-client-5.30.0.tgz → tryghost-mailgun-client-5.31.0.tgz} +0 -0
- package/components/tryghost-member-attribution-5.31.0.tgz +0 -0
- package/components/tryghost-member-events-5.31.0.tgz +0 -0
- package/components/{tryghost-members-api-5.30.0.tgz → tryghost-members-api-5.31.0.tgz} +0 -0
- package/components/{tryghost-members-csv-5.30.0.tgz → tryghost-members-csv-5.31.0.tgz} +0 -0
- package/components/{tryghost-members-events-service-5.30.0.tgz → tryghost-members-events-service-5.31.0.tgz} +0 -0
- package/components/{tryghost-members-importer-5.30.0.tgz → tryghost-members-importer-5.31.0.tgz} +0 -0
- package/components/tryghost-members-offers-5.31.0.tgz +0 -0
- package/components/tryghost-members-payments-5.31.0.tgz +0 -0
- package/components/{tryghost-members-ssr-5.30.0.tgz → tryghost-members-ssr-5.31.0.tgz} +0 -0
- package/components/{tryghost-members-stripe-service-5.30.0.tgz → tryghost-members-stripe-service-5.31.0.tgz} +0 -0
- package/components/{tryghost-minifier-5.30.0.tgz → tryghost-minifier-5.31.0.tgz} +0 -0
- package/components/{tryghost-mw-api-version-mismatch-5.30.0.tgz → tryghost-mw-api-version-mismatch-5.31.0.tgz} +0 -0
- package/components/{tryghost-mw-cache-control-5.30.0.tgz → tryghost-mw-cache-control-5.31.0.tgz} +0 -0
- package/components/{tryghost-mw-error-handler-5.30.0.tgz → tryghost-mw-error-handler-5.31.0.tgz} +0 -0
- package/components/tryghost-mw-session-from-token-5.31.0.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.31.0.tgz +0 -0
- package/components/{tryghost-mw-vhost-5.30.0.tgz → tryghost-mw-vhost-5.31.0.tgz} +0 -0
- package/components/tryghost-oembed-service-5.31.0.tgz +0 -0
- package/components/tryghost-package-json-5.31.0.tgz +0 -0
- package/components/tryghost-referrers-5.31.0.tgz +0 -0
- package/components/{tryghost-security-5.30.0.tgz → tryghost-security-5.31.0.tgz} +0 -0
- package/components/{tryghost-session-service-5.30.0.tgz → tryghost-session-service-5.31.0.tgz} +0 -0
- package/components/tryghost-settings-path-manager-5.31.0.tgz +0 -0
- package/components/{tryghost-staff-service-5.30.0.tgz → tryghost-staff-service-5.31.0.tgz} +0 -0
- package/components/tryghost-stats-service-5.31.0.tgz +0 -0
- package/components/{tryghost-tiers-5.30.0.tgz → tryghost-tiers-5.31.0.tgz} +0 -0
- package/components/{tryghost-update-check-service-5.30.0.tgz → tryghost-update-check-service-5.31.0.tgz} +0 -0
- package/components/tryghost-verification-trigger-5.31.0.tgz +0 -0
- package/components/{tryghost-version-notifications-data-service-5.30.0.tgz → tryghost-version-notifications-data-service-5.31.0.tgz} +0 -0
- package/components/tryghost-webmentions-5.31.0.tgz +0 -0
- package/content/themes/casper/author.hbs +6 -6
- package/content/themes/casper/package.json +1 -1
- package/core/boot.js +2 -0
- package/core/built/admin/assets/{chunk.143.f9aa3f7f1c45d1d921cd.js → chunk.143.7ef8d39b50a9ef3d6a6b.js} +5 -5
- package/core/built/admin/assets/{chunk.178.55d68318431345983298.js → chunk.178.8a8e070a8c2682df548a.js} +4 -4
- package/core/built/admin/assets/{ghost-d3e45940f0f1601232d464d0b429d45f.css → ghost-721a7adc4ca642c88e4ac85e1cb8b385.css} +1 -1
- package/core/built/admin/assets/{ghost-dark-c8dc36895dfcbc03f0edd94311c65bfd.css → ghost-dark-fb05eb50e216469c5626356731afa42f.css} +1 -1
- package/core/built/admin/assets/{ghost-23dc524374e35a582886c36f7dacdb05.js → ghost-fc0450a45ea5be2e5a10c4d897d5b430.js} +142 -124
- package/core/built/admin/assets/{vendor-fadbf85ad92c591dc4bd3755312b6ddf.js → vendor-0441964c34d58f2aacd5a04bbe240243.js} +34 -37
- package/core/built/admin/index.html +5 -5
- package/core/frontend/helpers/ghost_head.js +16 -0
- package/core/server/api/endpoints/index.js +4 -0
- package/core/server/api/endpoints/mentions.js +33 -0
- package/core/server/api/endpoints/oembed.js +1 -22
- package/core/server/api/endpoints/utils/serializers/input/settings.js +2 -1
- package/core/server/api/endpoints/utils/serializers/output/mappers/index.js +2 -1
- package/core/server/api/endpoints/utils/serializers/output/mappers/mentions.js +18 -0
- package/core/server/data/exporter/table-lists.js +1 -0
- package/core/server/data/importer/import-manager.js +21 -5
- package/core/server/data/migrations/versions/5.31/2022-12-05-09-56-update-newsletter-subscriptions.js +23 -0
- package/core/server/data/migrations/versions/5.31/2023-01-17-14-59-add-outbound-link-tagging-setting.js +8 -0
- package/core/server/data/migrations/versions/5.31/2023-01-19-07-46-add-mentions-table.js +17 -0
- package/core/server/data/schema/default-settings/default-settings.json +10 -0
- package/core/server/data/schema/schema.js +15 -0
- package/core/server/models/base/plugins/actions.js +1 -1
- package/core/server/models/base/plugins/sanitize.js +2 -0
- package/core/server/models/mention.js +9 -0
- package/core/server/models/redirect.js +2 -1
- package/core/server/services/api-version-compatibility/index.js +5 -8
- package/core/server/services/email-suppression-list/MailgunEmailSuppressionList.js +1 -1
- package/core/server/services/link-redirection/LinkRedirectRepository.js +2 -1
- package/core/server/services/link-tracking/PostLinkRepository.js +2 -1
- package/core/server/services/mega/post-email-serializer.js +2 -2
- package/core/server/services/member-attribution/index.js +2 -0
- package/core/server/services/mentions/BookshelfMentionRepository.js +117 -0
- package/core/server/services/mentions/MentionController.js +66 -0
- package/core/server/services/mentions/WebmentionMetadata.js +20 -0
- package/core/server/services/mentions/index.js +1 -0
- package/core/server/services/mentions/service.js +77 -0
- package/core/server/services/oembed/index.js +1 -0
- package/core/server/services/{nft-oembed.js → oembed/nft-oembed.js} +0 -0
- package/core/server/services/oembed/service.js +24 -0
- package/core/server/services/{twitter-embed.js → oembed/twitter-embed.js} +0 -0
- package/core/server/services/url/Resources.js +3 -17
- package/core/server/services/url/UrlService.js +3 -3
- package/core/server/web/api/endpoints/admin/routes.js +3 -0
- package/core/server/web/parent/frontend.js +1 -0
- package/core/server/web/webmentions/index.js +1 -0
- package/core/server/web/webmentions/routes.js +18 -0
- package/core/shared/labs.js +6 -4
- package/package.json +115 -111
- package/yarn.lock +620 -80
- package/components/tryghost-domain-events-5.30.0.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.30.0.tgz +0 -0
- package/components/tryghost-email-service-5.30.0.tgz +0 -0
- package/components/tryghost-extract-api-key-5.30.0.tgz +0 -0
- package/components/tryghost-importer-revue-5.30.0.tgz +0 -0
- package/components/tryghost-link-replacer-5.30.0.tgz +0 -0
- package/components/tryghost-member-attribution-5.30.0.tgz +0 -0
- package/components/tryghost-member-events-5.30.0.tgz +0 -0
- package/components/tryghost-members-offers-5.30.0.tgz +0 -0
- package/components/tryghost-members-payments-5.30.0.tgz +0 -0
- package/components/tryghost-mw-session-from-token-5.30.0.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.30.0.tgz +0 -0
- package/components/tryghost-oembed-service-5.30.0.tgz +0 -0
- package/components/tryghost-package-json-5.30.0.tgz +0 -0
- package/components/tryghost-referrers-5.30.0.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.30.0.tgz +0 -0
- package/components/tryghost-stats-service-5.30.0.tgz +0 -0
- package/components/tryghost-verification-trigger-5.30.0.tgz +0 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const {addTable} = require('../../utils');
|
|
2
|
+
|
|
3
|
+
module.exports = addTable('mentions', {
|
|
4
|
+
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
5
|
+
source: {type: 'string', maxlength: 2000, nullable: false},
|
|
6
|
+
source_title: {type: 'string', maxlength: 2000, nullable: true},
|
|
7
|
+
source_site_title: {type: 'string', maxlength: 2000, nullable: true},
|
|
8
|
+
source_excerpt: {type: 'string', maxlength: 2000, nullable: true},
|
|
9
|
+
source_author: {type: 'string', maxlength: 2000, nullable: true},
|
|
10
|
+
source_featured_image: {type: 'string', maxlength: 2000, nullable: true},
|
|
11
|
+
source_favicon: {type: 'string', maxlength: 2000, nullable: true},
|
|
12
|
+
target: {type: 'string', maxlength: 2000, nullable: false},
|
|
13
|
+
resource_id: {type: 'string', maxlength: 24, nullable: true},
|
|
14
|
+
resource_type: {type: 'string', maxlength: 50, nullable: true},
|
|
15
|
+
created_at: {type: 'dateTime', nullable: false},
|
|
16
|
+
payload: {type: 'text', maxlength: 65535, nullable: true}
|
|
17
|
+
});
|
|
@@ -979,5 +979,20 @@ module.exports = {
|
|
|
979
979
|
'@@UNIQUE_CONSTRAINTS@@': [
|
|
980
980
|
['email_id', 'member_id']
|
|
981
981
|
]
|
|
982
|
+
},
|
|
983
|
+
mentions: {
|
|
984
|
+
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
985
|
+
source: {type: 'string', maxlength: 2000, nullable: false},
|
|
986
|
+
source_title: {type: 'string', maxlength: 2000, nullable: true},
|
|
987
|
+
source_site_title: {type: 'string', maxlength: 2000, nullable: true},
|
|
988
|
+
source_excerpt: {type: 'string', maxlength: 2000, nullable: true},
|
|
989
|
+
source_author: {type: 'string', maxlength: 2000, nullable: true},
|
|
990
|
+
source_featured_image: {type: 'string', maxlength: 2000, nullable: true},
|
|
991
|
+
source_favicon: {type: 'string', maxlength: 2000, nullable: true},
|
|
992
|
+
target: {type: 'string', maxlength: 2000, nullable: false},
|
|
993
|
+
resource_id: {type: 'string', maxlength: 24, nullable: true},
|
|
994
|
+
resource_type: {type: 'string', maxlength: 50, nullable: true},
|
|
995
|
+
created_at: {type: 'dateTime', nullable: false},
|
|
996
|
+
payload: {type: 'text', maxlength: 65535, nullable: true}
|
|
982
997
|
}
|
|
983
998
|
};
|
|
@@ -38,6 +38,8 @@ module.exports = function (Bookshelf) {
|
|
|
38
38
|
return baseOptions.concat('shallow', 'columns', 'previous');
|
|
39
39
|
case 'destroy':
|
|
40
40
|
return baseOptions.concat(extraOptions, ['id', 'destroyBy', 'require']);
|
|
41
|
+
case 'add':
|
|
42
|
+
return baseOptions.concat(extraOptions, ['autoRefresh']);
|
|
41
43
|
case 'edit':
|
|
42
44
|
return baseOptions.concat(extraOptions, ['id', 'require']);
|
|
43
45
|
case 'findOne':
|
|
@@ -36,7 +36,8 @@ const Redirect = ghostBookshelf.Model.extend({
|
|
|
36
36
|
permittedOptions(methodName) {
|
|
37
37
|
let options = ghostBookshelf.Model.permittedOptions.call(this, methodName);
|
|
38
38
|
const validOptions = {
|
|
39
|
-
findAll: ['filter', 'columns', 'withRelated']
|
|
39
|
+
findAll: ['filter', 'columns', 'withRelated'],
|
|
40
|
+
edit: ['importing']
|
|
40
41
|
};
|
|
41
42
|
|
|
42
43
|
if (validOptions[methodName]) {
|
|
@@ -30,21 +30,18 @@ module.exports.errorHandler = (err, req, res, next) => {
|
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
33
|
+
* Set Content-Version on the response, and add 'Accept-Version' to VARY as
|
|
34
|
+
* it effects response caching
|
|
35
35
|
* TODO: move the method to mw once back-compatibility with 4.x is sorted
|
|
36
|
-
*
|
|
36
|
+
*
|
|
37
37
|
* @param {import('express').Request} req
|
|
38
38
|
* @param {import('express').Response} res
|
|
39
39
|
* @param {import('express').NextFunction} next
|
|
40
40
|
*/
|
|
41
41
|
module.exports.contentVersion = (req, res, next) => {
|
|
42
|
-
|
|
43
|
-
res.header('Content-Version', `v${ghostVersion.safe}`);
|
|
44
|
-
}
|
|
45
|
-
|
|
42
|
+
res.header('Content-Version', `v${ghostVersion.safe}`);
|
|
46
43
|
res.vary('Accept-Version');
|
|
47
|
-
|
|
44
|
+
|
|
48
45
|
next();
|
|
49
46
|
};
|
|
50
47
|
|
|
@@ -73,7 +73,7 @@ class MailgunEmailSuppressionList extends AbstractEmailSuppressionList {
|
|
|
73
73
|
|
|
74
74
|
try {
|
|
75
75
|
const collection = await this.Suppression.findAll({
|
|
76
|
-
filter: `email:[${emails.join(',')}]`
|
|
76
|
+
filter: `email:[${emails.map(email => `'${email}'`).join(',')}]`
|
|
77
77
|
});
|
|
78
78
|
|
|
79
79
|
return emails.map((email) => {
|
|
@@ -37,7 +37,8 @@ module.exports = class LinkRedirectRepository {
|
|
|
37
37
|
|
|
38
38
|
fromModel(model) {
|
|
39
39
|
// Store if link has been edited
|
|
40
|
-
|
|
40
|
+
// Note: in some edge cases updated_at is set directly after created_at, sometimes with a second difference, so we need to check for that
|
|
41
|
+
const edited = model.get('updated_at')?.getTime() > (model.get('created_at')?.getTime() + 1000);
|
|
41
42
|
|
|
42
43
|
return new LinkRedirect({
|
|
43
44
|
id: model.id,
|
|
@@ -80,7 +80,8 @@ module.exports = class PostLinkRepository {
|
|
|
80
80
|
await this.#LinkRedirect.edit({
|
|
81
81
|
post_id: postLink.post_id.toHexString()
|
|
82
82
|
}, {
|
|
83
|
-
id: postLink.link_id.toHexString()
|
|
83
|
+
id: postLink.link_id.toHexString(),
|
|
84
|
+
importing: true // skip setting updated_at when linking a post to a link
|
|
84
85
|
});
|
|
85
86
|
}
|
|
86
87
|
};
|
|
@@ -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.
|
|
403
|
+
url = memberAttribution.service.addOutboundLinkTagging(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.
|
|
409
|
+
url = memberAttribution.service.addOutboundLinkTagging(url);
|
|
410
410
|
}
|
|
411
411
|
|
|
412
412
|
// Add link click tracking
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const urlService = require('../url');
|
|
2
2
|
const urlUtils = require('../../../shared/url-utils');
|
|
3
3
|
const settingsCache = require('../../../shared/settings-cache');
|
|
4
|
+
const labs = require('../../../shared/labs');
|
|
4
5
|
|
|
5
6
|
class MemberAttributionServiceWrapper {
|
|
6
7
|
init() {
|
|
@@ -41,6 +42,7 @@ class MemberAttributionServiceWrapper {
|
|
|
41
42
|
},
|
|
42
43
|
attributionBuilder: this.attributionBuilder,
|
|
43
44
|
getTrackingEnabled: () => !!settingsCache.get('members_track_sources'),
|
|
45
|
+
getOutboundLinkTaggingEnabled: () => !labs.isSet('outboundLinkTagging') || !!settingsCache.get('outbound_link_tagging'),
|
|
44
46
|
getSiteTitle: () => settingsCache.get('title')
|
|
45
47
|
});
|
|
46
48
|
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
const {Mention} = require('@tryghost/webmentions');
|
|
2
|
+
const logging = require('@tryghost/logging');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {import('@tryghost/webmentions/lib/MentionsAPI').IMentionRepository} IMentionRepository
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @template Model
|
|
10
|
+
* @typedef {import('@tryghost/webmentions/lib/MentionsAPI').Page<Model>} Page
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {import('@tryghost/webmentions/lib/MentionsAPI').GetPageOptions} GetPageOptions
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @implements {IMentionRepository}
|
|
19
|
+
*/
|
|
20
|
+
module.exports = class BookshelfMentionRepository {
|
|
21
|
+
/** @type {Object} */
|
|
22
|
+
#MentionModel;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {object} deps
|
|
26
|
+
* @param {object} deps.MentionModel Bookshelf Model
|
|
27
|
+
*/
|
|
28
|
+
constructor(deps) {
|
|
29
|
+
this.#MentionModel = deps.MentionModel;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#modelToMention(model) {
|
|
33
|
+
let payload;
|
|
34
|
+
try {
|
|
35
|
+
payload = JSON.parse(model.get('payload'));
|
|
36
|
+
} catch (err) {
|
|
37
|
+
logging.error(err);
|
|
38
|
+
payload = {};
|
|
39
|
+
}
|
|
40
|
+
return Mention.create({
|
|
41
|
+
id: model.get('id'),
|
|
42
|
+
source: model.get('source'),
|
|
43
|
+
target: model.get('target'),
|
|
44
|
+
timestamp: model.get('created_at'),
|
|
45
|
+
payload,
|
|
46
|
+
resourceId: model.get('resource_id'),
|
|
47
|
+
sourceTitle: model.get('source_title'),
|
|
48
|
+
sourceSiteTitle: model.get('source_site_title'),
|
|
49
|
+
sourceAuthor: model.get('source_author'),
|
|
50
|
+
sourceExcerpt: model.get('source_excerpt'),
|
|
51
|
+
sourceFavicon: model.get('source_favicon'),
|
|
52
|
+
sourceFeaturedImaged: model.get('source_featured_image')
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {GetPageOptions} options
|
|
58
|
+
* @returns {Promise<Page<import('@tryghost/webmentions/lib/Mention')>>}
|
|
59
|
+
*/
|
|
60
|
+
async getPage(options) {
|
|
61
|
+
const page = await this.#MentionModel.findPage(options);
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
data: await Promise.all(page.data.map(model => this.#modelToMention(model))),
|
|
65
|
+
meta: page.meta
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param {URL} source
|
|
71
|
+
* @param {URL} target
|
|
72
|
+
* @returns {Promise<import('@tryghost/webmentions/lib/Mention')|null>}
|
|
73
|
+
*/
|
|
74
|
+
async getBySourceAndTarget(source, target) {
|
|
75
|
+
const model = await this.#MentionModel.findOne({
|
|
76
|
+
source: source.href,
|
|
77
|
+
target: target.href
|
|
78
|
+
}, {require: false});
|
|
79
|
+
|
|
80
|
+
if (!model) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return this.#modelToMention(model);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {import('@tryghost/webmentions/lib/Mention')} mention
|
|
89
|
+
* @returns {Promise<void>}
|
|
90
|
+
*/
|
|
91
|
+
async save(mention) {
|
|
92
|
+
const data = {
|
|
93
|
+
id: mention.id.toHexString(),
|
|
94
|
+
source: mention.source.href,
|
|
95
|
+
source_title: mention.sourceTitle,
|
|
96
|
+
source_site_title: mention.sourceSiteTitle,
|
|
97
|
+
source_excerpt: mention.sourceExcerpt,
|
|
98
|
+
source_author: mention.sourceAuthor,
|
|
99
|
+
source_featured_image: mention.sourceFeaturedImage?.href,
|
|
100
|
+
source_favicon: mention.sourceFavicon?.href,
|
|
101
|
+
target: mention.target.href,
|
|
102
|
+
resource_id: mention.resourceId?.toHexString(),
|
|
103
|
+
resource_type: mention.resourceId ? 'post' : null,
|
|
104
|
+
payload: mention.payload ? JSON.stringify(mention.payload) : null
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const existing = await this.#MentionModel.findOne({id: data.id}, {require: false});
|
|
108
|
+
|
|
109
|
+
if (!existing) {
|
|
110
|
+
await this.#MentionModel.add(data);
|
|
111
|
+
} else {
|
|
112
|
+
await this.#MentionModel.edit(data, {
|
|
113
|
+
id: data.id
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const logging = require('@tryghost/logging');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {import('@tryghost/webmentions/lib/webmentions').MentionsAPI} MentionsAPI
|
|
5
|
+
* @typedef {import('@tryghost/webmentions/lib/webmentions').Mention} Mention
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @template Model
|
|
10
|
+
* @typedef {import('@tryghost/webmentions/lib/MentionsAPI').Page} Page<Model>
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
module.exports = class MentionController {
|
|
14
|
+
/** @type {import('@tryghost/webmentions/lib/MentionsAPI')} */
|
|
15
|
+
#api;
|
|
16
|
+
|
|
17
|
+
async init(deps) {
|
|
18
|
+
this.#api = deps.api;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {import('@tryghost/api-framework').Frame} frame
|
|
23
|
+
* @returns {Promise<Page<Mention>>}
|
|
24
|
+
*/
|
|
25
|
+
async browse(frame) {
|
|
26
|
+
let limit;
|
|
27
|
+
if (!frame.options.limit || frame.options.limit === 'all') {
|
|
28
|
+
limit = 'all';
|
|
29
|
+
} else {
|
|
30
|
+
limit = parseInt(frame.options.limit);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let page;
|
|
34
|
+
if (frame.options.page) {
|
|
35
|
+
page = parseInt(frame.options.page);
|
|
36
|
+
} else {
|
|
37
|
+
page = 1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const results = await this.#api.listMentions({
|
|
41
|
+
filter: frame.options.filter,
|
|
42
|
+
limit,
|
|
43
|
+
page
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return results;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {import('@tryghost/api-framework').Frame} frame
|
|
51
|
+
* @returns {Promise<void>}
|
|
52
|
+
*/
|
|
53
|
+
async receive(frame) {
|
|
54
|
+
logging.info('[Webmention] ' + JSON.stringify(frame.data));
|
|
55
|
+
const {source, target, ...payload} = frame.data;
|
|
56
|
+
const result = this.#api.processWebmention({
|
|
57
|
+
source: new URL(source),
|
|
58
|
+
target: new URL(target),
|
|
59
|
+
payload
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
result.catch(function rejected(err) {
|
|
63
|
+
logging.error(err);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const oembedService = require('../oembed');
|
|
2
|
+
|
|
3
|
+
module.exports = class WebmentionMetadata {
|
|
4
|
+
/**
|
|
5
|
+
* @param {URL} url
|
|
6
|
+
* @returns {Promise<import('@tryghost/webmentions/lib/MentionsAPI').WebmentionMetadata>}
|
|
7
|
+
*/
|
|
8
|
+
async fetch(url) {
|
|
9
|
+
const data = await oembedService.fetchOembedDataFromUrl(url.href, 'bookmark');
|
|
10
|
+
const result = {
|
|
11
|
+
siteTitle: data.metadata.publisher,
|
|
12
|
+
title: data.metadata.title,
|
|
13
|
+
excerpt: data.metadata.description,
|
|
14
|
+
author: data.metadata.author,
|
|
15
|
+
image: data.metadata.thumbnail ? new URL(data.metadata.thumbnail) : null,
|
|
16
|
+
favicon: data.metadata.icon ? new URL(data.metadata.icon) : null
|
|
17
|
+
};
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./service');
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const ObjectID = require('bson-objectid').default;
|
|
2
|
+
const MentionController = require('./MentionController');
|
|
3
|
+
const WebmentionMetadata = require('./WebmentionMetadata');
|
|
4
|
+
const {
|
|
5
|
+
MentionsAPI,
|
|
6
|
+
MentionSendingService,
|
|
7
|
+
MentionDiscoveryService
|
|
8
|
+
} = require('@tryghost/webmentions');
|
|
9
|
+
const BookshelfMentionRepository = require('./BookshelfMentionRepository');
|
|
10
|
+
const models = require('../../models');
|
|
11
|
+
const events = require('../../lib/common/events');
|
|
12
|
+
const externalRequest = require('../../../server/lib/request-external.js');
|
|
13
|
+
const urlUtils = require('../../../shared/url-utils');
|
|
14
|
+
const outputSerializerUrlUtil = require('../../../server/api/endpoints/utils/serializers/output/utils/url');
|
|
15
|
+
const labs = require('../../../shared/labs');
|
|
16
|
+
const urlService = require('../url');
|
|
17
|
+
|
|
18
|
+
function getPostUrl(post) {
|
|
19
|
+
const jsonModel = {};
|
|
20
|
+
outputSerializerUrlUtil.forPost(post.id, jsonModel, {options: {}});
|
|
21
|
+
return jsonModel.url;
|
|
22
|
+
}
|
|
23
|
+
module.exports = {
|
|
24
|
+
controller: new MentionController(),
|
|
25
|
+
async init() {
|
|
26
|
+
const repository = new BookshelfMentionRepository({
|
|
27
|
+
MentionModel: models.Mention
|
|
28
|
+
});
|
|
29
|
+
const webmentionMetadata = new WebmentionMetadata();
|
|
30
|
+
const discoveryService = new MentionDiscoveryService({externalRequest});
|
|
31
|
+
const api = new MentionsAPI({
|
|
32
|
+
repository,
|
|
33
|
+
webmentionMetadata,
|
|
34
|
+
resourceService: {
|
|
35
|
+
async getByURL(url) {
|
|
36
|
+
const path = urlUtils.absoluteToRelative(url.href, {withoutSubdirectory: true});
|
|
37
|
+
const resource = urlService.getResource(path);
|
|
38
|
+
if (resource?.config?.type === 'posts') {
|
|
39
|
+
return {
|
|
40
|
+
type: 'post',
|
|
41
|
+
id: ObjectID.createFromHexString(resource.data.id)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
type: null,
|
|
46
|
+
id: null
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
routingService: {
|
|
51
|
+
async pageExists(url) {
|
|
52
|
+
const siteUrl = new URL(urlUtils.getSiteUrl());
|
|
53
|
+
if (siteUrl.origin !== url.origin) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
const subdir = urlUtils.getSubdir();
|
|
57
|
+
if (subdir && !url.pathname.startsWith(subdir)) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
this.controller.init({api});
|
|
67
|
+
|
|
68
|
+
const sendingService = new MentionSendingService({
|
|
69
|
+
discoveryService,
|
|
70
|
+
externalRequest,
|
|
71
|
+
getSiteUrl: () => urlUtils.urlFor('home', true),
|
|
72
|
+
getPostUrl: post => getPostUrl(post),
|
|
73
|
+
isEnabled: () => labs.isSet('webmentions')
|
|
74
|
+
});
|
|
75
|
+
sendingService.listen(events);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./service');
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const config = require('../../../shared/config');
|
|
2
|
+
const externalRequest = require('../../lib/request-external');
|
|
3
|
+
|
|
4
|
+
const OEmbed = require('@tryghost/oembed-service');
|
|
5
|
+
const oembed = new OEmbed({config, externalRequest});
|
|
6
|
+
|
|
7
|
+
const NFT = require('./nft-oembed');
|
|
8
|
+
const nft = new NFT({
|
|
9
|
+
config: {
|
|
10
|
+
apiKey: config.get('opensea').privateReadOnlyApiKey
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const Twitter = require('./twitter-embed');
|
|
15
|
+
const twitter = new Twitter({
|
|
16
|
+
config: {
|
|
17
|
+
bearerToken: config.get('twitter').privateReadOnlyToken
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
oembed.registerProvider(nft);
|
|
22
|
+
oembed.registerProvider(twitter);
|
|
23
|
+
|
|
24
|
+
module.exports = oembed;
|
|
File without changes
|
|
@@ -21,10 +21,11 @@ class Resources {
|
|
|
21
21
|
* @param {Object} options
|
|
22
22
|
* @param {Object} [options.resources] - resources to initialize with instead of fetching them from the database
|
|
23
23
|
* @param {Object} [options.queue] - instance of the Queue class
|
|
24
|
+
* @param {Object[]} [options.resourcesConfig] - resource config used when handling resource events and fetching
|
|
24
25
|
*/
|
|
25
|
-
constructor({resources = {}, queue} = {}) {
|
|
26
|
+
constructor({resources = {}, queue, resourcesConfig = []} = {}) {
|
|
26
27
|
this.queue = queue;
|
|
27
|
-
this.resourcesConfig =
|
|
28
|
+
this.resourcesConfig = resourcesConfig;
|
|
28
29
|
this.data = resources;
|
|
29
30
|
|
|
30
31
|
this.listeners = [];
|
|
@@ -47,20 +48,6 @@ class Resources {
|
|
|
47
48
|
events.on(eventName, listener);
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
/**
|
|
51
|
-
* @description Initialize the resource config. We currently fetch the data straight via the the model layer,
|
|
52
|
-
* but because Ghost supports multiple API versions, we have to ensure we load the correct data.
|
|
53
|
-
*
|
|
54
|
-
* @TODO: https://github.com/TryGhost/Ghost/issues/10360
|
|
55
|
-
*/
|
|
56
|
-
initResourceConfig() {
|
|
57
|
-
if (!_.isEmpty(this.resourcesConfig)) {
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
this.resourcesConfig = require('./config');
|
|
62
|
-
}
|
|
63
|
-
|
|
64
51
|
/**
|
|
65
52
|
* @description Helper function to initialize data fetching.
|
|
66
53
|
*/
|
|
@@ -440,7 +427,6 @@ class Resources {
|
|
|
440
427
|
|
|
441
428
|
this.listeners = [];
|
|
442
429
|
this.data = {};
|
|
443
|
-
this.resourcesConfig = null;
|
|
444
430
|
}
|
|
445
431
|
|
|
446
432
|
/**
|
|
@@ -8,6 +8,7 @@ const Queue = require('./Queue');
|
|
|
8
8
|
const Urls = require('./Urls');
|
|
9
9
|
const Resources = require('./Resources');
|
|
10
10
|
const urlUtils = require('../../../shared/url-utils');
|
|
11
|
+
const resourcesConfig = require('./config');
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* The url service class holds all instances in a centralized place.
|
|
@@ -17,7 +18,7 @@ const urlUtils = require('../../../shared/url-utils');
|
|
|
17
18
|
class UrlService {
|
|
18
19
|
/**
|
|
19
20
|
*
|
|
20
|
-
* @param {Object} options
|
|
21
|
+
* @param {Object} [options]
|
|
21
22
|
* @param {Object} [options.cache] - cache handler instance
|
|
22
23
|
* @param {Function} [options.cache.read] - read cache by type
|
|
23
24
|
* @param {Function} [options.cache.write] - write into cache by type
|
|
@@ -35,6 +36,7 @@ class UrlService {
|
|
|
35
36
|
// Way too many tests fail if the initialization is removed so leaving it as is for time being
|
|
36
37
|
this.urls = new Urls();
|
|
37
38
|
this.resources = new Resources({
|
|
39
|
+
resourcesConfig: resourcesConfig,
|
|
38
40
|
queue: this.queue
|
|
39
41
|
});
|
|
40
42
|
|
|
@@ -321,12 +323,10 @@ class UrlService {
|
|
|
321
323
|
if (persistedUrls && persistedResources) {
|
|
322
324
|
this.urls.urls = persistedUrls;
|
|
323
325
|
this.resources.data = persistedResources;
|
|
324
|
-
this.resources.initResourceConfig();
|
|
325
326
|
this.resources.initEventListeners();
|
|
326
327
|
|
|
327
328
|
this._onQueueEnded('init');
|
|
328
329
|
} else {
|
|
329
|
-
this.resources.initResourceConfig();
|
|
330
330
|
this.resources.initEventListeners();
|
|
331
331
|
await this.resources.fetchResources();
|
|
332
332
|
// CASE: all resources are fetched, start the queue
|
|
@@ -5,6 +5,7 @@ const apiMw = require('../../middleware');
|
|
|
5
5
|
const mw = require('./middleware');
|
|
6
6
|
|
|
7
7
|
const shared = require('../../../shared');
|
|
8
|
+
const labs = require('../../../../../shared/labs');
|
|
8
9
|
|
|
9
10
|
module.exports = function apiRoutes() {
|
|
10
11
|
const router = express.Router('admin api');
|
|
@@ -31,6 +32,8 @@ module.exports = function apiRoutes() {
|
|
|
31
32
|
router.put('/posts/:id', mw.authAdminApi, http(api.posts.edit));
|
|
32
33
|
router.del('/posts/:id', mw.authAdminApi, http(api.posts.destroy));
|
|
33
34
|
|
|
35
|
+
router.get('/mentions', labs.enabledMiddleware('webmentions'), mw.authAdminApi, http(api.mentions.browse));
|
|
36
|
+
|
|
34
37
|
router.put('/comments/:id', mw.authAdminApi, http(api.comments.edit));
|
|
35
38
|
|
|
36
39
|
// ## Pages
|
|
@@ -18,6 +18,7 @@ module.exports = (routerConfig) => {
|
|
|
18
18
|
frontendApp.use(shared.middleware.urlRedirects.frontendSSLRedirect);
|
|
19
19
|
|
|
20
20
|
frontendApp.lazyUse('/members', require('../members'));
|
|
21
|
+
frontendApp.lazyUse('/webmentions', require('../webmentions'));
|
|
21
22
|
frontendApp.use('/', require('../../../frontend/web')(routerConfig));
|
|
22
23
|
|
|
23
24
|
return frontendApp;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./routes');
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const express = require('../../../shared/express');
|
|
2
|
+
const api = require('../../api').endpoints;
|
|
3
|
+
const {http} = require('@tryghost/api-framework');
|
|
4
|
+
const shared = require('../shared');
|
|
5
|
+
|
|
6
|
+
const bodyParser = require('body-parser');
|
|
7
|
+
|
|
8
|
+
module.exports = function apiRoutes() {
|
|
9
|
+
const router = express.Router('webmentions');
|
|
10
|
+
|
|
11
|
+
// shouldn't be cached
|
|
12
|
+
router.use(shared.middleware.cacheControl('private'));
|
|
13
|
+
|
|
14
|
+
// Webmentions
|
|
15
|
+
router.post('/receive', bodyParser.urlencoded({extended: true, limit: '5mb'}), http(api.mentions.receive));
|
|
16
|
+
|
|
17
|
+
return router;
|
|
18
|
+
};
|