Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

ChatRoom.js 33KB

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