import { Injectable , NgZone } from '@angular/core' ; import { Client , xml } from '@xmpp/client' ; import { JID } from '@xmpp/jid' ; import { Element } from 'ltx' ; import { BehaviorSubject , Subject } from 'rxjs' ; import { LogInRequest } from '../../../core/log-in-request' ; import { IqResponseStanza , Stanza } from '../../../core/stanza' ; import { LogService } from '../../log.service' ; import { IqResponseError } from './iq-response.error' ; import { XmppClientFactoryService } from './xmpp-client-factory.service' ; export type XmppChatStates = 'disconnected' | 'online' | 'reconnecting' ; see https see https see https @ Injectable ( ) export class XmppChatConnectionService { public readonly state$ = new BehaviorSubject < XmppChatStates > ( 'disconnected' ) ; public readonly stanzaUnknown$ = new Subject < Stanza > ( ) ; /**
     * User JID with resource, not bare.
     */ public userJid ? : JID ; private iqId = new Date ( ) . getTime ( ) ; private readonly iqStanzaResponseHandlers = new Map < string , ( stanza : IqResponseStanza ) => void > ( ) ; public client ? : Client ; constructor ( private readonly logService , private readonly ngZone , private readonly xmppClientFactoryService ) { } public onOnline ( jid ) { this . logService . info ( 'online =' , 'online as' , jid . toString ( ) ) ; this . userJid = jid ; this . state$ . next ( 'online' ) ; } public async sendPresence ( ) { await this . send ( xml ( 'presence' ) ) ; } public async send ( content ) { this . logService . debug ( '>>>' , content ) ; await this . client . send ( content ) ; } public sendIq ( request ) { return new Promise ( ( resolve , reject ) => { request . attrs = { id : this . getNextIqId ( ) , from : this . userJid . toString ( ) , ... request . attrs , } ; const { id } = request . attrs ; if ( ! request . attrs . type ) { const message = 'iq stanza without type: ' + request . toString ( ) ; this . logService . error ( message ) ; throw new Error ( message ) ; } this . iqStanzaResponseHandlers . set ( id , ( response ) => { if ( response . attrs . type === 'result' ) { resolve ( response as IqResponseStanza < 'result' > ) ; } else { const errorResponse = response as IqResponseStanza < 'error' > ; reject ( new IqResponseError ( errorResponse ) ) ; } } ) ; this . send ( request ) . catch ( ( e ) => { this . logService . error ( 'error sending iq' , e ) ; this . iqStanzaResponseHandlers . delete ( id ) ; reject ( e ) ; } ) ; } ) ; } public async sendIqAckResult ( id ) { await this . send ( xml ( 'iq' , { from : this . userJid . toString ( ) , id , type : 'result' } ) ) ; } public onStanzaReceived ( stanza ) { let handled = false ; if ( this . isIqStanzaResponse ( stanza ) ) { const handleIqStanzaResponse = this . iqStanzaResponseHandlers . get ( stanza . attrs . id ) ; if ( handleIqStanzaResponse ) { this . logService . debug ( '<<<' , stanza . toString ( ) , 'handled by callback' , handleIqStanzaResponse ) ; this . iqStanzaResponseHandlers . delete ( stanza . attrs . id ) ; handleIqStanzaResponse ( stanza ) ; handled = true ; } } if ( ! handled ) { this . stanzaUnknown$ . next ( stanza ) ; } } private isIqStanzaResponse ( stanza ) { const stanzaType = stanza . attrs . type ; return stanza . name === 'iq' && ( stanzaType === 'result' || stanzaType === 'error' ) ; } async logIn ( logInRequest ) { await this . ngZone . runOutsideAngular ( async ( ) => { if ( logInRequest . username . indexOf ( '@' ) >= 0 ) { this . logService . warn ( 'username should not contain domain, only local part, this can lead to errors!' ) ; } this . client = this . xmppClientFactoryService . client ( logInRequest ) ; this . client . on ( 'error' , ( err ) => { this . ngZone . run ( ( ) => { this . logService . error ( 'chat service error =>' , err . toString ( ) , err ) ; } ) ; } ) ; this . client . on ( 'status' , ( status , value ) => { this . ngZone . run ( ( ) => { this . logService . info ( 'status update =' , status , value ? JSON . stringify ( value ) : '' ) ; if ( status === 'offline' ) { this . state$ . next ( 'disconnected' ) ; } } ) ; } ) ; this . client . on ( 'online' , ( jid ) => { return this . ngZone . run ( ( ) => { return this . onOnline ( jid ) ; } ) ; } ) ; this . client . on ( 'stanza' , ( stanza ) => { this . ngZone . run ( ( ) => { if ( this . skipXmppClientResponses ( stanza ) ) { return ; } this . onStanzaReceived ( stanza ) ; } ) ; } ) ; this . client . on ( 'disconnect' , ( ) => { this . ngZone . run ( ( ) => { this . state$ . next ( 'reconnecting' ) ; } ) ; } ) ; await this . client . start ( ) ; } ) ; } /**
     * We should skip our iq handling for the following xmpp/client response:
     * - resource bind on start by https://xmpp.org/rfcs/rfc6120.html#bind
     */ private skipXmppClientResponses ( stanza ) { const xmppBindNS = 'urn:ietf:params:xml:ns:xmpp-bind' ; return stanza . getChild ( 'bind' ) ?. getNS ( ) === xmppBindNS ; } async logOut ( ) { this . logService . debug ( 'logging out' ) ; if ( this . client ) { this . client . reconnect . stop ( ) ; try { await this . send ( xml ( 'presence' , { type : 'unavailable' } ) ) ; } catch ( e ) { this . logService . error ( 'error sending presence unavailable' ) ; } finally { this . client . stop ( ) ; } } } getNextIqId ( ) { return String ( this . iqId ++ ) ; } reconnectSilently ( ) { this . logService . warn ( 'hard reconnect...' ) ; this . state$ . next ( 'disconnected' ) ; } }