@headless-lms/core 0.2.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 +39 -21
- 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 +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 +291 -38
- package/dist/assets/service.js.map +1 -1
- package/dist/automations/actions.d.ts +13 -24
- package/dist/automations/actions.js +168 -43
- package/dist/automations/actions.js.map +1 -1
- package/dist/automations/catalog.d.ts +2 -1
- package/dist/automations/catalog.js +229 -13
- 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 +28 -0
- package/dist/automations/index.d.ts +4 -2
- package/dist/automations/index.js +3 -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/ports.d.ts +5 -1
- 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 +22 -3
- package/dist/automations/service.js +306 -43
- package/dist/automations/service.js.map +1 -1
- package/dist/content/events.d.ts +631 -19
- package/dist/content/events.js +44 -1
- package/dist/content/events.js.map +1 -1
- package/dist/content/index.d.ts +4 -4
- package/dist/content/model.d.ts +2 -2
- package/dist/content/ports.d.ts +71 -18
- package/dist/content/service.d.ts +57 -18
- package/dist/content/service.js +502 -23
- 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 +619 -56
- package/dist/discussion/events.js +106 -3
- 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/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 +32 -10
- package/dist/discussion/service.js +293 -78
- package/dist/discussion/service.js.map +1 -1
- 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 +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 +472 -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/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 +73 -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/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 +74 -32
- package/dist/organizations/events.js +6 -0
- package/dist/organizations/events.js.map +1 -1
- package/dist/organizations/index.d.ts +7 -6
- 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 +14 -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 +20 -11
- package/dist/organizations/service.js +157 -47
- package/dist/organizations/service.js.map +1 -1
- package/dist/organizations/types.d.ts +1 -10
- 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 +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/id.d.ts +11 -0
- package/dist/shared/id.js +12 -1
- package/dist/shared/id.js.map +1 -1
- package/dist/shared/mailer.d.ts +4 -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/types/assets.d.ts +2 -2
- package/dist/types/automations.d.ts +10 -2
- package/dist/types/content.d.ts +2 -2
- package/dist/types/discussion.d.ts +11 -3
- package/dist/types/email-templates.d.ts +121 -0
- package/dist/types/entitlements.d.ts +2 -2
- package/dist/types/identity.d.ts +48 -2
- package/dist/types/index.d.ts +9 -9
- 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 +18 -1
- package/dist/types/schemas/automations.js +15 -6
- package/dist/types/schemas/automations.js.map +1 -1
- package/dist/types/schemas/content.d.ts +335 -3
- package/dist/types/schemas/content.js +139 -3
- package/dist/types/schemas/content.js.map +1 -1
- package/dist/types/schemas/discussion.d.ts +370 -3
- package/dist/types/schemas/discussion.js +199 -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 +111 -1
- package/dist/types/schemas/identity.js +55 -2
- package/dist/types/schemas/identity.js.map +1 -1
- package/dist/types/schemas/index.d.ts +8 -8
- package/dist/types/schemas/index.js +8 -8
- 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/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/ports.js +0 -1
- 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
|
@@ -1,8 +1,10 @@
|
|
|
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";
|
|
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,29 +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
97
|
if (this.isStaff(actor)) return true;
|
|
95
98
|
const activity = await this.content.getActivity(orgId, activityId);
|
|
96
|
-
return activity ? this.access.
|
|
99
|
+
return activity ? this.access.hasAccess(orgId, actor.id, activity.courseId) : false;
|
|
97
100
|
}
|
|
98
|
-
async
|
|
99
|
-
|
|
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);
|
|
111
|
+
}
|
|
112
|
+
async postOnActivity(orgId, activityId, { actor, parentId, body }) {
|
|
113
|
+
await this.requireActivityOpen(orgId, activityId, actor);
|
|
100
114
|
const config = await this.resolveConfig(orgId, activityId);
|
|
101
|
-
if (!config.enabled) throw new ForbiddenError("
|
|
115
|
+
if (!config.enabled) throw new ForbiddenError("Discussion is not open on this activity");
|
|
102
116
|
if (parentId !== null) {
|
|
103
|
-
if (!config.threaded) throw new ForbiddenError("
|
|
117
|
+
if (!config.threaded) throw new ForbiddenError("Replies are off on this course");
|
|
104
118
|
const parent = await this.repo.findComment(orgId, parentId);
|
|
105
119
|
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("
|
|
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");
|
|
108
122
|
}
|
|
109
123
|
const status = config.requireReview && !this.isStaff(actor) ? "pending" : "published";
|
|
110
124
|
const comment = {
|
|
111
125
|
id: genId("comment"),
|
|
112
126
|
orgId,
|
|
113
127
|
activityId,
|
|
128
|
+
postId: null,
|
|
129
|
+
communityId: null,
|
|
114
130
|
parentId,
|
|
115
131
|
orgUserId: actor.id,
|
|
116
132
|
body,
|
|
@@ -128,47 +144,151 @@ var DiscussionServiceImpl = class {
|
|
|
128
144
|
this.logger.info("comment created", {
|
|
129
145
|
orgId,
|
|
130
146
|
commentId: row.id,
|
|
131
|
-
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
|
|
132
192
|
});
|
|
133
193
|
return row;
|
|
134
194
|
});
|
|
135
195
|
return this.renderOne(orgId, saved, actor);
|
|
136
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
|
+
}
|
|
137
250
|
async activityComments(orgId, activityId, actor) {
|
|
251
|
+
await this.requireActivityOpen(orgId, activityId, actor);
|
|
138
252
|
const config = await this.resolveConfig(orgId, activityId);
|
|
139
253
|
if (!config.enabled) return {
|
|
140
254
|
config,
|
|
141
255
|
comments: []
|
|
142
256
|
};
|
|
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({})]);
|
|
257
|
+
const rows = await this.repo.listByActivity(orgId, activityId);
|
|
153
258
|
return {
|
|
154
259
|
config,
|
|
155
|
-
comments:
|
|
260
|
+
comments: await this.thread(orgId, rows, actor, config.reactions, (person) => this.isStaff(person))
|
|
156
261
|
};
|
|
157
262
|
}
|
|
158
263
|
async edit(orgId, commentId, actor, body) {
|
|
159
264
|
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("
|
|
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");
|
|
163
268
|
const updated = await this.repo.updateComment(orgId, commentId, { body });
|
|
164
269
|
if (!updated) throw new NotFoundError("Comment", commentId);
|
|
165
270
|
return this.renderOne(orgId, updated, actor);
|
|
166
271
|
}
|
|
167
|
-
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) {
|
|
168
276
|
const comment = await this.getComment(orgId, commentId);
|
|
169
|
-
if (!comment
|
|
170
|
-
|
|
171
|
-
|
|
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
|
+
}
|
|
172
292
|
if (comment.status === "removed") return comment;
|
|
173
293
|
return this.uow.run(async (scope) => {
|
|
174
294
|
const updated = await scope.discussion.updateComment(orgId, commentId, {
|
|
@@ -176,28 +296,49 @@ var DiscussionServiceImpl = class {
|
|
|
176
296
|
removedBy: actor.id
|
|
177
297
|
});
|
|
178
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
|
+
});
|
|
179
304
|
await scope.outbox.append([discussionEvents.commentRemoved.make({
|
|
180
305
|
orgId,
|
|
181
|
-
data: updated
|
|
306
|
+
data: updated,
|
|
307
|
+
metadata: updated.postId ? { actorOrgUserId: actor.id } : void 0
|
|
182
308
|
})]);
|
|
183
309
|
this.logger.info("comment removed", {
|
|
184
310
|
orgId,
|
|
185
311
|
commentId,
|
|
186
|
-
|
|
312
|
+
actorId: actor.id
|
|
187
313
|
});
|
|
188
314
|
return updated;
|
|
189
315
|
});
|
|
190
316
|
}
|
|
191
|
-
async restore(orgId, commentId, actor) {
|
|
192
|
-
|
|
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");
|
|
193
322
|
const comment = await this.getComment(orgId, commentId);
|
|
194
|
-
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
|
+
}
|
|
195
331
|
if (comment.status !== "removed") return comment;
|
|
196
332
|
const updated = await this.repo.updateComment(orgId, commentId, {
|
|
197
333
|
status: "published",
|
|
198
334
|
removedBy: null
|
|
199
335
|
});
|
|
200
336
|
if (!updated) throw new NotFoundError("Comment", commentId);
|
|
337
|
+
this.logger.info("comment restored", {
|
|
338
|
+
orgId,
|
|
339
|
+
commentId,
|
|
340
|
+
actorId: actor.id
|
|
341
|
+
});
|
|
201
342
|
return updated;
|
|
202
343
|
}
|
|
203
344
|
/** Load a comment together with its activity's resolved comments config,
|
|
@@ -206,25 +347,45 @@ var DiscussionServiceImpl = class {
|
|
|
206
347
|
async loadWithConfig(orgId, commentId, actor) {
|
|
207
348
|
const comment = await this.getComment(orgId, commentId);
|
|
208
349
|
if (!comment) throw new NotFoundError("Comment", commentId);
|
|
209
|
-
if (
|
|
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");
|
|
210
364
|
return {
|
|
211
365
|
comment,
|
|
212
|
-
config: await this.resolveConfig(orgId,
|
|
366
|
+
config: await this.resolveConfig(orgId, activityId)
|
|
213
367
|
};
|
|
214
368
|
}
|
|
215
369
|
async setReaction(orgId, commentId, actor, emoji) {
|
|
216
370
|
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("
|
|
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");
|
|
220
374
|
await this.repo.setReaction(orgId, commentId, actor.id, emoji);
|
|
221
375
|
return (await this.repo.reactionsOf(orgId, [commentId], actor.id))[commentId] ?? EMPTY_REACTIONS;
|
|
222
376
|
}
|
|
223
377
|
async approve(orgId, commentId, actor) {
|
|
224
|
-
if (!this.isStaff(actor)) throw new ForbiddenError("
|
|
378
|
+
if (!this.isStaff(actor)) throw new ForbiddenError("You cannot approve this comment");
|
|
225
379
|
const comment = await this.getComment(orgId, commentId);
|
|
226
|
-
if (!comment)
|
|
227
|
-
|
|
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");
|
|
228
389
|
return this.uow.run(async (scope) => {
|
|
229
390
|
const updated = await scope.discussion.updateComment(orgId, commentId, { status: "published" });
|
|
230
391
|
if (!updated) throw new NotFoundError("Comment", commentId);
|
|
@@ -234,7 +395,8 @@ var DiscussionServiceImpl = class {
|
|
|
234
395
|
})]);
|
|
235
396
|
this.logger.info("comment published", {
|
|
236
397
|
orgId,
|
|
237
|
-
commentId
|
|
398
|
+
commentId,
|
|
399
|
+
actorId: actor.id
|
|
238
400
|
});
|
|
239
401
|
return updated;
|
|
240
402
|
});
|
|
@@ -243,26 +405,49 @@ var DiscussionServiceImpl = class {
|
|
|
243
405
|
* caller must not decide that itself. `approve` and `restore` still enforce
|
|
244
406
|
* their own staff check; the check here covers the third path, where a
|
|
245
407
|
* comment is already published and neither of them would run. */
|
|
246
|
-
async publish(orgId, commentId, actor) {
|
|
247
|
-
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");
|
|
248
410
|
const comment = await this.getComment(orgId, commentId);
|
|
249
|
-
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
|
+
}
|
|
250
419
|
if (comment.status === "pending") return this.approve(orgId, commentId, actor);
|
|
251
|
-
if (comment.status === "removed") return this.
|
|
420
|
+
if (comment.status === "removed") return this.restoreComment(orgId, commentId, actor);
|
|
252
421
|
return comment;
|
|
253
422
|
}
|
|
254
|
-
async resolveReports(orgId, commentId, actor) {
|
|
255
|
-
|
|
256
|
-
if (!
|
|
257
|
-
|
|
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
|
+
});
|
|
258
442
|
this.logger.info("comment reports resolved", {
|
|
259
443
|
orgId,
|
|
260
|
-
commentId
|
|
444
|
+
commentId,
|
|
445
|
+
actorId: actor.id
|
|
261
446
|
});
|
|
262
447
|
}
|
|
263
448
|
async reportComment(orgId, commentId, actor, reason) {
|
|
264
|
-
const { config } = await this.loadWithConfig(orgId, commentId, actor);
|
|
265
|
-
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");
|
|
266
451
|
const now = /* @__PURE__ */ new Date();
|
|
267
452
|
const report = {
|
|
268
453
|
id: genId("commentReport"),
|
|
@@ -271,6 +456,8 @@ var DiscussionServiceImpl = class {
|
|
|
271
456
|
orgUserId: actor.id,
|
|
272
457
|
reason,
|
|
273
458
|
resolvedAt: null,
|
|
459
|
+
resolution: null,
|
|
460
|
+
resolvedBy: null,
|
|
274
461
|
createdAt: now,
|
|
275
462
|
updatedAt: now
|
|
276
463
|
};
|
|
@@ -283,11 +470,16 @@ var DiscussionServiceImpl = class {
|
|
|
283
470
|
}
|
|
284
471
|
await scope.outbox.append([discussionEvents.commentReported.make({
|
|
285
472
|
orgId,
|
|
286
|
-
data:
|
|
473
|
+
data: {
|
|
474
|
+
...saved,
|
|
475
|
+
communityId: comment.communityId
|
|
476
|
+
},
|
|
477
|
+
metadata: comment.postId ? { actorOrgUserId: actor.id } : void 0
|
|
287
478
|
})]);
|
|
288
479
|
this.logger.info("comment reported", {
|
|
289
480
|
orgId,
|
|
290
|
-
commentId
|
|
481
|
+
commentId,
|
|
482
|
+
actorId: actor.id
|
|
291
483
|
});
|
|
292
484
|
return saved;
|
|
293
485
|
});
|
|
@@ -301,29 +493,30 @@ var DiscussionServiceImpl = class {
|
|
|
301
493
|
};
|
|
302
494
|
const reports = await this.repo.listOpenReports(orgId, rows.map((c) => c.id));
|
|
303
495
|
const authors = await this.repo.authorsOf(orgId, [.../* @__PURE__ */ new Set([...this.peopleIn(rows), ...reports.map((r) => r.orgUserId)])]);
|
|
496
|
+
const urls = this.fileUrls;
|
|
304
497
|
const byComment = /* @__PURE__ */ new Map();
|
|
305
498
|
for (const report of reports) byComment.set(report.commentId, [...byComment.get(report.commentId) ?? [], report]);
|
|
306
499
|
return {
|
|
307
500
|
...page,
|
|
308
501
|
rows: page.rows.map(({ comment, courseId, activityTitle }) => {
|
|
309
502
|
const author = authors[comment.orgUserId];
|
|
310
|
-
if (!author) throw new NotFoundError("
|
|
503
|
+
if (!author) throw new NotFoundError("Member", comment.orgUserId);
|
|
311
504
|
return {
|
|
312
505
|
id: comment.id,
|
|
313
506
|
parentId: comment.parentId,
|
|
314
|
-
activityId: comment
|
|
507
|
+
activityId: this.activityIdOf(comment),
|
|
315
508
|
activityTitle,
|
|
316
509
|
courseId,
|
|
317
510
|
body: comment.body,
|
|
318
511
|
status: comment.status,
|
|
319
|
-
author:
|
|
512
|
+
author: authorView(orgId, author, urls),
|
|
320
513
|
authorEmail: author.email,
|
|
321
514
|
removedBy: comment.removedBy,
|
|
322
515
|
reports: (byComment.get(comment.id) ?? []).map((r) => {
|
|
323
516
|
const reporter = authors[r.orgUserId];
|
|
324
|
-
if (!reporter) throw new NotFoundError("
|
|
517
|
+
if (!reporter) throw new NotFoundError("Member", r.orgUserId);
|
|
325
518
|
return {
|
|
326
|
-
reporter:
|
|
519
|
+
reporter: authorView(orgId, reporter, urls),
|
|
327
520
|
reason: r.reason,
|
|
328
521
|
createdAt: r.createdAt
|
|
329
522
|
};
|
|
@@ -337,6 +530,28 @@ var DiscussionServiceImpl = class {
|
|
|
337
530
|
getComment(orgId, commentId) {
|
|
338
531
|
return this.repo.findComment(orgId, commentId);
|
|
339
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
|
+
}
|
|
340
555
|
};
|
|
341
556
|
//#endregion
|
|
342
557
|
export { DEFAULT_SETTINGS, DiscussionServiceImpl };
|