stanza-extend
Version:
Modern XMPP in the browser, with a JSON API
510 lines (491 loc) • 18.6 kB
JavaScript
'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;