import type { APIClient } from '@wireapp/api-client' ; import * as Events from '@wireapp/api-client/src/event' ; import type { Notification } from '@wireapp/api-client/src/notification/' ; import { CRUDEngine , error as StoreEngineError } from '@wireapp/store-engine' ; import { EventEmitter } from 'events' ; import logdown from 'logdown' ; import { PayloadBundle , PayloadBundleSource , PayloadBundleType } from '../conversation' ; import type { AssetContent } from '../conversation/content' ; import { ConversationMapper } from '../conversation/ConversationMapper' ; import * as OtrMessage from '../conversation/message/OtrMessage' ; import * as UserMessage from '../conversation/message/UserMessage' ; import { CoreError , NotificationError } from '../CoreError' ; import type { CryptographyService } from '../cryptography' ; import { UserMapper } from '../user/UserMapper' ; import { NotificationBackendRepository } from './NotificationBackendRepository' ; import { NotificationDatabaseRepository } from './NotificationDatabaseRepository' ; enum TOPIC { NOTIFICATION_ERROR = 'NotificationService.TOPIC.NOTIFICATION_ERROR' } export type NotificationHandler = ( notification : Notification , source : PayloadBundleSource ) => Promise < void > ; export interface NotificationService { 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 class NotificationService extends EventEmitter { private readonly apiClient : APIClient ; private readonly backend : NotificationBackendRepository ; private readonly cryptographyService : CryptographyService ; private readonly database : NotificationDatabaseRepository ; private readonly logger = logdown ( '@wireapp/core/notification/NotificationService' , { logger : console , markdown : false , } ) ; public static readonly TOPIC = TOPIC ; constructor ( apiClient , cryptographyService , storeEngine ) { super ( ) ; this . apiClient = apiClient ; this . cryptographyService = cryptographyService ; this . backend = new NotificationBackendRepository ( this . apiClient ) ; this . database = new NotificationDatabaseRepository ( storeEngine ) ; } public async getAllNotifications ( ) { const clientId = this . apiClient . clientId ; const lastNotificationId = await this . database . getLastNotificationId ( ) ; return this . backend . getAllNotifications ( clientId , lastNotificationId ) ; } /** Should only be called with a completely new client. */ public async initializeNotificationStream ( ) { const clientId = this . apiClient . clientId ; await this . setLastEventDate ( new Date ( 0 ) ) ; const latestNotification = await this . backend . getLastNotification ( clientId ) ; return this . setLastNotificationId ( latestNotification ) ; } public async hasHistory ( ) { const notificationEvents = await this . getNotificationEventList ( ) ; return ! ! notificationEvents . length ; } public getNotificationEventList ( ) { return this . database . getNotificationEventList ( ) ; } public async setLastEventDate ( eventDate ) { let databaseLastEventDate ; try { databaseLastEventDate = await this . database . getLastEventDate ( ) ; } catch ( error ) { if ( error instanceof StoreEngineError . RecordNotFoundError || ( error as Error ) . constructor . name === StoreEngineError . RecordNotFoundError . name ) { return this . database . createLastEventDate ( eventDate ) ; } throw error ; } if ( databaseLastEventDate && eventDate > databaseLastEventDate ) { return this . database . updateLastEventDate ( eventDate ) ; } return databaseLastEventDate ; } public async setLastNotificationId ( lastNotification ) { return this . database . updateLastNotificationId ( lastNotification ) ; } public async handleNotificationStream ( notificationHandler ) { const notifications = await this . getAllNotifications ( ) ; for ( const notification of notifications ) { await notificationHandler ( notification , PayloadBundleSource . NOTIFICATION_STREAM ) . catch ( error => this . logger . error ( error ) ) ; } } public readonly handleNotification : NotificationHandler = async ( notification , source ) : Promise < void > => { for ( const event of notification . payload ) { let data ; try { this . logger . log ( `Handling event of type "${ event . type }" for notification with ID "${ notification . id }"` , event ) ; data = await this . handleEvent ( event , source ) ; if ( ! notification . transient ) { await this . setLastNotificationId ( notification ) ; } } catch ( error ) { this . logger . error ( `There was an error with notification ID "${ notification . id }": ${ ( error as Error ) . message }` , error ) ; const notificationError = { error : error as Error , notification , type : CoreError . NOTIFICATION_ERROR , } ; this . emit ( NotificationService . TOPIC . NOTIFICATION_ERROR , notificationError ) ; continue ; } if ( data ) { switch ( data . type ) { case PayloadBundleType . ASSET_IMAGE : case PayloadBundleType . BUTTON_ACTION : case PayloadBundleType . CALL : case PayloadBundleType . CLIENT_ACTION : case PayloadBundleType . CLIENT_ADD : case PayloadBundleType . CLIENT_REMOVE : case PayloadBundleType . CONFIRMATION : case PayloadBundleType . CONNECTION_REQUEST : case PayloadBundleType . LOCATION : case PayloadBundleType . MESSAGE_DELETE : case PayloadBundleType . MESSAGE_EDIT : case PayloadBundleType . MESSAGE_HIDE : case PayloadBundleType . PING : case PayloadBundleType . REACTION : case PayloadBundleType . TEXT : case PayloadBundleType . USER_UPDATE : this . emit ( data . type , data ) ; break ; case PayloadBundleType . ASSET : { const assetContent = data . content as AssetContent ; const isMetaData = ! ! assetContent && ! ! assetContent . original && ! assetContent . uploaded ; const isAbort = ! ! assetContent . abortReason || ( ! assetContent . original && ! assetContent . uploaded ) ; if ( isMetaData ) { data . type = PayloadBundleType . ASSET_META ; this . emit ( PayloadBundleType . ASSET_META , data ) ; } else if ( isAbort ) { data . type = PayloadBundleType . ASSET_ABORT ; this . emit ( PayloadBundleType . ASSET_ABORT , data ) ; } else { this . emit ( PayloadBundleType . ASSET , data ) ; } break ; } case PayloadBundleType . TIMER_UPDATE : case PayloadBundleType . CONVERSATION_RENAME : case PayloadBundleType . CONVERSATION_CLEAR : case PayloadBundleType . MEMBER_JOIN : case PayloadBundleType . TYPING : this . emit ( data . type , event ) ; break ; } } else { const { type , conversation , from } = event as Events . ConversationEvent ; const conversationText = conversation ? ` in conversation "${ conversation }"` : '' ; const fromText = from ? ` from user "${ from }".` : '' ; this . logger . log ( `Received unsupported event "${ type }"${ conversationText }${ fromText }` , { event } ) ; } } } ; private async handleEvent ( event , source ) { switch ( event . type ) { case Events . CONVERSATION_EVENT . OTR_MESSAGE_ADD : { return this . cryptographyService . decodeGenericMessage ( event , source ) ; } case Events . CONVERSATION_EVENT . MEMBER_JOIN : case Events . CONVERSATION_EVENT . MESSAGE_TIMER_UPDATE : case Events . CONVERSATION_EVENT . RENAME : case Events . CONVERSATION_EVENT . TYPING : { const { conversation , from } = event ; const metaEvent = { ... event , conversation , from } ; return ConversationMapper . mapConversationEvent ( metaEvent , source ) ; } case Events . USER_EVENT . CONNECTION : case Events . USER_EVENT . CLIENT_ADD : case Events . USER_EVENT . UPDATE : case Events . USER_EVENT . CLIENT_REMOVE : { return UserMapper . mapUserEvent ( event , this . apiClient . context ! . userId , source ) ; } } } }