@headless-lms/core 0.4.0 → 0.6.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 (249) hide show
  1. package/dist/assets/events.d.ts +15 -9
  2. package/dist/assets/file-urls.d.ts +14 -7
  3. package/dist/assets/file-urls.js +15 -4
  4. package/dist/assets/file-urls.js.map +1 -1
  5. package/dist/assets/index.d.ts +4 -4
  6. package/dist/assets/index.js +3 -3
  7. package/dist/assets/ports.d.ts +34 -15
  8. package/dist/assets/service.d.ts +30 -14
  9. package/dist/assets/service.js +233 -54
  10. package/dist/assets/service.js.map +1 -1
  11. package/dist/automations/actions.js.map +1 -1
  12. package/dist/automations/catalog.js +68 -3
  13. package/dist/automations/catalog.js.map +1 -1
  14. package/dist/automations/community-derivations.js +16 -16
  15. package/dist/automations/community-derivations.js.map +1 -1
  16. package/dist/automations/community-mail.d.ts +34 -4
  17. package/dist/automations/community-mail.js +32 -4
  18. package/dist/automations/community-mail.js.map +1 -1
  19. package/dist/automations/event-permissions.d.ts +12 -0
  20. package/dist/automations/event-permissions.js +38 -0
  21. package/dist/automations/event-permissions.js.map +1 -0
  22. package/dist/automations/index.d.ts +2 -1
  23. package/dist/automations/index.js +2 -1
  24. package/dist/automations/notifications/applicable.d.ts +8 -0
  25. package/dist/automations/notifications/applicable.js +12 -0
  26. package/dist/automations/notifications/applicable.js.map +1 -0
  27. package/dist/automations/notifications/debounce.js +1 -1
  28. package/dist/automations/notifications/debounce.js.map +1 -1
  29. package/dist/automations/notifications/defaults.js +33 -3
  30. package/dist/automations/notifications/defaults.js.map +1 -1
  31. package/dist/automations/notifications/processor.js +1 -1
  32. package/dist/automations/notifications/processor.js.map +1 -1
  33. package/dist/automations/notifications/rules/content.js +60 -3
  34. package/dist/automations/notifications/rules/content.js.map +1 -1
  35. package/dist/automations/notifications/rules/discussion.js +29 -29
  36. package/dist/automations/notifications/rules/discussion.js.map +1 -1
  37. package/dist/automations/notifications/rules/entitlements.js +36 -9
  38. package/dist/automations/notifications/rules/entitlements.js.map +1 -1
  39. package/dist/automations/notifications/rules/events.d.ts +9 -0
  40. package/dist/automations/notifications/rules/events.js +27 -0
  41. package/dist/automations/notifications/rules/events.js.map +1 -0
  42. package/dist/automations/notifications/rules/organizations.js +17 -10
  43. package/dist/automations/notifications/rules/organizations.js.map +1 -1
  44. package/dist/automations/notifications/rules/recipients.js +0 -1
  45. package/dist/automations/notifications/rules/recipients.js.map +1 -1
  46. package/dist/automations/notifications/service.d.ts +8 -6
  47. package/dist/automations/notifications/service.js +12 -12
  48. package/dist/automations/notifications/service.js.map +1 -1
  49. package/dist/automations/notifications/types.js +94 -7
  50. package/dist/automations/notifications/types.js.map +1 -1
  51. package/dist/automations/ports.d.ts +30 -31
  52. package/dist/automations/service.d.ts +20 -17
  53. package/dist/automations/service.js +90 -61
  54. package/dist/automations/service.js.map +1 -1
  55. package/dist/content/event-sight.d.ts +11 -0
  56. package/dist/content/event-sight.js +11 -0
  57. package/dist/content/event-sight.js.map +1 -0
  58. package/dist/content/events.d.ts +465 -88
  59. package/dist/content/events.js +34 -1
  60. package/dist/content/events.js.map +1 -1
  61. package/dist/content/index.d.ts +6 -4
  62. package/dist/content/index.js +3 -1
  63. package/dist/content/jobs.d.ts +16 -0
  64. package/dist/content/jobs.js +37 -0
  65. package/dist/content/jobs.js.map +1 -0
  66. package/dist/content/model.d.ts +2 -2
  67. package/dist/content/notification-categories.d.ts +5 -4
  68. package/dist/content/notification-categories.js +15 -6
  69. package/dist/content/notification-categories.js.map +1 -1
  70. package/dist/content/ports.d.ts +113 -68
  71. package/dist/content/service.d.ts +103 -67
  72. package/dist/content/service.js +739 -152
  73. package/dist/content/service.js.map +1 -1
  74. package/dist/content/titles.d.ts +6 -3
  75. package/dist/content/titles.js +37 -24
  76. package/dist/content/titles.js.map +1 -1
  77. package/dist/content/types.d.ts +2 -2
  78. package/dist/discussion/authors.d.ts +4 -2
  79. package/dist/discussion/authors.js +9 -5
  80. package/dist/discussion/authors.js.map +1 -1
  81. package/dist/discussion/chats.d.ts +56 -37
  82. package/dist/discussion/chats.js +392 -160
  83. package/dist/discussion/chats.js.map +1 -1
  84. package/dist/discussion/events.d.ts +85 -47
  85. package/dist/discussion/events.js +41 -3
  86. package/dist/discussion/events.js.map +1 -1
  87. package/dist/discussion/index.d.ts +8 -5
  88. package/dist/discussion/index.js +5 -3
  89. package/dist/discussion/live.d.ts +106 -0
  90. package/dist/discussion/live.js +642 -0
  91. package/dist/discussion/live.js.map +1 -0
  92. package/dist/discussion/mentions.js +2 -2
  93. package/dist/discussion/mentions.js.map +1 -1
  94. package/dist/discussion/ports.d.ts +182 -100
  95. package/dist/discussion/ports.js.map +1 -1
  96. package/dist/discussion/posts.d.ts +45 -39
  97. package/dist/discussion/posts.js +266 -152
  98. package/dist/discussion/posts.js.map +1 -1
  99. package/dist/discussion/rules.d.ts +18 -0
  100. package/dist/discussion/rules.js +22 -0
  101. package/dist/discussion/rules.js.map +1 -0
  102. package/dist/discussion/service.d.ts +37 -31
  103. package/dist/discussion/service.js +250 -186
  104. package/dist/discussion/service.js.map +1 -1
  105. package/dist/discussion/subscriptions.d.ts +8 -8
  106. package/dist/discussion/subscriptions.js +28 -12
  107. package/dist/discussion/subscriptions.js.map +1 -1
  108. package/dist/discussion/titles.d.ts +4 -2
  109. package/dist/discussion/titles.js +27 -13
  110. package/dist/discussion/titles.js.map +1 -1
  111. package/dist/entitlements/events.d.ts +58 -58
  112. package/dist/entitlements/index.d.ts +4 -4
  113. package/dist/entitlements/index.js +2 -2
  114. package/dist/entitlements/model.d.ts +2 -2
  115. package/dist/entitlements/ports.d.ts +65 -39
  116. package/dist/entitlements/rules.d.ts +3 -6
  117. package/dist/entitlements/rules.js +5 -10
  118. package/dist/entitlements/rules.js.map +1 -1
  119. package/dist/entitlements/service.d.ts +40 -28
  120. package/dist/entitlements/service.js +369 -162
  121. package/dist/entitlements/service.js.map +1 -1
  122. package/dist/identity/devices.d.ts +3 -3
  123. package/dist/identity/devices.js +6 -4
  124. package/dist/identity/devices.js.map +1 -1
  125. package/dist/identity/index.d.ts +2 -2
  126. package/dist/identity/index.js +2 -2
  127. package/dist/identity/notification-settings.d.ts +1 -1
  128. package/dist/identity/notification-settings.js +2 -1
  129. package/dist/identity/notification-settings.js.map +1 -1
  130. package/dist/identity/ports.d.ts +7 -7
  131. package/dist/identity/service.d.ts +8 -7
  132. package/dist/identity/service.js +16 -12
  133. package/dist/identity/service.js.map +1 -1
  134. package/dist/integrations/events.d.ts +3 -3
  135. package/dist/integrations/ports.d.ts +9 -8
  136. package/dist/integrations/service.d.ts +9 -8
  137. package/dist/integrations/service.js +31 -14
  138. package/dist/integrations/service.js.map +1 -1
  139. package/dist/organizations/default-roles.d.ts +22 -0
  140. package/dist/organizations/default-roles.js +89 -0
  141. package/dist/organizations/default-roles.js.map +1 -0
  142. package/dist/organizations/effective-permissions.d.ts +47 -0
  143. package/dist/organizations/effective-permissions.js +118 -0
  144. package/dist/organizations/effective-permissions.js.map +1 -0
  145. package/dist/organizations/events.d.ts +326 -181
  146. package/dist/organizations/events.js +90 -18
  147. package/dist/organizations/events.js.map +1 -1
  148. package/dist/organizations/index.d.ts +14 -10
  149. package/dist/organizations/index.js +7 -3
  150. package/dist/organizations/members.d.ts +66 -8
  151. package/dist/organizations/members.js.map +1 -1
  152. package/dist/organizations/model.d.ts +2 -2
  153. package/dist/organizations/notification-catalog.d.ts +31 -0
  154. package/dist/organizations/notification-catalog.js +32 -0
  155. package/dist/organizations/notification-catalog.js.map +1 -0
  156. package/dist/organizations/notification-categories.d.ts +6 -4
  157. package/dist/organizations/notification-categories.js +25 -10
  158. package/dist/organizations/notification-categories.js.map +1 -1
  159. package/dist/organizations/org-roles.d.ts +33 -0
  160. package/dist/organizations/org-roles.js +49 -0
  161. package/dist/organizations/org-roles.js.map +1 -0
  162. package/dist/organizations/permissions.d.ts +311 -0
  163. package/dist/organizations/permissions.js +327 -0
  164. package/dist/organizations/permissions.js.map +1 -0
  165. package/dist/organizations/ports.d.ts +187 -65
  166. package/dist/organizations/service.d.ts +102 -35
  167. package/dist/organizations/service.js +1254 -327
  168. package/dist/organizations/service.js.map +1 -1
  169. package/dist/organizations/testing.d.ts +18 -0
  170. package/dist/organizations/testing.js +37 -0
  171. package/dist/organizations/testing.js.map +1 -0
  172. package/dist/organizations/types.d.ts +5 -16
  173. package/dist/progress/events.d.ts +14 -14
  174. package/dist/progress/index.d.ts +2 -2
  175. package/dist/progress/ports.d.ts +11 -3
  176. package/dist/progress/service.d.ts +6 -5
  177. package/dist/progress/service.js +21 -6
  178. package/dist/progress/service.js.map +1 -1
  179. package/dist/reporting/admin/index.d.ts +6 -4
  180. package/dist/reporting/admin/model.d.ts +36 -3
  181. package/dist/reporting/admin/ports.d.ts +39 -14
  182. package/dist/reporting/admin/service.d.ts +32 -11
  183. package/dist/reporting/admin/service.js +102 -18
  184. package/dist/reporting/admin/service.js.map +1 -1
  185. package/dist/reporting/events/index.d.ts +27 -0
  186. package/dist/reporting/events/index.js +20 -0
  187. package/dist/reporting/events/index.js.map +1 -0
  188. package/dist/reporting/member/calendar.d.ts +11 -0
  189. package/dist/reporting/member/calendar.js +75 -0
  190. package/dist/reporting/member/calendar.js.map +1 -0
  191. package/dist/reporting/member/index.d.ts +3 -2
  192. package/dist/reporting/member/model.d.ts +29 -5
  193. package/dist/reporting/member/ports.d.ts +49 -29
  194. package/dist/reporting/member/service.d.ts +57 -29
  195. package/dist/reporting/member/service.js +345 -114
  196. package/dist/reporting/member/service.js.map +1 -1
  197. package/dist/shared/actor.d.ts +23 -3
  198. package/dist/shared/errors.d.ts +2 -1
  199. package/dist/shared/errors.js +3 -1
  200. package/dist/shared/errors.js.map +1 -1
  201. package/dist/shared/id.d.ts +4 -0
  202. package/dist/shared/id.js +4 -0
  203. package/dist/shared/id.js.map +1 -1
  204. package/dist/shared/logger.d.ts +2 -1
  205. package/dist/shared/logger.js +10 -1
  206. package/dist/shared/logger.js.map +1 -1
  207. package/dist/shared/ports.d.ts +25 -5
  208. package/dist/types/content.d.ts +2 -2
  209. package/dist/types/discussion.d.ts +4 -5
  210. package/dist/types/email-templates.d.ts +22 -3
  211. package/dist/types/entitlements.d.ts +2 -2
  212. package/dist/types/identity.d.ts +13 -16
  213. package/dist/types/index.d.ts +9 -9
  214. package/dist/types/index.js +2 -1
  215. package/dist/types/notifications.d.ts +4 -4
  216. package/dist/types/organizations.d.ts +3 -2
  217. package/dist/types/ports.d.ts +8 -1
  218. package/dist/types/ports.js +7 -1
  219. package/dist/types/ports.js.map +1 -0
  220. package/dist/types/schemas/assets.d.ts +5 -5
  221. package/dist/types/schemas/assets.js +4 -3
  222. package/dist/types/schemas/assets.js.map +1 -1
  223. package/dist/types/schemas/content.d.ts +599 -13
  224. package/dist/types/schemas/content.js +219 -8
  225. package/dist/types/schemas/content.js.map +1 -1
  226. package/dist/types/schemas/discussion.d.ts +280 -57
  227. package/dist/types/schemas/discussion.js +77 -3
  228. package/dist/types/schemas/discussion.js.map +1 -1
  229. package/dist/types/schemas/entitlements.d.ts +21 -8
  230. package/dist/types/schemas/entitlements.js +11 -4
  231. package/dist/types/schemas/entitlements.js.map +1 -1
  232. package/dist/types/schemas/index.d.ts +5 -5
  233. package/dist/types/schemas/index.js +5 -5
  234. package/dist/types/schemas/notifications.d.ts +11 -10
  235. package/dist/types/schemas/notifications.js +3 -2
  236. package/dist/types/schemas/notifications.js.map +1 -1
  237. package/dist/types/schemas/organizations.d.ts +99 -60
  238. package/dist/types/schemas/organizations.js +80 -27
  239. package/dist/types/schemas/organizations.js.map +1 -1
  240. package/dist/types/schemas/progress.d.ts +3 -4
  241. package/dist/types/schemas/progress.js +0 -1
  242. package/dist/types/schemas/progress.js.map +1 -1
  243. package/package.json +1 -1
  244. package/dist/organizations/access-scopes.d.ts +0 -19
  245. package/dist/organizations/access-scopes.js +0 -89
  246. package/dist/organizations/access-scopes.js.map +0 -1
  247. package/dist/organizations/roles.d.ts +0 -19
  248. package/dist/organizations/roles.js +0 -82
  249. package/dist/organizations/roles.js.map +0 -1
@@ -1,14 +1,16 @@
1
1
  import { genId } from "../shared/id.js";
2
2
  import { ConflictError, ForbiddenError, InvalidInputError, NotFoundError, UnavailableError } from "../shared/errors.js";
3
3
  import { noopLogger } from "../shared/logger.js";
4
- import { canModerate, isStaffRole } from "../organizations/roles.js";
5
4
  import { discussionEvents } from "./events.js";
6
5
  import { authorView } from "./authors.js";
7
6
  import { keepAllowed, mentionTokens, mentionViews } from "./mentions.js";
7
+ import { skipsReview, teamBadge } from "./rules.js";
8
8
  import { POST_PUBLISH_JOB } from "./jobs.js";
9
9
  import { activeSubscription } from "./subscriptions.js";
10
10
  import "./ports.js";
11
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;
12
14
  const PUBLISHED_ONLY = ["published"];
13
15
  const MODERATOR_VISIBLE = [
14
16
  "published",
@@ -32,6 +34,7 @@ var PostServiceImpl = class {
32
34
  mentions;
33
35
  images;
34
36
  fileUrls;
37
+ bodies;
35
38
  uow;
36
39
  scheduler;
37
40
  logger;
@@ -46,22 +49,27 @@ var PostServiceImpl = class {
46
49
  this.mentions = params.mentions;
47
50
  this.images = params.images;
48
51
  this.fileUrls = params.fileUrls;
52
+ this.bodies = params.bodies;
49
53
  this.uow = params.uow;
50
54
  this.scheduler = params.scheduler;
51
55
  this.logger = params.logger ?? noopLogger;
52
56
  }
53
- async visibleTitles(orgId, actor, postIds) {
57
+ async visibleTitles(postIds, actor) {
58
+ const orgId = actor.orgId;
54
59
  const found = await this.repo.findMany(orgId, [...new Set(postIds)]);
55
60
  const communityIds = [...new Set(found.map((post) => post.communityId))];
56
- 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());
57
62
  return Object.fromEntries(found.filter((post) => open.has(post.spaceId) && this.canSee(actor, post)).map((post) => [post.id, post.title]));
58
63
  }
59
64
  publishedBetween(orgId, communityId, from, to) {
60
65
  return this.repo.publishedBetween(orgId, communityId, from, to);
61
66
  }
62
- async create(orgId, actor, input) {
63
- const space = await this.requireOpen(orgId, actor, input.spaceId, input.communityId);
67
+ async create(input, actor) {
68
+ const orgId = actor.orgId;
69
+ const space = await this.requireOpen(actor, input.spaceId, input.communityId);
64
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);
65
73
  this.assertTopic(space, input.topicId);
66
74
  const now = /* @__PURE__ */ new Date();
67
75
  const landing = this.initial(input, space, actor, now);
@@ -83,7 +91,7 @@ var PostServiceImpl = class {
83
91
  updatedAt: now,
84
92
  ...landing
85
93
  };
86
- const mentioned = await this.mentionable(orgId, space.id, post.title, post.body);
94
+ const mentioned = await this.mentionable(orgId, space.id, post.title, parsed.mentions);
87
95
  const saved = await this.uow.run(async ({ posts, mentions, outbox }) => {
88
96
  const created = await posts.insert(orgId, post);
89
97
  await mentions.sync(orgId, "post", created.id, mentioned, now);
@@ -103,17 +111,21 @@ var PostServiceImpl = class {
103
111
  actorId: actor.id
104
112
  });
105
113
  if (saved.status === "scheduled") await this.arm(orgId, actor, saved, null);
106
- return this.viewOf(orgId, actor, saved);
114
+ await this.syncCover(saved);
115
+ return this.viewOf(actor, saved);
107
116
  }
108
- async update(orgId, postId, actor, patch) {
109
- 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);
110
120
  this.assertAuthor(actor, post);
111
121
  if (post.status === "removed") throw new ConflictError("This post was removed");
112
122
  if (patch.topicId !== void 0) this.assertTopic(space, patch.topicId);
113
123
  if (typeof patch.coverAssetId === "string") await this.requireOwnedImage(orgId, actor.id, patch.coverAssetId);
114
- if (!this.patchChanges(post, patch)) return this.viewOf(orgId, actor, post);
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);
115
127
  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) : [];
128
+ const candidates = retext ? await this.mentionable(orgId, post.spaceId, patch.title ?? post.title, parsed.mentions) : [];
117
129
  const updated = await this.uow.run(async ({ posts, mentions, outbox }) => {
118
130
  const next = await this.mustUpdate(posts, orgId, postId, patch);
119
131
  const added = retext ? await mentions.sync(orgId, "post", postId, candidates, /* @__PURE__ */ new Date()) : [];
@@ -126,14 +138,16 @@ var PostServiceImpl = class {
126
138
  fields: Object.keys(patch),
127
139
  actorId: actor.id
128
140
  });
129
- return this.viewOf(orgId, actor, updated);
141
+ await this.syncCover(updated);
142
+ return this.viewOf(actor, updated);
130
143
  }
131
- async submit(orgId, postId, actor) {
132
- 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);
133
147
  this.assertAuthor(actor, post);
134
148
  if (post.status !== "draft" && post.status !== "scheduled") throw new ConflictError("This post cannot be submitted");
135
149
  this.assertTopic(space, post.topicId);
136
- 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());
137
151
  this.logger.info("post submitted", {
138
152
  orgId,
139
153
  postId,
@@ -141,10 +155,12 @@ var PostServiceImpl = class {
141
155
  actorId: actor.id
142
156
  });
143
157
  if (post.status === "scheduled") await this.disarm(orgId, postId);
144
- return this.viewOf(orgId, actor, saved);
158
+ await this.syncCover(saved);
159
+ return this.viewOf(actor, saved);
145
160
  }
146
- async schedule(orgId, postId, actor, scheduledAt) {
147
- 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);
148
164
  this.assertAuthor(actor, post);
149
165
  if (post.status === "published" || post.status === "removed" || post.status === "pending") throw new ConflictError("This post cannot be scheduled");
150
166
  if (scheduledAt <= /* @__PURE__ */ new Date()) throw new InvalidInputError("A scheduled time must be in the future");
@@ -166,10 +182,12 @@ var PostServiceImpl = class {
166
182
  actorId: actor.id
167
183
  });
168
184
  await this.arm(orgId, actor, updated, post.status === "scheduled" ? post.scheduledAt : null);
169
- return this.viewOf(orgId, actor, updated);
185
+ await this.syncCover(updated);
186
+ return this.viewOf(actor, updated);
170
187
  }
171
- async unschedule(orgId, postId, actor) {
172
- const { post } = await this.loadVisible(orgId, actor, postId);
188
+ async unschedule(postId, actor) {
189
+ const orgId = actor.orgId;
190
+ const { post } = await this.loadVisible(actor, postId);
173
191
  this.assertAuthor(actor, post);
174
192
  if (post.status !== "scheduled") throw new ConflictError("This post is not scheduled");
175
193
  const updated = await this.uow.run(async ({ posts, outbox }) => {
@@ -184,7 +202,8 @@ var PostServiceImpl = class {
184
202
  actorId: actor.id
185
203
  });
186
204
  await this.disarm(orgId, postId);
187
- return this.viewOf(orgId, actor, updated);
205
+ await this.syncCover(updated);
206
+ return this.viewOf(actor, updated);
188
207
  }
189
208
  async publishScheduled(orgId, postId, now) {
190
209
  const post = await this.repo.find(orgId, postId);
@@ -207,7 +226,7 @@ var PostServiceImpl = class {
207
226
  });
208
227
  return;
209
228
  }
210
- const space = await this.content.getSpace(orgId, post.spaceId);
229
+ const space = await this.content.getSpace(post.spaceId, this.authors.systemActor(orgId));
211
230
  if (!space) {
212
231
  this.logger.error("scheduled post has no space", {
213
232
  orgId,
@@ -216,16 +235,17 @@ var PostServiceImpl = class {
216
235
  });
217
236
  return;
218
237
  }
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 }) => {
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 }) => {
223
243
  const next = await posts.updateIf(orgId, postId, {
224
244
  status: "scheduled",
225
245
  dueBy: now
226
246
  }, patch);
227
247
  if (!next) return null;
228
- await outbox.append([await this.landed(mentions, orgId, next, this.meta(null, post.status))]);
248
+ await outbox.append([this.landed(orgId, next, this.meta(null, post.status), mentioned)]);
229
249
  return next;
230
250
  });
231
251
  if (!saved) {
@@ -241,6 +261,7 @@ var PostServiceImpl = class {
241
261
  status: saved.status,
242
262
  refused
243
263
  });
264
+ await this.syncCover(saved);
244
265
  }
245
266
  async arm(orgId, actor, post, previousScheduledAt) {
246
267
  if (post.scheduledAt === null) return;
@@ -267,6 +288,9 @@ var PostServiceImpl = class {
267
288
  });
268
289
  throw new UnavailableError("The post could not be scheduled; try again", { postId: post.id });
269
290
  }
291
+ syncCover(post) {
292
+ return this.images.syncPublicImages(post.orgId, post.id, post.status === "published" && post.coverAssetId ? [post.coverAssetId] : []);
293
+ }
270
294
  async disarm(orgId, postId) {
271
295
  try {
272
296
  await this.scheduler.cancel(POST_PUBLISH_JOB, postId);
@@ -278,19 +302,21 @@ var PostServiceImpl = class {
278
302
  });
279
303
  }
280
304
  }
281
- async approve(orgId, postId, actor, communityId) {
282
- const { post } = await this.loadVisible(orgId, actor, postId, communityId);
283
- if (!this.canModerate(actor)) throw new ForbiddenError("You cannot review this post");
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");
284
309
  if (post.status !== "pending") throw new ConflictError("This post is not awaiting review");
285
310
  const now = /* @__PURE__ */ new Date();
286
- const updated = await this.uow.run(async ({ posts, mentions, outbox }) => {
311
+ const mentioned = await this.mentionedOpeners(orgId, post);
312
+ const updated = await this.uow.run(async ({ posts, outbox }) => {
287
313
  const next = await this.mustUpdate(posts, orgId, postId, {
288
314
  status: "published",
289
315
  publishedAt: now,
290
316
  scheduledAt: null,
291
317
  removedBy: null
292
318
  });
293
- await outbox.append([await this.landed(mentions, orgId, next, this.meta(actor, post.status))]);
319
+ await outbox.append([this.landed(orgId, next, this.meta(actor, post.status), mentioned)]);
294
320
  return next;
295
321
  });
296
322
  this.logger.info("post approved", {
@@ -298,11 +324,13 @@ var PostServiceImpl = class {
298
324
  postId,
299
325
  actorId: actor.id
300
326
  });
301
- return this.viewOf(orgId, actor, updated);
327
+ await this.syncCover(updated);
328
+ return this.viewOf(actor, updated);
302
329
  }
303
- async remove(orgId, postId, actor, communityId) {
304
- const { post } = await this.loadVisible(orgId, actor, postId, communityId);
305
- 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");
306
334
  if (post.status === "removed") throw new ConflictError("This post was removed");
307
335
  if (post.status !== "published" && post.status !== "pending") throw new ConflictError("This post cannot be removed");
308
336
  const updated = await this.uow.run(async ({ posts, outbox }) => {
@@ -327,16 +355,19 @@ var PostServiceImpl = class {
327
355
  postId,
328
356
  actorId: actor.id
329
357
  });
330
- return this.viewOf(orgId, actor, updated);
358
+ await this.syncCover(updated);
359
+ return this.viewOf(actor, updated);
331
360
  }
332
- async restore(orgId, postId, actor, communityId) {
333
- const { post, space } = await this.loadVisible(orgId, actor, postId, communityId);
334
- 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");
335
365
  if (post.status !== "removed") throw new ConflictError("This post was not removed");
336
366
  const now = /* @__PURE__ */ new Date();
337
- 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 });
338
369
  const updated = await this.uow.run(async ({ posts, outbox }) => {
339
- 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));
340
371
  const metadata = this.meta(actor, post.status);
341
372
  await outbox.append([next.status === "published" ? this.published(orgId, next, metadata, []) : this.updated(orgId, next, metadata)]);
342
373
  return next;
@@ -347,25 +378,27 @@ var PostServiceImpl = class {
347
378
  status: updated.status,
348
379
  actorId: actor.id
349
380
  });
350
- return this.viewOf(orgId, actor, updated);
381
+ await this.syncCover(updated);
382
+ return this.viewOf(actor, updated);
351
383
  }
352
- pin(orgId, postId, actor) {
353
- 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");
354
386
  }
355
- unpin(orgId, postId, actor) {
356
- 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");
357
389
  }
358
- closeComments(orgId, postId, actor) {
359
- 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");
360
392
  }
361
- openComments(orgId, postId, actor) {
362
- 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");
363
395
  }
364
- setReactions(orgId, postId, actor, enabled) {
365
- 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");
366
398
  }
367
- async setReaction(orgId, postId, actor, emoji) {
368
- 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);
369
402
  if (!(post.reactionsEnabled ?? space.settings.reactions)) throw new ForbiddenError("Reactions are off on this post");
370
403
  await this.uow.run(async ({ posts, outbox }) => {
371
404
  if (!await posts.setReaction(orgId, postId, actor.id, emoji) || emoji === null) return;
@@ -388,8 +421,9 @@ var PostServiceImpl = class {
388
421
  });
389
422
  return (await this.repo.reactionsOf(orgId, [postId], actor.id))[postId] ?? { reactions: {} };
390
423
  }
391
- async bookmark(orgId, postId, actor) {
392
- await this.loadVisible(orgId, actor, postId);
424
+ async bookmark(postId, actor) {
425
+ const orgId = actor.orgId;
426
+ await this.loadVisible(actor, postId);
393
427
  await this.repo.bookmark(orgId, postId, actor.id);
394
428
  this.logger.debug("post bookmarked", {
395
429
  orgId,
@@ -397,8 +431,9 @@ var PostServiceImpl = class {
397
431
  orgUserId: actor.id
398
432
  });
399
433
  }
400
- async unbookmark(orgId, postId, actor) {
401
- await this.loadVisible(orgId, actor, postId);
434
+ async unbookmark(postId, actor) {
435
+ const orgId = actor.orgId;
436
+ await this.loadVisible(actor, postId);
402
437
  await this.repo.unbookmark(orgId, postId, actor.id);
403
438
  this.logger.debug("post unbookmarked", {
404
439
  orgId,
@@ -406,8 +441,9 @@ var PostServiceImpl = class {
406
441
  orgUserId: actor.id
407
442
  });
408
443
  }
409
- async flag(orgId, postId, actor, reason) {
410
- 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);
411
447
  const now = /* @__PURE__ */ new Date();
412
448
  const flag = {
413
449
  orgId,
@@ -446,9 +482,10 @@ var PostServiceImpl = class {
446
482
  });
447
483
  return saved;
448
484
  }
449
- async resolveFlags(orgId, postId, actor, resolution, communityId) {
450
- await this.loadVisible(orgId, actor, postId, communityId);
451
- 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");
452
489
  await this.repo.resolveFlags(orgId, postId, {
453
490
  resolvedAt: /* @__PURE__ */ new Date(),
454
491
  resolvedBy: actor.id,
@@ -461,10 +498,11 @@ var PostServiceImpl = class {
461
498
  actorId: actor.id
462
499
  });
463
500
  }
464
- async spaceFeed(orgId, actor, communityId, spaceId, query) {
465
- 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);
466
504
  if (query.topicId && !space.topics.some((topic) => topic.id === query.topicId)) throw new NotFoundError("Topic", query.topicId);
467
- const page = await this.feedPage(orgId, actor, [spaceId], query);
505
+ const page = await this.feedPage(actor, [spaceId], query);
468
506
  this.logger.debug("space feed read", {
469
507
  orgId,
470
508
  spaceId,
@@ -472,9 +510,10 @@ var PostServiceImpl = class {
472
510
  });
473
511
  return page;
474
512
  }
475
- async memberFeed(orgId, actor, communityId, query) {
476
- const spaceIds = await this.openSpaceIdsIn(orgId, actor, communityId);
477
- 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);
478
517
  this.logger.debug("member feed read", {
479
518
  orgId,
480
519
  communityId,
@@ -483,12 +522,13 @@ var PostServiceImpl = class {
483
522
  });
484
523
  return page;
485
524
  }
486
- async openSpaceIdsIn(orgId, actor, communityId) {
487
- if (!await this.access.isCommunityMember(orgId, actor, communityId)) throw new NotFoundError("Community", communityId);
488
- 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);
489
528
  }
490
- async listOwn(orgId, actor, communityId, query) {
491
- 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);
492
532
  const page = await this.repo.listByAuthor(orgId, actor.id, spaceIds, query);
493
533
  this.logger.debug("own posts read", {
494
534
  orgId,
@@ -497,14 +537,15 @@ var PostServiceImpl = class {
497
537
  });
498
538
  return {
499
539
  ...page,
500
- rows: await this.viewsOf(orgId, actor, page.rows)
540
+ rows: await this.viewsOf(actor, page.rows)
501
541
  };
502
542
  }
503
- async listBookmarked(orgId, actor, communityId, query) {
504
- 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);
505
546
  const page = await this.repo.listBookmarked(orgId, actor.id, spaceIds, {
506
547
  ...query,
507
- visibleStatuses: this.visibleStatuses(actor)
548
+ visibleStatuses: this.visibleStatuses(actor, communityId)
508
549
  });
509
550
  this.logger.debug("bookmarked posts read", {
510
551
  orgId,
@@ -513,27 +554,47 @@ var PostServiceImpl = class {
513
554
  });
514
555
  return {
515
556
  ...page,
516
- rows: await this.viewsOf(orgId, actor, page.rows)
557
+ rows: await this.viewsOf(actor, page.rows)
517
558
  };
518
559
  }
519
- async coverFile(orgId, actor, postId) {
520
- const post = await this.repo.find(orgId, postId);
521
- if (!post?.coverAssetId || !this.canSee(actor, post)) return null;
522
- const space = await this.content.getSpace(orgId, post.spaceId);
523
- if (!space || !await this.access.canOpen(orgId, actor, space)) return null;
524
- return this.images.signFile(orgId, post.coverAssetId);
525
- }
526
- async getPost(orgId, actor, postId) {
527
- 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);
528
580
  this.logger.debug("post read", {
529
- orgId,
581
+ orgId: actor.orgId,
530
582
  postId
531
583
  });
532
- return this.viewOf(orgId, actor, post);
584
+ return this.viewOf(actor, post);
533
585
  }
534
- async moderationQueue(orgId, actor, communityId, spaceId) {
535
- await this.requireOpen(orgId, actor, spaceId, communityId);
536
- 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
+ }
537
598
  const [pending, flags, openCommentFlags] = await Promise.all([
538
599
  this.repo.listPending(orgId, spaceId, 100),
539
600
  this.repo.listOpenFlags(orgId, spaceId, 100),
@@ -547,47 +608,67 @@ var PostServiceImpl = class {
547
608
  commentFlags: openCommentFlags.length
548
609
  });
549
610
  return {
550
- pending: await this.viewsOf(orgId, actor, pending),
611
+ pending: await this.viewsOf(actor, pending),
551
612
  flags,
552
- commentFlags: openCommentFlags
613
+ commentFlags: await this.flaggedCommentViews(actor, space.communityId, openCommentFlags)
553
614
  };
554
615
  }
555
- visible(orgId, actor, postId) {
556
- 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);
557
635
  }
558
636
  find(orgId, postId) {
559
637
  return this.repo.find(orgId, postId);
560
638
  }
561
- async submittable(orgId, post, space, role) {
562
- if (role === null || !this.topicFits(space, post.topicId)) return false;
563
- return this.access.canOpen(orgId, {
564
- id: post.orgUserId,
565
- role
566
- }, space);
639
+ async submittable(post, space, author) {
640
+ if (!this.topicFits(space, post.topicId)) return false;
641
+ return this.access.canOpen(space, author);
567
642
  }
568
- async feedPage(orgId, actor, spaceIds, query) {
569
- 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);
570
645
  return {
571
646
  ...page,
572
- rows: await this.viewsOf(orgId, actor, page.rows)
647
+ rows: await this.viewsOf(actor, page.rows)
573
648
  };
574
649
  }
575
- async viewOf(orgId, actor, post) {
576
- const [view] = await this.viewsOf(orgId, actor, [post]);
650
+ async viewOf(actor, post) {
651
+ const [view] = await this.viewsOf(actor, [post]);
577
652
  return view;
578
653
  }
579
- async viewsOf(orgId, actor, posts) {
654
+ async viewsOf(actor, posts) {
580
655
  if (posts.length === 0) return [];
656
+ const orgId = actor.orgId;
581
657
  const postIds = posts.map((post) => post.id);
658
+ const authorIds = [...new Set(posts.map((post) => post.orgUserId))];
582
659
  const now = /* @__PURE__ */ new Date();
583
- const [records, reactions, commentCounts, subscribed, bookmarked, mentioned] = await Promise.all([
584
- this.profiles.authorsOf(orgId, [...new Set(posts.map((post) => post.orgUserId))]),
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),
585
664
  this.repo.reactionsOf(orgId, postIds, actor.id),
586
665
  this.comments.publishedCommentCounts(orgId, postIds),
587
666
  this.subscriptions.forViewer(orgId, actor.id, "post", postIds),
588
667
  this.repo.bookmarkedAmong(orgId, postIds, actor.id),
589
- mentionViews(this.mentions, this.profiles, orgId, "post", postIds)
668
+ mentionViews(this.mentions, this.profiles, orgId, "post", postIds),
669
+ this.images.memberUploads(orgId, [...new Set([...bodyAssets.values()].flat())])
590
670
  ]);
671
+ const uploadOf = new Map(uploads.map((upload) => [upload.id, upload]));
591
672
  const subscriptionOf = new Map(subscribed.map((row) => [row.itemId, row]));
592
673
  const saved = new Set(bookmarked);
593
674
  return posts.map((post) => {
@@ -596,8 +677,17 @@ var PostServiceImpl = class {
596
677
  const counts = reactions[post.id];
597
678
  return {
598
679
  ...post,
599
- coverUrl: post.coverAssetId ? this.fileUrls.postCover(post.id) : null,
600
- 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)),
601
691
  reactions: counts?.reactions ?? {},
602
692
  viewerReaction: counts?.viewerReaction ?? null,
603
693
  commentCount: commentCounts[post.id] ?? 0,
@@ -607,19 +697,21 @@ var PostServiceImpl = class {
607
697
  };
608
698
  });
609
699
  }
610
- async commitSubmission(orgId, post, space, role, actor, now) {
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));
700
+ async commitSubmission(orgId, post, space, skips, actor, now) {
701
+ const mentioned = await this.mentionedOpeners(orgId, post);
702
+ return this.uow.run(async ({ posts, outbox }) => {
703
+ const next = await posts.updateIf(orgId, post.id, { status: post.status }, this.submissionOf(space.settings.requireReview, skips, now));
613
704
  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))]);
705
+ await outbox.append([this.landed(orgId, next, this.meta(actor, post.status), mentioned)]);
615
706
  return next;
616
707
  });
617
708
  }
618
- async moderate(orgId, postId, actor, patch, unchanged, log) {
619
- const { post } = await this.loadVisible(orgId, actor, postId);
620
- 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);
621
713
  if (post.status === "removed") throw new ConflictError("This post was removed");
622
- if (unchanged(post)) return this.viewOf(orgId, actor, post);
714
+ if (unchanged(post)) return this.viewOf(actor, post);
623
715
  const updated = await this.uow.run(async ({ posts, outbox }) => {
624
716
  const next = await this.mustUpdate(posts, orgId, postId, patch);
625
717
  await outbox.append([this.updated(orgId, next, this.meta(actor, next.status === "pending" ? post.status : void 0))]);
@@ -630,7 +722,7 @@ var PostServiceImpl = class {
630
722
  postId,
631
723
  actorId: actor.id
632
724
  });
633
- return this.viewOf(orgId, actor, updated);
725
+ return this.viewOf(actor, updated);
634
726
  }
635
727
  async mustUpdate(posts, orgId, postId, patch) {
636
728
  const next = await posts.update(orgId, postId, patch);
@@ -654,10 +746,10 @@ var PostServiceImpl = class {
654
746
  removedBy: null
655
747
  };
656
748
  }
657
- return this.submissionOf(space.settings.requireReview, actor.role, now);
749
+ return this.submissionOf(space.settings.requireReview, skipsReview(actor.permissions, { communityId: space.communityId }), now);
658
750
  }
659
- submissionOf(requireReview, role, now) {
660
- if (requireReview && !(role && isStaffRole(role))) return {
751
+ submissionOf(requireReview, skips, now) {
752
+ if (requireReview && !skips) return {
661
753
  status: "pending",
662
754
  scheduledAt: null,
663
755
  publishedAt: null,
@@ -681,20 +773,31 @@ var PostServiceImpl = class {
681
773
  throw new NotFoundError("Asset", assetId);
682
774
  }
683
775
  }
684
- async openSpace(orgId, actor, spaceId, communityId) {
685
- const space = await this.content.getSpace(orgId, spaceId);
686
- if (!space || communityId !== void 0 && space.communityId !== communityId) return null;
687
- 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;
688
791
  }
689
- async requireOpen(orgId, actor, spaceId, communityId) {
690
- const space = await this.openSpace(orgId, actor, spaceId, communityId);
792
+ async requireOpen(actor, spaceId, communityId) {
793
+ const space = await this.openSpace(actor, spaceId, communityId);
691
794
  if (!space) throw new NotFoundError("Space", spaceId);
692
795
  return space;
693
796
  }
694
- async loadVisible(orgId, actor, postId, communityId) {
695
- const post = await this.repo.find(orgId, postId);
696
- if (!post || communityId !== void 0 && post.communityId !== communityId) throw new NotFoundError("Post", postId);
697
- 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);
698
801
  if (!space || !this.canSee(actor, post)) throw new NotFoundError("Post", postId);
699
802
  return {
700
803
  post,
@@ -702,25 +805,34 @@ var PostServiceImpl = class {
702
805
  };
703
806
  }
704
807
  canSee(actor, post) {
705
- return post.orgUserId === actor.id || this.visibleStatuses(actor).includes(post.status);
808
+ return post.orgUserId === actor.id || this.visibleStatuses(actor, post.communityId).includes(post.status);
706
809
  }
707
- visibleStatuses(actor) {
708
- return this.canModerate(actor) ? MODERATOR_VISIBLE : PUBLISHED_ONLY;
810
+ visibleStatuses(actor, communityId) {
811
+ return this.moderates(actor.permissions, communityId) ? MODERATOR_VISIBLE : PUBLISHED_ONLY;
709
812
  }
710
- canModerate(actor) {
711
- return canModerate(actor.role);
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
+ }
712
827
  }
713
828
  assertAuthor(actor, post) {
714
829
  if (post.orgUserId !== actor.id) throw new ForbiddenError("You can change only your own post");
715
830
  }
716
831
  topicFits(space, topicId) {
717
- if (topicId === null) return space.topics.length === 0;
718
- return space.topics.some((topic) => topic.id === topicId);
832
+ return topicId === null || space.topics.some((topic) => topic.id === topicId);
719
833
  }
720
834
  assertTopic(space, topicId) {
721
- if (this.topicFits(space, topicId)) return;
722
- if (topicId === null) throw new InvalidInputError("This space requires a topic");
723
- throw new NotFoundError("Topic", topicId);
835
+ if (topicId !== null && !this.topicFits(space, topicId)) throw new NotFoundError("Topic", topicId);
724
836
  }
725
837
  patchChanges(post, patch) {
726
838
  return Object.keys(patch).some((key) => {
@@ -731,15 +843,17 @@ var PostServiceImpl = class {
731
843
  });
732
844
  }
733
845
  async mentionable(orgId, spaceId, title, body) {
734
- return this.openersOf(orgId, spaceId, mentionTokens(title, body));
846
+ return this.openersOf(orgId, spaceId, [.../* @__PURE__ */ new Set([...mentionTokens(title), ...body])]);
735
847
  }
736
848
  async openersOf(orgId, spaceId, orgUserIds) {
737
849
  if (orgUserIds.length === 0) return [];
738
850
  return keepAllowed(orgUserIds, await this.access.spaceOpenersAmong(orgId, spaceId, orgUserIds));
739
851
  }
740
- async landed(mentions, orgId, post, metadata) {
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) {
741
856
  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
857
  return this.published(orgId, post, metadata, mentioned);
744
858
  }
745
859
  published(orgId, post, metadata, mentionedOrgUserIds) {
@@ -764,7 +878,7 @@ var PostServiceImpl = class {
764
878
  }
765
879
  meta(actor, previousStatus) {
766
880
  return {
767
- ...actor ? { actorOrgUserId: actor.id } : {},
881
+ ...actor ? { actorId: actor.id } : {},
768
882
  ...previousStatus !== void 0 ? { previousStatus } : {}
769
883
  };
770
884
  }