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,192 @@
|
|
|
1
|
+
const {MemberPageViewEvent, MemberCommentEvent, MemberLinkClickEvent} = require('@tryghost/member-events');
|
|
2
|
+
const moment = require('moment-timezone');
|
|
3
|
+
const {IncorrectUsageError} = require('@tryghost/errors');
|
|
4
|
+
const {EmailOpenedEvent} = require('@tryghost/email-events');
|
|
5
|
+
const logging = require('@tryghost/logging');
|
|
6
|
+
const LastSeenAtCache = require('./LastSeenAtCache');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Listen for `MemberViewEvent` to update the `member.last_seen_at` timestamp
|
|
10
|
+
*/
|
|
11
|
+
class LastSeenAtUpdater {
|
|
12
|
+
/**
|
|
13
|
+
* Initializes the event subscriber
|
|
14
|
+
* @param {Object} deps dependencies
|
|
15
|
+
* @param {Object} deps.services The list of service dependencies
|
|
16
|
+
* @param {any} deps.services.settingsCache The settings service
|
|
17
|
+
* @param {() => object} deps.getMembersApi - A function which returns an instance of members-api
|
|
18
|
+
* @param {any} deps.db Database connection
|
|
19
|
+
* @param {any} deps.events The event emitter
|
|
20
|
+
* @param {any} deps.lastSeenAtCache An instance of the last seen at cache
|
|
21
|
+
* @param {any} deps.config Ghost config for click tracking
|
|
22
|
+
*/
|
|
23
|
+
constructor({
|
|
24
|
+
services: {
|
|
25
|
+
settingsCache
|
|
26
|
+
},
|
|
27
|
+
getMembersApi,
|
|
28
|
+
db,
|
|
29
|
+
events,
|
|
30
|
+
lastSeenAtCache,
|
|
31
|
+
config
|
|
32
|
+
}) {
|
|
33
|
+
if (!getMembersApi) {
|
|
34
|
+
throw new IncorrectUsageError({message: 'Missing option getMembersApi'});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this._getMembersApi = getMembersApi;
|
|
38
|
+
this._settingsCacheService = settingsCache;
|
|
39
|
+
this._db = db;
|
|
40
|
+
this._events = events;
|
|
41
|
+
this._lastSeenAtCache = lastSeenAtCache || new LastSeenAtCache({services: {settingsCache}});
|
|
42
|
+
this._config = config;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Subscribe to events of this domainEvents service
|
|
46
|
+
* @param {any} domainEvents The DomainEvents service
|
|
47
|
+
*/
|
|
48
|
+
subscribe(domainEvents) {
|
|
49
|
+
domainEvents.subscribe(MemberPageViewEvent, async (event) => {
|
|
50
|
+
try {
|
|
51
|
+
await this.cachedUpdateLastSeenAt(event.data.memberId, event.data.memberLastSeenAt, event.timestamp);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
logging.error(`Error in LastSeenAtUpdater.MemberPageViewEvent listener for member ${event.data.memberId}`);
|
|
54
|
+
logging.error(err);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Only disable if explicitly set to false in config
|
|
59
|
+
const shouldUpdateForClickTracking = !this._config || this._config.get('backgroundJobs:clickTrackingLastSeenAtUpdater') !== false;
|
|
60
|
+
if (shouldUpdateForClickTracking) {
|
|
61
|
+
domainEvents.subscribe(MemberLinkClickEvent, async (event) => {
|
|
62
|
+
try {
|
|
63
|
+
await this.cachedUpdateLastSeenAt(event.data.memberId, event.data.memberLastSeenAt, event.timestamp);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
logging.error(`Error in LastSeenAtUpdater.MemberLinkClickEvent listener for member ${event.data.memberId}`);
|
|
66
|
+
logging.error(err);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
domainEvents.subscribe(MemberCommentEvent, async (event) => {
|
|
72
|
+
try {
|
|
73
|
+
await this.updateLastCommentedAt(event.data.memberId, event.timestamp);
|
|
74
|
+
} catch (err) {
|
|
75
|
+
logging.error(`Error in LastSeenAtUpdater.MemberCommentEvent listener for member ${event.data.memberId}`);
|
|
76
|
+
logging.error(err);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
domainEvents.subscribe(EmailOpenedEvent, async (event) => {
|
|
81
|
+
try {
|
|
82
|
+
await this.updateLastSeenAtWithoutKnownLastSeen(event.memberId, event.timestamp);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
logging.error(`Error in LastSeenAtUpdater.EmailOpenedEvent listener for member ${event.memberId}, emailRecipientId ${event.emailRecipientId}`);
|
|
85
|
+
logging.error(err);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Updates the member.last_seen_at field if it wasn't updated in the current day yet (in the publication timezone)
|
|
92
|
+
* Example: current time is 2022-02-28 18:00:00
|
|
93
|
+
* - memberLastSeenAt is 2022-02-27 23:00:00, timestamp is current time, then `last_seen_at` is set to the current time
|
|
94
|
+
* - memberLastSeenAt is 2022-02-28 01:00:00, timestamp is current time, then `last_seen_at` isn't changed
|
|
95
|
+
* @param {string} memberId The id of the member to be udpated
|
|
96
|
+
* @param {Date} timestamp The event timestamp
|
|
97
|
+
*/
|
|
98
|
+
async updateLastSeenAtWithoutKnownLastSeen(memberId, timestamp) {
|
|
99
|
+
// Note: we are not using Bookshelf / member repository to prevent firing webhooks + to prevent deadlock issues
|
|
100
|
+
// If we would use the member repostiory, we would create a forUpdate lock when editing the member, including when fetching the member labels. Creating a possible deadlock if somewhere else we do the reverse in a transaction.
|
|
101
|
+
const timezone = this._settingsCacheService.get('timezone') || 'Etc/UTC';
|
|
102
|
+
const startOfDayInSiteTimezone = moment.utc(timestamp).tz(timezone).startOf('day').utc().format('YYYY-MM-DD HH:mm:ss');
|
|
103
|
+
const formattedTimestamp = moment.utc(timestamp).format('YYYY-MM-DD HH:mm:ss');
|
|
104
|
+
await this._db.knex('members')
|
|
105
|
+
.where('id', '=', memberId)
|
|
106
|
+
.andWhere(builder => builder
|
|
107
|
+
.where('last_seen_at', '<', startOfDayInSiteTimezone)
|
|
108
|
+
.orWhereNull('last_seen_at')
|
|
109
|
+
)
|
|
110
|
+
.update({
|
|
111
|
+
last_seen_at: formattedTimestamp
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Updates the member.last_seen_at field if it wasn't updated in the current day yet (in the publication timezone)
|
|
117
|
+
*/
|
|
118
|
+
async cachedUpdateLastSeenAt(memberId, memberLastSeenAt, timestamp) {
|
|
119
|
+
if (this._lastSeenAtCache.shouldUpdateMember(memberId)) {
|
|
120
|
+
await this.updateLastSeenAt(memberId, memberLastSeenAt, timestamp);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Updates the member.last_seen_at field if it wasn't updated in the current day yet (in the publication timezone)
|
|
126
|
+
* Example: current time is 2022-02-28 18:00:00
|
|
127
|
+
* - memberLastSeenAt is 2022-02-27 23:00:00, timestamp is current time, then `last_seen_at` is set to the current time
|
|
128
|
+
* - memberLastSeenAt is 2022-02-28 01:00:00, timestamp is current time, then `last_seen_at` isn't changed
|
|
129
|
+
* @param {string} memberId The id of the member to be udpated
|
|
130
|
+
* @param {Date} timestamp The event timestamp
|
|
131
|
+
*/
|
|
132
|
+
async updateLastSeenAt(memberId, memberLastSeenAt, timestamp) {
|
|
133
|
+
const timezone = this._settingsCacheService.get('timezone');
|
|
134
|
+
// First, check if memberLastSeenAt is null or before the beginning of the current day in the publication timezone
|
|
135
|
+
// This isn't strictly necessary since we will fetch the member row for update and double check this
|
|
136
|
+
// This is an optimization to avoid unnecessary database queries if last_seen_at is already after the beginning of the current day
|
|
137
|
+
if (memberLastSeenAt === null || moment(moment.utc(timestamp).tz(timezone).startOf('day')).isAfter(memberLastSeenAt)) {
|
|
138
|
+
try {
|
|
139
|
+
// Pre-emptively update local cache so we don't update the same member again in the same day
|
|
140
|
+
this._lastSeenAtCache.add(memberId);
|
|
141
|
+
const membersApi = this._getMembersApi();
|
|
142
|
+
await this._db.knex.transaction(async (trx) => {
|
|
143
|
+
// To avoid a race condition, we lock the member row for update, then the last_seen_at field again to prevent simultaneous updates
|
|
144
|
+
const currentMember = await membersApi.members.get({id: memberId}, {require: true, transacting: trx, forUpdate: true});
|
|
145
|
+
const currentMemberLastSeenAt = currentMember.get('last_seen_at');
|
|
146
|
+
if (currentMemberLastSeenAt === null || moment(moment.utc(timestamp).tz(timezone).startOf('day')).isAfter(currentMemberLastSeenAt)) {
|
|
147
|
+
const memberToUpdate = await currentMember.refresh({transacting: trx, forUpdate: false, withRelated: ['labels', 'newsletters']});
|
|
148
|
+
const updatedMember = await memberToUpdate.save({last_seen_at: moment.utc(timestamp).format('YYYY-MM-DD HH:mm:ss')}, {transacting: trx, patch: true, method: 'update'});
|
|
149
|
+
// The standard event doesn't get emitted inside the transaction, so we do it manually
|
|
150
|
+
this._events.emit('member.edited', updatedMember);
|
|
151
|
+
return Promise.resolve(updatedMember);
|
|
152
|
+
}
|
|
153
|
+
return Promise.resolve(undefined);
|
|
154
|
+
});
|
|
155
|
+
} catch (err) {
|
|
156
|
+
// Remove the member from the cache if an error occurs
|
|
157
|
+
// This is to ensure that the member is updated on the next event if this one fails
|
|
158
|
+
this._lastSeenAtCache.remove(memberId);
|
|
159
|
+
// Bubble up the error to the event listener
|
|
160
|
+
throw err;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Updates the member.last_seen_at field if it wasn't updated in the current day yet (in the publication timezone)
|
|
167
|
+
* Example: current time is 2022-02-28 18:00:00
|
|
168
|
+
* - memberLastSeenAt is 2022-02-27 23:00:00, timestamp is current time, then `last_seen_at` is set to the current time
|
|
169
|
+
* - memberLastSeenAt is 2022-02-28 01:00:00, timestamp is current time, then `last_seen_at` isn't changed
|
|
170
|
+
* @param {string} memberId The id of the member to be udpated
|
|
171
|
+
* @param {Date} timestamp The event timestamp
|
|
172
|
+
*/
|
|
173
|
+
async updateLastCommentedAt(memberId, timestamp) {
|
|
174
|
+
const membersApi = this._getMembersApi();
|
|
175
|
+
const member = await membersApi.members.get({id: memberId}, {require: true});
|
|
176
|
+
const timezone = this._settingsCacheService.get('timezone');
|
|
177
|
+
|
|
178
|
+
const memberLastSeenAt = member.get('last_seen_at');
|
|
179
|
+
const memberLastCommentedAt = member.get('last_commented_at');
|
|
180
|
+
|
|
181
|
+
if (memberLastSeenAt === null || moment(moment.utc(timestamp).tz(timezone).startOf('day')).isAfter(memberLastSeenAt) || memberLastCommentedAt === null || moment(moment.utc(timestamp).tz(timezone).startOf('day')).isAfter(memberLastCommentedAt)) {
|
|
182
|
+
await membersApi.members.update({
|
|
183
|
+
last_seen_at: moment.utc(timestamp).format('YYYY-MM-DD HH:mm:ss'),
|
|
184
|
+
last_commented_at: moment.utc(timestamp).format('YYYY-MM-DD HH:mm:ss')
|
|
185
|
+
}, {
|
|
186
|
+
id: memberId
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
module.exports = LastSeenAtUpdater;
|
|
@@ -13,7 +13,9 @@ class MembersEventsServiceWrapper {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
// Wire up all the dependencies
|
|
16
|
-
const
|
|
16
|
+
const EventStorage = require('./EventStorage');
|
|
17
|
+
const LastSeenAtUpdater = require('./LastSeenAtUpdater');
|
|
18
|
+
const LastSeenAtCache = require('./LastSeenAtCache');
|
|
17
19
|
const models = require('../../models');
|
|
18
20
|
|
|
19
21
|
// Listen for events and store them in the database
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
module.exports = class MentionEmailReportJob {
|
|
2
|
+
/** @type {IMentionReportGenerator} */
|
|
3
|
+
#mentionReportGenerator;
|
|
4
|
+
|
|
5
|
+
/** @type {IMentionReportRecipientRepository} */
|
|
6
|
+
#mentionReportRecipientRepository;
|
|
7
|
+
|
|
8
|
+
/** @type {IMentionReportEmailView} */
|
|
9
|
+
#mentionReportEmailView;
|
|
10
|
+
|
|
11
|
+
/** @type {IMentionReportHistoryService} */
|
|
12
|
+
#mentionReportHistoryService;
|
|
13
|
+
|
|
14
|
+
/** @type {IEmailService} */
|
|
15
|
+
#emailService;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {object} deps
|
|
19
|
+
* @param {IMentionReportGenerator} deps.mentionReportGenerator
|
|
20
|
+
* @param {IMentionReportRecipientRepository} deps.mentionReportRecipientRepository
|
|
21
|
+
* @param {IMentionReportEmailView} deps.mentionReportEmailView
|
|
22
|
+
* @param {IMentionReportHistoryService} deps.mentionReportHistoryService
|
|
23
|
+
* @param {IEmailService} deps.emailService
|
|
24
|
+
*/
|
|
25
|
+
constructor(deps) {
|
|
26
|
+
this.#mentionReportGenerator = deps.mentionReportGenerator;
|
|
27
|
+
this.#mentionReportRecipientRepository = deps.mentionReportRecipientRepository;
|
|
28
|
+
this.#mentionReportEmailView = deps.mentionReportEmailView;
|
|
29
|
+
this.#mentionReportHistoryService = deps.mentionReportHistoryService;
|
|
30
|
+
this.#emailService = deps.emailService;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Checks for new mentions since the last report and sends an email to the recipients.
|
|
35
|
+
*
|
|
36
|
+
* @returns {Promise<number>} - A promise that resolves with the number of mentions found.
|
|
37
|
+
*/
|
|
38
|
+
async sendLatestReport() {
|
|
39
|
+
const lastReport = await this.#mentionReportHistoryService.getLatestReportDate();
|
|
40
|
+
const now = new Date();
|
|
41
|
+
|
|
42
|
+
if (now.valueOf() - lastReport.valueOf() < 24 * 60 * 60 * 1000) {
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const report = await this.#mentionReportGenerator.getMentionReport(lastReport, now);
|
|
47
|
+
|
|
48
|
+
report.mentions = report.mentions.map((mention) => {
|
|
49
|
+
return {
|
|
50
|
+
targetUrl: mention.target,
|
|
51
|
+
sourceUrl: mention.source,
|
|
52
|
+
sourceTitle: mention.sourceTitle,
|
|
53
|
+
sourceExcerpt: mention.sourceExcerpt,
|
|
54
|
+
sourceSiteTitle: mention.sourceSiteTitle,
|
|
55
|
+
sourceFavicon: mention.sourceFavicon,
|
|
56
|
+
sourceAuthor: mention.sourceAuthor,
|
|
57
|
+
sourceFeaturedImage: mention.sourceFeaturedImage
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (!report?.mentions?.length) {
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const recipients = await this.#mentionReportRecipientRepository.getMentionReportRecipients();
|
|
66
|
+
|
|
67
|
+
for (const recipient of recipients) {
|
|
68
|
+
const subject = await this.#mentionReportEmailView.renderSubject(report, recipient);
|
|
69
|
+
const html = await this.#mentionReportEmailView.renderHTML(report, recipient);
|
|
70
|
+
const text = await this.#mentionReportEmailView.renderText(report, recipient);
|
|
71
|
+
|
|
72
|
+
await this.#emailService.send(recipient.email, subject, html, text);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
await this.#mentionReportHistoryService.setLatestReportDate(now);
|
|
76
|
+
|
|
77
|
+
return report.mentions.length;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @typedef {object} MentionReportRecipient
|
|
83
|
+
* @prop {string} email
|
|
84
|
+
* @prop {string} slug
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @typedef {object} IMentionReportRecipientRepository
|
|
89
|
+
* @prop {() => Promise<MentionReportRecipient[]>} getMentionReportRecipients
|
|
90
|
+
*/
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @typedef {import('@tryghost/webmentions/lib/MentionsAPI').MentionReport} MentionReport
|
|
94
|
+
*/
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @typedef {object} IMentionReportGenerator
|
|
98
|
+
* @prop {(startDate: Date, endDate: Date) => Promise<MentionReport>} getMentionReport
|
|
99
|
+
*/
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @typedef {object} IMentionReportEmailView
|
|
103
|
+
* @prop {(report: MentionReport, recipient: MentionReportRecipient) => Promise<string>} renderHTML
|
|
104
|
+
* @prop {(report: MentionReport, recipient: MentionReportRecipient) => Promise<string>} renderText
|
|
105
|
+
* @prop {(report: MentionReport, recipient: MentionReportRecipient) => Promise<string>} renderSubject
|
|
106
|
+
*/
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @typedef {object} IEmailService
|
|
110
|
+
* @prop {(to: string, subject: string, html: string, text: string) => Promise<void>} send
|
|
111
|
+
*/
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @typedef {object} IMentionReportHistoryService
|
|
115
|
+
* @prop {() => Promise<Date>} getLatestReportDate
|
|
116
|
+
* @prop {(date: Date) => Promise<void>} setLatestReportDate
|
|
117
|
+
*/
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
const MentionEmailReportJob = require('
|
|
1
|
+
const MentionEmailReportJob = require('./MentionEmailReportJob');
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* @typedef {
|
|
5
|
-
* @typedef {
|
|
4
|
+
* @typedef {MentionEmailReportJob.MentionReport} MentionReport
|
|
5
|
+
* @typedef {MentionEmailReportJob.MentionReportRecipient} MentionReportRecipient
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
let initialised = false;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
const {MemberCreatedEvent, SubscriptionCancelledEvent, SubscriptionActivatedEvent} = require('@tryghost/member-events');
|
|
2
|
+
const {MilestoneCreatedEvent} = require('@tryghost/milestones');
|
|
3
|
+
|
|
4
|
+
// @NOTE: 'StaffService' is a vague name that does not describe what it's actually doing.
|
|
5
|
+
// Possibly, "StaffNotificationService" or "StaffEventNotificationService" would be a more accurate name
|
|
6
|
+
class StaffService {
|
|
7
|
+
constructor({logging, models, mailer, settingsCache, settingsHelpers, urlUtils, blogIcon, DomainEvents, labs, memberAttributionService}) {
|
|
8
|
+
this.logging = logging;
|
|
9
|
+
this.labs = labs;
|
|
10
|
+
/** @private */
|
|
11
|
+
this.settingsCache = settingsCache;
|
|
12
|
+
this.models = models;
|
|
13
|
+
this.DomainEvents = DomainEvents;
|
|
14
|
+
this.memberAttributionService = memberAttributionService;
|
|
15
|
+
|
|
16
|
+
const Emails = require('./StaffServiceEmails');
|
|
17
|
+
|
|
18
|
+
this.emails = new Emails({
|
|
19
|
+
logging,
|
|
20
|
+
models,
|
|
21
|
+
mailer,
|
|
22
|
+
settingsHelpers,
|
|
23
|
+
settingsCache,
|
|
24
|
+
urlUtils,
|
|
25
|
+
blogIcon,
|
|
26
|
+
labs
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** @private */
|
|
31
|
+
getSerializedData({member, tier = null, subscription = null, offer = null}) {
|
|
32
|
+
return {
|
|
33
|
+
offer: offer ? {
|
|
34
|
+
name: offer.name,
|
|
35
|
+
type: offer.discount_type,
|
|
36
|
+
currency: offer.currency,
|
|
37
|
+
duration: offer.duration,
|
|
38
|
+
durationInMonths: offer.duration_in_months,
|
|
39
|
+
amount: offer.discount_amount
|
|
40
|
+
} : null,
|
|
41
|
+
subscription: subscription ? {
|
|
42
|
+
id: subscription.id,
|
|
43
|
+
amount: subscription.plan?.amount,
|
|
44
|
+
interval: subscription.plan?.interval,
|
|
45
|
+
currency: subscription.plan?.currency,
|
|
46
|
+
startDate: subscription.start_date,
|
|
47
|
+
cancelAt: subscription.current_period_end,
|
|
48
|
+
cancellationReason: subscription.cancellation_reason
|
|
49
|
+
} : null,
|
|
50
|
+
member: member ? {
|
|
51
|
+
id: member.id,
|
|
52
|
+
name: member.name,
|
|
53
|
+
email: member.email,
|
|
54
|
+
geolocation: member.geolocation,
|
|
55
|
+
status: member.status,
|
|
56
|
+
created_at: member.created_at
|
|
57
|
+
} : null,
|
|
58
|
+
tier: tier ? {
|
|
59
|
+
id: tier.id,
|
|
60
|
+
name: tier.name
|
|
61
|
+
} : null
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** @private */
|
|
66
|
+
async getDataFromIds({memberId, tierId = null, subscriptionId = null, offerId = null}) {
|
|
67
|
+
const memberModel = memberId ? await this.models.Member.findOne({id: memberId}) : null;
|
|
68
|
+
const tierModel = tierId ? await this.models.Product.findOne({id: tierId}) : null;
|
|
69
|
+
const subscriptionModel = subscriptionId ? await this.models.StripeCustomerSubscription.findOne({id: subscriptionId}) : null;
|
|
70
|
+
const offerModel = offerId ? await this.models.Offer.findOne({id: offerId}) : null;
|
|
71
|
+
|
|
72
|
+
return this.getSerializedData({
|
|
73
|
+
member: memberModel?.toJSON(),
|
|
74
|
+
tier: tierModel?.toJSON(),
|
|
75
|
+
subscription: subscriptionModel?.toJSON(),
|
|
76
|
+
offer: offerModel?.toJSON()
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** @private */
|
|
81
|
+
async handleEvent(type, event) {
|
|
82
|
+
if (type === MilestoneCreatedEvent && event.data.milestone) {
|
|
83
|
+
await this.emails.notifyMilestoneReceived(event.data);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!['api', 'member'].includes(event.data.source)) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const {member, tier, subscription, offer} = await this.getDataFromIds({
|
|
91
|
+
memberId: event.data.memberId,
|
|
92
|
+
tierId: event.data.tierId,
|
|
93
|
+
subscriptionId: event.data.subscriptionId,
|
|
94
|
+
offerId: event.data.offerId
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (type === MemberCreatedEvent && member.status === 'free') {
|
|
98
|
+
let attribution;
|
|
99
|
+
if (event.data?.attribution) {
|
|
100
|
+
attribution = await this.memberAttributionService.fetchResource(event.data.attribution);
|
|
101
|
+
} else {
|
|
102
|
+
try {
|
|
103
|
+
attribution = await this.memberAttributionService.getMemberCreatedAttribution(event.data.memberId);
|
|
104
|
+
} catch (e) {
|
|
105
|
+
this.logging.warn(`Failed to get attribution for member - ${event?.data?.memberId}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
await this.emails.notifyFreeMemberSignup({
|
|
109
|
+
member,
|
|
110
|
+
attribution
|
|
111
|
+
});
|
|
112
|
+
} else if (type === SubscriptionActivatedEvent) {
|
|
113
|
+
let attribution;
|
|
114
|
+
if (event.data?.attribution) {
|
|
115
|
+
attribution = await this.memberAttributionService.fetchResource(event.data.attribution);
|
|
116
|
+
} else {
|
|
117
|
+
try {
|
|
118
|
+
attribution = await this.memberAttributionService.getSubscriptionCreatedAttribution(event.data.subscriptionId);
|
|
119
|
+
} catch (e) {
|
|
120
|
+
this.logging.warn(`Failed to get attribution for member - ${event?.data?.memberId}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
await this.emails.notifyPaidSubscriptionStarted({
|
|
124
|
+
member,
|
|
125
|
+
offer,
|
|
126
|
+
tier,
|
|
127
|
+
subscription,
|
|
128
|
+
attribution
|
|
129
|
+
});
|
|
130
|
+
} else if (type === SubscriptionCancelledEvent) {
|
|
131
|
+
await this.emails.notifyPaidSubscriptionCanceled({
|
|
132
|
+
member,
|
|
133
|
+
tier,
|
|
134
|
+
subscription,
|
|
135
|
+
...event.data
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
subscribeEvents() {
|
|
141
|
+
// Trigger email for free member signup
|
|
142
|
+
this.DomainEvents.subscribe(MemberCreatedEvent, async (event) => {
|
|
143
|
+
try {
|
|
144
|
+
await this.handleEvent(MemberCreatedEvent, event);
|
|
145
|
+
} catch (e) {
|
|
146
|
+
this.logging.error(e, `Failed to notify free member signup - ${event?.data?.memberId}`);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Trigger email on paid subscription start
|
|
151
|
+
this.DomainEvents.subscribe(SubscriptionActivatedEvent, async (event) => {
|
|
152
|
+
try {
|
|
153
|
+
await this.handleEvent(SubscriptionActivatedEvent, event);
|
|
154
|
+
} catch (e) {
|
|
155
|
+
this.logging.error(e, `Failed to notify paid member subscription start - ${event?.data?.memberId}`);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Trigger email when a member cancels their subscription
|
|
160
|
+
this.DomainEvents.subscribe(SubscriptionCancelledEvent, async (event) => {
|
|
161
|
+
try {
|
|
162
|
+
await this.handleEvent(SubscriptionCancelledEvent, event);
|
|
163
|
+
} catch (e) {
|
|
164
|
+
this.logging.error(e, `Failed to notify paid member subscription cancel - ${event?.data?.memberId}`);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Trigger email when a new milestone is reached
|
|
169
|
+
this.DomainEvents.subscribe(MilestoneCreatedEvent, async (event) => {
|
|
170
|
+
try {
|
|
171
|
+
await this.handleEvent(MilestoneCreatedEvent, event);
|
|
172
|
+
} catch (e) {
|
|
173
|
+
this.logging.error(e, `Failed to notify milestone`);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
module.exports = StaffService;
|