@headless-lms/core 0.3.0 → 0.4.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 (205) hide show
  1. package/dist/assets/events.d.ts +9 -9
  2. package/dist/assets/index.d.ts +3 -2
  3. package/dist/assets/index.js +2 -1
  4. package/dist/assets/jobs.d.ts +15 -0
  5. package/dist/assets/jobs.js +30 -0
  6. package/dist/assets/jobs.js.map +1 -0
  7. package/dist/assets/ports.d.ts +4 -3
  8. package/dist/assets/service.d.ts +0 -1
  9. package/dist/assets/service.js +0 -4
  10. package/dist/assets/service.js.map +1 -1
  11. package/dist/automations/actions.js +2 -63
  12. package/dist/automations/actions.js.map +1 -1
  13. package/dist/automations/catalog.d.ts +1 -2
  14. package/dist/automations/catalog.js +69 -62
  15. package/dist/automations/catalog.js.map +1 -1
  16. package/dist/automations/community-derivations.js +4 -4
  17. package/dist/automations/community-derivations.js.map +1 -1
  18. package/dist/automations/community-mail.d.ts +3 -17
  19. package/dist/automations/community-mail.js +1 -21
  20. package/dist/automations/community-mail.js.map +1 -1
  21. package/dist/automations/events.d.ts +0 -10
  22. package/dist/automations/index.d.ts +11 -4
  23. package/dist/automations/index.js +8 -1
  24. package/dist/automations/notifications/debounce.d.ts +25 -0
  25. package/dist/automations/notifications/debounce.js +93 -0
  26. package/dist/automations/notifications/debounce.js.map +1 -0
  27. package/dist/automations/notifications/defaults.d.ts +8 -0
  28. package/dist/automations/notifications/defaults.js +316 -0
  29. package/dist/automations/notifications/defaults.js.map +1 -0
  30. package/dist/automations/notifications/delivery.d.ts +28 -0
  31. package/dist/automations/notifications/delivery.js +88 -0
  32. package/dist/automations/notifications/delivery.js.map +1 -0
  33. package/dist/automations/notifications/processor.d.ts +64 -0
  34. package/dist/automations/notifications/processor.js +270 -0
  35. package/dist/automations/notifications/processor.js.map +1 -0
  36. package/dist/automations/notifications/rules/content.d.ts +6 -0
  37. package/dist/automations/notifications/rules/content.js +39 -0
  38. package/dist/automations/notifications/rules/content.js.map +1 -0
  39. package/dist/automations/notifications/rules/discussion.d.ts +7 -0
  40. package/dist/automations/notifications/rules/discussion.js +678 -0
  41. package/dist/automations/notifications/rules/discussion.js.map +1 -0
  42. package/dist/automations/notifications/rules/entitlements.d.ts +6 -0
  43. package/dist/automations/notifications/rules/entitlements.js +249 -0
  44. package/dist/automations/notifications/rules/entitlements.js.map +1 -0
  45. package/dist/automations/notifications/rules/index.d.ts +6 -0
  46. package/dist/automations/notifications/rules/index.js +15 -0
  47. package/dist/automations/notifications/rules/index.js.map +1 -0
  48. package/dist/automations/notifications/rules/organizations.d.ts +6 -0
  49. package/dist/automations/notifications/rules/organizations.js +28 -0
  50. package/dist/automations/notifications/rules/organizations.js.map +1 -0
  51. package/dist/automations/notifications/rules/recipients.d.ts +32 -0
  52. package/dist/automations/notifications/rules/recipients.js +99 -0
  53. package/dist/automations/notifications/rules/recipients.js.map +1 -0
  54. package/dist/automations/notifications/service.d.ts +27 -0
  55. package/dist/automations/notifications/service.js +45 -0
  56. package/dist/automations/notifications/service.js.map +1 -0
  57. package/dist/automations/notifications/types.d.ts +7 -0
  58. package/dist/automations/notifications/types.js +473 -0
  59. package/dist/automations/notifications/types.js.map +1 -0
  60. package/dist/automations/ports.d.ts +119 -5
  61. package/dist/automations/service.d.ts +10 -13
  62. package/dist/automations/service.js +21 -83
  63. package/dist/automations/service.js.map +1 -1
  64. package/dist/content/events.d.ts +130 -80
  65. package/dist/content/events.js +11 -0
  66. package/dist/content/events.js.map +1 -1
  67. package/dist/content/index.d.ts +6 -4
  68. package/dist/content/index.js +3 -1
  69. package/dist/content/model.d.ts +2 -2
  70. package/dist/content/notification-categories.d.ts +36 -0
  71. package/dist/content/notification-categories.js +64 -0
  72. package/dist/content/notification-categories.js.map +1 -0
  73. package/dist/content/ports.d.ts +25 -6
  74. package/dist/content/service.d.ts +4 -4
  75. package/dist/content/service.js +24 -8
  76. package/dist/content/service.js.map +1 -1
  77. package/dist/content/titles.d.ts +22 -0
  78. package/dist/content/titles.js +69 -0
  79. package/dist/content/titles.js.map +1 -0
  80. package/dist/content/types.d.ts +2 -2
  81. package/dist/discussion/chats.d.ts +9 -4
  82. package/dist/discussion/chats.js +80 -47
  83. package/dist/discussion/chats.js.map +1 -1
  84. package/dist/discussion/digests.d.ts +24 -0
  85. package/dist/discussion/digests.js +104 -0
  86. package/dist/discussion/digests.js.map +1 -0
  87. package/dist/discussion/events.d.ts +194 -28
  88. package/dist/discussion/events.js +74 -17
  89. package/dist/discussion/events.js.map +1 -1
  90. package/dist/discussion/index.d.ts +7 -2
  91. package/dist/discussion/index.js +6 -1
  92. package/dist/discussion/jobs.d.ts +16 -0
  93. package/dist/discussion/jobs.js +40 -0
  94. package/dist/discussion/jobs.js.map +1 -0
  95. package/dist/discussion/mentions.d.ts +11 -0
  96. package/dist/discussion/mentions.js +35 -0
  97. package/dist/discussion/mentions.js.map +1 -0
  98. package/dist/discussion/ports.d.ts +107 -24
  99. package/dist/discussion/ports.js.map +1 -1
  100. package/dist/discussion/posts.d.ts +18 -8
  101. package/dist/discussion/posts.js +207 -155
  102. package/dist/discussion/posts.js.map +1 -1
  103. package/dist/discussion/service.d.ts +17 -8
  104. package/dist/discussion/service.js +105 -41
  105. package/dist/discussion/service.js.map +1 -1
  106. package/dist/discussion/subscriptions.d.ts +37 -0
  107. package/dist/discussion/subscriptions.js +99 -0
  108. package/dist/discussion/subscriptions.js.map +1 -0
  109. package/dist/discussion/titles.d.ts +26 -0
  110. package/dist/discussion/titles.js +84 -0
  111. package/dist/discussion/titles.js.map +1 -0
  112. package/dist/entitlements/events.d.ts +20 -20
  113. package/dist/entitlements/index.d.ts +2 -1
  114. package/dist/entitlements/index.js +2 -1
  115. package/dist/entitlements/jobs.d.ts +12 -0
  116. package/dist/entitlements/jobs.js +18 -0
  117. package/dist/entitlements/jobs.js.map +1 -0
  118. package/dist/entitlements/ports.d.ts +7 -3
  119. package/dist/entitlements/service.d.ts +5 -2
  120. package/dist/entitlements/service.js +23 -3
  121. package/dist/entitlements/service.js.map +1 -1
  122. package/dist/identity/devices.d.ts +30 -0
  123. package/dist/identity/devices.js +107 -0
  124. package/dist/identity/devices.js.map +1 -0
  125. package/dist/identity/events.d.ts +2 -2
  126. package/dist/identity/index.d.ts +5 -4
  127. package/dist/identity/index.js +3 -2
  128. package/dist/identity/model.d.ts +2 -2
  129. package/dist/identity/notification-settings.d.ts +28 -0
  130. package/dist/identity/notification-settings.js +113 -0
  131. package/dist/identity/notification-settings.js.map +1 -0
  132. package/dist/identity/ports.d.ts +78 -54
  133. package/dist/integrations/events.d.ts +3 -3
  134. package/dist/organizations/events.d.ts +32 -32
  135. package/dist/organizations/index.d.ts +2 -1
  136. package/dist/organizations/index.js +2 -1
  137. package/dist/organizations/notification-categories.d.ts +32 -0
  138. package/dist/organizations/notification-categories.js +51 -0
  139. package/dist/organizations/notification-categories.js.map +1 -0
  140. package/dist/organizations/ports.d.ts +3 -0
  141. package/dist/organizations/service.d.ts +2 -0
  142. package/dist/organizations/service.js +7 -0
  143. package/dist/organizations/service.js.map +1 -1
  144. package/dist/progress/events.d.ts +14 -14
  145. package/dist/reporting/member/index.d.ts +2 -2
  146. package/dist/reporting/member/model.d.ts +56 -23
  147. package/dist/reporting/member/ports.d.ts +9 -6
  148. package/dist/reporting/member/service.d.ts +33 -14
  149. package/dist/reporting/member/service.js +138 -60
  150. package/dist/reporting/member/service.js.map +1 -1
  151. package/dist/shared/errors.d.ts +13 -1
  152. package/dist/shared/errors.js +18 -1
  153. package/dist/shared/errors.js.map +1 -1
  154. package/dist/shared/id.d.ts +3 -0
  155. package/dist/shared/id.js +4 -1
  156. package/dist/shared/id.js.map +1 -1
  157. package/dist/shared/mailer.d.ts +11 -1
  158. package/dist/shared/mailer.js +32 -5
  159. package/dist/shared/mailer.js.map +1 -1
  160. package/dist/shared/notification-categories.d.ts +16 -0
  161. package/dist/shared/notification-categories.js +94 -0
  162. package/dist/shared/notification-categories.js.map +1 -0
  163. package/dist/shared/notification-settings.d.ts +14 -0
  164. package/dist/shared/notification-settings.js +61 -0
  165. package/dist/shared/notification-settings.js.map +1 -0
  166. package/dist/shared/ports.d.ts +48 -2
  167. package/dist/shared/settings.d.ts +2 -1
  168. package/dist/shared/settings.js +1 -0
  169. package/dist/shared/settings.js.map +1 -1
  170. package/dist/types/automations.d.ts +8 -2
  171. package/dist/types/content.d.ts +2 -2
  172. package/dist/types/discussion.d.ts +2 -2
  173. package/dist/types/email-templates.d.ts +97 -3
  174. package/dist/types/identity.d.ts +2 -2
  175. package/dist/types/index.d.ts +7 -6
  176. package/dist/types/notifications.d.ts +157 -0
  177. package/dist/types/notifications.js +1 -0
  178. package/dist/types/schemas/automations.d.ts +1 -10
  179. package/dist/types/schemas/automations.js +1 -3
  180. package/dist/types/schemas/automations.js.map +1 -1
  181. package/dist/types/schemas/content.d.ts +62 -1
  182. package/dist/types/schemas/content.js +25 -3
  183. package/dist/types/schemas/content.js.map +1 -1
  184. package/dist/types/schemas/discussion.d.ts +85 -12
  185. package/dist/types/schemas/discussion.js +47 -13
  186. package/dist/types/schemas/discussion.js.map +1 -1
  187. package/dist/types/schemas/identity.d.ts +1 -85
  188. package/dist/types/schemas/identity.js +1 -42
  189. package/dist/types/schemas/identity.js.map +1 -1
  190. package/dist/types/schemas/index.d.ts +6 -5
  191. package/dist/types/schemas/index.js +6 -5
  192. package/dist/types/schemas/notifications.d.ts +95 -0
  193. package/dist/types/schemas/notifications.js +56 -0
  194. package/dist/types/schemas/notifications.js.map +1 -0
  195. package/dist/types/schemas/progress.d.ts +3 -3
  196. package/package.json +1 -1
  197. package/dist/automations/community-notifications.d.ts +0 -16
  198. package/dist/automations/community-notifications.js +0 -84
  199. package/dist/automations/community-notifications.js.map +0 -1
  200. package/dist/automations/seeds.d.ts +0 -17
  201. package/dist/automations/seeds.js +0 -235
  202. package/dist/automations/seeds.js.map +0 -1
  203. package/dist/identity/notifications.d.ts +0 -27
  204. package/dist/identity/notifications.js +0 -101
  205. package/dist/identity/notifications.js.map +0 -1
@@ -1,18 +1,26 @@
1
1
  import { genId } from "../shared/id.js";
2
- import { ConflictError, ForbiddenError, InvalidInputError, NotFoundError } from "../shared/errors.js";
2
+ import { ConflictError, ForbiddenError, InvalidInputError, NotFoundError, UnavailableError } from "../shared/errors.js";
3
3
  import { noopLogger } from "../shared/logger.js";
4
4
  import { canModerate, isStaffRole } from "../organizations/roles.js";
5
5
  import { discussionEvents } from "./events.js";
6
6
  import { authorView } from "./authors.js";
7
+ import { keepAllowed, mentionTokens, mentionViews } from "./mentions.js";
8
+ import { POST_PUBLISH_JOB } from "./jobs.js";
9
+ import { activeSubscription } from "./subscriptions.js";
7
10
  import "./ports.js";
8
11
  //#region src/discussion/posts.ts
9
12
  const PUBLISHED_ONLY = ["published"];
10
- const DUE_BATCH_SIZE = 100;
11
13
  const MODERATOR_VISIBLE = [
12
14
  "published",
13
15
  "pending",
14
16
  "removed"
15
17
  ];
18
+ const DRAFT = {
19
+ status: "draft",
20
+ scheduledAt: null,
21
+ publishedAt: null,
22
+ removedBy: null
23
+ };
16
24
  var PostServiceImpl = class {
17
25
  repo;
18
26
  access;
@@ -20,9 +28,12 @@ var PostServiceImpl = class {
20
28
  authors;
21
29
  profiles;
22
30
  comments;
31
+ subscriptions;
32
+ mentions;
23
33
  images;
24
34
  fileUrls;
25
35
  uow;
36
+ scheduler;
26
37
  logger;
27
38
  constructor(params) {
28
39
  this.repo = params.repo;
@@ -31,9 +42,12 @@ var PostServiceImpl = class {
31
42
  this.authors = params.authors;
32
43
  this.profiles = params.profiles;
33
44
  this.comments = params.comments;
45
+ this.subscriptions = params.subscriptions;
46
+ this.mentions = params.mentions;
34
47
  this.images = params.images;
35
48
  this.fileUrls = params.fileUrls;
36
49
  this.uow = params.uow;
50
+ this.scheduler = params.scheduler;
37
51
  this.logger = params.logger ?? noopLogger;
38
52
  }
39
53
  async visibleTitles(orgId, actor, postIds) {
@@ -42,6 +56,9 @@ var PostServiceImpl = class {
42
56
  const open = new Set((await Promise.all(communityIds.map((communityId) => this.access.openSpaceIds(orgId, actor, communityId)))).flat());
43
57
  return Object.fromEntries(found.filter((post) => open.has(post.spaceId) && this.canSee(actor, post)).map((post) => [post.id, post.title]));
44
58
  }
59
+ publishedBetween(orgId, communityId, from, to) {
60
+ return this.repo.publishedBetween(orgId, communityId, from, to);
61
+ }
45
62
  async create(orgId, actor, input) {
46
63
  const space = await this.requireOpen(orgId, actor, input.spaceId, input.communityId);
47
64
  await this.requireOwnedImage(orgId, actor.id, input.coverAssetId);
@@ -66,18 +83,16 @@ var PostServiceImpl = class {
66
83
  updatedAt: now,
67
84
  ...landing
68
85
  };
69
- const saved = await this.uow.run(async ({ posts, outbox }) => {
86
+ const mentioned = await this.mentionable(orgId, space.id, post.title, post.body);
87
+ const saved = await this.uow.run(async ({ posts, mentions, outbox }) => {
70
88
  const created = await posts.insert(orgId, post);
89
+ await mentions.sync(orgId, "post", created.id, mentioned, now);
71
90
  const events = [discussionEvents.postCreated.make({
72
91
  orgId,
73
92
  data: created,
74
93
  metadata: this.meta(actor, created.status === "pending" ? null : void 0)
75
94
  })];
76
- if (created.status === "published") events.push(discussionEvents.postPublished.make({
77
- orgId,
78
- data: created,
79
- metadata: this.meta(actor, null)
80
- }));
95
+ if (created.status === "published") events.push(this.published(orgId, created, this.meta(actor, null), mentioned));
81
96
  await outbox.append(events);
82
97
  return created;
83
98
  });
@@ -87,6 +102,7 @@ var PostServiceImpl = class {
87
102
  status: saved.status,
88
103
  actorId: actor.id
89
104
  });
105
+ if (saved.status === "scheduled") await this.arm(orgId, actor, saved, null);
90
106
  return this.viewOf(orgId, actor, saved);
91
107
  }
92
108
  async update(orgId, postId, actor, patch) {
@@ -96,13 +112,12 @@ var PostServiceImpl = class {
96
112
  if (patch.topicId !== void 0) this.assertTopic(space, patch.topicId);
97
113
  if (typeof patch.coverAssetId === "string") await this.requireOwnedImage(orgId, actor.id, patch.coverAssetId);
98
114
  if (!this.patchChanges(post, patch)) return this.viewOf(orgId, actor, post);
99
- const updated = await this.uow.run(async ({ posts, outbox }) => {
115
+ const retext = patch.title !== void 0 || patch.body !== void 0;
116
+ const candidates = retext ? await this.mentionable(orgId, post.spaceId, patch.title ?? post.title, patch.body ?? post.body) : [];
117
+ const updated = await this.uow.run(async ({ posts, mentions, outbox }) => {
100
118
  const next = await this.mustUpdate(posts, orgId, postId, patch);
101
- await outbox.append([discussionEvents.postUpdated.make({
102
- orgId,
103
- data: next,
104
- metadata: this.meta(actor, next.status === "pending" ? post.status : void 0)
105
- })]);
119
+ const added = retext ? await mentions.sync(orgId, "post", postId, candidates, /* @__PURE__ */ new Date()) : [];
120
+ await outbox.append([this.updated(orgId, next, this.meta(actor, next.status === "pending" ? post.status : void 0), next.status === "published" ? keepAllowed(candidates, added) : [])]);
106
121
  return next;
107
122
  });
108
123
  this.logger.info("post updated", {
@@ -125,6 +140,7 @@ var PostServiceImpl = class {
125
140
  status: saved.status,
126
141
  actorId: actor.id
127
142
  });
143
+ if (post.status === "scheduled") await this.disarm(orgId, postId);
128
144
  return this.viewOf(orgId, actor, saved);
129
145
  }
130
146
  async schedule(orgId, postId, actor, scheduledAt) {
@@ -133,17 +149,14 @@ var PostServiceImpl = class {
133
149
  if (post.status === "published" || post.status === "removed" || post.status === "pending") throw new ConflictError("This post cannot be scheduled");
134
150
  if (scheduledAt <= /* @__PURE__ */ new Date()) throw new InvalidInputError("A scheduled time must be in the future");
135
151
  const updated = await this.uow.run(async ({ posts, outbox }) => {
136
- const next = await this.mustUpdate(posts, orgId, postId, {
152
+ const next = await posts.updateIf(orgId, postId, { status: post.status }, {
137
153
  status: "scheduled",
138
154
  scheduledAt,
139
155
  publishedAt: null,
140
156
  removedBy: null
141
157
  });
142
- await outbox.append([discussionEvents.postUpdated.make({
143
- orgId,
144
- data: next,
145
- metadata: this.meta(actor, post.status)
146
- })]);
158
+ if (!next) throw new ConflictError("This post cannot be scheduled");
159
+ await outbox.append([this.updated(orgId, next, this.meta(actor, post.status))]);
147
160
  return next;
148
161
  });
149
162
  this.logger.info("post scheduled", {
@@ -152,25 +165,132 @@ var PostServiceImpl = class {
152
165
  scheduledAt: scheduledAt.toISOString(),
153
166
  actorId: actor.id
154
167
  });
168
+ await this.arm(orgId, actor, updated, post.status === "scheduled" ? post.scheduledAt : null);
169
+ return this.viewOf(orgId, actor, updated);
170
+ }
171
+ async unschedule(orgId, postId, actor) {
172
+ const { post } = await this.loadVisible(orgId, actor, postId);
173
+ this.assertAuthor(actor, post);
174
+ if (post.status !== "scheduled") throw new ConflictError("This post is not scheduled");
175
+ const updated = await this.uow.run(async ({ posts, outbox }) => {
176
+ const next = await posts.updateIf(orgId, postId, { status: "scheduled" }, DRAFT);
177
+ if (!next) throw new ConflictError("This post is not scheduled");
178
+ await outbox.append([this.updated(orgId, next, this.meta(actor, post.status))]);
179
+ return next;
180
+ });
181
+ this.logger.info("post unscheduled", {
182
+ orgId,
183
+ postId,
184
+ actorId: actor.id
185
+ });
186
+ await this.disarm(orgId, postId);
155
187
  return this.viewOf(orgId, actor, updated);
156
188
  }
189
+ async publishScheduled(orgId, postId, now) {
190
+ const post = await this.repo.find(orgId, postId);
191
+ if (!post || post.status !== "scheduled" || post.scheduledAt === null) {
192
+ this.logger.debug("scheduled post no longer scheduled", {
193
+ orgId,
194
+ postId
195
+ });
196
+ return;
197
+ }
198
+ if (post.scheduledAt > now) {
199
+ await this.scheduler.at(POST_PUBLISH_JOB, post.scheduledAt, postId, {
200
+ orgId,
201
+ postId
202
+ });
203
+ this.logger.info("scheduled post timer moved", {
204
+ orgId,
205
+ postId,
206
+ scheduledAt: post.scheduledAt.toISOString()
207
+ });
208
+ return;
209
+ }
210
+ const space = await this.content.getSpace(orgId, post.spaceId);
211
+ if (!space) {
212
+ this.logger.error("scheduled post has no space", {
213
+ orgId,
214
+ postId,
215
+ spaceId: post.spaceId
216
+ });
217
+ return;
218
+ }
219
+ const role = await this.authors.roleOf(orgId, post.orgUserId);
220
+ const refused = !await this.submittable(orgId, post, space, role);
221
+ const patch = refused ? DRAFT : this.submissionOf(space.settings.requireReview, role, now);
222
+ const saved = await this.uow.run(async ({ posts, mentions, outbox }) => {
223
+ const next = await posts.updateIf(orgId, postId, {
224
+ status: "scheduled",
225
+ dueBy: now
226
+ }, patch);
227
+ if (!next) return null;
228
+ await outbox.append([await this.landed(mentions, orgId, next, this.meta(null, post.status))]);
229
+ return next;
230
+ });
231
+ if (!saved) {
232
+ this.logger.debug("scheduled post already handled", {
233
+ orgId,
234
+ postId
235
+ });
236
+ return;
237
+ }
238
+ this.logger.info("scheduled post submitted", {
239
+ orgId,
240
+ postId,
241
+ status: saved.status,
242
+ refused
243
+ });
244
+ }
245
+ async arm(orgId, actor, post, previousScheduledAt) {
246
+ if (post.scheduledAt === null) return;
247
+ try {
248
+ await this.scheduler.at(POST_PUBLISH_JOB, post.scheduledAt, post.id, {
249
+ orgId,
250
+ postId: post.id
251
+ });
252
+ return;
253
+ } catch (err) {
254
+ this.logger.error("post timer not set", {
255
+ orgId,
256
+ postId: post.id,
257
+ err
258
+ });
259
+ }
260
+ await this.uow.run(async ({ posts, outbox }) => {
261
+ const reverted = await posts.updateIf(orgId, post.id, {
262
+ status: "scheduled",
263
+ scheduledAt: post.scheduledAt
264
+ }, previousScheduledAt ? { scheduledAt: previousScheduledAt } : DRAFT);
265
+ if (!reverted) return;
266
+ await outbox.append([this.updated(orgId, reverted, this.meta(actor, reverted.status === "draft" ? post.status : void 0))]);
267
+ });
268
+ throw new UnavailableError("The post could not be scheduled; try again", { postId: post.id });
269
+ }
270
+ async disarm(orgId, postId) {
271
+ try {
272
+ await this.scheduler.cancel(POST_PUBLISH_JOB, postId);
273
+ } catch (err) {
274
+ this.logger.warn("post timer not cancelled", {
275
+ orgId,
276
+ postId,
277
+ err
278
+ });
279
+ }
280
+ }
157
281
  async approve(orgId, postId, actor, communityId) {
158
282
  const { post } = await this.loadVisible(orgId, actor, postId, communityId);
159
283
  if (!this.canModerate(actor)) throw new ForbiddenError("You cannot review this post");
160
284
  if (post.status !== "pending") throw new ConflictError("This post is not awaiting review");
161
285
  const now = /* @__PURE__ */ new Date();
162
- const updated = await this.uow.run(async ({ posts, outbox }) => {
286
+ const updated = await this.uow.run(async ({ posts, mentions, outbox }) => {
163
287
  const next = await this.mustUpdate(posts, orgId, postId, {
164
288
  status: "published",
165
289
  publishedAt: now,
166
290
  scheduledAt: null,
167
291
  removedBy: null
168
292
  });
169
- await outbox.append([discussionEvents.postPublished.make({
170
- orgId,
171
- data: next,
172
- metadata: this.meta(actor, post.status)
173
- })]);
293
+ await outbox.append([await this.landed(mentions, orgId, next, this.meta(actor, post.status))]);
174
294
  return next;
175
295
  });
176
296
  this.logger.info("post approved", {
@@ -218,16 +338,7 @@ var PostServiceImpl = class {
218
338
  const updated = await this.uow.run(async ({ posts, outbox }) => {
219
339
  const next = await this.mustUpdate(posts, orgId, postId, this.submissionOf(space.settings.requireReview, role, now));
220
340
  const metadata = this.meta(actor, post.status);
221
- const event = next.status === "published" ? discussionEvents.postPublished.make({
222
- orgId,
223
- data: next,
224
- metadata
225
- }) : discussionEvents.postUpdated.make({
226
- orgId,
227
- data: next,
228
- metadata
229
- });
230
- await outbox.append([event]);
341
+ await outbox.append([next.status === "published" ? this.published(orgId, next, metadata, []) : this.updated(orgId, next, metadata)]);
231
342
  return next;
232
343
  });
233
344
  this.logger.info("post restored", {
@@ -256,7 +367,20 @@ var PostServiceImpl = class {
256
367
  async setReaction(orgId, postId, actor, emoji) {
257
368
  const { post, space } = await this.loadVisible(orgId, actor, postId);
258
369
  if (!(post.reactionsEnabled ?? space.settings.reactions)) throw new ForbiddenError("Reactions are off on this post");
259
- await this.repo.setReaction(orgId, postId, actor.id, emoji);
370
+ await this.uow.run(async ({ posts, outbox }) => {
371
+ if (!await posts.setReaction(orgId, postId, actor.id, emoji) || emoji === null) return;
372
+ await outbox.append([discussionEvents.postReactionAdded.make({
373
+ orgId,
374
+ data: {
375
+ postId,
376
+ spaceId: post.spaceId,
377
+ communityId: post.communityId,
378
+ orgUserId: actor.id,
379
+ reaction: emoji
380
+ },
381
+ metadata: this.meta(actor)
382
+ })]);
383
+ });
260
384
  this.logger.debug("post reaction set", {
261
385
  orgId,
262
386
  postId,
@@ -264,24 +388,6 @@ var PostServiceImpl = class {
264
388
  });
265
389
  return (await this.repo.reactionsOf(orgId, [postId], actor.id))[postId] ?? { reactions: {} };
266
390
  }
267
- async follow(orgId, postId, actor) {
268
- await this.loadVisible(orgId, actor, postId);
269
- await this.repo.follow(orgId, postId, actor.id);
270
- this.logger.debug("post followed", {
271
- orgId,
272
- postId,
273
- orgUserId: actor.id
274
- });
275
- }
276
- async unfollow(orgId, postId, actor) {
277
- await this.loadVisible(orgId, actor, postId);
278
- await this.repo.unfollow(orgId, postId, actor.id);
279
- this.logger.debug("post unfollowed", {
280
- orgId,
281
- postId,
282
- orgUserId: actor.id
283
- });
284
- }
285
391
  async bookmark(orgId, postId, actor) {
286
392
  await this.loadVisible(orgId, actor, postId);
287
393
  await this.repo.bookmark(orgId, postId, actor.id);
@@ -452,82 +558,6 @@ var PostServiceImpl = class {
452
558
  find(orgId, postId) {
453
559
  return this.repo.find(orgId, postId);
454
560
  }
455
- followerIds(orgId, postId) {
456
- return this.repo.followerIds(orgId, postId);
457
- }
458
- hasDue(orgId, now) {
459
- return this.repo.hasDue(orgId, now);
460
- }
461
- async publishDue(orgId, now) {
462
- let afterId;
463
- let count = 0;
464
- let refused = 0;
465
- for (;;) {
466
- const due = await this.repo.listDue(orgId, now, DUE_BATCH_SIZE, afterId);
467
- if (due.length === 0) break;
468
- const batch = await this.submitDue(orgId, now, due);
469
- count += batch.count;
470
- refused += batch.refused;
471
- if (due.length < DUE_BATCH_SIZE) break;
472
- afterId = due[due.length - 1].id;
473
- }
474
- if (count > 0) this.logger.info("due posts submitted", {
475
- orgId,
476
- count,
477
- refused
478
- });
479
- else this.logger.debug("no posts due", { orgId });
480
- }
481
- async submitDue(orgId, now, due) {
482
- const spaces = /* @__PURE__ */ new Map();
483
- const roles = /* @__PURE__ */ new Map();
484
- const refused = /* @__PURE__ */ new Set();
485
- for (const post of due) {
486
- if (!spaces.has(post.spaceId)) spaces.set(post.spaceId, await this.content.getSpace(orgId, post.spaceId));
487
- if (!roles.has(post.orgUserId)) roles.set(post.orgUserId, await this.authors.roleOf(orgId, post.orgUserId));
488
- const space = spaces.get(post.spaceId);
489
- const role = roles.get(post.orgUserId) ?? null;
490
- if (space && !await this.submittable(orgId, post, space, role)) refused.add(post.id);
491
- }
492
- return {
493
- count: await this.uow.run(async ({ posts, outbox }) => {
494
- const events = [];
495
- for (const post of due) {
496
- const current = await posts.find(orgId, post.id);
497
- if (!current || current.status !== "scheduled" || current.scheduledAt === null || current.scheduledAt > now) continue;
498
- const space = spaces.get(current.spaceId);
499
- if (!space) {
500
- this.logger.error("scheduled post has no space", {
501
- orgId,
502
- postId: current.id,
503
- spaceId: current.spaceId
504
- });
505
- continue;
506
- }
507
- const next = await posts.update(orgId, current.id, refused.has(current.id) ? {
508
- status: "draft",
509
- scheduledAt: null,
510
- publishedAt: null,
511
- removedBy: null
512
- } : this.submissionOf(space.settings.requireReview, roles.get(current.orgUserId) ?? null, now));
513
- if (!next) continue;
514
- const metadata = this.meta(null, current.status);
515
- events.push(next.status === "published" ? discussionEvents.postPublished.make({
516
- orgId,
517
- data: next,
518
- metadata
519
- }) : discussionEvents.postUpdated.make({
520
- orgId,
521
- data: next,
522
- metadata
523
- }));
524
- }
525
- if (events.length > 0) await outbox.append(events);
526
- return events.length;
527
- }),
528
- refused: refused.size
529
- };
530
- }
531
561
  async submittable(orgId, post, space, role) {
532
562
  if (role === null || !this.topicFits(space, post.topicId)) return false;
533
563
  return this.access.canOpen(orgId, {
@@ -549,14 +579,16 @@ var PostServiceImpl = class {
549
579
  async viewsOf(orgId, actor, posts) {
550
580
  if (posts.length === 0) return [];
551
581
  const postIds = posts.map((post) => post.id);
552
- const [records, reactions, commentCounts, followed, bookmarked] = await Promise.all([
582
+ const now = /* @__PURE__ */ new Date();
583
+ const [records, reactions, commentCounts, subscribed, bookmarked, mentioned] = await Promise.all([
553
584
  this.profiles.authorsOf(orgId, [...new Set(posts.map((post) => post.orgUserId))]),
554
585
  this.repo.reactionsOf(orgId, postIds, actor.id),
555
586
  this.comments.publishedCommentCounts(orgId, postIds),
556
- this.repo.followedAmong(orgId, postIds, actor.id),
557
- this.repo.bookmarkedAmong(orgId, postIds, actor.id)
587
+ this.subscriptions.forViewer(orgId, actor.id, "post", postIds),
588
+ this.repo.bookmarkedAmong(orgId, postIds, actor.id),
589
+ mentionViews(this.mentions, this.profiles, orgId, "post", postIds)
558
590
  ]);
559
- const following = new Set(followed);
591
+ const subscriptionOf = new Map(subscribed.map((row) => [row.itemId, row]));
560
592
  const saved = new Set(bookmarked);
561
593
  return posts.map((post) => {
562
594
  const record = records[post.orgUserId];
@@ -569,25 +601,17 @@ var PostServiceImpl = class {
569
601
  reactions: counts?.reactions ?? {},
570
602
  viewerReaction: counts?.viewerReaction ?? null,
571
603
  commentCount: commentCounts[post.id] ?? 0,
572
- following: following.has(post.id),
573
- bookmarked: saved.has(post.id)
604
+ subscription: activeSubscription(subscriptionOf.get(post.id), now),
605
+ bookmarked: saved.has(post.id),
606
+ mentions: mentioned[post.id] ?? []
574
607
  };
575
608
  });
576
609
  }
577
610
  async commitSubmission(orgId, post, space, role, actor, now) {
578
- return this.uow.run(async ({ posts, outbox }) => {
579
- const next = await this.mustUpdate(posts, orgId, post.id, this.submissionOf(space.settings.requireReview, role, now));
580
- const metadata = this.meta(actor, post.status);
581
- const event = next.status === "published" ? discussionEvents.postPublished.make({
582
- orgId,
583
- data: next,
584
- metadata
585
- }) : discussionEvents.postUpdated.make({
586
- orgId,
587
- data: next,
588
- metadata
589
- });
590
- await outbox.append([event]);
611
+ return this.uow.run(async ({ posts, mentions, outbox }) => {
612
+ const next = await posts.updateIf(orgId, post.id, { status: post.status }, this.submissionOf(space.settings.requireReview, role, now));
613
+ if (!next) throw new ConflictError("This post cannot be submitted");
614
+ await outbox.append([await this.landed(mentions, orgId, next, this.meta(actor, post.status))]);
591
615
  return next;
592
616
  });
593
617
  }
@@ -598,11 +622,7 @@ var PostServiceImpl = class {
598
622
  if (unchanged(post)) return this.viewOf(orgId, actor, post);
599
623
  const updated = await this.uow.run(async ({ posts, outbox }) => {
600
624
  const next = await this.mustUpdate(posts, orgId, postId, patch);
601
- await outbox.append([discussionEvents.postUpdated.make({
602
- orgId,
603
- data: next,
604
- metadata: this.meta(actor, next.status === "pending" ? post.status : void 0)
605
- })]);
625
+ await outbox.append([this.updated(orgId, next, this.meta(actor, next.status === "pending" ? post.status : void 0))]);
606
626
  return next;
607
627
  });
608
628
  this.logger.info(log, {
@@ -710,6 +730,38 @@ var PostServiceImpl = class {
710
730
  return post[key] !== value;
711
731
  });
712
732
  }
733
+ async mentionable(orgId, spaceId, title, body) {
734
+ return this.openersOf(orgId, spaceId, mentionTokens(title, body));
735
+ }
736
+ async openersOf(orgId, spaceId, orgUserIds) {
737
+ if (orgUserIds.length === 0) return [];
738
+ return keepAllowed(orgUserIds, await this.access.spaceOpenersAmong(orgId, spaceId, orgUserIds));
739
+ }
740
+ async landed(mentions, orgId, post, metadata) {
741
+ if (post.status !== "published") return this.updated(orgId, post, metadata);
742
+ const mentioned = await this.openersOf(orgId, post.spaceId, await mentions.ofItem(orgId, "post", post.id));
743
+ return this.published(orgId, post, metadata, mentioned);
744
+ }
745
+ published(orgId, post, metadata, mentionedOrgUserIds) {
746
+ return discussionEvents.postPublished.make({
747
+ orgId,
748
+ data: {
749
+ ...post,
750
+ mentionedOrgUserIds
751
+ },
752
+ metadata
753
+ });
754
+ }
755
+ updated(orgId, post, metadata, mentionedOrgUserIds = []) {
756
+ return discussionEvents.postUpdated.make({
757
+ orgId,
758
+ data: {
759
+ ...post,
760
+ mentionedOrgUserIds
761
+ },
762
+ metadata
763
+ });
764
+ }
713
765
  meta(actor, previousStatus) {
714
766
  return {
715
767
  ...actor ? { actorOrgUserId: actor.id } : {},