ghost 5.38.0 → 5.40.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 (183) hide show
  1. package/components/{tryghost-adapter-cache-memory-ttl-5.38.0.tgz → tryghost-adapter-cache-memory-ttl-5.40.0.tgz} +0 -0
  2. package/components/tryghost-adapter-cache-redis-5.40.0.tgz +0 -0
  3. package/components/tryghost-adapter-manager-5.40.0.tgz +0 -0
  4. package/components/{tryghost-api-framework-5.38.0.tgz → tryghost-api-framework-5.40.0.tgz} +0 -0
  5. package/components/tryghost-api-version-compatibility-service-5.40.0.tgz +0 -0
  6. package/components/tryghost-audience-feedback-5.40.0.tgz +0 -0
  7. package/components/tryghost-bootstrap-socket-5.40.0.tgz +0 -0
  8. package/components/tryghost-constants-5.40.0.tgz +0 -0
  9. package/components/{tryghost-custom-theme-settings-service-5.38.0.tgz → tryghost-custom-theme-settings-service-5.40.0.tgz} +0 -0
  10. package/components/tryghost-data-generator-5.40.0.tgz +0 -0
  11. package/components/{tryghost-domain-events-5.38.0.tgz → tryghost-domain-events-5.40.0.tgz} +0 -0
  12. package/components/tryghost-dynamic-routing-events-5.40.0.tgz +0 -0
  13. package/components/tryghost-email-analytics-provider-mailgun-5.40.0.tgz +0 -0
  14. package/components/tryghost-email-analytics-service-5.40.0.tgz +0 -0
  15. package/components/tryghost-email-content-generator-5.40.0.tgz +0 -0
  16. package/components/tryghost-email-events-5.40.0.tgz +0 -0
  17. package/components/tryghost-email-service-5.40.0.tgz +0 -0
  18. package/components/{tryghost-email-suppression-list-5.38.0.tgz → tryghost-email-suppression-list-5.40.0.tgz} +0 -0
  19. package/components/tryghost-event-aware-cache-wrapper-5.40.0.tgz +0 -0
  20. package/components/{tryghost-express-dynamic-redirects-5.38.0.tgz → tryghost-express-dynamic-redirects-5.40.0.tgz} +0 -0
  21. package/components/tryghost-external-media-inliner-5.40.0.tgz +0 -0
  22. package/components/tryghost-extract-api-key-5.40.0.tgz +0 -0
  23. package/components/tryghost-html-to-plaintext-5.40.0.tgz +0 -0
  24. package/components/tryghost-i18n-5.40.0.tgz +0 -0
  25. package/components/tryghost-importer-handler-content-files-5.40.0.tgz +0 -0
  26. package/components/tryghost-importer-revue-5.40.0.tgz +0 -0
  27. package/components/tryghost-job-manager-5.40.0.tgz +0 -0
  28. package/components/tryghost-link-redirects-5.40.0.tgz +0 -0
  29. package/components/tryghost-link-replacer-5.40.0.tgz +0 -0
  30. package/components/{tryghost-link-tracking-5.38.0.tgz → tryghost-link-tracking-5.40.0.tgz} +0 -0
  31. package/components/tryghost-magic-link-5.40.0.tgz +0 -0
  32. package/components/tryghost-mailgun-client-5.40.0.tgz +0 -0
  33. package/components/tryghost-member-attribution-5.40.0.tgz +0 -0
  34. package/components/tryghost-member-events-5.40.0.tgz +0 -0
  35. package/components/tryghost-members-api-5.40.0.tgz +0 -0
  36. package/components/tryghost-members-csv-5.40.0.tgz +0 -0
  37. package/components/{tryghost-members-events-service-5.38.0.tgz → tryghost-members-events-service-5.40.0.tgz} +0 -0
  38. package/components/tryghost-members-importer-5.40.0.tgz +0 -0
  39. package/components/tryghost-members-offers-5.40.0.tgz +0 -0
  40. package/components/tryghost-members-payments-5.40.0.tgz +0 -0
  41. package/components/tryghost-members-ssr-5.40.0.tgz +0 -0
  42. package/components/tryghost-members-stripe-service-5.40.0.tgz +0 -0
  43. package/components/tryghost-mentions-email-report-5.40.0.tgz +0 -0
  44. package/components/tryghost-milestones-5.40.0.tgz +0 -0
  45. package/components/{tryghost-minifier-5.38.0.tgz → tryghost-minifier-5.40.0.tgz} +0 -0
  46. package/components/tryghost-mw-api-version-mismatch-5.40.0.tgz +0 -0
  47. package/components/{tryghost-mw-cache-control-5.38.0.tgz → tryghost-mw-cache-control-5.40.0.tgz} +0 -0
  48. package/components/tryghost-mw-error-handler-5.40.0.tgz +0 -0
  49. package/components/tryghost-mw-session-from-token-5.40.0.tgz +0 -0
  50. package/components/tryghost-mw-update-user-last-seen-5.40.0.tgz +0 -0
  51. package/components/{tryghost-mw-version-match-5.38.0.tgz → tryghost-mw-version-match-5.40.0.tgz} +0 -0
  52. package/components/tryghost-mw-vhost-5.40.0.tgz +0 -0
  53. package/components/tryghost-oembed-service-5.40.0.tgz +0 -0
  54. package/components/tryghost-package-json-5.40.0.tgz +0 -0
  55. package/components/tryghost-posts-service-5.40.0.tgz +0 -0
  56. package/components/tryghost-referrers-5.40.0.tgz +0 -0
  57. package/components/tryghost-security-5.40.0.tgz +0 -0
  58. package/components/tryghost-session-service-5.40.0.tgz +0 -0
  59. package/components/tryghost-settings-path-manager-5.40.0.tgz +0 -0
  60. package/components/tryghost-slack-notifications-5.40.0.tgz +0 -0
  61. package/components/tryghost-staff-service-5.40.0.tgz +0 -0
  62. package/components/tryghost-stats-service-5.40.0.tgz +0 -0
  63. package/components/tryghost-tiers-5.40.0.tgz +0 -0
  64. package/components/{tryghost-update-check-service-5.38.0.tgz → tryghost-update-check-service-5.40.0.tgz} +0 -0
  65. package/components/tryghost-verification-trigger-5.40.0.tgz +0 -0
  66. package/components/tryghost-version-notifications-data-service-5.40.0.tgz +0 -0
  67. package/components/tryghost-webmentions-5.40.0.tgz +0 -0
  68. package/core/boot.js +7 -0
  69. package/core/built/admin/assets/{chunk.143.c6802c882a911797ce4f.js → chunk.143.9e105aa0a9236484523a.js} +8 -8
  70. package/core/built/admin/assets/{chunk.178.09faefd4027fcba4113d.js → chunk.178.9b53f20952b9ae4763a2.js} +4 -4
  71. package/core/built/admin/assets/{chunk.220.9ca2950240aba3fced21.js → chunk.462.b66cfed1f66c8d2678b2.js} +9407 -9041
  72. package/core/built/admin/assets/chunk.808.2e76eb12fa4d7be8cb23.js +5 -0
  73. package/core/built/admin/assets/{ghost-35103ff053c43f1dfa7f35821c3c2412.js → ghost-467b96c17c6bc0d06fa88e85074fbecf.js} +299 -275
  74. package/core/built/admin/assets/ghost-68ea49029c6d45b4aa090f4e218917aa.css +1 -0
  75. package/core/built/admin/assets/ghost-dark-c9b38252afc29364507e8c92e4ba9933.css +1 -0
  76. package/core/built/admin/assets/img/latest-posts-1-ea1e669b275f2c7dfa23abd4be8d7ad5.png +0 -0
  77. package/core/built/admin/assets/img/latest-posts-2-e6b0809353ac31642839a6f3bab0d4e8.png +0 -0
  78. package/core/built/admin/assets/img/latest-posts-3-ac8c2e95dd9adecb9029a7b037ed7f15.png +0 -0
  79. package/core/built/admin/assets/{vendor-b982e3bf1020bff77b2a3c44d5f59e55.js → vendor-d08b4de6f990b9cd7b5a92d61952a5bb.js} +54 -49
  80. package/core/built/admin/index.html +6 -6
  81. package/core/frontend/meta/schema.js +4 -16
  82. package/core/server/api/endpoints/db.js +10 -2
  83. package/core/server/api/endpoints/members.js +8 -1
  84. package/core/server/api/endpoints/posts.js +30 -1
  85. package/core/server/api/endpoints/utils/serializers/output/mappers/emails.js +2 -1
  86. package/core/server/api/endpoints/utils/serializers/output/posts.js +5 -0
  87. package/core/server/data/importer/import-manager.js +3 -8
  88. package/core/server/data/migrations/versions/5.39/2023-03-13-09-29-add-newsletter-show-post-title-section.js +7 -0
  89. package/core/server/data/migrations/versions/5.39/2023-03-13-13-11-add-newsletter-show-comment-cta.js +7 -0
  90. package/core/server/data/migrations/versions/5.39/2023-03-13-14-30-add-newsletter-show-subscription-details.js +7 -0
  91. package/core/server/data/migrations/versions/5.39/2023-03-14-12-26-add-last-mentions-email-report-timestamp-setting.js +8 -0
  92. package/core/server/data/migrations/versions/5.40/2023-03-13-14-05-add-newsletter-show-latest-posts.js +7 -0
  93. package/core/server/data/migrations/versions/5.40/2023-03-21-18-42-add-self-serve-integration-role.js +31 -0
  94. package/core/server/data/migrations/versions/5.40/2023-03-21-18-43-add-self-serve-migration-and-permissions.js +25 -0
  95. package/core/server/data/migrations/versions/5.40/2023-03-21-18-52-add-self-serve-integration.js +37 -0
  96. package/core/server/data/migrations/versions/5.40/2023-03-21-19-02-add-self-serve-integration-api-key.js +72 -0
  97. package/core/server/data/schema/default-settings/default-settings.json +4 -0
  98. package/core/server/data/schema/fixtures/fixtures.json +21 -0
  99. package/core/server/data/schema/schema.js +4 -0
  100. package/core/server/lib/request-external.js +8 -4
  101. package/core/server/models/base/plugins/relations.js +22 -0
  102. package/core/server/models/base/plugins/sanitize.js +1 -1
  103. package/core/server/models/newsletter.js +4 -0
  104. package/core/server/services/email-service/wrapper.js +4 -1
  105. package/core/server/services/i18n.js +14 -0
  106. package/core/server/services/member-attribution/index.js +2 -1
  107. package/core/server/services/members/api.js +43 -42
  108. package/core/server/services/members/emails/signin.js +9 -9
  109. package/core/server/services/mentions/BookshelfMentionRepository.js +11 -0
  110. package/core/server/services/mentions/service.js +4 -0
  111. package/core/server/services/mentions-email-report/StartMentionEmailReportJob.js +16 -0
  112. package/core/server/services/mentions-email-report/index.js +1 -0
  113. package/core/server/services/mentions-email-report/job.js +11 -0
  114. package/core/server/services/mentions-email-report/service.js +162 -0
  115. package/core/server/services/milestones/BookshelfMilestoneRepository.js +18 -6
  116. package/core/server/services/milestones/service.js +17 -15
  117. package/core/server/services/posts/posts-service.js +22 -119
  118. package/core/server/services/public-config/config.js +1 -1
  119. package/core/server/services/slack-notifications/service.js +1 -2
  120. package/core/server/services/webhooks/trigger.js +2 -2
  121. package/core/server/web/api/endpoints/admin/routes.js +2 -1
  122. package/core/shared/config/defaults.json +3 -4
  123. package/core/shared/config/overrides.json +3 -1
  124. package/core/shared/labs.js +3 -4
  125. package/package.json +149 -142
  126. package/yarn.lock +3544 -1240
  127. package/components/tryghost-adapter-cache-redis-5.38.0.tgz +0 -0
  128. package/components/tryghost-adapter-manager-5.38.0.tgz +0 -0
  129. package/components/tryghost-api-version-compatibility-service-5.38.0.tgz +0 -0
  130. package/components/tryghost-audience-feedback-5.38.0.tgz +0 -0
  131. package/components/tryghost-bootstrap-socket-5.38.0.tgz +0 -0
  132. package/components/tryghost-constants-5.38.0.tgz +0 -0
  133. package/components/tryghost-data-generator-5.38.0.tgz +0 -0
  134. package/components/tryghost-dynamic-routing-events-5.38.0.tgz +0 -0
  135. package/components/tryghost-email-analytics-provider-mailgun-5.38.0.tgz +0 -0
  136. package/components/tryghost-email-analytics-service-5.38.0.tgz +0 -0
  137. package/components/tryghost-email-content-generator-5.38.0.tgz +0 -0
  138. package/components/tryghost-email-events-5.38.0.tgz +0 -0
  139. package/components/tryghost-email-service-5.38.0.tgz +0 -0
  140. package/components/tryghost-event-aware-cache-wrapper-5.38.0.tgz +0 -0
  141. package/components/tryghost-external-media-inliner-5.38.0.tgz +0 -0
  142. package/components/tryghost-extract-api-key-5.38.0.tgz +0 -0
  143. package/components/tryghost-html-to-plaintext-5.38.0.tgz +0 -0
  144. package/components/tryghost-i18n-5.38.0.tgz +0 -0
  145. package/components/tryghost-importer-handler-content-files-5.38.0.tgz +0 -0
  146. package/components/tryghost-importer-revue-5.38.0.tgz +0 -0
  147. package/components/tryghost-job-manager-5.38.0.tgz +0 -0
  148. package/components/tryghost-link-redirects-5.38.0.tgz +0 -0
  149. package/components/tryghost-link-replacer-5.38.0.tgz +0 -0
  150. package/components/tryghost-magic-link-5.38.0.tgz +0 -0
  151. package/components/tryghost-mailgun-client-5.38.0.tgz +0 -0
  152. package/components/tryghost-member-attribution-5.38.0.tgz +0 -0
  153. package/components/tryghost-member-events-5.38.0.tgz +0 -0
  154. package/components/tryghost-members-api-5.38.0.tgz +0 -0
  155. package/components/tryghost-members-csv-5.38.0.tgz +0 -0
  156. package/components/tryghost-members-importer-5.38.0.tgz +0 -0
  157. package/components/tryghost-members-offers-5.38.0.tgz +0 -0
  158. package/components/tryghost-members-payments-5.38.0.tgz +0 -0
  159. package/components/tryghost-members-ssr-5.38.0.tgz +0 -0
  160. package/components/tryghost-members-stripe-service-5.38.0.tgz +0 -0
  161. package/components/tryghost-milestones-5.38.0.tgz +0 -0
  162. package/components/tryghost-mw-api-version-mismatch-5.38.0.tgz +0 -0
  163. package/components/tryghost-mw-error-handler-5.38.0.tgz +0 -0
  164. package/components/tryghost-mw-session-from-token-5.38.0.tgz +0 -0
  165. package/components/tryghost-mw-update-user-last-seen-5.38.0.tgz +0 -0
  166. package/components/tryghost-mw-vhost-5.38.0.tgz +0 -0
  167. package/components/tryghost-oembed-service-5.38.0.tgz +0 -0
  168. package/components/tryghost-package-json-5.38.0.tgz +0 -0
  169. package/components/tryghost-referrers-5.38.0.tgz +0 -0
  170. package/components/tryghost-security-5.38.0.tgz +0 -0
  171. package/components/tryghost-session-service-5.38.0.tgz +0 -0
  172. package/components/tryghost-settings-path-manager-5.38.0.tgz +0 -0
  173. package/components/tryghost-slack-notifications-5.38.0.tgz +0 -0
  174. package/components/tryghost-staff-service-5.38.0.tgz +0 -0
  175. package/components/tryghost-stats-service-5.38.0.tgz +0 -0
  176. package/components/tryghost-tiers-5.38.0.tgz +0 -0
  177. package/components/tryghost-verification-trigger-5.38.0.tgz +0 -0
  178. package/components/tryghost-version-notifications-data-service-5.38.0.tgz +0 -0
  179. package/components/tryghost-webmentions-5.38.0.tgz +0 -0
  180. package/core/built/admin/assets/chunk.79.acb7dd01e1c785f4920c.js +0 -287
  181. package/core/built/admin/assets/ghost-a9307c9cfe26a4bc621e02cd3bae421a.css +0 -1
  182. package/core/built/admin/assets/ghost-dark-f309cf445255344e4861a95ecb8f1920.css +0 -1
  183. /package/core/built/admin/assets/{chunk.220.9ca2950240aba3fced21.js.LICENSE.txt → chunk.462.b66cfed1f66c8d2678b2.js.LICENSE.txt} +0 -0
@@ -17,6 +17,7 @@ const tiersService = require('../tiers');
17
17
  const newslettersService = require('../newsletters');
18
18
  const memberAttributionService = require('../member-attribution');
19
19
  const emailSuppressionList = require('../email-suppression-list');
20
+ const {t} = require('../i18n');
20
21
 
21
22
  const MAGIC_LINK_TOKEN_VALIDITY = 24 * 60 * 60 * 1000;
22
23
  const MAGIC_LINK_TOKEN_VALIDITY_AFTER_USAGE = 10 * 60 * 1000;
@@ -58,16 +59,16 @@ function createApiInstance(config) {
58
59
  const siteTitle = settingsCache.get('title');
59
60
  switch (type) {
60
61
  case 'subscribe':
61
- return `📫 Confirm your subscription to ${siteTitle}`;
62
+ return `📫 ${t(`Confirm your subscription to {{siteTitle}}`, {siteTitle})}`;
62
63
  case 'signup':
63
- return `🙌 Complete your sign up to ${siteTitle}!`;
64
+ return `🙌 ${t(`Complete your sign up to {{siteTitle}}!`, {siteTitle})}`;
64
65
  case 'signup-paid':
65
- return `🙌 Thank you for signing up to ${siteTitle}!`;
66
+ return `🙌 ${t(`Thank you for signing up to {{siteTitle}}!`, {siteTitle})}`;
66
67
  case 'updateEmail':
67
- return `📫 Confirm your email update for ${siteTitle}!`;
68
+ return `📫 ${t(`Confirm your email update for {{siteTitle}}!`, {siteTitle})}`;
68
69
  case 'signin':
69
70
  default:
70
- return `🔑 Secure sign in link for ${siteTitle}`;
71
+ return `🔑 ${t(`Secure sign in link for {{siteTitle}}`, {siteTitle})}`;
71
72
  }
72
73
  },
73
74
  getText(url, type, email) {
@@ -75,87 +76,87 @@ function createApiInstance(config) {
75
76
  switch (type) {
76
77
  case 'subscribe':
77
78
  return `
78
- Hey there,
79
+ ${t(`Hey there,`)}
79
80
 
80
- You're one tap away from subscribing to ${siteTitle} — please confirm your email address with this link:
81
+ ${t('You\'re one tap away from subscribing to {{siteTitle}} — please confirm your email address with this link:', {siteTitle})}
81
82
 
82
83
  ${url}
83
84
 
84
- For your security, the link will expire in 24 hours time.
85
+ ${t('For your security, the link will expire in 24 hours time.')}
85
86
 
86
- All the best!
87
+ ${t('All the best!')}
87
88
 
88
89
  ---
89
90
 
90
- Sent to ${email}
91
- If you did not make this request, you can simply delete this message. You will not be subscribed.
91
+ ${t('Sent to {{email}}', {email})}
92
+ ${t('If you did not make this request, you can simply delete this message.')} ${t('You will not be subscribed.')}
92
93
  `;
93
94
  case 'signup':
94
95
  return `
95
- Hey there!
96
+ ${t(`Hey there,`)}
96
97
 
97
- Tap the link below to complete the signup process for ${siteTitle}, and be automatically signed in:
98
+ ${t('Tap the link below to complete the signup process for {{siteTitle}}, and be automatically signed in:', {siteTitle})}
98
99
 
99
100
  ${url}
100
101
 
101
- For your security, the link will expire in 24 hours time.
102
+ ${t('For your security, the link will expire in 24 hours time.')}
102
103
 
103
- See you soon!
104
+ ${t('See you soon!')}
104
105
 
105
106
  ---
106
107
 
107
- Sent to ${email}
108
- If you did not make this request, you can simply delete this message. You will not be signed up, and no account will be created for you.
108
+ ${t('Sent to {{email}}', {email})}
109
+ ${t('If you did not make this request, you can simply delete this message.')} ${t('You will not be signed up, and no account will be created for you.')}
109
110
  `;
110
111
  case 'signup-paid':
111
112
  return `
112
- Hey there!
113
+ ${t(`Hey there,`)}
113
114
 
114
- Thank you for subscribing to ${siteTitle}. Tap the link below to be automatically signed in:
115
+ ${t('Thank you for subscribing to {{siteTitle}}. Tap the link below to be automatically signed in:', {siteTitle})}
115
116
 
116
117
  ${url}
117
118
 
118
- For your security, the link will expire in 24 hours time.
119
+ ${t('For your security, the link will expire in 24 hours time.')}
119
120
 
120
- See you soon!
121
+ ${t('See you soon!')}
121
122
 
122
123
  ---
123
124
 
124
- Sent to ${email}
125
- Thank you for subscribing to ${siteTitle}!
125
+ ${t('Sent to {{email}}', {email})}
126
+ ${t('Thank you for subscribing to {{siteTitle}}!', {siteTitle})}
126
127
  `;
127
128
  case 'updateEmail':
128
129
  return `
129
- Hey there,
130
+ ${t(`Hey there,`)}
130
131
 
131
- Please confirm your email address with this link:
132
+ ${t('Please confirm your email address with this link:')}
132
133
 
133
- ${url}
134
+ ${url}
134
135
 
135
- For your security, the link will expire in 24 hours time.
136
+ ${t('For your security, the link will expire in 24 hours time.')}
136
137
 
137
- ---
138
+ ---
138
139
 
139
- Sent to ${email}
140
- If you did not make this request, you can simply delete this message. This email address will not be used.
141
- `;
140
+ ${t('Sent to {{email}}', {email})}
141
+ ${t('If you did not make this request, you can simply delete this message.')} ${t('This email address will not be used.')}
142
+ `;
142
143
  case 'signin':
143
144
  default:
144
145
  return `
145
- Hey there,
146
+ ${t(`Hey there,`)}
146
147
 
147
- Welcome back! Use this link to securely sign in to your ${siteTitle} account:
148
+ ${t('Welcome back! Use this link to securely sign in to your {{siteTitle}} account:', {siteTitle})}
148
149
 
149
150
  ${url}
150
151
 
151
- For your security, the link will expire in 24 hours time.
152
+ ${t('For your security, the link will expire in 24 hours time.')}
152
153
 
153
- See you soon!
154
+ ${t('See you soon!')}
154
155
 
155
156
  ---
156
157
 
157
- Sent to ${email}
158
- If you did not make this request, you can safely ignore this email.
158
+ ${t('Sent to {{email}}', {email})}
159
+ ${t('If you did not make this request, you can safely ignore this email.')}
159
160
  `;
160
161
  }
161
162
  },
@@ -167,16 +168,16 @@ function createApiInstance(config) {
167
168
  const accentColor = settingsCache.get('accent_color');
168
169
  switch (type) {
169
170
  case 'subscribe':
170
- return subscribeEmail({url, email, siteTitle, accentColor, siteDomain, siteUrl});
171
+ return subscribeEmail({t, url, email, siteTitle, accentColor, siteDomain, siteUrl});
171
172
  case 'signup':
172
- return signupEmail({url, email, siteTitle, accentColor, siteDomain, siteUrl});
173
+ return signupEmail({t, url, email, siteTitle, accentColor, siteDomain, siteUrl});
173
174
  case 'signup-paid':
174
- return signupPaidEmail({url, email, siteTitle, accentColor, siteDomain, siteUrl});
175
+ return signupPaidEmail({t, url, email, siteTitle, accentColor, siteDomain, siteUrl});
175
176
  case 'updateEmail':
176
- return updateEmail({url, email, siteTitle, accentColor, siteDomain, siteUrl});
177
+ return updateEmail({t, url, email, siteTitle, accentColor, siteDomain, siteUrl});
177
178
  case 'signin':
178
179
  default:
179
- return signinEmail({url, email, siteTitle, accentColor, siteDomain, siteUrl});
180
+ return signinEmail({t, url, email, siteTitle, accentColor, siteDomain, siteUrl});
180
181
  }
181
182
  }
182
183
  },
@@ -1,10 +1,10 @@
1
- module.exports = ({siteTitle, email, url, accentColor = '#15212A', siteDomain, siteUrl}) => `
1
+ module.exports = ({t, siteTitle, email, url, accentColor = '#15212A', siteDomain, siteUrl}) => `
2
2
  <!doctype html>
3
3
  <html>
4
4
  <head>
5
5
  <meta name="viewport" content="width=device-width">
6
6
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
7
- <title>🔑 Secure sign in link for ${siteTitle}</title>
7
+ <title>🔑 ${t('Secure sign in link for {{siteTitle}}', {siteTitle})}</title>
8
8
  <style>
9
9
  /* -------------------------------------
10
10
  RESPONSIVE AND MOBILE FRIENDLY STYLES
@@ -107,7 +107,7 @@ module.exports = ({siteTitle, email, url, accentColor = '#15212A', siteDomain, s
107
107
  <div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 600px; padding: 30px 20px;">
108
108
 
109
109
  <!-- START CENTERED CONTAINER -->
110
- <span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">Welcome back to ${siteTitle}!</span>
110
+ <span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">${t('Welcome back to {{siteTitle}}!', {siteTitle})}</span>
111
111
  <table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 8px;">
112
112
 
113
113
  <!-- START MAIN CONTENT AREA -->
@@ -116,8 +116,8 @@ module.exports = ({siteTitle, email, url, accentColor = '#15212A', siteDomain, s
116
116
  <table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
117
117
  <tr>
118
118
  <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">
119
- <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 20px; color: #15212A; font-weight: bold; line-height: 25px; margin: 0; margin-bottom: 15px;">Hey there,</p>
120
- <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 32px;">Welcome back! Use this link to securely sign in to your ${siteTitle} account:</p>
119
+ <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 20px; color: #15212A; font-weight: bold; line-height: 25px; margin: 0; margin-bottom: 15px;">${t('Hey there,')}</p>
120
+ <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 32px;">${t('Welcome back! Use this link to securely sign in to your {{siteTitle}} account:', {siteTitle})}</p>
121
121
  <table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
122
122
  <tbody>
123
123
  <tr>
@@ -125,7 +125,7 @@ module.exports = ({siteTitle, email, url, accentColor = '#15212A', siteDomain, s
125
125
  <table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;">
126
126
  <tbody>
127
127
  <tr>
128
- <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; vertical-align: top; background-color: ${accentColor}; border-radius: 5px; text-align: center;"> <a href="${url}" target="_blank" style="display: inline-block; color: #ffffff; background-color: ${accentColor}; border: solid 1px ${accentColor}; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 16px; font-weight: normal; margin: 0; padding: 9px 22px 10px; border-color: ${accentColor};">Sign in to ${siteTitle}</a> </td>
128
+ <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; vertical-align: top; background-color: ${accentColor}; border-radius: 5px; text-align: center;"> <a href="${url}" target="_blank" style="display: inline-block; color: #ffffff; background-color: ${accentColor}; border: solid 1px ${accentColor}; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 16px; font-weight: normal; margin: 0; padding: 9px 22px 10px; border-color: ${accentColor};">${t('Sign in to {{siteTitle}}', {siteTitle})}</a> </td>
129
129
  </tr>
130
130
  </tbody>
131
131
  </table>
@@ -133,10 +133,10 @@ module.exports = ({siteTitle, email, url, accentColor = '#15212A', siteDomain, s
133
133
  </tr>
134
134
  </tbody>
135
135
  </table>
136
- <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; line-height: 25px; margin: 0; margin-bottom: 25px;">For your security, the link will expire in 24 hours time.</p>
137
- <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; line-height: 25px; margin: 0; margin-bottom: 30px;">See you soon!</p>
136
+ <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; line-height: 25px; margin: 0; margin-bottom: 25px;">${t('For your security, the link will expire in 24 hours time.')}</p>
137
+ <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; line-height: 25px; margin: 0; margin-bottom: 30px;">${t('See you soon!')}</p>
138
138
  <hr/>
139
- <p style="word-break: break-all; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 5px;">You can also copy & paste this URL into your browser:</p>
139
+ <p style="word-break: break-all; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 5px;">${t('You can also copy & paste this URL into your browser:')}</p>
140
140
  <p style="word-break: break-all; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; line-height: 25px; margin-top:0; color: #3A464C;">${url}</p>
141
141
  </td>
142
142
  </tr>
@@ -12,6 +12,7 @@ const logging = require('@tryghost/logging');
12
12
 
13
13
  /**
14
14
  * @typedef {import('@tryghost/webmentions/lib/MentionsAPI').GetPageOptions} GetPageOptions
15
+ * @typedef {import('@tryghost/webmentions/lib/MentionsAPI').GetAllOptions} GetAllOptions
15
16
  */
16
17
 
17
18
  /**
@@ -84,6 +85,16 @@ module.exports = class BookshelfMentionRepository {
84
85
  };
85
86
  }
86
87
 
88
+ /**
89
+ * @param {GetAllOptions} options
90
+ * @returns {Promise<import('@tryghost/webmentions/lib/Mention')[]>}
91
+ */
92
+ async getAll(options) {
93
+ const models = await this.#MentionModel.findAll(options);
94
+
95
+ return await Promise.all(models.map(model => this.#modelToMention(model)));
96
+ }
97
+
87
98
  /**
88
99
  * @param {URL} source
89
100
  * @param {URL} target
@@ -25,6 +25,8 @@ function getPostUrl(post) {
25
25
  }
26
26
 
27
27
  module.exports = {
28
+ /** @type {import('@tryghost/webmentions/lib/MentionsAPI')} */
29
+ api: null,
28
30
  controller: new MentionController(),
29
31
  didInit: false,
30
32
  async init() {
@@ -56,6 +58,8 @@ module.exports = {
56
58
  routingService
57
59
  });
58
60
 
61
+ this.api = api;
62
+
59
63
  this.controller.init({
60
64
  api,
61
65
  jobService: {
@@ -0,0 +1,16 @@
1
+ module.exports = class StartMentionEmailReportJob {
2
+ /**
3
+ * @param {Date} timestamp
4
+ */
5
+ constructor(timestamp) {
6
+ this.data = null;
7
+ this.timestamp = timestamp;
8
+ }
9
+
10
+ /**
11
+ * @param {Date} [timestamp]
12
+ */
13
+ static create(data, timestamp) {
14
+ return new StartMentionEmailReportJob(timestamp ?? new Date);
15
+ }
16
+ };
@@ -0,0 +1 @@
1
+ module.exports = require('./service');
@@ -0,0 +1,11 @@
1
+ const {parentPort} = require('worker_threads');
2
+ const StartMentionEmailReportJob = require('./StartMentionEmailReportJob');
3
+
4
+ if (parentPort) {
5
+ parentPort.postMessage({
6
+ event: {
7
+ type: StartMentionEmailReportJob.name
8
+ }
9
+ });
10
+ parentPort.postMessage('done');
11
+ }
@@ -0,0 +1,162 @@
1
+ const MentionEmailReportJob = require('@tryghost/mentions-email-report');
2
+
3
+ /**
4
+ * @typedef {import('@tryghost/mentions-email-report/lib/mentions-email-report').MentionReport} MentionReport
5
+ * @typedef {import('@tryghost/mentions-email-report/lib/mentions-email-report').MentionReportRecipient} MentionReportRecipient
6
+ */
7
+
8
+ let initialised = false;
9
+
10
+ module.exports = {
11
+ async init() {
12
+ if (initialised) {
13
+ return;
14
+ }
15
+
16
+ const mentions = require('../mentions');
17
+ const mentionReportGenerator = {
18
+ getMentionReport(startDate, endDate) {
19
+ return mentions.api.getMentionReport(startDate, endDate);
20
+ }
21
+ };
22
+
23
+ const models = require('../../models');
24
+ const mentionReportRecipientRepository = {
25
+ async getMentionReportRecipients() {
26
+ const users = await models.User.getEmailAlertUsers('mention-received');
27
+ return users.map((model) => {
28
+ return {
29
+ email: model.email,
30
+ slug: model.slug
31
+ };
32
+ });
33
+ }
34
+ };
35
+
36
+ const staffService = require('../staff');
37
+ const mentionReportEmailView = {
38
+ /**
39
+ * @returns {Promise<string>}
40
+ */
41
+ async renderSubject(report) {
42
+ const sourceSiteTitles = report?.mentions?.map(mention => mention.sourceSiteTitle);
43
+ const uniqueSourceSiteTitles = [...new Set(sourceSiteTitles)];
44
+ const totalSiteMentions = uniqueSourceSiteTitles.length;
45
+ const firstMentionSite = uniqueSourceSiteTitles[0];
46
+
47
+ let subject = 'Mention Report';
48
+
49
+ if (totalSiteMentions === 1) {
50
+ subject = `${firstMentionSite} mentioned you`;
51
+ } else if (totalSiteMentions === 2) {
52
+ subject = `${firstMentionSite} & 1 other mentioned you`;
53
+ } else if (totalSiteMentions > 2) {
54
+ subject = `${firstMentionSite} & ${totalSiteMentions - 1} others mentioned you`;
55
+ }
56
+
57
+ return subject;
58
+ },
59
+
60
+ /**
61
+ * @param {MentionReport} report
62
+ * @param {MentionReportRecipient} recipient
63
+ * @returns {Promise<string>}
64
+ */
65
+ async renderHTML(report, recipient) {
66
+ // Filter out mentions with duplicate source url from the report
67
+ const uniqueMentions = report.mentions.filter((mention, index, self) => {
68
+ return self.findIndex(m => m.sourceUrl.href === mention.sourceUrl.href) === index;
69
+ });
70
+
71
+ return staffService.api.emails.renderHTML('mention-report', {
72
+ mentions: uniqueMentions,
73
+ recipient: recipient,
74
+ hasMoreMentions: report.mentions.length > 5
75
+ });
76
+ },
77
+
78
+ /**
79
+ * @param {MentionReport} report
80
+ * @param {MentionReportRecipient} recipient
81
+ * @returns {Promise<string>}
82
+ */
83
+ async renderText(report, recipient) {
84
+ return staffService.api.emails.renderText('mention-report', {
85
+ report: report,
86
+ recipient: recipient
87
+ });
88
+ }
89
+ };
90
+
91
+ const settingsCache = require('../../../shared/settings-cache');
92
+ const mentionReportHistoryService = {
93
+ async getLatestReportDate() {
94
+ const setting = settingsCache.get('last_mentions_report_email_timestamp');
95
+ const parsedInt = parseInt(setting);
96
+
97
+ // Protect against missing/bad data
98
+ if (Number.isNaN(parsedInt) || !parsedInt) {
99
+ const date = new Date();
100
+ date.setDate(date.getDate() - 1);
101
+ return date;
102
+ }
103
+
104
+ return new Date(parsedInt);
105
+ },
106
+ async setLatestReportDate(date) {
107
+ await models.Settings.edit({
108
+ key: 'last_mentions_report_email_timestamp',
109
+ value: date.getTime()
110
+ });
111
+ }
112
+ };
113
+
114
+ const mail = require('../mail');
115
+ const mailer = new mail.GhostMailer();
116
+ const emailService = {
117
+ async send(to, subject, html, text) {
118
+ return mailer.send({
119
+ to,
120
+ subject,
121
+ html,
122
+ text
123
+ });
124
+ }
125
+ };
126
+
127
+ const job = new MentionEmailReportJob({
128
+ mentionReportGenerator,
129
+ mentionReportRecipientRepository,
130
+ mentionReportEmailView,
131
+ mentionReportHistoryService,
132
+ emailService
133
+ });
134
+
135
+ const mentionsJobs = require('../mentions-jobs');
136
+
137
+ const DomainEvents = require('@tryghost/domain-events');
138
+ const StartMentionEmailReportJob = require('./StartMentionEmailReportJob');
139
+
140
+ const labs = require('../../../shared/labs');
141
+ DomainEvents.subscribe(StartMentionEmailReportJob, () => {
142
+ if (labs.isSet('webmentionEmails')) {
143
+ job.sendLatestReport();
144
+ }
145
+ });
146
+
147
+ // Kick off the job on boot, this will make sure that we send a missing report if needed
148
+ DomainEvents.dispatch(StartMentionEmailReportJob.create());
149
+
150
+ const s = Math.floor(Math.random() * 60); // 0-59
151
+ const m = Math.floor(Math.random() * 60); // 0-59
152
+
153
+ // Schedules a job every hour at a random minute and second to send the latest report
154
+ mentionsJobs.addJob({
155
+ name: 'mentions-email-report',
156
+ job: require('path').resolve(__dirname, './job.js'),
157
+ at: `${s} ${m} * * * *`
158
+ });
159
+
160
+ initialised = true;
161
+ }
162
+ };
@@ -68,9 +68,9 @@ module.exports = class BookshelfMilestoneRepository {
68
68
  * @param {'arr'|'members'} type
69
69
  * @param {string} [currency]
70
70
  *
71
- * @returns {Promise<import('@tryghost/milestones/lib/Milestone')|null>}
71
+ * @returns {Promise<import('@tryghost/milestones/lib/Milestone')[]>}
72
72
  */
73
- async getLatestByType(type, currency = 'usd') {
73
+ async getAllByType(type, currency = 'usd') {
74
74
  let milestone = null;
75
75
 
76
76
  if (type === 'arr') {
@@ -80,12 +80,24 @@ module.exports = class BookshelfMilestoneRepository {
80
80
  }
81
81
 
82
82
  if (!milestone || !milestone?.models?.length) {
83
- return null;
84
- } else {
85
- milestone = milestone.models?.[0];
83
+ return [];
86
84
  }
87
85
 
88
- return this.#modelToMilestone(milestone);
86
+ const milestones = await Promise.all(milestone.models.map(model => this.#modelToMilestone(model)));
87
+
88
+ // Enforce ordering by value as Bookshelf seems to ignore it
89
+ return milestones.sort((a, b) => b.value - a.value);
90
+ }
91
+
92
+ /**
93
+ * @param {'arr'|'members'} type
94
+ * @param {string} [currency]
95
+ *
96
+ * @returns {Promise<import('@tryghost/milestones/lib/Milestone')|null>}
97
+ */
98
+ async getLatestByType(type, currency = 'usd') {
99
+ const allMilestonesForType = await this.getAllByType(type, currency);
100
+ return allMilestonesForType?.[0] || null;
89
101
  }
90
102
 
91
103
  /**
@@ -10,7 +10,8 @@ const getStripeLiveEnabled = () => {
10
10
  const stripeConnect = settingsCache.get('stripe_connect_publishable_key');
11
11
  const stripeKey = settingsCache.get('stripe_publishable_key');
12
12
 
13
- const stripeLiveRegex = /pk_live_/;
13
+ // Allow Stripe test key when in development mode
14
+ const stripeLiveRegex = process.env.NODE_ENV === 'development' ? /pk_test_/ : /pk_live_/;
14
15
 
15
16
  if (stripeConnect && stripeConnect.match(stripeLiveRegex)) {
16
17
  return true;
@@ -59,22 +60,18 @@ module.exports = {
59
60
  * @returns {Promise<object>}
60
61
  */
61
62
  async run() {
62
- const labs = require('../../../shared/labs');
63
+ const members = await this.api.checkMilestones('members');
64
+ let arr;
65
+ const stripeLiveEnabled = getStripeLiveEnabled();
63
66
 
64
- if (labs.isSet('milestoneEmails')) {
65
- const members = await this.api.checkMilestones('members');
66
- let arr;
67
- const stripeLiveEnabled = getStripeLiveEnabled();
68
-
69
- if (stripeLiveEnabled) {
70
- arr = await this.api.checkMilestones('arr');
71
- }
72
-
73
- return {
74
- members,
75
- arr
76
- };
67
+ if (stripeLiveEnabled) {
68
+ arr = await this.api.checkMilestones('arr');
77
69
  }
70
+
71
+ return {
72
+ members,
73
+ arr
74
+ };
78
75
  },
79
76
 
80
77
  /**
@@ -84,6 +81,11 @@ module.exports = {
84
81
  * @returns {Promise<object>}
85
82
  */
86
83
  async scheduleRun(customTimeout) {
84
+ if (process.env.NODE_ENV === 'development') {
85
+ // Run the job within 5sec after boot when in local development mode
86
+ customTimeout = 5000;
87
+ }
88
+
87
89
  const timeOut = customTimeout || JOB_TIMEOUT;
88
90
 
89
91
  const today = new Date();