ghost 5.99.0 → 5.100.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 (206) hide show
  1. package/components/tryghost-activitypub-5.100.0.tgz +0 -0
  2. package/components/tryghost-adapter-cache-memory-ttl-5.100.0.tgz +0 -0
  3. package/components/tryghost-adapter-cache-redis-5.100.0.tgz +0 -0
  4. package/components/tryghost-adapter-manager-5.100.0.tgz +0 -0
  5. package/components/tryghost-announcement-bar-settings-5.100.0.tgz +0 -0
  6. package/components/tryghost-api-framework-5.100.0.tgz +0 -0
  7. package/components/tryghost-api-version-compatibility-service-5.100.0.tgz +0 -0
  8. package/components/tryghost-audience-feedback-5.100.0.tgz +0 -0
  9. package/components/tryghost-bookshelf-repository-5.100.0.tgz +0 -0
  10. package/components/tryghost-bootstrap-socket-5.100.0.tgz +0 -0
  11. package/components/tryghost-collections-5.100.0.tgz +0 -0
  12. package/components/tryghost-constants-5.100.0.tgz +0 -0
  13. package/components/tryghost-custom-fonts-5.100.0.tgz +0 -0
  14. package/components/tryghost-custom-theme-settings-service-5.100.0.tgz +0 -0
  15. package/components/{tryghost-data-generator-5.99.0.tgz → tryghost-data-generator-5.100.0.tgz} +0 -0
  16. package/components/tryghost-domain-events-5.100.0.tgz +0 -0
  17. package/components/tryghost-donations-5.100.0.tgz +0 -0
  18. package/components/tryghost-dynamic-routing-events-5.100.0.tgz +0 -0
  19. package/components/tryghost-email-addresses-5.100.0.tgz +0 -0
  20. package/components/tryghost-email-analytics-provider-mailgun-5.100.0.tgz +0 -0
  21. package/components/tryghost-email-analytics-service-5.100.0.tgz +0 -0
  22. package/components/tryghost-email-content-generator-5.100.0.tgz +0 -0
  23. package/components/tryghost-email-events-5.100.0.tgz +0 -0
  24. package/components/{tryghost-email-service-5.99.0.tgz → tryghost-email-service-5.100.0.tgz} +0 -0
  25. package/components/tryghost-email-suppression-list-5.100.0.tgz +0 -0
  26. package/components/tryghost-express-dynamic-redirects-5.100.0.tgz +0 -0
  27. package/components/tryghost-external-media-inliner-5.100.0.tgz +0 -0
  28. package/components/tryghost-extract-api-key-5.100.0.tgz +0 -0
  29. package/components/tryghost-ghost-5.100.0.tgz +0 -0
  30. package/components/tryghost-html-to-plaintext-5.100.0.tgz +0 -0
  31. package/components/tryghost-i18n-5.100.0.tgz +0 -0
  32. package/components/tryghost-identity-token-service-5.100.0.tgz +0 -0
  33. package/components/tryghost-importer-handler-content-files-5.100.0.tgz +0 -0
  34. package/components/tryghost-importer-revue-5.100.0.tgz +0 -0
  35. package/components/tryghost-in-memory-repository-5.100.0.tgz +0 -0
  36. package/components/tryghost-job-manager-5.100.0.tgz +0 -0
  37. package/components/tryghost-link-redirects-5.100.0.tgz +0 -0
  38. package/components/tryghost-link-replacer-5.100.0.tgz +0 -0
  39. package/components/tryghost-link-tracking-5.100.0.tgz +0 -0
  40. package/components/{tryghost-magic-link-5.99.0.tgz → tryghost-magic-link-5.100.0.tgz} +0 -0
  41. package/components/tryghost-mail-events-5.100.0.tgz +0 -0
  42. package/components/tryghost-mailgun-client-5.100.0.tgz +0 -0
  43. package/components/tryghost-member-attribution-5.100.0.tgz +0 -0
  44. package/components/tryghost-member-events-5.100.0.tgz +0 -0
  45. package/components/{tryghost-members-api-5.99.0.tgz → tryghost-members-api-5.100.0.tgz} +0 -0
  46. package/components/tryghost-members-csv-5.100.0.tgz +0 -0
  47. package/components/tryghost-members-events-service-5.100.0.tgz +0 -0
  48. package/components/tryghost-members-importer-5.100.0.tgz +0 -0
  49. package/components/tryghost-members-offers-5.100.0.tgz +0 -0
  50. package/components/tryghost-members-payments-5.100.0.tgz +0 -0
  51. package/components/tryghost-members-ssr-5.100.0.tgz +0 -0
  52. package/components/tryghost-members-stripe-service-5.100.0.tgz +0 -0
  53. package/components/tryghost-mentions-email-report-5.100.0.tgz +0 -0
  54. package/components/tryghost-milestones-5.100.0.tgz +0 -0
  55. package/components/tryghost-minifier-5.100.0.tgz +0 -0
  56. package/components/tryghost-model-to-domain-event-interceptor-5.100.0.tgz +0 -0
  57. package/components/tryghost-mw-api-version-mismatch-5.100.0.tgz +0 -0
  58. package/components/tryghost-mw-cache-control-5.100.0.tgz +0 -0
  59. package/components/tryghost-mw-error-handler-5.100.0.tgz +0 -0
  60. package/components/tryghost-mw-session-from-token-5.100.0.tgz +0 -0
  61. package/components/tryghost-mw-update-user-last-seen-5.100.0.tgz +0 -0
  62. package/components/tryghost-mw-version-match-5.100.0.tgz +0 -0
  63. package/components/tryghost-mw-vhost-5.100.0.tgz +0 -0
  64. package/components/tryghost-nql-filter-expansions-5.100.0.tgz +0 -0
  65. package/components/tryghost-oembed-service-5.100.0.tgz +0 -0
  66. package/components/tryghost-package-json-5.100.0.tgz +0 -0
  67. package/components/tryghost-post-events-5.100.0.tgz +0 -0
  68. package/components/tryghost-post-revisions-5.100.0.tgz +0 -0
  69. package/components/tryghost-posts-service-5.100.0.tgz +0 -0
  70. package/components/tryghost-prometheus-metrics-5.100.0.tgz +0 -0
  71. package/components/tryghost-recommendations-5.100.0.tgz +0 -0
  72. package/components/tryghost-referrers-5.100.0.tgz +0 -0
  73. package/components/tryghost-security-5.100.0.tgz +0 -0
  74. package/components/tryghost-session-service-5.100.0.tgz +0 -0
  75. package/components/tryghost-settings-path-manager-5.100.0.tgz +0 -0
  76. package/components/tryghost-slack-notifications-5.100.0.tgz +0 -0
  77. package/components/tryghost-staff-service-5.100.0.tgz +0 -0
  78. package/components/tryghost-stats-service-5.100.0.tgz +0 -0
  79. package/components/tryghost-tiers-5.100.0.tgz +0 -0
  80. package/components/tryghost-update-check-service-5.100.0.tgz +0 -0
  81. package/components/tryghost-verification-trigger-5.100.0.tgz +0 -0
  82. package/components/tryghost-version-notifications-data-service-5.100.0.tgz +0 -0
  83. package/components/tryghost-webmentions-5.100.0.tgz +0 -0
  84. package/core/boot.js +16 -5
  85. package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +2 -2
  86. package/core/built/admin/assets/admin-x-activitypub/{index-696e3897.mjs → index-ab50c736.mjs} +147 -141
  87. package/core/built/admin/assets/admin-x-activitypub/{modals-dc93ce65.mjs → modals-2e6f9c05.mjs} +2 -2
  88. package/core/built/admin/assets/admin-x-demo/admin-x-demo.js +1 -1
  89. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-e6f6e0b3.mjs → CodeEditorView-26137e6e.mjs} +2 -2
  90. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +3 -3
  91. package/core/built/admin/assets/admin-x-settings/{index-2a9ec06c.mjs → index-cdf09c6d.mjs} +2 -2
  92. package/core/built/admin/assets/admin-x-settings/{index-8ede56c6.mjs → index-f6338b55.mjs} +579 -571
  93. package/core/built/admin/assets/admin-x-settings/{modals-e3497e68.mjs → modals-15249255.mjs} +5657 -5622
  94. package/core/built/admin/assets/{chunk.524.85082593cb84282804e5.js → chunk.524.0982f6745ca0f52945b1.js} +5 -5
  95. package/core/built/admin/assets/{chunk.582.a756882ecb6d421c862c.js → chunk.582.d97692c812575ab051a9.js} +6 -6
  96. package/core/built/admin/assets/{ghost-fefa2e99b7dc389dfd6579049cdd2066.js → ghost-7ab268fc7cd7884eef525145d5fbb501.js} +13 -13
  97. package/core/built/admin/index.html +3 -3
  98. package/core/frontend/helpers/ghost_head.js +2 -1
  99. package/core/server/api/endpoints/comments-members.js +0 -1
  100. package/core/server/api/endpoints/identities.js +5 -31
  101. package/core/server/api/endpoints/utils/serializers/output/mappers/comments.js +9 -2
  102. package/core/server/data/migrations/versions/5.100/2024-10-31-15-27-42-add-jobs-queue-columns.js +14 -0
  103. package/core/server/data/migrations/versions/5.100/2024-11-05-14-48-08-add-comments-in-reply-to-id.js +10 -0
  104. package/core/server/data/migrations/versions/5.100/2024-11-06-04-45-15-add-activitypub-integration.js +40 -0
  105. package/core/server/data/migrations/versions/5.55/2023-07-10-05-16-55-add-built-in-collection-posts.js +1 -1
  106. package/core/server/data/migrations/versions/5.65/2023-09-22-06-42-55-repopulate-built-in-featured-collection-posts.js +1 -1
  107. package/core/server/data/migrations/versions/5.89/2024-07-30-19-51-06-backfill-offer-redemptions.js +1 -1
  108. package/core/server/data/schema/fixtures/fixtures.json +7 -0
  109. package/core/server/data/schema/schema.js +4 -1
  110. package/core/server/models/comment.js +17 -33
  111. package/core/server/services/activitypub/ActivityPubServiceWrapper.js +42 -0
  112. package/core/server/services/activitypub/index.js +1 -0
  113. package/core/server/services/comments/CommentsController.js +3 -1
  114. package/core/server/services/comments/CommentsService.js +24 -2
  115. package/core/server/services/comments/CommentsServiceEmails.js +7 -2
  116. package/core/server/services/email-analytics/EmailAnalyticsServiceWrapper.js +18 -2
  117. package/core/server/services/email-analytics/jobs/update-member-email-analytics/index.js +13 -0
  118. package/core/server/services/email-service/EmailServiceWrapper.js +20 -1
  119. package/core/server/services/identity-tokens/IdentityTokenServiceWrapper.js +28 -0
  120. package/core/server/services/identity-tokens/index.js +1 -0
  121. package/core/server/services/jobs/job-service.js +3 -2
  122. package/core/server/services/mentions-jobs/job-service.js +2 -2
  123. package/core/server/services/offers/OfferBookshelfRepository.js +3 -1
  124. package/core/shared/config/defaults.json +1 -1
  125. package/core/shared/config/env/config.testing.json +0 -3
  126. package/core/shared/labs.js +1 -1
  127. package/core/shared/prometheus-client.js +8 -31
  128. package/package.json +156 -153
  129. package/yarn.lock +176 -26
  130. package/components/tryghost-adapter-cache-memory-ttl-5.99.0.tgz +0 -0
  131. package/components/tryghost-adapter-cache-redis-5.99.0.tgz +0 -0
  132. package/components/tryghost-adapter-manager-5.99.0.tgz +0 -0
  133. package/components/tryghost-announcement-bar-settings-5.99.0.tgz +0 -0
  134. package/components/tryghost-api-framework-5.99.0.tgz +0 -0
  135. package/components/tryghost-api-version-compatibility-service-5.99.0.tgz +0 -0
  136. package/components/tryghost-audience-feedback-5.99.0.tgz +0 -0
  137. package/components/tryghost-bookshelf-repository-5.99.0.tgz +0 -0
  138. package/components/tryghost-bootstrap-socket-5.99.0.tgz +0 -0
  139. package/components/tryghost-collections-5.99.0.tgz +0 -0
  140. package/components/tryghost-constants-5.99.0.tgz +0 -0
  141. package/components/tryghost-custom-fonts-5.99.0.tgz +0 -0
  142. package/components/tryghost-custom-theme-settings-service-5.99.0.tgz +0 -0
  143. package/components/tryghost-domain-events-5.99.0.tgz +0 -0
  144. package/components/tryghost-donations-5.99.0.tgz +0 -0
  145. package/components/tryghost-dynamic-routing-events-5.99.0.tgz +0 -0
  146. package/components/tryghost-email-addresses-5.99.0.tgz +0 -0
  147. package/components/tryghost-email-analytics-provider-mailgun-5.99.0.tgz +0 -0
  148. package/components/tryghost-email-analytics-service-5.99.0.tgz +0 -0
  149. package/components/tryghost-email-content-generator-5.99.0.tgz +0 -0
  150. package/components/tryghost-email-events-5.99.0.tgz +0 -0
  151. package/components/tryghost-email-suppression-list-5.99.0.tgz +0 -0
  152. package/components/tryghost-express-dynamic-redirects-5.99.0.tgz +0 -0
  153. package/components/tryghost-external-media-inliner-5.99.0.tgz +0 -0
  154. package/components/tryghost-extract-api-key-5.99.0.tgz +0 -0
  155. package/components/tryghost-ghost-5.99.0.tgz +0 -0
  156. package/components/tryghost-html-to-plaintext-5.99.0.tgz +0 -0
  157. package/components/tryghost-i18n-5.99.0.tgz +0 -0
  158. package/components/tryghost-importer-handler-content-files-5.99.0.tgz +0 -0
  159. package/components/tryghost-importer-revue-5.99.0.tgz +0 -0
  160. package/components/tryghost-in-memory-repository-5.99.0.tgz +0 -0
  161. package/components/tryghost-job-manager-5.99.0.tgz +0 -0
  162. package/components/tryghost-link-redirects-5.99.0.tgz +0 -0
  163. package/components/tryghost-link-replacer-5.99.0.tgz +0 -0
  164. package/components/tryghost-link-tracking-5.99.0.tgz +0 -0
  165. package/components/tryghost-mail-events-5.99.0.tgz +0 -0
  166. package/components/tryghost-mailgun-client-5.99.0.tgz +0 -0
  167. package/components/tryghost-member-attribution-5.99.0.tgz +0 -0
  168. package/components/tryghost-member-events-5.99.0.tgz +0 -0
  169. package/components/tryghost-members-csv-5.99.0.tgz +0 -0
  170. package/components/tryghost-members-events-service-5.99.0.tgz +0 -0
  171. package/components/tryghost-members-importer-5.99.0.tgz +0 -0
  172. package/components/tryghost-members-offers-5.99.0.tgz +0 -0
  173. package/components/tryghost-members-payments-5.99.0.tgz +0 -0
  174. package/components/tryghost-members-ssr-5.99.0.tgz +0 -0
  175. package/components/tryghost-members-stripe-service-5.99.0.tgz +0 -0
  176. package/components/tryghost-mentions-email-report-5.99.0.tgz +0 -0
  177. package/components/tryghost-metrics-server-5.99.0.tgz +0 -0
  178. package/components/tryghost-milestones-5.99.0.tgz +0 -0
  179. package/components/tryghost-minifier-5.99.0.tgz +0 -0
  180. package/components/tryghost-model-to-domain-event-interceptor-5.99.0.tgz +0 -0
  181. package/components/tryghost-mw-api-version-mismatch-5.99.0.tgz +0 -0
  182. package/components/tryghost-mw-cache-control-5.99.0.tgz +0 -0
  183. package/components/tryghost-mw-error-handler-5.99.0.tgz +0 -0
  184. package/components/tryghost-mw-session-from-token-5.99.0.tgz +0 -0
  185. package/components/tryghost-mw-update-user-last-seen-5.99.0.tgz +0 -0
  186. package/components/tryghost-mw-version-match-5.99.0.tgz +0 -0
  187. package/components/tryghost-mw-vhost-5.99.0.tgz +0 -0
  188. package/components/tryghost-nql-filter-expansions-5.99.0.tgz +0 -0
  189. package/components/tryghost-oembed-service-5.99.0.tgz +0 -0
  190. package/components/tryghost-package-json-5.99.0.tgz +0 -0
  191. package/components/tryghost-post-events-5.99.0.tgz +0 -0
  192. package/components/tryghost-post-revisions-5.99.0.tgz +0 -0
  193. package/components/tryghost-posts-service-5.99.0.tgz +0 -0
  194. package/components/tryghost-recommendations-5.99.0.tgz +0 -0
  195. package/components/tryghost-referrers-5.99.0.tgz +0 -0
  196. package/components/tryghost-security-5.99.0.tgz +0 -0
  197. package/components/tryghost-session-service-5.99.0.tgz +0 -0
  198. package/components/tryghost-settings-path-manager-5.99.0.tgz +0 -0
  199. package/components/tryghost-slack-notifications-5.99.0.tgz +0 -0
  200. package/components/tryghost-staff-service-5.99.0.tgz +0 -0
  201. package/components/tryghost-stats-service-5.99.0.tgz +0 -0
  202. package/components/tryghost-tiers-5.99.0.tgz +0 -0
  203. package/components/tryghost-update-check-service-5.99.0.tgz +0 -0
  204. package/components/tryghost-verification-trigger-5.99.0.tgz +0 -0
  205. package/components/tryghost-version-notifications-data-service-5.99.0.tgz +0 -0
  206. package/components/tryghost-webmentions-5.99.0.tgz +0 -0
@@ -8,7 +8,7 @@
8
8
  <title>Ghost Admin</title>
9
9
 
10
10
 
11
- <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22cdnUrl%22%3A%22%22%2C%22editorUrl%22%3A%22%22%2C%22rootURL%22%3A%22%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%225.99%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%2C%22editorFilename%22%3A%22koenig-lexical.umd.js%22%2C%22editorHash%22%3A%22a724a31dfe%22%2C%22adminXDemoFilename%22%3A%22admin-x-demo.js%22%2C%22adminXDemoHash%22%3A%22118f338ce4%22%2C%22adminXSettingsFilename%22%3A%22admin-x-settings.js%22%2C%22adminXSettingsHash%22%3A%2224993c916e%22%2C%22adminXActivitypubFilename%22%3A%22admin-x-activitypub.js%22%2C%22adminXActivitypubHash%22%3A%22f172caf7c8%22%2C%22adminXActivitypubCustomUrl%22%3A%22https%3A%2F%2Fcdn.jsdelivr.net%2Fghost%2Fadmin-x-activitypub%400%2Fdist%2Fadmin-x-activitypub.js%22%7D" />
11
+ <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22cdnUrl%22%3A%22%22%2C%22editorUrl%22%3A%22%22%2C%22rootURL%22%3A%22%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%225.100%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%2C%22editorFilename%22%3A%22koenig-lexical.umd.js%22%2C%22editorHash%22%3A%22a724a31dfe%22%2C%22adminXDemoFilename%22%3A%22admin-x-demo.js%22%2C%22adminXDemoHash%22%3A%227b54e3e101%22%2C%22adminXSettingsFilename%22%3A%22admin-x-settings.js%22%2C%22adminXSettingsHash%22%3A%221b2f79bda3%22%2C%22adminXActivitypubFilename%22%3A%22admin-x-activitypub.js%22%2C%22adminXActivitypubHash%22%3A%22d3e0f0e348%22%2C%22adminXActivitypubCustomUrl%22%3A%22https%3A%2F%2Fcdn.jsdelivr.net%2Fghost%2Fadmin-x-activitypub%400%2Fdist%2Fadmin-x-activitypub.js%22%7D" />
12
12
 
13
13
  <meta name="HandheldFriendly" content="True" />
14
14
  <meta name="MobileOptimized" content="320" />
@@ -58,7 +58,7 @@
58
58
 
59
59
  <script src="assets/vendor-88cd9f5cf5eba65221e2d2a636531eaa.js"></script>
60
60
  <script src="assets/chunk.874.5eb920d19e75683234c2.js"></script>
61
- <script src="assets/chunk.524.85082593cb84282804e5.js"></script>
62
- <script src="assets/ghost-fefa2e99b7dc389dfd6579049cdd2066.js"></script>
61
+ <script src="assets/chunk.524.0982f6745ca0f52945b1.js"></script>
62
+ <script src="assets/ghost-7ab268fc7cd7884eef525145d5fbb501.js"></script>
63
63
  </body>
64
64
  </html>
@@ -62,7 +62,8 @@ function getMembersHelper(data, frontendKey, excludeList) {
62
62
  i18n: labs.isSet('i18n'),
63
63
  ghost: urlUtils.getSiteUrl(),
64
64
  key: frontendKey,
65
- api: urlUtils.urlFor('api', {type: 'content'}, true)
65
+ api: urlUtils.urlFor('api', {type: 'content'}, true),
66
+ locale: settingsCache.get('locale') || 'en'
66
67
  };
67
68
  if (colorString) {
68
69
  attributes['accent-color'] = colorString;
@@ -109,7 +109,6 @@ const controller = {
109
109
  },
110
110
  options: [
111
111
  'include'
112
-
113
112
  ],
114
113
  validation: {
115
114
  options: {
@@ -1,28 +1,4 @@
1
1
  const logging = require('@tryghost/logging');
2
- const settings = require('../../../shared/settings-cache');
3
- const urlUtils = require('../../../shared/url-utils');
4
- const jwt = require('jsonwebtoken');
5
- const jose = require('node-jose');
6
- const issuer = urlUtils.urlFor('admin', true);
7
-
8
- const dangerousPrivateKey = settings.get('ghost_private_key');
9
- const keyStore = jose.JWK.createKeyStore();
10
- const keyStoreReady = keyStore.add(dangerousPrivateKey, 'pem');
11
-
12
- const getKeyID = async () => {
13
- const key = await keyStoreReady;
14
- return key.kid;
15
- };
16
-
17
- const sign = async (claims, options = {}) => {
18
- const kid = await getKeyID();
19
- return jwt.sign(claims, dangerousPrivateKey, Object.assign({
20
- issuer,
21
- expiresIn: '5m',
22
- algorithm: 'RS256',
23
- keyid: kid
24
- }, options));
25
- };
26
2
 
27
3
  /** @type {import('@tryghost/api-framework').Controller} */
28
4
  const controller = {
@@ -33,6 +9,8 @@ const controller = {
33
9
  },
34
10
  permissions: true,
35
11
  async query(frame) {
12
+ const IdentityTokenService = require('../../services/identity-tokens');
13
+
36
14
  let role = null;
37
15
  try {
38
16
  await frame.user.load(['roles']);
@@ -40,13 +18,9 @@ const controller = {
40
18
  } catch (err) {
41
19
  logging.warn('Could not load role for identity');
42
20
  }
43
- const claims = {
44
- sub: frame.user.get('email')
45
- };
46
- if (typeof role === 'string') {
47
- claims.role = role;
48
- }
49
- const token = await sign(claims);
21
+
22
+ const token = await IdentityTokenService.instance.getTokenForUser(frame.user.get('email'), role);
23
+
50
24
  return {token};
51
25
  }
52
26
  }
@@ -1,9 +1,12 @@
1
1
  const _ = require('lodash');
2
2
  const utils = require('../../..');
3
3
  const url = require('../utils/url');
4
+ const htmlToPlaintext = require('@tryghost/html-to-plaintext');
4
5
 
5
6
  const commentFields = [
6
7
  'id',
8
+ 'in_reply_to_id',
9
+ 'in_reply_to_snippet',
7
10
  'status',
8
11
  'html',
9
12
  'created_at',
@@ -42,6 +45,10 @@ const countFields = [
42
45
  const commentMapper = (model, frame) => {
43
46
  const jsonModel = model.toJSON ? model.toJSON(frame.options) : model;
44
47
 
48
+ if (jsonModel.inReplyTo && jsonModel.inReplyTo.status === 'published') {
49
+ jsonModel.in_reply_to_snippet = htmlToPlaintext.commentSnippet(jsonModel.inReplyTo.html);
50
+ }
51
+
45
52
  const response = _.pick(jsonModel, commentFields);
46
53
 
47
54
  if (jsonModel.member) {
@@ -59,7 +66,7 @@ const commentMapper = (model, frame) => {
59
66
  }
60
67
 
61
68
  if (jsonModel.post) {
62
- // We could use the post mapper here, but we need less field + don't need al the async behavior support
69
+ // We could use the post mapper here, but we need less field + don't need all the async behavior support
63
70
  url.forPost(jsonModel.post.id, jsonModel.post, frame);
64
71
  response.post = _.pick(jsonModel.post, postFields);
65
72
  }
@@ -77,7 +84,7 @@ const commentMapper = (model, frame) => {
77
84
  response.html = null;
78
85
  }
79
86
  }
80
-
87
+
81
88
  return response;
82
89
  };
83
90
 
@@ -0,0 +1,14 @@
1
+ const {combineNonTransactionalMigrations, createAddColumnMigration} = require('../../utils');
2
+
3
+ module.exports = combineNonTransactionalMigrations(
4
+ createAddColumnMigration('jobs', 'metadata', {
5
+ type: 'string',
6
+ maxlength: 2000,
7
+ nullable: true
8
+ }),
9
+ createAddColumnMigration('jobs', 'queue_entry', {
10
+ type: 'integer',
11
+ nullable: true,
12
+ unsigned: true
13
+ })
14
+ );
@@ -0,0 +1,10 @@
1
+ const {createAddColumnMigration} = require('../../utils');
2
+
3
+ module.exports = createAddColumnMigration('comments', 'in_reply_to_id', {
4
+ type: 'string',
5
+ maxlength: 24,
6
+ nullable: true,
7
+ unique: false,
8
+ references: 'comments.id',
9
+ setNullDelete: true
10
+ });
@@ -0,0 +1,40 @@
1
+ const logging = require('@tryghost/logging');
2
+ const {createTransactionalMigration, meta} = require('../../utils');
3
+ const ObjectID = require('bson-objectid').default;
4
+
5
+ module.exports = createTransactionalMigration(
6
+ async function up(knex) {
7
+ logging.info('Adding Ghost ActivityPub integration');
8
+ const existing = await knex
9
+ .select('id')
10
+ .from('integrations')
11
+ .where('slug', '=', 'ghost-activitypub')
12
+ .andWhere('type', '=', 'internal')
13
+ .first();
14
+
15
+ if (existing) {
16
+ logging.warn('Found existing Ghost ActivityPub integration');
17
+ return;
18
+ }
19
+
20
+ await knex
21
+ .insert({
22
+ id: (new ObjectID).toHexString(),
23
+ type: 'internal',
24
+ slug: 'ghost-activitypub',
25
+ name: 'Ghost ActivityPub',
26
+ description: 'Internal Integration for ActivityPub',
27
+ created_at: knex.raw('current_timestamp'),
28
+ created_by: meta.MIGRATION_USER
29
+ })
30
+ .into('integrations');
31
+ },
32
+ async function down(knex) {
33
+ logging.info('Removing Ghost ActivityPub integration');
34
+ await knex
35
+ .del()
36
+ .from('integrations')
37
+ .where('slug', '=', 'ghost-activitypub')
38
+ .andWhere('type', '=', 'internal');
39
+ }
40
+ );
@@ -14,7 +14,7 @@ const insertPostCollections = async (knex, collectionId, postIds) => {
14
14
  };
15
15
  });
16
16
 
17
- await knex.batchInsert('collections_posts', collectionPosts, 1000);
17
+ await knex.batchInsert('collections_posts', collectionPosts, 100);
18
18
  };
19
19
 
20
20
  module.exports = createTransactionalMigration(
@@ -14,7 +14,7 @@ const insertPostCollections = async (knex, collectionId, postIds) => {
14
14
  };
15
15
  });
16
16
 
17
- await knex.batchInsert('collections_posts', collectionPosts, 1000);
17
+ await knex.batchInsert('collections_posts', collectionPosts, 100);
18
18
  };
19
19
 
20
20
  module.exports = createTransactionalMigration(
@@ -50,7 +50,7 @@ module.exports = createTransactionalMigration(
50
50
  };
51
51
  });
52
52
  // Batch insert rows into the offer_redemptions table
53
- await knex.batchInsert('offer_redemptions', offerRedemptions, 1000);
53
+ await knex.batchInsert('offer_redemptions', offerRedemptions, 100);
54
54
  } else {
55
55
  logging.info('No offer redemptions to backfill');
56
56
  }
@@ -814,6 +814,13 @@
814
814
  "description": "Internal Content API integration for Admin access",
815
815
  "type": "core",
816
816
  "api_keys": [{"type": "content"}]
817
+ },
818
+ {
819
+ "slug": "ghost-activitypub",
820
+ "name": "Ghost ActivityPub",
821
+ "description": "Internal Integration for ActivityPub",
822
+ "type": "internal",
823
+ "api_keys": []
817
824
  }
818
825
  ]
819
826
  }
@@ -958,6 +958,7 @@ module.exports = {
958
958
  post_id: {type: 'string', maxlength: 24, nullable: false, unique: false, references: 'posts.id', cascadeDelete: true},
959
959
  member_id: {type: 'string', maxlength: 24, nullable: true, unique: false, references: 'members.id', setNullDelete: true},
960
960
  parent_id: {type: 'string', maxlength: 24, nullable: true, unique: false, references: 'comments.id', cascadeDelete: true},
961
+ in_reply_to_id: {type: 'string', maxlength: 24, nullable: true, unique: false, references: 'comments.id', setNullDelete: true},
961
962
  status: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'published', validations: {isIn: [['published', 'hidden', 'deleted']]}},
962
963
  html: {type: 'text', maxlength: 1000000000, fieldtype: 'long', nullable: true},
963
964
  edited_at: {type: 'dateTime', nullable: true},
@@ -985,7 +986,9 @@ module.exports = {
985
986
  started_at: {type: 'dateTime', nullable: true},
986
987
  finished_at: {type: 'dateTime', nullable: true},
987
988
  created_at: {type: 'dateTime', nullable: false},
988
- updated_at: {type: 'dateTime', nullable: true}
989
+ updated_at: {type: 'dateTime', nullable: true},
990
+ metadata: {type: 'string', maxlength: 2000, nullable: true},
991
+ queue_entry: {type: 'integer', nullable: true, unsigned: true}
989
992
  },
990
993
  redirects: {
991
994
  id: {type: 'string', maxlength: 24, nullable: false, primary: true},
@@ -46,6 +46,10 @@ const Comment = ghostBookshelf.Model.extend({
46
46
  return this.belongsTo('Comment', 'parent_id');
47
47
  },
48
48
 
49
+ inReplyTo() {
50
+ return this.belongsTo('Comment', 'in_reply_to_id');
51
+ },
52
+
49
53
  likes() {
50
54
  return this.hasMany('CommentLike', 'comment_id');
51
55
  },
@@ -94,6 +98,12 @@ const Comment = ghostBookshelf.Model.extend({
94
98
  }
95
99
  },
96
100
 
101
+ orderAttributes: function orderAttributes() {
102
+ let keys = ghostBookshelf.Model.prototype.orderAttributes.call(this, arguments);
103
+ keys.push('count__likes');
104
+ return keys;
105
+ },
106
+
97
107
  onCreated: function onCreated(model, options) {
98
108
  ghostBookshelf.Model.prototype.onCreated.apply(this, arguments);
99
109
 
@@ -175,25 +185,26 @@ const Comment = ghostBookshelf.Model.extend({
175
185
  /**
176
186
  * We have to ensure consistency. If you listen on model events (e.g. `member.added`), you can expect that you always
177
187
  * receive all fields including relations. Otherwise you can't rely on a consistent flow. And we want to avoid
178
- * that event listeners have to re-fetch a resource. This function is used in the context of inserting
179
- * and updating resources. We won't return the relations by default for now.
188
+ * that event listeners have to re-fetch a resource.
180
189
  */
181
190
  defaultRelations: function defaultRelations(methodName, options) {
182
- // @todo: the default relations are not working for 'add' when we add it below
191
+ // @TODO: the default relations are not working for 'add' when we add it below
192
+ // this is because bookshelf does not automatically call `fetch` after adding so
193
+ // our bookshelf eager-load plugin doesn't use the `withRelated` options
183
194
  if (['findAll', 'findPage', 'edit', 'findOne', 'destroy'].indexOf(methodName) !== -1) {
184
195
  if (!options.withRelated || options.withRelated.length === 0) {
185
196
  if (options.parentId) {
186
197
  // Do not include replies for replies
187
198
  options.withRelated = [
188
199
  // Relations
189
- 'member', 'count.likes', 'count.liked'
200
+ 'inReplyTo', 'member', 'count.likes', 'count.liked'
190
201
  ];
191
202
  } else {
192
203
  options.withRelated = [
193
204
  // Relations
194
- 'member', 'count.replies', 'count.likes', 'count.liked',
205
+ 'member', 'inReplyTo', 'count.replies', 'count.likes', 'count.liked',
195
206
  // Replies (limited to 3)
196
- 'replies', 'replies.member' , 'replies.count.likes', 'replies.count.liked'
207
+ 'replies', 'replies.member', 'replies.inReplyTo', 'replies.count.likes', 'replies.count.liked'
197
208
  ];
198
209
  }
199
210
  }
@@ -202,47 +213,20 @@ const Comment = ghostBookshelf.Model.extend({
202
213
  return options;
203
214
  },
204
215
 
205
- async findMostLikedComment(options = {}) {
206
- let query = ghostBookshelf.knex('comments')
207
- .select('comments.*')
208
- .count('comment_likes.id as count__likes') // Counting likes for sorting
209
- .leftJoin('comment_likes', 'comments.id', 'comment_likes.comment_id')
210
- .groupBy('comments.id') // Group by comment ID to aggregate likes count
211
- .orderBy('count__likes', 'desc') // Order by likes in descending order (most likes first)
212
- .limit(1); // Limit to just 1 result
213
- // Execute the query and get the result
214
- const result = await query.first(); // Fetch the single top comment
215
- const id = result && result.id;
216
- // Fetch the comment model by ID
217
- return this.findOne({id}, options);
218
- },
219
-
220
216
  async findPage(options) {
221
217
  const {withRelated} = this.defaultRelations('findPage', options);
222
-
223
218
  const relationsToLoadIndividually = [
224
219
  'replies',
225
220
  'replies.member',
226
221
  'replies.count.likes',
227
222
  'replies.count.liked'
228
223
  ].filter(relation => withRelated.includes(relation));
229
-
230
224
  const result = await ghostBookshelf.Model.findPage.call(this, options);
231
225
 
232
226
  for (const model of result.data) {
233
227
  await model.load(relationsToLoadIndividually, _.omit(options, 'withRelated'));
234
228
  }
235
229
 
236
- // if options.order === 'best', we findMostLikedComment
237
- // then we remove it from the result set and add it as the first element
238
- if (options.order === 'best' && options.page === '1') {
239
- const mostLikedComment = await this.findMostLikedComment(options);
240
- if (mostLikedComment) {
241
- result.data = result.data.filter(comment => comment.id !== mostLikedComment.id);
242
- result.data.unshift(mostLikedComment);
243
- }
244
- }
245
-
246
230
  return result;
247
231
  },
248
232
 
@@ -0,0 +1,42 @@
1
+ const {ActivityPubService} = require('@tryghost/activitypub');
2
+
3
+ module.exports = class ActivityPubServiceWrapper {
4
+ /** @type ActivityPubService */
5
+ static instance;
6
+
7
+ static async init() {
8
+ if (ActivityPubServiceWrapper.instance) {
9
+ return;
10
+ }
11
+ const labs = require('../../../shared/labs');
12
+
13
+ if (!labs.isSet('ActivityPub')) {
14
+ return;
15
+ }
16
+
17
+ const urlUtils = require('../../../shared/url-utils');
18
+ const siteUrl = new URL(urlUtils.getSiteUrl());
19
+
20
+ const db = require('../../data/db');
21
+ const knex = db.knex;
22
+
23
+ const logging = require('@tryghost/logging');
24
+
25
+ const IdentityTokenServiceWrapper = require('../identity-tokens');
26
+
27
+ if (!IdentityTokenServiceWrapper.instance) {
28
+ logging.error(`IdentityTokenService needs to be initialised before ActivityPubService`);
29
+ }
30
+
31
+ ActivityPubServiceWrapper.instance = new ActivityPubService(
32
+ knex,
33
+ siteUrl,
34
+ logging,
35
+ IdentityTokenServiceWrapper.instance
36
+ );
37
+
38
+ if (labs.isSet('ActivityPub') && IdentityTokenServiceWrapper.instance) {
39
+ await ActivityPubServiceWrapper.instance.initialiseWebhooks();
40
+ }
41
+ }
42
+ };
@@ -0,0 +1 @@
1
+ module.exports = require('./ActivityPubServiceWrapper');
@@ -51,7 +51,8 @@ module.exports = class CommentsController {
51
51
  frame.options.filter = `post_id:${frame.options.post_id}`;
52
52
  }
53
53
  }
54
- return this.service.getComments(frame.options);
54
+
55
+ return await this.service.getComments(frame.options);
55
56
  }
56
57
 
57
58
  /**
@@ -114,6 +115,7 @@ module.exports = class CommentsController {
114
115
  if (data.parent_id) {
115
116
  result = await this.service.replyToComment(
116
117
  data.parent_id,
118
+ data.in_reply_to_id,
117
119
  frame.options.context.member.id,
118
120
  data.html,
119
121
  frame.options
@@ -83,7 +83,10 @@ class CommentsService {
83
83
  await this.emails.notifyPostAuthors(comment);
84
84
 
85
85
  if (comment.get('parent_id')) {
86
- await this.emails.notifyParentCommentAuthor(comment);
86
+ await this.emails.notifyParentCommentAuthor(comment, {type: 'parent'});
87
+ }
88
+ if (comment.get('in_reply_to_id')) {
89
+ await this.emails.notifyParentCommentAuthor(comment, {type: 'in_reply_to'});
87
90
  }
88
91
  }
89
92
 
@@ -253,11 +256,12 @@ class CommentsService {
253
256
 
254
257
  /**
255
258
  * @param {string} parent - The ID of the Comment to reply to
259
+ * @param {string} inReplyTo - The ID of the Reply to reply to
256
260
  * @param {string} member - The ID of the Member to comment as
257
261
  * @param {string} comment - The HTML content of the Comment
258
262
  * @param {any} options
259
263
  */
260
- async replyToComment(parent, member, comment, options) {
264
+ async replyToComment(parent, inReplyTo, member, comment, options) {
261
265
  this.checkEnabled();
262
266
  const memberModel = await this.models.Member.findOne({
263
267
  id: member
@@ -281,6 +285,7 @@ class CommentsService {
281
285
  message: tpl(messages.replyToReply)
282
286
  });
283
287
  }
288
+
284
289
  const postModel = await this.models.Post.findOne({
285
290
  id: parentComment.get('post_id')
286
291
  }, {
@@ -291,10 +296,27 @@ class CommentsService {
291
296
 
292
297
  this.checkPostAccess(postModel, memberModel);
293
298
 
299
+ let inReplyToComment;
300
+ if (parent && inReplyTo) {
301
+ inReplyToComment = await this.getCommentByID(inReplyTo, options);
302
+
303
+ // we only allow references to published comments to avoid leaking
304
+ // hidden data via the snippet included in API responses
305
+ if (inReplyToComment && inReplyToComment.get('status') !== 'published') {
306
+ inReplyToComment = null;
307
+ }
308
+
309
+ // we don't allow in_reply_to references across different parents
310
+ if (inReplyToComment && inReplyToComment.get('parent_id') !== parent) {
311
+ inReplyToComment = null;
312
+ }
313
+ }
314
+
294
315
  const model = await this.models.Comment.add({
295
316
  post_id: parentComment.get('post_id'),
296
317
  member_id: member,
297
318
  parent_id: parentComment.id,
319
+ in_reply_to_id: inReplyToComment && inReplyToComment.get('id'),
298
320
  html: comment,
299
321
  status: 'published'
300
322
  }, options);
@@ -60,8 +60,13 @@ class CommentsServiceEmails {
60
60
  }
61
61
  }
62
62
 
63
- async notifyParentCommentAuthor(reply) {
64
- const parent = await this.models.Comment.findOne({id: reply.get('parent_id')});
63
+ async notifyParentCommentAuthor(reply, {type = 'parent'} = {}) {
64
+ let parent;
65
+ if (type === 'in_reply_to') {
66
+ parent = await this.models.Comment.findOne({id: reply.get('in_reply_to_id')});
67
+ } else {
68
+ parent = await this.models.Comment.findOne({id: reply.get('parent_id')});
69
+ }
65
70
  const parentMember = parent.related('member');
66
71
 
67
72
  if (parent?.get('status') !== 'published' || !parentMember.get('enable_comment_notifications')) {
@@ -1,4 +1,6 @@
1
1
  const logging = require('@tryghost/logging');
2
+ const JobManager = require('../../services/jobs');
3
+ const path = require('path');
2
4
 
3
5
  class EmailAnalyticsServiceWrapper {
4
6
  init() {
@@ -11,7 +13,7 @@ class EmailAnalyticsServiceWrapper {
11
13
  const MailgunProvider = require('@tryghost/email-analytics-provider-mailgun');
12
14
  const {EmailRecipientFailure, EmailSpamComplaintEvent, Email} = require('../../models');
13
15
  const StartEmailAnalyticsJobEvent = require('./events/StartEmailAnalyticsJobEvent');
14
-
16
+ const {MemberEmailAnalyticsUpdateEvent} = require('@tryghost/member-events');
15
17
  const domainEvents = require('@tryghost/domain-events');
16
18
  const config = require('../../../shared/config');
17
19
  const settings = require('../../../shared/settings-cache');
@@ -47,7 +49,8 @@ class EmailAnalyticsServiceWrapper {
47
49
  providers: [
48
50
  new MailgunProvider({config, settings})
49
51
  ],
50
- queries
52
+ queries,
53
+ domainEvents
51
54
  });
52
55
 
53
56
  // We currently cannot trigger a non-offloaded job from the job manager
@@ -55,6 +58,19 @@ class EmailAnalyticsServiceWrapper {
55
58
  domainEvents.subscribe(StartEmailAnalyticsJobEvent, async () => {
56
59
  await this.startFetch();
57
60
  });
61
+
62
+ domainEvents.subscribe(MemberEmailAnalyticsUpdateEvent, async (event) => {
63
+ const memberId = event.data.memberId;
64
+ await JobManager.addQueuedJob({
65
+ name: `update-member-email-analytics-${memberId}`,
66
+ metadata: {
67
+ job: path.resolve(__dirname, 'jobs/update-member-email-analytics'),
68
+ data: {
69
+ memberId
70
+ }
71
+ }
72
+ });
73
+ });
58
74
  }
59
75
 
60
76
  async fetchLatestOpenedEvents({maxEvents} = {maxEvents: Infinity}) {
@@ -0,0 +1,13 @@
1
+ const queries = require('../../lib/queries');
2
+
3
+ /**
4
+ * Updates email analytics for a specific member
5
+ *
6
+ * @param {Object} options - The options object
7
+ * @param {string} options.memberId - The ID of the member to update analytics for
8
+ * @returns {Promise<Object>} The result of the aggregation query (1/0)
9
+ */
10
+ module.exports = async function updateMemberEmailAnalytics({memberId}) {
11
+ const result = await queries.aggregateMemberStats(memberId);
12
+ return result;
13
+ };
@@ -1,5 +1,7 @@
1
+ const debug = require('@tryghost/debug')('i18n');
1
2
  const logging = require('@tryghost/logging');
2
3
  const url = require('../../api/endpoints/utils/serializers/output/utils/url');
4
+ const events = require('../../lib/common/events');
3
5
 
4
6
  class EmailServiceWrapper {
5
7
  getPostUrl(post) {
@@ -49,8 +51,25 @@ class EmailServiceWrapper {
49
51
  const mailgunClient = new MailgunClient({
50
52
  config: configService, settings: settingsCache
51
53
  });
52
- const i18nLanguage = settingsCache.get('locale') || 'en';
54
+ const i18nLanguage = labs.isSet('i18n') ? settingsCache.get('locale') || 'en' : 'en';
53
55
  const i18n = i18nLib(i18nLanguage, 'newsletter');
56
+
57
+ events.on('settings.labs.edited', () => {
58
+ if (labs.isSet('i18n')) {
59
+ debug('labs i18n enabled, updating i18n to', settingsCache.get('locale'));
60
+ i18n.changeLanguage(settingsCache.get('locale'));
61
+ } else {
62
+ debug('labs i18n disabled, updating i18n to en');
63
+ i18n.changeLanguage('en');
64
+ }
65
+ });
66
+
67
+ events.on('settings.locale.edited', (model) => {
68
+ if (labs.isSet('i18n')) {
69
+ debug('locale changed, updating i18n to', model.get('value'));
70
+ i18n.changeLanguage(model.get('value'));
71
+ }
72
+ });
54
73
 
55
74
  const mailgunEmailProvider = new MailgunEmailProvider({
56
75
  mailgunClient,
@@ -0,0 +1,28 @@
1
+ const {IdentityTokenService} = require('@tryghost/identity-token-service');
2
+
3
+ module.exports = class IdentityTokenServiceWrapper {
4
+ /** @type IdentityTokenService */
5
+ static instance;
6
+
7
+ static async init() {
8
+ if (IdentityTokenServiceWrapper.instance) {
9
+ return;
10
+ }
11
+
12
+ const urlUtils = require('../../../shared/url-utils');
13
+ const issuer = urlUtils.urlFor('admin', true);
14
+
15
+ const settings = require('../../../shared/settings-cache');
16
+ const jose = require('node-jose');
17
+
18
+ const privateKey = settings.get('ghost_private_key');
19
+ const keyStore = jose.JWK.createKeyStore();
20
+ const key = await keyStore.add(privateKey, 'pem');
21
+
22
+ IdentityTokenServiceWrapper.instance = new IdentityTokenService(
23
+ privateKey,
24
+ issuer,
25
+ key.kid
26
+ );
27
+ }
28
+ };
@@ -0,0 +1 @@
1
+ module.exports = require('./IdentityTokenServiceWrapper');