@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.
- package/dist/assets/events.d.ts +24 -18
- 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 +5 -4
- package/dist/assets/index.js +4 -3
- package/dist/assets/jobs.d.ts +15 -0
- package/dist/assets/jobs.js +30 -0
- package/dist/assets/jobs.js.map +1 -0
- package/dist/assets/ports.d.ts +37 -17
- package/dist/assets/service.d.ts +30 -15
- package/dist/assets/service.js +229 -59
- package/dist/assets/service.js.map +1 -1
- package/dist/automations/actions.js +2 -63
- package/dist/automations/actions.js.map +1 -1
- package/dist/automations/catalog.d.ts +1 -2
- package/dist/automations/catalog.js +137 -65
- package/dist/automations/catalog.js.map +1 -1
- package/dist/automations/community-derivations.js +20 -20
- package/dist/automations/community-derivations.js.map +1 -1
- package/dist/automations/community-mail.d.ts +36 -20
- package/dist/automations/community-mail.js +32 -24
- 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/events.d.ts +0 -10
- package/dist/automations/index.d.ts +12 -4
- package/dist/automations/index.js +9 -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.d.ts +25 -0
- package/dist/automations/notifications/debounce.js +93 -0
- package/dist/automations/notifications/debounce.js.map +1 -0
- package/dist/automations/notifications/defaults.d.ts +8 -0
- package/dist/automations/notifications/defaults.js +346 -0
- package/dist/automations/notifications/defaults.js.map +1 -0
- package/dist/automations/notifications/delivery.d.ts +28 -0
- package/dist/automations/notifications/delivery.js +88 -0
- package/dist/automations/notifications/delivery.js.map +1 -0
- package/dist/automations/notifications/processor.d.ts +64 -0
- package/dist/automations/notifications/processor.js +270 -0
- package/dist/automations/notifications/processor.js.map +1 -0
- package/dist/automations/notifications/rules/content.d.ts +6 -0
- package/dist/automations/notifications/rules/content.js +96 -0
- package/dist/automations/notifications/rules/content.js.map +1 -0
- package/dist/automations/notifications/rules/discussion.d.ts +7 -0
- package/dist/automations/notifications/rules/discussion.js +678 -0
- package/dist/automations/notifications/rules/discussion.js.map +1 -0
- package/dist/automations/notifications/rules/entitlements.d.ts +6 -0
- package/dist/automations/notifications/rules/entitlements.js +276 -0
- package/dist/automations/notifications/rules/entitlements.js.map +1 -0
- 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/index.d.ts +6 -0
- package/dist/automations/notifications/rules/index.js +15 -0
- package/dist/automations/notifications/rules/index.js.map +1 -0
- package/dist/automations/notifications/rules/organizations.d.ts +6 -0
- package/dist/automations/notifications/rules/organizations.js +35 -0
- package/dist/automations/notifications/rules/organizations.js.map +1 -0
- package/dist/automations/notifications/rules/recipients.d.ts +32 -0
- package/dist/automations/notifications/rules/recipients.js +98 -0
- package/dist/automations/notifications/rules/recipients.js.map +1 -0
- package/dist/automations/notifications/service.d.ts +29 -0
- package/dist/automations/notifications/service.js +45 -0
- package/dist/automations/notifications/service.js.map +1 -0
- package/dist/automations/notifications/types.d.ts +7 -0
- package/dist/automations/notifications/types.js +560 -0
- package/dist/automations/notifications/types.js.map +1 -0
- package/dist/automations/ports.d.ts +134 -21
- package/dist/automations/service.d.ts +30 -30
- package/dist/automations/service.js +111 -144
- 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 +437 -46
- package/dist/content/events.js +45 -1
- package/dist/content/events.js.map +1 -1
- package/dist/content/index.d.ts +8 -4
- package/dist/content/index.js +5 -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 +37 -0
- package/dist/content/notification-categories.js +73 -0
- package/dist/content/notification-categories.js.map +1 -0
- package/dist/content/ports.d.ts +132 -68
- package/dist/content/service.d.ts +100 -68
- package/dist/content/service.js +703 -146
- package/dist/content/service.js.map +1 -1
- package/dist/content/titles.d.ts +25 -0
- package/dist/content/titles.js +82 -0
- package/dist/content/titles.js.map +1 -0
- 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 +63 -39
- package/dist/discussion/chats.js +455 -190
- package/dist/discussion/chats.js.map +1 -1
- package/dist/discussion/digests.d.ts +24 -0
- package/dist/discussion/digests.js +104 -0
- package/dist/discussion/digests.js.map +1 -0
- package/dist/discussion/events.d.ts +227 -23
- package/dist/discussion/events.js +114 -19
- package/dist/discussion/events.js.map +1 -1
- package/dist/discussion/index.d.ts +12 -4
- package/dist/discussion/index.js +9 -2
- package/dist/discussion/jobs.d.ts +16 -0
- package/dist/discussion/jobs.js +40 -0
- package/dist/discussion/jobs.js.map +1 -0
- 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.d.ts +11 -0
- package/dist/discussion/mentions.js +35 -0
- package/dist/discussion/mentions.js.map +1 -0
- package/dist/discussion/ports.d.ts +279 -114
- package/dist/discussion/ports.js.map +1 -1
- package/dist/discussion/posts.d.ts +60 -44
- package/dist/discussion/posts.js +453 -287
- 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 +50 -35
- package/dist/discussion/service.js +333 -205
- package/dist/discussion/service.js.map +1 -1
- package/dist/discussion/subscriptions.d.ts +37 -0
- package/dist/discussion/subscriptions.js +115 -0
- package/dist/discussion/subscriptions.js.map +1 -0
- package/dist/discussion/titles.d.ts +28 -0
- package/dist/discussion/titles.js +98 -0
- package/dist/discussion/titles.js.map +1 -0
- package/dist/entitlements/events.d.ts +20 -20
- package/dist/entitlements/index.d.ts +5 -4
- package/dist/entitlements/index.js +3 -2
- package/dist/entitlements/jobs.d.ts +12 -0
- package/dist/entitlements/jobs.js +18 -0
- package/dist/entitlements/jobs.js.map +1 -0
- package/dist/entitlements/model.d.ts +2 -2
- package/dist/entitlements/ports.d.ts +68 -38
- 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 +43 -28
- package/dist/entitlements/service.js +387 -160
- package/dist/entitlements/service.js.map +1 -1
- package/dist/identity/devices.d.ts +30 -0
- package/dist/identity/devices.js +109 -0
- package/dist/identity/devices.js.map +1 -0
- package/dist/identity/index.d.ts +6 -5
- package/dist/identity/index.js +4 -3
- package/dist/identity/model.d.ts +2 -2
- package/dist/identity/notification-settings.d.ts +28 -0
- package/dist/identity/notification-settings.js +114 -0
- package/dist/identity/notification-settings.js.map +1 -0
- package/dist/identity/ports.d.ts +80 -56
- 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/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 +15 -10
- package/dist/organizations/index.js +8 -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 +34 -0
- package/dist/organizations/notification-categories.js +66 -0
- package/dist/organizations/notification-categories.js.map +1 -0
- 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 +187 -65
- package/dist/organizations/service.d.ts +104 -35
- package/dist/organizations/service.js +1247 -323
- 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/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 +80 -26
- package/dist/reporting/member/ports.d.ts +50 -28
- package/dist/reporting/member/service.d.ts +81 -35
- package/dist/reporting/member/service.js +438 -145
- package/dist/reporting/member/service.js.map +1 -1
- package/dist/shared/actor.d.ts +22 -3
- package/dist/shared/errors.d.ts +15 -2
- package/dist/shared/errors.js +21 -2
- package/dist/shared/errors.js.map +1 -1
- package/dist/shared/id.d.ts +7 -0
- package/dist/shared/id.js +8 -1
- 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/mailer.d.ts +11 -1
- package/dist/shared/mailer.js +32 -5
- package/dist/shared/mailer.js.map +1 -1
- package/dist/shared/notification-categories.d.ts +16 -0
- package/dist/shared/notification-categories.js +94 -0
- package/dist/shared/notification-categories.js.map +1 -0
- package/dist/shared/notification-settings.d.ts +14 -0
- package/dist/shared/notification-settings.js +61 -0
- package/dist/shared/notification-settings.js.map +1 -0
- package/dist/shared/ports.d.ts +68 -2
- package/dist/shared/settings.d.ts +2 -1
- package/dist/shared/settings.js +1 -0
- package/dist/shared/settings.js.map +1 -1
- package/dist/types/automations.d.ts +8 -2
- package/dist/types/content.d.ts +2 -2
- package/dist/types/discussion.d.ts +4 -5
- package/dist/types/email-templates.d.ts +119 -6
- package/dist/types/entitlements.d.ts +2 -2
- package/dist/types/identity.d.ts +14 -17
- package/dist/types/index.d.ts +13 -12
- package/dist/types/index.js +2 -1
- package/dist/types/notifications.d.ts +157 -0
- package/dist/types/notifications.js +1 -0
- 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/automations.d.ts +1 -10
- package/dist/types/schemas/automations.js +1 -3
- package/dist/types/schemas/automations.js.map +1 -1
- package/dist/types/schemas/content.d.ts +596 -11
- package/dist/types/schemas/content.js +225 -9
- package/dist/types/schemas/content.js.map +1 -1
- package/dist/types/schemas/discussion.d.ts +362 -66
- package/dist/types/schemas/discussion.js +123 -15
- 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/identity.d.ts +1 -85
- package/dist/types/schemas/identity.js +1 -42
- package/dist/types/schemas/identity.js.map +1 -1
- package/dist/types/schemas/index.d.ts +8 -7
- package/dist/types/schemas/index.js +8 -7
- package/dist/types/schemas/notifications.d.ts +96 -0
- package/dist/types/schemas/notifications.js +57 -0
- package/dist/types/schemas/notifications.js.map +1 -0
- 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 +0 -1
- package/dist/types/schemas/progress.js +0 -1
- package/dist/types/schemas/progress.js.map +1 -1
- package/package.json +1 -1
- package/dist/automations/community-notifications.d.ts +0 -16
- package/dist/automations/community-notifications.js +0 -84
- package/dist/automations/community-notifications.js.map +0 -1
- package/dist/automations/seeds.d.ts +0 -17
- package/dist/automations/seeds.js +0 -235
- package/dist/automations/seeds.js.map +0 -1
- package/dist/identity/notifications.d.ts +0 -27
- package/dist/identity/notifications.js +0 -101
- package/dist/identity/notifications.js.map +0 -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
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { genId } from "../shared/id.js";
|
|
2
|
-
import { ConflictError, ForbiddenError, NotFoundError } from "../shared/errors.js";
|
|
3
1
|
import { noopLogger } from "../shared/logger.js";
|
|
4
|
-
import {
|
|
2
|
+
import { ConflictError, ForbiddenError, NotFoundError } from "../shared/errors.js";
|
|
3
|
+
import { genId } from "../shared/id.js";
|
|
5
4
|
import "../shared/settings.js";
|
|
6
5
|
import { discussionEvents } from "./events.js";
|
|
7
6
|
import { authorView } from "./authors.js";
|
|
7
|
+
import { keepAllowed, mentionTokens, mentionViews } from "./mentions.js";
|
|
8
|
+
import { skipsReview, teamBadge } from "./rules.js";
|
|
8
9
|
//#region src/discussion/service.ts
|
|
9
10
|
/** A course with no stored settings. Discussion is opt-in, so the common case
|
|
10
11
|
* persists no row at all and every existing course stays silent. */
|
|
@@ -20,9 +21,11 @@ var DiscussionServiceImpl = class {
|
|
|
20
21
|
access;
|
|
21
22
|
content;
|
|
22
23
|
posts;
|
|
24
|
+
spaces;
|
|
25
|
+
authors;
|
|
26
|
+
mentions;
|
|
23
27
|
uow;
|
|
24
28
|
settings;
|
|
25
|
-
communities;
|
|
26
29
|
fileUrls;
|
|
27
30
|
logger;
|
|
28
31
|
constructor(params) {
|
|
@@ -30,21 +33,21 @@ var DiscussionServiceImpl = class {
|
|
|
30
33
|
this.access = params.access;
|
|
31
34
|
this.content = params.content;
|
|
32
35
|
this.posts = params.posts;
|
|
36
|
+
this.spaces = params.spaces;
|
|
37
|
+
this.authors = params.authors;
|
|
38
|
+
this.mentions = params.mentions;
|
|
33
39
|
this.uow = params.uow;
|
|
34
40
|
this.settings = params.settings;
|
|
35
|
-
this.communities = params.communities;
|
|
36
41
|
this.fileUrls = params.fileUrls;
|
|
37
42
|
this.logger = params.logger ?? noopLogger;
|
|
38
43
|
}
|
|
39
|
-
async
|
|
40
|
-
const activity = await this.content.getActivity(orgId, activityId);
|
|
41
|
-
if (!activity) throw new NotFoundError("Activity", activityId);
|
|
44
|
+
async configOf(orgId, activity) {
|
|
42
45
|
const stored = await this.settings.get(orgId, "content", activity.courseId);
|
|
43
46
|
const settings = {
|
|
44
47
|
...DEFAULT_SETTINGS,
|
|
45
48
|
...stored?.comments
|
|
46
49
|
};
|
|
47
|
-
const rule = (activity
|
|
50
|
+
const rule = (activity.settings ?? {}).comments ?? "inherit";
|
|
48
51
|
return {
|
|
49
52
|
enabled: settings.enabled && rule !== "never",
|
|
50
53
|
threaded: settings.threaded,
|
|
@@ -52,10 +55,10 @@ var DiscussionServiceImpl = class {
|
|
|
52
55
|
reactions: settings.reactions
|
|
53
56
|
};
|
|
54
57
|
}
|
|
55
|
-
|
|
56
|
-
return
|
|
58
|
+
async badgesOf(orgId, orgUserIds) {
|
|
59
|
+
return this.authors.permissionsOfMany(orgId, [...new Set(orgUserIds)]);
|
|
57
60
|
}
|
|
58
|
-
toView(orgId, comment, authors,
|
|
61
|
+
toView(orgId, comment, itemId, authors, permissions, reactions, mentions) {
|
|
59
62
|
const author = authors[comment.orgUserId];
|
|
60
63
|
if (!author) throw new NotFoundError("Member", comment.orgUserId);
|
|
61
64
|
if (comment.removedBy && !authors[comment.removedBy]) throw new NotFoundError("Member", comment.removedBy);
|
|
@@ -66,12 +69,13 @@ var DiscussionServiceImpl = class {
|
|
|
66
69
|
postId: comment.postId,
|
|
67
70
|
communityId: comment.communityId,
|
|
68
71
|
parentId: comment.parentId,
|
|
69
|
-
author: authorView(orgId, author,
|
|
72
|
+
author: authorView(orgId, author, this.fileUrls, teamBadge(permissions.get(author.id), itemId)),
|
|
70
73
|
body: comment.status === "removed" ? null : comment.body,
|
|
71
74
|
status: comment.status,
|
|
72
|
-
removedBy: remover ? authorView(orgId, remover,
|
|
75
|
+
removedBy: remover ? authorView(orgId, remover, this.fileUrls, teamBadge(permissions.get(remover.id), itemId)) : null,
|
|
73
76
|
reactions: reactions.reactions,
|
|
74
77
|
viewerReaction: reactions.viewerReaction,
|
|
78
|
+
mentions: comment.status === "removed" ? [] : mentions,
|
|
75
79
|
createdAt: comment.createdAt,
|
|
76
80
|
updatedAt: comment.updatedAt
|
|
77
81
|
};
|
|
@@ -84,34 +88,97 @@ var DiscussionServiceImpl = class {
|
|
|
84
88
|
}
|
|
85
89
|
return [...ids];
|
|
86
90
|
}
|
|
87
|
-
|
|
88
|
-
|
|
91
|
+
itemOf(where) {
|
|
92
|
+
return where.kind === "course" ? where.courseId : where.communityId;
|
|
93
|
+
}
|
|
94
|
+
async renderOne(comment, where, actor) {
|
|
95
|
+
const orgId = actor.orgId;
|
|
96
|
+
const people = this.peopleIn([comment]);
|
|
97
|
+
const [authors, permissions, mentioned] = await Promise.all([
|
|
98
|
+
this.repo.authorsOf(orgId, people),
|
|
99
|
+
this.badgesOf(orgId, people),
|
|
100
|
+
mentionViews(this.mentions, this.repo, orgId, "comment", [comment.id])
|
|
101
|
+
]);
|
|
89
102
|
const stored = comment.status === "removed" ? EMPTY_REACTIONS : (await this.repo.reactionsOf(orgId, [comment.id], actor.id))[comment.id] ?? EMPTY_REACTIONS;
|
|
90
|
-
return this.toView(orgId, comment, authors,
|
|
103
|
+
return this.toView(orgId, comment, this.itemOf(where), authors, permissions, stored, mentioned[comment.id] ?? []);
|
|
91
104
|
}
|
|
92
105
|
activityIdOf(comment) {
|
|
93
106
|
if (comment.activityId === null) throw new NotFoundError("Activity", comment.id);
|
|
94
107
|
return comment.activityId;
|
|
95
108
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
109
|
+
system(orgId) {
|
|
110
|
+
return this.authors.systemActor(orgId);
|
|
111
|
+
}
|
|
112
|
+
async activityOf(orgId, activityId) {
|
|
113
|
+
return this.content.getActivity(activityId, this.system(orgId));
|
|
100
114
|
}
|
|
101
|
-
async
|
|
102
|
-
|
|
115
|
+
async opensCourse(actor, courseId) {
|
|
116
|
+
return actor.permissions.has("courses:read", courseId) || this.access.hasAccess(actor.orgId, actor.id, courseId);
|
|
117
|
+
}
|
|
118
|
+
async requireActivityOpen(activityId, actor) {
|
|
119
|
+
const activity = await this.activityOf(actor.orgId, activityId);
|
|
103
120
|
if (!activity) throw new NotFoundError("Activity", activityId);
|
|
104
|
-
if (!
|
|
121
|
+
if (!await this.opensCourse(actor, activity.courseId)) {
|
|
122
|
+
this.logger.warn("activity refused — no access", {
|
|
123
|
+
orgId: actor.orgId,
|
|
124
|
+
activityId,
|
|
125
|
+
actorId: actor.id
|
|
126
|
+
});
|
|
127
|
+
throw new ForbiddenError("You cannot open this activity");
|
|
128
|
+
}
|
|
129
|
+
return activity;
|
|
130
|
+
}
|
|
131
|
+
async courseOpeners(orgId, courseId, orgUserIds) {
|
|
132
|
+
if (orgUserIds.length === 0) return [];
|
|
133
|
+
const [people, permissions] = await Promise.all([this.repo.authorsOf(orgId, orgUserIds), this.badgesOf(orgId, orgUserIds)]);
|
|
134
|
+
const opens = await Promise.all(orgUserIds.map(async (orgUserId) => {
|
|
135
|
+
if (!people[orgUserId]) return false;
|
|
136
|
+
return (permissions.get(orgUserId)?.has("courses:read", courseId) ?? false) || this.access.hasAccess(orgId, orgUserId, courseId);
|
|
137
|
+
}));
|
|
138
|
+
return orgUserIds.filter((_, index) => opens[index]);
|
|
139
|
+
}
|
|
140
|
+
async spaceOpeners(orgId, spaceId, orgUserIds) {
|
|
141
|
+
if (orgUserIds.length === 0) return [];
|
|
142
|
+
return keepAllowed(orgUserIds, await this.spaces.spaceOpenersAmong(orgId, spaceId, orgUserIds));
|
|
143
|
+
}
|
|
144
|
+
async openersOf(orgId, comment, orgUserIds) {
|
|
145
|
+
if (orgUserIds.length === 0) return [];
|
|
146
|
+
if (comment.postId) {
|
|
147
|
+
const post = await this.posts.find(orgId, comment.postId);
|
|
148
|
+
return post ? this.spaceOpeners(orgId, post.spaceId, orgUserIds) : [];
|
|
149
|
+
}
|
|
150
|
+
const activity = await this.activityOf(orgId, this.activityIdOf(comment));
|
|
151
|
+
return activity ? this.courseOpeners(orgId, activity.courseId, orgUserIds) : [];
|
|
105
152
|
}
|
|
106
|
-
|
|
107
|
-
|
|
153
|
+
commenterIds(orgId, target) {
|
|
154
|
+
return this.repo.commenterIds(orgId, target);
|
|
155
|
+
}
|
|
156
|
+
async postComment(input, actor) {
|
|
157
|
+
if (input.postId) return this.postOnPost(input.postId, input, actor);
|
|
108
158
|
const activityId = input.activityId;
|
|
109
159
|
if (!activityId) throw new NotFoundError("Activity", activityId ?? "");
|
|
110
|
-
return this.postOnActivity(
|
|
111
|
-
}
|
|
112
|
-
async
|
|
113
|
-
await this.
|
|
114
|
-
|
|
160
|
+
return this.postOnActivity(activityId, input, actor);
|
|
161
|
+
}
|
|
162
|
+
async replyToComment(commentId, body, actor) {
|
|
163
|
+
const parent = await this.getComment(actor.orgId, commentId);
|
|
164
|
+
if (!parent || parent.activityId === null) throw new NotFoundError("Comment", commentId);
|
|
165
|
+
const activity = await this.activityOf(actor.orgId, parent.activityId);
|
|
166
|
+
if (!activity) throw new NotFoundError("Comment", commentId);
|
|
167
|
+
this.requireModerates(actor, {
|
|
168
|
+
kind: "course",
|
|
169
|
+
courseId: activity.courseId,
|
|
170
|
+
activity
|
|
171
|
+
});
|
|
172
|
+
return this.postOnActivity(parent.activityId, {
|
|
173
|
+
activityId: parent.activityId,
|
|
174
|
+
parentId: parent.parentId ?? parent.id,
|
|
175
|
+
body
|
|
176
|
+
}, actor);
|
|
177
|
+
}
|
|
178
|
+
async postOnActivity(activityId, { parentId, body }, actor) {
|
|
179
|
+
const orgId = actor.orgId;
|
|
180
|
+
const activity = await this.requireActivityOpen(activityId, actor);
|
|
181
|
+
const config = await this.configOf(orgId, activity);
|
|
115
182
|
if (!config.enabled) throw new ForbiddenError("Discussion is not open on this activity");
|
|
116
183
|
if (parentId !== null) {
|
|
117
184
|
if (!config.threaded) throw new ForbiddenError("Replies are off on this course");
|
|
@@ -120,7 +187,7 @@ var DiscussionServiceImpl = class {
|
|
|
120
187
|
if (parent.parentId !== null) throw new ForbiddenError("A reply cannot have replies of its own");
|
|
121
188
|
if (parent.status !== "published") throw new ForbiddenError("You can reply only to a published comment");
|
|
122
189
|
}
|
|
123
|
-
const status = config.requireReview && !
|
|
190
|
+
const status = config.requireReview && !skipsReview(actor.permissions, { courseId: activity.courseId }) ? "pending" : "published";
|
|
124
191
|
const comment = {
|
|
125
192
|
id: genId("comment"),
|
|
126
193
|
orgId,
|
|
@@ -135,11 +202,17 @@ var DiscussionServiceImpl = class {
|
|
|
135
202
|
createdAt: /* @__PURE__ */ new Date(),
|
|
136
203
|
updatedAt: /* @__PURE__ */ new Date()
|
|
137
204
|
};
|
|
205
|
+
const mentioned = await this.courseOpeners(orgId, activity.courseId, mentionTokens(body));
|
|
138
206
|
const saved = await this.uow.run(async (scope) => {
|
|
139
207
|
const row = await scope.discussion.insertComment(orgId, comment);
|
|
208
|
+
await scope.mentions.sync(orgId, "comment", row.id, mentioned, comment.createdAt);
|
|
140
209
|
await scope.outbox.append([discussionEvents.commentCreated.make({
|
|
141
210
|
orgId,
|
|
142
|
-
data:
|
|
211
|
+
data: {
|
|
212
|
+
...row,
|
|
213
|
+
mentionedOrgUserIds: status === "published" ? mentioned : []
|
|
214
|
+
},
|
|
215
|
+
metadata: { actorId: actor.id }
|
|
143
216
|
})]);
|
|
144
217
|
this.logger.info("comment created", {
|
|
145
218
|
orgId,
|
|
@@ -149,10 +222,15 @@ var DiscussionServiceImpl = class {
|
|
|
149
222
|
});
|
|
150
223
|
return row;
|
|
151
224
|
});
|
|
152
|
-
return this.renderOne(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
225
|
+
return this.renderOne(saved, {
|
|
226
|
+
kind: "course",
|
|
227
|
+
courseId: activity.courseId,
|
|
228
|
+
activity
|
|
229
|
+
}, actor);
|
|
230
|
+
}
|
|
231
|
+
async postOnPost(postId, { parentId, body }, actor) {
|
|
232
|
+
const orgId = actor.orgId;
|
|
233
|
+
const { post, space } = await this.posts.visible(postId, actor);
|
|
156
234
|
if (post.status !== "published") throw new ConflictError("This post is not published");
|
|
157
235
|
if (!this.commentsOpen(post, space)) throw new ForbiddenError("Comments are closed on this post");
|
|
158
236
|
if (parentId !== null) {
|
|
@@ -176,12 +254,17 @@ var DiscussionServiceImpl = class {
|
|
|
176
254
|
createdAt: now,
|
|
177
255
|
updatedAt: now
|
|
178
256
|
};
|
|
257
|
+
const mentioned = await this.spaceOpeners(orgId, post.spaceId, mentionTokens(body));
|
|
179
258
|
const saved = await this.uow.run(async (scope) => {
|
|
180
259
|
const row = await scope.discussion.insertComment(orgId, comment);
|
|
260
|
+
await scope.mentions.sync(orgId, "comment", row.id, mentioned, now);
|
|
181
261
|
await scope.outbox.append([discussionEvents.commentCreated.make({
|
|
182
262
|
orgId,
|
|
183
|
-
data:
|
|
184
|
-
|
|
263
|
+
data: {
|
|
264
|
+
...row,
|
|
265
|
+
mentionedOrgUserIds: mentioned
|
|
266
|
+
},
|
|
267
|
+
metadata: { actorId: actor.id }
|
|
185
268
|
})]);
|
|
186
269
|
this.logger.info("comment created", {
|
|
187
270
|
orgId,
|
|
@@ -192,17 +275,23 @@ var DiscussionServiceImpl = class {
|
|
|
192
275
|
});
|
|
193
276
|
return row;
|
|
194
277
|
});
|
|
195
|
-
return this.renderOne(
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
278
|
+
return this.renderOne(saved, {
|
|
279
|
+
kind: "post",
|
|
280
|
+
communityId: post.communityId,
|
|
281
|
+
post,
|
|
282
|
+
space
|
|
283
|
+
}, actor);
|
|
284
|
+
}
|
|
285
|
+
async postComments(postId, query, actor) {
|
|
286
|
+
const orgId = actor.orgId;
|
|
287
|
+
const { post, space } = await this.posts.visible(postId, actor);
|
|
288
|
+
const seesHeld = actor.permissions.has("communities:moderate", post.communityId);
|
|
200
289
|
const page = await this.repo.listThreadsByPost(orgId, postId, {
|
|
201
290
|
...query,
|
|
202
291
|
viewerId: actor.id,
|
|
203
|
-
seesHeld
|
|
292
|
+
seesHeld
|
|
204
293
|
});
|
|
205
|
-
const comments = await this.thread(orgId, page.rows, actor, post.reactionsEnabled ?? space.settings.reactions, seesHeld);
|
|
294
|
+
const comments = await this.thread(orgId, page.rows, actor, post.communityId, post.reactionsEnabled ?? space.settings.reactions, seesHeld);
|
|
206
295
|
this.logger.debug("post comments read", {
|
|
207
296
|
orgId,
|
|
208
297
|
postId,
|
|
@@ -213,84 +302,142 @@ var DiscussionServiceImpl = class {
|
|
|
213
302
|
rows: comments
|
|
214
303
|
};
|
|
215
304
|
}
|
|
216
|
-
async thread(orgId, rows, actor, reactionsOn, seesHeld) {
|
|
305
|
+
async thread(orgId, rows, actor, itemId, reactionsOn, seesHeld) {
|
|
217
306
|
const readable = rows.filter((comment) => {
|
|
218
307
|
if (comment.status === "published" || comment.status === "removed") return true;
|
|
219
|
-
return seesHeld
|
|
308
|
+
return seesHeld || comment.orgUserId === actor.id;
|
|
220
309
|
});
|
|
221
310
|
const replies = readable.filter((comment) => comment.parentId !== null && comment.status !== "removed");
|
|
222
311
|
const heldOpen = new Set(replies.map((reply) => reply.parentId));
|
|
223
312
|
const roots = readable.filter((comment) => comment.parentId === null && (comment.status !== "removed" || heldOpen.has(comment.id)));
|
|
224
313
|
const rootIds = new Set(roots.map((root) => root.id));
|
|
225
314
|
const served = [...roots, ...replies.filter((reply) => rootIds.has(reply.parentId))].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
226
|
-
const
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
315
|
+
const people = this.peopleIn(served);
|
|
316
|
+
const [authors, permissions, reactions, mentioned] = await Promise.all([
|
|
317
|
+
this.repo.authorsOf(orgId, people),
|
|
318
|
+
this.badgesOf(orgId, people),
|
|
319
|
+
reactionsOn ? this.repo.reactionsOf(orgId, served.map((comment) => comment.id), actor.id) : Promise.resolve({}),
|
|
320
|
+
mentionViews(this.mentions, this.repo, orgId, "comment", served.map((comment) => comment.id))
|
|
321
|
+
]);
|
|
322
|
+
return served.map((comment) => this.toView(orgId, comment, itemId, authors, permissions, reactions[comment.id] ?? EMPTY_REACTIONS, mentioned[comment.id] ?? []));
|
|
323
|
+
}
|
|
324
|
+
async postOfComment(actor, commentId, postId) {
|
|
234
325
|
try {
|
|
235
|
-
return await this.
|
|
326
|
+
return await this.posts.visible(postId, actor);
|
|
236
327
|
} catch (error) {
|
|
237
328
|
if (error instanceof NotFoundError) throw new NotFoundError("Comment", commentId);
|
|
238
329
|
throw error;
|
|
239
330
|
}
|
|
240
331
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
return communityId === void 0 || comment.communityId === communityId;
|
|
332
|
+
isWithin(comment, within) {
|
|
333
|
+
if (within === "any") return true;
|
|
334
|
+
if (within === "lesson") return comment.activityId !== null;
|
|
335
|
+
return comment.communityId === within.communityId;
|
|
246
336
|
}
|
|
247
337
|
commentsOpen(post, space) {
|
|
248
338
|
return post.commentsClosed === null ? space.settings.comments : !post.commentsClosed;
|
|
249
339
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
340
|
+
/** Where the comment sits, having proved the actor may reach it: the post
|
|
341
|
+
* is visible to them, or they open the course. */
|
|
342
|
+
async whereOf(comment, actor) {
|
|
343
|
+
if (comment.postId) {
|
|
344
|
+
const { post, space } = await this.postOfComment(actor, comment.id, comment.postId);
|
|
345
|
+
return {
|
|
346
|
+
kind: "post",
|
|
347
|
+
communityId: post.communityId,
|
|
348
|
+
post,
|
|
349
|
+
space
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
const activity = await this.activityOf(actor.orgId, this.activityIdOf(comment));
|
|
353
|
+
if (!activity) throw new NotFoundError("Comment", comment.id);
|
|
354
|
+
if (!await this.opensCourse(actor, activity.courseId)) {
|
|
355
|
+
this.logger.warn("comment refused — no access", {
|
|
356
|
+
orgId: actor.orgId,
|
|
357
|
+
commentId: comment.id,
|
|
358
|
+
actorId: actor.id
|
|
359
|
+
});
|
|
360
|
+
throw new ForbiddenError("You cannot open this activity");
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
kind: "course",
|
|
364
|
+
courseId: activity.courseId,
|
|
365
|
+
activity
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
moderates(actor, where) {
|
|
369
|
+
return where.kind === "course" ? actor.permissions.has("courses:moderate", where.courseId) : actor.permissions.has("communities:moderate", where.communityId);
|
|
370
|
+
}
|
|
371
|
+
requireModerates(actor, where) {
|
|
372
|
+
actor.permissions.require(where.kind === "course" ? "courses:moderate" : "communities:moderate", this.itemOf(where));
|
|
373
|
+
}
|
|
374
|
+
async findIn(commentId, within, actor, action) {
|
|
375
|
+
const comment = await this.getComment(actor.orgId, commentId);
|
|
376
|
+
if (!comment || !this.isWithin(comment, within)) {
|
|
377
|
+
this.logger.warn(`comment ${action} rejected — not found`, {
|
|
378
|
+
orgId: actor.orgId,
|
|
379
|
+
commentId,
|
|
380
|
+
actorId: actor.id
|
|
381
|
+
});
|
|
382
|
+
throw new NotFoundError("Comment", commentId);
|
|
383
|
+
}
|
|
384
|
+
return comment;
|
|
385
|
+
}
|
|
386
|
+
async activityComments(activityId, actor) {
|
|
387
|
+
const activity = await this.requireActivityOpen(activityId, actor);
|
|
388
|
+
const config = await this.configOf(actor.orgId, activity);
|
|
253
389
|
if (!config.enabled) return {
|
|
254
390
|
config,
|
|
255
391
|
comments: []
|
|
256
392
|
};
|
|
257
|
-
const rows = await this.repo.listByActivity(orgId, activityId);
|
|
393
|
+
const rows = await this.repo.listByActivity(actor.orgId, activityId);
|
|
258
394
|
return {
|
|
259
395
|
config,
|
|
260
|
-
comments: await this.thread(orgId, rows, actor, config.reactions, (
|
|
396
|
+
comments: await this.thread(actor.orgId, rows, actor, activity.courseId, config.reactions, actor.permissions.has("courses:moderate", activity.courseId))
|
|
261
397
|
};
|
|
262
398
|
}
|
|
263
|
-
async edit(
|
|
264
|
-
const
|
|
399
|
+
async edit(commentId, body, actor) {
|
|
400
|
+
const orgId = actor.orgId;
|
|
401
|
+
const { comment, config, where } = await this.loadWithConfig(commentId, actor);
|
|
265
402
|
if (!config.enabled) throw new ForbiddenError(comment.postId ? "Comments are closed on this post" : "Discussion is not open on this activity");
|
|
266
403
|
if (comment.orgUserId !== actor.id) throw new ForbiddenError("You can edit only your own comment");
|
|
267
404
|
if (comment.status === "removed") throw new ForbiddenError("A removed comment cannot be edited");
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
405
|
+
const candidates = await this.openersOf(orgId, comment, mentionTokens(body));
|
|
406
|
+
const updated = await this.uow.run(async (scope) => {
|
|
407
|
+
const next = await scope.discussion.updateComment(orgId, commentId, { body });
|
|
408
|
+
if (!next) throw new NotFoundError("Comment", commentId);
|
|
409
|
+
const added = await scope.mentions.sync(orgId, "comment", commentId, candidates, /* @__PURE__ */ new Date());
|
|
410
|
+
await scope.outbox.append([discussionEvents.commentUpdated.make({
|
|
411
|
+
orgId,
|
|
412
|
+
data: {
|
|
413
|
+
...next,
|
|
414
|
+
mentionedOrgUserIds: next.status === "published" ? keepAllowed(candidates, added) : []
|
|
415
|
+
},
|
|
416
|
+
metadata: { actorId: actor.id }
|
|
417
|
+
})]);
|
|
418
|
+
return next;
|
|
419
|
+
});
|
|
420
|
+
this.logger.info("comment edited", {
|
|
421
|
+
orgId,
|
|
422
|
+
commentId,
|
|
423
|
+
actorId: actor.id
|
|
424
|
+
});
|
|
425
|
+
return this.renderOne(updated, where, actor);
|
|
426
|
+
}
|
|
427
|
+
async remove(commentId, within, actor) {
|
|
428
|
+
const orgId = actor.orgId;
|
|
429
|
+
const comment = await this.findIn(commentId, within, actor, "remove");
|
|
430
|
+
const where = await this.whereOf(comment, actor);
|
|
431
|
+
if (comment.orgUserId !== actor.id && !this.moderates(actor, where)) {
|
|
432
|
+
this.logger.warn("comment remove refused — not the author or a moderator", {
|
|
279
433
|
orgId,
|
|
280
434
|
commentId,
|
|
281
435
|
actorId: actor.id
|
|
282
436
|
});
|
|
283
|
-
throw new
|
|
437
|
+
throw new ForbiddenError("You cannot remove this comment");
|
|
284
438
|
}
|
|
285
|
-
if (comment.
|
|
286
|
-
|
|
287
|
-
if (comment.orgUserId !== actor.id && !this.canModerate(actor)) throw new ForbiddenError("You cannot remove this comment");
|
|
288
|
-
} else {
|
|
289
|
-
if (!await this.hasAccess(orgId, this.activityIdOf(comment), actor)) throw new ForbiddenError("You cannot open this activity");
|
|
290
|
-
if (comment.orgUserId !== actor.id && !this.isStaff(actor)) throw new ForbiddenError("You cannot remove this comment");
|
|
291
|
-
}
|
|
292
|
-
if (comment.status === "removed") return comment;
|
|
293
|
-
return this.uow.run(async (scope) => {
|
|
439
|
+
if (comment.status === "removed") return this.renderOne(comment, where, actor);
|
|
440
|
+
const removed = await this.uow.run(async (scope) => {
|
|
294
441
|
const updated = await scope.discussion.updateComment(orgId, commentId, {
|
|
295
442
|
status: "removed",
|
|
296
443
|
removedBy: actor.id
|
|
@@ -304,7 +451,7 @@ var DiscussionServiceImpl = class {
|
|
|
304
451
|
await scope.outbox.append([discussionEvents.commentRemoved.make({
|
|
305
452
|
orgId,
|
|
306
453
|
data: updated,
|
|
307
|
-
metadata:
|
|
454
|
+
metadata: { actorId: actor.id }
|
|
308
455
|
})]);
|
|
309
456
|
this.logger.info("comment removed", {
|
|
310
457
|
orgId,
|
|
@@ -313,85 +460,96 @@ var DiscussionServiceImpl = class {
|
|
|
313
460
|
});
|
|
314
461
|
return updated;
|
|
315
462
|
});
|
|
463
|
+
return this.renderOne(removed, where, actor);
|
|
316
464
|
}
|
|
317
|
-
async restore(
|
|
318
|
-
|
|
465
|
+
async restore(commentId, within, actor) {
|
|
466
|
+
const comment = await this.findIn(commentId, within, actor, "restore");
|
|
467
|
+
const where = await this.whereOf(comment, actor);
|
|
468
|
+
this.requireModerates(actor, where);
|
|
469
|
+
return this.renderOne(await this.restoreComment(comment, actor), where, actor);
|
|
319
470
|
}
|
|
320
|
-
async restoreComment(
|
|
321
|
-
if (!this.isStaff(actor)) throw new ForbiddenError("You cannot restore this comment");
|
|
322
|
-
const comment = await this.getComment(orgId, commentId);
|
|
323
|
-
if (!comment || !this.inCommunity(comment, communityId)) {
|
|
324
|
-
this.logger.warn("comment restore rejected — not found", {
|
|
325
|
-
orgId,
|
|
326
|
-
commentId,
|
|
327
|
-
actorId: actor.id
|
|
328
|
-
});
|
|
329
|
-
throw new NotFoundError("Comment", commentId);
|
|
330
|
-
}
|
|
471
|
+
async restoreComment(comment, actor) {
|
|
331
472
|
if (comment.status !== "removed") return comment;
|
|
332
|
-
const updated = await this.repo.updateComment(orgId,
|
|
473
|
+
const updated = await this.repo.updateComment(actor.orgId, comment.id, {
|
|
333
474
|
status: "published",
|
|
334
475
|
removedBy: null
|
|
335
476
|
});
|
|
336
|
-
if (!updated) throw new NotFoundError("Comment",
|
|
477
|
+
if (!updated) throw new NotFoundError("Comment", comment.id);
|
|
337
478
|
this.logger.info("comment restored", {
|
|
338
|
-
orgId,
|
|
339
|
-
commentId,
|
|
479
|
+
orgId: actor.orgId,
|
|
480
|
+
commentId: comment.id,
|
|
340
481
|
actorId: actor.id
|
|
341
482
|
});
|
|
342
483
|
return updated;
|
|
343
484
|
}
|
|
344
|
-
/** Load a comment together with
|
|
345
|
-
* having proved the actor may reach
|
|
346
|
-
* acts on one comment needs
|
|
347
|
-
async loadWithConfig(
|
|
348
|
-
const comment = await this.getComment(orgId, commentId);
|
|
485
|
+
/** Load a comment together with where it sits and the comments config that
|
|
486
|
+
* applies there, having proved the actor may reach it at all. Every path
|
|
487
|
+
* that acts on one comment needs them, and none should read the row twice. */
|
|
488
|
+
async loadWithConfig(commentId, actor) {
|
|
489
|
+
const comment = await this.getComment(actor.orgId, commentId);
|
|
349
490
|
if (!comment) throw new NotFoundError("Comment", commentId);
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}
|
|
362
|
-
const activityId = this.activityIdOf(comment);
|
|
363
|
-
if (!await this.hasAccess(orgId, activityId, actor)) throw new ForbiddenError("You cannot open this activity");
|
|
491
|
+
const where = await this.whereOf(comment, actor);
|
|
492
|
+
if (where.kind === "post") return {
|
|
493
|
+
comment,
|
|
494
|
+
config: {
|
|
495
|
+
enabled: this.commentsOpen(where.post, where.space),
|
|
496
|
+
threaded: true,
|
|
497
|
+
requireReview: false,
|
|
498
|
+
reactions: where.post.reactionsEnabled ?? where.space.settings.reactions
|
|
499
|
+
},
|
|
500
|
+
where
|
|
501
|
+
};
|
|
364
502
|
return {
|
|
365
503
|
comment,
|
|
366
|
-
config: await this.
|
|
504
|
+
config: await this.configOf(actor.orgId, where.activity),
|
|
505
|
+
where
|
|
367
506
|
};
|
|
368
507
|
}
|
|
369
|
-
async setReaction(
|
|
370
|
-
const
|
|
508
|
+
async setReaction(commentId, emoji, actor) {
|
|
509
|
+
const orgId = actor.orgId;
|
|
510
|
+
const { comment, config, where } = await this.loadWithConfig(commentId, actor);
|
|
371
511
|
if (!config.enabled && !comment.postId) throw new ForbiddenError("Discussion is not open on this activity");
|
|
372
512
|
if (!config.reactions && emoji !== null) throw new ForbiddenError(comment.postId ? "Reactions are off on this post" : "Reactions are off on this course");
|
|
373
513
|
if (comment.status === "removed") throw new ForbiddenError("A removed comment cannot be reacted to");
|
|
374
|
-
await this.
|
|
514
|
+
await this.uow.run(async (scope) => {
|
|
515
|
+
if (!await scope.discussion.setReaction(orgId, commentId, actor.id, emoji) || emoji === null) return;
|
|
516
|
+
await scope.outbox.append([discussionEvents.commentReactionAdded.make({
|
|
517
|
+
orgId,
|
|
518
|
+
data: {
|
|
519
|
+
commentId,
|
|
520
|
+
postId: comment.postId,
|
|
521
|
+
activityId: comment.activityId,
|
|
522
|
+
spaceId: where.kind === "post" ? where.post.spaceId : null,
|
|
523
|
+
communityId: comment.communityId,
|
|
524
|
+
orgUserId: actor.id,
|
|
525
|
+
reaction: emoji
|
|
526
|
+
},
|
|
527
|
+
metadata: { actorId: actor.id }
|
|
528
|
+
})]);
|
|
529
|
+
});
|
|
375
530
|
return (await this.repo.reactionsOf(orgId, [commentId], actor.id))[commentId] ?? EMPTY_REACTIONS;
|
|
376
531
|
}
|
|
377
|
-
async approve(
|
|
378
|
-
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
throw new NotFoundError("Comment", commentId);
|
|
387
|
-
}
|
|
532
|
+
async approve(commentId, actor) {
|
|
533
|
+
const comment = await this.findIn(commentId, "any", actor, "approve");
|
|
534
|
+
const where = await this.whereOf(comment, actor);
|
|
535
|
+
this.requireModerates(actor, where);
|
|
536
|
+
return this.approveComment(comment, actor);
|
|
537
|
+
}
|
|
538
|
+
async approveComment(comment, actor) {
|
|
539
|
+
const orgId = actor.orgId;
|
|
540
|
+
const commentId = comment.id;
|
|
388
541
|
if (comment.status !== "pending") throw new ForbiddenError("Only a comment awaiting review can be approved");
|
|
542
|
+
const mentioned = await this.openersOf(orgId, comment, await this.mentions.ofItem(orgId, "comment", commentId));
|
|
389
543
|
return this.uow.run(async (scope) => {
|
|
390
544
|
const updated = await scope.discussion.updateComment(orgId, commentId, { status: "published" });
|
|
391
545
|
if (!updated) throw new NotFoundError("Comment", commentId);
|
|
392
546
|
await scope.outbox.append([discussionEvents.commentPublished.make({
|
|
393
547
|
orgId,
|
|
394
|
-
data:
|
|
548
|
+
data: {
|
|
549
|
+
...updated,
|
|
550
|
+
mentionedOrgUserIds: mentioned
|
|
551
|
+
},
|
|
552
|
+
metadata: { actorId: actor.id }
|
|
395
553
|
})]);
|
|
396
554
|
this.logger.info("comment published", {
|
|
397
555
|
orgId,
|
|
@@ -402,51 +560,33 @@ var DiscussionServiceImpl = class {
|
|
|
402
560
|
});
|
|
403
561
|
}
|
|
404
562
|
/** Loads the comment and dispatches to whichever transition applies — the
|
|
405
|
-
* caller must not decide that itself.
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
if (
|
|
412
|
-
this.logger.warn("comment publish rejected — not found", {
|
|
413
|
-
orgId,
|
|
414
|
-
commentId,
|
|
415
|
-
actorId: actor.id
|
|
416
|
-
});
|
|
417
|
-
throw new NotFoundError("Comment", commentId);
|
|
418
|
-
}
|
|
419
|
-
if (comment.status === "pending") return this.approve(orgId, commentId, actor);
|
|
420
|
-
if (comment.status === "removed") return this.restoreComment(orgId, commentId, actor);
|
|
563
|
+
* caller must not decide that itself. */
|
|
564
|
+
async publish(commentId, within, actor) {
|
|
565
|
+
const comment = await this.findIn(commentId, within, actor, "publish");
|
|
566
|
+
const where = await this.whereOf(comment, actor);
|
|
567
|
+
this.requireModerates(actor, where);
|
|
568
|
+
if (comment.status === "pending") return this.approveComment(comment, actor);
|
|
569
|
+
if (comment.status === "removed") return this.restoreComment(comment, actor);
|
|
421
570
|
return comment;
|
|
422
571
|
}
|
|
423
|
-
async resolveReports(
|
|
424
|
-
const comment = await this.
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
commentId,
|
|
429
|
-
actorId: actor.id
|
|
430
|
-
});
|
|
431
|
-
throw new NotFoundError("Comment", commentId);
|
|
432
|
-
}
|
|
433
|
-
if (comment.postId) {
|
|
434
|
-
await this.postOfComment(orgId, actor, commentId, comment.postId);
|
|
435
|
-
if (!this.canModerate(actor)) throw new ForbiddenError("You cannot work this queue");
|
|
436
|
-
} else if (!this.isStaff(actor)) throw new ForbiddenError("You cannot resolve this report");
|
|
437
|
-
await this.repo.resolveReportsFor(orgId, commentId, {
|
|
572
|
+
async resolveReports(commentId, within, actor) {
|
|
573
|
+
const comment = await this.findIn(commentId, within, actor, "reports resolve");
|
|
574
|
+
const where = await this.whereOf(comment, actor);
|
|
575
|
+
this.requireModerates(actor, where);
|
|
576
|
+
await this.repo.resolveReportsFor(actor.orgId, commentId, {
|
|
438
577
|
resolvedAt: /* @__PURE__ */ new Date(),
|
|
439
578
|
resolvedBy: actor.id,
|
|
440
579
|
resolution: "dismissed"
|
|
441
580
|
});
|
|
442
581
|
this.logger.info("comment reports resolved", {
|
|
443
|
-
orgId,
|
|
582
|
+
orgId: actor.orgId,
|
|
444
583
|
commentId,
|
|
445
584
|
actorId: actor.id
|
|
446
585
|
});
|
|
447
586
|
}
|
|
448
|
-
async reportComment(
|
|
449
|
-
const
|
|
587
|
+
async reportComment(commentId, reason, actor) {
|
|
588
|
+
const orgId = actor.orgId;
|
|
589
|
+
const { comment, config } = await this.loadWithConfig(commentId, actor);
|
|
450
590
|
if (!config.enabled && !comment.postId) throw new ForbiddenError("Discussion is not open on this activity");
|
|
451
591
|
const now = /* @__PURE__ */ new Date();
|
|
452
592
|
const report = {
|
|
@@ -474,7 +614,7 @@ var DiscussionServiceImpl = class {
|
|
|
474
614
|
...saved,
|
|
475
615
|
communityId: comment.communityId
|
|
476
616
|
},
|
|
477
|
-
metadata:
|
|
617
|
+
metadata: { actorId: actor.id }
|
|
478
618
|
})]);
|
|
479
619
|
this.logger.info("comment reported", {
|
|
480
620
|
orgId,
|
|
@@ -484,15 +624,25 @@ var DiscussionServiceImpl = class {
|
|
|
484
624
|
return saved;
|
|
485
625
|
});
|
|
486
626
|
}
|
|
487
|
-
async listComments(
|
|
488
|
-
const
|
|
627
|
+
async listComments(query, actor) {
|
|
628
|
+
const orgId = actor.orgId;
|
|
629
|
+
const where = actor.permissions.where("courses:moderate");
|
|
630
|
+
if (!where.org && where.content.length === 0) {
|
|
631
|
+
this.logger.warn("comment list refused — moderates no course", {
|
|
632
|
+
orgId,
|
|
633
|
+
actorId: actor.id
|
|
634
|
+
});
|
|
635
|
+
throw new ForbiddenError("You need courses:moderate");
|
|
636
|
+
}
|
|
637
|
+
const page = await this.repo.listComments(orgId, query, where.org ? void 0 : where.content);
|
|
489
638
|
const rows = page.rows.map((r) => r.comment);
|
|
490
639
|
if (rows.length === 0) return {
|
|
491
640
|
...page,
|
|
492
641
|
rows: []
|
|
493
642
|
};
|
|
494
643
|
const reports = await this.repo.listOpenReports(orgId, rows.map((c) => c.id));
|
|
495
|
-
const
|
|
644
|
+
const people = [.../* @__PURE__ */ new Set([...this.peopleIn(rows), ...reports.map((r) => r.orgUserId)])];
|
|
645
|
+
const [authors, permissions] = await Promise.all([this.repo.authorsOf(orgId, people), this.badgesOf(orgId, people)]);
|
|
496
646
|
const urls = this.fileUrls;
|
|
497
647
|
const byComment = /* @__PURE__ */ new Map();
|
|
498
648
|
for (const report of reports) byComment.set(report.commentId, [...byComment.get(report.commentId) ?? [], report]);
|
|
@@ -509,14 +659,14 @@ var DiscussionServiceImpl = class {
|
|
|
509
659
|
courseId,
|
|
510
660
|
body: comment.body,
|
|
511
661
|
status: comment.status,
|
|
512
|
-
author: authorView(orgId, author, urls),
|
|
662
|
+
author: authorView(orgId, author, urls, teamBadge(permissions.get(author.id), courseId)),
|
|
513
663
|
authorEmail: author.email,
|
|
514
664
|
removedBy: comment.removedBy,
|
|
515
665
|
reports: (byComment.get(comment.id) ?? []).map((r) => {
|
|
516
666
|
const reporter = authors[r.orgUserId];
|
|
517
667
|
if (!reporter) throw new NotFoundError("Member", r.orgUserId);
|
|
518
668
|
return {
|
|
519
|
-
reporter: authorView(orgId, reporter, urls),
|
|
669
|
+
reporter: authorView(orgId, reporter, urls, teamBadge(permissions.get(reporter.id), courseId)),
|
|
520
670
|
reason: r.reason,
|
|
521
671
|
createdAt: r.createdAt
|
|
522
672
|
};
|
|
@@ -530,28 +680,6 @@ var DiscussionServiceImpl = class {
|
|
|
530
680
|
getComment(orgId, commentId) {
|
|
531
681
|
return this.repo.findComment(orgId, commentId);
|
|
532
682
|
}
|
|
533
|
-
async sendDigest(orgId, now) {
|
|
534
|
-
const communityIds = await this.communities.listIds(orgId);
|
|
535
|
-
if (communityIds.length === 0) {
|
|
536
|
-
this.logger.debug("no communities for digest", { orgId });
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
|
-
const period = now.toISOString().slice(0, 10);
|
|
540
|
-
await this.uow.run(async ({ outbox }) => {
|
|
541
|
-
await outbox.append(communityIds.map((communityId) => discussionEvents.digestDue.make({
|
|
542
|
-
orgId,
|
|
543
|
-
data: {
|
|
544
|
-
communityId,
|
|
545
|
-
period
|
|
546
|
-
}
|
|
547
|
-
})));
|
|
548
|
-
});
|
|
549
|
-
this.logger.info("digest due", {
|
|
550
|
-
orgId,
|
|
551
|
-
communities: communityIds.length,
|
|
552
|
-
period
|
|
553
|
-
});
|
|
554
|
-
}
|
|
555
683
|
};
|
|
556
684
|
//#endregion
|
|
557
685
|
export { DEFAULT_SETTINGS, DiscussionServiceImpl };
|