import type { APIClient } from '@wireapp/api-client' ; import { MessageSendingStatus , Conversation , CONVERSATION_TYPE , DefaultConversationRoleName , MutedStatus , NewConversation , QualifiedUserClients , UserClients , ClientMismatch , } from '@wireapp/api-client/src/conversation' ; import { CONVERSATION_TYPING , ConversationMemberUpdateData } from '@wireapp/api-client/src/conversation/data' ; import type { ConversationMemberLeaveEvent } from '@wireapp/api-client/src/event' ; import type { QualifiedId , QualifiedUserPreKeyBundleMap , UserPreKeyBundleMap } from '@wireapp/api-client/src/user' ; import { Asset , ButtonAction , ButtonActionConfirmation , Calling , Cleared , ClientAction , Composite , Confirmation , DataTransfer , Ephemeral , GenericMessage , Knock , LastRead , Location , MessageDelete , MessageEdit , MessageHide , Reaction , } from '@wireapp/protocol-messaging' ; import { AssetTransferState , GenericMessageType , MessageTimer , PayloadBundleSource , PayloadBundleState , PayloadBundleType , } from '../conversation/' ; import type { AssetContent , ClearedContent , DeletedContent , HiddenContent , RemoteData } from '../conversation/content/' ; import type { CryptographyService } from '../cryptography/' ; import { decryptAsset } from '../cryptography/AssetCryptography' ; import { isStringArray , isQualifiedIdArray , isQualifiedUserClients , isUserClients } from '../util/TypePredicateUtil' ; import { MessageBuilder } from './message/MessageBuilder' ; import { MessageService } from './message/MessageService' ; import { MessageToProtoMapper } from './message/MessageToProtoMapper' ; import type { ButtonActionConfirmationMessage , ButtonActionMessage , CallMessage , ClearConversationMessage , CompositeMessage , ConfirmationMessage , DeleteMessage , EditedTextMessage , FileAssetAbortMessage , FileAssetMessage , FileAssetMetaDataMessage , HideMessage , ImageAssetMessageOutgoing , LocationMessage , OtrMessage , PingMessage , ReactionMessage , ResetSessionMessage , TextMessage , } from './message/OtrMessage' ; export enum MessageTargetMode { NONE , USERS , USERS_CLIENTS } interface MessageSendingOptions { /**
     * The federated domain the server runs on. Should only be set for federation enabled envs
     */ conversationDomain ? : string ; /**
     * can be either a QualifiedId[] or QualfiedUserClients or undefined. The type has some effect on the behavior of the method.
     *    When given undefined the method will fetch both the members of the conversations and their devices. No ClientMismatch can happen in that case
     *    When given a QualifiedId[] the method will fetch the freshest list of devices for those users (since they are not given by the consumer). As a consequence no ClientMismatch error will trigger and we will ignore missing clients when sending
     *    When given a QualifiedUserClients the method will only send to the clients listed in the userIds. This could lead to ClientMismatch (since the given list of devices might not be the freshest one and new clients could have been created)
     */ userIds ? : string [ ] | QualifiedId [ ] | UserClients | QualifiedUserClients ; /**
     * Will send the message as a protobuf payload
     */ sendAsProtobuf ? : boolean ; nativePush ? : boolean ; /**
     * Will be called whenever there is a clientmismatch returned from the server. Needs to be combined with a userIds of type QualifiedUserClients
     */ onClientMismatch ? : MessageSendingCallbacks [ 'onClientMismatch' ] ; /**
     * Defines the behavior to use when a mismatch happens on backend side:
     *     - NONE -> Not a targetted message, we want to send to all the users/clients in the conversation. Will report all missing users and clients (default mode)
     *     - USERS -> A message targetted to all the clients of the given users (according to params.userIds). Will ignore missing users and only report missing clients for the given params.userIds
     *     - USERS_CLIENTS -> A message targetted at some specific clients of specific users (according to params.userIds). Will force sending the message even if users or clients are missing
     */ targetMode ? : MessageTargetMode ; } export interface MessageSendingCallbacks { param message return onStart ? : ( message : GenericMessage ) => void | boolean | Promise < boolean > ; onSuccess ? : ( message : GenericMessage , sentTime ? : string ) => void ; param status param wasSent return onClientMismatch ? : ( status : ClientMismatch | MessageSendingStatus , wasSent : boolean ) => void | boolean | Promise < boolean > ; } export class ConversationService { public readonly messageTimer : MessageTimer ; private readonly messageService : MessageService ; private selfConversationId ? : QualifiedId ; constructor ( private readonly apiClient , cryptographyService ) { this . messageTimer = new MessageTimer ( ) ; this . messageService = new MessageService ( this . apiClient , cryptographyService ) ; } private createEphemeral ( originalGenericMessage , expireAfterMillis ) { const ephemeralMessage = Ephemeral . create ( { expireAfterMillis , [ originalGenericMessage . content ! ] : originalGenericMessage [ originalGenericMessage . content ! ] , } ) ; const genericMessage = GenericMessage . create ( { [ GenericMessageType . EPHEMERAL ] : ephemeralMessage , messageId : originalGenericMessage . messageId , } ) ; return genericMessage ; } private async getConversationQualifiedMembers ( conversationId ) { const conversation = await this . apiClient . conversation . api . getConversation ( conversationId , true ) ; return ( conversation . members . others . filter ( member => ! ! member . qualified_id ) . map ( member => member . qualified_id ! ) . concat ( { domain : this . apiClient . context ! . domain ! , id : conversation . members . self . id } ) ) ; } param { QualifiedId [ ] | QualifiedUserClients } userIds returns { Promise < QualifiedUserPreKeyBundleMap } private async getQualifiedPreKeyBundle ( userIds ) { type Target = { id : QualifiedId ; clients ? : string [ ] ; } ; let targets = [ ] ; if ( userIds ) { if ( isQualifiedIdArray ( userIds ) ) { targets = userIds . map ( id => ( { id } ) ) ; } else { targets = Object . entries ( userIds ) . reduce < Target [ ] > ( ( accumulator , [ domain , userClients ] ) => { for ( const userId in userClients ) { accumulator . push ( { id : { id : userId , domain } , clients : userClients [ userId ] } ) ; } return accumulator ; } , [ ] ) ; } } const preKeys = await Promise . all ( targets . map ( async ( { id : userId , clients } ) => { const prekeyBundle = await this . apiClient . user . api . getUserPreKeys ( userId ) ; const userClients = clients ? prekeyBundle . clients . filter ( client => clients . includes ( client . client ) ) : prekeyBundle . clients ; return { user : userId , clients : userClients } ; } ) ) ; return preKeys . reduce < QualifiedUserPreKeyBundleMap > ( ( bundleMap , qualifiedPrekey ) => { bundleMap [ qualifiedPrekey . user . domain ] ||= { } ; for ( const client of qualifiedPrekey . clients ) { bundleMap [ qualifiedPrekey . user . domain ] [ qualifiedPrekey . user . id ] ||= { } ; bundleMap [ qualifiedPrekey . user . domain ] [ qualifiedPrekey . user . id ] [ client . client ] = client . prekey ; } return bundleMap ; } , { } ) ; } async getPreKeyBundleMap ( conversationId , userIds ? ) { let members = [ ] ; if ( userIds ) { if ( isStringArray ( userIds ) ) { members = userIds ; } else if ( isUserClients ( userIds ) ) { members = Object . keys ( userIds ) ; } } if ( ! members . length ) { const conversation = await this . apiClient . conversation . api . getConversation ( conversationId ) ; members = conversation . members . others . map ( member => member . id ) . concat ( conversation . members . self . id ) ; } const preKeys = await Promise . all ( members . map ( member => this . apiClient . user . api . getUserPreKeys ( member ) ) ) ; return preKeys . reduce ( ( bundleMap , bundle ) => { const userId = bundle . user ; bundleMap [ userId ] ||= { } ; for ( const client of bundle . clients ) { bundleMap [ userId ] [ client . client ] = client . prekey ; } return bundleMap ; } , { } ) ; } private async getSelfConversationId ( ) { if ( ! this . selfConversationId ) { const { userId } = this . apiClient . context ! ; const { qualified_id , id } = await this . apiClient . conversation . api . getConversation ( userId ) ; this . selfConversationId = qualified_id || { id , domain : '' } ; } return this . selfConversationId ; } private async getQualifiedRecipientsForConversation ( conversationId , userIds ? ) { if ( isQualifiedUserClients ( userIds ) ) { return userIds ; } const recipientIds = userIds || ( await this . getConversationQualifiedMembers ( conversationId ) ) ; return this . getQualifiedPreKeyBundle ( recipientIds ) ; } private async getRecipientsForConversation ( conversationId , userIds ? ) { if ( isUserClients ( userIds ) ) { return userIds ; } return this . getPreKeyBundleMap ( conversationId , userIds ) ; } param sendingClientId param conversationId param genericMessage return private async sendGenericMessage ( sendingClientId , conversationId , genericMessage , { conversationDomain , userIds , nativePush , sendAsProtobuf , onClientMismatch , targetMode = MessageTargetMode . NONE , } = { } ) { const plainText = GenericMessage . encode ( genericMessage ) . finish ( ) ; if ( targetMode !== MessageTargetMode . NONE && ! userIds ) { throw new Error ( 'Cannot send targetted message when no userIds are given' ) ; } if ( conversationDomain ) { if ( isStringArray ( userIds ) || isUserClients ( userIds ) ) { throw new Error ( 'Invalid userIds option for sending to federated backend' ) ; } const recipients = await this . getQualifiedRecipientsForConversation ( { id : conversationId , domain : conversationDomain } , userIds ) ; let reportMissing ; if ( targetMode === MessageTargetMode . NONE ) { reportMissing = isQualifiedUserClients ( userIds ) ; } else if ( targetMode === MessageTargetMode . USERS ) { reportMissing = this . extractQualifiedUserIds ( userIds ) ; } else { reportMissing = false ; } return this . messageService . sendFederatedMessage ( sendingClientId , recipients , plainText , { conversationId : { id : conversationId , domain : conversationDomain } , nativePush , reportMissing , onClientMismatch : mismatch => onClientMismatch ?. ( mismatch , false ) , } ) ; } if ( isQualifiedIdArray ( userIds ) || isQualifiedUserClients ( userIds ) ) { throw new Error ( 'Invalid userIds option for sending' ) ; } const recipients = await this . getRecipientsForConversation ( conversationId , userIds ) ; let reportMissing ; if ( targetMode === MessageTargetMode . NONE ) { reportMissing = isUserClients ( userIds ) ; } else if ( targetMode === MessageTargetMode . USERS ) { reportMissing = this . extractUserIds ( userIds ) ; } else { reportMissing = false ; } return this . messageService . sendMessage ( sendingClientId , recipients , plainText , { conversationId , sendAsProtobuf , nativePush , reportMissing , onClientMismatch : mistmatch => onClientMismatch ?. ( mistmatch , false ) , } ) ; } private extractUserIds ( userIds ? ) { if ( isUserClients ( userIds ) ) { return Object . keys ( userIds ) ; } return userIds ; } private extractQualifiedUserIds ( userIds ? ) { if ( isQualifiedUserClients ( userIds ) ) { return Object . entries ( userIds ) . reduce < QualifiedId [ ] > ( ( ids , [ domain , userClients ] ) => { return ids . concat ( Object . keys ( userClients ) . map ( userId => ( { domain , id : userId } ) ) ) ; } , [ ] ) ; } return userIds ; } private generateButtonActionGenericMessage ( payloadBundle ) { return GenericMessage . create ( { [ GenericMessageType . BUTTON_ACTION ] : ButtonAction . create ( payloadBundle . content ) , messageId : payloadBundle . id , } ) ; } private generateButtonActionConfirmationGenericMessage ( payloadBundle ) { return GenericMessage . create ( { [ GenericMessageType . BUTTON_ACTION_CONFIRMATION ] : ButtonActionConfirmation . create ( payloadBundle . content ) , messageId : payloadBundle . id , } ) ; } private generateCompositeGenericMessage ( payloadBundle ) { return GenericMessage . create ( { [ GenericMessageType . COMPOSITE ] : Composite . create ( payloadBundle . content ) , messageId : payloadBundle . id , } ) ; } private generateConfirmationGenericMessage ( payloadBundle ) { const content = Confirmation . create ( payloadBundle . content ) ; return GenericMessage . create ( { [ GenericMessageType . CONFIRMATION ] : content , messageId : payloadBundle . id , } ) ; } private generateEditedTextGenericMessage ( payloadBundle ) { const editedMessage = MessageEdit . create ( { replacingMessageId : payloadBundle . content . originalMessageId , text : MessageToProtoMapper . mapText ( payloadBundle ) , } ) ; return GenericMessage . create ( { [ GenericMessageType . EDITED ] : editedMessage , messageId : payloadBundle . id , } ) ; } private generateFileDataGenericMessage ( payloadBundle ) { if ( ! payloadBundle . content ) { throw new Error ( 'No content for sendFileData provided.' ) ; } const { asset , expectsReadConfirmation , legalHoldStatus } = payloadBundle . content ; const remoteData = Asset . RemoteData . create ( { assetId : asset . key , assetToken : asset . token , otrKey : asset . keyBytes , sha256 : asset . sha256 , assetDomain : asset . domain , } ) ; const assetMessage = Asset . create ( { expectsReadConfirmation , legalHoldStatus , uploaded : remoteData , } ) ; assetMessage . status = AssetTransferState . UPLOADED ; const genericMessage = GenericMessage . create ( { [ GenericMessageType . ASSET ] : assetMessage , messageId : payloadBundle . id , } ) ; const expireAfterMillis = this . messageTimer . getMessageTimer ( payloadBundle . conversation ) ; return expireAfterMillis > 0 ? this . createEphemeral ( genericMessage , expireAfterMillis ) : genericMessage ; } private generateFileMetaDataGenericMessage ( payloadBundle ) { if ( ! payloadBundle . content ) { throw new Error ( 'No content for sendFileMetaData provided.' ) ; } const { expectsReadConfirmation , legalHoldStatus , metaData } = payloadBundle . content ; const original = Asset . Original . create ( { audio : metaData . audio , mimeType : metaData . type , name : metaData . name , size : metaData . length , video : metaData . video , image : metaData . image , } ) ; const assetMessage = Asset . create ( { expectsReadConfirmation , legalHoldStatus , original , } ) ; const genericMessage = GenericMessage . create ( { [ GenericMessageType . ASSET ] : assetMessage , messageId : payloadBundle . id , } ) ; const expireAfterMillis = this . messageTimer . getMessageTimer ( payloadBundle . conversation ) ; return expireAfterMillis > 0 ? this . createEphemeral ( genericMessage , expireAfterMillis ) : genericMessage ; } private generateFileAbortGenericMessage ( payloadBundle ) { if ( ! payloadBundle . content ) { throw new Error ( 'No content for sendFileAbort provided.' ) ; } const { expectsReadConfirmation , legalHoldStatus , reason } = payloadBundle . content ; const assetMessage = Asset . create ( { expectsReadConfirmation , legalHoldStatus , notUploaded : reason , } ) ; assetMessage . status = AssetTransferState . NOT_UPLOADED ; const genericMessage = GenericMessage . create ( { [ GenericMessageType . ASSET ] : assetMessage , messageId : payloadBundle . id , } ) ; const expireAfterMillis = this . messageTimer . getMessageTimer ( payloadBundle . conversation ) ; return expireAfterMillis > 0 ? this . createEphemeral ( genericMessage , expireAfterMillis ) : genericMessage ; } private generateImageGenericMessage ( payloadBundle ) { if ( ! payloadBundle . content ) { throw new Error ( 'No content for sendImage provided.' ) ; } const { asset , expectsReadConfirmation , image , legalHoldStatus } = payloadBundle . content ; const imageMetadata = Asset . ImageMetaData . create ( { height : image . height , width : image . width , } ) ; const original = Asset . Original . create ( { [ GenericMessageType . IMAGE ] : imageMetadata , mimeType : image . type , name : null , size : image . data . length , } ) ; const remoteData = Asset . RemoteData . create ( { assetId : asset . key , assetToken : asset . token , assetDomain : asset . domain , otrKey : asset . keyBytes , sha256 : asset . sha256 , } ) ; const assetMessage = Asset . create ( { expectsReadConfirmation , legalHoldStatus , original , uploaded : remoteData , } ) ; assetMessage . status = AssetTransferState . UPLOADED ; let genericMessage = GenericMessage . create ( { [ GenericMessageType . ASSET ] : assetMessage , messageId : payloadBundle . id , } ) ; const expireAfterMillis = this . messageTimer . getMessageTimer ( payloadBundle . conversation ) ; if ( expireAfterMillis ) { genericMessage = this . createEphemeral ( genericMessage , expireAfterMillis ) ; } return { content : assetMessage as AssetContent , genericMessage } ; } private generateLocationGenericMessage ( payloadBundle ) { const { expectsReadConfirmation , latitude , legalHoldStatus , longitude , name , zoom } = payloadBundle . content ; const locationMessage = Location . create ( { expectsReadConfirmation , latitude , legalHoldStatus , longitude , name , zoom , } ) ; const genericMessage = GenericMessage . create ( { [ GenericMessageType . LOCATION ] : locationMessage , messageId : payloadBundle . id , } ) ; const expireAfterMillis = this . messageTimer . getMessageTimer ( payloadBundle . conversation ) ; return expireAfterMillis > 0 ? this . createEphemeral ( genericMessage , expireAfterMillis ) : genericMessage ; } private generatePingGenericMessage ( payloadBundle ) { const content = Knock . create ( payloadBundle . content ) ; const genericMessage = GenericMessage . create ( { [ GenericMessageType . KNOCK ] : content , messageId : payloadBundle . id , } ) ; const expireAfterMillis = this . messageTimer . getMessageTimer ( payloadBundle . conversation ) ; return expireAfterMillis > 0 ? this . createEphemeral ( genericMessage , expireAfterMillis ) : genericMessage ; } private generateReactionGenericMessage ( payloadBundle ) { const { legalHoldStatus , originalMessageId , type } = payloadBundle . content ; const reaction = Reaction . create ( { emoji : type , legalHoldStatus , messageId : originalMessageId , } ) ; const genericMessage = GenericMessage . create ( { [ GenericMessageType . REACTION ] : reaction , messageId : payloadBundle . id , } ) ; return genericMessage ; } private generateSessionResetGenericMessage ( payloadBundle ) { return GenericMessage . create ( { [ GenericMessageType . CLIENT_ACTION ] : ClientAction . RESET_SESSION , messageId : payloadBundle . id , } ) ; } private generateCallGenericMessage ( payloadBundle ) { const callMessage = Calling . create ( { content : payloadBundle . content , } ) ; return GenericMessage . create ( { [ GenericMessageType . CALLING ] : callMessage , messageId : payloadBundle . id , } ) ; } private generateTextGenericMessage ( payloadBundle ) { const genericMessage = GenericMessage . create ( { messageId : payloadBundle . id , [ GenericMessageType . TEXT ] : MessageToProtoMapper . mapText ( payloadBundle ) , } ) ; const expireAfterMillis = this . messageTimer . getMessageTimer ( payloadBundle . conversation ) ; return expireAfterMillis > 0 ? this . createEphemeral ( genericMessage , expireAfterMillis ) : genericMessage ; } public async clearConversation ( conversationId , timestamp = new Date ( ) , messageId = MessageBuilder . createId ( ) , sendAsProtobuf ? ) { if ( timestamp instanceof Date ) { timestamp = timestamp . getTime ( ) ; } const content = { clearedTimestamp : timestamp , conversationId , } ; const clearedMessage = Cleared . create ( content ) ; const genericMessage = GenericMessage . create ( { [ GenericMessageType . CLEARED ] : clearedMessage , messageId , } ) ; const { id : selfConversationId , domain } = await this . getSelfConversationId ( ) ; await this . sendGenericMessage ( this . apiClient . validatedClientId , selfConversationId , genericMessage , { conversationDomain : domain , sendAsProtobuf , } ) ; return { content , conversation : conversationId , from : this . apiClient . context ! . userId , id : messageId , messageTimer : 0 , source : PayloadBundleSource . LOCAL , state : PayloadBundleState . OUTGOING_SENT , timestamp : Date . now ( ) , type : PayloadBundleType . CONVERSATION_CLEAR , } ; } param conversationId param lastReadTimestamp param sendAsProtobuf return public async sendLastRead ( conversationId , lastReadTimestamp , sendAsProtobuf ? ) { const lastRead = new LastRead ( { conversationId , lastReadTimestamp , } ) ; const genericMessage = GenericMessage . create ( { [ GenericMessageType . LAST_READ ] : lastRead , messageId : MessageBuilder . createId ( ) , } ) ; const { id : selfConversationId , domain : selfConversationDomain } = await this . getSelfConversationId ( ) ; return this . sendGenericMessage ( this . apiClient . validatedClientId , selfConversationId , genericMessage , { conversationDomain : selfConversationDomain , sendAsProtobuf , } ) ; } param countlyId param sendAsProtobuf return public async sendCountlySync ( countlyId , sendAsProtobuf ? ) { const { id : selfConversationId , domain : selfConversationDomain } = await this . getSelfConversationId ( ) ; const dataTransfer = new DataTransfer ( { trackingIdentifier : { identifier : countlyId , } , } ) ; const genericMessage = new GenericMessage ( { [ GenericMessageType . DATA_TRANSFER ] : dataTransfer , messageId : MessageBuilder . createId ( ) , } ) ; return this . sendGenericMessage ( this . apiClient . validatedClientId , selfConversationId , genericMessage , { conversationDomain : selfConversationDomain , sendAsProtobuf , } ) ; } param { string } conversationId param { string } conversationDomain public getAllParticipantsClients ( conversationId , conversationDomain ? ) { const sendingClientId = this . apiClient . validatedClientId ; const recipients = { } ; const text = new Uint8Array ( ) ; return new Promise ( async ( resolve ) => { const onClientMismatch = ( mismatch ) => { resolve ( mismatch . missing ) ; return false ; } ; if ( conversationDomain ) { await this . messageService . sendFederatedMessage ( sendingClientId , recipients , text , { conversationId : { id : conversationId , domain : conversationDomain } , onClientMismatch , reportMissing : true , } ) ; } else { await this . messageService . sendMessage ( sendingClientId , recipients , text , { conversationId , onClientMismatch , } ) ; } } ) ; } public async deleteMessageLocal ( conversationId , messageIdToHide , sendAsProtobuf ? , conversationDomain ? ) { const messageId = MessageBuilder . createId ( ) ; const content = MessageHide . create ( { conversationId , messageId : messageIdToHide , } ) ; const genericMessage = GenericMessage . create ( { [ GenericMessageType . HIDDEN ] : content , messageId , } ) ; const { id : selfConversationId } = await this . getSelfConversationId ( ) ; await this . sendGenericMessage ( this . apiClient . validatedClientId , selfConversationId , genericMessage , { sendAsProtobuf , conversationDomain , } ) ; return { content , conversation : conversationId , from : this . apiClient . context ! . userId , id : messageId , messageTimer : this . messageTimer . getMessageTimer ( conversationId ) , source : PayloadBundleSource . LOCAL , state : PayloadBundleState . OUTGOING_SENT , timestamp : Date . now ( ) , type : PayloadBundleType . MESSAGE_HIDE , } ; } public async deleteMessageEveryone ( conversationId , messageIdToDelete , userIds ? , sendAsProtobuf ? , conversationDomain ? , callbacks ? ) { const messageId = MessageBuilder . createId ( ) ; const content = MessageDelete . create ( { messageId : messageIdToDelete , } ) ; const genericMessage = GenericMessage . create ( { [ GenericMessageType . DELETED ] : content , messageId , } ) ; callbacks ?. onStart ?. ( genericMessage ) ; const response = await this . sendGenericMessage ( this . apiClient . validatedClientId , conversationId , genericMessage , { userIds , sendAsProtobuf , conversationDomain , } ) ; callbacks ?. onSuccess ?. ( genericMessage , response ?. time ) ; return { content , conversation : conversationId , from : this . apiClient . context ! . userId , id : messageId , messageTimer : this . messageTimer . getMessageTimer ( conversationId ) , source : PayloadBundleSource . LOCAL , state : PayloadBundleState . OUTGOING_SENT , timestamp : Date . now ( ) , type : PayloadBundleType . MESSAGE_DELETE , } ; } public leaveConversation ( conversationId ) { return this . apiClient . conversation . api . deleteMember ( conversationId , this . apiClient . context ! . userId ) ; } public async leaveConversations ( conversationIds ? ) { if ( ! conversationIds ) { const conversation = await this . getConversations ( ) ; conversationIds = conversation . filter ( conversation => conversation . type === CONVERSATION_TYPE . REGULAR ) . map ( conversation => conversation . id ) ; } return Promise . all ( conversationIds . map ( conversationId => this . leaveConversation ( conversationId ) ) ) ; } public createConversation ( name , otherUserIds = [ ] ) { const ids = typeof otherUserIds === 'string' ? [ otherUserIds ] : otherUserIds ; const newConversation = { name , receipt_mode : null , users : ids , } ; return this . apiClient . conversation . api . postConversation ( newConversation ) ; } public async getConversations ( conversationId ) ; public async getConversations ( conversationIds ? ) ; public async getConversations ( conversationIds ? ) { if ( ! conversationIds || ! conversationIds . length ) { return this . apiClient . conversation . api . getAllConversations ( ) ; } if ( typeof conversationIds === 'string' ) { return this . apiClient . conversation . api . getConversation ( conversationIds ) ; } return this . apiClient . conversation . api . getConversationsByIds ( conversationIds ) ; } public async getAsset ( { assetId , assetToken , otrKey , sha256 } ) { const request = this . apiClient . asset . api . getAssetV3 ( assetId , assetToken ) ; const encryptedBuffer = ( await request . response ) . buffer ; return decryptAsset ( { cipherText : Buffer . from ( encryptedBuffer ) , keyBytes : Buffer . from ( otrKey ) , sha256 : Buffer . from ( sha256 ) , } ) ; } public async getUnencryptedAsset ( assetId , assetToken ? ) { const request = await this . apiClient . asset . api . getAssetV3 ( assetId , assetToken ) ; return ( await request . response ) . buffer ; } public async addUser < T extends string | string [ ] | QualifiedId | QualifiedId [ ] > ( conversationId , userIds ) { const ids = Array . isArray ( userIds ) ? userIds : [ userIds ] ; if ( isStringArray ( ids ) ) { await this . apiClient . conversation . api . postMembers ( conversationId , ids ) ; } else if ( isQualifiedIdArray ( ids ) ) { await this . apiClient . conversation . api . postMembersV2 ( conversationId , ids ) ; } return userIds ; } public async removeUser ( conversationId , userId ) { await this . apiClient . conversation . api . deleteMember ( conversationId , userId ) ; return userId ; } param params . payloadBundle param params . userIds return public async send < T extends OtrMessage = OtrMessage > ( { payloadBundle , userIds , sendAsProtobuf , conversationDomain , nativePush , targetMode , callbacks , } ) { let genericMessage ; let processedContent = undefined ; switch ( payloadBundle . type ) { case PayloadBundleType . ASSET : genericMessage = this . generateFileDataGenericMessage ( payloadBundle ) ; break ; case PayloadBundleType . ASSET_ABORT : genericMessage = this . generateFileAbortGenericMessage ( payloadBundle ) ; break ; case PayloadBundleType . ASSET_META : genericMessage = this . generateFileMetaDataGenericMessage ( payloadBundle ) ; break ; case PayloadBundleType . ASSET_IMAGE : const res = this . generateImageGenericMessage ( payloadBundle as ImageAssetMessageOutgoing ) ; genericMessage = res . genericMessage ; processedContent = res . content ; break ; case PayloadBundleType . BUTTON_ACTION : genericMessage = this . generateButtonActionGenericMessage ( payloadBundle ) ; break ; case PayloadBundleType . BUTTON_ACTION_CONFIRMATION : genericMessage = this . generateButtonActionConfirmationGenericMessage ( payloadBundle ) ; break ; case PayloadBundleType . CALL : genericMessage = this . generateCallGenericMessage ( payloadBundle ) ; break ; case PayloadBundleType . CLIENT_ACTION : { if ( payloadBundle . content . clientAction !== ClientAction . RESET_SESSION ) { throw new Error ( `No send method implemented for "${ payloadBundle . type }" and ClientAction "${ payloadBundle . content }".` ) ; } genericMessage = this . generateSessionResetGenericMessage ( payloadBundle ) ; break ; } case PayloadBundleType . COMPOSITE : genericMessage = this . generateCompositeGenericMessage ( payloadBundle ) ; break ; case PayloadBundleType . CONFIRMATION : genericMessage = this . generateConfirmationGenericMessage ( payloadBundle ) ; break ; case PayloadBundleType . LOCATION : genericMessage = this . generateLocationGenericMessage ( payloadBundle ) ; break ; case PayloadBundleType . MESSAGE_EDIT : genericMessage = this . generateEditedTextGenericMessage ( payloadBundle ) ; break ; case PayloadBundleType . PING : genericMessage = this . generatePingGenericMessage ( payloadBundle ) ; break ; case PayloadBundleType . REACTION : genericMessage = this . generateReactionGenericMessage ( payloadBundle ) ; break ; case PayloadBundleType . TEXT : genericMessage = this . generateTextGenericMessage ( payloadBundle ) ; break ; default : throw new Error ( `No send method implemented for "${ payloadBundle [ 'type' ] }".` ) ; } if ( ( await callbacks ?. onStart ?. ( genericMessage ) ) === false ) { return { ... payloadBundle , state : PayloadBundleState . CANCELLED } ; } const response = await this . sendGenericMessage ( this . apiClient . validatedClientId , payloadBundle . conversation , genericMessage , { userIds , sendAsProtobuf , conversationDomain , nativePush , targetMode , onClientMismatch : callbacks ?. onClientMismatch , } ) ; if ( ! response . errored ) { if ( ! this . isClearFromMismatch ( response ) ) { await callbacks ?. onClientMismatch ?. ( response , true ) ; } callbacks ?. onSuccess ?. ( genericMessage , response . time ) ; } return { ... payloadBundle , content : processedContent || payloadBundle . content , messageTimer : genericMessage . ephemeral ?. expireAfterMillis || 0 , state : response . errored ? PayloadBundleState . CANCELLED : PayloadBundleState . OUTGOING_SENT , } ; } public sendTypingStart ( conversationId ) { return this . apiClient . conversation . api . postTyping ( conversationId , { status : CONVERSATION_TYPING . STARTED } ) ; } public sendTypingStop ( conversationId ) { return this . apiClient . conversation . api . postTyping ( conversationId , { status : CONVERSATION_TYPING . STOPPED } ) ; } public setConversationMutedStatus ( conversationId , status , muteTimestamp ) { if ( typeof muteTimestamp === 'number' ) { muteTimestamp = new Date ( muteTimestamp ) ; } const payload = { otr_muted_ref : muteTimestamp . toISOString ( ) , otr_muted_status : status , } ; return this . apiClient . conversation . api . putMembershipProperties ( conversationId , payload ) ; } public toggleArchiveConversation ( conversationId , archived , archiveTimestamp = new Date ( ) ) { if ( typeof archiveTimestamp === 'number' ) { archiveTimestamp = new Date ( archiveTimestamp ) ; } const payload = { otr_archived : archived , otr_archived_ref : archiveTimestamp . toISOString ( ) , } ; return this . apiClient . conversation . api . putMembershipProperties ( conversationId , payload ) ; } public setMemberConversationRole ( conversationId , userId , conversationRole ) { return this . apiClient . conversation . api . putOtherMember ( userId , conversationId , { conversation_role : conversationRole , } ) ; } private isClearFromMismatch ( mismatch ) { const hasMissing = Object . keys ( mismatch . missing || { } ) . length > 0 ; const hasDeleted = Object . keys ( mismatch . deleted || { } ) . length > 0 ; const hasRedundant = Object . keys ( mismatch . redundant || { } ) . length > 0 ; const hasFailed = Object . keys ( ( mismatch as MessageSendingStatus ) . failed_to_send || { } ) . length > 0 ; return ! hasMissing && ! hasDeleted && ! hasRedundant && ! hasFailed ; } }