/** * */ discoRoomInfo() { // https://xmpp.org/extensions/xep-0045.html#disco-roominfo const getInfo = $iq({ type: 'get', to: this.roomjid }) .c('query', { xmlns: Strophe.NS.DISCO_INFO }); this.connection.sendIQ(getInfo, result => { const locked = $(result).find('>query>feature[var="muc_passwordprotected"]') .length === 1; if (locked !== this.locked) { this.eventEmitter.emit(XMPPEvents.MUC_LOCK_CHANGED, locked); this.locked = locked; } }, error => { GlobalOnErrorHandler.callErrorHandler(error); logger.error('Error getting room info: ', error); }); }
/** * Sends a jibri command using an iq. * * @private * @param {string} action - The action to send ('start' or 'stop'). */ _sendJibriIQ(action) { const attributes = { 'xmlns': 'http://jitsi.org/protocol/jibri', 'action': action, sipaddress: this.sipAddress }; attributes.displayname = this.displayName; const iq = $iq({ to: this.chatRoom.focusMucJid, type: 'set' }) .c('jibri', attributes) .up(); logger.debug(`${action} video SIP GW session`, iq.nodeTree); this.chatRoom.connection.sendIQ( iq, () => {}, // eslint-disable-line no-empty-function error => { logger.error( `Failed to ${action} video SIP GW session, error: `, error); this.setState(VideoSIPGWConstants.STATE_FAILED); }); }
/** * * @param fromJoin */ sendPresence(fromJoin) { const to = this.presMap.to; if (!to || (!this.joined && !fromJoin)) { // Too early to send presence - not initialized return; } const pres = $pres({ to }); // xep-0045 defines: "including in the initial presence stanza an empty // <x/> element qualified by the 'http://jabber.org/protocol/muc' // namespace" and subsequent presences should not include that or it can // be considered as joining, and server can send us the message history // for the room on every presence if (fromJoin) { pres.c('x', { xmlns: this.presMap.xns }); if (this.password) { pres.c('password').t(this.password).up(); } pres.up(); } parser.json2packet(this.presMap.nodes, pres); this.connection.send(pres); if (fromJoin) { // XXX We're pressed for time here because we're beginning a complex // and/or lengthy conference-establishment process which supposedly // involves multiple RTTs. We don't have the time to wait for // Strophe to decide to send our IQ. this.connection.flush(); } }
Moderator.prototype.logout = function(callback) { const iq = $iq({ to: this.getFocusComponent(), type: 'set' }); const { sessionId } = Settings; if (!sessionId) { callback(); return; } iq.c('logout', { xmlns: 'http://jitsi.org/protocol/focus', 'session-id': sessionId }); this.connection.sendIQ( iq, result => { // eslint-disable-next-line newline-per-chained-call let logoutUrl = $(result).find('logout').attr('logout-url'); if (logoutUrl) { logoutUrl = decodeURIComponent(logoutUrl); } logger.info(`Log out OK, url: ${logoutUrl}`, result); Settings.sessionId = undefined; callback(logoutUrl); }, error => { const errmsg = 'Logout error'; GlobalOnErrorHandler.callErrorHandler(new Error(errmsg)); logger.error(errmsg, error); } ); };
this.connection.sendIQ(getForm, form => { if (!$(form).find( '>query>x[xmlns="jabber:x:data"]' + '>field[var="muc#roomconfig_whois"]').length) { const errmsg = 'non-anonymous rooms not supported'; GlobalOnErrorHandler.callErrorHandler(new Error(errmsg)); logger.error(errmsg); return; } const formSubmit = $iq({ to: self.roomjid, type: 'set' }) .c('query', { xmlns: 'http://jabber.org/protocol/muc#owner' }); formSubmit.c('x', { xmlns: 'jabber:x:data', type: 'submit' }); formSubmit.c('field', { 'var': 'FORM_TYPE' }) .c('value') .t('http://jabber.org/protocol/muc#roomconfig').up().up(); formSubmit.c('field', { 'var': 'muc#roomconfig_whois' }) .c('value').t('anyone').up().up(); self.connection.sendIQ(formSubmit); }, error => {
/** * * @param subject */ setSubject(subject) { const msg = $msg({ to: this.roomjid, type: 'groupchat' }); msg.c('subject', subject); this.connection.send(msg); }
/* eslint-disable max-params */ /** * * @param key * @param onSuccess * @param onError * @param onNotSupported */ lockRoom(key, onSuccess, onError, onNotSupported) { // http://xmpp.org/extensions/xep-0045.html#roomconfig this.connection.sendIQ( $iq({ to: this.roomjid, type: 'get' }) .c('query', { xmlns: 'http://jabber.org/protocol/muc#owner' }), res => { if ($(res) .find( '>query>x[xmlns="jabber:x:data"]' + '>field[var="muc#roomconfig_roomsecret"]') .length) { const formsubmit = $iq({ to: this.roomjid, type: 'set' }) .c('query', { xmlns: 'http://jabber.org/protocol/muc#owner' }); formsubmit.c('x', { xmlns: 'jabber:x:data', type: 'submit' }); formsubmit .c('field', { 'var': 'FORM_TYPE' }) .c('value') .t('http://jabber.org/protocol/muc#roomconfig') .up() .up(); formsubmit .c('field', { 'var': 'muc#roomconfig_roomsecret' }) .c('value') .t(key) .up() .up(); // Fixes a bug in prosody 0.9.+ // https://prosody.im/issues/issue/373 formsubmit .c('field', { 'var': 'muc#roomconfig_whois' }) .c('value') .t('anyone') .up() .up(); // FIXME: is muc#roomconfig_passwordprotectedroom required? this.connection.sendIQ(formsubmit, onSuccess, onError); } else { onNotSupported(); } }, onError); }
/* eslint-disable max-params */ /** * Sends "ping" to given <tt>jid</tt> * @param jid the JID to which ping request will be sent. * @param success callback called on success. * @param error callback called on error. * @param timeout ms how long are we going to wait for the response. On * timeout <tt>error<//t> callback is called with undefined error argument. */ ping(jid, success, error, timeout) { this._addPingExecutionTimestamp(); const iq = $iq({ type: 'get', to: jid }); iq.c('ping', { xmlns: Strophe.NS.PING }); this.connection.sendIQ(iq, success, error, timeout); }
/** * * @param jid */ kick(jid) { const kickIQ = $iq({ to: this.roomjid, type: 'set' }) .c('query', { xmlns: 'http://jabber.org/protocol/muc#admin' }) .c('item', { nick: Strophe.getResourceFromJid(jid), role: 'none' }) .c('reason').t('You have been kicked.').up().up().up(); this.connection.sendIQ( kickIQ, result => logger.log('Kick participant with jid: ', jid, result), error => logger.log('Kick participant error: ', error)); }
Recording.prototype.setRecordingJibri = function( state, callback, errCallback, options = {}) { if (state === this.state) { errCallback(JitsiRecorderErrors.INVALID_STATE); } // FIXME jibri does not accept IQ without 'url' attribute set ? const iq = $iq({ to: this.focusMucJid, type: 'set' }) .c('jibri', { 'xmlns': 'http://jitsi.org/protocol/jibri', 'action': state === Recording.status.ON ? Recording.action.START : Recording.action.STOP, 'recording_mode': this.type === Recording.types.JIBRI_FILE ? 'file' : 'stream', 'streamid': this.type === Recording.types.JIBRI ? options.streamId : undefined }) .up(); logger.log(`Set jibri recording: ${state}`, iq.nodeTree); logger.log(iq.nodeTree); this.connection.sendIQ( iq, result => { logger.log('Result', result); const jibri = $(result).find('jibri'); callback(jibri.attr('state'), jibri.attr('url')); }, error => { logger.log( 'Failed to start recording, error: ', getJibriErrorDetails(error)); errCallback(error); }); };
/** * Send text message to the other participants in the conference * @param body * @param nickname */ sendMessage(body, nickname) { const msg = $msg({ to: this.roomjid, type: 'groupchat' }); msg.c('body', body).up(); if (nickname) { msg.c('nick', { xmlns: 'http://jabber.org/protocol/nick' }) .t(nickname) .up() .up(); } this.connection.send(msg); this.eventEmitter.emit(XMPPEvents.SENDING_CHAT_MESSAGE, body); }
/** * Generates the message to change the status of the recording session. * * @param {string} status - The new status to which the recording session * should transition. * @param {string} [options.appData] - Data specific to the app/service that * the result file will be uploaded. * @param {string} [options.broadcastId] - The broadcast ID of an * associated YouTube stream, used for knowing the URL from which the stream * can be viewed. * @param {string} options.focusMucJid - The JID of the focus participant * that controls recording. * @param {streamId} options.streamId - Necessary for live streaming, this * is the the stream key needed to start a live streaming session with the * streaming service provider. * @returns Object - The XMPP IQ message. */ _createIQ({ action, appData, broadcastId, focusMucJid, streamId }) { return $iq({ to: focusMucJid, type: 'set' }) .c('jibri', { 'xmlns': 'http://jitsi.org/protocol/jibri', 'action': action, 'app_data': appData, 'recording_mode': this._mode, 'streamid': streamId, 'you_tube_broadcast_id': broadcastId }) .up(); }
/** * Send private text message to another participant of the conference * @param id id/muc resource of the receiver * @param body * @param nickname */ sendPrivateMessage(id, body, nickname) { const msg = $msg({ to: `${this.roomjid}/${id}`, type: 'chat' }); msg.c('body', body).up(); if (nickname) { msg.c('nick', { xmlns: 'http://jabber.org/protocol/nick' }) .t(nickname) .up() .up(); } this.connection.send(msg); this.eventEmitter.emit(XMPPEvents.SENDING_PRIVATE_CHAT_MESSAGE, body); }
Recording.prototype.setRecordingColibri = function( state, callback, errCallback, options) { const elem = $iq({ to: this.focusMucJid, type: 'set' }); elem.c('conference', { xmlns: 'http://jitsi.org/protocol/colibri' }); elem.c('recording', { state, token: options.token }); const self = this; this.connection.sendIQ( elem, result => { logger.log('Set recording "', state, '". Result:', result); const recordingElem = $(result).find('>conference>recording'); const newState = recordingElem.attr('state'); self.state = newState; callback(newState); if (newState === 'pending') { self.connection.addHandler(iq => { // eslint-disable-next-line newline-per-chained-call const s = $(iq).find('recording').attr('state'); if (s) { self.state = newState; callback(s); } }, 'http://jitsi.org/protocol/colibri', 'iq', null, null, null); } }, error => { logger.warn(error); errCallback(error); } ); };
= function(state, callback, errCallback) { if (state === this.state) { errCallback(new Error('Invalid state!')); } const iq = $iq({ to: this.jirecon, type: 'set' }) .c('recording', { xmlns: 'http://jitsi.org/protocol/jirecon', action: state === Recording.status.ON ? Recording.action.START : Recording.action.STOP, mucjid: this.roomjid }); if (state === Recording.status.OFF) { iq.attrs({ rid: this.jireconRid }); } logger.log('Start recording'); const self = this; this.connection.sendIQ( iq, result => { // TODO wait for an IQ with the real status, since this is // provisional? // eslint-disable-next-line newline-per-chained-call self.jireconRid = $(result).find('recording').attr('rid'); const stateStr = state === Recording.status.ON ? 'started' : 'stopped'; logger.log(`Recording ${stateStr}(jirecon)${result}`); self.state = state; if (state === Recording.status.OFF) { self.jireconRid = null; } callback(state); }, error => { logger.log('Failed to start recording, error: ', error); errCallback(error); }); };
/** * Mutes remote participant. * @param jid of the participant * @param mute */ muteParticipant(jid, mute) { logger.info('set mute', mute); const iqToFocus = $iq( { to: this.focusMucJid, type: 'set' }) .c('mute', { xmlns: 'http://jitsi.org/jitmeet/audio', jid }) .t(mute.toString()) .up(); this.connection.sendIQ( iqToFocus, result => logger.log('set mute', result), error => logger.log('set mute error', error)); }
Moderator.prototype._getLoginUrl = function(popup, urlCb, failureCb) { const iq = $iq({ to: this.getFocusComponent(), type: 'get' }); const attrs = { xmlns: 'http://jitsi.org/protocol/focus', room: this.roomName, 'machine-uid': Settings.machineId }; let str = 'auth url'; // for logger if (popup) { attrs.popup = true; str = `POPUP ${str}`; } iq.c('login-url', attrs); /** * Implements a failure callback which reports an error message and an error * through (1) GlobalOnErrorHandler, (2) logger, and (3) failureCb. * * @param {string} errmsg the error messsage to report * @param {*} error the error to report (in addition to errmsg) */ function reportError(errmsg, err) { GlobalOnErrorHandler.callErrorHandler(new Error(errmsg)); logger.error(errmsg, err); failureCb(err); } this.connection.sendIQ( iq, result => { // eslint-disable-next-line newline-per-chained-call let url = $(result).find('login-url').attr('url'); url = decodeURIComponent(url); if (url) { logger.info(`Got ${str}: ${url}`); urlCb(url); } else { reportError(`Failed to get ${str} from the focus`, result); } }, reportError.bind(undefined, `Get ${str} error`) ); };
/** * */ createNonAnonymousRoom() { // http://xmpp.org/extensions/xep-0045.html#createroom-reserved const getForm = $iq({ type: 'get', to: this.roomjid }) .c('query', { xmlns: 'http://jabber.org/protocol/muc#owner' }) .c('x', { xmlns: 'jabber:x:data', type: 'submit' }); const self = this; this.connection.sendIQ(getForm, form => { if (!$(form).find( '>query>x[xmlns="jabber:x:data"]' + '>field[var="muc#roomconfig_whois"]').length) { const errmsg = 'non-anonymous rooms not supported'; GlobalOnErrorHandler.callErrorHandler(new Error(errmsg)); logger.error(errmsg); return; } const formSubmit = $iq({ to: self.roomjid, type: 'set' }) .c('query', { xmlns: 'http://jabber.org/protocol/muc#owner' }); formSubmit.c('x', { xmlns: 'jabber:x:data', type: 'submit' }); formSubmit.c('field', { 'var': 'FORM_TYPE' }) .c('value') .t('http://jabber.org/protocol/muc#roomconfig').up().up(); formSubmit.c('field', { 'var': 'muc#roomconfig_whois' }) .c('value').t('anyone').up().up(); self.connection.sendIQ(formSubmit); }, error => { GlobalOnErrorHandler.callErrorHandler(error); logger.error('Error getting room configuration form: ', error); }); }
/** * Sends the presence unavailable, signaling the server * we want to leave the room. */ doLeave() { logger.log('do leave', this.myroomjid); const pres = $pres({ to: this.myroomjid, type: 'unavailable' }); this.presMap.length = 0; // XXX Strophe is asynchronously sending by default. Unfortunately, that // means that there may not be enough time to send the unavailable // presence. Switching Strophe to synchronous sending is not much of an // option because it may lead to a noticeable delay in navigating away // from the current location. As a compromise, we will try to increase // the chances of sending the unavailable presence within the short time // span that we have upon unloading by invoking flush() on the // connection. We flush() once before sending/queuing the unavailable // presence in order to attemtp to have the unavailable presence at the // top of the send queue. We flush() once more after sending/queuing the // unavailable presence in order to attempt to have it sent as soon as // possible. this.connection.flush(); this.connection.send(pres); this.connection.flush(); }
Moderator.prototype.createConferenceIq = function() { // Generate create conference IQ const elem = $iq({ to: this.getFocusComponent(), type: 'set' }); // Session Id used for authentication const { sessionId } = Settings; const machineUID = Settings.machineId; const config = this.options.conference; logger.info(`Session ID: ${sessionId} machine UID: ${machineUID}`); elem.c('conference', { xmlns: 'http://jitsi.org/protocol/focus', room: this.roomName, 'machine-uid': machineUID }); if (sessionId) { elem.attrs({ 'session-id': sessionId }); } if (this.options.connection.enforcedBridge !== undefined) { elem.c( 'property', { name: 'enforcedBridge', value: this.options.connection.enforcedBridge }).up(); } // Tell the focus we have Jigasi configured if (this.options.connection.hosts !== undefined && this.options.connection.hosts.call_control !== undefined) { elem.c( 'property', { name: 'call_control', value: this.options.connection.hosts.call_control }).up(); } if (config.channelLastN !== undefined) { elem.c( 'property', { name: 'channelLastN', value: config.channelLastN }).up(); } elem.c( 'property', { name: 'disableRtx', value: Boolean(config.disableRtx) }).up(); if (config.enableTcc !== undefined) { elem.c( 'property', { name: 'enableTcc', value: Boolean(config.enableTcc) }).up(); } if (config.enableRemb !== undefined) { elem.c( 'property', { name: 'enableRemb', value: Boolean(config.enableRemb) }).up(); } if (config.minParticipants !== undefined) { elem.c( 'property', { name: 'minParticipants', value: config.minParticipants }).up(); } elem.c( 'property', { name: 'enableLipSync', value: this.options.connection.enableLipSync !== false }).up(); if (config.audioPacketDelay !== undefined) { elem.c( 'property', { name: 'audioPacketDelay', value: config.audioPacketDelay }).up(); } if (config.startBitrate) { elem.c( 'property', { name: 'startBitrate', value: config.startBitrate }).up(); } if (config.minBitrate) { elem.c( 'property', { name: 'minBitrate', value: config.minBitrate }).up(); } if (config.testing && config.testing.octo && typeof config.testing.octo.probability === 'number') { if (Math.random() < config.testing.octo.probability) { elem.c( 'property', { name: 'octo', value: true }).up(); } } let openSctp; switch (this.options.conference.openBridgeChannel) { case 'datachannel': case true: case undefined: openSctp = true; break; case 'websocket': openSctp = false; break; } if (openSctp && !browser.supportsDataChannels()) { openSctp = false; } elem.c( 'property', { name: 'openSctp', value: openSctp }).up(); if (this.options.conference.startAudioMuted !== undefined) { elem.c( 'property', { name: 'startAudioMuted', value: this.options.conference.startAudioMuted }).up(); } if (this.options.conference.startVideoMuted !== undefined) { elem.c( 'property', { name: 'startVideoMuted', value: this.options.conference.startVideoMuted }).up(); } if (this.options.conference.stereo !== undefined) { elem.c( 'property', { name: 'stereo', value: this.options.conference.stereo }).up(); } if (this.options.conference.useRoomAsSharedDocumentName !== undefined) { elem.c( 'property', { name: 'useRoomAsSharedDocumentName', value: this.options.conference.useRoomAsSharedDocumentName }).up(); } elem.up(); return elem; };