converse-muc.js 73 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528
  1. // Converse.js
  2. // http://conversejs.org
  3. //
  4. // Copyright (c) 2013-2018, the Converse.js developers
  5. // Licensed under the Mozilla Public License (MPLv2)
  6. (function (root, factory) {
  7. define([
  8. "./utils/form",
  9. "./converse-core",
  10. "./converse-disco",
  11. "backbone.overview/backbone.overview",
  12. "backbone.overview/backbone.orderedlistview",
  13. "backbone.vdomview",
  14. "./utils/muc",
  15. "./utils/emoji"
  16. ], factory);
  17. }(this, function (u, converse) {
  18. "use strict";
  19. const MUC_ROLE_WEIGHTS = {
  20. 'moderator': 1,
  21. 'participant': 2,
  22. 'visitor': 3,
  23. 'none': 2,
  24. };
  25. const { Strophe, Backbone, Promise, $iq, $build, $msg, $pres, b64_sha1, sizzle, f, moment, _ } = converse.env;
  26. // Add Strophe Namespaces
  27. Strophe.addNamespace('MUC_ADMIN', Strophe.NS.MUC + "#admin");
  28. Strophe.addNamespace('MUC_OWNER', Strophe.NS.MUC + "#owner");
  29. Strophe.addNamespace('MUC_REGISTER', "jabber:iq:register");
  30. Strophe.addNamespace('MUC_ROOMCONF', Strophe.NS.MUC + "#roomconfig");
  31. Strophe.addNamespace('MUC_USER', Strophe.NS.MUC + "#user");
  32. converse.MUC_NICK_CHANGED_CODE = "303";
  33. converse.ROOM_FEATURES = [
  34. 'passwordprotected', 'unsecured', 'hidden',
  35. 'publicroom', 'membersonly', 'open', 'persistent',
  36. 'temporary', 'nonanonymous', 'semianonymous',
  37. 'moderated', 'unmoderated', 'mam_enabled'
  38. ];
  39. converse.ROOMSTATUS = {
  40. CONNECTED: 0,
  41. CONNECTING: 1,
  42. NICKNAME_REQUIRED: 2,
  43. PASSWORD_REQUIRED: 3,
  44. DISCONNECTED: 4,
  45. ENTERED: 5
  46. };
  47. converse.plugins.add('converse-muc', {
  48. /* Optional dependencies are other plugins which might be
  49. * overridden or relied upon, and therefore need to be loaded before
  50. * this plugin. They are called "optional" because they might not be
  51. * available, in which case any overrides applicable to them will be
  52. * ignored.
  53. *
  54. * It's possible however to make optional dependencies non-optional.
  55. * If the setting "strict_plugin_dependencies" is set to true,
  56. * an error will be raised if the plugin is not found.
  57. *
  58. * NB: These plugins need to have already been loaded via require.js.
  59. */
  60. dependencies: ["converse-chatboxes", "converse-disco", "converse-controlbox"],
  61. overrides: {
  62. tearDown () {
  63. const { _converse } = this.__super__,
  64. groupchats = this.chatboxes.where({'type': _converse.CHATROOMS_TYPE});
  65. _.each(groupchats, gc => u.safeSave(gc, {'connection_status': converse.ROOMSTATUS.DISCONNECTED}));
  66. this.__super__.tearDown.call(this, arguments);
  67. },
  68. ChatBoxes: {
  69. model (attrs, options) {
  70. const { _converse } = this.__super__;
  71. if (attrs.type == _converse.CHATROOMS_TYPE) {
  72. return new _converse.ChatRoom(attrs, options);
  73. } else {
  74. return this.__super__.model.apply(this, arguments);
  75. }
  76. },
  77. }
  78. },
  79. initialize () {
  80. /* The initialize function gets called as soon as the plugin is
  81. * loaded by converse.js's plugin machinery.
  82. */
  83. const { _converse } = this,
  84. { __ } = _converse;
  85. // Configuration values for this plugin
  86. // ====================================
  87. // Refer to docs/source/configuration.rst for explanations of these
  88. // configuration settings.
  89. _converse.api.settings.update({
  90. allow_muc: true,
  91. allow_muc_invitations: true,
  92. auto_join_on_invite: false,
  93. auto_join_rooms: [],
  94. auto_register_muc_nickname: false,
  95. muc_domain: undefined,
  96. muc_history_max_stanzas: undefined,
  97. muc_instant_rooms: true,
  98. muc_nickname_from_jid: false
  99. });
  100. _converse.api.promises.add(['roomsAutoJoined']);
  101. function openRoom (jid) {
  102. if (!u.isValidMUCJID(jid)) {
  103. return _converse.log(
  104. `Invalid JID "${jid}" provided in URL fragment`,
  105. Strophe.LogLevel.WARN
  106. );
  107. }
  108. const promises = [_converse.api.waitUntil('roomsAutoJoined')]
  109. if (_converse.allow_bookmarks) {
  110. promises.push( _converse.api.waitUntil('bookmarksInitialized'));
  111. }
  112. Promise.all(promises).then(() => {
  113. _converse.api.rooms.open(jid);
  114. });
  115. }
  116. _converse.router.route('converse/room?jid=:jid', openRoom);
  117. _converse.openChatRoom = function (jid, settings, bring_to_foreground) {
  118. /* Opens a groupchat, making sure that certain attributes
  119. * are correct, for example that the "type" is set to
  120. * "chatroom".
  121. */
  122. settings.type = _converse.CHATROOMS_TYPE;
  123. settings.id = jid;
  124. settings.box_id = b64_sha1(jid)
  125. const chatbox = _converse.chatboxes.getChatBox(jid, settings, true);
  126. chatbox.trigger('show', true);
  127. return chatbox;
  128. }
  129. _converse.ChatRoom = _converse.ChatBox.extend({
  130. defaults () {
  131. return _.assign(
  132. _.clone(_converse.ChatBox.prototype.defaults),
  133. _.zipObject(converse.ROOM_FEATURES, _.map(converse.ROOM_FEATURES, _.stubFalse)),
  134. {
  135. // For group chats, we distinguish between generally unread
  136. // messages and those ones that specifically mention the
  137. // user.
  138. //
  139. // To keep things simple, we reuse `num_unread` from
  140. // _converse.ChatBox to indicate unread messages which
  141. // mention the user and `num_unread_general` to indicate
  142. // generally unread messages (which *includes* mentions!).
  143. 'num_unread_general': 0,
  144. 'affiliation': null,
  145. 'connection_status': converse.ROOMSTATUS.DISCONNECTED,
  146. 'name': '',
  147. 'nick': _converse.xmppstatus.get('nickname') || _converse.nickname,
  148. 'description': '',
  149. 'features_fetched': false,
  150. 'roomconfig': {},
  151. 'type': _converse.CHATROOMS_TYPE,
  152. 'message_type': 'groupchat'
  153. }
  154. );
  155. },
  156. initialize() {
  157. this.constructor.__super__.initialize.apply(this, arguments);
  158. this.on('change:connection_status', this.onConnectionStatusChanged, this);
  159. this.occupants = new _converse.ChatRoomOccupants();
  160. this.occupants.browserStorage = new Backbone.BrowserStorage.session(
  161. b64_sha1(`converse.occupants-${_converse.bare_jid}${this.get('jid')}`)
  162. );
  163. this.occupants.chatroom = this;
  164. this.registerHandlers();
  165. },
  166. async onConnectionStatusChanged () {
  167. if (this.get('connection_status') === converse.ROOMSTATUS.ENTERED &&
  168. _converse.auto_register_muc_nickname &&
  169. !this.get('reserved_nick')) {
  170. const result = await _converse.api.disco.supports(Strophe.NS.MUC_REGISTER, this.get('jid'));
  171. if (result.length) {
  172. this.registerNickname()
  173. }
  174. }
  175. },
  176. registerHandlers () {
  177. /* Register presence and message handlers for this chat
  178. * groupchat
  179. */
  180. const room_jid = this.get('jid');
  181. this.removeHandlers();
  182. this.presence_handler = _converse.connection.addHandler((stanza) => {
  183. _.each(_.values(this.handlers.presence), (callback) => callback(stanza));
  184. this.onPresence(stanza);
  185. return true;
  186. },
  187. null, 'presence', null, null, room_jid,
  188. {'ignoreNamespaceFragment': true, 'matchBareFromJid': true}
  189. );
  190. this.message_handler = _converse.connection.addHandler((stanza) => {
  191. _.each(_.values(this.handlers.message), (callback) => callback(stanza));
  192. this.onMessage(stanza);
  193. return true;
  194. }, null, 'message', 'groupchat', null, room_jid,
  195. {'matchBareFromJid': true}
  196. );
  197. },
  198. removeHandlers () {
  199. /* Remove the presence and message handlers that were
  200. * registered for this groupchat.
  201. */
  202. if (this.message_handler) {
  203. _converse.connection.deleteHandler(this.message_handler);
  204. delete this.message_handler;
  205. }
  206. if (this.presence_handler) {
  207. _converse.connection.deleteHandler(this.presence_handler);
  208. delete this.presence_handler;
  209. }
  210. return this;
  211. },
  212. addHandler (type, name, callback) {
  213. /* Allows 'presence' and 'message' handlers to be
  214. * registered. These will be executed once presence or
  215. * message stanzas are received, and *before* this model's
  216. * own handlers are executed.
  217. */
  218. if (_.isNil(this.handlers)) {
  219. this.handlers = {};
  220. }
  221. if (_.isNil(this.handlers[type])) {
  222. this.handlers[type] = {};
  223. }
  224. this.handlers[type][name] = callback;
  225. },
  226. getDisplayName () {
  227. return this.get('name') || this.get('jid');
  228. },
  229. join (nick, password) {
  230. /* Join the groupchat.
  231. *
  232. * Parameters:
  233. * (String) nick: The user's nickname
  234. * (String) password: Optional password, if required by
  235. * the groupchat.
  236. */
  237. nick = nick ? nick : this.get('nick');
  238. if (!nick) {
  239. throw new TypeError('join: You need to provide a valid nickname');
  240. }
  241. if (this.get('connection_status') === converse.ROOMSTATUS.ENTERED) {
  242. // We have restored a groupchat from session storage,
  243. // so we don't send out a presence stanza again.
  244. return this;
  245. }
  246. const stanza = $pres({
  247. 'from': _converse.connection.jid,
  248. 'to': this.getRoomJIDAndNick(nick)
  249. }).c("x", {'xmlns': Strophe.NS.MUC})
  250. .c("history", {'maxstanzas': this.get('mam_enabled') ? 0 : _converse.muc_history_max_stanzas}).up();
  251. if (password) {
  252. stanza.cnode(Strophe.xmlElement("password", [], password));
  253. }
  254. this.save('connection_status', converse.ROOMSTATUS.CONNECTING);
  255. _converse.connection.send(stanza);
  256. return this;
  257. },
  258. leave (exit_msg) {
  259. /* Leave the groupchat.
  260. *
  261. * Parameters:
  262. * (String) exit_msg: Optional message to indicate your
  263. * reason for leaving.
  264. */
  265. this.occupants.browserStorage._clear();
  266. this.occupants.reset();
  267. const disco_entity = _converse.disco_entities.get(this.get('jid'));
  268. if (disco_entity) {
  269. disco_entity.destroy();
  270. }
  271. if (_converse.connection.connected) {
  272. this.sendUnavailablePresence(exit_msg);
  273. }
  274. u.safeSave(this, {'connection_status': converse.ROOMSTATUS.DISCONNECTED});
  275. this.removeHandlers();
  276. },
  277. sendUnavailablePresence (exit_msg) {
  278. const presence = $pres({
  279. type: "unavailable",
  280. from: _converse.connection.jid,
  281. to: this.getRoomJIDAndNick()
  282. });
  283. if (exit_msg !== null) {
  284. presence.c("status", exit_msg);
  285. }
  286. _converse.connection.sendPresence(presence);
  287. },
  288. getReferenceForMention (mention, index) {
  289. const longest_match = u.getLongestSubstring(
  290. mention,
  291. this.occupants.map(o => o.getDisplayName())
  292. );
  293. if (!longest_match) {
  294. return null;
  295. }
  296. if ((mention[longest_match.length] || '').match(/[A-Za-zäëïöüâêîôûáéíóúàèìòùÄËÏÖÜÂÊÎÔÛÁÉÍÓÚÀÈÌÒÙ]/i)) {
  297. // avoid false positives, i.e. mentions that have
  298. // further alphabetical characters than our longest
  299. // match.
  300. return null;
  301. }
  302. const occupant = this.occupants.findOccupant({'nick': longest_match}) ||
  303. this.occupants.findOccupant({'jid': longest_match});
  304. if (!occupant) {
  305. return null;
  306. }
  307. const obj = {
  308. 'begin': index,
  309. 'end': index + longest_match.length,
  310. 'value': longest_match,
  311. 'type': 'mention'
  312. };
  313. if (occupant.get('jid')) {
  314. obj.uri = `xmpp:${occupant.get('jid')}`
  315. }
  316. return obj;
  317. },
  318. extractReference (text, index) {
  319. for (let i=index; i<text.length; i++) {
  320. if (text[i] !== '@') {
  321. continue
  322. } else {
  323. const match = text.slice(i+1),
  324. ref = this.getReferenceForMention(match, i);
  325. if (ref) {
  326. return [text.slice(0, i) + match, ref, i]
  327. }
  328. }
  329. }
  330. return;
  331. },
  332. parseTextForReferences (text) {
  333. const refs = [];
  334. let index = 0;
  335. while (index < (text || '').length) {
  336. const result = this.extractReference(text, index);
  337. if (result) {
  338. text = result[0]; // @ gets filtered out
  339. refs.push(result[1]);
  340. index = result[2];
  341. } else {
  342. break;
  343. }
  344. }
  345. return [text, refs];
  346. },
  347. getOutgoingMessageAttributes (text, spoiler_hint) {
  348. const is_spoiler = this.get('composing_spoiler');
  349. var references;
  350. [text, references] = this.parseTextForReferences(text);
  351. return {
  352. 'from': `${this.get('jid')}/${this.get('nick')}`,
  353. 'fullname': this.get('nick'),
  354. 'is_spoiler': is_spoiler,
  355. 'message': text ? u.httpToGeoUri(u.shortnameToUnicode(text), _converse) : undefined,
  356. 'nick': this.get('nick'),
  357. 'references': references,
  358. 'sender': 'me',
  359. 'spoiler_hint': is_spoiler ? spoiler_hint : undefined,
  360. 'type': 'groupchat'
  361. };
  362. },
  363. getRoomJIDAndNick (nick) {
  364. /* Utility method to construct the JID for the current user
  365. * as occupant of the groupchat.
  366. *
  367. * This is the groupchat JID, with the user's nick added at the
  368. * end.
  369. *
  370. * For example: groupchat@conference.example.org/nickname
  371. */
  372. if (nick) {
  373. this.save({'nick': nick});
  374. } else {
  375. nick = this.get('nick');
  376. }
  377. const groupchat = this.get('jid');
  378. const jid = Strophe.getBareJidFromJid(groupchat);
  379. return jid + (nick !== null ? `/${nick}` : "");
  380. },
  381. sendChatState () {
  382. /* Sends a message with the status of the user in this chat session
  383. * as taken from the 'chat_state' attribute of the chat box.
  384. * See XEP-0085 Chat State Notifications.
  385. */
  386. if (this.get('connection_status') !== converse.ROOMSTATUS.ENTERED) {
  387. return;
  388. }
  389. const chat_state = this.get('chat_state');
  390. if (chat_state === _converse.GONE) {
  391. // <gone/> is not applicable within MUC context
  392. return;
  393. }
  394. _converse.connection.send(
  395. $msg({'to':this.get('jid'), 'type': 'groupchat'})
  396. .c(chat_state, {'xmlns': Strophe.NS.CHATSTATES}).up()
  397. .c('no-store', {'xmlns': Strophe.NS.HINTS}).up()
  398. .c('no-permanent-store', {'xmlns': Strophe.NS.HINTS})
  399. );
  400. },
  401. directInvite (recipient, reason) {
  402. /* Send a direct invitation as per XEP-0249
  403. *
  404. * Parameters:
  405. * (String) recipient - JID of the person being invited
  406. * (String) reason - Optional reason for the invitation
  407. */
  408. if (this.get('membersonly')) {
  409. // When inviting to a members-only groupchat, we first add
  410. // the person to the member list by giving them an
  411. // affiliation of 'member' (if they're not affiliated
  412. // already), otherwise they won't be able to join.
  413. const map = {}; map[recipient] = 'member';
  414. const deltaFunc = _.partial(u.computeAffiliationsDelta, true, false);
  415. this.updateMemberLists(
  416. [{'jid': recipient, 'affiliation': 'member', 'reason': reason}],
  417. ['member', 'owner', 'admin'],
  418. deltaFunc
  419. );
  420. }
  421. const attrs = {
  422. 'xmlns': 'jabber:x:conference',
  423. 'jid': this.get('jid')
  424. };
  425. if (reason !== null) { attrs.reason = reason; }
  426. if (this.get('password')) { attrs.password = this.get('password'); }
  427. const invitation = $msg({
  428. from: _converse.connection.jid,
  429. to: recipient,
  430. id: _converse.connection.getUniqueId()
  431. }).c('x', attrs);
  432. _converse.connection.send(invitation);
  433. _converse.emit('roomInviteSent', {
  434. 'room': this,
  435. 'recipient': recipient,
  436. 'reason': reason
  437. });
  438. },
  439. async refreshRoomFeatures () {
  440. await _converse.api.disco.refreshFeatures(this.get('jid'));
  441. return this.getRoomFeatures();
  442. },
  443. async getRoomFeatures () {
  444. const features = await _converse.api.disco.getFeatures(this.get('jid')),
  445. fields = await _converse.api.disco.getFields(this.get('jid')),
  446. identity = await _converse.api.disco.getIdentity('conference', 'text', this.get('jid')),
  447. attrs = {
  448. 'features_fetched': moment().format(),
  449. 'name': identity && identity.get('name')
  450. };
  451. features.each(feature => {
  452. const fieldname = feature.get('var');
  453. if (!fieldname.startsWith('muc_')) {
  454. if (fieldname === Strophe.NS.MAM) {
  455. attrs.mam_enabled = true;
  456. }
  457. return;
  458. }
  459. attrs[fieldname.replace('muc_', '')] = true;
  460. });
  461. attrs.description = _.get(fields.findWhere({'var': "muc#roominfo_description"}), 'attributes.value');
  462. this.save(attrs);
  463. },
  464. requestMemberList (affiliation) {
  465. /* Send an IQ stanza to the server, asking it for the
  466. * member-list of this groupchat.
  467. *
  468. * See: http://xmpp.org/extensions/xep-0045.html#modifymember
  469. *
  470. * Parameters:
  471. * (String) affiliation: The specific member list to
  472. * fetch. 'admin', 'owner' or 'member'.
  473. *
  474. * Returns:
  475. * A promise which resolves once the list has been
  476. * retrieved.
  477. */
  478. affiliation = affiliation || 'member';
  479. const iq = $iq({to: this.get('jid'), type: "get"})
  480. .c("query", {xmlns: Strophe.NS.MUC_ADMIN})
  481. .c("item", {'affiliation': affiliation});
  482. return _converse.api.sendIQ(iq);
  483. },
  484. setAffiliation (affiliation, members) {
  485. /* Send IQ stanzas to the server to set an affiliation for
  486. * the provided JIDs.
  487. *
  488. * See: http://xmpp.org/extensions/xep-0045.html#modifymember
  489. *
  490. * XXX: Prosody doesn't accept multiple JIDs' affiliations
  491. * being set in one IQ stanza, so as a workaround we send
  492. * a separate stanza for each JID.
  493. * Related ticket: https://issues.prosody.im/345
  494. *
  495. * Parameters:
  496. * (String) affiliation: The affiliation
  497. * (Object) members: A map of jids, affiliations and
  498. * optionally reasons. Only those entries with the
  499. * same affiliation as being currently set will be
  500. * considered.
  501. *
  502. * Returns:
  503. * A promise which resolves and fails depending on the
  504. * XMPP server response.
  505. */
  506. members = _.filter(members, (member) =>
  507. // We only want those members who have the right
  508. // affiliation (or none, which implies the provided one).
  509. _.isUndefined(member.affiliation) ||
  510. member.affiliation === affiliation
  511. );
  512. const promises = _.map(members, _.bind(this.sendAffiliationIQ, this, affiliation));
  513. return Promise.all(promises);
  514. },
  515. saveConfiguration (form) {
  516. /* Submit the groupchat configuration form by sending an IQ
  517. * stanza to the server.
  518. *
  519. * Returns a promise which resolves once the XMPP server
  520. * has return a response IQ.
  521. *
  522. * Parameters:
  523. * (HTMLElement) form: The configuration form DOM element.
  524. * If no form is provided, the default configuration
  525. * values will be used.
  526. */
  527. return new Promise((resolve, reject) => {
  528. const inputs = form ? sizzle(':input:not([type=button]):not([type=submit])', form) : [],
  529. configArray = _.map(inputs, u.webForm2xForm);
  530. this.sendConfiguration(configArray, resolve, reject);
  531. });
  532. },
  533. autoConfigureChatRoom () {
  534. /* Automatically configure groupchat based on this model's
  535. * 'roomconfig' data.
  536. *
  537. * Returns a promise which resolves once a response IQ has
  538. * been received.
  539. */
  540. return new Promise((resolve, reject) => {
  541. this.fetchRoomConfiguration().then((stanza) => {
  542. const configArray = [],
  543. fields = stanza.querySelectorAll('field'),
  544. config = this.get('roomconfig');
  545. let count = fields.length;
  546. _.each(fields, (field) => {
  547. const fieldname = field.getAttribute('var').replace('muc#roomconfig_', ''),
  548. type = field.getAttribute('type');
  549. let value;
  550. if (fieldname in config) {
  551. switch (type) {
  552. case 'boolean':
  553. value = config[fieldname] ? 1 : 0;
  554. break;
  555. case 'list-multi':
  556. // TODO: we don't yet handle "list-multi" types
  557. value = field.innerHTML;
  558. break;
  559. default:
  560. value = config[fieldname];
  561. }
  562. field.innerHTML = $build('value').t(value);
  563. }
  564. configArray.push(field);
  565. if (!--count) {
  566. this.sendConfiguration(configArray, resolve, reject);
  567. }
  568. });
  569. });
  570. });
  571. },
  572. fetchRoomConfiguration () {
  573. /* Send an IQ stanza to fetch the groupchat configuration data.
  574. * Returns a promise which resolves once the response IQ
  575. * has been received.
  576. */
  577. return new Promise((resolve, reject) => {
  578. _converse.connection.sendIQ(
  579. $iq({
  580. 'to': this.get('jid'),
  581. 'type': "get"
  582. }).c("query", {xmlns: Strophe.NS.MUC_OWNER}),
  583. resolve,
  584. reject
  585. );
  586. });
  587. },
  588. sendConfiguration (config, callback, errback) {
  589. /* Send an IQ stanza with the groupchat configuration.
  590. *
  591. * Parameters:
  592. * (Array) config: The groupchat configuration
  593. * (Function) callback: Callback upon succesful IQ response
  594. * The first parameter passed in is IQ containing the
  595. * groupchat configuration.
  596. * The second is the response IQ from the server.
  597. * (Function) errback: Callback upon error IQ response
  598. * The first parameter passed in is IQ containing the
  599. * groupchat configuration.
  600. * The second is the response IQ from the server.
  601. */
  602. const iq = $iq({to: this.get('jid'), type: "set"})
  603. .c("query", {xmlns: Strophe.NS.MUC_OWNER})
  604. .c("x", {xmlns: Strophe.NS.XFORM, type: "submit"});
  605. _.each(config || [], function (node) { iq.cnode(node).up(); });
  606. callback = _.isUndefined(callback) ? _.noop : _.partial(callback, iq.nodeTree);
  607. errback = _.isUndefined(errback) ? _.noop : _.partial(errback, iq.nodeTree);
  608. return _converse.connection.sendIQ(iq, callback, errback);
  609. },
  610. saveAffiliationAndRole (pres) {
  611. /* Parse the presence stanza for the current user's
  612. * affiliation.
  613. *
  614. * Parameters:
  615. * (XMLElement) pres: A <presence> stanza.
  616. */
  617. const item = sizzle(`x[xmlns="${Strophe.NS.MUC_USER}"] item`, pres).pop();
  618. const is_self = pres.querySelector("status[code='110']");
  619. if (is_self && !_.isNil(item)) {
  620. const affiliation = item.getAttribute('affiliation');
  621. const role = item.getAttribute('role');
  622. if (affiliation) {
  623. this.save({'affiliation': affiliation});
  624. }
  625. if (role) {
  626. this.save({'role': role});
  627. }
  628. }
  629. },
  630. sendAffiliationIQ (affiliation, member) {
  631. /* Send an IQ stanza specifying an affiliation change.
  632. *
  633. * Paremeters:
  634. * (String) affiliation: affiliation (could also be stored
  635. * on the member object).
  636. * (Object) member: Map containing the member's jid and
  637. * optionally a reason and affiliation.
  638. */
  639. return new Promise((resolve, reject) => {
  640. const iq = $iq({to: this.get('jid'), type: "set"})
  641. .c("query", {xmlns: Strophe.NS.MUC_ADMIN})
  642. .c("item", {
  643. 'affiliation': member.affiliation || affiliation,
  644. 'nick': member.nick,
  645. 'jid': member.jid
  646. });
  647. if (!_.isUndefined(member.reason)) {
  648. iq.c("reason", member.reason);
  649. }
  650. _converse.connection.sendIQ(iq, resolve, reject);
  651. });
  652. },
  653. setAffiliations (members) {
  654. /* Send IQ stanzas to the server to modify the
  655. * affiliations in this groupchat.
  656. *
  657. * See: http://xmpp.org/extensions/xep-0045.html#modifymember
  658. *
  659. * Parameters:
  660. * (Object) members: A map of jids, affiliations and optionally reasons
  661. * (Function) onSuccess: callback for a succesful response
  662. * (Function) onError: callback for an error response
  663. */
  664. const affiliations = _.uniq(_.map(members, 'affiliation'));
  665. return Promise.all(_.map(affiliations, _.partial(this.setAffiliation.bind(this), _, members)));
  666. },
  667. async getJidsWithAffiliations (affiliations) {
  668. /* Returns a map of JIDs that have the affiliations
  669. * as provided.
  670. */
  671. if (_.isString(affiliations)) {
  672. affiliations = [affiliations];
  673. }
  674. const result = await Promise.all(affiliations.map(a =>
  675. this.requestMemberList(a)
  676. .then(iq => u.parseMemberListIQ(iq))
  677. .catch(iq => {
  678. _converse.log(iq, Strophe.LogLevel.ERROR);
  679. })
  680. ));
  681. return [].concat.apply([], result).filter(p => p);
  682. },
  683. updateMemberLists (members, affiliations, deltaFunc) {
  684. /* Fetch the lists of users with the given affiliations.
  685. * Then compute the delta between those users and
  686. * the passed in members, and if it exists, send the delta
  687. * to the XMPP server to update the member list.
  688. *
  689. * Parameters:
  690. * (Object) members: Map of member jids and affiliations.
  691. * (String|Array) affiliation: An array of affiliations or
  692. * a string if only one affiliation.
  693. * (Function) deltaFunc: The function to compute the delta
  694. * between old and new member lists.
  695. *
  696. * Returns:
  697. * A promise which is resolved once the list has been
  698. * updated or once it's been established there's no need
  699. * to update the list.
  700. */
  701. this.getJidsWithAffiliations(affiliations)
  702. .then(old_members => this.setAffiliations(deltaFunc(members, old_members)))
  703. .then(() => this.occupants.fetchMembers())
  704. .catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
  705. },
  706. getDefaultNick () {
  707. /* The default nickname (used when muc_nickname_from_jid is true)
  708. * is the node part of the user's JID.
  709. * We put this in a separate method so that it can be
  710. * overridden by plugins.
  711. */
  712. const nick = _converse.xmppstatus.vcard.get('nickname');
  713. if (nick) {
  714. return nick;
  715. } else if (_converse.muc_nickname_from_jid) {
  716. return Strophe.unescapeNode(Strophe.getNodeFromJid(_converse.bare_jid));
  717. }
  718. },
  719. checkForReservedNick () {
  720. /* Use service-discovery to ask the XMPP server whether
  721. * this user has a reserved nickname for this groupchat.
  722. * If so, we'll use that, otherwise we render the nickname form.
  723. *
  724. * Parameters:
  725. * (Function) callback: Callback upon succesful IQ response
  726. * (Function) errback: Callback upon error IQ response
  727. */
  728. return _converse.api.sendIQ(
  729. $iq({
  730. 'to': this.get('jid'),
  731. 'from': _converse.connection.jid,
  732. 'type': "get"
  733. }).c("query", {
  734. 'xmlns': Strophe.NS.DISCO_INFO,
  735. 'node': 'x-roomuser-item'
  736. })
  737. ).then(iq => {
  738. const identity_el = iq.querySelector('query[node="x-roomuser-item"] identity'),
  739. nick = identity_el ? identity_el.getAttribute('name') : null;
  740. this.save({
  741. 'reserved_nick': nick,
  742. 'nick': nick
  743. }, {'silent': true});
  744. return iq;
  745. });
  746. },
  747. async registerNickname () {
  748. // See https://xmpp.org/extensions/xep-0045.html#register
  749. const nick = this.get('nick'),
  750. jid = this.get('jid');
  751. let iq, err_msg;
  752. try {
  753. iq = await _converse.api.sendIQ(
  754. $iq({
  755. 'to': jid,
  756. 'from': _converse.connection.jid,
  757. 'type': 'get'
  758. }).c('query', {'xmlns': Strophe.NS.MUC_REGISTER})
  759. );
  760. } catch (e) {
  761. if (sizzle('not-allowed[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]', e).length) {
  762. err_msg = __("You're not allowed to register yourself in this groupchat.");
  763. } else if (sizzle('registration-required[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]', e).length) {
  764. err_msg = __("You're not allowed to register in this groupchat because it's members-only.");
  765. }
  766. _converse.log(e, Strophe.LogLevel.ERROR);
  767. return err_msg;
  768. }
  769. const required_fields = sizzle('field required', iq).map(f => f.parentElement);
  770. if (required_fields.length > 1 && required_fields[0].getAttribute('var') !== 'muc#register_roomnick') {
  771. return _converse.log(`Can't register the user register in the groupchat ${jid} due to the required fields`);
  772. }
  773. try {
  774. await _converse.api.sendIQ($iq({
  775. 'to': jid,
  776. 'from': _converse.connection.jid,
  777. 'type': 'set'
  778. }).c('query', {'xmlns': Strophe.NS.MUC_REGISTER})
  779. .c('x', {'xmlns': Strophe.NS.XFORM, 'type': 'submit'})
  780. .c('field', {'var': 'FORM_TYPE'}).c('value').t('http://jabber.org/protocol/muc#register').up().up()
  781. .c('field', {'var': 'muc#register_roomnick'}).c('value').t(nick)
  782. );
  783. } catch (e) {
  784. if (sizzle('service-unavailable[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]', e).length) {
  785. err_msg = __("Can't register your nickname in this groupchat, it doesn't support registration.");
  786. } else if (sizzle('bad-request[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]', e).length) {
  787. err_msg = __("Can't register your nickname in this groupchat, invalid data form supplied.");
  788. }
  789. _converse.log(err_msg);
  790. _converse.log(e, Strophe.LogLevel.ERROR);
  791. return err_msg;
  792. }
  793. },
  794. updateOccupantsOnPresence (pres) {
  795. /* Given a presence stanza, update the occupant model
  796. * based on its contents.
  797. *
  798. * Parameters:
  799. * (XMLElement) pres: The presence stanza
  800. */
  801. const data = this.parsePresence(pres);
  802. if (data.type === 'error' || (!data.jid && !data.nick)) {
  803. return true;
  804. }
  805. const occupant = this.occupants.findOccupant(data);
  806. if (data.type === 'unavailable' && occupant) {
  807. if (!_.includes(data.states, converse.MUC_NICK_CHANGED_CODE) && !occupant.isMember()) {
  808. // We only destroy the occupant if this is not a nickname change operation.
  809. // and if they're not on the member lists.
  810. // Before destroying we set the new data, so
  811. // that we can show the disconnection message.
  812. occupant.set(data);
  813. occupant.destroy();
  814. return;
  815. }
  816. }
  817. const jid = Strophe.getBareJidFromJid(data.jid);
  818. const attributes = _.extend(data, {
  819. 'jid': jid ? jid : undefined,
  820. 'resource': data.jid ? Strophe.getResourceFromJid(data.jid) : undefined
  821. });
  822. if (occupant) {
  823. occupant.save(attributes);
  824. } else {
  825. this.occupants.create(attributes);
  826. }
  827. },
  828. parsePresence (pres) {
  829. const from = pres.getAttribute("from"),
  830. type = pres.getAttribute("type"),
  831. data = {
  832. 'from': from,
  833. 'nick': Strophe.getResourceFromJid(from),
  834. 'type': type,
  835. 'states': [],
  836. 'show': type !== 'unavailable' ? 'online' : 'offline'
  837. };
  838. _.each(pres.childNodes, function (child) {
  839. switch (child.nodeName) {
  840. case "status":
  841. data.status = child.textContent || null;
  842. break;
  843. case "show":
  844. data.show = child.textContent || 'online';
  845. break;
  846. case "x":
  847. if (child.getAttribute("xmlns") === Strophe.NS.MUC_USER) {
  848. _.each(child.childNodes, function (item) {
  849. switch (item.nodeName) {
  850. case "item":
  851. data.affiliation = item.getAttribute("affiliation");
  852. data.role = item.getAttribute("role");
  853. data.jid = item.getAttribute("jid");
  854. data.nick = item.getAttribute("nick") || data.nick;
  855. break;
  856. case "status":
  857. if (item.getAttribute("code")) {
  858. data.states.push(item.getAttribute("code"));
  859. }
  860. }
  861. });
  862. } else if (child.getAttribute("xmlns") === Strophe.NS.VCARDUPDATE) {
  863. data.image_hash = _.get(child.querySelector('photo'), 'textContent');
  864. }
  865. }
  866. });
  867. return data;
  868. },
  869. isDuplicate (message, original_stanza) {
  870. const msgid = message.getAttribute('id'),
  871. jid = message.getAttribute('from');
  872. if (msgid) {
  873. return this.messages.where({'msgid': msgid, 'from': jid}).length;
  874. }
  875. return false;
  876. },
  877. fetchFeaturesIfConfigurationChanged (stanza) {
  878. const configuration_changed = stanza.querySelector("status[code='104']"),
  879. logging_enabled = stanza.querySelector("status[code='170']"),
  880. logging_disabled = stanza.querySelector("status[code='171']"),
  881. room_no_longer_anon = stanza.querySelector("status[code='172']"),
  882. room_now_semi_anon = stanza.querySelector("status[code='173']"),
  883. room_now_fully_anon = stanza.querySelector("status[code='173']");
  884. if (configuration_changed || logging_enabled || logging_disabled ||
  885. room_no_longer_anon || room_now_semi_anon || room_now_fully_anon) {
  886. this.refreshRoomFeatures();
  887. }
  888. },
  889. onMessage (stanza) {
  890. /* Handler for all MUC messages sent to this groupchat.
  891. *
  892. * Parameters:
  893. * (XMLElement) stanza: The message stanza.
  894. */
  895. this.fetchFeaturesIfConfigurationChanged(stanza);
  896. const original_stanza = stanza,
  897. forwarded = stanza.querySelector('forwarded');
  898. if (!_.isNull(forwarded)) {
  899. stanza = forwarded.querySelector('message');
  900. }
  901. if (this.isDuplicate(stanza, original_stanza)) {
  902. return;
  903. }
  904. const jid = stanza.getAttribute('from'),
  905. resource = Strophe.getResourceFromJid(jid),
  906. sender = resource && Strophe.unescapeNode(resource) || '';
  907. if (!this.handleMessageCorrection(stanza)) {
  908. if (sender === '') {
  909. return;
  910. }
  911. const subject_el = stanza.querySelector('subject');
  912. if (subject_el) {
  913. const subject = _.propertyOf(subject_el)('textContent') || '';
  914. u.safeSave(this, {'subject': {'author': sender, 'text': subject}});
  915. }
  916. this.createMessage(stanza, original_stanza)
  917. .then(msg => this.incrementUnreadMsgCounter(msg))
  918. .catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
  919. }
  920. if (sender !== this.get('nick')) {
  921. // We only emit an event if it's not our own message
  922. _converse.emit('message', {'stanza': original_stanza, 'chatbox': this});
  923. }
  924. },
  925. onPresence (pres) {
  926. /* Handles all MUC presence stanzas.
  927. *
  928. * Parameters:
  929. * (XMLElement) pres: The stanza
  930. */
  931. if (pres.getAttribute('type') === 'error') {
  932. this.save('connection_status', converse.ROOMSTATUS.DISCONNECTED);
  933. return;
  934. }
  935. const is_self = pres.querySelector("status[code='110']");
  936. if (is_self && pres.getAttribute('type') !== 'unavailable') {
  937. this.onOwnPresence(pres);
  938. }
  939. this.updateOccupantsOnPresence(pres);
  940. if (this.get('role') !== 'none' && this.get('connection_status') === converse.ROOMSTATUS.CONNECTING) {
  941. this.save('connection_status', converse.ROOMSTATUS.CONNECTED);
  942. }
  943. },
  944. onOwnPresence (pres) {
  945. /* Handles a received presence relating to the current
  946. * user.
  947. *
  948. * For locked groupchats (which are by definition "new"), the
  949. * groupchat will either be auto-configured or created instantly
  950. * (with default config) or a configuration groupchat will be
  951. * rendered.
  952. *
  953. * If the groupchat is not locked, then the groupchat will be
  954. * auto-configured only if applicable and if the current
  955. * user is the groupchat's owner.
  956. *
  957. * Parameters:
  958. * (XMLElement) pres: The stanza
  959. */
  960. this.saveAffiliationAndRole(pres);
  961. const locked_room = pres.querySelector("status[code='201']");
  962. if (locked_room) {
  963. if (this.get('auto_configure')) {
  964. this.autoConfigureChatRoom().then(() => this.refreshRoomFeatures());
  965. } else if (_converse.muc_instant_rooms) {
  966. // Accept default configuration
  967. this.saveConfiguration().then(() => this.getRoomFeatures());
  968. } else {
  969. this.trigger('configurationNeeded');
  970. return; // We haven't yet entered the groupchat, so bail here.
  971. }
  972. } else if (!this.get('features_fetched')) {
  973. // The features for this groupchat weren't fetched.
  974. // That must mean it's a new groupchat without locking
  975. // (in which case Prosody doesn't send a 201 status),
  976. // otherwise the features would have been fetched in
  977. // the "initialize" method already.
  978. if (this.get('affiliation') === 'owner' && this.get('auto_configure')) {
  979. this.autoConfigureChatRoom().then(() => this.refreshRoomFeatures());
  980. } else {
  981. this.getRoomFeatures();
  982. }
  983. }
  984. this.save('connection_status', converse.ROOMSTATUS.ENTERED);
  985. },
  986. isUserMentioned (message) {
  987. /* Returns a boolean to indicate whether the current user
  988. * was mentioned in a message.
  989. *
  990. * Parameters:
  991. * (String): The text message
  992. */
  993. const nick = this.get('nick');
  994. if (message.get('references').length) {
  995. const mentions = message.get('references').filter(ref => (ref.type === 'mention')).map(ref => ref.value);
  996. return _.includes(mentions, nick);
  997. } else {
  998. return (new RegExp(`\\b${nick}\\b`)).test(message.get('message'));
  999. }
  1000. },
  1001. incrementUnreadMsgCounter (message) {
  1002. /* Given a newly received message, update the unread counter if
  1003. * necessary.
  1004. *
  1005. * Parameters:
  1006. * (XMLElement): The <messsage> stanza
  1007. */
  1008. if (!message) { return; }
  1009. const body = message.get('message');
  1010. if (_.isNil(body)) { return; }
  1011. if (u.isNewMessage(message) && this.isHidden()) {
  1012. const settings = {'num_unread_general': this.get('num_unread_general') + 1};
  1013. if (this.isUserMentioned(message)) {
  1014. settings.num_unread = this.get('num_unread') + 1;
  1015. _converse.incrementMsgCounter();
  1016. }
  1017. this.save(settings);
  1018. }
  1019. },
  1020. clearUnreadMsgCounter() {
  1021. u.safeSave(this, {
  1022. 'num_unread': 0,
  1023. 'num_unread_general': 0
  1024. });
  1025. }
  1026. });
  1027. _converse.ChatRoomOccupant = Backbone.Model.extend({
  1028. defaults: {
  1029. 'show': 'offline'
  1030. },
  1031. initialize (attributes) {
  1032. this.set(_.extend({
  1033. 'id': _converse.connection.getUniqueId(),
  1034. }, attributes));
  1035. this.on('change:image_hash', this.onAvatarChanged, this);
  1036. },
  1037. onAvatarChanged () {
  1038. const hash = this.get('image_hash');
  1039. const vcards = [];
  1040. if (this.get('jid')) {
  1041. vcards.push(_converse.vcards.findWhere({'jid': this.get('jid')}));
  1042. }
  1043. vcards.push(_converse.vcards.findWhere({'jid': this.get('from')}));
  1044. _.forEach(_.filter(vcards, undefined), (vcard) => {
  1045. if (hash && vcard.get('image_hash') !== hash) {
  1046. _converse.api.vcard.update(vcard);
  1047. }
  1048. });
  1049. },
  1050. getDisplayName () {
  1051. return this.get('nick') || this.get('jid');
  1052. },
  1053. isMember () {
  1054. return _.includes(['admin', 'owner', 'member'], this.get('affiliation'));
  1055. }
  1056. });
  1057. _converse.ChatRoomOccupants = Backbone.Collection.extend({
  1058. model: _converse.ChatRoomOccupant,
  1059. comparator (occupant1, occupant2) {
  1060. const role1 = occupant1.get('role') || 'none';
  1061. const role2 = occupant2.get('role') || 'none';
  1062. if (MUC_ROLE_WEIGHTS[role1] === MUC_ROLE_WEIGHTS[role2]) {
  1063. const nick1 = occupant1.getDisplayName().toLowerCase();
  1064. const nick2 = occupant2.getDisplayName().toLowerCase();
  1065. return nick1 < nick2 ? -1 : (nick1 > nick2? 1 : 0);
  1066. } else {
  1067. return MUC_ROLE_WEIGHTS[role1] < MUC_ROLE_WEIGHTS[role2] ? -1 : 1;
  1068. }
  1069. },
  1070. fetchMembers () {
  1071. this.chatroom.getJidsWithAffiliations(['member', 'owner', 'admin'])
  1072. .then(new_members => {
  1073. const new_jids = new_members.map(m => m.jid).filter(m => !_.isUndefined(m)),
  1074. new_nicks = new_members.map(m => !m.jid && m.nick || undefined).filter(m => !_.isUndefined(m)),
  1075. removed_members = this.filter(m => {
  1076. return f.includes(m.get('affiliation'), ['admin', 'member', 'owner']) &&
  1077. !f.includes(m.get('nick'), new_nicks) &&
  1078. !f.includes(m.get('jid'), new_jids);
  1079. });
  1080. _.each(removed_members, (occupant) => {
  1081. if (occupant.get('jid') === _converse.bare_jid) { return; }
  1082. if (occupant.get('show') === 'offline') {
  1083. occupant.destroy();
  1084. }
  1085. });
  1086. _.each(new_members, (attrs) => {
  1087. let occupant;
  1088. if (attrs.jid) {
  1089. occupant = this.findOccupant({'jid': attrs.jid});
  1090. } else {
  1091. occupant = this.findOccupant({'nick': attrs.nick});
  1092. }
  1093. if (occupant) {
  1094. occupant.save(attrs);
  1095. } else {
  1096. this.create(attrs);
  1097. }
  1098. });
  1099. }).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
  1100. },
  1101. findOccupant (data) {
  1102. /* Try to find an existing occupant based on the passed in
  1103. * data object.
  1104. *
  1105. * If we have a JID, we use that as lookup variable,
  1106. * otherwise we use the nick. We don't always have both,
  1107. * but should have at least one or the other.
  1108. */
  1109. const jid = Strophe.getBareJidFromJid(data.jid);
  1110. if (jid !== null) {
  1111. return this.where({'jid': jid}).pop();
  1112. } else {
  1113. return this.where({'nick': data.nick}).pop();
  1114. }
  1115. }
  1116. });
  1117. _converse.RoomsPanelModel = Backbone.Model.extend({
  1118. defaults: {
  1119. 'muc_domain': '',
  1120. },
  1121. });
  1122. _converse.onDirectMUCInvitation = function (message) {
  1123. /* A direct MUC invitation to join a groupchat has been received
  1124. * See XEP-0249: Direct MUC invitations.
  1125. *
  1126. * Parameters:
  1127. * (XMLElement) message: The message stanza containing the
  1128. * invitation.
  1129. */
  1130. const x_el = sizzle('x[xmlns="jabber:x:conference"]', message).pop(),
  1131. from = Strophe.getBareJidFromJid(message.getAttribute('from')),
  1132. room_jid = x_el.getAttribute('jid'),
  1133. reason = x_el.getAttribute('reason');
  1134. let contact = _converse.roster.get(from),
  1135. result;
  1136. if (_converse.auto_join_on_invite) {
  1137. result = true;
  1138. } else {
  1139. // Invite request might come from someone not your roster list
  1140. contact = contact? contact.get('fullname'): Strophe.getNodeFromJid(from);
  1141. if (!reason) {
  1142. result = confirm(
  1143. __("%1$s has invited you to join a groupchat: %2$s", contact, room_jid)
  1144. );
  1145. } else {
  1146. result = confirm(
  1147. __('%1$s has invited you to join a groupchat: %2$s, and left the following reason: "%3$s"',
  1148. contact, room_jid, reason)
  1149. );
  1150. }
  1151. }
  1152. if (result === true) {
  1153. const chatroom = _converse.openChatRoom(
  1154. room_jid, {'password': x_el.getAttribute('password') });
  1155. if (chatroom.get('connection_status') === converse.ROOMSTATUS.DISCONNECTED) {
  1156. _converse.chatboxviews.get(room_jid).join();
  1157. }
  1158. }
  1159. };
  1160. if (_converse.allow_muc_invitations) {
  1161. const registerDirectInvitationHandler = function () {
  1162. _converse.connection.addHandler(
  1163. (message) => {
  1164. _converse.onDirectMUCInvitation(message);
  1165. return true;
  1166. }, 'jabber:x:conference', 'message');
  1167. };
  1168. _converse.on('connected', registerDirectInvitationHandler);
  1169. _converse.on('reconnected', registerDirectInvitationHandler);
  1170. }
  1171. const getChatRoom = function (jid, attrs, create) {
  1172. jid = jid.toLowerCase();
  1173. attrs.type = _converse.CHATROOMS_TYPE;
  1174. attrs.id = jid;
  1175. attrs.box_id = b64_sha1(jid)
  1176. return _converse.chatboxes.getChatBox(jid, attrs, create);
  1177. };
  1178. const createChatRoom = function (jid, attrs) {
  1179. if (jid.startsWith('xmpp:') && jid.endsWith('?join')) {
  1180. jid = jid.replace(/^xmpp:/, '').replace(/\?join$/, '');
  1181. }
  1182. return getChatRoom(jid, attrs, true);
  1183. };
  1184. function autoJoinRooms () {
  1185. /* Automatically join groupchats, based on the
  1186. * "auto_join_rooms" configuration setting, which is an array
  1187. * of strings (groupchat JIDs) or objects (with groupchat JID and other
  1188. * settings).
  1189. */
  1190. _.each(_converse.auto_join_rooms, function (groupchat) {
  1191. if (_converse.chatboxes.where({'jid': groupchat}).length) {
  1192. return;
  1193. }
  1194. if (_.isString(groupchat)) {
  1195. _converse.api.rooms.open(groupchat);
  1196. } else if (_.isObject(groupchat)) {
  1197. _converse.api.rooms.open(groupchat.jid, groupchat.nick);
  1198. } else {
  1199. _converse.log(
  1200. 'Invalid groupchat criteria specified for "auto_join_rooms"',
  1201. Strophe.LogLevel.ERROR);
  1202. }
  1203. });
  1204. _converse.emit('roomsAutoJoined');
  1205. }
  1206. function disconnectChatRooms () {
  1207. /* When disconnecting, mark all groupchats as
  1208. * disconnected, so that they will be properly entered again
  1209. * when fetched from session storage.
  1210. */
  1211. _converse.chatboxes.each(function (model) {
  1212. if (model.get('type') === _converse.CHATROOMS_TYPE) {
  1213. model.save('connection_status', converse.ROOMSTATUS.DISCONNECTED);
  1214. }
  1215. });
  1216. }
  1217. function fetchRegistrationForm (room_jid, user_jid) {
  1218. _converse.api.sendIQ(
  1219. $iq({
  1220. 'from': user_jid,
  1221. 'to': room_jid,
  1222. 'type': 'get'
  1223. }).c('query', {'xmlns': Strophe.NS.REGISTER})
  1224. ).then(iq => {
  1225. }).catch(iq => {
  1226. if (sizzle('item-not-found[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]', iq).length) {
  1227. this.feedback.set('error', __(`Error: the groupchat ${this.model.getDisplayName()} does not exist.`));
  1228. } else if (sizzle('not-allowed[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]').length) {
  1229. this.feedback.set('error', __(`Sorry, you're not allowed to register in this groupchat`));
  1230. }
  1231. });
  1232. }
  1233. /************************ BEGIN Event Handlers ************************/
  1234. _converse.on('addClientFeatures', () => {
  1235. if (_converse.allow_muc) {
  1236. _converse.api.disco.own.features.add(Strophe.NS.MUC);
  1237. }
  1238. if (_converse.allow_muc_invitations) {
  1239. _converse.api.disco.own.features.add('jabber:x:conference'); // Invites
  1240. }
  1241. });
  1242. _converse.api.listen.on('chatBoxesFetched', autoJoinRooms);
  1243. _converse.api.listen.on('disconnecting', disconnectChatRooms);
  1244. _converse.api.listen.on('statusInitialized', () => {
  1245. // XXX: For websocket connections, we disconnect from all
  1246. // chatrooms when the page reloads. This is a workaround for
  1247. // issue #1111 and should be removed once we support XEP-0198
  1248. const options = {'once': true, 'passive': true};
  1249. window.addEventListener(_converse.unloadevent, () => {
  1250. if (_converse.connection._proto instanceof Strophe.Websocket) {
  1251. disconnectChatRooms();
  1252. }
  1253. });
  1254. });
  1255. /************************ END Event Handlers ************************/
  1256. /************************ BEGIN API ************************/
  1257. // We extend the default converse.js API to add methods specific to MUC groupchats.
  1258. _.extend(_converse.api, {
  1259. /**
  1260. * The "rooms" namespace groups methods relevant to chatrooms
  1261. * (aka groupchats).
  1262. *
  1263. * @namespace _converse.api.rooms
  1264. * @memberOf _converse.api
  1265. */
  1266. 'rooms': {
  1267. /**
  1268. * Creates a new MUC chatroom (aka groupchat)
  1269. *
  1270. * Similar to {@link _converse.api.rooms.open}, but creates
  1271. * the chatroom in the background (i.e. doesn't cause a
  1272. * view to open).
  1273. *
  1274. * @method _converse.api.rooms.create
  1275. * @param {(string[]|string)} jid|jids The JID or array of
  1276. * JIDs of the chatroom(s) to create
  1277. * @param {object} [attrs] attrs The room attributes
  1278. */
  1279. 'create' (jids, attrs) {
  1280. if (_.isString(attrs)) {
  1281. attrs = {'nick': attrs};
  1282. } else if (_.isUndefined(attrs)) {
  1283. attrs = {};
  1284. }
  1285. if (_.isUndefined(attrs.maximize)) {
  1286. attrs.maximize = false;
  1287. }
  1288. if (!attrs.nick && _converse.muc_nickname_from_jid) {
  1289. attrs.nick = Strophe.getNodeFromJid(_converse.bare_jid);
  1290. }
  1291. if (_.isUndefined(jids)) {
  1292. throw new TypeError('rooms.create: You need to provide at least one JID');
  1293. } else if (_.isString(jids)) {
  1294. return createChatRoom(jids, attrs);
  1295. }
  1296. return _.map(jids, _.partial(createChatRoom, _, attrs));
  1297. },
  1298. /**
  1299. * Opens a MUC chatroom (aka groupchat)
  1300. *
  1301. * Similar to {@link _converse.api.chats.open}, but for groupchats.
  1302. *
  1303. * @method _converse.api.rooms.open
  1304. * @param {string} jid The room JID or JIDs (if not specified, all
  1305. * currently open rooms will be returned).
  1306. * @param {string} attrs A map containing any extra room attributes.
  1307. * @param {string} [attrs.nick] The current user's nickname for the MUC
  1308. * @param {boolean} [attrs.auto_configure] A boolean, indicating
  1309. * whether the room should be configured automatically or not.
  1310. * If set to `true`, then it makes sense to pass in configuration settings.
  1311. * @param {object} [attrs.roomconfig] A map of configuration settings to be used when the room gets
  1312. * configured automatically. Currently it doesn't make sense to specify
  1313. * `roomconfig` values if `auto_configure` is set to `false`.
  1314. * For a list of configuration values that can be passed in, refer to these values
  1315. * in the [XEP-0045 MUC specification](http://xmpp.org/extensions/xep-0045.html#registrar-formtype-owner).
  1316. * The values should be named without the `muc#roomconfig_` prefix.
  1317. * @param {boolean} [attrs.maximize] A boolean, indicating whether minimized rooms should also be
  1318. * maximized, when opened. Set to `false` by default.
  1319. * @param {boolean} [attrs.bring_to_foreground] A boolean indicating whether the room should be
  1320. * brought to the foreground and therefore replace the currently shown chat.
  1321. * If there is no chat currently open, then this option is ineffective.
  1322. *
  1323. * @example
  1324. * this._converse.api.rooms.open('group@muc.example.com')
  1325. *
  1326. * @example
  1327. * // To return an array of rooms, provide an array of room JIDs:
  1328. * _converse.api.rooms.open(['group1@muc.example.com', 'group2@muc.example.com'])
  1329. *
  1330. * @example
  1331. * // To setup a custom nickname when joining the room, provide the optional nick argument:
  1332. * _converse.api.rooms.open('group@muc.example.com', {'nick': 'mycustomnick'})
  1333. *
  1334. * @example
  1335. * // For example, opening a room with a specific default configuration:
  1336. * _converse.api.rooms.open(
  1337. * 'myroom@conference.example.org',
  1338. * { 'nick': 'coolguy69',
  1339. * 'auto_configure': true,
  1340. * 'roomconfig': {
  1341. * 'changesubject': false,
  1342. * 'membersonly': true,
  1343. * 'persistentroom': true,
  1344. * 'publicroom': true,
  1345. * 'roomdesc': 'Comfy room for hanging out',
  1346. * 'whois': 'anyone'
  1347. * }
  1348. * },
  1349. * true
  1350. * );
  1351. */
  1352. 'open' (jids, attrs) {
  1353. return new Promise((resolve, reject) => {
  1354. _converse.api.waitUntil('chatBoxesFetched').then(() => {
  1355. if (_.isUndefined(jids)) {
  1356. const err_msg = 'rooms.open: You need to provide at least one JID';
  1357. _converse.log(err_msg, Strophe.LogLevel.ERROR);
  1358. reject(new TypeError(err_msg));
  1359. } else if (_.isString(jids)) {
  1360. resolve(_converse.api.rooms.create(jids, attrs).trigger('show'));
  1361. } else {
  1362. resolve(_.map(jids, (jid) => _converse.api.rooms.create(jid, attrs).trigger('show')));
  1363. }
  1364. });
  1365. });
  1366. },
  1367. /**
  1368. * Returns an object representing a MUC chatroom (aka groupchat)
  1369. *
  1370. * @method _converse.api.rooms.get
  1371. * @param {string} [jid] The room JID (if not specified, all rooms will be returned).
  1372. * @param {object} attrs A map containing any extra room attributes For example, if you want
  1373. * to specify the nickname, use `{'nick': 'bloodninja'}`. Previously (before
  1374. * version 1.0.7, the second parameter only accepted the nickname (as a string
  1375. * value). This is currently still accepted, but then you can't pass in any
  1376. * other room attributes. If the nickname is not specified then the node part of
  1377. * the user's JID will be used.
  1378. * @param {boolean} create A boolean indicating whether the room should be created
  1379. * if not found (default: `false`)
  1380. * @example
  1381. * _converse.api.waitUntil('roomsAutoJoined').then(() => {
  1382. * const create_if_not_found = true;
  1383. * _converse.api.rooms.get(
  1384. * 'group@muc.example.com',
  1385. * {'nick': 'dread-pirate-roberts'},
  1386. * create_if_not_found
  1387. * )
  1388. * });
  1389. */
  1390. 'get' (jids, attrs, create) {
  1391. if (_.isString(attrs)) {
  1392. attrs = {'nick': attrs};
  1393. } else if (_.isUndefined(attrs)) {
  1394. attrs = {};
  1395. }
  1396. if (_.isUndefined(jids)) {
  1397. const result = [];
  1398. _converse.chatboxes.each(function (chatbox) {
  1399. if (chatbox.get('type') === _converse.CHATROOMS_TYPE) {
  1400. result.push(chatbox);
  1401. }
  1402. });
  1403. return result;
  1404. }
  1405. if (!attrs.nick) {
  1406. attrs.nick = Strophe.getNodeFromJid(_converse.bare_jid);
  1407. }
  1408. if (_.isString(jids)) {
  1409. return getChatRoom(jids, attrs);
  1410. }
  1411. return _.map(jids, _.partial(getChatRoom, _, attrs));
  1412. }
  1413. }
  1414. });
  1415. /************************ END API ************************/
  1416. }
  1417. });
  1418. }));