@headless-lms/core 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (307) hide show
  1. package/dist/assets/events.d.ts +42 -24
  2. package/dist/assets/events.js +1 -1
  3. package/dist/assets/events.js.map +1 -1
  4. package/dist/assets/file-urls.d.ts +20 -0
  5. package/dist/assets/file-urls.js +25 -0
  6. package/dist/assets/file-urls.js.map +1 -0
  7. package/dist/assets/index.d.ts +6 -4
  8. package/dist/assets/index.js +4 -2
  9. package/dist/assets/jobs.d.ts +15 -0
  10. package/dist/assets/jobs.js +30 -0
  11. package/dist/assets/jobs.js.map +1 -0
  12. package/dist/assets/model.d.ts +2 -2
  13. package/dist/assets/ports.d.ts +46 -15
  14. package/dist/assets/service.d.ts +38 -7
  15. package/dist/assets/service.js +287 -38
  16. package/dist/assets/service.js.map +1 -1
  17. package/dist/automations/actions.d.ts +13 -24
  18. package/dist/automations/actions.js +106 -42
  19. package/dist/automations/actions.js.map +1 -1
  20. package/dist/automations/catalog.js +226 -3
  21. package/dist/automations/catalog.js.map +1 -1
  22. package/dist/automations/community-derivations.d.ts +14 -0
  23. package/dist/automations/community-derivations.js +439 -0
  24. package/dist/automations/community-derivations.js.map +1 -0
  25. package/dist/automations/community-mail.d.ts +98 -0
  26. package/dist/automations/community-mail.js +70 -0
  27. package/dist/automations/community-mail.js.map +1 -0
  28. package/dist/automations/events.d.ts +18 -0
  29. package/dist/automations/index.d.ts +13 -4
  30. package/dist/automations/index.js +10 -2
  31. package/dist/automations/model.d.ts +10 -3
  32. package/dist/automations/model.js +15 -2
  33. package/dist/automations/model.js.map +1 -1
  34. package/dist/automations/notifications/debounce.d.ts +25 -0
  35. package/dist/automations/notifications/debounce.js +93 -0
  36. package/dist/automations/notifications/debounce.js.map +1 -0
  37. package/dist/automations/notifications/defaults.d.ts +8 -0
  38. package/dist/automations/notifications/defaults.js +316 -0
  39. package/dist/automations/notifications/defaults.js.map +1 -0
  40. package/dist/automations/notifications/delivery.d.ts +28 -0
  41. package/dist/automations/notifications/delivery.js +88 -0
  42. package/dist/automations/notifications/delivery.js.map +1 -0
  43. package/dist/automations/notifications/processor.d.ts +64 -0
  44. package/dist/automations/notifications/processor.js +270 -0
  45. package/dist/automations/notifications/processor.js.map +1 -0
  46. package/dist/automations/notifications/rules/content.d.ts +6 -0
  47. package/dist/automations/notifications/rules/content.js +39 -0
  48. package/dist/automations/notifications/rules/content.js.map +1 -0
  49. package/dist/automations/notifications/rules/discussion.d.ts +7 -0
  50. package/dist/automations/notifications/rules/discussion.js +678 -0
  51. package/dist/automations/notifications/rules/discussion.js.map +1 -0
  52. package/dist/automations/notifications/rules/entitlements.d.ts +6 -0
  53. package/dist/automations/notifications/rules/entitlements.js +249 -0
  54. package/dist/automations/notifications/rules/entitlements.js.map +1 -0
  55. package/dist/automations/notifications/rules/index.d.ts +6 -0
  56. package/dist/automations/notifications/rules/index.js +15 -0
  57. package/dist/automations/notifications/rules/index.js.map +1 -0
  58. package/dist/automations/notifications/rules/organizations.d.ts +6 -0
  59. package/dist/automations/notifications/rules/organizations.js +28 -0
  60. package/dist/automations/notifications/rules/organizations.js.map +1 -0
  61. package/dist/automations/notifications/rules/recipients.d.ts +32 -0
  62. package/dist/automations/notifications/rules/recipients.js +99 -0
  63. package/dist/automations/notifications/rules/recipients.js.map +1 -0
  64. package/dist/automations/notifications/service.d.ts +27 -0
  65. package/dist/automations/notifications/service.js +45 -0
  66. package/dist/automations/notifications/service.js.map +1 -0
  67. package/dist/automations/notifications/types.d.ts +7 -0
  68. package/dist/automations/notifications/types.js +473 -0
  69. package/dist/automations/notifications/types.js.map +1 -0
  70. package/dist/automations/ports.d.ts +121 -3
  71. package/dist/automations/schedule.d.ts +6 -0
  72. package/dist/automations/schedule.js +41 -0
  73. package/dist/automations/schedule.js.map +1 -0
  74. package/dist/automations/service.d.ts +20 -4
  75. package/dist/automations/service.js +246 -45
  76. package/dist/automations/service.js.map +1 -1
  77. package/dist/content/events.d.ts +712 -50
  78. package/dist/content/events.js +55 -1
  79. package/dist/content/events.js.map +1 -1
  80. package/dist/content/index.d.ts +6 -4
  81. package/dist/content/index.js +3 -1
  82. package/dist/content/model.d.ts +2 -2
  83. package/dist/content/notification-categories.d.ts +36 -0
  84. package/dist/content/notification-categories.js +64 -0
  85. package/dist/content/notification-categories.js.map +1 -0
  86. package/dist/content/ports.d.ts +90 -18
  87. package/dist/content/service.d.ts +57 -18
  88. package/dist/content/service.js +520 -25
  89. package/dist/content/service.js.map +1 -1
  90. package/dist/content/titles.d.ts +22 -0
  91. package/dist/content/titles.js +69 -0
  92. package/dist/content/titles.js.map +1 -0
  93. package/dist/content/types.d.ts +2 -2
  94. package/dist/discussion/authors.d.ts +10 -0
  95. package/dist/discussion/authors.js +23 -0
  96. package/dist/discussion/authors.js.map +1 -0
  97. package/dist/discussion/chats.d.ts +109 -0
  98. package/dist/discussion/chats.js +831 -0
  99. package/dist/discussion/chats.js.map +1 -0
  100. package/dist/discussion/digests.d.ts +24 -0
  101. package/dist/discussion/digests.js +104 -0
  102. package/dist/discussion/digests.js.map +1 -0
  103. package/dist/discussion/events.d.ts +785 -56
  104. package/dist/discussion/events.js +165 -5
  105. package/dist/discussion/events.js.map +1 -1
  106. package/dist/discussion/index.d.ts +12 -3
  107. package/dist/discussion/index.js +9 -1
  108. package/dist/discussion/jobs.d.ts +16 -0
  109. package/dist/discussion/jobs.js +40 -0
  110. package/dist/discussion/jobs.js.map +1 -0
  111. package/dist/discussion/mentions.d.ts +11 -0
  112. package/dist/discussion/mentions.js +35 -0
  113. package/dist/discussion/mentions.js.map +1 -0
  114. package/dist/discussion/ports.d.ts +372 -28
  115. package/dist/discussion/ports.js +6 -1
  116. package/dist/discussion/ports.js.map +1 -0
  117. package/dist/discussion/posts.d.ts +113 -0
  118. package/dist/discussion/posts.js +775 -0
  119. package/dist/discussion/posts.js.map +1 -0
  120. package/dist/discussion/service.d.ts +42 -11
  121. package/dist/discussion/service.js +364 -85
  122. package/dist/discussion/service.js.map +1 -1
  123. package/dist/discussion/subscriptions.d.ts +37 -0
  124. package/dist/discussion/subscriptions.js +99 -0
  125. package/dist/discussion/subscriptions.js.map +1 -0
  126. package/dist/discussion/titles.d.ts +26 -0
  127. package/dist/discussion/titles.js +84 -0
  128. package/dist/discussion/titles.js.map +1 -0
  129. package/dist/entitlements/events.d.ts +178 -11
  130. package/dist/entitlements/events.js +28 -1
  131. package/dist/entitlements/events.js.map +1 -1
  132. package/dist/entitlements/index.d.ts +6 -4
  133. package/dist/entitlements/index.js +3 -1
  134. package/dist/entitlements/jobs.d.ts +12 -0
  135. package/dist/entitlements/jobs.js +18 -0
  136. package/dist/entitlements/jobs.js.map +1 -0
  137. package/dist/entitlements/model.d.ts +3 -2
  138. package/dist/entitlements/ports.d.ts +128 -11
  139. package/dist/entitlements/rules.d.ts +12 -0
  140. package/dist/entitlements/rules.js +17 -0
  141. package/dist/entitlements/rules.js.map +1 -0
  142. package/dist/entitlements/service.d.ts +57 -3
  143. package/dist/entitlements/service.js +492 -2
  144. package/dist/entitlements/service.js.map +1 -1
  145. package/dist/identity/access.d.ts +6 -0
  146. package/dist/identity/access.js +7 -0
  147. package/dist/identity/access.js.map +1 -0
  148. package/dist/identity/devices.d.ts +30 -0
  149. package/dist/identity/devices.js +107 -0
  150. package/dist/identity/devices.js.map +1 -0
  151. package/dist/identity/events.d.ts +2 -2
  152. package/dist/identity/index.d.ts +7 -4
  153. package/dist/identity/index.js +4 -1
  154. package/dist/identity/model.d.ts +2 -1
  155. package/dist/identity/notification-settings.d.ts +28 -0
  156. package/dist/identity/notification-settings.js +113 -0
  157. package/dist/identity/notification-settings.js.map +1 -0
  158. package/dist/identity/ports.d.ts +97 -3
  159. package/dist/identity/service.d.ts +14 -3
  160. package/dist/identity/service.js +62 -5
  161. package/dist/identity/service.js.map +1 -1
  162. package/dist/integrations/events.d.ts +3 -3
  163. package/dist/integrations/model.js +1 -1
  164. package/dist/integrations/model.js.map +1 -1
  165. package/dist/integrations/service.js +2 -2
  166. package/dist/integrations/service.js.map +1 -1
  167. package/dist/organizations/access-scopes.d.ts +19 -0
  168. package/dist/organizations/access-scopes.js +89 -0
  169. package/dist/organizations/access-scopes.js.map +1 -0
  170. package/dist/organizations/events.d.ts +75 -33
  171. package/dist/organizations/events.js +6 -0
  172. package/dist/organizations/events.js.map +1 -1
  173. package/dist/organizations/index.d.ts +8 -6
  174. package/dist/organizations/index.js +5 -3
  175. package/dist/organizations/members.d.ts +8 -5
  176. package/dist/organizations/members.js.map +1 -1
  177. package/dist/organizations/notification-categories.d.ts +32 -0
  178. package/dist/organizations/notification-categories.js +51 -0
  179. package/dist/organizations/notification-categories.js.map +1 -0
  180. package/dist/organizations/ports.d.ts +17 -11
  181. package/dist/organizations/roles.d.ts +6 -10
  182. package/dist/organizations/roles.js +19 -7
  183. package/dist/organizations/roles.js.map +1 -1
  184. package/dist/organizations/service.d.ts +22 -11
  185. package/dist/organizations/service.js +164 -47
  186. package/dist/organizations/service.js.map +1 -1
  187. package/dist/organizations/types.d.ts +1 -10
  188. package/dist/progress/events.d.ts +14 -14
  189. package/dist/progress/index.d.ts +2 -2
  190. package/dist/progress/ports.d.ts +6 -1
  191. package/dist/progress/service.d.ts +3 -1
  192. package/dist/progress/service.js +6 -2
  193. package/dist/progress/service.js.map +1 -1
  194. package/dist/reporting/admin/index.d.ts +4 -0
  195. package/dist/reporting/admin/index.js +2 -0
  196. package/dist/reporting/admin/model.d.ts +108 -0
  197. package/dist/reporting/admin/ports.d.ts +27 -0
  198. package/dist/reporting/admin/service.d.ts +22 -0
  199. package/dist/reporting/admin/service.js +38 -0
  200. package/dist/reporting/admin/service.js.map +1 -0
  201. package/dist/reporting/member/index.d.ts +5 -0
  202. package/dist/reporting/member/index.js +2 -0
  203. package/dist/reporting/member/model.d.ts +157 -0
  204. package/dist/reporting/member/ports.d.ts +48 -0
  205. package/dist/reporting/member/service.d.ts +92 -0
  206. package/dist/reporting/member/service.js +364 -0
  207. package/dist/reporting/member/service.js.map +1 -0
  208. package/dist/shared/actor.d.ts +9 -0
  209. package/dist/shared/errors.d.ts +19 -1
  210. package/dist/shared/errors.js +27 -1
  211. package/dist/shared/errors.js.map +1 -1
  212. package/dist/shared/id.d.ts +14 -0
  213. package/dist/shared/id.js +15 -1
  214. package/dist/shared/id.js.map +1 -1
  215. package/dist/shared/mailer.d.ts +15 -2
  216. package/dist/shared/mailer.js +32 -5
  217. package/dist/shared/mailer.js.map +1 -1
  218. package/dist/shared/network.d.ts +10 -0
  219. package/dist/shared/network.js +211 -0
  220. package/dist/shared/network.js.map +1 -0
  221. package/dist/shared/notification-categories.d.ts +16 -0
  222. package/dist/shared/notification-categories.js +94 -0
  223. package/dist/shared/notification-categories.js.map +1 -0
  224. package/dist/shared/notification-settings.d.ts +14 -0
  225. package/dist/shared/notification-settings.js +61 -0
  226. package/dist/shared/notification-settings.js.map +1 -0
  227. package/dist/shared/ports.d.ts +48 -2
  228. package/dist/shared/settings.d.ts +2 -1
  229. package/dist/shared/settings.js +1 -0
  230. package/dist/shared/settings.js.map +1 -1
  231. package/dist/types/assets.d.ts +2 -2
  232. package/dist/types/automations.d.ts +15 -1
  233. package/dist/types/content.d.ts +2 -2
  234. package/dist/types/discussion.d.ts +11 -3
  235. package/dist/types/email-templates.d.ts +216 -1
  236. package/dist/types/entitlements.d.ts +2 -2
  237. package/dist/types/identity.d.ts +48 -2
  238. package/dist/types/index.d.ts +9 -8
  239. package/dist/types/notifications.d.ts +157 -0
  240. package/dist/types/ports.d.ts +6 -0
  241. package/dist/types/schemas/assets.d.ts +65 -37
  242. package/dist/types/schemas/assets.js +17 -12
  243. package/dist/types/schemas/assets.js.map +1 -1
  244. package/dist/types/schemas/automations.d.ts +9 -1
  245. package/dist/types/schemas/automations.js +13 -6
  246. package/dist/types/schemas/automations.js.map +1 -1
  247. package/dist/types/schemas/content.d.ts +396 -3
  248. package/dist/types/schemas/content.js +161 -3
  249. package/dist/types/schemas/content.js.map +1 -1
  250. package/dist/types/schemas/discussion.d.ts +443 -3
  251. package/dist/types/schemas/discussion.js +233 -4
  252. package/dist/types/schemas/discussion.js.map +1 -1
  253. package/dist/types/schemas/entitlements.d.ts +36 -2
  254. package/dist/types/schemas/entitlements.js +23 -1
  255. package/dist/types/schemas/entitlements.js.map +1 -1
  256. package/dist/types/schemas/identity.d.ts +27 -1
  257. package/dist/types/schemas/identity.js +14 -2
  258. package/dist/types/schemas/identity.js.map +1 -1
  259. package/dist/types/schemas/index.d.ts +9 -8
  260. package/dist/types/schemas/index.js +9 -8
  261. package/dist/types/schemas/notifications.d.ts +95 -0
  262. package/dist/types/schemas/notifications.js +56 -0
  263. package/dist/types/schemas/notifications.js.map +1 -0
  264. package/dist/types/schemas/organizations.d.ts +8 -0
  265. package/dist/types/schemas/organizations.js +2 -0
  266. package/dist/types/schemas/organizations.js.map +1 -1
  267. package/dist/types/schemas/progress.d.ts +3 -3
  268. package/dist/types/schemas/shared.d.ts +3 -1
  269. package/dist/types/schemas/shared.js +5 -1
  270. package/dist/types/schemas/shared.js.map +1 -1
  271. package/package.json +2 -2
  272. package/dist/reporting/courses/index.d.ts +0 -4
  273. package/dist/reporting/courses/index.js +0 -2
  274. package/dist/reporting/courses/model.d.ts +0 -24
  275. package/dist/reporting/courses/ports.d.ts +0 -13
  276. package/dist/reporting/courses/service.d.ts +0 -19
  277. package/dist/reporting/courses/service.js +0 -20
  278. package/dist/reporting/courses/service.js.map +0 -1
  279. package/dist/reporting/dashboard/index.d.ts +0 -4
  280. package/dist/reporting/dashboard/index.js +0 -2
  281. package/dist/reporting/dashboard/model.d.ts +0 -14
  282. package/dist/reporting/dashboard/ports.d.ts +0 -13
  283. package/dist/reporting/dashboard/service.d.ts +0 -19
  284. package/dist/reporting/dashboard/service.js +0 -20
  285. package/dist/reporting/dashboard/service.js.map +0 -1
  286. package/dist/reporting/learn/index.d.ts +0 -5
  287. package/dist/reporting/learn/index.js +0 -2
  288. package/dist/reporting/learn/model.d.ts +0 -23
  289. package/dist/reporting/learn/ports.d.ts +0 -43
  290. package/dist/reporting/learn/service.d.ts +0 -49
  291. package/dist/reporting/learn/service.js +0 -116
  292. package/dist/reporting/learn/service.js.map +0 -1
  293. package/dist/reporting/students/index.d.ts +0 -4
  294. package/dist/reporting/students/index.js +0 -2
  295. package/dist/reporting/students/model.d.ts +0 -51
  296. package/dist/reporting/students/model.js +0 -1
  297. package/dist/reporting/students/ports.d.ts +0 -15
  298. package/dist/reporting/students/ports.js +0 -1
  299. package/dist/reporting/students/service.d.ts +0 -20
  300. package/dist/reporting/students/service.js +0 -23
  301. package/dist/reporting/students/service.js.map +0 -1
  302. /package/dist/reporting/{courses → admin}/model.js +0 -0
  303. /package/dist/reporting/{courses → admin}/ports.js +0 -0
  304. /package/dist/reporting/{dashboard → member}/model.js +0 -0
  305. /package/dist/reporting/{dashboard → member}/ports.js +0 -0
  306. /package/dist/{reporting/learn/model.js → shared/actor.js} +0 -0
  307. /package/dist/{reporting/learn/ports.js → types/notifications.js} +0 -0
@@ -0,0 +1,775 @@
1
+ import { genId } from "../shared/id.js";
2
+ import { ConflictError, ForbiddenError, InvalidInputError, NotFoundError, UnavailableError } from "../shared/errors.js";
3
+ import { noopLogger } from "../shared/logger.js";
4
+ import { canModerate, isStaffRole } from "../organizations/roles.js";
5
+ import { discussionEvents } from "./events.js";
6
+ import { authorView } from "./authors.js";
7
+ import { keepAllowed, mentionTokens, mentionViews } from "./mentions.js";
8
+ import { POST_PUBLISH_JOB } from "./jobs.js";
9
+ import { activeSubscription } from "./subscriptions.js";
10
+ import "./ports.js";
11
+ //#region src/discussion/posts.ts
12
+ const PUBLISHED_ONLY = ["published"];
13
+ const MODERATOR_VISIBLE = [
14
+ "published",
15
+ "pending",
16
+ "removed"
17
+ ];
18
+ const DRAFT = {
19
+ status: "draft",
20
+ scheduledAt: null,
21
+ publishedAt: null,
22
+ removedBy: null
23
+ };
24
+ var PostServiceImpl = class {
25
+ repo;
26
+ access;
27
+ content;
28
+ authors;
29
+ profiles;
30
+ comments;
31
+ subscriptions;
32
+ mentions;
33
+ images;
34
+ fileUrls;
35
+ uow;
36
+ scheduler;
37
+ logger;
38
+ constructor(params) {
39
+ this.repo = params.repo;
40
+ this.access = params.access;
41
+ this.content = params.content;
42
+ this.authors = params.authors;
43
+ this.profiles = params.profiles;
44
+ this.comments = params.comments;
45
+ this.subscriptions = params.subscriptions;
46
+ this.mentions = params.mentions;
47
+ this.images = params.images;
48
+ this.fileUrls = params.fileUrls;
49
+ this.uow = params.uow;
50
+ this.scheduler = params.scheduler;
51
+ this.logger = params.logger ?? noopLogger;
52
+ }
53
+ async visibleTitles(orgId, actor, postIds) {
54
+ const found = await this.repo.findMany(orgId, [...new Set(postIds)]);
55
+ 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());
57
+ return Object.fromEntries(found.filter((post) => open.has(post.spaceId) && this.canSee(actor, post)).map((post) => [post.id, post.title]));
58
+ }
59
+ publishedBetween(orgId, communityId, from, to) {
60
+ return this.repo.publishedBetween(orgId, communityId, from, to);
61
+ }
62
+ async create(orgId, actor, input) {
63
+ const space = await this.requireOpen(orgId, actor, input.spaceId, input.communityId);
64
+ await this.requireOwnedImage(orgId, actor.id, input.coverAssetId);
65
+ this.assertTopic(space, input.topicId);
66
+ const now = /* @__PURE__ */ new Date();
67
+ const landing = this.initial(input, space, actor, now);
68
+ const post = {
69
+ orgId,
70
+ id: genId("post"),
71
+ communityId: space.communityId,
72
+ spaceId: space.id,
73
+ orgUserId: actor.id,
74
+ topicId: input.topicId,
75
+ title: input.title,
76
+ body: input.body,
77
+ extra: input.extra,
78
+ coverAssetId: input.coverAssetId,
79
+ pinned: false,
80
+ commentsClosed: null,
81
+ reactionsEnabled: null,
82
+ createdAt: now,
83
+ updatedAt: now,
84
+ ...landing
85
+ };
86
+ const mentioned = await this.mentionable(orgId, space.id, post.title, post.body);
87
+ const saved = await this.uow.run(async ({ posts, mentions, outbox }) => {
88
+ const created = await posts.insert(orgId, post);
89
+ await mentions.sync(orgId, "post", created.id, mentioned, now);
90
+ const events = [discussionEvents.postCreated.make({
91
+ orgId,
92
+ data: created,
93
+ metadata: this.meta(actor, created.status === "pending" ? null : void 0)
94
+ })];
95
+ if (created.status === "published") events.push(this.published(orgId, created, this.meta(actor, null), mentioned));
96
+ await outbox.append(events);
97
+ return created;
98
+ });
99
+ this.logger.info("post created", {
100
+ orgId,
101
+ postId: saved.id,
102
+ status: saved.status,
103
+ actorId: actor.id
104
+ });
105
+ if (saved.status === "scheduled") await this.arm(orgId, actor, saved, null);
106
+ return this.viewOf(orgId, actor, saved);
107
+ }
108
+ async update(orgId, postId, actor, patch) {
109
+ const { post, space } = await this.loadVisible(orgId, actor, postId);
110
+ this.assertAuthor(actor, post);
111
+ if (post.status === "removed") throw new ConflictError("This post was removed");
112
+ if (patch.topicId !== void 0) this.assertTopic(space, patch.topicId);
113
+ 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);
115
+ const retext = patch.title !== void 0 || patch.body !== void 0;
116
+ const candidates = retext ? await this.mentionable(orgId, post.spaceId, patch.title ?? post.title, patch.body ?? post.body) : [];
117
+ const updated = await this.uow.run(async ({ posts, mentions, outbox }) => {
118
+ const next = await this.mustUpdate(posts, orgId, postId, patch);
119
+ const added = retext ? await mentions.sync(orgId, "post", postId, candidates, /* @__PURE__ */ new Date()) : [];
120
+ await outbox.append([this.updated(orgId, next, this.meta(actor, next.status === "pending" ? post.status : void 0), next.status === "published" ? keepAllowed(candidates, added) : [])]);
121
+ return next;
122
+ });
123
+ this.logger.info("post updated", {
124
+ orgId,
125
+ postId,
126
+ fields: Object.keys(patch),
127
+ actorId: actor.id
128
+ });
129
+ return this.viewOf(orgId, actor, updated);
130
+ }
131
+ async submit(orgId, postId, actor) {
132
+ const { post, space } = await this.loadVisible(orgId, actor, postId);
133
+ this.assertAuthor(actor, post);
134
+ if (post.status !== "draft" && post.status !== "scheduled") throw new ConflictError("This post cannot be submitted");
135
+ this.assertTopic(space, post.topicId);
136
+ const saved = await this.commitSubmission(orgId, post, space, actor.role, actor, /* @__PURE__ */ new Date());
137
+ this.logger.info("post submitted", {
138
+ orgId,
139
+ postId,
140
+ status: saved.status,
141
+ actorId: actor.id
142
+ });
143
+ if (post.status === "scheduled") await this.disarm(orgId, postId);
144
+ return this.viewOf(orgId, actor, saved);
145
+ }
146
+ async schedule(orgId, postId, actor, scheduledAt) {
147
+ const { post } = await this.loadVisible(orgId, actor, postId);
148
+ this.assertAuthor(actor, post);
149
+ if (post.status === "published" || post.status === "removed" || post.status === "pending") throw new ConflictError("This post cannot be scheduled");
150
+ if (scheduledAt <= /* @__PURE__ */ new Date()) throw new InvalidInputError("A scheduled time must be in the future");
151
+ const updated = await this.uow.run(async ({ posts, outbox }) => {
152
+ const next = await posts.updateIf(orgId, postId, { status: post.status }, {
153
+ status: "scheduled",
154
+ scheduledAt,
155
+ publishedAt: null,
156
+ removedBy: null
157
+ });
158
+ if (!next) throw new ConflictError("This post cannot be scheduled");
159
+ await outbox.append([this.updated(orgId, next, this.meta(actor, post.status))]);
160
+ return next;
161
+ });
162
+ this.logger.info("post scheduled", {
163
+ orgId,
164
+ postId,
165
+ scheduledAt: scheduledAt.toISOString(),
166
+ actorId: actor.id
167
+ });
168
+ await this.arm(orgId, actor, updated, post.status === "scheduled" ? post.scheduledAt : null);
169
+ return this.viewOf(orgId, actor, updated);
170
+ }
171
+ async unschedule(orgId, postId, actor) {
172
+ const { post } = await this.loadVisible(orgId, actor, postId);
173
+ this.assertAuthor(actor, post);
174
+ if (post.status !== "scheduled") throw new ConflictError("This post is not scheduled");
175
+ const updated = await this.uow.run(async ({ posts, outbox }) => {
176
+ const next = await posts.updateIf(orgId, postId, { status: "scheduled" }, DRAFT);
177
+ if (!next) throw new ConflictError("This post is not scheduled");
178
+ await outbox.append([this.updated(orgId, next, this.meta(actor, post.status))]);
179
+ return next;
180
+ });
181
+ this.logger.info("post unscheduled", {
182
+ orgId,
183
+ postId,
184
+ actorId: actor.id
185
+ });
186
+ await this.disarm(orgId, postId);
187
+ return this.viewOf(orgId, actor, updated);
188
+ }
189
+ async publishScheduled(orgId, postId, now) {
190
+ const post = await this.repo.find(orgId, postId);
191
+ if (!post || post.status !== "scheduled" || post.scheduledAt === null) {
192
+ this.logger.debug("scheduled post no longer scheduled", {
193
+ orgId,
194
+ postId
195
+ });
196
+ return;
197
+ }
198
+ if (post.scheduledAt > now) {
199
+ await this.scheduler.at(POST_PUBLISH_JOB, post.scheduledAt, postId, {
200
+ orgId,
201
+ postId
202
+ });
203
+ this.logger.info("scheduled post timer moved", {
204
+ orgId,
205
+ postId,
206
+ scheduledAt: post.scheduledAt.toISOString()
207
+ });
208
+ return;
209
+ }
210
+ const space = await this.content.getSpace(orgId, post.spaceId);
211
+ if (!space) {
212
+ this.logger.error("scheduled post has no space", {
213
+ orgId,
214
+ postId,
215
+ spaceId: post.spaceId
216
+ });
217
+ return;
218
+ }
219
+ const role = await this.authors.roleOf(orgId, post.orgUserId);
220
+ const refused = !await this.submittable(orgId, post, space, role);
221
+ const patch = refused ? DRAFT : this.submissionOf(space.settings.requireReview, role, now);
222
+ const saved = await this.uow.run(async ({ posts, mentions, outbox }) => {
223
+ const next = await posts.updateIf(orgId, postId, {
224
+ status: "scheduled",
225
+ dueBy: now
226
+ }, patch);
227
+ if (!next) return null;
228
+ await outbox.append([await this.landed(mentions, orgId, next, this.meta(null, post.status))]);
229
+ return next;
230
+ });
231
+ if (!saved) {
232
+ this.logger.debug("scheduled post already handled", {
233
+ orgId,
234
+ postId
235
+ });
236
+ return;
237
+ }
238
+ this.logger.info("scheduled post submitted", {
239
+ orgId,
240
+ postId,
241
+ status: saved.status,
242
+ refused
243
+ });
244
+ }
245
+ async arm(orgId, actor, post, previousScheduledAt) {
246
+ if (post.scheduledAt === null) return;
247
+ try {
248
+ await this.scheduler.at(POST_PUBLISH_JOB, post.scheduledAt, post.id, {
249
+ orgId,
250
+ postId: post.id
251
+ });
252
+ return;
253
+ } catch (err) {
254
+ this.logger.error("post timer not set", {
255
+ orgId,
256
+ postId: post.id,
257
+ err
258
+ });
259
+ }
260
+ await this.uow.run(async ({ posts, outbox }) => {
261
+ const reverted = await posts.updateIf(orgId, post.id, {
262
+ status: "scheduled",
263
+ scheduledAt: post.scheduledAt
264
+ }, previousScheduledAt ? { scheduledAt: previousScheduledAt } : DRAFT);
265
+ if (!reverted) return;
266
+ await outbox.append([this.updated(orgId, reverted, this.meta(actor, reverted.status === "draft" ? post.status : void 0))]);
267
+ });
268
+ throw new UnavailableError("The post could not be scheduled; try again", { postId: post.id });
269
+ }
270
+ async disarm(orgId, postId) {
271
+ try {
272
+ await this.scheduler.cancel(POST_PUBLISH_JOB, postId);
273
+ } catch (err) {
274
+ this.logger.warn("post timer not cancelled", {
275
+ orgId,
276
+ postId,
277
+ err
278
+ });
279
+ }
280
+ }
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");
284
+ if (post.status !== "pending") throw new ConflictError("This post is not awaiting review");
285
+ const now = /* @__PURE__ */ new Date();
286
+ const updated = await this.uow.run(async ({ posts, mentions, outbox }) => {
287
+ const next = await this.mustUpdate(posts, orgId, postId, {
288
+ status: "published",
289
+ publishedAt: now,
290
+ scheduledAt: null,
291
+ removedBy: null
292
+ });
293
+ await outbox.append([await this.landed(mentions, orgId, next, this.meta(actor, post.status))]);
294
+ return next;
295
+ });
296
+ this.logger.info("post approved", {
297
+ orgId,
298
+ postId,
299
+ actorId: actor.id
300
+ });
301
+ return this.viewOf(orgId, actor, updated);
302
+ }
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");
306
+ if (post.status === "removed") throw new ConflictError("This post was removed");
307
+ if (post.status !== "published" && post.status !== "pending") throw new ConflictError("This post cannot be removed");
308
+ const updated = await this.uow.run(async ({ posts, outbox }) => {
309
+ const next = await this.mustUpdate(posts, orgId, postId, {
310
+ status: "removed",
311
+ removedBy: actor.id
312
+ });
313
+ if (post.orgUserId !== actor.id) await posts.resolveFlags(orgId, postId, {
314
+ resolvedAt: /* @__PURE__ */ new Date(),
315
+ resolvedBy: actor.id,
316
+ resolution: "removed"
317
+ });
318
+ await outbox.append([discussionEvents.postRemoved.make({
319
+ orgId,
320
+ data: next,
321
+ metadata: this.meta(actor, post.status)
322
+ })]);
323
+ return next;
324
+ });
325
+ this.logger.info("post removed", {
326
+ orgId,
327
+ postId,
328
+ actorId: actor.id
329
+ });
330
+ return this.viewOf(orgId, actor, updated);
331
+ }
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");
335
+ if (post.status !== "removed") throw new ConflictError("This post was not removed");
336
+ const now = /* @__PURE__ */ new Date();
337
+ const role = await this.authors.roleOf(orgId, post.orgUserId);
338
+ 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));
340
+ const metadata = this.meta(actor, post.status);
341
+ await outbox.append([next.status === "published" ? this.published(orgId, next, metadata, []) : this.updated(orgId, next, metadata)]);
342
+ return next;
343
+ });
344
+ this.logger.info("post restored", {
345
+ orgId,
346
+ postId,
347
+ status: updated.status,
348
+ actorId: actor.id
349
+ });
350
+ return this.viewOf(orgId, actor, updated);
351
+ }
352
+ pin(orgId, postId, actor) {
353
+ return this.moderate(orgId, postId, actor, { pinned: true }, (post) => post.pinned, "post pinned");
354
+ }
355
+ unpin(orgId, postId, actor) {
356
+ return this.moderate(orgId, postId, actor, { pinned: false }, (post) => !post.pinned, "post unpinned");
357
+ }
358
+ closeComments(orgId, postId, actor) {
359
+ return this.moderate(orgId, postId, actor, { commentsClosed: true }, (post) => post.commentsClosed === true, "post comments closed");
360
+ }
361
+ openComments(orgId, postId, actor) {
362
+ return this.moderate(orgId, postId, actor, { commentsClosed: false }, (post) => post.commentsClosed === false, "post comments opened");
363
+ }
364
+ setReactions(orgId, postId, actor, enabled) {
365
+ return this.moderate(orgId, postId, actor, { reactionsEnabled: enabled }, (post) => post.reactionsEnabled === enabled, "post reactions changed");
366
+ }
367
+ async setReaction(orgId, postId, actor, emoji) {
368
+ const { post, space } = await this.loadVisible(orgId, actor, postId);
369
+ if (!(post.reactionsEnabled ?? space.settings.reactions)) throw new ForbiddenError("Reactions are off on this post");
370
+ await this.uow.run(async ({ posts, outbox }) => {
371
+ if (!await posts.setReaction(orgId, postId, actor.id, emoji) || emoji === null) return;
372
+ await outbox.append([discussionEvents.postReactionAdded.make({
373
+ orgId,
374
+ data: {
375
+ postId,
376
+ spaceId: post.spaceId,
377
+ communityId: post.communityId,
378
+ orgUserId: actor.id,
379
+ reaction: emoji
380
+ },
381
+ metadata: this.meta(actor)
382
+ })]);
383
+ });
384
+ this.logger.debug("post reaction set", {
385
+ orgId,
386
+ postId,
387
+ orgUserId: actor.id
388
+ });
389
+ return (await this.repo.reactionsOf(orgId, [postId], actor.id))[postId] ?? { reactions: {} };
390
+ }
391
+ async bookmark(orgId, postId, actor) {
392
+ await this.loadVisible(orgId, actor, postId);
393
+ await this.repo.bookmark(orgId, postId, actor.id);
394
+ this.logger.debug("post bookmarked", {
395
+ orgId,
396
+ postId,
397
+ orgUserId: actor.id
398
+ });
399
+ }
400
+ async unbookmark(orgId, postId, actor) {
401
+ await this.loadVisible(orgId, actor, postId);
402
+ await this.repo.unbookmark(orgId, postId, actor.id);
403
+ this.logger.debug("post unbookmarked", {
404
+ orgId,
405
+ postId,
406
+ orgUserId: actor.id
407
+ });
408
+ }
409
+ async flag(orgId, postId, actor, reason) {
410
+ const { post } = await this.loadVisible(orgId, actor, postId);
411
+ const now = /* @__PURE__ */ new Date();
412
+ const flag = {
413
+ orgId,
414
+ id: genId("postFlag"),
415
+ postId,
416
+ orgUserId: actor.id,
417
+ reason,
418
+ resolvedAt: null,
419
+ resolution: null,
420
+ resolvedBy: null,
421
+ createdAt: now,
422
+ updatedAt: now
423
+ };
424
+ const saved = await this.uow.run(async ({ posts, outbox }) => {
425
+ const created = await posts.insertFlag(orgId, flag);
426
+ if (!created) {
427
+ const existing = await posts.findFlag(orgId, postId, actor.id);
428
+ if (!existing) throw new NotFoundError("Flag", postId);
429
+ return existing;
430
+ }
431
+ await outbox.append([discussionEvents.postReported.make({
432
+ orgId,
433
+ data: {
434
+ ...created,
435
+ communityId: post.communityId
436
+ },
437
+ metadata: this.meta(actor)
438
+ })]);
439
+ return created;
440
+ });
441
+ this.logger.info("post flagged", {
442
+ orgId,
443
+ postId,
444
+ flagId: saved.id,
445
+ actorId: actor.id
446
+ });
447
+ return saved;
448
+ }
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");
452
+ await this.repo.resolveFlags(orgId, postId, {
453
+ resolvedAt: /* @__PURE__ */ new Date(),
454
+ resolvedBy: actor.id,
455
+ resolution
456
+ });
457
+ this.logger.info("post flags resolved", {
458
+ orgId,
459
+ postId,
460
+ resolution,
461
+ actorId: actor.id
462
+ });
463
+ }
464
+ async spaceFeed(orgId, actor, communityId, spaceId, query) {
465
+ const space = await this.requireOpen(orgId, actor, spaceId, communityId);
466
+ 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);
468
+ this.logger.debug("space feed read", {
469
+ orgId,
470
+ spaceId,
471
+ rows: page.rows.length
472
+ });
473
+ return page;
474
+ }
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);
478
+ this.logger.debug("member feed read", {
479
+ orgId,
480
+ communityId,
481
+ spaces: spaceIds.length,
482
+ rows: page.rows.length
483
+ });
484
+ return page;
485
+ }
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);
489
+ }
490
+ async listOwn(orgId, actor, communityId, query) {
491
+ const spaceIds = await this.openSpaceIdsIn(orgId, actor, communityId);
492
+ const page = await this.repo.listByAuthor(orgId, actor.id, spaceIds, query);
493
+ this.logger.debug("own posts read", {
494
+ orgId,
495
+ communityId,
496
+ rows: page.rows.length
497
+ });
498
+ return {
499
+ ...page,
500
+ rows: await this.viewsOf(orgId, actor, page.rows)
501
+ };
502
+ }
503
+ async listBookmarked(orgId, actor, communityId, query) {
504
+ const spaceIds = await this.openSpaceIdsIn(orgId, actor, communityId);
505
+ const page = await this.repo.listBookmarked(orgId, actor.id, spaceIds, {
506
+ ...query,
507
+ visibleStatuses: this.visibleStatuses(actor)
508
+ });
509
+ this.logger.debug("bookmarked posts read", {
510
+ orgId,
511
+ communityId,
512
+ rows: page.rows.length
513
+ });
514
+ return {
515
+ ...page,
516
+ rows: await this.viewsOf(orgId, actor, page.rows)
517
+ };
518
+ }
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);
528
+ this.logger.debug("post read", {
529
+ orgId,
530
+ postId
531
+ });
532
+ return this.viewOf(orgId, actor, post);
533
+ }
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");
537
+ const [pending, flags, openCommentFlags] = await Promise.all([
538
+ this.repo.listPending(orgId, spaceId, 100),
539
+ this.repo.listOpenFlags(orgId, spaceId, 100),
540
+ this.comments.listOpenFlagsForSpace(orgId, spaceId, 100)
541
+ ]);
542
+ this.logger.debug("space queue read", {
543
+ orgId,
544
+ spaceId,
545
+ pending: pending.length,
546
+ flags: flags.length,
547
+ commentFlags: openCommentFlags.length
548
+ });
549
+ return {
550
+ pending: await this.viewsOf(orgId, actor, pending),
551
+ flags,
552
+ commentFlags: openCommentFlags
553
+ };
554
+ }
555
+ visible(orgId, actor, postId) {
556
+ return this.loadVisible(orgId, actor, postId);
557
+ }
558
+ find(orgId, postId) {
559
+ return this.repo.find(orgId, postId);
560
+ }
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);
567
+ }
568
+ async feedPage(orgId, actor, spaceIds, query) {
569
+ const page = await this.repo.listPublished(orgId, spaceIds, query);
570
+ return {
571
+ ...page,
572
+ rows: await this.viewsOf(orgId, actor, page.rows)
573
+ };
574
+ }
575
+ async viewOf(orgId, actor, post) {
576
+ const [view] = await this.viewsOf(orgId, actor, [post]);
577
+ return view;
578
+ }
579
+ async viewsOf(orgId, actor, posts) {
580
+ if (posts.length === 0) return [];
581
+ const postIds = posts.map((post) => post.id);
582
+ 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))]),
585
+ this.repo.reactionsOf(orgId, postIds, actor.id),
586
+ this.comments.publishedCommentCounts(orgId, postIds),
587
+ this.subscriptions.forViewer(orgId, actor.id, "post", postIds),
588
+ this.repo.bookmarkedAmong(orgId, postIds, actor.id),
589
+ mentionViews(this.mentions, this.profiles, orgId, "post", postIds)
590
+ ]);
591
+ const subscriptionOf = new Map(subscribed.map((row) => [row.itemId, row]));
592
+ const saved = new Set(bookmarked);
593
+ return posts.map((post) => {
594
+ const record = records[post.orgUserId];
595
+ if (!record) throw new NotFoundError("Member", post.orgUserId);
596
+ const counts = reactions[post.id];
597
+ return {
598
+ ...post,
599
+ coverUrl: post.coverAssetId ? this.fileUrls.postCover(post.id) : null,
600
+ author: authorView(orgId, record, this.fileUrls),
601
+ reactions: counts?.reactions ?? {},
602
+ viewerReaction: counts?.viewerReaction ?? null,
603
+ commentCount: commentCounts[post.id] ?? 0,
604
+ subscription: activeSubscription(subscriptionOf.get(post.id), now),
605
+ bookmarked: saved.has(post.id),
606
+ mentions: mentioned[post.id] ?? []
607
+ };
608
+ });
609
+ }
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));
613
+ if (!next) throw new ConflictError("This post cannot be submitted");
614
+ await outbox.append([await this.landed(mentions, orgId, next, this.meta(actor, post.status))]);
615
+ return next;
616
+ });
617
+ }
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");
621
+ if (post.status === "removed") throw new ConflictError("This post was removed");
622
+ if (unchanged(post)) return this.viewOf(orgId, actor, post);
623
+ const updated = await this.uow.run(async ({ posts, outbox }) => {
624
+ const next = await this.mustUpdate(posts, orgId, postId, patch);
625
+ await outbox.append([this.updated(orgId, next, this.meta(actor, next.status === "pending" ? post.status : void 0))]);
626
+ return next;
627
+ });
628
+ this.logger.info(log, {
629
+ orgId,
630
+ postId,
631
+ actorId: actor.id
632
+ });
633
+ return this.viewOf(orgId, actor, updated);
634
+ }
635
+ async mustUpdate(posts, orgId, postId, patch) {
636
+ const next = await posts.update(orgId, postId, patch);
637
+ if (!next) throw new NotFoundError("Post", postId);
638
+ return next;
639
+ }
640
+ initial(input, space, actor, now) {
641
+ if (input.mode === "draft") return {
642
+ status: "draft",
643
+ scheduledAt: null,
644
+ publishedAt: null,
645
+ removedBy: null
646
+ };
647
+ if (input.mode === "schedule") {
648
+ if (!input.scheduledAt) throw new InvalidInputError("A scheduled post needs a time");
649
+ if (input.scheduledAt <= now) throw new InvalidInputError("A scheduled time must be in the future");
650
+ return {
651
+ status: "scheduled",
652
+ scheduledAt: input.scheduledAt,
653
+ publishedAt: null,
654
+ removedBy: null
655
+ };
656
+ }
657
+ return this.submissionOf(space.settings.requireReview, actor.role, now);
658
+ }
659
+ submissionOf(requireReview, role, now) {
660
+ if (requireReview && !(role && isStaffRole(role))) return {
661
+ status: "pending",
662
+ scheduledAt: null,
663
+ publishedAt: null,
664
+ removedBy: null
665
+ };
666
+ return {
667
+ status: "published",
668
+ scheduledAt: null,
669
+ publishedAt: now,
670
+ removedBy: null
671
+ };
672
+ }
673
+ async requireOwnedImage(orgId, orgUserId, assetId) {
674
+ if (assetId === null) return;
675
+ if (!await this.images.ownsImage(orgId, assetId, orgUserId)) {
676
+ this.logger.debug("cover refused", {
677
+ orgId,
678
+ orgUserId,
679
+ assetId
680
+ });
681
+ throw new NotFoundError("Asset", assetId);
682
+ }
683
+ }
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;
688
+ }
689
+ async requireOpen(orgId, actor, spaceId, communityId) {
690
+ const space = await this.openSpace(orgId, actor, spaceId, communityId);
691
+ if (!space) throw new NotFoundError("Space", spaceId);
692
+ return space;
693
+ }
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);
698
+ if (!space || !this.canSee(actor, post)) throw new NotFoundError("Post", postId);
699
+ return {
700
+ post,
701
+ space
702
+ };
703
+ }
704
+ canSee(actor, post) {
705
+ return post.orgUserId === actor.id || this.visibleStatuses(actor).includes(post.status);
706
+ }
707
+ visibleStatuses(actor) {
708
+ return this.canModerate(actor) ? MODERATOR_VISIBLE : PUBLISHED_ONLY;
709
+ }
710
+ canModerate(actor) {
711
+ return canModerate(actor.role);
712
+ }
713
+ assertAuthor(actor, post) {
714
+ if (post.orgUserId !== actor.id) throw new ForbiddenError("You can change only your own post");
715
+ }
716
+ topicFits(space, topicId) {
717
+ if (topicId === null) return space.topics.length === 0;
718
+ return space.topics.some((topic) => topic.id === topicId);
719
+ }
720
+ 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);
724
+ }
725
+ patchChanges(post, patch) {
726
+ return Object.keys(patch).some((key) => {
727
+ const value = patch[key];
728
+ if (value === void 0) return false;
729
+ if (key === "extra") return JSON.stringify(post.extra) !== JSON.stringify(value);
730
+ return post[key] !== value;
731
+ });
732
+ }
733
+ async mentionable(orgId, spaceId, title, body) {
734
+ return this.openersOf(orgId, spaceId, mentionTokens(title, body));
735
+ }
736
+ async openersOf(orgId, spaceId, orgUserIds) {
737
+ if (orgUserIds.length === 0) return [];
738
+ return keepAllowed(orgUserIds, await this.access.spaceOpenersAmong(orgId, spaceId, orgUserIds));
739
+ }
740
+ async landed(mentions, orgId, post, metadata) {
741
+ if (post.status !== "published") return this.updated(orgId, post, metadata);
742
+ const mentioned = await this.openersOf(orgId, post.spaceId, await mentions.ofItem(orgId, "post", post.id));
743
+ return this.published(orgId, post, metadata, mentioned);
744
+ }
745
+ published(orgId, post, metadata, mentionedOrgUserIds) {
746
+ return discussionEvents.postPublished.make({
747
+ orgId,
748
+ data: {
749
+ ...post,
750
+ mentionedOrgUserIds
751
+ },
752
+ metadata
753
+ });
754
+ }
755
+ updated(orgId, post, metadata, mentionedOrgUserIds = []) {
756
+ return discussionEvents.postUpdated.make({
757
+ orgId,
758
+ data: {
759
+ ...post,
760
+ mentionedOrgUserIds
761
+ },
762
+ metadata
763
+ });
764
+ }
765
+ meta(actor, previousStatus) {
766
+ return {
767
+ ...actor ? { actorOrgUserId: actor.id } : {},
768
+ ...previousStatus !== void 0 ? { previousStatus } : {}
769
+ };
770
+ }
771
+ };
772
+ //#endregion
773
+ export { PostServiceImpl };
774
+
775
+ //# sourceMappingURL=posts.js.map