import axios , { AxiosRequestConfig } from 'axios' ; import { AssetRetentionPolicy } from './AssetRetentionPolicy' ; import { base64MD5FromBuffer , concatToBuffer } from '../shims/node/buffer' ; import { BackendError , handleProgressEvent , HttpClient , ProgressCallback , RequestCancelable , SyntheticErrorLabel , } from '../http/' ; import { isValidToken , isValidUUID } from './AssetUtil' ; import { RequestCancellationError } from '../user' ; import { unsafeAlphanumeric } from '../shims/node/random' ; import type { AssetUploadData } from './AssetUploadData' ; export interface CipherOptions { /** Set a custom algorithm for encryption */ algorithm ? : string ; /** Set a custom hash for encryption */ hash ? : Buffer ; } export interface AssetOptions extends CipherOptions { public ? : boolean ; retention ? : AssetRetentionPolicy ; /** If given, will upload an asset that can be shared in a federated env */ domain ? : string ; } export interface AssetResponse { buffer : ArrayBuffer ; mimeType : string ; } export class AssetAPI { private static readonly ASSET_V3_URL = '/assets/v3' ; private static readonly ASSET_V4_URL = '/assets/v4' ; private static readonly ASSET_SERVICE_URL = '/bot/assets' ; private static readonly ASSET_V2_URL = '/otr/assets' ; private static readonly ASSET_V2_CONVERSATION_URL = '/conversations' ; private static readonly ASSET_V1_URL = '/assets' ; constructor ( private readonly client ) { } private getAssetShared ( assetUrl , token ? , forceCaching = false , progressCallback ? ) { if ( token && ! isValidToken ( token ) ) { throw new TypeError ( `Expected token "${ token . substr ( 0 , 5 ) }..." (redacted) to be base64 encoded string.` ) ; } const cancelSource = axios . CancelToken . source ( ) ; const config = { cancelToken : cancelSource . token , method : 'get' , onDownloadProgress : handleProgressEvent ( progressCallback ) , onUploadProgress : handleProgressEvent ( progressCallback ) , params : { } , responseType : 'arraybuffer' , url : assetUrl , } ; if ( token ) { config . params . asset_token = token ; } if ( forceCaching ) { config . params . forceCaching = forceCaching ; } const handleRequest = async ( ) : Promise < AssetResponse > => { try { const response = await this . client . sendRequest < ArrayBuffer > ( config , true ) ; return { buffer : response . data , mimeType : response . headers [ 'content-type' ] , } ; } catch ( error ) { if ( ( error as BackendError ) . message === SyntheticErrorLabel . REQUEST_CANCELLED ) { throw new RequestCancellationError ( 'Asset download got cancelled.' ) ; } throw error ; } } ; return { cancel : ( ) => cancelSource . cancel ( SyntheticErrorLabel . REQUEST_CANCELLED ) , response : handleRequest ( ) , } ; } private postAssetShared ( assetBaseUrl , asset , options ? , progressCallback ? ) { const BOUNDARY = `Frontier${ unsafeAlphanumeric ( ) }` ; const metadata = JSON . stringify ( { public : options ?. public ?? true , retention : options ?. retention || AssetRetentionPolicy . PERSISTENT , domain : options ?. domain , } ) ; const body = `--${ BOUNDARY }\r\n` + 'Content-Type: application/json;charset=utf-8\r\n' + `Content-length: ${ metadata . length }\r\n` + '\r\n' + `${ metadata }\r\n` + `--${ BOUNDARY }\r\n` + 'Content-Type: application/octet-stream\r\n' + `Content-length: ${ asset . length }\r\n` + `Content-MD5: ${ base64MD5FromBuffer ( asset . buffer ) }\r\n` + '\r\n' ; const footer = `\r\n--${ BOUNDARY }--\r\n` ; const cancelSource = axios . CancelToken . source ( ) ; const config = { cancelToken : cancelSource . token , data : concatToBuffer ( body , asset , footer ) , headers : { 'Content-Type' : `multipart/mixed; boundary=${ BOUNDARY }` , } , method : 'post' , onDownloadProgress : handleProgressEvent ( progressCallback ) , onUploadProgress : handleProgressEvent ( progressCallback ) , url : assetBaseUrl , } ; const handleRequest = async ( ) : Promise < AssetUploadData > => { try { const response = await this . client . sendRequest < AssetUploadData > ( config ) ; return response . data ; } catch ( error ) { if ( ( error as BackendError ) . message === SyntheticErrorLabel . REQUEST_CANCELLED ) { throw new RequestCancellationError ( 'Asset upload got cancelled.' ) ; } throw error ; } } ; return { cancel : ( ) => cancelSource . cancel ( SyntheticErrorLabel . REQUEST_CANCELLED ) , response : handleRequest ( ) , } ; } getAssetV1 ( assetId , conversationId , forceCaching = false , progressCallback ? ) { if ( ! isValidUUID ( assetId ) ) { throw new TypeError ( `Expected asset ID "${ assetId }" to only contain alphanumeric values and dashes.` ) ; } if ( ! isValidUUID ( conversationId ) ) { throw new TypeError ( `Expected conversation ID "${ conversationId }" to only contain alphanumeric values and dashes.` ) ; } const cancelSource = axios . CancelToken . source ( ) ; const config = { cancelToken : cancelSource . token , method : 'get' , onDownloadProgress : handleProgressEvent ( progressCallback ) , onUploadProgress : handleProgressEvent ( progressCallback ) , params : { conv_id : conversationId , } , responseType : 'arraybuffer' , url : `${ AssetAPI . ASSET_V1_URL }/${ assetId }` , } ; if ( forceCaching ) { config . params . forceCaching = forceCaching ; } const handleRequest = async ( ) : Promise < AssetResponse > => { try { const response = await this . client . sendRequest < ArrayBuffer > ( config , true ) ; return { buffer : response . data , mimeType : response . headers [ 'content-type' ] , } ; } catch ( error ) { if ( ( error as BackendError ) . message === SyntheticErrorLabel . REQUEST_CANCELLED ) { throw new RequestCancellationError ( 'Asset download got cancelled.' ) ; } throw error ; } } ; return { cancel : ( ) => cancelSource . cancel ( SyntheticErrorLabel . REQUEST_CANCELLED ) , response : handleRequest ( ) , } ; } getAssetV2 ( assetId , conversationId , forceCaching = false , progressCallback ? ) { if ( ! isValidUUID ( assetId ) ) { throw new TypeError ( `Expected asset ID "${ assetId }" to only contain alphanumeric values and dashes.` ) ; } if ( ! isValidUUID ( conversationId ) ) { throw new TypeError ( `Expected conversation ID "${ conversationId }" to only contain alphanumeric values and dashes.` ) ; } const cancelSource = axios . CancelToken . source ( ) ; const config = { cancelToken : cancelSource . token , method : 'get' , onDownloadProgress : handleProgressEvent ( progressCallback ) , onUploadProgress : handleProgressEvent ( progressCallback ) , params : { } , responseType : 'arraybuffer' , url : `${ AssetAPI . ASSET_V2_CONVERSATION_URL }/${ conversationId }${ AssetAPI . ASSET_V2_URL }/${ assetId }` , } ; if ( forceCaching ) { config . params . forceCaching = forceCaching ; } const handleRequest = async ( ) : Promise < AssetResponse > => { try { const response = await this . client . sendRequest < ArrayBuffer > ( config , true ) ; return { buffer : response . data , mimeType : response . headers [ 'content-type' ] , } ; } catch ( error ) { if ( ( error as BackendError ) . message === SyntheticErrorLabel . REQUEST_CANCELLED ) { throw new RequestCancellationError ( 'Asset download got cancelled.' ) ; } throw error ; } } ; return { cancel : ( ) => cancelSource . cancel ( SyntheticErrorLabel . REQUEST_CANCELLED ) , response : handleRequest ( ) , } ; } getAssetV3 ( assetId , token ? , forceCaching = false , progressCallback ? ) { if ( ! isValidUUID ( assetId ) ) { throw new TypeError ( `Expected asset ID "${ assetId }" to only contain alphanumeric values and dashes.` ) ; } const assetBaseUrl = `${ AssetAPI . ASSET_V3_URL }/${ assetId }` ; return this . getAssetShared ( assetBaseUrl , token , forceCaching , progressCallback ) ; } getAssetV4 ( assetId , assetDomain , token ? , forceCaching = false , progressCallback ? ) { if ( ! isValidUUID ( assetId ) ) { throw new TypeError ( `Expected asset ID "${ assetId }" to only contain alphanumeric values and dashes.` ) ; } const assetBaseUrl = `${ AssetAPI . ASSET_V4_URL }/${ assetDomain }/${ assetId }` ; return this . getAssetShared ( assetBaseUrl , token , forceCaching , progressCallback ) ; } getServiceAsset ( assetId , token ? , forceCaching = false , progressCallback ? ) { if ( ! isValidUUID ( assetId ) ) { throw new TypeError ( `Expected asset ID "${ assetId }" to only contain alphanumeric values and dashes.` ) ; } const assetBaseUrl = `${ AssetAPI . ASSET_SERVICE_URL }/${ assetId }` ; return this . getAssetShared ( assetBaseUrl , token , forceCaching , progressCallback ) ; } param asset param options param progressCallback postAsset ( asset , options ? , progressCallback ? ) { return this . postAssetShared ( AssetAPI . ASSET_V3_URL , asset , options , progressCallback ) ; } postServiceAsset ( asset , options ? , progressCallback ? ) { const assetBaseUrl = AssetAPI . ASSET_SERVICE_URL ; return this . postAssetShared ( assetBaseUrl , asset , options , progressCallback ) ; } }