@headless-lms/core 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/events.d.ts +15 -9
- package/dist/assets/file-urls.d.ts +14 -7
- package/dist/assets/file-urls.js +15 -4
- package/dist/assets/file-urls.js.map +1 -1
- package/dist/assets/index.d.ts +4 -4
- package/dist/assets/index.js +3 -3
- package/dist/assets/ports.d.ts +34 -15
- package/dist/assets/service.d.ts +30 -14
- package/dist/assets/service.js +233 -54
- package/dist/assets/service.js.map +1 -1
- package/dist/automations/actions.js.map +1 -1
- package/dist/automations/catalog.js +68 -3
- package/dist/automations/catalog.js.map +1 -1
- package/dist/automations/community-derivations.js +16 -16
- package/dist/automations/community-derivations.js.map +1 -1
- package/dist/automations/community-mail.d.ts +34 -4
- package/dist/automations/community-mail.js +32 -4
- package/dist/automations/community-mail.js.map +1 -1
- package/dist/automations/event-permissions.d.ts +12 -0
- package/dist/automations/event-permissions.js +38 -0
- package/dist/automations/event-permissions.js.map +1 -0
- package/dist/automations/index.d.ts +2 -1
- package/dist/automations/index.js +2 -1
- package/dist/automations/notifications/applicable.d.ts +8 -0
- package/dist/automations/notifications/applicable.js +12 -0
- package/dist/automations/notifications/applicable.js.map +1 -0
- package/dist/automations/notifications/debounce.js +1 -1
- package/dist/automations/notifications/debounce.js.map +1 -1
- package/dist/automations/notifications/defaults.js +33 -3
- package/dist/automations/notifications/defaults.js.map +1 -1
- package/dist/automations/notifications/processor.js +1 -1
- package/dist/automations/notifications/processor.js.map +1 -1
- package/dist/automations/notifications/rules/content.js +60 -3
- package/dist/automations/notifications/rules/content.js.map +1 -1
- package/dist/automations/notifications/rules/discussion.js +29 -29
- package/dist/automations/notifications/rules/discussion.js.map +1 -1
- package/dist/automations/notifications/rules/entitlements.js +36 -9
- package/dist/automations/notifications/rules/entitlements.js.map +1 -1
- package/dist/automations/notifications/rules/events.d.ts +9 -0
- package/dist/automations/notifications/rules/events.js +27 -0
- package/dist/automations/notifications/rules/events.js.map +1 -0
- package/dist/automations/notifications/rules/organizations.js +17 -10
- package/dist/automations/notifications/rules/organizations.js.map +1 -1
- package/dist/automations/notifications/rules/recipients.js +0 -1
- package/dist/automations/notifications/rules/recipients.js.map +1 -1
- package/dist/automations/notifications/service.d.ts +8 -6
- package/dist/automations/notifications/service.js +12 -12
- package/dist/automations/notifications/service.js.map +1 -1
- package/dist/automations/notifications/types.js +94 -7
- package/dist/automations/notifications/types.js.map +1 -1
- package/dist/automations/ports.d.ts +30 -31
- package/dist/automations/service.d.ts +20 -17
- package/dist/automations/service.js +90 -61
- package/dist/automations/service.js.map +1 -1
- package/dist/content/event-sight.d.ts +11 -0
- package/dist/content/event-sight.js +11 -0
- package/dist/content/event-sight.js.map +1 -0
- package/dist/content/events.d.ts +465 -88
- package/dist/content/events.js +34 -1
- package/dist/content/events.js.map +1 -1
- package/dist/content/index.d.ts +6 -4
- package/dist/content/index.js +3 -1
- package/dist/content/jobs.d.ts +16 -0
- package/dist/content/jobs.js +37 -0
- package/dist/content/jobs.js.map +1 -0
- package/dist/content/model.d.ts +2 -2
- package/dist/content/notification-categories.d.ts +5 -4
- package/dist/content/notification-categories.js +15 -6
- package/dist/content/notification-categories.js.map +1 -1
- package/dist/content/ports.d.ts +113 -68
- package/dist/content/service.d.ts +103 -67
- package/dist/content/service.js +739 -152
- package/dist/content/service.js.map +1 -1
- package/dist/content/titles.d.ts +6 -3
- package/dist/content/titles.js +37 -24
- package/dist/content/titles.js.map +1 -1
- package/dist/content/types.d.ts +2 -2
- package/dist/discussion/authors.d.ts +4 -2
- package/dist/discussion/authors.js +9 -5
- package/dist/discussion/authors.js.map +1 -1
- package/dist/discussion/chats.d.ts +56 -37
- package/dist/discussion/chats.js +392 -160
- package/dist/discussion/chats.js.map +1 -1
- package/dist/discussion/events.d.ts +85 -47
- package/dist/discussion/events.js +41 -3
- package/dist/discussion/events.js.map +1 -1
- package/dist/discussion/index.d.ts +8 -5
- package/dist/discussion/index.js +5 -3
- package/dist/discussion/live.d.ts +106 -0
- package/dist/discussion/live.js +642 -0
- package/dist/discussion/live.js.map +1 -0
- package/dist/discussion/mentions.js +2 -2
- package/dist/discussion/mentions.js.map +1 -1
- package/dist/discussion/ports.d.ts +182 -100
- package/dist/discussion/ports.js.map +1 -1
- package/dist/discussion/posts.d.ts +45 -39
- package/dist/discussion/posts.js +266 -152
- package/dist/discussion/posts.js.map +1 -1
- package/dist/discussion/rules.d.ts +18 -0
- package/dist/discussion/rules.js +22 -0
- package/dist/discussion/rules.js.map +1 -0
- package/dist/discussion/service.d.ts +37 -31
- package/dist/discussion/service.js +250 -186
- package/dist/discussion/service.js.map +1 -1
- package/dist/discussion/subscriptions.d.ts +8 -8
- package/dist/discussion/subscriptions.js +28 -12
- package/dist/discussion/subscriptions.js.map +1 -1
- package/dist/discussion/titles.d.ts +4 -2
- package/dist/discussion/titles.js +27 -13
- package/dist/discussion/titles.js.map +1 -1
- package/dist/entitlements/events.d.ts +58 -58
- package/dist/entitlements/index.d.ts +4 -4
- package/dist/entitlements/index.js +2 -2
- package/dist/entitlements/model.d.ts +2 -2
- package/dist/entitlements/ports.d.ts +65 -39
- package/dist/entitlements/rules.d.ts +3 -6
- package/dist/entitlements/rules.js +5 -10
- package/dist/entitlements/rules.js.map +1 -1
- package/dist/entitlements/service.d.ts +40 -28
- package/dist/entitlements/service.js +369 -162
- package/dist/entitlements/service.js.map +1 -1
- package/dist/identity/devices.d.ts +3 -3
- package/dist/identity/devices.js +6 -4
- package/dist/identity/devices.js.map +1 -1
- package/dist/identity/index.d.ts +2 -2
- package/dist/identity/index.js +2 -2
- package/dist/identity/notification-settings.d.ts +1 -1
- package/dist/identity/notification-settings.js +2 -1
- package/dist/identity/notification-settings.js.map +1 -1
- package/dist/identity/ports.d.ts +7 -7
- package/dist/identity/service.d.ts +8 -7
- package/dist/identity/service.js +16 -12
- package/dist/identity/service.js.map +1 -1
- package/dist/integrations/events.d.ts +3 -3
- package/dist/integrations/ports.d.ts +9 -8
- package/dist/integrations/service.d.ts +9 -8
- package/dist/integrations/service.js +31 -14
- package/dist/integrations/service.js.map +1 -1
- package/dist/organizations/default-roles.d.ts +22 -0
- package/dist/organizations/default-roles.js +89 -0
- package/dist/organizations/default-roles.js.map +1 -0
- package/dist/organizations/effective-permissions.d.ts +47 -0
- package/dist/organizations/effective-permissions.js +118 -0
- package/dist/organizations/effective-permissions.js.map +1 -0
- package/dist/organizations/events.d.ts +326 -181
- package/dist/organizations/events.js +90 -18
- package/dist/organizations/events.js.map +1 -1
- package/dist/organizations/index.d.ts +14 -10
- package/dist/organizations/index.js +7 -3
- package/dist/organizations/members.d.ts +66 -8
- package/dist/organizations/members.js.map +1 -1
- package/dist/organizations/model.d.ts +2 -2
- package/dist/organizations/notification-catalog.d.ts +31 -0
- package/dist/organizations/notification-catalog.js +32 -0
- package/dist/organizations/notification-catalog.js.map +1 -0
- package/dist/organizations/notification-categories.d.ts +6 -4
- package/dist/organizations/notification-categories.js +25 -10
- package/dist/organizations/notification-categories.js.map +1 -1
- package/dist/organizations/org-roles.d.ts +33 -0
- package/dist/organizations/org-roles.js +49 -0
- package/dist/organizations/org-roles.js.map +1 -0
- package/dist/organizations/permissions.d.ts +311 -0
- package/dist/organizations/permissions.js +327 -0
- package/dist/organizations/permissions.js.map +1 -0
- package/dist/organizations/ports.d.ts +187 -65
- package/dist/organizations/service.d.ts +102 -35
- package/dist/organizations/service.js +1254 -327
- package/dist/organizations/service.js.map +1 -1
- package/dist/organizations/testing.d.ts +18 -0
- package/dist/organizations/testing.js +37 -0
- package/dist/organizations/testing.js.map +1 -0
- package/dist/organizations/types.d.ts +5 -16
- package/dist/progress/events.d.ts +14 -14
- package/dist/progress/index.d.ts +2 -2
- package/dist/progress/ports.d.ts +11 -3
- package/dist/progress/service.d.ts +6 -5
- package/dist/progress/service.js +21 -6
- package/dist/progress/service.js.map +1 -1
- package/dist/reporting/admin/index.d.ts +6 -4
- package/dist/reporting/admin/model.d.ts +36 -3
- package/dist/reporting/admin/ports.d.ts +39 -14
- package/dist/reporting/admin/service.d.ts +32 -11
- package/dist/reporting/admin/service.js +102 -18
- package/dist/reporting/admin/service.js.map +1 -1
- package/dist/reporting/events/index.d.ts +27 -0
- package/dist/reporting/events/index.js +20 -0
- package/dist/reporting/events/index.js.map +1 -0
- package/dist/reporting/member/calendar.d.ts +11 -0
- package/dist/reporting/member/calendar.js +75 -0
- package/dist/reporting/member/calendar.js.map +1 -0
- package/dist/reporting/member/index.d.ts +3 -2
- package/dist/reporting/member/model.d.ts +29 -5
- package/dist/reporting/member/ports.d.ts +49 -29
- package/dist/reporting/member/service.d.ts +57 -29
- package/dist/reporting/member/service.js +345 -114
- package/dist/reporting/member/service.js.map +1 -1
- package/dist/shared/actor.d.ts +23 -3
- package/dist/shared/errors.d.ts +2 -1
- package/dist/shared/errors.js +3 -1
- package/dist/shared/errors.js.map +1 -1
- package/dist/shared/id.d.ts +4 -0
- package/dist/shared/id.js +4 -0
- package/dist/shared/id.js.map +1 -1
- package/dist/shared/logger.d.ts +2 -1
- package/dist/shared/logger.js +10 -1
- package/dist/shared/logger.js.map +1 -1
- package/dist/shared/ports.d.ts +25 -5
- package/dist/types/content.d.ts +2 -2
- package/dist/types/discussion.d.ts +4 -5
- package/dist/types/email-templates.d.ts +22 -3
- package/dist/types/entitlements.d.ts +2 -2
- package/dist/types/identity.d.ts +13 -16
- package/dist/types/index.d.ts +9 -9
- package/dist/types/index.js +2 -1
- package/dist/types/notifications.d.ts +4 -4
- package/dist/types/organizations.d.ts +3 -2
- package/dist/types/ports.d.ts +8 -1
- package/dist/types/ports.js +7 -1
- package/dist/types/ports.js.map +1 -0
- package/dist/types/schemas/assets.d.ts +5 -5
- package/dist/types/schemas/assets.js +4 -3
- package/dist/types/schemas/assets.js.map +1 -1
- package/dist/types/schemas/content.d.ts +599 -13
- package/dist/types/schemas/content.js +219 -8
- package/dist/types/schemas/content.js.map +1 -1
- package/dist/types/schemas/discussion.d.ts +280 -57
- package/dist/types/schemas/discussion.js +77 -3
- package/dist/types/schemas/discussion.js.map +1 -1
- package/dist/types/schemas/entitlements.d.ts +21 -8
- package/dist/types/schemas/entitlements.js +11 -4
- package/dist/types/schemas/entitlements.js.map +1 -1
- package/dist/types/schemas/index.d.ts +5 -5
- package/dist/types/schemas/index.js +5 -5
- package/dist/types/schemas/notifications.d.ts +11 -10
- package/dist/types/schemas/notifications.js +3 -2
- package/dist/types/schemas/notifications.js.map +1 -1
- package/dist/types/schemas/organizations.d.ts +99 -60
- package/dist/types/schemas/organizations.js +80 -27
- package/dist/types/schemas/organizations.js.map +1 -1
- package/dist/types/schemas/progress.d.ts +3 -4
- package/dist/types/schemas/progress.js +0 -1
- package/dist/types/schemas/progress.js.map +1 -1
- package/package.json +1 -1
- package/dist/organizations/access-scopes.d.ts +0 -19
- package/dist/organizations/access-scopes.js +0 -89
- package/dist/organizations/access-scopes.js.map +0 -1
- package/dist/organizations/roles.d.ts +0 -19
- package/dist/organizations/roles.js +0 -82
- package/dist/organizations/roles.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","names":[],"sources":["../../src/discussion/service.ts"],"sourcesContent":["import { genId } from '../shared/id.js';\nimport { ConflictError, ForbiddenError, NotFoundError } from '../shared/errors.js';\nimport { canModerate, isStaffRole } from '../organizations/index.js';\nimport type { Logger } from '../shared/ports.js';\nimport { noopLogger } from '../shared/logger.js';\nimport type { Comment, CommentReport, CommentSettings } from './model.js';\nimport type {\n Activity,\n ActivitySettings,\n CourseSettings,\n MentionView,\n Post,\n SpaceDetail,\n} from '../types/index.js';\nimport type {\n CommentListItem,\n CommentsConfig,\n CommentView,\n ListCommentsQuery,\n Page,\n ReactionEmoji,\n} from './types.js';\nimport { SettingsNamespace, type SettingsService } from '../shared/settings.js';\nimport type {\n ActivityComments,\n ActivityGetter,\n Actor,\n AuthorRecord,\n CommentReactions,\n ContentAccessReader,\n DiscussionRepository,\n DiscussionService,\n DiscussionUnitOfWork,\n MentionsRepository,\n PageQuery,\n PostCommentInput,\n PostService,\n} from './ports.js';\nimport { discussionEvents } from './events.js';\nimport { authorView } from './authors.js';\nimport { keepAllowed, mentionTokens, mentionViews } from './mentions.js';\nimport type { CommunityAccessReader } from '../entitlements/index.js';\nimport type { FileUrls } from '../assets/index.js';\n\n/** A course with no stored settings. Discussion is opt-in, so the common case\n * persists no row at all and every existing course stays silent. */\nexport const DEFAULT_SETTINGS = {\n enabled: false,\n threaded: true,\n requireReview: false,\n reactions: true,\n} as const satisfies CommentSettings;\n\nconst EMPTY_REACTIONS: CommentReactions = { reactions: {} };\n\nexport type DiscussionServiceParams = {\n repo: DiscussionRepository;\n access: ContentAccessReader;\n content: ActivityGetter;\n posts: Pick<PostService, 'visible' | 'find'>;\n spaces: Pick<CommunityAccessReader, 'spaceOpenersAmong'>;\n mentions: MentionsRepository;\n uow: DiscussionUnitOfWork;\n settings: SettingsService;\n fileUrls: FileUrls;\n logger?: Logger;\n};\n\nexport class DiscussionServiceImpl implements DiscussionService {\n private readonly repo: DiscussionRepository;\n private readonly access: ContentAccessReader;\n private readonly content: ActivityGetter;\n private readonly posts: Pick<PostService, 'visible' | 'find'>;\n private readonly spaces: Pick<CommunityAccessReader, 'spaceOpenersAmong'>;\n private readonly mentions: MentionsRepository;\n private readonly uow: DiscussionUnitOfWork;\n private readonly settings: SettingsService;\n private readonly fileUrls: FileUrls;\n private readonly logger: Logger;\n\n constructor(params: DiscussionServiceParams) {\n this.repo = params.repo;\n this.access = params.access;\n this.content = params.content;\n this.posts = params.posts;\n this.spaces = params.spaces;\n this.mentions = params.mentions;\n this.uow = params.uow;\n this.settings = params.settings;\n this.fileUrls = params.fileUrls;\n this.logger = params.logger ?? noopLogger;\n }\n\n async resolveConfig(orgId: string, activityId: string): Promise<CommentsConfig> {\n const activity = await this.content.getActivity(orgId, activityId);\n if (!activity) {\n throw new NotFoundError('Activity', activityId);\n }\n\n // The `content` namespace row is the whole CourseSettings, owned and written\n // by content; this context only reads its `comments` key.\n const stored = await this.settings.get<Partial<CourseSettings>>(\n orgId,\n SettingsNamespace.content,\n activity.courseId,\n );\n const settings = { ...DEFAULT_SETTINGS, ...stored?.comments };\n // Comments off for the course cannot be overridden back on by an activity:\n // the course switch is the master.\n const rule = ((activity?.settings ?? {}) as ActivitySettings).comments ?? 'inherit';\n return {\n enabled: settings.enabled && rule !== 'never',\n threaded: settings.threaded,\n requireReview: settings.requireReview,\n reactions: settings.reactions,\n };\n }\n\n private isStaff(actor: Actor): boolean {\n return isStaffRole(actor.role);\n }\n\n private toView(\n orgId: string,\n comment: Comment,\n authors: Record<string, AuthorRecord>,\n urls: FileUrls,\n reactions: CommentReactions,\n mentions: MentionView[],\n ): CommentView {\n const author = authors[comment.orgUserId];\n if (!author) {\n throw new NotFoundError('Member', comment.orgUserId);\n }\n if (comment.removedBy && !authors[comment.removedBy]) {\n throw new NotFoundError('Member', comment.removedBy);\n }\n const remover = comment.removedBy ? authors[comment.removedBy] : undefined;\n return {\n id: comment.id,\n activityId: comment.activityId,\n postId: comment.postId,\n communityId: comment.communityId,\n parentId: comment.parentId,\n author: authorView(orgId, author, urls),\n body: comment.status === 'removed' ? null : comment.body,\n status: comment.status,\n removedBy: remover ? authorView(orgId, remover, urls) : null,\n reactions: reactions.reactions,\n viewerReaction: reactions.viewerReaction,\n mentions: comment.status === 'removed' ? [] : mentions,\n createdAt: comment.createdAt,\n updatedAt: comment.updatedAt,\n };\n }\n\n private peopleIn(comments: Comment[]): string[] {\n const ids = new Set<string>();\n for (const c of comments) {\n ids.add(c.orgUserId);\n if (c.removedBy) {\n ids.add(c.removedBy);\n }\n }\n return [...ids];\n }\n\n private async renderOne(orgId: string, comment: Comment, actor: Actor): Promise<CommentView> {\n const [authors, mentioned] = await Promise.all([\n this.repo.authorsOf(orgId, this.peopleIn([comment])),\n mentionViews(this.mentions, this.repo, orgId, 'comment', [comment.id]),\n ]);\n const stored =\n comment.status === 'removed'\n ? EMPTY_REACTIONS\n : ((await this.repo.reactionsOf(orgId, [comment.id], actor.id))[comment.id] ??\n EMPTY_REACTIONS);\n return this.toView(orgId, comment, authors, this.fileUrls, stored, mentioned[comment.id] ?? []);\n }\n\n private activityIdOf(comment: Comment): string {\n if (comment.activityId === null) {\n throw new NotFoundError('Activity', comment.id);\n }\n return comment.activityId;\n }\n\n private async hasAccess(orgId: string, activityId: string, actor: Actor): Promise<boolean> {\n if (this.isStaff(actor)) {\n return true;\n }\n const activity = await this.content.getActivity(orgId, activityId);\n return activity ? this.access.hasAccess(orgId, actor.id, activity.courseId) : false;\n }\n\n private async requireActivityOpen(\n orgId: string,\n activityId: string,\n actor: Actor,\n ): Promise<Activity> {\n const activity = await this.content.getActivity(orgId, activityId);\n if (!activity) {\n throw new NotFoundError('Activity', activityId);\n }\n if (\n !this.isStaff(actor) &&\n !(await this.access.hasAccess(orgId, actor.id, activity.courseId))\n ) {\n throw new ForbiddenError('You cannot open this activity');\n }\n return activity;\n }\n\n private async courseOpeners(\n orgId: string,\n courseId: string,\n orgUserIds: string[],\n ): Promise<string[]> {\n if (orgUserIds.length === 0) {\n return [];\n }\n const people = await this.repo.authorsOf(orgId, orgUserIds);\n const opens = await Promise.all(\n orgUserIds.map(async (orgUserId) => {\n const person = people[orgUserId];\n if (!person) {\n return false;\n }\n return isStaffRole(person.role) || this.access.hasAccess(orgId, orgUserId, courseId);\n }),\n );\n return orgUserIds.filter((_, index) => opens[index]);\n }\n\n private async spaceOpeners(orgId: string, spaceId: string, orgUserIds: string[]) {\n if (orgUserIds.length === 0) {\n return [];\n }\n return keepAllowed(orgUserIds, await this.spaces.spaceOpenersAmong(orgId, spaceId, orgUserIds));\n }\n\n private async openersOf(orgId: string, comment: Comment, orgUserIds: string[]) {\n if (orgUserIds.length === 0) {\n return [];\n }\n if (comment.postId) {\n const post = await this.posts.find(orgId, comment.postId);\n return post ? this.spaceOpeners(orgId, post.spaceId, orgUserIds) : [];\n }\n const activity = await this.content.getActivity(orgId, this.activityIdOf(comment));\n return activity ? this.courseOpeners(orgId, activity.courseId, orgUserIds) : [];\n }\n\n commenterIds(\n orgId: string,\n target: { postId: string } | { activityId: string },\n ): Promise<string[]> {\n return this.repo.commenterIds(orgId, target);\n }\n\n async postComment(orgId: string, input: PostCommentInput): Promise<CommentView> {\n if (input.postId) {\n return this.postOnPost(orgId, input.postId, input);\n }\n const activityId = input.activityId;\n if (!activityId) {\n throw new NotFoundError('Activity', activityId ?? '');\n }\n return this.postOnActivity(orgId, activityId, input);\n }\n\n private async postOnActivity(\n orgId: string,\n activityId: string,\n { actor, parentId, body }: PostCommentInput,\n ): Promise<CommentView> {\n const activity = await this.requireActivityOpen(orgId, activityId, actor);\n const config = await this.resolveConfig(orgId, activityId);\n\n if (!config.enabled) {\n throw new ForbiddenError('Discussion is not open on this activity');\n }\n\n if (parentId !== null) {\n if (!config.threaded) {\n throw new ForbiddenError('Replies are off on this course');\n }\n const parent = await this.repo.findComment(orgId, parentId);\n if (!parent || parent.activityId !== activityId) {\n throw new NotFoundError('Comment', parentId);\n }\n\n if (parent.parentId !== null) {\n throw new ForbiddenError('A reply cannot have replies of its own');\n }\n\n if (parent.status !== 'published') {\n throw new ForbiddenError('You can reply only to a published comment');\n }\n }\n\n const status: Comment['status'] =\n config.requireReview && !this.isStaff(actor) ? 'pending' : 'published';\n\n const comment: Comment = {\n id: genId('comment'),\n orgId,\n activityId,\n postId: null,\n communityId: null,\n parentId,\n orgUserId: actor.id,\n body,\n status,\n removedBy: null,\n createdAt: new Date(),\n updatedAt: new Date(),\n };\n const mentioned = await this.courseOpeners(orgId, activity.courseId, mentionTokens(body));\n const saved = await this.uow.run(async (scope) => {\n const row = await scope.discussion.insertComment(orgId, comment);\n await scope.mentions.sync(orgId, 'comment', row.id, mentioned, comment.createdAt);\n await scope.outbox.append([\n discussionEvents.commentCreated.make({\n orgId,\n data: { ...row, mentionedOrgUserIds: status === 'published' ? mentioned : [] },\n metadata: { actorOrgUserId: actor.id },\n }),\n ]);\n this.logger.info('comment created', { orgId, commentId: row.id, status, actorId: actor.id });\n return row;\n });\n return this.renderOne(orgId, saved, actor);\n }\n\n private async postOnPost(\n orgId: string,\n postId: string,\n { actor, parentId, body }: PostCommentInput,\n ): Promise<CommentView> {\n const { post, space } = await this.visiblePost(orgId, actor, postId);\n if (post.status !== 'published') {\n throw new ConflictError('This post is not published');\n }\n if (!this.commentsOpen(post, space)) {\n throw new ForbiddenError('Comments are closed on this post');\n }\n if (parentId !== null) {\n const parent = await this.repo.findComment(orgId, parentId);\n if (!parent || parent.postId !== postId) {\n throw new NotFoundError('Comment', parentId);\n }\n if (parent.parentId !== null) {\n throw new ForbiddenError('A reply cannot have replies of its own');\n }\n if (parent.status !== 'published') {\n throw new ForbiddenError('You can reply only to a published comment');\n }\n }\n const now = new Date();\n const comment: Comment = {\n id: genId('comment'),\n orgId,\n activityId: null,\n postId,\n communityId: post.communityId,\n parentId,\n orgUserId: actor.id,\n body,\n status: 'published',\n removedBy: null,\n createdAt: now,\n updatedAt: now,\n };\n const mentioned = await this.spaceOpeners(orgId, post.spaceId, mentionTokens(body));\n const saved = await this.uow.run(async (scope) => {\n const row = await scope.discussion.insertComment(orgId, comment);\n await scope.mentions.sync(orgId, 'comment', row.id, mentioned, now);\n await scope.outbox.append([\n discussionEvents.commentCreated.make({\n orgId,\n data: { ...row, mentionedOrgUserIds: mentioned },\n metadata: { actorOrgUserId: actor.id },\n }),\n ]);\n this.logger.info('comment created', {\n orgId,\n commentId: row.id,\n postId,\n status: 'published',\n actorId: actor.id,\n });\n return row;\n });\n return this.renderOne(orgId, saved, actor);\n }\n\n async postComments(\n orgId: string,\n postId: string,\n actor: Actor,\n query: PageQuery,\n ): Promise<Page<CommentView>> {\n const { post, space } = await this.visiblePost(orgId, actor, postId);\n const seesHeld = (person: Actor) => this.canModerate(person);\n const page = await this.repo.listThreadsByPost(orgId, postId, {\n ...query,\n viewerId: actor.id,\n seesHeld: seesHeld(actor),\n });\n const comments = await this.thread(\n orgId,\n page.rows,\n actor,\n post.reactionsEnabled ?? space.settings.reactions,\n seesHeld,\n );\n this.logger.debug('post comments read', { orgId, postId, count: comments.length });\n return { ...page, rows: comments };\n }\n\n private async thread(\n orgId: string,\n rows: Comment[],\n actor: Actor,\n reactionsOn: boolean,\n seesHeld: (actor: Actor) => boolean,\n ): Promise<CommentView[]> {\n const readable = rows.filter((comment) => {\n if (comment.status === 'published' || comment.status === 'removed') {\n return true;\n }\n return seesHeld(actor) || comment.orgUserId === actor.id;\n });\n const replies = readable.filter(\n (comment) => comment.parentId !== null && comment.status !== 'removed',\n );\n const heldOpen = new Set(replies.map((reply) => reply.parentId));\n const roots = readable.filter(\n (comment) =>\n comment.parentId === null && (comment.status !== 'removed' || heldOpen.has(comment.id)),\n );\n const rootIds = new Set(roots.map((root) => root.id));\n const served = [...roots, ...replies.filter((reply) => rootIds.has(reply.parentId!))].sort(\n (a, b) => a.createdAt.getTime() - b.createdAt.getTime(),\n );\n const [authors, reactions, mentioned] = await Promise.all([\n this.repo.authorsOf(orgId, this.peopleIn(served)),\n reactionsOn\n ? this.repo.reactionsOf(\n orgId,\n served.map((comment) => comment.id),\n actor.id,\n )\n : Promise.resolve<Record<string, CommentReactions>>({}),\n mentionViews(\n this.mentions,\n this.repo,\n orgId,\n 'comment',\n served.map((comment) => comment.id),\n ),\n ]);\n const urls = this.fileUrls;\n return served.map((comment) =>\n this.toView(\n orgId,\n comment,\n authors,\n urls,\n reactions[comment.id] ?? EMPTY_REACTIONS,\n mentioned[comment.id] ?? [],\n ),\n );\n }\n\n private async visiblePost(\n orgId: string,\n actor: Actor,\n postId: string,\n ): Promise<{ post: Post; space: SpaceDetail }> {\n return this.posts.visible(orgId, actor, postId);\n }\n\n private async postOfComment(\n orgId: string,\n actor: Actor,\n commentId: string,\n postId: string,\n ): Promise<{ post: Post; space: SpaceDetail }> {\n try {\n return await this.visiblePost(orgId, actor, postId);\n } catch (error) {\n if (error instanceof NotFoundError) {\n throw new NotFoundError('Comment', commentId);\n }\n throw error;\n }\n }\n\n private canModerate(actor: Actor): boolean {\n return canModerate(actor.role);\n }\n\n private inCommunity(comment: Comment, communityId: string | null | undefined): boolean {\n return communityId === undefined || comment.communityId === communityId;\n }\n\n private commentsOpen(post: Post, space: SpaceDetail): boolean {\n return post.commentsClosed === null ? space.settings.comments : !post.commentsClosed;\n }\n\n async activityComments(\n orgId: string,\n activityId: string,\n actor: Actor,\n ): Promise<ActivityComments> {\n await this.requireActivityOpen(orgId, activityId, actor);\n const config = await this.resolveConfig(orgId, activityId);\n if (!config.enabled) {\n return { config, comments: [] };\n }\n const rows = await this.repo.listByActivity(orgId, activityId);\n return {\n config,\n comments: await this.thread(orgId, rows, actor, config.reactions, (person) =>\n this.isStaff(person),\n ),\n };\n }\n\n async edit(orgId: string, commentId: string, actor: Actor, body: string): Promise<CommentView> {\n const { comment, config } = await this.loadWithConfig(orgId, commentId, actor);\n if (!config.enabled) {\n throw new ForbiddenError(\n comment.postId\n ? 'Comments are closed on this post'\n : 'Discussion is not open on this activity',\n );\n }\n // Editing is the author's alone — moderators remove, they do not rewrite.\n if (comment.orgUserId !== actor.id) {\n throw new ForbiddenError('You can edit only your own comment');\n }\n if (comment.status === 'removed') {\n throw new ForbiddenError('A removed comment cannot be edited');\n }\n const candidates = await this.openersOf(orgId, comment, mentionTokens(body));\n const updated = await this.uow.run(async (scope) => {\n const next = await scope.discussion.updateComment(orgId, commentId, {\n body,\n });\n if (!next) {\n // getComment() proved it existed; a null here means it vanished mid-write.\n // Returning the pre-edit comment would report success and hand back\n // the old body.\n throw new NotFoundError('Comment', commentId);\n }\n const added = await scope.mentions.sync(orgId, 'comment', commentId, candidates, new Date());\n await scope.outbox.append([\n discussionEvents.commentUpdated.make({\n orgId,\n data: {\n ...next,\n mentionedOrgUserIds: next.status === 'published' ? keepAllowed(candidates, added) : [],\n },\n metadata: { actorOrgUserId: actor.id },\n }),\n ]);\n return next;\n });\n this.logger.info('comment edited', { orgId, commentId, actorId: actor.id });\n return this.renderOne(orgId, updated, actor);\n }\n\n async remove(\n orgId: string,\n commentId: string,\n actor: Actor,\n communityId?: string | null,\n ): Promise<CommentView> {\n return this.renderOne(\n orgId,\n await this.removeComment(orgId, commentId, actor, communityId),\n actor,\n );\n }\n\n private async removeComment(\n orgId: string,\n commentId: string,\n actor: Actor,\n communityId?: string | null,\n ): Promise<Comment> {\n const comment = await this.getComment(orgId, commentId);\n if (!comment || !this.inCommunity(comment, communityId)) {\n this.logger.warn('comment remove rejected — not found', {\n orgId,\n commentId,\n actorId: actor.id,\n });\n throw new NotFoundError('Comment', commentId);\n }\n\n if (comment.postId) {\n await this.postOfComment(orgId, actor, commentId, comment.postId);\n if (comment.orgUserId !== actor.id && !this.canModerate(actor)) {\n throw new ForbiddenError('You cannot remove this comment');\n }\n } else {\n if (!(await this.hasAccess(orgId, this.activityIdOf(comment), actor))) {\n throw new ForbiddenError('You cannot open this activity');\n }\n if (comment.orgUserId !== actor.id && !this.isStaff(actor)) {\n throw new ForbiddenError('You cannot remove this comment');\n }\n }\n\n if (comment.status === 'removed') {\n return comment;\n }\n\n return this.uow.run(async (scope) => {\n const updated = await scope.discussion.updateComment(orgId, commentId, {\n status: 'removed',\n removedBy: actor.id,\n });\n if (!updated) {\n // getComment() proved it existed; a null here means it vanished mid-operation.\n // Roll back rather than emit an event for a transition that didn't happen.\n throw new NotFoundError('Comment', commentId);\n }\n if (comment.orgUserId !== actor.id) {\n await scope.discussion.resolveReportsFor(orgId, commentId, {\n resolvedAt: new Date(),\n resolvedBy: actor.id,\n resolution: 'removed',\n });\n }\n await scope.outbox.append([\n discussionEvents.commentRemoved.make({\n orgId,\n data: updated,\n metadata: { actorOrgUserId: actor.id },\n }),\n ]);\n this.logger.info('comment removed', { orgId, commentId, actorId: actor.id });\n return updated;\n });\n }\n\n async restore(\n orgId: string,\n commentId: string,\n actor: Actor,\n communityId?: string | null,\n ): Promise<CommentView> {\n return this.renderOne(\n orgId,\n await this.restoreComment(orgId, commentId, actor, communityId),\n actor,\n );\n }\n\n private async restoreComment(\n orgId: string,\n commentId: string,\n actor: Actor,\n communityId?: string | null,\n ): Promise<Comment> {\n if (!this.isStaff(actor)) {\n throw new ForbiddenError('You cannot restore this comment');\n }\n const comment = await this.getComment(orgId, commentId);\n if (!comment || !this.inCommunity(comment, communityId)) {\n this.logger.warn('comment restore rejected — not found', {\n orgId,\n commentId,\n actorId: actor.id,\n });\n throw new NotFoundError('Comment', commentId);\n }\n if (comment.status !== 'removed') {\n return comment;\n }\n const updated = await this.repo.updateComment(orgId, commentId, {\n status: 'published',\n removedBy: null,\n });\n if (!updated) {\n // getComment() proved it existed; a null here means it vanished mid-write.\n // Returning the still-removed comment would report success while\n // handing back the removed row.\n throw new NotFoundError('Comment', commentId);\n }\n this.logger.info('comment restored', { orgId, commentId, actorId: actor.id });\n return updated;\n }\n\n /** Load a comment together with its activity's resolved comments config,\n * having proved the actor may reach that activity at all. Every path that\n * acts on one comment needs the pair, and none should read the row twice. */\n private async loadWithConfig(\n orgId: string,\n commentId: string,\n actor: Actor,\n ): Promise<{ comment: Comment; config: CommentsConfig; spaceId: string | null }> {\n const comment = await this.getComment(orgId, commentId);\n if (!comment) {\n throw new NotFoundError('Comment', commentId);\n }\n if (comment.postId) {\n const { post, space } = await this.postOfComment(orgId, actor, commentId, comment.postId);\n const config: CommentsConfig = {\n enabled: this.commentsOpen(post, space),\n threaded: true,\n requireReview: false,\n reactions: post.reactionsEnabled ?? space.settings.reactions,\n };\n return { comment, config, spaceId: post.spaceId };\n }\n const activityId = this.activityIdOf(comment);\n if (!(await this.hasAccess(orgId, activityId, actor))) {\n throw new ForbiddenError('You cannot open this activity');\n }\n const config = await this.resolveConfig(orgId, activityId);\n return { comment, config, spaceId: null };\n }\n\n async setReaction(\n orgId: string,\n commentId: string,\n actor: Actor,\n emoji: ReactionEmoji | null,\n ): Promise<CommentReactions> {\n const { comment, config, spaceId } = await this.loadWithConfig(orgId, commentId, actor);\n if (!config.enabled && !comment.postId) {\n throw new ForbiddenError('Discussion is not open on this activity');\n }\n if (!config.reactions && emoji !== null) {\n throw new ForbiddenError(\n comment.postId ? 'Reactions are off on this post' : 'Reactions are off on this course',\n );\n }\n if (comment.status === 'removed') {\n throw new ForbiddenError('A removed comment cannot be reacted to');\n }\n await this.uow.run(async (scope) => {\n const first = await scope.discussion.setReaction(orgId, commentId, actor.id, emoji);\n if (!first || emoji === null) {\n return;\n }\n await scope.outbox.append([\n discussionEvents.commentReactionAdded.make({\n orgId,\n data: {\n commentId,\n postId: comment.postId,\n activityId: comment.activityId,\n spaceId,\n communityId: comment.communityId,\n orgUserId: actor.id,\n reaction: emoji,\n },\n metadata: { actorOrgUserId: actor.id },\n }),\n ]);\n });\n const stored = await this.repo.reactionsOf(orgId, [commentId], actor.id);\n return stored[commentId] ?? EMPTY_REACTIONS;\n }\n\n async approve(orgId: string, commentId: string, actor: Actor): Promise<Comment> {\n if (!this.isStaff(actor)) {\n throw new ForbiddenError('You cannot approve this comment');\n }\n const comment = await this.getComment(orgId, commentId);\n if (!comment) {\n this.logger.warn('comment approve rejected — not found', {\n orgId,\n commentId,\n actorId: actor.id,\n });\n throw new NotFoundError('Comment', commentId);\n }\n if (comment.status !== 'pending') {\n throw new ForbiddenError('Only a comment awaiting review can be approved');\n }\n return this.uow.run(async (scope) => {\n const updated = await scope.discussion.updateComment(orgId, commentId, {\n status: 'published',\n });\n if (!updated) {\n // getComment() proved it existed; a null here means it vanished mid-operation.\n // Roll back rather than emit an event for a transition that didn't happen.\n throw new NotFoundError('Comment', commentId);\n }\n const mentioned = await this.openersOf(\n orgId,\n updated,\n await scope.mentions.ofItem(orgId, 'comment', commentId),\n );\n await scope.outbox.append([\n discussionEvents.commentPublished.make({\n orgId,\n data: { ...updated, mentionedOrgUserIds: mentioned },\n metadata: { actorOrgUserId: actor.id },\n }),\n ]);\n this.logger.info('comment published', { orgId, commentId, actorId: actor.id });\n return updated;\n });\n }\n\n /** Loads the comment and dispatches to whichever transition applies — the\n * caller must not decide that itself. `approve` and `restore` still enforce\n * their own staff check; the check here covers the third path, where a\n * comment is already published and neither of them would run. */\n async publish(\n orgId: string,\n commentId: string,\n actor: Actor,\n communityId?: string | null,\n ): Promise<Comment> {\n if (!this.isStaff(actor)) {\n throw new ForbiddenError('You cannot publish this comment');\n }\n const comment = await this.getComment(orgId, commentId);\n if (!comment || !this.inCommunity(comment, communityId)) {\n this.logger.warn('comment publish rejected — not found', {\n orgId,\n commentId,\n actorId: actor.id,\n });\n throw new NotFoundError('Comment', commentId);\n }\n if (comment.status === 'pending') {\n return this.approve(orgId, commentId, actor);\n }\n if (comment.status === 'removed') {\n return this.restoreComment(orgId, commentId, actor);\n }\n return comment;\n }\n\n async resolveReports(\n orgId: string,\n commentId: string,\n actor: Actor,\n communityId?: string | null,\n ): Promise<void> {\n const comment = await this.getComment(orgId, commentId);\n if (!comment || !this.inCommunity(comment, communityId)) {\n this.logger.warn('comment reports resolve rejected — not found', {\n orgId,\n commentId,\n actorId: actor.id,\n });\n throw new NotFoundError('Comment', commentId);\n }\n if (comment.postId) {\n await this.postOfComment(orgId, actor, commentId, comment.postId);\n if (!this.canModerate(actor)) {\n throw new ForbiddenError('You cannot work this queue');\n }\n } else if (!this.isStaff(actor)) {\n throw new ForbiddenError('You cannot resolve this report');\n }\n await this.repo.resolveReportsFor(orgId, commentId, {\n resolvedAt: new Date(),\n resolvedBy: actor.id,\n resolution: 'dismissed',\n });\n this.logger.info('comment reports resolved', { orgId, commentId, actorId: actor.id });\n }\n\n async reportComment(\n orgId: string,\n commentId: string,\n actor: Actor,\n reason: string,\n ): Promise<CommentReport> {\n const { comment, config } = await this.loadWithConfig(orgId, commentId, actor);\n // Locked accepts reports — a locked activity can still hold something a\n // moderator needs to see. Hidden does not: nothing in it is being served,\n // so nobody is looking at a comment to flag.\n if (!config.enabled && !comment.postId) {\n throw new ForbiddenError('Discussion is not open on this activity');\n }\n const now = new Date();\n const report: CommentReport = {\n id: genId('commentReport'),\n orgId,\n commentId,\n orgUserId: actor.id,\n reason,\n resolvedAt: null,\n resolution: null,\n resolvedBy: null,\n createdAt: now,\n updatedAt: now,\n };\n return this.uow.run(async (scope) => {\n const saved = await scope.discussion.insertReport(orgId, report);\n if (!saved) {\n // Already reported by this person. Return the report that actually\n // exists — a fabricated id resolves to nothing — and emit no second\n // event, which would double-count any threshold automation.\n const open = await scope.discussion.listOpenReports(orgId, [commentId]);\n const mine = open.find((r) => r.orgUserId === actor.id);\n if (mine) {\n return mine;\n }\n return report;\n }\n await scope.outbox.append([\n discussionEvents.commentReported.make({\n orgId,\n data: { ...saved, communityId: comment.communityId },\n metadata: { actorOrgUserId: actor.id },\n }),\n ]);\n this.logger.info('comment reported', { orgId, commentId, actorId: actor.id });\n return saved;\n });\n }\n\n async listComments(orgId: string, query: ListCommentsQuery): Promise<Page<CommentListItem>> {\n const page = await this.repo.listComments(orgId, query);\n const rows = page.rows.map((r) => r.comment);\n if (rows.length === 0) {\n return { ...page, rows: [] };\n }\n const reports = await this.repo.listOpenReports(\n orgId,\n rows.map((c) => c.id),\n );\n const authors = await this.repo.authorsOf(orgId, [\n ...new Set([...this.peopleIn(rows), ...reports.map((r) => r.orgUserId)]),\n ]);\n const urls = this.fileUrls;\n\n const byComment = new Map<string, CommentReport[]>();\n for (const report of reports) {\n byComment.set(report.commentId, [...(byComment.get(report.commentId) ?? []), report]);\n }\n\n return {\n ...page,\n rows: page.rows.map(({ comment, courseId, activityTitle }) => {\n const author = authors[comment.orgUserId];\n if (!author) {\n throw new NotFoundError('Member', comment.orgUserId);\n }\n return {\n id: comment.id,\n parentId: comment.parentId,\n activityId: this.activityIdOf(comment),\n activityTitle,\n courseId,\n body: comment.body,\n status: comment.status,\n author: authorView(orgId, author, urls),\n authorEmail: author.email,\n removedBy: comment.removedBy,\n reports: (byComment.get(comment.id) ?? []).map((r) => {\n const reporter = authors[r.orgUserId];\n if (!reporter) {\n throw new NotFoundError('Member', r.orgUserId);\n }\n return {\n reporter: authorView(orgId, reporter, urls),\n reason: r.reason,\n createdAt: r.createdAt,\n };\n }),\n createdAt: comment.createdAt,\n updatedAt: comment.updatedAt,\n };\n }),\n };\n }\n\n getComment(orgId: string, commentId: string): Promise<Comment | null> {\n return this.repo.findComment(orgId, commentId);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"service.js","names":[],"sources":["../../src/discussion/service.ts"],"sourcesContent":["import { genId } from '../shared/id.js';\nimport { ConflictError, ForbiddenError, NotFoundError } from '../shared/errors.js';\nimport type { Logger } from '../shared/ports.js';\nimport { noopLogger } from '../shared/logger.js';\nimport type { EffectivePermissions } from '../shared/actor.js';\nimport type { Comment, CommentReport, CommentSettings } from './model.js';\nimport type {\n Activity,\n ActivitySettings,\n CourseSettings,\n MentionView,\n Post,\n SpaceDetail,\n} from '../types/index.js';\nimport type {\n CommentListItem,\n CommentsConfig,\n CommentView,\n ListCommentsQuery,\n Page,\n ReactionEmoji,\n} from './types.js';\nimport { SettingsNamespace, type SettingsService } from '../shared/settings.js';\nimport type {\n ActivityComments,\n ActivityGetter,\n Actor,\n AuthorPermissionReader,\n AuthorRecord,\n CommentReactions,\n CommentWithin,\n CommunityAccess,\n ContentAccessReader,\n DiscussionRepository,\n DiscussionService,\n DiscussionUnitOfWork,\n MentionsRepository,\n PageQuery,\n PostCommentInput,\n PostService,\n} from './ports.js';\nimport { discussionEvents } from './events.js';\nimport { authorView } from './authors.js';\nimport { keepAllowed, mentionTokens, mentionViews } from './mentions.js';\nimport { skipsReview, teamBadge } from './rules.js';\nimport type { FileUrls } from '../assets/index.js';\n\n/** A course with no stored settings. Discussion is opt-in, so the common case\n * persists no row at all and every existing course stays silent. */\nexport const DEFAULT_SETTINGS = {\n enabled: false,\n threaded: true,\n requireReview: false,\n reactions: true,\n} as const satisfies CommentSettings;\n\nconst EMPTY_REACTIONS: CommentReactions = { reactions: {} };\n\ntype Where =\n | { kind: 'course'; courseId: string; activity: Activity }\n | { kind: 'post'; communityId: string; post: Post; space: SpaceDetail };\n\nexport type DiscussionServiceParams = {\n repo: DiscussionRepository;\n access: ContentAccessReader;\n content: ActivityGetter;\n posts: Pick<PostService, 'visible' | 'find'>;\n spaces: Pick<CommunityAccess, 'spaceOpenersAmong'>;\n authors: AuthorPermissionReader;\n mentions: MentionsRepository;\n uow: DiscussionUnitOfWork;\n settings: SettingsService;\n fileUrls: FileUrls;\n logger?: Logger;\n};\n\nexport class DiscussionServiceImpl implements DiscussionService {\n private readonly repo: DiscussionRepository;\n private readonly access: ContentAccessReader;\n private readonly content: ActivityGetter;\n private readonly posts: Pick<PostService, 'visible' | 'find'>;\n private readonly spaces: Pick<CommunityAccess, 'spaceOpenersAmong'>;\n private readonly authors: AuthorPermissionReader;\n private readonly mentions: MentionsRepository;\n private readonly uow: DiscussionUnitOfWork;\n private readonly settings: SettingsService;\n private readonly fileUrls: FileUrls;\n private readonly logger: Logger;\n\n constructor(params: DiscussionServiceParams) {\n this.repo = params.repo;\n this.access = params.access;\n this.content = params.content;\n this.posts = params.posts;\n this.spaces = params.spaces;\n this.authors = params.authors;\n this.mentions = params.mentions;\n this.uow = params.uow;\n this.settings = params.settings;\n this.fileUrls = params.fileUrls;\n this.logger = params.logger ?? noopLogger;\n }\n\n private async configOf(orgId: string, activity: Activity): Promise<CommentsConfig> {\n // The `content` namespace row is the whole CourseSettings, owned and written\n // by content; this context only reads its `comments` key.\n const stored = await this.settings.get<Partial<CourseSettings>>(\n orgId,\n SettingsNamespace.content,\n activity.courseId,\n );\n const settings = { ...DEFAULT_SETTINGS, ...stored?.comments };\n // Comments off for the course cannot be overridden back on by an activity:\n // the course switch is the master.\n const rule = ((activity.settings ?? {}) as ActivitySettings).comments ?? 'inherit';\n return {\n enabled: settings.enabled && rule !== 'never',\n threaded: settings.threaded,\n requireReview: settings.requireReview,\n reactions: settings.reactions,\n };\n }\n\n private async badgesOf(\n orgId: string,\n orgUserIds: string[],\n ): Promise<Map<string, EffectivePermissions>> {\n return this.authors.permissionsOfMany(orgId, [...new Set(orgUserIds)]);\n }\n\n private toView(\n orgId: string,\n comment: Comment,\n itemId: string,\n authors: Record<string, AuthorRecord>,\n permissions: Map<string, EffectivePermissions>,\n reactions: CommentReactions,\n mentions: MentionView[],\n ): CommentView {\n const author = authors[comment.orgUserId];\n if (!author) {\n throw new NotFoundError('Member', comment.orgUserId);\n }\n if (comment.removedBy && !authors[comment.removedBy]) {\n throw new NotFoundError('Member', comment.removedBy);\n }\n const remover = comment.removedBy ? authors[comment.removedBy] : undefined;\n return {\n id: comment.id,\n activityId: comment.activityId,\n postId: comment.postId,\n communityId: comment.communityId,\n parentId: comment.parentId,\n author: authorView(\n orgId,\n author,\n this.fileUrls,\n teamBadge(permissions.get(author.id), itemId),\n ),\n body: comment.status === 'removed' ? null : comment.body,\n status: comment.status,\n removedBy: remover\n ? authorView(orgId, remover, this.fileUrls, teamBadge(permissions.get(remover.id), itemId))\n : null,\n reactions: reactions.reactions,\n viewerReaction: reactions.viewerReaction,\n mentions: comment.status === 'removed' ? [] : mentions,\n createdAt: comment.createdAt,\n updatedAt: comment.updatedAt,\n };\n }\n\n private peopleIn(comments: Comment[]): string[] {\n const ids = new Set<string>();\n for (const c of comments) {\n ids.add(c.orgUserId);\n if (c.removedBy) {\n ids.add(c.removedBy);\n }\n }\n return [...ids];\n }\n\n private itemOf(where: Where): string {\n return where.kind === 'course' ? where.courseId : where.communityId;\n }\n\n private async renderOne(comment: Comment, where: Where, actor: Actor): Promise<CommentView> {\n const orgId = actor.orgId;\n const people = this.peopleIn([comment]);\n const [authors, permissions, mentioned] = await Promise.all([\n this.repo.authorsOf(orgId, people),\n this.badgesOf(orgId, people),\n mentionViews(this.mentions, this.repo, orgId, 'comment', [comment.id]),\n ]);\n const stored =\n comment.status === 'removed'\n ? EMPTY_REACTIONS\n : ((await this.repo.reactionsOf(orgId, [comment.id], actor.id))[comment.id] ??\n EMPTY_REACTIONS);\n return this.toView(\n orgId,\n comment,\n this.itemOf(where),\n authors,\n permissions,\n stored,\n mentioned[comment.id] ?? [],\n );\n }\n\n private activityIdOf(comment: Comment): string {\n if (comment.activityId === null) {\n throw new NotFoundError('Activity', comment.id);\n }\n return comment.activityId;\n }\n\n private system(orgId: string): Actor {\n return this.authors.systemActor(orgId);\n }\n\n private async activityOf(orgId: string, activityId: string): Promise<Activity | null> {\n return this.content.getActivity(activityId, this.system(orgId));\n }\n\n private async opensCourse(actor: Actor, courseId: string): Promise<boolean> {\n return (\n actor.permissions.has('courses:read', courseId) ||\n this.access.hasAccess(actor.orgId, actor.id, courseId)\n );\n }\n\n private async requireActivityOpen(activityId: string, actor: Actor): Promise<Activity> {\n const activity = await this.activityOf(actor.orgId, activityId);\n if (!activity) {\n throw new NotFoundError('Activity', activityId);\n }\n if (!(await this.opensCourse(actor, activity.courseId))) {\n this.logger.warn('activity refused — no access', {\n orgId: actor.orgId,\n activityId,\n actorId: actor.id,\n });\n throw new ForbiddenError('You cannot open this activity');\n }\n return activity;\n }\n\n private async courseOpeners(\n orgId: string,\n courseId: string,\n orgUserIds: string[],\n ): Promise<string[]> {\n if (orgUserIds.length === 0) {\n return [];\n }\n const [people, permissions] = await Promise.all([\n this.repo.authorsOf(orgId, orgUserIds),\n this.badgesOf(orgId, orgUserIds),\n ]);\n const opens = await Promise.all(\n orgUserIds.map(async (orgUserId) => {\n if (!people[orgUserId]) {\n return false;\n }\n return (\n (permissions.get(orgUserId)?.has('courses:read', courseId) ?? false) ||\n this.access.hasAccess(orgId, orgUserId, courseId)\n );\n }),\n );\n return orgUserIds.filter((_, index) => opens[index]);\n }\n\n private async spaceOpeners(orgId: string, spaceId: string, orgUserIds: string[]) {\n if (orgUserIds.length === 0) {\n return [];\n }\n return keepAllowed(orgUserIds, await this.spaces.spaceOpenersAmong(orgId, spaceId, orgUserIds));\n }\n\n private async openersOf(orgId: string, comment: Comment, orgUserIds: string[]) {\n if (orgUserIds.length === 0) {\n return [];\n }\n if (comment.postId) {\n const post = await this.posts.find(orgId, comment.postId);\n return post ? this.spaceOpeners(orgId, post.spaceId, orgUserIds) : [];\n }\n const activity = await this.activityOf(orgId, this.activityIdOf(comment));\n return activity ? this.courseOpeners(orgId, activity.courseId, orgUserIds) : [];\n }\n\n commenterIds(\n orgId: string,\n target: { postId: string } | { activityId: string },\n ): Promise<string[]> {\n return this.repo.commenterIds(orgId, target);\n }\n\n async postComment(input: PostCommentInput, actor: Actor): Promise<CommentView> {\n if (input.postId) {\n return this.postOnPost(input.postId, input, actor);\n }\n const activityId = input.activityId;\n if (!activityId) {\n throw new NotFoundError('Activity', activityId ?? '');\n }\n return this.postOnActivity(activityId, input, actor);\n }\n\n async replyToComment(commentId: string, body: string, actor: Actor): Promise<CommentView> {\n const parent = await this.getComment(actor.orgId, commentId);\n if (!parent || parent.activityId === null) {\n throw new NotFoundError('Comment', commentId);\n }\n const activity = await this.activityOf(actor.orgId, parent.activityId);\n if (!activity) {\n throw new NotFoundError('Comment', commentId);\n }\n this.requireModerates(actor, { kind: 'course', courseId: activity.courseId, activity });\n return this.postOnActivity(\n parent.activityId,\n { activityId: parent.activityId, parentId: parent.parentId ?? parent.id, body },\n actor,\n );\n }\n\n private async postOnActivity(\n activityId: string,\n { parentId, body }: PostCommentInput,\n actor: Actor,\n ): Promise<CommentView> {\n const orgId = actor.orgId;\n const activity = await this.requireActivityOpen(activityId, actor);\n const config = await this.configOf(orgId, activity);\n\n if (!config.enabled) {\n throw new ForbiddenError('Discussion is not open on this activity');\n }\n\n if (parentId !== null) {\n if (!config.threaded) {\n throw new ForbiddenError('Replies are off on this course');\n }\n const parent = await this.repo.findComment(orgId, parentId);\n if (!parent || parent.activityId !== activityId) {\n throw new NotFoundError('Comment', parentId);\n }\n\n if (parent.parentId !== null) {\n throw new ForbiddenError('A reply cannot have replies of its own');\n }\n\n if (parent.status !== 'published') {\n throw new ForbiddenError('You can reply only to a published comment');\n }\n }\n\n const status: Comment['status'] =\n config.requireReview && !skipsReview(actor.permissions, { courseId: activity.courseId })\n ? 'pending'\n : 'published';\n\n const comment: Comment = {\n id: genId('comment'),\n orgId,\n activityId,\n postId: null,\n communityId: null,\n parentId,\n orgUserId: actor.id,\n body,\n status,\n removedBy: null,\n createdAt: new Date(),\n updatedAt: new Date(),\n };\n const mentioned = await this.courseOpeners(orgId, activity.courseId, mentionTokens(body));\n const saved = await this.uow.run(async (scope) => {\n const row = await scope.discussion.insertComment(orgId, comment);\n await scope.mentions.sync(orgId, 'comment', row.id, mentioned, comment.createdAt);\n await scope.outbox.append([\n discussionEvents.commentCreated.make({\n orgId,\n data: { ...row, mentionedOrgUserIds: status === 'published' ? mentioned : [] },\n metadata: { actorId: actor.id },\n }),\n ]);\n this.logger.info('comment created', { orgId, commentId: row.id, status, actorId: actor.id });\n return row;\n });\n return this.renderOne(saved, { kind: 'course', courseId: activity.courseId, activity }, actor);\n }\n\n private async postOnPost(\n postId: string,\n { parentId, body }: PostCommentInput,\n actor: Actor,\n ): Promise<CommentView> {\n const orgId = actor.orgId;\n const { post, space } = await this.posts.visible(postId, actor);\n if (post.status !== 'published') {\n throw new ConflictError('This post is not published');\n }\n if (!this.commentsOpen(post, space)) {\n throw new ForbiddenError('Comments are closed on this post');\n }\n if (parentId !== null) {\n const parent = await this.repo.findComment(orgId, parentId);\n if (!parent || parent.postId !== postId) {\n throw new NotFoundError('Comment', parentId);\n }\n if (parent.parentId !== null) {\n throw new ForbiddenError('A reply cannot have replies of its own');\n }\n if (parent.status !== 'published') {\n throw new ForbiddenError('You can reply only to a published comment');\n }\n }\n const now = new Date();\n const comment: Comment = {\n id: genId('comment'),\n orgId,\n activityId: null,\n postId,\n communityId: post.communityId,\n parentId,\n orgUserId: actor.id,\n body,\n status: 'published',\n removedBy: null,\n createdAt: now,\n updatedAt: now,\n };\n const mentioned = await this.spaceOpeners(orgId, post.spaceId, mentionTokens(body));\n const saved = await this.uow.run(async (scope) => {\n const row = await scope.discussion.insertComment(orgId, comment);\n await scope.mentions.sync(orgId, 'comment', row.id, mentioned, now);\n await scope.outbox.append([\n discussionEvents.commentCreated.make({\n orgId,\n data: { ...row, mentionedOrgUserIds: mentioned },\n metadata: { actorId: actor.id },\n }),\n ]);\n this.logger.info('comment created', {\n orgId,\n commentId: row.id,\n postId,\n status: 'published',\n actorId: actor.id,\n });\n return row;\n });\n return this.renderOne(\n saved,\n { kind: 'post', communityId: post.communityId, post, space },\n actor,\n );\n }\n\n async postComments(postId: string, query: PageQuery, actor: Actor): Promise<Page<CommentView>> {\n const orgId = actor.orgId;\n const { post, space } = await this.posts.visible(postId, actor);\n const seesHeld = actor.permissions.has('communities:moderate', post.communityId);\n const page = await this.repo.listThreadsByPost(orgId, postId, {\n ...query,\n viewerId: actor.id,\n seesHeld,\n });\n const comments = await this.thread(\n orgId,\n page.rows,\n actor,\n post.communityId,\n post.reactionsEnabled ?? space.settings.reactions,\n seesHeld,\n );\n this.logger.debug('post comments read', { orgId, postId, count: comments.length });\n return { ...page, rows: comments };\n }\n\n private async thread(\n orgId: string,\n rows: Comment[],\n actor: Actor,\n itemId: string,\n reactionsOn: boolean,\n seesHeld: boolean,\n ): Promise<CommentView[]> {\n const readable = rows.filter((comment) => {\n if (comment.status === 'published' || comment.status === 'removed') {\n return true;\n }\n return seesHeld || comment.orgUserId === actor.id;\n });\n const replies = readable.filter(\n (comment) => comment.parentId !== null && comment.status !== 'removed',\n );\n const heldOpen = new Set(replies.map((reply) => reply.parentId));\n const roots = readable.filter(\n (comment) =>\n comment.parentId === null && (comment.status !== 'removed' || heldOpen.has(comment.id)),\n );\n const rootIds = new Set(roots.map((root) => root.id));\n const served = [...roots, ...replies.filter((reply) => rootIds.has(reply.parentId!))].sort(\n (a, b) => a.createdAt.getTime() - b.createdAt.getTime(),\n );\n const people = this.peopleIn(served);\n const [authors, permissions, reactions, mentioned] = await Promise.all([\n this.repo.authorsOf(orgId, people),\n this.badgesOf(orgId, people),\n reactionsOn\n ? this.repo.reactionsOf(\n orgId,\n served.map((comment) => comment.id),\n actor.id,\n )\n : Promise.resolve<Record<string, CommentReactions>>({}),\n mentionViews(\n this.mentions,\n this.repo,\n orgId,\n 'comment',\n served.map((comment) => comment.id),\n ),\n ]);\n return served.map((comment) =>\n this.toView(\n orgId,\n comment,\n itemId,\n authors,\n permissions,\n reactions[comment.id] ?? EMPTY_REACTIONS,\n mentioned[comment.id] ?? [],\n ),\n );\n }\n\n private async postOfComment(\n actor: Actor,\n commentId: string,\n postId: string,\n ): Promise<{ post: Post; space: SpaceDetail }> {\n try {\n return await this.posts.visible(postId, actor);\n } catch (error) {\n if (error instanceof NotFoundError) {\n throw new NotFoundError('Comment', commentId);\n }\n throw error;\n }\n }\n\n private isWithin(comment: Comment, within: CommentWithin): boolean {\n if (within === 'any') {\n return true;\n }\n if (within === 'lesson') {\n return comment.activityId !== null;\n }\n return comment.communityId === within.communityId;\n }\n\n private commentsOpen(post: Post, space: SpaceDetail): boolean {\n return post.commentsClosed === null ? space.settings.comments : !post.commentsClosed;\n }\n\n /** Where the comment sits, having proved the actor may reach it: the post\n * is visible to them, or they open the course. */\n private async whereOf(comment: Comment, actor: Actor): Promise<Where> {\n if (comment.postId) {\n const { post, space } = await this.postOfComment(actor, comment.id, comment.postId);\n return { kind: 'post', communityId: post.communityId, post, space };\n }\n const activity = await this.activityOf(actor.orgId, this.activityIdOf(comment));\n if (!activity) {\n throw new NotFoundError('Comment', comment.id);\n }\n if (!(await this.opensCourse(actor, activity.courseId))) {\n this.logger.warn('comment refused — no access', {\n orgId: actor.orgId,\n commentId: comment.id,\n actorId: actor.id,\n });\n throw new ForbiddenError('You cannot open this activity');\n }\n return { kind: 'course', courseId: activity.courseId, activity };\n }\n\n private moderates(actor: Actor, where: Where): boolean {\n return where.kind === 'course'\n ? actor.permissions.has('courses:moderate', where.courseId)\n : actor.permissions.has('communities:moderate', where.communityId);\n }\n\n private requireModerates(actor: Actor, where: Where): void {\n actor.permissions.require(\n where.kind === 'course' ? 'courses:moderate' : 'communities:moderate',\n this.itemOf(where),\n );\n }\n\n private async findIn(\n commentId: string,\n within: CommentWithin,\n actor: Actor,\n action: string,\n ): Promise<Comment> {\n const comment = await this.getComment(actor.orgId, commentId);\n if (!comment || !this.isWithin(comment, within)) {\n this.logger.warn(`comment ${action} rejected — not found`, {\n orgId: actor.orgId,\n commentId,\n actorId: actor.id,\n });\n throw new NotFoundError('Comment', commentId);\n }\n return comment;\n }\n\n async activityComments(activityId: string, actor: Actor): Promise<ActivityComments> {\n const activity = await this.requireActivityOpen(activityId, actor);\n const config = await this.configOf(actor.orgId, activity);\n if (!config.enabled) {\n return { config, comments: [] };\n }\n const rows = await this.repo.listByActivity(actor.orgId, activityId);\n return {\n config,\n comments: await this.thread(\n actor.orgId,\n rows,\n actor,\n activity.courseId,\n config.reactions,\n actor.permissions.has('courses:moderate', activity.courseId),\n ),\n };\n }\n\n async edit(commentId: string, body: string, actor: Actor): Promise<CommentView> {\n const orgId = actor.orgId;\n const { comment, config, where } = await this.loadWithConfig(commentId, actor);\n if (!config.enabled) {\n throw new ForbiddenError(\n comment.postId\n ? 'Comments are closed on this post'\n : 'Discussion is not open on this activity',\n );\n }\n // Editing is the author's alone — moderators remove, they do not rewrite.\n if (comment.orgUserId !== actor.id) {\n throw new ForbiddenError('You can edit only your own comment');\n }\n if (comment.status === 'removed') {\n throw new ForbiddenError('A removed comment cannot be edited');\n }\n const candidates = await this.openersOf(orgId, comment, mentionTokens(body));\n const updated = await this.uow.run(async (scope) => {\n const next = await scope.discussion.updateComment(orgId, commentId, {\n body,\n });\n if (!next) {\n // getComment() proved it existed; a null here means it vanished mid-write.\n // Returning the pre-edit comment would report success and hand back\n // the old body.\n throw new NotFoundError('Comment', commentId);\n }\n const added = await scope.mentions.sync(orgId, 'comment', commentId, candidates, new Date());\n await scope.outbox.append([\n discussionEvents.commentUpdated.make({\n orgId,\n data: {\n ...next,\n mentionedOrgUserIds: next.status === 'published' ? keepAllowed(candidates, added) : [],\n },\n metadata: { actorId: actor.id },\n }),\n ]);\n return next;\n });\n this.logger.info('comment edited', { orgId, commentId, actorId: actor.id });\n return this.renderOne(updated, where, actor);\n }\n\n async remove(commentId: string, within: CommentWithin, actor: Actor): Promise<CommentView> {\n const orgId = actor.orgId;\n const comment = await this.findIn(commentId, within, actor, 'remove');\n const where = await this.whereOf(comment, actor);\n if (comment.orgUserId !== actor.id && !this.moderates(actor, where)) {\n this.logger.warn('comment remove refused — not the author or a moderator', {\n orgId,\n commentId,\n actorId: actor.id,\n });\n throw new ForbiddenError('You cannot remove this comment');\n }\n\n if (comment.status === 'removed') {\n return this.renderOne(comment, where, actor);\n }\n\n const removed = await this.uow.run(async (scope) => {\n const updated = await scope.discussion.updateComment(orgId, commentId, {\n status: 'removed',\n removedBy: actor.id,\n });\n if (!updated) {\n // getComment() proved it existed; a null here means it vanished mid-operation.\n // Roll back rather than emit an event for a transition that didn't happen.\n throw new NotFoundError('Comment', commentId);\n }\n if (comment.orgUserId !== actor.id) {\n await scope.discussion.resolveReportsFor(orgId, commentId, {\n resolvedAt: new Date(),\n resolvedBy: actor.id,\n resolution: 'removed',\n });\n }\n await scope.outbox.append([\n discussionEvents.commentRemoved.make({\n orgId,\n data: updated,\n metadata: { actorId: actor.id },\n }),\n ]);\n this.logger.info('comment removed', { orgId, commentId, actorId: actor.id });\n return updated;\n });\n return this.renderOne(removed, where, actor);\n }\n\n async restore(commentId: string, within: CommentWithin, actor: Actor): Promise<CommentView> {\n const comment = await this.findIn(commentId, within, actor, 'restore');\n const where = await this.whereOf(comment, actor);\n this.requireModerates(actor, where);\n return this.renderOne(await this.restoreComment(comment, actor), where, actor);\n }\n\n private async restoreComment(comment: Comment, actor: Actor): Promise<Comment> {\n if (comment.status !== 'removed') {\n return comment;\n }\n const updated = await this.repo.updateComment(actor.orgId, comment.id, {\n status: 'published',\n removedBy: null,\n });\n if (!updated) {\n // getComment() proved it existed; a null here means it vanished mid-write.\n // Returning the still-removed comment would report success while\n // handing back the removed row.\n throw new NotFoundError('Comment', comment.id);\n }\n this.logger.info('comment restored', {\n orgId: actor.orgId,\n commentId: comment.id,\n actorId: actor.id,\n });\n return updated;\n }\n\n /** Load a comment together with where it sits and the comments config that\n * applies there, having proved the actor may reach it at all. Every path\n * that acts on one comment needs them, and none should read the row twice. */\n private async loadWithConfig(\n commentId: string,\n actor: Actor,\n ): Promise<{ comment: Comment; config: CommentsConfig; where: Where }> {\n const comment = await this.getComment(actor.orgId, commentId);\n if (!comment) {\n throw new NotFoundError('Comment', commentId);\n }\n const where = await this.whereOf(comment, actor);\n if (where.kind === 'post') {\n const config: CommentsConfig = {\n enabled: this.commentsOpen(where.post, where.space),\n threaded: true,\n requireReview: false,\n reactions: where.post.reactionsEnabled ?? where.space.settings.reactions,\n };\n return { comment, config, where };\n }\n return { comment, config: await this.configOf(actor.orgId, where.activity), where };\n }\n\n async setReaction(\n commentId: string,\n emoji: ReactionEmoji | null,\n actor: Actor,\n ): Promise<CommentReactions> {\n const orgId = actor.orgId;\n const { comment, config, where } = await this.loadWithConfig(commentId, actor);\n if (!config.enabled && !comment.postId) {\n throw new ForbiddenError('Discussion is not open on this activity');\n }\n if (!config.reactions && emoji !== null) {\n throw new ForbiddenError(\n comment.postId ? 'Reactions are off on this post' : 'Reactions are off on this course',\n );\n }\n if (comment.status === 'removed') {\n throw new ForbiddenError('A removed comment cannot be reacted to');\n }\n await this.uow.run(async (scope) => {\n const first = await scope.discussion.setReaction(orgId, commentId, actor.id, emoji);\n if (!first || emoji === null) {\n return;\n }\n await scope.outbox.append([\n discussionEvents.commentReactionAdded.make({\n orgId,\n data: {\n commentId,\n postId: comment.postId,\n activityId: comment.activityId,\n spaceId: where.kind === 'post' ? where.post.spaceId : null,\n communityId: comment.communityId,\n orgUserId: actor.id,\n reaction: emoji,\n },\n metadata: { actorId: actor.id },\n }),\n ]);\n });\n const stored = await this.repo.reactionsOf(orgId, [commentId], actor.id);\n return stored[commentId] ?? EMPTY_REACTIONS;\n }\n\n async approve(commentId: string, actor: Actor): Promise<Comment> {\n const comment = await this.findIn(commentId, 'any', actor, 'approve');\n const where = await this.whereOf(comment, actor);\n this.requireModerates(actor, where);\n return this.approveComment(comment, actor);\n }\n\n private async approveComment(comment: Comment, actor: Actor): Promise<Comment> {\n const orgId = actor.orgId;\n const commentId = comment.id;\n if (comment.status !== 'pending') {\n throw new ForbiddenError('Only a comment awaiting review can be approved');\n }\n const mentioned = await this.openersOf(\n orgId,\n comment,\n await this.mentions.ofItem(orgId, 'comment', commentId),\n );\n return this.uow.run(async (scope) => {\n const updated = await scope.discussion.updateComment(orgId, commentId, {\n status: 'published',\n });\n if (!updated) {\n // getComment() proved it existed; a null here means it vanished mid-operation.\n // Roll back rather than emit an event for a transition that didn't happen.\n throw new NotFoundError('Comment', commentId);\n }\n await scope.outbox.append([\n discussionEvents.commentPublished.make({\n orgId,\n data: { ...updated, mentionedOrgUserIds: mentioned },\n metadata: { actorId: actor.id },\n }),\n ]);\n this.logger.info('comment published', { orgId, commentId, actorId: actor.id });\n return updated;\n });\n }\n\n /** Loads the comment and dispatches to whichever transition applies — the\n * caller must not decide that itself. */\n async publish(commentId: string, within: CommentWithin, actor: Actor): Promise<Comment> {\n const comment = await this.findIn(commentId, within, actor, 'publish');\n const where = await this.whereOf(comment, actor);\n this.requireModerates(actor, where);\n if (comment.status === 'pending') {\n return this.approveComment(comment, actor);\n }\n if (comment.status === 'removed') {\n return this.restoreComment(comment, actor);\n }\n return comment;\n }\n\n async resolveReports(commentId: string, within: CommentWithin, actor: Actor): Promise<void> {\n const comment = await this.findIn(commentId, within, actor, 'reports resolve');\n const where = await this.whereOf(comment, actor);\n this.requireModerates(actor, where);\n await this.repo.resolveReportsFor(actor.orgId, commentId, {\n resolvedAt: new Date(),\n resolvedBy: actor.id,\n resolution: 'dismissed',\n });\n this.logger.info('comment reports resolved', {\n orgId: actor.orgId,\n commentId,\n actorId: actor.id,\n });\n }\n\n async reportComment(commentId: string, reason: string, actor: Actor): Promise<CommentReport> {\n const orgId = actor.orgId;\n const { comment, config } = await this.loadWithConfig(commentId, actor);\n // Locked accepts reports — a locked activity can still hold something a\n // moderator needs to see. Hidden does not: nothing in it is being served,\n // so nobody is looking at a comment to flag.\n if (!config.enabled && !comment.postId) {\n throw new ForbiddenError('Discussion is not open on this activity');\n }\n const now = new Date();\n const report: CommentReport = {\n id: genId('commentReport'),\n orgId,\n commentId,\n orgUserId: actor.id,\n reason,\n resolvedAt: null,\n resolution: null,\n resolvedBy: null,\n createdAt: now,\n updatedAt: now,\n };\n return this.uow.run(async (scope) => {\n const saved = await scope.discussion.insertReport(orgId, report);\n if (!saved) {\n // Already reported by this person. Return the report that actually\n // exists — a fabricated id resolves to nothing — and emit no second\n // event, which would double-count any threshold automation.\n const open = await scope.discussion.listOpenReports(orgId, [commentId]);\n const mine = open.find((r) => r.orgUserId === actor.id);\n if (mine) {\n return mine;\n }\n return report;\n }\n await scope.outbox.append([\n discussionEvents.commentReported.make({\n orgId,\n data: { ...saved, communityId: comment.communityId },\n metadata: { actorId: actor.id },\n }),\n ]);\n this.logger.info('comment reported', { orgId, commentId, actorId: actor.id });\n return saved;\n });\n }\n\n async listComments(query: ListCommentsQuery, actor: Actor): Promise<Page<CommentListItem>> {\n const orgId = actor.orgId;\n const where = actor.permissions.where('courses:moderate');\n if (!where.org && where.content.length === 0) {\n this.logger.warn('comment list refused — moderates no course', { orgId, actorId: actor.id });\n throw new ForbiddenError('You need courses:moderate');\n }\n const page = await this.repo.listComments(orgId, query, where.org ? undefined : where.content);\n const rows = page.rows.map((r) => r.comment);\n if (rows.length === 0) {\n return { ...page, rows: [] };\n }\n const reports = await this.repo.listOpenReports(\n orgId,\n rows.map((c) => c.id),\n );\n const people = [...new Set([...this.peopleIn(rows), ...reports.map((r) => r.orgUserId)])];\n const [authors, permissions] = await Promise.all([\n this.repo.authorsOf(orgId, people),\n this.badgesOf(orgId, people),\n ]);\n const urls = this.fileUrls;\n\n const byComment = new Map<string, CommentReport[]>();\n for (const report of reports) {\n byComment.set(report.commentId, [...(byComment.get(report.commentId) ?? []), report]);\n }\n\n return {\n ...page,\n rows: page.rows.map(({ comment, courseId, activityTitle }) => {\n const author = authors[comment.orgUserId];\n if (!author) {\n throw new NotFoundError('Member', comment.orgUserId);\n }\n return {\n id: comment.id,\n parentId: comment.parentId,\n activityId: this.activityIdOf(comment),\n activityTitle,\n courseId,\n body: comment.body,\n status: comment.status,\n author: authorView(orgId, author, urls, teamBadge(permissions.get(author.id), courseId)),\n authorEmail: author.email,\n removedBy: comment.removedBy,\n reports: (byComment.get(comment.id) ?? []).map((r) => {\n const reporter = authors[r.orgUserId];\n if (!reporter) {\n throw new NotFoundError('Member', r.orgUserId);\n }\n return {\n reporter: authorView(\n orgId,\n reporter,\n urls,\n teamBadge(permissions.get(reporter.id), courseId),\n ),\n reason: r.reason,\n createdAt: r.createdAt,\n };\n }),\n createdAt: comment.createdAt,\n updatedAt: comment.updatedAt,\n };\n }),\n };\n }\n\n getComment(orgId: string, commentId: string): Promise<Comment | null> {\n return this.repo.findComment(orgId, commentId);\n }\n}\n"],"mappings":"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"}
|
|
@@ -2,18 +2,17 @@ import { Logger } from "../types/ports.js";
|
|
|
2
2
|
import { Subscription, SubscriptionItemType, SubscriptionView } from "../types/schemas/discussion.js";
|
|
3
3
|
import { SubjectRef } from "../types/notifications.js";
|
|
4
4
|
import "../types/index.js";
|
|
5
|
-
import "../shared/ports.js";
|
|
6
5
|
import { Actor } from "../shared/actor.js";
|
|
7
|
-
import
|
|
8
|
-
import "
|
|
9
|
-
import { ChatService, MuteInput, PostService, SubscriptionInput, SubscriptionService, SubscriptionsRepository } from "./ports.js";
|
|
6
|
+
import "../shared/ports.js";
|
|
7
|
+
import { ChatService, CommunityAccess, MuteInput, PostService, SubscriptionInput, SubscriptionService, SubscriptionUnitOfWork, SubscriptionsRepository } from "./ports.js";
|
|
10
8
|
//#region src/discussion/subscriptions.d.ts
|
|
11
9
|
declare function activeSubscription(subscription: Subscription | undefined, at: Date): SubscriptionView | null;
|
|
12
10
|
type SubscriptionServiceParams = {
|
|
13
11
|
repo: SubscriptionsRepository;
|
|
14
|
-
access: Pick<
|
|
12
|
+
access: Pick<CommunityAccess, 'canOpenSpace' | 'isCommunityMember'>;
|
|
15
13
|
posts: Pick<PostService, 'visible'>;
|
|
16
14
|
chats: Pick<ChatService, 'chatSummary'>;
|
|
15
|
+
uow: SubscriptionUnitOfWork;
|
|
17
16
|
logger?: Logger;
|
|
18
17
|
};
|
|
19
18
|
declare class SubscriptionServiceImpl implements SubscriptionService {
|
|
@@ -21,11 +20,12 @@ declare class SubscriptionServiceImpl implements SubscriptionService {
|
|
|
21
20
|
private readonly access;
|
|
22
21
|
private readonly posts;
|
|
23
22
|
private readonly chats;
|
|
23
|
+
private readonly uow;
|
|
24
24
|
private readonly logger;
|
|
25
25
|
constructor(params: SubscriptionServiceParams);
|
|
26
|
-
setPost(
|
|
27
|
-
setSpace(
|
|
28
|
-
setChat(
|
|
26
|
+
setPost(postId: string, input: SubscriptionInput, actor: Actor): Promise<void>;
|
|
27
|
+
setSpace(spaceId: string, input: MuteInput, actor: Actor): Promise<void>;
|
|
28
|
+
setChat(chatId: string, input: MuteInput, actor: Actor): Promise<void>;
|
|
29
29
|
viewerSubscriptions(orgId: string, orgUserId: string, itemType: SubscriptionItemType, itemIds: string[], at: Date): Promise<Record<string, SubscriptionView>>;
|
|
30
30
|
followerIds(orgId: string, postId: string): Promise<string[]>;
|
|
31
31
|
mutedAmong(orgId: string, item: SubjectRef, orgUserIds: string[], at: Date): Promise<string[]>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ForbiddenError, InvalidInputError, NotFoundError } from "../shared/errors.js";
|
|
2
2
|
import { noopLogger } from "../shared/logger.js";
|
|
3
|
+
import { discussionEvents } from "./events.js";
|
|
3
4
|
//#region src/discussion/subscriptions.ts
|
|
4
5
|
const ITEM_TYPES = [
|
|
5
6
|
"post",
|
|
@@ -22,28 +23,43 @@ var SubscriptionServiceImpl = class {
|
|
|
22
23
|
access;
|
|
23
24
|
posts;
|
|
24
25
|
chats;
|
|
26
|
+
uow;
|
|
25
27
|
logger;
|
|
26
28
|
constructor(params) {
|
|
27
29
|
this.repo = params.repo;
|
|
28
30
|
this.access = params.access;
|
|
29
31
|
this.posts = params.posts;
|
|
30
32
|
this.chats = params.chats;
|
|
33
|
+
this.uow = params.uow;
|
|
31
34
|
this.logger = params.logger ?? noopLogger;
|
|
32
35
|
}
|
|
33
|
-
async setPost(
|
|
34
|
-
await this.posts.visible(
|
|
35
|
-
await this.write(orgId, actor, "post", postId, input);
|
|
36
|
+
async setPost(postId, input, actor) {
|
|
37
|
+
await this.posts.visible(postId, actor);
|
|
38
|
+
await this.write(this.repo, actor.orgId, actor, "post", postId, input);
|
|
36
39
|
}
|
|
37
|
-
async setSpace(
|
|
38
|
-
if (!await this.access.canOpenSpace(
|
|
39
|
-
await this.write(orgId, actor, "space", spaceId, input);
|
|
40
|
+
async setSpace(spaceId, input, actor) {
|
|
41
|
+
if (!await this.access.canOpenSpace(spaceId, actor)) throw new NotFoundError("Space", spaceId);
|
|
42
|
+
await this.write(this.repo, actor.orgId, actor, "space", spaceId, input);
|
|
40
43
|
}
|
|
41
|
-
async setChat(
|
|
44
|
+
async setChat(chatId, input, actor) {
|
|
45
|
+
const orgId = actor.orgId;
|
|
42
46
|
const summary = await this.chats.chatSummary(orgId, chatId);
|
|
43
47
|
if (!summary) throw new NotFoundError("Chat", chatId);
|
|
44
|
-
if (!await this.access.isCommunityMember(
|
|
48
|
+
if (!await this.access.isCommunityMember(summary.chat.communityId, actor)) throw new ForbiddenError("You are not a member of this community");
|
|
45
49
|
if (!summary.participants.some((user) => user.orgUserId === actor.id && user.leftAt === null)) throw new ForbiddenError("You are not in this chat");
|
|
46
|
-
await this.
|
|
50
|
+
await this.uow.run(async ({ subscriptions, outbox }) => {
|
|
51
|
+
await this.write(subscriptions, orgId, actor, "chat", chatId, input);
|
|
52
|
+
await outbox.append([discussionEvents.chatMuted.make({
|
|
53
|
+
orgId,
|
|
54
|
+
data: {
|
|
55
|
+
chatId,
|
|
56
|
+
communityId: summary.chat.communityId,
|
|
57
|
+
orgUserId: actor.id,
|
|
58
|
+
mutedUntil: input.state === "muted" ? input.mutedUntil ?? null : null
|
|
59
|
+
},
|
|
60
|
+
metadata: { actorId: actor.id }
|
|
61
|
+
})]);
|
|
62
|
+
});
|
|
47
63
|
}
|
|
48
64
|
async viewerSubscriptions(orgId, orgUserId, itemType, itemIds, at) {
|
|
49
65
|
if (itemIds.length === 0) return {};
|
|
@@ -65,9 +81,9 @@ var SubscriptionServiceImpl = class {
|
|
|
65
81
|
const itemType = itemTypeOf(item);
|
|
66
82
|
return itemType === null ? [] : this.repo.ofItem(orgId, itemType, item.id);
|
|
67
83
|
}
|
|
68
|
-
async write(orgId, actor, itemType, itemId, input) {
|
|
84
|
+
async write(repo, orgId, actor, itemType, itemId, input) {
|
|
69
85
|
if (input.state === "none") {
|
|
70
|
-
await
|
|
86
|
+
await repo.remove(orgId, actor.id, itemType, itemId);
|
|
71
87
|
this.logger.debug("subscription removed", {
|
|
72
88
|
orgId,
|
|
73
89
|
itemType,
|
|
@@ -76,7 +92,7 @@ var SubscriptionServiceImpl = class {
|
|
|
76
92
|
return;
|
|
77
93
|
}
|
|
78
94
|
if (input.state === "following" && itemType !== "post") throw new InvalidInputError("Only a post can be followed");
|
|
79
|
-
await
|
|
95
|
+
await repo.upsert(orgId, {
|
|
80
96
|
orgId,
|
|
81
97
|
orgUserId: actor.id,
|
|
82
98
|
itemType,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subscriptions.js","names":[],"sources":["../../src/discussion/subscriptions.ts"],"sourcesContent":["import { ForbiddenError, InvalidInputError, NotFoundError } from '../shared/errors.js';\nimport type { Logger } from '../shared/ports.js';\nimport { noopLogger } from '../shared/logger.js';\nimport type {
|
|
1
|
+
{"version":3,"file":"subscriptions.js","names":[],"sources":["../../src/discussion/subscriptions.ts"],"sourcesContent":["import { ForbiddenError, InvalidInputError, NotFoundError } from '../shared/errors.js';\nimport type { Logger } from '../shared/ports.js';\nimport { noopLogger } from '../shared/logger.js';\nimport type { Actor } from '../shared/actor.js';\nimport type {\n SubjectRef,\n Subscription,\n SubscriptionItemType,\n SubscriptionView,\n} from '../types/index.js';\nimport type {\n ChatService,\n CommunityAccess,\n MuteInput,\n PostService,\n SubscriptionInput,\n SubscriptionService,\n SubscriptionsRepository,\n SubscriptionUnitOfWork,\n} from './ports.js';\nimport { discussionEvents } from './events.js';\n\nconst ITEM_TYPES: readonly SubscriptionItemType[] = ['post', 'space', 'chat'];\n\nfunction itemTypeOf(item: SubjectRef): SubscriptionItemType | null {\n return (ITEM_TYPES as readonly string[]).includes(item.type)\n ? (item.type as SubscriptionItemType)\n : null;\n}\n\nexport function activeSubscription(\n subscription: Subscription | undefined,\n at: Date,\n): SubscriptionView | null {\n if (!subscription) {\n return null;\n }\n if (\n subscription.state === 'muted' &&\n subscription.mutedUntil !== null &&\n subscription.mutedUntil <= at\n ) {\n return null;\n }\n return { state: subscription.state, mutedUntil: subscription.mutedUntil };\n}\n\nexport type SubscriptionServiceParams = {\n repo: SubscriptionsRepository;\n access: Pick<CommunityAccess, 'canOpenSpace' | 'isCommunityMember'>;\n posts: Pick<PostService, 'visible'>;\n chats: Pick<ChatService, 'chatSummary'>;\n uow: SubscriptionUnitOfWork;\n logger?: Logger;\n};\n\nexport class SubscriptionServiceImpl implements SubscriptionService {\n private readonly repo: SubscriptionsRepository;\n private readonly access: Pick<CommunityAccess, 'canOpenSpace' | 'isCommunityMember'>;\n private readonly posts: Pick<PostService, 'visible'>;\n private readonly chats: Pick<ChatService, 'chatSummary'>;\n private readonly uow: SubscriptionUnitOfWork;\n private readonly logger: Logger;\n\n constructor(params: SubscriptionServiceParams) {\n this.repo = params.repo;\n this.access = params.access;\n this.posts = params.posts;\n this.chats = params.chats;\n this.uow = params.uow;\n this.logger = params.logger ?? noopLogger;\n }\n\n async setPost(postId: string, input: SubscriptionInput, actor: Actor): Promise<void> {\n await this.posts.visible(postId, actor);\n await this.write(this.repo, actor.orgId, actor, 'post', postId, input);\n }\n\n async setSpace(spaceId: string, input: MuteInput, actor: Actor): Promise<void> {\n if (!(await this.access.canOpenSpace(spaceId, actor))) {\n throw new NotFoundError('Space', spaceId);\n }\n await this.write(this.repo, actor.orgId, actor, 'space', spaceId, input);\n }\n\n async setChat(chatId: string, input: MuteInput, actor: Actor): Promise<void> {\n const orgId = actor.orgId;\n const summary = await this.chats.chatSummary(orgId, chatId);\n if (!summary) {\n throw new NotFoundError('Chat', chatId);\n }\n if (!(await this.access.isCommunityMember(summary.chat.communityId, actor))) {\n throw new ForbiddenError('You are not a member of this community');\n }\n if (!summary.participants.some((user) => user.orgUserId === actor.id && user.leftAt === null)) {\n throw new ForbiddenError('You are not in this chat');\n }\n await this.uow.run(async ({ subscriptions, outbox }) => {\n await this.write(subscriptions, orgId, actor, 'chat', chatId, input);\n await outbox.append([\n discussionEvents.chatMuted.make({\n orgId,\n data: {\n chatId,\n communityId: summary.chat.communityId,\n orgUserId: actor.id,\n mutedUntil: input.state === 'muted' ? (input.mutedUntil ?? null) : null,\n },\n metadata: { actorId: actor.id },\n }),\n ]);\n });\n }\n\n async viewerSubscriptions(\n orgId: string,\n orgUserId: string,\n itemType: SubscriptionItemType,\n itemIds: string[],\n at: Date,\n ): Promise<Record<string, SubscriptionView>> {\n if (itemIds.length === 0) {\n return {};\n }\n const rows = await this.repo.forViewer(orgId, orgUserId, itemType, [...new Set(itemIds)]);\n return Object.fromEntries(\n rows.flatMap((row) => {\n const view = activeSubscription(row, at);\n return view ? [[row.itemId, view]] : [];\n }),\n );\n }\n\n followerIds(orgId: string, postId: string): Promise<string[]> {\n return this.repo.followerIds(orgId, postId);\n }\n\n async mutedAmong(\n orgId: string,\n item: SubjectRef,\n orgUserIds: string[],\n at: Date,\n ): Promise<string[]> {\n const itemType = itemTypeOf(item);\n if (itemType === null || orgUserIds.length === 0) {\n return [];\n }\n return this.repo.mutedAmong(orgId, itemType, item.id, [...new Set(orgUserIds)], at);\n }\n\n async itemSubscriptions(orgId: string, item: SubjectRef): Promise<Subscription[]> {\n const itemType = itemTypeOf(item);\n return itemType === null ? [] : this.repo.ofItem(orgId, itemType, item.id);\n }\n\n private async write(\n repo: SubscriptionsRepository,\n orgId: string,\n actor: Actor,\n itemType: SubscriptionItemType,\n itemId: string,\n input: SubscriptionInput,\n ): Promise<void> {\n if (input.state === 'none') {\n await repo.remove(orgId, actor.id, itemType, itemId);\n this.logger.debug('subscription removed', { orgId, itemType, itemId });\n return;\n }\n if (input.state === 'following' && itemType !== 'post') {\n throw new InvalidInputError('Only a post can be followed');\n }\n await repo.upsert(orgId, {\n orgId,\n orgUserId: actor.id,\n itemType,\n itemId,\n state: input.state,\n mutedUntil: input.state === 'muted' ? (input.mutedUntil ?? null) : null,\n createdAt: new Date(),\n });\n this.logger.debug('subscription set', { orgId, itemType, itemId, state: input.state });\n }\n}\n"],"mappings":";;;;AAsBA,MAAM,aAA8C;CAAC;CAAQ;CAAS;AAAM;AAE5E,SAAS,WAAW,MAA+C;CACjE,OAAQ,WAAiC,SAAS,KAAK,IAAI,IACtD,KAAK,OACN;AACN;AAEA,SAAgB,mBACd,cACA,IACyB;CACzB,IAAI,CAAC,cACH,OAAO;CAET,IACE,aAAa,UAAU,WACvB,aAAa,eAAe,QAC5B,aAAa,cAAc,IAE3B,OAAO;CAET,OAAO;EAAE,OAAO,aAAa;EAAO,YAAY,aAAa;CAAW;AAC1E;AAWA,IAAa,0BAAb,MAAoE;CAClE;CACA;CACA;CACA;CACA;CACA;CAEA,YAAY,QAAmC;EAC7C,KAAK,OAAO,OAAO;EACnB,KAAK,SAAS,OAAO;EACrB,KAAK,QAAQ,OAAO;EACpB,KAAK,QAAQ,OAAO;EACpB,KAAK,MAAM,OAAO;EAClB,KAAK,SAAS,OAAO,UAAU;CACjC;CAEA,MAAM,QAAQ,QAAgB,OAA0B,OAA6B;EACnF,MAAM,KAAK,MAAM,QAAQ,QAAQ,KAAK;EACtC,MAAM,KAAK,MAAM,KAAK,MAAM,MAAM,OAAO,OAAO,QAAQ,QAAQ,KAAK;CACvE;CAEA,MAAM,SAAS,SAAiB,OAAkB,OAA6B;EAC7E,IAAI,CAAE,MAAM,KAAK,OAAO,aAAa,SAAS,KAAK,GACjD,MAAM,IAAI,cAAc,SAAS,OAAO;EAE1C,MAAM,KAAK,MAAM,KAAK,MAAM,MAAM,OAAO,OAAO,SAAS,SAAS,KAAK;CACzE;CAEA,MAAM,QAAQ,QAAgB,OAAkB,OAA6B;EAC3E,MAAM,QAAQ,MAAM;EACpB,MAAM,UAAU,MAAM,KAAK,MAAM,YAAY,OAAO,MAAM;EAC1D,IAAI,CAAC,SACH,MAAM,IAAI,cAAc,QAAQ,MAAM;EAExC,IAAI,CAAE,MAAM,KAAK,OAAO,kBAAkB,QAAQ,KAAK,aAAa,KAAK,GACvE,MAAM,IAAI,eAAe,wCAAwC;EAEnE,IAAI,CAAC,QAAQ,aAAa,MAAM,SAAS,KAAK,cAAc,MAAM,MAAM,KAAK,WAAW,IAAI,GAC1F,MAAM,IAAI,eAAe,0BAA0B;EAErD,MAAM,KAAK,IAAI,IAAI,OAAO,EAAE,eAAe,aAAa;GACtD,MAAM,KAAK,MAAM,eAAe,OAAO,OAAO,QAAQ,QAAQ,KAAK;GACnE,MAAM,OAAO,OAAO,CAClB,iBAAiB,UAAU,KAAK;IAC9B;IACA,MAAM;KACJ;KACA,aAAa,QAAQ,KAAK;KAC1B,WAAW,MAAM;KACjB,YAAY,MAAM,UAAU,UAAW,MAAM,cAAc,OAAQ;IACrE;IACA,UAAU,EAAE,SAAS,MAAM,GAAG;GAChC,CAAC,CACH,CAAC;EACH,CAAC;CACH;CAEA,MAAM,oBACJ,OACA,WACA,UACA,SACA,IAC2C;EAC3C,IAAI,QAAQ,WAAW,GACrB,OAAO,CAAC;EAEV,MAAM,OAAO,MAAM,KAAK,KAAK,UAAU,OAAO,WAAW,UAAU,CAAC,GAAG,IAAI,IAAI,OAAO,CAAC,CAAC;EACxF,OAAO,OAAO,YACZ,KAAK,SAAS,QAAQ;GACpB,MAAM,OAAO,mBAAmB,KAAK,EAAE;GACvC,OAAO,OAAO,CAAC,CAAC,IAAI,QAAQ,IAAI,CAAC,IAAI,CAAC;EACxC,CAAC,CACH;CACF;CAEA,YAAY,OAAe,QAAmC;EAC5D,OAAO,KAAK,KAAK,YAAY,OAAO,MAAM;CAC5C;CAEA,MAAM,WACJ,OACA,MACA,YACA,IACmB;EACnB,MAAM,WAAW,WAAW,IAAI;EAChC,IAAI,aAAa,QAAQ,WAAW,WAAW,GAC7C,OAAO,CAAC;EAEV,OAAO,KAAK,KAAK,WAAW,OAAO,UAAU,KAAK,IAAI,CAAC,GAAG,IAAI,IAAI,UAAU,CAAC,GAAG,EAAE;CACpF;CAEA,MAAM,kBAAkB,OAAe,MAA2C;EAChF,MAAM,WAAW,WAAW,IAAI;EAChC,OAAO,aAAa,OAAO,CAAC,IAAI,KAAK,KAAK,OAAO,OAAO,UAAU,KAAK,EAAE;CAC3E;CAEA,MAAc,MACZ,MACA,OACA,OACA,UACA,QACA,OACe;EACf,IAAI,MAAM,UAAU,QAAQ;GAC1B,MAAM,KAAK,OAAO,OAAO,MAAM,IAAI,UAAU,MAAM;GACnD,KAAK,OAAO,MAAM,wBAAwB;IAAE;IAAO;IAAU;GAAO,CAAC;GACrE;EACF;EACA,IAAI,MAAM,UAAU,eAAe,aAAa,QAC9C,MAAM,IAAI,kBAAkB,6BAA6B;EAE3D,MAAM,KAAK,OAAO,OAAO;GACvB;GACA,WAAW,MAAM;GACjB;GACA;GACA,OAAO,MAAM;GACb,YAAY,MAAM,UAAU,UAAW,MAAM,cAAc,OAAQ;GACnE,2BAAW,IAAI,KAAK;EACtB,CAAC;EACD,KAAK,OAAO,MAAM,oBAAoB;GAAE;GAAO;GAAU;GAAQ,OAAO,MAAM;EAAM,CAAC;CACvF;AACF"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { SubjectRef } from "../types/notifications.js";
|
|
2
2
|
import "../types/index.js";
|
|
3
3
|
import { Actor } from "../shared/actor.js";
|
|
4
|
-
import { ActivityGetter, AuthorProfileReader, ChatRepository, ChatService, ContentAccessReader, DiscussionRepository, ItemTitle, ItemTitles, PostService } from "./ports.js";
|
|
4
|
+
import { ActivityGetter, AuthorPermissionReader, AuthorProfileReader, ChatRepository, ChatService, ContentAccessReader, DiscussionRepository, ItemTitle, ItemTitles, PostService } from "./ports.js";
|
|
5
5
|
//#region src/discussion/titles.d.ts
|
|
6
6
|
type DiscussionItemTitlesParams = {
|
|
7
7
|
posts: Pick<PostService, 'visibleTitles'>;
|
|
@@ -11,12 +11,14 @@ type DiscussionItemTitlesParams = {
|
|
|
11
11
|
profiles: AuthorProfileReader;
|
|
12
12
|
activities: ActivityGetter;
|
|
13
13
|
access: ContentAccessReader;
|
|
14
|
+
actors: Pick<AuthorPermissionReader, 'systemActor'>;
|
|
14
15
|
};
|
|
15
16
|
declare class DiscussionItemTitles implements ItemTitles {
|
|
16
17
|
private readonly deps;
|
|
17
18
|
constructor(deps: DiscussionItemTitlesParams);
|
|
18
|
-
titlesFor(
|
|
19
|
+
titlesFor(refs: SubjectRef[], actor: Actor): Promise<Record<string, ItemTitle>>;
|
|
19
20
|
private found;
|
|
21
|
+
private activitiesOf;
|
|
20
22
|
private seesComment;
|
|
21
23
|
private openActivities;
|
|
22
24
|
private names;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { canModerate, isStaffRole } from "../organizations/roles.js";
|
|
2
1
|
import { activityTitle } from "../content/titles.js";
|
|
3
|
-
import {
|
|
2
|
+
import { authorName } from "./authors.js";
|
|
4
3
|
import { mentionTokens } from "./mentions.js";
|
|
5
4
|
//#region src/discussion/titles.ts
|
|
6
5
|
const COMMENT_EXCERPT = 280;
|
|
@@ -18,16 +17,18 @@ var DiscussionItemTitles = class {
|
|
|
18
17
|
constructor(deps) {
|
|
19
18
|
this.deps = deps;
|
|
20
19
|
}
|
|
21
|
-
async titlesFor(
|
|
20
|
+
async titlesFor(refs, actor) {
|
|
21
|
+
const orgId = actor.orgId;
|
|
22
22
|
const [comments, messages] = await Promise.all([this.found(idsOf(refs, "comment"), (id) => this.deps.comments.findComment(orgId, id)), this.found(idsOf(refs, "message"), (id) => this.deps.messages.findMessage(orgId, id))]);
|
|
23
|
-
const
|
|
23
|
+
const activityOf = await this.activitiesOf(orgId, comments);
|
|
24
|
+
const shownComments = comments.filter((comment) => this.seesComment(actor, comment, activityOf));
|
|
24
25
|
const shownMessages = messages.filter((message) => message.deletedAt === null);
|
|
25
26
|
const postIds = [...idsOf(refs, "post"), ...shownComments.flatMap((comment) => comment.postId ?? [])];
|
|
26
27
|
const chatIds = [...idsOf(refs, "chat"), ...shownMessages.map((message) => message.chatId)];
|
|
27
28
|
const [posts, chats, activities, names] = await Promise.all([
|
|
28
|
-
postIds.length > 0 ? this.deps.posts.visibleTitles(
|
|
29
|
-
chatIds.length > 0 ? this.deps.chats.titlesFor(
|
|
30
|
-
this.openActivities(
|
|
29
|
+
postIds.length > 0 ? this.deps.posts.visibleTitles(postIds, actor) : {},
|
|
30
|
+
chatIds.length > 0 ? this.deps.chats.titlesFor(chatIds, actor) : {},
|
|
31
|
+
this.openActivities(actor, shownComments, activityOf),
|
|
31
32
|
this.names(orgId, [...shownComments, ...shownMessages].map((item) => item.body))
|
|
32
33
|
]);
|
|
33
34
|
const titles = {};
|
|
@@ -58,16 +59,29 @@ var DiscussionItemTitles = class {
|
|
|
58
59
|
async found(ids, find) {
|
|
59
60
|
return (await Promise.all(ids.map(find))).filter((row) => row !== null);
|
|
60
61
|
}
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
async activitiesOf(orgId, comments) {
|
|
63
|
+
const ids = [...new Set(comments.flatMap((comment) => comment.postId ? [] : comment.activityId ?? []))];
|
|
64
|
+
if (ids.length === 0) return /* @__PURE__ */ new Map();
|
|
65
|
+
const system = this.deps.actors.systemActor(orgId);
|
|
66
|
+
const found = await Promise.all(ids.map((id) => this.deps.activities.getActivity(id, system)));
|
|
67
|
+
return new Map(found.filter((activity) => activity !== null).map((a) => [a.id, a]));
|
|
68
|
+
}
|
|
69
|
+
seesComment(actor, comment, activityOf) {
|
|
70
|
+
if (comment.status === "published") return true;
|
|
71
|
+
if (comment.postId) {
|
|
72
|
+
if (comment.communityId !== null && actor.permissions.has("communities:moderate", comment.communityId)) return true;
|
|
73
|
+
} else {
|
|
74
|
+
const activity = comment.activityId ? activityOf.get(comment.activityId) : void 0;
|
|
75
|
+
if (activity && actor.permissions.has("courses:moderate", activity.courseId)) return true;
|
|
76
|
+
}
|
|
63
77
|
return comment.orgUserId === actor.id && comment.status === "pending";
|
|
64
78
|
}
|
|
65
|
-
async openActivities(
|
|
79
|
+
async openActivities(actor, comments, activityOf) {
|
|
66
80
|
const ids = [...new Set(comments.flatMap((comment) => comment.postId ? [] : comment.activityId ?? []))];
|
|
67
81
|
const open = await Promise.all(ids.map(async (id) => {
|
|
68
|
-
const activity =
|
|
82
|
+
const activity = activityOf.get(id);
|
|
69
83
|
if (!activity) return null;
|
|
70
|
-
return
|
|
84
|
+
return actor.permissions.has("courses:read", activity.courseId) || await this.deps.access.hasAccess(actor.orgId, actor.id, activity.courseId) ? [id, activityTitle(activity)] : null;
|
|
71
85
|
}));
|
|
72
86
|
return Object.fromEntries(open.filter((entry) => entry !== null));
|
|
73
87
|
}
|
|
@@ -75,7 +89,7 @@ var DiscussionItemTitles = class {
|
|
|
75
89
|
const ids = mentionTokens(...bodies);
|
|
76
90
|
if (ids.length === 0) return {};
|
|
77
91
|
const records = await this.deps.profiles.authorsOf(orgId, ids);
|
|
78
|
-
return Object.fromEntries(Object.entries(records).map(([id, record]) => [id,
|
|
92
|
+
return Object.fromEntries(Object.entries(records).map(([id, record]) => [id, authorName(record) || "A member"]));
|
|
79
93
|
}
|
|
80
94
|
};
|
|
81
95
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"titles.js","names":[],"sources":["../../src/discussion/titles.ts"],"sourcesContent":["import { activityTitle } from '../content/index.js';\nimport { canModerate, isStaffRole } from '../organizations/index.js';\nimport type { Actor } from '../shared/actor.js';\nimport type { SubjectRef } from '../types/index.js';\nimport { authorView } from './authors.js';\nimport { mentionTokens } from './mentions.js';\nimport type {\n ActivityGetter,\n AuthorProfileReader,\n ChatRepository,\n ChatService,\n ContentAccessReader,\n DiscussionRepository,\n ItemTitle,\n ItemTitles,\n PostService,\n} from './ports.js';\nimport type { Comment } from '../types/index.js';\n\nconst COMMENT_EXCERPT = 280;\nconst MESSAGE_EXCERPT = 140;\nconst TOKEN = /<@([A-Za-z0-9_-]+)>/g;\n\nexport type DiscussionItemTitlesParams = {\n posts: Pick<PostService, 'visibleTitles'>;\n chats: Pick<ChatService, 'titlesFor'>;\n comments: Pick<DiscussionRepository, 'findComment'>;\n messages: Pick<ChatRepository, 'findMessage'>;\n profiles: AuthorProfileReader;\n activities: ActivityGetter;\n access: ContentAccessReader;\n};\n\nfunction idsOf(refs: SubjectRef[], type: SubjectRef['type']): string[] {\n return [...new Set(refs.filter((ref) => ref.type === type).map((ref) => ref.id))];\n}\n\nfunction excerpt(body: string, names: Record<string, string>, max: number): string {\n const named = body.replace(TOKEN, (_, id: string) => `@${names[id] ?? 'A member'}`);\n const flat = named.replace(/\\s+/g, ' ').trim();\n return flat.length <= max ? flat : flat.slice(0, max);\n}\n\nexport class DiscussionItemTitles implements ItemTitles {\n constructor(private readonly deps: DiscussionItemTitlesParams) {}\n\n async titlesFor(\n orgId: string,\n actor: Actor,\n refs: SubjectRef[],\n ): Promise<Record<string, ItemTitle>> {\n const [comments, messages] = await Promise.all([\n this.found(idsOf(refs, 'comment'), (id) => this.deps.comments.findComment(orgId, id)),\n this.found(idsOf(refs, 'message'), (id) => this.deps.messages.findMessage(orgId, id)),\n ]);\n const shownComments = comments.filter((comment) => this.seesComment(actor, comment));\n const shownMessages = messages.filter((message) => message.deletedAt === null);\n const postIds = [\n ...idsOf(refs, 'post'),\n ...shownComments.flatMap((comment) => comment.postId ?? []),\n ];\n const chatIds = [...idsOf(refs, 'chat'), ...shownMessages.map((message) => message.chatId)];\n const [posts, chats, activities, names] = await Promise.all([\n postIds.length > 0\n ? this.deps.posts.visibleTitles(orgId, actor, postIds)\n : ({} as Record<string, string>),\n chatIds.length > 0\n ? this.deps.chats.titlesFor(orgId, actor, chatIds)\n : ({} as Record<string, string>),\n this.openActivities(orgId, actor, shownComments),\n this.names(\n orgId,\n [...shownComments, ...shownMessages].map((item) => item.body),\n ),\n ]);\n const titles: Record<string, ItemTitle> = {};\n for (const id of idsOf(refs, 'post')) {\n const title = posts[id];\n if (title !== undefined) {\n titles[`post:${id}`] = { title };\n }\n }\n for (const id of idsOf(refs, 'chat')) {\n const title = chats[id];\n if (title !== undefined) {\n titles[`chat:${id}`] = { title };\n }\n }\n for (const comment of shownComments) {\n const title = comment.postId\n ? posts[comment.postId]\n : comment.activityId\n ? activities[comment.activityId]\n : undefined;\n if (title !== undefined) {\n titles[`comment:${comment.id}`] = {\n title,\n excerpt: excerpt(comment.body, names, COMMENT_EXCERPT),\n };\n }\n }\n for (const message of shownMessages) {\n const title = chats[message.chatId];\n if (title !== undefined) {\n titles[`message:${message.id}`] = {\n title,\n excerpt: excerpt(message.body, names, MESSAGE_EXCERPT),\n };\n }\n }\n return titles;\n }\n\n private async found<T>(ids: string[], find: (id: string) => Promise<T | null>): Promise<T[]> {\n const rows: (T | null)[] = await Promise.all(ids.map(find));\n return rows.filter((row): row is T => row !== null);\n }\n\n private seesComment(actor: Actor, comment: Comment): boolean {\n if (comment.status === 'published' || canModerate(actor.role)) {\n return true;\n }\n return comment.orgUserId === actor.id && comment.status === 'pending';\n }\n\n private async openActivities(\n orgId: string,\n actor: Actor,\n comments: Comment[],\n ): Promise<Record<string, string>> {\n const ids = [\n ...new Set(comments.flatMap((comment) => (comment.postId ? [] : (comment.activityId ?? [])))),\n ];\n const open = await Promise.all(\n ids.map(async (id): Promise<[string, string] | null> => {\n const activity = await this.deps.activities.getActivity(orgId, id);\n if (!activity) {\n return null;\n }\n const opens =\n isStaffRole(actor.role) ||\n (await this.deps.access.hasAccess(orgId, actor.id, activity.courseId));\n return opens ? [id, activityTitle(activity)] : null;\n }),\n );\n return Object.fromEntries(open.filter((entry): entry is [string, string] => entry !== null));\n }\n\n private async names(orgId: string, bodies: string[]): Promise<Record<string, string>> {\n const ids = mentionTokens(...bodies);\n if (ids.length === 0) {\n return {};\n }\n const records = await this.deps.profiles.authorsOf(orgId, ids);\n return Object.fromEntries(\n Object.entries(records).map(([id, record]) => [\n id,\n authorView(orgId, record, null).displayName || 'A member',\n ]),\n );\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"titles.js","names":[],"sources":["../../src/discussion/titles.ts"],"sourcesContent":["import { activityTitle } from '../content/index.js';\nimport type { Actor } from '../shared/actor.js';\nimport type { Activity, SubjectRef } from '../types/index.js';\nimport { authorName } from './authors.js';\nimport { mentionTokens } from './mentions.js';\nimport type {\n ActivityGetter,\n AuthorPermissionReader,\n AuthorProfileReader,\n ChatRepository,\n ChatService,\n ContentAccessReader,\n DiscussionRepository,\n ItemTitle,\n ItemTitles,\n PostService,\n} from './ports.js';\nimport type { Comment } from '../types/index.js';\n\nconst COMMENT_EXCERPT = 280;\nconst MESSAGE_EXCERPT = 140;\nconst TOKEN = /<@([A-Za-z0-9_-]+)>/g;\n\nexport type DiscussionItemTitlesParams = {\n posts: Pick<PostService, 'visibleTitles'>;\n chats: Pick<ChatService, 'titlesFor'>;\n comments: Pick<DiscussionRepository, 'findComment'>;\n messages: Pick<ChatRepository, 'findMessage'>;\n profiles: AuthorProfileReader;\n activities: ActivityGetter;\n access: ContentAccessReader;\n actors: Pick<AuthorPermissionReader, 'systemActor'>;\n};\n\nfunction idsOf(refs: SubjectRef[], type: SubjectRef['type']): string[] {\n return [...new Set(refs.filter((ref) => ref.type === type).map((ref) => ref.id))];\n}\n\nfunction excerpt(body: string, names: Record<string, string>, max: number): string {\n const named = body.replace(TOKEN, (_, id: string) => `@${names[id] ?? 'A member'}`);\n const flat = named.replace(/\\s+/g, ' ').trim();\n return flat.length <= max ? flat : flat.slice(0, max);\n}\n\nexport class DiscussionItemTitles implements ItemTitles {\n constructor(private readonly deps: DiscussionItemTitlesParams) {}\n\n async titlesFor(refs: SubjectRef[], actor: Actor): Promise<Record<string, ItemTitle>> {\n const orgId = actor.orgId;\n const [comments, messages] = await Promise.all([\n this.found(idsOf(refs, 'comment'), (id) => this.deps.comments.findComment(orgId, id)),\n this.found(idsOf(refs, 'message'), (id) => this.deps.messages.findMessage(orgId, id)),\n ]);\n const activityOf = await this.activitiesOf(orgId, comments);\n const shownComments = comments.filter((comment) =>\n this.seesComment(actor, comment, activityOf),\n );\n const shownMessages = messages.filter((message) => message.deletedAt === null);\n const postIds = [\n ...idsOf(refs, 'post'),\n ...shownComments.flatMap((comment) => comment.postId ?? []),\n ];\n const chatIds = [...idsOf(refs, 'chat'), ...shownMessages.map((message) => message.chatId)];\n const [posts, chats, activities, names] = await Promise.all([\n postIds.length > 0\n ? this.deps.posts.visibleTitles(postIds, actor)\n : ({} as Record<string, string>),\n chatIds.length > 0\n ? this.deps.chats.titlesFor(chatIds, actor)\n : ({} as Record<string, string>),\n this.openActivities(actor, shownComments, activityOf),\n this.names(\n orgId,\n [...shownComments, ...shownMessages].map((item) => item.body),\n ),\n ]);\n const titles: Record<string, ItemTitle> = {};\n for (const id of idsOf(refs, 'post')) {\n const title = posts[id];\n if (title !== undefined) {\n titles[`post:${id}`] = { title };\n }\n }\n for (const id of idsOf(refs, 'chat')) {\n const title = chats[id];\n if (title !== undefined) {\n titles[`chat:${id}`] = { title };\n }\n }\n for (const comment of shownComments) {\n const title = comment.postId\n ? posts[comment.postId]\n : comment.activityId\n ? activities[comment.activityId]\n : undefined;\n if (title !== undefined) {\n titles[`comment:${comment.id}`] = {\n title,\n excerpt: excerpt(comment.body, names, COMMENT_EXCERPT),\n };\n }\n }\n for (const message of shownMessages) {\n const title = chats[message.chatId];\n if (title !== undefined) {\n titles[`message:${message.id}`] = {\n title,\n excerpt: excerpt(message.body, names, MESSAGE_EXCERPT),\n };\n }\n }\n return titles;\n }\n\n private async found<T>(ids: string[], find: (id: string) => Promise<T | null>): Promise<T[]> {\n const rows: (T | null)[] = await Promise.all(ids.map(find));\n return rows.filter((row): row is T => row !== null);\n }\n\n private async activitiesOf(orgId: string, comments: Comment[]): Promise<Map<string, Activity>> {\n const ids = [\n ...new Set(comments.flatMap((comment) => (comment.postId ? [] : (comment.activityId ?? [])))),\n ];\n if (ids.length === 0) {\n return new Map();\n }\n const system = this.deps.actors.systemActor(orgId);\n const found = await Promise.all(ids.map((id) => this.deps.activities.getActivity(id, system)));\n return new Map(\n found.filter((activity): activity is Activity => activity !== null).map((a) => [a.id, a]),\n );\n }\n\n private seesComment(actor: Actor, comment: Comment, activityOf: Map<string, Activity>): boolean {\n if (comment.status === 'published') {\n return true;\n }\n if (comment.postId) {\n if (\n comment.communityId !== null &&\n actor.permissions.has('communities:moderate', comment.communityId)\n ) {\n return true;\n }\n } else {\n const activity = comment.activityId ? activityOf.get(comment.activityId) : undefined;\n if (activity && actor.permissions.has('courses:moderate', activity.courseId)) {\n return true;\n }\n }\n return comment.orgUserId === actor.id && comment.status === 'pending';\n }\n\n private async openActivities(\n actor: Actor,\n comments: Comment[],\n activityOf: Map<string, Activity>,\n ): Promise<Record<string, string>> {\n const ids = [\n ...new Set(comments.flatMap((comment) => (comment.postId ? [] : (comment.activityId ?? [])))),\n ];\n const open = await Promise.all(\n ids.map(async (id): Promise<[string, string] | null> => {\n const activity = activityOf.get(id);\n if (!activity) {\n return null;\n }\n const opens =\n actor.permissions.has('courses:read', activity.courseId) ||\n (await this.deps.access.hasAccess(actor.orgId, actor.id, activity.courseId));\n return opens ? [id, activityTitle(activity)] : null;\n }),\n );\n return Object.fromEntries(open.filter((entry): entry is [string, string] => entry !== null));\n }\n\n private async names(orgId: string, bodies: string[]): Promise<Record<string, string>> {\n const ids = mentionTokens(...bodies);\n if (ids.length === 0) {\n return {};\n }\n const records = await this.deps.profiles.authorsOf(orgId, ids);\n return Object.fromEntries(\n Object.entries(records).map(([id, record]) => [id, authorName(record) || 'A member']),\n );\n }\n}\n"],"mappings":"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"}
|