ghost 5.115.1 → 5.116.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-api-framework-5.115.1.tgz → tryghost-api-framework-5.116.0.tgz} +0 -0
- package/components/tryghost-constants-5.116.0.tgz +0 -0
- package/components/tryghost-custom-fonts-5.116.0.tgz +0 -0
- package/components/{tryghost-custom-theme-settings-service-5.115.1.tgz → tryghost-custom-theme-settings-service-5.116.0.tgz} +0 -0
- package/components/tryghost-domain-events-5.116.0.tgz +0 -0
- package/components/tryghost-donations-5.116.0.tgz +0 -0
- package/components/tryghost-email-addresses-5.116.0.tgz +0 -0
- package/components/tryghost-email-service-5.116.0.tgz +0 -0
- package/components/tryghost-email-suppression-list-5.116.0.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.116.0.tgz +0 -0
- package/components/tryghost-i18n-5.116.0.tgz +0 -0
- package/components/tryghost-job-manager-5.116.0.tgz +0 -0
- package/components/tryghost-link-replacer-5.116.0.tgz +0 -0
- package/components/tryghost-magic-link-5.116.0.tgz +0 -0
- package/components/{tryghost-member-attribution-5.115.1.tgz → tryghost-member-attribution-5.116.0.tgz} +0 -0
- package/components/tryghost-member-events-5.116.0.tgz +0 -0
- package/components/tryghost-members-api-5.116.0.tgz +0 -0
- package/components/tryghost-members-csv-5.116.0.tgz +0 -0
- package/components/{tryghost-members-offers-5.115.1.tgz → tryghost-members-offers-5.116.0.tgz} +0 -0
- package/components/{tryghost-milestones-5.115.1.tgz → tryghost-milestones-5.116.0.tgz} +0 -0
- package/components/{tryghost-mw-error-handler-5.115.1.tgz → tryghost-mw-error-handler-5.116.0.tgz} +0 -0
- package/components/tryghost-mw-vhost-5.116.0.tgz +0 -0
- package/components/tryghost-post-events-5.116.0.tgz +0 -0
- package/components/{tryghost-post-revisions-5.115.1.tgz → tryghost-post-revisions-5.116.0.tgz} +0 -0
- package/components/tryghost-posts-service-5.116.0.tgz +0 -0
- package/components/{tryghost-prometheus-metrics-5.115.1.tgz → tryghost-prometheus-metrics-5.116.0.tgz} +0 -0
- package/components/tryghost-security-5.116.0.tgz +0 -0
- package/components/{tryghost-tiers-5.115.1.tgz → tryghost-tiers-5.116.0.tgz} +0 -0
- package/components/tryghost-webmentions-5.116.0.tgz +0 -0
- 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.8371443ef8f60db429d0.js +35 -0
- package/core/built/admin/assets/chunk.582.f90151775f2e53dd21d9.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-domain-events-5.115.1.tgz +0 -0
- package/components/tryghost-donations-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-post-events-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,281 @@
|
|
|
1
|
+
import errors from '@tryghost/errors';
|
|
2
|
+
import {InMemoryRepository} from '../../lib/InMemoryRepository';
|
|
3
|
+
import logging from '@tryghost/logging';
|
|
4
|
+
import tpl from '@tryghost/tpl';
|
|
5
|
+
|
|
6
|
+
import {IncludeOption, OrderOption} from './BookshelfRepository';
|
|
7
|
+
import {ClickEvent} from './ClickEvent';
|
|
8
|
+
import {AddRecommendation, Recommendation, RecommendationPlain} from './Recommendation';
|
|
9
|
+
import {RecommendationRepository} from './RecommendationRepository';
|
|
10
|
+
import {SubscribeEvent} from './SubscribeEvent';
|
|
11
|
+
import {WellknownService} from './WellknownService';
|
|
12
|
+
import {RecommendationMetadataService} from './RecommendationMetadataService';
|
|
13
|
+
|
|
14
|
+
type MentionSendingService = {
|
|
15
|
+
sendAll(options: {url: URL, links: URL[]}): Promise<void>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type RecommendationEnablerService = {
|
|
19
|
+
getSetting(): string,
|
|
20
|
+
setSetting(value: string): Promise<void>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const messages = {
|
|
24
|
+
notFound: 'Recommendation with id {id} not found'
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export class RecommendationService {
|
|
28
|
+
repository: RecommendationRepository;
|
|
29
|
+
clickEventRepository: InMemoryRepository<string, ClickEvent>;
|
|
30
|
+
subscribeEventRepository: InMemoryRepository<string, SubscribeEvent>;
|
|
31
|
+
|
|
32
|
+
wellknownService: WellknownService;
|
|
33
|
+
mentionSendingService: MentionSendingService;
|
|
34
|
+
recommendationEnablerService: RecommendationEnablerService;
|
|
35
|
+
recommendationMetadataService: RecommendationMetadataService;
|
|
36
|
+
|
|
37
|
+
constructor(deps: {
|
|
38
|
+
repository: RecommendationRepository,
|
|
39
|
+
clickEventRepository: InMemoryRepository<string, ClickEvent>,
|
|
40
|
+
subscribeEventRepository: InMemoryRepository<string, SubscribeEvent>,
|
|
41
|
+
wellknownService: WellknownService,
|
|
42
|
+
mentionSendingService: MentionSendingService,
|
|
43
|
+
recommendationEnablerService: RecommendationEnablerService,
|
|
44
|
+
recommendationMetadataService: RecommendationMetadataService
|
|
45
|
+
}) {
|
|
46
|
+
this.repository = deps.repository;
|
|
47
|
+
this.wellknownService = deps.wellknownService;
|
|
48
|
+
this.mentionSendingService = deps.mentionSendingService;
|
|
49
|
+
this.recommendationEnablerService = deps.recommendationEnablerService;
|
|
50
|
+
this.clickEventRepository = deps.clickEventRepository;
|
|
51
|
+
this.subscribeEventRepository = deps.subscribeEventRepository;
|
|
52
|
+
this.recommendationMetadataService = deps.recommendationMetadataService;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async init() {
|
|
56
|
+
const recommendations = await this.#listRecommendations();
|
|
57
|
+
await this.updateWellknown(recommendations);
|
|
58
|
+
|
|
59
|
+
// Do a slow update of all the recommendation metadata (keeping logo up to date, one-click-subscribe, etc.)
|
|
60
|
+
// We better move this to a job in the future
|
|
61
|
+
if (!process.env.NODE_ENV?.startsWith('test')) {
|
|
62
|
+
setTimeout(async () => {
|
|
63
|
+
try {
|
|
64
|
+
await this.updateAllRecommendationsMetadata();
|
|
65
|
+
} catch (e) {
|
|
66
|
+
logging.error('[Recommendations] Failed to update all recommendations metadata on boot', e);
|
|
67
|
+
}
|
|
68
|
+
}, 2 * 60 * 1000 + Math.random() * 5 * 60 * 1000);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async updateAllRecommendationsMetadata() {
|
|
73
|
+
const recommendations = await this.#listRecommendations();
|
|
74
|
+
logging.info('[Recommendations] Updating recommendations metadata');
|
|
75
|
+
for (const recommendation of recommendations) {
|
|
76
|
+
try {
|
|
77
|
+
await this._updateRecommendationMetadata(recommendation);
|
|
78
|
+
await this.repository.save(recommendation);
|
|
79
|
+
} catch (e) {
|
|
80
|
+
logging.error('[Recommendations] Failed to save updated metadata for recommendation ' + recommendation.url.toString(), e);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async updateWellknown(recommendations: Recommendation[]) {
|
|
86
|
+
await this.wellknownService.set(recommendations);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async updateRecommendationsEnabledSetting(recommendations: Recommendation[]) {
|
|
90
|
+
const expectedSetting = (recommendations.length > 0).toString();
|
|
91
|
+
const currentSetting = this.recommendationEnablerService.getSetting();
|
|
92
|
+
|
|
93
|
+
if (currentSetting && currentSetting === expectedSetting) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
await this.recommendationEnablerService.setSetting(expectedSetting);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private sendMentionToRecommendation(recommendation: Recommendation) {
|
|
101
|
+
this.mentionSendingService.sendAll({
|
|
102
|
+
url: this.wellknownService.getURL(),
|
|
103
|
+
links: [
|
|
104
|
+
recommendation.url
|
|
105
|
+
]
|
|
106
|
+
}).catch((err) => {
|
|
107
|
+
logging.error('Failed to send mention to recommendation', err);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async readRecommendation(id: string): Promise<RecommendationPlain> {
|
|
112
|
+
const recommendation = await this.repository.getById(id);
|
|
113
|
+
|
|
114
|
+
if (!recommendation) {
|
|
115
|
+
throw new errors.NotFoundError({
|
|
116
|
+
message: tpl(messages.notFound, {id})
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return recommendation.plain;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async addRecommendation(addRecommendation: AddRecommendation): Promise<RecommendationPlain> {
|
|
124
|
+
const recommendation = Recommendation.create(addRecommendation);
|
|
125
|
+
|
|
126
|
+
// If a recommendation with this URL already exists, throw an error
|
|
127
|
+
const existing = await this.repository.getByUrl(recommendation.url);
|
|
128
|
+
if (existing) {
|
|
129
|
+
throw new errors.ValidationError({
|
|
130
|
+
message: 'A recommendation with this URL already exists.'
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
await this.repository.save(recommendation);
|
|
135
|
+
|
|
136
|
+
const recommendations = await this.#listRecommendations();
|
|
137
|
+
await this.updateWellknown(recommendations);
|
|
138
|
+
await this.updateRecommendationsEnabledSetting(recommendations);
|
|
139
|
+
|
|
140
|
+
// Only send an update for the mentioned URL
|
|
141
|
+
this.sendMentionToRecommendation(recommendation);
|
|
142
|
+
return recommendation.plain;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async checkRecommendation(url: URL): Promise<Partial<RecommendationPlain>> {
|
|
146
|
+
// If a recommendation with this URL already exists, return it, but with updated metadata
|
|
147
|
+
const existing = await this.repository.getByUrl(url);
|
|
148
|
+
if (existing) {
|
|
149
|
+
this._updateRecommendationMetadata(existing);
|
|
150
|
+
await this.repository.save(existing);
|
|
151
|
+
return existing.plain;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let metadata;
|
|
155
|
+
try {
|
|
156
|
+
metadata = await this.recommendationMetadataService.fetch(url);
|
|
157
|
+
} catch (e) {
|
|
158
|
+
logging.error('[Recommendations] Failed to fetch metadata for url ' + url, e);
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
url: url,
|
|
162
|
+
title: undefined,
|
|
163
|
+
excerpt: undefined,
|
|
164
|
+
featuredImage: undefined,
|
|
165
|
+
favicon: undefined,
|
|
166
|
+
oneClickSubscribe: false
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
url: url,
|
|
172
|
+
title: metadata.title ?? undefined,
|
|
173
|
+
excerpt: metadata.excerpt ?? undefined,
|
|
174
|
+
featuredImage: metadata.featuredImage ?? undefined,
|
|
175
|
+
favicon: metadata.favicon ?? undefined,
|
|
176
|
+
oneClickSubscribe: !!metadata.oneClickSubscribe
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async _updateRecommendationMetadata(recommendation: Recommendation) {
|
|
181
|
+
// Fetch data
|
|
182
|
+
try {
|
|
183
|
+
const metadata = await this.recommendationMetadataService.fetch(recommendation.url);
|
|
184
|
+
|
|
185
|
+
// Set null values to undefined so we don't trigger an update
|
|
186
|
+
recommendation.edit({
|
|
187
|
+
// Don't set title if it's already set on the recommendation
|
|
188
|
+
title: recommendation.title ? undefined : (metadata.title ?? undefined),
|
|
189
|
+
excerpt: metadata.excerpt ?? undefined,
|
|
190
|
+
featuredImage: metadata.featuredImage ?? undefined,
|
|
191
|
+
favicon: metadata.favicon ?? undefined,
|
|
192
|
+
oneClickSubscribe: !!metadata.oneClickSubscribe
|
|
193
|
+
});
|
|
194
|
+
} catch (e) {
|
|
195
|
+
logging.error('[Recommendations] Failed to update metadata for recommendation ' + recommendation.url.toString(), e);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async editRecommendation(id: string, recommendationEdit: Partial<Recommendation>): Promise<RecommendationPlain> {
|
|
200
|
+
// Check if it exists
|
|
201
|
+
const existing = await this.repository.getById(id);
|
|
202
|
+
if (!existing) {
|
|
203
|
+
throw new errors.NotFoundError({
|
|
204
|
+
message: tpl(messages.notFound, {id})
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
existing.edit(recommendationEdit);
|
|
209
|
+
await this._updateRecommendationMetadata(existing);
|
|
210
|
+
await this.repository.save(existing);
|
|
211
|
+
|
|
212
|
+
const recommendations = await this.#listRecommendations();
|
|
213
|
+
await this.updateWellknown(recommendations);
|
|
214
|
+
|
|
215
|
+
this.sendMentionToRecommendation(existing);
|
|
216
|
+
return existing.plain;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async deleteRecommendation(id: string) {
|
|
220
|
+
const existing = await this.repository.getById(id);
|
|
221
|
+
if (!existing) {
|
|
222
|
+
throw new errors.NotFoundError({
|
|
223
|
+
message: tpl(messages.notFound, {id})
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
existing.delete();
|
|
228
|
+
await this.repository.save(existing);
|
|
229
|
+
|
|
230
|
+
const recommendations = await this.#listRecommendations();
|
|
231
|
+
await this.updateWellknown(recommendations);
|
|
232
|
+
await this.updateRecommendationsEnabledSetting(recommendations);
|
|
233
|
+
|
|
234
|
+
// Send a mention (because it was deleted, according to the webmentions spec)
|
|
235
|
+
this.sendMentionToRecommendation(existing);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Sames as listRecommendations, but returns Entities instead of plain objects (Entities are only used internally)
|
|
240
|
+
*/
|
|
241
|
+
async #listRecommendations(options: { filter?: string; order?: OrderOption<Recommendation>; page?: number; limit?: number|'all', include?: IncludeOption<Recommendation> } = {page: 1, limit: 'all'}): Promise<Recommendation[]> {
|
|
242
|
+
if (options.limit === 'all') {
|
|
243
|
+
return await this.repository.getAll({
|
|
244
|
+
...options
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
return await this.repository.getPage({
|
|
248
|
+
...options,
|
|
249
|
+
page: options.page || 1,
|
|
250
|
+
limit: options.limit || 15
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async listRecommendations(options: { filter?: string; order?: OrderOption<Recommendation>; page?: number; limit?: number|'all', include?: IncludeOption<Recommendation> } = {page: 1, limit: 'all', include: []}): Promise<RecommendationPlain[]> {
|
|
255
|
+
const list = await this.#listRecommendations(options);
|
|
256
|
+
return list.map(e => e.plain);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async countRecommendations({filter}: { filter?: string }) {
|
|
260
|
+
return await this.repository.getCount({filter});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async trackClicked({id, memberId}: { id: string, memberId?: string }) {
|
|
264
|
+
const clickEvent = ClickEvent.create({recommendationId: id, memberId});
|
|
265
|
+
await this.clickEventRepository.save(clickEvent);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async trackSubscribed({id, memberId}: { id: string, memberId: string }) {
|
|
269
|
+
const subscribeEvent = SubscribeEvent.create({recommendationId: id, memberId});
|
|
270
|
+
await this.subscribeEventRepository.save(subscribeEvent);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async readRecommendationByUrl(url: URL): Promise<RecommendationPlain|null> {
|
|
274
|
+
const recommendation = await this.repository.getByUrl(url);
|
|
275
|
+
|
|
276
|
+
if (!recommendation) {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
return recommendation.plain;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.SubscribeEvent = void 0;
|
|
7
|
+
const bson_objectid_1 = __importDefault(require("bson-objectid"));
|
|
8
|
+
class SubscribeEvent {
|
|
9
|
+
id;
|
|
10
|
+
recommendationId;
|
|
11
|
+
memberId;
|
|
12
|
+
createdAt;
|
|
13
|
+
get deleted() {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
constructor(data) {
|
|
17
|
+
this.id = data.id;
|
|
18
|
+
this.recommendationId = data.recommendationId;
|
|
19
|
+
this.memberId = data.memberId;
|
|
20
|
+
this.createdAt = data.createdAt;
|
|
21
|
+
}
|
|
22
|
+
static create(data) {
|
|
23
|
+
const id = data.id ?? (0, bson_objectid_1.default)().toString();
|
|
24
|
+
const d = {
|
|
25
|
+
id,
|
|
26
|
+
recommendationId: data.recommendationId,
|
|
27
|
+
memberId: data.memberId,
|
|
28
|
+
createdAt: data.createdAt ?? new Date()
|
|
29
|
+
};
|
|
30
|
+
return new SubscribeEvent(d);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
exports.SubscribeEvent = SubscribeEvent;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import ObjectId from 'bson-objectid';
|
|
2
|
+
|
|
3
|
+
export class SubscribeEvent {
|
|
4
|
+
id: string;
|
|
5
|
+
recommendationId: string;
|
|
6
|
+
memberId: string|null;
|
|
7
|
+
createdAt: Date;
|
|
8
|
+
|
|
9
|
+
get deleted() {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
private constructor(data: {id: string, recommendationId: string, memberId: string|null, createdAt: Date}) {
|
|
14
|
+
this.id = data.id;
|
|
15
|
+
this.recommendationId = data.recommendationId;
|
|
16
|
+
this.memberId = data.memberId;
|
|
17
|
+
this.createdAt = data.createdAt;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static create(data: {id?: string, recommendationId: string, memberId: string, createdAt?: Date}) {
|
|
21
|
+
const id = data.id ?? ObjectId().toString();
|
|
22
|
+
|
|
23
|
+
const d = {
|
|
24
|
+
id,
|
|
25
|
+
recommendationId: data.recommendationId,
|
|
26
|
+
memberId: data.memberId,
|
|
27
|
+
createdAt: data.createdAt ?? new Date()
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return new SubscribeEvent(d);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.UnsafeData = void 0;
|
|
7
|
+
const errors_1 = __importDefault(require("@tryghost/errors"));
|
|
8
|
+
function serializeField(field) {
|
|
9
|
+
if (field.length === 0) {
|
|
10
|
+
return 'data';
|
|
11
|
+
}
|
|
12
|
+
return field.join('.');
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* NOTE: should be moved to a separate package in case this pattern is found to be useful
|
|
16
|
+
*/
|
|
17
|
+
class UnsafeData {
|
|
18
|
+
data;
|
|
19
|
+
context;
|
|
20
|
+
constructor(data, context = {}) {
|
|
21
|
+
this.data = data;
|
|
22
|
+
this.context = context;
|
|
23
|
+
}
|
|
24
|
+
get field() {
|
|
25
|
+
return serializeField(this.context.field ?? []);
|
|
26
|
+
}
|
|
27
|
+
addKeyToField(key) {
|
|
28
|
+
return this.context.field ? [...this.context.field, key] : [key];
|
|
29
|
+
}
|
|
30
|
+
fieldWithKey(key) {
|
|
31
|
+
return serializeField(this.addKeyToField(key));
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Returns undefined if the key is not present on the object. Note that this doesn't check for null.
|
|
35
|
+
*/
|
|
36
|
+
optionalKey(key) {
|
|
37
|
+
if (typeof this.data !== 'object' || this.data === null) {
|
|
38
|
+
throw new errors_1.default.ValidationError({ message: `${this.field} must be an object` });
|
|
39
|
+
}
|
|
40
|
+
if (!Object.prototype.hasOwnProperty.call(this.data, key)) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
return new UnsafeData(this.data[key], {
|
|
44
|
+
field: this.addKeyToField(key)
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
key(key) {
|
|
48
|
+
if (typeof this.data !== 'object' || this.data === null) {
|
|
49
|
+
throw new errors_1.default.ValidationError({ message: `${this.field} must be an object` });
|
|
50
|
+
}
|
|
51
|
+
if (!Object.prototype.hasOwnProperty.call(this.data, key)) {
|
|
52
|
+
throw new errors_1.default.ValidationError({ message: `${this.fieldWithKey(key)} is required` });
|
|
53
|
+
}
|
|
54
|
+
return new UnsafeData(this.data[key], {
|
|
55
|
+
field: this.addKeyToField(key)
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Use this to get a nullable value:
|
|
60
|
+
* ```
|
|
61
|
+
* const url: string|null = data.key('url').nullable.string
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
get nullable() {
|
|
65
|
+
if (this.data === null) {
|
|
66
|
+
const d = {
|
|
67
|
+
get string() {
|
|
68
|
+
return null;
|
|
69
|
+
},
|
|
70
|
+
get boolean() {
|
|
71
|
+
return null;
|
|
72
|
+
},
|
|
73
|
+
get number() {
|
|
74
|
+
return null;
|
|
75
|
+
},
|
|
76
|
+
get integer() {
|
|
77
|
+
return null;
|
|
78
|
+
},
|
|
79
|
+
get url() {
|
|
80
|
+
return null;
|
|
81
|
+
},
|
|
82
|
+
enum() {
|
|
83
|
+
return null;
|
|
84
|
+
},
|
|
85
|
+
key() {
|
|
86
|
+
return d;
|
|
87
|
+
},
|
|
88
|
+
optionalKey() {
|
|
89
|
+
return d;
|
|
90
|
+
},
|
|
91
|
+
get array() {
|
|
92
|
+
return null;
|
|
93
|
+
},
|
|
94
|
+
index() {
|
|
95
|
+
return d;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
return d;
|
|
99
|
+
}
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
get string() {
|
|
103
|
+
if (typeof this.data !== 'string') {
|
|
104
|
+
throw new errors_1.default.ValidationError({ message: `${this.field} must be a string` });
|
|
105
|
+
}
|
|
106
|
+
return this.data;
|
|
107
|
+
}
|
|
108
|
+
get boolean() {
|
|
109
|
+
if (typeof this.data !== 'boolean') {
|
|
110
|
+
throw new errors_1.default.ValidationError({ message: `${this.field} must be a boolean` });
|
|
111
|
+
}
|
|
112
|
+
return this.data;
|
|
113
|
+
}
|
|
114
|
+
get number() {
|
|
115
|
+
if (typeof this.data === 'string') {
|
|
116
|
+
const parsed = parseFloat(this.data);
|
|
117
|
+
if (isNaN(parsed) || parsed.toString() !== this.data) {
|
|
118
|
+
throw new errors_1.default.ValidationError({ message: `${this.field} must be a number, got ${typeof this.data}` });
|
|
119
|
+
}
|
|
120
|
+
return new UnsafeData(parsed, this.context).number;
|
|
121
|
+
}
|
|
122
|
+
if (typeof this.data !== 'number') {
|
|
123
|
+
throw new errors_1.default.ValidationError({ message: `${this.field} must be a number, got ${typeof this.data}` });
|
|
124
|
+
}
|
|
125
|
+
if (Number.isNaN(this.data) || !Number.isFinite(this.data)) {
|
|
126
|
+
throw new errors_1.default.ValidationError({ message: `${this.field} must be a finite number` });
|
|
127
|
+
}
|
|
128
|
+
return this.data;
|
|
129
|
+
}
|
|
130
|
+
get integer() {
|
|
131
|
+
if (typeof this.data === 'string') {
|
|
132
|
+
const parsed = parseInt(this.data);
|
|
133
|
+
if (isNaN(parsed) || parsed.toString() !== this.data) {
|
|
134
|
+
throw new errors_1.default.ValidationError({ message: `${this.field} must be an integer` });
|
|
135
|
+
}
|
|
136
|
+
return new UnsafeData(parseInt(this.data), this.context).integer;
|
|
137
|
+
}
|
|
138
|
+
const number = this.number;
|
|
139
|
+
if (!Number.isSafeInteger(number)) {
|
|
140
|
+
throw new errors_1.default.ValidationError({ message: `${this.field} must be an integer` });
|
|
141
|
+
}
|
|
142
|
+
return number;
|
|
143
|
+
}
|
|
144
|
+
get url() {
|
|
145
|
+
if (this.data instanceof URL) {
|
|
146
|
+
return this.data;
|
|
147
|
+
}
|
|
148
|
+
const string = this.string;
|
|
149
|
+
try {
|
|
150
|
+
const url = new URL(string);
|
|
151
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
152
|
+
throw new errors_1.default.ValidationError({ message: `${this.field} must be a valid URL` });
|
|
153
|
+
}
|
|
154
|
+
return url;
|
|
155
|
+
}
|
|
156
|
+
catch (e) {
|
|
157
|
+
throw new errors_1.default.ValidationError({ message: `${this.field} must be a valid URL` });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
enum(allowedValues) {
|
|
161
|
+
if (!allowedValues.includes(this.data)) {
|
|
162
|
+
throw new errors_1.default.ValidationError({ message: `${this.field} must be one of ${allowedValues.join(', ')}` });
|
|
163
|
+
}
|
|
164
|
+
return this.data;
|
|
165
|
+
}
|
|
166
|
+
get array() {
|
|
167
|
+
if (!Array.isArray(this.data)) {
|
|
168
|
+
throw new errors_1.default.ValidationError({ message: `${this.field} must be an array` });
|
|
169
|
+
}
|
|
170
|
+
return this.data.map((d, i) => new UnsafeData(d, { field: this.addKeyToField(`${i}`) }));
|
|
171
|
+
}
|
|
172
|
+
index(index) {
|
|
173
|
+
const arr = this.array;
|
|
174
|
+
if (index < 0 || !Number.isSafeInteger(index)) {
|
|
175
|
+
throw new errors_1.default.IncorrectUsageError({ message: `index must be a positive integer` });
|
|
176
|
+
}
|
|
177
|
+
if (index >= arr.length) {
|
|
178
|
+
throw new errors_1.default.ValidationError({ message: `${this.field} must be an array of length ${index + 1}` });
|
|
179
|
+
}
|
|
180
|
+
return arr[index];
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
exports.UnsafeData = UnsafeData;
|