ghost 5.115.1 → 5.116.1
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-api-framework-5.115.1.tgz → tryghost-api-framework-5.116.1.tgz} +0 -0
- package/components/tryghost-constants-5.116.1.tgz +0 -0
- package/components/tryghost-custom-fonts-5.116.1.tgz +0 -0
- package/components/{tryghost-custom-theme-settings-service-5.115.1.tgz → tryghost-custom-theme-settings-service-5.116.1.tgz} +0 -0
- package/components/{tryghost-domain-events-5.115.1.tgz → tryghost-domain-events-5.116.1.tgz} +0 -0
- package/components/{tryghost-donations-5.115.1.tgz → tryghost-donations-5.116.1.tgz} +0 -0
- package/components/tryghost-email-addresses-5.116.1.tgz +0 -0
- package/components/tryghost-email-service-5.116.1.tgz +0 -0
- package/components/tryghost-email-suppression-list-5.116.1.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.116.1.tgz +0 -0
- package/components/tryghost-i18n-5.116.1.tgz +0 -0
- package/components/tryghost-job-manager-5.116.1.tgz +0 -0
- package/components/tryghost-link-replacer-5.116.1.tgz +0 -0
- package/components/tryghost-magic-link-5.116.1.tgz +0 -0
- package/components/{tryghost-member-attribution-5.115.1.tgz → tryghost-member-attribution-5.116.1.tgz} +0 -0
- package/components/tryghost-member-events-5.116.1.tgz +0 -0
- package/components/tryghost-members-api-5.116.1.tgz +0 -0
- package/components/tryghost-members-csv-5.116.1.tgz +0 -0
- package/components/{tryghost-members-offers-5.115.1.tgz → tryghost-members-offers-5.116.1.tgz} +0 -0
- package/components/{tryghost-milestones-5.115.1.tgz → tryghost-milestones-5.116.1.tgz} +0 -0
- package/components/{tryghost-mw-error-handler-5.115.1.tgz → tryghost-mw-error-handler-5.116.1.tgz} +0 -0
- package/components/tryghost-mw-vhost-5.116.1.tgz +0 -0
- package/components/{tryghost-post-events-5.115.1.tgz → tryghost-post-events-5.116.1.tgz} +0 -0
- package/components/{tryghost-post-revisions-5.115.1.tgz → tryghost-post-revisions-5.116.1.tgz} +0 -0
- package/components/tryghost-posts-service-5.116.1.tgz +0 -0
- package/components/{tryghost-prometheus-metrics-5.115.1.tgz → tryghost-prometheus-metrics-5.116.1.tgz} +0 -0
- package/components/tryghost-security-5.116.1.tgz +0 -0
- package/components/{tryghost-tiers-5.115.1.tgz → tryghost-tiers-5.116.1.tgz} +0 -0
- package/components/tryghost-webmentions-5.116.1.tgz +0 -0
- package/content/themes/casper/LICENSE +1 -1
- package/content/themes/casper/README.md +1 -1
- package/content/themes/source/LICENSE +1 -1
- package/content/themes/source/README.md +1 -1
- package/content/themes/source/assets/built/screen.css +1 -1
- package/content/themes/source/assets/built/screen.css.map +1 -1
- package/content/themes/source/assets/css/screen.css +6 -11
- package/content/themes/source/partials/feature-image.hbs +2 -2
- package/core/boot.js +0 -42
- package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +24764 -24129
- package/core/built/admin/assets/admin-x-demo/admin-x-demo.js +1 -1
- package/core/built/admin/assets/admin-x-demo/{index-15df2af5.mjs → index-a9601514.mjs} +3 -3
- package/core/built/admin/assets/admin-x-demo/{modals-8ca61d78.mjs → modals-c1789d04.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{CodeEditorView-d2e6872f.mjs → CodeEditorView-e9c9deb8.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-8e8821e5.mjs → index-84580c3a.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-f5cb3db3.mjs → index-f744cab7.mjs} +49 -35
- package/core/built/admin/assets/admin-x-settings/{modals-e8ae4d46.mjs → modals-d9ca60c5.mjs} +1198 -1192
- package/core/built/admin/assets/chunk.524.cb72a86e19c9ffd6172e.js +35 -0
- package/core/built/admin/assets/chunk.582.4f4d38ffe79fbdbd26f7.js +37 -0
- package/core/built/admin/assets/{chunk.874.461cb3cf5b6b36915f8c.js → chunk.713.e9027c0cc3c56110f5da.js} +125 -98
- package/core/built/admin/assets/{ghost-df7b9558260aa27d18b195ee895b487d.js → ghost-03b64c086f3c60cabc85fe7a7e2b640a.js} +144 -145
- package/core/built/admin/assets/ghost-ba58e9822f7384461e926c7e23f04a75.css +1 -0
- package/core/built/admin/assets/ghost-dark-f1f29683b14ffa11615b3bba8b6ab92c.css +1 -0
- package/core/built/admin/assets/koenig-lexical/index.css +1 -1
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +20563 -20891
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +139 -139
- package/core/built/admin/assets/posts/posts.js +5732 -5667
- package/core/built/admin/assets/stats/stats.js +71082 -7533
- package/core/built/admin/assets/{vendor-68a4aa424a179a90f5bbc2b750def576.js → vendor-72026232b36d97babc6320917c16c321.js} +36 -34
- package/core/built/admin/index.html +6 -6
- package/core/cli/generate-data.js +1 -1
- package/core/frontend/helpers/ghost_head.js +6 -1
- package/core/frontend/public/ghost-stats.js +55 -2
- package/core/frontend/services/assets-minification/AdminAuthAssets.js +2 -1
- package/core/frontend/services/assets-minification/CardAssets.js +1 -1
- package/core/frontend/services/assets-minification/CommentCountsAssets.js +1 -1
- package/core/frontend/services/assets-minification/MemberAttributionAssets.js +1 -1
- package/core/frontend/services/assets-minification/Minifier.js +191 -0
- package/core/frontend/services/routing/controllers/previews.js +2 -1
- package/core/server/adapters/cache/Redis.js +1 -1
- package/core/server/adapters/lib/redis/AdapterCacheRedis.js +287 -0
- package/core/server/adapters/lib/redis/redis-store-factory.js +22 -0
- package/core/server/api/endpoints/posts.js +9 -3
- package/core/server/api/endpoints/previews.js +35 -1
- package/core/server/api/endpoints/utils/serializers/output/utils/post-gating.js +6 -9
- package/core/server/api/endpoints/utils/validators/input/settings.js +1 -1
- package/core/server/data/db/connection.js +2 -0
- package/core/server/data/db/index.js +1 -0
- package/core/server/data/importer/handlers/ImporterContentFileHandler.js +90 -0
- package/core/server/data/importer/import-manager.js +1 -1
- package/core/server/data/seeders/DataGenerator.js +288 -0
- package/core/server/data/seeders/importers/BenefitsImporter.js +28 -0
- package/core/server/data/seeders/importers/CommentsImporter.js +73 -0
- package/core/server/data/seeders/importers/EmailBatchesImporter.js +38 -0
- package/core/server/data/seeders/importers/EmailRecipientFailuresImporter.js +67 -0
- package/core/server/data/seeders/importers/EmailRecipientsImporter.js +212 -0
- package/core/server/data/seeders/importers/EmailsImporter.js +99 -0
- package/core/server/data/seeders/importers/LabelsImporter.js +41 -0
- package/core/server/data/seeders/importers/MembersClickEventsImporter.js +69 -0
- package/core/server/data/seeders/importers/MembersCreatedEventsImporter.js +103 -0
- package/core/server/data/seeders/importers/MembersFeedbackImporter.js +45 -0
- package/core/server/data/seeders/importers/MembersImporter.js +111 -0
- package/core/server/data/seeders/importers/MembersLabelsImporter.js +39 -0
- package/core/server/data/seeders/importers/MembersLoginEventsImporter.js +69 -0
- package/core/server/data/seeders/importers/MembersNewslettersImporter.js +38 -0
- package/core/server/data/seeders/importers/MembersPaidSubscriptionEventsImporter.js +99 -0
- package/core/server/data/seeders/importers/MembersProductsImporter.js +42 -0
- package/core/server/data/seeders/importers/MembersStatusEventsImporter.js +58 -0
- package/core/server/data/seeders/importers/MembersStripeCustomersImporter.js +60 -0
- package/core/server/data/seeders/importers/MembersStripeCustomersSubscriptionsImporter.js +259 -0
- package/core/server/data/seeders/importers/MembersSubscribeEventsImporter.js +69 -0
- package/core/server/data/seeders/importers/MembersSubscriptionCreatedEventsImporter.js +95 -0
- package/core/server/data/seeders/importers/NewslettersImporter.js +40 -0
- package/core/server/data/seeders/importers/OffersImporter.js +70 -0
- package/core/server/data/seeders/importers/PostsAuthorsImporter.js +32 -0
- package/core/server/data/seeders/importers/PostsImporter.js +102 -0
- package/core/server/data/seeders/importers/PostsProductsImporter.js +35 -0
- package/core/server/data/seeders/importers/PostsTagsImporter.js +46 -0
- package/core/server/data/seeders/importers/ProductsBenefitsImporter.js +54 -0
- package/core/server/data/seeders/importers/ProductsImporter.js +90 -0
- package/core/server/data/seeders/importers/RecommendationClickEventsImporter.js +32 -0
- package/core/server/data/seeders/importers/RecommendationSubscribeEventsImporter.js +32 -0
- package/core/server/data/seeders/importers/RecommendationsImporter.js +34 -0
- package/core/server/data/seeders/importers/RedirectsImporter.js +49 -0
- package/core/server/data/seeders/importers/RolesUsersImporter.js +42 -0
- package/core/server/data/seeders/importers/StripePricesImporter.js +69 -0
- package/core/server/data/seeders/importers/StripeProductsImporter.js +34 -0
- package/core/server/data/seeders/importers/TableImporter.js +187 -0
- package/core/server/data/seeders/importers/TagsImporter.js +41 -0
- package/core/server/data/seeders/importers/UsersImporter.js +31 -0
- package/core/server/data/seeders/importers/WebMentionsImporter.js +42 -0
- package/core/server/data/seeders/importers/index.js +41 -0
- package/core/server/data/seeders/utils/JsonImporter.js +39 -0
- package/core/server/data/seeders/utils/blog-info.js +3 -0
- package/core/server/data/seeders/utils/database-date.js +7 -0
- package/core/server/data/seeders/utils/event-generator.js +48 -0
- package/core/server/data/seeders/utils/random.js +13 -0
- package/core/server/data/seeders/utils/topological-sort.js +33 -0
- package/core/server/services/adapter-manager/AdapterManager.js +161 -0
- package/core/server/services/adapter-manager/index.js +1 -1
- package/core/server/services/announcement-bar-service/AnnouncementBarSettings.js +54 -0
- package/core/server/services/announcement-bar-service/AnnouncementVisibilityValues.js +11 -0
- package/core/server/services/announcement-bar-service/index.js +1 -1
- package/core/server/services/api-version-compatibility/APIVersionCompatibilityService.js +1 -1
- package/core/server/services/auth/session/session-service.js +15 -5
- package/core/server/services/custom-redirects/index.js +1 -1
- package/core/server/services/email-analytics/EmailAnalyticsProviderMailgun.js +1 -1
- package/core/server/services/email-service/EmailServiceWrapper.js +4 -4
- package/core/server/services/email-suppression-list/MailgunEmailSuppressionList.js +1 -1
- package/core/server/services/email-suppression-list/service.js +1 -1
- package/core/server/services/lib/DynamicRedirectManager.js +156 -0
- package/core/server/services/lib/EmailContentGenerator.js +54 -0
- package/core/server/services/lib/InMemoryRepository.js +62 -0
- package/core/server/services/lib/InMemoryRepository.ts +80 -0
- package/core/server/services/lib/MailgunClient.js +364 -0
- package/core/server/services/link-redirection/LinkRedirect.js +26 -0
- package/core/server/services/link-redirection/LinkRedirectRepository.js +7 -7
- package/core/server/services/link-redirection/LinkRedirectsService.js +123 -0
- package/core/server/services/link-redirection/README.md +151 -0
- package/core/server/services/link-redirection/RedirectEvent.js +24 -0
- package/core/server/services/link-redirection/index.js +1 -1
- package/core/server/services/link-tracking/LinkClickTrackingService.js +1 -1
- package/core/server/services/mail/index.js +1 -1
- package/core/server/services/mail-events/InMemoryMailEventRepository.js +2 -2
- package/core/server/services/mail-events/InMemoryMailEventRepository.ts +1 -1
- package/core/server/services/members-events/LastSeenAtUpdater.js +1 -1
- package/core/server/services/offers/service.js +1 -1
- package/core/server/services/recommendations/RecommendationServiceWrapper.js +8 -8
- package/core/server/services/recommendations/service/BookshelfClickEventRepository.js +48 -0
- package/core/server/services/recommendations/service/BookshelfClickEventRepository.ts +49 -0
- package/core/server/services/recommendations/service/BookshelfRecommendationRepository.js +98 -0
- package/core/server/services/recommendations/service/BookshelfRecommendationRepository.ts +117 -0
- package/core/server/services/recommendations/service/BookshelfRepository.js +134 -0
- package/core/server/services/recommendations/service/BookshelfRepository.ts +196 -0
- package/core/server/services/recommendations/service/BookshelfSubscribeEventRepository.js +48 -0
- package/core/server/services/recommendations/service/BookshelfSubscribeEventRepository.ts +49 -0
- package/core/server/services/recommendations/service/ClickEvent.js +33 -0
- package/core/server/services/recommendations/service/ClickEvent.ts +32 -0
- package/core/server/services/recommendations/service/InMemoryRecommendationRepository.js +19 -0
- package/core/server/services/recommendations/service/InMemoryRecommendationRepository.ts +20 -0
- package/core/server/services/recommendations/service/IncomingRecommendationController.js +34 -0
- package/core/server/services/recommendations/service/IncomingRecommendationController.ts +51 -0
- package/core/server/services/recommendations/service/IncomingRecommendationEmailRenderer.js +25 -0
- package/core/server/services/recommendations/service/IncomingRecommendationEmailRenderer.ts +37 -0
- package/core/server/services/recommendations/service/IncomingRecommendationService.js +93 -0
- package/core/server/services/recommendations/service/IncomingRecommendationService.ts +160 -0
- package/core/server/services/recommendations/service/Recommendation.js +140 -0
- package/core/server/services/recommendations/service/Recommendation.ts +201 -0
- package/core/server/services/recommendations/service/RecommendationController.js +208 -0
- package/core/server/services/recommendations/service/RecommendationController.ts +258 -0
- package/core/server/services/recommendations/service/RecommendationMetadataService.js +86 -0
- package/core/server/services/recommendations/service/RecommendationMetadataService.ts +128 -0
- package/core/server/services/recommendations/service/RecommendationRepository.js +2 -0
- package/core/server/services/recommendations/service/RecommendationRepository.ts +13 -0
- package/core/server/services/recommendations/service/RecommendationService.js +228 -0
- package/core/server/services/recommendations/service/RecommendationService.ts +281 -0
- package/core/server/services/recommendations/service/SubscribeEvent.js +33 -0
- package/core/server/services/recommendations/service/SubscribeEvent.ts +32 -0
- package/core/server/services/recommendations/service/UnsafeData.js +183 -0
- package/core/server/services/recommendations/service/UnsafeData.ts +217 -0
- package/core/server/services/recommendations/service/WellknownService.js +36 -0
- package/core/server/services/recommendations/service/WellknownService.ts +47 -0
- package/core/server/services/recommendations/service/index.js +31 -0
- package/core/server/services/recommendations/service/index.ts +15 -0
- package/core/server/services/recommendations/service/libraries.d.ts +5 -0
- package/core/server/services/slack-notifications/SlackNotifications.js +211 -0
- package/core/server/services/slack-notifications/SlackNotificationsService.js +90 -0
- package/core/server/services/slack-notifications/service.js +4 -6
- package/core/server/web/api/endpoints/admin/app.js +1 -21
- package/core/server/web/api/middleware/version-match.js +41 -0
- package/core/shared/labs.js +2 -2
- package/package.json +87 -104
- package/tsconfig.tsbuildinfo +1 -1
- package/yarn.lock +1470 -1540
- package/components/tryghost-adapter-cache-redis-5.115.1.tgz +0 -0
- package/components/tryghost-adapter-manager-5.115.1.tgz +0 -0
- package/components/tryghost-announcement-bar-settings-5.115.1.tgz +0 -0
- package/components/tryghost-constants-5.115.1.tgz +0 -0
- package/components/tryghost-custom-fonts-5.115.1.tgz +0 -0
- package/components/tryghost-data-generator-5.115.1.tgz +0 -0
- package/components/tryghost-email-addresses-5.115.1.tgz +0 -0
- package/components/tryghost-email-content-generator-5.115.1.tgz +0 -0
- package/components/tryghost-email-events-5.115.1.tgz +0 -0
- package/components/tryghost-email-service-5.115.1.tgz +0 -0
- package/components/tryghost-email-suppression-list-5.115.1.tgz +0 -0
- package/components/tryghost-express-dynamic-redirects-5.115.1.tgz +0 -0
- package/components/tryghost-ghost-5.115.1.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.115.1.tgz +0 -0
- package/components/tryghost-i18n-5.115.1.tgz +0 -0
- package/components/tryghost-importer-handler-content-files-5.115.1.tgz +0 -0
- package/components/tryghost-in-memory-repository-5.115.1.tgz +0 -0
- package/components/tryghost-job-manager-5.115.1.tgz +0 -0
- package/components/tryghost-link-redirects-5.115.1.tgz +0 -0
- package/components/tryghost-link-replacer-5.115.1.tgz +0 -0
- package/components/tryghost-magic-link-5.115.1.tgz +0 -0
- package/components/tryghost-mailgun-client-5.115.1.tgz +0 -0
- package/components/tryghost-member-events-5.115.1.tgz +0 -0
- package/components/tryghost-members-api-5.115.1.tgz +0 -0
- package/components/tryghost-members-csv-5.115.1.tgz +0 -0
- package/components/tryghost-members-payments-5.115.1.tgz +0 -0
- package/components/tryghost-minifier-5.115.1.tgz +0 -0
- package/components/tryghost-mw-version-match-5.115.1.tgz +0 -0
- package/components/tryghost-mw-vhost-5.115.1.tgz +0 -0
- package/components/tryghost-posts-service-5.115.1.tgz +0 -0
- package/components/tryghost-recommendations-5.115.1.tgz +0 -0
- package/components/tryghost-security-5.115.1.tgz +0 -0
- package/components/tryghost-slack-notifications-5.115.1.tgz +0 -0
- package/components/tryghost-webmentions-5.115.1.tgz +0 -0
- package/core/built/admin/assets/chunk.524.2439684964c164c598ab.js +0 -35
- package/core/built/admin/assets/chunk.582.bf5a2bbb2c4eb69ef1e7.js +0 -37
- package/core/built/admin/assets/ghost-327b17ea23cb8c89bd7e6a51e18e8506.css +0 -1
- package/core/built/admin/assets/ghost-dark-f30a597ac19632a118939492591c531b.css +0 -1
- /package/core/built/admin/assets/{chunk.874.461cb3cf5b6b36915f8c.js.LICENSE.txt → chunk.713.e9027c0cc3c56110f5da.js.LICENSE.txt} +0 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {IncomingRecommendationService} from './IncomingRecommendationService';
|
|
2
|
+
import {IncomingRecommendation} from './IncomingRecommendationService';
|
|
3
|
+
import {UnsafeData} from './UnsafeData';
|
|
4
|
+
|
|
5
|
+
type Frame = {
|
|
6
|
+
data: unknown,
|
|
7
|
+
options: unknown,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type Meta = {
|
|
11
|
+
pagination: object,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class IncomingRecommendationController {
|
|
15
|
+
service: IncomingRecommendationService;
|
|
16
|
+
|
|
17
|
+
constructor(deps: {service: IncomingRecommendationService}) {
|
|
18
|
+
this.service = deps.service;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async browse(frame: Frame) {
|
|
22
|
+
const options = new UnsafeData(frame.options);
|
|
23
|
+
|
|
24
|
+
const page = options.optionalKey('page')?.integer ?? 1;
|
|
25
|
+
const limit = options.optionalKey('limit')?.integer ?? 5;
|
|
26
|
+
|
|
27
|
+
const {incomingRecommendations, meta} = await this.service.listIncomingRecommendations({page, limit});
|
|
28
|
+
|
|
29
|
+
return this.#serialize(
|
|
30
|
+
incomingRecommendations,
|
|
31
|
+
meta
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#serialize(recommendations: IncomingRecommendation[], meta?: Meta) {
|
|
36
|
+
return {
|
|
37
|
+
data: recommendations.map((entity) => {
|
|
38
|
+
return {
|
|
39
|
+
id: entity.id,
|
|
40
|
+
title: entity.title,
|
|
41
|
+
excerpt: entity.excerpt,
|
|
42
|
+
featured_image: entity.featuredImage?.toString() ?? null,
|
|
43
|
+
favicon: entity.favicon?.toString() ?? null,
|
|
44
|
+
url: entity.url.toString(),
|
|
45
|
+
recommending_back: !!entity.recommendingBack
|
|
46
|
+
};
|
|
47
|
+
}),
|
|
48
|
+
meta
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.IncomingRecommendationEmailRenderer = void 0;
|
|
4
|
+
class IncomingRecommendationEmailRenderer {
|
|
5
|
+
#staffService;
|
|
6
|
+
constructor({ staffService }) {
|
|
7
|
+
this.#staffService = staffService;
|
|
8
|
+
}
|
|
9
|
+
async renderSubject(recommendation) {
|
|
10
|
+
return `👍 New recommendation: ${recommendation.title}`;
|
|
11
|
+
}
|
|
12
|
+
async renderHTML(recommendation, recipient) {
|
|
13
|
+
return this.#staffService.api.emails.renderHTML('recommendation-received', {
|
|
14
|
+
recommendation,
|
|
15
|
+
recipient
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
async renderText(recommendation, recipient) {
|
|
19
|
+
return this.#staffService.api.emails.renderText('recommendation-received', {
|
|
20
|
+
recommendation,
|
|
21
|
+
recipient
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.IncomingRecommendationEmailRenderer = IncomingRecommendationEmailRenderer;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {EmailRecipient} from './IncomingRecommendationService';
|
|
2
|
+
import {IncomingRecommendation} from './IncomingRecommendationService';
|
|
3
|
+
|
|
4
|
+
type StaffService = {
|
|
5
|
+
api: {
|
|
6
|
+
emails: {
|
|
7
|
+
renderHTML(template: string, data: unknown): Promise<string>,
|
|
8
|
+
renderText(template: string, data: unknown): Promise<string>
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class IncomingRecommendationEmailRenderer {
|
|
14
|
+
#staffService: StaffService;
|
|
15
|
+
|
|
16
|
+
constructor({staffService}: {staffService: StaffService}) {
|
|
17
|
+
this.#staffService = staffService;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async renderSubject(recommendation: IncomingRecommendation) {
|
|
21
|
+
return `👍 New recommendation: ${recommendation.title}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async renderHTML(recommendation: IncomingRecommendation, recipient: EmailRecipient) {
|
|
25
|
+
return this.#staffService.api.emails.renderHTML('recommendation-received', {
|
|
26
|
+
recommendation,
|
|
27
|
+
recipient
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async renderText(recommendation: IncomingRecommendation, recipient: EmailRecipient) {
|
|
32
|
+
return this.#staffService.api.emails.renderText('recommendation-received', {
|
|
33
|
+
recommendation,
|
|
34
|
+
recipient
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* eslint-disable ghost/filenames/match-exported-class */
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.IncomingRecommendationService = void 0;
|
|
8
|
+
const logging_1 = __importDefault(require("@tryghost/logging"));
|
|
9
|
+
class IncomingRecommendationService {
|
|
10
|
+
#mentionsApi;
|
|
11
|
+
#recommendationService;
|
|
12
|
+
#emailService;
|
|
13
|
+
#emailRenderer;
|
|
14
|
+
#getEmailRecipients;
|
|
15
|
+
constructor(deps) {
|
|
16
|
+
this.#recommendationService = deps.recommendationService;
|
|
17
|
+
this.#mentionsApi = deps.mentionsApi;
|
|
18
|
+
this.#emailService = deps.emailService;
|
|
19
|
+
this.#emailRenderer = deps.emailRenderer;
|
|
20
|
+
this.#getEmailRecipients = deps.getEmailRecipients;
|
|
21
|
+
}
|
|
22
|
+
async init() {
|
|
23
|
+
// When we boot, it is possible that we missed some webmentions from other sites recommending you
|
|
24
|
+
// More importantly, we might have missed some deletes which we can detect.
|
|
25
|
+
// So we do a slow revalidation of all incoming recommendations
|
|
26
|
+
// This also prevents doing multiple external fetches when doing quick reboots of Ghost after each other (requires Ghost to be up for at least 15 seconds)
|
|
27
|
+
if (!process.env.NODE_ENV?.startsWith('test') && process.env.NODE_ENV !== 'development') {
|
|
28
|
+
setTimeout(() => {
|
|
29
|
+
logging_1.default.info('Updating incoming recommendations on boot');
|
|
30
|
+
this.#updateIncomingRecommendations().catch((err) => {
|
|
31
|
+
logging_1.default.error('Failed to update incoming recommendations on boot', err);
|
|
32
|
+
});
|
|
33
|
+
}, 15 * 1000 + Math.random() * 5 * 60 * 1000);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
#getMentionFilter() {
|
|
37
|
+
return `source:~$'/.well-known/recommendations.json'`;
|
|
38
|
+
}
|
|
39
|
+
async #updateIncomingRecommendations() {
|
|
40
|
+
// We refresh all incoming recommendations, including:
|
|
41
|
+
// - recommendations that were not verified, as the verification could have failed
|
|
42
|
+
// - recommendations that were deleted previously. Implementation note: given that we have `deleted:false` as default filter in the Mention model, we need to override it here
|
|
43
|
+
const filter = this.#getMentionFilter() + '+deleted:[true,false]';
|
|
44
|
+
await this.#mentionsApi.refreshMentions({ filter, limit: 100 });
|
|
45
|
+
}
|
|
46
|
+
async #mentionToIncomingRecommendation(mention) {
|
|
47
|
+
try {
|
|
48
|
+
const url = new URL(mention.source.toString().replace(/\/.well-known\/recommendations\.json$/, ''));
|
|
49
|
+
// Check if we are also recommending this URL
|
|
50
|
+
const existing = await this.#recommendationService.readRecommendationByUrl(url);
|
|
51
|
+
const recommendingBack = !!existing;
|
|
52
|
+
return {
|
|
53
|
+
id: mention.id,
|
|
54
|
+
title: mention.sourceSiteTitle || mention.sourceTitle,
|
|
55
|
+
url,
|
|
56
|
+
excerpt: mention.sourceExcerpt,
|
|
57
|
+
favicon: mention.sourceFavicon,
|
|
58
|
+
featuredImage: mention.sourceFeaturedImage,
|
|
59
|
+
recommendingBack
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
logging_1.default.error('Failed to parse mention to incoming recommendation data type', e);
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
async sendRecommendationEmail(mention) {
|
|
68
|
+
const recommendation = await this.#mentionToIncomingRecommendation(mention);
|
|
69
|
+
if (!recommendation) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const recipients = await this.#getEmailRecipients();
|
|
73
|
+
for (const recipient of recipients) {
|
|
74
|
+
const subject = await this.#emailRenderer.renderSubject(recommendation);
|
|
75
|
+
const html = await this.#emailRenderer.renderHTML(recommendation, recipient);
|
|
76
|
+
const text = await this.#emailRenderer.renderText(recommendation, recipient);
|
|
77
|
+
await this.#emailService.send(recipient.email, subject, html, text);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async listIncomingRecommendations(options) {
|
|
81
|
+
const page = options.page ?? 1;
|
|
82
|
+
const limit = options.limit ?? 5;
|
|
83
|
+
const filter = this.#getMentionFilter();
|
|
84
|
+
const mentions = await this.#mentionsApi.listMentions({ filter, page, limit });
|
|
85
|
+
const mentionsToIncomingRecommendations = await Promise.all(mentions.data.map(mention => this.#mentionToIncomingRecommendation(mention)));
|
|
86
|
+
const incomingRecommendations = mentionsToIncomingRecommendations.filter((recommendation) => !!recommendation);
|
|
87
|
+
return {
|
|
88
|
+
incomingRecommendations,
|
|
89
|
+
meta: mentions.meta
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
exports.IncomingRecommendationService = IncomingRecommendationService;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/* eslint-disable ghost/filenames/match-exported-class */
|
|
2
|
+
|
|
3
|
+
import {IncomingRecommendationEmailRenderer} from './IncomingRecommendationEmailRenderer';
|
|
4
|
+
import {RecommendationService} from './RecommendationService';
|
|
5
|
+
import logging from '@tryghost/logging';
|
|
6
|
+
|
|
7
|
+
export type IncomingRecommendation = {
|
|
8
|
+
id: string;
|
|
9
|
+
title: string;
|
|
10
|
+
url: URL;
|
|
11
|
+
excerpt: string|null;
|
|
12
|
+
favicon: URL|null;
|
|
13
|
+
featuredImage: URL|null;
|
|
14
|
+
recommendingBack: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type Report = {
|
|
18
|
+
startDate: Date,
|
|
19
|
+
endDate: Date,
|
|
20
|
+
recommendations: IncomingRecommendation[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type Mention = {
|
|
24
|
+
id: string,
|
|
25
|
+
source: URL,
|
|
26
|
+
sourceTitle: string,
|
|
27
|
+
sourceSiteTitle: string|null,
|
|
28
|
+
sourceAuthor: string|null,
|
|
29
|
+
sourceExcerpt: string|null,
|
|
30
|
+
sourceFavicon: URL|null,
|
|
31
|
+
sourceFeaturedImage: URL|null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type MentionMeta = {
|
|
35
|
+
pagination: {
|
|
36
|
+
page: number;
|
|
37
|
+
limit: number;
|
|
38
|
+
pages: number;
|
|
39
|
+
total: number;
|
|
40
|
+
next: null | number;
|
|
41
|
+
prev: null | number;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type MentionsAPI = {
|
|
46
|
+
refreshMentions(options: {filter: string, limit: number|'all'}): Promise<void>
|
|
47
|
+
listMentions(options: {filter: string, page: number, limit: number|'all'}): Promise<{data: Mention[], meta?: MentionMeta}>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type EmailRecipient = {
|
|
51
|
+
email: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type EmailService = {
|
|
55
|
+
send(to: string, subject: string, html: string, text: string): Promise<void>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class IncomingRecommendationService {
|
|
59
|
+
#mentionsApi: MentionsAPI;
|
|
60
|
+
#recommendationService: RecommendationService;
|
|
61
|
+
|
|
62
|
+
#emailService: EmailService;
|
|
63
|
+
#emailRenderer: IncomingRecommendationEmailRenderer;
|
|
64
|
+
#getEmailRecipients: () => Promise<EmailRecipient[]>;
|
|
65
|
+
|
|
66
|
+
constructor(deps: {
|
|
67
|
+
recommendationService: RecommendationService,
|
|
68
|
+
mentionsApi: MentionsAPI,
|
|
69
|
+
emailService: EmailService,
|
|
70
|
+
emailRenderer: IncomingRecommendationEmailRenderer,
|
|
71
|
+
getEmailRecipients: () => Promise<EmailRecipient[]>,
|
|
72
|
+
}) {
|
|
73
|
+
this.#recommendationService = deps.recommendationService;
|
|
74
|
+
this.#mentionsApi = deps.mentionsApi;
|
|
75
|
+
this.#emailService = deps.emailService;
|
|
76
|
+
this.#emailRenderer = deps.emailRenderer;
|
|
77
|
+
this.#getEmailRecipients = deps.getEmailRecipients;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async init() {
|
|
81
|
+
// When we boot, it is possible that we missed some webmentions from other sites recommending you
|
|
82
|
+
// More importantly, we might have missed some deletes which we can detect.
|
|
83
|
+
// So we do a slow revalidation of all incoming recommendations
|
|
84
|
+
// This also prevents doing multiple external fetches when doing quick reboots of Ghost after each other (requires Ghost to be up for at least 15 seconds)
|
|
85
|
+
if (!process.env.NODE_ENV?.startsWith('test') && process.env.NODE_ENV !== 'development') {
|
|
86
|
+
setTimeout(() => {
|
|
87
|
+
logging.info('Updating incoming recommendations on boot');
|
|
88
|
+
this.#updateIncomingRecommendations().catch((err) => {
|
|
89
|
+
logging.error('Failed to update incoming recommendations on boot', err);
|
|
90
|
+
});
|
|
91
|
+
}, 15 * 1000 + Math.random() * 5 * 60 * 1000);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#getMentionFilter() {
|
|
96
|
+
return `source:~$'/.well-known/recommendations.json'`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async #updateIncomingRecommendations() {
|
|
100
|
+
// We refresh all incoming recommendations, including:
|
|
101
|
+
// - recommendations that were not verified, as the verification could have failed
|
|
102
|
+
// - recommendations that were deleted previously. Implementation note: given that we have `deleted:false` as default filter in the Mention model, we need to override it here
|
|
103
|
+
const filter = this.#getMentionFilter() + '+deleted:[true,false]';
|
|
104
|
+
await this.#mentionsApi.refreshMentions({filter, limit: 100});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async #mentionToIncomingRecommendation(mention: Mention): Promise<IncomingRecommendation|null> {
|
|
108
|
+
try {
|
|
109
|
+
const url = new URL(mention.source.toString().replace(/\/.well-known\/recommendations\.json$/, ''));
|
|
110
|
+
|
|
111
|
+
// Check if we are also recommending this URL
|
|
112
|
+
const existing = await this.#recommendationService.readRecommendationByUrl(url);
|
|
113
|
+
const recommendingBack = !!existing;
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
id: mention.id,
|
|
117
|
+
title: mention.sourceSiteTitle || mention.sourceTitle,
|
|
118
|
+
url,
|
|
119
|
+
excerpt: mention.sourceExcerpt,
|
|
120
|
+
favicon: mention.sourceFavicon,
|
|
121
|
+
featuredImage: mention.sourceFeaturedImage,
|
|
122
|
+
recommendingBack
|
|
123
|
+
};
|
|
124
|
+
} catch (e) {
|
|
125
|
+
logging.error('Failed to parse mention to incoming recommendation data type', e);
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async sendRecommendationEmail(mention: Mention) {
|
|
131
|
+
const recommendation = await this.#mentionToIncomingRecommendation(mention);
|
|
132
|
+
if (!recommendation) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const recipients = await this.#getEmailRecipients();
|
|
136
|
+
|
|
137
|
+
for (const recipient of recipients) {
|
|
138
|
+
const subject = await this.#emailRenderer.renderSubject(recommendation);
|
|
139
|
+
const html = await this.#emailRenderer.renderHTML(recommendation, recipient);
|
|
140
|
+
const text = await this.#emailRenderer.renderText(recommendation, recipient);
|
|
141
|
+
|
|
142
|
+
await this.#emailService.send(recipient.email, subject, html, text);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async listIncomingRecommendations(options: { page?: number; limit?: number|'all'}): Promise<{ incomingRecommendations: IncomingRecommendation[]; meta?: MentionMeta }> {
|
|
147
|
+
const page = options.page ?? 1;
|
|
148
|
+
const limit = options.limit ?? 5;
|
|
149
|
+
const filter = this.#getMentionFilter();
|
|
150
|
+
|
|
151
|
+
const mentions = await this.#mentionsApi.listMentions({filter, page, limit});
|
|
152
|
+
const mentionsToIncomingRecommendations = await Promise.all(mentions.data.map(mention => this.#mentionToIncomingRecommendation(mention)));
|
|
153
|
+
const incomingRecommendations = mentionsToIncomingRecommendations.filter((recommendation): recommendation is IncomingRecommendation => !!recommendation);
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
incomingRecommendations,
|
|
157
|
+
meta: mentions.meta
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* eslint-disable ghost/filenames/match-exported-class */
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.Recommendation = void 0;
|
|
8
|
+
const bson_objectid_1 = __importDefault(require("bson-objectid"));
|
|
9
|
+
const errors_1 = __importDefault(require("@tryghost/errors"));
|
|
10
|
+
const UnsafeData_1 = require("./UnsafeData");
|
|
11
|
+
class Recommendation {
|
|
12
|
+
id;
|
|
13
|
+
title;
|
|
14
|
+
description;
|
|
15
|
+
excerpt; // Fetched from the site meta data
|
|
16
|
+
featuredImage; // Fetched from the site meta data
|
|
17
|
+
favicon; // Fetched from the site meta data
|
|
18
|
+
url;
|
|
19
|
+
oneClickSubscribe;
|
|
20
|
+
createdAt;
|
|
21
|
+
updatedAt;
|
|
22
|
+
#clickCount;
|
|
23
|
+
#subscriberCount;
|
|
24
|
+
#deleted;
|
|
25
|
+
get deleted() {
|
|
26
|
+
return this.#deleted;
|
|
27
|
+
}
|
|
28
|
+
get clickCount() {
|
|
29
|
+
return this.#clickCount;
|
|
30
|
+
}
|
|
31
|
+
get subscriberCount() {
|
|
32
|
+
return this.#subscriberCount;
|
|
33
|
+
}
|
|
34
|
+
constructor(data) {
|
|
35
|
+
this.id = data.id;
|
|
36
|
+
this.title = data.title;
|
|
37
|
+
this.description = data.description;
|
|
38
|
+
this.excerpt = data.excerpt;
|
|
39
|
+
this.featuredImage = data.featuredImage;
|
|
40
|
+
this.favicon = data.favicon;
|
|
41
|
+
this.url = data.url;
|
|
42
|
+
this.oneClickSubscribe = data.oneClickSubscribe;
|
|
43
|
+
this.createdAt = data.createdAt;
|
|
44
|
+
this.updatedAt = data.updatedAt;
|
|
45
|
+
this.#clickCount = data.clickCount;
|
|
46
|
+
this.#subscriberCount = data.subscriberCount;
|
|
47
|
+
this.#deleted = false;
|
|
48
|
+
}
|
|
49
|
+
static validate(properties) {
|
|
50
|
+
if (properties.title.length === 0) {
|
|
51
|
+
throw new errors_1.default.ValidationError({
|
|
52
|
+
message: 'Title must not be empty'
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (properties.title.length > 2000) {
|
|
56
|
+
throw new errors_1.default.ValidationError({
|
|
57
|
+
message: 'Title must be less than 2000 characters'
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (properties.description && properties.description.length > 200) {
|
|
61
|
+
throw new errors_1.default.ValidationError({
|
|
62
|
+
message: 'Description must be less than 200 characters'
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
clean() {
|
|
67
|
+
if (this.description !== null && this.description.length === 0) {
|
|
68
|
+
this.description = null;
|
|
69
|
+
}
|
|
70
|
+
if (this.excerpt !== null && this.excerpt.length === 0) {
|
|
71
|
+
this.excerpt = null;
|
|
72
|
+
}
|
|
73
|
+
if (this.excerpt !== null && this.excerpt.length > 2000) {
|
|
74
|
+
this.excerpt = this.excerpt.slice(0, 1997) + '...';
|
|
75
|
+
}
|
|
76
|
+
this.createdAt.setMilliseconds(0);
|
|
77
|
+
this.updatedAt?.setMilliseconds(0);
|
|
78
|
+
}
|
|
79
|
+
static create(data) {
|
|
80
|
+
const id = data.id ?? (0, bson_objectid_1.default)().toString();
|
|
81
|
+
const d = {
|
|
82
|
+
id,
|
|
83
|
+
title: data.title,
|
|
84
|
+
description: data.description,
|
|
85
|
+
excerpt: data.excerpt,
|
|
86
|
+
featuredImage: new UnsafeData_1.UnsafeData(data.featuredImage, { field: ['featuredImage'] }).nullable.url,
|
|
87
|
+
favicon: new UnsafeData_1.UnsafeData(data.favicon, { field: ['favicon'] }).nullable.url,
|
|
88
|
+
url: new UnsafeData_1.UnsafeData(data.url, { field: ['url'] }).url,
|
|
89
|
+
oneClickSubscribe: data.oneClickSubscribe,
|
|
90
|
+
createdAt: data.createdAt ?? new Date(),
|
|
91
|
+
updatedAt: data.updatedAt ?? null,
|
|
92
|
+
clickCount: data.clickCount,
|
|
93
|
+
subscriberCount: data.subscriberCount
|
|
94
|
+
};
|
|
95
|
+
this.validate(d);
|
|
96
|
+
const recommendation = new Recommendation(d);
|
|
97
|
+
recommendation.clean();
|
|
98
|
+
return recommendation;
|
|
99
|
+
}
|
|
100
|
+
get plain() {
|
|
101
|
+
return {
|
|
102
|
+
id: this.id,
|
|
103
|
+
title: this.title,
|
|
104
|
+
description: this.description,
|
|
105
|
+
excerpt: this.excerpt,
|
|
106
|
+
featuredImage: this.featuredImage,
|
|
107
|
+
favicon: this.favicon,
|
|
108
|
+
url: this.url,
|
|
109
|
+
oneClickSubscribe: this.oneClickSubscribe,
|
|
110
|
+
createdAt: this.createdAt,
|
|
111
|
+
updatedAt: this.updatedAt,
|
|
112
|
+
clickCount: this.clickCount,
|
|
113
|
+
subscriberCount: this.subscriberCount
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Change the specified properties. Properties that are set to undefined will not be changed
|
|
118
|
+
*/
|
|
119
|
+
edit(properties) {
|
|
120
|
+
// Delete undefined properties
|
|
121
|
+
const newProperties = this.plain;
|
|
122
|
+
let didChange = false;
|
|
123
|
+
for (const key of Object.keys(properties)) {
|
|
124
|
+
if (Object.prototype.hasOwnProperty.call(properties, key) && properties[key] !== undefined && properties[key] !== newProperties[key]) {
|
|
125
|
+
newProperties[key] = properties[key];
|
|
126
|
+
didChange = true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (!didChange) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
newProperties.updatedAt = new Date();
|
|
133
|
+
const created = Recommendation.create(newProperties);
|
|
134
|
+
Object.assign(this, created);
|
|
135
|
+
}
|
|
136
|
+
delete() {
|
|
137
|
+
this.#deleted = true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
exports.Recommendation = Recommendation;
|