import * as signalR from "@microsoft/signalr" ; import * as signalRMsgPack from "@microsoft/signalr-protocol-msgpack" ; import { NotificationType } from "../enums/notificationType" ; import { ApiService } from "../abstractions/api.service" ; import { AppIdService } from "../abstractions/appId.service" ; import { EnvironmentService } from "../abstractions/environment.service" ; import { LogService } from "../abstractions/log.service" ; import { NotificationsService as NotificationsServiceAbstraction } from "../abstractions/notifications.service" ; import { StateService } from "../abstractions/state.service" ; import { SyncService } from "../abstractions/sync.service" ; import { VaultTimeoutService } from "../abstractions/vaultTimeout.service" ; import { NotificationResponse , SyncCipherNotification , SyncFolderNotification , SyncSendNotification , } from "../models/response/notificationResponse" ; export class NotificationsService implements NotificationsServiceAbstraction { private signalrConnection : signalR . HubConnection ; private url : string ; private connected = false ; private inited = false ; private inactive = false ; private reconnectTimer : any = null ; constructor ( private syncService , private appIdService , private apiService , private vaultTimeoutService , private environmentService , private logoutCallback , private logService , private stateService ) { this . environmentService . urls . subscribe ( ( ) => { if ( ! this . inited ) { return ; } this . init ( ) ; } ) ; } async init ( ) { this . inited = false ; this . url = this . environmentService . getNotificationsUrl ( ) ; if ( this . url === "https://-" ) { return ; } if ( this . signalrConnection != null ) { this . signalrConnection . off ( "ReceiveMessage" ) ; this . signalrConnection . off ( "Heartbeat" ) ; await this . signalrConnection . stop ( ) ; this . connected = false ; this . signalrConnection = null ; } this . signalrConnection = new signalR . HubConnectionBuilder ( ) . withUrl ( this . url + "/hub" , { accessTokenFactory : ( ) => this . apiService . getActiveBearerToken ( ) , skipNegotiation : true , transport : signalR . HttpTransportType . WebSockets , } ) . withHubProtocol ( new signalRMsgPack . MessagePackHubProtocol ( ) as signalR . IHubProtocol ) . build ( ) ; this . signalrConnection . on ( "ReceiveMessage" , ( data ) => this . processNotification ( new NotificationResponse ( data ) ) ) ; this . signalrConnection . on ( "Heartbeat" , ( data ) => { } ) ; this . signalrConnection . onclose ( ( ) => { this . connected = false ; this . reconnect ( true ) ; } ) ; this . inited = true ; if ( await this . isAuthedAndUnlocked ( ) ) { await this . reconnect ( false ) ; } } async updateConnection ( sync = false ) { if ( ! this . inited ) { return ; } try { if ( await this . isAuthedAndUnlocked ( ) ) { await this . reconnect ( sync ) ; } else { await this . signalrConnection . stop ( ) ; } } catch ( e ) { this . logService . error ( e . toString ( ) ) ; } } async reconnectFromActivity ( ) { this . inactive = false ; if ( this . inited && ! this . connected ) { await this . reconnect ( true ) ; } } async disconnectFromInactivity ( ) { this . inactive = true ; if ( this . inited && this . connected ) { await this . signalrConnection . stop ( ) ; } } private async processNotification ( notification ) { const appId = await this . appIdService . getAppId ( ) ; if ( notification == null || notification . contextId === appId ) { return ; } const isAuthenticated = await this . stateService . getIsAuthenticated ( ) ; const payloadUserId = notification . payload . userId || notification . payload . UserId ; const myUserId = await this . stateService . getUserId ( ) ; if ( isAuthenticated && payloadUserId != null && payloadUserId !== myUserId ) { return ; } switch ( notification . type ) { case NotificationType . SyncCipherCreate : case NotificationType . SyncCipherUpdate : await this . syncService . syncUpsertCipher ( notification . payload as SyncCipherNotification , notification . type === NotificationType . SyncCipherUpdate ) ; break ; case NotificationType . SyncCipherDelete : case NotificationType . SyncLoginDelete : await this . syncService . syncDeleteCipher ( notification . payload as SyncCipherNotification ) ; break ; case NotificationType . SyncFolderCreate : case NotificationType . SyncFolderUpdate : await this . syncService . syncUpsertFolder ( notification . payload as SyncFolderNotification , notification . type === NotificationType . SyncFolderUpdate ) ; break ; case NotificationType . SyncFolderDelete : await this . syncService . syncDeleteFolder ( notification . payload as SyncFolderNotification ) ; break ; case NotificationType . SyncVault : case NotificationType . SyncCiphers : case NotificationType . SyncSettings : if ( isAuthenticated ) { await this . syncService . fullSync ( false ) ; } break ; case NotificationType . SyncOrgKeys : if ( isAuthenticated ) { await this . syncService . fullSync ( true ) ; await this . signalrConnection . stop ( ) ; } break ; case NotificationType . LogOut : if ( isAuthenticated ) { this . logoutCallback ( ) ; } break ; case NotificationType . SyncSendCreate : case NotificationType . SyncSendUpdate : await this . syncService . syncUpsertSend ( notification . payload as SyncSendNotification , notification . type === NotificationType . SyncSendUpdate ) ; break ; case NotificationType . SyncSendDelete : await this . syncService . syncDeleteSend ( notification . payload as SyncSendNotification ) ; default : break ; } } private async reconnect ( sync ) { if ( this . reconnectTimer != null ) { clearTimeout ( this . reconnectTimer ) ; this . reconnectTimer = null ; } if ( this . connected || ! this . inited || this . inactive ) { return ; } const authedAndUnlocked = await this . isAuthedAndUnlocked ( ) ; if ( ! authedAndUnlocked ) { return ; } try { await this . signalrConnection . start ( ) ; this . connected = true ; if ( sync ) { await this . syncService . fullSync ( false ) ; } } catch ( e ) { this . logService . error ( e ) ; } if ( ! this . connected ) { this . reconnectTimer = setTimeout ( ( ) => this . reconnect ( sync ) , this . random ( 120000 , 300000 ) ) ; } } private async isAuthedAndUnlocked ( ) { if ( await this . stateService . getIsAuthenticated ( ) ) { const locked = await this . vaultTimeoutService . isLocked ( ) ; return ! locked ; } return false ; } private random ( min , max ) { min = Math . ceil ( min ) ; max = Math . floor ( max ) ; return Math . floor ( Math . random ( ) * ( max - min + 1 ) ) + min ; } }