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 44KB

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