You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ChatRoom.js 47KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506
  1. /* global $, __filename */
  2. import { getLogger } from 'jitsi-meet-logger';
  3. import { $iq, $msg, $pres, Strophe } from 'strophe.js';
  4. import GlobalOnErrorHandler from '../util/GlobalOnErrorHandler';
  5. import * as JitsiTranscriptionStatus from '../../JitsiTranscriptionStatus';
  6. import Listenable from '../util/Listenable';
  7. import * as MediaType from '../../service/RTC/MediaType';
  8. import XMPPEvents from '../../service/xmpp/XMPPEvents';
  9. import Moderator from './moderator';
  10. import XmppConnection from './XmppConnection';
  11. const logger = getLogger(__filename);
  12. export const parser = {
  13. packet2JSON(xmlElement, nodes) {
  14. for (const child of Array.from(xmlElement.children)) {
  15. const node = {
  16. attributes: {},
  17. children: [],
  18. tagName: child.tagName
  19. };
  20. for (const attr of Array.from(child.attributes)) {
  21. node.attributes[attr.name] = attr.value;
  22. }
  23. const text = Strophe.getText(child);
  24. if (text) {
  25. // Using Strophe.getText will do work for traversing all direct
  26. // child text nodes but returns an escaped value, which is not
  27. // desirable at this point.
  28. node.value = Strophe.xmlunescape(text);
  29. }
  30. nodes.push(node);
  31. this.packet2JSON(child, node.children);
  32. }
  33. },
  34. json2packet(nodes, packet) {
  35. for (let i = 0; i < nodes.length; i++) {
  36. const node = nodes[i];
  37. if (node) {
  38. packet.c(node.tagName, node.attributes);
  39. if (node.value) {
  40. packet.t(node.value);
  41. }
  42. if (node.children) {
  43. this.json2packet(node.children, packet);
  44. }
  45. packet.up();
  46. }
  47. }
  48. // packet.up();
  49. }
  50. };
  51. /**
  52. * Returns array of JS objects from the presence JSON associated with the passed
  53. / nodeName
  54. * @param pres the presence JSON
  55. * @param nodeName the name of the node (videomuted, audiomuted, etc)
  56. */
  57. function filterNodeFromPresenceJSON(pres, nodeName) {
  58. const res = [];
  59. for (let i = 0; i < pres.length; i++) {
  60. if (pres[i].tagName === nodeName) {
  61. res.push(pres[i]);
  62. }
  63. }
  64. return res;
  65. }
  66. // XXX As ChatRoom constructs XMPP stanzas and Strophe is build around the idea
  67. // of chaining function calls, allow long function call chains.
  68. /* eslint-disable newline-per-chained-call */
  69. /**
  70. *
  71. */
  72. export default class ChatRoom extends Listenable {
  73. /* eslint-disable max-params */
  74. /**
  75. *
  76. * @param {XmppConnection} connection - The XMPP connection instance.
  77. * @param jid
  78. * @param password
  79. * @param XMPP
  80. * @param options
  81. * @param {boolean} options.disableFocus - when set to {@code false} will
  82. * not invite Jicofo into the room. This is intended to be used only by
  83. * jitsi-meet-spot.
  84. */
  85. constructor(connection, jid, password, XMPP, options) {
  86. super();
  87. this.xmpp = XMPP;
  88. this.connection = connection;
  89. this.roomjid = Strophe.getBareJidFromJid(jid);
  90. this.myroomjid = jid;
  91. this.password = password;
  92. logger.info(`Joined MUC as ${this.myroomjid}`);
  93. this.members = {};
  94. this.presMap = {};
  95. this.presHandlers = {};
  96. this._removeConnListeners = [];
  97. this.joined = false;
  98. this.role = null;
  99. this.focusMucJid = null;
  100. this.noBridgeAvailable = false;
  101. this.options = options || {};
  102. this.moderator
  103. = new Moderator(this.roomjid, this.xmpp, this.eventEmitter, {
  104. connection: this.xmpp.options,
  105. conference: this.options
  106. });
  107. this.initPresenceMap(options);
  108. this.lastPresences = {};
  109. this.phoneNumber = null;
  110. this.phonePin = null;
  111. this.connectionTimes = {};
  112. this.participantPropertyListener = null;
  113. this.locked = false;
  114. this.transcriptionStatus = JitsiTranscriptionStatus.OFF;
  115. }
  116. /* eslint-enable max-params */
  117. /**
  118. *
  119. */
  120. initPresenceMap(options = {}) {
  121. this.presMap.to = this.myroomjid;
  122. this.presMap.xns = 'http://jabber.org/protocol/muc';
  123. this.presMap.nodes = [];
  124. if (options.statsId) {
  125. this.presMap.nodes.push({
  126. 'tagName': 'stats-id',
  127. 'value': options.statsId
  128. });
  129. }
  130. // We need to broadcast 'videomuted' status from the beginning, cause
  131. // Jicofo makes decisions based on that. Initialize it with 'false'
  132. // here.
  133. this.addVideoInfoToPresence(false);
  134. if (options.deploymentInfo && options.deploymentInfo.userRegion) {
  135. this.presMap.nodes.push({
  136. 'tagName': 'region',
  137. 'attributes': {
  138. id: options.deploymentInfo.userRegion,
  139. xmlns: 'http://jitsi.org/jitsi-meet'
  140. }
  141. });
  142. }
  143. }
  144. /**
  145. * Joins the chat room.
  146. * @param password
  147. * @returns {Promise} - resolved when join completes. At the time of this
  148. * writing it's never rejected.
  149. */
  150. join(password) {
  151. this.password = password;
  152. return new Promise(resolve => {
  153. this.options.disableFocus
  154. && logger.info('Conference focus disabled');
  155. const preJoin
  156. = this.options.disableFocus
  157. ? Promise.resolve()
  158. : this.moderator.allocateConferenceFocus();
  159. preJoin.then(() => {
  160. this.sendPresence(true);
  161. this._removeConnListeners.push(
  162. this.connection.addEventListener(
  163. XmppConnection.Events.CONN_STATUS_CHANGED,
  164. this.onConnStatusChanged.bind(this))
  165. );
  166. resolve();
  167. });
  168. });
  169. }
  170. /**
  171. *
  172. * @param fromJoin
  173. */
  174. sendPresence(fromJoin) {
  175. const to = this.presMap.to;
  176. if (!this.connection || !this.connection.connected || !to || (!this.joined && !fromJoin)) {
  177. // Too early to send presence - not initialized
  178. return;
  179. }
  180. const pres = $pres({ to });
  181. // xep-0045 defines: "including in the initial presence stanza an empty
  182. // <x/> element qualified by the 'http://jabber.org/protocol/muc'
  183. // namespace" and subsequent presences should not include that or it can
  184. // be considered as joining, and server can send us the message history
  185. // for the room on every presence
  186. if (fromJoin) {
  187. pres.c('x', { xmlns: this.presMap.xns });
  188. if (this.password) {
  189. pres.c('password').t(this.password).up();
  190. }
  191. pres.up();
  192. }
  193. parser.json2packet(this.presMap.nodes, pres);
  194. this.connection.send(pres);
  195. if (fromJoin) {
  196. // XXX We're pressed for time here because we're beginning a complex
  197. // and/or lengthy conference-establishment process which supposedly
  198. // involves multiple RTTs. We don't have the time to wait for
  199. // Strophe to decide to send our IQ.
  200. this.connection.flush();
  201. }
  202. }
  203. /**
  204. * Sends the presence unavailable, signaling the server
  205. * we want to leave the room.
  206. */
  207. doLeave() {
  208. logger.log('do leave', this.myroomjid);
  209. const pres = $pres({ to: this.myroomjid,
  210. type: 'unavailable' });
  211. this.presMap.length = 0;
  212. // XXX Strophe is asynchronously sending by default. Unfortunately, that
  213. // means that there may not be enough time to send the unavailable
  214. // presence. Switching Strophe to synchronous sending is not much of an
  215. // option because it may lead to a noticeable delay in navigating away
  216. // from the current location. As a compromise, we will try to increase
  217. // the chances of sending the unavailable presence within the short time
  218. // span that we have upon unloading by invoking flush() on the
  219. // connection. We flush() once before sending/queuing the unavailable
  220. // presence in order to attemtp to have the unavailable presence at the
  221. // top of the send queue. We flush() once more after sending/queuing the
  222. // unavailable presence in order to attempt to have it sent as soon as
  223. // possible.
  224. // FIXME do not use Strophe.Connection in the ChatRoom directly
  225. !this.connection.isUsingWebSocket && this.connection.flush();
  226. this.connection.send(pres);
  227. this.connection.flush();
  228. }
  229. /**
  230. *
  231. */
  232. discoRoomInfo() {
  233. // https://xmpp.org/extensions/xep-0045.html#disco-roominfo
  234. const getInfo
  235. = $iq({
  236. type: 'get',
  237. to: this.roomjid
  238. })
  239. .c('query', { xmlns: Strophe.NS.DISCO_INFO });
  240. this.connection.sendIQ(getInfo, result => {
  241. const locked
  242. = $(result).find('>query>feature[var="muc_passwordprotected"]')
  243. .length
  244. === 1;
  245. if (locked !== this.locked) {
  246. this.eventEmitter.emit(XMPPEvents.MUC_LOCK_CHANGED, locked);
  247. this.locked = locked;
  248. }
  249. const meetingIdValEl
  250. = $(result).find('>query>x[type="result"]>field[var="muc#roominfo_meetingId"]>value');
  251. if (meetingIdValEl.length) {
  252. this.setMeetingId(meetingIdValEl.text());
  253. } else {
  254. logger.trace('No meeting ID from backend');
  255. }
  256. }, error => {
  257. GlobalOnErrorHandler.callErrorHandler(error);
  258. logger.error('Error getting room info: ', error);
  259. });
  260. }
  261. /**
  262. * Sets the meeting unique Id (received from the backend).
  263. *
  264. * @param {string} meetingId - The new meetings id.
  265. * @returns {void}
  266. */
  267. setMeetingId(meetingId) {
  268. if (this.meetingId !== meetingId) {
  269. if (this.meetingId) {
  270. logger.warn(`Meeting Id changed from:${this.meetingId} to:${meetingId}`);
  271. }
  272. this.meetingId = meetingId;
  273. this.eventEmitter.emit(XMPPEvents.MEETING_ID_SET, meetingId);
  274. }
  275. }
  276. /**
  277. *
  278. */
  279. createNonAnonymousRoom() {
  280. // http://xmpp.org/extensions/xep-0045.html#createroom-reserved
  281. const getForm = $iq({ type: 'get',
  282. to: this.roomjid })
  283. .c('query', { xmlns: 'http://jabber.org/protocol/muc#owner' })
  284. .c('x', { xmlns: 'jabber:x:data',
  285. type: 'submit' });
  286. const self = this;
  287. this.connection.sendIQ(getForm, form => {
  288. if (!$(form).find(
  289. '>query>x[xmlns="jabber:x:data"]'
  290. + '>field[var="muc#roomconfig_whois"]').length) {
  291. const errmsg = 'non-anonymous rooms not supported';
  292. GlobalOnErrorHandler.callErrorHandler(new Error(errmsg));
  293. logger.error(errmsg);
  294. return;
  295. }
  296. const formSubmit = $iq({ to: self.roomjid,
  297. type: 'set' })
  298. .c('query', { xmlns: 'http://jabber.org/protocol/muc#owner' });
  299. formSubmit.c('x', { xmlns: 'jabber:x:data',
  300. type: 'submit' });
  301. formSubmit.c('field', { 'var': 'FORM_TYPE' })
  302. .c('value')
  303. .t('http://jabber.org/protocol/muc#roomconfig').up().up();
  304. formSubmit.c('field', { 'var': 'muc#roomconfig_whois' })
  305. .c('value').t('anyone').up().up();
  306. self.connection.sendIQ(formSubmit);
  307. }, error => {
  308. GlobalOnErrorHandler.callErrorHandler(error);
  309. logger.error('Error getting room configuration form: ', error);
  310. });
  311. }
  312. /**
  313. * Handles Xmpp Connection status updates.
  314. *
  315. * @param {Strophe.Status} status - The Strophe connection status.
  316. */
  317. onConnStatusChanged(status) {
  318. // Send cached presence when the XMPP connection is re-established.
  319. if (status === XmppConnection.Status.CONNECTED) {
  320. this.sendPresence();
  321. }
  322. }
  323. /**
  324. *
  325. * @param pres
  326. */
  327. onPresence(pres) {
  328. const from = pres.getAttribute('from');
  329. const member = {};
  330. const statusEl = pres.getElementsByTagName('status')[0];
  331. if (statusEl) {
  332. member.status = statusEl.textContent || '';
  333. }
  334. let hasStatusUpdate = false;
  335. let hasVersionUpdate = false;
  336. const xElement
  337. = pres.getElementsByTagNameNS(
  338. 'http://jabber.org/protocol/muc#user', 'x')[0];
  339. const mucUserItem
  340. = xElement && xElement.getElementsByTagName('item')[0];
  341. member.affiliation
  342. = mucUserItem && mucUserItem.getAttribute('affiliation');
  343. member.role = mucUserItem && mucUserItem.getAttribute('role');
  344. // Focus recognition
  345. const jid = mucUserItem && mucUserItem.getAttribute('jid');
  346. member.jid = jid;
  347. member.isFocus
  348. = jid && jid.indexOf(`${this.moderator.getFocusUserJid()}/`) === 0;
  349. member.isHiddenDomain
  350. = jid && jid.indexOf('@') > 0
  351. && this.options.hiddenDomain
  352. === jid.substring(jid.indexOf('@') + 1, jid.indexOf('/'));
  353. this.eventEmitter.emit(XMPPEvents.PRESENCE_RECEIVED, {
  354. fromHiddenDomain: member.isHiddenDomain,
  355. presence: pres
  356. });
  357. const xEl = pres.querySelector('x');
  358. if (xEl) {
  359. xEl.remove();
  360. }
  361. const nodes = [];
  362. parser.packet2JSON(pres, nodes);
  363. this.lastPresences[from] = nodes;
  364. // process nodes to extract data needed for MUC_JOINED and
  365. // MUC_MEMBER_JOINED events
  366. const extractIdentityInformation = node => {
  367. const identity = {};
  368. const userInfo = node.children.find(c => c.tagName === 'user');
  369. if (userInfo) {
  370. identity.user = {};
  371. for (const tag of [ 'id', 'name', 'avatar' ]) {
  372. const child
  373. = userInfo.children.find(c => c.tagName === tag);
  374. if (child) {
  375. identity.user[tag] = child.value;
  376. }
  377. }
  378. }
  379. const groupInfo = node.children.find(c => c.tagName === 'group');
  380. if (groupInfo) {
  381. identity.group = groupInfo.value;
  382. }
  383. return identity;
  384. };
  385. for (let i = 0; i < nodes.length; i++) {
  386. const node = nodes[i];
  387. switch (node.tagName) {
  388. case 'bot': {
  389. const { attributes } = node;
  390. if (!attributes) {
  391. break;
  392. }
  393. const { type } = attributes;
  394. member.botType = type;
  395. break;
  396. }
  397. case 'nick':
  398. member.nick = node.value;
  399. break;
  400. case 'userId':
  401. member.id = node.value;
  402. break;
  403. case 'stats-id':
  404. member.statsID = node.value;
  405. break;
  406. case 'identity':
  407. member.identity = extractIdentityInformation(node);
  408. break;
  409. case 'stat': {
  410. const { attributes } = node;
  411. if (!attributes) {
  412. break;
  413. }
  414. const { name } = attributes;
  415. if (name === 'version') {
  416. member.version = attributes.value;
  417. }
  418. break;
  419. }
  420. }
  421. }
  422. if (from === this.myroomjid) {
  423. const newRole
  424. = member.affiliation === 'owner' ? member.role : 'none';
  425. if (this.role !== newRole) {
  426. this.role = newRole;
  427. this.eventEmitter.emit(
  428. XMPPEvents.LOCAL_ROLE_CHANGED,
  429. this.role);
  430. }
  431. if (!this.joined) {
  432. this.joined = true;
  433. const now = this.connectionTimes['muc.joined']
  434. = window.performance.now();
  435. logger.log('(TIME) MUC joined:\t', now);
  436. // set correct initial state of locked
  437. if (this.password) {
  438. this.locked = true;
  439. }
  440. // Re-send presence in case any presence updates were added,
  441. // but blocked from sending, during the join process.
  442. this.sendPresence();
  443. this.eventEmitter.emit(XMPPEvents.MUC_JOINED);
  444. // Now let's check the disco-info to retrieve the
  445. // meeting Id if any
  446. this.discoRoomInfo();
  447. }
  448. } else if (jid === undefined) {
  449. logger.info('Ignoring member with undefined JID');
  450. } else if (this.members[from] === undefined) {
  451. // new participant
  452. this.members[from] = member;
  453. logger.log('entered', from, member);
  454. hasStatusUpdate = member.status !== undefined;
  455. hasVersionUpdate = member.version !== undefined;
  456. if (member.isFocus) {
  457. this._initFocus(from, jid);
  458. } else {
  459. // identity is being added to member joined, so external
  460. // services can be notified for that (currently identity is
  461. // not used inside library)
  462. this.eventEmitter.emit(
  463. XMPPEvents.MUC_MEMBER_JOINED,
  464. from,
  465. member.nick,
  466. member.role,
  467. member.isHiddenDomain,
  468. member.statsID,
  469. member.status,
  470. member.identity,
  471. member.botType);
  472. // we are reporting the status with the join
  473. // so we do not want a second event about status update
  474. hasStatusUpdate = false;
  475. }
  476. } else {
  477. // Presence update for existing participant
  478. // Watch role change:
  479. const memberOfThis = this.members[from];
  480. if (memberOfThis.role !== member.role) {
  481. memberOfThis.role = member.role;
  482. this.eventEmitter.emit(
  483. XMPPEvents.MUC_ROLE_CHANGED, from, member.role);
  484. }
  485. // fire event that botType had changed
  486. if (memberOfThis.botType !== member.botType) {
  487. memberOfThis.botType = member.botType;
  488. this.eventEmitter.emit(
  489. XMPPEvents.MUC_MEMBER_BOT_TYPE_CHANGED,
  490. from,
  491. member.botType);
  492. }
  493. if (member.isFocus) {
  494. // From time to time first few presences of the focus are not
  495. // containing it's jid. That way we can mark later the focus
  496. // member instead of not marking it at all and not starting the
  497. // conference.
  498. // FIXME: Maybe there is a better way to handle this issue. It
  499. // seems there is some period of time in prosody that the
  500. // configuration form is received but not applied. And if any
  501. // participant joins during that period of time the first
  502. // presence from the focus won't contain
  503. // <item jid="focus..." />.
  504. memberOfThis.isFocus = true;
  505. this._initFocus(from, jid);
  506. }
  507. // store the new display name
  508. if (member.displayName) {
  509. memberOfThis.displayName = member.displayName;
  510. }
  511. // update stored status message to be able to detect changes
  512. if (memberOfThis.status !== member.status) {
  513. hasStatusUpdate = true;
  514. memberOfThis.status = member.status;
  515. }
  516. if (memberOfThis.version !== member.version) {
  517. hasVersionUpdate = true;
  518. memberOfThis.version = member.version;
  519. }
  520. }
  521. // after we had fired member or room joined events, lets fire events
  522. // for the rest info we got in presence
  523. for (let i = 0; i < nodes.length; i++) {
  524. const node = nodes[i];
  525. switch (node.tagName) {
  526. case 'nick':
  527. if (!member.isFocus) {
  528. const displayName
  529. = this.xmpp.options.displayJids
  530. ? Strophe.getResourceFromJid(from)
  531. : member.nick;
  532. this.eventEmitter.emit(
  533. XMPPEvents.DISPLAY_NAME_CHANGED,
  534. from,
  535. displayName);
  536. }
  537. break;
  538. case 'bridgeNotAvailable':
  539. if (member.isFocus && !this.noBridgeAvailable) {
  540. this.noBridgeAvailable = true;
  541. this.eventEmitter.emit(XMPPEvents.BRIDGE_DOWN);
  542. }
  543. break;
  544. case 'conference-properties':
  545. if (member.isFocus) {
  546. const properties = {};
  547. for (let j = 0; j < node.children.length; j++) {
  548. const { attributes } = node.children[j];
  549. if (attributes && attributes.key) {
  550. properties[attributes.key] = attributes.value;
  551. }
  552. }
  553. this.eventEmitter.emit(
  554. XMPPEvents.CONFERENCE_PROPERTIES_CHANGED, properties);
  555. }
  556. break;
  557. case 'transcription-status': {
  558. const { attributes } = node;
  559. if (!attributes) {
  560. break;
  561. }
  562. const { status } = attributes;
  563. if (status && status !== this.transcriptionStatus) {
  564. this.transcriptionStatus = status;
  565. this.eventEmitter.emit(
  566. XMPPEvents.TRANSCRIPTION_STATUS_CHANGED,
  567. status
  568. );
  569. }
  570. break;
  571. }
  572. case 'call-control': {
  573. const att = node.attributes;
  574. if (!att) {
  575. break;
  576. }
  577. this.phoneNumber = att.phone || null;
  578. this.phonePin = att.pin || null;
  579. this.eventEmitter.emit(XMPPEvents.PHONE_NUMBER_CHANGED);
  580. break;
  581. }
  582. default:
  583. this.processNode(node, from);
  584. }
  585. }
  586. // Trigger status message update if necessary
  587. if (hasStatusUpdate) {
  588. this.eventEmitter.emit(
  589. XMPPEvents.PRESENCE_STATUS,
  590. from,
  591. member.status);
  592. }
  593. if (hasVersionUpdate) {
  594. logger.info(`Received version for ${jid}: ${member.version}`);
  595. }
  596. }
  597. /**
  598. * Initialize some properties when the focus participant is verified.
  599. * @param from jid of the focus
  600. * @param mucJid the jid of the focus in the muc
  601. */
  602. _initFocus(from, mucJid) {
  603. this.focusMucJid = from;
  604. logger.info(`Ignore focus: ${from}, real JID: ${mucJid}`);
  605. }
  606. /**
  607. * Sets the special listener to be used for "command"s whose name starts
  608. * with "jitsi_participant_".
  609. */
  610. setParticipantPropertyListener(listener) {
  611. this.participantPropertyListener = listener;
  612. }
  613. /**
  614. *
  615. * @param node
  616. * @param from
  617. */
  618. processNode(node, from) {
  619. // make sure we catch all errors coming from any handler
  620. // otherwise we can remove the presence handler from strophe
  621. try {
  622. let tagHandlers = this.presHandlers[node.tagName];
  623. if (node.tagName.startsWith('jitsi_participant_')) {
  624. tagHandlers = [ this.participantPropertyListener ];
  625. }
  626. if (tagHandlers) {
  627. tagHandlers.forEach(handler => {
  628. handler(node, Strophe.getResourceFromJid(from), from);
  629. });
  630. }
  631. } catch (e) {
  632. GlobalOnErrorHandler.callErrorHandler(e);
  633. logger.error(`Error processing:${node.tagName} node.`, e);
  634. }
  635. }
  636. /**
  637. * Send text message to the other participants in the conference
  638. * @param message
  639. * @param elementName
  640. * @param nickname
  641. */
  642. sendMessage(message, elementName, nickname) {
  643. const msg = $msg({ to: this.roomjid,
  644. type: 'groupchat' });
  645. // We are adding the message in a packet extension. If this element
  646. // is different from 'body', we add a custom namespace.
  647. // e.g. for 'json-message' extension of message stanza.
  648. if (elementName === 'body') {
  649. msg.c(elementName, message).up();
  650. } else {
  651. msg.c(elementName, { xmlns: 'http://jitsi.org/jitmeet' }, message)
  652. .up();
  653. }
  654. if (nickname) {
  655. msg.c('nick', { xmlns: 'http://jabber.org/protocol/nick' })
  656. .t(nickname)
  657. .up()
  658. .up();
  659. }
  660. this.connection.send(msg);
  661. this.eventEmitter.emit(XMPPEvents.SENDING_CHAT_MESSAGE, message);
  662. }
  663. /* eslint-disable max-params */
  664. /**
  665. * Send private text message to another participant of the conference
  666. * @param id id/muc resource of the receiver
  667. * @param message
  668. * @param elementName
  669. * @param nickname
  670. */
  671. sendPrivateMessage(id, message, elementName, nickname) {
  672. const msg = $msg({ to: `${this.roomjid}/${id}`,
  673. type: 'chat' });
  674. // We are adding the message in packet. If this element is different
  675. // from 'body', we add our custom namespace for the same.
  676. // e.g. for 'json-message' message extension.
  677. if (elementName === 'body') {
  678. msg.c(elementName, message).up();
  679. } else {
  680. msg.c(elementName, { xmlns: 'http://jitsi.org/jitmeet' }, message)
  681. .up();
  682. }
  683. if (nickname) {
  684. msg.c('nick', { xmlns: 'http://jabber.org/protocol/nick' })
  685. .t(nickname)
  686. .up()
  687. .up();
  688. }
  689. this.connection.send(msg);
  690. this.eventEmitter.emit(
  691. XMPPEvents.SENDING_PRIVATE_CHAT_MESSAGE, message);
  692. }
  693. /* eslint-enable max-params */
  694. /**
  695. *
  696. * @param subject
  697. */
  698. setSubject(subject) {
  699. const msg = $msg({ to: this.roomjid,
  700. type: 'groupchat' });
  701. msg.c('subject', subject);
  702. this.connection.send(msg);
  703. }
  704. /**
  705. * Called when participant leaves.
  706. * @param jid the jid of the participant that leaves
  707. * @param skipEvents optional params to skip any events, including check
  708. * whether this is the focus that left
  709. */
  710. onParticipantLeft(jid, skipEvents) {
  711. delete this.lastPresences[jid];
  712. if (skipEvents) {
  713. return;
  714. }
  715. this.eventEmitter.emit(XMPPEvents.MUC_MEMBER_LEFT, jid);
  716. this.moderator.onMucMemberLeft(jid);
  717. }
  718. /**
  719. *
  720. * @param pres
  721. * @param from
  722. */
  723. onPresenceUnavailable(pres, from) {
  724. // ignore presence
  725. if ($(pres).find('>ignore[xmlns="http://jitsi.org/jitmeet/"]').length) {
  726. return true;
  727. }
  728. // room destroyed ?
  729. if ($(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]'
  730. + '>destroy').length) {
  731. let reason;
  732. const reasonSelect
  733. = $(pres).find(
  734. '>x[xmlns="http://jabber.org/protocol/muc#user"]'
  735. + '>destroy>reason');
  736. if (reasonSelect.length) {
  737. reason = reasonSelect.text();
  738. }
  739. this.eventEmitter.emit(XMPPEvents.MUC_DESTROYED, reason);
  740. this.connection.emuc.doLeave(this.roomjid);
  741. return true;
  742. }
  743. // Status code 110 indicates that this notification is "self-presence".
  744. const isSelfPresence
  745. = $(pres)
  746. .find(
  747. '>x[xmlns="http://jabber.org/protocol/muc#user"]>'
  748. + 'status[code="110"]')
  749. .length;
  750. const isKick
  751. = $(pres)
  752. .find(
  753. '>x[xmlns="http://jabber.org/protocol/muc#user"]'
  754. + '>status[code="307"]')
  755. .length;
  756. const membersKeys = Object.keys(this.members);
  757. if (isKick) {
  758. const actorSelect
  759. = $(pres)
  760. .find('>x[xmlns="http://jabber.org/protocol/muc#user"]>item>actor');
  761. let actorNick;
  762. if (actorSelect.length) {
  763. actorNick = actorSelect.attr('nick');
  764. }
  765. // if no member is found this is the case we had kicked someone
  766. // and we are not in the list of members
  767. if (membersKeys.find(jid => Strophe.getResourceFromJid(jid) === actorNick)) {
  768. // we first fire the kicked so we can show the participant
  769. // who kicked, before notifying that participant left
  770. // we fire kicked for us and for any participant kicked
  771. this.eventEmitter.emit(
  772. XMPPEvents.KICKED,
  773. isSelfPresence,
  774. actorNick,
  775. Strophe.getResourceFromJid(from));
  776. }
  777. }
  778. if (!isSelfPresence) {
  779. delete this.members[from];
  780. this.onParticipantLeft(from, false);
  781. } else if (membersKeys.length > 0) {
  782. // If the status code is 110 this means we're leaving and we would
  783. // like to remove everyone else from our view, so we trigger the
  784. // event.
  785. membersKeys.forEach(jid => {
  786. const member = this.members[jid];
  787. delete this.members[jid];
  788. this.onParticipantLeft(jid, member.isFocus);
  789. });
  790. this.connection.emuc.doLeave(this.roomjid);
  791. // we fire muc_left only if this is not a kick,
  792. // kick has both statuses 110 and 307.
  793. if (!isKick) {
  794. this.eventEmitter.emit(XMPPEvents.MUC_LEFT);
  795. }
  796. }
  797. }
  798. /**
  799. *
  800. * @param msg
  801. * @param from
  802. */
  803. onMessage(msg, from) {
  804. const nick
  805. = $(msg).find('>nick[xmlns="http://jabber.org/protocol/nick"]')
  806. .text()
  807. || Strophe.getResourceFromJid(from);
  808. const type = msg.getAttribute('type');
  809. if (type === 'error') {
  810. const errorMsg = $(msg).find('>error>text').text();
  811. this.eventEmitter.emit(XMPPEvents.CHAT_ERROR_RECEIVED, errorMsg);
  812. return true;
  813. }
  814. const txt = $(msg).find('>body').text();
  815. const subject = $(msg).find('>subject');
  816. if (subject.length) {
  817. const subjectText = subject.text();
  818. if (subjectText || subjectText === '') {
  819. this.eventEmitter.emit(XMPPEvents.SUBJECT_CHANGED, subjectText);
  820. logger.log(`Subject is changed to ${subjectText}`);
  821. }
  822. }
  823. // xep-0203 delay
  824. let stamp = $(msg).find('>delay').attr('stamp');
  825. if (!stamp) {
  826. // or xep-0091 delay, UTC timestamp
  827. stamp = $(msg).find('>[xmlns="jabber:x:delay"]').attr('stamp');
  828. if (stamp) {
  829. // the format is CCYYMMDDThh:mm:ss
  830. const dateParts
  831. = stamp.match(/(\d{4})(\d{2})(\d{2}T\d{2}:\d{2}:\d{2})/);
  832. stamp = `${dateParts[1]}-${dateParts[2]}-${dateParts[3]}Z`;
  833. }
  834. }
  835. if (from === this.roomjid
  836. && $(msg).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="104"]').length) {
  837. this.discoRoomInfo();
  838. }
  839. const jsonMessage = $(msg).find('>json-message').text();
  840. const parsedJson = this.xmpp.tryParseJSONAndVerify(jsonMessage);
  841. // We emit this event if the message is a valid json, and is not
  842. // delivered after a delay, i.e. stamp is undefined.
  843. // e.g. - subtitles should not be displayed if delayed.
  844. if (parsedJson && stamp === undefined) {
  845. this.eventEmitter.emit(XMPPEvents.JSON_MESSAGE_RECEIVED,
  846. from, parsedJson);
  847. return;
  848. }
  849. if (txt) {
  850. if (type === 'chat') {
  851. this.eventEmitter.emit(XMPPEvents.PRIVATE_MESSAGE_RECEIVED,
  852. from, nick, txt, this.myroomjid, stamp);
  853. } else if (type === 'groupchat') {
  854. this.eventEmitter.emit(XMPPEvents.MESSAGE_RECEIVED,
  855. from, nick, txt, this.myroomjid, stamp);
  856. }
  857. }
  858. }
  859. /**
  860. *
  861. * @param pres
  862. * @param from
  863. */
  864. onPresenceError(pres, from) {
  865. if ($(pres)
  866. .find(
  867. '>error[type="auth"]'
  868. + '>not-authorized['
  869. + 'xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]')
  870. .length) {
  871. logger.log('on password required', from);
  872. this.eventEmitter.emit(XMPPEvents.PASSWORD_REQUIRED);
  873. } else if ($(pres)
  874. .find(
  875. '>error[type="cancel"]'
  876. + '>not-allowed['
  877. + 'xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]')
  878. .length) {
  879. const toDomain = Strophe.getDomainFromJid(pres.getAttribute('to'));
  880. if (toDomain === this.xmpp.options.hosts.anonymousdomain) {
  881. // enter the room by replying with 'not-authorized'. This would
  882. // result in reconnection from authorized domain.
  883. // We're either missing Jicofo/Prosody config for anonymous
  884. // domains or something is wrong.
  885. this.eventEmitter.emit(XMPPEvents.ROOM_JOIN_ERROR);
  886. } else {
  887. logger.warn('onPresError ', pres);
  888. this.eventEmitter.emit(
  889. XMPPEvents.ROOM_CONNECT_NOT_ALLOWED_ERROR);
  890. }
  891. } else if ($(pres).find('>error>service-unavailable').length) {
  892. logger.warn('Maximum users limit for the room has been reached',
  893. pres);
  894. this.eventEmitter.emit(XMPPEvents.ROOM_MAX_USERS_ERROR);
  895. } else {
  896. logger.warn('onPresError ', pres);
  897. this.eventEmitter.emit(XMPPEvents.ROOM_CONNECT_ERROR);
  898. }
  899. }
  900. /**
  901. *
  902. * @param jid
  903. */
  904. kick(jid) {
  905. const kickIQ = $iq({ to: this.roomjid,
  906. type: 'set' })
  907. .c('query', { xmlns: 'http://jabber.org/protocol/muc#admin' })
  908. .c('item', { nick: Strophe.getResourceFromJid(jid),
  909. role: 'none' })
  910. .c('reason').t('You have been kicked.').up().up().up();
  911. this.connection.sendIQ(
  912. kickIQ,
  913. result => logger.log('Kick participant with jid: ', jid, result),
  914. error => logger.log('Kick participant error: ', error));
  915. }
  916. /* eslint-disable max-params */
  917. /**
  918. *
  919. * @param key
  920. * @param onSuccess
  921. * @param onError
  922. * @param onNotSupported
  923. */
  924. lockRoom(key, onSuccess, onError, onNotSupported) {
  925. // http://xmpp.org/extensions/xep-0045.html#roomconfig
  926. this.connection.sendIQ(
  927. $iq({
  928. to: this.roomjid,
  929. type: 'get'
  930. })
  931. .c('query', { xmlns: 'http://jabber.org/protocol/muc#owner' }),
  932. res => {
  933. if ($(res)
  934. .find(
  935. '>query>x[xmlns="jabber:x:data"]'
  936. + '>field[var="muc#roomconfig_roomsecret"]')
  937. .length) {
  938. const formsubmit
  939. = $iq({
  940. to: this.roomjid,
  941. type: 'set'
  942. })
  943. .c('query', {
  944. xmlns: 'http://jabber.org/protocol/muc#owner'
  945. });
  946. formsubmit.c('x', {
  947. xmlns: 'jabber:x:data',
  948. type: 'submit'
  949. });
  950. formsubmit
  951. .c('field', { 'var': 'FORM_TYPE' })
  952. .c('value')
  953. .t('http://jabber.org/protocol/muc#roomconfig')
  954. .up()
  955. .up();
  956. formsubmit
  957. .c('field', { 'var': 'muc#roomconfig_roomsecret' })
  958. .c('value')
  959. .t(key)
  960. .up()
  961. .up();
  962. formsubmit
  963. .c('field',
  964. { 'var': 'muc#roomconfig_passwordprotectedroom' })
  965. .c('value')
  966. .t(key === null || key.length === 0 ? '0' : '1')
  967. .up()
  968. .up();
  969. // Fixes a bug in prosody 0.9.+
  970. // https://prosody.im/issues/issue/373
  971. formsubmit
  972. .c('field', { 'var': 'muc#roomconfig_whois' })
  973. .c('value')
  974. .t('anyone')
  975. .up()
  976. .up();
  977. this.connection.sendIQ(formsubmit, onSuccess, onError);
  978. } else {
  979. onNotSupported();
  980. }
  981. },
  982. onError);
  983. }
  984. /* eslint-enable max-params */
  985. /**
  986. *
  987. * @param key
  988. * @param values
  989. */
  990. addToPresence(key, values) {
  991. values.tagName = key;
  992. this.removeFromPresence(key);
  993. this.presMap.nodes.push(values);
  994. }
  995. /**
  996. * Retreives a value from the presence map.
  997. *
  998. * @param {string} key - The key to find the value for.
  999. * @returns {Object?}
  1000. */
  1001. getFromPresence(key) {
  1002. return this.presMap.nodes.find(node => key === node.tagName);
  1003. }
  1004. /**
  1005. *
  1006. * @param key
  1007. */
  1008. removeFromPresence(key) {
  1009. const nodes = this.presMap.nodes.filter(node => key !== node.tagName);
  1010. this.presMap.nodes = nodes;
  1011. }
  1012. /**
  1013. *
  1014. * @param name
  1015. * @param handler
  1016. */
  1017. addPresenceListener(name, handler) {
  1018. if (typeof handler !== 'function') {
  1019. throw new Error('"handler" is not a function');
  1020. }
  1021. let tagHandlers = this.presHandlers[name];
  1022. if (!tagHandlers) {
  1023. this.presHandlers[name] = tagHandlers = [];
  1024. }
  1025. if (tagHandlers.indexOf(handler) === -1) {
  1026. tagHandlers.push(handler);
  1027. } else {
  1028. logger.warn(
  1029. `Trying to add the same handler more than once for: ${name}`);
  1030. }
  1031. }
  1032. /**
  1033. *
  1034. * @param name
  1035. * @param handler
  1036. */
  1037. removePresenceListener(name, handler) {
  1038. const tagHandlers = this.presHandlers[name];
  1039. const handlerIdx = tagHandlers ? tagHandlers.indexOf(handler) : -1;
  1040. // eslint-disable-next-line no-negated-condition
  1041. if (handlerIdx !== -1) {
  1042. tagHandlers.splice(handlerIdx, 1);
  1043. } else {
  1044. logger.warn(`Handler for: ${name} was not registered`);
  1045. }
  1046. }
  1047. /**
  1048. * Checks if the user identified by given <tt>mucJid</tt> is the conference
  1049. * focus.
  1050. * @param mucJid the full MUC address of the user to be checked.
  1051. * @returns {boolean|null} <tt>true</tt> if MUC user is the conference focus
  1052. * or <tt>false</tt> if is not. When given <tt>mucJid</tt> does not exist in
  1053. * the MUC then <tt>null</tt> is returned.
  1054. */
  1055. isFocus(mucJid) {
  1056. const member = this.members[mucJid];
  1057. if (member) {
  1058. return member.isFocus;
  1059. }
  1060. return null;
  1061. }
  1062. /**
  1063. *
  1064. */
  1065. isModerator() {
  1066. return this.role === 'moderator';
  1067. }
  1068. /**
  1069. *
  1070. * @param peerJid
  1071. */
  1072. getMemberRole(peerJid) {
  1073. if (this.members[peerJid]) {
  1074. return this.members[peerJid].role;
  1075. }
  1076. return null;
  1077. }
  1078. /**
  1079. *
  1080. * @param mute
  1081. * @param callback
  1082. */
  1083. setVideoMute(mute, callback) {
  1084. this.sendVideoInfoPresence(mute);
  1085. if (callback) {
  1086. callback(mute);
  1087. }
  1088. }
  1089. /**
  1090. *
  1091. * @param mute
  1092. * @param callback
  1093. */
  1094. setAudioMute(mute, callback) {
  1095. return this.sendAudioInfoPresence(mute, callback);
  1096. }
  1097. /**
  1098. *
  1099. * @param mute
  1100. */
  1101. addAudioInfoToPresence(mute) {
  1102. this.removeFromPresence('audiomuted');
  1103. this.addToPresence(
  1104. 'audiomuted',
  1105. {
  1106. attributes: { 'xmlns': 'http://jitsi.org/jitmeet/audio' },
  1107. value: mute.toString()
  1108. });
  1109. }
  1110. /**
  1111. *
  1112. * @param mute
  1113. * @param callback
  1114. */
  1115. sendAudioInfoPresence(mute, callback) {
  1116. this.addAudioInfoToPresence(mute);
  1117. // FIXME resend presence on CONNECTED
  1118. this.sendPresence();
  1119. if (callback) {
  1120. callback();
  1121. }
  1122. }
  1123. /**
  1124. *
  1125. * @param mute
  1126. */
  1127. addVideoInfoToPresence(mute) {
  1128. this.removeFromPresence('videomuted');
  1129. this.addToPresence(
  1130. 'videomuted',
  1131. {
  1132. attributes: { 'xmlns': 'http://jitsi.org/jitmeet/video' },
  1133. value: mute.toString()
  1134. });
  1135. }
  1136. /**
  1137. *
  1138. * @param mute
  1139. */
  1140. sendVideoInfoPresence(mute) {
  1141. this.addVideoInfoToPresence(mute);
  1142. this.sendPresence();
  1143. }
  1144. /**
  1145. * Obtains the info about given media advertised in the MUC presence of
  1146. * the participant identified by the given endpoint JID.
  1147. * @param {string} endpointId the endpoint ID mapped to the participant
  1148. * which corresponds to MUC nickname.
  1149. * @param {MediaType} mediaType the type of the media for which presence
  1150. * info will be obtained.
  1151. * @return {PeerMediaInfo} presenceInfo an object with media presence
  1152. * info or <tt>null</tt> either if there is no presence available or if
  1153. * the media type given is invalid.
  1154. */
  1155. getMediaPresenceInfo(endpointId, mediaType) {
  1156. // Will figure out current muted status by looking up owner's presence
  1157. const pres = this.lastPresences[`${this.roomjid}/${endpointId}`];
  1158. if (!pres) {
  1159. // No presence available
  1160. return null;
  1161. }
  1162. const data = {
  1163. muted: false, // unmuted by default
  1164. videoType: undefined // no video type by default
  1165. };
  1166. let mutedNode = null;
  1167. if (mediaType === MediaType.AUDIO) {
  1168. mutedNode = filterNodeFromPresenceJSON(pres, 'audiomuted');
  1169. } else if (mediaType === MediaType.VIDEO) {
  1170. mutedNode = filterNodeFromPresenceJSON(pres, 'videomuted');
  1171. const videoTypeNode = filterNodeFromPresenceJSON(pres, 'videoType');
  1172. if (videoTypeNode.length > 0) {
  1173. data.videoType = videoTypeNode[0].value;
  1174. }
  1175. } else {
  1176. logger.error(`Unsupported media type: ${mediaType}`);
  1177. return null;
  1178. }
  1179. data.muted = mutedNode.length > 0 && mutedNode[0].value === 'true';
  1180. return data;
  1181. }
  1182. /**
  1183. * Returns true if the SIP calls are supported and false otherwise
  1184. */
  1185. isSIPCallingSupported() {
  1186. if (this.moderator) {
  1187. return this.moderator.isSipGatewayEnabled();
  1188. }
  1189. return false;
  1190. }
  1191. /**
  1192. * Dials a number.
  1193. * @param number the number
  1194. */
  1195. dial(number) {
  1196. return this.connection.rayo.dial(number, 'fromnumber',
  1197. Strophe.getBareJidFromJid(this.myroomjid), this.password,
  1198. this.focusMucJid);
  1199. }
  1200. /**
  1201. * Hangup an existing call
  1202. */
  1203. hangup() {
  1204. return this.connection.rayo.hangup();
  1205. }
  1206. /**
  1207. * Returns the phone number for joining the conference.
  1208. */
  1209. getPhoneNumber() {
  1210. return this.phoneNumber;
  1211. }
  1212. /**
  1213. * Returns the pin for joining the conference with phone.
  1214. */
  1215. getPhonePin() {
  1216. return this.phonePin;
  1217. }
  1218. /**
  1219. * Returns the meeting unique ID if any came from backend.
  1220. *
  1221. * @returns {string} - The meeting ID.
  1222. */
  1223. getMeetingId() {
  1224. return this.meetingId;
  1225. }
  1226. /**
  1227. * Mutes remote participant.
  1228. * @param jid of the participant
  1229. * @param mute
  1230. */
  1231. muteParticipant(jid, mute) {
  1232. logger.info('set mute', mute);
  1233. const iqToFocus = $iq(
  1234. { to: this.focusMucJid,
  1235. type: 'set' })
  1236. .c('mute', {
  1237. xmlns: 'http://jitsi.org/jitmeet/audio',
  1238. jid
  1239. })
  1240. .t(mute.toString())
  1241. .up();
  1242. this.connection.sendIQ(
  1243. iqToFocus,
  1244. result => logger.log('set mute', result),
  1245. error => logger.log('set mute error', error));
  1246. }
  1247. /**
  1248. * TODO: Document
  1249. * @param iq
  1250. */
  1251. onMute(iq) {
  1252. const from = iq.getAttribute('from');
  1253. if (from !== this.focusMucJid) {
  1254. logger.warn('Ignored mute from non focus peer');
  1255. return;
  1256. }
  1257. const mute = $(iq).find('mute');
  1258. if (mute.length && mute.text() === 'true') {
  1259. this.eventEmitter.emit(XMPPEvents.AUDIO_MUTED_BY_FOCUS, mute.attr('actor'));
  1260. } else {
  1261. // XXX Why do we support anything but muting? Why do we encode the
  1262. // value in the text of the element? Why do we use a separate XML
  1263. // namespace?
  1264. logger.warn('Ignoring a mute request which does not explicitly '
  1265. + 'specify a positive mute command.');
  1266. }
  1267. }
  1268. /**
  1269. * Leaves the room. Closes the jingle session.
  1270. * @returns {Promise} which is resolved if XMPPEvents.MUC_LEFT is received
  1271. * less than 5s after sending presence unavailable. Otherwise the promise is
  1272. * rejected.
  1273. */
  1274. leave() {
  1275. return new Promise((resolve, reject) => {
  1276. const timeout = setTimeout(() => onMucLeft(true), 5000);
  1277. const eventEmitter = this.eventEmitter;
  1278. this._removeConnListeners.forEach(remove => remove());
  1279. this._removeConnListeners = [];
  1280. /**
  1281. *
  1282. * @param doReject
  1283. */
  1284. function onMucLeft(doReject = false) {
  1285. eventEmitter.removeListener(XMPPEvents.MUC_LEFT, onMucLeft);
  1286. clearTimeout(timeout);
  1287. if (doReject) {
  1288. // the timeout expired
  1289. reject(new Error('The timeout for the confirmation about '
  1290. + 'leaving the room expired.'));
  1291. } else {
  1292. resolve();
  1293. }
  1294. }
  1295. eventEmitter.on(XMPPEvents.MUC_LEFT, onMucLeft);
  1296. this.doLeave();
  1297. });
  1298. }
  1299. }
  1300. /* eslint-enable newline-per-chained-call */