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