@headless-lms/core 0.3.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 (307) hide show
  1. package/dist/assets/events.d.ts +24 -18
  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 +5 -4
  6. package/dist/assets/index.js +4 -3
  7. package/dist/assets/jobs.d.ts +15 -0
  8. package/dist/assets/jobs.js +30 -0
  9. package/dist/assets/jobs.js.map +1 -0
  10. package/dist/assets/ports.d.ts +37 -17
  11. package/dist/assets/service.d.ts +30 -15
  12. package/dist/assets/service.js +229 -59
  13. package/dist/assets/service.js.map +1 -1
  14. package/dist/automations/actions.js +2 -63
  15. package/dist/automations/actions.js.map +1 -1
  16. package/dist/automations/catalog.d.ts +1 -2
  17. package/dist/automations/catalog.js +137 -65
  18. package/dist/automations/catalog.js.map +1 -1
  19. package/dist/automations/community-derivations.js +20 -20
  20. package/dist/automations/community-derivations.js.map +1 -1
  21. package/dist/automations/community-mail.d.ts +36 -20
  22. package/dist/automations/community-mail.js +32 -24
  23. package/dist/automations/community-mail.js.map +1 -1
  24. package/dist/automations/event-permissions.d.ts +12 -0
  25. package/dist/automations/event-permissions.js +38 -0
  26. package/dist/automations/event-permissions.js.map +1 -0
  27. package/dist/automations/events.d.ts +0 -10
  28. package/dist/automations/index.d.ts +12 -4
  29. package/dist/automations/index.js +9 -1
  30. package/dist/automations/notifications/applicable.d.ts +8 -0
  31. package/dist/automations/notifications/applicable.js +12 -0
  32. package/dist/automations/notifications/applicable.js.map +1 -0
  33. package/dist/automations/notifications/debounce.d.ts +25 -0
  34. package/dist/automations/notifications/debounce.js +93 -0
  35. package/dist/automations/notifications/debounce.js.map +1 -0
  36. package/dist/automations/notifications/defaults.d.ts +8 -0
  37. package/dist/automations/notifications/defaults.js +346 -0
  38. package/dist/automations/notifications/defaults.js.map +1 -0
  39. package/dist/automations/notifications/delivery.d.ts +28 -0
  40. package/dist/automations/notifications/delivery.js +88 -0
  41. package/dist/automations/notifications/delivery.js.map +1 -0
  42. package/dist/automations/notifications/processor.d.ts +64 -0
  43. package/dist/automations/notifications/processor.js +270 -0
  44. package/dist/automations/notifications/processor.js.map +1 -0
  45. package/dist/automations/notifications/rules/content.d.ts +6 -0
  46. package/dist/automations/notifications/rules/content.js +96 -0
  47. package/dist/automations/notifications/rules/content.js.map +1 -0
  48. package/dist/automations/notifications/rules/discussion.d.ts +7 -0
  49. package/dist/automations/notifications/rules/discussion.js +678 -0
  50. package/dist/automations/notifications/rules/discussion.js.map +1 -0
  51. package/dist/automations/notifications/rules/entitlements.d.ts +6 -0
  52. package/dist/automations/notifications/rules/entitlements.js +276 -0
  53. package/dist/automations/notifications/rules/entitlements.js.map +1 -0
  54. package/dist/automations/notifications/rules/events.d.ts +9 -0
  55. package/dist/automations/notifications/rules/events.js +27 -0
  56. package/dist/automations/notifications/rules/events.js.map +1 -0
  57. package/dist/automations/notifications/rules/index.d.ts +6 -0
  58. package/dist/automations/notifications/rules/index.js +15 -0
  59. package/dist/automations/notifications/rules/index.js.map +1 -0
  60. package/dist/automations/notifications/rules/organizations.d.ts +6 -0
  61. package/dist/automations/notifications/rules/organizations.js +35 -0
  62. package/dist/automations/notifications/rules/organizations.js.map +1 -0
  63. package/dist/automations/notifications/rules/recipients.d.ts +32 -0
  64. package/dist/automations/notifications/rules/recipients.js +98 -0
  65. package/dist/automations/notifications/rules/recipients.js.map +1 -0
  66. package/dist/automations/notifications/service.d.ts +29 -0
  67. package/dist/automations/notifications/service.js +45 -0
  68. package/dist/automations/notifications/service.js.map +1 -0
  69. package/dist/automations/notifications/types.d.ts +7 -0
  70. package/dist/automations/notifications/types.js +560 -0
  71. package/dist/automations/notifications/types.js.map +1 -0
  72. package/dist/automations/ports.d.ts +134 -21
  73. package/dist/automations/service.d.ts +30 -30
  74. package/dist/automations/service.js +111 -144
  75. package/dist/automations/service.js.map +1 -1
  76. package/dist/content/event-sight.d.ts +11 -0
  77. package/dist/content/event-sight.js +11 -0
  78. package/dist/content/event-sight.js.map +1 -0
  79. package/dist/content/events.d.ts +437 -46
  80. package/dist/content/events.js +45 -1
  81. package/dist/content/events.js.map +1 -1
  82. package/dist/content/index.d.ts +8 -4
  83. package/dist/content/index.js +5 -1
  84. package/dist/content/jobs.d.ts +16 -0
  85. package/dist/content/jobs.js +37 -0
  86. package/dist/content/jobs.js.map +1 -0
  87. package/dist/content/model.d.ts +2 -2
  88. package/dist/content/notification-categories.d.ts +37 -0
  89. package/dist/content/notification-categories.js +73 -0
  90. package/dist/content/notification-categories.js.map +1 -0
  91. package/dist/content/ports.d.ts +132 -68
  92. package/dist/content/service.d.ts +100 -68
  93. package/dist/content/service.js +703 -146
  94. package/dist/content/service.js.map +1 -1
  95. package/dist/content/titles.d.ts +25 -0
  96. package/dist/content/titles.js +82 -0
  97. package/dist/content/titles.js.map +1 -0
  98. package/dist/content/types.d.ts +2 -2
  99. package/dist/discussion/authors.d.ts +4 -2
  100. package/dist/discussion/authors.js +9 -5
  101. package/dist/discussion/authors.js.map +1 -1
  102. package/dist/discussion/chats.d.ts +63 -39
  103. package/dist/discussion/chats.js +455 -190
  104. package/dist/discussion/chats.js.map +1 -1
  105. package/dist/discussion/digests.d.ts +24 -0
  106. package/dist/discussion/digests.js +104 -0
  107. package/dist/discussion/digests.js.map +1 -0
  108. package/dist/discussion/events.d.ts +227 -23
  109. package/dist/discussion/events.js +114 -19
  110. package/dist/discussion/events.js.map +1 -1
  111. package/dist/discussion/index.d.ts +12 -4
  112. package/dist/discussion/index.js +9 -2
  113. package/dist/discussion/jobs.d.ts +16 -0
  114. package/dist/discussion/jobs.js +40 -0
  115. package/dist/discussion/jobs.js.map +1 -0
  116. package/dist/discussion/live.d.ts +106 -0
  117. package/dist/discussion/live.js +642 -0
  118. package/dist/discussion/live.js.map +1 -0
  119. package/dist/discussion/mentions.d.ts +11 -0
  120. package/dist/discussion/mentions.js +35 -0
  121. package/dist/discussion/mentions.js.map +1 -0
  122. package/dist/discussion/ports.d.ts +279 -114
  123. package/dist/discussion/ports.js.map +1 -1
  124. package/dist/discussion/posts.d.ts +60 -44
  125. package/dist/discussion/posts.js +453 -287
  126. package/dist/discussion/posts.js.map +1 -1
  127. package/dist/discussion/rules.d.ts +18 -0
  128. package/dist/discussion/rules.js +22 -0
  129. package/dist/discussion/rules.js.map +1 -0
  130. package/dist/discussion/service.d.ts +50 -35
  131. package/dist/discussion/service.js +333 -205
  132. package/dist/discussion/service.js.map +1 -1
  133. package/dist/discussion/subscriptions.d.ts +37 -0
  134. package/dist/discussion/subscriptions.js +115 -0
  135. package/dist/discussion/subscriptions.js.map +1 -0
  136. package/dist/discussion/titles.d.ts +28 -0
  137. package/dist/discussion/titles.js +98 -0
  138. package/dist/discussion/titles.js.map +1 -0
  139. package/dist/entitlements/events.d.ts +20 -20
  140. package/dist/entitlements/index.d.ts +5 -4
  141. package/dist/entitlements/index.js +3 -2
  142. package/dist/entitlements/jobs.d.ts +12 -0
  143. package/dist/entitlements/jobs.js +18 -0
  144. package/dist/entitlements/jobs.js.map +1 -0
  145. package/dist/entitlements/model.d.ts +2 -2
  146. package/dist/entitlements/ports.d.ts +68 -38
  147. package/dist/entitlements/rules.d.ts +3 -6
  148. package/dist/entitlements/rules.js +5 -10
  149. package/dist/entitlements/rules.js.map +1 -1
  150. package/dist/entitlements/service.d.ts +43 -28
  151. package/dist/entitlements/service.js +387 -160
  152. package/dist/entitlements/service.js.map +1 -1
  153. package/dist/identity/devices.d.ts +30 -0
  154. package/dist/identity/devices.js +109 -0
  155. package/dist/identity/devices.js.map +1 -0
  156. package/dist/identity/index.d.ts +6 -5
  157. package/dist/identity/index.js +4 -3
  158. package/dist/identity/model.d.ts +2 -2
  159. package/dist/identity/notification-settings.d.ts +28 -0
  160. package/dist/identity/notification-settings.js +114 -0
  161. package/dist/identity/notification-settings.js.map +1 -0
  162. package/dist/identity/ports.d.ts +80 -56
  163. package/dist/identity/service.d.ts +8 -7
  164. package/dist/identity/service.js +17 -13
  165. package/dist/identity/service.js.map +1 -1
  166. package/dist/integrations/ports.d.ts +9 -8
  167. package/dist/integrations/service.d.ts +9 -8
  168. package/dist/integrations/service.js +33 -16
  169. package/dist/integrations/service.js.map +1 -1
  170. package/dist/organizations/default-roles.d.ts +22 -0
  171. package/dist/organizations/default-roles.js +89 -0
  172. package/dist/organizations/default-roles.js.map +1 -0
  173. package/dist/organizations/effective-permissions.d.ts +45 -0
  174. package/dist/organizations/effective-permissions.js +113 -0
  175. package/dist/organizations/effective-permissions.js.map +1 -0
  176. package/dist/organizations/events.d.ts +320 -181
  177. package/dist/organizations/events.js +90 -18
  178. package/dist/organizations/events.js.map +1 -1
  179. package/dist/organizations/index.d.ts +15 -10
  180. package/dist/organizations/index.js +8 -3
  181. package/dist/organizations/members.d.ts +66 -8
  182. package/dist/organizations/members.js.map +1 -1
  183. package/dist/organizations/notification-catalog.d.ts +31 -0
  184. package/dist/organizations/notification-catalog.js +32 -0
  185. package/dist/organizations/notification-catalog.js.map +1 -0
  186. package/dist/organizations/notification-categories.d.ts +34 -0
  187. package/dist/organizations/notification-categories.js +66 -0
  188. package/dist/organizations/notification-categories.js.map +1 -0
  189. package/dist/organizations/org-roles.d.ts +33 -0
  190. package/dist/organizations/org-roles.js +49 -0
  191. package/dist/organizations/org-roles.js.map +1 -0
  192. package/dist/organizations/permissions.d.ts +311 -0
  193. package/dist/organizations/permissions.js +327 -0
  194. package/dist/organizations/permissions.js.map +1 -0
  195. package/dist/organizations/ports.d.ts +187 -65
  196. package/dist/organizations/service.d.ts +104 -35
  197. package/dist/organizations/service.js +1247 -323
  198. package/dist/organizations/service.js.map +1 -1
  199. package/dist/organizations/testing.d.ts +18 -0
  200. package/dist/organizations/testing.js +37 -0
  201. package/dist/organizations/testing.js.map +1 -0
  202. package/dist/organizations/types.d.ts +5 -16
  203. package/dist/progress/index.d.ts +2 -2
  204. package/dist/progress/ports.d.ts +11 -3
  205. package/dist/progress/service.d.ts +6 -5
  206. package/dist/progress/service.js +23 -8
  207. package/dist/progress/service.js.map +1 -1
  208. package/dist/reporting/admin/index.d.ts +6 -4
  209. package/dist/reporting/admin/model.d.ts +36 -3
  210. package/dist/reporting/admin/ports.d.ts +39 -14
  211. package/dist/reporting/admin/service.d.ts +32 -11
  212. package/dist/reporting/admin/service.js +102 -18
  213. package/dist/reporting/admin/service.js.map +1 -1
  214. package/dist/reporting/events/index.d.ts +27 -0
  215. package/dist/reporting/events/index.js +20 -0
  216. package/dist/reporting/events/index.js.map +1 -0
  217. package/dist/reporting/member/calendar.d.ts +11 -0
  218. package/dist/reporting/member/calendar.js +75 -0
  219. package/dist/reporting/member/calendar.js.map +1 -0
  220. package/dist/reporting/member/index.d.ts +3 -2
  221. package/dist/reporting/member/model.d.ts +80 -26
  222. package/dist/reporting/member/ports.d.ts +50 -28
  223. package/dist/reporting/member/service.d.ts +81 -35
  224. package/dist/reporting/member/service.js +438 -145
  225. package/dist/reporting/member/service.js.map +1 -1
  226. package/dist/shared/actor.d.ts +22 -3
  227. package/dist/shared/errors.d.ts +15 -2
  228. package/dist/shared/errors.js +21 -2
  229. package/dist/shared/errors.js.map +1 -1
  230. package/dist/shared/id.d.ts +7 -0
  231. package/dist/shared/id.js +8 -1
  232. package/dist/shared/id.js.map +1 -1
  233. package/dist/shared/logger.d.ts +2 -1
  234. package/dist/shared/logger.js +10 -1
  235. package/dist/shared/logger.js.map +1 -1
  236. package/dist/shared/mailer.d.ts +11 -1
  237. package/dist/shared/mailer.js +32 -5
  238. package/dist/shared/mailer.js.map +1 -1
  239. package/dist/shared/notification-categories.d.ts +16 -0
  240. package/dist/shared/notification-categories.js +94 -0
  241. package/dist/shared/notification-categories.js.map +1 -0
  242. package/dist/shared/notification-settings.d.ts +14 -0
  243. package/dist/shared/notification-settings.js +61 -0
  244. package/dist/shared/notification-settings.js.map +1 -0
  245. package/dist/shared/ports.d.ts +68 -2
  246. package/dist/shared/settings.d.ts +2 -1
  247. package/dist/shared/settings.js +1 -0
  248. package/dist/shared/settings.js.map +1 -1
  249. package/dist/types/automations.d.ts +8 -2
  250. package/dist/types/content.d.ts +2 -2
  251. package/dist/types/discussion.d.ts +4 -5
  252. package/dist/types/email-templates.d.ts +119 -6
  253. package/dist/types/entitlements.d.ts +2 -2
  254. package/dist/types/identity.d.ts +14 -17
  255. package/dist/types/index.d.ts +13 -12
  256. package/dist/types/index.js +2 -1
  257. package/dist/types/notifications.d.ts +157 -0
  258. package/dist/types/notifications.js +1 -0
  259. package/dist/types/organizations.d.ts +3 -2
  260. package/dist/types/ports.d.ts +8 -1
  261. package/dist/types/ports.js +7 -1
  262. package/dist/types/ports.js.map +1 -0
  263. package/dist/types/schemas/assets.d.ts +5 -5
  264. package/dist/types/schemas/assets.js +4 -3
  265. package/dist/types/schemas/assets.js.map +1 -1
  266. package/dist/types/schemas/automations.d.ts +1 -10
  267. package/dist/types/schemas/automations.js +1 -3
  268. package/dist/types/schemas/automations.js.map +1 -1
  269. package/dist/types/schemas/content.d.ts +596 -11
  270. package/dist/types/schemas/content.js +225 -9
  271. package/dist/types/schemas/content.js.map +1 -1
  272. package/dist/types/schemas/discussion.d.ts +362 -66
  273. package/dist/types/schemas/discussion.js +123 -15
  274. package/dist/types/schemas/discussion.js.map +1 -1
  275. package/dist/types/schemas/entitlements.d.ts +14 -1
  276. package/dist/types/schemas/entitlements.js +11 -4
  277. package/dist/types/schemas/entitlements.js.map +1 -1
  278. package/dist/types/schemas/identity.d.ts +1 -85
  279. package/dist/types/schemas/identity.js +1 -42
  280. package/dist/types/schemas/identity.js.map +1 -1
  281. package/dist/types/schemas/index.d.ts +8 -7
  282. package/dist/types/schemas/index.js +8 -7
  283. package/dist/types/schemas/notifications.d.ts +96 -0
  284. package/dist/types/schemas/notifications.js +57 -0
  285. package/dist/types/schemas/notifications.js.map +1 -0
  286. package/dist/types/schemas/organizations.d.ts +96 -60
  287. package/dist/types/schemas/organizations.js +78 -27
  288. package/dist/types/schemas/organizations.js.map +1 -1
  289. package/dist/types/schemas/progress.d.ts +0 -1
  290. package/dist/types/schemas/progress.js +0 -1
  291. package/dist/types/schemas/progress.js.map +1 -1
  292. package/package.json +1 -1
  293. package/dist/automations/community-notifications.d.ts +0 -16
  294. package/dist/automations/community-notifications.js +0 -84
  295. package/dist/automations/community-notifications.js.map +0 -1
  296. package/dist/automations/seeds.d.ts +0 -17
  297. package/dist/automations/seeds.js +0 -235
  298. package/dist/automations/seeds.js.map +0 -1
  299. package/dist/identity/notifications.d.ts +0 -27
  300. package/dist/identity/notifications.js +0 -101
  301. package/dist/identity/notifications.js.map +0 -1
  302. package/dist/organizations/access-scopes.d.ts +0 -19
  303. package/dist/organizations/access-scopes.js +0 -89
  304. package/dist/organizations/access-scopes.js.map +0 -1
  305. package/dist/organizations/roles.d.ts +0 -19
  306. package/dist/organizations/roles.js +0 -82
  307. package/dist/organizations/roles.js.map +0 -1
@@ -1,45 +1,76 @@
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";
6
+ import { keepAllowed, mentionTokens, mentionViews } from "./mentions.js";
7
+ import { isTeam, teamBadge } from "./rules.js";
8
+ import { activeSubscription } from "./subscriptions.js";
7
9
  import "./ports.js";
10
+ import { ChatHub, TYPING, publishChange } from "./live.js";
8
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
+ }
9
18
  var ChatServiceImpl = class {
19
+ /** This process's live chat hub; null without a PubSub. */
20
+ hub;
10
21
  repo;
11
22
  access;
12
23
  content;
13
24
  authors;
14
25
  profiles;
26
+ subscriptions;
27
+ mentions;
15
28
  images;
16
29
  fileUrls;
17
30
  uow;
31
+ pubsub;
18
32
  logger;
33
+ typed = /* @__PURE__ */ new Map();
19
34
  constructor(params) {
20
35
  this.repo = params.repo;
21
36
  this.access = params.access;
22
37
  this.content = params.content;
23
38
  this.authors = params.authors;
24
39
  this.profiles = params.profiles;
40
+ this.subscriptions = params.subscriptions;
41
+ this.mentions = params.mentions;
25
42
  this.images = params.images;
26
43
  this.fileUrls = params.fileUrls;
27
44
  this.uow = params.uow;
45
+ this.pubsub = params.pubsub ?? null;
28
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;
29
53
  }
30
- async startDirect(orgId, actor, input) {
54
+ async startDirect(input, actor) {
55
+ const orgId = actor.orgId;
31
56
  if (actor.id === input.orgUserId) throw new InvalidInputError("You cannot message yourself");
32
57
  if (input.body === null && input.assetIds.length > 0) throw new InvalidInputError("An attachment needs a message");
33
58
  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);
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);
37
63
  const first = input.body === null ? null : {
38
64
  body: input.body,
39
65
  replyToId: null,
40
66
  assetIds: input.assetIds
41
67
  };
42
- const saved = await this.uow.run(async ({ chats, outbox }) => {
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
+ };
73
+ const saved = await this.uow.run(async ({ chats, mentions, outbox }) => {
43
74
  await chats.lockDirectPair(orgId, input.communityId, actor.id, input.orgUserId);
44
75
  const existing = await chats.findDirect(orgId, input.communityId, actor.id, input.orgUserId);
45
76
  if (existing) {
@@ -49,22 +80,21 @@ var ChatServiceImpl = class {
49
80
  ...user,
50
81
  leftAt: null
51
82
  } : user);
52
- 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);
53
84
  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;
85
+ const written = first ? await this.writeMessage(chats, mentions, orgId, actor, existing, present, first) : null;
55
86
  const chat = written?.chat ?? existing;
56
87
  const events = [];
57
88
  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));
89
+ if (written) events.push(this.sentEvent(orgId, actor, written.chat, written.message, written.mentioned));
59
90
  if (events.length > 0) await outbox.append(events);
60
91
  return {
61
92
  chat,
62
93
  created: false
63
94
  };
64
95
  }
65
- await this.requireOwnedImages(orgId, actor.id, input.assetIds);
66
96
  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);
97
+ const request = settings.firstMessageIsRequest && !actorTeam && !targetTeam;
68
98
  if (request && !first) throw new InvalidInputError("A message request needs a message");
69
99
  const now = /* @__PURE__ */ new Date();
70
100
  const chat = await chats.insertChat(orgId, {
@@ -83,8 +113,8 @@ var ChatServiceImpl = class {
83
113
  for (const user of users) await chats.join(orgId, user);
84
114
  const events = [this.createdEvent(orgId, actor, chat, users)];
85
115
  if (first) {
86
- const written = await this.writeMessage(chats, orgId, actor, chat, first);
87
- events.push(this.sentEvent(orgId, actor, written.chat, written.message));
116
+ const written = await this.writeMessage(chats, mentions, orgId, actor, chat, users, first);
117
+ events.push(this.sentEvent(orgId, actor, written.chat, written.message, written.mentioned));
88
118
  }
89
119
  await outbox.append(events);
90
120
  return {
@@ -106,16 +136,18 @@ var ChatServiceImpl = class {
106
136
  });
107
137
  return saved.chat;
108
138
  }
109
- async startGroup(orgId, actor, input) {
139
+ async startGroup(input, actor) {
140
+ const orgId = actor.orgId;
110
141
  const settings = await this.requireSettings(orgId, input.communityId);
111
142
  this.assertGroup(settings);
112
- await this.requireActor(orgId, actor, input.communityId);
143
+ await this.requireActor(actor, input.communityId);
113
144
  const others = [...new Set(input.orgUserIds.filter((id) => id !== actor.id))].sort();
114
145
  if (others.length < 2) throw new InvalidInputError("A group chat needs three people");
115
146
  if (others.length + 1 > settings.groupSize) throw new ConflictError("This group is full");
147
+ const actorTeam = isTeam(actor.permissions, input.communityId);
116
148
  for (const orgUserId of others) {
117
- const role = await this.requirePerson(orgId, input.communityId, orgUserId);
118
- this.assertMayMessage(settings, actor, role);
149
+ const targetTeam = await this.requirePerson(orgId, input.communityId, orgUserId);
150
+ this.assertMayMessage(settings, actorTeam, targetTeam);
119
151
  }
120
152
  const saved = await this.uow.run(async ({ chats, outbox }) => {
121
153
  if (await chats.anyBlockedAmong(orgId, input.communityId, [actor.id, ...others])) throw new ForbiddenError("You cannot message this person");
@@ -146,15 +178,16 @@ var ChatServiceImpl = class {
146
178
  });
147
179
  return saved;
148
180
  }
149
- async addParticipant(orgId, actor, chatId, orgUserId) {
181
+ async addParticipant(chatId, orgUserId, actor) {
182
+ const orgId = actor.orgId;
150
183
  const chat = await this.mustChat(this.repo, orgId, chatId);
151
- await this.requireActor(orgId, actor, chat.communityId);
184
+ await this.requireActor(actor, chat.communityId);
152
185
  await this.requireActive(this.repo, orgId, chat.id, actor.id);
153
186
  if (chat.kind !== "group") throw new ConflictError("This chat is not a group");
154
187
  const settings = await this.requireSettings(orgId, chat.communityId);
155
188
  this.assertGroup(settings);
156
- const role = await this.requirePerson(orgId, chat.communityId, orgUserId);
157
- 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);
158
191
  const saved = await this.uow.run(async ({ chats, outbox }) => {
159
192
  const current = await this.mustChat(chats, orgId, chatId);
160
193
  const users = await chats.participants(orgId, chatId);
@@ -177,9 +210,10 @@ var ChatServiceImpl = class {
177
210
  });
178
211
  return saved;
179
212
  }
180
- async leave(orgId, actor, chatId) {
213
+ async leave(chatId, actor) {
214
+ const orgId = actor.orgId;
181
215
  const chat = await this.mustChat(this.repo, orgId, chatId);
182
- await this.requireActor(orgId, actor, chat.communityId);
216
+ await this.requireActor(actor, chat.communityId);
183
217
  await this.requireActive(this.repo, orgId, chat.id, actor.id);
184
218
  const saved = await this.uow.run(async ({ chats, outbox }) => {
185
219
  const current = await this.mustChat(chats, orgId, chatId);
@@ -201,9 +235,10 @@ var ChatServiceImpl = class {
201
235
  });
202
236
  return saved;
203
237
  }
204
- async accept(orgId, actor, chatId) {
238
+ async accept(chatId, actor) {
239
+ const orgId = actor.orgId;
205
240
  const chat = await this.mustChat(this.repo, orgId, chatId);
206
- await this.requireActor(orgId, actor, chat.communityId);
241
+ await this.requireActor(actor, chat.communityId);
207
242
  await this.requireActive(this.repo, orgId, chat.id, actor.id);
208
243
  if (chat.createdBy === actor.id) throw new ForbiddenError("You cannot accept this request");
209
244
  if (chat.kind !== "direct" || chat.status !== "pending") throw new ConflictError("This chat is not a request");
@@ -222,60 +257,119 @@ var ChatServiceImpl = class {
222
257
  });
223
258
  return saved;
224
259
  }
225
- async send(orgId, actor, chatId, input) {
260
+ async send(chatId, input, actor) {
261
+ const orgId = actor.orgId;
226
262
  const preview = await this.mustChat(this.repo, orgId, chatId);
227
- await this.requireActor(orgId, actor, preview.communityId);
263
+ await this.requireActor(actor, preview.communityId);
228
264
  const settings = await this.requireSettings(orgId, preview.communityId);
229
265
  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;
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
240
305
  });
241
- this.logger.info("message sent", {
306
+ else this.logger.info("message sent", {
242
307
  orgId,
243
308
  chatId,
244
- messageId: saved.id,
309
+ messageId: saved.message.id,
245
310
  actorId: actor.id
246
311
  });
247
- return this.viewOf(orgId, actor, saved);
312
+ return this.viewOf(actor, saved.message);
248
313
  }
249
- async editMessage(orgId, actor, messageId, body) {
314
+ async editMessage(messageId, body, actor) {
315
+ const orgId = actor.orgId;
250
316
  const message = await this.mustMessage(this.repo, orgId, messageId);
251
- await this.requireWritableChat(orgId, actor, message.chatId);
317
+ await this.requireWritableChat(actor, message.chatId);
252
318
  if (message.orgUserId !== actor.id) throw new ForbiddenError("You can change only your own message");
253
319
  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()
320
+ if (message.body === body) return this.viewOf(actor, message);
321
+ const now = /* @__PURE__ */ new Date();
322
+ const saved = await this.uow.run(async ({ chats, mentions, outbox }) => {
323
+ const next = await chats.updateMessage(orgId, messageId, {
324
+ body,
325
+ editedAt: now
326
+ });
327
+ if (!next) throw new NotFoundError("Message", messageId);
328
+ const chat = await this.mustChat(chats, orgId, next.chatId);
329
+ const users = await chats.participants(orgId, next.chatId);
330
+ const candidates = this.mentionable(body, users);
331
+ const added = await mentions.sync(orgId, "message", messageId, candidates, now);
332
+ await outbox.append([discussionEvents.messageUpdated.make({
333
+ orgId,
334
+ data: {
335
+ ...eventMessage(next),
336
+ mentionedOrgUserIds: keepAllowed(candidates, added)
337
+ },
338
+ metadata: {
339
+ ...this.meta(actor),
340
+ chatStatus: chat.status
341
+ }
342
+ })]);
343
+ return next;
258
344
  });
259
- if (!saved) throw new NotFoundError("Message", messageId);
260
345
  this.logger.info("message edited", {
261
346
  orgId,
262
347
  messageId,
263
348
  actorId: actor.id
264
349
  });
265
- return this.viewOf(orgId, actor, saved);
350
+ return this.viewOf(actor, saved);
266
351
  }
267
- async deleteMessage(orgId, actor, messageId) {
352
+ async deleteMessage(messageId, actor) {
353
+ const orgId = actor.orgId;
268
354
  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);
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);
273
366
  }
274
367
  if (message.deletedAt) throw new ConflictError("This message was deleted");
275
368
  const now = /* @__PURE__ */ new Date();
276
- const saved = await this.uow.run(async ({ chats, outbox }) => {
369
+ const saved = await this.uow.run(async ({ chats, mentions, outbox }) => {
277
370
  const next = await chats.updateMessage(orgId, messageId, { deletedAt: now });
278
371
  if (!next) throw new NotFoundError("Message", messageId);
372
+ await mentions.removeItem(orgId, "message", messageId);
279
373
  await chats.resolveFlags(orgId, messageId, {
280
374
  resolvedAt: now,
281
375
  resolvedBy: actor.id,
@@ -283,7 +377,7 @@ var ChatServiceImpl = class {
283
377
  });
284
378
  await outbox.append([discussionEvents.messageDeleted.make({
285
379
  orgId,
286
- data: next,
380
+ data: eventMessage(next),
287
381
  metadata: this.meta(actor)
288
382
  })]);
289
383
  return next;
@@ -293,55 +387,78 @@ var ChatServiceImpl = class {
293
387
  messageId,
294
388
  actorId: actor.id
295
389
  });
296
- return this.viewOf(orgId, actor, saved);
390
+ return this.viewOf(actor, saved);
297
391
  }
298
- async setReaction(orgId, actor, messageId, emoji) {
392
+ async setReaction(messageId, emoji, actor) {
393
+ const orgId = actor.orgId;
299
394
  const message = await this.mustMessage(this.repo, orgId, messageId);
300
- await this.requireActor(orgId, actor, message.communityId);
395
+ await this.requireActor(actor, message.communityId);
301
396
  await this.requireActive(this.repo, orgId, message.chatId, actor.id);
302
- await this.repo.setReaction(orgId, message.communityId, messageId, actor.id, emoji);
397
+ await this.uow.run(async ({ chats, outbox }) => {
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({
408
+ orgId,
409
+ data: {
410
+ ...where,
411
+ reaction: emoji
412
+ },
413
+ metadata: this.meta(actor)
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);
424
+ });
303
425
  this.logger.debug("message reaction set", {
304
426
  orgId,
305
427
  messageId
306
428
  });
307
429
  return (await this.repo.reactionsOf(orgId, [messageId], actor.id))[messageId] ?? { reactions: {} };
308
430
  }
309
- async markRead(orgId, actor, chatId, messageId) {
431
+ async markRead(chatId, messageId, actor) {
432
+ const orgId = actor.orgId;
310
433
  const chat = await this.mustChat(this.repo, orgId, chatId);
311
- await this.requireActor(orgId, actor, chat.communityId);
434
+ await this.requireActor(actor, chat.communityId);
312
435
  await this.requireActive(this.repo, orgId, chat.id, actor.id);
313
436
  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", {
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", {
316
452
  orgId,
317
453
  chatId,
318
454
  messageId
319
455
  });
320
456
  }
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) {
457
+ async block(communityId, orgUserId, actor) {
458
+ const orgId = actor.orgId;
342
459
  if (actor.id === orgUserId) throw new InvalidInputError("You cannot block yourself");
343
460
  await this.requireSettings(orgId, communityId);
344
- await this.requireActor(orgId, actor, communityId);
461
+ await this.requireActor(actor, communityId);
345
462
  await this.requirePerson(orgId, communityId, orgUserId);
346
463
  await this.uow.run(async ({ chats }) => {
347
464
  await chats.lockDirectPair(orgId, communityId, actor.id, orgUserId);
@@ -353,10 +470,11 @@ var ChatServiceImpl = class {
353
470
  actorId: actor.id
354
471
  });
355
472
  }
356
- async unblock(orgId, actor, communityId, orgUserId) {
473
+ async unblock(communityId, orgUserId, actor) {
474
+ const orgId = actor.orgId;
357
475
  if (actor.id === orgUserId) throw new InvalidInputError("You cannot block yourself");
358
476
  await this.requireSettings(orgId, communityId);
359
- await this.requireActor(orgId, actor, communityId);
477
+ await this.requireActor(actor, communityId);
360
478
  await this.uow.run(async ({ chats }) => {
361
479
  await chats.lockDirectPair(orgId, communityId, actor.id, orgUserId);
362
480
  await chats.unblock(orgId, communityId, actor.id, orgUserId);
@@ -367,9 +485,10 @@ var ChatServiceImpl = class {
367
485
  actorId: actor.id
368
486
  });
369
487
  }
370
- async flagMessage(orgId, actor, messageId, reason) {
488
+ async flagMessage(messageId, reason, actor) {
489
+ const orgId = actor.orgId;
371
490
  const message = await this.mustMessage(this.repo, orgId, messageId);
372
- await this.requireActor(orgId, actor, message.communityId);
491
+ await this.requireActor(actor, message.communityId);
373
492
  await this.requireActive(this.repo, orgId, message.chatId, actor.id);
374
493
  const now = /* @__PURE__ */ new Date();
375
494
  const saved = await this.uow.run(async ({ chats, outbox }) => {
@@ -405,11 +524,12 @@ var ChatServiceImpl = class {
405
524
  });
406
525
  return saved;
407
526
  }
408
- async resolveMessageFlags(orgId, actor, messageId, resolution, communityId) {
409
- if (!isStaffRole(actor.role)) throw new ForbiddenError("You cannot work this queue");
527
+ async resolveMessageFlags(messageId, resolution, communityId, actor) {
528
+ const orgId = actor.orgId;
410
529
  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);
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);
413
533
  await this.repo.resolveFlags(orgId, messageId, {
414
534
  resolvedAt: /* @__PURE__ */ new Date(),
415
535
  resolvedBy: actor.id,
@@ -423,28 +543,18 @@ var ChatServiceImpl = class {
423
543
  actorId: actor.id
424
544
  });
425
545
  }
426
- async lock(orgId, actor, chatId, communityId) {
427
- return this.setLock(orgId, actor, chatId, true, communityId);
546
+ async lock(chatId, communityId, actor) {
547
+ return this.setLock(actor, chatId, true, communityId);
428
548
  }
429
- async unlock(orgId, actor, chatId, communityId) {
430
- return this.setLock(orgId, actor, chatId, false, communityId);
549
+ async unlock(chatId, communityId, actor) {
550
+ return this.setLock(actor, chatId, false, communityId);
431
551
  }
432
- async listChats(orgId, actor, communityId, query) {
552
+ async listChats(communityId, query, actor) {
553
+ const orgId = actor.orgId;
433
554
  await this.requireSettings(orgId, communityId);
434
- await this.requireActor(orgId, actor, communityId);
435
- const now = /* @__PURE__ */ new Date();
555
+ await this.requireActor(actor, communityId);
436
556
  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
- }));
557
+ const items = await this.listItems(actor, communityId, page.rows);
448
558
  this.logger.debug("chats listed", {
449
559
  orgId,
450
560
  communityId,
@@ -455,9 +565,100 @@ var ChatServiceImpl = class {
455
565
  rows: items
456
566
  };
457
567
  }
458
- async messages(orgId, actor, chatId, query) {
568
+ async chat(chatId, actor) {
569
+ const orgId = actor.orgId;
459
570
  const chat = await this.mustChat(this.repo, orgId, chatId);
460
- 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);
461
662
  await this.requireActive(this.repo, orgId, chat.id, actor.id);
462
663
  const page = await this.repo.listMessages(orgId, chatId, query);
463
664
  this.logger.debug("chat messages read", {
@@ -469,18 +670,23 @@ var ChatServiceImpl = class {
469
670
  rows: await this.withSenders(orgId, actor, page.rows)
470
671
  };
471
672
  }
472
- async attachmentFile(orgId, actor, chatId, messageId, assetId) {
673
+ async attachmentFile(chatId, messageId, assetId, actor) {
674
+ const orgId = actor.orgId;
473
675
  const chat = await this.repo.findChat(orgId, chatId);
474
676
  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);
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);
484
690
  await this.requireSettings(orgId, communityId);
485
691
  const flags = await this.repo.listOpenFlags(orgId, communityId, 100);
486
692
  const messages = await this.repo.messagesByIds(orgId, [...new Set(flags.map((flag) => flag.messageId))]);
@@ -511,14 +717,15 @@ var ChatServiceImpl = class {
511
717
  participants: await this.repo.participants(orgId, chatId)
512
718
  };
513
719
  }
514
- async titlesFor(orgId, actor, chatIds) {
720
+ async titlesFor(chatIds, actor) {
721
+ const orgId = actor.orgId;
515
722
  if (chatIds.length === 0) return {};
516
723
  const summaries = (await this.repo.chatSummaries(orgId, [...new Set(chatIds)])).filter((summary) => summary.participantIds.includes(actor.id));
517
724
  const others = [...new Set(summaries.flatMap((summary) => summary.participantIds.filter((id) => id !== actor.id)))];
518
725
  const records = others.length > 0 ? await this.profiles.authorsOf(orgId, others) : {};
519
726
  const nameOf = (id) => {
520
727
  const record = records[id];
521
- return record && authorView(orgId, record, null).displayName || "A member";
728
+ return record && authorName(record) || "A member";
522
729
  };
523
730
  return Object.fromEntries(summaries.flatMap((summary) => {
524
731
  const names = summary.participantIds.filter((id) => id !== actor.id).map(nameOf);
@@ -526,6 +733,12 @@ var ChatServiceImpl = class {
526
733
  return summary.chat.kind === "group" ? [[summary.chat.id, "Group chat"]] : [];
527
734
  }));
528
735
  }
736
+ unreadInChat(orgId, chatId, orgUserId) {
737
+ return this.repo.unreadFor(orgId, chatId, orgUserId);
738
+ }
739
+ findMessage(orgId, messageId) {
740
+ return this.repo.findMessage(orgId, messageId);
741
+ }
529
742
  async latestMessageBody(orgId, chatId) {
530
743
  return (await this.repo.listMessages(orgId, chatId, {
531
744
  page: 1,
@@ -535,11 +748,12 @@ var ChatServiceImpl = class {
535
748
  isBlocked(orgId, communityId, a, b) {
536
749
  return this.repo.isBlocked(orgId, communityId, a, b);
537
750
  }
538
- async setLock(orgId, actor, chatId, locked, communityId) {
539
- if (!isStaffRole(actor.role)) throw new ForbiddenError("You cannot lock this chat");
751
+ async setLock(actor, chatId, locked, communityId) {
752
+ const orgId = actor.orgId;
540
753
  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);
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);
543
757
  if (Boolean(chat.lockedAt) === locked) return chat;
544
758
  const now = /* @__PURE__ */ new Date();
545
759
  const saved = await this.uow.run(async ({ chats, outbox }) => {
@@ -563,44 +777,62 @@ var ChatServiceImpl = class {
563
777
  });
564
778
  return saved;
565
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
+ }
566
791
  async requireSettings(orgId, communityId) {
567
- const settings = await this.content.getDmSettings(orgId, communityId);
792
+ const settings = await this.content.getDmSettings(communityId, this.authors.systemActor(orgId));
568
793
  if (!settings) throw new NotFoundError("Community", communityId);
569
794
  return settings;
570
795
  }
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");
796
+ async requireActor(actor, communityId) {
797
+ if (!await this.access.isCommunityMember(communityId, actor)) throw new ForbiddenError("You are not a member of this community");
573
798
  }
799
+ /** Whether the person is team in the community; throws when they are not a
800
+ * member of it. */
574
801
  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) {
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) {
593
824
  this.assertActive(users, actor.id);
594
825
  if (chat.lockedAt) throw new ForbiddenError("This chat is locked");
595
826
  if (chat.status === "closed") throw new ForbiddenError("You cannot send in this chat");
596
827
  if (this.pendingCreator(chat, actor)) throw new ForbiddenError("You cannot send in this chat");
597
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");
598
830
  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;
831
+ const recipients = chat.kind === "group" ? others.filter((other) => teams.team.has(other)) : others;
832
+ const actorTeam = isTeam(actor.permissions, chat.communityId);
601
833
  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");
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");
604
836
  }
605
837
  if (recipients.length > 0) {
606
838
  if ((await chats.blockedWith(orgId, chat.communityId, actor.id, recipients)).length > 0) throw new ForbiddenError("You cannot send in this chat");
@@ -610,30 +842,35 @@ var ChatServiceImpl = class {
610
842
  const reply = await chats.findMessage(orgId, input.replyToId);
611
843
  if (!reply || reply.chatId !== chat.id) throw new NotFoundError("Message", input.replyToId);
612
844
  }
613
- await this.requireOwnedImages(orgId, actor.id, input.assetIds);
614
845
  }
615
- async writeMessage(chats, orgId, actor, chat, input) {
846
+ mentionable(body, users) {
847
+ return keepAllowed(mentionTokens(body), users.filter((user) => user.leftAt === null).map((user) => user.orgUserId));
848
+ }
849
+ async writeMessage(chats, mentions, orgId, actor, chat, users, input) {
616
850
  const opened = chat.kind === "direct" && chat.status === "pending" && chat.createdBy !== actor.id;
617
851
  const current = opened ? await this.mustUpdate(chats, orgId, chat.id, { status: "open" }) : chat;
618
852
  const message = await chats.insertMessage(orgId, this.newMessage(orgId, current, actor, input, /* @__PURE__ */ new Date()));
619
853
  if (input.assetIds.length > 0) await chats.attach(orgId, current.communityId, message.id, input.assetIds);
854
+ const mentioned = this.mentionable(input.body, users);
855
+ await mentions.sync(orgId, "message", message.id, mentioned, message.createdAt);
620
856
  return {
621
857
  chat: current,
622
858
  message,
623
- opened
859
+ opened,
860
+ mentioned
624
861
  };
625
862
  }
626
- assertMayMessage(settings, actor, role) {
863
+ assertMayMessage(settings, actorTeam, targetTeam) {
627
864
  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");
865
+ if (!this.mayMessage(settings, actorTeam, targetTeam)) throw new ForbiddenError("You cannot message this person");
629
866
  }
630
867
  assertGroup(settings) {
631
868
  if (!settings.enabled) throw new ForbiddenError("Direct messages are off");
632
869
  if (!settings.groupChats) throw new ForbiddenError("Group chats are off");
633
870
  }
634
- mayMessage(settings, actor, target) {
635
- if (isStaffRole(actor.role)) return settings.staffToAnyone;
636
- if (isStaffRole(target)) return settings.memberToStaff;
871
+ mayMessage(settings, actorTeam, targetTeam) {
872
+ if (actorTeam) return settings.teamToAnyone;
873
+ if (targetTeam) return settings.memberToTeam;
637
874
  return settings.memberToMember;
638
875
  }
639
876
  async requireActive(repo, orgId, chatId, orgUserId) {
@@ -653,10 +890,10 @@ var ChatServiceImpl = class {
653
890
  throw new NotFoundError("Asset", assetId);
654
891
  }
655
892
  }
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);
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);
660
897
  if (chat.lockedAt || chat.status === "closed") throw new ForbiddenError("You cannot send in this chat");
661
898
  }
662
899
  pendingCreator(chat, actor) {
@@ -686,8 +923,6 @@ var ChatServiceImpl = class {
686
923
  joinedAt: now,
687
924
  leftAt: null,
688
925
  lastReadMessageId: null,
689
- muted: false,
690
- mutedUntil: null,
691
926
  createdAt: now,
692
927
  updatedAt: now
693
928
  };
@@ -701,6 +936,7 @@ var ChatServiceImpl = class {
701
936
  orgUserId: actor.id,
702
937
  body: input.body,
703
938
  replyToId: input.replyToId,
939
+ clientId: input.clientId ?? null,
704
940
  editedAt: null,
705
941
  deletedAt: null,
706
942
  createdAt: now,
@@ -717,7 +953,7 @@ var ChatServiceImpl = class {
717
953
  };
718
954
  }
719
955
  meta(actor) {
720
- return { actorOrgUserId: actor.id };
956
+ return { actorId: actor.id };
721
957
  }
722
958
  createdEvent(orgId, actor, chat, users) {
723
959
  return discussionEvents.chatCreated.make({
@@ -745,51 +981,80 @@ var ChatServiceImpl = class {
745
981
  metadata: this.meta(actor)
746
982
  });
747
983
  }
748
- sentEvent(orgId, actor, chat, message) {
984
+ sentEvent(orgId, actor, chat, message, mentionedOrgUserIds) {
749
985
  return discussionEvents.messageSent.make({
750
986
  orgId,
751
- data: message,
987
+ data: {
988
+ ...eventMessage(message),
989
+ mentionedOrgUserIds
990
+ },
752
991
  metadata: {
753
992
  ...this.meta(actor),
754
993
  chatStatus: chat.status
755
994
  }
756
995
  });
757
996
  }
758
- async viewOf(orgId, actor, message) {
759
- const [view] = await this.withSenders(orgId, actor, [message]);
997
+ async viewOf(actor, message) {
998
+ const [view] = await this.withSenders(actor.orgId, actor, [message]);
760
999
  return view;
761
1000
  }
762
- async withSenders(orgId, actor, messages) {
1001
+ async withSenders(orgId, viewer, messages) {
763
1002
  if (messages.length === 0) return [];
764
1003
  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))]),
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),
767
1008
  this.repo.attachmentsOf(orgId, messageIds),
768
- this.repo.reactionsOf(orgId, messageIds, actor.id)
1009
+ this.repo.reactionsOf(orgId, messageIds, viewer?.id ?? ""),
1010
+ mentionViews(this.mentions, this.profiles, orgId, "message", messageIds)
769
1011
  ]);
770
- 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] ?? [];
771
1014
  return messages.map((message) => {
772
1015
  const record = records[message.orgUserId];
773
1016
  if (!record) throw new NotFoundError("Member", message.orgUserId);
774
1017
  const counts = reactions[message.id];
775
1018
  return {
776
- ...this.present(message, actor),
777
- 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)),
778
1024
  attachments: shown(message).map((assetId) => ({
779
1025
  assetId,
780
1026
  url: this.fileUrls.messageAttachment(message.chatId, message.id, assetId)
781
1027
  })),
782
1028
  reactions: counts?.reactions ?? {},
783
- viewerReaction: counts?.viewerReaction ?? null
1029
+ viewerReaction: counts?.viewerReaction ?? null,
1030
+ mentions: hidden(message) ? [] : mentioned[message.id] ?? []
784
1031
  };
785
1032
  });
786
1033
  }
787
- present(message, actor) {
788
- if (message.deletedAt !== null && !isStaffRole(actor.role)) return {
789
- ...message,
790
- body: ""
791
- };
792
- 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
+ }));
793
1058
  }
794
1059
  };
795
1060
  //#endregion