@headless-lms/core 0.4.0 → 0.6.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 +15 -9
- package/dist/assets/file-urls.d.ts +14 -7
- package/dist/assets/file-urls.js +15 -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 +233 -54
- package/dist/assets/service.js.map +1 -1
- package/dist/automations/actions.js.map +1 -1
- package/dist/automations/catalog.js +68 -3
- 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 +1 -1
- 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 +12 -12
- 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 +90 -61
- 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 +465 -88
- 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 +103 -67
- package/dist/content/service.js +739 -152
- 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 +392 -160
- package/dist/discussion/chats.js.map +1 -1
- package/dist/discussion/events.d.ts +85 -47
- 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 +266 -152
- 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 +250 -186
- package/dist/discussion/service.js.map +1 -1
- package/dist/discussion/subscriptions.d.ts +8 -8
- package/dist/discussion/subscriptions.js +28 -12
- 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 +58 -58
- 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 +369 -162
- package/dist/entitlements/service.js.map +1 -1
- package/dist/identity/devices.d.ts +3 -3
- package/dist/identity/devices.js +6 -4
- package/dist/identity/devices.js.map +1 -1
- 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 +2 -1
- 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 +16 -12
- 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 +31 -14
- 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 +47 -0
- package/dist/organizations/effective-permissions.js +118 -0
- package/dist/organizations/effective-permissions.js.map +1 -0
- package/dist/organizations/events.d.ts +326 -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/model.d.ts +2 -2
- 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 +187 -65
- package/dist/organizations/service.d.ts +102 -35
- package/dist/organizations/service.js +1254 -327
- 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 +21 -6
- 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 +29 -5
- package/dist/reporting/member/ports.d.ts +49 -29
- package/dist/reporting/member/service.d.ts +57 -29
- package/dist/reporting/member/service.js +345 -114
- package/dist/reporting/member/service.js.map +1 -1
- package/dist/shared/actor.d.ts +23 -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 +599 -13
- package/dist/types/schemas/content.js +219 -8
- package/dist/types/schemas/content.js.map +1 -1
- package/dist/types/schemas/discussion.d.ts +280 -57
- package/dist/types/schemas/discussion.js +77 -3
- package/dist/types/schemas/discussion.js.map +1 -1
- package/dist/types/schemas/entitlements.d.ts +21 -8
- 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 +11 -10
- package/dist/types/schemas/notifications.js +3 -2
- package/dist/types/schemas/notifications.js.map +1 -1
- package/dist/types/schemas/organizations.d.ts +99 -60
- package/dist/types/schemas/organizations.js +80 -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
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { genId } from "../shared/id.js";
|
|
2
2
|
import { ConflictError, ForbiddenError, NotFoundError } from "../shared/errors.js";
|
|
3
3
|
import { noopLogger } from "../shared/logger.js";
|
|
4
|
-
import { canModerate, isStaffRole } from "../organizations/roles.js";
|
|
5
4
|
import "../shared/settings.js";
|
|
6
5
|
import { discussionEvents } from "./events.js";
|
|
7
6
|
import { authorView } from "./authors.js";
|
|
8
7
|
import { keepAllowed, mentionTokens, mentionViews } from "./mentions.js";
|
|
8
|
+
import { skipsReview, teamBadge } from "./rules.js";
|
|
9
9
|
//#region src/discussion/service.ts
|
|
10
10
|
/** A course with no stored settings. Discussion is opt-in, so the common case
|
|
11
11
|
* persists no row at all and every existing course stays silent. */
|
|
@@ -22,6 +22,7 @@ var DiscussionServiceImpl = class {
|
|
|
22
22
|
content;
|
|
23
23
|
posts;
|
|
24
24
|
spaces;
|
|
25
|
+
authors;
|
|
25
26
|
mentions;
|
|
26
27
|
uow;
|
|
27
28
|
settings;
|
|
@@ -33,21 +34,20 @@ var DiscussionServiceImpl = class {
|
|
|
33
34
|
this.content = params.content;
|
|
34
35
|
this.posts = params.posts;
|
|
35
36
|
this.spaces = params.spaces;
|
|
37
|
+
this.authors = params.authors;
|
|
36
38
|
this.mentions = params.mentions;
|
|
37
39
|
this.uow = params.uow;
|
|
38
40
|
this.settings = params.settings;
|
|
39
41
|
this.fileUrls = params.fileUrls;
|
|
40
42
|
this.logger = params.logger ?? noopLogger;
|
|
41
43
|
}
|
|
42
|
-
async
|
|
43
|
-
const activity = await this.content.getActivity(orgId, activityId);
|
|
44
|
-
if (!activity) throw new NotFoundError("Activity", activityId);
|
|
44
|
+
async configOf(orgId, activity) {
|
|
45
45
|
const stored = await this.settings.get(orgId, "content", activity.courseId);
|
|
46
46
|
const settings = {
|
|
47
47
|
...DEFAULT_SETTINGS,
|
|
48
48
|
...stored?.comments
|
|
49
49
|
};
|
|
50
|
-
const rule = (activity
|
|
50
|
+
const rule = (activity.settings ?? {}).comments ?? "inherit";
|
|
51
51
|
return {
|
|
52
52
|
enabled: settings.enabled && rule !== "never",
|
|
53
53
|
threaded: settings.threaded,
|
|
@@ -55,10 +55,10 @@ var DiscussionServiceImpl = class {
|
|
|
55
55
|
reactions: settings.reactions
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
|
-
|
|
59
|
-
return
|
|
58
|
+
async badgesOf(orgId, orgUserIds) {
|
|
59
|
+
return this.authors.permissionsOfMany(orgId, [...new Set(orgUserIds)]);
|
|
60
60
|
}
|
|
61
|
-
toView(orgId, comment, authors,
|
|
61
|
+
toView(orgId, comment, itemId, authors, permissions, reactions, mentions) {
|
|
62
62
|
const author = authors[comment.orgUserId];
|
|
63
63
|
if (!author) throw new NotFoundError("Member", comment.orgUserId);
|
|
64
64
|
if (comment.removedBy && !authors[comment.removedBy]) throw new NotFoundError("Member", comment.removedBy);
|
|
@@ -69,10 +69,10 @@ var DiscussionServiceImpl = class {
|
|
|
69
69
|
postId: comment.postId,
|
|
70
70
|
communityId: comment.communityId,
|
|
71
71
|
parentId: comment.parentId,
|
|
72
|
-
author: authorView(orgId, author,
|
|
72
|
+
author: authorView(orgId, author, this.fileUrls, teamBadge(permissions.get(author.id), itemId)),
|
|
73
73
|
body: comment.status === "removed" ? null : comment.body,
|
|
74
74
|
status: comment.status,
|
|
75
|
-
removedBy: remover ? authorView(orgId, remover,
|
|
75
|
+
removedBy: remover ? authorView(orgId, remover, this.fileUrls, teamBadge(permissions.get(remover.id), itemId)) : null,
|
|
76
76
|
reactions: reactions.reactions,
|
|
77
77
|
viewerReaction: reactions.viewerReaction,
|
|
78
78
|
mentions: comment.status === "removed" ? [] : mentions,
|
|
@@ -88,33 +88,52 @@ var DiscussionServiceImpl = class {
|
|
|
88
88
|
}
|
|
89
89
|
return [...ids];
|
|
90
90
|
}
|
|
91
|
-
|
|
92
|
-
|
|
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
|
+
]);
|
|
93
102
|
const stored = comment.status === "removed" ? EMPTY_REACTIONS : (await this.repo.reactionsOf(orgId, [comment.id], actor.id))[comment.id] ?? EMPTY_REACTIONS;
|
|
94
|
-
return this.toView(orgId, comment, authors,
|
|
103
|
+
return this.toView(orgId, comment, this.itemOf(where), authors, permissions, stored, mentioned[comment.id] ?? []);
|
|
95
104
|
}
|
|
96
105
|
activityIdOf(comment) {
|
|
97
106
|
if (comment.activityId === null) throw new NotFoundError("Activity", comment.id);
|
|
98
107
|
return comment.activityId;
|
|
99
108
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const activity = await this.content.getActivity(orgId, activityId);
|
|
103
|
-
return activity ? this.access.hasAccess(orgId, actor.id, activity.courseId) : false;
|
|
109
|
+
system(orgId) {
|
|
110
|
+
return this.authors.systemActor(orgId);
|
|
104
111
|
}
|
|
105
|
-
async
|
|
106
|
-
|
|
112
|
+
async activityOf(orgId, activityId) {
|
|
113
|
+
return this.content.getActivity(activityId, this.system(orgId));
|
|
114
|
+
}
|
|
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);
|
|
107
120
|
if (!activity) throw new NotFoundError("Activity", activityId);
|
|
108
|
-
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
|
+
}
|
|
109
129
|
return activity;
|
|
110
130
|
}
|
|
111
131
|
async courseOpeners(orgId, courseId, orgUserIds) {
|
|
112
132
|
if (orgUserIds.length === 0) return [];
|
|
113
|
-
const people = await this.repo.authorsOf(orgId, orgUserIds);
|
|
133
|
+
const [people, permissions] = await Promise.all([this.repo.authorsOf(orgId, orgUserIds), this.badgesOf(orgId, orgUserIds)]);
|
|
114
134
|
const opens = await Promise.all(orgUserIds.map(async (orgUserId) => {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return isStaffRole(person.role) || this.access.hasAccess(orgId, orgUserId, courseId);
|
|
135
|
+
if (!people[orgUserId]) return false;
|
|
136
|
+
return (permissions.get(orgUserId)?.has("courses:read", courseId) ?? false) || this.access.hasAccess(orgId, orgUserId, courseId);
|
|
118
137
|
}));
|
|
119
138
|
return orgUserIds.filter((_, index) => opens[index]);
|
|
120
139
|
}
|
|
@@ -128,21 +147,38 @@ var DiscussionServiceImpl = class {
|
|
|
128
147
|
const post = await this.posts.find(orgId, comment.postId);
|
|
129
148
|
return post ? this.spaceOpeners(orgId, post.spaceId, orgUserIds) : [];
|
|
130
149
|
}
|
|
131
|
-
const activity = await this.
|
|
150
|
+
const activity = await this.activityOf(orgId, this.activityIdOf(comment));
|
|
132
151
|
return activity ? this.courseOpeners(orgId, activity.courseId, orgUserIds) : [];
|
|
133
152
|
}
|
|
134
153
|
commenterIds(orgId, target) {
|
|
135
154
|
return this.repo.commenterIds(orgId, target);
|
|
136
155
|
}
|
|
137
|
-
async postComment(
|
|
138
|
-
if (input.postId) return this.postOnPost(
|
|
156
|
+
async postComment(input, actor) {
|
|
157
|
+
if (input.postId) return this.postOnPost(input.postId, input, actor);
|
|
139
158
|
const activityId = input.activityId;
|
|
140
159
|
if (!activityId) throw new NotFoundError("Activity", activityId ?? "");
|
|
141
|
-
return this.postOnActivity(
|
|
142
|
-
}
|
|
143
|
-
async
|
|
144
|
-
const
|
|
145
|
-
|
|
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);
|
|
146
182
|
if (!config.enabled) throw new ForbiddenError("Discussion is not open on this activity");
|
|
147
183
|
if (parentId !== null) {
|
|
148
184
|
if (!config.threaded) throw new ForbiddenError("Replies are off on this course");
|
|
@@ -151,7 +187,7 @@ var DiscussionServiceImpl = class {
|
|
|
151
187
|
if (parent.parentId !== null) throw new ForbiddenError("A reply cannot have replies of its own");
|
|
152
188
|
if (parent.status !== "published") throw new ForbiddenError("You can reply only to a published comment");
|
|
153
189
|
}
|
|
154
|
-
const status = config.requireReview && !
|
|
190
|
+
const status = config.requireReview && !skipsReview(actor.permissions, { courseId: activity.courseId }) ? "pending" : "published";
|
|
155
191
|
const comment = {
|
|
156
192
|
id: genId("comment"),
|
|
157
193
|
orgId,
|
|
@@ -176,7 +212,7 @@ var DiscussionServiceImpl = class {
|
|
|
176
212
|
...row,
|
|
177
213
|
mentionedOrgUserIds: status === "published" ? mentioned : []
|
|
178
214
|
},
|
|
179
|
-
metadata: {
|
|
215
|
+
metadata: { actorId: actor.id }
|
|
180
216
|
})]);
|
|
181
217
|
this.logger.info("comment created", {
|
|
182
218
|
orgId,
|
|
@@ -186,10 +222,15 @@ var DiscussionServiceImpl = class {
|
|
|
186
222
|
});
|
|
187
223
|
return row;
|
|
188
224
|
});
|
|
189
|
-
return this.renderOne(
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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);
|
|
193
234
|
if (post.status !== "published") throw new ConflictError("This post is not published");
|
|
194
235
|
if (!this.commentsOpen(post, space)) throw new ForbiddenError("Comments are closed on this post");
|
|
195
236
|
if (parentId !== null) {
|
|
@@ -223,7 +264,7 @@ var DiscussionServiceImpl = class {
|
|
|
223
264
|
...row,
|
|
224
265
|
mentionedOrgUserIds: mentioned
|
|
225
266
|
},
|
|
226
|
-
metadata: {
|
|
267
|
+
metadata: { actorId: actor.id }
|
|
227
268
|
})]);
|
|
228
269
|
this.logger.info("comment created", {
|
|
229
270
|
orgId,
|
|
@@ -234,17 +275,23 @@ var DiscussionServiceImpl = class {
|
|
|
234
275
|
});
|
|
235
276
|
return row;
|
|
236
277
|
});
|
|
237
|
-
return this.renderOne(
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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);
|
|
242
289
|
const page = await this.repo.listThreadsByPost(orgId, postId, {
|
|
243
290
|
...query,
|
|
244
291
|
viewerId: actor.id,
|
|
245
|
-
seesHeld
|
|
292
|
+
seesHeld
|
|
246
293
|
});
|
|
247
|
-
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);
|
|
248
295
|
this.logger.debug("post comments read", {
|
|
249
296
|
orgId,
|
|
250
297
|
postId,
|
|
@@ -255,59 +302,103 @@ var DiscussionServiceImpl = class {
|
|
|
255
302
|
rows: comments
|
|
256
303
|
};
|
|
257
304
|
}
|
|
258
|
-
async thread(orgId, rows, actor, reactionsOn, seesHeld) {
|
|
305
|
+
async thread(orgId, rows, actor, itemId, reactionsOn, seesHeld) {
|
|
259
306
|
const readable = rows.filter((comment) => {
|
|
260
307
|
if (comment.status === "published" || comment.status === "removed") return true;
|
|
261
|
-
return seesHeld
|
|
308
|
+
return seesHeld || comment.orgUserId === actor.id;
|
|
262
309
|
});
|
|
263
310
|
const replies = readable.filter((comment) => comment.parentId !== null && comment.status !== "removed");
|
|
264
311
|
const heldOpen = new Set(replies.map((reply) => reply.parentId));
|
|
265
312
|
const roots = readable.filter((comment) => comment.parentId === null && (comment.status !== "removed" || heldOpen.has(comment.id)));
|
|
266
313
|
const rootIds = new Set(roots.map((root) => root.id));
|
|
267
314
|
const served = [...roots, ...replies.filter((reply) => rootIds.has(reply.parentId))].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
268
|
-
const
|
|
269
|
-
|
|
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),
|
|
270
319
|
reactionsOn ? this.repo.reactionsOf(orgId, served.map((comment) => comment.id), actor.id) : Promise.resolve({}),
|
|
271
320
|
mentionViews(this.mentions, this.repo, orgId, "comment", served.map((comment) => comment.id))
|
|
272
321
|
]);
|
|
273
|
-
|
|
274
|
-
return served.map((comment) => this.toView(orgId, comment, authors, urls, reactions[comment.id] ?? EMPTY_REACTIONS, mentioned[comment.id] ?? []));
|
|
322
|
+
return served.map((comment) => this.toView(orgId, comment, itemId, authors, permissions, reactions[comment.id] ?? EMPTY_REACTIONS, mentioned[comment.id] ?? []));
|
|
275
323
|
}
|
|
276
|
-
async
|
|
277
|
-
return this.posts.visible(orgId, actor, postId);
|
|
278
|
-
}
|
|
279
|
-
async postOfComment(orgId, actor, commentId, postId) {
|
|
324
|
+
async postOfComment(actor, commentId, postId) {
|
|
280
325
|
try {
|
|
281
|
-
return await this.
|
|
326
|
+
return await this.posts.visible(postId, actor);
|
|
282
327
|
} catch (error) {
|
|
283
328
|
if (error instanceof NotFoundError) throw new NotFoundError("Comment", commentId);
|
|
284
329
|
throw error;
|
|
285
330
|
}
|
|
286
331
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
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;
|
|
292
336
|
}
|
|
293
337
|
commentsOpen(post, space) {
|
|
294
338
|
return post.commentsClosed === null ? space.settings.comments : !post.commentsClosed;
|
|
295
339
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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);
|
|
299
389
|
if (!config.enabled) return {
|
|
300
390
|
config,
|
|
301
391
|
comments: []
|
|
302
392
|
};
|
|
303
|
-
const rows = await this.repo.listByActivity(orgId, activityId);
|
|
393
|
+
const rows = await this.repo.listByActivity(actor.orgId, activityId);
|
|
304
394
|
return {
|
|
305
395
|
config,
|
|
306
|
-
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))
|
|
307
397
|
};
|
|
308
398
|
}
|
|
309
|
-
async edit(
|
|
310
|
-
const
|
|
399
|
+
async edit(commentId, body, actor) {
|
|
400
|
+
const orgId = actor.orgId;
|
|
401
|
+
const { comment, config, where } = await this.loadWithConfig(commentId, actor);
|
|
311
402
|
if (!config.enabled) throw new ForbiddenError(comment.postId ? "Comments are closed on this post" : "Discussion is not open on this activity");
|
|
312
403
|
if (comment.orgUserId !== actor.id) throw new ForbiddenError("You can edit only your own comment");
|
|
313
404
|
if (comment.status === "removed") throw new ForbiddenError("A removed comment cannot be edited");
|
|
@@ -322,7 +413,7 @@ var DiscussionServiceImpl = class {
|
|
|
322
413
|
...next,
|
|
323
414
|
mentionedOrgUserIds: next.status === "published" ? keepAllowed(candidates, added) : []
|
|
324
415
|
},
|
|
325
|
-
metadata: {
|
|
416
|
+
metadata: { actorId: actor.id }
|
|
326
417
|
})]);
|
|
327
418
|
return next;
|
|
328
419
|
});
|
|
@@ -331,30 +422,22 @@ var DiscussionServiceImpl = class {
|
|
|
331
422
|
commentId,
|
|
332
423
|
actorId: actor.id
|
|
333
424
|
});
|
|
334
|
-
return this.renderOne(
|
|
335
|
-
}
|
|
336
|
-
async remove(
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
this.logger.warn("comment remove rejected — not found", {
|
|
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", {
|
|
343
433
|
orgId,
|
|
344
434
|
commentId,
|
|
345
435
|
actorId: actor.id
|
|
346
436
|
});
|
|
347
|
-
throw new
|
|
437
|
+
throw new ForbiddenError("You cannot remove this comment");
|
|
348
438
|
}
|
|
349
|
-
if (comment.
|
|
350
|
-
|
|
351
|
-
if (comment.orgUserId !== actor.id && !this.canModerate(actor)) throw new ForbiddenError("You cannot remove this comment");
|
|
352
|
-
} else {
|
|
353
|
-
if (!await this.hasAccess(orgId, this.activityIdOf(comment), actor)) throw new ForbiddenError("You cannot open this activity");
|
|
354
|
-
if (comment.orgUserId !== actor.id && !this.isStaff(actor)) throw new ForbiddenError("You cannot remove this comment");
|
|
355
|
-
}
|
|
356
|
-
if (comment.status === "removed") return comment;
|
|
357
|
-
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) => {
|
|
358
441
|
const updated = await scope.discussion.updateComment(orgId, commentId, {
|
|
359
442
|
status: "removed",
|
|
360
443
|
removedBy: actor.id
|
|
@@ -368,7 +451,7 @@ var DiscussionServiceImpl = class {
|
|
|
368
451
|
await scope.outbox.append([discussionEvents.commentRemoved.make({
|
|
369
452
|
orgId,
|
|
370
453
|
data: updated,
|
|
371
|
-
metadata: {
|
|
454
|
+
metadata: { actorId: actor.id }
|
|
372
455
|
})]);
|
|
373
456
|
this.logger.info("comment removed", {
|
|
374
457
|
orgId,
|
|
@@ -377,63 +460,54 @@ var DiscussionServiceImpl = class {
|
|
|
377
460
|
});
|
|
378
461
|
return updated;
|
|
379
462
|
});
|
|
463
|
+
return this.renderOne(removed, where, actor);
|
|
380
464
|
}
|
|
381
|
-
async restore(
|
|
382
|
-
|
|
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);
|
|
383
470
|
}
|
|
384
|
-
async restoreComment(
|
|
385
|
-
if (!this.isStaff(actor)) throw new ForbiddenError("You cannot restore this comment");
|
|
386
|
-
const comment = await this.getComment(orgId, commentId);
|
|
387
|
-
if (!comment || !this.inCommunity(comment, communityId)) {
|
|
388
|
-
this.logger.warn("comment restore rejected — not found", {
|
|
389
|
-
orgId,
|
|
390
|
-
commentId,
|
|
391
|
-
actorId: actor.id
|
|
392
|
-
});
|
|
393
|
-
throw new NotFoundError("Comment", commentId);
|
|
394
|
-
}
|
|
471
|
+
async restoreComment(comment, actor) {
|
|
395
472
|
if (comment.status !== "removed") return comment;
|
|
396
|
-
const updated = await this.repo.updateComment(orgId,
|
|
473
|
+
const updated = await this.repo.updateComment(actor.orgId, comment.id, {
|
|
397
474
|
status: "published",
|
|
398
475
|
removedBy: null
|
|
399
476
|
});
|
|
400
|
-
if (!updated) throw new NotFoundError("Comment",
|
|
477
|
+
if (!updated) throw new NotFoundError("Comment", comment.id);
|
|
401
478
|
this.logger.info("comment restored", {
|
|
402
|
-
orgId,
|
|
403
|
-
commentId,
|
|
479
|
+
orgId: actor.orgId,
|
|
480
|
+
commentId: comment.id,
|
|
404
481
|
actorId: actor.id
|
|
405
482
|
});
|
|
406
483
|
return updated;
|
|
407
484
|
}
|
|
408
|
-
/** Load a comment together with
|
|
409
|
-
* having proved the actor may reach
|
|
410
|
-
* acts on one comment needs
|
|
411
|
-
async loadWithConfig(
|
|
412
|
-
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);
|
|
413
490
|
if (!comment) throw new NotFoundError("Comment", commentId);
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
};
|
|
426
|
-
}
|
|
427
|
-
const activityId = this.activityIdOf(comment);
|
|
428
|
-
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
|
+
};
|
|
429
502
|
return {
|
|
430
503
|
comment,
|
|
431
|
-
config: await this.
|
|
432
|
-
|
|
504
|
+
config: await this.configOf(actor.orgId, where.activity),
|
|
505
|
+
where
|
|
433
506
|
};
|
|
434
507
|
}
|
|
435
|
-
async setReaction(
|
|
436
|
-
const
|
|
508
|
+
async setReaction(commentId, emoji, actor) {
|
|
509
|
+
const orgId = actor.orgId;
|
|
510
|
+
const { comment, config, where } = await this.loadWithConfig(commentId, actor);
|
|
437
511
|
if (!config.enabled && !comment.postId) throw new ForbiddenError("Discussion is not open on this activity");
|
|
438
512
|
if (!config.reactions && emoji !== null) throw new ForbiddenError(comment.postId ? "Reactions are off on this post" : "Reactions are off on this course");
|
|
439
513
|
if (comment.status === "removed") throw new ForbiddenError("A removed comment cannot be reacted to");
|
|
@@ -445,39 +519,37 @@ var DiscussionServiceImpl = class {
|
|
|
445
519
|
commentId,
|
|
446
520
|
postId: comment.postId,
|
|
447
521
|
activityId: comment.activityId,
|
|
448
|
-
spaceId,
|
|
522
|
+
spaceId: where.kind === "post" ? where.post.spaceId : null,
|
|
449
523
|
communityId: comment.communityId,
|
|
450
524
|
orgUserId: actor.id,
|
|
451
525
|
reaction: emoji
|
|
452
526
|
},
|
|
453
|
-
metadata: {
|
|
527
|
+
metadata: { actorId: actor.id }
|
|
454
528
|
})]);
|
|
455
529
|
});
|
|
456
530
|
return (await this.repo.reactionsOf(orgId, [commentId], actor.id))[commentId] ?? EMPTY_REACTIONS;
|
|
457
531
|
}
|
|
458
|
-
async approve(
|
|
459
|
-
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
throw new NotFoundError("Comment", commentId);
|
|
468
|
-
}
|
|
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;
|
|
469
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));
|
|
470
543
|
return this.uow.run(async (scope) => {
|
|
471
544
|
const updated = await scope.discussion.updateComment(orgId, commentId, { status: "published" });
|
|
472
545
|
if (!updated) throw new NotFoundError("Comment", commentId);
|
|
473
|
-
const mentioned = await this.openersOf(orgId, updated, await scope.mentions.ofItem(orgId, "comment", commentId));
|
|
474
546
|
await scope.outbox.append([discussionEvents.commentPublished.make({
|
|
475
547
|
orgId,
|
|
476
548
|
data: {
|
|
477
549
|
...updated,
|
|
478
550
|
mentionedOrgUserIds: mentioned
|
|
479
551
|
},
|
|
480
|
-
metadata: {
|
|
552
|
+
metadata: { actorId: actor.id }
|
|
481
553
|
})]);
|
|
482
554
|
this.logger.info("comment published", {
|
|
483
555
|
orgId,
|
|
@@ -488,51 +560,33 @@ var DiscussionServiceImpl = class {
|
|
|
488
560
|
});
|
|
489
561
|
}
|
|
490
562
|
/** Loads the comment and dispatches to whichever transition applies — the
|
|
491
|
-
* caller must not decide that itself.
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
if (
|
|
498
|
-
this.logger.warn("comment publish rejected — not found", {
|
|
499
|
-
orgId,
|
|
500
|
-
commentId,
|
|
501
|
-
actorId: actor.id
|
|
502
|
-
});
|
|
503
|
-
throw new NotFoundError("Comment", commentId);
|
|
504
|
-
}
|
|
505
|
-
if (comment.status === "pending") return this.approve(orgId, commentId, actor);
|
|
506
|
-
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);
|
|
507
570
|
return comment;
|
|
508
571
|
}
|
|
509
|
-
async resolveReports(
|
|
510
|
-
const comment = await this.
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
commentId,
|
|
515
|
-
actorId: actor.id
|
|
516
|
-
});
|
|
517
|
-
throw new NotFoundError("Comment", commentId);
|
|
518
|
-
}
|
|
519
|
-
if (comment.postId) {
|
|
520
|
-
await this.postOfComment(orgId, actor, commentId, comment.postId);
|
|
521
|
-
if (!this.canModerate(actor)) throw new ForbiddenError("You cannot work this queue");
|
|
522
|
-
} else if (!this.isStaff(actor)) throw new ForbiddenError("You cannot resolve this report");
|
|
523
|
-
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, {
|
|
524
577
|
resolvedAt: /* @__PURE__ */ new Date(),
|
|
525
578
|
resolvedBy: actor.id,
|
|
526
579
|
resolution: "dismissed"
|
|
527
580
|
});
|
|
528
581
|
this.logger.info("comment reports resolved", {
|
|
529
|
-
orgId,
|
|
582
|
+
orgId: actor.orgId,
|
|
530
583
|
commentId,
|
|
531
584
|
actorId: actor.id
|
|
532
585
|
});
|
|
533
586
|
}
|
|
534
|
-
async reportComment(
|
|
535
|
-
const
|
|
587
|
+
async reportComment(commentId, reason, actor) {
|
|
588
|
+
const orgId = actor.orgId;
|
|
589
|
+
const { comment, config } = await this.loadWithConfig(commentId, actor);
|
|
536
590
|
if (!config.enabled && !comment.postId) throw new ForbiddenError("Discussion is not open on this activity");
|
|
537
591
|
const now = /* @__PURE__ */ new Date();
|
|
538
592
|
const report = {
|
|
@@ -560,7 +614,7 @@ var DiscussionServiceImpl = class {
|
|
|
560
614
|
...saved,
|
|
561
615
|
communityId: comment.communityId
|
|
562
616
|
},
|
|
563
|
-
metadata: {
|
|
617
|
+
metadata: { actorId: actor.id }
|
|
564
618
|
})]);
|
|
565
619
|
this.logger.info("comment reported", {
|
|
566
620
|
orgId,
|
|
@@ -570,15 +624,25 @@ var DiscussionServiceImpl = class {
|
|
|
570
624
|
return saved;
|
|
571
625
|
});
|
|
572
626
|
}
|
|
573
|
-
async listComments(
|
|
574
|
-
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);
|
|
575
638
|
const rows = page.rows.map((r) => r.comment);
|
|
576
639
|
if (rows.length === 0) return {
|
|
577
640
|
...page,
|
|
578
641
|
rows: []
|
|
579
642
|
};
|
|
580
643
|
const reports = await this.repo.listOpenReports(orgId, rows.map((c) => c.id));
|
|
581
|
-
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)]);
|
|
582
646
|
const urls = this.fileUrls;
|
|
583
647
|
const byComment = /* @__PURE__ */ new Map();
|
|
584
648
|
for (const report of reports) byComment.set(report.commentId, [...byComment.get(report.commentId) ?? [], report]);
|
|
@@ -595,14 +659,14 @@ var DiscussionServiceImpl = class {
|
|
|
595
659
|
courseId,
|
|
596
660
|
body: comment.body,
|
|
597
661
|
status: comment.status,
|
|
598
|
-
author: authorView(orgId, author, urls),
|
|
662
|
+
author: authorView(orgId, author, urls, teamBadge(permissions.get(author.id), courseId)),
|
|
599
663
|
authorEmail: author.email,
|
|
600
664
|
removedBy: comment.removedBy,
|
|
601
665
|
reports: (byComment.get(comment.id) ?? []).map((r) => {
|
|
602
666
|
const reporter = authors[r.orgUserId];
|
|
603
667
|
if (!reporter) throw new NotFoundError("Member", r.orgUserId);
|
|
604
668
|
return {
|
|
605
|
-
reporter: authorView(orgId, reporter, urls),
|
|
669
|
+
reporter: authorView(orgId, reporter, urls, teamBadge(permissions.get(reporter.id), courseId)),
|
|
606
670
|
reason: r.reason,
|
|
607
671
|
createdAt: r.createdAt
|
|
608
672
|
};
|