ghost 5.35.0 → 5.36.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-cache-memory-ttl-5.36.0.tgz +0 -0
- package/components/tryghost-adapter-cache-redis-5.36.0.tgz +0 -0
- package/components/{tryghost-adapter-manager-5.35.0.tgz → tryghost-adapter-manager-5.36.0.tgz} +0 -0
- package/components/tryghost-api-framework-5.36.0.tgz +0 -0
- package/components/tryghost-api-version-compatibility-service-5.36.0.tgz +0 -0
- package/components/{tryghost-audience-feedback-5.35.0.tgz → tryghost-audience-feedback-5.36.0.tgz} +0 -0
- package/components/tryghost-bootstrap-socket-5.36.0.tgz +0 -0
- package/components/{tryghost-constants-5.35.0.tgz → tryghost-constants-5.36.0.tgz} +0 -0
- package/components/tryghost-custom-theme-settings-service-5.36.0.tgz +0 -0
- package/components/tryghost-data-generator-5.36.0.tgz +0 -0
- package/components/{tryghost-domain-events-5.35.0.tgz → tryghost-domain-events-5.36.0.tgz} +0 -0
- package/components/tryghost-dynamic-routing-events-5.36.0.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.36.0.tgz +0 -0
- package/components/tryghost-email-analytics-service-5.36.0.tgz +0 -0
- package/components/{tryghost-email-content-generator-5.35.0.tgz → tryghost-email-content-generator-5.36.0.tgz} +0 -0
- package/components/{tryghost-email-events-5.35.0.tgz → tryghost-email-events-5.36.0.tgz} +0 -0
- package/components/tryghost-email-service-5.36.0.tgz +0 -0
- package/components/{tryghost-email-suppression-list-5.35.0.tgz → tryghost-email-suppression-list-5.36.0.tgz} +0 -0
- package/components/tryghost-event-aware-cache-wrapper-5.36.0.tgz +0 -0
- package/components/{tryghost-express-dynamic-redirects-5.35.0.tgz → tryghost-express-dynamic-redirects-5.36.0.tgz} +0 -0
- package/components/tryghost-extract-api-key-5.36.0.tgz +0 -0
- package/components/{tryghost-html-to-plaintext-5.35.0.tgz → tryghost-html-to-plaintext-5.36.0.tgz} +0 -0
- package/components/tryghost-i18n-5.36.0.tgz +0 -0
- package/components/tryghost-importer-revue-5.36.0.tgz +0 -0
- package/components/tryghost-job-manager-5.36.0.tgz +0 -0
- package/components/{tryghost-link-redirects-5.35.0.tgz → tryghost-link-redirects-5.36.0.tgz} +0 -0
- package/components/{tryghost-link-replacer-5.35.0.tgz → tryghost-link-replacer-5.36.0.tgz} +0 -0
- package/components/tryghost-link-tracking-5.36.0.tgz +0 -0
- package/components/{tryghost-magic-link-5.35.0.tgz → tryghost-magic-link-5.36.0.tgz} +0 -0
- package/components/tryghost-mailgun-client-5.36.0.tgz +0 -0
- package/components/{tryghost-member-attribution-5.35.0.tgz → tryghost-member-attribution-5.36.0.tgz} +0 -0
- package/components/{tryghost-member-events-5.35.0.tgz → tryghost-member-events-5.36.0.tgz} +0 -0
- package/components/tryghost-members-api-5.36.0.tgz +0 -0
- package/components/{tryghost-members-csv-5.35.0.tgz → tryghost-members-csv-5.36.0.tgz} +0 -0
- package/components/{tryghost-members-events-service-5.35.0.tgz → tryghost-members-events-service-5.36.0.tgz} +0 -0
- package/components/tryghost-members-importer-5.36.0.tgz +0 -0
- package/components/{tryghost-members-offers-5.35.0.tgz → tryghost-members-offers-5.36.0.tgz} +0 -0
- package/components/{tryghost-members-payments-5.35.0.tgz → tryghost-members-payments-5.36.0.tgz} +0 -0
- package/components/{tryghost-members-ssr-5.35.0.tgz → tryghost-members-ssr-5.36.0.tgz} +0 -0
- package/components/tryghost-members-stripe-service-5.36.0.tgz +0 -0
- package/components/tryghost-milestones-5.36.0.tgz +0 -0
- package/components/tryghost-minifier-5.36.0.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.36.0.tgz +0 -0
- package/components/{tryghost-mw-cache-control-5.35.0.tgz → tryghost-mw-cache-control-5.36.0.tgz} +0 -0
- package/components/tryghost-mw-error-handler-5.36.0.tgz +0 -0
- package/components/tryghost-mw-session-from-token-5.36.0.tgz +0 -0
- package/components/{tryghost-mw-update-user-last-seen-5.35.0.tgz → tryghost-mw-update-user-last-seen-5.36.0.tgz} +0 -0
- package/components/tryghost-mw-vhost-5.36.0.tgz +0 -0
- package/components/tryghost-oembed-service-5.36.0.tgz +0 -0
- package/components/tryghost-package-json-5.36.0.tgz +0 -0
- package/components/tryghost-referrers-5.36.0.tgz +0 -0
- package/components/tryghost-security-5.36.0.tgz +0 -0
- package/components/tryghost-session-service-5.36.0.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.36.0.tgz +0 -0
- package/components/tryghost-slack-notifications-5.36.0.tgz +0 -0
- package/components/tryghost-staff-service-5.36.0.tgz +0 -0
- package/components/tryghost-stats-service-5.36.0.tgz +0 -0
- package/components/tryghost-tiers-5.36.0.tgz +0 -0
- package/components/tryghost-update-check-service-5.36.0.tgz +0 -0
- package/components/tryghost-verification-trigger-5.36.0.tgz +0 -0
- package/components/{tryghost-version-notifications-data-service-5.35.0.tgz → tryghost-version-notifications-data-service-5.36.0.tgz} +0 -0
- package/components/tryghost-webmentions-5.36.0.tgz +0 -0
- package/core/built/admin/assets/{chunk.143.d49ad252968f2ef3966d.js → chunk.143.d5eaed4616c55cdbdabb.js} +6 -6
- package/core/built/admin/assets/{chunk.178.3d45fff87e08a5be5eb8.js → chunk.178.8cafcc33fe672738cc5b.js} +4 -4
- package/core/built/admin/assets/{chunk.502.c4afca88c98edad8b268.js → chunk.502.800e1515996bcc900013.js} +3 -3
- package/core/built/admin/assets/{chunk.79.ec143a398298020c87e6.js → chunk.79.53e8aa9671b2d5dae8ba.js} +1 -1
- package/core/built/admin/assets/codemirror/{codemirror-6c43f4894cbd8db73d7f35cde836c58e.js → codemirror-3f3b9966a7237652dd31484694e38ad5.js} +1 -1
- package/core/built/admin/assets/ghost-7ecf5c7934d90798485ee5ac2956f7fe.css +1 -0
- package/core/built/admin/assets/{ghost-4a6ed62455c9e367434183980b3ca3e9.js → ghost-b828e9e3c161aae92909c2e163656bb1.js} +295 -267
- package/core/built/admin/assets/ghost-dark-e50717df8e57d3e7fee67a0bcea895ad.css +1 -0
- package/core/built/admin/assets/img/mentions-background-fa39b7597e875c165b12190eda606993.png +0 -0
- package/core/built/admin/assets/simplemde/{simplemde-28049a9bd7f432b0648747eb26958a33.js → simplemde-9cd5549b68db674742d6ec2ecd72ac30.js} +1 -1
- package/core/built/admin/assets/vendor-c4684647d4f5213e5dbb6763de430e7e.js +22 -21
- package/core/built/admin/index.html +5 -5
- package/core/server/adapters/cache/MemoryTTL.js +3 -0
- package/core/server/api/endpoints/emails.js +35 -0
- package/core/server/api/endpoints/pages-public.js +1 -2
- package/core/server/api/endpoints/posts-public.js +2 -1
- package/core/server/api/endpoints/tags-public.js +2 -1
- package/core/server/api/endpoints/utils/serializers/input/index.js +4 -0
- package/core/server/api/endpoints/utils/serializers/input/mentions.js +11 -0
- package/core/server/api/endpoints/utils/serializers/output/emails.js +4 -0
- package/core/server/api/endpoints/utils/serializers/output/mappers/posts.js +2 -5
- package/core/server/api/endpoints/utils/serializers/output/utils/clean.js +1 -0
- package/core/server/api/endpoints/utils/serializers/output/utils/extra-attrs.js +19 -11
- package/core/server/data/exporter/table-lists.js +2 -1
- package/core/server/data/migrations/versions/5.36/2023-02-20-12-22-add-milestones-table.js +10 -0
- package/core/server/data/migrations/versions/5.36/2023-02-21-12-29-add-milestone-notifications-column.js +7 -0
- package/core/server/data/migrations/versions/5.36/2023-02-23-10-40-set-outbound-link-tagging-based-on-source-tracking.js +31 -0
- package/core/server/data/schema/schema.js +9 -0
- package/core/server/lib/request-external.js +14 -13
- package/core/server/models/milestone.js +9 -0
- package/core/server/models/user.js +4 -1
- package/core/server/services/email-analytics/lib/queries.js +18 -3
- package/core/server/services/email-analytics/wrapper.js +34 -15
- package/core/server/services/email-suppression-list/MailgunEmailSuppressionList.js +2 -0
- package/core/server/services/mentions/BookshelfMentionRepository.js +2 -1
- package/core/server/services/mentions/ResourceService.js +6 -0
- package/core/server/services/mentions/RoutingService.js +2 -1
- package/core/server/services/mentions/service.js +1 -3
- package/core/server/services/milestones/BookshelfMilestoneRepository.js +136 -0
- package/core/server/services/milestones/MilestoneQueries.js +8 -3
- package/core/server/services/milestones/service.js +47 -9
- package/core/server/services/oembed/nft-oembed.js +1 -2
- package/core/server/services/posts-public/service.js +21 -9
- package/core/server/services/tags-public/service.js +21 -10
- package/core/server/services/websockets/service.js +2 -1
- package/core/server/web/api/endpoints/admin/routes.js +3 -0
- package/core/shared/config/defaults.json +5 -2
- package/core/shared/labs.js +5 -4
- package/package.json +125 -124
- package/yarn.lock +151 -199
- package/components/tryghost-adapter-cache-redis-5.35.0.tgz +0 -0
- package/components/tryghost-api-framework-5.35.0.tgz +0 -0
- package/components/tryghost-api-version-compatibility-service-5.35.0.tgz +0 -0
- package/components/tryghost-bootstrap-socket-5.35.0.tgz +0 -0
- package/components/tryghost-custom-theme-settings-service-5.35.0.tgz +0 -0
- package/components/tryghost-data-generator-5.35.0.tgz +0 -0
- package/components/tryghost-dynamic-routing-events-5.35.0.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.35.0.tgz +0 -0
- package/components/tryghost-email-analytics-service-5.35.0.tgz +0 -0
- package/components/tryghost-email-service-5.35.0.tgz +0 -0
- package/components/tryghost-extract-api-key-5.35.0.tgz +0 -0
- package/components/tryghost-i18n-5.35.0.tgz +0 -0
- package/components/tryghost-importer-revue-5.35.0.tgz +0 -0
- package/components/tryghost-job-manager-5.35.0.tgz +0 -0
- package/components/tryghost-link-tracking-5.35.0.tgz +0 -0
- package/components/tryghost-mailgun-client-5.35.0.tgz +0 -0
- package/components/tryghost-members-api-5.35.0.tgz +0 -0
- package/components/tryghost-members-importer-5.35.0.tgz +0 -0
- package/components/tryghost-members-stripe-service-5.35.0.tgz +0 -0
- package/components/tryghost-milestones-5.35.0.tgz +0 -0
- package/components/tryghost-minifier-5.35.0.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.35.0.tgz +0 -0
- package/components/tryghost-mw-error-handler-5.35.0.tgz +0 -0
- package/components/tryghost-mw-session-from-token-5.35.0.tgz +0 -0
- package/components/tryghost-mw-vhost-5.35.0.tgz +0 -0
- package/components/tryghost-oembed-service-5.35.0.tgz +0 -0
- package/components/tryghost-package-json-5.35.0.tgz +0 -0
- package/components/tryghost-public-resource-repository-5.35.0.tgz +0 -0
- package/components/tryghost-referrers-5.35.0.tgz +0 -0
- package/components/tryghost-security-5.35.0.tgz +0 -0
- package/components/tryghost-session-service-5.35.0.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.35.0.tgz +0 -0
- package/components/tryghost-slack-notifications-5.35.0.tgz +0 -0
- package/components/tryghost-staff-service-5.35.0.tgz +0 -0
- package/components/tryghost-stats-service-5.35.0.tgz +0 -0
- package/components/tryghost-tiers-5.35.0.tgz +0 -0
- package/components/tryghost-update-check-service-5.35.0.tgz +0 -0
- package/components/tryghost-verification-trigger-5.35.0.tgz +0 -0
- package/components/tryghost-webmentions-5.35.0.tgz +0 -0
- package/core/built/admin/assets/ghost-558c1e319d6e025bfab2054bc0f7fe83.css +0 -1
- package/core/built/admin/assets/ghost-dark-a15754df1f9070dc2525482ce22e2251.css +0 -1
- /package/core/built/admin/assets/{chunk.502.c4afca88c98edad8b268.js.LICENSE.txt → chunk.502.800e1515996bcc900013.js.LICENSE.txt} +0 -0
|
@@ -15,9 +15,24 @@ module.exports = {
|
|
|
15
15
|
const startDate = new Date();
|
|
16
16
|
|
|
17
17
|
// three separate queries is much faster than using max/greatest (with coalesce to handle nulls) across columns
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
let {maxDeliveredAt} = await db.knex('email_recipients').select(db.knex.raw('MAX(delivered_at) as maxDeliveredAt')).first() || {};
|
|
19
|
+
let {maxOpenedAt} = await db.knex('email_recipients').select(db.knex.raw('MAX(opened_at) as maxOpenedAt')).first() || {};
|
|
20
|
+
let {maxFailedAt} = await db.knex('email_recipients').select(db.knex.raw('MAX(failed_at) as maxFailedAt')).first() || {};
|
|
21
|
+
|
|
22
|
+
if (maxDeliveredAt && !(maxDeliveredAt instanceof Date)) {
|
|
23
|
+
// SQLite returns a string instead of a Date
|
|
24
|
+
maxDeliveredAt = new Date(maxDeliveredAt);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (maxOpenedAt && !(maxOpenedAt instanceof Date)) {
|
|
28
|
+
// SQLite returns a string instead of a Date
|
|
29
|
+
maxOpenedAt = new Date(maxOpenedAt);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (maxFailedAt && !(maxFailedAt instanceof Date)) {
|
|
33
|
+
// SQLite returns a string instead of a Date
|
|
34
|
+
maxFailedAt = new Date(maxFailedAt);
|
|
35
|
+
}
|
|
21
36
|
|
|
22
37
|
const lastSeenEventTimestamp = _.max([maxDeliveredAt, maxOpenedAt, maxFailedAt]);
|
|
23
38
|
debug(`getLastSeenEventTimestamp: finished in ${Date.now() - startDate}ms`);
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const logging = require('@tryghost/logging');
|
|
2
|
-
const debug = require('@tryghost/debug')('jobs:email-analytics:fetch-latest');
|
|
3
2
|
|
|
4
3
|
class EmailAnalyticsServiceWrapper {
|
|
5
4
|
init() {
|
|
@@ -55,22 +54,40 @@ class EmailAnalyticsServiceWrapper {
|
|
|
55
54
|
});
|
|
56
55
|
}
|
|
57
56
|
|
|
58
|
-
async fetchLatest() {
|
|
57
|
+
async fetchLatest({maxEvents} = {maxEvents: Infinity}) {
|
|
58
|
+
logging.info('[EmailAnalytics] Fetch latest started');
|
|
59
|
+
|
|
59
60
|
const fetchStartDate = new Date();
|
|
60
|
-
|
|
61
|
-
const eventStats = await this.service.fetchLatest();
|
|
61
|
+
const totalEvents = await this.service.fetchLatest({maxEvents});
|
|
62
62
|
const fetchEndDate = new Date();
|
|
63
|
-
debug(`Finished fetching ${eventStats.totalEvents} analytics events in ${fetchEndDate.getTime() - fetchStartDate.getTime()}ms`);
|
|
64
63
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const aggregateEndDate = new Date();
|
|
69
|
-
debug(`Finished aggregating email analytics in ${aggregateEndDate.getTime() - aggregateStartDate.getTime()}ms`);
|
|
64
|
+
logging.info(`[EmailAnalytics] Fetched ${totalEvents} events and aggregated stats in ${fetchEndDate.getTime() - fetchStartDate.getTime()}ms (latest)`);
|
|
65
|
+
return totalEvents;
|
|
66
|
+
}
|
|
70
67
|
|
|
71
|
-
|
|
68
|
+
async fetchMissing({maxEvents} = {maxEvents: Infinity}) {
|
|
69
|
+
logging.info('[EmailAnalytics] Fetch missing started');
|
|
72
70
|
|
|
73
|
-
|
|
71
|
+
const fetchStartDate = new Date();
|
|
72
|
+
const totalEvents = await this.service.fetchMissing({maxEvents});
|
|
73
|
+
const fetchEndDate = new Date();
|
|
74
|
+
|
|
75
|
+
logging.info(`[EmailAnalytics] Fetched ${totalEvents} events and aggregated stats in ${fetchEndDate.getTime() - fetchStartDate.getTime()}ms (missing)`);
|
|
76
|
+
return totalEvents;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async fetchScheduled({maxEvents}) {
|
|
80
|
+
if (maxEvents < 300) {
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
logging.info('[EmailAnalytics] Fetch scheduled started');
|
|
84
|
+
|
|
85
|
+
const fetchStartDate = new Date();
|
|
86
|
+
const totalEvents = await this.service.fetchScheduled({maxEvents});
|
|
87
|
+
const fetchEndDate = new Date();
|
|
88
|
+
|
|
89
|
+
logging.info(`[EmailAnalytics] Fetched ${totalEvents} events and aggregated stats in ${fetchEndDate.getTime() - fetchStartDate.getTime()}ms (scheduled)`);
|
|
90
|
+
return totalEvents;
|
|
74
91
|
}
|
|
75
92
|
|
|
76
93
|
async startFetch() {
|
|
@@ -80,12 +97,14 @@ class EmailAnalyticsServiceWrapper {
|
|
|
80
97
|
}
|
|
81
98
|
this.fetching = true;
|
|
82
99
|
|
|
83
|
-
logging.info('Email analytics fetch started');
|
|
84
100
|
try {
|
|
85
|
-
const
|
|
101
|
+
const c1 = await this.fetchLatest({maxEvents: Infinity});
|
|
102
|
+
const c2 = await this.fetchMissing({maxEvents: Infinity});
|
|
103
|
+
|
|
104
|
+
// Only fetch scheduled if we didn't fetch a lot of normal events
|
|
105
|
+
await this.fetchScheduled({maxEvents: 20000 - c1 - c2});
|
|
86
106
|
|
|
87
107
|
this.fetching = false;
|
|
88
|
-
return eventStats;
|
|
89
108
|
} catch (e) {
|
|
90
109
|
logging.error(e, 'Error while fetching email analytics');
|
|
91
110
|
|
|
@@ -2,6 +2,7 @@ const {AbstractEmailSuppressionList, EmailSuppressionData, EmailSuppressedEvent}
|
|
|
2
2
|
const {SpamComplaintEvent, EmailBouncedEvent} = require('@tryghost/email-events');
|
|
3
3
|
const DomainEvents = require('@tryghost/domain-events');
|
|
4
4
|
const logging = require('@tryghost/logging');
|
|
5
|
+
const models = require('../../models');
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* @typedef {object} IMailgunAPIClient
|
|
@@ -95,6 +96,7 @@ class MailgunEmailSuppressionList extends AbstractEmailSuppressionList {
|
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
async init() {
|
|
99
|
+
this.Suppression = models.Suppression;
|
|
98
100
|
const handleEvent = reason => async (event) => {
|
|
99
101
|
try {
|
|
100
102
|
if (reason === 'bounce') {
|
|
@@ -49,6 +49,7 @@ module.exports = class BookshelfMentionRepository {
|
|
|
49
49
|
timestamp: model.get('created_at'),
|
|
50
50
|
payload,
|
|
51
51
|
resourceId: model.get('resource_id'),
|
|
52
|
+
resourceType: model.get('resource_type'),
|
|
52
53
|
sourceTitle: model.get('source_title'),
|
|
53
54
|
sourceSiteTitle: model.get('source_site_title'),
|
|
54
55
|
sourceAuthor: model.get('source_author'),
|
|
@@ -106,7 +107,7 @@ module.exports = class BookshelfMentionRepository {
|
|
|
106
107
|
source_favicon: mention.sourceFavicon?.href,
|
|
107
108
|
target: mention.target.href,
|
|
108
109
|
resource_id: mention.resourceId?.toHexString(),
|
|
109
|
-
resource_type: mention.
|
|
110
|
+
resource_type: mention.resourceType,
|
|
110
111
|
payload: mention.payload ? JSON.stringify(mention.payload) : null,
|
|
111
112
|
deleted: Mention.isDeleted(mention),
|
|
112
113
|
verified: mention.verified
|
|
@@ -37,6 +37,12 @@ module.exports = class ResourceService {
|
|
|
37
37
|
id: ObjectID.createFromHexString(resource.data.id)
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
|
+
if (resource?.config?.type === 'pages') {
|
|
41
|
+
return {
|
|
42
|
+
type: 'page',
|
|
43
|
+
id: ObjectID.createFromHexString(resource.data.id)
|
|
44
|
+
};
|
|
45
|
+
}
|
|
40
46
|
return {
|
|
41
47
|
type: null,
|
|
42
48
|
id: null
|
|
@@ -56,7 +56,8 @@ module.exports = class RoutingService {
|
|
|
56
56
|
|
|
57
57
|
try {
|
|
58
58
|
const response = await this.#externalRequest.head(url, {
|
|
59
|
-
followRedirect: false
|
|
59
|
+
followRedirect: false,
|
|
60
|
+
throwHttpErrors: false
|
|
60
61
|
});
|
|
61
62
|
if (response.statusCode < 400 && response.statusCode > 199) {
|
|
62
63
|
return true;
|
|
@@ -70,17 +70,15 @@ module.exports = {
|
|
|
70
70
|
if (!id) {
|
|
71
71
|
return null;
|
|
72
72
|
}
|
|
73
|
-
|
|
74
73
|
const post = await models.Post.findOne({id: id.toHexString()});
|
|
75
74
|
|
|
76
75
|
if (!post) {
|
|
77
76
|
return null;
|
|
78
77
|
}
|
|
79
|
-
|
|
80
78
|
return {
|
|
81
79
|
id: id,
|
|
82
80
|
name: post.get('title'),
|
|
83
|
-
type:
|
|
81
|
+
type: post.get('type')
|
|
84
82
|
};
|
|
85
83
|
}
|
|
86
84
|
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
const {Milestone} = require('@tryghost/milestones');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {import('@tryghost/milestones/lib/MilestonesService').IMilestoneRepository} IMilestoneRepository
|
|
5
|
+
* @typedef {import('@tryghost/milestones/lib/MilestonesService')} Milestone
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @implements {IMilestoneRepository}
|
|
10
|
+
*/
|
|
11
|
+
module.exports = class BookshelfMilestoneRepository {
|
|
12
|
+
/** @type {Object} */
|
|
13
|
+
#MilestoneModel;
|
|
14
|
+
|
|
15
|
+
/** @type {import('@tryghost/domain-events')} */
|
|
16
|
+
#DomainEvents;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {object} deps
|
|
20
|
+
* @param {object} deps.MilestoneModel Bookshelf Model
|
|
21
|
+
* @param {import('@tryghost/domain-events')} deps.DomainEvents
|
|
22
|
+
*/
|
|
23
|
+
constructor(deps) {
|
|
24
|
+
this.#MilestoneModel = deps.MilestoneModel;
|
|
25
|
+
this.#DomainEvents = deps.DomainEvents;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#modelToMilestone(model) {
|
|
29
|
+
return Milestone.create({
|
|
30
|
+
id: model.get('id'),
|
|
31
|
+
type: model.get('type'),
|
|
32
|
+
value: model.get('value'),
|
|
33
|
+
currency: model.get('currency'),
|
|
34
|
+
createdAt: model.get('created_at'),
|
|
35
|
+
emailSentAt: model.get('email_sent_at')
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {import('@tryghost/milestones/lib/Milestone')} milestone
|
|
41
|
+
* @returns {Promise<void>}
|
|
42
|
+
*/
|
|
43
|
+
async save(milestone) {
|
|
44
|
+
const data = {
|
|
45
|
+
id: milestone.id.toHexString(),
|
|
46
|
+
type: milestone.type,
|
|
47
|
+
value: milestone.value,
|
|
48
|
+
currency: milestone?.currency,
|
|
49
|
+
created_at: milestone?.createdAt,
|
|
50
|
+
email_sent_at: milestone?.emailSentAt
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const existing = await this.#MilestoneModel.findOne({id: data.id}, {require: false});
|
|
54
|
+
|
|
55
|
+
if (!existing) {
|
|
56
|
+
await this.#MilestoneModel.add(data);
|
|
57
|
+
} else {
|
|
58
|
+
await this.#MilestoneModel.edit(data, {
|
|
59
|
+
id: data.id
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
for (const event of milestone.events) {
|
|
63
|
+
this.#DomainEvents.dispatch(event);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {'arr'|'members'} type
|
|
69
|
+
* @param {string} [currency]
|
|
70
|
+
*
|
|
71
|
+
* @returns {Promise<import('@tryghost/milestones/lib/Milestone')|null>}
|
|
72
|
+
*/
|
|
73
|
+
async getLatestByType(type, currency = 'usd') {
|
|
74
|
+
let milestone = null;
|
|
75
|
+
|
|
76
|
+
if (type === 'arr') {
|
|
77
|
+
milestone = await this.#MilestoneModel.findAll({filter: `currency:${currency}+type:arr`, order: 'created_at ASC, value DESC'}, {require: false});
|
|
78
|
+
} else {
|
|
79
|
+
milestone = await this.#MilestoneModel.findAll({filter: 'type:members', order: 'created_at ASC, value DESC'}, {require: false});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!milestone || !milestone?.models?.length) {
|
|
83
|
+
return null;
|
|
84
|
+
} else {
|
|
85
|
+
milestone = milestone.models?.[0];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return this.#modelToMilestone(milestone);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @returns {Promise<import('@tryghost/milestones/lib/Milestone')|null>}
|
|
93
|
+
*/
|
|
94
|
+
async getLastEmailSent() {
|
|
95
|
+
let milestone = await this.#MilestoneModel.findAll({filter: 'email_sent_at:-null', order: 'email_sent_at ASC'}, {require: false});
|
|
96
|
+
|
|
97
|
+
if (!milestone || !milestone?.models?.length) {
|
|
98
|
+
return null;
|
|
99
|
+
} else {
|
|
100
|
+
milestone = milestone.models?.[0];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return this.#modelToMilestone(milestone);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @param {number} value
|
|
108
|
+
* @param {string} [currency]
|
|
109
|
+
*
|
|
110
|
+
* @returns {Promise<import('@tryghost/milestones/lib/Milestone')|null>}
|
|
111
|
+
*/
|
|
112
|
+
async getByARR(value, currency = 'usd') {
|
|
113
|
+
// find a milestone of the ARR type by a given value
|
|
114
|
+
const milestone = await this.#MilestoneModel.findOne({type: 'arr', currency: currency, value: value}, {require: false});
|
|
115
|
+
|
|
116
|
+
if (!milestone) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
return this.#modelToMilestone(milestone);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @param {number} value
|
|
124
|
+
*
|
|
125
|
+
* @returns {Promise<import('@tryghost/milestones/lib/Milestone')|null>}
|
|
126
|
+
*/
|
|
127
|
+
async getByCount(value) {
|
|
128
|
+
// find a milestone of the members type by a given value
|
|
129
|
+
const milestone = await this.#MilestoneModel.findOne({type: 'members', value: value}, {require: false});
|
|
130
|
+
|
|
131
|
+
if (!milestone) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
return this.#modelToMilestone(milestone);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
const MIN_DAYS_SINCE_IMPORTED = 7;
|
|
2
|
-
|
|
3
1
|
module.exports = class MilestoneQueries {
|
|
4
2
|
#db;
|
|
5
3
|
|
|
4
|
+
/** @type {number} */
|
|
5
|
+
#minDaysSinceImported;
|
|
6
|
+
|
|
6
7
|
constructor(deps) {
|
|
7
8
|
this.#db = deps.db;
|
|
9
|
+
this.#minDaysSinceImported = deps.minDaysSinceImported;
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
/**
|
|
@@ -31,10 +33,13 @@ module.exports = class MilestoneQueries {
|
|
|
31
33
|
* @returns {Promise<boolean>}
|
|
32
34
|
*/
|
|
33
35
|
async hasImportedMembersInPeriod() {
|
|
36
|
+
const importedThreshold = new Date();
|
|
37
|
+
importedThreshold.setDate(importedThreshold.getDate() - this.#minDaysSinceImported);
|
|
38
|
+
|
|
34
39
|
const [hasImportedMembers] = await this.#db.knex('members_subscribe_events')
|
|
35
40
|
.count('id as count')
|
|
36
41
|
.where('source', '=', 'import')
|
|
37
|
-
.where('created_at', '>=',
|
|
42
|
+
.where('created_at', '>=', importedThreshold);
|
|
38
43
|
|
|
39
44
|
return hasImportedMembers?.count > 0;
|
|
40
45
|
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
const DomainEvents = require('@tryghost/domain-events');
|
|
2
|
+
const logging = require('@tryghost/logging');
|
|
3
|
+
const models = require('../../models');
|
|
4
|
+
const BookshelfMilestoneRepository = require('./BookshelfMilestoneRepository');
|
|
5
|
+
|
|
6
|
+
const JOB_TIMEOUT = 1000 * 60 * 60 * 24 * (Math.floor(Math.random() * 4)); // 0 - 4 days;
|
|
2
7
|
|
|
3
8
|
const getStripeLiveEnabled = () => {
|
|
4
9
|
const settingsCache = require('../../../shared/settings-cache');
|
|
@@ -28,19 +33,23 @@ module.exports = {
|
|
|
28
33
|
const db = require('../../data/db');
|
|
29
34
|
const MilestoneQueries = require('./MilestoneQueries');
|
|
30
35
|
|
|
31
|
-
const {
|
|
32
|
-
MilestonesService,
|
|
33
|
-
InMemoryMilestoneRepository
|
|
34
|
-
} = require('@tryghost/milestones');
|
|
36
|
+
const {MilestonesService} = require('@tryghost/milestones');
|
|
35
37
|
const config = require('../../../shared/config');
|
|
36
38
|
const milestonesConfig = config.get('milestones');
|
|
37
39
|
|
|
38
|
-
const repository = new
|
|
39
|
-
|
|
40
|
+
const repository = new BookshelfMilestoneRepository({
|
|
41
|
+
DomainEvents,
|
|
42
|
+
MilestoneModel: models.Milestone
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const queries = new MilestoneQueries({
|
|
46
|
+
db,
|
|
47
|
+
minDaysSinceImported: milestonesConfig?.minDaysSinceImported || 7
|
|
48
|
+
});
|
|
40
49
|
|
|
41
50
|
this.api = new MilestonesService({
|
|
42
51
|
repository,
|
|
43
|
-
milestonesConfig,
|
|
52
|
+
milestonesConfig,
|
|
44
53
|
queries
|
|
45
54
|
});
|
|
46
55
|
}
|
|
@@ -69,10 +78,39 @@ module.exports = {
|
|
|
69
78
|
},
|
|
70
79
|
|
|
71
80
|
/**
|
|
81
|
+
*
|
|
82
|
+
* @param {number} [customTimeout]
|
|
83
|
+
*
|
|
84
|
+
* @returns {Promise<object>}
|
|
85
|
+
*/
|
|
86
|
+
async scheduleRun(customTimeout) {
|
|
87
|
+
const timeOut = customTimeout || JOB_TIMEOUT;
|
|
88
|
+
|
|
89
|
+
const today = new Date();
|
|
90
|
+
const msNow = today.getMilliseconds();
|
|
91
|
+
const newMs = msNow + timeOut;
|
|
92
|
+
const jobDate = today.setMilliseconds(newMs);
|
|
93
|
+
|
|
94
|
+
logging.info(`Running milestone emails job on ${new Date(jobDate).toString()}`);
|
|
95
|
+
|
|
96
|
+
return new Promise((resolve) => {
|
|
97
|
+
setTimeout(async () => {
|
|
98
|
+
const result = await this.run();
|
|
99
|
+
return resolve(result);
|
|
100
|
+
}, timeOut);
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @param {number} [customTimeout]
|
|
106
|
+
* Only used temporary for testing purposes.
|
|
107
|
+
* Will be removed, after job scheduling implementation.
|
|
108
|
+
*
|
|
72
109
|
* @returns {Promise<object>}
|
|
73
110
|
*/
|
|
74
|
-
async initAndRun() {
|
|
111
|
+
async initAndRun(customTimeout) {
|
|
75
112
|
await this.init();
|
|
76
|
-
|
|
113
|
+
|
|
114
|
+
return this.scheduleRun(customTimeout);
|
|
77
115
|
}
|
|
78
116
|
};
|
|
@@ -42,9 +42,8 @@ class NFTOEmbedProvider {
|
|
|
42
42
|
headers['X-API-KEY'] = this.dependencies.config.apiKey;
|
|
43
43
|
}
|
|
44
44
|
const result = await externalRequest(`https://api.opensea.io/api/v1/asset/${transaction}/${asset}/?format=json`, {
|
|
45
|
-
json: true,
|
|
46
45
|
headers
|
|
47
|
-
});
|
|
46
|
+
}).json();
|
|
48
47
|
return {
|
|
49
48
|
version: '1.0',
|
|
50
49
|
type: 'nft',
|
|
@@ -6,24 +6,36 @@ class PostsPublicServiceWrapper {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
// Wire up all the dependencies
|
|
9
|
-
const {Post} = require('../../models');
|
|
10
9
|
const adapterManager = require('../adapter-manager');
|
|
11
10
|
const config = require('../../../shared/config');
|
|
11
|
+
const EventAwareCacheWrapper = require('@tryghost/event-aware-cache-wrapper');
|
|
12
|
+
const EventRegistry = require('../../lib/common/events');
|
|
12
13
|
|
|
13
14
|
let postsCache;
|
|
14
15
|
if (config.get('hostSettings:postsPublicCache:enabled')) {
|
|
15
|
-
|
|
16
|
+
const cache = adapterManager.getAdapter('cache:postsPublic');
|
|
17
|
+
postsCache = new EventAwareCacheWrapper({
|
|
18
|
+
cache: cache,
|
|
19
|
+
resetEvents: ['site.changed'],
|
|
20
|
+
eventRegistry: EventRegistry
|
|
21
|
+
});
|
|
16
22
|
}
|
|
17
23
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
let cache;
|
|
25
|
+
if (postsCache) {
|
|
26
|
+
// @NOTE: exposing cache through getter and setter to not loose the context of "this"
|
|
27
|
+
cache = {
|
|
28
|
+
get() {
|
|
29
|
+
return postsCache.get(...arguments);
|
|
30
|
+
},
|
|
31
|
+
set() {
|
|
32
|
+
return postsCache.set(...arguments);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
24
36
|
|
|
25
37
|
this.api = {
|
|
26
|
-
|
|
38
|
+
cache: cache
|
|
27
39
|
};
|
|
28
40
|
}
|
|
29
41
|
}
|
|
@@ -6,24 +6,35 @@ class TagsPublicServiceWrapper {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
// Wire up all the dependencies
|
|
9
|
-
const {TagPublic} = require('../../models');
|
|
10
9
|
const adapterManager = require('../adapter-manager');
|
|
11
10
|
const config = require('../../../shared/config');
|
|
11
|
+
const EventAwareCacheWrapper = require('@tryghost/event-aware-cache-wrapper');
|
|
12
|
+
const EventRegistry = require('../../lib/common/events');
|
|
12
13
|
|
|
13
14
|
let tagsCache;
|
|
14
15
|
if (config.get('hostSettings:tagsPublicCache:enabled')) {
|
|
15
|
-
|
|
16
|
+
let tagsPublicCache = adapterManager.getAdapter('cache:tagsPublic');
|
|
17
|
+
tagsCache = new EventAwareCacheWrapper({
|
|
18
|
+
cache: tagsPublicCache,
|
|
19
|
+
resetEvents: ['site.changed'],
|
|
20
|
+
eventRegistry: EventRegistry
|
|
21
|
+
});
|
|
16
22
|
}
|
|
17
23
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
let cache;
|
|
25
|
+
if (tagsCache) {
|
|
26
|
+
// @NOTE: exposing cache through getter and setter to not loose the context of "this"
|
|
27
|
+
cache = {
|
|
28
|
+
get() {
|
|
29
|
+
return tagsCache.get(...arguments);
|
|
30
|
+
},
|
|
31
|
+
set() {
|
|
32
|
+
return tagsCache.set(...arguments);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
25
36
|
this.api = {
|
|
26
|
-
|
|
37
|
+
cache: cache
|
|
27
38
|
};
|
|
28
39
|
}
|
|
29
40
|
}
|
|
@@ -15,7 +15,8 @@ module.exports = {
|
|
|
15
15
|
let count = 0;
|
|
16
16
|
|
|
17
17
|
io.on(`connection`, (socket) => {
|
|
18
|
-
|
|
18
|
+
logging.info(`Websockets client connected (id: ${socket.id})`);
|
|
19
|
+
|
|
19
20
|
// on connect, send current value
|
|
20
21
|
socket.emit('addCount', count);
|
|
21
22
|
// listen to to changes in value from client
|
|
@@ -300,6 +300,9 @@ module.exports = function apiRoutes() {
|
|
|
300
300
|
router.put('/emails/:id/retry', mw.authAdminApi, http(api.emails.retry));
|
|
301
301
|
router.get('/emails/:id/batches', mw.authAdminApi, http(api.emails.browseBatches));
|
|
302
302
|
router.get('/emails/:id/recipient-failures', mw.authAdminApi, http(api.emails.browseFailures));
|
|
303
|
+
router.get('/emails/:id/analytics', mw.authAdminApi, http(api.emails.analyticsStatus));
|
|
304
|
+
router.put('/emails/:id/analytics', mw.authAdminApi, http(api.emails.scheduleAnalytics));
|
|
305
|
+
router.delete('/emails/analytics', mw.authAdminApi, http(api.emails.cancelScheduledAnalytics));
|
|
303
306
|
|
|
304
307
|
// ## Snippets
|
|
305
308
|
router.get('/snippets', mw.authAdminApi, http(api.snippets.browse));
|
|
@@ -176,7 +176,7 @@
|
|
|
176
176
|
},
|
|
177
177
|
"portal": {
|
|
178
178
|
"url": "https://cdn.jsdelivr.net/ghost/portal@~{version}/umd/portal.min.js",
|
|
179
|
-
"version": "2.
|
|
179
|
+
"version": "2.25"
|
|
180
180
|
},
|
|
181
181
|
"sodoSearch": {
|
|
182
182
|
"url": "https://cdn.jsdelivr.net/ghost/sodo-search@~{version}/umd/sodo-search.min.js",
|
|
@@ -213,6 +213,9 @@
|
|
|
213
213
|
"values": [100, 1000, 10000, 50000, 100000, 250000, 500000, 1000000]
|
|
214
214
|
}
|
|
215
215
|
],
|
|
216
|
-
"members": [100, 1000, 10000, 25000, 50000, 100000, 250000, 500000, 1000000]
|
|
216
|
+
"members": [100, 1000, 10000, 25000, 50000, 100000, 250000, 500000, 1000000],
|
|
217
|
+
"minDaysSinceImported": 7,
|
|
218
|
+
"minDaysSinceLastEmail": 14,
|
|
219
|
+
"maxPercentageFromMilestone": 0.1
|
|
217
220
|
}
|
|
218
221
|
}
|
package/core/shared/labs.js
CHANGED
|
@@ -20,7 +20,9 @@ const GA_FEATURES = [
|
|
|
20
20
|
'memberAttribution',
|
|
21
21
|
'audienceFeedback',
|
|
22
22
|
'themeErrorsNotification',
|
|
23
|
-
'emailStability'
|
|
23
|
+
'emailStability',
|
|
24
|
+
'emailErrors',
|
|
25
|
+
'outboundLinkTagging'
|
|
24
26
|
];
|
|
25
27
|
|
|
26
28
|
// NOTE: this allowlist is meant to be used to filter out any unexpected
|
|
@@ -28,7 +30,6 @@ const GA_FEATURES = [
|
|
|
28
30
|
const BETA_FEATURES = [
|
|
29
31
|
'activitypub',
|
|
30
32
|
'webmentions',
|
|
31
|
-
'emailErrors',
|
|
32
33
|
'milestoneEmails'
|
|
33
34
|
];
|
|
34
35
|
|
|
@@ -36,8 +37,8 @@ const ALPHA_FEATURES = [
|
|
|
36
37
|
'urlCache',
|
|
37
38
|
'beforeAfterCard',
|
|
38
39
|
'lexicalEditor',
|
|
39
|
-
'
|
|
40
|
-
'
|
|
40
|
+
'websockets',
|
|
41
|
+
'webmentionEmails'
|
|
41
42
|
];
|
|
42
43
|
|
|
43
44
|
module.exports.GA_KEYS = [...GA_FEATURES];
|