@headless-lms/core 0.2.0 → 0.4.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 +42 -24
- package/dist/assets/events.js +1 -1
- package/dist/assets/events.js.map +1 -1
- package/dist/assets/file-urls.d.ts +20 -0
- package/dist/assets/file-urls.js +25 -0
- package/dist/assets/file-urls.js.map +1 -0
- package/dist/assets/index.d.ts +6 -4
- package/dist/assets/index.js +4 -2
- 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/model.d.ts +2 -2
- package/dist/assets/ports.d.ts +46 -15
- package/dist/assets/service.d.ts +38 -7
- package/dist/assets/service.js +287 -38
- package/dist/assets/service.js.map +1 -1
- package/dist/automations/actions.d.ts +13 -24
- package/dist/automations/actions.js +106 -42
- package/dist/automations/actions.js.map +1 -1
- package/dist/automations/catalog.js +226 -3
- package/dist/automations/catalog.js.map +1 -1
- package/dist/automations/community-derivations.d.ts +14 -0
- package/dist/automations/community-derivations.js +439 -0
- package/dist/automations/community-derivations.js.map +1 -0
- package/dist/automations/community-mail.d.ts +98 -0
- package/dist/automations/community-mail.js +70 -0
- package/dist/automations/community-mail.js.map +1 -0
- package/dist/automations/events.d.ts +18 -0
- package/dist/automations/index.d.ts +13 -4
- package/dist/automations/index.js +10 -2
- package/dist/automations/model.d.ts +10 -3
- package/dist/automations/model.js +15 -2
- package/dist/automations/model.js.map +1 -1
- 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 +316 -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 +39 -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 +249 -0
- package/dist/automations/notifications/rules/entitlements.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 +28 -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 +99 -0
- package/dist/automations/notifications/rules/recipients.js.map +1 -0
- package/dist/automations/notifications/service.d.ts +27 -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 +473 -0
- package/dist/automations/notifications/types.js.map +1 -0
- package/dist/automations/ports.d.ts +121 -3
- package/dist/automations/schedule.d.ts +6 -0
- package/dist/automations/schedule.js +41 -0
- package/dist/automations/schedule.js.map +1 -0
- package/dist/automations/service.d.ts +20 -4
- package/dist/automations/service.js +246 -45
- package/dist/automations/service.js.map +1 -1
- package/dist/content/events.d.ts +712 -50
- package/dist/content/events.js +55 -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/model.d.ts +2 -2
- package/dist/content/notification-categories.d.ts +36 -0
- package/dist/content/notification-categories.js +64 -0
- package/dist/content/notification-categories.js.map +1 -0
- package/dist/content/ports.d.ts +90 -18
- package/dist/content/service.d.ts +57 -18
- package/dist/content/service.js +520 -25
- package/dist/content/service.js.map +1 -1
- package/dist/content/titles.d.ts +22 -0
- package/dist/content/titles.js +69 -0
- package/dist/content/titles.js.map +1 -0
- package/dist/content/types.d.ts +2 -2
- package/dist/discussion/authors.d.ts +10 -0
- package/dist/discussion/authors.js +23 -0
- package/dist/discussion/authors.js.map +1 -0
- package/dist/discussion/chats.d.ts +109 -0
- package/dist/discussion/chats.js +831 -0
- package/dist/discussion/chats.js.map +1 -0
- 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 +785 -56
- package/dist/discussion/events.js +165 -5
- package/dist/discussion/events.js.map +1 -1
- package/dist/discussion/index.d.ts +12 -3
- package/dist/discussion/index.js +9 -1
- 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/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 +372 -28
- package/dist/discussion/ports.js +6 -1
- package/dist/discussion/ports.js.map +1 -0
- package/dist/discussion/posts.d.ts +113 -0
- package/dist/discussion/posts.js +775 -0
- package/dist/discussion/posts.js.map +1 -0
- package/dist/discussion/service.d.ts +42 -11
- package/dist/discussion/service.js +364 -85
- package/dist/discussion/service.js.map +1 -1
- package/dist/discussion/subscriptions.d.ts +37 -0
- package/dist/discussion/subscriptions.js +99 -0
- package/dist/discussion/subscriptions.js.map +1 -0
- package/dist/discussion/titles.d.ts +26 -0
- package/dist/discussion/titles.js +84 -0
- package/dist/discussion/titles.js.map +1 -0
- package/dist/entitlements/events.d.ts +178 -11
- package/dist/entitlements/events.js +28 -1
- package/dist/entitlements/events.js.map +1 -1
- package/dist/entitlements/index.d.ts +6 -4
- package/dist/entitlements/index.js +3 -1
- 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 +3 -2
- package/dist/entitlements/ports.d.ts +128 -11
- package/dist/entitlements/rules.d.ts +12 -0
- package/dist/entitlements/rules.js +17 -0
- package/dist/entitlements/rules.js.map +1 -0
- package/dist/entitlements/service.d.ts +57 -3
- package/dist/entitlements/service.js +492 -2
- package/dist/entitlements/service.js.map +1 -1
- package/dist/identity/access.d.ts +6 -0
- package/dist/identity/access.js +7 -0
- package/dist/identity/access.js.map +1 -0
- package/dist/identity/devices.d.ts +30 -0
- package/dist/identity/devices.js +107 -0
- package/dist/identity/devices.js.map +1 -0
- package/dist/identity/events.d.ts +2 -2
- package/dist/identity/index.d.ts +7 -4
- package/dist/identity/index.js +4 -1
- package/dist/identity/model.d.ts +2 -1
- package/dist/identity/notification-settings.d.ts +28 -0
- package/dist/identity/notification-settings.js +113 -0
- package/dist/identity/notification-settings.js.map +1 -0
- package/dist/identity/ports.d.ts +97 -3
- package/dist/identity/service.d.ts +14 -3
- package/dist/identity/service.js +62 -5
- package/dist/identity/service.js.map +1 -1
- package/dist/integrations/events.d.ts +3 -3
- package/dist/integrations/model.js +1 -1
- package/dist/integrations/model.js.map +1 -1
- package/dist/integrations/service.js +2 -2
- package/dist/integrations/service.js.map +1 -1
- package/dist/organizations/access-scopes.d.ts +19 -0
- package/dist/organizations/access-scopes.js +89 -0
- package/dist/organizations/access-scopes.js.map +1 -0
- package/dist/organizations/events.d.ts +75 -33
- package/dist/organizations/events.js +6 -0
- package/dist/organizations/events.js.map +1 -1
- package/dist/organizations/index.d.ts +8 -6
- package/dist/organizations/index.js +5 -3
- package/dist/organizations/members.d.ts +8 -5
- package/dist/organizations/members.js.map +1 -1
- package/dist/organizations/notification-categories.d.ts +32 -0
- package/dist/organizations/notification-categories.js +51 -0
- package/dist/organizations/notification-categories.js.map +1 -0
- package/dist/organizations/ports.d.ts +17 -11
- package/dist/organizations/roles.d.ts +6 -10
- package/dist/organizations/roles.js +19 -7
- package/dist/organizations/roles.js.map +1 -1
- package/dist/organizations/service.d.ts +22 -11
- package/dist/organizations/service.js +164 -47
- package/dist/organizations/service.js.map +1 -1
- package/dist/organizations/types.d.ts +1 -10
- package/dist/progress/events.d.ts +14 -14
- package/dist/progress/index.d.ts +2 -2
- package/dist/progress/ports.d.ts +6 -1
- package/dist/progress/service.d.ts +3 -1
- package/dist/progress/service.js +6 -2
- package/dist/progress/service.js.map +1 -1
- package/dist/reporting/admin/index.d.ts +4 -0
- package/dist/reporting/admin/index.js +2 -0
- package/dist/reporting/admin/model.d.ts +108 -0
- package/dist/reporting/admin/ports.d.ts +27 -0
- package/dist/reporting/admin/service.d.ts +22 -0
- package/dist/reporting/admin/service.js +38 -0
- package/dist/reporting/admin/service.js.map +1 -0
- package/dist/reporting/member/index.d.ts +5 -0
- package/dist/reporting/member/index.js +2 -0
- package/dist/reporting/member/model.d.ts +157 -0
- package/dist/reporting/member/ports.d.ts +48 -0
- package/dist/reporting/member/service.d.ts +92 -0
- package/dist/reporting/member/service.js +364 -0
- package/dist/reporting/member/service.js.map +1 -0
- package/dist/shared/actor.d.ts +9 -0
- package/dist/shared/errors.d.ts +19 -1
- package/dist/shared/errors.js +27 -1
- package/dist/shared/errors.js.map +1 -1
- package/dist/shared/id.d.ts +14 -0
- package/dist/shared/id.js +15 -1
- package/dist/shared/id.js.map +1 -1
- package/dist/shared/mailer.d.ts +15 -2
- package/dist/shared/mailer.js +32 -5
- package/dist/shared/mailer.js.map +1 -1
- package/dist/shared/network.d.ts +10 -0
- package/dist/shared/network.js +211 -0
- package/dist/shared/network.js.map +1 -0
- 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 +48 -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/assets.d.ts +2 -2
- package/dist/types/automations.d.ts +15 -1
- package/dist/types/content.d.ts +2 -2
- package/dist/types/discussion.d.ts +11 -3
- package/dist/types/email-templates.d.ts +216 -1
- package/dist/types/entitlements.d.ts +2 -2
- package/dist/types/identity.d.ts +48 -2
- package/dist/types/index.d.ts +9 -8
- package/dist/types/notifications.d.ts +157 -0
- package/dist/types/ports.d.ts +6 -0
- package/dist/types/schemas/assets.d.ts +65 -37
- package/dist/types/schemas/assets.js +17 -12
- package/dist/types/schemas/assets.js.map +1 -1
- package/dist/types/schemas/automations.d.ts +9 -1
- package/dist/types/schemas/automations.js +13 -6
- package/dist/types/schemas/automations.js.map +1 -1
- package/dist/types/schemas/content.d.ts +396 -3
- package/dist/types/schemas/content.js +161 -3
- package/dist/types/schemas/content.js.map +1 -1
- package/dist/types/schemas/discussion.d.ts +443 -3
- package/dist/types/schemas/discussion.js +233 -4
- package/dist/types/schemas/discussion.js.map +1 -1
- package/dist/types/schemas/entitlements.d.ts +36 -2
- package/dist/types/schemas/entitlements.js +23 -1
- package/dist/types/schemas/entitlements.js.map +1 -1
- package/dist/types/schemas/identity.d.ts +27 -1
- package/dist/types/schemas/identity.js +14 -2
- package/dist/types/schemas/identity.js.map +1 -1
- package/dist/types/schemas/index.d.ts +9 -8
- package/dist/types/schemas/index.js +9 -8
- package/dist/types/schemas/notifications.d.ts +95 -0
- package/dist/types/schemas/notifications.js +56 -0
- package/dist/types/schemas/notifications.js.map +1 -0
- package/dist/types/schemas/organizations.d.ts +8 -0
- package/dist/types/schemas/organizations.js +2 -0
- package/dist/types/schemas/organizations.js.map +1 -1
- package/dist/types/schemas/progress.d.ts +3 -3
- package/dist/types/schemas/shared.d.ts +3 -1
- package/dist/types/schemas/shared.js +5 -1
- package/dist/types/schemas/shared.js.map +1 -1
- package/package.json +2 -2
- package/dist/reporting/courses/index.d.ts +0 -4
- package/dist/reporting/courses/index.js +0 -2
- package/dist/reporting/courses/model.d.ts +0 -24
- package/dist/reporting/courses/ports.d.ts +0 -13
- package/dist/reporting/courses/service.d.ts +0 -19
- package/dist/reporting/courses/service.js +0 -20
- package/dist/reporting/courses/service.js.map +0 -1
- package/dist/reporting/dashboard/index.d.ts +0 -4
- package/dist/reporting/dashboard/index.js +0 -2
- package/dist/reporting/dashboard/model.d.ts +0 -14
- package/dist/reporting/dashboard/ports.d.ts +0 -13
- package/dist/reporting/dashboard/service.d.ts +0 -19
- package/dist/reporting/dashboard/service.js +0 -20
- package/dist/reporting/dashboard/service.js.map +0 -1
- package/dist/reporting/learn/index.d.ts +0 -5
- package/dist/reporting/learn/index.js +0 -2
- package/dist/reporting/learn/model.d.ts +0 -23
- package/dist/reporting/learn/ports.d.ts +0 -43
- package/dist/reporting/learn/service.d.ts +0 -49
- package/dist/reporting/learn/service.js +0 -116
- package/dist/reporting/learn/service.js.map +0 -1
- package/dist/reporting/students/index.d.ts +0 -4
- package/dist/reporting/students/index.js +0 -2
- package/dist/reporting/students/model.d.ts +0 -51
- package/dist/reporting/students/model.js +0 -1
- package/dist/reporting/students/ports.d.ts +0 -15
- package/dist/reporting/students/ports.js +0 -1
- package/dist/reporting/students/service.d.ts +0 -20
- package/dist/reporting/students/service.js +0 -23
- package/dist/reporting/students/service.js.map +0 -1
- /package/dist/reporting/{courses → admin}/model.js +0 -0
- /package/dist/reporting/{courses → admin}/ports.js +0 -0
- /package/dist/reporting/{dashboard → member}/model.js +0 -0
- /package/dist/reporting/{dashboard → member}/ports.js +0 -0
- /package/dist/{reporting/learn/model.js → shared/actor.js} +0 -0
- /package/dist/{reporting/learn/ports.js → types/notifications.js} +0 -0
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { noopLogger } from "../shared/logger.js";
|
|
2
1
|
import { genId } from "../shared/id.js";
|
|
3
|
-
import { ForbiddenError, NotFoundError } from "../shared/errors.js";
|
|
2
|
+
import { ConflictError, ForbiddenError, NotFoundError } from "../shared/errors.js";
|
|
3
|
+
import { noopLogger } from "../shared/logger.js";
|
|
4
|
+
import { canModerate, isStaffRole } from "../organizations/roles.js";
|
|
4
5
|
import "../shared/settings.js";
|
|
5
6
|
import { discussionEvents } from "./events.js";
|
|
7
|
+
import { authorView } from "./authors.js";
|
|
8
|
+
import { keepAllowed, mentionTokens, mentionViews } from "./mentions.js";
|
|
6
9
|
//#region src/discussion/service.ts
|
|
7
10
|
/** A course with no stored settings. Discussion is opt-in, so the common case
|
|
8
11
|
* persists no row at all and every existing course stays silent. */
|
|
@@ -17,15 +20,23 @@ var DiscussionServiceImpl = class {
|
|
|
17
20
|
repo;
|
|
18
21
|
access;
|
|
19
22
|
content;
|
|
23
|
+
posts;
|
|
24
|
+
spaces;
|
|
25
|
+
mentions;
|
|
20
26
|
uow;
|
|
21
27
|
settings;
|
|
28
|
+
fileUrls;
|
|
22
29
|
logger;
|
|
23
30
|
constructor(params) {
|
|
24
31
|
this.repo = params.repo;
|
|
25
32
|
this.access = params.access;
|
|
26
33
|
this.content = params.content;
|
|
34
|
+
this.posts = params.posts;
|
|
35
|
+
this.spaces = params.spaces;
|
|
36
|
+
this.mentions = params.mentions;
|
|
27
37
|
this.uow = params.uow;
|
|
28
38
|
this.settings = params.settings;
|
|
39
|
+
this.fileUrls = params.fileUrls;
|
|
29
40
|
this.logger = params.logger ?? noopLogger;
|
|
30
41
|
}
|
|
31
42
|
async resolveConfig(orgId, activityId) {
|
|
@@ -44,35 +55,27 @@ var DiscussionServiceImpl = class {
|
|
|
44
55
|
reactions: settings.reactions
|
|
45
56
|
};
|
|
46
57
|
}
|
|
47
|
-
/** Any org user that is not a learner moderates. The role arrives from
|
|
48
|
-
* the edge already resolved; core never looks one up to authorise. */
|
|
49
58
|
isStaff(actor) {
|
|
50
|
-
return actor.role
|
|
59
|
+
return isStaffRole(actor.role);
|
|
51
60
|
}
|
|
52
|
-
|
|
53
|
-
return {
|
|
54
|
-
id: record.id,
|
|
55
|
-
firstName: record.firstName,
|
|
56
|
-
lastName: record.lastName,
|
|
57
|
-
image: record.image,
|
|
58
|
-
role: record.role
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
toView(comment, authors, reactions) {
|
|
61
|
+
toView(orgId, comment, authors, urls, reactions, mentions) {
|
|
62
62
|
const author = authors[comment.orgUserId];
|
|
63
|
-
if (!author) throw new NotFoundError("
|
|
64
|
-
if (comment.removedBy && !authors[comment.removedBy]) throw new NotFoundError("
|
|
63
|
+
if (!author) throw new NotFoundError("Member", comment.orgUserId);
|
|
64
|
+
if (comment.removedBy && !authors[comment.removedBy]) throw new NotFoundError("Member", comment.removedBy);
|
|
65
65
|
const remover = comment.removedBy ? authors[comment.removedBy] : void 0;
|
|
66
66
|
return {
|
|
67
67
|
id: comment.id,
|
|
68
68
|
activityId: comment.activityId,
|
|
69
|
+
postId: comment.postId,
|
|
70
|
+
communityId: comment.communityId,
|
|
69
71
|
parentId: comment.parentId,
|
|
70
|
-
author:
|
|
72
|
+
author: authorView(orgId, author, urls),
|
|
71
73
|
body: comment.status === "removed" ? null : comment.body,
|
|
72
74
|
status: comment.status,
|
|
73
|
-
removedBy: remover ?
|
|
75
|
+
removedBy: remover ? authorView(orgId, remover, urls) : null,
|
|
74
76
|
reactions: reactions.reactions,
|
|
75
77
|
viewerReaction: reactions.viewerReaction,
|
|
78
|
+
mentions: comment.status === "removed" ? [] : mentions,
|
|
76
79
|
createdAt: comment.createdAt,
|
|
77
80
|
updatedAt: comment.updatedAt
|
|
78
81
|
};
|
|
@@ -86,31 +89,75 @@ var DiscussionServiceImpl = class {
|
|
|
86
89
|
return [...ids];
|
|
87
90
|
}
|
|
88
91
|
async renderOne(orgId, comment, actor) {
|
|
89
|
-
const authors = await this.repo.authorsOf(orgId, this.peopleIn([comment]));
|
|
92
|
+
const [authors, mentioned] = await Promise.all([this.repo.authorsOf(orgId, this.peopleIn([comment])), mentionViews(this.mentions, this.repo, orgId, "comment", [comment.id])]);
|
|
90
93
|
const stored = comment.status === "removed" ? EMPTY_REACTIONS : (await this.repo.reactionsOf(orgId, [comment.id], actor.id))[comment.id] ?? EMPTY_REACTIONS;
|
|
91
|
-
return this.toView(comment, authors, stored);
|
|
94
|
+
return this.toView(orgId, comment, authors, this.fileUrls, stored, mentioned[comment.id] ?? []);
|
|
95
|
+
}
|
|
96
|
+
activityIdOf(comment) {
|
|
97
|
+
if (comment.activityId === null) throw new NotFoundError("Activity", comment.id);
|
|
98
|
+
return comment.activityId;
|
|
92
99
|
}
|
|
93
100
|
async hasAccess(orgId, activityId, actor) {
|
|
94
101
|
if (this.isStaff(actor)) return true;
|
|
95
102
|
const activity = await this.content.getActivity(orgId, activityId);
|
|
96
|
-
return activity ? this.access.
|
|
103
|
+
return activity ? this.access.hasAccess(orgId, actor.id, activity.courseId) : false;
|
|
104
|
+
}
|
|
105
|
+
async requireActivityOpen(orgId, activityId, actor) {
|
|
106
|
+
const activity = await this.content.getActivity(orgId, activityId);
|
|
107
|
+
if (!activity) throw new NotFoundError("Activity", activityId);
|
|
108
|
+
if (!this.isStaff(actor) && !await this.access.hasAccess(orgId, actor.id, activity.courseId)) throw new ForbiddenError("You cannot open this activity");
|
|
109
|
+
return activity;
|
|
110
|
+
}
|
|
111
|
+
async courseOpeners(orgId, courseId, orgUserIds) {
|
|
112
|
+
if (orgUserIds.length === 0) return [];
|
|
113
|
+
const people = await this.repo.authorsOf(orgId, orgUserIds);
|
|
114
|
+
const opens = await Promise.all(orgUserIds.map(async (orgUserId) => {
|
|
115
|
+
const person = people[orgUserId];
|
|
116
|
+
if (!person) return false;
|
|
117
|
+
return isStaffRole(person.role) || this.access.hasAccess(orgId, orgUserId, courseId);
|
|
118
|
+
}));
|
|
119
|
+
return orgUserIds.filter((_, index) => opens[index]);
|
|
120
|
+
}
|
|
121
|
+
async spaceOpeners(orgId, spaceId, orgUserIds) {
|
|
122
|
+
if (orgUserIds.length === 0) return [];
|
|
123
|
+
return keepAllowed(orgUserIds, await this.spaces.spaceOpenersAmong(orgId, spaceId, orgUserIds));
|
|
124
|
+
}
|
|
125
|
+
async openersOf(orgId, comment, orgUserIds) {
|
|
126
|
+
if (orgUserIds.length === 0) return [];
|
|
127
|
+
if (comment.postId) {
|
|
128
|
+
const post = await this.posts.find(orgId, comment.postId);
|
|
129
|
+
return post ? this.spaceOpeners(orgId, post.spaceId, orgUserIds) : [];
|
|
130
|
+
}
|
|
131
|
+
const activity = await this.content.getActivity(orgId, this.activityIdOf(comment));
|
|
132
|
+
return activity ? this.courseOpeners(orgId, activity.courseId, orgUserIds) : [];
|
|
133
|
+
}
|
|
134
|
+
commenterIds(orgId, target) {
|
|
135
|
+
return this.repo.commenterIds(orgId, target);
|
|
97
136
|
}
|
|
98
|
-
async postComment(orgId,
|
|
99
|
-
if (
|
|
137
|
+
async postComment(orgId, input) {
|
|
138
|
+
if (input.postId) return this.postOnPost(orgId, input.postId, input);
|
|
139
|
+
const activityId = input.activityId;
|
|
140
|
+
if (!activityId) throw new NotFoundError("Activity", activityId ?? "");
|
|
141
|
+
return this.postOnActivity(orgId, activityId, input);
|
|
142
|
+
}
|
|
143
|
+
async postOnActivity(orgId, activityId, { actor, parentId, body }) {
|
|
144
|
+
const activity = await this.requireActivityOpen(orgId, activityId, actor);
|
|
100
145
|
const config = await this.resolveConfig(orgId, activityId);
|
|
101
|
-
if (!config.enabled) throw new ForbiddenError("
|
|
146
|
+
if (!config.enabled) throw new ForbiddenError("Discussion is not open on this activity");
|
|
102
147
|
if (parentId !== null) {
|
|
103
|
-
if (!config.threaded) throw new ForbiddenError("
|
|
148
|
+
if (!config.threaded) throw new ForbiddenError("Replies are off on this course");
|
|
104
149
|
const parent = await this.repo.findComment(orgId, parentId);
|
|
105
150
|
if (!parent || parent.activityId !== activityId) throw new NotFoundError("Comment", parentId);
|
|
106
|
-
if (parent.parentId !== null) throw new ForbiddenError("replies
|
|
107
|
-
if (parent.status !== "published") throw new ForbiddenError("
|
|
151
|
+
if (parent.parentId !== null) throw new ForbiddenError("A reply cannot have replies of its own");
|
|
152
|
+
if (parent.status !== "published") throw new ForbiddenError("You can reply only to a published comment");
|
|
108
153
|
}
|
|
109
154
|
const status = config.requireReview && !this.isStaff(actor) ? "pending" : "published";
|
|
110
155
|
const comment = {
|
|
111
156
|
id: genId("comment"),
|
|
112
157
|
orgId,
|
|
113
158
|
activityId,
|
|
159
|
+
postId: null,
|
|
160
|
+
communityId: null,
|
|
114
161
|
parentId,
|
|
115
162
|
orgUserId: actor.id,
|
|
116
163
|
body,
|
|
@@ -119,56 +166,193 @@ var DiscussionServiceImpl = class {
|
|
|
119
166
|
createdAt: /* @__PURE__ */ new Date(),
|
|
120
167
|
updatedAt: /* @__PURE__ */ new Date()
|
|
121
168
|
};
|
|
169
|
+
const mentioned = await this.courseOpeners(orgId, activity.courseId, mentionTokens(body));
|
|
122
170
|
const saved = await this.uow.run(async (scope) => {
|
|
123
171
|
const row = await scope.discussion.insertComment(orgId, comment);
|
|
172
|
+
await scope.mentions.sync(orgId, "comment", row.id, mentioned, comment.createdAt);
|
|
124
173
|
await scope.outbox.append([discussionEvents.commentCreated.make({
|
|
125
174
|
orgId,
|
|
126
|
-
data:
|
|
175
|
+
data: {
|
|
176
|
+
...row,
|
|
177
|
+
mentionedOrgUserIds: status === "published" ? mentioned : []
|
|
178
|
+
},
|
|
179
|
+
metadata: { actorOrgUserId: actor.id }
|
|
127
180
|
})]);
|
|
128
181
|
this.logger.info("comment created", {
|
|
129
182
|
orgId,
|
|
130
183
|
commentId: row.id,
|
|
131
|
-
status
|
|
184
|
+
status,
|
|
185
|
+
actorId: actor.id
|
|
132
186
|
});
|
|
133
187
|
return row;
|
|
134
188
|
});
|
|
135
189
|
return this.renderOne(orgId, saved, actor);
|
|
136
190
|
}
|
|
191
|
+
async postOnPost(orgId, postId, { actor, parentId, body }) {
|
|
192
|
+
const { post, space } = await this.visiblePost(orgId, actor, postId);
|
|
193
|
+
if (post.status !== "published") throw new ConflictError("This post is not published");
|
|
194
|
+
if (!this.commentsOpen(post, space)) throw new ForbiddenError("Comments are closed on this post");
|
|
195
|
+
if (parentId !== null) {
|
|
196
|
+
const parent = await this.repo.findComment(orgId, parentId);
|
|
197
|
+
if (!parent || parent.postId !== postId) throw new NotFoundError("Comment", parentId);
|
|
198
|
+
if (parent.parentId !== null) throw new ForbiddenError("A reply cannot have replies of its own");
|
|
199
|
+
if (parent.status !== "published") throw new ForbiddenError("You can reply only to a published comment");
|
|
200
|
+
}
|
|
201
|
+
const now = /* @__PURE__ */ new Date();
|
|
202
|
+
const comment = {
|
|
203
|
+
id: genId("comment"),
|
|
204
|
+
orgId,
|
|
205
|
+
activityId: null,
|
|
206
|
+
postId,
|
|
207
|
+
communityId: post.communityId,
|
|
208
|
+
parentId,
|
|
209
|
+
orgUserId: actor.id,
|
|
210
|
+
body,
|
|
211
|
+
status: "published",
|
|
212
|
+
removedBy: null,
|
|
213
|
+
createdAt: now,
|
|
214
|
+
updatedAt: now
|
|
215
|
+
};
|
|
216
|
+
const mentioned = await this.spaceOpeners(orgId, post.spaceId, mentionTokens(body));
|
|
217
|
+
const saved = await this.uow.run(async (scope) => {
|
|
218
|
+
const row = await scope.discussion.insertComment(orgId, comment);
|
|
219
|
+
await scope.mentions.sync(orgId, "comment", row.id, mentioned, now);
|
|
220
|
+
await scope.outbox.append([discussionEvents.commentCreated.make({
|
|
221
|
+
orgId,
|
|
222
|
+
data: {
|
|
223
|
+
...row,
|
|
224
|
+
mentionedOrgUserIds: mentioned
|
|
225
|
+
},
|
|
226
|
+
metadata: { actorOrgUserId: actor.id }
|
|
227
|
+
})]);
|
|
228
|
+
this.logger.info("comment created", {
|
|
229
|
+
orgId,
|
|
230
|
+
commentId: row.id,
|
|
231
|
+
postId,
|
|
232
|
+
status: "published",
|
|
233
|
+
actorId: actor.id
|
|
234
|
+
});
|
|
235
|
+
return row;
|
|
236
|
+
});
|
|
237
|
+
return this.renderOne(orgId, saved, actor);
|
|
238
|
+
}
|
|
239
|
+
async postComments(orgId, postId, actor, query) {
|
|
240
|
+
const { post, space } = await this.visiblePost(orgId, actor, postId);
|
|
241
|
+
const seesHeld = (person) => this.canModerate(person);
|
|
242
|
+
const page = await this.repo.listThreadsByPost(orgId, postId, {
|
|
243
|
+
...query,
|
|
244
|
+
viewerId: actor.id,
|
|
245
|
+
seesHeld: seesHeld(actor)
|
|
246
|
+
});
|
|
247
|
+
const comments = await this.thread(orgId, page.rows, actor, post.reactionsEnabled ?? space.settings.reactions, seesHeld);
|
|
248
|
+
this.logger.debug("post comments read", {
|
|
249
|
+
orgId,
|
|
250
|
+
postId,
|
|
251
|
+
count: comments.length
|
|
252
|
+
});
|
|
253
|
+
return {
|
|
254
|
+
...page,
|
|
255
|
+
rows: comments
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
async thread(orgId, rows, actor, reactionsOn, seesHeld) {
|
|
259
|
+
const readable = rows.filter((comment) => {
|
|
260
|
+
if (comment.status === "published" || comment.status === "removed") return true;
|
|
261
|
+
return seesHeld(actor) || comment.orgUserId === actor.id;
|
|
262
|
+
});
|
|
263
|
+
const replies = readable.filter((comment) => comment.parentId !== null && comment.status !== "removed");
|
|
264
|
+
const heldOpen = new Set(replies.map((reply) => reply.parentId));
|
|
265
|
+
const roots = readable.filter((comment) => comment.parentId === null && (comment.status !== "removed" || heldOpen.has(comment.id)));
|
|
266
|
+
const rootIds = new Set(roots.map((root) => root.id));
|
|
267
|
+
const served = [...roots, ...replies.filter((reply) => rootIds.has(reply.parentId))].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
268
|
+
const [authors, reactions, mentioned] = await Promise.all([
|
|
269
|
+
this.repo.authorsOf(orgId, this.peopleIn(served)),
|
|
270
|
+
reactionsOn ? this.repo.reactionsOf(orgId, served.map((comment) => comment.id), actor.id) : Promise.resolve({}),
|
|
271
|
+
mentionViews(this.mentions, this.repo, orgId, "comment", served.map((comment) => comment.id))
|
|
272
|
+
]);
|
|
273
|
+
const urls = this.fileUrls;
|
|
274
|
+
return served.map((comment) => this.toView(orgId, comment, authors, urls, reactions[comment.id] ?? EMPTY_REACTIONS, mentioned[comment.id] ?? []));
|
|
275
|
+
}
|
|
276
|
+
async visiblePost(orgId, actor, postId) {
|
|
277
|
+
return this.posts.visible(orgId, actor, postId);
|
|
278
|
+
}
|
|
279
|
+
async postOfComment(orgId, actor, commentId, postId) {
|
|
280
|
+
try {
|
|
281
|
+
return await this.visiblePost(orgId, actor, postId);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
if (error instanceof NotFoundError) throw new NotFoundError("Comment", commentId);
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
canModerate(actor) {
|
|
288
|
+
return canModerate(actor.role);
|
|
289
|
+
}
|
|
290
|
+
inCommunity(comment, communityId) {
|
|
291
|
+
return communityId === void 0 || comment.communityId === communityId;
|
|
292
|
+
}
|
|
293
|
+
commentsOpen(post, space) {
|
|
294
|
+
return post.commentsClosed === null ? space.settings.comments : !post.commentsClosed;
|
|
295
|
+
}
|
|
137
296
|
async activityComments(orgId, activityId, actor) {
|
|
297
|
+
await this.requireActivityOpen(orgId, activityId, actor);
|
|
138
298
|
const config = await this.resolveConfig(orgId, activityId);
|
|
139
299
|
if (!config.enabled) return {
|
|
140
300
|
config,
|
|
141
301
|
comments: []
|
|
142
302
|
};
|
|
143
|
-
const
|
|
144
|
-
if (c.status === "published" || c.status === "removed") return true;
|
|
145
|
-
return this.isStaff(actor) || c.orgUserId === actor.id;
|
|
146
|
-
});
|
|
147
|
-
const replies = readable.filter((c) => c.parentId !== null && c.status !== "removed");
|
|
148
|
-
const heldOpen = new Set(replies.map((r) => r.parentId));
|
|
149
|
-
const roots = readable.filter((c) => c.parentId === null && (c.status !== "removed" || heldOpen.has(c.id)));
|
|
150
|
-
const rootIds = new Set(roots.map((r) => r.id));
|
|
151
|
-
const served = [...roots, ...replies.filter((r) => rootIds.has(r.parentId))].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
152
|
-
const [authors, reactions] = await Promise.all([this.repo.authorsOf(orgId, this.peopleIn(served)), config.reactions ? this.repo.reactionsOf(orgId, served.map((c) => c.id), actor.id) : Promise.resolve({})]);
|
|
303
|
+
const rows = await this.repo.listByActivity(orgId, activityId);
|
|
153
304
|
return {
|
|
154
305
|
config,
|
|
155
|
-
comments:
|
|
306
|
+
comments: await this.thread(orgId, rows, actor, config.reactions, (person) => this.isStaff(person))
|
|
156
307
|
};
|
|
157
308
|
}
|
|
158
309
|
async edit(orgId, commentId, actor, body) {
|
|
159
310
|
const { comment, config } = await this.loadWithConfig(orgId, commentId, actor);
|
|
160
|
-
if (!config.enabled) throw new ForbiddenError("
|
|
161
|
-
if (comment.orgUserId !== actor.id) throw new ForbiddenError("
|
|
162
|
-
if (comment.status === "removed") throw new ForbiddenError("
|
|
163
|
-
const
|
|
164
|
-
|
|
311
|
+
if (!config.enabled) throw new ForbiddenError(comment.postId ? "Comments are closed on this post" : "Discussion is not open on this activity");
|
|
312
|
+
if (comment.orgUserId !== actor.id) throw new ForbiddenError("You can edit only your own comment");
|
|
313
|
+
if (comment.status === "removed") throw new ForbiddenError("A removed comment cannot be edited");
|
|
314
|
+
const candidates = await this.openersOf(orgId, comment, mentionTokens(body));
|
|
315
|
+
const updated = await this.uow.run(async (scope) => {
|
|
316
|
+
const next = await scope.discussion.updateComment(orgId, commentId, { body });
|
|
317
|
+
if (!next) throw new NotFoundError("Comment", commentId);
|
|
318
|
+
const added = await scope.mentions.sync(orgId, "comment", commentId, candidates, /* @__PURE__ */ new Date());
|
|
319
|
+
await scope.outbox.append([discussionEvents.commentUpdated.make({
|
|
320
|
+
orgId,
|
|
321
|
+
data: {
|
|
322
|
+
...next,
|
|
323
|
+
mentionedOrgUserIds: next.status === "published" ? keepAllowed(candidates, added) : []
|
|
324
|
+
},
|
|
325
|
+
metadata: { actorOrgUserId: actor.id }
|
|
326
|
+
})]);
|
|
327
|
+
return next;
|
|
328
|
+
});
|
|
329
|
+
this.logger.info("comment edited", {
|
|
330
|
+
orgId,
|
|
331
|
+
commentId,
|
|
332
|
+
actorId: actor.id
|
|
333
|
+
});
|
|
165
334
|
return this.renderOne(orgId, updated, actor);
|
|
166
335
|
}
|
|
167
|
-
async remove(orgId, commentId, actor) {
|
|
336
|
+
async remove(orgId, commentId, actor, communityId) {
|
|
337
|
+
return this.renderOne(orgId, await this.removeComment(orgId, commentId, actor, communityId), actor);
|
|
338
|
+
}
|
|
339
|
+
async removeComment(orgId, commentId, actor, communityId) {
|
|
168
340
|
const comment = await this.getComment(orgId, commentId);
|
|
169
|
-
if (!comment
|
|
170
|
-
|
|
171
|
-
|
|
341
|
+
if (!comment || !this.inCommunity(comment, communityId)) {
|
|
342
|
+
this.logger.warn("comment remove rejected — not found", {
|
|
343
|
+
orgId,
|
|
344
|
+
commentId,
|
|
345
|
+
actorId: actor.id
|
|
346
|
+
});
|
|
347
|
+
throw new NotFoundError("Comment", commentId);
|
|
348
|
+
}
|
|
349
|
+
if (comment.postId) {
|
|
350
|
+
await this.postOfComment(orgId, actor, commentId, comment.postId);
|
|
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
|
+
}
|
|
172
356
|
if (comment.status === "removed") return comment;
|
|
173
357
|
return this.uow.run(async (scope) => {
|
|
174
358
|
const updated = await scope.discussion.updateComment(orgId, commentId, {
|
|
@@ -176,28 +360,49 @@ var DiscussionServiceImpl = class {
|
|
|
176
360
|
removedBy: actor.id
|
|
177
361
|
});
|
|
178
362
|
if (!updated) throw new NotFoundError("Comment", commentId);
|
|
363
|
+
if (comment.orgUserId !== actor.id) await scope.discussion.resolveReportsFor(orgId, commentId, {
|
|
364
|
+
resolvedAt: /* @__PURE__ */ new Date(),
|
|
365
|
+
resolvedBy: actor.id,
|
|
366
|
+
resolution: "removed"
|
|
367
|
+
});
|
|
179
368
|
await scope.outbox.append([discussionEvents.commentRemoved.make({
|
|
180
369
|
orgId,
|
|
181
|
-
data: updated
|
|
370
|
+
data: updated,
|
|
371
|
+
metadata: { actorOrgUserId: actor.id }
|
|
182
372
|
})]);
|
|
183
373
|
this.logger.info("comment removed", {
|
|
184
374
|
orgId,
|
|
185
375
|
commentId,
|
|
186
|
-
|
|
376
|
+
actorId: actor.id
|
|
187
377
|
});
|
|
188
378
|
return updated;
|
|
189
379
|
});
|
|
190
380
|
}
|
|
191
|
-
async restore(orgId, commentId, actor) {
|
|
192
|
-
|
|
381
|
+
async restore(orgId, commentId, actor, communityId) {
|
|
382
|
+
return this.renderOne(orgId, await this.restoreComment(orgId, commentId, actor, communityId), actor);
|
|
383
|
+
}
|
|
384
|
+
async restoreComment(orgId, commentId, actor, communityId) {
|
|
385
|
+
if (!this.isStaff(actor)) throw new ForbiddenError("You cannot restore this comment");
|
|
193
386
|
const comment = await this.getComment(orgId, commentId);
|
|
194
|
-
if (!comment
|
|
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
|
+
}
|
|
195
395
|
if (comment.status !== "removed") return comment;
|
|
196
396
|
const updated = await this.repo.updateComment(orgId, commentId, {
|
|
197
397
|
status: "published",
|
|
198
398
|
removedBy: null
|
|
199
399
|
});
|
|
200
400
|
if (!updated) throw new NotFoundError("Comment", commentId);
|
|
401
|
+
this.logger.info("comment restored", {
|
|
402
|
+
orgId,
|
|
403
|
+
commentId,
|
|
404
|
+
actorId: actor.id
|
|
405
|
+
});
|
|
201
406
|
return updated;
|
|
202
407
|
}
|
|
203
408
|
/** Load a comment together with its activity's resolved comments config,
|
|
@@ -206,35 +411,78 @@ var DiscussionServiceImpl = class {
|
|
|
206
411
|
async loadWithConfig(orgId, commentId, actor) {
|
|
207
412
|
const comment = await this.getComment(orgId, commentId);
|
|
208
413
|
if (!comment) throw new NotFoundError("Comment", commentId);
|
|
209
|
-
if (
|
|
414
|
+
if (comment.postId) {
|
|
415
|
+
const { post, space } = await this.postOfComment(orgId, actor, commentId, comment.postId);
|
|
416
|
+
return {
|
|
417
|
+
comment,
|
|
418
|
+
config: {
|
|
419
|
+
enabled: this.commentsOpen(post, space),
|
|
420
|
+
threaded: true,
|
|
421
|
+
requireReview: false,
|
|
422
|
+
reactions: post.reactionsEnabled ?? space.settings.reactions
|
|
423
|
+
},
|
|
424
|
+
spaceId: post.spaceId
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
const activityId = this.activityIdOf(comment);
|
|
428
|
+
if (!await this.hasAccess(orgId, activityId, actor)) throw new ForbiddenError("You cannot open this activity");
|
|
210
429
|
return {
|
|
211
430
|
comment,
|
|
212
|
-
config: await this.resolveConfig(orgId,
|
|
431
|
+
config: await this.resolveConfig(orgId, activityId),
|
|
432
|
+
spaceId: null
|
|
213
433
|
};
|
|
214
434
|
}
|
|
215
435
|
async setReaction(orgId, commentId, actor, emoji) {
|
|
216
|
-
const { comment, config } = await this.loadWithConfig(orgId, commentId, actor);
|
|
217
|
-
if (!config.enabled) throw new ForbiddenError("
|
|
218
|
-
if (!config.reactions && emoji !== null) throw new ForbiddenError("
|
|
219
|
-
if (comment.status === "removed") throw new ForbiddenError("
|
|
220
|
-
await this.
|
|
436
|
+
const { comment, config, spaceId } = await this.loadWithConfig(orgId, commentId, actor);
|
|
437
|
+
if (!config.enabled && !comment.postId) throw new ForbiddenError("Discussion is not open on this activity");
|
|
438
|
+
if (!config.reactions && emoji !== null) throw new ForbiddenError(comment.postId ? "Reactions are off on this post" : "Reactions are off on this course");
|
|
439
|
+
if (comment.status === "removed") throw new ForbiddenError("A removed comment cannot be reacted to");
|
|
440
|
+
await this.uow.run(async (scope) => {
|
|
441
|
+
if (!await scope.discussion.setReaction(orgId, commentId, actor.id, emoji) || emoji === null) return;
|
|
442
|
+
await scope.outbox.append([discussionEvents.commentReactionAdded.make({
|
|
443
|
+
orgId,
|
|
444
|
+
data: {
|
|
445
|
+
commentId,
|
|
446
|
+
postId: comment.postId,
|
|
447
|
+
activityId: comment.activityId,
|
|
448
|
+
spaceId,
|
|
449
|
+
communityId: comment.communityId,
|
|
450
|
+
orgUserId: actor.id,
|
|
451
|
+
reaction: emoji
|
|
452
|
+
},
|
|
453
|
+
metadata: { actorOrgUserId: actor.id }
|
|
454
|
+
})]);
|
|
455
|
+
});
|
|
221
456
|
return (await this.repo.reactionsOf(orgId, [commentId], actor.id))[commentId] ?? EMPTY_REACTIONS;
|
|
222
457
|
}
|
|
223
458
|
async approve(orgId, commentId, actor) {
|
|
224
|
-
if (!this.isStaff(actor)) throw new ForbiddenError("
|
|
459
|
+
if (!this.isStaff(actor)) throw new ForbiddenError("You cannot approve this comment");
|
|
225
460
|
const comment = await this.getComment(orgId, commentId);
|
|
226
|
-
if (!comment)
|
|
227
|
-
|
|
461
|
+
if (!comment) {
|
|
462
|
+
this.logger.warn("comment approve rejected — not found", {
|
|
463
|
+
orgId,
|
|
464
|
+
commentId,
|
|
465
|
+
actorId: actor.id
|
|
466
|
+
});
|
|
467
|
+
throw new NotFoundError("Comment", commentId);
|
|
468
|
+
}
|
|
469
|
+
if (comment.status !== "pending") throw new ForbiddenError("Only a comment awaiting review can be approved");
|
|
228
470
|
return this.uow.run(async (scope) => {
|
|
229
471
|
const updated = await scope.discussion.updateComment(orgId, commentId, { status: "published" });
|
|
230
472
|
if (!updated) throw new NotFoundError("Comment", commentId);
|
|
473
|
+
const mentioned = await this.openersOf(orgId, updated, await scope.mentions.ofItem(orgId, "comment", commentId));
|
|
231
474
|
await scope.outbox.append([discussionEvents.commentPublished.make({
|
|
232
475
|
orgId,
|
|
233
|
-
data:
|
|
476
|
+
data: {
|
|
477
|
+
...updated,
|
|
478
|
+
mentionedOrgUserIds: mentioned
|
|
479
|
+
},
|
|
480
|
+
metadata: { actorOrgUserId: actor.id }
|
|
234
481
|
})]);
|
|
235
482
|
this.logger.info("comment published", {
|
|
236
483
|
orgId,
|
|
237
|
-
commentId
|
|
484
|
+
commentId,
|
|
485
|
+
actorId: actor.id
|
|
238
486
|
});
|
|
239
487
|
return updated;
|
|
240
488
|
});
|
|
@@ -243,26 +491,49 @@ var DiscussionServiceImpl = class {
|
|
|
243
491
|
* caller must not decide that itself. `approve` and `restore` still enforce
|
|
244
492
|
* their own staff check; the check here covers the third path, where a
|
|
245
493
|
* comment is already published and neither of them would run. */
|
|
246
|
-
async publish(orgId, commentId, actor) {
|
|
247
|
-
if (!this.isStaff(actor)) throw new ForbiddenError("
|
|
494
|
+
async publish(orgId, commentId, actor, communityId) {
|
|
495
|
+
if (!this.isStaff(actor)) throw new ForbiddenError("You cannot publish this comment");
|
|
248
496
|
const comment = await this.getComment(orgId, commentId);
|
|
249
|
-
if (!comment
|
|
497
|
+
if (!comment || !this.inCommunity(comment, communityId)) {
|
|
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
|
+
}
|
|
250
505
|
if (comment.status === "pending") return this.approve(orgId, commentId, actor);
|
|
251
|
-
if (comment.status === "removed") return this.
|
|
506
|
+
if (comment.status === "removed") return this.restoreComment(orgId, commentId, actor);
|
|
252
507
|
return comment;
|
|
253
508
|
}
|
|
254
|
-
async resolveReports(orgId, commentId, actor) {
|
|
255
|
-
|
|
256
|
-
if (!
|
|
257
|
-
|
|
509
|
+
async resolveReports(orgId, commentId, actor, communityId) {
|
|
510
|
+
const comment = await this.getComment(orgId, commentId);
|
|
511
|
+
if (!comment || !this.inCommunity(comment, communityId)) {
|
|
512
|
+
this.logger.warn("comment reports resolve rejected — not found", {
|
|
513
|
+
orgId,
|
|
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, {
|
|
524
|
+
resolvedAt: /* @__PURE__ */ new Date(),
|
|
525
|
+
resolvedBy: actor.id,
|
|
526
|
+
resolution: "dismissed"
|
|
527
|
+
});
|
|
258
528
|
this.logger.info("comment reports resolved", {
|
|
259
529
|
orgId,
|
|
260
|
-
commentId
|
|
530
|
+
commentId,
|
|
531
|
+
actorId: actor.id
|
|
261
532
|
});
|
|
262
533
|
}
|
|
263
534
|
async reportComment(orgId, commentId, actor, reason) {
|
|
264
|
-
const { config } = await this.loadWithConfig(orgId, commentId, actor);
|
|
265
|
-
if (!config.enabled) throw new ForbiddenError("
|
|
535
|
+
const { comment, config } = await this.loadWithConfig(orgId, commentId, actor);
|
|
536
|
+
if (!config.enabled && !comment.postId) throw new ForbiddenError("Discussion is not open on this activity");
|
|
266
537
|
const now = /* @__PURE__ */ new Date();
|
|
267
538
|
const report = {
|
|
268
539
|
id: genId("commentReport"),
|
|
@@ -271,6 +542,8 @@ var DiscussionServiceImpl = class {
|
|
|
271
542
|
orgUserId: actor.id,
|
|
272
543
|
reason,
|
|
273
544
|
resolvedAt: null,
|
|
545
|
+
resolution: null,
|
|
546
|
+
resolvedBy: null,
|
|
274
547
|
createdAt: now,
|
|
275
548
|
updatedAt: now
|
|
276
549
|
};
|
|
@@ -283,11 +556,16 @@ var DiscussionServiceImpl = class {
|
|
|
283
556
|
}
|
|
284
557
|
await scope.outbox.append([discussionEvents.commentReported.make({
|
|
285
558
|
orgId,
|
|
286
|
-
data:
|
|
559
|
+
data: {
|
|
560
|
+
...saved,
|
|
561
|
+
communityId: comment.communityId
|
|
562
|
+
},
|
|
563
|
+
metadata: { actorOrgUserId: actor.id }
|
|
287
564
|
})]);
|
|
288
565
|
this.logger.info("comment reported", {
|
|
289
566
|
orgId,
|
|
290
|
-
commentId
|
|
567
|
+
commentId,
|
|
568
|
+
actorId: actor.id
|
|
291
569
|
});
|
|
292
570
|
return saved;
|
|
293
571
|
});
|
|
@@ -301,29 +579,30 @@ var DiscussionServiceImpl = class {
|
|
|
301
579
|
};
|
|
302
580
|
const reports = await this.repo.listOpenReports(orgId, rows.map((c) => c.id));
|
|
303
581
|
const authors = await this.repo.authorsOf(orgId, [.../* @__PURE__ */ new Set([...this.peopleIn(rows), ...reports.map((r) => r.orgUserId)])]);
|
|
582
|
+
const urls = this.fileUrls;
|
|
304
583
|
const byComment = /* @__PURE__ */ new Map();
|
|
305
584
|
for (const report of reports) byComment.set(report.commentId, [...byComment.get(report.commentId) ?? [], report]);
|
|
306
585
|
return {
|
|
307
586
|
...page,
|
|
308
587
|
rows: page.rows.map(({ comment, courseId, activityTitle }) => {
|
|
309
588
|
const author = authors[comment.orgUserId];
|
|
310
|
-
if (!author) throw new NotFoundError("
|
|
589
|
+
if (!author) throw new NotFoundError("Member", comment.orgUserId);
|
|
311
590
|
return {
|
|
312
591
|
id: comment.id,
|
|
313
592
|
parentId: comment.parentId,
|
|
314
|
-
activityId: comment
|
|
593
|
+
activityId: this.activityIdOf(comment),
|
|
315
594
|
activityTitle,
|
|
316
595
|
courseId,
|
|
317
596
|
body: comment.body,
|
|
318
597
|
status: comment.status,
|
|
319
|
-
author:
|
|
598
|
+
author: authorView(orgId, author, urls),
|
|
320
599
|
authorEmail: author.email,
|
|
321
600
|
removedBy: comment.removedBy,
|
|
322
601
|
reports: (byComment.get(comment.id) ?? []).map((r) => {
|
|
323
602
|
const reporter = authors[r.orgUserId];
|
|
324
|
-
if (!reporter) throw new NotFoundError("
|
|
603
|
+
if (!reporter) throw new NotFoundError("Member", r.orgUserId);
|
|
325
604
|
return {
|
|
326
|
-
reporter:
|
|
605
|
+
reporter: authorView(orgId, reporter, urls),
|
|
327
606
|
reason: r.reason,
|
|
328
607
|
createdAt: r.createdAt
|
|
329
608
|
};
|