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 {
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,
48 window.addEventListener('beforeunload', this.handleWindowUnload)
51 handleWindowUnload = (event: BeforeUnloadEvent): void => {
52 if (this.routeExecutor.inflight !== 0) {
53 event.preventDefault()
57 async getDocumentMeta(lookup: NodeMeta | PublicNodeMeta): Promise<ApiResult<GetDocumentMetaResponse>> {
58 if (isPublicNodeMeta(lookup) && !this.publicContextHeaders) {
59 throw new Error('Public context headers not set')
62 const route = isPublicNodeMeta(lookup)
63 ? new DocsApiPublicRouteBuilder({
65 linkId: lookup.linkId,
66 headers: this.publicContextHeaders!,
68 : new DocsApiPrivateRouteBuilder({ volumeId: lookup.volumeId, linkId: lookup.linkId }).meta()
70 return this.routeExecutor.execute(route)
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')
78 const route = isPublicNodeMeta(lookup)
79 ? new DocsApiPublicRouteBuilder({
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)
89 async fetchRecentDocuments(): Promise<ApiResult<GetRecentsResponse>> {
90 const route = new DocsApiRouteBuilder('docs').recentDocuments()
91 return this.routeExecutor.execute(route)
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')
99 const route = new DocsApiPrivateRouteBuilder({
100 volumeId: docMeta.volumeId,
101 linkId: docMeta.linkId,
102 }).seedInitialCommit({
103 data: commit.serializeBinary(),
106 return this.routeExecutor.execute(route)
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)
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)
127 async createDocument(lookup: NodeMeta): Promise<ApiResult<CreateDocumentResponse>> {
128 const route = new DocsApiPrivateRouteBuilder({
129 volumeId: lookup.volumeId,
130 linkId: lookup.linkId,
133 return this.routeExecutor.execute(route)
136 async createRealtimeValetToken(
137 lookup: NodeMeta | PublicNodeMeta,
139 ): Promise<ApiResult<CreateValetTokenResponse>> {
140 if (isPublicNodeMeta(lookup) && !this.publicContextHeaders) {
141 throw new Error('Public context headers not set')
144 const route = isPublicNodeMeta(lookup)
145 ? new DocsApiPublicRouteBuilder({
147 linkId: lookup.linkId,
148 headers: this.publicContextHeaders!,
149 }).createRealtimeValetToken({ commitId })
150 : new DocsApiPrivateRouteBuilder({ volumeId: lookup.volumeId, linkId: lookup.linkId }).createRealtimeValetToken({
154 return this.routeExecutor.execute(route)
157 async getAllThreadIDs(nodeMeta: NodeMeta | PublicNodeMeta): Promise<ApiResult<GetAllThreadIDsResponse>> {
158 if (isPublicNodeMeta(nodeMeta) && !this.publicContextHeaders) {
159 throw new Error('Public context headers not set')
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)
174 dto: CreateThreadDTO,
175 entitlements: PublicDocumentEntitlements | DocumentEntitlements,
176 ): Promise<ApiResult<CreateThreadResponse>> {
177 const result = await this.apiCreateThread.execute(dto, entitlements)
181 async getThread(dto: {
182 nodeMeta: NodeMeta | PublicNodeMeta
184 }): Promise<ApiResult<GetCommentThreadResponse>> {
185 const result = await this.apiGetThread.execute(dto)
189 async deleteThread(dto: {
190 nodeMeta: NodeMeta | PublicNodeMeta
192 }): Promise<ApiResult<DeleteThreadResponse>> {
193 if (isPublicNodeMeta(dto.nodeMeta) && !this.publicContextHeaders) {
194 throw new Error('Public context headers not set')
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)
211 async addCommentToThread(
212 dto: AddCommentToThreadDTO,
213 entitlements: PublicDocumentEntitlements | DocumentEntitlements,
214 ): Promise<ApiResult<AddCommentToThreadResponse>> {
215 const result = await this.apiAddCommentToThread.execute(dto, entitlements)
221 entitlements: PublicDocumentEntitlements | DocumentEntitlements,
222 ): Promise<ApiResult<EditCommentResponse>> {
223 const result = await this.apiEditComment.execute(dto, entitlements)
227 async deleteComment(dto: {
228 nodeMeta: NodeMeta | PublicNodeMeta
231 }): Promise<ApiResult<DeleteCommentResponse>> {
232 if (isPublicNodeMeta(dto.nodeMeta) && !this.publicContextHeaders) {
233 throw new Error('Public context headers not set')
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,
247 return this.routeExecutor.execute(route)
250 async resolveThread(dto: {
251 nodeMeta: NodeMeta | PublicNodeMeta
253 }): Promise<ApiResult<ResolveThreadResponse>> {
254 if (isPublicNodeMeta(dto.nodeMeta) && !this.publicContextHeaders) {
255 throw new Error('Public context headers not set')
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,
268 return this.routeExecutor.execute(route)
271 async unresolveThread(dto: {
272 nodeMeta: NodeMeta | PublicNodeMeta
274 }): Promise<ApiResult<UnresolveThreadResponse>> {
275 if (isPublicNodeMeta(dto.nodeMeta) && !this.publicContextHeaders) {
276 throw new Error('Public context headers not set')
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,
289 threadId: dto.threadId,
292 return this.routeExecutor.execute(route)
295 async changeSuggestionThreadState(dto: {
296 nodeMeta: NodeMeta | PublicNodeMeta
298 action: SuggestionThreadStateAction
299 }): Promise<ApiResult<UnresolveThreadResponse>> {
300 if (isPublicNodeMeta(dto.nodeMeta) && !this.publicContextHeaders) {
301 throw new Error('Public context headers not set')
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,
318 return this.routeExecutor.execute(route)
321 async fetchExternalImageAsBase64(url: string): Promise<Result<string>> {
322 if (!this.imageProxyParams) {
323 return Result.fail('Image proxy params not set')
327 this.routeExecutor.inflight++
328 const forgedImageURL = forgeImageURL({
330 origin: window.location.origin,
331 apiUrl: this.imageProxyParams.apiUrl,
332 uid: this.imageProxyParams.uid,
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')
339 const reader = new FileReader()
340 const base64 = await new Promise<string>((resolve) => {
341 reader.onload = () => {
342 resolve(reader.result as string)
344 reader.readAsDataURL(blob)
346 if (typeof base64 !== 'string') {
347 return Result.fail('Failed to convert image to base64')
349 return Result.ok(base64)
351 return Result.fail(getErrorString(error) || 'Unknown error')
353 this.routeExecutor.inflight--