@meith/runtime 0.21.2 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meith/runtime",
3
- "version": "0.21.2",
3
+ "version": "0.22.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -19,24 +19,24 @@
19
19
  "access": "public"
20
20
  },
21
21
  "dependencies": {
22
- "@meith/accounts": "0.21.2",
23
- "@meith/attachments": "0.21.2",
24
- "@meith/core": "0.21.2",
25
- "@meith/avatars": "0.21.2",
26
- "@meith/api": "0.21.2",
27
- "@meith/db": "0.21.2",
28
- "@meith/drivers": "0.21.2",
29
- "@meith/events": "0.21.2",
30
- "@meith/mail": "0.21.2",
31
- "@meith/markdown": "0.21.2",
32
- "@meith/marketplace": "0.21.2",
33
- "@meith/i18n": "0.21.2",
34
- "@meith/notifications": "0.21.2",
35
- "@meith/plugin-kit": "0.21.2",
36
- "@meith/settings": "0.21.2",
37
- "@meith/moderation": "0.21.2",
38
- "@meith/tasks": "0.21.2",
39
- "@meith/groups": "0.21.2",
40
- "@meith/theme-default": "0.21.2"
22
+ "@meith/accounts": "0.22.0",
23
+ "@meith/attachments": "0.22.0",
24
+ "@meith/api": "0.22.0",
25
+ "@meith/avatars": "0.22.0",
26
+ "@meith/core": "0.22.0",
27
+ "@meith/db": "0.22.0",
28
+ "@meith/drivers": "0.22.0",
29
+ "@meith/events": "0.22.0",
30
+ "@meith/groups": "0.22.0",
31
+ "@meith/i18n": "0.22.0",
32
+ "@meith/mail": "0.22.0",
33
+ "@meith/marketplace": "0.22.0",
34
+ "@meith/markdown": "0.22.0",
35
+ "@meith/notifications": "0.22.0",
36
+ "@meith/plugin-kit": "0.22.0",
37
+ "@meith/settings": "0.22.0",
38
+ "@meith/moderation": "0.22.0",
39
+ "@meith/tasks": "0.22.0",
40
+ "@meith/theme-default": "0.22.0"
41
41
  }
42
42
  }
@@ -18,12 +18,6 @@ import {
18
18
 
19
19
  export const PLUGIN_STATEMENT_TIMEOUT_MS = 30_000
20
20
 
21
- /**
22
- * The context every plugin callback is handed: its resolved settings, a logger
23
- * that says which plugin is talking, and the four runtime capabilities. Built
24
- * in one place so a task, an install and a purge cannot drift into offering a
25
- * plugin different powers depending on which door it came through.
26
- */
27
21
  export async function pluginRuntimeContext(input: {
28
22
  readonly db: Database
29
23
  readonly plugin: PluginDefinition
@@ -12,14 +12,6 @@ const CALLBACKS: Readonly<Record<PluginLifecyclePhase, keyof PluginDefinition>>
12
12
  uninstall: 'onUninstall',
13
13
  }
14
14
 
15
- /**
16
- * Run one lifecycle callback, or report that the plugin declares none.
17
- *
18
- * It throws what the callback throws, deliberately. These do not run inside the
19
- * host's try/catch, because that isolation exists to keep a page rendering and
20
- * none of these is on a page: an install that half-worked should stop the
21
- * deploy rather than be swallowed, and each caller states its own policy.
22
- */
23
15
  export async function runPluginLifecycle(input: {
24
16
  readonly db: Database
25
17
  readonly plugin: PluginDefinition
@@ -24,11 +24,6 @@ export function pluginMarkdownPipeline(host: PluginHost): MarkdownPipeline {
24
24
  }
25
25
  }
26
26
 
27
- /**
28
- * A message on its way out of the worker, past the plugins first. Returning
29
- * null from `mail.send.before` drops it; nothing tells the recipient, because
30
- * nobody was waiting on a page for it.
31
- */
32
27
  export async function sendAudited(
33
28
  host: PluginHost,
34
29
  mail: MailDriver,
@@ -36,8 +36,8 @@ import {
36
36
  recordPluginFailure,
37
37
  syncRenderSignature,
38
38
  } from '@meith/db'
39
- import { EN_CATALOG, sourceTranslator } from '@meith/i18n'
40
- import { renderMail } from '@meith/mail'
39
+ import { EN_CATALOG, sourceTranslator, type Translator } from '@meith/i18n'
40
+ import { absoluteUrl, type MailFooterLine, renderMail } from '@meith/mail'
41
41
  import {
42
42
  deliverNotificationEmail,
43
43
  deliverNotificationPush,
@@ -47,6 +47,7 @@ import {
47
47
  } from '@meith/notifications'
48
48
  import { type PluginDefinition, PluginHost, renderingSignature } from '@meith/plugin-kit'
49
49
  import { resolvePushConfig, SettingsSnapshot } from '@meith/settings'
50
+ import { mintUnsubscribeToken } from '@meith/subscriptions'
50
51
  import { builtinTasks, type TaskDefinition, type TaskRepository } from '@meith/tasks'
51
52
 
52
53
  import { buildEventRegistry } from './event-handlers'
@@ -131,11 +132,6 @@ export function buildSchedulerBundle(deps: {
131
132
  'Scheduled task run duration in seconds, labelled by task and outcome.',
132
133
  )
133
134
 
134
- /**
135
- * Every task, announced. The host is told before and after each run — a
136
- * plugin watching its own task, or the board's, gets the same two events
137
- * whichever scheduled it.
138
- */
139
135
  const announced = (tasks: readonly TaskDefinition[]): TaskDefinition[] =>
140
136
  tasks.map((task) => ({
141
137
  ...task,
@@ -218,9 +214,11 @@ export function buildSchedulerBundle(deps: {
218
214
  },
219
215
  ...optional(mail, (mail) => ({
220
216
  massMail: {
221
- async send({ massMailId, email }) {
222
- const campaign = await new PostgresUserBulkRepository(db).readMassMail(massMailId)
217
+ async send({ massMailId, userId, email }) {
218
+ const bulk = new PostgresUserBulkRepository(db)
219
+ const campaign = await bulk.readMassMail(massMailId)
223
220
  if (campaign === null) return
221
+ if (!(await bulk.mayReceiveMassMail(userId))) return
224
222
 
225
223
  const brand = await resolveMailBrand({ db, ...themeDeps })
226
224
  const t = await (
@@ -240,6 +238,8 @@ export function buildSchedulerBundle(deps: {
240
238
  brand.boardName === '' ? t.t('mail.boardFallback') : brand.boardName,
241
239
  }),
242
240
  },
241
+ { text: t.t('mail.footer.announcementsConsent') },
242
+ massMailUnsubscribeLine({ boardUrl: brand.boardUrl, userId, t }),
243
243
  ],
244
244
  },
245
245
  })
@@ -371,3 +371,30 @@ function taskFailureNotifier(
371
371
  })
372
372
  }
373
373
  }
374
+
375
+ const UNSUBSCRIBE_PATH = '/unsubscribe'
376
+
377
+ const MAIL_PREFERENCES_PATH = '/notifications/preferences'
378
+
379
+ function massMailUnsubscribeLine(input: {
380
+ readonly boardUrl: string
381
+ readonly userId: number
382
+ readonly t: Translator
383
+ }): MailFooterLine {
384
+ const secret = env.AUTH_SECRET
385
+ const token =
386
+ secret === undefined
387
+ ? null
388
+ : mintUnsubscribeToken({ userId: input.userId, scope: 'mass-mail', targetId: 0 }, secret)
389
+
390
+ const href = absoluteUrl(
391
+ input.boardUrl,
392
+ token === null
393
+ ? MAIL_PREFERENCES_PATH
394
+ : `${UNSUBSCRIBE_PATH}?token=${encodeURIComponent(token)}`,
395
+ )
396
+
397
+ return href === null
398
+ ? { text: input.t.t('mail.footer.announcementsNoLink') }
399
+ : { text: input.t.t('mail.footer.announcementsStop'), href }
400
+ }