UNPKG

stanza-extend

Version:

Modern XMPP in the browser, with a JSON API

510 lines (491 loc) 18.6 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); const tslib_1 = require('tslib'); const async_1 = require('async'); const events_1 = require('events'); const StreamManagement_1 = tslib_1.__importDefault(require('./helpers/StreamManagement')); const JID = tslib_1.__importStar(require('./JID')); const JXT = tslib_1.__importStar(require('./jxt')); const SASL = tslib_1.__importStar(require('./lib/sasl')); const plugins_1 = require('./plugins'); const protocol_1 = tslib_1.__importDefault(require('./protocol')); const bosh_1 = tslib_1.__importDefault(require('./transports/bosh')); const websocket_1 = tslib_1.__importDefault(require('./transports/websocket')); const Utils_1 = require('./Utils'); class Client extends events_1.EventEmitter { constructor(opts = {}) { super(); this.reconnectAttempts = 0; this.setMaxListeners(100); // Some EventEmitter shims don't include off() this.off = this.removeListener; this.updateConfig(opts); this.jid = ''; this.sasl = new SASL.Factory(); this.sasl.register('EXTERNAL', SASL.EXTERNAL, 1000); this.sasl.register('SCRAM-SHA-256-PLUS', SASL.SCRAM, 350); this.sasl.register('SCRAM-SHA-256', SASL.SCRAM, 300); this.sasl.register('SCRAM-SHA-1-PLUS', SASL.SCRAM, 250); this.sasl.register('SCRAM-SHA-1', SASL.SCRAM, 200); this.sasl.register('DIGEST-MD5', SASL.DIGEST, 100); this.sasl.register('OAUTHBEARER', SASL.OAUTH, 100); this.sasl.register('X-OAUTH2', SASL.PLAIN, 50); this.sasl.register('PLAIN', SASL.PLAIN, 1); this.sasl.register('ANONYMOUS', SASL.ANONYMOUS, 0); this.stanzas = new JXT.Registry(); this.stanzas.define(protocol_1.default); this.use(plugins_1.core); this.sm = new StreamManagement_1.default(); if (this.config.allowResumption !== undefined) { this.sm.allowResume = this.config.allowResumption; } this.sm.on('prebound', jid => { this.jid = jid; this.emit('session:bound', jid); }); this.on('session:bound', jid => this.sm.bind(jid)); this.sm.on('send', sm => this.send('sm', sm)); this.sm.on('acked', acked => this.emit('stanza:acked', acked)); this.sm.on('failed', failed => this.emit('stanza:failed', failed)); this.sm.on('hibernated', data => this.emit('stanza:hibernated', data)); // We disable outgoing processing while stanza resends are queued up // to prevent any interleaving. this.sm.on('begin-resend', () => this.outgoingDataQueue.pause()); this.sm.on('resend', ({ kind, stanza }) => this.send(kind, stanza, true)); this.sm.on('end-resend', () => this.outgoingDataQueue.resume()); // Create message:* flavors of stanza:* SM events for (const type of ['acked', 'hibernated', 'failed']) { this.on(`stanza:${type}`, (data) => { //yjing modify start if (data.kind === 'message') { this.emit(`message:${type}`, data.stanza); this.emit(`message:${type}:${data.stanza.id}`, { type, data }); } else if (data.kind == 'presence') { this.emit(`presence:${type}:${data.stanza.id}`, { type, data }); } //yjing modify end }); } this.transports = { bosh: bosh_1.default, websocket: websocket_1.default }; this.incomingDataQueue = async_1.priorityQueue(async (task, done) => { const { kind, stanza } = task; this.emit(kind, stanza); if (stanza.id) { this.emit((kind + ':id:' + stanza.id), stanza); } if (kind === 'message' || kind === 'presence' || kind === 'iq') { this.emit('stanza', stanza); await this.sm.handle(); } else if (kind === 'sm') { if (stanza.type === 'ack') { await this.sm.process(stanza); this.emit('stream:management:ack', stanza); } if (stanza.type === 'request') { this.sm.ack(); } } if (done) { done(); } }, 1); this.outgoingDataQueue = async_1.priorityQueue(async (task, done) => { var _a; const { kind, stanza, replay } = task; const ackRequest = replay || (await this.sm.track(kind, stanza)); if (kind === 'message') { if (replay) { this.emit('message:retry', stanza); } else { this.emit('message:sent', stanza, false); } } try { if (!this.transport) { throw new Error('Missing transport'); } await this.transport.send(kind, stanza); if (ackRequest) { (_a = this.transport) === null || _a === void 0 ? void 0 : _a.send('sm', { type: 'request' }); } } catch (err) { if (['message', 'presence', 'iq'].includes(kind)) { if (!this.sm.started || !this.sm.resumable) { this.emit('stanza:failed', { kind, stanza }); } else if (this.sm.resumable && !this.transport) { this.emit('stanza:hibernated', { kind, stanza }); } } } if (done) { done(); } }, 1); this.on('stream:data', (json, kind) => { this.incomingDataQueue.push({ kind, stanza: json }, 0); }); this.on('--transport-disconnected', async () => { const drains = []; if (!this.incomingDataQueue.idle()) { drains.push(this.incomingDataQueue.drain()); } if (!this.outgoingDataQueue.idle()) { drains.push(this.outgoingDataQueue.drain()); } await Promise.all(drains); await this.sm.hibernate(); if (this.transport) { delete this.transport; } this.emit('--reset-stream-features'); if (!this.sessionTerminating && this.config.autoReconnect) { this.reconnectAttempts += 1; clearTimeout(this.reconnectTimer); this.reconnectTimer = setTimeout(() => { this.connect(); }, 1000 * Math.min(Math.pow(2, this.reconnectAttempts) + Math.random(), this.config.maxReconnectBackoff || 32)); } this.emit('disconnected'); }); this.on('iq', (iq) => { const iqType = iq.type; //yjing modify start let payloadType = iq.payloadType; payloadType = 'ping' //yjing modify end const iqEvent = 'iq:' + iqType + ':' + payloadType; if (iqType === 'get' || iqType === 'set') { if (payloadType === 'invalid-payload-count') { return this.sendIQError(iq, { error: { condition: 'bad-request', type: 'modify' } }); } if (payloadType === 'unknown-payload' || this.listenerCount(iqEvent) === 0) { return this.sendIQError(iq, { error: { condition: 'service-unavailable', type: 'cancel' } }); } this.emit(iqEvent, iq); } }); this.on('message', msg => { const isChat = (msg.alternateLanguageBodies && msg.alternateLanguageBodies.length) || (msg.links && msg.links.length); const isMarker = msg.marker && msg.marker.type !== 'markable'; if (isChat && !isMarker) { if (msg.type === 'chat' || msg.type === 'normal') { this.emit('chat', msg); } else if (msg.type === 'groupchat') { this.emit('groupchat', msg); } } if (msg.type === 'error') { this.emit('message:error', msg); } }); this.on('presence', (pres) => { let presType = pres.type || 'available'; if (presType === 'error') { presType = 'presence:error'; } this.emit(presType, pres); }); this.on('session:started', () => { this.sessionStarting = false; this.reconnectAttempts = 0; if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); } }); } updateConfig(opts = {}) { const currConfig = this.config || {}; this.config = { allowResumption: true, jid: '', transports: { bosh: true, websocket: true }, useStreamManagement: true, ...currConfig, ...opts }; if (!this.config.server) { this.config.server = JID.getDomain(this.config.jid); } if (this.config.password) { this.config.credentials = this.config.credentials || {}; this.config.credentials.password = this.config.password; delete this.config.password; } } get stream() { return this.transport ? this.transport.stream : undefined; } emit(name, ...args) { // Continue supporting the most common and useful wildcard events const res = super.emit(name, ...args); if (name === 'raw') { super.emit(`raw:${args[0]}`, args[1]); super.emit('raw:*', `raw:${args[0]}`, args[1]); super.emit('*', `raw:${args[0]}`, args[1]); } else { super.emit('*', name, ...args); } return res; } use(pluginInit) { if (typeof pluginInit !== 'function') { return; } pluginInit(this, this.stanzas, this.config); } nextId() { return Utils_1.uuid(); } async getCredentials() { return this._getConfiguredCredentials(); } async connect() { this.sessionTerminating = false; this.sessionStarting = true; this.emit('--reset-stream-features'); if (this.transport) { this.transport.disconnect(false); } const transportPref = ['websocket', 'bosh']; let endpoints; for (const name of transportPref) { let conf = this.config.transports[name]; if (!conf) { continue; } if (typeof conf === 'string') { conf = { url: conf }; } else if (conf === true) { if (!endpoints) { try { endpoints = await this.discoverBindings(this.config.server); } catch (err) { console.error(err); continue; } } endpoints[name] = (endpoints[name] || []).filter(url => url.startsWith('wss:') || url.startsWith('https:')); if (!endpoints[name] || !endpoints[name].length) { continue; } conf = { url: endpoints[name][0] }; } this.transport = new this.transports[name](this, this.sm, this.stanzas); this.transport.connect({ acceptLanguages: this.config.acceptLanguages || ['en'], jid: this.config.jid, lang: this.config.lang || 'en', server: this.config.server, url: conf.url, ...conf }); return; } console.error('No endpoints found for the requested transports.'); this.emit('--transport-disconnected'); } async disconnect() { this.sessionTerminating = true; this.outgoingDataQueue.pause(); if (this.sessionStarted && !this.sm.started) { // Only emit session:end if we had a session, and we aren't using // stream management to keep the session alive. this.emit('session:end'); } this.emit('--reset-stream-features'); this.sessionStarted = false; if (this.transport) { this.transport.disconnect(); } else { this.emit('--transport-disconnected'); } this.outgoingDataQueue.resume(); if (!this.outgoingDataQueue.idle()) { await this.outgoingDataQueue.drain(); } await this.sm.shutdown(); } async send(kind, stanza, replay = false) { return new Promise((resolve, reject) => { this.outgoingDataQueue.push({ kind, stanza, replay }, replay ? 0 : 1, err => err ? reject(err) : resolve()); }); } sendMessage(data) { const id = data.id || this.nextId(); const msg = { id, originId: id, ...data }; //yjing modify start const respEvent = 'message:acked:' + id; const failedEvent = 'message:failed:' + id; const request = new Promise((resolve, reject) => { const handler = (res) => { this.off(respEvent, handler); this.off(failedEvent, handler); if (res.type == 'acked') { resolve(res); } else { reject(res); } }; this.on(respEvent, handler); this.on(failedEvent, handler); }); return this.send('message', msg).then(res => { const timeout = this.config.timeout || 15; return Utils_1.timeoutPromise(request, timeout * 1000, () => ({ ...msg, error: { condition: 'timeout', text: `Request timed out after ${timeout} seconds.` }, type: 'error' })); }); //return msg.id //yjing modify end } sendPresence(data = {}) { //yjing modify start const id = data.id || this.nextId(); const pres = { id: id, ...data }; const respEvent = 'presence:acked:' + id; const failedEvent = 'presence:failed:' + id; const request = new Promise((resolve, reject) => { const handler = (res) => { this.off(respEvent, handler); this.off(failedEvent, handler); if (res.type == 'acked') { resolve(res); } else { reject(res); } }; this.on(respEvent, handler); this.on(failedEvent, handler); }); return this.send('presence', pres).then(res => { const timeout = this.config.timeout || 15; return Utils_1.timeoutPromise(request, timeout * 1000, () => ({ ...pres, error: { condition: 'timeout', text: `Request timed out after ${timeout} seconds.` }, type: 'error' })); }); //return pres.id; //yjing modify end } sendIQ(data) { const iq = { id: this.nextId(), ...data }; const allowed = JID.allowedResponders(this.jid, data.to); const respEvent = 'iq:id:' + iq.id; const request = new Promise((resolve, reject) => { const handler = (res) => { // Only process result from the correct responder if (!allowed.has(res.from)) { return; } // Only process result or error responses, if the responder // happened to send us a request using the same ID value at // the same time. if (res.type !== 'result' && res.type !== 'error') { return; } this.off(respEvent, handler); if (res.type === 'result') { resolve(res); } else { reject(res); } }; this.on(respEvent, handler); }); this.send('iq', iq); const timeout = this.config.timeout || 15; return Utils_1.timeoutPromise(request, timeout * 1000, () => ({ ...iq, to: undefined, from: undefined, error: { condition: 'timeout', text: `Request timed out after ${timeout} seconds.` }, id: iq.id, type: 'error' })); } sendIQResult(original, reply) { this.send('iq', { ...reply, id: original.id, to: original.from, type: 'result' }); } sendIQError(original, error) { this.send('iq', { ...error, id: original.id, to: original.from, type: 'error' }); } sendStreamError(error) { this.emit('stream:error', error); this.send('error', error); this.disconnect(); } _getConfiguredCredentials() { const creds = this.config.credentials || {}; const requestedJID = JID.parse(this.config.jid || ''); const username = creds.username || requestedJID.local; const server = creds.host || requestedJID.domain; return { host: server, password: this.config.password, realm: server, serviceName: server, serviceType: 'xmpp', username, ...creds }; } } exports.default = Client;