Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

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