ghost 5.30.1 → 5.32.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.1.tgz → tryghost-adapter-manager-5.32.0.tgz} +0 -0
- package/components/{tryghost-api-framework-5.30.1.tgz → tryghost-api-framework-5.32.0.tgz} +0 -0
- package/components/{tryghost-api-version-compatibility-service-5.30.1.tgz → tryghost-api-version-compatibility-service-5.32.0.tgz} +0 -0
- package/components/{tryghost-audience-feedback-5.30.1.tgz → tryghost-audience-feedback-5.32.0.tgz} +0 -0
- package/components/{tryghost-bootstrap-socket-5.30.1.tgz → tryghost-bootstrap-socket-5.32.0.tgz} +0 -0
- package/components/tryghost-constants-5.32.0.tgz +0 -0
- package/components/{tryghost-custom-theme-settings-service-5.30.1.tgz → tryghost-custom-theme-settings-service-5.32.0.tgz} +0 -0
- package/components/tryghost-data-generator-5.32.0.tgz +0 -0
- package/components/tryghost-domain-events-5.32.0.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.32.0.tgz +0 -0
- package/components/{tryghost-email-analytics-service-5.30.1.tgz → tryghost-email-analytics-service-5.32.0.tgz} +0 -0
- package/components/{tryghost-email-content-generator-5.30.1.tgz → tryghost-email-content-generator-5.32.0.tgz} +0 -0
- package/components/{tryghost-email-events-5.30.1.tgz → tryghost-email-events-5.32.0.tgz} +0 -0
- package/components/tryghost-email-service-5.32.0.tgz +0 -0
- package/components/tryghost-email-suppression-list-5.32.0.tgz +0 -0
- package/components/{tryghost-express-dynamic-redirects-5.30.1.tgz → tryghost-express-dynamic-redirects-5.32.0.tgz} +0 -0
- package/components/{tryghost-extract-api-key-5.30.1.tgz → tryghost-extract-api-key-5.32.0.tgz} +0 -0
- package/components/{tryghost-html-to-plaintext-5.30.1.tgz → tryghost-html-to-plaintext-5.32.0.tgz} +0 -0
- package/components/tryghost-i18n-5.32.0.tgz +0 -0
- package/components/{tryghost-importer-revue-5.30.1.tgz → tryghost-importer-revue-5.32.0.tgz} +0 -0
- package/components/{tryghost-job-manager-5.30.1.tgz → tryghost-job-manager-5.32.0.tgz} +0 -0
- package/components/tryghost-link-redirects-5.32.0.tgz +0 -0
- package/components/tryghost-link-replacer-5.32.0.tgz +0 -0
- package/components/{tryghost-link-tracking-5.30.1.tgz → tryghost-link-tracking-5.32.0.tgz} +0 -0
- package/components/tryghost-magic-link-5.32.0.tgz +0 -0
- package/components/{tryghost-mailgun-client-5.30.1.tgz → tryghost-mailgun-client-5.32.0.tgz} +0 -0
- package/components/tryghost-member-attribution-5.32.0.tgz +0 -0
- package/components/{tryghost-member-events-5.30.1.tgz → tryghost-member-events-5.32.0.tgz} +0 -0
- package/components/{tryghost-members-api-5.30.1.tgz → tryghost-members-api-5.32.0.tgz} +0 -0
- package/components/{tryghost-members-csv-5.30.1.tgz → tryghost-members-csv-5.32.0.tgz} +0 -0
- package/components/{tryghost-members-events-service-5.30.1.tgz → tryghost-members-events-service-5.32.0.tgz} +0 -0
- package/components/{tryghost-members-importer-5.30.1.tgz → tryghost-members-importer-5.32.0.tgz} +0 -0
- package/components/tryghost-members-offers-5.32.0.tgz +0 -0
- package/components/tryghost-members-payments-5.32.0.tgz +0 -0
- package/components/{tryghost-members-ssr-5.30.1.tgz → tryghost-members-ssr-5.32.0.tgz} +0 -0
- package/components/{tryghost-members-stripe-service-5.30.1.tgz → tryghost-members-stripe-service-5.32.0.tgz} +0 -0
- package/components/{tryghost-minifier-5.30.1.tgz → tryghost-minifier-5.32.0.tgz} +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.32.0.tgz +0 -0
- package/components/tryghost-mw-cache-control-5.32.0.tgz +0 -0
- package/components/{tryghost-mw-error-handler-5.30.1.tgz → tryghost-mw-error-handler-5.32.0.tgz} +0 -0
- package/components/tryghost-mw-session-from-token-5.32.0.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.32.0.tgz +0 -0
- package/components/tryghost-mw-vhost-5.32.0.tgz +0 -0
- package/components/tryghost-oembed-service-5.32.0.tgz +0 -0
- package/components/tryghost-package-json-5.32.0.tgz +0 -0
- package/components/tryghost-referrers-5.32.0.tgz +0 -0
- package/components/{tryghost-security-5.30.1.tgz → tryghost-security-5.32.0.tgz} +0 -0
- package/components/{tryghost-session-service-5.30.1.tgz → tryghost-session-service-5.32.0.tgz} +0 -0
- package/components/{tryghost-settings-path-manager-5.30.1.tgz → tryghost-settings-path-manager-5.32.0.tgz} +0 -0
- package/components/{tryghost-staff-service-5.30.1.tgz → tryghost-staff-service-5.32.0.tgz} +0 -0
- package/components/{tryghost-stats-service-5.30.1.tgz → tryghost-stats-service-5.32.0.tgz} +0 -0
- package/components/{tryghost-tiers-5.30.1.tgz → tryghost-tiers-5.32.0.tgz} +0 -0
- package/components/tryghost-update-check-service-5.32.0.tgz +0 -0
- package/components/tryghost-verification-trigger-5.32.0.tgz +0 -0
- package/components/{tryghost-version-notifications-data-service-5.30.1.tgz → tryghost-version-notifications-data-service-5.32.0.tgz} +0 -0
- package/components/tryghost-webmentions-5.32.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.ab7136d5c88fd7e056e4.js → chunk.143.ad05239cc363254caed7.js} +5 -5
- package/core/built/admin/assets/{chunk.178.663428ba9c9d39ba275c.js → chunk.178.5260f900f09f859bf8ed.js} +4 -4
- package/core/built/admin/assets/{chunk.47.f29250a4560868c21293.js → chunk.47.f231a64fe3fbaba23b84.js} +4 -4
- package/core/built/admin/assets/{chunk.47.f29250a4560868c21293.js.LICENSE.txt → chunk.47.f231a64fe3fbaba23b84.js.LICENSE.txt} +0 -0
- package/core/built/admin/assets/{ghost-23dc524374e35a582886c36f7dacdb05.js → ghost-4571ad33bd158ced8a9f5c13598fdb7f.js} +142 -124
- 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/{vendor-fadbf85ad92c591dc4bd3755312b6ddf.js → vendor-0441964c34d58f2aacd5a04bbe240243.js} +34 -37
- package/core/built/admin/index.html +6 -6
- package/core/frontend/helpers/ghost_head.js +1 -1
- 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/emails.js +8 -0
- 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/migrations/versions/5.32/2023-01-24-08-00-fix-invalid-tier-expiry-for-paid-members.js +35 -0
- package/core/server/data/migrations/versions/5.32/2023-01-24-08-09-restore-incorrect-expired-tiers-for-members.js +46 -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/member.js +2 -2
- 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/bulk-email/bulk-email-processor.js +7 -4
- package/core/server/services/email-service/wrapper.js +2 -1
- 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 -3
- package/package.json +114 -110
- package/yarn.lock +616 -76
- package/components/tryghost-constants-5.30.1.tgz +0 -0
- package/components/tryghost-data-generator-5.30.1.tgz +0 -0
- package/components/tryghost-domain-events-5.30.1.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.30.1.tgz +0 -0
- package/components/tryghost-email-service-5.30.1.tgz +0 -0
- package/components/tryghost-email-suppression-list-5.30.1.tgz +0 -0
- package/components/tryghost-link-redirects-5.30.1.tgz +0 -0
- package/components/tryghost-link-replacer-5.30.1.tgz +0 -0
- package/components/tryghost-magic-link-5.30.1.tgz +0 -0
- package/components/tryghost-member-attribution-5.30.1.tgz +0 -0
- package/components/tryghost-members-offers-5.30.1.tgz +0 -0
- package/components/tryghost-members-payments-5.30.1.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.30.1.tgz +0 -0
- package/components/tryghost-mw-cache-control-5.30.1.tgz +0 -0
- package/components/tryghost-mw-session-from-token-5.30.1.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.30.1.tgz +0 -0
- package/components/tryghost-mw-vhost-5.30.1.tgz +0 -0
- package/components/tryghost-oembed-service-5.30.1.tgz +0 -0
- package/components/tryghost-package-json-5.30.1.tgz +0 -0
- package/components/tryghost-referrers-5.30.1.tgz +0 -0
- package/components/tryghost-update-check-service-5.30.1.tgz +0 -0
- package/components/tryghost-verification-trigger-5.30.1.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
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const logging = require('@tryghost/logging');
|
|
2
|
+
const {createTransactionalMigration} = require('../../utils');
|
|
3
|
+
|
|
4
|
+
module.exports = createTransactionalMigration(
|
|
5
|
+
async function up(knex) {
|
|
6
|
+
logging.info('Removing expiry dates for paid members');
|
|
7
|
+
try {
|
|
8
|
+
// Fetch all members with a paid status that have an expiry date
|
|
9
|
+
// Paid members should not have an expiry date
|
|
10
|
+
const invalidExpiryIds = await knex('members_products')
|
|
11
|
+
.select('members_products.id')
|
|
12
|
+
.leftJoin('members', 'members_products.member_id', 'members.id')
|
|
13
|
+
.where('members.status', '=', 'paid')
|
|
14
|
+
.whereNotNull('members_products.expiry_at').pluck('members_products.id');
|
|
15
|
+
|
|
16
|
+
logging.info(`Found ${invalidExpiryIds.length} paid members with expiry dates`);
|
|
17
|
+
|
|
18
|
+
if (invalidExpiryIds.length === 0) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
logging.info(`Removing expiry dates for ${invalidExpiryIds.length} paid members`);
|
|
23
|
+
|
|
24
|
+
await knex('members_products')
|
|
25
|
+
.update('expiry_at', null)
|
|
26
|
+
.whereIn('id', invalidExpiryIds);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
logging.warn('Failed to remove expiry dates for paid members');
|
|
29
|
+
logging.warn(err);
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
async function down() {
|
|
33
|
+
// no-op: we don't want to reintroduce the incorrect expiry dates for member tiers
|
|
34
|
+
}
|
|
35
|
+
);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const logging = require('@tryghost/logging');
|
|
2
|
+
const ObjectId = require('bson-objectid').default;
|
|
3
|
+
const {createTransactionalMigration} = require('../../utils');
|
|
4
|
+
|
|
5
|
+
module.exports = createTransactionalMigration(
|
|
6
|
+
async function up(knex) {
|
|
7
|
+
logging.info('Restoring member<>tier mapping for members with paid status');
|
|
8
|
+
try {
|
|
9
|
+
// fetch all members with a paid status that don't have a members_products record
|
|
10
|
+
// and have a members_product_events record with an action of "added"
|
|
11
|
+
// and fetch the product_id from the most recent record for that member
|
|
12
|
+
const memberWithTiers = await knex.select('m.id as member_id', 'mpe.product_id as product_id')
|
|
13
|
+
.from('members as m')
|
|
14
|
+
.leftJoin('members_products as mp', 'm.id', 'mp.member_id')
|
|
15
|
+
.leftJoin('members_product_events as mpe', function () {
|
|
16
|
+
this.on('m.id', 'mpe.member_id')
|
|
17
|
+
.andOn(knex.raw('mpe.created_at = (SELECT max(created_at) FROM members_product_events WHERE member_id = mpe.member_id and action = "added")'));
|
|
18
|
+
})
|
|
19
|
+
.where({'m.status': 'paid', 'mp.member_id': null, 'mpe.action': 'added'});
|
|
20
|
+
|
|
21
|
+
// create a new members_products record for each member with id, member_id and product_id
|
|
22
|
+
const toInsert = memberWithTiers.map((memberTier) => {
|
|
23
|
+
return {
|
|
24
|
+
...memberTier,
|
|
25
|
+
id: ObjectId().toHexString()
|
|
26
|
+
};
|
|
27
|
+
}).filter((memberTier) => {
|
|
28
|
+
// filter out any members that don't have a product_id for some reason
|
|
29
|
+
if (!memberTier.product_id) {
|
|
30
|
+
logging.warn(`Invalid record found - member_id: ${memberTier.member_id} is without product_id`);
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
logging.info(`Inserting ${toInsert.length} records into members_products`);
|
|
37
|
+
await knex.batchInsert('members_products', toInsert);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
logging.warn('Failed to restore member<>tier mapping for members with paid status');
|
|
40
|
+
logging.warn(err);
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
async function down() {
|
|
44
|
+
// np-op: we don't want to delete the missing records we've just inserted
|
|
45
|
+
}
|
|
46
|
+
);
|
|
@@ -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':
|
|
@@ -219,8 +219,8 @@ const Member = ghostBookshelf.Model.extend({
|
|
|
219
219
|
|
|
220
220
|
async updateTierExpiry(products = [], options = {}) {
|
|
221
221
|
for (const product of products) {
|
|
222
|
-
if (product?.
|
|
223
|
-
const expiry = new Date(product.expiry_at);
|
|
222
|
+
if (product?.id) {
|
|
223
|
+
const expiry = product.expiry_at ? new Date(product.expiry_at) : null;
|
|
224
224
|
const queryOptions = _.extend({}, options, {
|
|
225
225
|
query: {where: {product_id: product.id}}
|
|
226
226
|
});
|
|
@@ -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
|
|
|
@@ -93,7 +93,7 @@ module.exports = {
|
|
|
93
93
|
} catch (error) {
|
|
94
94
|
return new FailedBatch(emailBatchId, error);
|
|
95
95
|
}
|
|
96
|
-
}, {concurrency:
|
|
96
|
+
}, {concurrency: 2});
|
|
97
97
|
|
|
98
98
|
const successes = batchResults.filter(response => (response instanceof SuccessfulBatch));
|
|
99
99
|
const failures = batchResults.filter(response => (response instanceof FailedBatch));
|
|
@@ -196,8 +196,11 @@ module.exports = {
|
|
|
196
196
|
|
|
197
197
|
// log any error that didn't come from the provider which would have already logged it
|
|
198
198
|
if (!error.code || error.code !== 'BULK_EMAIL_SEND_FAILED') {
|
|
199
|
-
let ghostError = new errors.
|
|
200
|
-
err: error
|
|
199
|
+
let ghostError = new errors.EmailError({
|
|
200
|
+
err: error,
|
|
201
|
+
code: 'BULK_EMAIL_SEND_FAILED',
|
|
202
|
+
message: `Error sending email batch ${emailBatchId}`,
|
|
203
|
+
context: error.message
|
|
201
204
|
});
|
|
202
205
|
sentry.captureException(ghostError);
|
|
203
206
|
logging.error(ghostError);
|
|
@@ -274,7 +277,7 @@ module.exports = {
|
|
|
274
277
|
});
|
|
275
278
|
|
|
276
279
|
sentry.captureException(ghostError);
|
|
277
|
-
logging.
|
|
280
|
+
logging.error(ghostError);
|
|
278
281
|
|
|
279
282
|
debug(`failed to send message (${Date.now() - startTime}ms)`);
|
|
280
283
|
throw ghostError;
|
|
@@ -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
|