@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
@@ -1,8 +1,11 @@
1
- import { noopLogger } from "../shared/logger.js";
2
1
  import { genId } from "../shared/id.js";
3
- import { ForbiddenError, NotFoundError } from "../shared/errors.js";
2
+ import { ConflictError, ForbiddenError, NotFoundError } from "../shared/errors.js";
3
+ import { noopLogger } from "../shared/logger.js";
4
+ import { canModerate, isStaffRole } from "../organizations/roles.js";
4
5
  import "../shared/settings.js";
5
6
  import { discussionEvents } from "./events.js";
7
+ import { authorView } from "./authors.js";
8
+ import { keepAllowed, mentionTokens, mentionViews } from "./mentions.js";
6
9
  //#region src/discussion/service.ts
7
10
  /** A course with no stored settings. Discussion is opt-in, so the common case
8
11
  * persists no row at all and every existing course stays silent. */
@@ -17,15 +20,23 @@ var DiscussionServiceImpl = class {
17
20
  repo;
18
21
  access;
19
22
  content;
23
+ posts;
24
+ spaces;
25
+ mentions;
20
26
  uow;
21
27
  settings;
28
+ fileUrls;
22
29
  logger;
23
30
  constructor(params) {
24
31
  this.repo = params.repo;
25
32
  this.access = params.access;
26
33
  this.content = params.content;
34
+ this.posts = params.posts;
35
+ this.spaces = params.spaces;
36
+ this.mentions = params.mentions;
27
37
  this.uow = params.uow;
28
38
  this.settings = params.settings;
39
+ this.fileUrls = params.fileUrls;
29
40
  this.logger = params.logger ?? noopLogger;
30
41
  }
31
42
  async resolveConfig(orgId, activityId) {
@@ -44,35 +55,27 @@ var DiscussionServiceImpl = class {
44
55
  reactions: settings.reactions
45
56
  };
46
57
  }
47
- /** Any org user that is not a learner moderates. The role arrives from
48
- * the edge already resolved; core never looks one up to authorise. */
49
58
  isStaff(actor) {
50
- return actor.role !== "student";
59
+ return isStaffRole(actor.role);
51
60
  }
52
- toAuthor(record) {
53
- return {
54
- id: record.id,
55
- firstName: record.firstName,
56
- lastName: record.lastName,
57
- image: record.image,
58
- role: record.role
59
- };
60
- }
61
- toView(comment, authors, reactions) {
61
+ toView(orgId, comment, authors, urls, reactions, mentions) {
62
62
  const author = authors[comment.orgUserId];
63
- if (!author) throw new NotFoundError("OrgUser", comment.orgUserId);
64
- if (comment.removedBy && !authors[comment.removedBy]) throw new NotFoundError("OrgUser", comment.removedBy);
63
+ if (!author) throw new NotFoundError("Member", comment.orgUserId);
64
+ if (comment.removedBy && !authors[comment.removedBy]) throw new NotFoundError("Member", comment.removedBy);
65
65
  const remover = comment.removedBy ? authors[comment.removedBy] : void 0;
66
66
  return {
67
67
  id: comment.id,
68
68
  activityId: comment.activityId,
69
+ postId: comment.postId,
70
+ communityId: comment.communityId,
69
71
  parentId: comment.parentId,
70
- author: this.toAuthor(author),
72
+ author: authorView(orgId, author, urls),
71
73
  body: comment.status === "removed" ? null : comment.body,
72
74
  status: comment.status,
73
- removedBy: remover ? this.toAuthor(remover) : null,
75
+ removedBy: remover ? authorView(orgId, remover, urls) : null,
74
76
  reactions: reactions.reactions,
75
77
  viewerReaction: reactions.viewerReaction,
78
+ mentions: comment.status === "removed" ? [] : mentions,
76
79
  createdAt: comment.createdAt,
77
80
  updatedAt: comment.updatedAt
78
81
  };
@@ -86,31 +89,75 @@ var DiscussionServiceImpl = class {
86
89
  return [...ids];
87
90
  }
88
91
  async renderOne(orgId, comment, actor) {
89
- const authors = await this.repo.authorsOf(orgId, this.peopleIn([comment]));
92
+ const [authors, mentioned] = await Promise.all([this.repo.authorsOf(orgId, this.peopleIn([comment])), mentionViews(this.mentions, this.repo, orgId, "comment", [comment.id])]);
90
93
  const stored = comment.status === "removed" ? EMPTY_REACTIONS : (await this.repo.reactionsOf(orgId, [comment.id], actor.id))[comment.id] ?? EMPTY_REACTIONS;
91
- return this.toView(comment, authors, stored);
94
+ return this.toView(orgId, comment, authors, this.fileUrls, stored, mentioned[comment.id] ?? []);
95
+ }
96
+ activityIdOf(comment) {
97
+ if (comment.activityId === null) throw new NotFoundError("Activity", comment.id);
98
+ return comment.activityId;
92
99
  }
93
100
  async hasAccess(orgId, activityId, actor) {
94
101
  if (this.isStaff(actor)) return true;
95
102
  const activity = await this.content.getActivity(orgId, activityId);
96
- return activity ? this.access.hasCourseAccess(orgId, actor.id, activity.courseId) : false;
103
+ return activity ? this.access.hasAccess(orgId, actor.id, activity.courseId) : false;
104
+ }
105
+ async requireActivityOpen(orgId, activityId, actor) {
106
+ const activity = await this.content.getActivity(orgId, activityId);
107
+ if (!activity) throw new NotFoundError("Activity", activityId);
108
+ if (!this.isStaff(actor) && !await this.access.hasAccess(orgId, actor.id, activity.courseId)) throw new ForbiddenError("You cannot open this activity");
109
+ return activity;
110
+ }
111
+ async courseOpeners(orgId, courseId, orgUserIds) {
112
+ if (orgUserIds.length === 0) return [];
113
+ const people = await this.repo.authorsOf(orgId, orgUserIds);
114
+ const opens = await Promise.all(orgUserIds.map(async (orgUserId) => {
115
+ const person = people[orgUserId];
116
+ if (!person) return false;
117
+ return isStaffRole(person.role) || this.access.hasAccess(orgId, orgUserId, courseId);
118
+ }));
119
+ return orgUserIds.filter((_, index) => opens[index]);
120
+ }
121
+ async spaceOpeners(orgId, spaceId, orgUserIds) {
122
+ if (orgUserIds.length === 0) return [];
123
+ return keepAllowed(orgUserIds, await this.spaces.spaceOpenersAmong(orgId, spaceId, orgUserIds));
124
+ }
125
+ async openersOf(orgId, comment, orgUserIds) {
126
+ if (orgUserIds.length === 0) return [];
127
+ if (comment.postId) {
128
+ const post = await this.posts.find(orgId, comment.postId);
129
+ return post ? this.spaceOpeners(orgId, post.spaceId, orgUserIds) : [];
130
+ }
131
+ const activity = await this.content.getActivity(orgId, this.activityIdOf(comment));
132
+ return activity ? this.courseOpeners(orgId, activity.courseId, orgUserIds) : [];
133
+ }
134
+ commenterIds(orgId, target) {
135
+ return this.repo.commenterIds(orgId, target);
97
136
  }
98
- async postComment(orgId, { activityId, actor, parentId, body }) {
99
- if (!await this.hasAccess(orgId, activityId, actor)) throw new ForbiddenError("no access to this activity");
137
+ async postComment(orgId, input) {
138
+ if (input.postId) return this.postOnPost(orgId, input.postId, input);
139
+ const activityId = input.activityId;
140
+ if (!activityId) throw new NotFoundError("Activity", activityId ?? "");
141
+ return this.postOnActivity(orgId, activityId, input);
142
+ }
143
+ async postOnActivity(orgId, activityId, { actor, parentId, body }) {
144
+ const activity = await this.requireActivityOpen(orgId, activityId, actor);
100
145
  const config = await this.resolveConfig(orgId, activityId);
101
- if (!config.enabled) throw new ForbiddenError("discussion is not open on this activity");
146
+ if (!config.enabled) throw new ForbiddenError("Discussion is not open on this activity");
102
147
  if (parentId !== null) {
103
- if (!config.threaded) throw new ForbiddenError("replies are disabled on this course");
148
+ if (!config.threaded) throw new ForbiddenError("Replies are off on this course");
104
149
  const parent = await this.repo.findComment(orgId, parentId);
105
150
  if (!parent || parent.activityId !== activityId) throw new NotFoundError("Comment", parentId);
106
- if (parent.parentId !== null) throw new ForbiddenError("replies nest one level");
107
- if (parent.status !== "published") throw new ForbiddenError("cannot reply to a comment that is not published");
151
+ if (parent.parentId !== null) throw new ForbiddenError("A reply cannot have replies of its own");
152
+ if (parent.status !== "published") throw new ForbiddenError("You can reply only to a published comment");
108
153
  }
109
154
  const status = config.requireReview && !this.isStaff(actor) ? "pending" : "published";
110
155
  const comment = {
111
156
  id: genId("comment"),
112
157
  orgId,
113
158
  activityId,
159
+ postId: null,
160
+ communityId: null,
114
161
  parentId,
115
162
  orgUserId: actor.id,
116
163
  body,
@@ -119,56 +166,193 @@ var DiscussionServiceImpl = class {
119
166
  createdAt: /* @__PURE__ */ new Date(),
120
167
  updatedAt: /* @__PURE__ */ new Date()
121
168
  };
169
+ const mentioned = await this.courseOpeners(orgId, activity.courseId, mentionTokens(body));
122
170
  const saved = await this.uow.run(async (scope) => {
123
171
  const row = await scope.discussion.insertComment(orgId, comment);
172
+ await scope.mentions.sync(orgId, "comment", row.id, mentioned, comment.createdAt);
124
173
  await scope.outbox.append([discussionEvents.commentCreated.make({
125
174
  orgId,
126
- data: row
175
+ data: {
176
+ ...row,
177
+ mentionedOrgUserIds: status === "published" ? mentioned : []
178
+ },
179
+ metadata: { actorOrgUserId: actor.id }
127
180
  })]);
128
181
  this.logger.info("comment created", {
129
182
  orgId,
130
183
  commentId: row.id,
131
- status
184
+ status,
185
+ actorId: actor.id
132
186
  });
133
187
  return row;
134
188
  });
135
189
  return this.renderOne(orgId, saved, actor);
136
190
  }
191
+ async postOnPost(orgId, postId, { actor, parentId, body }) {
192
+ const { post, space } = await this.visiblePost(orgId, actor, postId);
193
+ if (post.status !== "published") throw new ConflictError("This post is not published");
194
+ if (!this.commentsOpen(post, space)) throw new ForbiddenError("Comments are closed on this post");
195
+ if (parentId !== null) {
196
+ const parent = await this.repo.findComment(orgId, parentId);
197
+ if (!parent || parent.postId !== postId) throw new NotFoundError("Comment", parentId);
198
+ if (parent.parentId !== null) throw new ForbiddenError("A reply cannot have replies of its own");
199
+ if (parent.status !== "published") throw new ForbiddenError("You can reply only to a published comment");
200
+ }
201
+ const now = /* @__PURE__ */ new Date();
202
+ const comment = {
203
+ id: genId("comment"),
204
+ orgId,
205
+ activityId: null,
206
+ postId,
207
+ communityId: post.communityId,
208
+ parentId,
209
+ orgUserId: actor.id,
210
+ body,
211
+ status: "published",
212
+ removedBy: null,
213
+ createdAt: now,
214
+ updatedAt: now
215
+ };
216
+ const mentioned = await this.spaceOpeners(orgId, post.spaceId, mentionTokens(body));
217
+ const saved = await this.uow.run(async (scope) => {
218
+ const row = await scope.discussion.insertComment(orgId, comment);
219
+ await scope.mentions.sync(orgId, "comment", row.id, mentioned, now);
220
+ await scope.outbox.append([discussionEvents.commentCreated.make({
221
+ orgId,
222
+ data: {
223
+ ...row,
224
+ mentionedOrgUserIds: mentioned
225
+ },
226
+ metadata: { actorOrgUserId: actor.id }
227
+ })]);
228
+ this.logger.info("comment created", {
229
+ orgId,
230
+ commentId: row.id,
231
+ postId,
232
+ status: "published",
233
+ actorId: actor.id
234
+ });
235
+ return row;
236
+ });
237
+ return this.renderOne(orgId, saved, actor);
238
+ }
239
+ async postComments(orgId, postId, actor, query) {
240
+ const { post, space } = await this.visiblePost(orgId, actor, postId);
241
+ const seesHeld = (person) => this.canModerate(person);
242
+ const page = await this.repo.listThreadsByPost(orgId, postId, {
243
+ ...query,
244
+ viewerId: actor.id,
245
+ seesHeld: seesHeld(actor)
246
+ });
247
+ const comments = await this.thread(orgId, page.rows, actor, post.reactionsEnabled ?? space.settings.reactions, seesHeld);
248
+ this.logger.debug("post comments read", {
249
+ orgId,
250
+ postId,
251
+ count: comments.length
252
+ });
253
+ return {
254
+ ...page,
255
+ rows: comments
256
+ };
257
+ }
258
+ async thread(orgId, rows, actor, reactionsOn, seesHeld) {
259
+ const readable = rows.filter((comment) => {
260
+ if (comment.status === "published" || comment.status === "removed") return true;
261
+ return seesHeld(actor) || comment.orgUserId === actor.id;
262
+ });
263
+ const replies = readable.filter((comment) => comment.parentId !== null && comment.status !== "removed");
264
+ const heldOpen = new Set(replies.map((reply) => reply.parentId));
265
+ const roots = readable.filter((comment) => comment.parentId === null && (comment.status !== "removed" || heldOpen.has(comment.id)));
266
+ const rootIds = new Set(roots.map((root) => root.id));
267
+ const served = [...roots, ...replies.filter((reply) => rootIds.has(reply.parentId))].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
268
+ const [authors, reactions, mentioned] = await Promise.all([
269
+ this.repo.authorsOf(orgId, this.peopleIn(served)),
270
+ reactionsOn ? this.repo.reactionsOf(orgId, served.map((comment) => comment.id), actor.id) : Promise.resolve({}),
271
+ mentionViews(this.mentions, this.repo, orgId, "comment", served.map((comment) => comment.id))
272
+ ]);
273
+ const urls = this.fileUrls;
274
+ return served.map((comment) => this.toView(orgId, comment, authors, urls, reactions[comment.id] ?? EMPTY_REACTIONS, mentioned[comment.id] ?? []));
275
+ }
276
+ async visiblePost(orgId, actor, postId) {
277
+ return this.posts.visible(orgId, actor, postId);
278
+ }
279
+ async postOfComment(orgId, actor, commentId, postId) {
280
+ try {
281
+ return await this.visiblePost(orgId, actor, postId);
282
+ } catch (error) {
283
+ if (error instanceof NotFoundError) throw new NotFoundError("Comment", commentId);
284
+ throw error;
285
+ }
286
+ }
287
+ canModerate(actor) {
288
+ return canModerate(actor.role);
289
+ }
290
+ inCommunity(comment, communityId) {
291
+ return communityId === void 0 || comment.communityId === communityId;
292
+ }
293
+ commentsOpen(post, space) {
294
+ return post.commentsClosed === null ? space.settings.comments : !post.commentsClosed;
295
+ }
137
296
  async activityComments(orgId, activityId, actor) {
297
+ await this.requireActivityOpen(orgId, activityId, actor);
138
298
  const config = await this.resolveConfig(orgId, activityId);
139
299
  if (!config.enabled) return {
140
300
  config,
141
301
  comments: []
142
302
  };
143
- const readable = (await this.repo.listByActivity(orgId, activityId)).filter((c) => {
144
- if (c.status === "published" || c.status === "removed") return true;
145
- return this.isStaff(actor) || c.orgUserId === actor.id;
146
- });
147
- const replies = readable.filter((c) => c.parentId !== null && c.status !== "removed");
148
- const heldOpen = new Set(replies.map((r) => r.parentId));
149
- const roots = readable.filter((c) => c.parentId === null && (c.status !== "removed" || heldOpen.has(c.id)));
150
- const rootIds = new Set(roots.map((r) => r.id));
151
- const served = [...roots, ...replies.filter((r) => rootIds.has(r.parentId))].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
152
- const [authors, reactions] = await Promise.all([this.repo.authorsOf(orgId, this.peopleIn(served)), config.reactions ? this.repo.reactionsOf(orgId, served.map((c) => c.id), actor.id) : Promise.resolve({})]);
303
+ const rows = await this.repo.listByActivity(orgId, activityId);
153
304
  return {
154
305
  config,
155
- comments: served.map((c) => this.toView(c, authors, reactions[c.id] ?? EMPTY_REACTIONS))
306
+ comments: await this.thread(orgId, rows, actor, config.reactions, (person) => this.isStaff(person))
156
307
  };
157
308
  }
158
309
  async edit(orgId, commentId, actor, body) {
159
310
  const { comment, config } = await this.loadWithConfig(orgId, commentId, actor);
160
- if (!config.enabled) throw new ForbiddenError("discussion is not open on this activity");
161
- if (comment.orgUserId !== actor.id) throw new ForbiddenError("only the author may edit a comment");
162
- if (comment.status === "removed") throw new ForbiddenError("a removed comment cannot be edited");
163
- const updated = await this.repo.updateComment(orgId, commentId, { body });
164
- if (!updated) throw new NotFoundError("Comment", commentId);
311
+ if (!config.enabled) throw new ForbiddenError(comment.postId ? "Comments are closed on this post" : "Discussion is not open on this activity");
312
+ if (comment.orgUserId !== actor.id) throw new ForbiddenError("You can edit only your own comment");
313
+ if (comment.status === "removed") throw new ForbiddenError("A removed comment cannot be edited");
314
+ const candidates = await this.openersOf(orgId, comment, mentionTokens(body));
315
+ const updated = await this.uow.run(async (scope) => {
316
+ const next = await scope.discussion.updateComment(orgId, commentId, { body });
317
+ if (!next) throw new NotFoundError("Comment", commentId);
318
+ const added = await scope.mentions.sync(orgId, "comment", commentId, candidates, /* @__PURE__ */ new Date());
319
+ await scope.outbox.append([discussionEvents.commentUpdated.make({
320
+ orgId,
321
+ data: {
322
+ ...next,
323
+ mentionedOrgUserIds: next.status === "published" ? keepAllowed(candidates, added) : []
324
+ },
325
+ metadata: { actorOrgUserId: actor.id }
326
+ })]);
327
+ return next;
328
+ });
329
+ this.logger.info("comment edited", {
330
+ orgId,
331
+ commentId,
332
+ actorId: actor.id
333
+ });
165
334
  return this.renderOne(orgId, updated, actor);
166
335
  }
167
- async remove(orgId, commentId, actor) {
336
+ async remove(orgId, commentId, actor, communityId) {
337
+ return this.renderOne(orgId, await this.removeComment(orgId, commentId, actor, communityId), actor);
338
+ }
339
+ async removeComment(orgId, commentId, actor, communityId) {
168
340
  const comment = await this.getComment(orgId, commentId);
169
- if (!comment) throw new NotFoundError("Comment", commentId);
170
- if (!await this.hasAccess(orgId, comment.activityId, actor)) throw new ForbiddenError("no access to this activity");
171
- if (comment.orgUserId !== actor.id && !this.isStaff(actor)) throw new ForbiddenError("only the author or a moderator may remove a comment");
341
+ if (!comment || !this.inCommunity(comment, communityId)) {
342
+ this.logger.warn("comment remove rejected — not found", {
343
+ orgId,
344
+ commentId,
345
+ actorId: actor.id
346
+ });
347
+ throw new NotFoundError("Comment", commentId);
348
+ }
349
+ if (comment.postId) {
350
+ await this.postOfComment(orgId, actor, commentId, comment.postId);
351
+ if (comment.orgUserId !== actor.id && !this.canModerate(actor)) throw new ForbiddenError("You cannot remove this comment");
352
+ } else {
353
+ if (!await this.hasAccess(orgId, this.activityIdOf(comment), actor)) throw new ForbiddenError("You cannot open this activity");
354
+ if (comment.orgUserId !== actor.id && !this.isStaff(actor)) throw new ForbiddenError("You cannot remove this comment");
355
+ }
172
356
  if (comment.status === "removed") return comment;
173
357
  return this.uow.run(async (scope) => {
174
358
  const updated = await scope.discussion.updateComment(orgId, commentId, {
@@ -176,28 +360,49 @@ var DiscussionServiceImpl = class {
176
360
  removedBy: actor.id
177
361
  });
178
362
  if (!updated) throw new NotFoundError("Comment", commentId);
363
+ if (comment.orgUserId !== actor.id) await scope.discussion.resolveReportsFor(orgId, commentId, {
364
+ resolvedAt: /* @__PURE__ */ new Date(),
365
+ resolvedBy: actor.id,
366
+ resolution: "removed"
367
+ });
179
368
  await scope.outbox.append([discussionEvents.commentRemoved.make({
180
369
  orgId,
181
- data: updated
370
+ data: updated,
371
+ metadata: { actorOrgUserId: actor.id }
182
372
  })]);
183
373
  this.logger.info("comment removed", {
184
374
  orgId,
185
375
  commentId,
186
- by: actor.id
376
+ actorId: actor.id
187
377
  });
188
378
  return updated;
189
379
  });
190
380
  }
191
- async restore(orgId, commentId, actor) {
192
- if (!this.isStaff(actor)) throw new ForbiddenError("only a moderator may restore a comment");
381
+ async restore(orgId, commentId, actor, communityId) {
382
+ return this.renderOne(orgId, await this.restoreComment(orgId, commentId, actor, communityId), actor);
383
+ }
384
+ async restoreComment(orgId, commentId, actor, communityId) {
385
+ if (!this.isStaff(actor)) throw new ForbiddenError("You cannot restore this comment");
193
386
  const comment = await this.getComment(orgId, commentId);
194
- if (!comment) throw new NotFoundError("Comment", commentId);
387
+ if (!comment || !this.inCommunity(comment, communityId)) {
388
+ this.logger.warn("comment restore rejected — not found", {
389
+ orgId,
390
+ commentId,
391
+ actorId: actor.id
392
+ });
393
+ throw new NotFoundError("Comment", commentId);
394
+ }
195
395
  if (comment.status !== "removed") return comment;
196
396
  const updated = await this.repo.updateComment(orgId, commentId, {
197
397
  status: "published",
198
398
  removedBy: null
199
399
  });
200
400
  if (!updated) throw new NotFoundError("Comment", commentId);
401
+ this.logger.info("comment restored", {
402
+ orgId,
403
+ commentId,
404
+ actorId: actor.id
405
+ });
201
406
  return updated;
202
407
  }
203
408
  /** Load a comment together with its activity's resolved comments config,
@@ -206,35 +411,78 @@ var DiscussionServiceImpl = class {
206
411
  async loadWithConfig(orgId, commentId, actor) {
207
412
  const comment = await this.getComment(orgId, commentId);
208
413
  if (!comment) throw new NotFoundError("Comment", commentId);
209
- if (!await this.hasAccess(orgId, comment.activityId, actor)) throw new ForbiddenError("no access to this activity");
414
+ if (comment.postId) {
415
+ const { post, space } = await this.postOfComment(orgId, actor, commentId, comment.postId);
416
+ return {
417
+ comment,
418
+ config: {
419
+ enabled: this.commentsOpen(post, space),
420
+ threaded: true,
421
+ requireReview: false,
422
+ reactions: post.reactionsEnabled ?? space.settings.reactions
423
+ },
424
+ spaceId: post.spaceId
425
+ };
426
+ }
427
+ const activityId = this.activityIdOf(comment);
428
+ if (!await this.hasAccess(orgId, activityId, actor)) throw new ForbiddenError("You cannot open this activity");
210
429
  return {
211
430
  comment,
212
- config: await this.resolveConfig(orgId, comment.activityId)
431
+ config: await this.resolveConfig(orgId, activityId),
432
+ spaceId: null
213
433
  };
214
434
  }
215
435
  async setReaction(orgId, commentId, actor, emoji) {
216
- const { comment, config } = await this.loadWithConfig(orgId, commentId, actor);
217
- if (!config.enabled) throw new ForbiddenError("discussion is not open on this activity");
218
- if (!config.reactions && emoji !== null) throw new ForbiddenError("reactions are disabled on this course");
219
- if (comment.status === "removed") throw new ForbiddenError("a removed comment cannot be reacted to");
220
- await this.repo.setReaction(orgId, commentId, actor.id, emoji);
436
+ const { comment, config, spaceId } = await this.loadWithConfig(orgId, commentId, actor);
437
+ if (!config.enabled && !comment.postId) throw new ForbiddenError("Discussion is not open on this activity");
438
+ if (!config.reactions && emoji !== null) throw new ForbiddenError(comment.postId ? "Reactions are off on this post" : "Reactions are off on this course");
439
+ if (comment.status === "removed") throw new ForbiddenError("A removed comment cannot be reacted to");
440
+ await this.uow.run(async (scope) => {
441
+ if (!await scope.discussion.setReaction(orgId, commentId, actor.id, emoji) || emoji === null) return;
442
+ await scope.outbox.append([discussionEvents.commentReactionAdded.make({
443
+ orgId,
444
+ data: {
445
+ commentId,
446
+ postId: comment.postId,
447
+ activityId: comment.activityId,
448
+ spaceId,
449
+ communityId: comment.communityId,
450
+ orgUserId: actor.id,
451
+ reaction: emoji
452
+ },
453
+ metadata: { actorOrgUserId: actor.id }
454
+ })]);
455
+ });
221
456
  return (await this.repo.reactionsOf(orgId, [commentId], actor.id))[commentId] ?? EMPTY_REACTIONS;
222
457
  }
223
458
  async approve(orgId, commentId, actor) {
224
- if (!this.isStaff(actor)) throw new ForbiddenError("only a moderator may approve a comment");
459
+ if (!this.isStaff(actor)) throw new ForbiddenError("You cannot approve this comment");
225
460
  const comment = await this.getComment(orgId, commentId);
226
- if (!comment) throw new NotFoundError("Comment", commentId);
227
- if (comment.status !== "pending") throw new ForbiddenError("only a pending comment can be approved");
461
+ if (!comment) {
462
+ this.logger.warn("comment approve rejected — not found", {
463
+ orgId,
464
+ commentId,
465
+ actorId: actor.id
466
+ });
467
+ throw new NotFoundError("Comment", commentId);
468
+ }
469
+ if (comment.status !== "pending") throw new ForbiddenError("Only a comment awaiting review can be approved");
228
470
  return this.uow.run(async (scope) => {
229
471
  const updated = await scope.discussion.updateComment(orgId, commentId, { status: "published" });
230
472
  if (!updated) throw new NotFoundError("Comment", commentId);
473
+ const mentioned = await this.openersOf(orgId, updated, await scope.mentions.ofItem(orgId, "comment", commentId));
231
474
  await scope.outbox.append([discussionEvents.commentPublished.make({
232
475
  orgId,
233
- data: updated
476
+ data: {
477
+ ...updated,
478
+ mentionedOrgUserIds: mentioned
479
+ },
480
+ metadata: { actorOrgUserId: actor.id }
234
481
  })]);
235
482
  this.logger.info("comment published", {
236
483
  orgId,
237
- commentId
484
+ commentId,
485
+ actorId: actor.id
238
486
  });
239
487
  return updated;
240
488
  });
@@ -243,26 +491,49 @@ var DiscussionServiceImpl = class {
243
491
  * caller must not decide that itself. `approve` and `restore` still enforce
244
492
  * their own staff check; the check here covers the third path, where a
245
493
  * comment is already published and neither of them would run. */
246
- async publish(orgId, commentId, actor) {
247
- if (!this.isStaff(actor)) throw new ForbiddenError("only a moderator may publish a comment");
494
+ async publish(orgId, commentId, actor, communityId) {
495
+ if (!this.isStaff(actor)) throw new ForbiddenError("You cannot publish this comment");
248
496
  const comment = await this.getComment(orgId, commentId);
249
- if (!comment) throw new NotFoundError("Comment", commentId);
497
+ if (!comment || !this.inCommunity(comment, communityId)) {
498
+ this.logger.warn("comment publish rejected — not found", {
499
+ orgId,
500
+ commentId,
501
+ actorId: actor.id
502
+ });
503
+ throw new NotFoundError("Comment", commentId);
504
+ }
250
505
  if (comment.status === "pending") return this.approve(orgId, commentId, actor);
251
- if (comment.status === "removed") return this.restore(orgId, commentId, actor);
506
+ if (comment.status === "removed") return this.restoreComment(orgId, commentId, actor);
252
507
  return comment;
253
508
  }
254
- async resolveReports(orgId, commentId, actor) {
255
- if (!this.isStaff(actor)) throw new ForbiddenError("only a moderator may resolve a report");
256
- if (!await this.getComment(orgId, commentId)) throw new NotFoundError("Comment", commentId);
257
- await this.repo.resolveReportsFor(orgId, commentId, /* @__PURE__ */ new Date());
509
+ async resolveReports(orgId, commentId, actor, communityId) {
510
+ const comment = await this.getComment(orgId, commentId);
511
+ if (!comment || !this.inCommunity(comment, communityId)) {
512
+ this.logger.warn("comment reports resolve rejected — not found", {
513
+ orgId,
514
+ commentId,
515
+ actorId: actor.id
516
+ });
517
+ throw new NotFoundError("Comment", commentId);
518
+ }
519
+ if (comment.postId) {
520
+ await this.postOfComment(orgId, actor, commentId, comment.postId);
521
+ if (!this.canModerate(actor)) throw new ForbiddenError("You cannot work this queue");
522
+ } else if (!this.isStaff(actor)) throw new ForbiddenError("You cannot resolve this report");
523
+ await this.repo.resolveReportsFor(orgId, commentId, {
524
+ resolvedAt: /* @__PURE__ */ new Date(),
525
+ resolvedBy: actor.id,
526
+ resolution: "dismissed"
527
+ });
258
528
  this.logger.info("comment reports resolved", {
259
529
  orgId,
260
- commentId
530
+ commentId,
531
+ actorId: actor.id
261
532
  });
262
533
  }
263
534
  async reportComment(orgId, commentId, actor, reason) {
264
- const { config } = await this.loadWithConfig(orgId, commentId, actor);
265
- if (!config.enabled) throw new ForbiddenError("discussion is not open on this activity");
535
+ const { comment, config } = await this.loadWithConfig(orgId, commentId, actor);
536
+ if (!config.enabled && !comment.postId) throw new ForbiddenError("Discussion is not open on this activity");
266
537
  const now = /* @__PURE__ */ new Date();
267
538
  const report = {
268
539
  id: genId("commentReport"),
@@ -271,6 +542,8 @@ var DiscussionServiceImpl = class {
271
542
  orgUserId: actor.id,
272
543
  reason,
273
544
  resolvedAt: null,
545
+ resolution: null,
546
+ resolvedBy: null,
274
547
  createdAt: now,
275
548
  updatedAt: now
276
549
  };
@@ -283,11 +556,16 @@ var DiscussionServiceImpl = class {
283
556
  }
284
557
  await scope.outbox.append([discussionEvents.commentReported.make({
285
558
  orgId,
286
- data: saved
559
+ data: {
560
+ ...saved,
561
+ communityId: comment.communityId
562
+ },
563
+ metadata: { actorOrgUserId: actor.id }
287
564
  })]);
288
565
  this.logger.info("comment reported", {
289
566
  orgId,
290
- commentId
567
+ commentId,
568
+ actorId: actor.id
291
569
  });
292
570
  return saved;
293
571
  });
@@ -301,29 +579,30 @@ var DiscussionServiceImpl = class {
301
579
  };
302
580
  const reports = await this.repo.listOpenReports(orgId, rows.map((c) => c.id));
303
581
  const authors = await this.repo.authorsOf(orgId, [.../* @__PURE__ */ new Set([...this.peopleIn(rows), ...reports.map((r) => r.orgUserId)])]);
582
+ const urls = this.fileUrls;
304
583
  const byComment = /* @__PURE__ */ new Map();
305
584
  for (const report of reports) byComment.set(report.commentId, [...byComment.get(report.commentId) ?? [], report]);
306
585
  return {
307
586
  ...page,
308
587
  rows: page.rows.map(({ comment, courseId, activityTitle }) => {
309
588
  const author = authors[comment.orgUserId];
310
- if (!author) throw new NotFoundError("OrgUser", comment.orgUserId);
589
+ if (!author) throw new NotFoundError("Member", comment.orgUserId);
311
590
  return {
312
591
  id: comment.id,
313
592
  parentId: comment.parentId,
314
- activityId: comment.activityId,
593
+ activityId: this.activityIdOf(comment),
315
594
  activityTitle,
316
595
  courseId,
317
596
  body: comment.body,
318
597
  status: comment.status,
319
- author: this.toAuthor(author),
598
+ author: authorView(orgId, author, urls),
320
599
  authorEmail: author.email,
321
600
  removedBy: comment.removedBy,
322
601
  reports: (byComment.get(comment.id) ?? []).map((r) => {
323
602
  const reporter = authors[r.orgUserId];
324
- if (!reporter) throw new NotFoundError("OrgUser", r.orgUserId);
603
+ if (!reporter) throw new NotFoundError("Member", r.orgUserId);
325
604
  return {
326
- reporter: this.toAuthor(reporter),
605
+ reporter: authorView(orgId, reporter, urls),
327
606
  reason: r.reason,
328
607
  createdAt: r.createdAt
329
608
  };