UNPKG

@jsxc/jsxc

Version:

Real-time XMPP chat application with video calls, file transfer and encrypted communication

349 lines (275 loc) 8.77 kB
import Message from '../Message'; import JID from '../JID'; import * as NS from './xmpp/namespace'; import Log from '../util/Log'; import { Strophe, $iq, $msg, $pres } from '../vendor/Strophe'; import Account from '../Account'; import PEPService from './services/PEP'; import SearchService from './services/Search'; import PubSubService from './services/PubSub'; import MUCService from './services/MUC'; import RosterService from './services/Roster'; import VcardService from './services/Vcard'; import DiscoService from './services/Disco'; export const STANZA_KEY = 'stanza'; export const STANZA_IQ_KEY = 'stanzaIQ'; export const STANZA_JINGLE_KEY = 'stanzaJingle'; enum Presence { online, chat, away, xa, dnd, offline, } type ExtensivePresence = { presence: Presence; status: string }; abstract class AbstractConnection { protected abstract connection; protected abstract send(stanzaElement: Element); protected abstract send(stanzaElement: Strophe.Builder); protected abstract sendIQ(stanzaElement: Element): Promise<Element>; protected abstract sendIQ(stanzaElement: Strophe.Builder): Promise<Element>; public abstract registerHandler( handler: (stanza: string) => boolean, ns?: string, name?: string, type?: string, id?: string, from?: string ); protected jingleHandler; public abstract getJingleHandler(); protected node = 'https://jsxc.org'; protected services = { pubSub: null, pep: null }; constructor(protected account: Account) { let discoInfo = this.account.getDiscoInfo(); discoInfo.addIdentity('client', 'web', 'JSXC'); NS.register('VCARD', 'vcard-temp'); NS.register('FORWARD', 'urn:xmpp:forward:0'); } public getPubSubService = (): PubSubService => { //@TODO connect? supported? return this.getService('pubsub', PubSubService); }; public getPEPService = (): PEPService => { return this.getService('pep', PEPService); }; public getSearchService = (): SearchService => { return this.getService('search', SearchService); }; public getMUCService = (): MUCService => { return this.getService('muc', MUCService); }; public getRosterService = (): RosterService => { return this.getService('roster', RosterService); }; public getVcardService = (): VcardService => { return this.getService('vcard', VcardService); }; public getDiscoService = (): DiscoService => { return this.getService('disco', DiscoService); }; private getService(key: string, Service) { if (!this.services[key]) { let self = this; this.services[key] = new Service( function () { return self.send.apply(self, arguments); }, function () { return self.sendIQ.apply(self, arguments); }, this, this.account ); } return this.services[key]; } public pluginOnlySend(stanzaElement: Element); public pluginOnlySend(stanzaElement: Strophe.Builder); public pluginOnlySend(stanzaElement) { this.send(stanzaElement); } public pluginOnlySendIQ(stanzaElement: Element): Promise<Element>; public pluginOnlySendIQ(stanzaElement: Strophe.Builder): Promise<Element>; public pluginOnlySendIQ(stanzaElement) { return this.sendIQ(stanzaElement); } public getJID(): JID { return this.account.getJID(); } public getServerJID(): JID { return new JID('', this.getJID().domain, ''); } public sendMessage(message: Message) { if (message.getDirection() !== Message.DIRECTION.OUT) { return; } let xmlMsg = $msg({ to: message.getPeer().full, type: message.getType(), id: message.getAttrId(), }); let htmlMessage = this.getMessage(message, message.getEncryptedHtmlMessage, message.getHtmlMessage); if (htmlMessage) { xmlMsg .c('html', { xmlns: Strophe.NS.XHTML_IM, }) .c('body', { xmlns: Strophe.NS.XHTML, }); for (const node of $(htmlMessage).get()) { xmlMsg.cnode(node).up(); } xmlMsg.up().up(); } let plaintextMessage = this.getMessage( message, message.getEncryptedPlaintextMessage, message.getPlaintextMessage ); if (plaintextMessage) { xmlMsg.c('body').t(plaintextMessage).up(); } xmlMsg .c('origin-id', { xmlns: 'urn:xmpp:sid:0', id: message.getUid(), }) .up(); let pipe = this.account.getPipe('preSendMessageStanza'); pipe .run(message, xmlMsg) .then(([message, xmlMsg]: [Message, Element]) => { if (message.hasAttachment() && !message.getAttachment().isProcessed()) { Log.warn('Attachment was not processed'); if (!message.getErrorMessage()) { message.setErrorMessage('Attachment was not processed'); } if (!message.getPlaintextMessage()) { message.aborted(); return; } } this.send(xmlMsg); message.transferred(); }) .catch(err => { message.aborted(); Log.warn('Error during preSendMessageStanza pipe:', err); }); } private getMessage(message: Message, getEncryptedMessage: () => string, getMessage: () => string): string { if (message.isEncrypted() && getEncryptedMessage.call(message)) { return getEncryptedMessage.call(message); } else if (getMessage.call(message)) { if (!message.isEncrypted()) { return getMessage.call(message); } Log.warn('This message should be encrypted'); } } public async sendPresence(presence?: Presence, statusText?: string) { let presenceStanza = $pres(); presenceStanza.c('c', this.generateCapsAttributes()).up(); if (typeof presence !== 'undefined' && presence !== Presence.online) { presenceStanza.c('show').t(Presence[presence]).up(); } if (statusText) { presenceStanza.c('status').t(statusText).up(); } let avatarHash = ''; try { const avatar = await this.account.getContact().getAvatar(); avatarHash = avatar.getHash(); } catch (err) { // we don't have an avatar } presenceStanza.c('x', { xmlns: 'vcard-temp:x:update' }).c('photo').t(avatarHash).up().up(); Log.debug('Send presence', presenceStanza.toString()); this.send(presenceStanza); } public queryArchive( archive: JID, version: string, queryId: string, contact?: JID, beforeResultId?: string, end?: Date ): Promise<Element> { let iq = $iq({ type: 'set', to: archive.bare, }); iq.c('query', { xmlns: version, queryid: queryId, }); iq.c('x', { xmlns: 'jabber:x:data', type: 'submit', }); iq.c('field', { var: 'FORM_TYPE', type: 'hidden', }) .c('value') .t(version) .up() .up(); if (contact) { iq.c('field', { var: 'with', }) .c('value') .t(contact.bare) .up() .up(); } if (end) { iq.c('field', { var: 'end', }) .c('value') .t(end.toISOString()) .up() .up(); } iq.up() .c('set', { xmlns: 'http://jabber.org/protocol/rsm', }) .c('max') .t('20') .up(); if (typeof beforeResultId === 'string' || typeof beforeResultId === 'number') { iq.c('before').t(beforeResultId); } iq.up(); return this.sendIQ(iq); } public changePassword(newPassword: string): Promise<Element> { let iq = $iq({ type: 'set', }); iq.c('query', { xmlns: 'jabber:iq:register', }); iq.c('username').t(this.getJID().node).up(); iq.c('password').t(newPassword); return this.sendIQ(iq); } protected getStorage() { return this.account.getSessionStorage(); } private generateCapsAttributes() { return { xmlns: NS.get('CAPS'), hash: 'sha-1', node: this.node, ver: this.account.getDiscoInfo().getCapsVersion(), }; } } export { AbstractConnection, Presence, ExtensivePresence };