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.

muc.js 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. /* jshint -W117 */
  2. /* a simple MUC connection plugin
  3. * can only handle a single MUC room
  4. */
  5. Strophe.addConnectionPlugin('emuc', {
  6. connection: null,
  7. roomjid: null,
  8. myroomjid: null,
  9. members: {},
  10. list_members: [], // so we can elect a new focus
  11. presMap: {},
  12. preziMap: {},
  13. joined: false,
  14. isOwner: false,
  15. role: null,
  16. init: function (conn) {
  17. this.connection = conn;
  18. },
  19. initPresenceMap: function (myroomjid) {
  20. this.presMap['to'] = myroomjid;
  21. this.presMap['xns'] = 'http://jabber.org/protocol/muc';
  22. },
  23. doJoin: function (jid, password) {
  24. this.myroomjid = jid;
  25. console.info("Joined MUC as " + this.myroomjid);
  26. this.initPresenceMap(this.myroomjid);
  27. if (!this.roomjid) {
  28. this.roomjid = Strophe.getBareJidFromJid(jid);
  29. // add handlers (just once)
  30. this.connection.addHandler(this.onPresence.bind(this), null, 'presence', null, null, this.roomjid, {matchBare: true});
  31. this.connection.addHandler(this.onPresenceUnavailable.bind(this), null, 'presence', 'unavailable', null, this.roomjid, {matchBare: true});
  32. this.connection.addHandler(this.onPresenceError.bind(this), null, 'presence', 'error', null, this.roomjid, {matchBare: true});
  33. this.connection.addHandler(this.onMessage.bind(this), null, 'message', null, null, this.roomjid, {matchBare: true});
  34. }
  35. if (password !== undefined) {
  36. this.presMap['password'] = password;
  37. }
  38. this.sendPresence();
  39. },
  40. doLeave: function() {
  41. console.log("do leave", this.myroomjid);
  42. var pres = $pres({to: this.myroomjid, type: 'unavailable' });
  43. this.presMap.length = 0;
  44. this.connection.send(pres);
  45. },
  46. onPresence: function (pres) {
  47. var from = pres.getAttribute('from');
  48. var type = pres.getAttribute('type');
  49. if (type != null) {
  50. return true;
  51. }
  52. // Parse etherpad tag.
  53. var etherpad = $(pres).find('>etherpad');
  54. if (etherpad.length) {
  55. $(document).trigger('etherpadadded.muc', [from, etherpad.text()]);
  56. }
  57. // Parse prezi tag.
  58. var presentation = $(pres).find('>prezi');
  59. if (presentation.length)
  60. {
  61. var url = presentation.attr('url');
  62. var current = presentation.find('>current').text();
  63. console.log('presentation info received from', from, url);
  64. if (this.preziMap[from] == null) {
  65. this.preziMap[from] = url;
  66. $(document).trigger('presentationadded.muc', [from, url, current]);
  67. }
  68. else {
  69. $(document).trigger('gotoslide.muc', [from, url, current]);
  70. }
  71. }
  72. else if (this.preziMap[from] != null) {
  73. var url = this.preziMap[from];
  74. delete this.preziMap[from];
  75. $(document).trigger('presentationremoved.muc', [from, url]);
  76. }
  77. // Parse audio info tag.
  78. var audioMuted = $(pres).find('>audiomuted');
  79. if (audioMuted.length) {
  80. $(document).trigger('audiomuted.muc', [from, audioMuted.text()]);
  81. }
  82. // Parse video info tag.
  83. var videoMuted = $(pres).find('>videomuted');
  84. if (videoMuted.length) {
  85. $(document).trigger('videomuted.muc', [from, videoMuted.text()]);
  86. }
  87. var stats = $(pres).find('>stats');
  88. if(stats.length)
  89. {
  90. var statsObj = {};
  91. Strophe.forEachChild(stats[0], "stat", function (el) {
  92. statsObj[el.getAttribute("name")] = el.getAttribute("value");
  93. });
  94. ConnectionQuality.updateRemoteStats(from, statsObj);
  95. }
  96. // Parse status.
  97. if ($(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="201"]').length) {
  98. // http://xmpp.org/extensions/xep-0045.html#createroom-instant
  99. this.isOwner = true;
  100. var create = $iq({type: 'set', to: this.roomjid})
  101. .c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'})
  102. .c('x', {xmlns: 'jabber:x:data', type: 'submit'});
  103. this.connection.sendIQ(create); // fire away
  104. }
  105. // Parse roles.
  106. var member = {};
  107. member.show = $(pres).find('>show').text();
  108. member.status = $(pres).find('>status').text();
  109. var tmp = $(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>item');
  110. member.affiliation = tmp.attr('affiliation');
  111. member.role = tmp.attr('role');
  112. // Focus recognition
  113. member.jid = tmp.attr('jid');
  114. member.isFocus = false;
  115. if (member.jid
  116. && member.jid.indexOf(Moderator.getFocusUserJid() + "/") == 0) {
  117. member.isFocus = true;
  118. }
  119. var nicktag = $(pres).find('>nick[xmlns="http://jabber.org/protocol/nick"]');
  120. member.displayName = (nicktag.length > 0 ? nicktag.text() : null);
  121. if (from == this.myroomjid) {
  122. if (member.affiliation == 'owner') this.isOwner = true;
  123. if (this.role !== member.role) {
  124. this.role = member.role;
  125. $(document).trigger('local.role.changed.muc', [from, member, pres]);
  126. }
  127. if (!this.joined) {
  128. this.joined = true;
  129. $(document).trigger('joined.muc', [from, member]);
  130. this.list_members.push(from);
  131. }
  132. } else if (this.members[from] === undefined) {
  133. // new participant
  134. this.members[from] = member;
  135. this.list_members.push(from);
  136. $(document).trigger('entered.muc', [from, member, pres]);
  137. } else {
  138. // Presence update for existing participant
  139. // Watch role change:
  140. if (this.members[from].role != member.role) {
  141. this.members[from].role = member.role
  142. $(document).trigger('role.changed.muc', [from, member, pres]);
  143. }
  144. }
  145. // Always trigger presence to update bindings
  146. console.log('presence change from', from, pres);
  147. $(document).trigger('presence.muc', [from, member, pres]);
  148. // Trigger status message update
  149. if (member.status) {
  150. $(document).trigger('presence.status.muc', [from, member, pres]);
  151. }
  152. return true;
  153. },
  154. onPresenceUnavailable: function (pres) {
  155. var from = pres.getAttribute('from');
  156. // Status code 110 indicates that this notification is "self-presence".
  157. if (!$(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="110"]').length) {
  158. delete this.members[from];
  159. this.list_members.splice(this.list_members.indexOf(from), 1);
  160. $(document).trigger('left.muc', [from]);
  161. }
  162. // If the status code is 110 this means we're leaving and we would like
  163. // to remove everyone else from our view, so we trigger the event.
  164. else if (this.list_members.length > 1) {
  165. for (var i = 0; i < this.list_members.length; i++) {
  166. var member = this.list_members[i];
  167. delete this.members[i];
  168. this.list_members.splice(i, 1);
  169. $(document).trigger('left.muc', member);
  170. }
  171. }
  172. if ($(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="307"]').length) {
  173. $(document).trigger('kicked.muc', [from]);
  174. }
  175. return true;
  176. },
  177. onPresenceError: function (pres) {
  178. var from = pres.getAttribute('from');
  179. if ($(pres).find('>error[type="auth"]>not-authorized[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]').length) {
  180. $(document).trigger('passwordrequired.muc', [from]);
  181. } else if ($(pres).find(
  182. '>error[type="cancel"]>not-allowed[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]').length) {
  183. var toDomain = Strophe.getDomainFromJid(pres.getAttribute('to'));
  184. if(toDomain === config.hosts.anonymousdomain) {
  185. // we are connected with anonymous domain and only non anonymous users can create rooms
  186. // we must authorize the user
  187. $(document).trigger('passwordrequired.main');
  188. } else {
  189. console.warn('onPresError ', pres);
  190. messageHandler.openReportDialog(null,
  191. 'Oops! Something went wrong and we couldn`t connect to the conference.',
  192. pres);
  193. }
  194. } else {
  195. console.warn('onPresError ', pres);
  196. messageHandler.openReportDialog(null,
  197. 'Oops! Something went wrong and we couldn`t connect to the conference.',
  198. pres);
  199. }
  200. return true;
  201. },
  202. sendMessage: function (body, nickname) {
  203. var msg = $msg({to: this.roomjid, type: 'groupchat'});
  204. msg.c('body', body).up();
  205. if (nickname) {
  206. msg.c('nick', {xmlns: 'http://jabber.org/protocol/nick'}).t(nickname).up().up();
  207. }
  208. this.connection.send(msg);
  209. if(APIConnector.isEnabled() && APIConnector.isEventEnabled("outgoingMessage"))
  210. {
  211. APIConnector.triggerEvent("outgoingMessage", {"message": body});
  212. }
  213. },
  214. setSubject: function (subject){
  215. var msg = $msg({to: this.roomjid, type: 'groupchat'});
  216. msg.c('subject', subject);
  217. this.connection.send(msg);
  218. console.log("topic changed to " + subject);
  219. },
  220. onMessage: function (msg) {
  221. // FIXME: this is a hack. but jingle on muc makes nickchanges hard
  222. var from = msg.getAttribute('from');
  223. var nick = $(msg).find('>nick[xmlns="http://jabber.org/protocol/nick"]').text() || Strophe.getResourceFromJid(from);
  224. var txt = $(msg).find('>body').text();
  225. var type = msg.getAttribute("type");
  226. if(type == "error")
  227. {
  228. Chat.chatAddError($(msg).find('>text').text(), txt);
  229. return true;
  230. }
  231. var subject = $(msg).find('>subject');
  232. if(subject.length)
  233. {
  234. var subjectText = subject.text();
  235. if(subjectText || subjectText == "") {
  236. Chat.chatSetSubject(subjectText);
  237. console.log("Subject is changed to " + subjectText);
  238. }
  239. }
  240. if (txt) {
  241. console.log('chat', nick, txt);
  242. Chat.updateChatConversation(from, nick, txt);
  243. if(APIConnector.isEnabled() && APIConnector.isEventEnabled("incomingMessage"))
  244. {
  245. if(from != this.myroomjid)
  246. APIConnector.triggerEvent("incomingMessage",
  247. {"from": from, "nick": nick, "message": txt});
  248. }
  249. }
  250. return true;
  251. },
  252. lockRoom: function (key) {
  253. //http://xmpp.org/extensions/xep-0045.html#roomconfig
  254. var ob = this;
  255. this.connection.sendIQ($iq({to: this.roomjid, type: 'get'}).c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'}),
  256. function (res) {
  257. if ($(res).find('>query>x[xmlns="jabber:x:data"]>field[var="muc#roomconfig_roomsecret"]').length) {
  258. var formsubmit = $iq({to: ob.roomjid, type: 'set'}).c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'});
  259. formsubmit.c('x', {xmlns: 'jabber:x:data', type: 'submit'});
  260. formsubmit.c('field', {'var': 'FORM_TYPE'}).c('value').t('http://jabber.org/protocol/muc#roomconfig').up().up();
  261. formsubmit.c('field', {'var': 'muc#roomconfig_roomsecret'}).c('value').t(key).up().up();
  262. // Fixes a bug in prosody 0.9.+ https://code.google.com/p/lxmppd/issues/detail?id=373
  263. formsubmit.c('field', {'var': 'muc#roomconfig_whois'}).c('value').t('anyone').up().up();
  264. // FIXME: is muc#roomconfig_passwordprotectedroom required?
  265. this.connection.sendIQ(formsubmit,
  266. function (res) {
  267. // password is required
  268. if (sharedKey)
  269. {
  270. console.log('set room password');
  271. Toolbar.lockLockButton();
  272. }
  273. else
  274. {
  275. console.log('removed room password');
  276. Toolbar.unlockLockButton();
  277. }
  278. },
  279. function (err) {
  280. console.warn('setting password failed', err);
  281. messageHandler.showError('Lock failed',
  282. 'Failed to lock conference.',
  283. err);
  284. setSharedKey('');
  285. }
  286. );
  287. } else {
  288. console.warn('room passwords not supported');
  289. messageHandler.showError('Warning',
  290. 'Room passwords are currently not supported.');
  291. setSharedKey('');
  292. }
  293. },
  294. function (err) {
  295. console.warn('setting password failed', err);
  296. messageHandler.showError('Lock failed',
  297. 'Failed to lock conference.',
  298. err);
  299. setSharedKey('');
  300. }
  301. );
  302. },
  303. kick: function (jid) {
  304. var kickIQ = $iq({to: this.roomjid, type: 'set'})
  305. .c('query', {xmlns: 'http://jabber.org/protocol/muc#admin'})
  306. .c('item', {nick: Strophe.getResourceFromJid(jid), role: 'none'})
  307. .c('reason').t('You have been kicked.').up().up().up();
  308. this.connection.sendIQ(
  309. kickIQ,
  310. function (result) {
  311. console.log('Kick participant with jid: ', jid, result);
  312. },
  313. function (error) {
  314. console.log('Kick participant error: ', error);
  315. });
  316. },
  317. sendPresence: function () {
  318. var pres = $pres({to: this.presMap['to'] });
  319. pres.c('x', {xmlns: this.presMap['xns']});
  320. if (this.presMap['password']) {
  321. pres.c('password').t(this.presMap['password']).up();
  322. }
  323. pres.up();
  324. // Send XEP-0115 'c' stanza that contains our capabilities info
  325. if (connection.caps) {
  326. connection.caps.node = config.clientNode;
  327. pres.c('c', connection.caps.generateCapsAttrs()).up();
  328. }
  329. pres.c('user-agent', {xmlns: 'http://jitsi.org/jitmeet/user-agent'})
  330. .t(navigator.userAgent).up();
  331. if(this.presMap['bridgeIsDown']) {
  332. pres.c('bridgeIsDown').up();
  333. }
  334. if(this.presMap['email']) {
  335. pres.c('email').t(this.presMap['email']).up();
  336. }
  337. if(this.presMap['userId']) {
  338. pres.c('userId').t(this.presMap['userId']).up();
  339. }
  340. if (this.presMap['displayName']) {
  341. // XEP-0172
  342. pres.c('nick', {xmlns: 'http://jabber.org/protocol/nick'})
  343. .t(this.presMap['displayName']).up();
  344. }
  345. if (this.presMap['audions']) {
  346. pres.c('audiomuted', {xmlns: this.presMap['audions']})
  347. .t(this.presMap['audiomuted']).up();
  348. }
  349. if (this.presMap['videons']) {
  350. pres.c('videomuted', {xmlns: this.presMap['videons']})
  351. .t(this.presMap['videomuted']).up();
  352. }
  353. if(this.presMap['statsns'])
  354. {
  355. var stats = pres.c('stats', {xmlns: this.presMap['statsns']});
  356. for(var stat in this.presMap["stats"])
  357. if(this.presMap["stats"][stat] != null)
  358. stats.c("stat",{name: stat, value: this.presMap["stats"][stat]}).up();
  359. pres.up();
  360. }
  361. if (this.presMap['prezins']) {
  362. pres.c('prezi',
  363. {xmlns: this.presMap['prezins'],
  364. 'url': this.presMap['preziurl']})
  365. .c('current').t(this.presMap['prezicurrent']).up().up();
  366. }
  367. if (this.presMap['etherpadns']) {
  368. pres.c('etherpad', {xmlns: this.presMap['etherpadns']})
  369. .t(this.presMap['etherpadname']).up();
  370. }
  371. if (this.presMap['medians'])
  372. {
  373. pres.c('media', {xmlns: this.presMap['medians']});
  374. var sourceNumber = 0;
  375. Object.keys(this.presMap).forEach(function (key) {
  376. if (key.indexOf('source') >= 0) {
  377. sourceNumber++;
  378. }
  379. });
  380. if (sourceNumber > 0)
  381. for (var i = 1; i <= sourceNumber/3; i ++) {
  382. pres.c('source',
  383. {type: this.presMap['source' + i + '_type'],
  384. ssrc: this.presMap['source' + i + '_ssrc'],
  385. direction: this.presMap['source'+ i + '_direction']
  386. || 'sendrecv' }
  387. ).up();
  388. }
  389. }
  390. pres.up();
  391. connection.send(pres);
  392. },
  393. addDisplayNameToPresence: function (displayName) {
  394. this.presMap['displayName'] = displayName;
  395. },
  396. addMediaToPresence: function (sourceNumber, mtype, ssrcs, direction) {
  397. if (!this.presMap['medians'])
  398. this.presMap['medians'] = 'http://estos.de/ns/mjs';
  399. this.presMap['source' + sourceNumber + '_type'] = mtype;
  400. this.presMap['source' + sourceNumber + '_ssrc'] = ssrcs;
  401. this.presMap['source' + sourceNumber + '_direction'] = direction;
  402. },
  403. clearPresenceMedia: function () {
  404. var self = this;
  405. Object.keys(this.presMap).forEach( function(key) {
  406. if(key.indexOf('source') != -1) {
  407. delete self.presMap[key];
  408. }
  409. });
  410. },
  411. addPreziToPresence: function (url, currentSlide) {
  412. this.presMap['prezins'] = 'http://jitsi.org/jitmeet/prezi';
  413. this.presMap['preziurl'] = url;
  414. this.presMap['prezicurrent'] = currentSlide;
  415. },
  416. removePreziFromPresence: function () {
  417. delete this.presMap['prezins'];
  418. delete this.presMap['preziurl'];
  419. delete this.presMap['prezicurrent'];
  420. },
  421. addCurrentSlideToPresence: function (currentSlide) {
  422. this.presMap['prezicurrent'] = currentSlide;
  423. },
  424. getPrezi: function (roomjid) {
  425. return this.preziMap[roomjid];
  426. },
  427. addEtherpadToPresence: function(etherpadName) {
  428. this.presMap['etherpadns'] = 'http://jitsi.org/jitmeet/etherpad';
  429. this.presMap['etherpadname'] = etherpadName;
  430. },
  431. addAudioInfoToPresence: function(isMuted) {
  432. this.presMap['audions'] = 'http://jitsi.org/jitmeet/audio';
  433. this.presMap['audiomuted'] = isMuted.toString();
  434. },
  435. addVideoInfoToPresence: function(isMuted) {
  436. this.presMap['videons'] = 'http://jitsi.org/jitmeet/video';
  437. this.presMap['videomuted'] = isMuted.toString();
  438. },
  439. addConnectionInfoToPresence: function(stats) {
  440. this.presMap['statsns'] = 'http://jitsi.org/jitmeet/stats';
  441. this.presMap['stats'] = stats;
  442. },
  443. findJidFromResource: function(resourceJid) {
  444. if(resourceJid &&
  445. resourceJid === Strophe.getResourceFromJid(connection.emuc.myroomjid)) {
  446. return connection.emuc.myroomjid;
  447. }
  448. var peerJid = null;
  449. Object.keys(this.members).some(function (jid) {
  450. peerJid = jid;
  451. return Strophe.getResourceFromJid(jid) === resourceJid;
  452. });
  453. return peerJid;
  454. },
  455. addBridgeIsDownToPresence: function() {
  456. this.presMap['bridgeIsDown'] = true;
  457. },
  458. addEmailToPresence: function(email) {
  459. this.presMap['email'] = email;
  460. },
  461. addUserIdToPresence: function(userId) {
  462. this.presMap['userId'] = userId;
  463. },
  464. isModerator: function() {
  465. return this.role === 'moderator';
  466. }
  467. });