@headless-lms/core 0.2.0 → 0.3.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 (225) hide show
  1. package/dist/assets/events.d.ts +39 -21
  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 +5 -4
  8. package/dist/assets/index.js +3 -2
  9. package/dist/assets/model.d.ts +2 -2
  10. package/dist/assets/ports.d.ts +45 -15
  11. package/dist/assets/service.d.ts +39 -7
  12. package/dist/assets/service.js +291 -38
  13. package/dist/assets/service.js.map +1 -1
  14. package/dist/automations/actions.d.ts +13 -24
  15. package/dist/automations/actions.js +168 -43
  16. package/dist/automations/actions.js.map +1 -1
  17. package/dist/automations/catalog.d.ts +2 -1
  18. package/dist/automations/catalog.js +229 -13
  19. package/dist/automations/catalog.js.map +1 -1
  20. package/dist/automations/community-derivations.d.ts +14 -0
  21. package/dist/automations/community-derivations.js +439 -0
  22. package/dist/automations/community-derivations.js.map +1 -0
  23. package/dist/automations/community-mail.d.ts +112 -0
  24. package/dist/automations/community-mail.js +90 -0
  25. package/dist/automations/community-mail.js.map +1 -0
  26. package/dist/automations/community-notifications.d.ts +16 -0
  27. package/dist/automations/community-notifications.js +84 -0
  28. package/dist/automations/community-notifications.js.map +1 -0
  29. package/dist/automations/events.d.ts +28 -0
  30. package/dist/automations/index.d.ts +4 -2
  31. package/dist/automations/index.js +3 -2
  32. package/dist/automations/model.d.ts +10 -3
  33. package/dist/automations/model.js +15 -2
  34. package/dist/automations/model.js.map +1 -1
  35. package/dist/automations/ports.d.ts +5 -1
  36. package/dist/automations/schedule.d.ts +6 -0
  37. package/dist/automations/schedule.js +41 -0
  38. package/dist/automations/schedule.js.map +1 -0
  39. package/dist/automations/seeds.d.ts +17 -0
  40. package/dist/automations/seeds.js +235 -0
  41. package/dist/automations/seeds.js.map +1 -0
  42. package/dist/automations/service.d.ts +22 -3
  43. package/dist/automations/service.js +306 -43
  44. package/dist/automations/service.js.map +1 -1
  45. package/dist/content/events.d.ts +631 -19
  46. package/dist/content/events.js +44 -1
  47. package/dist/content/events.js.map +1 -1
  48. package/dist/content/index.d.ts +4 -4
  49. package/dist/content/model.d.ts +2 -2
  50. package/dist/content/ports.d.ts +71 -18
  51. package/dist/content/service.d.ts +57 -18
  52. package/dist/content/service.js +502 -23
  53. package/dist/content/service.js.map +1 -1
  54. package/dist/content/types.d.ts +2 -2
  55. package/dist/discussion/authors.d.ts +10 -0
  56. package/dist/discussion/authors.js +23 -0
  57. package/dist/discussion/authors.js.map +1 -0
  58. package/dist/discussion/chats.d.ts +104 -0
  59. package/dist/discussion/chats.js +798 -0
  60. package/dist/discussion/chats.js.map +1 -0
  61. package/dist/discussion/events.d.ts +619 -56
  62. package/dist/discussion/events.js +106 -3
  63. package/dist/discussion/events.js.map +1 -1
  64. package/dist/discussion/index.d.ts +7 -3
  65. package/dist/discussion/index.js +4 -1
  66. package/dist/discussion/ports.d.ts +288 -27
  67. package/dist/discussion/ports.js +6 -1
  68. package/dist/discussion/ports.js.map +1 -0
  69. package/dist/discussion/posts.d.ts +103 -0
  70. package/dist/discussion/posts.js +723 -0
  71. package/dist/discussion/posts.js.map +1 -0
  72. package/dist/discussion/service.d.ts +32 -10
  73. package/dist/discussion/service.js +293 -78
  74. package/dist/discussion/service.js.map +1 -1
  75. package/dist/entitlements/events.d.ts +178 -11
  76. package/dist/entitlements/events.js +28 -1
  77. package/dist/entitlements/events.js.map +1 -1
  78. package/dist/entitlements/index.d.ts +5 -4
  79. package/dist/entitlements/index.js +2 -1
  80. package/dist/entitlements/model.d.ts +3 -2
  81. package/dist/entitlements/ports.d.ts +124 -11
  82. package/dist/entitlements/rules.d.ts +12 -0
  83. package/dist/entitlements/rules.js +17 -0
  84. package/dist/entitlements/rules.js.map +1 -0
  85. package/dist/entitlements/service.d.ts +54 -3
  86. package/dist/entitlements/service.js +472 -2
  87. package/dist/entitlements/service.js.map +1 -1
  88. package/dist/identity/access.d.ts +6 -0
  89. package/dist/identity/access.js +7 -0
  90. package/dist/identity/access.js.map +1 -0
  91. package/dist/identity/index.d.ts +6 -4
  92. package/dist/identity/index.js +3 -1
  93. package/dist/identity/model.d.ts +2 -1
  94. package/dist/identity/notifications.d.ts +27 -0
  95. package/dist/identity/notifications.js +101 -0
  96. package/dist/identity/notifications.js.map +1 -0
  97. package/dist/identity/ports.d.ts +73 -3
  98. package/dist/identity/service.d.ts +14 -3
  99. package/dist/identity/service.js +62 -5
  100. package/dist/identity/service.js.map +1 -1
  101. package/dist/integrations/model.js +1 -1
  102. package/dist/integrations/model.js.map +1 -1
  103. package/dist/integrations/service.js +2 -2
  104. package/dist/integrations/service.js.map +1 -1
  105. package/dist/organizations/access-scopes.d.ts +19 -0
  106. package/dist/organizations/access-scopes.js +89 -0
  107. package/dist/organizations/access-scopes.js.map +1 -0
  108. package/dist/organizations/events.d.ts +74 -32
  109. package/dist/organizations/events.js +6 -0
  110. package/dist/organizations/events.js.map +1 -1
  111. package/dist/organizations/index.d.ts +7 -6
  112. package/dist/organizations/index.js +4 -3
  113. package/dist/organizations/members.d.ts +8 -5
  114. package/dist/organizations/members.js.map +1 -1
  115. package/dist/organizations/ports.d.ts +14 -11
  116. package/dist/organizations/roles.d.ts +6 -10
  117. package/dist/organizations/roles.js +19 -7
  118. package/dist/organizations/roles.js.map +1 -1
  119. package/dist/organizations/service.d.ts +20 -11
  120. package/dist/organizations/service.js +157 -47
  121. package/dist/organizations/service.js.map +1 -1
  122. package/dist/organizations/types.d.ts +1 -10
  123. package/dist/progress/index.d.ts +2 -2
  124. package/dist/progress/ports.d.ts +6 -1
  125. package/dist/progress/service.d.ts +3 -1
  126. package/dist/progress/service.js +6 -2
  127. package/dist/progress/service.js.map +1 -1
  128. package/dist/reporting/admin/index.d.ts +4 -0
  129. package/dist/reporting/admin/index.js +2 -0
  130. package/dist/reporting/admin/model.d.ts +108 -0
  131. package/dist/reporting/admin/ports.d.ts +27 -0
  132. package/dist/reporting/admin/service.d.ts +22 -0
  133. package/dist/reporting/admin/service.js +38 -0
  134. package/dist/reporting/admin/service.js.map +1 -0
  135. package/dist/reporting/member/index.d.ts +5 -0
  136. package/dist/reporting/member/index.js +2 -0
  137. package/dist/reporting/member/model.d.ts +124 -0
  138. package/dist/reporting/member/ports.d.ts +45 -0
  139. package/dist/reporting/member/service.d.ts +73 -0
  140. package/dist/reporting/member/service.js +286 -0
  141. package/dist/reporting/member/service.js.map +1 -0
  142. package/dist/shared/actor.d.ts +9 -0
  143. package/dist/shared/errors.d.ts +7 -1
  144. package/dist/shared/errors.js +10 -1
  145. package/dist/shared/errors.js.map +1 -1
  146. package/dist/shared/id.d.ts +11 -0
  147. package/dist/shared/id.js +12 -1
  148. package/dist/shared/id.js.map +1 -1
  149. package/dist/shared/mailer.d.ts +4 -1
  150. package/dist/shared/mailer.js.map +1 -1
  151. package/dist/shared/network.d.ts +10 -0
  152. package/dist/shared/network.js +211 -0
  153. package/dist/shared/network.js.map +1 -0
  154. package/dist/types/assets.d.ts +2 -2
  155. package/dist/types/automations.d.ts +10 -2
  156. package/dist/types/content.d.ts +2 -2
  157. package/dist/types/discussion.d.ts +11 -3
  158. package/dist/types/email-templates.d.ts +121 -0
  159. package/dist/types/entitlements.d.ts +2 -2
  160. package/dist/types/identity.d.ts +48 -2
  161. package/dist/types/index.d.ts +9 -9
  162. package/dist/types/ports.d.ts +6 -0
  163. package/dist/types/schemas/assets.d.ts +65 -37
  164. package/dist/types/schemas/assets.js +17 -12
  165. package/dist/types/schemas/assets.js.map +1 -1
  166. package/dist/types/schemas/automations.d.ts +18 -1
  167. package/dist/types/schemas/automations.js +15 -6
  168. package/dist/types/schemas/automations.js.map +1 -1
  169. package/dist/types/schemas/content.d.ts +335 -3
  170. package/dist/types/schemas/content.js +139 -3
  171. package/dist/types/schemas/content.js.map +1 -1
  172. package/dist/types/schemas/discussion.d.ts +370 -3
  173. package/dist/types/schemas/discussion.js +199 -4
  174. package/dist/types/schemas/discussion.js.map +1 -1
  175. package/dist/types/schemas/entitlements.d.ts +36 -2
  176. package/dist/types/schemas/entitlements.js +23 -1
  177. package/dist/types/schemas/entitlements.js.map +1 -1
  178. package/dist/types/schemas/identity.d.ts +111 -1
  179. package/dist/types/schemas/identity.js +55 -2
  180. package/dist/types/schemas/identity.js.map +1 -1
  181. package/dist/types/schemas/index.d.ts +8 -8
  182. package/dist/types/schemas/index.js +8 -8
  183. package/dist/types/schemas/organizations.d.ts +8 -0
  184. package/dist/types/schemas/organizations.js +2 -0
  185. package/dist/types/schemas/organizations.js.map +1 -1
  186. package/dist/types/schemas/shared.d.ts +3 -1
  187. package/dist/types/schemas/shared.js +5 -1
  188. package/dist/types/schemas/shared.js.map +1 -1
  189. package/package.json +2 -2
  190. package/dist/reporting/courses/index.d.ts +0 -4
  191. package/dist/reporting/courses/index.js +0 -2
  192. package/dist/reporting/courses/model.d.ts +0 -24
  193. package/dist/reporting/courses/ports.d.ts +0 -13
  194. package/dist/reporting/courses/service.d.ts +0 -19
  195. package/dist/reporting/courses/service.js +0 -20
  196. package/dist/reporting/courses/service.js.map +0 -1
  197. package/dist/reporting/dashboard/index.d.ts +0 -4
  198. package/dist/reporting/dashboard/index.js +0 -2
  199. package/dist/reporting/dashboard/model.d.ts +0 -14
  200. package/dist/reporting/dashboard/ports.d.ts +0 -13
  201. package/dist/reporting/dashboard/service.d.ts +0 -19
  202. package/dist/reporting/dashboard/service.js +0 -20
  203. package/dist/reporting/dashboard/service.js.map +0 -1
  204. package/dist/reporting/learn/index.d.ts +0 -5
  205. package/dist/reporting/learn/index.js +0 -2
  206. package/dist/reporting/learn/model.d.ts +0 -23
  207. package/dist/reporting/learn/ports.d.ts +0 -43
  208. package/dist/reporting/learn/ports.js +0 -1
  209. package/dist/reporting/learn/service.d.ts +0 -49
  210. package/dist/reporting/learn/service.js +0 -116
  211. package/dist/reporting/learn/service.js.map +0 -1
  212. package/dist/reporting/students/index.d.ts +0 -4
  213. package/dist/reporting/students/index.js +0 -2
  214. package/dist/reporting/students/model.d.ts +0 -51
  215. package/dist/reporting/students/model.js +0 -1
  216. package/dist/reporting/students/ports.d.ts +0 -15
  217. package/dist/reporting/students/ports.js +0 -1
  218. package/dist/reporting/students/service.d.ts +0 -20
  219. package/dist/reporting/students/service.js +0 -23
  220. package/dist/reporting/students/service.js.map +0 -1
  221. /package/dist/reporting/{courses → admin}/model.js +0 -0
  222. /package/dist/reporting/{courses → admin}/ports.js +0 -0
  223. /package/dist/reporting/{dashboard → member}/model.js +0 -0
  224. /package/dist/reporting/{dashboard → member}/ports.js +0 -0
  225. /package/dist/{reporting/learn/model.js → shared/actor.js} +0 -0
@@ -0,0 +1,798 @@
1
+ import { genId } from "../shared/id.js";
2
+ import { ConflictError, ForbiddenError, InvalidInputError, NotFoundError } from "../shared/errors.js";
3
+ import { noopLogger } from "../shared/logger.js";
4
+ import { isStaffRole } from "../organizations/roles.js";
5
+ import { discussionEvents } from "./events.js";
6
+ import { authorView } from "./authors.js";
7
+ import "./ports.js";
8
+ //#region src/discussion/chats.ts
9
+ var ChatServiceImpl = class {
10
+ repo;
11
+ access;
12
+ content;
13
+ authors;
14
+ profiles;
15
+ images;
16
+ fileUrls;
17
+ uow;
18
+ logger;
19
+ constructor(params) {
20
+ this.repo = params.repo;
21
+ this.access = params.access;
22
+ this.content = params.content;
23
+ this.authors = params.authors;
24
+ this.profiles = params.profiles;
25
+ this.images = params.images;
26
+ this.fileUrls = params.fileUrls;
27
+ this.uow = params.uow;
28
+ this.logger = params.logger ?? noopLogger;
29
+ }
30
+ async startDirect(orgId, actor, input) {
31
+ if (actor.id === input.orgUserId) throw new InvalidInputError("You cannot message yourself");
32
+ if (input.body === null && input.assetIds.length > 0) throw new InvalidInputError("An attachment needs a message");
33
+ const settings = await this.requireSettings(orgId, input.communityId);
34
+ await this.requireActor(orgId, actor, input.communityId);
35
+ const role = await this.requirePerson(orgId, input.communityId, input.orgUserId);
36
+ this.assertMayMessage(settings, actor, role);
37
+ const first = input.body === null ? null : {
38
+ body: input.body,
39
+ replyToId: null,
40
+ assetIds: input.assetIds
41
+ };
42
+ const saved = await this.uow.run(async ({ chats, outbox }) => {
43
+ await chats.lockDirectPair(orgId, input.communityId, actor.id, input.orgUserId);
44
+ const existing = await chats.findDirect(orgId, input.communityId, actor.id, input.orgUserId);
45
+ if (existing) {
46
+ const users = await chats.participants(orgId, existing.id);
47
+ const rejoin = users.some((user) => user.orgUserId === actor.id && user.leftAt !== null);
48
+ const present = users.map((user) => user.orgUserId === actor.id ? {
49
+ ...user,
50
+ leftAt: null
51
+ } : user);
52
+ if (first) await this.assertMaySend(chats, orgId, actor, existing, present, settings, first);
53
+ if (rejoin) await chats.join(orgId, this.memberRow(orgId, existing.communityId, existing.id, actor.id, /* @__PURE__ */ new Date()));
54
+ const written = first ? await this.writeMessage(chats, orgId, actor, existing, first) : null;
55
+ const chat = written?.chat ?? existing;
56
+ const events = [];
57
+ if (rejoin || written?.opened) events.push(this.updatedEvent(orgId, actor, chat, present));
58
+ if (written) events.push(this.sentEvent(orgId, actor, written.chat, written.message));
59
+ if (events.length > 0) await outbox.append(events);
60
+ return {
61
+ chat,
62
+ created: false
63
+ };
64
+ }
65
+ await this.requireOwnedImages(orgId, actor.id, input.assetIds);
66
+ if (await chats.isBlocked(orgId, input.communityId, actor.id, input.orgUserId)) throw new ForbiddenError("You cannot message this person");
67
+ const request = settings.firstMessageIsRequest && !isStaffRole(actor.role) && !isStaffRole(role);
68
+ if (request && !first) throw new InvalidInputError("A message request needs a message");
69
+ const now = /* @__PURE__ */ new Date();
70
+ const chat = await chats.insertChat(orgId, {
71
+ orgId,
72
+ id: genId("chat"),
73
+ communityId: input.communityId,
74
+ kind: "direct",
75
+ status: request ? "pending" : "open",
76
+ createdBy: actor.id,
77
+ lockedAt: null,
78
+ lockedBy: null,
79
+ createdAt: now,
80
+ updatedAt: now
81
+ });
82
+ const users = [actor.id, input.orgUserId].map((orgUserId) => this.memberRow(orgId, chat.communityId, chat.id, orgUserId, now));
83
+ for (const user of users) await chats.join(orgId, user);
84
+ const events = [this.createdEvent(orgId, actor, chat, users)];
85
+ if (first) {
86
+ const written = await this.writeMessage(chats, orgId, actor, chat, first);
87
+ events.push(this.sentEvent(orgId, actor, written.chat, written.message));
88
+ }
89
+ await outbox.append(events);
90
+ return {
91
+ chat,
92
+ created: true
93
+ };
94
+ });
95
+ if (saved.created) this.logger.info("direct chat started", {
96
+ orgId,
97
+ communityId: input.communityId,
98
+ chatId: saved.chat.id,
99
+ status: saved.chat.status,
100
+ actorId: actor.id
101
+ });
102
+ else this.logger.debug("direct chat already exists", {
103
+ orgId,
104
+ communityId: input.communityId,
105
+ chatId: saved.chat.id
106
+ });
107
+ return saved.chat;
108
+ }
109
+ async startGroup(orgId, actor, input) {
110
+ const settings = await this.requireSettings(orgId, input.communityId);
111
+ this.assertGroup(settings);
112
+ await this.requireActor(orgId, actor, input.communityId);
113
+ const others = [...new Set(input.orgUserIds.filter((id) => id !== actor.id))].sort();
114
+ if (others.length < 2) throw new InvalidInputError("A group chat needs three people");
115
+ if (others.length + 1 > settings.groupSize) throw new ConflictError("This group is full");
116
+ for (const orgUserId of others) {
117
+ const role = await this.requirePerson(orgId, input.communityId, orgUserId);
118
+ this.assertMayMessage(settings, actor, role);
119
+ }
120
+ const saved = await this.uow.run(async ({ chats, outbox }) => {
121
+ if (await chats.anyBlockedAmong(orgId, input.communityId, [actor.id, ...others])) throw new ForbiddenError("You cannot message this person");
122
+ const now = /* @__PURE__ */ new Date();
123
+ const chat = await chats.insertChat(orgId, {
124
+ orgId,
125
+ id: genId("chat"),
126
+ communityId: input.communityId,
127
+ kind: "group",
128
+ status: "open",
129
+ createdBy: actor.id,
130
+ lockedAt: null,
131
+ lockedBy: null,
132
+ createdAt: now,
133
+ updatedAt: now
134
+ });
135
+ const users = [actor.id, ...others].map((orgUserId) => this.memberRow(orgId, chat.communityId, chat.id, orgUserId, now));
136
+ for (const user of users) await chats.join(orgId, user);
137
+ const events = [this.createdEvent(orgId, actor, chat, users), ...others.map((orgUserId) => this.addedEvent(orgId, actor, chat, orgUserId))];
138
+ await outbox.append(events);
139
+ return chat;
140
+ });
141
+ this.logger.info("group chat started", {
142
+ orgId,
143
+ communityId: input.communityId,
144
+ chatId: saved.id,
145
+ actorId: actor.id
146
+ });
147
+ return saved;
148
+ }
149
+ async addParticipant(orgId, actor, chatId, orgUserId) {
150
+ const chat = await this.mustChat(this.repo, orgId, chatId);
151
+ await this.requireActor(orgId, actor, chat.communityId);
152
+ await this.requireActive(this.repo, orgId, chat.id, actor.id);
153
+ if (chat.kind !== "group") throw new ConflictError("This chat is not a group");
154
+ const settings = await this.requireSettings(orgId, chat.communityId);
155
+ this.assertGroup(settings);
156
+ const role = await this.requirePerson(orgId, chat.communityId, orgUserId);
157
+ this.assertMayMessage(settings, actor, role);
158
+ const saved = await this.uow.run(async ({ chats, outbox }) => {
159
+ const current = await this.mustChat(chats, orgId, chatId);
160
+ const users = await chats.participants(orgId, chatId);
161
+ this.assertActive(users, actor.id);
162
+ const existing = users.find((user) => user.orgUserId === orgUserId);
163
+ if (existing && existing.leftAt === null) throw new ConflictError("That person is already in this chat");
164
+ const active = users.filter((user) => user.leftAt === null);
165
+ if (active.length + 1 > settings.groupSize) throw new ConflictError("This group is full");
166
+ if ((await chats.blockedWith(orgId, current.communityId, orgUserId, active.map((user) => user.orgUserId))).length > 0) throw new ForbiddenError("You cannot message this person");
167
+ const now = /* @__PURE__ */ new Date();
168
+ await chats.join(orgId, this.memberRow(orgId, current.communityId, current.id, orgUserId, now));
169
+ await outbox.append([this.addedEvent(orgId, actor, current, orgUserId)]);
170
+ return current;
171
+ });
172
+ this.logger.info("chat participant added", {
173
+ orgId,
174
+ chatId,
175
+ orgUserId,
176
+ actorId: actor.id
177
+ });
178
+ return saved;
179
+ }
180
+ async leave(orgId, actor, chatId) {
181
+ const chat = await this.mustChat(this.repo, orgId, chatId);
182
+ await this.requireActor(orgId, actor, chat.communityId);
183
+ await this.requireActive(this.repo, orgId, chat.id, actor.id);
184
+ const saved = await this.uow.run(async ({ chats, outbox }) => {
185
+ const current = await this.mustChat(chats, orgId, chatId);
186
+ const users = await chats.participants(orgId, chatId);
187
+ this.assertActive(users, actor.id);
188
+ const now = /* @__PURE__ */ new Date();
189
+ await chats.markLeft(orgId, chatId, actor.id, now);
190
+ const remaining = users.map((user) => user.orgUserId === actor.id ? {
191
+ ...user,
192
+ leftAt: now
193
+ } : user);
194
+ await outbox.append([this.updatedEvent(orgId, actor, current, remaining)]);
195
+ return current;
196
+ });
197
+ this.logger.info("chat left", {
198
+ orgId,
199
+ chatId,
200
+ actorId: actor.id
201
+ });
202
+ return saved;
203
+ }
204
+ async accept(orgId, actor, chatId) {
205
+ const chat = await this.mustChat(this.repo, orgId, chatId);
206
+ await this.requireActor(orgId, actor, chat.communityId);
207
+ await this.requireActive(this.repo, orgId, chat.id, actor.id);
208
+ if (chat.createdBy === actor.id) throw new ForbiddenError("You cannot accept this request");
209
+ if (chat.kind !== "direct" || chat.status !== "pending") throw new ConflictError("This chat is not a request");
210
+ const saved = await this.uow.run(async ({ chats, outbox }) => {
211
+ const current = await this.mustChat(chats, orgId, chatId);
212
+ if (current.kind !== "direct" || current.status !== "pending") throw new ConflictError("This chat is not a request");
213
+ const next = await this.mustUpdate(chats, orgId, chatId, { status: "open" });
214
+ const users = await chats.participants(orgId, chatId);
215
+ await outbox.append([this.updatedEvent(orgId, actor, next, users)]);
216
+ return next;
217
+ });
218
+ this.logger.info("chat accepted", {
219
+ orgId,
220
+ chatId,
221
+ actorId: actor.id
222
+ });
223
+ return saved;
224
+ }
225
+ async send(orgId, actor, chatId, input) {
226
+ const preview = await this.mustChat(this.repo, orgId, chatId);
227
+ await this.requireActor(orgId, actor, preview.communityId);
228
+ const settings = await this.requireSettings(orgId, preview.communityId);
229
+ if (!settings.enabled) throw new ForbiddenError("Direct messages are off");
230
+ const saved = await this.uow.run(async ({ chats, outbox }) => {
231
+ const chat = await this.mustChat(chats, orgId, chatId);
232
+ const users = await chats.participants(orgId, chatId);
233
+ await this.assertMaySend(chats, orgId, actor, chat, users, settings, input);
234
+ const written = await this.writeMessage(chats, orgId, actor, chat, input);
235
+ const events = [];
236
+ if (written.opened) events.push(this.updatedEvent(orgId, actor, written.chat, users));
237
+ events.push(this.sentEvent(orgId, actor, written.chat, written.message));
238
+ await outbox.append(events);
239
+ return written.message;
240
+ });
241
+ this.logger.info("message sent", {
242
+ orgId,
243
+ chatId,
244
+ messageId: saved.id,
245
+ actorId: actor.id
246
+ });
247
+ return this.viewOf(orgId, actor, saved);
248
+ }
249
+ async editMessage(orgId, actor, messageId, body) {
250
+ const message = await this.mustMessage(this.repo, orgId, messageId);
251
+ await this.requireWritableChat(orgId, actor, message.chatId);
252
+ if (message.orgUserId !== actor.id) throw new ForbiddenError("You can change only your own message");
253
+ if (message.deletedAt) throw new ConflictError("This message was deleted");
254
+ if (message.body === body) return this.viewOf(orgId, actor, message);
255
+ const saved = await this.repo.updateMessage(orgId, messageId, {
256
+ body,
257
+ editedAt: /* @__PURE__ */ new Date()
258
+ });
259
+ if (!saved) throw new NotFoundError("Message", messageId);
260
+ this.logger.info("message edited", {
261
+ orgId,
262
+ messageId,
263
+ actorId: actor.id
264
+ });
265
+ return this.viewOf(orgId, actor, saved);
266
+ }
267
+ async deleteMessage(orgId, actor, messageId) {
268
+ const message = await this.mustMessage(this.repo, orgId, messageId);
269
+ await this.requireActor(orgId, actor, message.communityId);
270
+ if (!isStaffRole(actor.role)) {
271
+ if (message.orgUserId !== actor.id) throw new ForbiddenError("You cannot delete this message");
272
+ await this.requireWritableChat(orgId, actor, message.chatId);
273
+ }
274
+ if (message.deletedAt) throw new ConflictError("This message was deleted");
275
+ const now = /* @__PURE__ */ new Date();
276
+ const saved = await this.uow.run(async ({ chats, outbox }) => {
277
+ const next = await chats.updateMessage(orgId, messageId, { deletedAt: now });
278
+ if (!next) throw new NotFoundError("Message", messageId);
279
+ await chats.resolveFlags(orgId, messageId, {
280
+ resolvedAt: now,
281
+ resolvedBy: actor.id,
282
+ resolution: "deleted"
283
+ });
284
+ await outbox.append([discussionEvents.messageDeleted.make({
285
+ orgId,
286
+ data: next,
287
+ metadata: this.meta(actor)
288
+ })]);
289
+ return next;
290
+ });
291
+ this.logger.info("message deleted", {
292
+ orgId,
293
+ messageId,
294
+ actorId: actor.id
295
+ });
296
+ return this.viewOf(orgId, actor, saved);
297
+ }
298
+ async setReaction(orgId, actor, messageId, emoji) {
299
+ const message = await this.mustMessage(this.repo, orgId, messageId);
300
+ await this.requireActor(orgId, actor, message.communityId);
301
+ await this.requireActive(this.repo, orgId, message.chatId, actor.id);
302
+ await this.repo.setReaction(orgId, message.communityId, messageId, actor.id, emoji);
303
+ this.logger.debug("message reaction set", {
304
+ orgId,
305
+ messageId
306
+ });
307
+ return (await this.repo.reactionsOf(orgId, [messageId], actor.id))[messageId] ?? { reactions: {} };
308
+ }
309
+ async markRead(orgId, actor, chatId, messageId) {
310
+ const chat = await this.mustChat(this.repo, orgId, chatId);
311
+ await this.requireActor(orgId, actor, chat.communityId);
312
+ await this.requireActive(this.repo, orgId, chat.id, actor.id);
313
+ if ((await this.mustMessage(this.repo, orgId, messageId)).chatId !== chatId) throw new NotFoundError("Message", messageId);
314
+ await this.repo.setRead(orgId, chatId, actor.id, messageId);
315
+ this.logger.debug("chat marked read", {
316
+ orgId,
317
+ chatId,
318
+ messageId
319
+ });
320
+ }
321
+ async mute(orgId, actor, chatId, until) {
322
+ const chat = await this.mustChat(this.repo, orgId, chatId);
323
+ await this.requireActor(orgId, actor, chat.communityId);
324
+ await this.requireActive(this.repo, orgId, chat.id, actor.id);
325
+ await this.repo.setMuted(orgId, chatId, actor.id, true, until ?? null);
326
+ this.logger.debug("chat muted", {
327
+ orgId,
328
+ chatId
329
+ });
330
+ }
331
+ async unmute(orgId, actor, chatId) {
332
+ const chat = await this.mustChat(this.repo, orgId, chatId);
333
+ await this.requireActor(orgId, actor, chat.communityId);
334
+ await this.requireActive(this.repo, orgId, chat.id, actor.id);
335
+ await this.repo.setMuted(orgId, chatId, actor.id, false, null);
336
+ this.logger.debug("chat unmuted", {
337
+ orgId,
338
+ chatId
339
+ });
340
+ }
341
+ async block(orgId, actor, communityId, orgUserId) {
342
+ if (actor.id === orgUserId) throw new InvalidInputError("You cannot block yourself");
343
+ await this.requireSettings(orgId, communityId);
344
+ await this.requireActor(orgId, actor, communityId);
345
+ await this.requirePerson(orgId, communityId, orgUserId);
346
+ await this.uow.run(async ({ chats }) => {
347
+ await chats.lockDirectPair(orgId, communityId, actor.id, orgUserId);
348
+ await chats.block(orgId, communityId, actor.id, orgUserId);
349
+ });
350
+ this.logger.info("user blocked", {
351
+ orgId,
352
+ communityId,
353
+ actorId: actor.id
354
+ });
355
+ }
356
+ async unblock(orgId, actor, communityId, orgUserId) {
357
+ if (actor.id === orgUserId) throw new InvalidInputError("You cannot block yourself");
358
+ await this.requireSettings(orgId, communityId);
359
+ await this.requireActor(orgId, actor, communityId);
360
+ await this.uow.run(async ({ chats }) => {
361
+ await chats.lockDirectPair(orgId, communityId, actor.id, orgUserId);
362
+ await chats.unblock(orgId, communityId, actor.id, orgUserId);
363
+ });
364
+ this.logger.info("user unblocked", {
365
+ orgId,
366
+ communityId,
367
+ actorId: actor.id
368
+ });
369
+ }
370
+ async flagMessage(orgId, actor, messageId, reason) {
371
+ const message = await this.mustMessage(this.repo, orgId, messageId);
372
+ await this.requireActor(orgId, actor, message.communityId);
373
+ await this.requireActive(this.repo, orgId, message.chatId, actor.id);
374
+ const now = /* @__PURE__ */ new Date();
375
+ const saved = await this.uow.run(async ({ chats, outbox }) => {
376
+ const flag = await chats.insertFlag(orgId, {
377
+ orgId,
378
+ id: genId("messageFlag"),
379
+ communityId: message.communityId,
380
+ messageId,
381
+ orgUserId: actor.id,
382
+ reason,
383
+ resolvedAt: null,
384
+ resolution: null,
385
+ resolvedBy: null,
386
+ createdAt: now,
387
+ updatedAt: now
388
+ });
389
+ if (!flag) {
390
+ const existing = await chats.findFlag(orgId, messageId, actor.id);
391
+ if (!existing) throw new NotFoundError("Message", messageId);
392
+ return existing;
393
+ }
394
+ await outbox.append([discussionEvents.messageReported.make({
395
+ orgId,
396
+ data: flag,
397
+ metadata: this.meta(actor)
398
+ })]);
399
+ return flag;
400
+ });
401
+ this.logger.info("message flagged", {
402
+ orgId,
403
+ messageId,
404
+ actorId: actor.id
405
+ });
406
+ return saved;
407
+ }
408
+ async resolveMessageFlags(orgId, actor, messageId, resolution, communityId) {
409
+ if (!isStaffRole(actor.role)) throw new ForbiddenError("You cannot work this queue");
410
+ const message = await this.mustMessage(this.repo, orgId, messageId);
411
+ if (communityId !== void 0 && message.communityId !== communityId) throw new NotFoundError("Message", messageId);
412
+ await this.requireActor(orgId, actor, message.communityId);
413
+ await this.repo.resolveFlags(orgId, messageId, {
414
+ resolvedAt: /* @__PURE__ */ new Date(),
415
+ resolvedBy: actor.id,
416
+ resolution
417
+ });
418
+ this.logger.info("message flags resolved", {
419
+ orgId,
420
+ communityId: message.communityId,
421
+ messageId,
422
+ resolution,
423
+ actorId: actor.id
424
+ });
425
+ }
426
+ async lock(orgId, actor, chatId, communityId) {
427
+ return this.setLock(orgId, actor, chatId, true, communityId);
428
+ }
429
+ async unlock(orgId, actor, chatId, communityId) {
430
+ return this.setLock(orgId, actor, chatId, false, communityId);
431
+ }
432
+ async listChats(orgId, actor, communityId, query) {
433
+ await this.requireSettings(orgId, communityId);
434
+ await this.requireActor(orgId, actor, communityId);
435
+ const now = /* @__PURE__ */ new Date();
436
+ const page = await this.repo.listForUser(orgId, communityId, actor.id, query);
437
+ const items = page.rows.map((item) => ({
438
+ ...item,
439
+ ...item.muted && item.mutedUntil !== null && item.mutedUntil <= now ? {
440
+ muted: false,
441
+ mutedUntil: null
442
+ } : {},
443
+ lastMessage: item.lastMessage && item.lastMessage.deletedAt !== null && !isStaffRole(actor.role) ? {
444
+ ...item.lastMessage,
445
+ body: ""
446
+ } : item.lastMessage
447
+ }));
448
+ this.logger.debug("chats listed", {
449
+ orgId,
450
+ communityId,
451
+ count: items.length
452
+ });
453
+ return {
454
+ ...page,
455
+ rows: items
456
+ };
457
+ }
458
+ async messages(orgId, actor, chatId, query) {
459
+ const chat = await this.mustChat(this.repo, orgId, chatId);
460
+ await this.requireActor(orgId, actor, chat.communityId);
461
+ await this.requireActive(this.repo, orgId, chat.id, actor.id);
462
+ const page = await this.repo.listMessages(orgId, chatId, query);
463
+ this.logger.debug("chat messages read", {
464
+ orgId,
465
+ chatId
466
+ });
467
+ return {
468
+ ...page,
469
+ rows: await this.withSenders(orgId, actor, page.rows)
470
+ };
471
+ }
472
+ async attachmentFile(orgId, actor, chatId, messageId, assetId) {
473
+ const chat = await this.repo.findChat(orgId, chatId);
474
+ const message = await this.repo.findMessage(orgId, messageId);
475
+ if (!chat || !message || message.chatId !== chatId) return null;
476
+ if (!((await this.repo.attachmentsOf(orgId, [messageId]))[messageId] ?? []).includes(assetId)) return null;
477
+ if (!await this.access.isCommunityMember(orgId, actor, chat.communityId)) return null;
478
+ if (!((await this.repo.participants(orgId, chatId)).some((user) => user.orgUserId === actor.id && user.leftAt === null) ? message.deletedAt === null || isStaffRole(actor.role) : isStaffRole(actor.role) && (await this.repo.listOpenFlags(orgId, chat.communityId)).some((flag) => flag.messageId === messageId))) return null;
479
+ return this.images.signFile(orgId, assetId);
480
+ }
481
+ async messageQueue(orgId, actor, communityId) {
482
+ if (!isStaffRole(actor.role)) throw new ForbiddenError("You cannot work this queue");
483
+ await this.requireActor(orgId, actor, communityId);
484
+ await this.requireSettings(orgId, communityId);
485
+ const flags = await this.repo.listOpenFlags(orgId, communityId, 100);
486
+ const messages = await this.repo.messagesByIds(orgId, [...new Set(flags.map((flag) => flag.messageId))]);
487
+ const summaries = await this.repo.chatSummaries(orgId, [...new Set(messages.map((message) => message.chatId))]);
488
+ const byChat = new Map(summaries.map((summary) => [summary.chat.id, summary]));
489
+ const entries = (await this.withSenders(orgId, actor, messages)).flatMap((message) => {
490
+ const summary = byChat.get(message.chatId);
491
+ if (!summary) return [];
492
+ return [{
493
+ message,
494
+ flags: flags.filter((flag) => flag.messageId === message.id),
495
+ chat: summary.chat,
496
+ participantIds: summary.participantIds
497
+ }];
498
+ });
499
+ this.logger.debug("message queue read", {
500
+ orgId,
501
+ communityId,
502
+ count: entries.length
503
+ });
504
+ return { entries };
505
+ }
506
+ async chatSummary(orgId, chatId) {
507
+ const chat = await this.repo.findChat(orgId, chatId);
508
+ if (!chat) return null;
509
+ return {
510
+ chat,
511
+ participants: await this.repo.participants(orgId, chatId)
512
+ };
513
+ }
514
+ async titlesFor(orgId, actor, chatIds) {
515
+ if (chatIds.length === 0) return {};
516
+ const summaries = (await this.repo.chatSummaries(orgId, [...new Set(chatIds)])).filter((summary) => summary.participantIds.includes(actor.id));
517
+ const others = [...new Set(summaries.flatMap((summary) => summary.participantIds.filter((id) => id !== actor.id)))];
518
+ const records = others.length > 0 ? await this.profiles.authorsOf(orgId, others) : {};
519
+ const nameOf = (id) => {
520
+ const record = records[id];
521
+ return record && authorView(orgId, record, null).displayName || "A member";
522
+ };
523
+ return Object.fromEntries(summaries.flatMap((summary) => {
524
+ const names = summary.participantIds.filter((id) => id !== actor.id).map(nameOf);
525
+ if (names.length > 0) return [[summary.chat.id, names.join(", ")]];
526
+ return summary.chat.kind === "group" ? [[summary.chat.id, "Group chat"]] : [];
527
+ }));
528
+ }
529
+ async latestMessageBody(orgId, chatId) {
530
+ return (await this.repo.listMessages(orgId, chatId, {
531
+ page: 1,
532
+ pageSize: 1
533
+ })).rows[0]?.body ?? null;
534
+ }
535
+ isBlocked(orgId, communityId, a, b) {
536
+ return this.repo.isBlocked(orgId, communityId, a, b);
537
+ }
538
+ async setLock(orgId, actor, chatId, locked, communityId) {
539
+ if (!isStaffRole(actor.role)) throw new ForbiddenError("You cannot lock this chat");
540
+ const chat = await this.mustChat(this.repo, orgId, chatId);
541
+ if (communityId !== void 0 && chat.communityId !== communityId) throw new NotFoundError("Chat", chatId);
542
+ await this.requireActor(orgId, actor, chat.communityId);
543
+ if (Boolean(chat.lockedAt) === locked) return chat;
544
+ const now = /* @__PURE__ */ new Date();
545
+ const saved = await this.uow.run(async ({ chats, outbox }) => {
546
+ const next = await this.mustUpdate(chats, orgId, chatId, {
547
+ lockedAt: locked ? now : null,
548
+ lockedBy: locked ? actor.id : null
549
+ });
550
+ if (locked) await chats.resolveOpenFlagsForChat(orgId, chatId, {
551
+ resolvedAt: now,
552
+ resolvedBy: actor.id,
553
+ resolution: "locked"
554
+ });
555
+ const users = await chats.participants(orgId, chatId);
556
+ await outbox.append([this.updatedEvent(orgId, actor, next, users)]);
557
+ return next;
558
+ });
559
+ this.logger.info(locked ? "chat locked" : "chat unlocked", {
560
+ orgId,
561
+ chatId,
562
+ actorId: actor.id
563
+ });
564
+ return saved;
565
+ }
566
+ async requireSettings(orgId, communityId) {
567
+ const settings = await this.content.getDmSettings(orgId, communityId);
568
+ if (!settings) throw new NotFoundError("Community", communityId);
569
+ return settings;
570
+ }
571
+ async requireActor(orgId, actor, communityId) {
572
+ if (!await this.access.isCommunityMember(orgId, actor, communityId)) throw new ForbiddenError("You are not a member of this community");
573
+ }
574
+ async requirePerson(orgId, communityId, orgUserId) {
575
+ const role = await this.personRole(orgId, communityId, orgUserId);
576
+ if (!role) throw new ForbiddenError("That person is not a member of this community");
577
+ return role;
578
+ }
579
+ async personRole(orgId, communityId, orgUserId) {
580
+ const role = await this.authors.roleOf(orgId, orgUserId);
581
+ if (!role) return null;
582
+ return await this.access.isCommunityMember(orgId, {
583
+ id: orgUserId,
584
+ role
585
+ }, communityId) ? role : null;
586
+ }
587
+ async personRoles(orgId, communityId, orgUserIds) {
588
+ const roles = await this.authors.rolesOf(orgId, orgUserIds);
589
+ const members = new Set(await this.access.communityMembersAmong(orgId, communityId, Object.keys(roles)));
590
+ return Object.fromEntries(Object.entries(roles).filter(([orgUserId]) => members.has(orgUserId)));
591
+ }
592
+ async assertMaySend(chats, orgId, actor, chat, users, settings, input) {
593
+ this.assertActive(users, actor.id);
594
+ if (chat.lockedAt) throw new ForbiddenError("This chat is locked");
595
+ if (chat.status === "closed") throw new ForbiddenError("You cannot send in this chat");
596
+ if (this.pendingCreator(chat, actor)) throw new ForbiddenError("You cannot send in this chat");
597
+ const others = users.filter((other) => other.orgUserId !== actor.id && (chat.kind === "direct" || other.leftAt === null)).map((other) => other.orgUserId);
598
+ if (others.length > 0) {
599
+ const roles = await this.personRoles(orgId, chat.communityId, others);
600
+ const recipients = chat.kind === "group" ? others.filter((other) => roles[other]) : others;
601
+ for (const other of recipients) {
602
+ const role = roles[other];
603
+ if (!role || !this.mayMessage(settings, actor, role)) throw new ForbiddenError("You cannot send in this chat");
604
+ }
605
+ if (recipients.length > 0) {
606
+ if ((await chats.blockedWith(orgId, chat.communityId, actor.id, recipients)).length > 0) throw new ForbiddenError("You cannot send in this chat");
607
+ }
608
+ }
609
+ if (input.replyToId) {
610
+ const reply = await chats.findMessage(orgId, input.replyToId);
611
+ if (!reply || reply.chatId !== chat.id) throw new NotFoundError("Message", input.replyToId);
612
+ }
613
+ await this.requireOwnedImages(orgId, actor.id, input.assetIds);
614
+ }
615
+ async writeMessage(chats, orgId, actor, chat, input) {
616
+ const opened = chat.kind === "direct" && chat.status === "pending" && chat.createdBy !== actor.id;
617
+ const current = opened ? await this.mustUpdate(chats, orgId, chat.id, { status: "open" }) : chat;
618
+ const message = await chats.insertMessage(orgId, this.newMessage(orgId, current, actor, input, /* @__PURE__ */ new Date()));
619
+ if (input.assetIds.length > 0) await chats.attach(orgId, current.communityId, message.id, input.assetIds);
620
+ return {
621
+ chat: current,
622
+ message,
623
+ opened
624
+ };
625
+ }
626
+ assertMayMessage(settings, actor, role) {
627
+ if (!settings.enabled) throw new ForbiddenError("Direct messages are off");
628
+ if (!this.mayMessage(settings, actor, role)) throw new ForbiddenError("You cannot message this person");
629
+ }
630
+ assertGroup(settings) {
631
+ if (!settings.enabled) throw new ForbiddenError("Direct messages are off");
632
+ if (!settings.groupChats) throw new ForbiddenError("Group chats are off");
633
+ }
634
+ mayMessage(settings, actor, target) {
635
+ if (isStaffRole(actor.role)) return settings.staffToAnyone;
636
+ if (isStaffRole(target)) return settings.memberToStaff;
637
+ return settings.memberToMember;
638
+ }
639
+ async requireActive(repo, orgId, chatId, orgUserId) {
640
+ const users = await repo.participants(orgId, chatId);
641
+ this.assertActive(users, orgUserId);
642
+ }
643
+ assertActive(users, orgUserId) {
644
+ if (!users.some((user) => user.orgUserId === orgUserId && user.leftAt === null)) throw new ForbiddenError("You are not in this chat");
645
+ }
646
+ async requireOwnedImages(orgId, orgUserId, assetIds) {
647
+ for (const assetId of assetIds) if (!await this.images.ownsImage(orgId, assetId, orgUserId)) {
648
+ this.logger.debug("attachment refused", {
649
+ orgId,
650
+ orgUserId,
651
+ assetId
652
+ });
653
+ throw new NotFoundError("Asset", assetId);
654
+ }
655
+ }
656
+ async requireWritableChat(orgId, actor, chatId) {
657
+ const chat = await this.mustChat(this.repo, orgId, chatId);
658
+ await this.requireActor(orgId, actor, chat.communityId);
659
+ await this.requireActive(this.repo, orgId, chat.id, actor.id);
660
+ if (chat.lockedAt || chat.status === "closed") throw new ForbiddenError("You cannot send in this chat");
661
+ }
662
+ pendingCreator(chat, actor) {
663
+ return chat.kind === "direct" && chat.status === "pending" && chat.createdBy === actor.id;
664
+ }
665
+ async mustChat(repo, orgId, chatId) {
666
+ const chat = await repo.findChat(orgId, chatId);
667
+ if (!chat) throw new NotFoundError("Chat", chatId);
668
+ return chat;
669
+ }
670
+ async mustMessage(repo, orgId, messageId) {
671
+ const message = await repo.findMessage(orgId, messageId);
672
+ if (!message) throw new NotFoundError("Message", messageId);
673
+ return message;
674
+ }
675
+ async mustUpdate(chats, orgId, chatId, patch) {
676
+ const next = await chats.updateChat(orgId, chatId, patch);
677
+ if (!next) throw new NotFoundError("Chat", chatId);
678
+ return next;
679
+ }
680
+ memberRow(orgId, communityId, chatId, orgUserId, now) {
681
+ return {
682
+ orgId,
683
+ communityId,
684
+ chatId,
685
+ orgUserId,
686
+ joinedAt: now,
687
+ leftAt: null,
688
+ lastReadMessageId: null,
689
+ muted: false,
690
+ mutedUntil: null,
691
+ createdAt: now,
692
+ updatedAt: now
693
+ };
694
+ }
695
+ newMessage(orgId, chat, actor, input, now) {
696
+ return {
697
+ orgId,
698
+ id: genId("message"),
699
+ communityId: chat.communityId,
700
+ chatId: chat.id,
701
+ orgUserId: actor.id,
702
+ body: input.body,
703
+ replyToId: input.replyToId,
704
+ editedAt: null,
705
+ deletedAt: null,
706
+ createdAt: now,
707
+ updatedAt: now
708
+ };
709
+ }
710
+ activeIds(users) {
711
+ return users.filter((user) => user.leftAt === null).map((user) => user.orgUserId).sort();
712
+ }
713
+ chatData(chat, users) {
714
+ return {
715
+ ...chat,
716
+ participantIds: this.activeIds(users)
717
+ };
718
+ }
719
+ meta(actor) {
720
+ return { actorOrgUserId: actor.id };
721
+ }
722
+ createdEvent(orgId, actor, chat, users) {
723
+ return discussionEvents.chatCreated.make({
724
+ orgId,
725
+ data: this.chatData(chat, users),
726
+ metadata: this.meta(actor)
727
+ });
728
+ }
729
+ updatedEvent(orgId, actor, chat, users) {
730
+ return discussionEvents.chatUpdated.make({
731
+ orgId,
732
+ data: this.chatData(chat, users),
733
+ metadata: this.meta(actor)
734
+ });
735
+ }
736
+ addedEvent(orgId, actor, chat, orgUserId) {
737
+ return discussionEvents.chatParticipantAdded.make({
738
+ orgId,
739
+ data: {
740
+ chatId: chat.id,
741
+ communityId: chat.communityId,
742
+ orgUserId,
743
+ addedBy: actor.id
744
+ },
745
+ metadata: this.meta(actor)
746
+ });
747
+ }
748
+ sentEvent(orgId, actor, chat, message) {
749
+ return discussionEvents.messageSent.make({
750
+ orgId,
751
+ data: message,
752
+ metadata: {
753
+ ...this.meta(actor),
754
+ chatStatus: chat.status
755
+ }
756
+ });
757
+ }
758
+ async viewOf(orgId, actor, message) {
759
+ const [view] = await this.withSenders(orgId, actor, [message]);
760
+ return view;
761
+ }
762
+ async withSenders(orgId, actor, messages) {
763
+ if (messages.length === 0) return [];
764
+ const messageIds = messages.map((message) => message.id);
765
+ const [records, attached, reactions] = await Promise.all([
766
+ this.profiles.authorsOf(orgId, [...new Set(messages.map((message) => message.orgUserId))]),
767
+ this.repo.attachmentsOf(orgId, messageIds),
768
+ this.repo.reactionsOf(orgId, messageIds, actor.id)
769
+ ]);
770
+ const shown = (message) => message.deletedAt !== null && !isStaffRole(actor.role) ? [] : attached[message.id] ?? [];
771
+ return messages.map((message) => {
772
+ const record = records[message.orgUserId];
773
+ if (!record) throw new NotFoundError("Member", message.orgUserId);
774
+ const counts = reactions[message.id];
775
+ return {
776
+ ...this.present(message, actor),
777
+ sender: authorView(orgId, record, this.fileUrls),
778
+ attachments: shown(message).map((assetId) => ({
779
+ assetId,
780
+ url: this.fileUrls.messageAttachment(message.chatId, message.id, assetId)
781
+ })),
782
+ reactions: counts?.reactions ?? {},
783
+ viewerReaction: counts?.viewerReaction ?? null
784
+ };
785
+ });
786
+ }
787
+ present(message, actor) {
788
+ if (message.deletedAt !== null && !isStaffRole(actor.role)) return {
789
+ ...message,
790
+ body: ""
791
+ };
792
+ return message;
793
+ }
794
+ };
795
+ //#endregion
796
+ export { ChatServiceImpl };
797
+
798
+ //# sourceMappingURL=chats.js.map