ghost 5.110.4 → 5.112.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.112.0.tgz +0 -0
- package/components/tryghost-adapter-cache-redis-5.112.0.tgz +0 -0
- package/components/{tryghost-adapter-manager-5.110.4.tgz → tryghost-adapter-manager-5.112.0.tgz} +0 -0
- package/components/tryghost-announcement-bar-settings-5.112.0.tgz +0 -0
- package/components/{tryghost-api-framework-5.110.4.tgz → tryghost-api-framework-5.112.0.tgz} +0 -0
- package/components/tryghost-api-version-compatibility-service-5.112.0.tgz +0 -0
- package/components/{tryghost-audience-feedback-5.110.4.tgz → tryghost-audience-feedback-5.112.0.tgz} +0 -0
- package/components/tryghost-bookshelf-repository-5.112.0.tgz +0 -0
- package/components/tryghost-bootstrap-socket-5.112.0.tgz +0 -0
- package/components/tryghost-captcha-service-5.112.0.tgz +0 -0
- package/components/tryghost-constants-5.112.0.tgz +0 -0
- package/components/tryghost-custom-fonts-5.112.0.tgz +0 -0
- package/components/{tryghost-custom-theme-settings-service-5.110.4.tgz → tryghost-custom-theme-settings-service-5.112.0.tgz} +0 -0
- package/components/{tryghost-data-generator-5.110.4.tgz → tryghost-data-generator-5.112.0.tgz} +0 -0
- package/components/{tryghost-domain-events-5.110.4.tgz → tryghost-domain-events-5.112.0.tgz} +0 -0
- package/components/tryghost-donations-5.112.0.tgz +0 -0
- package/components/tryghost-email-addresses-5.112.0.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.112.0.tgz +0 -0
- package/components/{tryghost-email-analytics-service-5.110.4.tgz → tryghost-email-analytics-service-5.112.0.tgz} +0 -0
- package/components/tryghost-email-content-generator-5.112.0.tgz +0 -0
- package/components/tryghost-email-events-5.112.0.tgz +0 -0
- package/components/tryghost-email-service-5.112.0.tgz +0 -0
- package/components/tryghost-email-suppression-list-5.112.0.tgz +0 -0
- package/components/{tryghost-express-dynamic-redirects-5.110.4.tgz → tryghost-express-dynamic-redirects-5.112.0.tgz} +0 -0
- package/components/{tryghost-external-media-inliner-5.110.4.tgz → tryghost-external-media-inliner-5.112.0.tgz} +0 -0
- package/components/tryghost-extract-api-key-5.112.0.tgz +0 -0
- package/components/tryghost-ghost-5.112.0.tgz +0 -0
- package/components/{tryghost-html-to-plaintext-5.110.4.tgz → tryghost-html-to-plaintext-5.112.0.tgz} +0 -0
- package/components/tryghost-i18n-5.112.0.tgz +0 -0
- package/components/tryghost-identity-token-service-5.112.0.tgz +0 -0
- package/components/tryghost-importer-handler-content-files-5.112.0.tgz +0 -0
- package/components/{tryghost-importer-revue-5.110.4.tgz → tryghost-importer-revue-5.112.0.tgz} +0 -0
- package/components/tryghost-in-memory-repository-5.112.0.tgz +0 -0
- package/components/{tryghost-job-manager-5.110.4.tgz → tryghost-job-manager-5.112.0.tgz} +0 -0
- package/components/{tryghost-link-redirects-5.110.4.tgz → tryghost-link-redirects-5.112.0.tgz} +0 -0
- package/components/tryghost-link-replacer-5.112.0.tgz +0 -0
- package/components/{tryghost-magic-link-5.110.4.tgz → tryghost-magic-link-5.112.0.tgz} +0 -0
- package/components/tryghost-mail-events-5.112.0.tgz +0 -0
- package/components/tryghost-mailgun-client-5.112.0.tgz +0 -0
- package/components/{tryghost-member-attribution-5.110.4.tgz → tryghost-member-attribution-5.112.0.tgz} +0 -0
- package/components/{tryghost-member-events-5.110.4.tgz → tryghost-member-events-5.112.0.tgz} +0 -0
- package/components/{tryghost-members-api-5.110.4.tgz → tryghost-members-api-5.112.0.tgz} +0 -0
- package/components/{tryghost-members-csv-5.110.4.tgz → tryghost-members-csv-5.112.0.tgz} +0 -0
- package/components/{tryghost-members-importer-5.110.4.tgz → tryghost-members-importer-5.112.0.tgz} +0 -0
- package/components/{tryghost-members-offers-5.110.4.tgz → tryghost-members-offers-5.112.0.tgz} +0 -0
- package/components/{tryghost-members-payments-5.110.4.tgz → tryghost-members-payments-5.112.0.tgz} +0 -0
- package/components/{tryghost-members-ssr-5.110.4.tgz → tryghost-members-ssr-5.112.0.tgz} +0 -0
- package/components/{tryghost-members-stripe-service-5.110.4.tgz → tryghost-members-stripe-service-5.112.0.tgz} +0 -0
- package/components/{tryghost-milestones-5.110.4.tgz → tryghost-milestones-5.112.0.tgz} +0 -0
- package/components/tryghost-minifier-5.112.0.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.112.0.tgz +0 -0
- package/components/{tryghost-mw-cache-control-5.110.4.tgz → tryghost-mw-cache-control-5.112.0.tgz} +0 -0
- package/components/tryghost-mw-error-handler-5.112.0.tgz +0 -0
- package/components/{tryghost-mw-session-from-token-5.110.4.tgz → tryghost-mw-session-from-token-5.112.0.tgz} +0 -0
- package/components/{tryghost-mw-update-user-last-seen-5.110.4.tgz → tryghost-mw-update-user-last-seen-5.112.0.tgz} +0 -0
- package/components/tryghost-mw-version-match-5.112.0.tgz +0 -0
- package/components/tryghost-mw-vhost-5.112.0.tgz +0 -0
- package/components/tryghost-package-json-5.112.0.tgz +0 -0
- package/components/{tryghost-post-events-5.110.4.tgz → tryghost-post-events-5.112.0.tgz} +0 -0
- package/components/{tryghost-post-revisions-5.110.4.tgz → tryghost-post-revisions-5.112.0.tgz} +0 -0
- package/components/{tryghost-posts-service-5.110.4.tgz → tryghost-posts-service-5.112.0.tgz} +0 -0
- package/components/tryghost-prometheus-metrics-5.112.0.tgz +0 -0
- package/components/tryghost-recommendations-5.112.0.tgz +0 -0
- package/components/tryghost-referrers-5.112.0.tgz +0 -0
- package/components/{tryghost-security-5.110.4.tgz → tryghost-security-5.112.0.tgz} +0 -0
- package/components/tryghost-session-service-5.112.0.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.112.0.tgz +0 -0
- package/components/{tryghost-slack-notifications-5.110.4.tgz → tryghost-slack-notifications-5.112.0.tgz} +0 -0
- package/components/tryghost-tiers-5.112.0.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-5.112.0.tgz +0 -0
- package/components/tryghost-webmentions-5.112.0.tgz +0 -0
- package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +12799 -10270
- package/core/built/admin/assets/admin-x-demo/admin-x-demo.js +2 -2
- package/core/built/admin/assets/admin-x-demo/{index-82e381fb.mjs → index-0040480a.mjs} +3252 -2891
- package/core/built/admin/assets/admin-x-demo/{modals-b20a9ede.mjs → modals-fb35c86c.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{CodeEditorView-ea62c29b.mjs → CodeEditorView-ad8698fe.mjs} +624 -618
- package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +3 -3
- package/core/built/admin/assets/admin-x-settings/{index-af8cf9cf.mjs → index-2713e469.mjs} +6892 -6469
- package/core/built/admin/assets/admin-x-settings/{index-4b25c788.mjs → index-463cec50.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{modals-cb2dc7b7.mjs → modals-033e8fc4.mjs} +7888 -7669
- package/core/built/admin/assets/{chunk.524.3096e68df5b51dacf872.js → chunk.524.db49da6fd8ae155205a4.js} +6 -6
- package/core/built/admin/assets/{chunk.582.e225422f90639ff30544.js → chunk.582.0bf715eb6807f7641706.js} +8 -8
- package/core/built/admin/assets/{ghost-98d002d50a5e01d2100b2c387a849249.js → ghost-62bd4d4c837d453e1038808dc1cd1e4c.js} +43 -42
- package/core/built/admin/assets/img/ap-nodes-01ee317529e6353a1c34a062c388f1e7.png +0 -0
- package/core/built/admin/assets/koenig-lexical/index.css +1 -1
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +18314 -17680
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +229 -200
- package/core/built/admin/assets/posts/posts.js +24137 -24156
- package/core/built/admin/index.html +3 -3
- package/core/frontend/helpers/get.js +2 -3
- package/core/frontend/services/sitemap/SiteMapManager.js +1 -1
- package/core/frontend/src/cards/css/cta.css +40 -30
- package/core/frontend/src/cards/css/video.css +1 -0
- package/core/server/api/endpoints/settings-public.js +3 -2
- package/core/server/api/endpoints/utils/serializers/input/settings.js +3 -1
- package/core/server/api/endpoints/utils/serializers/input/utils/settings-key-group-mapper.js +2 -1
- package/core/server/api/endpoints/utils/serializers/input/utils/settings-key-type-mapper.js +2 -1
- package/core/server/data/migrations/versions/5.111/2025-03-05-16-36-39-add-captcha-setting.js +8 -0
- package/core/server/data/migrations/versions/5.112/2025-03-10-10-01-01-add-require-mfa-setting.js +8 -0
- package/core/server/data/schema/default-settings/default-settings.json +14 -0
- package/core/server/models/invite.js +4 -5
- package/core/server/models/post.js +3 -9
- package/core/server/models/relations/authors.js +2 -4
- package/core/server/models/role-utils.js +38 -0
- package/core/server/models/role.js +5 -3
- package/core/server/models/user.js +5 -3
- package/core/server/services/activitypub/ActivityPubService.js +116 -0
- package/core/server/services/activitypub/ActivityPubService.ts +139 -0
- package/core/server/services/activitypub/ActivityPubServiceWrapper.js +1 -1
- package/core/server/services/link-tracking/ClickEvent.js +25 -0
- package/core/server/services/link-tracking/FullPostLink.js +36 -0
- package/core/server/services/link-tracking/LinkClickRepository.js +1 -1
- package/core/server/services/link-tracking/LinkClickTrackingService.js +237 -0
- package/core/server/services/link-tracking/PostLink.js +29 -0
- package/core/server/services/link-tracking/PostLinkRepository.js +2 -2
- package/core/server/services/link-tracking/index.js +1 -1
- package/core/server/services/members-events/EventStorage.js +61 -0
- package/core/server/services/members-events/LastSeenAtCache.js +96 -0
- package/core/server/services/members-events/LastSeenAtUpdater.js +192 -0
- package/core/server/services/members-events/index.js +3 -1
- package/core/server/services/mentions-email-report/MentionEmailReportJob.js +117 -0
- package/core/server/services/mentions-email-report/service.js +3 -3
- package/core/server/services/staff/StaffService.js +179 -0
- package/core/server/services/staff/StaffServiceEmails.js +527 -0
- package/core/server/services/staff/email-templates/donation.hbs +119 -0
- package/core/server/services/staff/email-templates/donation.txt.js +15 -0
- package/core/server/services/staff/email-templates/mention-report.hbs +136 -0
- package/core/server/services/staff/email-templates/mention-report.txt.js +19 -0
- package/core/server/services/staff/email-templates/new-free-signup.hbs +118 -0
- package/core/server/services/staff/email-templates/new-free-signup.txt.js +13 -0
- package/core/server/services/staff/email-templates/new-milestone-received.hbs +142 -0
- package/core/server/services/staff/email-templates/new-milestone-received.txt.js +13 -0
- package/core/server/services/staff/email-templates/new-paid-cancellation.hbs +125 -0
- package/core/server/services/staff/email-templates/new-paid-cancellation.txt.js +13 -0
- package/core/server/services/staff/email-templates/new-paid-started.hbs +124 -0
- package/core/server/services/staff/email-templates/new-paid-started.txt.js +13 -0
- package/core/server/services/staff/email-templates/partials/preview.hbs +6 -0
- package/core/server/services/staff/email-templates/partials/styles.hbs +114 -0
- package/core/server/services/staff/email-templates/recommendation-received.hbs +154 -0
- package/core/server/services/staff/email-templates/recommendation-received.txt.js +13 -0
- package/core/server/services/staff/index.js +1 -1
- package/core/server/services/staff/milestone-email-config.js +207 -0
- package/core/server/services/stats/MembersStatsService.js +167 -0
- package/core/server/services/stats/MrrStatsService.js +161 -0
- package/core/server/services/stats/ReferrersStatsService.js +164 -0
- package/core/server/services/stats/StatsService.js +63 -0
- package/core/server/services/stats/SubscriptionStatsService.js +180 -0
- package/core/server/services/stats/service.js +1 -1
- package/core/server/services/url/Resources.js +2 -2
- package/core/shared/config/defaults.json +2 -1
- package/core/shared/events/URLResourceUpdatedEvent.js +33 -0
- package/core/shared/settings-cache/public.js +1 -0
- package/package.json +155 -158
- package/tsconfig.json +105 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/yarn.lock +347 -136
- package/components/tryghost-activitypub-5.110.4.tgz +0 -0
- package/components/tryghost-adapter-cache-memory-ttl-5.110.4.tgz +0 -0
- package/components/tryghost-adapter-cache-redis-5.110.4.tgz +0 -0
- package/components/tryghost-announcement-bar-settings-5.110.4.tgz +0 -0
- package/components/tryghost-api-version-compatibility-service-5.110.4.tgz +0 -0
- package/components/tryghost-bookshelf-repository-5.110.4.tgz +0 -0
- package/components/tryghost-bootstrap-socket-5.110.4.tgz +0 -0
- package/components/tryghost-captcha-service-5.110.4.tgz +0 -0
- package/components/tryghost-constants-5.110.4.tgz +0 -0
- package/components/tryghost-custom-fonts-5.110.4.tgz +0 -0
- package/components/tryghost-donations-5.110.4.tgz +0 -0
- package/components/tryghost-dynamic-routing-events-5.110.4.tgz +0 -0
- package/components/tryghost-email-addresses-5.110.4.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.110.4.tgz +0 -0
- package/components/tryghost-email-content-generator-5.110.4.tgz +0 -0
- package/components/tryghost-email-events-5.110.4.tgz +0 -0
- package/components/tryghost-email-service-5.110.4.tgz +0 -0
- package/components/tryghost-email-suppression-list-5.110.4.tgz +0 -0
- package/components/tryghost-extract-api-key-5.110.4.tgz +0 -0
- package/components/tryghost-ghost-5.110.4.tgz +0 -0
- package/components/tryghost-i18n-5.110.4.tgz +0 -0
- package/components/tryghost-identity-token-service-5.110.4.tgz +0 -0
- package/components/tryghost-importer-handler-content-files-5.110.4.tgz +0 -0
- package/components/tryghost-in-memory-repository-5.110.4.tgz +0 -0
- package/components/tryghost-link-replacer-5.110.4.tgz +0 -0
- package/components/tryghost-link-tracking-5.110.4.tgz +0 -0
- package/components/tryghost-mail-events-5.110.4.tgz +0 -0
- package/components/tryghost-mailgun-client-5.110.4.tgz +0 -0
- package/components/tryghost-members-events-service-5.110.4.tgz +0 -0
- package/components/tryghost-mentions-email-report-5.110.4.tgz +0 -0
- package/components/tryghost-minifier-5.110.4.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.110.4.tgz +0 -0
- package/components/tryghost-mw-error-handler-5.110.4.tgz +0 -0
- package/components/tryghost-mw-version-match-5.110.4.tgz +0 -0
- package/components/tryghost-mw-vhost-5.110.4.tgz +0 -0
- package/components/tryghost-package-json-5.110.4.tgz +0 -0
- package/components/tryghost-prometheus-metrics-5.110.4.tgz +0 -0
- package/components/tryghost-recommendations-5.110.4.tgz +0 -0
- package/components/tryghost-referrers-5.110.4.tgz +0 -0
- package/components/tryghost-session-service-5.110.4.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.110.4.tgz +0 -0
- package/components/tryghost-staff-service-5.110.4.tgz +0 -0
- package/components/tryghost-stats-service-5.110.4.tgz +0 -0
- package/components/tryghost-tiers-5.110.4.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-5.110.4.tgz +0 -0
- package/components/tryghost-webmentions-5.110.4.tgz +0 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import ObjectID from 'bson-objectid';
|
|
2
|
+
import {Knex} from 'knex';
|
|
3
|
+
import {IdentityTokenService} from '@tryghost/identity-token-service';
|
|
4
|
+
import fetch from 'node-fetch';
|
|
5
|
+
|
|
6
|
+
type ExpectedWebhook = {
|
|
7
|
+
event: string;
|
|
8
|
+
target_url: URL;
|
|
9
|
+
api_version: string;
|
|
10
|
+
secret: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
interface Logger {
|
|
14
|
+
info(message: string): void
|
|
15
|
+
warn(message: string): void
|
|
16
|
+
error(message: string): void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class ActivityPubService {
|
|
20
|
+
constructor(
|
|
21
|
+
private knex: Knex,
|
|
22
|
+
private siteUrl: URL,
|
|
23
|
+
private logging: Logger,
|
|
24
|
+
private identityTokenService: IdentityTokenService
|
|
25
|
+
) {}
|
|
26
|
+
|
|
27
|
+
getExpectedWebhooks(secret: string): ExpectedWebhook[] {
|
|
28
|
+
return [{
|
|
29
|
+
event: 'post.published',
|
|
30
|
+
target_url: new URL('.ghost/activitypub/webhooks/post/published', this.siteUrl),
|
|
31
|
+
api_version: 'v5.100.0',
|
|
32
|
+
secret
|
|
33
|
+
}, {
|
|
34
|
+
event: 'site.changed',
|
|
35
|
+
target_url: new URL('.ghost/activitypub/webhooks/site/changed', this.siteUrl),
|
|
36
|
+
api_version: 'v5.100.0',
|
|
37
|
+
secret
|
|
38
|
+
}];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async checkWebhookState(expectedWebhooks: ExpectedWebhook[], integration: {id: string}) {
|
|
42
|
+
this.logging.info(`Checking ActivityPub Webhook state`);
|
|
43
|
+
|
|
44
|
+
const webhooks = await this.knex
|
|
45
|
+
.select('*')
|
|
46
|
+
.from('webhooks')
|
|
47
|
+
.where('integration_id', '=', integration.id);
|
|
48
|
+
|
|
49
|
+
if (webhooks.length !== expectedWebhooks.length) {
|
|
50
|
+
this.logging.warn(`Expected ${expectedWebhooks.length} webhooks for ActivityPub`);
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const expectedWebhook of expectedWebhooks) {
|
|
55
|
+
const foundWebhook = webhooks.find((webhook) => {
|
|
56
|
+
return webhook.event === expectedWebhook.event && webhook.target_url === expectedWebhook.target_url.href && webhook.secret === expectedWebhook.secret;
|
|
57
|
+
});
|
|
58
|
+
if (!foundWebhook) {
|
|
59
|
+
this.logging.error(`Could not find webhook for ${expectedWebhook.event} ${expectedWebhook.target_url}`);
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async getWebhookSecret(): Promise<string | null> {
|
|
68
|
+
try {
|
|
69
|
+
const ownerUser = await this.knex.select('*').from('users').where('id', '=', '1').first();
|
|
70
|
+
const token = await this.identityTokenService.getTokenForUser(ownerUser.email, 'Owner');
|
|
71
|
+
|
|
72
|
+
const res = await fetch(new URL('.ghost/activitypub/site', this.siteUrl), {
|
|
73
|
+
headers: {
|
|
74
|
+
Authorization: `Bearer ${token}`
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const body = await res.json();
|
|
79
|
+
|
|
80
|
+
return body.webhook_secret;
|
|
81
|
+
} catch (err: unknown) {
|
|
82
|
+
this.logging.error(`Could not get webhook secret for ActivityPub ${err}`);
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async initialiseWebhooks() {
|
|
88
|
+
const integration = await this.knex
|
|
89
|
+
.select('*')
|
|
90
|
+
.from('integrations')
|
|
91
|
+
.where('slug', '=', 'ghost-activitypub')
|
|
92
|
+
.andWhere('type', '=', 'internal')
|
|
93
|
+
.first();
|
|
94
|
+
|
|
95
|
+
if (!integration) {
|
|
96
|
+
this.logging.error('No ActivityPub integration found - cannot initialise');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const secret = await this.getWebhookSecret();
|
|
101
|
+
|
|
102
|
+
if (!secret) {
|
|
103
|
+
this.logging.error('No webhook secret found - cannot initialise');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const expectedWebhooks = this.getExpectedWebhooks(secret);
|
|
108
|
+
const isInCorrectState = await this.checkWebhookState(expectedWebhooks, integration);
|
|
109
|
+
|
|
110
|
+
if (isInCorrectState) {
|
|
111
|
+
this.logging.info(`ActivityPub webhooks in correct state`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this.logging.info(`ActivityPub webhooks in incorrect state, deleting all of them and starting fresh`);
|
|
116
|
+
await this.knex
|
|
117
|
+
.del()
|
|
118
|
+
.from('webhooks')
|
|
119
|
+
.where('integration_id', '=', integration.id);
|
|
120
|
+
|
|
121
|
+
const webhooksToInsert = expectedWebhooks.map((expectedWebhook) => {
|
|
122
|
+
return {
|
|
123
|
+
id: (new ObjectID).toHexString(),
|
|
124
|
+
event: expectedWebhook.event,
|
|
125
|
+
target_url: expectedWebhook.target_url.href,
|
|
126
|
+
api_version: expectedWebhook.api_version,
|
|
127
|
+
name: `ActivityPub ${expectedWebhook.event} Webhook`,
|
|
128
|
+
secret: secret,
|
|
129
|
+
integration_id: integration.id,
|
|
130
|
+
created_at: this.knex.raw('current_timestamp'),
|
|
131
|
+
created_by: '1'
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
await this.knex
|
|
136
|
+
.insert(webhooksToInsert)
|
|
137
|
+
.into('webhooks');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const ObjectID = require('bson-objectid').default;
|
|
2
|
+
|
|
3
|
+
module.exports = class ClickEvent {
|
|
4
|
+
/** @type {ObjectID} */
|
|
5
|
+
event_id;
|
|
6
|
+
/** @type {string} */
|
|
7
|
+
member_uuid;
|
|
8
|
+
/** @type {ObjectID} */
|
|
9
|
+
link_id;
|
|
10
|
+
|
|
11
|
+
constructor(data) {
|
|
12
|
+
if (!data.id) {
|
|
13
|
+
this.event_id = new ObjectID();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (typeof data.id === 'string') {
|
|
17
|
+
this.event_id = ObjectID.createFromHexString(data.id);
|
|
18
|
+
} else {
|
|
19
|
+
this.event_id = data.id;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
this.member_uuid = data.member_uuid;
|
|
23
|
+
this.link_id = data.link_id;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const ObjectID = require('bson-objectid').default;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} FullPostLinkCount
|
|
5
|
+
* @property {number} clicks
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Stores the connection between a LinkRedirect and a Post
|
|
10
|
+
*/
|
|
11
|
+
module.exports = class FullPostLink {
|
|
12
|
+
/** @type {ObjectID} */
|
|
13
|
+
post_id;
|
|
14
|
+
|
|
15
|
+
/** @type {import('@tryghost/link-redirects/lib/LinkRedirect')} */
|
|
16
|
+
link;
|
|
17
|
+
|
|
18
|
+
/** @type {FullPostLinkCount} */
|
|
19
|
+
count;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {object} data
|
|
23
|
+
* @param {string|ObjectID} data.post_id
|
|
24
|
+
* @param {import('@tryghost/link-redirects/lib/LinkRedirect')} data.link
|
|
25
|
+
* @param {FullPostLinkCount} data.count
|
|
26
|
+
*/
|
|
27
|
+
constructor(data) {
|
|
28
|
+
if (typeof data.post_id === 'string') {
|
|
29
|
+
this.post_id = ObjectID.createFromHexString(data.post_id);
|
|
30
|
+
} else {
|
|
31
|
+
this.post_id = data.post_id;
|
|
32
|
+
}
|
|
33
|
+
this.link = data.link;
|
|
34
|
+
this.count = data.count;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
const {RedirectEvent} = require('@tryghost/link-redirects');
|
|
2
|
+
const LinkClick = require('./ClickEvent');
|
|
3
|
+
const PostLink = require('./PostLink');
|
|
4
|
+
const ObjectID = require('bson-objectid').default;
|
|
5
|
+
const errors = require('@tryghost/errors');
|
|
6
|
+
const nql = require('@tryghost/nql');
|
|
7
|
+
const _ = require('lodash');
|
|
8
|
+
const tpl = require('@tryghost/tpl');
|
|
9
|
+
const moment = require('moment');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {object} ILinkClickRepository
|
|
13
|
+
* @prop {(event: LinkClick) => Promise<void>} save
|
|
14
|
+
* @prop {({filter: string}) => Promise<LinkClick[]>} getAll
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {object} ILinkRedirect
|
|
19
|
+
* @prop {ObjectID} link_id
|
|
20
|
+
* @prop {URL} to
|
|
21
|
+
* @prop {URL} from
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {import('./FullPostLink')} FullPostLink
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {object} ILinkRedirectService
|
|
30
|
+
* @prop {(to: URL, slug: string) => Promise<ILinkRedirect>} addRedirect
|
|
31
|
+
* @prop {() => Promise<string>} getSlug
|
|
32
|
+
* @prop {({filter: string}) => Promise<ILinkRedirect[]>} getAll
|
|
33
|
+
* @prop {({filter: string}) => Promise<string[]>} getFilteredIds
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {object} IPostLinkRepository
|
|
38
|
+
* @prop {(postLink: PostLink) => Promise<void>} save
|
|
39
|
+
* @prop {({filter: string}) => Promise<FullPostLink[]>} getAll
|
|
40
|
+
* @prop {(linkIds: array, data, options) => Promise<FullPostLink[]>} updateLinks
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
const messages = {
|
|
44
|
+
invalidFilter: 'Invalid filter value received',
|
|
45
|
+
unsupportedBulkAction: 'Unsupported bulk action',
|
|
46
|
+
invalidRedirectUrl: 'Invalid redirect URL value'
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
class LinkClickTrackingService {
|
|
50
|
+
#initialised = false;
|
|
51
|
+
|
|
52
|
+
/** @type ILinkClickRepository */
|
|
53
|
+
#linkClickRepository;
|
|
54
|
+
/** @type ILinkRedirectService */
|
|
55
|
+
#linkRedirectService;
|
|
56
|
+
/** @type IPostLinkRepository */
|
|
57
|
+
#postLinkRepository;
|
|
58
|
+
/** @type DomainEvents */
|
|
59
|
+
#DomainEvents;
|
|
60
|
+
/** @type {Object} */
|
|
61
|
+
#LinkRedirect;
|
|
62
|
+
/** @type {Object} */
|
|
63
|
+
#urlUtils;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {object} deps
|
|
67
|
+
* @param {ILinkClickRepository} deps.linkClickRepository
|
|
68
|
+
* @param {ILinkRedirectService} deps.linkRedirectService
|
|
69
|
+
* @param {IPostLinkRepository} deps.postLinkRepository
|
|
70
|
+
* @param {DomainEvents} deps.DomainEvents
|
|
71
|
+
* @param {urlUtils} deps.urlUtils
|
|
72
|
+
*/
|
|
73
|
+
constructor(deps) {
|
|
74
|
+
this.#linkClickRepository = deps.linkClickRepository;
|
|
75
|
+
this.#linkRedirectService = deps.linkRedirectService;
|
|
76
|
+
this.#postLinkRepository = deps.postLinkRepository;
|
|
77
|
+
this.#DomainEvents = deps.DomainEvents;
|
|
78
|
+
this.#urlUtils = deps.urlUtils;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async init() {
|
|
82
|
+
if (this.#initialised) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
this.subscribe();
|
|
86
|
+
this.#initialised = true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {object} options
|
|
91
|
+
* @param {string} options.filter
|
|
92
|
+
* @return {Promise<FullPostLink[]>}
|
|
93
|
+
*/
|
|
94
|
+
async getLinks(options) {
|
|
95
|
+
return await this.#postLinkRepository.getAll({
|
|
96
|
+
filter: options.filter
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* validate and manage the new redirect url in filter
|
|
102
|
+
* `to` url needs decoding and transformation to relative url for comparision
|
|
103
|
+
* @param {string} filter
|
|
104
|
+
* @returns {Object} parsed filter
|
|
105
|
+
* @throws {errors.BadRequestError}
|
|
106
|
+
*/
|
|
107
|
+
#parseLinkFilter(filter) {
|
|
108
|
+
try {
|
|
109
|
+
const filterJson = nql(filter).parse();
|
|
110
|
+
const postId = filterJson?.$and?.[0]?.post_id;
|
|
111
|
+
const redirectUrl = new URL(filterJson?.$and?.[1]?.to);
|
|
112
|
+
if (!postId || !redirectUrl) {
|
|
113
|
+
throw new errors.BadRequestError({
|
|
114
|
+
message: tpl(messages.invalidFilter)
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
postId,
|
|
119
|
+
redirectUrl
|
|
120
|
+
};
|
|
121
|
+
} catch (e) {
|
|
122
|
+
throw new errors.BadRequestError({
|
|
123
|
+
message: tpl(messages.invalidFilter),
|
|
124
|
+
context: e.message
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
#getRedirectLinkWithAttribution({newLink, oldLink, postId}) {
|
|
130
|
+
const newUrl = new URL(newLink);
|
|
131
|
+
const oldUrl = new URL(oldLink);
|
|
132
|
+
// append newsletter ref query param from oldUrl to newUrl
|
|
133
|
+
if (oldUrl.searchParams.has('ref')) {
|
|
134
|
+
newUrl.searchParams.set('ref', oldUrl.searchParams.get('ref'));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// append post attribution to site urls
|
|
138
|
+
const isSite = this.#urlUtils.isSiteUrl(newUrl);
|
|
139
|
+
if (isSite) {
|
|
140
|
+
newUrl.searchParams.set('attribution_type', 'post');
|
|
141
|
+
newUrl.searchParams.set('attribution_id', postId);
|
|
142
|
+
}
|
|
143
|
+
return newUrl;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async #updateLinks(data, options) {
|
|
147
|
+
const filterOptions = _.pick(options, ['transacting', 'context', 'filter']);
|
|
148
|
+
|
|
149
|
+
// decode and parse filter to manage new redirect url
|
|
150
|
+
const {postId, redirectUrl} = this.#parseLinkFilter(filterOptions.filter);
|
|
151
|
+
|
|
152
|
+
// manages transformation of current url to relative for comparision
|
|
153
|
+
const transformedOldUrl = this.#urlUtils.absoluteToTransformReady(redirectUrl.href);
|
|
154
|
+
const filterQuery = `post_id:'${postId}'+to:'${transformedOldUrl}'`;
|
|
155
|
+
|
|
156
|
+
const updatedFilterOptions = {
|
|
157
|
+
...filterOptions,
|
|
158
|
+
filter: filterQuery
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// get new redirect link with proper attribution
|
|
162
|
+
const newRedirectUrl = this.#getRedirectLinkWithAttribution({
|
|
163
|
+
newLink: data.meta?.link?.to,
|
|
164
|
+
oldLink: redirectUrl.href,
|
|
165
|
+
postId
|
|
166
|
+
});
|
|
167
|
+
const linkIds = await this.#linkRedirectService.getFilteredIds(updatedFilterOptions);
|
|
168
|
+
|
|
169
|
+
const bulkUpdateOptions = _.pick(options, ['transacting']);
|
|
170
|
+
const updateData = {
|
|
171
|
+
to: this.#urlUtils.absoluteToTransformReady(newRedirectUrl.href),
|
|
172
|
+
updated_at: moment().format('YYYY-MM-DD HH:mm:ss')
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
return await this.#postLinkRepository.updateLinks(linkIds, updateData, bulkUpdateOptions);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async bulkEdit(data, options) {
|
|
179
|
+
if (data.action === 'updateLink') {
|
|
180
|
+
return await this.#updateLinks(data, options);
|
|
181
|
+
}
|
|
182
|
+
throw new errors.IncorrectUsageError({
|
|
183
|
+
message: tpl(messages.unsupportedBulkAction)
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* @private (not using # to allow tests)
|
|
189
|
+
* Replace URL with a redirect that redirects to the original URL, and link that redirect with the given post
|
|
190
|
+
*/
|
|
191
|
+
async addRedirectToUrl(url, post) {
|
|
192
|
+
// Generate a unique redirect slug
|
|
193
|
+
const slugUrl = await this.#linkRedirectService.getSlugUrl();
|
|
194
|
+
|
|
195
|
+
// Add redirect for link click tracking
|
|
196
|
+
const redirect = await this.#linkRedirectService.addRedirect(slugUrl, url);
|
|
197
|
+
|
|
198
|
+
// Store a reference of the link against the post
|
|
199
|
+
const postLink = new PostLink({
|
|
200
|
+
link_id: redirect.link_id,
|
|
201
|
+
post_id: ObjectID.createFromHexString(post.id)
|
|
202
|
+
});
|
|
203
|
+
await this.#postLinkRepository.save(postLink);
|
|
204
|
+
|
|
205
|
+
return redirect.from;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Add tracking to a URL and returns a new URL (if link click tracking is enabled)
|
|
210
|
+
* @param {URL} url
|
|
211
|
+
* @param {Post} post
|
|
212
|
+
* @param {string} memberUuid
|
|
213
|
+
* @return {Promise<URL>}
|
|
214
|
+
*/
|
|
215
|
+
async addTrackingToUrl(url, post, memberUuid) {
|
|
216
|
+
url = await this.addRedirectToUrl(url, post);
|
|
217
|
+
url.searchParams.set('m', memberUuid);
|
|
218
|
+
return url;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
subscribe() {
|
|
222
|
+
this.#DomainEvents.subscribe(RedirectEvent, async (event) => {
|
|
223
|
+
const uuid = event.data.url.searchParams.get('m');
|
|
224
|
+
if (!uuid) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const click = new LinkClick({
|
|
229
|
+
member_uuid: uuid,
|
|
230
|
+
link_id: event.data.link.link_id
|
|
231
|
+
});
|
|
232
|
+
await this.#linkClickRepository.save(click);
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
module.exports = LinkClickTrackingService;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const ObjectID = require('bson-objectid').default;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stores the connection between a LinkRedirect and a Post
|
|
5
|
+
*/
|
|
6
|
+
module.exports = class PostLink {
|
|
7
|
+
/** @type {ObjectID} */
|
|
8
|
+
post_id;
|
|
9
|
+
/** @type {ObjectID} */
|
|
10
|
+
link_id;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {object} data
|
|
14
|
+
* @param {string|ObjectID} data.post_id
|
|
15
|
+
* @param {string|ObjectID} data.link_id
|
|
16
|
+
*/
|
|
17
|
+
constructor(data) {
|
|
18
|
+
if (typeof data.post_id === 'string') {
|
|
19
|
+
this.post_id = ObjectID.createFromHexString(data.post_id);
|
|
20
|
+
} else {
|
|
21
|
+
this.post_id = data.post_id;
|
|
22
|
+
}
|
|
23
|
+
if (typeof data.link_id === 'string') {
|
|
24
|
+
this.link_id = ObjectID.createFromHexString(data.link_id);
|
|
25
|
+
} else {
|
|
26
|
+
this.link_id = data.link_id;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
const
|
|
1
|
+
const FullPostLink = require('./FullPostLink');
|
|
2
2
|
const _ = require('lodash');
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @typedef {import('bson-objectid').default} ObjectID
|
|
6
|
-
* @typedef {import('
|
|
6
|
+
* @typedef {import('./PostLink')} PostLink
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
module.exports = class PostLinkRepository {
|
|
@@ -20,7 +20,7 @@ class LinkTrackingServiceWrapper {
|
|
|
20
20
|
const {MemberLinkClickEvent} = require('@tryghost/member-events');
|
|
21
21
|
const DomainEvents = require('@tryghost/domain-events');
|
|
22
22
|
|
|
23
|
-
const
|
|
23
|
+
const LinkClickTrackingService = require('./LinkClickTrackingService');
|
|
24
24
|
|
|
25
25
|
const postLinkRepository = new PostLinkRepository({
|
|
26
26
|
LinkRedirect: models.Redirect,
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const {MemberCreatedEvent, SubscriptionCreatedEvent} = require('@tryghost/member-events');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Store events in the database
|
|
5
|
+
*/
|
|
6
|
+
class EventStorage {
|
|
7
|
+
/**
|
|
8
|
+
*
|
|
9
|
+
* @param {Object} deps
|
|
10
|
+
* @param {Object} deps.labsService
|
|
11
|
+
* @param {Object} deps.models
|
|
12
|
+
* @param {Object} deps.models.MemberCreatedEvent
|
|
13
|
+
* @param {Object} deps.models.SubscriptionCreatedEvent
|
|
14
|
+
*/
|
|
15
|
+
constructor({labsService, models}) {
|
|
16
|
+
this.models = models;
|
|
17
|
+
this.labsService = labsService;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Subscribe to events of this domainEvents service
|
|
22
|
+
* @param {Object} domainEvents The DomainEvents service
|
|
23
|
+
*/
|
|
24
|
+
subscribe(domainEvents) {
|
|
25
|
+
domainEvents.subscribe(MemberCreatedEvent, async (event) => {
|
|
26
|
+
let attribution = event.data.attribution;
|
|
27
|
+
|
|
28
|
+
await this.models.MemberCreatedEvent.add({
|
|
29
|
+
member_id: event.data.memberId,
|
|
30
|
+
created_at: event.timestamp,
|
|
31
|
+
attribution_id: attribution?.id ?? null,
|
|
32
|
+
attribution_url: attribution?.url ?? null,
|
|
33
|
+
attribution_type: attribution?.type ?? null,
|
|
34
|
+
source: event.data.source,
|
|
35
|
+
referrer_source: attribution?.referrerSource ?? null,
|
|
36
|
+
referrer_medium: attribution?.referrerMedium ?? null,
|
|
37
|
+
referrer_url: attribution?.referrerUrl ?? null,
|
|
38
|
+
batch_id: event.data.batchId ?? null
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
domainEvents.subscribe(SubscriptionCreatedEvent, async (event) => {
|
|
43
|
+
let attribution = event.data.attribution;
|
|
44
|
+
|
|
45
|
+
await this.models.SubscriptionCreatedEvent.add({
|
|
46
|
+
member_id: event.data.memberId,
|
|
47
|
+
subscription_id: event.data.subscriptionId,
|
|
48
|
+
created_at: event.timestamp,
|
|
49
|
+
attribution_id: attribution?.id ?? null,
|
|
50
|
+
attribution_url: attribution?.url ?? null,
|
|
51
|
+
attribution_type: attribution?.type ?? null,
|
|
52
|
+
referrer_source: attribution?.referrerSource ?? null,
|
|
53
|
+
referrer_medium: attribution?.referrerMedium ?? null,
|
|
54
|
+
referrer_url: attribution?.referrerUrl ?? null,
|
|
55
|
+
batch_id: event.data.batchId ?? null
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = EventStorage;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const moment = require('moment-timezone');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A cache that stores the member ids that have been seen today. This cache is used to avoid having to query the database for the last_seen_at timestamp of a member multiple times in the same day.
|
|
5
|
+
*
|
|
6
|
+
* @constructor
|
|
7
|
+
* @param {Object} settingsCache - An instance of the settings cache
|
|
8
|
+
* @property {Set} _cache - A set that stores all the member ids that have been seen today
|
|
9
|
+
* @property {Object} _settingsCache - An instance of the settings cache
|
|
10
|
+
* @property {string} _startOfDay - The start of the current day in the site timezone, formatted in ISO 8601
|
|
11
|
+
*/
|
|
12
|
+
class LastSeenAtCache {
|
|
13
|
+
/**
|
|
14
|
+
*
|
|
15
|
+
* @param {Object} deps - Dependencies
|
|
16
|
+
* @param {Object} deps.services - The list of service dependencies
|
|
17
|
+
* @param {Object} deps.services.settingsCache - The settings service
|
|
18
|
+
*/
|
|
19
|
+
constructor({services: {settingsCache}}) {
|
|
20
|
+
this._cache = new Set();
|
|
21
|
+
this._settingsCache = settingsCache;
|
|
22
|
+
this._startOfDay = this._getStartOfCurrentDay();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @method add - Adds a member id to the cache
|
|
27
|
+
* @param {string} memberId
|
|
28
|
+
*/
|
|
29
|
+
add(memberId) {
|
|
30
|
+
this._cache.add(memberId);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @method remove - Removes a member id from the cache
|
|
35
|
+
* @param {string} memberId
|
|
36
|
+
*/
|
|
37
|
+
remove(memberId) {
|
|
38
|
+
this._cache.delete(memberId);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @method shouldUpdateMember - Checks if a member should be updated
|
|
43
|
+
* @param {string} memberId
|
|
44
|
+
* @returns {boolean} - Returns true if the member should be updated
|
|
45
|
+
*/
|
|
46
|
+
shouldUpdateMember(memberId) {
|
|
47
|
+
return !this._has(memberId);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @method clear - Clears the cache
|
|
52
|
+
*/
|
|
53
|
+
clear() {
|
|
54
|
+
this._cache.clear();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @method _has - Refreshes the cache and checks if a member id is in the cache
|
|
59
|
+
* @param {string} memberId
|
|
60
|
+
* @returns {boolean}
|
|
61
|
+
*/
|
|
62
|
+
_has(memberId) {
|
|
63
|
+
this._refresh();
|
|
64
|
+
return this._cache.has(memberId);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @method _shouldClear - Checks if the cache should be cleared, based on the current day
|
|
69
|
+
* @returns {boolean} - Returns true if the cache should be cleared
|
|
70
|
+
*/
|
|
71
|
+
_shouldClear() {
|
|
72
|
+
return this._startOfDay !== this._getStartOfCurrentDay();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @method _refresh - Clears the cache if the day has changed
|
|
77
|
+
*/
|
|
78
|
+
_refresh() {
|
|
79
|
+
if (this._shouldClear()) {
|
|
80
|
+
this.clear();
|
|
81
|
+
this._startOfDay = this._getStartOfCurrentDay();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Returns the start of the current day in the site timezone
|
|
87
|
+
* @returns {string} The start of the current day in the site timezone, formatted as a ISO 8601 string
|
|
88
|
+
*/
|
|
89
|
+
_getStartOfCurrentDay() {
|
|
90
|
+
const timezone = this._settingsCache.get('timezone') || 'Etc/UTC';
|
|
91
|
+
const startOfDay = moment().tz(timezone).startOf('day').utc().toISOString();
|
|
92
|
+
return startOfDay;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = LastSeenAtCache;
|