1 import { CommentThread } from '../Models'
2 import { CommentThreadType, CommentType, ServerTime } from '@proton/docs-shared'
3 import { Result } from '@proton/docs-shared'
4 import metrics from '@proton/metrics'
5 import type { DecryptComment } from './DecryptComment'
6 import type { DocsApi } from '../Api/DocsApi'
7 import type { DocumentEntitlements, PublicDocumentEntitlements } from '../Types/DocumentEntitlements'
8 import type { LocalCommentsState } from '../Services/Comments/LocalCommentsState'
9 import type { LoggerInterface } from '@proton/utils/logs'
10 import type { NodeMeta } from '@proton/drive-store'
11 import type { UseCaseInterface } from '../Domain/UseCase/UseCaseInterface'
12 import { isPrivateNodeMeta } from '@proton/drive-store/lib/interface'
15 * Updates the local comment state by loading and decrypting all threads from the API for the document.
17 export class LoadThreads implements UseCaseInterface<void> {
20 private decryptComment: DecryptComment,
21 private logger: LoggerInterface,
25 entitlements: PublicDocumentEntitlements | DocumentEntitlements
26 commentsState: LocalCommentsState
27 }): Promise<Result<void>> {
28 const result = await this.api.getAllThreadIDs(dto.entitlements.nodeMeta)
29 if (result.isFailed()) {
30 metrics.docs_comments_download_error_total.increment({
31 reason: 'server_error',
34 return Result.fail(result.getError().message)
37 const response = result.getValue()
40 response.CommentThreads.map(async (threadID) => {
41 return this.loadThread({ threadID, entitlements: dto.entitlements, commentsState: dto.commentsState })
45 dto.commentsState.sortThreadsAndNotify()
50 private async loadThread(dto: {
52 entitlements: PublicDocumentEntitlements | DocumentEntitlements
53 commentsState: LocalCommentsState
54 }): Promise<Result<void>> {
55 const thread = await this.api.getThread({
56 nodeMeta: dto.entitlements.nodeMeta,
57 threadId: dto.threadID,
59 if (thread.isFailed()) {
60 metrics.docs_comments_download_error_total.increment({
61 reason: 'server_error',
64 return Result.fail(thread.getError().message)
67 const { CommentThread: commentThreadDto } = thread.getValue()
68 const corruptThreadIds: Set<string> = new Set()
70 const comments = await Promise.all(
71 commentThreadDto.Comments.map(async (commentDto) => {
72 const result = await this.decryptComment.execute(commentDto, commentThreadDto.Mark, dto.entitlements.keys)
73 if (!result.isFailed()) {
74 return result.getValue()
78 * If the comment or suggestion refers to a date before the encryption incident was resolved,
79 * and given that decryption has failed, we delete this comment thread from the API.
80 * See DRVDOC-1194 for more information.
82 if (commentDto.Type === CommentType.Suggestion || commentThreadDto.Type === CommentThreadType.Suggestion) {
83 this.logger.error(`[LoadThreads] Failed to decrypt suggestion comment: ${result.getError()}`)
84 const dateEncryptionIncidentWasResolved = new Date('2024-10-03T12:00:00+02:00')
85 const dateCommentWasCreated = new ServerTime(commentDto.CreateTime).date
86 const commentWasCreatedBeforeEncryptionResolution = dateCommentWasCreated < dateEncryptionIncidentWasResolved
87 if (commentWasCreatedBeforeEncryptionResolution) {
88 corruptThreadIds.add(dto.threadID)
91 this.logger.error(`[LoadThreads] Failed to decrypt comment: ${result.getError()}`)
98 if (corruptThreadIds.size > 0 && isPrivateNodeMeta(dto.entitlements.nodeMeta)) {
99 void this.deleteCorruptSuggestionThreads(Array.from(corruptThreadIds), dto.entitlements.nodeMeta)
101 if (corruptThreadIds.has(dto.threadID)) {
106 const successfulComments = comments.filter((result) => !!result)
108 const localThread = new CommentThread(
109 commentThreadDto.CommentThreadID,
110 new ServerTime(commentThreadDto.CreateTime),
111 new ServerTime(commentThreadDto.ModifyTime),
112 commentThreadDto.Mark,
115 commentThreadDto.State,
116 commentThreadDto.Type,
117 commentThreadDto.CommentThreadID,
120 dto.commentsState.addThread(localThread)
125 private async deleteCorruptSuggestionThreads(threadIds: string[], nodeMeta: NodeMeta): Promise<void> {
126 for (const threadId of threadIds) {
127 this.logger.error(`[LoadThreads] Deleting corrupt suggestion thread: ${threadId}`)
129 /** First reject the thread with the API, so that the API will allow deletion, since it otherwise won't */
130 const rejectResponse = await this.api.changeSuggestionThreadState({
132 volumeId: nodeMeta.volumeId,
133 linkId: nodeMeta.linkId,
139 if (rejectResponse.isFailed()) {
140 this.logger.info(`[LoadThreads] Failed to reject corrupt thread: ${rejectResponse.getError()}`)
144 void this.api.deleteThread({
146 volumeId: nodeMeta.volumeId,
147 linkId: nodeMeta.linkId,