import { jid as parseJid , xml } from '@xmpp/client' ; import { Contact } from '../../../../core/contact' ; import { Presence } from '../../../../core/presence' ; import { PresenceStanza , Stanza } from '../../../../core/stanza' ; import { ContactSubscription } from '../../../../core/subscription' ; import { LogService } from '../../../log.service' ; import { XmppChatAdapter } from '../xmpp-chat-adapter.service' ; import { AbstractXmppPlugin } from './abstract-xmpp-plugin' ; /**
 * https://xmpp.org/rfcs/rfc6121.html#roster-add-success
 */ export class RosterPlugin extends AbstractXmppPlugin { constructor ( private chatService , private logService ) { super ( ) ; } handleStanza ( stanza ) { if ( this . isCapabilitiesStanza ( stanza ) ) { return false ; } if ( this . isRosterPushStanza ( stanza ) ) { return this . handleRosterPushStanza ( stanza ) ; } else if ( this . isPresenceStanza ( stanza ) ) { return this . handlePresenceStanza ( stanza ) ; } return false ; } private isRosterPushStanza ( stanza ) { return stanza . name === 'iq' && stanza . attrs . type === 'set' && stanza . getChild ( 'query' , 'jabber:iq:roster' ) ; } private isPresenceStanza ( stanza ) { return stanza . name === 'presence' && ! stanza . getChild ( 'x' , 'http://jabber.org/protocol/muc#user' ) ; } private isCapabilitiesStanza ( stanza ) { const child = stanza . getChild ( 'c' , 'http://jabber.org/protocol/caps' ) ; return ! ! child ; } private handleRosterPushStanza ( stanza ) { const itemChild = stanza . getChild ( 'query' ) . getChild ( 'item' ) ; const contact = this . chatService . getOrCreateContactById ( itemChild . attrs . jid , itemChild . attrs . name || itemChild . attrs . jid ) ; contact . pendingOut$ . next ( itemChild . attrs . ask === 'subscribe' ) ; const subscriptionStatus = itemChild . attrs . subscription || 'none' ; this . chatService . chatConnectionService . sendIqAckResult ( stanza . attrs . id ) ; let handled = false ; if ( subscriptionStatus === 'remove' ) { contact . pendingOut$ . next ( false ) ; contact . subscription$ . next ( ContactSubscription . none ) ; handled = true ; } else if ( subscriptionStatus === 'none' ) { contact . subscription$ . next ( ContactSubscription . none ) ; handled = true ; } else if ( subscriptionStatus === 'to' ) { contact . subscription$ . next ( ContactSubscription . to ) ; handled = true ; } else if ( subscriptionStatus === 'from' ) { contact . subscription$ . next ( ContactSubscription . from ) ; handled = true ; } else if ( subscriptionStatus === 'both' ) { contact . subscription$ . next ( ContactSubscription . both ) ; handled = true ; } if ( handled ) { const existingContacts = this . chatService . contacts$ . getValue ( ) ; this . chatService . contacts$ . next ( existingContacts ) ; } return handled ; } private handlePresenceStanza ( stanza ) { const fromAsContact = this . chatService . getOrCreateContactById ( stanza . attrs . from ) ; const isAddressedToMe = this . chatService . chatConnectionService . userJid . bare ( ) . equals ( parseJid ( stanza . attrs . to ) . bare ( ) ) ; if ( isAddressedToMe ) { if ( ! stanza . attrs . type ) { const show = stanza . getChildText ( 'show' ) ; const presenceMapping = { chat : Presence . present , null : Presence . present , away : Presence . away , dnd : Presence . away , xa : Presence . away , } ; const presence = presenceMapping [ show ] ; if ( presence ) { fromAsContact . updateResourcePresence ( stanza . attrs . from , presence ) ; } else { this . logService . error ( 'illegal presence:' , stanza . attrs . from , show ) ; } return true ; } else if ( stanza . attrs . type === 'unavailable' ) { fromAsContact . updateResourcePresence ( stanza . attrs . from , Presence . unavailable ) ; return true ; } else if ( stanza . attrs . type === 'subscribe' ) { if ( fromAsContact . isSubscribed ( ) || fromAsContact . pendingOut$ . getValue ( ) ) { fromAsContact . pendingIn$ . next ( false ) ; this . sendAcceptPresenceSubscriptionRequest ( stanza . attrs . from ) ; fromAsContact . subscription$ . next ( this . transitionSubscriptionRequestReceivedAccepted ( fromAsContact . subscription$ . getValue ( ) ) ) ; this . chatService . contacts$ . next ( this . chatService . contacts$ . getValue ( ) ) ; return true ; } else if ( fromAsContact ) { fromAsContact . pendingIn$ . next ( true ) ; this . chatService . contacts$ . next ( this . chatService . contacts$ . getValue ( ) ) ; return true ; } } else if ( stanza . attrs . type === 'subscribed' ) { fromAsContact . pendingOut$ . next ( false ) ; fromAsContact . subscription$ . next ( this . transitionSubscriptionRequestSentAccepted ( fromAsContact . subscription$ . getValue ( ) ) ) ; this . chatService . contacts$ . next ( this . chatService . contacts$ . getValue ( ) ) ; return true ; } else if ( stanza . attrs . type === 'unsubscribed' ) { } else if ( stanza . attrs . type === 'unsubscribe' ) { } } return false ; } private transitionSubscriptionRequestReceivedAccepted ( subscription ) { switch ( subscription ) { case ContactSubscription . none : return ContactSubscription . from ; case ContactSubscription . to : return ContactSubscription . both ; default : return subscription ; } } private transitionSubscriptionRequestSentAccepted ( subscription ) { switch ( subscription ) { case ContactSubscription . none : return ContactSubscription . to ; case ContactSubscription . from : return ContactSubscription . both ; default : return subscription ; } } private sendAcceptPresenceSubscriptionRequest ( jid ) { const contact = this . chatService . getOrCreateContactById ( jid ) ; contact . pendingIn$ . next ( false ) ; this . chatService . chatConnectionService . send ( xml ( 'presence' , { to : jid , type : 'subscribed' , id : this . chatService . chatConnectionService . getNextIqId ( ) } ) ) ; } public onBeforeOnline ( ) { return this . refreshRosterContacts ( ) ; } getRosterContacts ( ) { return new Promise ( ( resolve ) => this . chatService . chatConnectionService . sendIq ( xml ( 'iq' , { type : 'get' } , xml ( 'query' , { xmlns : 'jabber:iq:roster' } ) ) ) . then ( ( responseStanza ) => resolve ( this . convertToContacts ( responseStanza ) ) , ( responseStanza ) => { this . logService . error ( 'error converting roster contact push' , responseStanza . toString ( ) ) ; resolve ( [ ] ) ; } ) ) ; } private convertToContacts ( responseStanza ) { return responseStanza . getChild ( 'query' ) . getChildElements ( ) . map ( rosterElement => { const contact = this . chatService . getOrCreateContactById ( rosterElement . attrs . jid , rosterElement . attrs . name || rosterElement . attrs . jid ) ; contact . subscription$ . next ( this . parseSubscription ( rosterElement . attrs . subscription ) ) ; contact . pendingOut$ . next ( rosterElement . attrs . ask === 'subscribe' ) ; return contact ; } ) ; } private parseSubscription ( subscription ) { switch ( subscription ) { case 'to' : return ContactSubscription . to ; case 'from' : return ContactSubscription . from ; case 'both' : return ContactSubscription . both ; case 'none' : default : return ContactSubscription . none ; } } addRosterContact ( jid ) { this . sendAcceptPresenceSubscriptionRequest ( jid ) ; this . sendAddToRoster ( jid ) ; this . sendSubscribeToPresence ( jid ) ; } private sendAddToRoster ( jid ) { return this . chatService . chatConnectionService . sendIq ( xml ( 'iq' , { type : 'set' } , xml ( 'query' , { xmlns : 'jabber:iq:roster' } , xml ( 'item' , { jid } ) ) ) ) ; } private sendSubscribeToPresence ( jid ) { this . chatService . chatConnectionService . send ( xml ( 'presence' , { id : this . chatService . chatConnectionService . getNextIqId ( ) , to : jid , type : 'subscribe' } ) ) ; } removeRosterContact ( jid ) { const contact = this . chatService . getContactById ( jid ) ; if ( contact ) { contact . subscription$ . next ( ContactSubscription . none ) ; contact . pendingOut$ . next ( false ) ; contact . pendingIn$ . next ( false ) ; this . sendRemoveFromRoster ( jid ) ; this . sendWithdrawPresenceSubscription ( jid ) ; } } private sendRemoveFromRoster ( jid ) { this . chatService . chatConnectionService . sendIq ( xml ( 'iq' , { type : 'set' } , xml ( 'query' , { xmlns : 'jabber:iq:roster' } , xml ( 'item' , { jid , subscription : 'remove' } ) ) ) ) ; } private sendWithdrawPresenceSubscription ( jid ) { this . chatService . chatConnectionService . send ( xml ( 'presence' , { id : this . chatService . chatConnectionService . getNextIqId ( ) , to : jid , type : 'unsubscribed' } ) ) ; } refreshRosterContacts ( ) { return this . getRosterContacts ( ) ; } }