ghost 5.115.0 → 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.
Files changed (341) hide show
  1. package/components/{tryghost-api-framework-5.115.0.tgz → tryghost-api-framework-5.116.0.tgz} +0 -0
  2. package/components/tryghost-constants-5.116.0.tgz +0 -0
  3. package/components/tryghost-custom-fonts-5.116.0.tgz +0 -0
  4. package/components/{tryghost-custom-theme-settings-service-5.115.0.tgz → tryghost-custom-theme-settings-service-5.116.0.tgz} +0 -0
  5. package/components/tryghost-domain-events-5.116.0.tgz +0 -0
  6. package/components/{tryghost-donations-5.115.0.tgz → tryghost-donations-5.116.0.tgz} +0 -0
  7. package/components/tryghost-email-addresses-5.116.0.tgz +0 -0
  8. package/components/tryghost-email-service-5.116.0.tgz +0 -0
  9. package/components/tryghost-email-suppression-list-5.116.0.tgz +0 -0
  10. package/components/tryghost-html-to-plaintext-5.116.0.tgz +0 -0
  11. package/components/tryghost-i18n-5.116.0.tgz +0 -0
  12. package/components/tryghost-job-manager-5.116.0.tgz +0 -0
  13. package/components/tryghost-link-replacer-5.116.0.tgz +0 -0
  14. package/components/tryghost-magic-link-5.116.0.tgz +0 -0
  15. package/components/tryghost-member-attribution-5.116.0.tgz +0 -0
  16. package/components/tryghost-member-events-5.116.0.tgz +0 -0
  17. package/components/tryghost-members-api-5.116.0.tgz +0 -0
  18. package/components/tryghost-members-csv-5.116.0.tgz +0 -0
  19. package/components/{tryghost-members-offers-5.115.0.tgz → tryghost-members-offers-5.116.0.tgz} +0 -0
  20. package/components/{tryghost-milestones-5.115.0.tgz → tryghost-milestones-5.116.0.tgz} +0 -0
  21. package/components/{tryghost-mw-error-handler-5.115.0.tgz → tryghost-mw-error-handler-5.116.0.tgz} +0 -0
  22. package/components/tryghost-mw-vhost-5.116.0.tgz +0 -0
  23. package/components/{tryghost-post-events-5.115.0.tgz → tryghost-post-events-5.116.0.tgz} +0 -0
  24. package/components/{tryghost-post-revisions-5.115.0.tgz → tryghost-post-revisions-5.116.0.tgz} +0 -0
  25. package/components/tryghost-posts-service-5.116.0.tgz +0 -0
  26. package/components/{tryghost-prometheus-metrics-5.115.0.tgz → tryghost-prometheus-metrics-5.116.0.tgz} +0 -0
  27. package/components/tryghost-security-5.116.0.tgz +0 -0
  28. package/components/{tryghost-tiers-5.115.0.tgz → tryghost-tiers-5.116.0.tgz} +0 -0
  29. package/components/tryghost-webmentions-5.116.0.tgz +0 -0
  30. package/content/themes/casper/LICENSE +1 -1
  31. package/content/themes/casper/README.md +1 -1
  32. package/content/themes/source/LICENSE +1 -1
  33. package/content/themes/source/README.md +1 -1
  34. package/content/themes/source/assets/built/screen.css +1 -1
  35. package/content/themes/source/assets/built/screen.css.map +1 -1
  36. package/content/themes/source/assets/css/screen.css +11 -6
  37. package/content/themes/source/partials/feature-image.hbs +2 -2
  38. package/core/boot.js +3 -43
  39. package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +30494 -29403
  40. package/core/built/admin/assets/admin-x-demo/admin-x-demo.js +1 -1
  41. package/core/built/admin/assets/admin-x-demo/{index-0040480a.mjs → index-a9601514.mjs} +5 -4
  42. package/core/built/admin/assets/admin-x-demo/{modals-fb35c86c.mjs → modals-c1789d04.mjs} +67 -65
  43. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-806ef39c.mjs → CodeEditorView-e9c9deb8.mjs} +2 -2
  44. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +2 -2
  45. package/core/built/admin/assets/admin-x-settings/{index-376f847c.mjs → index-84580c3a.mjs} +2 -2
  46. package/core/built/admin/assets/admin-x-settings/{index-8fa19303.mjs → index-f744cab7.mjs} +3147 -3123
  47. package/core/built/admin/assets/admin-x-settings/{modals-36775d71.mjs → modals-d9ca60c5.mjs} +1198 -1192
  48. package/core/built/admin/assets/chunk.524.8371443ef8f60db429d0.js +35 -0
  49. package/core/built/admin/assets/chunk.582.f90151775f2e53dd21d9.js +37 -0
  50. package/core/built/admin/assets/{chunk.874.461cb3cf5b6b36915f8c.js → chunk.713.e9027c0cc3c56110f5da.js} +125 -98
  51. package/core/built/admin/assets/{ghost-938b3d9c29e3564a53a22f8c8f82d351.js → ghost-03b64c086f3c60cabc85fe7a7e2b640a.js} +272 -251
  52. package/core/built/admin/assets/ghost-ba58e9822f7384461e926c7e23f04a75.css +1 -0
  53. package/core/built/admin/assets/ghost-dark-f1f29683b14ffa11615b3bba8b6ab92c.css +1 -0
  54. package/core/built/admin/assets/koenig-lexical/index.css +1 -1
  55. package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +20563 -20891
  56. package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +139 -139
  57. package/core/built/admin/assets/posts/posts.js +5732 -5667
  58. package/core/built/admin/assets/stats/stats.js +75373 -0
  59. package/core/built/admin/assets/{vendor-68a4aa424a179a90f5bbc2b750def576.js → vendor-72026232b36d97babc6320917c16c321.js} +36 -34
  60. package/core/built/admin/index.html +6 -6
  61. package/core/cli/generate-data.js +1 -1
  62. package/core/frontend/helpers/ghost_head.js +8 -1
  63. package/core/frontend/public/ghost-stats.js +55 -2
  64. package/core/frontend/services/assets-minification/AdminAuthAssets.js +2 -1
  65. package/core/frontend/services/assets-minification/CardAssets.js +1 -1
  66. package/core/frontend/services/assets-minification/CommentCountsAssets.js +1 -1
  67. package/core/frontend/services/assets-minification/MemberAttributionAssets.js +1 -1
  68. package/core/frontend/services/assets-minification/Minifier.js +191 -0
  69. package/core/frontend/services/routing/controllers/previews.js +2 -1
  70. package/core/frontend/src/cards/css/cta.css +1 -1
  71. package/core/server/adapters/cache/Redis.js +1 -1
  72. package/core/server/adapters/lib/redis/AdapterCacheRedis.js +287 -0
  73. package/core/server/adapters/lib/redis/redis-store-factory.js +22 -0
  74. package/core/server/api/endpoints/posts.js +9 -3
  75. package/core/server/api/endpoints/previews.js +35 -1
  76. package/core/server/api/endpoints/slugs.js +6 -2
  77. package/core/server/api/endpoints/utils/serializers/output/utils/post-gating.js +6 -9
  78. package/core/server/api/endpoints/utils/validators/input/settings.js +1 -1
  79. package/core/server/data/db/connection.js +2 -0
  80. package/core/server/data/db/index.js +1 -0
  81. package/core/server/data/importer/handlers/ImporterContentFileHandler.js +90 -0
  82. package/core/server/data/importer/import-manager.js +3 -3
  83. package/core/server/data/importer/importers/importer-revue.js +128 -0
  84. package/core/server/data/importer/importers/json-to-html.js +107 -0
  85. package/core/server/data/migrations/utils/tables.js +2 -4
  86. package/core/server/data/seeders/DataGenerator.js +288 -0
  87. package/core/server/data/seeders/importers/BenefitsImporter.js +28 -0
  88. package/core/server/data/seeders/importers/CommentsImporter.js +73 -0
  89. package/core/server/data/seeders/importers/EmailBatchesImporter.js +38 -0
  90. package/core/server/data/seeders/importers/EmailRecipientFailuresImporter.js +67 -0
  91. package/core/server/data/seeders/importers/EmailRecipientsImporter.js +212 -0
  92. package/core/server/data/seeders/importers/EmailsImporter.js +99 -0
  93. package/core/server/data/seeders/importers/LabelsImporter.js +41 -0
  94. package/core/server/data/seeders/importers/MembersClickEventsImporter.js +69 -0
  95. package/core/server/data/seeders/importers/MembersCreatedEventsImporter.js +103 -0
  96. package/core/server/data/seeders/importers/MembersFeedbackImporter.js +45 -0
  97. package/core/server/data/seeders/importers/MembersImporter.js +111 -0
  98. package/core/server/data/seeders/importers/MembersLabelsImporter.js +39 -0
  99. package/core/server/data/seeders/importers/MembersLoginEventsImporter.js +69 -0
  100. package/core/server/data/seeders/importers/MembersNewslettersImporter.js +38 -0
  101. package/core/server/data/seeders/importers/MembersPaidSubscriptionEventsImporter.js +99 -0
  102. package/core/server/data/seeders/importers/MembersProductsImporter.js +42 -0
  103. package/core/server/data/seeders/importers/MembersStatusEventsImporter.js +58 -0
  104. package/core/server/data/seeders/importers/MembersStripeCustomersImporter.js +60 -0
  105. package/core/server/data/seeders/importers/MembersStripeCustomersSubscriptionsImporter.js +259 -0
  106. package/core/server/data/seeders/importers/MembersSubscribeEventsImporter.js +69 -0
  107. package/core/server/data/seeders/importers/MembersSubscriptionCreatedEventsImporter.js +95 -0
  108. package/core/server/data/seeders/importers/NewslettersImporter.js +40 -0
  109. package/core/server/data/seeders/importers/OffersImporter.js +70 -0
  110. package/core/server/data/seeders/importers/PostsAuthorsImporter.js +32 -0
  111. package/core/server/data/seeders/importers/PostsImporter.js +102 -0
  112. package/core/server/data/seeders/importers/PostsProductsImporter.js +35 -0
  113. package/core/server/data/seeders/importers/PostsTagsImporter.js +46 -0
  114. package/core/server/data/seeders/importers/ProductsBenefitsImporter.js +54 -0
  115. package/core/server/data/seeders/importers/ProductsImporter.js +90 -0
  116. package/core/server/data/seeders/importers/RecommendationClickEventsImporter.js +32 -0
  117. package/core/server/data/seeders/importers/RecommendationSubscribeEventsImporter.js +32 -0
  118. package/core/server/data/seeders/importers/RecommendationsImporter.js +34 -0
  119. package/core/server/data/seeders/importers/RedirectsImporter.js +49 -0
  120. package/core/server/data/seeders/importers/RolesUsersImporter.js +42 -0
  121. package/core/server/data/seeders/importers/StripePricesImporter.js +69 -0
  122. package/core/server/data/seeders/importers/StripeProductsImporter.js +34 -0
  123. package/core/server/data/seeders/importers/TableImporter.js +187 -0
  124. package/core/server/data/seeders/importers/TagsImporter.js +41 -0
  125. package/core/server/data/seeders/importers/UsersImporter.js +31 -0
  126. package/core/server/data/seeders/importers/WebMentionsImporter.js +42 -0
  127. package/core/server/data/seeders/importers/index.js +41 -0
  128. package/core/server/data/seeders/utils/JsonImporter.js +39 -0
  129. package/core/server/data/seeders/utils/blog-info.js +3 -0
  130. package/core/server/data/seeders/utils/database-date.js +7 -0
  131. package/core/server/data/seeders/utils/event-generator.js +48 -0
  132. package/core/server/data/seeders/utils/random.js +13 -0
  133. package/core/server/data/seeders/utils/topological-sort.js +33 -0
  134. package/core/server/lib/bootstrap-socket.js +87 -0
  135. package/core/server/lib/package-json/index.js +1 -0
  136. package/core/server/lib/package-json/package-json.js +160 -0
  137. package/core/server/lib/package-json/parse.js +57 -0
  138. package/core/server/models/base/plugins/actions.js +44 -31
  139. package/core/server/models/base/plugins/generate-slug.js +6 -0
  140. package/core/server/notify.js +1 -1
  141. package/core/server/services/activitypub/ActivityPubService.ts +1 -1
  142. package/core/server/services/adapter-manager/AdapterManager.js +161 -0
  143. package/core/server/services/adapter-manager/index.js +1 -1
  144. package/core/server/services/announcement-bar-service/AnnouncementBarSettings.js +54 -0
  145. package/core/server/services/announcement-bar-service/AnnouncementVisibilityValues.js +11 -0
  146. package/core/server/services/announcement-bar-service/index.js +1 -1
  147. package/core/server/services/api-version-compatibility/APIVersionCompatibilityService.js +99 -0
  148. package/core/server/services/api-version-compatibility/VersionNotificationsDataService.js +80 -0
  149. package/core/server/services/api-version-compatibility/extract-api-key.js +57 -0
  150. package/core/server/services/api-version-compatibility/index.js +2 -2
  151. package/core/server/services/api-version-compatibility/mw-api-version-mismatch.js +31 -0
  152. package/core/server/services/audience-feedback/AudienceFeedbackController.js +85 -0
  153. package/core/server/services/audience-feedback/AudienceFeedbackService.js +34 -0
  154. package/core/server/services/audience-feedback/Feedback.js +35 -0
  155. package/core/server/services/audience-feedback/index.js +4 -2
  156. package/core/server/services/auth/session/emails/signin.js +168 -0
  157. package/core/server/services/auth/session/index.js +2 -2
  158. package/core/server/services/auth/session/session-from-token.js +69 -0
  159. package/core/server/services/auth/session/session-service.js +374 -0
  160. package/core/server/services/custom-redirects/index.js +1 -1
  161. package/core/server/services/email-analytics/EmailAnalyticsProviderMailgun.js +62 -0
  162. package/core/server/services/email-analytics/EmailAnalyticsService.js +552 -0
  163. package/core/server/services/email-analytics/EmailAnalyticsServiceWrapper.js +3 -3
  164. package/core/server/services/email-analytics/EventProcessingResult.js +66 -0
  165. package/core/server/services/email-service/EmailServiceWrapper.js +4 -4
  166. package/core/server/services/email-suppression-list/MailgunEmailSuppressionList.js +1 -1
  167. package/core/server/services/email-suppression-list/service.js +1 -1
  168. package/core/server/services/explore-ping/ExplorePingService.js +106 -0
  169. package/core/server/services/explore-ping/index.js +31 -0
  170. package/core/server/services/identity-tokens/IdentityTokenService.js +30 -0
  171. package/core/server/services/identity-tokens/IdentityTokenService.ts +28 -0
  172. package/core/server/services/identity-tokens/IdentityTokenServiceWrapper.js +1 -1
  173. package/core/server/services/invitations/accept.js +5 -2
  174. package/core/server/services/lib/DynamicRedirectManager.js +156 -0
  175. package/core/server/services/lib/EmailContentGenerator.js +54 -0
  176. package/core/server/services/lib/InMemoryRepository.js +62 -0
  177. package/core/server/services/lib/InMemoryRepository.ts +80 -0
  178. package/core/server/services/lib/MailgunClient.js +364 -0
  179. package/core/server/services/link-redirection/LinkRedirect.js +26 -0
  180. package/core/server/services/link-redirection/LinkRedirectRepository.js +7 -7
  181. package/core/server/services/link-redirection/LinkRedirectsService.js +123 -0
  182. package/core/server/services/link-redirection/README.md +151 -0
  183. package/core/server/services/link-redirection/RedirectEvent.js +24 -0
  184. package/core/server/services/link-redirection/index.js +1 -1
  185. package/core/server/services/link-tracking/LinkClickTrackingService.js +1 -1
  186. package/core/server/services/mail/index.js +1 -1
  187. package/core/server/services/mail-events/BookshelfMailEventRepository.js +2 -2
  188. package/core/server/services/mail-events/InMemoryMailEventRepository.js +10 -0
  189. package/core/server/services/mail-events/InMemoryMailEventRepository.ts +8 -0
  190. package/core/server/services/mail-events/MailEvent.js +20 -0
  191. package/core/server/services/mail-events/MailEvent.ts +10 -0
  192. package/core/server/services/mail-events/MailEventRepository.js +2 -0
  193. package/core/server/services/mail-events/MailEventRepository.ts +5 -0
  194. package/core/server/services/mail-events/MailEventService.js +124 -0
  195. package/core/server/services/mail-events/MailEventService.ts +169 -0
  196. package/core/server/services/mail-events/index.js +1 -1
  197. package/core/server/services/mail-events/libraries.d.ts +2 -0
  198. package/core/server/services/members/CaptchaService.js +80 -0
  199. package/core/server/services/members/api.js +1 -1
  200. package/core/server/services/members/importer/MembersCSVImporter.js +464 -0
  201. package/core/server/services/members/importer/MembersCSVImporterStripeUtils.js +194 -0
  202. package/core/server/services/members/importer/email-template.js +182 -0
  203. package/core/server/services/members/importer/index.js +30 -0
  204. package/core/server/services/members/members-ssr.js +333 -0
  205. package/core/server/services/members/service.js +2 -2
  206. package/core/server/services/members-events/LastSeenAtUpdater.js +1 -1
  207. package/core/server/services/offers/service.js +1 -1
  208. package/core/server/services/posts/stats/PostStats.js +13 -0
  209. package/core/server/services/recommendations/RecommendationServiceWrapper.js +8 -8
  210. package/core/server/services/recommendations/service/BookshelfClickEventRepository.js +48 -0
  211. package/core/server/services/recommendations/service/BookshelfClickEventRepository.ts +49 -0
  212. package/core/server/services/recommendations/service/BookshelfRecommendationRepository.js +98 -0
  213. package/core/server/services/recommendations/service/BookshelfRecommendationRepository.ts +117 -0
  214. package/core/server/services/recommendations/service/BookshelfRepository.js +134 -0
  215. package/core/server/services/recommendations/service/BookshelfRepository.ts +196 -0
  216. package/core/server/services/recommendations/service/BookshelfSubscribeEventRepository.js +48 -0
  217. package/core/server/services/recommendations/service/BookshelfSubscribeEventRepository.ts +49 -0
  218. package/core/server/services/recommendations/service/ClickEvent.js +33 -0
  219. package/core/server/services/recommendations/service/ClickEvent.ts +32 -0
  220. package/core/server/services/recommendations/service/InMemoryRecommendationRepository.js +19 -0
  221. package/core/server/services/recommendations/service/InMemoryRecommendationRepository.ts +20 -0
  222. package/core/server/services/recommendations/service/IncomingRecommendationController.js +34 -0
  223. package/core/server/services/recommendations/service/IncomingRecommendationController.ts +51 -0
  224. package/core/server/services/recommendations/service/IncomingRecommendationEmailRenderer.js +25 -0
  225. package/core/server/services/recommendations/service/IncomingRecommendationEmailRenderer.ts +37 -0
  226. package/core/server/services/recommendations/service/IncomingRecommendationService.js +93 -0
  227. package/core/server/services/recommendations/service/IncomingRecommendationService.ts +160 -0
  228. package/core/server/services/recommendations/service/Recommendation.js +140 -0
  229. package/core/server/services/recommendations/service/Recommendation.ts +201 -0
  230. package/core/server/services/recommendations/service/RecommendationController.js +208 -0
  231. package/core/server/services/recommendations/service/RecommendationController.ts +258 -0
  232. package/core/server/services/recommendations/service/RecommendationMetadataService.js +86 -0
  233. package/core/server/services/recommendations/service/RecommendationMetadataService.ts +128 -0
  234. package/core/server/services/recommendations/service/RecommendationRepository.js +2 -0
  235. package/core/server/services/recommendations/service/RecommendationRepository.ts +13 -0
  236. package/core/server/services/recommendations/service/RecommendationService.js +228 -0
  237. package/core/server/services/recommendations/service/RecommendationService.ts +281 -0
  238. package/core/server/services/recommendations/service/SubscribeEvent.js +33 -0
  239. package/core/server/services/recommendations/service/SubscribeEvent.ts +32 -0
  240. package/core/server/services/recommendations/service/UnsafeData.js +183 -0
  241. package/core/server/services/recommendations/service/UnsafeData.ts +217 -0
  242. package/core/server/services/recommendations/service/WellknownService.js +36 -0
  243. package/core/server/services/recommendations/service/WellknownService.ts +47 -0
  244. package/core/server/services/recommendations/service/index.js +31 -0
  245. package/core/server/services/recommendations/service/index.ts +15 -0
  246. package/core/server/services/recommendations/service/libraries.d.ts +5 -0
  247. package/core/server/services/route-settings/SettingsPathManager.js +47 -0
  248. package/core/server/services/route-settings/index.js +1 -1
  249. package/core/server/services/slack-notifications/SlackNotifications.js +211 -0
  250. package/core/server/services/slack-notifications/SlackNotificationsService.js +90 -0
  251. package/core/server/services/slack-notifications/service.js +4 -6
  252. package/core/server/services/stripe/README.md +63 -0
  253. package/core/server/services/stripe/StripeAPI.js +931 -0
  254. package/core/server/services/stripe/StripeMigrations.js +613 -0
  255. package/core/server/services/stripe/StripeService.js +175 -0
  256. package/core/server/services/stripe/WebhookController.js +100 -0
  257. package/core/server/services/stripe/WebhookManager.js +175 -0
  258. package/core/server/services/stripe/events/StripeLiveDisabledEvent.js +23 -0
  259. package/core/server/services/stripe/events/StripeLiveEnabledEvent.js +23 -0
  260. package/core/server/services/stripe/events/index.js +4 -0
  261. package/core/server/services/stripe/service.js +1 -1
  262. package/core/server/services/stripe/services/webhook/CheckoutSessionEventService.js +255 -0
  263. package/core/server/services/stripe/services/webhook/InvoiceEventService.js +70 -0
  264. package/core/server/services/stripe/services/webhook/SubscriptionEventService.js +54 -0
  265. package/core/server/services/themes/loader.js +1 -1
  266. package/core/server/services/themes/to-json.js +1 -1
  267. package/core/server/web/api/endpoints/admin/app.js +1 -21
  268. package/core/server/web/api/endpoints/admin/routes.js +1 -0
  269. package/core/server/web/api/middleware/version-match.js +41 -0
  270. package/core/server/web/shared/middleware/cache-control.js +51 -0
  271. package/core/server/web/shared/middleware/index.js +1 -1
  272. package/core/server/web/well-known.js +1 -1
  273. package/core/shared/labs.js +5 -3
  274. package/core/shared/settings-cache/CacheManager.js +64 -6
  275. package/package.json +98 -146
  276. package/tsconfig.tsbuildinfo +1 -1
  277. package/yarn.lock +1478 -1634
  278. package/components/tryghost-adapter-cache-redis-5.115.0.tgz +0 -0
  279. package/components/tryghost-adapter-manager-5.115.0.tgz +0 -0
  280. package/components/tryghost-announcement-bar-settings-5.115.0.tgz +0 -0
  281. package/components/tryghost-api-version-compatibility-service-5.115.0.tgz +0 -0
  282. package/components/tryghost-audience-feedback-5.115.0.tgz +0 -0
  283. package/components/tryghost-bookshelf-repository-5.115.0.tgz +0 -0
  284. package/components/tryghost-bootstrap-socket-5.115.0.tgz +0 -0
  285. package/components/tryghost-captcha-service-5.115.0.tgz +0 -0
  286. package/components/tryghost-constants-5.115.0.tgz +0 -0
  287. package/components/tryghost-custom-fonts-5.115.0.tgz +0 -0
  288. package/components/tryghost-data-generator-5.115.0.tgz +0 -0
  289. package/components/tryghost-domain-events-5.115.0.tgz +0 -0
  290. package/components/tryghost-email-addresses-5.115.0.tgz +0 -0
  291. package/components/tryghost-email-analytics-provider-mailgun-5.115.0.tgz +0 -0
  292. package/components/tryghost-email-analytics-service-5.115.0.tgz +0 -0
  293. package/components/tryghost-email-content-generator-5.115.0.tgz +0 -0
  294. package/components/tryghost-email-events-5.115.0.tgz +0 -0
  295. package/components/tryghost-email-service-5.115.0.tgz +0 -0
  296. package/components/tryghost-email-suppression-list-5.115.0.tgz +0 -0
  297. package/components/tryghost-express-dynamic-redirects-5.115.0.tgz +0 -0
  298. package/components/tryghost-extract-api-key-5.115.0.tgz +0 -0
  299. package/components/tryghost-ghost-5.115.0.tgz +0 -0
  300. package/components/tryghost-html-to-plaintext-5.115.0.tgz +0 -0
  301. package/components/tryghost-i18n-5.115.0.tgz +0 -0
  302. package/components/tryghost-identity-token-service-5.115.0.tgz +0 -0
  303. package/components/tryghost-importer-handler-content-files-5.115.0.tgz +0 -0
  304. package/components/tryghost-importer-revue-5.115.0.tgz +0 -0
  305. package/components/tryghost-in-memory-repository-5.115.0.tgz +0 -0
  306. package/components/tryghost-job-manager-5.115.0.tgz +0 -0
  307. package/components/tryghost-link-redirects-5.115.0.tgz +0 -0
  308. package/components/tryghost-link-replacer-5.115.0.tgz +0 -0
  309. package/components/tryghost-magic-link-5.115.0.tgz +0 -0
  310. package/components/tryghost-mail-events-5.115.0.tgz +0 -0
  311. package/components/tryghost-mailgun-client-5.115.0.tgz +0 -0
  312. package/components/tryghost-member-attribution-5.115.0.tgz +0 -0
  313. package/components/tryghost-member-events-5.115.0.tgz +0 -0
  314. package/components/tryghost-members-api-5.115.0.tgz +0 -0
  315. package/components/tryghost-members-csv-5.115.0.tgz +0 -0
  316. package/components/tryghost-members-importer-5.115.0.tgz +0 -0
  317. package/components/tryghost-members-payments-5.115.0.tgz +0 -0
  318. package/components/tryghost-members-ssr-5.115.0.tgz +0 -0
  319. package/components/tryghost-members-stripe-service-5.115.0.tgz +0 -0
  320. package/components/tryghost-minifier-5.115.0.tgz +0 -0
  321. package/components/tryghost-mw-api-version-mismatch-5.115.0.tgz +0 -0
  322. package/components/tryghost-mw-cache-control-5.115.0.tgz +0 -0
  323. package/components/tryghost-mw-session-from-token-5.115.0.tgz +0 -0
  324. package/components/tryghost-mw-update-user-last-seen-5.115.0.tgz +0 -0
  325. package/components/tryghost-mw-version-match-5.115.0.tgz +0 -0
  326. package/components/tryghost-mw-vhost-5.115.0.tgz +0 -0
  327. package/components/tryghost-package-json-5.115.0.tgz +0 -0
  328. package/components/tryghost-posts-service-5.115.0.tgz +0 -0
  329. package/components/tryghost-recommendations-5.115.0.tgz +0 -0
  330. package/components/tryghost-referrers-5.115.0.tgz +0 -0
  331. package/components/tryghost-security-5.115.0.tgz +0 -0
  332. package/components/tryghost-session-service-5.115.0.tgz +0 -0
  333. package/components/tryghost-settings-path-manager-5.115.0.tgz +0 -0
  334. package/components/tryghost-slack-notifications-5.115.0.tgz +0 -0
  335. package/components/tryghost-version-notifications-data-service-5.115.0.tgz +0 -0
  336. package/components/tryghost-webmentions-5.115.0.tgz +0 -0
  337. package/core/built/admin/assets/chunk.524.31419fdf6fb3859ecc1e.js +0 -35
  338. package/core/built/admin/assets/chunk.582.08c816d5e4ab766486a7.js +0 -37
  339. package/core/built/admin/assets/ghost-c2a7c4a1b76550c4219adb2ed4124ce0.css +0 -1
  340. package/core/built/admin/assets/ghost-dark-f91e4a479c6d38d94d5d1b14727871dc.css +0 -1
  341. /package/core/built/admin/assets/{chunk.874.461cb3cf5b6b36915f8c.js.LICENSE.txt → chunk.713.e9027c0cc3c56110f5da.js.LICENSE.txt} +0 -0
@@ -0,0 +1,49 @@
1
+ import {BookshelfRepository, ModelClass, ModelInstance} from './BookshelfRepository';
2
+ import logger from '@tryghost/logging';
3
+ import {SubscribeEvent} from './SubscribeEvent';
4
+
5
+ type Sentry = {
6
+ captureException(err: unknown): void;
7
+ }
8
+
9
+ export class BookshelfSubscribeEventRepository extends BookshelfRepository<string, SubscribeEvent> {
10
+ sentry?: Sentry;
11
+
12
+ constructor(Model: ModelClass<string>, deps: {sentry?: Sentry} = {}) {
13
+ super(Model);
14
+ this.sentry = deps.sentry;
15
+ }
16
+
17
+ toPrimitive(entity: SubscribeEvent): object {
18
+ return {
19
+ id: entity.id,
20
+ recommendation_id: entity.recommendationId,
21
+ member_id: entity.memberId,
22
+ created_at: entity.createdAt
23
+ };
24
+ }
25
+
26
+ modelToEntity(model: ModelInstance<string>): SubscribeEvent | null {
27
+ try {
28
+ return SubscribeEvent.create({
29
+ id: model.id,
30
+ recommendationId: model.get('recommendation_id') as string,
31
+ memberId: model.get('member_id') as string,
32
+ createdAt: model.get('created_at') as Date
33
+ });
34
+ } catch (err) {
35
+ logger.error(err);
36
+ this.sentry?.captureException(err);
37
+ return null;
38
+ }
39
+ }
40
+
41
+ getFieldToColumnMap() {
42
+ return {
43
+ id: 'id',
44
+ recommendationId: 'recommendation_id',
45
+ memberId: 'member_id',
46
+ createdAt: 'created_at'
47
+ } as Record<keyof SubscribeEvent, string>;
48
+ }
49
+ }
@@ -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.ClickEvent = void 0;
7
+ const bson_objectid_1 = __importDefault(require("bson-objectid"));
8
+ class ClickEvent {
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 ?? null,
28
+ createdAt: data.createdAt ?? new Date()
29
+ };
30
+ return new ClickEvent(d);
31
+ }
32
+ }
33
+ exports.ClickEvent = ClickEvent;
@@ -0,0 +1,32 @@
1
+ import ObjectId from 'bson-objectid';
2
+
3
+ export class ClickEvent {
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|null, createdAt?: Date}) {
21
+ const id = data.id ?? ObjectId().toString();
22
+
23
+ const d = {
24
+ id,
25
+ recommendationId: data.recommendationId,
26
+ memberId: data.memberId ?? null,
27
+ createdAt: data.createdAt ?? new Date()
28
+ };
29
+
30
+ return new ClickEvent(d);
31
+ }
32
+ }
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InMemoryRecommendationRepository = void 0;
4
+ const InMemoryRepository_1 = require("../../lib/InMemoryRepository");
5
+ class InMemoryRecommendationRepository extends InMemoryRepository_1.InMemoryRepository {
6
+ toPrimitive(entity) {
7
+ return entity;
8
+ }
9
+ async getByUrl(url) {
10
+ // Find URL based on the hostname and pathname.
11
+ // Query params, hash fragements, protocol and www are ignored.
12
+ const existing = this.store.find((r) => {
13
+ return r.url.hostname.replace('www.', '') === url.hostname.replace('www.', '') &&
14
+ r.url.pathname.replace(/\/$/, '') === url.pathname.replace(/\/$/, '');
15
+ }) || null;
16
+ return existing;
17
+ }
18
+ }
19
+ exports.InMemoryRecommendationRepository = InMemoryRecommendationRepository;
@@ -0,0 +1,20 @@
1
+ import {Recommendation} from './Recommendation';
2
+ import {RecommendationRepository} from './RecommendationRepository';
3
+ import {InMemoryRepository} from '../../lib/InMemoryRepository';
4
+
5
+ export class InMemoryRecommendationRepository extends InMemoryRepository<string, Recommendation> implements RecommendationRepository {
6
+ toPrimitive(entity: Recommendation): object {
7
+ return entity;
8
+ }
9
+
10
+ async getByUrl(url: URL): Promise<Recommendation | null > {
11
+ // Find URL based on the hostname and pathname.
12
+ // Query params, hash fragements, protocol and www are ignored.
13
+ const existing = this.store.find((r) => {
14
+ return r.url.hostname.replace('www.', '') === url.hostname.replace('www.', '') &&
15
+ r.url.pathname.replace(/\/$/, '') === url.pathname.replace(/\/$/, '');
16
+ }) || null;
17
+
18
+ return existing;
19
+ }
20
+ }
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.IncomingRecommendationController = void 0;
4
+ const UnsafeData_1 = require("./UnsafeData");
5
+ class IncomingRecommendationController {
6
+ service;
7
+ constructor(deps) {
8
+ this.service = deps.service;
9
+ }
10
+ async browse(frame) {
11
+ const options = new UnsafeData_1.UnsafeData(frame.options);
12
+ const page = options.optionalKey('page')?.integer ?? 1;
13
+ const limit = options.optionalKey('limit')?.integer ?? 5;
14
+ const { incomingRecommendations, meta } = await this.service.listIncomingRecommendations({ page, limit });
15
+ return this.#serialize(incomingRecommendations, meta);
16
+ }
17
+ #serialize(recommendations, meta) {
18
+ return {
19
+ data: recommendations.map((entity) => {
20
+ return {
21
+ id: entity.id,
22
+ title: entity.title,
23
+ excerpt: entity.excerpt,
24
+ featured_image: entity.featuredImage?.toString() ?? null,
25
+ favicon: entity.favicon?.toString() ?? null,
26
+ url: entity.url.toString(),
27
+ recommending_back: !!entity.recommendingBack
28
+ };
29
+ }),
30
+ meta
31
+ };
32
+ }
33
+ }
34
+ exports.IncomingRecommendationController = IncomingRecommendationController;
@@ -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
+ }