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.
Files changed (243) hide show
  1. package/components/{tryghost-api-framework-5.115.1.tgz → tryghost-api-framework-5.116.1.tgz} +0 -0
  2. package/components/tryghost-constants-5.116.1.tgz +0 -0
  3. package/components/tryghost-custom-fonts-5.116.1.tgz +0 -0
  4. package/components/{tryghost-custom-theme-settings-service-5.115.1.tgz → tryghost-custom-theme-settings-service-5.116.1.tgz} +0 -0
  5. package/components/{tryghost-domain-events-5.115.1.tgz → tryghost-domain-events-5.116.1.tgz} +0 -0
  6. package/components/{tryghost-donations-5.115.1.tgz → tryghost-donations-5.116.1.tgz} +0 -0
  7. package/components/tryghost-email-addresses-5.116.1.tgz +0 -0
  8. package/components/tryghost-email-service-5.116.1.tgz +0 -0
  9. package/components/tryghost-email-suppression-list-5.116.1.tgz +0 -0
  10. package/components/tryghost-html-to-plaintext-5.116.1.tgz +0 -0
  11. package/components/tryghost-i18n-5.116.1.tgz +0 -0
  12. package/components/tryghost-job-manager-5.116.1.tgz +0 -0
  13. package/components/tryghost-link-replacer-5.116.1.tgz +0 -0
  14. package/components/tryghost-magic-link-5.116.1.tgz +0 -0
  15. package/components/{tryghost-member-attribution-5.115.1.tgz → tryghost-member-attribution-5.116.1.tgz} +0 -0
  16. package/components/tryghost-member-events-5.116.1.tgz +0 -0
  17. package/components/tryghost-members-api-5.116.1.tgz +0 -0
  18. package/components/tryghost-members-csv-5.116.1.tgz +0 -0
  19. package/components/{tryghost-members-offers-5.115.1.tgz → tryghost-members-offers-5.116.1.tgz} +0 -0
  20. package/components/{tryghost-milestones-5.115.1.tgz → tryghost-milestones-5.116.1.tgz} +0 -0
  21. package/components/{tryghost-mw-error-handler-5.115.1.tgz → tryghost-mw-error-handler-5.116.1.tgz} +0 -0
  22. package/components/tryghost-mw-vhost-5.116.1.tgz +0 -0
  23. package/components/{tryghost-post-events-5.115.1.tgz → tryghost-post-events-5.116.1.tgz} +0 -0
  24. package/components/{tryghost-post-revisions-5.115.1.tgz → tryghost-post-revisions-5.116.1.tgz} +0 -0
  25. package/components/tryghost-posts-service-5.116.1.tgz +0 -0
  26. package/components/{tryghost-prometheus-metrics-5.115.1.tgz → tryghost-prometheus-metrics-5.116.1.tgz} +0 -0
  27. package/components/tryghost-security-5.116.1.tgz +0 -0
  28. package/components/{tryghost-tiers-5.115.1.tgz → tryghost-tiers-5.116.1.tgz} +0 -0
  29. package/components/tryghost-webmentions-5.116.1.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 +6 -11
  37. package/content/themes/source/partials/feature-image.hbs +2 -2
  38. package/core/boot.js +0 -42
  39. package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +24764 -24129
  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-15df2af5.mjs → index-a9601514.mjs} +3 -3
  42. package/core/built/admin/assets/admin-x-demo/{modals-8ca61d78.mjs → modals-c1789d04.mjs} +2 -2
  43. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-d2e6872f.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-8e8821e5.mjs → index-84580c3a.mjs} +2 -2
  46. package/core/built/admin/assets/admin-x-settings/{index-f5cb3db3.mjs → index-f744cab7.mjs} +49 -35
  47. package/core/built/admin/assets/admin-x-settings/{modals-e8ae4d46.mjs → modals-d9ca60c5.mjs} +1198 -1192
  48. package/core/built/admin/assets/chunk.524.cb72a86e19c9ffd6172e.js +35 -0
  49. package/core/built/admin/assets/chunk.582.4f4d38ffe79fbdbd26f7.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-df7b9558260aa27d18b195ee895b487d.js → ghost-03b64c086f3c60cabc85fe7a7e2b640a.js} +144 -145
  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 +71082 -7533
  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 +6 -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/server/adapters/cache/Redis.js +1 -1
  71. package/core/server/adapters/lib/redis/AdapterCacheRedis.js +287 -0
  72. package/core/server/adapters/lib/redis/redis-store-factory.js +22 -0
  73. package/core/server/api/endpoints/posts.js +9 -3
  74. package/core/server/api/endpoints/previews.js +35 -1
  75. package/core/server/api/endpoints/utils/serializers/output/utils/post-gating.js +6 -9
  76. package/core/server/api/endpoints/utils/validators/input/settings.js +1 -1
  77. package/core/server/data/db/connection.js +2 -0
  78. package/core/server/data/db/index.js +1 -0
  79. package/core/server/data/importer/handlers/ImporterContentFileHandler.js +90 -0
  80. package/core/server/data/importer/import-manager.js +1 -1
  81. package/core/server/data/seeders/DataGenerator.js +288 -0
  82. package/core/server/data/seeders/importers/BenefitsImporter.js +28 -0
  83. package/core/server/data/seeders/importers/CommentsImporter.js +73 -0
  84. package/core/server/data/seeders/importers/EmailBatchesImporter.js +38 -0
  85. package/core/server/data/seeders/importers/EmailRecipientFailuresImporter.js +67 -0
  86. package/core/server/data/seeders/importers/EmailRecipientsImporter.js +212 -0
  87. package/core/server/data/seeders/importers/EmailsImporter.js +99 -0
  88. package/core/server/data/seeders/importers/LabelsImporter.js +41 -0
  89. package/core/server/data/seeders/importers/MembersClickEventsImporter.js +69 -0
  90. package/core/server/data/seeders/importers/MembersCreatedEventsImporter.js +103 -0
  91. package/core/server/data/seeders/importers/MembersFeedbackImporter.js +45 -0
  92. package/core/server/data/seeders/importers/MembersImporter.js +111 -0
  93. package/core/server/data/seeders/importers/MembersLabelsImporter.js +39 -0
  94. package/core/server/data/seeders/importers/MembersLoginEventsImporter.js +69 -0
  95. package/core/server/data/seeders/importers/MembersNewslettersImporter.js +38 -0
  96. package/core/server/data/seeders/importers/MembersPaidSubscriptionEventsImporter.js +99 -0
  97. package/core/server/data/seeders/importers/MembersProductsImporter.js +42 -0
  98. package/core/server/data/seeders/importers/MembersStatusEventsImporter.js +58 -0
  99. package/core/server/data/seeders/importers/MembersStripeCustomersImporter.js +60 -0
  100. package/core/server/data/seeders/importers/MembersStripeCustomersSubscriptionsImporter.js +259 -0
  101. package/core/server/data/seeders/importers/MembersSubscribeEventsImporter.js +69 -0
  102. package/core/server/data/seeders/importers/MembersSubscriptionCreatedEventsImporter.js +95 -0
  103. package/core/server/data/seeders/importers/NewslettersImporter.js +40 -0
  104. package/core/server/data/seeders/importers/OffersImporter.js +70 -0
  105. package/core/server/data/seeders/importers/PostsAuthorsImporter.js +32 -0
  106. package/core/server/data/seeders/importers/PostsImporter.js +102 -0
  107. package/core/server/data/seeders/importers/PostsProductsImporter.js +35 -0
  108. package/core/server/data/seeders/importers/PostsTagsImporter.js +46 -0
  109. package/core/server/data/seeders/importers/ProductsBenefitsImporter.js +54 -0
  110. package/core/server/data/seeders/importers/ProductsImporter.js +90 -0
  111. package/core/server/data/seeders/importers/RecommendationClickEventsImporter.js +32 -0
  112. package/core/server/data/seeders/importers/RecommendationSubscribeEventsImporter.js +32 -0
  113. package/core/server/data/seeders/importers/RecommendationsImporter.js +34 -0
  114. package/core/server/data/seeders/importers/RedirectsImporter.js +49 -0
  115. package/core/server/data/seeders/importers/RolesUsersImporter.js +42 -0
  116. package/core/server/data/seeders/importers/StripePricesImporter.js +69 -0
  117. package/core/server/data/seeders/importers/StripeProductsImporter.js +34 -0
  118. package/core/server/data/seeders/importers/TableImporter.js +187 -0
  119. package/core/server/data/seeders/importers/TagsImporter.js +41 -0
  120. package/core/server/data/seeders/importers/UsersImporter.js +31 -0
  121. package/core/server/data/seeders/importers/WebMentionsImporter.js +42 -0
  122. package/core/server/data/seeders/importers/index.js +41 -0
  123. package/core/server/data/seeders/utils/JsonImporter.js +39 -0
  124. package/core/server/data/seeders/utils/blog-info.js +3 -0
  125. package/core/server/data/seeders/utils/database-date.js +7 -0
  126. package/core/server/data/seeders/utils/event-generator.js +48 -0
  127. package/core/server/data/seeders/utils/random.js +13 -0
  128. package/core/server/data/seeders/utils/topological-sort.js +33 -0
  129. package/core/server/services/adapter-manager/AdapterManager.js +161 -0
  130. package/core/server/services/adapter-manager/index.js +1 -1
  131. package/core/server/services/announcement-bar-service/AnnouncementBarSettings.js +54 -0
  132. package/core/server/services/announcement-bar-service/AnnouncementVisibilityValues.js +11 -0
  133. package/core/server/services/announcement-bar-service/index.js +1 -1
  134. package/core/server/services/api-version-compatibility/APIVersionCompatibilityService.js +1 -1
  135. package/core/server/services/auth/session/session-service.js +15 -5
  136. package/core/server/services/custom-redirects/index.js +1 -1
  137. package/core/server/services/email-analytics/EmailAnalyticsProviderMailgun.js +1 -1
  138. package/core/server/services/email-service/EmailServiceWrapper.js +4 -4
  139. package/core/server/services/email-suppression-list/MailgunEmailSuppressionList.js +1 -1
  140. package/core/server/services/email-suppression-list/service.js +1 -1
  141. package/core/server/services/lib/DynamicRedirectManager.js +156 -0
  142. package/core/server/services/lib/EmailContentGenerator.js +54 -0
  143. package/core/server/services/lib/InMemoryRepository.js +62 -0
  144. package/core/server/services/lib/InMemoryRepository.ts +80 -0
  145. package/core/server/services/lib/MailgunClient.js +364 -0
  146. package/core/server/services/link-redirection/LinkRedirect.js +26 -0
  147. package/core/server/services/link-redirection/LinkRedirectRepository.js +7 -7
  148. package/core/server/services/link-redirection/LinkRedirectsService.js +123 -0
  149. package/core/server/services/link-redirection/README.md +151 -0
  150. package/core/server/services/link-redirection/RedirectEvent.js +24 -0
  151. package/core/server/services/link-redirection/index.js +1 -1
  152. package/core/server/services/link-tracking/LinkClickTrackingService.js +1 -1
  153. package/core/server/services/mail/index.js +1 -1
  154. package/core/server/services/mail-events/InMemoryMailEventRepository.js +2 -2
  155. package/core/server/services/mail-events/InMemoryMailEventRepository.ts +1 -1
  156. package/core/server/services/members-events/LastSeenAtUpdater.js +1 -1
  157. package/core/server/services/offers/service.js +1 -1
  158. package/core/server/services/recommendations/RecommendationServiceWrapper.js +8 -8
  159. package/core/server/services/recommendations/service/BookshelfClickEventRepository.js +48 -0
  160. package/core/server/services/recommendations/service/BookshelfClickEventRepository.ts +49 -0
  161. package/core/server/services/recommendations/service/BookshelfRecommendationRepository.js +98 -0
  162. package/core/server/services/recommendations/service/BookshelfRecommendationRepository.ts +117 -0
  163. package/core/server/services/recommendations/service/BookshelfRepository.js +134 -0
  164. package/core/server/services/recommendations/service/BookshelfRepository.ts +196 -0
  165. package/core/server/services/recommendations/service/BookshelfSubscribeEventRepository.js +48 -0
  166. package/core/server/services/recommendations/service/BookshelfSubscribeEventRepository.ts +49 -0
  167. package/core/server/services/recommendations/service/ClickEvent.js +33 -0
  168. package/core/server/services/recommendations/service/ClickEvent.ts +32 -0
  169. package/core/server/services/recommendations/service/InMemoryRecommendationRepository.js +19 -0
  170. package/core/server/services/recommendations/service/InMemoryRecommendationRepository.ts +20 -0
  171. package/core/server/services/recommendations/service/IncomingRecommendationController.js +34 -0
  172. package/core/server/services/recommendations/service/IncomingRecommendationController.ts +51 -0
  173. package/core/server/services/recommendations/service/IncomingRecommendationEmailRenderer.js +25 -0
  174. package/core/server/services/recommendations/service/IncomingRecommendationEmailRenderer.ts +37 -0
  175. package/core/server/services/recommendations/service/IncomingRecommendationService.js +93 -0
  176. package/core/server/services/recommendations/service/IncomingRecommendationService.ts +160 -0
  177. package/core/server/services/recommendations/service/Recommendation.js +140 -0
  178. package/core/server/services/recommendations/service/Recommendation.ts +201 -0
  179. package/core/server/services/recommendations/service/RecommendationController.js +208 -0
  180. package/core/server/services/recommendations/service/RecommendationController.ts +258 -0
  181. package/core/server/services/recommendations/service/RecommendationMetadataService.js +86 -0
  182. package/core/server/services/recommendations/service/RecommendationMetadataService.ts +128 -0
  183. package/core/server/services/recommendations/service/RecommendationRepository.js +2 -0
  184. package/core/server/services/recommendations/service/RecommendationRepository.ts +13 -0
  185. package/core/server/services/recommendations/service/RecommendationService.js +228 -0
  186. package/core/server/services/recommendations/service/RecommendationService.ts +281 -0
  187. package/core/server/services/recommendations/service/SubscribeEvent.js +33 -0
  188. package/core/server/services/recommendations/service/SubscribeEvent.ts +32 -0
  189. package/core/server/services/recommendations/service/UnsafeData.js +183 -0
  190. package/core/server/services/recommendations/service/UnsafeData.ts +217 -0
  191. package/core/server/services/recommendations/service/WellknownService.js +36 -0
  192. package/core/server/services/recommendations/service/WellknownService.ts +47 -0
  193. package/core/server/services/recommendations/service/index.js +31 -0
  194. package/core/server/services/recommendations/service/index.ts +15 -0
  195. package/core/server/services/recommendations/service/libraries.d.ts +5 -0
  196. package/core/server/services/slack-notifications/SlackNotifications.js +211 -0
  197. package/core/server/services/slack-notifications/SlackNotificationsService.js +90 -0
  198. package/core/server/services/slack-notifications/service.js +4 -6
  199. package/core/server/web/api/endpoints/admin/app.js +1 -21
  200. package/core/server/web/api/middleware/version-match.js +41 -0
  201. package/core/shared/labs.js +2 -2
  202. package/package.json +87 -104
  203. package/tsconfig.tsbuildinfo +1 -1
  204. package/yarn.lock +1470 -1540
  205. package/components/tryghost-adapter-cache-redis-5.115.1.tgz +0 -0
  206. package/components/tryghost-adapter-manager-5.115.1.tgz +0 -0
  207. package/components/tryghost-announcement-bar-settings-5.115.1.tgz +0 -0
  208. package/components/tryghost-constants-5.115.1.tgz +0 -0
  209. package/components/tryghost-custom-fonts-5.115.1.tgz +0 -0
  210. package/components/tryghost-data-generator-5.115.1.tgz +0 -0
  211. package/components/tryghost-email-addresses-5.115.1.tgz +0 -0
  212. package/components/tryghost-email-content-generator-5.115.1.tgz +0 -0
  213. package/components/tryghost-email-events-5.115.1.tgz +0 -0
  214. package/components/tryghost-email-service-5.115.1.tgz +0 -0
  215. package/components/tryghost-email-suppression-list-5.115.1.tgz +0 -0
  216. package/components/tryghost-express-dynamic-redirects-5.115.1.tgz +0 -0
  217. package/components/tryghost-ghost-5.115.1.tgz +0 -0
  218. package/components/tryghost-html-to-plaintext-5.115.1.tgz +0 -0
  219. package/components/tryghost-i18n-5.115.1.tgz +0 -0
  220. package/components/tryghost-importer-handler-content-files-5.115.1.tgz +0 -0
  221. package/components/tryghost-in-memory-repository-5.115.1.tgz +0 -0
  222. package/components/tryghost-job-manager-5.115.1.tgz +0 -0
  223. package/components/tryghost-link-redirects-5.115.1.tgz +0 -0
  224. package/components/tryghost-link-replacer-5.115.1.tgz +0 -0
  225. package/components/tryghost-magic-link-5.115.1.tgz +0 -0
  226. package/components/tryghost-mailgun-client-5.115.1.tgz +0 -0
  227. package/components/tryghost-member-events-5.115.1.tgz +0 -0
  228. package/components/tryghost-members-api-5.115.1.tgz +0 -0
  229. package/components/tryghost-members-csv-5.115.1.tgz +0 -0
  230. package/components/tryghost-members-payments-5.115.1.tgz +0 -0
  231. package/components/tryghost-minifier-5.115.1.tgz +0 -0
  232. package/components/tryghost-mw-version-match-5.115.1.tgz +0 -0
  233. package/components/tryghost-mw-vhost-5.115.1.tgz +0 -0
  234. package/components/tryghost-posts-service-5.115.1.tgz +0 -0
  235. package/components/tryghost-recommendations-5.115.1.tgz +0 -0
  236. package/components/tryghost-security-5.115.1.tgz +0 -0
  237. package/components/tryghost-slack-notifications-5.115.1.tgz +0 -0
  238. package/components/tryghost-webmentions-5.115.1.tgz +0 -0
  239. package/core/built/admin/assets/chunk.524.2439684964c164c598ab.js +0 -35
  240. package/core/built/admin/assets/chunk.582.bf5a2bbb2c4eb69ef1e7.js +0 -37
  241. package/core/built/admin/assets/ghost-327b17ea23cb8c89bd7e6a51e18e8506.css +0 -1
  242. package/core/built/admin/assets/ghost-dark-f30a597ac19632a118939492591c531b.css +0 -1
  243. /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;