Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / packages / docs-core / lib / Api / DocsApi.ts
blobe6c7f44e43045854846afd2d6d11a337a1efa728
1 import { DocsApiPrivateRouteBuilder } from './Routes/DocsApiPrivateRouteBuilder'
2 import { DocsApiPublicRouteBuilder } from './Routes/DocsApiPublicRouteBuilder'
3 import { DocsApiRouteBuilder } from './Routes/DocsApiRouteBuilder'
4 import { forgeImageURL } from '@proton/shared/lib/helpers/image'
5 import { getErrorString } from '../Util/GetErrorString'
6 import { isPublicNodeMeta } from '@proton/drive-store/lib/interface'
7 import { Result } from '@proton/docs-shared'
8 import { type ApiAddCommentToThread } from './Requests/ApiAddCommentToThread'
9 import { type SuggestionThreadStateAction } from '@proton/docs-shared'
10 import type { AddCommentToThreadDTO } from './Requests/ApiAddCommentToThread'
11 import type { AddCommentToThreadResponse } from './Types/AddCommentToThreadResponse'
12 import type { ApiCreateThread, CreateThreadDTO } from './Requests/ApiCreateThread'
13 import type { ApiEditComment, EditCommentDTO } from './Requests/ApiEditComment'
14 import type { ApiGetThread } from './Requests/ApiGetThread'
15 import type { ApiResult } from '@proton/docs-shared'
16 import type { Commit, SquashCommit } from '@proton/docs-proto'
17 import type { CreateDocumentResponse } from './Types/CreateDocumentResponse'
18 import type { CreateThreadResponse } from './Types/CreateThreadResponse'
19 import type { CreateValetTokenResponse } from './Types/CreateValetTokenResponse'
20 import type { DeleteCommentResponse } from './Types/DeleteCommentResponse'
21 import type { DeleteThreadResponse } from './Types/DeleteThreadResponse'
22 import type { DocumentEntitlements } from '../Types/DocumentEntitlements'
23 import type { EditCommentResponse } from './Types/EditCommentResponse'
24 import type { GetAllThreadIDsResponse } from './Types/GetAllThreadIDsResponse'
25 import type { GetCommentThreadResponse } from './Types/GetCommentThreadResponse'
26 import type { GetDocumentMetaResponse } from './Types/GetDocumentMetaResponse'
27 import type { GetRecentsResponse } from './Types/GetRecentsResponse'
28 import type { HttpHeaders } from './Types/HttpHeaders'
29 import type { ImageProxyParams } from './Types/ImageProxyParams'
30 import type { NodeMeta, PublicNodeMeta } from '@proton/drive-store'
31 import type { PublicDocumentEntitlements } from '../Types/DocumentEntitlements'
32 import type { ResolveThreadResponse } from './Types/ResolveThreadResponse'
33 import type { RouteExecutor } from './RouteExecutor'
34 import type { SeedInitialCommitApiResponse } from './Types/SeedInitialCommitApiResponse'
35 import type { UnresolveThreadResponse } from './Types/UnresolveThreadResponse'
37 export class DocsApi {
38   constructor(
39     private routeExecutor: RouteExecutor,
40     /** Headers to use when fetching public documents */
41     private publicContextHeaders: HttpHeaders | undefined,
42     private imageProxyParams: ImageProxyParams | undefined,
43     private apiCreateThread: ApiCreateThread,
44     private apiAddCommentToThread: ApiAddCommentToThread,
45     private apiGetThread: ApiGetThread,
46     private apiEditComment: ApiEditComment,
47   ) {
48     window.addEventListener('beforeunload', this.handleWindowUnload)
49   }
51   handleWindowUnload = (event: BeforeUnloadEvent): void => {
52     if (this.routeExecutor.inflight !== 0) {
53       event.preventDefault()
54     }
55   }
57   async getDocumentMeta(lookup: NodeMeta | PublicNodeMeta): Promise<ApiResult<GetDocumentMetaResponse>> {
58     if (isPublicNodeMeta(lookup) && !this.publicContextHeaders) {
59       throw new Error('Public context headers not set')
60     }
62     const route = isPublicNodeMeta(lookup)
63       ? new DocsApiPublicRouteBuilder({
64           token: lookup.token,
65           linkId: lookup.linkId,
66           headers: this.publicContextHeaders!,
67         }).meta()
68       : new DocsApiPrivateRouteBuilder({ volumeId: lookup.volumeId, linkId: lookup.linkId }).meta()
70     return this.routeExecutor.execute(route)
71   }
73   async getCommitData(lookup: NodeMeta | PublicNodeMeta, commitId: string): Promise<ApiResult<Uint8Array>> {
74     if (isPublicNodeMeta(lookup) && !this.publicContextHeaders) {
75       throw new Error('Public context headers not set')
76     }
78     const route = isPublicNodeMeta(lookup)
79       ? new DocsApiPublicRouteBuilder({
80           token: lookup.token,
81           linkId: lookup.linkId,
82           headers: this.publicContextHeaders!,
83         }).commit({ commitId })
84       : new DocsApiPrivateRouteBuilder({ volumeId: lookup.volumeId, linkId: lookup.linkId }).commit({ commitId })
86     return this.routeExecutor.execute(route)
87   }
89   async fetchRecentDocuments(): Promise<ApiResult<GetRecentsResponse>> {
90     const route = new DocsApiRouteBuilder('docs').recentDocuments()
91     return this.routeExecutor.execute(route)
92   }
94   async seedInitialCommit(docMeta: NodeMeta, commit: Commit): Promise<ApiResult<SeedInitialCommitApiResponse>> {
95     if (isPublicNodeMeta(docMeta)) {
96       throw new Error('Cannot seed initial commit for public node')
97     }
99     const route = new DocsApiPrivateRouteBuilder({
100       volumeId: docMeta.volumeId,
101       linkId: docMeta.linkId,
102     }).seedInitialCommit({
103       data: commit.serializeBinary(),
104     })
106     return this.routeExecutor.execute(route)
107   }
109   async lockDocument(nodeMeta: NodeMeta, fetchCommitId?: string): Promise<ApiResult<Uint8Array>> {
110     const route = new DocsApiPrivateRouteBuilder({
111       volumeId: nodeMeta.volumeId,
112       linkId: nodeMeta.linkId,
113     }).lock({ fetchCommitId })
115     return this.routeExecutor.execute(route)
116   }
118   async squashCommit(nodeMeta: NodeMeta, commitId: string, squash: SquashCommit): Promise<ApiResult<Uint8Array>> {
119     const route = new DocsApiPrivateRouteBuilder({
120       volumeId: nodeMeta.volumeId,
121       linkId: nodeMeta.linkId,
122     }).squashCommit({ commitId, data: squash.serializeBinary() })
124     return this.routeExecutor.execute(route)
125   }
127   async createDocument(lookup: NodeMeta): Promise<ApiResult<CreateDocumentResponse>> {
128     const route = new DocsApiPrivateRouteBuilder({
129       volumeId: lookup.volumeId,
130       linkId: lookup.linkId,
131     }).createDocument()
133     return this.routeExecutor.execute(route)
134   }
136   async createRealtimeValetToken(
137     lookup: NodeMeta | PublicNodeMeta,
138     commitId?: string,
139   ): Promise<ApiResult<CreateValetTokenResponse>> {
140     if (isPublicNodeMeta(lookup) && !this.publicContextHeaders) {
141       throw new Error('Public context headers not set')
142     }
144     const route = isPublicNodeMeta(lookup)
145       ? new DocsApiPublicRouteBuilder({
146           token: lookup.token,
147           linkId: lookup.linkId,
148           headers: this.publicContextHeaders!,
149         }).createRealtimeValetToken({ commitId })
150       : new DocsApiPrivateRouteBuilder({ volumeId: lookup.volumeId, linkId: lookup.linkId }).createRealtimeValetToken({
151           commitId,
152         })
154     return this.routeExecutor.execute(route)
155   }
157   async getAllThreadIDs(nodeMeta: NodeMeta | PublicNodeMeta): Promise<ApiResult<GetAllThreadIDsResponse>> {
158     if (isPublicNodeMeta(nodeMeta) && !this.publicContextHeaders) {
159       throw new Error('Public context headers not set')
160     }
162     const route = isPublicNodeMeta(nodeMeta)
163       ? new DocsApiPublicRouteBuilder({
164           token: nodeMeta.token,
165           linkId: nodeMeta.linkId,
166           headers: this.publicContextHeaders!,
167         }).getCommentThreads()
168       : new DocsApiPrivateRouteBuilder({ volumeId: nodeMeta.volumeId, linkId: nodeMeta.linkId }).getCommentThreads()
170     return this.routeExecutor.execute(route)
171   }
173   async createThread(
174     dto: CreateThreadDTO,
175     entitlements: PublicDocumentEntitlements | DocumentEntitlements,
176   ): Promise<ApiResult<CreateThreadResponse>> {
177     const result = await this.apiCreateThread.execute(dto, entitlements)
178     return result
179   }
181   async getThread(dto: {
182     nodeMeta: NodeMeta | PublicNodeMeta
183     threadId: string
184   }): Promise<ApiResult<GetCommentThreadResponse>> {
185     const result = await this.apiGetThread.execute(dto)
186     return result
187   }
189   async deleteThread(dto: {
190     nodeMeta: NodeMeta | PublicNodeMeta
191     threadId: string
192   }): Promise<ApiResult<DeleteThreadResponse>> {
193     if (isPublicNodeMeta(dto.nodeMeta) && !this.publicContextHeaders) {
194       throw new Error('Public context headers not set')
195     }
197     const route = isPublicNodeMeta(dto.nodeMeta)
198       ? new DocsApiPublicRouteBuilder({
199           token: dto.nodeMeta.token,
200           linkId: dto.nodeMeta.linkId,
201           headers: this.publicContextHeaders!,
202         }).deleteThread({ threadId: dto.threadId })
203       : new DocsApiPrivateRouteBuilder({
204           volumeId: dto.nodeMeta.volumeId,
205           linkId: dto.nodeMeta.linkId,
206         }).deleteThread({ threadId: dto.threadId })
208     return this.routeExecutor.execute(route)
209   }
211   async addCommentToThread(
212     dto: AddCommentToThreadDTO,
213     entitlements: PublicDocumentEntitlements | DocumentEntitlements,
214   ): Promise<ApiResult<AddCommentToThreadResponse>> {
215     const result = await this.apiAddCommentToThread.execute(dto, entitlements)
216     return result
217   }
219   async editComment(
220     dto: EditCommentDTO,
221     entitlements: PublicDocumentEntitlements | DocumentEntitlements,
222   ): Promise<ApiResult<EditCommentResponse>> {
223     const result = await this.apiEditComment.execute(dto, entitlements)
224     return result
225   }
227   async deleteComment(dto: {
228     nodeMeta: NodeMeta | PublicNodeMeta
229     threadId: string
230     commentId: string
231   }): Promise<ApiResult<DeleteCommentResponse>> {
232     if (isPublicNodeMeta(dto.nodeMeta) && !this.publicContextHeaders) {
233       throw new Error('Public context headers not set')
234     }
236     const route = isPublicNodeMeta(dto.nodeMeta)
237       ? new DocsApiPublicRouteBuilder({
238           token: dto.nodeMeta.token,
239           linkId: dto.nodeMeta.linkId,
240           headers: this.publicContextHeaders!,
241         }).deleteComment({ threadId: dto.threadId, commentId: dto.commentId })
242       : new DocsApiPrivateRouteBuilder({ volumeId: dto.nodeMeta.volumeId, linkId: dto.nodeMeta.linkId }).deleteComment({
243           threadId: dto.threadId,
244           commentId: dto.commentId,
245         })
247     return this.routeExecutor.execute(route)
248   }
250   async resolveThread(dto: {
251     nodeMeta: NodeMeta | PublicNodeMeta
252     threadId: string
253   }): Promise<ApiResult<ResolveThreadResponse>> {
254     if (isPublicNodeMeta(dto.nodeMeta) && !this.publicContextHeaders) {
255       throw new Error('Public context headers not set')
256     }
258     const route = isPublicNodeMeta(dto.nodeMeta)
259       ? new DocsApiPublicRouteBuilder({
260           token: dto.nodeMeta.token,
261           linkId: dto.nodeMeta.linkId,
262           headers: this.publicContextHeaders!,
263         }).resolveThread({ threadId: dto.threadId })
264       : new DocsApiPrivateRouteBuilder({ volumeId: dto.nodeMeta.volumeId, linkId: dto.nodeMeta.linkId }).resolveThread({
265           threadId: dto.threadId,
266         })
268     return this.routeExecutor.execute(route)
269   }
271   async unresolveThread(dto: {
272     nodeMeta: NodeMeta | PublicNodeMeta
273     threadId: string
274   }): Promise<ApiResult<UnresolveThreadResponse>> {
275     if (isPublicNodeMeta(dto.nodeMeta) && !this.publicContextHeaders) {
276       throw new Error('Public context headers not set')
277     }
279     const route = isPublicNodeMeta(dto.nodeMeta)
280       ? new DocsApiPublicRouteBuilder({
281           token: dto.nodeMeta.token,
282           linkId: dto.nodeMeta.linkId,
283           headers: this.publicContextHeaders!,
284         }).unresolveThread({ threadId: dto.threadId })
285       : new DocsApiPrivateRouteBuilder({
286           volumeId: dto.nodeMeta.volumeId,
287           linkId: dto.nodeMeta.linkId,
288         }).unresolveThread({
289           threadId: dto.threadId,
290         })
292     return this.routeExecutor.execute(route)
293   }
295   async changeSuggestionThreadState(dto: {
296     nodeMeta: NodeMeta | PublicNodeMeta
297     threadId: string
298     action: SuggestionThreadStateAction
299   }): Promise<ApiResult<UnresolveThreadResponse>> {
300     if (isPublicNodeMeta(dto.nodeMeta) && !this.publicContextHeaders) {
301       throw new Error('Public context headers not set')
302     }
304     const route = isPublicNodeMeta(dto.nodeMeta)
305       ? new DocsApiPublicRouteBuilder({
306           token: dto.nodeMeta.token,
307           linkId: dto.nodeMeta.linkId,
308           headers: this.publicContextHeaders!,
309         }).changeSuggestionState({ threadId: dto.threadId, action: dto.action })
310       : new DocsApiPrivateRouteBuilder({
311           volumeId: dto.nodeMeta.volumeId,
312           linkId: dto.nodeMeta.linkId,
313         }).changeSuggestionState({
314           threadId: dto.threadId,
315           action: dto.action,
316         })
318     return this.routeExecutor.execute(route)
319   }
321   async fetchExternalImageAsBase64(url: string): Promise<Result<string>> {
322     if (!this.imageProxyParams) {
323       return Result.fail('Image proxy params not set')
324     }
326     try {
327       this.routeExecutor.inflight++
328       const forgedImageURL = forgeImageURL({
329         url,
330         origin: window.location.origin,
331         apiUrl: this.imageProxyParams.apiUrl,
332         uid: this.imageProxyParams.uid,
333       })
334       const response = await fetch(forgedImageURL)
335       const blob = await response.blob()
336       if (!blob.type.startsWith('image/')) {
337         return Result.fail('Not an image')
338       }
339       const reader = new FileReader()
340       const base64 = await new Promise<string>((resolve) => {
341         reader.onload = () => {
342           resolve(reader.result as string)
343         }
344         reader.readAsDataURL(blob)
345       })
346       if (typeof base64 !== 'string') {
347         return Result.fail('Failed to convert image to base64')
348       }
349       return Result.ok(base64)
350     } catch (error) {
351       return Result.fail(getErrorString(error) || 'Unknown error')
352     } finally {
353       this.routeExecutor.inflight--
354     }
355   }