import type { AxiosError } from 'axios' ; import { StatusCodes as HTTP_STATUS } from 'http-status-codes' ; import { APIClient } from '@wireapp/api-client' ; import type { RegisterData } from '@wireapp/api-client/src/auth' ; import { AUTH_COOKIE_KEY , AUTH_TABLE_NAME , Context , Cookie , CookieStore , LoginData } from '@wireapp/api-client/src/auth/' ; import { ClientType , RegisteredClient } from '@wireapp/api-client/src/client/' ; import * as Events from '@wireapp/api-client/src/event' ; import { WebSocketClient } from '@wireapp/api-client/src/tcp/' ; import * as cryptobox from '@wireapp/cryptobox' ; import { CRUDEngine , MemoryEngine , error as StoreEngineError } from '@wireapp/store-engine' ; import { EventEmitter } from 'events' ; import logdown from 'logdown' ; import { LoginSanitizer } from './auth/' ; import { BroadcastService } from './broadcast/' ; import { ClientInfo , ClientService } from './client/' ; import { ConnectionService } from './connection/' ; import { AssetService , ConversationService , PayloadBundle , PayloadBundleSource , PayloadBundleType , } from './conversation/' ; import * as OtrMessage from './conversation/message/OtrMessage' ; import * as UserMessage from './conversation/message/UserMessage' ; import type { CoreError , NotificationError } from './CoreError' ; import { CryptographyService } from './cryptography/' ; import { GiphyService } from './giphy/' ; import { NotificationHandler , NotificationService } from './notification/' ; import { SelfService } from './self/' ; import { TeamService } from './team/' ; import { UserService } from './user/' ; import { AccountService } from './account/' ; import { LinkPreviewService } from './linkPreview' ; enum TOPIC { ERROR = 'Account.TOPIC.ERROR' } export interface Account { on ( event , listener ) : this ; on ( event , listener ) : this ; on ( event , listener ) : this ; on ( event , listener ) : this ; on ( event , listener ) : this ; on ( event , listener ) : this ; on ( event , listener ) : this ; on ( event , listener ) : this ; on ( event , listener ) : this ; on ( event , listener ) : this ; on ( event , listener ) : this ; on ( event , listener ) : this ; on ( event , listener ) : this ; on ( event , listener ) : this ; on ( event , listener ) : this ; on ( event , listener ) : this ; on ( event , listener ) : this ; on ( event , listener ) : this ; on ( event , listener ) : this ; on ( event , listener ) : this ; on ( event , listener ) : this ; on ( event , listener ) : this ; on ( event , listener ) : this ; on ( event , listener ) : this ; on ( event , listener ) : this ; on ( event , listener ) : this ; } export type StoreEngineProvider = ( storeName : string ) => Promise < CRUDEngine > ; type AccountConfig = { federationDomain ? : string ; } ; export class Account extends EventEmitter { private readonly apiClient : APIClient ; private readonly logger : logdown . Logger ; private readonly storeEngineProvider : StoreEngineProvider ; private storeEngine ? : CRUDEngine ; public static readonly TOPIC = TOPIC ; public service ? : { account : AccountService ; asset : AssetService ; broadcast : BroadcastService ; client : ClientService ; connection : ConnectionService ; conversation : ConversationService ; cryptography : CryptographyService ; giphy : GiphyService ; linkPreview : LinkPreviewService ; notification : NotificationService ; self : SelfService ; team : TeamService ; user : UserService ; } ; param apiClient param storeEngineProvider param config . federationDomain constructor ( apiClient = new APIClient ( ) , storeEngineProvider ? , private readonly config ? ) { super ( ) ; this . apiClient = apiClient ; if ( storeEngineProvider ) { this . storeEngineProvider = storeEngineProvider ; } else { this . storeEngineProvider = async ( storeName ) => { const engine = new MemoryEngine ( ) ; await engine . init ( storeName ) ; return engine ; } ; } apiClient . on ( APIClient . TOPIC . COOKIE_REFRESH , async ( cookie ? ) => { if ( cookie && this . storeEngine ) { try { await this . persistCookie ( this . storeEngine , cookie ) ; } catch ( error ) { this . logger . error ( `Failed to save cookie: ${ ( error as Error ) . message }` , error ) ; } } } ) ; this . logger = logdown ( '@wireapp/core/Account' , { logger : console , markdown : false , } ) ; } private persistCookie ( storeEngine , cookie ) { const entity = { expiration : cookie . expiration , zuid : cookie . zuid } ; return storeEngine . updateOrCreate ( AUTH_TABLE_NAME , AUTH_COOKIE_KEY , entity ) ; } get clientId ( ) : string { return this . apiClient . validatedClientId ; } get userId ( ) : string { return this . apiClient . validatedUserId ; } public async register ( registration , clientType ) { const context = await this . apiClient . register ( registration , clientType ) ; const storeEngine = await this . initEngine ( context ) ; await this . initServices ( storeEngine ) ; return context ; } public async init ( clientType , cookie ? , initializedStoreEngine ? ) { const context = await this . apiClient . init ( clientType , cookie ) ; if ( initializedStoreEngine ) { this . storeEngine = initializedStoreEngine ; this . logger . log ( `Initialized store with existing engine "${ this . storeEngine . storeName }".` ) ; } else { this . storeEngine = await this . initEngine ( context ) ; } await this . initServices ( this . storeEngine ) ; if ( initializedStoreEngine ) { await this . initClient ( { clientType , } ) ; } return context ; } public async initServices ( storeEngine ) { const accountService = new AccountService ( this . apiClient ) ; const assetService = new AssetService ( this . apiClient ) ; const cryptographyService = new CryptographyService ( this . apiClient , storeEngine , this . config ) ; const clientService = new ClientService ( this . apiClient , storeEngine , cryptographyService ) ; const connectionService = new ConnectionService ( this . apiClient ) ; const giphyService = new GiphyService ( this . apiClient ) ; const linkPreviewService = new LinkPreviewService ( assetService ) ; const conversationService = new ConversationService ( this . apiClient , cryptographyService ) ; const notificationService = new NotificationService ( this . apiClient , cryptographyService , storeEngine ) ; const selfService = new SelfService ( this . apiClient ) ; const teamService = new TeamService ( this . apiClient ) ; const broadcastService = new BroadcastService ( this . apiClient , cryptographyService ) ; const userService = new UserService ( this . apiClient , broadcastService , conversationService , connectionService ) ; this . service = { account : accountService , asset : assetService , broadcast : broadcastService , client : clientService , connection : connectionService , conversation : conversationService , cryptography : cryptographyService , giphy : giphyService , linkPreview : linkPreviewService , notification : notificationService , self : selfService , team : teamService , user : userService , } ; } public async login ( loginData , initClient = true , clientInfo ? , initializedStoreEngine ? ) { this . resetContext ( ) ; LoginSanitizer . removeNonPrintableCharacters ( loginData ) ; const context = await this . apiClient . login ( loginData ) ; if ( initializedStoreEngine ) { this . storeEngine = initializedStoreEngine ; this . logger . log ( `Initialized store with existing engine "${ this . storeEngine . storeName }".` ) ; } else { this . storeEngine = await this . initEngine ( context ) ; } await this . initServices ( this . storeEngine ) ; if ( initClient ) { await this . initClient ( loginData , clientInfo ) ; } return context ; } public async initClient ( loginData , clientInfo ? ) { if ( ! this . service ) { throw new Error ( 'Services are not set.' ) ; } try { const localClient = await this . loadAndValidateLocalClient ( ) ; return { isNewClient : false , localClient } ; } catch ( error ) { const notFoundInDatabase = error instanceof cryptobox . error . CryptoboxError || ( error as Error ) . constructor . name === 'CryptoboxError' || error instanceof StoreEngineError . RecordNotFoundError || ( error as Error ) . constructor . name === StoreEngineError . RecordNotFoundError . name ; const notFoundOnBackend = ( error as AxiosError ) . response ?. status === HTTP_STATUS . NOT_FOUND ; if ( notFoundInDatabase ) { this . logger . log ( `Could not find valid client in database "${ this . storeEngine ?. storeName }".` ) ; return this . registerClient ( loginData , clientInfo ) ; } if ( notFoundOnBackend ) { this . logger . log ( 'Could not find valid client on backend' ) ; const client = await this . service ! . client . getLocalClient ( ) ; const shouldDeleteWholeDatabase = client . type === ClientType . TEMPORARY ; if ( shouldDeleteWholeDatabase ) { this . logger . log ( 'Last client was temporary - Deleting database' ) ; if ( this . storeEngine ) { await this . storeEngine . clearTables ( ) ; } const context = await this . apiClient . init ( loginData . clientType ) ; await this . initEngine ( context ) ; return this . registerClient ( loginData , clientInfo ) ; } this . logger . log ( 'Last client was permanent - Deleting cryptography stores' ) ; await this . service ! . cryptography . deleteCryptographyStores ( ) ; return this . registerClient ( loginData , clientInfo ) ; } throw error ; } } public async loadAndValidateLocalClient ( ) { await this . service ! . cryptography . initCryptobox ( ) ; const loadedClient = await this . service ! . client . getLocalClient ( ) ; await this . apiClient . client . api . getClient ( loadedClient . id ) ; this . apiClient . context ! . clientId = loadedClient . id ; return loadedClient ; } private async registerClient ( loginData , clientInfo ? ) { if ( ! this . service ) { throw new Error ( 'Services are not set.' ) ; } const registeredClient = await this . service . client . register ( loginData , clientInfo ) ; this . apiClient . context ! . clientId = registeredClient . id ; this . logger . log ( 'Client is created' ) ; await this . service ! . notification . initializeNotificationStream ( ) ; await this . service ! . client . synchronizeClients ( ) ; return { isNewClient : true , localClient : registeredClient } ; } private resetContext ( ) { delete this . apiClient . context ; delete this . service ; } public async logout ( ) { await this . apiClient . logout ( ) ; this . resetContext ( ) ; } public async listen ( notificationHandler = this . service ! . notification . handleNotification ) { if ( ! this . apiClient . context ) { throw new Error ( 'Context is not set - please login first' ) ; } this . apiClient . transport . ws . removeAllListeners ( WebSocketClient . TOPIC . ON_MESSAGE ) ; this . apiClient . transport . ws . on ( WebSocketClient . TOPIC . ON_MESSAGE , notification => { notificationHandler ( notification , PayloadBundleSource . WEBSOCKET ) . catch ( error => { this . logger . error ( `Failed to handle notification ID "${ notification . id }": ${ error . message }` , error ) ; } ) ; } ) ; this . service ! . notification . removeAllListeners ( NotificationService . TOPIC . NOTIFICATION_ERROR ) ; this . service ! . notification . on ( NotificationService . TOPIC . NOTIFICATION_ERROR , this . handleError ) ; for ( const payloadType of Object . values ( PayloadBundleType ) ) { this . service ! . notification . removeAllListeners ( payloadType ) ; this . service ! . notification . on ( payloadType as any , this . handlePayload ) ; } const onBeforeConnect = async ( ) => this . service ! . notification . handleNotificationStream ( notificationHandler ) ; await this . apiClient . connect ( onBeforeConnect ) ; return this ; } private readonly handlePayload = async ( payload ) : Promise < void > => { switch ( payload . type ) { case PayloadBundleType . TIMER_UPDATE : { const { data : { message_timer } , conversation , } = payload as unknown as Events . ConversationMessageTimerUpdateEvent ; const expireAfterMillis = Number ( message_timer ) ; this . service ! . conversation . messageTimer . setConversationLevelTimer ( conversation , expireAfterMillis ) ; break ; } } this . emit ( payload . type , payload ) ; } ; private readonly handleError = ( accountError ) : void => { this . emit ( Account . TOPIC . ERROR , accountError ) ; } ; private async initEngine ( context ) { const clientType = context . clientType === ClientType . NONE ? '' : `@${ context . clientType }` ; const dbName = `wire@${ this . apiClient . config . urls . name }@${ context . userId }${ clientType }` ; this . logger . log ( `Initialising store with name "${ dbName }"...` ) ; this . storeEngine = await this . storeEngineProvider ( dbName ) ; const cookie = CookieStore . getCookie ( ) ; if ( cookie ) { await this . persistCookie ( this . storeEngine , cookie ) ; } return this . storeEngine ; } }