@headless-lms/core 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/dist/assets/events.d.ts +27 -21
  2. package/dist/assets/file-urls.d.ts +11 -7
  3. package/dist/assets/file-urls.js +14 -4
  4. package/dist/assets/file-urls.js.map +1 -1
  5. package/dist/assets/index.d.ts +4 -4
  6. package/dist/assets/index.js +3 -3
  7. package/dist/assets/ports.d.ts +34 -15
  8. package/dist/assets/service.d.ts +30 -14
  9. package/dist/assets/service.js +230 -56
  10. package/dist/assets/service.js.map +1 -1
  11. package/dist/automations/actions.js.map +1 -1
  12. package/dist/automations/catalog.js +69 -4
  13. package/dist/automations/catalog.js.map +1 -1
  14. package/dist/automations/community-derivations.js +16 -16
  15. package/dist/automations/community-derivations.js.map +1 -1
  16. package/dist/automations/community-mail.d.ts +34 -4
  17. package/dist/automations/community-mail.js +32 -4
  18. package/dist/automations/community-mail.js.map +1 -1
  19. package/dist/automations/event-permissions.d.ts +12 -0
  20. package/dist/automations/event-permissions.js +38 -0
  21. package/dist/automations/event-permissions.js.map +1 -0
  22. package/dist/automations/index.d.ts +2 -1
  23. package/dist/automations/index.js +2 -1
  24. package/dist/automations/notifications/applicable.d.ts +8 -0
  25. package/dist/automations/notifications/applicable.js +12 -0
  26. package/dist/automations/notifications/applicable.js.map +1 -0
  27. package/dist/automations/notifications/debounce.js +1 -1
  28. package/dist/automations/notifications/debounce.js.map +1 -1
  29. package/dist/automations/notifications/defaults.js +33 -3
  30. package/dist/automations/notifications/defaults.js.map +1 -1
  31. package/dist/automations/notifications/processor.js +3 -3
  32. package/dist/automations/notifications/processor.js.map +1 -1
  33. package/dist/automations/notifications/rules/content.js +60 -3
  34. package/dist/automations/notifications/rules/content.js.map +1 -1
  35. package/dist/automations/notifications/rules/discussion.js +29 -29
  36. package/dist/automations/notifications/rules/discussion.js.map +1 -1
  37. package/dist/automations/notifications/rules/entitlements.js +36 -9
  38. package/dist/automations/notifications/rules/entitlements.js.map +1 -1
  39. package/dist/automations/notifications/rules/events.d.ts +9 -0
  40. package/dist/automations/notifications/rules/events.js +27 -0
  41. package/dist/automations/notifications/rules/events.js.map +1 -0
  42. package/dist/automations/notifications/rules/organizations.js +17 -10
  43. package/dist/automations/notifications/rules/organizations.js.map +1 -1
  44. package/dist/automations/notifications/rules/recipients.js +0 -1
  45. package/dist/automations/notifications/rules/recipients.js.map +1 -1
  46. package/dist/automations/notifications/service.d.ts +8 -6
  47. package/dist/automations/notifications/service.js +13 -13
  48. package/dist/automations/notifications/service.js.map +1 -1
  49. package/dist/automations/notifications/types.js +94 -7
  50. package/dist/automations/notifications/types.js.map +1 -1
  51. package/dist/automations/ports.d.ts +30 -31
  52. package/dist/automations/service.d.ts +20 -17
  53. package/dist/automations/service.js +91 -62
  54. package/dist/automations/service.js.map +1 -1
  55. package/dist/content/event-sight.d.ts +11 -0
  56. package/dist/content/event-sight.js +11 -0
  57. package/dist/content/event-sight.js.map +1 -0
  58. package/dist/content/events.d.ts +413 -72
  59. package/dist/content/events.js +34 -1
  60. package/dist/content/events.js.map +1 -1
  61. package/dist/content/index.d.ts +6 -4
  62. package/dist/content/index.js +3 -1
  63. package/dist/content/jobs.d.ts +16 -0
  64. package/dist/content/jobs.js +37 -0
  65. package/dist/content/jobs.js.map +1 -0
  66. package/dist/content/model.d.ts +2 -2
  67. package/dist/content/notification-categories.d.ts +5 -4
  68. package/dist/content/notification-categories.js +15 -6
  69. package/dist/content/notification-categories.js.map +1 -1
  70. package/dist/content/ports.d.ts +113 -68
  71. package/dist/content/service.d.ts +99 -67
  72. package/dist/content/service.js +680 -139
  73. package/dist/content/service.js.map +1 -1
  74. package/dist/content/titles.d.ts +6 -3
  75. package/dist/content/titles.js +37 -24
  76. package/dist/content/titles.js.map +1 -1
  77. package/dist/content/types.d.ts +2 -2
  78. package/dist/discussion/authors.d.ts +4 -2
  79. package/dist/discussion/authors.js +9 -5
  80. package/dist/discussion/authors.js.map +1 -1
  81. package/dist/discussion/chats.d.ts +56 -37
  82. package/dist/discussion/chats.js +393 -161
  83. package/dist/discussion/chats.js.map +1 -1
  84. package/dist/discussion/events.d.ts +77 -39
  85. package/dist/discussion/events.js +41 -3
  86. package/dist/discussion/events.js.map +1 -1
  87. package/dist/discussion/index.d.ts +8 -5
  88. package/dist/discussion/index.js +5 -3
  89. package/dist/discussion/live.d.ts +106 -0
  90. package/dist/discussion/live.js +642 -0
  91. package/dist/discussion/live.js.map +1 -0
  92. package/dist/discussion/mentions.js +2 -2
  93. package/dist/discussion/mentions.js.map +1 -1
  94. package/dist/discussion/ports.d.ts +182 -100
  95. package/dist/discussion/ports.js.map +1 -1
  96. package/dist/discussion/posts.d.ts +45 -39
  97. package/dist/discussion/posts.js +268 -154
  98. package/dist/discussion/posts.js.map +1 -1
  99. package/dist/discussion/rules.d.ts +18 -0
  100. package/dist/discussion/rules.js +22 -0
  101. package/dist/discussion/rules.js.map +1 -0
  102. package/dist/discussion/service.d.ts +37 -31
  103. package/dist/discussion/service.js +252 -188
  104. package/dist/discussion/service.js.map +1 -1
  105. package/dist/discussion/subscriptions.d.ts +8 -8
  106. package/dist/discussion/subscriptions.js +29 -13
  107. package/dist/discussion/subscriptions.js.map +1 -1
  108. package/dist/discussion/titles.d.ts +4 -2
  109. package/dist/discussion/titles.js +27 -13
  110. package/dist/discussion/titles.js.map +1 -1
  111. package/dist/entitlements/events.d.ts +20 -20
  112. package/dist/entitlements/index.d.ts +4 -4
  113. package/dist/entitlements/index.js +2 -2
  114. package/dist/entitlements/model.d.ts +2 -2
  115. package/dist/entitlements/ports.d.ts +65 -39
  116. package/dist/entitlements/rules.d.ts +3 -6
  117. package/dist/entitlements/rules.js +5 -10
  118. package/dist/entitlements/rules.js.map +1 -1
  119. package/dist/entitlements/service.d.ts +40 -28
  120. package/dist/entitlements/service.js +370 -163
  121. package/dist/entitlements/service.js.map +1 -1
  122. package/dist/identity/devices.d.ts +3 -3
  123. package/dist/identity/devices.js +7 -5
  124. package/dist/identity/devices.js.map +1 -1
  125. package/dist/identity/events.d.ts +2 -2
  126. package/dist/identity/index.d.ts +2 -2
  127. package/dist/identity/index.js +2 -2
  128. package/dist/identity/notification-settings.d.ts +1 -1
  129. package/dist/identity/notification-settings.js +3 -2
  130. package/dist/identity/notification-settings.js.map +1 -1
  131. package/dist/identity/ports.d.ts +7 -7
  132. package/dist/identity/service.d.ts +8 -7
  133. package/dist/identity/service.js +17 -13
  134. package/dist/identity/service.js.map +1 -1
  135. package/dist/integrations/events.d.ts +3 -3
  136. package/dist/integrations/ports.d.ts +9 -8
  137. package/dist/integrations/service.d.ts +9 -8
  138. package/dist/integrations/service.js +33 -16
  139. package/dist/integrations/service.js.map +1 -1
  140. package/dist/organizations/default-roles.d.ts +22 -0
  141. package/dist/organizations/default-roles.js +89 -0
  142. package/dist/organizations/default-roles.js.map +1 -0
  143. package/dist/organizations/effective-permissions.d.ts +45 -0
  144. package/dist/organizations/effective-permissions.js +113 -0
  145. package/dist/organizations/effective-permissions.js.map +1 -0
  146. package/dist/organizations/events.d.ts +320 -181
  147. package/dist/organizations/events.js +90 -18
  148. package/dist/organizations/events.js.map +1 -1
  149. package/dist/organizations/index.d.ts +14 -10
  150. package/dist/organizations/index.js +7 -3
  151. package/dist/organizations/members.d.ts +66 -8
  152. package/dist/organizations/members.js.map +1 -1
  153. package/dist/organizations/notification-catalog.d.ts +31 -0
  154. package/dist/organizations/notification-catalog.js +32 -0
  155. package/dist/organizations/notification-catalog.js.map +1 -0
  156. package/dist/organizations/notification-categories.d.ts +6 -4
  157. package/dist/organizations/notification-categories.js +25 -10
  158. package/dist/organizations/notification-categories.js.map +1 -1
  159. package/dist/organizations/org-roles.d.ts +33 -0
  160. package/dist/organizations/org-roles.js +49 -0
  161. package/dist/organizations/org-roles.js.map +1 -0
  162. package/dist/organizations/permissions.d.ts +311 -0
  163. package/dist/organizations/permissions.js +327 -0
  164. package/dist/organizations/permissions.js.map +1 -0
  165. package/dist/organizations/ports.d.ts +184 -65
  166. package/dist/organizations/service.d.ts +102 -35
  167. package/dist/organizations/service.js +1245 -328
  168. package/dist/organizations/service.js.map +1 -1
  169. package/dist/organizations/testing.d.ts +18 -0
  170. package/dist/organizations/testing.js +37 -0
  171. package/dist/organizations/testing.js.map +1 -0
  172. package/dist/organizations/types.d.ts +5 -16
  173. package/dist/progress/events.d.ts +14 -14
  174. package/dist/progress/index.d.ts +2 -2
  175. package/dist/progress/ports.d.ts +11 -3
  176. package/dist/progress/service.d.ts +6 -5
  177. package/dist/progress/service.js +23 -8
  178. package/dist/progress/service.js.map +1 -1
  179. package/dist/reporting/admin/index.d.ts +6 -4
  180. package/dist/reporting/admin/model.d.ts +36 -3
  181. package/dist/reporting/admin/ports.d.ts +39 -14
  182. package/dist/reporting/admin/service.d.ts +32 -11
  183. package/dist/reporting/admin/service.js +102 -18
  184. package/dist/reporting/admin/service.js.map +1 -1
  185. package/dist/reporting/events/index.d.ts +27 -0
  186. package/dist/reporting/events/index.js +20 -0
  187. package/dist/reporting/events/index.js.map +1 -0
  188. package/dist/reporting/member/calendar.d.ts +11 -0
  189. package/dist/reporting/member/calendar.js +75 -0
  190. package/dist/reporting/member/calendar.js.map +1 -0
  191. package/dist/reporting/member/index.d.ts +3 -2
  192. package/dist/reporting/member/model.d.ts +26 -5
  193. package/dist/reporting/member/ports.d.ts +48 -29
  194. package/dist/reporting/member/service.d.ts +56 -29
  195. package/dist/reporting/member/service.js +328 -113
  196. package/dist/reporting/member/service.js.map +1 -1
  197. package/dist/shared/actor.d.ts +22 -3
  198. package/dist/shared/errors.d.ts +2 -1
  199. package/dist/shared/errors.js +3 -1
  200. package/dist/shared/errors.js.map +1 -1
  201. package/dist/shared/id.d.ts +4 -0
  202. package/dist/shared/id.js +4 -0
  203. package/dist/shared/id.js.map +1 -1
  204. package/dist/shared/logger.d.ts +2 -1
  205. package/dist/shared/logger.js +10 -1
  206. package/dist/shared/logger.js.map +1 -1
  207. package/dist/shared/ports.d.ts +25 -5
  208. package/dist/types/content.d.ts +2 -2
  209. package/dist/types/discussion.d.ts +4 -5
  210. package/dist/types/email-templates.d.ts +22 -3
  211. package/dist/types/entitlements.d.ts +2 -2
  212. package/dist/types/identity.d.ts +13 -16
  213. package/dist/types/index.d.ts +9 -9
  214. package/dist/types/index.js +2 -1
  215. package/dist/types/notifications.d.ts +4 -4
  216. package/dist/types/organizations.d.ts +3 -2
  217. package/dist/types/ports.d.ts +8 -1
  218. package/dist/types/ports.js +7 -1
  219. package/dist/types/ports.js.map +1 -0
  220. package/dist/types/schemas/assets.d.ts +5 -5
  221. package/dist/types/schemas/assets.js +4 -3
  222. package/dist/types/schemas/assets.js.map +1 -1
  223. package/dist/types/schemas/content.d.ts +537 -13
  224. package/dist/types/schemas/content.js +201 -7
  225. package/dist/types/schemas/content.js.map +1 -1
  226. package/dist/types/schemas/discussion.d.ts +282 -59
  227. package/dist/types/schemas/discussion.js +77 -3
  228. package/dist/types/schemas/discussion.js.map +1 -1
  229. package/dist/types/schemas/entitlements.d.ts +14 -1
  230. package/dist/types/schemas/entitlements.js +11 -4
  231. package/dist/types/schemas/entitlements.js.map +1 -1
  232. package/dist/types/schemas/index.d.ts +5 -5
  233. package/dist/types/schemas/index.js +5 -5
  234. package/dist/types/schemas/notifications.d.ts +13 -12
  235. package/dist/types/schemas/notifications.js +3 -2
  236. package/dist/types/schemas/notifications.js.map +1 -1
  237. package/dist/types/schemas/organizations.d.ts +96 -60
  238. package/dist/types/schemas/organizations.js +78 -27
  239. package/dist/types/schemas/organizations.js.map +1 -1
  240. package/dist/types/schemas/progress.d.ts +3 -4
  241. package/dist/types/schemas/progress.js +0 -1
  242. package/dist/types/schemas/progress.js.map +1 -1
  243. package/package.json +1 -1
  244. package/dist/organizations/access-scopes.d.ts +0 -19
  245. package/dist/organizations/access-scopes.js +0 -89
  246. package/dist/organizations/access-scopes.js.map +0 -1
  247. package/dist/organizations/roles.d.ts +0 -19
  248. package/dist/organizations/roles.js +0 -82
  249. package/dist/organizations/roles.js.map +0 -1
@@ -1,14 +1,23 @@
1
- import { genId } from "../shared/id.js";
2
- import { ConflictError, ForbiddenError, InvalidInputError, NotFoundError } from "../shared/errors.js";
3
1
  import { noopLogger } from "../shared/logger.js";
4
- import { isStaffRole } from "../organizations/roles.js";
2
+ import { ConflictError, ForbiddenError, InvalidInputError, NotFoundError, UnavailableError } from "../shared/errors.js";
3
+ import { genId } from "../shared/id.js";
5
4
  import { discussionEvents } from "./events.js";
6
- import { authorView } from "./authors.js";
5
+ import { authorName, authorView } from "./authors.js";
7
6
  import { keepAllowed, mentionTokens, mentionViews } from "./mentions.js";
7
+ import { isTeam, teamBadge } from "./rules.js";
8
8
  import { activeSubscription } from "./subscriptions.js";
9
9
  import "./ports.js";
10
+ import { ChatHub, TYPING, publishChange } from "./live.js";
10
11
  //#region src/discussion/chats.ts
12
+ const TYPING_FOR_MS = 5e3;
13
+ const TYPING_REPEAT_MS = 2e3;
14
+ function eventMessage(message) {
15
+ const { clientId: _clientId, ...rest } = message;
16
+ return rest;
17
+ }
11
18
  var ChatServiceImpl = class {
19
+ /** This process's live chat hub; null without a PubSub. */
20
+ hub;
12
21
  repo;
13
22
  access;
14
23
  content;
@@ -19,7 +28,9 @@ var ChatServiceImpl = class {
19
28
  images;
20
29
  fileUrls;
21
30
  uow;
31
+ pubsub;
22
32
  logger;
33
+ typed = /* @__PURE__ */ new Map();
23
34
  constructor(params) {
24
35
  this.repo = params.repo;
25
36
  this.access = params.access;
@@ -31,20 +42,34 @@ var ChatServiceImpl = class {
31
42
  this.images = params.images;
32
43
  this.fileUrls = params.fileUrls;
33
44
  this.uow = params.uow;
45
+ this.pubsub = params.pubsub ?? null;
34
46
  this.logger = params.logger ?? noopLogger;
47
+ this.hub = this.pubsub ? new ChatHub({
48
+ pubsub: this.pubsub,
49
+ reads: this,
50
+ logger: this.logger,
51
+ ...params.live
52
+ }) : null;
35
53
  }
36
- async startDirect(orgId, actor, input) {
54
+ async startDirect(input, actor) {
55
+ const orgId = actor.orgId;
37
56
  if (actor.id === input.orgUserId) throw new InvalidInputError("You cannot message yourself");
38
57
  if (input.body === null && input.assetIds.length > 0) throw new InvalidInputError("An attachment needs a message");
39
58
  const settings = await this.requireSettings(orgId, input.communityId);
40
- await this.requireActor(orgId, actor, input.communityId);
41
- const role = await this.requirePerson(orgId, input.communityId, input.orgUserId);
42
- this.assertMayMessage(settings, actor, role);
59
+ await this.requireActor(actor, input.communityId);
60
+ const targetTeam = await this.requirePerson(orgId, input.communityId, input.orgUserId);
61
+ const actorTeam = isTeam(actor.permissions, input.communityId);
62
+ this.assertMayMessage(settings, actorTeam, targetTeam);
43
63
  const first = input.body === null ? null : {
44
64
  body: input.body,
45
65
  replyToId: null,
46
66
  assetIds: input.assetIds
47
67
  };
68
+ await this.requireOwnedImages(orgId, actor.id, input.assetIds);
69
+ const teams = {
70
+ asked: /* @__PURE__ */ new Set([input.orgUserId]),
71
+ team: /* @__PURE__ */ new Map([[input.orgUserId, targetTeam]])
72
+ };
48
73
  const saved = await this.uow.run(async ({ chats, mentions, outbox }) => {
49
74
  await chats.lockDirectPair(orgId, input.communityId, actor.id, input.orgUserId);
50
75
  const existing = await chats.findDirect(orgId, input.communityId, actor.id, input.orgUserId);
@@ -55,7 +80,7 @@ var ChatServiceImpl = class {
55
80
  ...user,
56
81
  leftAt: null
57
82
  } : user);
58
- if (first) await this.assertMaySend(chats, orgId, actor, existing, present, settings, first);
83
+ if (first) await this.assertMaySend(chats, orgId, actor, existing, present, settings, first, teams);
59
84
  if (rejoin) await chats.join(orgId, this.memberRow(orgId, existing.communityId, existing.id, actor.id, /* @__PURE__ */ new Date()));
60
85
  const written = first ? await this.writeMessage(chats, mentions, orgId, actor, existing, present, first) : null;
61
86
  const chat = written?.chat ?? existing;
@@ -68,9 +93,8 @@ var ChatServiceImpl = class {
68
93
  created: false
69
94
  };
70
95
  }
71
- await this.requireOwnedImages(orgId, actor.id, input.assetIds);
72
96
  if (await chats.isBlocked(orgId, input.communityId, actor.id, input.orgUserId)) throw new ForbiddenError("You cannot message this person");
73
- const request = settings.firstMessageIsRequest && !isStaffRole(actor.role) && !isStaffRole(role);
97
+ const request = settings.firstMessageIsRequest && !actorTeam && !targetTeam;
74
98
  if (request && !first) throw new InvalidInputError("A message request needs a message");
75
99
  const now = /* @__PURE__ */ new Date();
76
100
  const chat = await chats.insertChat(orgId, {
@@ -112,16 +136,18 @@ var ChatServiceImpl = class {
112
136
  });
113
137
  return saved.chat;
114
138
  }
115
- async startGroup(orgId, actor, input) {
139
+ async startGroup(input, actor) {
140
+ const orgId = actor.orgId;
116
141
  const settings = await this.requireSettings(orgId, input.communityId);
117
142
  this.assertGroup(settings);
118
- await this.requireActor(orgId, actor, input.communityId);
143
+ await this.requireActor(actor, input.communityId);
119
144
  const others = [...new Set(input.orgUserIds.filter((id) => id !== actor.id))].sort();
120
145
  if (others.length < 2) throw new InvalidInputError("A group chat needs three people");
121
146
  if (others.length + 1 > settings.groupSize) throw new ConflictError("This group is full");
147
+ const actorTeam = isTeam(actor.permissions, input.communityId);
122
148
  for (const orgUserId of others) {
123
- const role = await this.requirePerson(orgId, input.communityId, orgUserId);
124
- this.assertMayMessage(settings, actor, role);
149
+ const targetTeam = await this.requirePerson(orgId, input.communityId, orgUserId);
150
+ this.assertMayMessage(settings, actorTeam, targetTeam);
125
151
  }
126
152
  const saved = await this.uow.run(async ({ chats, outbox }) => {
127
153
  if (await chats.anyBlockedAmong(orgId, input.communityId, [actor.id, ...others])) throw new ForbiddenError("You cannot message this person");
@@ -152,15 +178,16 @@ var ChatServiceImpl = class {
152
178
  });
153
179
  return saved;
154
180
  }
155
- async addParticipant(orgId, actor, chatId, orgUserId) {
181
+ async addParticipant(chatId, orgUserId, actor) {
182
+ const orgId = actor.orgId;
156
183
  const chat = await this.mustChat(this.repo, orgId, chatId);
157
- await this.requireActor(orgId, actor, chat.communityId);
184
+ await this.requireActor(actor, chat.communityId);
158
185
  await this.requireActive(this.repo, orgId, chat.id, actor.id);
159
186
  if (chat.kind !== "group") throw new ConflictError("This chat is not a group");
160
187
  const settings = await this.requireSettings(orgId, chat.communityId);
161
188
  this.assertGroup(settings);
162
- const role = await this.requirePerson(orgId, chat.communityId, orgUserId);
163
- this.assertMayMessage(settings, actor, role);
189
+ const targetTeam = await this.requirePerson(orgId, chat.communityId, orgUserId);
190
+ this.assertMayMessage(settings, isTeam(actor.permissions, chat.communityId), targetTeam);
164
191
  const saved = await this.uow.run(async ({ chats, outbox }) => {
165
192
  const current = await this.mustChat(chats, orgId, chatId);
166
193
  const users = await chats.participants(orgId, chatId);
@@ -183,9 +210,10 @@ var ChatServiceImpl = class {
183
210
  });
184
211
  return saved;
185
212
  }
186
- async leave(orgId, actor, chatId) {
213
+ async leave(chatId, actor) {
214
+ const orgId = actor.orgId;
187
215
  const chat = await this.mustChat(this.repo, orgId, chatId);
188
- await this.requireActor(orgId, actor, chat.communityId);
216
+ await this.requireActor(actor, chat.communityId);
189
217
  await this.requireActive(this.repo, orgId, chat.id, actor.id);
190
218
  const saved = await this.uow.run(async ({ chats, outbox }) => {
191
219
  const current = await this.mustChat(chats, orgId, chatId);
@@ -207,9 +235,10 @@ var ChatServiceImpl = class {
207
235
  });
208
236
  return saved;
209
237
  }
210
- async accept(orgId, actor, chatId) {
238
+ async accept(chatId, actor) {
239
+ const orgId = actor.orgId;
211
240
  const chat = await this.mustChat(this.repo, orgId, chatId);
212
- await this.requireActor(orgId, actor, chat.communityId);
241
+ await this.requireActor(actor, chat.communityId);
213
242
  await this.requireActive(this.repo, orgId, chat.id, actor.id);
214
243
  if (chat.createdBy === actor.id) throw new ForbiddenError("You cannot accept this request");
215
244
  if (chat.kind !== "direct" || chat.status !== "pending") throw new ConflictError("This chat is not a request");
@@ -228,36 +257,67 @@ var ChatServiceImpl = class {
228
257
  });
229
258
  return saved;
230
259
  }
231
- async send(orgId, actor, chatId, input) {
260
+ async send(chatId, input, actor) {
261
+ const orgId = actor.orgId;
232
262
  const preview = await this.mustChat(this.repo, orgId, chatId);
233
- await this.requireActor(orgId, actor, preview.communityId);
263
+ await this.requireActor(actor, preview.communityId);
234
264
  const settings = await this.requireSettings(orgId, preview.communityId);
235
265
  if (!settings.enabled) throw new ForbiddenError("Direct messages are off");
236
- const saved = await this.uow.run(async ({ chats, mentions, outbox }) => {
237
- const chat = await this.mustChat(chats, orgId, chatId);
238
- const users = await chats.participants(orgId, chatId);
239
- await this.assertMaySend(chats, orgId, actor, chat, users, settings, input);
240
- const written = await this.writeMessage(chats, mentions, orgId, actor, chat, users, input);
241
- const events = [];
242
- if (written.opened) events.push(this.updatedEvent(orgId, actor, written.chat, users));
243
- events.push(this.sentEvent(orgId, actor, written.chat, written.message, written.mentioned));
244
- await outbox.append(events);
245
- return written.message;
266
+ const clientId = input.clientId;
267
+ const before = await this.repo.participants(orgId, chatId);
268
+ const teams = await this.teamsOf(orgId, preview.communityId, before.map((user) => user.orgUserId).filter((orgUserId) => orgUserId !== actor.id));
269
+ await this.requireOwnedImages(orgId, actor.id, input.assetIds);
270
+ let saved;
271
+ try {
272
+ saved = await this.uow.run(async ({ chats, mentions, outbox }) => {
273
+ const chat = await this.mustChat(chats, orgId, chatId);
274
+ if (clientId) {
275
+ const stored = await chats.findByClientId(orgId, chatId, actor.id, clientId);
276
+ if (stored) return {
277
+ message: stored,
278
+ repeat: true
279
+ };
280
+ }
281
+ const users = await chats.participants(orgId, chatId);
282
+ await this.assertMaySend(chats, orgId, actor, chat, users, settings, input, teams);
283
+ const written = await this.writeMessage(chats, mentions, orgId, actor, chat, users, input);
284
+ const events = [];
285
+ if (written.opened) events.push(this.updatedEvent(orgId, actor, written.chat, users));
286
+ events.push(this.sentEvent(orgId, actor, written.chat, written.message, written.mentioned));
287
+ await outbox.append(events);
288
+ return {
289
+ message: written.message,
290
+ repeat: false
291
+ };
292
+ });
293
+ } catch (error) {
294
+ const stored = clientId && error instanceof ConflictError ? await this.repo.findByClientId(orgId, chatId, actor.id, clientId) : null;
295
+ if (!stored) throw error;
296
+ saved = {
297
+ message: stored,
298
+ repeat: true
299
+ };
300
+ }
301
+ if (saved.repeat) this.logger.debug("message send repeated", {
302
+ orgId,
303
+ chatId,
304
+ messageId: saved.message.id
246
305
  });
247
- this.logger.info("message sent", {
306
+ else this.logger.info("message sent", {
248
307
  orgId,
249
308
  chatId,
250
- messageId: saved.id,
309
+ messageId: saved.message.id,
251
310
  actorId: actor.id
252
311
  });
253
- return this.viewOf(orgId, actor, saved);
312
+ return this.viewOf(actor, saved.message);
254
313
  }
255
- async editMessage(orgId, actor, messageId, body) {
314
+ async editMessage(messageId, body, actor) {
315
+ const orgId = actor.orgId;
256
316
  const message = await this.mustMessage(this.repo, orgId, messageId);
257
- await this.requireWritableChat(orgId, actor, message.chatId);
317
+ await this.requireWritableChat(actor, message.chatId);
258
318
  if (message.orgUserId !== actor.id) throw new ForbiddenError("You can change only your own message");
259
319
  if (message.deletedAt) throw new ConflictError("This message was deleted");
260
- if (message.body === body) return this.viewOf(orgId, actor, message);
320
+ if (message.body === body) return this.viewOf(actor, message);
261
321
  const now = /* @__PURE__ */ new Date();
262
322
  const saved = await this.uow.run(async ({ chats, mentions, outbox }) => {
263
323
  const next = await chats.updateMessage(orgId, messageId, {
@@ -272,7 +332,7 @@ var ChatServiceImpl = class {
272
332
  await outbox.append([discussionEvents.messageUpdated.make({
273
333
  orgId,
274
334
  data: {
275
- ...next,
335
+ ...eventMessage(next),
276
336
  mentionedOrgUserIds: keepAllowed(candidates, added)
277
337
  },
278
338
  metadata: {
@@ -287,14 +347,22 @@ var ChatServiceImpl = class {
287
347
  messageId,
288
348
  actorId: actor.id
289
349
  });
290
- return this.viewOf(orgId, actor, saved);
350
+ return this.viewOf(actor, saved);
291
351
  }
292
- async deleteMessage(orgId, actor, messageId) {
352
+ async deleteMessage(messageId, actor) {
353
+ const orgId = actor.orgId;
293
354
  const message = await this.mustMessage(this.repo, orgId, messageId);
294
- await this.requireActor(orgId, actor, message.communityId);
295
- if (!isStaffRole(actor.role)) {
296
- if (message.orgUserId !== actor.id) throw new ForbiddenError("You cannot delete this message");
297
- await this.requireWritableChat(orgId, actor, message.chatId);
355
+ await this.requireActor(actor, message.communityId);
356
+ if (!actor.permissions.has("communities:moderate", message.communityId)) {
357
+ if (message.orgUserId !== actor.id) {
358
+ this.logger.warn("message delete refused — not the sender or a moderator", {
359
+ orgId,
360
+ messageId,
361
+ actorId: actor.id
362
+ });
363
+ throw new ForbiddenError("You cannot delete this message");
364
+ }
365
+ await this.requireWritableChat(actor, message.chatId);
298
366
  }
299
367
  if (message.deletedAt) throw new ConflictError("This message was deleted");
300
368
  const now = /* @__PURE__ */ new Date();
@@ -309,7 +377,7 @@ var ChatServiceImpl = class {
309
377
  });
310
378
  await outbox.append([discussionEvents.messageDeleted.make({
311
379
  orgId,
312
- data: next,
380
+ data: eventMessage(next),
313
381
  metadata: this.meta(actor)
314
382
  })]);
315
383
  return next;
@@ -319,25 +387,40 @@ var ChatServiceImpl = class {
319
387
  messageId,
320
388
  actorId: actor.id
321
389
  });
322
- return this.viewOf(orgId, actor, saved);
390
+ return this.viewOf(actor, saved);
323
391
  }
324
- async setReaction(orgId, actor, messageId, emoji) {
392
+ async setReaction(messageId, emoji, actor) {
393
+ const orgId = actor.orgId;
325
394
  const message = await this.mustMessage(this.repo, orgId, messageId);
326
- await this.requireActor(orgId, actor, message.communityId);
395
+ await this.requireActor(actor, message.communityId);
327
396
  await this.requireActive(this.repo, orgId, message.chatId, actor.id);
328
397
  await this.uow.run(async ({ chats, outbox }) => {
329
- if (!await chats.setReaction(orgId, message.communityId, messageId, actor.id, emoji) || emoji === null) return;
330
- await outbox.append([discussionEvents.messageReactionAdded.make({
398
+ const change = await chats.setReaction(orgId, message.communityId, messageId, actor.id, emoji);
399
+ if (!change.changed) return;
400
+ const where = {
401
+ messageId,
402
+ chatId: message.chatId,
403
+ communityId: message.communityId,
404
+ orgUserId: actor.id
405
+ };
406
+ const events = [];
407
+ if (change.inserted && emoji !== null) events.push(discussionEvents.messageReactionAdded.make({
331
408
  orgId,
332
409
  data: {
333
- messageId,
334
- chatId: message.chatId,
335
- communityId: message.communityId,
336
- orgUserId: actor.id,
410
+ ...where,
337
411
  reaction: emoji
338
412
  },
339
413
  metadata: this.meta(actor)
340
- })]);
414
+ }));
415
+ events.push(discussionEvents.messageReactionsChanged.make({
416
+ orgId,
417
+ data: {
418
+ ...where,
419
+ reaction: emoji
420
+ },
421
+ metadata: this.meta(actor)
422
+ }));
423
+ await outbox.append(events);
341
424
  });
342
425
  this.logger.debug("message reaction set", {
343
426
  orgId,
@@ -345,22 +428,37 @@ var ChatServiceImpl = class {
345
428
  });
346
429
  return (await this.repo.reactionsOf(orgId, [messageId], actor.id))[messageId] ?? { reactions: {} };
347
430
  }
348
- async markRead(orgId, actor, chatId, messageId) {
431
+ async markRead(chatId, messageId, actor) {
432
+ const orgId = actor.orgId;
349
433
  const chat = await this.mustChat(this.repo, orgId, chatId);
350
- await this.requireActor(orgId, actor, chat.communityId);
434
+ await this.requireActor(actor, chat.communityId);
351
435
  await this.requireActive(this.repo, orgId, chat.id, actor.id);
352
436
  if ((await this.mustMessage(this.repo, orgId, messageId)).chatId !== chatId) throw new NotFoundError("Message", messageId);
353
- await this.repo.setRead(orgId, chatId, actor.id, messageId);
354
- this.logger.debug("chat marked read", {
437
+ const moved = await this.uow.run(async ({ chats, outbox }) => {
438
+ if (!await chats.setRead(orgId, chatId, actor.id, messageId)) return false;
439
+ await outbox.append([discussionEvents.chatRead.make({
440
+ orgId,
441
+ data: {
442
+ chatId,
443
+ communityId: chat.communityId,
444
+ orgUserId: actor.id,
445
+ lastReadMessageId: messageId
446
+ },
447
+ metadata: this.meta(actor)
448
+ })]);
449
+ return true;
450
+ });
451
+ this.logger.debug(moved ? "chat marked read" : "chat read mark kept", {
355
452
  orgId,
356
453
  chatId,
357
454
  messageId
358
455
  });
359
456
  }
360
- async block(orgId, actor, communityId, orgUserId) {
457
+ async block(communityId, orgUserId, actor) {
458
+ const orgId = actor.orgId;
361
459
  if (actor.id === orgUserId) throw new InvalidInputError("You cannot block yourself");
362
460
  await this.requireSettings(orgId, communityId);
363
- await this.requireActor(orgId, actor, communityId);
461
+ await this.requireActor(actor, communityId);
364
462
  await this.requirePerson(orgId, communityId, orgUserId);
365
463
  await this.uow.run(async ({ chats }) => {
366
464
  await chats.lockDirectPair(orgId, communityId, actor.id, orgUserId);
@@ -372,10 +470,11 @@ var ChatServiceImpl = class {
372
470
  actorId: actor.id
373
471
  });
374
472
  }
375
- async unblock(orgId, actor, communityId, orgUserId) {
473
+ async unblock(communityId, orgUserId, actor) {
474
+ const orgId = actor.orgId;
376
475
  if (actor.id === orgUserId) throw new InvalidInputError("You cannot block yourself");
377
476
  await this.requireSettings(orgId, communityId);
378
- await this.requireActor(orgId, actor, communityId);
477
+ await this.requireActor(actor, communityId);
379
478
  await this.uow.run(async ({ chats }) => {
380
479
  await chats.lockDirectPair(orgId, communityId, actor.id, orgUserId);
381
480
  await chats.unblock(orgId, communityId, actor.id, orgUserId);
@@ -386,9 +485,10 @@ var ChatServiceImpl = class {
386
485
  actorId: actor.id
387
486
  });
388
487
  }
389
- async flagMessage(orgId, actor, messageId, reason) {
488
+ async flagMessage(messageId, reason, actor) {
489
+ const orgId = actor.orgId;
390
490
  const message = await this.mustMessage(this.repo, orgId, messageId);
391
- await this.requireActor(orgId, actor, message.communityId);
491
+ await this.requireActor(actor, message.communityId);
392
492
  await this.requireActive(this.repo, orgId, message.chatId, actor.id);
393
493
  const now = /* @__PURE__ */ new Date();
394
494
  const saved = await this.uow.run(async ({ chats, outbox }) => {
@@ -424,11 +524,12 @@ var ChatServiceImpl = class {
424
524
  });
425
525
  return saved;
426
526
  }
427
- async resolveMessageFlags(orgId, actor, messageId, resolution, communityId) {
428
- if (!isStaffRole(actor.role)) throw new ForbiddenError("You cannot work this queue");
527
+ async resolveMessageFlags(messageId, resolution, communityId, actor) {
528
+ const orgId = actor.orgId;
429
529
  const message = await this.mustMessage(this.repo, orgId, messageId);
430
- if (communityId !== void 0 && message.communityId !== communityId) throw new NotFoundError("Message", messageId);
431
- await this.requireActor(orgId, actor, message.communityId);
530
+ if (communityId !== null && message.communityId !== communityId) throw new NotFoundError("Message", messageId);
531
+ this.requireModerator(actor, message.communityId, "resolve message flags");
532
+ await this.requireActor(actor, message.communityId);
432
533
  await this.repo.resolveFlags(orgId, messageId, {
433
534
  resolvedAt: /* @__PURE__ */ new Date(),
434
535
  resolvedBy: actor.id,
@@ -442,27 +543,18 @@ var ChatServiceImpl = class {
442
543
  actorId: actor.id
443
544
  });
444
545
  }
445
- async lock(orgId, actor, chatId, communityId) {
446
- return this.setLock(orgId, actor, chatId, true, communityId);
546
+ async lock(chatId, communityId, actor) {
547
+ return this.setLock(actor, chatId, true, communityId);
447
548
  }
448
- async unlock(orgId, actor, chatId, communityId) {
449
- return this.setLock(orgId, actor, chatId, false, communityId);
549
+ async unlock(chatId, communityId, actor) {
550
+ return this.setLock(actor, chatId, false, communityId);
450
551
  }
451
- async listChats(orgId, actor, communityId, query) {
552
+ async listChats(communityId, query, actor) {
553
+ const orgId = actor.orgId;
452
554
  await this.requireSettings(orgId, communityId);
453
- await this.requireActor(orgId, actor, communityId);
454
- const now = /* @__PURE__ */ new Date();
555
+ await this.requireActor(actor, communityId);
455
556
  const page = await this.repo.listForUser(orgId, communityId, actor.id, query);
456
- const subscribed = await this.subscriptions.forViewer(orgId, actor.id, "chat", page.rows.map((item) => item.chat.id));
457
- const subscriptionOf = new Map(subscribed.map((row) => [row.itemId, row]));
458
- const items = page.rows.map((item) => ({
459
- ...item,
460
- subscription: activeSubscription(subscriptionOf.get(item.chat.id), now),
461
- lastMessage: item.lastMessage && item.lastMessage.deletedAt !== null && !isStaffRole(actor.role) ? {
462
- ...item.lastMessage,
463
- body: ""
464
- } : item.lastMessage
465
- }));
557
+ const items = await this.listItems(actor, communityId, page.rows);
466
558
  this.logger.debug("chats listed", {
467
559
  orgId,
468
560
  communityId,
@@ -473,9 +565,100 @@ var ChatServiceImpl = class {
473
565
  rows: items
474
566
  };
475
567
  }
476
- async messages(orgId, actor, chatId, query) {
568
+ async chat(chatId, actor) {
569
+ const orgId = actor.orgId;
477
570
  const chat = await this.mustChat(this.repo, orgId, chatId);
478
- await this.requireActor(orgId, actor, chat.communityId);
571
+ await this.requireActor(actor, chat.communityId);
572
+ await this.requireActive(this.repo, orgId, chat.id, actor.id);
573
+ const row = await this.repo.inboxRow(orgId, chatId, actor.id);
574
+ if (!row) throw new NotFoundError("Chat", chatId);
575
+ const [item] = await this.listItems(actor, chat.communityId, [row]);
576
+ this.logger.debug("chat read", {
577
+ orgId,
578
+ chatId
579
+ });
580
+ return item;
581
+ }
582
+ async directChat(communityId, orgUserId, actor) {
583
+ const orgId = actor.orgId;
584
+ await this.requireSettings(orgId, communityId);
585
+ await this.requireActor(actor, communityId);
586
+ const chat = orgUserId === actor.id ? null : await this.repo.findDirect(orgId, communityId, actor.id, orgUserId);
587
+ const row = chat ? await this.repo.inboxRow(orgId, chat.id, actor.id) : null;
588
+ if (!row) throw new NotFoundError("Chat", orgUserId);
589
+ const [item] = await this.listItems(actor, communityId, [row]);
590
+ this.logger.debug("direct chat read", {
591
+ orgId,
592
+ communityId,
593
+ chatId: row.chat.id
594
+ });
595
+ return item;
596
+ }
597
+ async unreadSummary(communityId, actor) {
598
+ const orgId = actor.orgId;
599
+ await this.requireSettings(orgId, communityId);
600
+ await this.requireActor(actor, communityId);
601
+ return this.repo.unreadSummary(orgId, communityId, actor.id, /* @__PURE__ */ new Date());
602
+ }
603
+ async typing(chatId, actor) {
604
+ const orgId = actor.orgId;
605
+ const chat = await this.mustChat(this.repo, orgId, chatId);
606
+ await this.requireActor(actor, chat.communityId);
607
+ await this.requireActive(this.repo, orgId, chat.id, actor.id);
608
+ if (chat.lockedAt || chat.status === "closed") throw new ForbiddenError("You cannot send in this chat");
609
+ if (!this.pubsub) return;
610
+ const now = Date.now();
611
+ const key = `${orgId}:${chatId}:${actor.id}`;
612
+ const last = this.typed.get(key);
613
+ if (last !== void 0 && now - last < TYPING_REPEAT_MS) return;
614
+ this.typed.set(key, now);
615
+ for (const [seen, at] of this.typed) if (now - at >= TYPING_REPEAT_MS) this.typed.delete(seen);
616
+ await publishChange(this.pubsub, {
617
+ event: TYPING,
618
+ orgId,
619
+ communityId: chat.communityId,
620
+ chatId,
621
+ orgUserId: actor.id,
622
+ until: new Date(now + TYPING_FOR_MS).toISOString()
623
+ }, this.logger);
624
+ }
625
+ async watch(communityId, actor) {
626
+ if (!this.hub) throw new UnavailableError("Live chat is not available");
627
+ await this.requireSettings(actor.orgId, communityId);
628
+ await this.requireActor(actor, communityId);
629
+ return this.hub.open({
630
+ communityId,
631
+ actor
632
+ });
633
+ }
634
+ async messageViews(orgId, messageIds) {
635
+ const messages = await this.repo.messagesByIds(orgId, [...new Set(messageIds)]);
636
+ return (await this.withSenders(orgId, null, messages)).map(({ viewerReaction: _viewerReaction, ...view }) => view);
637
+ }
638
+ activeParticipants(orgId, chatId) {
639
+ return this.repo.activeParticipants(orgId, chatId);
640
+ }
641
+ async messageReactions(orgId, messageId) {
642
+ return (await this.repo.reactionsOf(orgId, [messageId], ""))[messageId]?.reactions ?? {};
643
+ }
644
+ async liveChat(orgId, chatId) {
645
+ const chat = await this.repo.findChat(orgId, chatId);
646
+ if (!chat) return null;
647
+ const participantIds = await this.repo.activeParticipants(orgId, chatId);
648
+ const profiles = await this.participantsOf(orgId, chat.communityId, participantIds);
649
+ return {
650
+ chat,
651
+ participantIds,
652
+ participants: participantIds.flatMap((id) => profiles.get(id) ?? [])
653
+ };
654
+ }
655
+ memberStates(orgId, communityId, orgUserIds) {
656
+ return this.repo.memberStates(orgId, communityId, orgUserIds, /* @__PURE__ */ new Date());
657
+ }
658
+ async messages(chatId, query, actor) {
659
+ const orgId = actor.orgId;
660
+ const chat = await this.mustChat(this.repo, orgId, chatId);
661
+ await this.requireActor(actor, chat.communityId);
479
662
  await this.requireActive(this.repo, orgId, chat.id, actor.id);
480
663
  const page = await this.repo.listMessages(orgId, chatId, query);
481
664
  this.logger.debug("chat messages read", {
@@ -487,18 +670,23 @@ var ChatServiceImpl = class {
487
670
  rows: await this.withSenders(orgId, actor, page.rows)
488
671
  };
489
672
  }
490
- async attachmentFile(orgId, actor, chatId, messageId, assetId) {
673
+ async attachmentFile(chatId, messageId, assetId, actor) {
674
+ const orgId = actor.orgId;
491
675
  const chat = await this.repo.findChat(orgId, chatId);
492
676
  const message = await this.repo.findMessage(orgId, messageId);
493
- if (!chat || !message || message.chatId !== chatId) return null;
494
- if (!((await this.repo.attachmentsOf(orgId, [messageId]))[messageId] ?? []).includes(assetId)) return null;
495
- if (!await this.access.isCommunityMember(orgId, actor, chat.communityId)) return null;
496
- 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;
497
- return this.images.signFile(orgId, assetId);
498
- }
499
- async messageQueue(orgId, actor, communityId) {
500
- if (!isStaffRole(actor.role)) throw new ForbiddenError("You cannot work this queue");
501
- await this.requireActor(orgId, actor, communityId);
677
+ if (!chat || !message || message.chatId !== chatId) throw new NotFoundError("Message attachment", assetId);
678
+ if (!((await this.repo.attachmentsOf(orgId, [messageId]))[messageId] ?? []).includes(assetId)) throw new NotFoundError("Message attachment", assetId);
679
+ if (!await this.access.isCommunityMember(chat.communityId, actor)) throw new NotFoundError("Message attachment", assetId);
680
+ const moderates = actor.permissions.has("communities:moderate", chat.communityId);
681
+ if (!((await this.repo.participants(orgId, chatId)).some((user) => user.orgUserId === actor.id && user.leftAt === null) ? message.deletedAt === null || moderates : moderates && (await this.repo.listOpenFlags(orgId, chat.communityId)).some((flag) => flag.messageId === messageId))) throw new NotFoundError("Message attachment", assetId);
682
+ const url = await this.images.signFile(orgId, assetId);
683
+ if (!url) throw new NotFoundError("Message attachment", assetId);
684
+ return url;
685
+ }
686
+ async messageQueue(communityId, actor) {
687
+ const orgId = actor.orgId;
688
+ this.requireModerator(actor, communityId, "message queue");
689
+ await this.requireActor(actor, communityId);
502
690
  await this.requireSettings(orgId, communityId);
503
691
  const flags = await this.repo.listOpenFlags(orgId, communityId, 100);
504
692
  const messages = await this.repo.messagesByIds(orgId, [...new Set(flags.map((flag) => flag.messageId))]);
@@ -529,14 +717,15 @@ var ChatServiceImpl = class {
529
717
  participants: await this.repo.participants(orgId, chatId)
530
718
  };
531
719
  }
532
- async titlesFor(orgId, actor, chatIds) {
720
+ async titlesFor(chatIds, actor) {
721
+ const orgId = actor.orgId;
533
722
  if (chatIds.length === 0) return {};
534
723
  const summaries = (await this.repo.chatSummaries(orgId, [...new Set(chatIds)])).filter((summary) => summary.participantIds.includes(actor.id));
535
724
  const others = [...new Set(summaries.flatMap((summary) => summary.participantIds.filter((id) => id !== actor.id)))];
536
725
  const records = others.length > 0 ? await this.profiles.authorsOf(orgId, others) : {};
537
726
  const nameOf = (id) => {
538
727
  const record = records[id];
539
- return record && authorView(orgId, record, null).displayName || "A member";
728
+ return record && authorName(record) || "A member";
540
729
  };
541
730
  return Object.fromEntries(summaries.flatMap((summary) => {
542
731
  const names = summary.participantIds.filter((id) => id !== actor.id).map(nameOf);
@@ -559,11 +748,12 @@ var ChatServiceImpl = class {
559
748
  isBlocked(orgId, communityId, a, b) {
560
749
  return this.repo.isBlocked(orgId, communityId, a, b);
561
750
  }
562
- async setLock(orgId, actor, chatId, locked, communityId) {
563
- if (!isStaffRole(actor.role)) throw new ForbiddenError("You cannot lock this chat");
751
+ async setLock(actor, chatId, locked, communityId) {
752
+ const orgId = actor.orgId;
564
753
  const chat = await this.mustChat(this.repo, orgId, chatId);
565
- if (communityId !== void 0 && chat.communityId !== communityId) throw new NotFoundError("Chat", chatId);
566
- await this.requireActor(orgId, actor, chat.communityId);
754
+ if (communityId !== null && chat.communityId !== communityId) throw new NotFoundError("Chat", chatId);
755
+ this.requireModerator(actor, chat.communityId, locked ? "lock chat" : "unlock chat");
756
+ await this.requireActor(actor, chat.communityId);
567
757
  if (Boolean(chat.lockedAt) === locked) return chat;
568
758
  const now = /* @__PURE__ */ new Date();
569
759
  const saved = await this.uow.run(async ({ chats, outbox }) => {
@@ -587,44 +777,62 @@ var ChatServiceImpl = class {
587
777
  });
588
778
  return saved;
589
779
  }
780
+ requireModerator(actor, communityId, action) {
781
+ if (!actor.permissions.has("communities:moderate", communityId)) {
782
+ this.logger.warn("chat moderation refused — not a moderator", {
783
+ orgId: actor.orgId,
784
+ communityId,
785
+ action,
786
+ actorId: actor.id
787
+ });
788
+ throw new ForbiddenError("You need communities:moderate");
789
+ }
790
+ }
590
791
  async requireSettings(orgId, communityId) {
591
- const settings = await this.content.getDmSettings(orgId, communityId);
792
+ const settings = await this.content.getDmSettings(communityId, this.authors.systemActor(orgId));
592
793
  if (!settings) throw new NotFoundError("Community", communityId);
593
794
  return settings;
594
795
  }
595
- async requireActor(orgId, actor, communityId) {
596
- if (!await this.access.isCommunityMember(orgId, actor, communityId)) throw new ForbiddenError("You are not a member of this community");
796
+ async requireActor(actor, communityId) {
797
+ if (!await this.access.isCommunityMember(communityId, actor)) throw new ForbiddenError("You are not a member of this community");
597
798
  }
799
+ /** Whether the person is team in the community; throws when they are not a
800
+ * member of it. */
598
801
  async requirePerson(orgId, communityId, orgUserId) {
599
- const role = await this.personRole(orgId, communityId, orgUserId);
600
- if (!role) throw new ForbiddenError("That person is not a member of this community");
601
- return role;
602
- }
603
- async personRole(orgId, communityId, orgUserId) {
604
- const role = await this.authors.roleOf(orgId, orgUserId);
605
- if (!role) return null;
606
- return await this.access.isCommunityMember(orgId, {
607
- id: orgUserId,
608
- role
609
- }, communityId) ? role : null;
610
- }
611
- async personRoles(orgId, communityId, orgUserIds) {
612
- const roles = await this.authors.rolesOf(orgId, orgUserIds);
613
- const members = new Set(await this.access.communityMembersAmong(orgId, communityId, Object.keys(roles)));
614
- return Object.fromEntries(Object.entries(roles).filter(([orgUserId]) => members.has(orgUserId)));
615
- }
616
- async assertMaySend(chats, orgId, actor, chat, users, settings, input) {
802
+ const team = (await this.membersTeam(orgId, communityId, [orgUserId])).get(orgUserId);
803
+ if (team === void 0) throw new ForbiddenError("That person is not a member of this community");
804
+ return team;
805
+ }
806
+ /** Each member of the community among the given people, with whether they
807
+ * are team there. People who are not members are left out. */
808
+ async membersTeam(orgId, communityId, orgUserIds) {
809
+ const members = await this.access.communityMembersAmong(orgId, communityId, orgUserIds);
810
+ if (members.length === 0) return /* @__PURE__ */ new Map();
811
+ const permissions = await this.authors.permissionsOfMany(orgId, members);
812
+ return new Map(members.map((orgUserId) => {
813
+ const held = permissions.get(orgUserId);
814
+ return [orgUserId, held !== void 0 && isTeam(held, communityId)];
815
+ }));
816
+ }
817
+ async teamsOf(orgId, communityId, orgUserIds) {
818
+ return {
819
+ asked: new Set(orgUserIds),
820
+ team: await this.membersTeam(orgId, communityId, orgUserIds)
821
+ };
822
+ }
823
+ async assertMaySend(chats, orgId, actor, chat, users, settings, input, teams) {
617
824
  this.assertActive(users, actor.id);
618
825
  if (chat.lockedAt) throw new ForbiddenError("This chat is locked");
619
826
  if (chat.status === "closed") throw new ForbiddenError("You cannot send in this chat");
620
827
  if (this.pendingCreator(chat, actor)) throw new ForbiddenError("You cannot send in this chat");
621
828
  const others = users.filter((other) => other.orgUserId !== actor.id && (chat.kind === "direct" || other.leftAt === null)).map((other) => other.orgUserId);
829
+ if (others.some((other) => !teams.asked.has(other))) throw new ConflictError("This chat changed while you were sending; send again");
622
830
  if (others.length > 0) {
623
- const roles = await this.personRoles(orgId, chat.communityId, others);
624
- const recipients = chat.kind === "group" ? others.filter((other) => roles[other]) : others;
831
+ const recipients = chat.kind === "group" ? others.filter((other) => teams.team.has(other)) : others;
832
+ const actorTeam = isTeam(actor.permissions, chat.communityId);
625
833
  for (const other of recipients) {
626
- const role = roles[other];
627
- if (!role || !this.mayMessage(settings, actor, role)) throw new ForbiddenError("You cannot send in this chat");
834
+ const team = teams.team.get(other);
835
+ if (team === void 0 || !this.mayMessage(settings, actorTeam, team)) throw new ForbiddenError("You cannot send in this chat");
628
836
  }
629
837
  if (recipients.length > 0) {
630
838
  if ((await chats.blockedWith(orgId, chat.communityId, actor.id, recipients)).length > 0) throw new ForbiddenError("You cannot send in this chat");
@@ -634,7 +842,6 @@ var ChatServiceImpl = class {
634
842
  const reply = await chats.findMessage(orgId, input.replyToId);
635
843
  if (!reply || reply.chatId !== chat.id) throw new NotFoundError("Message", input.replyToId);
636
844
  }
637
- await this.requireOwnedImages(orgId, actor.id, input.assetIds);
638
845
  }
639
846
  mentionable(body, users) {
640
847
  return keepAllowed(mentionTokens(body), users.filter((user) => user.leftAt === null).map((user) => user.orgUserId));
@@ -653,17 +860,17 @@ var ChatServiceImpl = class {
653
860
  mentioned
654
861
  };
655
862
  }
656
- assertMayMessage(settings, actor, role) {
863
+ assertMayMessage(settings, actorTeam, targetTeam) {
657
864
  if (!settings.enabled) throw new ForbiddenError("Direct messages are off");
658
- if (!this.mayMessage(settings, actor, role)) throw new ForbiddenError("You cannot message this person");
865
+ if (!this.mayMessage(settings, actorTeam, targetTeam)) throw new ForbiddenError("You cannot message this person");
659
866
  }
660
867
  assertGroup(settings) {
661
868
  if (!settings.enabled) throw new ForbiddenError("Direct messages are off");
662
869
  if (!settings.groupChats) throw new ForbiddenError("Group chats are off");
663
870
  }
664
- mayMessage(settings, actor, target) {
665
- if (isStaffRole(actor.role)) return settings.staffToAnyone;
666
- if (isStaffRole(target)) return settings.memberToStaff;
871
+ mayMessage(settings, actorTeam, targetTeam) {
872
+ if (actorTeam) return settings.teamToAnyone;
873
+ if (targetTeam) return settings.memberToTeam;
667
874
  return settings.memberToMember;
668
875
  }
669
876
  async requireActive(repo, orgId, chatId, orgUserId) {
@@ -683,10 +890,10 @@ var ChatServiceImpl = class {
683
890
  throw new NotFoundError("Asset", assetId);
684
891
  }
685
892
  }
686
- async requireWritableChat(orgId, actor, chatId) {
687
- const chat = await this.mustChat(this.repo, orgId, chatId);
688
- await this.requireActor(orgId, actor, chat.communityId);
689
- await this.requireActive(this.repo, orgId, chat.id, actor.id);
893
+ async requireWritableChat(actor, chatId) {
894
+ const chat = await this.mustChat(this.repo, actor.orgId, chatId);
895
+ await this.requireActor(actor, chat.communityId);
896
+ await this.requireActive(this.repo, actor.orgId, chat.id, actor.id);
690
897
  if (chat.lockedAt || chat.status === "closed") throw new ForbiddenError("You cannot send in this chat");
691
898
  }
692
899
  pendingCreator(chat, actor) {
@@ -729,6 +936,7 @@ var ChatServiceImpl = class {
729
936
  orgUserId: actor.id,
730
937
  body: input.body,
731
938
  replyToId: input.replyToId,
939
+ clientId: input.clientId ?? null,
732
940
  editedAt: null,
733
941
  deletedAt: null,
734
942
  createdAt: now,
@@ -745,7 +953,7 @@ var ChatServiceImpl = class {
745
953
  };
746
954
  }
747
955
  meta(actor) {
748
- return { actorOrgUserId: actor.id };
956
+ return { actorId: actor.id };
749
957
  }
750
958
  createdEvent(orgId, actor, chat, users) {
751
959
  return discussionEvents.chatCreated.make({
@@ -777,7 +985,7 @@ var ChatServiceImpl = class {
777
985
  return discussionEvents.messageSent.make({
778
986
  orgId,
779
987
  data: {
780
- ...message,
988
+ ...eventMessage(message),
781
989
  mentionedOrgUserIds
782
990
  },
783
991
  metadata: {
@@ -786,43 +994,67 @@ var ChatServiceImpl = class {
786
994
  }
787
995
  });
788
996
  }
789
- async viewOf(orgId, actor, message) {
790
- const [view] = await this.withSenders(orgId, actor, [message]);
997
+ async viewOf(actor, message) {
998
+ const [view] = await this.withSenders(actor.orgId, actor, [message]);
791
999
  return view;
792
1000
  }
793
- async withSenders(orgId, actor, messages) {
1001
+ async withSenders(orgId, viewer, messages) {
794
1002
  if (messages.length === 0) return [];
795
1003
  const messageIds = messages.map((message) => message.id);
796
- const [records, attached, reactions, mentioned] = await Promise.all([
797
- this.profiles.authorsOf(orgId, [...new Set(messages.map((message) => message.orgUserId))]),
1004
+ const senderIds = [...new Set(messages.map((message) => message.orgUserId))];
1005
+ const [records, permissions, attached, reactions, mentioned] = await Promise.all([
1006
+ this.profiles.authorsOf(orgId, senderIds),
1007
+ this.authors.permissionsOfMany(orgId, senderIds),
798
1008
  this.repo.attachmentsOf(orgId, messageIds),
799
- this.repo.reactionsOf(orgId, messageIds, actor.id),
1009
+ this.repo.reactionsOf(orgId, messageIds, viewer?.id ?? ""),
800
1010
  mentionViews(this.mentions, this.profiles, orgId, "message", messageIds)
801
1011
  ]);
802
- const shown = (message) => message.deletedAt !== null && !isStaffRole(actor.role) ? [] : attached[message.id] ?? [];
1012
+ const hidden = (message) => message.deletedAt !== null && !viewer?.permissions.has("communities:moderate", message.communityId);
1013
+ const shown = (message) => hidden(message) ? [] : attached[message.id] ?? [];
803
1014
  return messages.map((message) => {
804
1015
  const record = records[message.orgUserId];
805
1016
  if (!record) throw new NotFoundError("Member", message.orgUserId);
806
1017
  const counts = reactions[message.id];
807
1018
  return {
808
- ...this.present(message, actor),
809
- sender: authorView(orgId, record, this.fileUrls),
1019
+ ...hidden(message) ? {
1020
+ ...message,
1021
+ body: ""
1022
+ } : message,
1023
+ sender: authorView(orgId, record, this.fileUrls, teamBadge(permissions.get(message.orgUserId), message.communityId)),
810
1024
  attachments: shown(message).map((assetId) => ({
811
1025
  assetId,
812
1026
  url: this.fileUrls.messageAttachment(message.chatId, message.id, assetId)
813
1027
  })),
814
1028
  reactions: counts?.reactions ?? {},
815
1029
  viewerReaction: counts?.viewerReaction ?? null,
816
- mentions: message.deletedAt !== null && !isStaffRole(actor.role) ? [] : mentioned[message.id] ?? []
1030
+ mentions: hidden(message) ? [] : mentioned[message.id] ?? []
817
1031
  };
818
1032
  });
819
1033
  }
820
- present(message, actor) {
821
- if (message.deletedAt !== null && !isStaffRole(actor.role)) return {
822
- ...message,
823
- body: ""
824
- };
825
- return message;
1034
+ async listItems(actor, communityId, rows) {
1035
+ const orgId = actor.orgId;
1036
+ const seesDeleted = actor.permissions.has("communities:moderate", communityId);
1037
+ const now = /* @__PURE__ */ new Date();
1038
+ const [subscribed, profiles] = await Promise.all([this.subscriptions.forViewer(orgId, actor.id, "chat", rows.map((item) => item.chat.id)), this.participantsOf(orgId, communityId, rows.flatMap((item) => item.participantIds))]);
1039
+ const subscriptionOf = new Map(subscribed.map((row) => [row.itemId, row]));
1040
+ return rows.map((item) => ({
1041
+ ...item,
1042
+ participants: item.participantIds.flatMap((id) => profiles.get(id) ?? []),
1043
+ subscription: activeSubscription(subscriptionOf.get(item.chat.id), now),
1044
+ lastMessage: item.lastMessage && item.lastMessage.deletedAt !== null && !seesDeleted ? {
1045
+ ...item.lastMessage,
1046
+ body: ""
1047
+ } : item.lastMessage
1048
+ }));
1049
+ }
1050
+ async participantsOf(orgId, communityId, orgUserIds) {
1051
+ const ids = [...new Set(orgUserIds)];
1052
+ if (ids.length === 0) return /* @__PURE__ */ new Map();
1053
+ const [records, permissions] = await Promise.all([this.profiles.authorsOf(orgId, ids), this.authors.permissionsOfMany(orgId, ids)]);
1054
+ return new Map(ids.flatMap((id) => {
1055
+ const record = records[id];
1056
+ return record ? [[id, authorView(orgId, record, this.fileUrls, teamBadge(permissions.get(id), communityId))]] : [];
1057
+ }));
826
1058
  }
827
1059
  };
828
1060
  //#endregion