@headless-lms/core 0.3.0 → 0.5.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 (307) hide show
  1. package/dist/assets/events.d.ts +24 -18
  2. package/dist/assets/file-urls.d.ts +11 -7
  3. package/dist/assets/file-urls.js +14 -4
  4. package/dist/assets/file-urls.js.map +1 -1
  5. package/dist/assets/index.d.ts +5 -4
  6. package/dist/assets/index.js +4 -3
  7. package/dist/assets/jobs.d.ts +15 -0
  8. package/dist/assets/jobs.js +30 -0
  9. package/dist/assets/jobs.js.map +1 -0
  10. package/dist/assets/ports.d.ts +37 -17
  11. package/dist/assets/service.d.ts +30 -15
  12. package/dist/assets/service.js +229 -59
  13. package/dist/assets/service.js.map +1 -1
  14. package/dist/automations/actions.js +2 -63
  15. package/dist/automations/actions.js.map +1 -1
  16. package/dist/automations/catalog.d.ts +1 -2
  17. package/dist/automations/catalog.js +137 -65
  18. package/dist/automations/catalog.js.map +1 -1
  19. package/dist/automations/community-derivations.js +20 -20
  20. package/dist/automations/community-derivations.js.map +1 -1
  21. package/dist/automations/community-mail.d.ts +36 -20
  22. package/dist/automations/community-mail.js +32 -24
  23. package/dist/automations/community-mail.js.map +1 -1
  24. package/dist/automations/event-permissions.d.ts +12 -0
  25. package/dist/automations/event-permissions.js +38 -0
  26. package/dist/automations/event-permissions.js.map +1 -0
  27. package/dist/automations/events.d.ts +0 -10
  28. package/dist/automations/index.d.ts +12 -4
  29. package/dist/automations/index.js +9 -1
  30. package/dist/automations/notifications/applicable.d.ts +8 -0
  31. package/dist/automations/notifications/applicable.js +12 -0
  32. package/dist/automations/notifications/applicable.js.map +1 -0
  33. package/dist/automations/notifications/debounce.d.ts +25 -0
  34. package/dist/automations/notifications/debounce.js +93 -0
  35. package/dist/automations/notifications/debounce.js.map +1 -0
  36. package/dist/automations/notifications/defaults.d.ts +8 -0
  37. package/dist/automations/notifications/defaults.js +346 -0
  38. package/dist/automations/notifications/defaults.js.map +1 -0
  39. package/dist/automations/notifications/delivery.d.ts +28 -0
  40. package/dist/automations/notifications/delivery.js +88 -0
  41. package/dist/automations/notifications/delivery.js.map +1 -0
  42. package/dist/automations/notifications/processor.d.ts +64 -0
  43. package/dist/automations/notifications/processor.js +270 -0
  44. package/dist/automations/notifications/processor.js.map +1 -0
  45. package/dist/automations/notifications/rules/content.d.ts +6 -0
  46. package/dist/automations/notifications/rules/content.js +96 -0
  47. package/dist/automations/notifications/rules/content.js.map +1 -0
  48. package/dist/automations/notifications/rules/discussion.d.ts +7 -0
  49. package/dist/automations/notifications/rules/discussion.js +678 -0
  50. package/dist/automations/notifications/rules/discussion.js.map +1 -0
  51. package/dist/automations/notifications/rules/entitlements.d.ts +6 -0
  52. package/dist/automations/notifications/rules/entitlements.js +276 -0
  53. package/dist/automations/notifications/rules/entitlements.js.map +1 -0
  54. package/dist/automations/notifications/rules/events.d.ts +9 -0
  55. package/dist/automations/notifications/rules/events.js +27 -0
  56. package/dist/automations/notifications/rules/events.js.map +1 -0
  57. package/dist/automations/notifications/rules/index.d.ts +6 -0
  58. package/dist/automations/notifications/rules/index.js +15 -0
  59. package/dist/automations/notifications/rules/index.js.map +1 -0
  60. package/dist/automations/notifications/rules/organizations.d.ts +6 -0
  61. package/dist/automations/notifications/rules/organizations.js +35 -0
  62. package/dist/automations/notifications/rules/organizations.js.map +1 -0
  63. package/dist/automations/notifications/rules/recipients.d.ts +32 -0
  64. package/dist/automations/notifications/rules/recipients.js +98 -0
  65. package/dist/automations/notifications/rules/recipients.js.map +1 -0
  66. package/dist/automations/notifications/service.d.ts +29 -0
  67. package/dist/automations/notifications/service.js +45 -0
  68. package/dist/automations/notifications/service.js.map +1 -0
  69. package/dist/automations/notifications/types.d.ts +7 -0
  70. package/dist/automations/notifications/types.js +560 -0
  71. package/dist/automations/notifications/types.js.map +1 -0
  72. package/dist/automations/ports.d.ts +134 -21
  73. package/dist/automations/service.d.ts +30 -30
  74. package/dist/automations/service.js +111 -144
  75. package/dist/automations/service.js.map +1 -1
  76. package/dist/content/event-sight.d.ts +11 -0
  77. package/dist/content/event-sight.js +11 -0
  78. package/dist/content/event-sight.js.map +1 -0
  79. package/dist/content/events.d.ts +437 -46
  80. package/dist/content/events.js +45 -1
  81. package/dist/content/events.js.map +1 -1
  82. package/dist/content/index.d.ts +8 -4
  83. package/dist/content/index.js +5 -1
  84. package/dist/content/jobs.d.ts +16 -0
  85. package/dist/content/jobs.js +37 -0
  86. package/dist/content/jobs.js.map +1 -0
  87. package/dist/content/model.d.ts +2 -2
  88. package/dist/content/notification-categories.d.ts +37 -0
  89. package/dist/content/notification-categories.js +73 -0
  90. package/dist/content/notification-categories.js.map +1 -0
  91. package/dist/content/ports.d.ts +132 -68
  92. package/dist/content/service.d.ts +100 -68
  93. package/dist/content/service.js +703 -146
  94. package/dist/content/service.js.map +1 -1
  95. package/dist/content/titles.d.ts +25 -0
  96. package/dist/content/titles.js +82 -0
  97. package/dist/content/titles.js.map +1 -0
  98. package/dist/content/types.d.ts +2 -2
  99. package/dist/discussion/authors.d.ts +4 -2
  100. package/dist/discussion/authors.js +9 -5
  101. package/dist/discussion/authors.js.map +1 -1
  102. package/dist/discussion/chats.d.ts +63 -39
  103. package/dist/discussion/chats.js +455 -190
  104. package/dist/discussion/chats.js.map +1 -1
  105. package/dist/discussion/digests.d.ts +24 -0
  106. package/dist/discussion/digests.js +104 -0
  107. package/dist/discussion/digests.js.map +1 -0
  108. package/dist/discussion/events.d.ts +227 -23
  109. package/dist/discussion/events.js +114 -19
  110. package/dist/discussion/events.js.map +1 -1
  111. package/dist/discussion/index.d.ts +12 -4
  112. package/dist/discussion/index.js +9 -2
  113. package/dist/discussion/jobs.d.ts +16 -0
  114. package/dist/discussion/jobs.js +40 -0
  115. package/dist/discussion/jobs.js.map +1 -0
  116. package/dist/discussion/live.d.ts +106 -0
  117. package/dist/discussion/live.js +642 -0
  118. package/dist/discussion/live.js.map +1 -0
  119. package/dist/discussion/mentions.d.ts +11 -0
  120. package/dist/discussion/mentions.js +35 -0
  121. package/dist/discussion/mentions.js.map +1 -0
  122. package/dist/discussion/ports.d.ts +279 -114
  123. package/dist/discussion/ports.js.map +1 -1
  124. package/dist/discussion/posts.d.ts +60 -44
  125. package/dist/discussion/posts.js +453 -287
  126. package/dist/discussion/posts.js.map +1 -1
  127. package/dist/discussion/rules.d.ts +18 -0
  128. package/dist/discussion/rules.js +22 -0
  129. package/dist/discussion/rules.js.map +1 -0
  130. package/dist/discussion/service.d.ts +50 -35
  131. package/dist/discussion/service.js +333 -205
  132. package/dist/discussion/service.js.map +1 -1
  133. package/dist/discussion/subscriptions.d.ts +37 -0
  134. package/dist/discussion/subscriptions.js +115 -0
  135. package/dist/discussion/subscriptions.js.map +1 -0
  136. package/dist/discussion/titles.d.ts +28 -0
  137. package/dist/discussion/titles.js +98 -0
  138. package/dist/discussion/titles.js.map +1 -0
  139. package/dist/entitlements/events.d.ts +20 -20
  140. package/dist/entitlements/index.d.ts +5 -4
  141. package/dist/entitlements/index.js +3 -2
  142. package/dist/entitlements/jobs.d.ts +12 -0
  143. package/dist/entitlements/jobs.js +18 -0
  144. package/dist/entitlements/jobs.js.map +1 -0
  145. package/dist/entitlements/model.d.ts +2 -2
  146. package/dist/entitlements/ports.d.ts +68 -38
  147. package/dist/entitlements/rules.d.ts +3 -6
  148. package/dist/entitlements/rules.js +5 -10
  149. package/dist/entitlements/rules.js.map +1 -1
  150. package/dist/entitlements/service.d.ts +43 -28
  151. package/dist/entitlements/service.js +387 -160
  152. package/dist/entitlements/service.js.map +1 -1
  153. package/dist/identity/devices.d.ts +30 -0
  154. package/dist/identity/devices.js +109 -0
  155. package/dist/identity/devices.js.map +1 -0
  156. package/dist/identity/index.d.ts +6 -5
  157. package/dist/identity/index.js +4 -3
  158. package/dist/identity/model.d.ts +2 -2
  159. package/dist/identity/notification-settings.d.ts +28 -0
  160. package/dist/identity/notification-settings.js +114 -0
  161. package/dist/identity/notification-settings.js.map +1 -0
  162. package/dist/identity/ports.d.ts +80 -56
  163. package/dist/identity/service.d.ts +8 -7
  164. package/dist/identity/service.js +17 -13
  165. package/dist/identity/service.js.map +1 -1
  166. package/dist/integrations/ports.d.ts +9 -8
  167. package/dist/integrations/service.d.ts +9 -8
  168. package/dist/integrations/service.js +33 -16
  169. package/dist/integrations/service.js.map +1 -1
  170. package/dist/organizations/default-roles.d.ts +22 -0
  171. package/dist/organizations/default-roles.js +89 -0
  172. package/dist/organizations/default-roles.js.map +1 -0
  173. package/dist/organizations/effective-permissions.d.ts +45 -0
  174. package/dist/organizations/effective-permissions.js +113 -0
  175. package/dist/organizations/effective-permissions.js.map +1 -0
  176. package/dist/organizations/events.d.ts +320 -181
  177. package/dist/organizations/events.js +90 -18
  178. package/dist/organizations/events.js.map +1 -1
  179. package/dist/organizations/index.d.ts +15 -10
  180. package/dist/organizations/index.js +8 -3
  181. package/dist/organizations/members.d.ts +66 -8
  182. package/dist/organizations/members.js.map +1 -1
  183. package/dist/organizations/notification-catalog.d.ts +31 -0
  184. package/dist/organizations/notification-catalog.js +32 -0
  185. package/dist/organizations/notification-catalog.js.map +1 -0
  186. package/dist/organizations/notification-categories.d.ts +34 -0
  187. package/dist/organizations/notification-categories.js +66 -0
  188. package/dist/organizations/notification-categories.js.map +1 -0
  189. package/dist/organizations/org-roles.d.ts +33 -0
  190. package/dist/organizations/org-roles.js +49 -0
  191. package/dist/organizations/org-roles.js.map +1 -0
  192. package/dist/organizations/permissions.d.ts +311 -0
  193. package/dist/organizations/permissions.js +327 -0
  194. package/dist/organizations/permissions.js.map +1 -0
  195. package/dist/organizations/ports.d.ts +187 -65
  196. package/dist/organizations/service.d.ts +104 -35
  197. package/dist/organizations/service.js +1247 -323
  198. package/dist/organizations/service.js.map +1 -1
  199. package/dist/organizations/testing.d.ts +18 -0
  200. package/dist/organizations/testing.js +37 -0
  201. package/dist/organizations/testing.js.map +1 -0
  202. package/dist/organizations/types.d.ts +5 -16
  203. package/dist/progress/index.d.ts +2 -2
  204. package/dist/progress/ports.d.ts +11 -3
  205. package/dist/progress/service.d.ts +6 -5
  206. package/dist/progress/service.js +23 -8
  207. package/dist/progress/service.js.map +1 -1
  208. package/dist/reporting/admin/index.d.ts +6 -4
  209. package/dist/reporting/admin/model.d.ts +36 -3
  210. package/dist/reporting/admin/ports.d.ts +39 -14
  211. package/dist/reporting/admin/service.d.ts +32 -11
  212. package/dist/reporting/admin/service.js +102 -18
  213. package/dist/reporting/admin/service.js.map +1 -1
  214. package/dist/reporting/events/index.d.ts +27 -0
  215. package/dist/reporting/events/index.js +20 -0
  216. package/dist/reporting/events/index.js.map +1 -0
  217. package/dist/reporting/member/calendar.d.ts +11 -0
  218. package/dist/reporting/member/calendar.js +75 -0
  219. package/dist/reporting/member/calendar.js.map +1 -0
  220. package/dist/reporting/member/index.d.ts +3 -2
  221. package/dist/reporting/member/model.d.ts +80 -26
  222. package/dist/reporting/member/ports.d.ts +50 -28
  223. package/dist/reporting/member/service.d.ts +81 -35
  224. package/dist/reporting/member/service.js +438 -145
  225. package/dist/reporting/member/service.js.map +1 -1
  226. package/dist/shared/actor.d.ts +22 -3
  227. package/dist/shared/errors.d.ts +15 -2
  228. package/dist/shared/errors.js +21 -2
  229. package/dist/shared/errors.js.map +1 -1
  230. package/dist/shared/id.d.ts +7 -0
  231. package/dist/shared/id.js +8 -1
  232. package/dist/shared/id.js.map +1 -1
  233. package/dist/shared/logger.d.ts +2 -1
  234. package/dist/shared/logger.js +10 -1
  235. package/dist/shared/logger.js.map +1 -1
  236. package/dist/shared/mailer.d.ts +11 -1
  237. package/dist/shared/mailer.js +32 -5
  238. package/dist/shared/mailer.js.map +1 -1
  239. package/dist/shared/notification-categories.d.ts +16 -0
  240. package/dist/shared/notification-categories.js +94 -0
  241. package/dist/shared/notification-categories.js.map +1 -0
  242. package/dist/shared/notification-settings.d.ts +14 -0
  243. package/dist/shared/notification-settings.js +61 -0
  244. package/dist/shared/notification-settings.js.map +1 -0
  245. package/dist/shared/ports.d.ts +68 -2
  246. package/dist/shared/settings.d.ts +2 -1
  247. package/dist/shared/settings.js +1 -0
  248. package/dist/shared/settings.js.map +1 -1
  249. package/dist/types/automations.d.ts +8 -2
  250. package/dist/types/content.d.ts +2 -2
  251. package/dist/types/discussion.d.ts +4 -5
  252. package/dist/types/email-templates.d.ts +119 -6
  253. package/dist/types/entitlements.d.ts +2 -2
  254. package/dist/types/identity.d.ts +14 -17
  255. package/dist/types/index.d.ts +13 -12
  256. package/dist/types/index.js +2 -1
  257. package/dist/types/notifications.d.ts +157 -0
  258. package/dist/types/notifications.js +1 -0
  259. package/dist/types/organizations.d.ts +3 -2
  260. package/dist/types/ports.d.ts +8 -1
  261. package/dist/types/ports.js +7 -1
  262. package/dist/types/ports.js.map +1 -0
  263. package/dist/types/schemas/assets.d.ts +5 -5
  264. package/dist/types/schemas/assets.js +4 -3
  265. package/dist/types/schemas/assets.js.map +1 -1
  266. package/dist/types/schemas/automations.d.ts +1 -10
  267. package/dist/types/schemas/automations.js +1 -3
  268. package/dist/types/schemas/automations.js.map +1 -1
  269. package/dist/types/schemas/content.d.ts +596 -11
  270. package/dist/types/schemas/content.js +225 -9
  271. package/dist/types/schemas/content.js.map +1 -1
  272. package/dist/types/schemas/discussion.d.ts +362 -66
  273. package/dist/types/schemas/discussion.js +123 -15
  274. package/dist/types/schemas/discussion.js.map +1 -1
  275. package/dist/types/schemas/entitlements.d.ts +14 -1
  276. package/dist/types/schemas/entitlements.js +11 -4
  277. package/dist/types/schemas/entitlements.js.map +1 -1
  278. package/dist/types/schemas/identity.d.ts +1 -85
  279. package/dist/types/schemas/identity.js +1 -42
  280. package/dist/types/schemas/identity.js.map +1 -1
  281. package/dist/types/schemas/index.d.ts +8 -7
  282. package/dist/types/schemas/index.js +8 -7
  283. package/dist/types/schemas/notifications.d.ts +96 -0
  284. package/dist/types/schemas/notifications.js +57 -0
  285. package/dist/types/schemas/notifications.js.map +1 -0
  286. package/dist/types/schemas/organizations.d.ts +96 -60
  287. package/dist/types/schemas/organizations.js +78 -27
  288. package/dist/types/schemas/organizations.js.map +1 -1
  289. package/dist/types/schemas/progress.d.ts +0 -1
  290. package/dist/types/schemas/progress.js +0 -1
  291. package/dist/types/schemas/progress.js.map +1 -1
  292. package/package.json +1 -1
  293. package/dist/automations/community-notifications.d.ts +0 -16
  294. package/dist/automations/community-notifications.js +0 -84
  295. package/dist/automations/community-notifications.js.map +0 -1
  296. package/dist/automations/seeds.d.ts +0 -17
  297. package/dist/automations/seeds.js +0 -235
  298. package/dist/automations/seeds.js.map +0 -1
  299. package/dist/identity/notifications.d.ts +0 -27
  300. package/dist/identity/notifications.js +0 -101
  301. package/dist/identity/notifications.js.map +0 -1
  302. package/dist/organizations/access-scopes.d.ts +0 -19
  303. package/dist/organizations/access-scopes.js +0 -89
  304. package/dist/organizations/access-scopes.js.map +0 -1
  305. package/dist/organizations/roles.d.ts +0 -19
  306. package/dist/organizations/roles.js +0 -82
  307. package/dist/organizations/roles.js.map +0 -1
@@ -1,18 +1,28 @@
1
- import { genId } from "../shared/id.js";
2
- import { ConflictError, ForbiddenError, InvalidInputError, NotFoundError } from "../shared/errors.js";
3
1
  import { noopLogger } from "../shared/logger.js";
4
- import { canModerate, isStaffRole } from "../organizations/roles.js";
2
+ import { ConflictError, ForbiddenError, InvalidInputError, NotFoundError, UnavailableError } from "../shared/errors.js";
3
+ import { genId } from "../shared/id.js";
5
4
  import { discussionEvents } from "./events.js";
6
5
  import { authorView } from "./authors.js";
6
+ import { keepAllowed, mentionTokens, mentionViews } from "./mentions.js";
7
+ import { skipsReview, teamBadge } from "./rules.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
12
+ const MAX_BODY_ASSETS = 20;
13
+ const INLINE_TYPES = /^(image\/(png|jpeg|gif|webp|avif)|video\/.+|audio\/.+|application\/pdf)$/i;
9
14
  const PUBLISHED_ONLY = ["published"];
10
- const DUE_BATCH_SIZE = 100;
11
15
  const MODERATOR_VISIBLE = [
12
16
  "published",
13
17
  "pending",
14
18
  "removed"
15
19
  ];
20
+ const DRAFT = {
21
+ status: "draft",
22
+ scheduledAt: null,
23
+ publishedAt: null,
24
+ removedBy: null
25
+ };
16
26
  var PostServiceImpl = class {
17
27
  repo;
18
28
  access;
@@ -20,9 +30,13 @@ var PostServiceImpl = class {
20
30
  authors;
21
31
  profiles;
22
32
  comments;
33
+ subscriptions;
34
+ mentions;
23
35
  images;
24
36
  fileUrls;
37
+ bodies;
25
38
  uow;
39
+ scheduler;
26
40
  logger;
27
41
  constructor(params) {
28
42
  this.repo = params.repo;
@@ -31,20 +45,31 @@ var PostServiceImpl = class {
31
45
  this.authors = params.authors;
32
46
  this.profiles = params.profiles;
33
47
  this.comments = params.comments;
48
+ this.subscriptions = params.subscriptions;
49
+ this.mentions = params.mentions;
34
50
  this.images = params.images;
35
51
  this.fileUrls = params.fileUrls;
52
+ this.bodies = params.bodies;
36
53
  this.uow = params.uow;
54
+ this.scheduler = params.scheduler;
37
55
  this.logger = params.logger ?? noopLogger;
38
56
  }
39
- async visibleTitles(orgId, actor, postIds) {
57
+ async visibleTitles(postIds, actor) {
58
+ const orgId = actor.orgId;
40
59
  const found = await this.repo.findMany(orgId, [...new Set(postIds)]);
41
60
  const communityIds = [...new Set(found.map((post) => post.communityId))];
42
- const open = new Set((await Promise.all(communityIds.map((communityId) => this.access.openSpaceIds(orgId, actor, communityId)))).flat());
61
+ const open = new Set((await Promise.all(communityIds.map((communityId) => this.access.openSpaceIds(communityId, actor)))).flat());
43
62
  return Object.fromEntries(found.filter((post) => open.has(post.spaceId) && this.canSee(actor, post)).map((post) => [post.id, post.title]));
44
63
  }
45
- async create(orgId, actor, input) {
46
- const space = await this.requireOpen(orgId, actor, input.spaceId, input.communityId);
64
+ publishedBetween(orgId, communityId, from, to) {
65
+ return this.repo.publishedBetween(orgId, communityId, from, to);
66
+ }
67
+ async create(input, actor) {
68
+ const orgId = actor.orgId;
69
+ const space = await this.requireOpen(actor, input.spaceId, input.communityId);
47
70
  await this.requireOwnedImage(orgId, actor.id, input.coverAssetId);
71
+ const parsed = this.bodies.parse(input.body);
72
+ await this.requireBodyAssets(orgId, actor.id, parsed.assetIds);
48
73
  this.assertTopic(space, input.topicId);
49
74
  const now = /* @__PURE__ */ new Date();
50
75
  const landing = this.initial(input, space, actor, now);
@@ -66,18 +91,16 @@ var PostServiceImpl = class {
66
91
  updatedAt: now,
67
92
  ...landing
68
93
  };
69
- const saved = await this.uow.run(async ({ posts, outbox }) => {
94
+ const mentioned = await this.mentionable(orgId, space.id, post.title, parsed.mentions);
95
+ const saved = await this.uow.run(async ({ posts, mentions, outbox }) => {
70
96
  const created = await posts.insert(orgId, post);
97
+ await mentions.sync(orgId, "post", created.id, mentioned, now);
71
98
  const events = [discussionEvents.postCreated.make({
72
99
  orgId,
73
100
  data: created,
74
101
  metadata: this.meta(actor, created.status === "pending" ? null : void 0)
75
102
  })];
76
- if (created.status === "published") events.push(discussionEvents.postPublished.make({
77
- orgId,
78
- data: created,
79
- metadata: this.meta(actor, null)
80
- }));
103
+ if (created.status === "published") events.push(this.published(orgId, created, this.meta(actor, null), mentioned));
81
104
  await outbox.append(events);
82
105
  return created;
83
106
  });
@@ -87,22 +110,26 @@ var PostServiceImpl = class {
87
110
  status: saved.status,
88
111
  actorId: actor.id
89
112
  });
90
- return this.viewOf(orgId, actor, saved);
113
+ if (saved.status === "scheduled") await this.arm(orgId, actor, saved, null);
114
+ await this.syncCover(saved);
115
+ return this.viewOf(actor, saved);
91
116
  }
92
- async update(orgId, postId, actor, patch) {
93
- const { post, space } = await this.loadVisible(orgId, actor, postId);
117
+ async update(postId, patch, actor) {
118
+ const orgId = actor.orgId;
119
+ const { post, space } = await this.loadVisible(actor, postId);
94
120
  this.assertAuthor(actor, post);
95
121
  if (post.status === "removed") throw new ConflictError("This post was removed");
96
122
  if (patch.topicId !== void 0) this.assertTopic(space, patch.topicId);
97
123
  if (typeof patch.coverAssetId === "string") await this.requireOwnedImage(orgId, actor.id, patch.coverAssetId);
98
- if (!this.patchChanges(post, patch)) return this.viewOf(orgId, actor, post);
99
- const updated = await this.uow.run(async ({ posts, outbox }) => {
124
+ const parsed = this.bodies.parse(patch.body ?? post.body);
125
+ if (patch.body !== void 0) await this.requireBodyAssets(orgId, actor.id, parsed.assetIds);
126
+ if (!this.patchChanges(post, patch)) return this.viewOf(actor, post);
127
+ const retext = patch.title !== void 0 || patch.body !== void 0;
128
+ const candidates = retext ? await this.mentionable(orgId, post.spaceId, patch.title ?? post.title, parsed.mentions) : [];
129
+ const updated = await this.uow.run(async ({ posts, mentions, outbox }) => {
100
130
  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
- })]);
131
+ const added = retext ? await mentions.sync(orgId, "post", postId, candidates, /* @__PURE__ */ new Date()) : [];
132
+ await outbox.append([this.updated(orgId, next, this.meta(actor, next.status === "pending" ? post.status : void 0), next.status === "published" ? keepAllowed(candidates, added) : [])]);
106
133
  return next;
107
134
  });
108
135
  this.logger.info("post updated", {
@@ -111,39 +138,41 @@ var PostServiceImpl = class {
111
138
  fields: Object.keys(patch),
112
139
  actorId: actor.id
113
140
  });
114
- return this.viewOf(orgId, actor, updated);
141
+ await this.syncCover(updated);
142
+ return this.viewOf(actor, updated);
115
143
  }
116
- async submit(orgId, postId, actor) {
117
- const { post, space } = await this.loadVisible(orgId, actor, postId);
144
+ async submit(postId, actor) {
145
+ const orgId = actor.orgId;
146
+ const { post, space } = await this.loadVisible(actor, postId);
118
147
  this.assertAuthor(actor, post);
119
148
  if (post.status !== "draft" && post.status !== "scheduled") throw new ConflictError("This post cannot be submitted");
120
149
  this.assertTopic(space, post.topicId);
121
- const saved = await this.commitSubmission(orgId, post, space, actor.role, actor, /* @__PURE__ */ new Date());
150
+ const saved = await this.commitSubmission(orgId, post, space, skipsReview(actor.permissions, { communityId: post.communityId }), actor, /* @__PURE__ */ new Date());
122
151
  this.logger.info("post submitted", {
123
152
  orgId,
124
153
  postId,
125
154
  status: saved.status,
126
155
  actorId: actor.id
127
156
  });
128
- return this.viewOf(orgId, actor, saved);
157
+ if (post.status === "scheduled") await this.disarm(orgId, postId);
158
+ await this.syncCover(saved);
159
+ return this.viewOf(actor, saved);
129
160
  }
130
- async schedule(orgId, postId, actor, scheduledAt) {
131
- const { post } = await this.loadVisible(orgId, actor, postId);
161
+ async schedule(postId, scheduledAt, actor) {
162
+ const orgId = actor.orgId;
163
+ const { post } = await this.loadVisible(actor, postId);
132
164
  this.assertAuthor(actor, post);
133
165
  if (post.status === "published" || post.status === "removed" || post.status === "pending") throw new ConflictError("This post cannot be scheduled");
134
166
  if (scheduledAt <= /* @__PURE__ */ new Date()) throw new InvalidInputError("A scheduled time must be in the future");
135
167
  const updated = await this.uow.run(async ({ posts, outbox }) => {
136
- const next = await this.mustUpdate(posts, orgId, postId, {
168
+ const next = await posts.updateIf(orgId, postId, { status: post.status }, {
137
169
  status: "scheduled",
138
170
  scheduledAt,
139
171
  publishedAt: null,
140
172
  removedBy: null
141
173
  });
142
- await outbox.append([discussionEvents.postUpdated.make({
143
- orgId,
144
- data: next,
145
- metadata: this.meta(actor, post.status)
146
- })]);
174
+ if (!next) throw new ConflictError("This post cannot be scheduled");
175
+ await outbox.append([this.updated(orgId, next, this.meta(actor, post.status))]);
147
176
  return next;
148
177
  });
149
178
  this.logger.info("post scheduled", {
@@ -152,13 +181,134 @@ var PostServiceImpl = class {
152
181
  scheduledAt: scheduledAt.toISOString(),
153
182
  actorId: actor.id
154
183
  });
155
- return this.viewOf(orgId, actor, updated);
184
+ await this.arm(orgId, actor, updated, post.status === "scheduled" ? post.scheduledAt : null);
185
+ await this.syncCover(updated);
186
+ return this.viewOf(actor, updated);
187
+ }
188
+ async unschedule(postId, actor) {
189
+ const orgId = actor.orgId;
190
+ const { post } = await this.loadVisible(actor, postId);
191
+ this.assertAuthor(actor, post);
192
+ if (post.status !== "scheduled") throw new ConflictError("This post is not scheduled");
193
+ const updated = await this.uow.run(async ({ posts, outbox }) => {
194
+ const next = await posts.updateIf(orgId, postId, { status: "scheduled" }, DRAFT);
195
+ if (!next) throw new ConflictError("This post is not scheduled");
196
+ await outbox.append([this.updated(orgId, next, this.meta(actor, post.status))]);
197
+ return next;
198
+ });
199
+ this.logger.info("post unscheduled", {
200
+ orgId,
201
+ postId,
202
+ actorId: actor.id
203
+ });
204
+ await this.disarm(orgId, postId);
205
+ await this.syncCover(updated);
206
+ return this.viewOf(actor, updated);
156
207
  }
157
- async approve(orgId, postId, actor, communityId) {
158
- const { post } = await this.loadVisible(orgId, actor, postId, communityId);
159
- if (!this.canModerate(actor)) throw new ForbiddenError("You cannot review this post");
208
+ async publishScheduled(orgId, postId, now) {
209
+ const post = await this.repo.find(orgId, postId);
210
+ if (!post || post.status !== "scheduled" || post.scheduledAt === null) {
211
+ this.logger.debug("scheduled post no longer scheduled", {
212
+ orgId,
213
+ postId
214
+ });
215
+ return;
216
+ }
217
+ if (post.scheduledAt > now) {
218
+ await this.scheduler.at(POST_PUBLISH_JOB, post.scheduledAt, postId, {
219
+ orgId,
220
+ postId
221
+ });
222
+ this.logger.info("scheduled post timer moved", {
223
+ orgId,
224
+ postId,
225
+ scheduledAt: post.scheduledAt.toISOString()
226
+ });
227
+ return;
228
+ }
229
+ const space = await this.content.getSpace(post.spaceId, this.authors.systemActor(orgId));
230
+ if (!space) {
231
+ this.logger.error("scheduled post has no space", {
232
+ orgId,
233
+ postId,
234
+ spaceId: post.spaceId
235
+ });
236
+ return;
237
+ }
238
+ const author = await this.authors.actorFor(orgId, post.orgUserId);
239
+ const refused = author === null || !await this.submittable(post, space, author);
240
+ const patch = author === null || refused ? DRAFT : this.submissionOf(space.settings.requireReview, skipsReview(author.permissions, { communityId: post.communityId }), now);
241
+ const mentioned = await this.mentionedOpeners(orgId, post);
242
+ const saved = await this.uow.run(async ({ posts, outbox }) => {
243
+ const next = await posts.updateIf(orgId, postId, {
244
+ status: "scheduled",
245
+ dueBy: now
246
+ }, patch);
247
+ if (!next) return null;
248
+ await outbox.append([this.landed(orgId, next, this.meta(null, post.status), mentioned)]);
249
+ return next;
250
+ });
251
+ if (!saved) {
252
+ this.logger.debug("scheduled post already handled", {
253
+ orgId,
254
+ postId
255
+ });
256
+ return;
257
+ }
258
+ this.logger.info("scheduled post submitted", {
259
+ orgId,
260
+ postId,
261
+ status: saved.status,
262
+ refused
263
+ });
264
+ await this.syncCover(saved);
265
+ }
266
+ async arm(orgId, actor, post, previousScheduledAt) {
267
+ if (post.scheduledAt === null) return;
268
+ try {
269
+ await this.scheduler.at(POST_PUBLISH_JOB, post.scheduledAt, post.id, {
270
+ orgId,
271
+ postId: post.id
272
+ });
273
+ return;
274
+ } catch (err) {
275
+ this.logger.error("post timer not set", {
276
+ orgId,
277
+ postId: post.id,
278
+ err
279
+ });
280
+ }
281
+ await this.uow.run(async ({ posts, outbox }) => {
282
+ const reverted = await posts.updateIf(orgId, post.id, {
283
+ status: "scheduled",
284
+ scheduledAt: post.scheduledAt
285
+ }, previousScheduledAt ? { scheduledAt: previousScheduledAt } : DRAFT);
286
+ if (!reverted) return;
287
+ await outbox.append([this.updated(orgId, reverted, this.meta(actor, reverted.status === "draft" ? post.status : void 0))]);
288
+ });
289
+ throw new UnavailableError("The post could not be scheduled; try again", { postId: post.id });
290
+ }
291
+ syncCover(post) {
292
+ return this.images.syncPublicImage(post.orgId, post.id, post.status === "published" ? post.coverAssetId : null);
293
+ }
294
+ async disarm(orgId, postId) {
295
+ try {
296
+ await this.scheduler.cancel(POST_PUBLISH_JOB, postId);
297
+ } catch (err) {
298
+ this.logger.warn("post timer not cancelled", {
299
+ orgId,
300
+ postId,
301
+ err
302
+ });
303
+ }
304
+ }
305
+ async approve(postId, communityId, actor) {
306
+ const orgId = actor.orgId;
307
+ const { post } = await this.loadVisible(actor, postId, communityId);
308
+ this.requireModerator(actor, post, "approve");
160
309
  if (post.status !== "pending") throw new ConflictError("This post is not awaiting review");
161
310
  const now = /* @__PURE__ */ new Date();
311
+ const mentioned = await this.mentionedOpeners(orgId, post);
162
312
  const updated = await this.uow.run(async ({ posts, outbox }) => {
163
313
  const next = await this.mustUpdate(posts, orgId, postId, {
164
314
  status: "published",
@@ -166,11 +316,7 @@ var PostServiceImpl = class {
166
316
  scheduledAt: null,
167
317
  removedBy: null
168
318
  });
169
- await outbox.append([discussionEvents.postPublished.make({
170
- orgId,
171
- data: next,
172
- metadata: this.meta(actor, post.status)
173
- })]);
319
+ await outbox.append([this.landed(orgId, next, this.meta(actor, post.status), mentioned)]);
174
320
  return next;
175
321
  });
176
322
  this.logger.info("post approved", {
@@ -178,11 +324,13 @@ var PostServiceImpl = class {
178
324
  postId,
179
325
  actorId: actor.id
180
326
  });
181
- return this.viewOf(orgId, actor, updated);
327
+ await this.syncCover(updated);
328
+ return this.viewOf(actor, updated);
182
329
  }
183
- async remove(orgId, postId, actor, communityId) {
184
- const { post } = await this.loadVisible(orgId, actor, postId, communityId);
185
- if (post.orgUserId !== actor.id && !this.canModerate(actor)) throw new ForbiddenError("You cannot remove this post");
330
+ async remove(postId, communityId, actor) {
331
+ const orgId = actor.orgId;
332
+ const { post } = await this.loadVisible(actor, postId, communityId);
333
+ if (post.orgUserId !== actor.id) this.requireModerator(actor, post, "remove");
186
334
  if (post.status === "removed") throw new ConflictError("This post was removed");
187
335
  if (post.status !== "published" && post.status !== "pending") throw new ConflictError("This post cannot be removed");
188
336
  const updated = await this.uow.run(async ({ posts, outbox }) => {
@@ -207,27 +355,21 @@ var PostServiceImpl = class {
207
355
  postId,
208
356
  actorId: actor.id
209
357
  });
210
- return this.viewOf(orgId, actor, updated);
358
+ await this.syncCover(updated);
359
+ return this.viewOf(actor, updated);
211
360
  }
212
- async restore(orgId, postId, actor, communityId) {
213
- const { post, space } = await this.loadVisible(orgId, actor, postId, communityId);
214
- if (!this.canModerate(actor)) throw new ForbiddenError("You cannot moderate this post");
361
+ async restore(postId, communityId, actor) {
362
+ const orgId = actor.orgId;
363
+ const { post, space } = await this.loadVisible(actor, postId, communityId);
364
+ this.requireModerator(actor, post, "restore");
215
365
  if (post.status !== "removed") throw new ConflictError("This post was not removed");
216
366
  const now = /* @__PURE__ */ new Date();
217
- const role = await this.authors.roleOf(orgId, post.orgUserId);
367
+ const author = (await this.authors.permissionsOfMany(orgId, [post.orgUserId])).get(post.orgUserId);
368
+ const skips = author !== void 0 && skipsReview(author, { communityId: post.communityId });
218
369
  const updated = await this.uow.run(async ({ posts, outbox }) => {
219
- const next = await this.mustUpdate(posts, orgId, postId, this.submissionOf(space.settings.requireReview, role, now));
370
+ const next = await this.mustUpdate(posts, orgId, postId, this.submissionOf(space.settings.requireReview, skips, now));
220
371
  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]);
372
+ await outbox.append([next.status === "published" ? this.published(orgId, next, metadata, []) : this.updated(orgId, next, metadata)]);
231
373
  return next;
232
374
  });
233
375
  this.logger.info("post restored", {
@@ -236,27 +378,42 @@ var PostServiceImpl = class {
236
378
  status: updated.status,
237
379
  actorId: actor.id
238
380
  });
239
- return this.viewOf(orgId, actor, updated);
381
+ await this.syncCover(updated);
382
+ return this.viewOf(actor, updated);
240
383
  }
241
- pin(orgId, postId, actor) {
242
- return this.moderate(orgId, postId, actor, { pinned: true }, (post) => post.pinned, "post pinned");
384
+ pin(postId, actor) {
385
+ return this.moderate(postId, actor, { pinned: true }, (post) => post.pinned, "post pinned");
243
386
  }
244
- unpin(orgId, postId, actor) {
245
- return this.moderate(orgId, postId, actor, { pinned: false }, (post) => !post.pinned, "post unpinned");
387
+ unpin(postId, actor) {
388
+ return this.moderate(postId, actor, { pinned: false }, (post) => !post.pinned, "post unpinned");
246
389
  }
247
- closeComments(orgId, postId, actor) {
248
- return this.moderate(orgId, postId, actor, { commentsClosed: true }, (post) => post.commentsClosed === true, "post comments closed");
390
+ closeComments(postId, actor) {
391
+ return this.moderate(postId, actor, { commentsClosed: true }, (post) => post.commentsClosed === true, "post comments closed");
249
392
  }
250
- openComments(orgId, postId, actor) {
251
- return this.moderate(orgId, postId, actor, { commentsClosed: false }, (post) => post.commentsClosed === false, "post comments opened");
393
+ openComments(postId, actor) {
394
+ return this.moderate(postId, actor, { commentsClosed: false }, (post) => post.commentsClosed === false, "post comments opened");
252
395
  }
253
- setReactions(orgId, postId, actor, enabled) {
254
- return this.moderate(orgId, postId, actor, { reactionsEnabled: enabled }, (post) => post.reactionsEnabled === enabled, "post reactions changed");
396
+ setReactions(postId, enabled, actor) {
397
+ return this.moderate(postId, actor, { reactionsEnabled: enabled }, (post) => post.reactionsEnabled === enabled, "post reactions changed");
255
398
  }
256
- async setReaction(orgId, postId, actor, emoji) {
257
- const { post, space } = await this.loadVisible(orgId, actor, postId);
399
+ async setReaction(postId, emoji, actor) {
400
+ const orgId = actor.orgId;
401
+ const { post, space } = await this.loadVisible(actor, postId);
258
402
  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);
403
+ await this.uow.run(async ({ posts, outbox }) => {
404
+ if (!await posts.setReaction(orgId, postId, actor.id, emoji) || emoji === null) return;
405
+ await outbox.append([discussionEvents.postReactionAdded.make({
406
+ orgId,
407
+ data: {
408
+ postId,
409
+ spaceId: post.spaceId,
410
+ communityId: post.communityId,
411
+ orgUserId: actor.id,
412
+ reaction: emoji
413
+ },
414
+ metadata: this.meta(actor)
415
+ })]);
416
+ });
260
417
  this.logger.debug("post reaction set", {
261
418
  orgId,
262
419
  postId,
@@ -264,26 +421,9 @@ var PostServiceImpl = class {
264
421
  });
265
422
  return (await this.repo.reactionsOf(orgId, [postId], actor.id))[postId] ?? { reactions: {} };
266
423
  }
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
- async bookmark(orgId, postId, actor) {
286
- await this.loadVisible(orgId, actor, postId);
424
+ async bookmark(postId, actor) {
425
+ const orgId = actor.orgId;
426
+ await this.loadVisible(actor, postId);
287
427
  await this.repo.bookmark(orgId, postId, actor.id);
288
428
  this.logger.debug("post bookmarked", {
289
429
  orgId,
@@ -291,8 +431,9 @@ var PostServiceImpl = class {
291
431
  orgUserId: actor.id
292
432
  });
293
433
  }
294
- async unbookmark(orgId, postId, actor) {
295
- await this.loadVisible(orgId, actor, postId);
434
+ async unbookmark(postId, actor) {
435
+ const orgId = actor.orgId;
436
+ await this.loadVisible(actor, postId);
296
437
  await this.repo.unbookmark(orgId, postId, actor.id);
297
438
  this.logger.debug("post unbookmarked", {
298
439
  orgId,
@@ -300,8 +441,9 @@ var PostServiceImpl = class {
300
441
  orgUserId: actor.id
301
442
  });
302
443
  }
303
- async flag(orgId, postId, actor, reason) {
304
- const { post } = await this.loadVisible(orgId, actor, postId);
444
+ async flag(postId, reason, actor) {
445
+ const orgId = actor.orgId;
446
+ const { post } = await this.loadVisible(actor, postId);
305
447
  const now = /* @__PURE__ */ new Date();
306
448
  const flag = {
307
449
  orgId,
@@ -340,9 +482,10 @@ var PostServiceImpl = class {
340
482
  });
341
483
  return saved;
342
484
  }
343
- async resolveFlags(orgId, postId, actor, resolution, communityId) {
344
- await this.loadVisible(orgId, actor, postId, communityId);
345
- if (!this.canModerate(actor)) throw new ForbiddenError("You cannot work this queue");
485
+ async resolveFlags(postId, resolution, communityId, actor) {
486
+ const orgId = actor.orgId;
487
+ const { post } = await this.loadVisible(actor, postId, communityId);
488
+ this.requireModerator(actor, post, "resolve flags");
346
489
  await this.repo.resolveFlags(orgId, postId, {
347
490
  resolvedAt: /* @__PURE__ */ new Date(),
348
491
  resolvedBy: actor.id,
@@ -355,10 +498,11 @@ var PostServiceImpl = class {
355
498
  actorId: actor.id
356
499
  });
357
500
  }
358
- async spaceFeed(orgId, actor, communityId, spaceId, query) {
359
- const space = await this.requireOpen(orgId, actor, spaceId, communityId);
501
+ async spaceFeed(communityId, spaceId, query, actor) {
502
+ const orgId = actor.orgId;
503
+ const space = await this.requireOpen(actor, spaceId, communityId);
360
504
  if (query.topicId && !space.topics.some((topic) => topic.id === query.topicId)) throw new NotFoundError("Topic", query.topicId);
361
- const page = await this.feedPage(orgId, actor, [spaceId], query);
505
+ const page = await this.feedPage(actor, [spaceId], query);
362
506
  this.logger.debug("space feed read", {
363
507
  orgId,
364
508
  spaceId,
@@ -366,9 +510,10 @@ var PostServiceImpl = class {
366
510
  });
367
511
  return page;
368
512
  }
369
- async memberFeed(orgId, actor, communityId, query) {
370
- const spaceIds = await this.openSpaceIdsIn(orgId, actor, communityId);
371
- const page = await this.feedPage(orgId, actor, spaceIds, query);
513
+ async memberFeed(communityId, query, actor) {
514
+ const orgId = actor.orgId;
515
+ const spaceIds = await this.openSpaceIdsIn(actor, communityId);
516
+ const page = await this.feedPage(actor, spaceIds, query);
372
517
  this.logger.debug("member feed read", {
373
518
  orgId,
374
519
  communityId,
@@ -377,12 +522,13 @@ var PostServiceImpl = class {
377
522
  });
378
523
  return page;
379
524
  }
380
- async openSpaceIdsIn(orgId, actor, communityId) {
381
- if (!await this.access.isCommunityMember(orgId, actor, communityId)) throw new NotFoundError("Community", communityId);
382
- return this.access.openSpaceIds(orgId, actor, communityId);
525
+ async openSpaceIdsIn(actor, communityId) {
526
+ if (!await this.access.isCommunityMember(communityId, actor)) throw new NotFoundError("Community", communityId);
527
+ return this.access.openSpaceIds(communityId, actor);
383
528
  }
384
- async listOwn(orgId, actor, communityId, query) {
385
- const spaceIds = await this.openSpaceIdsIn(orgId, actor, communityId);
529
+ async listOwn(communityId, query, actor) {
530
+ const orgId = actor.orgId;
531
+ const spaceIds = await this.openSpaceIdsIn(actor, communityId);
386
532
  const page = await this.repo.listByAuthor(orgId, actor.id, spaceIds, query);
387
533
  this.logger.debug("own posts read", {
388
534
  orgId,
@@ -391,14 +537,15 @@ var PostServiceImpl = class {
391
537
  });
392
538
  return {
393
539
  ...page,
394
- rows: await this.viewsOf(orgId, actor, page.rows)
540
+ rows: await this.viewsOf(actor, page.rows)
395
541
  };
396
542
  }
397
- async listBookmarked(orgId, actor, communityId, query) {
398
- const spaceIds = await this.openSpaceIdsIn(orgId, actor, communityId);
543
+ async listBookmarked(communityId, query, actor) {
544
+ const orgId = actor.orgId;
545
+ const spaceIds = await this.openSpaceIdsIn(actor, communityId);
399
546
  const page = await this.repo.listBookmarked(orgId, actor.id, spaceIds, {
400
547
  ...query,
401
- visibleStatuses: this.visibleStatuses(actor)
548
+ visibleStatuses: this.visibleStatuses(actor, communityId)
402
549
  });
403
550
  this.logger.debug("bookmarked posts read", {
404
551
  orgId,
@@ -407,27 +554,47 @@ var PostServiceImpl = class {
407
554
  });
408
555
  return {
409
556
  ...page,
410
- rows: await this.viewsOf(orgId, actor, page.rows)
557
+ rows: await this.viewsOf(actor, page.rows)
411
558
  };
412
559
  }
413
- async coverFile(orgId, actor, postId) {
414
- const post = await this.repo.find(orgId, postId);
415
- if (!post?.coverAssetId || !this.canSee(actor, post)) return null;
416
- const space = await this.content.getSpace(orgId, post.spaceId);
417
- if (!space || !await this.access.canOpen(orgId, actor, space)) return null;
418
- return this.images.signFile(orgId, post.coverAssetId);
419
- }
420
- async getPost(orgId, actor, postId) {
421
- const { post } = await this.loadVisible(orgId, actor, postId);
560
+ async coverFile(postId, actor) {
561
+ const post = await this.repo.find(actor.orgId, postId);
562
+ return this.signShown(actor, post, post?.coverAssetId ?? null, "Post cover", postId);
563
+ }
564
+ async assetFile(postId, assetId, actor) {
565
+ const post = await this.repo.find(actor.orgId, postId);
566
+ const [upload] = post !== null && this.bodies.parse(post.body).assetIds.includes(assetId) ? await this.images.memberUploads(actor.orgId, [assetId]) : [];
567
+ return this.signShown(actor, post, upload ? assetId : null, "Post file", assetId, upload && !INLINE_TYPES.test(upload.contentType) ? upload.filename : void 0);
568
+ }
569
+ async signShown(actor, post, assetId, label, id, saveAs) {
570
+ const orgId = actor.orgId;
571
+ if (!post || !assetId || !this.canSee(actor, post)) throw new NotFoundError(label, id);
572
+ const space = await this.content.getSpace(post.spaceId, this.authors.systemActor(orgId));
573
+ if (!space || !await this.access.canOpen(space, actor)) throw new NotFoundError(label, id);
574
+ const url = await this.images.signFile(orgId, assetId, saveAs ? { saveAs } : {});
575
+ if (!url) throw new NotFoundError(label, id);
576
+ return url;
577
+ }
578
+ async getPost(postId, actor) {
579
+ const { post } = await this.loadVisible(actor, postId);
422
580
  this.logger.debug("post read", {
423
- orgId,
581
+ orgId: actor.orgId,
424
582
  postId
425
583
  });
426
- return this.viewOf(orgId, actor, post);
584
+ return this.viewOf(actor, post);
427
585
  }
428
- async moderationQueue(orgId, actor, communityId, spaceId) {
429
- await this.requireOpen(orgId, actor, spaceId, communityId);
430
- if (!this.canModerate(actor)) throw new ForbiddenError("You cannot work this queue");
586
+ async moderationQueue(communityId, spaceId, actor) {
587
+ const orgId = actor.orgId;
588
+ const space = await this.requireOpen(actor, spaceId, communityId);
589
+ if (!actor.permissions.has("communities:moderate", space.communityId)) {
590
+ this.logger.warn("space queue refused — not a moderator", {
591
+ orgId,
592
+ communityId,
593
+ spaceId,
594
+ actorId: actor.id
595
+ });
596
+ throw new ForbiddenError("You need communities:moderate");
597
+ }
431
598
  const [pending, flags, openCommentFlags] = await Promise.all([
432
599
  this.repo.listPending(orgId, spaceId, 100),
433
600
  this.repo.listOpenFlags(orgId, spaceId, 100),
@@ -441,122 +608,68 @@ var PostServiceImpl = class {
441
608
  commentFlags: openCommentFlags.length
442
609
  });
443
610
  return {
444
- pending: await this.viewsOf(orgId, actor, pending),
611
+ pending: await this.viewsOf(actor, pending),
445
612
  flags,
446
- commentFlags: openCommentFlags
613
+ commentFlags: await this.flaggedCommentViews(actor, space.communityId, openCommentFlags)
447
614
  };
448
615
  }
449
- visible(orgId, actor, postId) {
450
- return this.loadVisible(orgId, actor, postId);
616
+ async flaggedCommentViews(actor, communityId, flags) {
617
+ if (flags.length === 0) return [];
618
+ const orgId = actor.orgId;
619
+ const authorIds = [...new Set(flags.map((flag) => flag.comment.orgUserId))];
620
+ const [records, permissions] = await Promise.all([this.profiles.authorsOf(orgId, authorIds), this.authors.permissionsOfMany(orgId, authorIds)]);
621
+ return flags.map((flag) => {
622
+ const record = records[flag.comment.orgUserId];
623
+ if (!record) throw new NotFoundError("Member", flag.comment.orgUserId);
624
+ return {
625
+ ...flag,
626
+ comment: {
627
+ ...flag.comment,
628
+ author: authorView(orgId, record, this.fileUrls, teamBadge(permissions.get(flag.comment.orgUserId), communityId))
629
+ }
630
+ };
631
+ });
632
+ }
633
+ visible(postId, actor) {
634
+ return this.loadVisible(actor, postId);
451
635
  }
452
636
  find(orgId, postId) {
453
637
  return this.repo.find(orgId, postId);
454
638
  }
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
- };
639
+ async submittable(post, space, author) {
640
+ if (!this.topicFits(space, post.topicId)) return false;
641
+ return this.access.canOpen(space, author);
530
642
  }
531
- async submittable(orgId, post, space, role) {
532
- if (role === null || !this.topicFits(space, post.topicId)) return false;
533
- return this.access.canOpen(orgId, {
534
- id: post.orgUserId,
535
- role
536
- }, space);
537
- }
538
- async feedPage(orgId, actor, spaceIds, query) {
539
- const page = await this.repo.listPublished(orgId, spaceIds, query);
643
+ async feedPage(actor, spaceIds, query) {
644
+ const page = await this.repo.listPublished(actor.orgId, spaceIds, query);
540
645
  return {
541
646
  ...page,
542
- rows: await this.viewsOf(orgId, actor, page.rows)
647
+ rows: await this.viewsOf(actor, page.rows)
543
648
  };
544
649
  }
545
- async viewOf(orgId, actor, post) {
546
- const [view] = await this.viewsOf(orgId, actor, [post]);
650
+ async viewOf(actor, post) {
651
+ const [view] = await this.viewsOf(actor, [post]);
547
652
  return view;
548
653
  }
549
- async viewsOf(orgId, actor, posts) {
654
+ async viewsOf(actor, posts) {
550
655
  if (posts.length === 0) return [];
656
+ const orgId = actor.orgId;
551
657
  const postIds = posts.map((post) => post.id);
552
- const [records, reactions, commentCounts, followed, bookmarked] = await Promise.all([
553
- this.profiles.authorsOf(orgId, [...new Set(posts.map((post) => post.orgUserId))]),
658
+ const authorIds = [...new Set(posts.map((post) => post.orgUserId))];
659
+ const now = /* @__PURE__ */ new Date();
660
+ const bodyAssets = new Map(posts.map((post) => [post.id, this.bodies.parse(post.body).assetIds]));
661
+ const [records, permissions, reactions, commentCounts, subscribed, bookmarked, mentioned, uploads] = await Promise.all([
662
+ this.profiles.authorsOf(orgId, authorIds),
663
+ this.authors.permissionsOfMany(orgId, authorIds),
554
664
  this.repo.reactionsOf(orgId, postIds, actor.id),
555
665
  this.comments.publishedCommentCounts(orgId, postIds),
556
- this.repo.followedAmong(orgId, postIds, actor.id),
557
- this.repo.bookmarkedAmong(orgId, postIds, actor.id)
666
+ this.subscriptions.forViewer(orgId, actor.id, "post", postIds),
667
+ this.repo.bookmarkedAmong(orgId, postIds, actor.id),
668
+ mentionViews(this.mentions, this.profiles, orgId, "post", postIds),
669
+ this.images.memberUploads(orgId, [...new Set([...bodyAssets.values()].flat())])
558
670
  ]);
559
- const following = new Set(followed);
671
+ const uploadOf = new Map(uploads.map((upload) => [upload.id, upload]));
672
+ const subscriptionOf = new Map(subscribed.map((row) => [row.itemId, row]));
560
673
  const saved = new Set(bookmarked);
561
674
  return posts.map((post) => {
562
675
  const record = records[post.orgUserId];
@@ -564,45 +677,44 @@ var PostServiceImpl = class {
564
677
  const counts = reactions[post.id];
565
678
  return {
566
679
  ...post,
567
- coverUrl: post.coverAssetId ? this.fileUrls.postCover(post.id) : null,
568
- author: authorView(orgId, record, this.fileUrls),
680
+ coverUrl: !post.coverAssetId ? null : post.status === "published" ? this.fileUrls.publicImage(orgId, post.id, post.coverAssetId) : this.fileUrls.postCover(post.id),
681
+ assets: Object.fromEntries((bodyAssets.get(post.id) ?? []).flatMap((assetId) => {
682
+ const upload = uploadOf.get(assetId);
683
+ return upload ? [[assetId, {
684
+ url: this.fileUrls.postAsset(post.id, assetId),
685
+ kind: upload.kind,
686
+ filename: upload.filename,
687
+ size: upload.size
688
+ }]] : [];
689
+ })),
690
+ author: authorView(orgId, record, this.fileUrls, teamBadge(permissions.get(post.orgUserId), post.communityId)),
569
691
  reactions: counts?.reactions ?? {},
570
692
  viewerReaction: counts?.viewerReaction ?? null,
571
693
  commentCount: commentCounts[post.id] ?? 0,
572
- following: following.has(post.id),
573
- bookmarked: saved.has(post.id)
694
+ subscription: activeSubscription(subscriptionOf.get(post.id), now),
695
+ bookmarked: saved.has(post.id),
696
+ mentions: mentioned[post.id] ?? []
574
697
  };
575
698
  });
576
699
  }
577
- async commitSubmission(orgId, post, space, role, actor, now) {
700
+ async commitSubmission(orgId, post, space, skips, actor, now) {
701
+ const mentioned = await this.mentionedOpeners(orgId, post);
578
702
  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]);
703
+ const next = await posts.updateIf(orgId, post.id, { status: post.status }, this.submissionOf(space.settings.requireReview, skips, now));
704
+ if (!next) throw new ConflictError("This post cannot be submitted");
705
+ await outbox.append([this.landed(orgId, next, this.meta(actor, post.status), mentioned)]);
591
706
  return next;
592
707
  });
593
708
  }
594
- async moderate(orgId, postId, actor, patch, unchanged, log) {
595
- const { post } = await this.loadVisible(orgId, actor, postId);
596
- if (!this.canModerate(actor)) throw new ForbiddenError("You cannot moderate this post");
709
+ async moderate(postId, actor, patch, unchanged, log) {
710
+ const orgId = actor.orgId;
711
+ const { post } = await this.loadVisible(actor, postId);
712
+ this.requireModerator(actor, post, log);
597
713
  if (post.status === "removed") throw new ConflictError("This post was removed");
598
- if (unchanged(post)) return this.viewOf(orgId, actor, post);
714
+ if (unchanged(post)) return this.viewOf(actor, post);
599
715
  const updated = await this.uow.run(async ({ posts, outbox }) => {
600
716
  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
- })]);
717
+ await outbox.append([this.updated(orgId, next, this.meta(actor, next.status === "pending" ? post.status : void 0))]);
606
718
  return next;
607
719
  });
608
720
  this.logger.info(log, {
@@ -610,7 +722,7 @@ var PostServiceImpl = class {
610
722
  postId,
611
723
  actorId: actor.id
612
724
  });
613
- return this.viewOf(orgId, actor, updated);
725
+ return this.viewOf(actor, updated);
614
726
  }
615
727
  async mustUpdate(posts, orgId, postId, patch) {
616
728
  const next = await posts.update(orgId, postId, patch);
@@ -634,10 +746,10 @@ var PostServiceImpl = class {
634
746
  removedBy: null
635
747
  };
636
748
  }
637
- return this.submissionOf(space.settings.requireReview, actor.role, now);
749
+ return this.submissionOf(space.settings.requireReview, skipsReview(actor.permissions, { communityId: space.communityId }), now);
638
750
  }
639
- submissionOf(requireReview, role, now) {
640
- if (requireReview && !(role && isStaffRole(role))) return {
751
+ submissionOf(requireReview, skips, now) {
752
+ if (requireReview && !skips) return {
641
753
  status: "pending",
642
754
  scheduledAt: null,
643
755
  publishedAt: null,
@@ -661,20 +773,31 @@ var PostServiceImpl = class {
661
773
  throw new NotFoundError("Asset", assetId);
662
774
  }
663
775
  }
664
- async openSpace(orgId, actor, spaceId, communityId) {
665
- const space = await this.content.getSpace(orgId, spaceId);
666
- if (!space || communityId !== void 0 && space.communityId !== communityId) return null;
667
- return await this.access.canOpen(orgId, actor, space) ? space : null;
776
+ async requireBodyAssets(orgId, orgUserId, assetIds) {
777
+ if (assetIds.length > MAX_BODY_ASSETS) throw new InvalidInputError(`A post shows at most ${MAX_BODY_ASSETS} files`);
778
+ for (const assetId of assetIds) if (!await this.images.ownsUpload(orgId, assetId, orgUserId)) {
779
+ this.logger.debug("body file refused", {
780
+ orgId,
781
+ orgUserId,
782
+ assetId
783
+ });
784
+ throw new NotFoundError("Asset", assetId);
785
+ }
786
+ }
787
+ async openSpace(actor, spaceId, communityId) {
788
+ const space = await this.content.getSpace(spaceId, this.authors.systemActor(actor.orgId));
789
+ if (!space || communityId != null && space.communityId !== communityId) return null;
790
+ return await this.access.canOpen(space, actor) ? space : null;
668
791
  }
669
- async requireOpen(orgId, actor, spaceId, communityId) {
670
- const space = await this.openSpace(orgId, actor, spaceId, communityId);
792
+ async requireOpen(actor, spaceId, communityId) {
793
+ const space = await this.openSpace(actor, spaceId, communityId);
671
794
  if (!space) throw new NotFoundError("Space", spaceId);
672
795
  return space;
673
796
  }
674
- async loadVisible(orgId, actor, postId, communityId) {
675
- const post = await this.repo.find(orgId, postId);
676
- if (!post || communityId !== void 0 && post.communityId !== communityId) throw new NotFoundError("Post", postId);
677
- const space = await this.openSpace(orgId, actor, post.spaceId);
797
+ async loadVisible(actor, postId, communityId) {
798
+ const post = await this.repo.find(actor.orgId, postId);
799
+ if (!post || communityId != null && post.communityId !== communityId) throw new NotFoundError("Post", postId);
800
+ const space = await this.openSpace(actor, post.spaceId);
678
801
  if (!space || !this.canSee(actor, post)) throw new NotFoundError("Post", postId);
679
802
  return {
680
803
  post,
@@ -682,25 +805,34 @@ var PostServiceImpl = class {
682
805
  };
683
806
  }
684
807
  canSee(actor, post) {
685
- return post.orgUserId === actor.id || this.visibleStatuses(actor).includes(post.status);
686
- }
687
- visibleStatuses(actor) {
688
- return this.canModerate(actor) ? MODERATOR_VISIBLE : PUBLISHED_ONLY;
689
- }
690
- canModerate(actor) {
691
- return canModerate(actor.role);
808
+ return post.orgUserId === actor.id || this.visibleStatuses(actor, post.communityId).includes(post.status);
809
+ }
810
+ visibleStatuses(actor, communityId) {
811
+ return this.moderates(actor.permissions, communityId) ? MODERATOR_VISIBLE : PUBLISHED_ONLY;
812
+ }
813
+ moderates(permissions, communityId) {
814
+ return permissions.has("communities:moderate", communityId);
815
+ }
816
+ requireModerator(actor, post, action) {
817
+ if (!this.moderates(actor.permissions, post.communityId)) {
818
+ this.logger.warn("post moderation refused — not a moderator", {
819
+ orgId: actor.orgId,
820
+ postId: post.id,
821
+ communityId: post.communityId,
822
+ action,
823
+ actorId: actor.id
824
+ });
825
+ throw new ForbiddenError("You need communities:moderate");
826
+ }
692
827
  }
693
828
  assertAuthor(actor, post) {
694
829
  if (post.orgUserId !== actor.id) throw new ForbiddenError("You can change only your own post");
695
830
  }
696
831
  topicFits(space, topicId) {
697
- if (topicId === null) return space.topics.length === 0;
698
- return space.topics.some((topic) => topic.id === topicId);
832
+ return topicId === null || space.topics.some((topic) => topic.id === topicId);
699
833
  }
700
834
  assertTopic(space, topicId) {
701
- if (this.topicFits(space, topicId)) return;
702
- if (topicId === null) throw new InvalidInputError("This space requires a topic");
703
- throw new NotFoundError("Topic", topicId);
835
+ if (topicId !== null && !this.topicFits(space, topicId)) throw new NotFoundError("Topic", topicId);
704
836
  }
705
837
  patchChanges(post, patch) {
706
838
  return Object.keys(patch).some((key) => {
@@ -710,9 +842,43 @@ var PostServiceImpl = class {
710
842
  return post[key] !== value;
711
843
  });
712
844
  }
845
+ async mentionable(orgId, spaceId, title, body) {
846
+ return this.openersOf(orgId, spaceId, [.../* @__PURE__ */ new Set([...mentionTokens(title), ...body])]);
847
+ }
848
+ async openersOf(orgId, spaceId, orgUserIds) {
849
+ if (orgUserIds.length === 0) return [];
850
+ return keepAllowed(orgUserIds, await this.access.spaceOpenersAmong(orgId, spaceId, orgUserIds));
851
+ }
852
+ async mentionedOpeners(orgId, post) {
853
+ return this.openersOf(orgId, post.spaceId, await this.mentions.ofItem(orgId, "post", post.id));
854
+ }
855
+ landed(orgId, post, metadata, mentioned) {
856
+ if (post.status !== "published") return this.updated(orgId, post, metadata);
857
+ return this.published(orgId, post, metadata, mentioned);
858
+ }
859
+ published(orgId, post, metadata, mentionedOrgUserIds) {
860
+ return discussionEvents.postPublished.make({
861
+ orgId,
862
+ data: {
863
+ ...post,
864
+ mentionedOrgUserIds
865
+ },
866
+ metadata
867
+ });
868
+ }
869
+ updated(orgId, post, metadata, mentionedOrgUserIds = []) {
870
+ return discussionEvents.postUpdated.make({
871
+ orgId,
872
+ data: {
873
+ ...post,
874
+ mentionedOrgUserIds
875
+ },
876
+ metadata
877
+ });
878
+ }
713
879
  meta(actor, previousStatus) {
714
880
  return {
715
- ...actor ? { actorOrgUserId: actor.id } : {},
881
+ ...actor ? { actorId: actor.id } : {},
716
882
  ...previousStatus !== void 0 ? { previousStatus } : {}
717
883
  };
718
884
  }