@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.
- package/dist/assets/events.d.ts +27 -21
- package/dist/assets/file-urls.d.ts +11 -7
- package/dist/assets/file-urls.js +14 -4
- package/dist/assets/file-urls.js.map +1 -1
- package/dist/assets/index.d.ts +4 -4
- package/dist/assets/index.js +3 -3
- package/dist/assets/ports.d.ts +34 -15
- package/dist/assets/service.d.ts +30 -14
- package/dist/assets/service.js +230 -56
- package/dist/assets/service.js.map +1 -1
- package/dist/automations/actions.js.map +1 -1
- package/dist/automations/catalog.js +69 -4
- package/dist/automations/catalog.js.map +1 -1
- package/dist/automations/community-derivations.js +16 -16
- package/dist/automations/community-derivations.js.map +1 -1
- package/dist/automations/community-mail.d.ts +34 -4
- package/dist/automations/community-mail.js +32 -4
- package/dist/automations/community-mail.js.map +1 -1
- package/dist/automations/event-permissions.d.ts +12 -0
- package/dist/automations/event-permissions.js +38 -0
- package/dist/automations/event-permissions.js.map +1 -0
- package/dist/automations/index.d.ts +2 -1
- package/dist/automations/index.js +2 -1
- package/dist/automations/notifications/applicable.d.ts +8 -0
- package/dist/automations/notifications/applicable.js +12 -0
- package/dist/automations/notifications/applicable.js.map +1 -0
- package/dist/automations/notifications/debounce.js +1 -1
- package/dist/automations/notifications/debounce.js.map +1 -1
- package/dist/automations/notifications/defaults.js +33 -3
- package/dist/automations/notifications/defaults.js.map +1 -1
- package/dist/automations/notifications/processor.js +3 -3
- package/dist/automations/notifications/processor.js.map +1 -1
- package/dist/automations/notifications/rules/content.js +60 -3
- package/dist/automations/notifications/rules/content.js.map +1 -1
- package/dist/automations/notifications/rules/discussion.js +29 -29
- package/dist/automations/notifications/rules/discussion.js.map +1 -1
- package/dist/automations/notifications/rules/entitlements.js +36 -9
- package/dist/automations/notifications/rules/entitlements.js.map +1 -1
- package/dist/automations/notifications/rules/events.d.ts +9 -0
- package/dist/automations/notifications/rules/events.js +27 -0
- package/dist/automations/notifications/rules/events.js.map +1 -0
- package/dist/automations/notifications/rules/organizations.js +17 -10
- package/dist/automations/notifications/rules/organizations.js.map +1 -1
- package/dist/automations/notifications/rules/recipients.js +0 -1
- package/dist/automations/notifications/rules/recipients.js.map +1 -1
- package/dist/automations/notifications/service.d.ts +8 -6
- package/dist/automations/notifications/service.js +13 -13
- package/dist/automations/notifications/service.js.map +1 -1
- package/dist/automations/notifications/types.js +94 -7
- package/dist/automations/notifications/types.js.map +1 -1
- package/dist/automations/ports.d.ts +30 -31
- package/dist/automations/service.d.ts +20 -17
- package/dist/automations/service.js +91 -62
- package/dist/automations/service.js.map +1 -1
- package/dist/content/event-sight.d.ts +11 -0
- package/dist/content/event-sight.js +11 -0
- package/dist/content/event-sight.js.map +1 -0
- package/dist/content/events.d.ts +413 -72
- package/dist/content/events.js +34 -1
- package/dist/content/events.js.map +1 -1
- package/dist/content/index.d.ts +6 -4
- package/dist/content/index.js +3 -1
- package/dist/content/jobs.d.ts +16 -0
- package/dist/content/jobs.js +37 -0
- package/dist/content/jobs.js.map +1 -0
- package/dist/content/model.d.ts +2 -2
- package/dist/content/notification-categories.d.ts +5 -4
- package/dist/content/notification-categories.js +15 -6
- package/dist/content/notification-categories.js.map +1 -1
- package/dist/content/ports.d.ts +113 -68
- package/dist/content/service.d.ts +99 -67
- package/dist/content/service.js +680 -139
- package/dist/content/service.js.map +1 -1
- package/dist/content/titles.d.ts +6 -3
- package/dist/content/titles.js +37 -24
- package/dist/content/titles.js.map +1 -1
- package/dist/content/types.d.ts +2 -2
- package/dist/discussion/authors.d.ts +4 -2
- package/dist/discussion/authors.js +9 -5
- package/dist/discussion/authors.js.map +1 -1
- package/dist/discussion/chats.d.ts +56 -37
- package/dist/discussion/chats.js +393 -161
- package/dist/discussion/chats.js.map +1 -1
- package/dist/discussion/events.d.ts +77 -39
- package/dist/discussion/events.js +41 -3
- package/dist/discussion/events.js.map +1 -1
- package/dist/discussion/index.d.ts +8 -5
- package/dist/discussion/index.js +5 -3
- package/dist/discussion/live.d.ts +106 -0
- package/dist/discussion/live.js +642 -0
- package/dist/discussion/live.js.map +1 -0
- package/dist/discussion/mentions.js +2 -2
- package/dist/discussion/mentions.js.map +1 -1
- package/dist/discussion/ports.d.ts +182 -100
- package/dist/discussion/ports.js.map +1 -1
- package/dist/discussion/posts.d.ts +45 -39
- package/dist/discussion/posts.js +268 -154
- package/dist/discussion/posts.js.map +1 -1
- package/dist/discussion/rules.d.ts +18 -0
- package/dist/discussion/rules.js +22 -0
- package/dist/discussion/rules.js.map +1 -0
- package/dist/discussion/service.d.ts +37 -31
- package/dist/discussion/service.js +252 -188
- package/dist/discussion/service.js.map +1 -1
- package/dist/discussion/subscriptions.d.ts +8 -8
- package/dist/discussion/subscriptions.js +29 -13
- package/dist/discussion/subscriptions.js.map +1 -1
- package/dist/discussion/titles.d.ts +4 -2
- package/dist/discussion/titles.js +27 -13
- package/dist/discussion/titles.js.map +1 -1
- package/dist/entitlements/events.d.ts +20 -20
- package/dist/entitlements/index.d.ts +4 -4
- package/dist/entitlements/index.js +2 -2
- package/dist/entitlements/model.d.ts +2 -2
- package/dist/entitlements/ports.d.ts +65 -39
- package/dist/entitlements/rules.d.ts +3 -6
- package/dist/entitlements/rules.js +5 -10
- package/dist/entitlements/rules.js.map +1 -1
- package/dist/entitlements/service.d.ts +40 -28
- package/dist/entitlements/service.js +370 -163
- package/dist/entitlements/service.js.map +1 -1
- package/dist/identity/devices.d.ts +3 -3
- package/dist/identity/devices.js +7 -5
- package/dist/identity/devices.js.map +1 -1
- package/dist/identity/events.d.ts +2 -2
- package/dist/identity/index.d.ts +2 -2
- package/dist/identity/index.js +2 -2
- package/dist/identity/notification-settings.d.ts +1 -1
- package/dist/identity/notification-settings.js +3 -2
- package/dist/identity/notification-settings.js.map +1 -1
- package/dist/identity/ports.d.ts +7 -7
- package/dist/identity/service.d.ts +8 -7
- package/dist/identity/service.js +17 -13
- package/dist/identity/service.js.map +1 -1
- package/dist/integrations/events.d.ts +3 -3
- package/dist/integrations/ports.d.ts +9 -8
- package/dist/integrations/service.d.ts +9 -8
- package/dist/integrations/service.js +33 -16
- package/dist/integrations/service.js.map +1 -1
- package/dist/organizations/default-roles.d.ts +22 -0
- package/dist/organizations/default-roles.js +89 -0
- package/dist/organizations/default-roles.js.map +1 -0
- package/dist/organizations/effective-permissions.d.ts +45 -0
- package/dist/organizations/effective-permissions.js +113 -0
- package/dist/organizations/effective-permissions.js.map +1 -0
- package/dist/organizations/events.d.ts +320 -181
- package/dist/organizations/events.js +90 -18
- package/dist/organizations/events.js.map +1 -1
- package/dist/organizations/index.d.ts +14 -10
- package/dist/organizations/index.js +7 -3
- package/dist/organizations/members.d.ts +66 -8
- package/dist/organizations/members.js.map +1 -1
- package/dist/organizations/notification-catalog.d.ts +31 -0
- package/dist/organizations/notification-catalog.js +32 -0
- package/dist/organizations/notification-catalog.js.map +1 -0
- package/dist/organizations/notification-categories.d.ts +6 -4
- package/dist/organizations/notification-categories.js +25 -10
- package/dist/organizations/notification-categories.js.map +1 -1
- package/dist/organizations/org-roles.d.ts +33 -0
- package/dist/organizations/org-roles.js +49 -0
- package/dist/organizations/org-roles.js.map +1 -0
- package/dist/organizations/permissions.d.ts +311 -0
- package/dist/organizations/permissions.js +327 -0
- package/dist/organizations/permissions.js.map +1 -0
- package/dist/organizations/ports.d.ts +184 -65
- package/dist/organizations/service.d.ts +102 -35
- package/dist/organizations/service.js +1245 -328
- package/dist/organizations/service.js.map +1 -1
- package/dist/organizations/testing.d.ts +18 -0
- package/dist/organizations/testing.js +37 -0
- package/dist/organizations/testing.js.map +1 -0
- package/dist/organizations/types.d.ts +5 -16
- package/dist/progress/events.d.ts +14 -14
- package/dist/progress/index.d.ts +2 -2
- package/dist/progress/ports.d.ts +11 -3
- package/dist/progress/service.d.ts +6 -5
- package/dist/progress/service.js +23 -8
- package/dist/progress/service.js.map +1 -1
- package/dist/reporting/admin/index.d.ts +6 -4
- package/dist/reporting/admin/model.d.ts +36 -3
- package/dist/reporting/admin/ports.d.ts +39 -14
- package/dist/reporting/admin/service.d.ts +32 -11
- package/dist/reporting/admin/service.js +102 -18
- package/dist/reporting/admin/service.js.map +1 -1
- package/dist/reporting/events/index.d.ts +27 -0
- package/dist/reporting/events/index.js +20 -0
- package/dist/reporting/events/index.js.map +1 -0
- package/dist/reporting/member/calendar.d.ts +11 -0
- package/dist/reporting/member/calendar.js +75 -0
- package/dist/reporting/member/calendar.js.map +1 -0
- package/dist/reporting/member/index.d.ts +3 -2
- package/dist/reporting/member/model.d.ts +26 -5
- package/dist/reporting/member/ports.d.ts +48 -29
- package/dist/reporting/member/service.d.ts +56 -29
- package/dist/reporting/member/service.js +328 -113
- package/dist/reporting/member/service.js.map +1 -1
- package/dist/shared/actor.d.ts +22 -3
- package/dist/shared/errors.d.ts +2 -1
- package/dist/shared/errors.js +3 -1
- package/dist/shared/errors.js.map +1 -1
- package/dist/shared/id.d.ts +4 -0
- package/dist/shared/id.js +4 -0
- package/dist/shared/id.js.map +1 -1
- package/dist/shared/logger.d.ts +2 -1
- package/dist/shared/logger.js +10 -1
- package/dist/shared/logger.js.map +1 -1
- package/dist/shared/ports.d.ts +25 -5
- package/dist/types/content.d.ts +2 -2
- package/dist/types/discussion.d.ts +4 -5
- package/dist/types/email-templates.d.ts +22 -3
- package/dist/types/entitlements.d.ts +2 -2
- package/dist/types/identity.d.ts +13 -16
- package/dist/types/index.d.ts +9 -9
- package/dist/types/index.js +2 -1
- package/dist/types/notifications.d.ts +4 -4
- package/dist/types/organizations.d.ts +3 -2
- package/dist/types/ports.d.ts +8 -1
- package/dist/types/ports.js +7 -1
- package/dist/types/ports.js.map +1 -0
- package/dist/types/schemas/assets.d.ts +5 -5
- package/dist/types/schemas/assets.js +4 -3
- package/dist/types/schemas/assets.js.map +1 -1
- package/dist/types/schemas/content.d.ts +537 -13
- package/dist/types/schemas/content.js +201 -7
- package/dist/types/schemas/content.js.map +1 -1
- package/dist/types/schemas/discussion.d.ts +282 -59
- package/dist/types/schemas/discussion.js +77 -3
- package/dist/types/schemas/discussion.js.map +1 -1
- package/dist/types/schemas/entitlements.d.ts +14 -1
- package/dist/types/schemas/entitlements.js +11 -4
- package/dist/types/schemas/entitlements.js.map +1 -1
- package/dist/types/schemas/index.d.ts +5 -5
- package/dist/types/schemas/index.js +5 -5
- package/dist/types/schemas/notifications.d.ts +13 -12
- package/dist/types/schemas/notifications.js +3 -2
- package/dist/types/schemas/notifications.js.map +1 -1
- package/dist/types/schemas/organizations.d.ts +96 -60
- package/dist/types/schemas/organizations.js +78 -27
- package/dist/types/schemas/organizations.js.map +1 -1
- package/dist/types/schemas/progress.d.ts +3 -4
- package/dist/types/schemas/progress.js +0 -1
- package/dist/types/schemas/progress.js.map +1 -1
- package/package.json +1 -1
- package/dist/organizations/access-scopes.d.ts +0 -19
- package/dist/organizations/access-scopes.js +0 -89
- package/dist/organizations/access-scopes.js.map +0 -1
- package/dist/organizations/roles.d.ts +0 -19
- package/dist/organizations/roles.js +0 -82
- package/dist/organizations/roles.js.map +0 -1
package/dist/discussion/chats.js
CHANGED
|
@@ -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 {
|
|
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(
|
|
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(
|
|
41
|
-
const
|
|
42
|
-
|
|
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 && !
|
|
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(
|
|
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(
|
|
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
|
|
124
|
-
this.assertMayMessage(settings,
|
|
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(
|
|
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(
|
|
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
|
|
163
|
-
this.assertMayMessage(settings, actor,
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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(
|
|
312
|
+
return this.viewOf(actor, saved.message);
|
|
254
313
|
}
|
|
255
|
-
async editMessage(
|
|
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(
|
|
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(
|
|
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(
|
|
350
|
+
return this.viewOf(actor, saved);
|
|
291
351
|
}
|
|
292
|
-
async deleteMessage(
|
|
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(
|
|
295
|
-
if (!
|
|
296
|
-
if (message.orgUserId !== actor.id)
|
|
297
|
-
|
|
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(
|
|
390
|
+
return this.viewOf(actor, saved);
|
|
323
391
|
}
|
|
324
|
-
async setReaction(
|
|
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(
|
|
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
|
-
|
|
330
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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.
|
|
354
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
428
|
-
|
|
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 !==
|
|
431
|
-
|
|
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(
|
|
446
|
-
return this.setLock(
|
|
546
|
+
async lock(chatId, communityId, actor) {
|
|
547
|
+
return this.setLock(actor, chatId, true, communityId);
|
|
447
548
|
}
|
|
448
|
-
async unlock(
|
|
449
|
-
return this.setLock(
|
|
549
|
+
async unlock(chatId, communityId, actor) {
|
|
550
|
+
return this.setLock(actor, chatId, false, communityId);
|
|
450
551
|
}
|
|
451
|
-
async listChats(
|
|
552
|
+
async listChats(communityId, query, actor) {
|
|
553
|
+
const orgId = actor.orgId;
|
|
452
554
|
await this.requireSettings(orgId, communityId);
|
|
453
|
-
await this.requireActor(
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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)
|
|
494
|
-
if (!((await this.repo.attachmentsOf(orgId, [messageId]))[messageId] ?? []).includes(assetId))
|
|
495
|
-
if (!await this.access.isCommunityMember(
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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(
|
|
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 &&
|
|
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(
|
|
563
|
-
|
|
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 !==
|
|
566
|
-
|
|
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(
|
|
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(
|
|
596
|
-
if (!await this.access.isCommunityMember(
|
|
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
|
|
600
|
-
if (
|
|
601
|
-
return
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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
|
|
624
|
-
const
|
|
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
|
|
627
|
-
if (
|
|
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,
|
|
863
|
+
assertMayMessage(settings, actorTeam, targetTeam) {
|
|
657
864
|
if (!settings.enabled) throw new ForbiddenError("Direct messages are off");
|
|
658
|
-
if (!this.mayMessage(settings,
|
|
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,
|
|
665
|
-
if (
|
|
666
|
-
if (
|
|
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(
|
|
687
|
-
const chat = await this.mustChat(this.repo, orgId, chatId);
|
|
688
|
-
await this.requireActor(
|
|
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 {
|
|
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(
|
|
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,
|
|
1001
|
+
async withSenders(orgId, viewer, messages) {
|
|
794
1002
|
if (messages.length === 0) return [];
|
|
795
1003
|
const messageIds = messages.map((message) => message.id);
|
|
796
|
-
const
|
|
797
|
-
|
|
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,
|
|
1009
|
+
this.repo.reactionsOf(orgId, messageIds, viewer?.id ?? ""),
|
|
800
1010
|
mentionViews(this.mentions, this.profiles, orgId, "message", messageIds)
|
|
801
1011
|
]);
|
|
802
|
-
const
|
|
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
|
-
...
|
|
809
|
-
|
|
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
|
|
1030
|
+
mentions: hidden(message) ? [] : mentioned[message.id] ?? []
|
|
817
1031
|
};
|
|
818
1032
|
});
|
|
819
1033
|
}
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
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
|