converse-muc.js 101 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243
  1. // Converse.js
  2. // https://conversejs.org
  3. //
  4. // Copyright (c) 2013-2019, the Converse.js developers
  5. // Licensed under the Mozilla Public License (MPLv2)
  6. //
  7. // XEP-0045 Multi-User Chat
  8. import "./converse-disco";
  9. import "./utils/emoji";
  10. import "./utils/muc";
  11. import BrowserStorage from "backbone.browserStorage";
  12. import converse from "./converse-core";
  13. import u from "./utils/form";
  14. const MUC_ROLE_WEIGHTS = {
  15. 'moderator': 1,
  16. 'participant': 2,
  17. 'visitor': 3,
  18. 'none': 2,
  19. };
  20. const { Strophe, Backbone, Promise, $iq, $build, $msg, $pres, sizzle, dayjs, _ } = converse.env;
  21. // Add Strophe Namespaces
  22. Strophe.addNamespace('MUC_ADMIN', Strophe.NS.MUC + "#admin");
  23. Strophe.addNamespace('MUC_OWNER', Strophe.NS.MUC + "#owner");
  24. Strophe.addNamespace('MUC_REGISTER', "jabber:iq:register");
  25. Strophe.addNamespace('MUC_ROOMCONF', Strophe.NS.MUC + "#roomconfig");
  26. Strophe.addNamespace('MUC_USER', Strophe.NS.MUC + "#user");
  27. converse.MUC_NICK_CHANGED_CODE = "303";
  28. converse.ROOM_FEATURES = [
  29. 'passwordprotected', 'unsecured', 'hidden',
  30. 'publicroom', 'membersonly', 'open', 'persistent',
  31. 'temporary', 'nonanonymous', 'semianonymous',
  32. 'moderated', 'unmoderated', 'mam_enabled'
  33. ];
  34. // No longer used in code, but useful as reference.
  35. //
  36. // const ROOM_FEATURES_MAP = {
  37. // 'passwordprotected': 'unsecured',
  38. // 'unsecured': 'passwordprotected',
  39. // 'hidden': 'publicroom',
  40. // 'publicroom': 'hidden',
  41. // 'membersonly': 'open',
  42. // 'open': 'membersonly',
  43. // 'persistent': 'temporary',
  44. // 'temporary': 'persistent',
  45. // 'nonanonymous': 'semianonymous',
  46. // 'semianonymous': 'nonanonymous',
  47. // 'moderated': 'unmoderated',
  48. // 'unmoderated': 'moderated'
  49. // };
  50. converse.ROOMSTATUS = {
  51. CONNECTED: 0,
  52. CONNECTING: 1,
  53. NICKNAME_REQUIRED: 2,
  54. PASSWORD_REQUIRED: 3,
  55. DISCONNECTED: 4,
  56. ENTERED: 5,
  57. DESTROYED: 6
  58. };
  59. converse.plugins.add('converse-muc', {
  60. /* Optional dependencies are other plugins which might be
  61. * overridden or relied upon, and therefore need to be loaded before
  62. * this plugin. They are called "optional" because they might not be
  63. * available, in which case any overrides applicable to them will be
  64. * ignored.
  65. *
  66. * It's possible however to make optional dependencies non-optional.
  67. * If the setting "strict_plugin_dependencies" is set to true,
  68. * an error will be raised if the plugin is not found.
  69. *
  70. * NB: These plugins need to have already been loaded via require.js.
  71. */
  72. dependencies: ["converse-chatboxes", "converse-disco", "converse-controlbox"],
  73. overrides: {
  74. ChatBoxes: {
  75. model (attrs, options) {
  76. const { _converse } = this.__super__;
  77. if (attrs.type == _converse.CHATROOMS_TYPE) {
  78. return new _converse.ChatRoom(attrs, options);
  79. } else {
  80. return this.__super__.model.apply(this, arguments);
  81. }
  82. },
  83. }
  84. },
  85. initialize () {
  86. /* The initialize function gets called as soon as the plugin is
  87. * loaded by converse.js's plugin machinery.
  88. */
  89. const { _converse } = this,
  90. { __ } = _converse;
  91. // Configuration values for this plugin
  92. // ====================================
  93. // Refer to docs/source/configuration.rst for explanations of these
  94. // configuration settings.
  95. _converse.api.settings.update({
  96. 'allow_muc': true,
  97. 'allow_muc_invitations': true,
  98. 'auto_join_on_invite': false,
  99. 'auto_join_rooms': [],
  100. 'auto_register_muc_nickname': false,
  101. 'locked_muc_domain': false,
  102. 'muc_domain': undefined,
  103. 'muc_history_max_stanzas': undefined,
  104. 'muc_instant_rooms': true,
  105. 'muc_nickname_from_jid': false
  106. });
  107. _converse.api.promises.add(['roomsAutoJoined']);
  108. if (_converse.locked_muc_domain && !_.isString(_converse.muc_domain)) {
  109. throw new Error("Config Error: it makes no sense to set locked_muc_domain "+
  110. "to true when muc_domain is not set");
  111. }
  112. function ___ (str) {
  113. /* This is part of a hack to get gettext to scan strings to be
  114. * translated. Strings we cannot send to the function above because
  115. * they require variable interpolation and we don't yet have the
  116. * variables at scan time.
  117. */
  118. return str;
  119. }
  120. /* https://xmpp.org/extensions/xep-0045.html
  121. * ----------------------------------------
  122. * 100 message Entering a groupchat Inform user that any occupant is allowed to see the user's full JID
  123. * 101 message (out of band) Affiliation change Inform user that his or her affiliation changed while not in the groupchat
  124. * 102 message Configuration change Inform occupants that groupchat now shows unavailable members
  125. * 103 message Configuration change Inform occupants that groupchat now does not show unavailable members
  126. * 104 message Configuration change Inform occupants that a non-privacy-related groupchat configuration change has occurred
  127. * 110 presence Any groupchat presence Inform user that presence refers to one of its own groupchat occupants
  128. * 170 message or initial presence Configuration change Inform occupants that groupchat logging is now enabled
  129. * 171 message Configuration change Inform occupants that groupchat logging is now disabled
  130. * 172 message Configuration change Inform occupants that the groupchat is now non-anonymous
  131. * 173 message Configuration change Inform occupants that the groupchat is now semi-anonymous
  132. * 174 message Configuration change Inform occupants that the groupchat is now fully-anonymous
  133. * 201 presence Entering a groupchat Inform user that a new groupchat has been created
  134. * 210 presence Entering a groupchat Inform user that the service has assigned or modified the occupant's roomnick
  135. * 301 presence Removal from groupchat Inform user that he or she has been banned from the groupchat
  136. * 303 presence Exiting a groupchat Inform all occupants of new groupchat nickname
  137. * 307 presence Removal from groupchat Inform user that he or she has been kicked from the groupchat
  138. * 321 presence Removal from groupchat Inform user that he or she is being removed from the groupchat because of an affiliation change
  139. * 322 presence Removal from groupchat Inform user that he or she is being removed from the groupchat because the groupchat has been changed to members-only and the user is not a member
  140. * 332 presence Removal from groupchat Inform user that he or she is being removed from the groupchat because of a system shutdown
  141. */
  142. _converse.muc = {
  143. info_messages: {
  144. 100: __('This groupchat is not anonymous'),
  145. 102: __('This groupchat now shows unavailable members'),
  146. 103: __('This groupchat does not show unavailable members'),
  147. 104: __('The groupchat configuration has changed'),
  148. 170: __('groupchat logging is now enabled'),
  149. 171: __('groupchat logging is now disabled'),
  150. 172: __('This groupchat is now no longer anonymous'),
  151. 173: __('This groupchat is now semi-anonymous'),
  152. 174: __('This groupchat is now fully-anonymous'),
  153. 201: __('A new groupchat has been created')
  154. },
  155. new_nickname_messages: {
  156. // XXX: Note the triple underscore function and not double underscore.
  157. 210: ___('Your nickname has been automatically set to %1$s'),
  158. 303: ___('Your nickname has been changed to %1$s')
  159. },
  160. disconnect_messages: {
  161. 301: __('You have been banned from this groupchat'),
  162. 307: __('You have been kicked from this groupchat'),
  163. 321: __("You have been removed from this groupchat because of an affiliation change"),
  164. 322: __("You have been removed from this groupchat because the groupchat has changed to members-only and you're not a member"),
  165. 332: __("You have been removed from this groupchat because the service hosting it is being shut down")
  166. },
  167. action_info_messages: {
  168. // XXX: Note the triple underscore function and not double underscore.
  169. 301: ___("%1$s has been banned"),
  170. 303: ___("%1$s's nickname has changed"),
  171. 307: ___("%1$s has been kicked out"),
  172. 321: ___("%1$s has been removed because of an affiliation change"),
  173. 322: ___("%1$s has been removed for not being a member")
  174. }
  175. }
  176. async function openRoom (jid) {
  177. if (!u.isValidMUCJID(jid)) {
  178. return _converse.log(
  179. `Invalid JID "${jid}" provided in URL fragment`,
  180. Strophe.LogLevel.WARN
  181. );
  182. }
  183. await _converse.api.waitUntil('roomsAutoJoined');
  184. if (_converse.allow_bookmarks) {
  185. await _converse.api.waitUntil('bookmarksInitialized');
  186. }
  187. _converse.api.rooms.open(jid);
  188. }
  189. _converse.router.route('converse/room?jid=:jid', openRoom);
  190. _converse.getDefaultMUCNickname = function () {
  191. // XXX: if anything changes here, update the docs for the
  192. // locked_muc_nickname setting.
  193. if (!_converse.xmppstatus) {
  194. throw new Error(
  195. "Can't call _converse.getDefaultMUCNickname before the statusInitialized has been fired.");
  196. }
  197. const nick = _converse.nickname || (_converse.vcards ? _converse.xmppstatus.vcard.get('nickname') : undefined);
  198. if (nick) {
  199. return nick;
  200. } else if (_converse.muc_nickname_from_jid) {
  201. return Strophe.unescapeNode(Strophe.getNodeFromJid(_converse.bare_jid));
  202. }
  203. }
  204. function openChatRoom (jid, settings) {
  205. /* Opens a groupchat, making sure that certain attributes
  206. * are correct, for example that the "type" is set to
  207. * "chatroom".
  208. */
  209. settings.type = _converse.CHATROOMS_TYPE;
  210. settings.id = jid;
  211. const chatbox = _converse.chatboxes.getChatBox(jid, settings, true);
  212. chatbox.maybeShow(true);
  213. return chatbox;
  214. }
  215. /**
  216. * Represents a MUC message
  217. * @class
  218. * @namespace _converse.ChatRoomMessage
  219. * @memberOf _converse
  220. */
  221. _converse.ChatRoomMessage = _converse.Message.extend({
  222. initialize () {
  223. if (this.get('file')) {
  224. this.on('change:put', this.uploadFile, this);
  225. }
  226. if (this.isEphemeral()) {
  227. window.setTimeout(() => {
  228. try {
  229. this.destroy()
  230. } catch (e) {
  231. _converse.log(e, Strophe.LogLevel.ERROR);
  232. }
  233. }, 10000);
  234. } else {
  235. this.occupantAdded = u.getResolveablePromise();
  236. this.setOccupant();
  237. this.setVCard();
  238. }
  239. },
  240. setOccupant () {
  241. const chatbox = _.get(this, 'collection.chatbox');
  242. if (!chatbox) { return; }
  243. const nick = Strophe.getResourceFromJid(this.get('from'));
  244. this.occupant = chatbox.occupants.findWhere({'nick': nick});
  245. this.occupantAdded.resolve();
  246. },
  247. getVCardForChatroomOccupant () {
  248. const chatbox = _.get(this, 'collection.chatbox');
  249. const nick = Strophe.getResourceFromJid(this.get('from'));
  250. if (chatbox && chatbox.get('nick') === nick) {
  251. return _converse.xmppstatus.vcard;
  252. } else {
  253. let vcard;
  254. if (this.get('vcard_jid')) {
  255. vcard = _converse.vcards.findWhere({'jid': this.get('vcard_jid')});
  256. }
  257. if (!vcard) {
  258. let jid;
  259. if (this.occupant && this.occupant.get('jid')) {
  260. jid = this.occupant.get('jid');
  261. this.save({'vcard_jid': jid}, {'silent': true});
  262. } else {
  263. jid = this.get('from');
  264. }
  265. if (jid) {
  266. vcard = _converse.vcards.findWhere({'jid': jid}) || _converse.vcards.create({'jid': jid});
  267. } else {
  268. _converse.log(
  269. `Could not assign VCard for message because no JID found! msgid: ${this.get('msgid')}`,
  270. Strophe.LogLevel.ERROR
  271. );
  272. return;
  273. }
  274. }
  275. return vcard;
  276. }
  277. },
  278. setVCard () {
  279. if (!_converse.vcards) {
  280. // VCards aren't supported
  281. return;
  282. }
  283. if (['error', 'info'].includes(this.get('type'))) {
  284. return;
  285. } else {
  286. this.vcard = this.getVCardForChatroomOccupant();
  287. }
  288. },
  289. });
  290. /**
  291. * Collection which stores MUC messages
  292. * @class
  293. * @namespace _converse.ChatRoomMessages
  294. * @memberOf _converse
  295. */
  296. _converse.ChatRoomMessages = Backbone.Collection.extend({
  297. model: _converse.ChatRoomMessage,
  298. comparator: 'time'
  299. });
  300. /**
  301. * Represents an open/ongoing groupchat conversation.
  302. * @class
  303. * @namespace _converse.ChatRoom
  304. * @memberOf _converse
  305. */
  306. _converse.ChatRoom = _converse.ChatBox.extend({
  307. messagesCollection: _converse.ChatRoomMessages,
  308. defaults () {
  309. return {
  310. // For group chats, we distinguish between generally unread
  311. // messages and those ones that specifically mention the
  312. // user.
  313. //
  314. // To keep things simple, we reuse `num_unread` from
  315. // _converse.ChatBox to indicate unread messages which
  316. // mention the user and `num_unread_general` to indicate
  317. // generally unread messages (which *includes* mentions!).
  318. 'num_unread_general': 0,
  319. 'affiliation': null,
  320. 'bookmarked': false,
  321. 'chat_state': undefined,
  322. 'connection_status': converse.ROOMSTATUS.DISCONNECTED,
  323. 'description': '',
  324. 'hidden': ['mobile', 'fullscreen'].includes(_converse.view_mode),
  325. 'message_type': 'groupchat',
  326. 'name': '',
  327. 'nick': _converse.getDefaultMUCNickname(),
  328. 'num_unread': 0,
  329. 'roomconfig': {},
  330. 'time_opened': this.get('time_opened') || (new Date()).getTime(),
  331. 'type': _converse.CHATROOMS_TYPE
  332. }
  333. },
  334. initialize() {
  335. if (_converse.vcards) {
  336. this.vcard = _converse.vcards.findWhere({'jid': this.get('jid')}) ||
  337. _converse.vcards.create({'jid': this.get('jid')});
  338. }
  339. this.set('box_id', `box-${btoa(this.get('jid'))}`);
  340. this.on('change:chat_state', this.sendChatState, this);
  341. this.on('change:connection_status', this.onConnectionStatusChanged, this);
  342. this.initFeatures();
  343. this.initOccupants();
  344. this.registerHandlers();
  345. this.initMessages();
  346. this.enterRoom();
  347. },
  348. async enterRoom () {
  349. const conn_status = this.get('connection_status');
  350. _converse.log(
  351. `${this.get('jid')} initialized with connection_status ${conn_status}`,
  352. Strophe.LogLevel.DEBUG
  353. );
  354. if (conn_status !== converse.ROOMSTATUS.ENTERED) {
  355. // We're not restoring a room from cache, so let's clear
  356. // the cache (which might be stale).
  357. this.removeNonMembers();
  358. await this.refreshRoomFeatures();
  359. if (_converse.clear_messages_on_reconnection) {
  360. this.clearMessages();
  361. }
  362. if (!u.isPersistableModel(this)) {
  363. // XXX: Happens during tests, nothing to do if this
  364. // is a hanging chatbox (i.e. not in the collection anymore).
  365. return;
  366. }
  367. this.join();
  368. } else if (!(await this.rejoinIfNecessary())) {
  369. this.features.fetch();
  370. this.fetchMessages();
  371. }
  372. },
  373. async onConnectionStatusChanged () {
  374. if (this.get('connection_status') === converse.ROOMSTATUS.ENTERED) {
  375. await this.occupants.fetchMembers();
  376. // It's possible to fetch messages before entering a MUC,
  377. // but we don't support this use-case currently. By
  378. // fetching messages after members we can immediately
  379. // assign an occupant to the message before rendering it,
  380. // thereby avoiding re-renders (and therefore DOM reflows).
  381. this.fetchMessages();
  382. if (_converse.auto_register_muc_nickname &&
  383. await _converse.api.disco.supports(Strophe.NS.MUC_REGISTER, this.get('jid'))) {
  384. this.registerNickname()
  385. }
  386. }
  387. },
  388. removeNonMembers () {
  389. const non_members = this.occupants.filter(o => !o.isMember());
  390. if (non_members.length) {
  391. non_members.forEach(o => o.destroy());
  392. }
  393. },
  394. async onReconnection () {
  395. this.save('connection_status', converse.ROOMSTATUS.DISCONNECTED);
  396. this.registerHandlers();
  397. await this.enterRoom();
  398. this.announceReconnection();
  399. },
  400. initFeatures () {
  401. const storage = _converse.config.get('storage');
  402. const id = `converse.muc-features-${_converse.bare_jid}-${this.get('jid')}`;
  403. this.features = new Backbone.Model(
  404. _.assign({id}, _.zipObject(converse.ROOM_FEATURES, _.map(converse.ROOM_FEATURES, _.stubFalse)))
  405. );
  406. this.features.browserStorage = new BrowserStorage.session(id);
  407. },
  408. initOccupants () {
  409. this.occupants = new _converse.ChatRoomOccupants();
  410. this.occupants.browserStorage = new BrowserStorage.session(
  411. `converse.occupants-${_converse.bare_jid}${this.get('jid')}`
  412. );
  413. this.occupants.chatroom = this;
  414. this.occupants.fetched = new Promise(resolve => {
  415. this.occupants.fetch({
  416. 'add': true,
  417. 'silent': true,
  418. 'success': resolve,
  419. 'error': resolve
  420. });
  421. });
  422. },
  423. registerHandlers () {
  424. /* Register presence and message handlers for this chat
  425. * groupchat
  426. */
  427. const room_jid = this.get('jid');
  428. this.removeHandlers();
  429. this.presence_handler = _converse.connection.addHandler(stanza => {
  430. this.onPresence(stanza);
  431. return true;
  432. },
  433. null, 'presence', null, null, room_jid,
  434. {'ignoreNamespaceFragment': true, 'matchBareFromJid': true}
  435. );
  436. this.message_handler = _converse.connection.addHandler(stanza => {
  437. this.onMessage(stanza);
  438. return true;
  439. }, null, 'message', 'groupchat', null, room_jid,
  440. {'matchBareFromJid': true}
  441. );
  442. },
  443. removeHandlers () {
  444. /* Remove the presence and message handlers that were
  445. * registered for this groupchat.
  446. */
  447. if (this.message_handler) {
  448. _converse.connection.deleteHandler(this.message_handler);
  449. delete this.message_handler;
  450. }
  451. if (this.presence_handler) {
  452. _converse.connection.deleteHandler(this.presence_handler);
  453. delete this.presence_handler;
  454. }
  455. return this;
  456. },
  457. getDisplayName () {
  458. const name = this.get('name');
  459. if (name) {
  460. return name;
  461. } else if (_converse.locked_muc_domain === 'hidden') {
  462. return Strophe.getNodeFromJid(this.get('jid'));
  463. } else {
  464. return this.get('jid');
  465. }
  466. },
  467. /**
  468. * Join the groupchat.
  469. * @private
  470. * @method _converse.ChatRoom#join
  471. * @param { String } nick - The user's nickname
  472. * @param { String } [password] - Optional password, if required by the groupchat.
  473. */
  474. async join (nick, password) {
  475. if (this.get('connection_status') === converse.ROOMSTATUS.ENTERED) {
  476. // We have restored a groupchat from session storage,
  477. // so we don't send out a presence stanza again.
  478. return this;
  479. }
  480. nick = await this.getAndPersistNickname(nick);
  481. if (!nick) {
  482. u.safeSave(this, {'connection_status': converse.ROOMSTATUS.NICKNAME_REQUIRED});
  483. return this;
  484. }
  485. const stanza = $pres({
  486. 'from': _converse.connection.jid,
  487. 'to': this.getRoomJIDAndNick()
  488. }).c("x", {'xmlns': Strophe.NS.MUC})
  489. .c("history", {'maxstanzas': this.features.get('mam_enabled') ? 0 : _converse.muc_history_max_stanzas}).up();
  490. if (password) {
  491. stanza.cnode(Strophe.xmlElement("password", [], password));
  492. }
  493. this.save('connection_status', converse.ROOMSTATUS.CONNECTING);
  494. _converse.api.send(stanza);
  495. return this;
  496. },
  497. /**
  498. * Sends an IQ stanza to the XMPP server to destroy this groupchat. Not
  499. * to be confused with the {@link _converse.ChatRoom#destroy}
  500. * method, which simply removes the room from the local browser storage cache.
  501. * @private
  502. * @method _converse.ChatRoom#sendDestroyIQ
  503. * @param { string } [reason] - The reason for destroying the groupchat
  504. * @param { string } [new_jid] - The JID of the new groupchat which
  505. * replaces this one.
  506. */
  507. sendDestroyIQ (reason, new_jid) {
  508. const destroy = $build("destroy");
  509. if (new_jid) {
  510. destroy.attrs({'jid': new_jid});
  511. }
  512. const iq = $iq({
  513. 'to': this.get('jid'),
  514. 'type': "set"
  515. }).c("query", {'xmlns': Strophe.NS.MUC_OWNER}).cnode(destroy.node);
  516. if (reason && reason.length > 0) {
  517. iq.c("reason", reason);
  518. }
  519. return _converse.api.sendIQ(iq);
  520. },
  521. /**
  522. * Leave the groupchat.
  523. * @private
  524. * @method _converse.ChatRoom#leave
  525. * @param { string } [exit_msg] - Message to indicate your reason for leaving
  526. */
  527. leave (exit_msg) {
  528. this.features.destroy();
  529. this.occupants.browserStorage._clear();
  530. this.occupants.reset();
  531. if (_converse.disco_entities) {
  532. const disco_entity = _converse.disco_entities.get(this.get('jid'));
  533. if (disco_entity) {
  534. disco_entity.destroy();
  535. }
  536. }
  537. if (_converse.connection.connected) {
  538. this.sendUnavailablePresence(exit_msg);
  539. }
  540. u.safeSave(this, {'connection_status': converse.ROOMSTATUS.DISCONNECTED});
  541. this.removeHandlers();
  542. },
  543. sendUnavailablePresence (exit_msg) {
  544. const presence = $pres({
  545. type: "unavailable",
  546. from: _converse.connection.jid,
  547. to: this.getRoomJIDAndNick()
  548. });
  549. if (exit_msg !== null) {
  550. presence.c("status", exit_msg);
  551. }
  552. _converse.connection.sendPresence(presence);
  553. },
  554. getReferenceForMention (mention, index) {
  555. const longest_match = u.getLongestSubstring(
  556. mention,
  557. this.occupants.map(o => o.getDisplayName())
  558. );
  559. if (!longest_match) {
  560. return null;
  561. }
  562. if ((mention[longest_match.length] || '').match(/[A-Za-zäëïöüâêîôûáéíóúàèìòùÄËÏÖÜÂÊÎÔÛÁÉÍÓÚÀÈÌÒÙ0-9]/i)) {
  563. // avoid false positives, i.e. mentions that have
  564. // further alphabetical characters than our longest
  565. // match.
  566. return null;
  567. }
  568. const occupant = this.occupants.findOccupant({'nick': longest_match}) ||
  569. this.occupants.findOccupant({'jid': longest_match});
  570. if (!occupant) {
  571. return null;
  572. }
  573. const obj = {
  574. 'begin': index,
  575. 'end': index + longest_match.length,
  576. 'value': longest_match,
  577. 'type': 'mention'
  578. };
  579. if (occupant.get('jid')) {
  580. obj.uri = `xmpp:${occupant.get('jid')}`;
  581. } else {
  582. obj.uri = `xmpp:${this.get('jid')}/${occupant.get('nick')}`;
  583. }
  584. return obj;
  585. },
  586. extractReference (text, index) {
  587. for (let i=index; i<text.length; i++) {
  588. if (text[i] === '@' && (i === 0 || text[i - 1] === ' ')) {
  589. const match = text.slice(i+1),
  590. ref = this.getReferenceForMention(match, i);
  591. if (ref) {
  592. return [text.slice(0, i) + match, ref, i]
  593. }
  594. }
  595. }
  596. return;
  597. },
  598. parseTextForReferences (text) {
  599. const refs = [];
  600. let index = 0;
  601. while (index < (text || '').length) {
  602. const result = this.extractReference(text, index);
  603. if (result) {
  604. text = result[0]; // @ gets filtered out
  605. refs.push(result[1]);
  606. index = result[2];
  607. } else {
  608. break;
  609. }
  610. }
  611. return [text, refs];
  612. },
  613. getOutgoingMessageAttributes (text, spoiler_hint) {
  614. const is_spoiler = this.get('composing_spoiler');
  615. var references;
  616. [text, references] = this.parseTextForReferences(text);
  617. const origin_id = _converse.connection.getUniqueId();
  618. return {
  619. 'id': origin_id,
  620. 'msgid': origin_id,
  621. 'origin_id': origin_id,
  622. 'from': `${this.get('jid')}/${this.get('nick')}`,
  623. 'fullname': this.get('nick'),
  624. 'is_single_emoji': text ? u.isSingleEmoji(text) : false,
  625. 'is_spoiler': is_spoiler,
  626. 'message': text ? u.httpToGeoUri(u.shortnameToUnicode(text), _converse) : undefined,
  627. 'nick': this.get('nick'),
  628. 'references': references,
  629. 'sender': 'me',
  630. 'spoiler_hint': is_spoiler ? spoiler_hint : undefined,
  631. 'type': 'groupchat'
  632. };
  633. },
  634. /**
  635. * Utility method to construct the JID for the current user
  636. * as occupant of the groupchat.
  637. *
  638. * @returns {string} - The groupchat JID with the user's nickname added at the end.
  639. * @example groupchat@conference.example.org/nickname
  640. */
  641. getRoomJIDAndNick () {
  642. const nick = this.get('nick');
  643. const jid = Strophe.getBareJidFromJid(this.get('jid'));
  644. return jid + (nick !== null ? `/${nick}` : "");
  645. },
  646. /**
  647. * Sends a message with the status of the user in this chat session
  648. * as taken from the 'chat_state' attribute of the chat box.
  649. * See XEP-0085 Chat State Notifications.
  650. */
  651. sendChatState () {
  652. if (!_converse.send_chat_state_notifications || this.get('connection_status') !== converse.ROOMSTATUS.ENTERED) {
  653. return;
  654. }
  655. const chat_state = this.get('chat_state');
  656. if (chat_state === _converse.GONE) {
  657. // <gone/> is not applicable within MUC context
  658. return;
  659. }
  660. _converse.api.send(
  661. $msg({'to':this.get('jid'), 'type': 'groupchat'})
  662. .c(chat_state, {'xmlns': Strophe.NS.CHATSTATES}).up()
  663. .c('no-store', {'xmlns': Strophe.NS.HINTS}).up()
  664. .c('no-permanent-store', {'xmlns': Strophe.NS.HINTS})
  665. );
  666. },
  667. /**
  668. * Send a direct invitation as per XEP-0249
  669. * @private
  670. * @method _converse.ChatRoom#directInvite
  671. * @param { String } recipient - JID of the person being invited
  672. * @param { String } [reason] - Reason for the invitation
  673. */
  674. directInvite (recipient, reason) {
  675. if (this.features.get('membersonly')) {
  676. // When inviting to a members-only groupchat, we first add
  677. // the person to the member list by giving them an
  678. // affiliation of 'member' (if they're not affiliated
  679. // already), otherwise they won't be able to join.
  680. const map = {}; map[recipient] = 'member';
  681. const deltaFunc = _.partial(u.computeAffiliationsDelta, true, false);
  682. this.updateMemberLists(
  683. [{'jid': recipient, 'affiliation': 'member', 'reason': reason}],
  684. ['member', 'owner', 'admin'],
  685. deltaFunc
  686. );
  687. }
  688. const attrs = {
  689. 'xmlns': 'jabber:x:conference',
  690. 'jid': this.get('jid')
  691. };
  692. if (reason !== null) { attrs.reason = reason; }
  693. if (this.get('password')) { attrs.password = this.get('password'); }
  694. const invitation = $msg({
  695. 'from': _converse.connection.jid,
  696. 'to': recipient,
  697. 'id': _converse.connection.getUniqueId()
  698. }).c('x', attrs);
  699. _converse.api.send(invitation);
  700. /**
  701. * After the user has sent out a direct invitation (as per XEP-0249),
  702. * to a roster contact, asking them to join a room.
  703. * @event _converse#chatBoxMaximized
  704. * @type { object }
  705. * @property { _converse.ChatRoom } room
  706. * @property { string } recipient - The JID of the person being invited
  707. * @property { string } reason - The original reason for the invitation
  708. * @example _converse.api.listen.on('chatBoxMaximized', view => { ... });
  709. */
  710. _converse.api.trigger('roomInviteSent', {
  711. 'room': this,
  712. 'recipient': recipient,
  713. 'reason': reason
  714. });
  715. },
  716. async refreshRoomFeatures () {
  717. await _converse.api.disco.refreshFeatures(this.get('jid'));
  718. return this.getRoomFeatures();
  719. },
  720. async getRoomFeatures () {
  721. let identity;
  722. try {
  723. identity = await _converse.api.disco.getIdentity('conference', 'text', this.get('jid'));
  724. } catch (e) {
  725. // Getting the identity probably failed because this room doesn't exist yet.
  726. return _converse.log(e, Strophe.LogLevel.ERROR);
  727. }
  728. const fields = await _converse.api.disco.getFields(this.get('jid'));
  729. this.save({
  730. 'name': identity && identity.get('name'),
  731. 'description': _.get(fields.findWhere({'var': "muc#roominfo_description"}), 'attributes.value')
  732. });
  733. const features = await _converse.api.disco.getFeatures(this.get('jid'));
  734. const attrs = Object.assign(
  735. _.zipObject(converse.ROOM_FEATURES, _.map(converse.ROOM_FEATURES, _.stubFalse)),
  736. {'fetched': (new Date()).toISOString()}
  737. );
  738. features.each(feature => {
  739. const fieldname = feature.get('var');
  740. if (!fieldname.startsWith('muc_')) {
  741. if (fieldname === Strophe.NS.MAM) {
  742. attrs.mam_enabled = true;
  743. }
  744. return;
  745. }
  746. attrs[fieldname.replace('muc_', '')] = true;
  747. });
  748. this.features.save(attrs);
  749. },
  750. /* Send an IQ stanza to the server, asking it for the
  751. * member-list of this groupchat.
  752. * See: https://xmpp.org/extensions/xep-0045.html#modifymember
  753. * @private
  754. * @method _converse.ChatRoom#requestMemberList
  755. * @param { string } affiliation - The specific member list to
  756. * fetch. 'admin', 'owner' or 'member'.
  757. * @returns:
  758. * A promise which resolves once the list has been retrieved.
  759. */
  760. requestMemberList (affiliation) {
  761. affiliation = affiliation || 'member';
  762. const iq = $iq({to: this.get('jid'), type: "get"})
  763. .c("query", {xmlns: Strophe.NS.MUC_ADMIN})
  764. .c("item", {'affiliation': affiliation});
  765. return _converse.api.sendIQ(iq);
  766. },
  767. /**
  768. * Send IQ stanzas to the server to set an affiliation for
  769. * the provided JIDs.
  770. * See: https://xmpp.org/extensions/xep-0045.html#modifymember
  771. *
  772. * Prosody doesn't accept multiple JIDs' affiliations
  773. * being set in one IQ stanza, so as a workaround we send
  774. * a separate stanza for each JID.
  775. * Related ticket: https://issues.prosody.im/345
  776. *
  777. * @private
  778. * @method _converse.ChatRoom#setAffiliation
  779. * @param { string } affiliation - The affiliation
  780. * @param { object } members - A map of jids, affiliations and
  781. * optionally reasons. Only those entries with the
  782. * same affiliation as being currently set will be considered.
  783. * @returns
  784. * A promise which resolves and fails depending on the XMPP server response.
  785. */
  786. setAffiliation (affiliation, members) {
  787. members = _.filter(members, (member) =>
  788. // We only want those members who have the right
  789. // affiliation (or none, which implies the provided one).
  790. _.isUndefined(member.affiliation) ||
  791. member.affiliation === affiliation
  792. );
  793. const promises = _.map(members, _.bind(this.sendAffiliationIQ, this, affiliation));
  794. return Promise.all(promises);
  795. },
  796. /**
  797. * Submit the groupchat configuration form by sending an IQ
  798. * stanza to the server.
  799. * @private
  800. * @method _converse.ChatRoom#saveConfiguration
  801. * @param { HTMLElement } form - The configuration form DOM element.
  802. * If no form is provided, the default configuration
  803. * values will be used.
  804. * @returns { promise }
  805. * Returns a promise which resolves once the XMPP server
  806. * has return a response IQ.
  807. */
  808. saveConfiguration (form) {
  809. return new Promise((resolve, reject) => {
  810. const inputs = form ? sizzle(':input:not([type=button]):not([type=submit])', form) : [],
  811. configArray = _.map(inputs, u.webForm2xForm);
  812. this.sendConfiguration(configArray, resolve, reject);
  813. });
  814. },
  815. /**
  816. * Given a <field> element, return a copy with a <value> child if
  817. * we can find a value for it in this rooms config.
  818. * @private
  819. * @method _converse.ChatRoom#addFieldValue
  820. * @returns { Element }
  821. */
  822. addFieldValue (field) {
  823. const type = field.getAttribute('type');
  824. if (type === 'fixed') {
  825. return field;
  826. }
  827. const fieldname = field.getAttribute('var').replace('muc#roomconfig_', '');
  828. const config = this.get('roomconfig');
  829. if (fieldname in config) {
  830. let values;
  831. switch (type) {
  832. case 'boolean':
  833. values = [config[fieldname] ? 1 : 0];
  834. break;
  835. case 'list-multi':
  836. values = config[fieldname];
  837. break;
  838. default:
  839. values= [config[fieldname]];
  840. }
  841. field.innerHTML = values.map(v => $build('value').t(v)).join('');
  842. }
  843. return field;
  844. },
  845. /**
  846. * Automatically configure the groupchat based on this model's
  847. * 'roomconfig' data.
  848. * @private
  849. * @method _converse.ChatRoom#autoConfigureChatRoom
  850. * @returns { promise }
  851. * Returns a promise which resolves once a response IQ has
  852. * been received.
  853. */
  854. autoConfigureChatRoom () {
  855. return new Promise(async (resolve, reject) => {
  856. const stanza = await this.fetchRoomConfiguration();
  857. const fields = sizzle('field', stanza);
  858. const configArray = fields.map(f => this.addFieldValue(f))
  859. if (configArray.length) {
  860. this.sendConfiguration(configArray, resolve, reject);
  861. }
  862. });
  863. },
  864. fetchRoomConfiguration () {
  865. /* Send an IQ stanza to fetch the groupchat configuration data.
  866. * Returns a promise which resolves once the response IQ
  867. * has been received.
  868. */
  869. return _converse.api.sendIQ(
  870. $iq({'to': this.get('jid'), 'type': "get"})
  871. .c("query", {xmlns: Strophe.NS.MUC_OWNER})
  872. );
  873. },
  874. /**
  875. * Send an IQ stanza with the groupchat configuration.
  876. * @private
  877. * @method _converse.ChatRoom#sendConfiguration
  878. * @param { Array } config - The groupchat configuration
  879. * @param { Function } callback - Callback upon succesful IQ response
  880. * The first parameter passed in is IQ containing the
  881. * groupchat configuration.
  882. * The second is the response IQ from the server.
  883. * @param { Function } errback - Callback upon error IQ response
  884. * The first parameter passed in is IQ containing the
  885. * groupchat configuration.
  886. * The second is the response IQ from the server.
  887. */
  888. sendConfiguration (config=[], callback, errback) {
  889. const iq = $iq({to: this.get('jid'), type: "set"})
  890. .c("query", {xmlns: Strophe.NS.MUC_OWNER})
  891. .c("x", {xmlns: Strophe.NS.XFORM, type: "submit"});
  892. config.forEach(node => iq.cnode(node).up());
  893. callback = _.isUndefined(callback) ? _.noop : _.partial(callback, iq.nodeTree);
  894. errback = _.isUndefined(errback) ? _.noop : _.partial(errback, iq.nodeTree);
  895. return _converse.api.sendIQ(iq).then(callback).catch(errback);
  896. },
  897. /**
  898. * Parse the presence stanza for the current user's affiliation.
  899. * @private
  900. * @method _converse.ChatRoom#saveAffiliationAndRole
  901. * @param { XMLElement } pres - A <presence> stanza.
  902. */
  903. saveAffiliationAndRole (pres) {
  904. const item = sizzle(`x[xmlns="${Strophe.NS.MUC_USER}"] item`, pres).pop();
  905. const is_self = !_.isNull(pres.querySelector("status[code='110']"));
  906. if (is_self && !_.isNil(item)) {
  907. const affiliation = item.getAttribute('affiliation');
  908. const role = item.getAttribute('role');
  909. if (affiliation) {
  910. this.save({'affiliation': affiliation});
  911. }
  912. if (role) {
  913. this.save({'role': role});
  914. }
  915. }
  916. },
  917. /**
  918. * Send an IQ stanza specifying an affiliation change.
  919. * @private
  920. * @method _converse.ChatRoom#
  921. * @param { String } affiliation: affiliation
  922. * (could also be stored on the member object).
  923. * @param { Object } member: Map containing the member's jid and
  924. * optionally a reason and affiliation.
  925. */
  926. sendAffiliationIQ (affiliation, member) {
  927. const iq = $iq({to: this.get('jid'), type: "set"})
  928. .c("query", {xmlns: Strophe.NS.MUC_ADMIN})
  929. .c("item", {
  930. 'affiliation': member.affiliation || affiliation,
  931. 'nick': member.nick,
  932. 'jid': member.jid
  933. });
  934. if (!_.isUndefined(member.reason)) {
  935. iq.c("reason", member.reason);
  936. }
  937. return _converse.api.sendIQ(iq);
  938. },
  939. /**
  940. * Send IQ stanzas to the server to modify the
  941. * affiliations in this groupchat.
  942. * See: https://xmpp.org/extensions/xep-0045.html#modifymember
  943. * @private
  944. * @method _converse.ChatRoom#setAffiliations
  945. * @param { object } members - A map of jids, affiliations and optionally reasons
  946. * @param { function } onSuccess - callback for a succesful response
  947. * @param { function } onError - callback for an error response
  948. */
  949. setAffiliations (members) {
  950. const affiliations = _.uniq(_.map(members, 'affiliation'));
  951. return Promise.all(_.map(affiliations, _.partial(this.setAffiliation.bind(this), _, members)));
  952. },
  953. /**
  954. * Send an IQ stanza to modify an occupant's role
  955. * @private
  956. * @method _converse.ChatRoom#setRole
  957. * @param { _converse.ChatRoomOccupant } occupant
  958. * @param { String } role
  959. * @param { String } reason
  960. * @param { function } onSuccess - callback for a succesful response
  961. * @param { function } onError - callback for an error response
  962. */
  963. setRole (occupant, role, reason, onSuccess, onError) {
  964. const item = $build("item", {
  965. 'nick': occupant.get('nick'),
  966. role
  967. });
  968. const iq = $iq({
  969. 'to': this.get('jid'),
  970. 'type': 'set'
  971. }).c("query", {xmlns: Strophe.NS.MUC_ADMIN}).cnode(item.node);
  972. if (reason !== null) {
  973. iq.c("reason", reason);
  974. }
  975. return _converse.api.sendIQ(iq).then(onSuccess).catch(onError);
  976. },
  977. /**
  978. * @private
  979. * @method _converse.ChatRoom#getOccupant
  980. * @param { String } nick_or_jid - The nickname or JID of the occupant to be returned
  981. * @returns { _converse.ChatRoomOccupant }
  982. */
  983. getOccupant (nick_or_jid) {
  984. return (u.isValidJID(nick_or_jid) &&
  985. this.occupants.findWhere({'jid': nick_or_jid})) ||
  986. this.occupants.findWhere({'nick': nick_or_jid});
  987. },
  988. async getJidsWithAffiliations (affiliations) {
  989. /* Returns a map of JIDs that have the affiliations
  990. * as provided.
  991. */
  992. if (_.isString(affiliations)) {
  993. affiliations = [affiliations];
  994. }
  995. const result = await Promise.all(affiliations.map(a =>
  996. this.requestMemberList(a)
  997. .then(iq => u.parseMemberListIQ(iq))
  998. .catch(iq => {
  999. _converse.log(iq, Strophe.LogLevel.ERROR);
  1000. })
  1001. ));
  1002. return [].concat.apply([], result).filter(p => p);
  1003. },
  1004. /**
  1005. * Fetch the lists of users with the given affiliations.
  1006. * Then compute the delta between those users and
  1007. * the passed in members, and if it exists, send the delta
  1008. * to the XMPP server to update the member list.
  1009. * @private
  1010. * @method _converse.ChatRoom#updateMemberLists
  1011. * @param { object } members - Map of member jids and affiliations.
  1012. * @param { string|array } affiliation - An array of affiliations or
  1013. * a string if only one affiliation.
  1014. * @param { function } deltaFunc - The function to compute the delta
  1015. * between old and new member lists.
  1016. * @returns { promise }
  1017. * A promise which is resolved once the list has been
  1018. * updated or once it's been established there's no need
  1019. * to update the list.
  1020. */
  1021. updateMemberLists (members, affiliations, deltaFunc) {
  1022. this.getJidsWithAffiliations(affiliations)
  1023. .then(old_members => this.setAffiliations(deltaFunc(members, old_members)))
  1024. .then(() => this.occupants.fetchMembers())
  1025. .catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
  1026. },
  1027. /**
  1028. * Given a nick name, save it to the model state, otherwise, look
  1029. * for a server-side reserved nickname or default configured
  1030. * nickname and if found, persist that to the model state.
  1031. * @private
  1032. * @method _converse.ChatRoom#getAndPersistNickname
  1033. * @returns { promise } A promise which resolves with the nickname
  1034. */
  1035. async getAndPersistNickname (nick) {
  1036. nick = nick ||
  1037. this.get('nick') ||
  1038. await this.getReservedNick() ||
  1039. _converse.getDefaultMUCNickname();
  1040. if (nick) {
  1041. this.save({'nick': nick}, {'silent': true});
  1042. }
  1043. return nick;
  1044. },
  1045. /**
  1046. * Use service-discovery to ask the XMPP server whether
  1047. * this user has a reserved nickname for this groupchat.
  1048. * If so, we'll use that, otherwise we render the nickname form.
  1049. * @private
  1050. * @method _converse.ChatRoom#getReservedNick
  1051. * @returns { promise } A promise which resolves with the reserved nick or null
  1052. */
  1053. async getReservedNick () {
  1054. let iq;
  1055. try {
  1056. iq = await _converse.api.sendIQ(
  1057. $iq({
  1058. 'to': this.get('jid'),
  1059. 'from': _converse.connection.jid,
  1060. 'type': "get"
  1061. }).c("query", {
  1062. 'xmlns': Strophe.NS.DISCO_INFO,
  1063. 'node': 'x-roomuser-item'
  1064. })
  1065. );
  1066. } catch (e) {
  1067. if (_.isElement(e)) {
  1068. // IQ stanza of type 'error'
  1069. return;
  1070. } else {
  1071. throw e;
  1072. }
  1073. }
  1074. const identity_el = iq.querySelector('query[node="x-roomuser-item"] identity');
  1075. return identity_el ? identity_el.getAttribute('name') : null;
  1076. },
  1077. async registerNickname () {
  1078. // See https://xmpp.org/extensions/xep-0045.html#register
  1079. const nick = this.get('nick'),
  1080. jid = this.get('jid');
  1081. let iq, err_msg;
  1082. try {
  1083. iq = await _converse.api.sendIQ(
  1084. $iq({
  1085. 'to': jid,
  1086. 'from': _converse.connection.jid,
  1087. 'type': 'get'
  1088. }).c('query', {'xmlns': Strophe.NS.MUC_REGISTER})
  1089. );
  1090. } catch (e) {
  1091. if (sizzle(`not-allowed[xmlns="${Strophe.NS.STANZAS}"]`, e).length) {
  1092. err_msg = __("You're not allowed to register yourself in this groupchat.");
  1093. } else if (sizzle(`registration-required[xmlns="${Strophe.NS.STANZAS}"]`, e).length) {
  1094. err_msg = __("You're not allowed to register in this groupchat because it's members-only.");
  1095. }
  1096. _converse.log(e, Strophe.LogLevel.ERROR);
  1097. return err_msg;
  1098. }
  1099. const required_fields = sizzle('field required', iq).map(f => f.parentElement);
  1100. if (required_fields.length > 1 && required_fields[0].getAttribute('var') !== 'muc#register_roomnick') {
  1101. return _converse.log(`Can't register the user register in the groupchat ${jid} due to the required fields`);
  1102. }
  1103. try {
  1104. await _converse.api.sendIQ($iq({
  1105. 'to': jid,
  1106. 'from': _converse.connection.jid,
  1107. 'type': 'set'
  1108. }).c('query', {'xmlns': Strophe.NS.MUC_REGISTER})
  1109. .c('x', {'xmlns': Strophe.NS.XFORM, 'type': 'submit'})
  1110. .c('field', {'var': 'FORM_TYPE'}).c('value').t('http://jabber.org/protocol/muc#register').up().up()
  1111. .c('field', {'var': 'muc#register_roomnick'}).c('value').t(nick)
  1112. );
  1113. } catch (e) {
  1114. if (sizzle(`service-unavailable[xmlns="${Strophe.NS.STANZAS}"]`, e).length) {
  1115. err_msg = __("Can't register your nickname in this groupchat, it doesn't support registration.");
  1116. } else if (sizzle(`bad-request[xmlns="${Strophe.NS.STANZAS}"]`, e).length) {
  1117. err_msg = __("Can't register your nickname in this groupchat, invalid data form supplied.");
  1118. }
  1119. _converse.log(err_msg);
  1120. _converse.log(e, Strophe.LogLevel.ERROR);
  1121. return err_msg;
  1122. }
  1123. },
  1124. /**
  1125. * Given a presence stanza, update the occupant model
  1126. * based on its contents.
  1127. * @private
  1128. * @method _converse.ChatRoom#updateOccupantsOnPresence
  1129. * @param { XMLElement } pres - The presence stanza
  1130. */
  1131. updateOccupantsOnPresence (pres) {
  1132. const data = this.parsePresence(pres);
  1133. if (data.type === 'error' || (!data.jid && !data.nick)) {
  1134. return true;
  1135. }
  1136. const occupant = this.occupants.findOccupant(data);
  1137. if (data.type === 'unavailable' && occupant) {
  1138. if (!_.includes(data.states, converse.MUC_NICK_CHANGED_CODE) && !occupant.isMember()) {
  1139. // We only destroy the occupant if this is not a nickname change operation.
  1140. // and if they're not on the member lists.
  1141. // Before destroying we set the new data, so
  1142. // that we can show the disconnection message.
  1143. occupant.set(data);
  1144. occupant.destroy();
  1145. return;
  1146. }
  1147. }
  1148. const jid = data.jid || '';
  1149. const attributes = Object.assign(data, {
  1150. 'jid': Strophe.getBareJidFromJid(jid) || _.get(occupant, 'attributes.jid'),
  1151. 'resource': Strophe.getResourceFromJid(jid) || _.get(occupant, 'attributes.resource')
  1152. });
  1153. if (occupant) {
  1154. occupant.save(attributes);
  1155. } else {
  1156. this.occupants.create(attributes);
  1157. }
  1158. },
  1159. parsePresence (pres) {
  1160. const from = pres.getAttribute("from"),
  1161. type = pres.getAttribute("type"),
  1162. data = {
  1163. 'from': from,
  1164. 'nick': Strophe.getResourceFromJid(from),
  1165. 'type': type,
  1166. 'states': [],
  1167. 'show': type !== 'unavailable' ? 'online' : 'offline'
  1168. };
  1169. pres.childNodes.forEach(child => {
  1170. switch (child.nodeName) {
  1171. case "status":
  1172. data.status = child.textContent || null;
  1173. break;
  1174. case "show":
  1175. data.show = child.textContent || 'online';
  1176. break;
  1177. case "x":
  1178. if (child.getAttribute("xmlns") === Strophe.NS.MUC_USER) {
  1179. child.childNodes.forEach(item => {
  1180. switch (item.nodeName) {
  1181. case "item":
  1182. data.affiliation = item.getAttribute("affiliation");
  1183. data.role = item.getAttribute("role");
  1184. data.jid = item.getAttribute("jid");
  1185. data.nick = item.getAttribute("nick") || data.nick;
  1186. break;
  1187. case "status":
  1188. if (item.getAttribute("code")) {
  1189. data.states.push(item.getAttribute("code"));
  1190. }
  1191. }
  1192. });
  1193. } else if (child.getAttribute("xmlns") === Strophe.NS.VCARDUPDATE) {
  1194. data.image_hash = _.get(child.querySelector('photo'), 'textContent');
  1195. }
  1196. }
  1197. });
  1198. return data;
  1199. },
  1200. fetchFeaturesIfConfigurationChanged (stanza) {
  1201. // 104: configuration change
  1202. // 170: logging enabled
  1203. // 171: logging disabled
  1204. // 172: room no longer anonymous
  1205. // 173: room now semi-anonymous
  1206. // 174: room now fully anonymous
  1207. const codes = ['104', '170', '171', '172', '173', '174'];
  1208. if (sizzle('status', stanza).filter(e => codes.includes(e.getAttribute('status'))).length) {
  1209. this.refreshRoomFeatures();
  1210. }
  1211. },
  1212. isReceipt (stanza) {
  1213. return sizzle(`received[xmlns="${Strophe.NS.RECEIPTS}"]`, stanza).length > 0;
  1214. },
  1215. isChatMarker (stanza) {
  1216. return sizzle(
  1217. `received[xmlns="${Strophe.NS.MARKERS}"],
  1218. displayed[xmlns="${Strophe.NS.MARKERS}"],
  1219. acknowledged[xmlns="${Strophe.NS.MARKERS}"]`, stanza).length > 0;
  1220. },
  1221. /**
  1222. * Handle a subject change and return `true` if so.
  1223. * @private
  1224. * @method _converse.ChatRoom#subjectChangeHandled
  1225. * @param { object } attrs - The message attributes
  1226. */
  1227. subjectChangeHandled (attrs) {
  1228. if (attrs.subject && !attrs.thread && !attrs.message) {
  1229. // https://xmpp.org/extensions/xep-0045.html#subject-mod
  1230. // -----------------------------------------------------
  1231. // The subject is changed by sending a message of type "groupchat" to the <room@service>,
  1232. // where the <message/> MUST contain a <subject/> element that specifies the new subject but
  1233. // MUST NOT contain a <body/> element (or a <thread/> element).
  1234. u.safeSave(this, {'subject': {'author': attrs.nick, 'text': attrs.subject || ''}});
  1235. return true;
  1236. }
  1237. return false;
  1238. },
  1239. /**
  1240. * Set the subject for this {@link _converse.ChatRoom}
  1241. * @private
  1242. * @method _converse.ChatRoom#setSubject
  1243. * @param { String } value
  1244. */
  1245. setSubject(value='') {
  1246. _converse.api.send(
  1247. $msg({
  1248. to: this.get('jid'),
  1249. from: _converse.connection.jid,
  1250. type: "groupchat"
  1251. }).c("subject", {xmlns: "jabber:client"}).t(value).tree()
  1252. );
  1253. },
  1254. /**
  1255. * Is this a chat state notification that can be ignored,
  1256. * because it's old or because it's from us.
  1257. * @private
  1258. * @method _converse.ChatRoom#ignorableCSN
  1259. * @param { Object } attrs - The message attributes
  1260. */
  1261. ignorableCSN (attrs) {
  1262. const is_csn = u.isOnlyChatStateNotification(attrs);
  1263. return is_csn && (attrs.is_delayed || this.isOwnMessage(attrs));
  1264. },
  1265. /**
  1266. * Determines whether the message is from ourselves by checking
  1267. * the `from` attribute. Doesn't check the `type` attribute.
  1268. * @private
  1269. * @method _converse.ChatRoom#isOwnMessage
  1270. * @param { Object|XMLElement|_converse.Message } msg
  1271. * @returns { boolean }
  1272. */
  1273. isOwnMessage (msg) {
  1274. let from;
  1275. if (_.isElement(msg)) {
  1276. from = msg.getAttribute('from');
  1277. } else if (msg instanceof _converse.Message) {
  1278. from = msg.get('from');
  1279. } else {
  1280. from = msg.from;
  1281. }
  1282. return Strophe.getResourceFromJid(from) == this.get('nick');
  1283. },
  1284. getUpdatedMessageAttributes (message, stanza) {
  1285. // Overridden in converse-muc and converse-mam
  1286. const attrs = _converse.ChatBox.prototype.getUpdatedMessageAttributes.call(this, message, stanza);
  1287. if (this.isOwnMessage(message)) {
  1288. const stanza_id = sizzle(`stanza-id[xmlns="${Strophe.NS.SID}"]`, stanza).pop();
  1289. const by_jid = stanza_id ? stanza_id.getAttribute('by') : undefined;
  1290. if (by_jid) {
  1291. const key = `stanza_id ${by_jid}`;
  1292. attrs[key] = stanza_id.getAttribute('id');
  1293. }
  1294. if (!message.get('received')) {
  1295. attrs.received = (new Date()).toISOString();
  1296. }
  1297. }
  1298. return attrs;
  1299. },
  1300. /**
  1301. * Send a MUC-0410 MUC Self-Ping stanza to room to determine
  1302. * whether we're still joined.
  1303. * @async
  1304. * @private
  1305. * @method _converse.ChatRoom#isJoined
  1306. * @returns {Promise<boolean>}
  1307. */
  1308. async isJoined () {
  1309. const ping = $iq({
  1310. 'to': `${this.get('jid')}/${this.get('nick')}`,
  1311. 'type': "get"
  1312. }).c("ping", {'xmlns': Strophe.NS.PING});
  1313. let result;
  1314. try {
  1315. result = await _converse.api.sendIQ(ping);
  1316. } catch (e) {
  1317. const sel = `error not-acceptable[xmlns="${Strophe.NS.STANZAS}"]`;
  1318. if (_.isElement(e) && sizzle(sel, e).length) {
  1319. return false;
  1320. }
  1321. }
  1322. return true;
  1323. },
  1324. /**
  1325. * Check whether we're still joined and re-join if not
  1326. * @async
  1327. * @private
  1328. * @method _converse.ChatRoom#rejoinIfNecessary
  1329. */
  1330. async rejoinIfNecessary () {
  1331. const is_joined = await this.isJoined();
  1332. if (!is_joined) {
  1333. this.save('connection_status', converse.ROOMSTATUS.DISCONNECTED);
  1334. this.enterRoom();
  1335. return true;
  1336. }
  1337. },
  1338. /**
  1339. * @private
  1340. * @method _converse.ChatRoom#shouldShowErrorMessage
  1341. * @returns {Promise<boolean>}
  1342. */
  1343. async shouldShowErrorMessage (stanza) {
  1344. if (sizzle(`not-acceptable[xmlns="${Strophe.NS.STANZAS}"]`, stanza).length) {
  1345. if (await this.rejoinIfNecessary()) {
  1346. return false;
  1347. }
  1348. }
  1349. return _converse.ChatBox.prototype.shouldShowErrorMessage.call(this, stanza);
  1350. },
  1351. getErrorMessage (stanza) {
  1352. if (sizzle(`forbidden[xmlns="${Strophe.NS.STANZAS}"]`, stanza).length) {
  1353. return __("Your message was not delivered because you're not allowed to send messages in this groupchat.");
  1354. } else if (sizzle(`not-acceptable[xmlns="${Strophe.NS.STANZAS}"]`, stanza).length) {
  1355. return __("Your message was not delivered because you're not present in the groupchat.");
  1356. } else {
  1357. return _converse.ChatBox.prototype.getErrorMessage.call(this, stanza);
  1358. }
  1359. },
  1360. /**
  1361. * Handler for all MUC messages sent to this groupchat.
  1362. * @private
  1363. * @method _converse.ChatRoom#onMessage
  1364. * @param { XMLElement } stanza - The message stanza.
  1365. */
  1366. async onMessage (stanza) {
  1367. this.createInfoMessages(stanza);
  1368. this.fetchFeaturesIfConfigurationChanged(stanza);
  1369. const original_stanza = stanza;
  1370. const forwarded = sizzle(`forwarded[xmlns="${Strophe.NS.FORWARD}"]`, stanza).pop();
  1371. if (forwarded) {
  1372. stanza = forwarded.querySelector('message');
  1373. }
  1374. const message = await this.getDuplicateMessage(original_stanza);
  1375. if (message) {
  1376. this.updateMessage(message, original_stanza);
  1377. }
  1378. if (message ||
  1379. this.isReceipt(stanza) ||
  1380. this.isChatMarker(stanza)) {
  1381. return _converse.api.trigger('message', {'stanza': original_stanza});
  1382. }
  1383. const attrs = await this.getMessageAttributesFromStanza(stanza, original_stanza);
  1384. if (attrs.nick &&
  1385. !this.subjectChangeHandled(attrs) &&
  1386. !this.ignorableCSN(attrs) &&
  1387. (attrs['chat_state'] || !u.isEmptyMessage(attrs))) {
  1388. const msg = this.correctMessage(attrs) || this.messages.create(attrs);
  1389. this.incrementUnreadMsgCounter(msg);
  1390. }
  1391. _converse.api.trigger('message', {'stanza': original_stanza, 'chatbox': this});
  1392. },
  1393. handleModifyError(pres) {
  1394. const text = _.get(pres.querySelector('error text'), 'textContent');
  1395. if (text) {
  1396. if (this.get('connection_status') === converse.ROOMSTATUS.CONNECTING) {
  1397. this.setDisconnectionMessage(text);
  1398. } else {
  1399. const attrs = {
  1400. 'type': 'error',
  1401. 'message': text
  1402. }
  1403. this.messages.create(attrs);
  1404. }
  1405. }
  1406. },
  1407. handleDisconnection (stanza) {
  1408. const is_self = !_.isNull(stanza.querySelector("status[code='110']"));
  1409. const x = sizzle(`x[xmlns="${Strophe.NS.MUC_USER}"]`, stanza).pop();
  1410. if (!x) {
  1411. return;
  1412. }
  1413. const codes = sizzle('status', x).map(s => s.getAttribute('code'));
  1414. const disconnection_codes = _.intersection(codes, Object.keys(_converse.muc.disconnect_messages));
  1415. const disconnected = is_self && disconnection_codes.length > 0;
  1416. if (!disconnected) {
  1417. return;
  1418. }
  1419. // By using querySelector we assume here there is
  1420. // one <item> per <x xmlns='http://jabber.org/protocol/muc#user'>
  1421. // element. This appears to be a safe assumption, since
  1422. // each <x/> element pertains to a single user.
  1423. const item = x.querySelector('item');
  1424. const reason = item ? _.get(item.querySelector('reason'), 'textContent') : undefined;
  1425. const actor = item ? _.invoke(item.querySelector('actor'), 'getAttribute', 'nick') : undefined;
  1426. const message = _converse.muc.disconnect_messages[disconnection_codes[0]];
  1427. this.setDisconnectionMessage(message, reason, actor);
  1428. },
  1429. /**
  1430. * Create info messages based on a received presence stanza
  1431. * @private
  1432. * @method _converse.ChatRoom#createInfoMessages
  1433. * @param { XMLElement } stanza: The presence stanza received
  1434. */
  1435. createInfoMessages (stanza) {
  1436. const is_self = !_.isNull(stanza.querySelector("status[code='110']"));
  1437. const x = sizzle(`x[xmlns="${Strophe.NS.MUC_USER}"]`, stanza).pop();
  1438. if (!x) {
  1439. return;
  1440. }
  1441. const codes = sizzle('status', x).map(s => s.getAttribute('code'));
  1442. codes.forEach(code => {
  1443. let message;
  1444. if (code === '110' || (code === '100' && !is_self)) {
  1445. return;
  1446. } else if (code in _converse.muc.info_messages) {
  1447. message = _converse.muc.info_messages[code];
  1448. } else if (!is_self && (code in _converse.muc.action_info_messages)) {
  1449. const nick = Strophe.getResourceFromJid(stanza.getAttribute('from'));
  1450. message = __(_converse.muc.action_info_messages[code], nick);
  1451. const item = x.querySelector('item');
  1452. const reason = item ? _.get(item.querySelector('reason'), 'textContent') : undefined;
  1453. const actor = item ? _.invoke(item.querySelector('actor'), 'getAttribute', 'nick') : undefined;
  1454. if (actor) {
  1455. message += '\n' + __('This action was done by %1$s.', actor);
  1456. }
  1457. if (reason) {
  1458. message += '\n' + __('The reason given is: "%1$s".', reason);
  1459. }
  1460. } else if (is_self && (code in _converse.muc.new_nickname_messages)) {
  1461. let nick;
  1462. if (is_self && code === "210") {
  1463. nick = Strophe.getResourceFromJid(stanza.getAttribute('from'));
  1464. } else if (is_self && code === "303") {
  1465. nick = stanza.querySelector('x item').getAttribute('nick');
  1466. }
  1467. this.save('nick', nick);
  1468. message = __(_converse.muc.new_nickname_messages[code], nick);
  1469. }
  1470. if (message) {
  1471. this.messages.create({'type': 'info', message});
  1472. }
  1473. });
  1474. },
  1475. setDisconnectionMessage (message, reason, actor) {
  1476. this.save({
  1477. 'connection_status': converse.ROOMSTATUS.DISCONNECTED,
  1478. 'disconnection_message': message,
  1479. 'disconnection_reason': reason,
  1480. 'disconnection_actor': actor
  1481. });
  1482. },
  1483. onNicknameClash (presence) {
  1484. if (_converse.muc_nickname_from_jid) {
  1485. const nick = presence.getAttribute('from').split('/')[1];
  1486. if (nick === _converse.getDefaultMUCNickname()) {
  1487. this.join(nick + '-2');
  1488. } else {
  1489. const del= nick.lastIndexOf("-");
  1490. const num = nick.substring(del+1, nick.length);
  1491. this.join(nick.substring(0, del+1) + String(Number(num)+1));
  1492. }
  1493. } else {
  1494. this.save({
  1495. 'nickname_validation_message': __(
  1496. "The nickname you chose is reserved or "+
  1497. "currently in use, please choose a different one."),
  1498. 'connection_status': converse.ROOMSTATUS.NICKNAME_REQUIRED
  1499. });
  1500. }
  1501. },
  1502. /**
  1503. * Parses a <presence> stanza with type "error" and sets the proper
  1504. * `connection_status` value for this {@link _converse.ChatRoom} as
  1505. * well as any additional output that can be shown to the user.
  1506. * @private
  1507. * @param { XMLElement } stanza - The presence stanza
  1508. */
  1509. onErrorPresence (stanza) {
  1510. const error = stanza.querySelector('error');
  1511. const error_type = error.getAttribute('type');
  1512. const reason = _.get(sizzle(`text[xmlns="${Strophe.NS.STANZAS}"]`, error).pop(), 'textContent');
  1513. if (error_type === 'modify') {
  1514. this.handleModifyError(stanza);
  1515. } else if (error_type === 'auth') {
  1516. if (sizzle(`not-authorized[xmlns="${Strophe.NS.STANZAS}"]`, error).length) {
  1517. this.save({
  1518. 'password_validation_message': reason || __("Password incorrect"),
  1519. 'connection_status': converse.ROOMSTATUS.PASSWORD_REQUIRED
  1520. });
  1521. }
  1522. if (error.querySelector('registration-required')) {
  1523. const message = __('You are not on the member list of this groupchat.');
  1524. this.setDisconnectionMessage(message, reason);
  1525. } else if (error.querySelector('forbidden')) {
  1526. const message = __('You have been banned from this groupchat.');
  1527. this.setDisconnectionMessage(message, reason);
  1528. }
  1529. } else if (error_type === 'cancel') {
  1530. if (error.querySelector('not-allowed')) {
  1531. const message = __('You are not allowed to create new groupchats.');
  1532. this.setDisconnectionMessage(message, reason);
  1533. } else if (error.querySelector('not-acceptable')) {
  1534. const message = __("Your nickname doesn't conform to this groupchat's policies.");
  1535. this.setDisconnectionMessage(message, reason);
  1536. } else if (sizzle(`gone[xmlns="${Strophe.NS.STANZAS}"]`, error).length) {
  1537. const moved_jid = _.get(sizzle(`gone[xmlns="${Strophe.NS.STANZAS}"]`, error).pop(), 'textContent')
  1538. .replace(/^xmpp:/, '')
  1539. .replace(/\?join$/, '');
  1540. this.save({
  1541. 'connection_status': converse.ROOMSTATUS.DESTROYED,
  1542. 'destroyed_reason': reason,
  1543. 'moved_jid': moved_jid
  1544. });
  1545. } else if (error.querySelector('conflict')) {
  1546. this.onNicknameClash(stanza);
  1547. } else if (error.querySelector('item-not-found')) {
  1548. const message = __("This groupchat does not (yet) exist.");
  1549. this.setDisconnectionMessage(message, reason);
  1550. } else if (error.querySelector('service-unavailable')) {
  1551. const message = __("This groupchat has reached its maximum number of participants.");
  1552. this.setDisconnectionMessage(message, reason);
  1553. } else if (error.querySelector('remote-server-not-found')) {
  1554. const message = __("Remote server not found");
  1555. const feedback = reason ? __('The explanation given is: "%1$s".', reason) : undefined;
  1556. this.setDisconnectionMessage(message, feedback);
  1557. }
  1558. }
  1559. },
  1560. /**
  1561. * Handles all MUC presence stanzas.
  1562. * @private
  1563. * @method _converse.ChatRoom#onPresence
  1564. * @param { XMLElement } stanza
  1565. */
  1566. onPresence (stanza) {
  1567. if (stanza.getAttribute('type') === 'error') {
  1568. return this.onErrorPresence(stanza);
  1569. }
  1570. if (stanza.querySelector("status[code='110']")) {
  1571. this.onOwnPresence(stanza);
  1572. }
  1573. this.createInfoMessages(stanza);
  1574. this.updateOccupantsOnPresence(stanza);
  1575. if (this.get('role') !== 'none' && this.get('connection_status') === converse.ROOMSTATUS.CONNECTING) {
  1576. this.save('connection_status', converse.ROOMSTATUS.CONNECTED);
  1577. }
  1578. },
  1579. /**
  1580. * Handles a received presence relating to the current user.
  1581. *
  1582. * For locked groupchats (which are by definition "new"), the
  1583. * groupchat will either be auto-configured or created instantly
  1584. * (with default config) or a configuration groupchat will be
  1585. * rendered.
  1586. *
  1587. * If the groupchat is not locked, then the groupchat will be
  1588. * auto-configured only if applicable and if the current
  1589. * user is the groupchat's owner.
  1590. * @private
  1591. * @method _converse.ChatRoom#onOwnPresence
  1592. * @param { XMLElement } pres - The stanza
  1593. */
  1594. onOwnPresence (stanza) {
  1595. this.saveAffiliationAndRole(stanza);
  1596. if (stanza.getAttribute('type') === 'unavailable') {
  1597. this.handleDisconnection(stanza);
  1598. } else {
  1599. const locked_room = stanza.querySelector("status[code='201']");
  1600. if (locked_room) {
  1601. if (this.get('auto_configure')) {
  1602. this.autoConfigureChatRoom().then(() => this.refreshRoomFeatures());
  1603. } else if (_converse.muc_instant_rooms) {
  1604. // Accept default configuration
  1605. this.saveConfiguration().then(() => this.refreshRoomFeatures());
  1606. } else {
  1607. /**
  1608. * Triggered when a new room has been created which first needs to be configured
  1609. * and when `auto_configure` is set to `false`.
  1610. * Used by `_converse.ChatRoomView` in order to know when to render the
  1611. * configuration form for a new room.
  1612. * @event _converse.ChatRoom#configurationNeeded
  1613. * @example _converse.api.listen.on('configurationNeeded', () => { ... });
  1614. */
  1615. this.trigger('configurationNeeded');
  1616. return; // We haven't yet entered the groupchat, so bail here.
  1617. }
  1618. } else if (!this.features.get('fetched')) {
  1619. // The features for this groupchat weren't fetched.
  1620. // That must mean it's a new groupchat without locking
  1621. // (in which case Prosody doesn't send a 201 status),
  1622. // otherwise the features would have been fetched in
  1623. // the "initialize" method already.
  1624. if (this.get('affiliation') === 'owner' && this.get('auto_configure')) {
  1625. this.autoConfigureChatRoom().then(() => this.refreshRoomFeatures());
  1626. } else {
  1627. this.getRoomFeatures();
  1628. }
  1629. }
  1630. this.save('connection_status', converse.ROOMSTATUS.ENTERED);
  1631. }
  1632. },
  1633. /**
  1634. * Returns a boolean to indicate whether the current user
  1635. * was mentioned in a message.
  1636. * @private
  1637. * @method _converse.ChatRoom#isUserMentioned
  1638. * @param { String } - The text message
  1639. */
  1640. isUserMentioned (message) {
  1641. const nick = this.get('nick');
  1642. if (message.get('references').length) {
  1643. const mentions = message.get('references').filter(ref => (ref.type === 'mention')).map(ref => ref.value);
  1644. return _.includes(mentions, nick);
  1645. } else {
  1646. return (new RegExp(`\\b${nick}\\b`)).test(message.get('message'));
  1647. }
  1648. },
  1649. /* Given a newly received message, update the unread counter if necessary.
  1650. * @private
  1651. * @method _converse.ChatRoom#incrementUnreadMsgCounter
  1652. * @param { XMLElement } - The <messsage> stanza
  1653. */
  1654. incrementUnreadMsgCounter (message) {
  1655. if (!message) { return; }
  1656. const body = message.get('message');
  1657. if (_.isNil(body)) { return; }
  1658. if (u.isNewMessage(message) && this.isHidden()) {
  1659. const settings = {'num_unread_general': this.get('num_unread_general') + 1};
  1660. if (this.isUserMentioned(message)) {
  1661. settings.num_unread = this.get('num_unread') + 1;
  1662. _converse.incrementMsgCounter();
  1663. }
  1664. this.save(settings);
  1665. }
  1666. },
  1667. clearUnreadMsgCounter() {
  1668. u.safeSave(this, {
  1669. 'num_unread': 0,
  1670. 'num_unread_general': 0
  1671. });
  1672. }
  1673. });
  1674. _converse.ChatRoomOccupant = Backbone.Model.extend({
  1675. defaults: {
  1676. 'show': 'offline',
  1677. 'states': []
  1678. },
  1679. initialize (attributes) {
  1680. this.set(Object.assign(
  1681. {'id': _converse.connection.getUniqueId()},
  1682. attributes)
  1683. );
  1684. this.on('change:image_hash', this.onAvatarChanged, this);
  1685. },
  1686. onAvatarChanged () {
  1687. const hash = this.get('image_hash');
  1688. const vcards = [];
  1689. if (this.get('jid')) {
  1690. vcards.push(_converse.vcards.findWhere({'jid': this.get('jid')}));
  1691. }
  1692. vcards.push(_converse.vcards.findWhere({'jid': this.get('from')}));
  1693. _.forEach(_.filter(vcards, undefined), (vcard) => {
  1694. if (hash && vcard.get('image_hash') !== hash) {
  1695. _converse.api.vcard.update(vcard, true);
  1696. }
  1697. });
  1698. },
  1699. getDisplayName () {
  1700. return this.get('nick') || this.get('jid');
  1701. },
  1702. isMember () {
  1703. return ['admin', 'owner', 'member'].includes(this.get('affiliation'));
  1704. },
  1705. isModerator () {
  1706. return ['admin', 'owner'].includes(this.get('affiliation')) || this.get('role') === 'moderator';
  1707. },
  1708. isSelf () {
  1709. return this.get('states').includes('110');
  1710. }
  1711. });
  1712. _converse.ChatRoomOccupants = Backbone.Collection.extend({
  1713. model: _converse.ChatRoomOccupant,
  1714. comparator (occupant1, occupant2) {
  1715. const role1 = occupant1.get('role') || 'none';
  1716. const role2 = occupant2.get('role') || 'none';
  1717. if (MUC_ROLE_WEIGHTS[role1] === MUC_ROLE_WEIGHTS[role2]) {
  1718. const nick1 = occupant1.getDisplayName().toLowerCase();
  1719. const nick2 = occupant2.getDisplayName().toLowerCase();
  1720. return nick1 < nick2 ? -1 : (nick1 > nick2? 1 : 0);
  1721. } else {
  1722. return MUC_ROLE_WEIGHTS[role1] < MUC_ROLE_WEIGHTS[role2] ? -1 : 1;
  1723. }
  1724. },
  1725. async fetchMembers () {
  1726. const new_members = await this.chatroom.getJidsWithAffiliations(['member', 'owner', 'admin']);
  1727. const new_jids = new_members.map(m => m.jid).filter(m => !_.isUndefined(m)),
  1728. new_nicks = new_members.map(m => !m.jid && m.nick || undefined).filter(m => !_.isUndefined(m)),
  1729. removed_members = this.filter(m => {
  1730. return ['admin', 'member', 'owner'].includes(m.get('affiliation')) &&
  1731. !new_nicks.includes(m.get('nick')) &&
  1732. !new_jids.includes(m.get('jid'));
  1733. });
  1734. removed_members.forEach(occupant => {
  1735. if (occupant.get('jid') === _converse.bare_jid) { return; }
  1736. if (occupant.get('show') === 'offline') {
  1737. occupant.destroy();
  1738. } else {
  1739. occupant.save('affiliation', null);
  1740. }
  1741. });
  1742. new_members.forEach(attrs => {
  1743. let occupant;
  1744. if (attrs.jid) {
  1745. occupant = this.findOccupant({'jid': attrs.jid});
  1746. } else {
  1747. occupant = this.findOccupant({'nick': attrs.nick});
  1748. }
  1749. if (occupant) {
  1750. occupant.save(attrs);
  1751. } else {
  1752. this.create(attrs);
  1753. }
  1754. });
  1755. },
  1756. findOccupant (data) {
  1757. /* Try to find an existing occupant based on the passed in
  1758. * data object.
  1759. *
  1760. * If we have a JID, we use that as lookup variable,
  1761. * otherwise we use the nick. We don't always have both,
  1762. * but should have at least one or the other.
  1763. */
  1764. const jid = Strophe.getBareJidFromJid(data.jid);
  1765. if (jid !== null) {
  1766. return this.findWhere({'jid': jid});
  1767. } else {
  1768. return this.findWhere({'nick': data.nick});
  1769. }
  1770. }
  1771. });
  1772. _converse.RoomsPanelModel = Backbone.Model.extend({
  1773. defaults: function () {
  1774. return {
  1775. 'muc_domain': _converse.muc_domain,
  1776. 'nick': _converse.getDefaultMUCNickname()
  1777. }
  1778. },
  1779. setDomain (jid) {
  1780. if (!_converse.locked_muc_domain) {
  1781. this.save('muc_domain', Strophe.getDomainFromJid(jid));
  1782. }
  1783. }
  1784. });
  1785. /**
  1786. * A direct MUC invitation to join a groupchat has been received
  1787. * See XEP-0249: Direct MUC invitations.
  1788. * @private
  1789. * @method _converse.ChatRoom#onDirectMUCInvitation
  1790. * @param { XMLElement } message - The message stanza containing the invitation.
  1791. */
  1792. _converse.onDirectMUCInvitation = function (message) {
  1793. const x_el = sizzle('x[xmlns="jabber:x:conference"]', message).pop(),
  1794. from = Strophe.getBareJidFromJid(message.getAttribute('from')),
  1795. room_jid = x_el.getAttribute('jid'),
  1796. reason = x_el.getAttribute('reason');
  1797. let contact = _converse.roster.get(from),
  1798. result;
  1799. if (_converse.auto_join_on_invite) {
  1800. result = true;
  1801. } else {
  1802. // Invite request might come from someone not your roster list
  1803. contact = contact? contact.getDisplayName(): Strophe.getNodeFromJid(from);
  1804. if (!reason) {
  1805. result = confirm(
  1806. __("%1$s has invited you to join a groupchat: %2$s", contact, room_jid)
  1807. );
  1808. } else {
  1809. result = confirm(
  1810. __('%1$s has invited you to join a groupchat: %2$s, and left the following reason: "%3$s"',
  1811. contact, room_jid, reason)
  1812. );
  1813. }
  1814. }
  1815. if (result === true) {
  1816. const chatroom = openChatRoom(room_jid, {'password': x_el.getAttribute('password') });
  1817. if (chatroom.get('connection_status') === converse.ROOMSTATUS.DISCONNECTED) {
  1818. _converse.chatboxes.get(room_jid).join();
  1819. }
  1820. }
  1821. };
  1822. if (_converse.allow_muc_invitations) {
  1823. const registerDirectInvitationHandler = function () {
  1824. _converse.connection.addHandler(
  1825. (message) => {
  1826. _converse.onDirectMUCInvitation(message);
  1827. return true;
  1828. }, 'jabber:x:conference', 'message');
  1829. };
  1830. _converse.api.listen.on('connected', registerDirectInvitationHandler);
  1831. _converse.api.listen.on('reconnected', registerDirectInvitationHandler);
  1832. }
  1833. const getChatRoom = function (jid, attrs, create) {
  1834. jid = jid.toLowerCase();
  1835. attrs.type = _converse.CHATROOMS_TYPE;
  1836. attrs.id = jid;
  1837. return _converse.chatboxes.getChatBox(jid, attrs, create);
  1838. };
  1839. const createChatRoom = function (jid, attrs) {
  1840. if (jid.startsWith('xmpp:') && jid.endsWith('?join')) {
  1841. jid = jid.replace(/^xmpp:/, '').replace(/\?join$/, '');
  1842. }
  1843. return getChatRoom(jid, attrs, true);
  1844. };
  1845. function autoJoinRooms () {
  1846. /* Automatically join groupchats, based on the
  1847. * "auto_join_rooms" configuration setting, which is an array
  1848. * of strings (groupchat JIDs) or objects (with groupchat JID and other
  1849. * settings).
  1850. */
  1851. _converse.auto_join_rooms.forEach(groupchat => {
  1852. if (_converse.chatboxes.where({'jid': groupchat}).length) {
  1853. return;
  1854. }
  1855. if (_.isString(groupchat)) {
  1856. _converse.api.rooms.open(groupchat);
  1857. } else if (_.isObject(groupchat)) {
  1858. _converse.api.rooms.open(groupchat.jid, groupchat.nick);
  1859. } else {
  1860. _converse.log(
  1861. 'Invalid groupchat criteria specified for "auto_join_rooms"',
  1862. Strophe.LogLevel.ERROR);
  1863. }
  1864. });
  1865. /**
  1866. * Triggered once any rooms that have been configured to be automatically joined,
  1867. * specified via the _`auto_join_rooms` setting, have been entered.
  1868. * @event _converse#roomsAutoJoined
  1869. * @example _converse.api.listen.on('roomsAutoJoined', () => { ... });
  1870. * @example _converse.api.waitUntil('roomsAutoJoined').then(() => { ... });
  1871. */
  1872. _converse.api.trigger('roomsAutoJoined');
  1873. }
  1874. /************************ BEGIN Event Handlers ************************/
  1875. _converse.api.listen.on('beforeTearDown', () => {
  1876. const groupchats = _converse.chatboxes.where({'type': _converse.CHATROOMS_TYPE});
  1877. groupchats.forEach(gc => u.safeSave(gc, {'connection_status': converse.ROOMSTATUS.DISCONNECTED}));
  1878. });
  1879. _converse.api.listen.on('addClientFeatures', () => {
  1880. if (_converse.allow_muc) {
  1881. _converse.api.disco.own.features.add(Strophe.NS.MUC);
  1882. }
  1883. if (_converse.allow_muc_invitations) {
  1884. _converse.api.disco.own.features.add('jabber:x:conference'); // Invites
  1885. }
  1886. });
  1887. _converse.api.listen.on('chatBoxesFetched', autoJoinRooms);
  1888. function disconnectChatRooms () {
  1889. /* When disconnecting, mark all groupchats as
  1890. * disconnected, so that they will be properly entered again
  1891. * when fetched from session storage.
  1892. */
  1893. return _converse.chatboxes
  1894. .filter(m => (m.get('type') === _converse.CHATROOMS_TYPE))
  1895. .forEach(m => m.save('connection_status', converse.ROOMSTATUS.DISCONNECTED))
  1896. }
  1897. _converse.api.listen.on('disconnected', disconnectChatRooms);
  1898. _converse.api.listen.on('statusInitialized', () => {
  1899. window.addEventListener(_converse.unloadevent, () => {
  1900. const using_websocket = _converse.api.connection.isType('websocket');
  1901. if (using_websocket &&
  1902. (!_converse.enable_smacks || !_converse.session.get('smacks_stream_id'))) {
  1903. // For non-SMACKS websocket connections, or non-resumeable
  1904. // connections, we disconnect all chatrooms when the page unloads.
  1905. // See issue #1111
  1906. disconnectChatRooms();
  1907. }
  1908. });
  1909. });
  1910. /************************ END Event Handlers ************************/
  1911. /************************ BEGIN API ************************/
  1912. // We extend the default converse.js API to add methods specific to MUC groupchats.
  1913. Object.assign(_converse.api, {
  1914. /**
  1915. * The "rooms" namespace groups methods relevant to chatrooms
  1916. * (aka groupchats).
  1917. *
  1918. * @namespace _converse.api.rooms
  1919. * @memberOf _converse.api
  1920. */
  1921. 'rooms': {
  1922. /**
  1923. * Creates a new MUC chatroom (aka groupchat)
  1924. *
  1925. * Similar to {@link _converse.api.rooms.open}, but creates
  1926. * the chatroom in the background (i.e. doesn't cause a view to open).
  1927. *
  1928. * @method _converse.api.rooms.create
  1929. * @param {(string[]|string)} jid|jids The JID or array of
  1930. * JIDs of the chatroom(s) to create
  1931. * @param {object} [attrs] attrs The room attributes
  1932. */
  1933. create (jids, attrs) {
  1934. if (_.isString(attrs)) {
  1935. attrs = {'nick': attrs};
  1936. } else if (_.isUndefined(attrs)) {
  1937. attrs = {};
  1938. }
  1939. if (_.isUndefined(attrs.maximize)) {
  1940. attrs.maximize = false;
  1941. }
  1942. if (!attrs.nick && _converse.muc_nickname_from_jid) {
  1943. attrs.nick = Strophe.getNodeFromJid(_converse.bare_jid);
  1944. }
  1945. if (_.isUndefined(jids)) {
  1946. throw new TypeError('rooms.create: You need to provide at least one JID');
  1947. } else if (_.isString(jids)) {
  1948. return createChatRoom(jids, attrs);
  1949. }
  1950. return _.map(jids, _.partial(createChatRoom, _, attrs));
  1951. },
  1952. /**
  1953. * Opens a MUC chatroom (aka groupchat)
  1954. *
  1955. * Similar to {@link _converse.api.chats.open}, but for groupchats.
  1956. *
  1957. * @method _converse.api.rooms.open
  1958. * @param {string} jid The room JID or JIDs (if not specified, all
  1959. * currently open rooms will be returned).
  1960. * @param {string} attrs A map containing any extra room attributes.
  1961. * @param {string} [attrs.nick] The current user's nickname for the MUC
  1962. * @param {boolean} [attrs.auto_configure] A boolean, indicating
  1963. * whether the room should be configured automatically or not.
  1964. * If set to `true`, then it makes sense to pass in configuration settings.
  1965. * @param {object} [attrs.roomconfig] A map of configuration settings to be used when the room gets
  1966. * configured automatically. Currently it doesn't make sense to specify
  1967. * `roomconfig` values if `auto_configure` is set to `false`.
  1968. * For a list of configuration values that can be passed in, refer to these values
  1969. * in the [XEP-0045 MUC specification](https://xmpp.org/extensions/xep-0045.html#registrar-formtype-owner).
  1970. * The values should be named without the `muc#roomconfig_` prefix.
  1971. * @param {boolean} [attrs.maximize] A boolean, indicating whether minimized rooms should also be
  1972. * maximized, when opened. Set to `false` by default.
  1973. * @param {boolean} [attrs.bring_to_foreground] A boolean indicating whether the room should be
  1974. * brought to the foreground and therefore replace the currently shown chat.
  1975. * If there is no chat currently open, then this option is ineffective.
  1976. * @param {Boolean} [force=false] - By default, a minimized
  1977. * room won't be maximized (in `overlayed` view mode) and in
  1978. * `fullscreen` view mode a newly opened room won't replace
  1979. * another chat already in the foreground.
  1980. * Set `force` to `true` if you want to force the room to be
  1981. * maximized or shown.
  1982. *
  1983. * @example
  1984. * this._converse.api.rooms.open('group@muc.example.com')
  1985. *
  1986. * @example
  1987. * // To return an array of rooms, provide an array of room JIDs:
  1988. * _converse.api.rooms.open(['group1@muc.example.com', 'group2@muc.example.com'])
  1989. *
  1990. * @example
  1991. * // To setup a custom nickname when joining the room, provide the optional nick argument:
  1992. * _converse.api.rooms.open('group@muc.example.com', {'nick': 'mycustomnick'})
  1993. *
  1994. * @example
  1995. * // For example, opening a room with a specific default configuration:
  1996. * _converse.api.rooms.open(
  1997. * 'myroom@conference.example.org',
  1998. * { 'nick': 'coolguy69',
  1999. * 'auto_configure': true,
  2000. * 'roomconfig': {
  2001. * 'changesubject': false,
  2002. * 'membersonly': true,
  2003. * 'persistentroom': true,
  2004. * 'publicroom': true,
  2005. * 'roomdesc': 'Comfy room for hanging out',
  2006. * 'whois': 'anyone'
  2007. * }
  2008. * }
  2009. * );
  2010. */
  2011. async open (jids, attrs, force=false) {
  2012. await _converse.api.waitUntil('chatBoxesFetched');
  2013. if (_.isUndefined(jids)) {
  2014. const err_msg = 'rooms.open: You need to provide at least one JID';
  2015. _converse.log(err_msg, Strophe.LogLevel.ERROR);
  2016. throw(new TypeError(err_msg));
  2017. } else if (_.isString(jids)) {
  2018. const room = _converse.api.rooms.create(jids, attrs);
  2019. if (room) {
  2020. room.maybeShow(force);
  2021. }
  2022. return room;
  2023. } else {
  2024. return _.map(jids, jid => _converse.api.rooms.create(jid, attrs).maybeShow(force));
  2025. }
  2026. },
  2027. /**
  2028. * Returns an object representing a MUC chatroom (aka groupchat)
  2029. *
  2030. * @method _converse.api.rooms.get
  2031. * @param {string} [jid] The room JID (if not specified, all rooms will be returned).
  2032. * @param {object} attrs A map containing any extra room attributes For example, if you want
  2033. * to specify the nickname, use `{'nick': 'bloodninja'}`. Previously (before
  2034. * version 1.0.7, the second parameter only accepted the nickname (as a string
  2035. * value). This is currently still accepted, but then you can't pass in any
  2036. * other room attributes. If the nickname is not specified then the node part of
  2037. * the user's JID will be used.
  2038. * @param {boolean} create A boolean indicating whether the room should be created
  2039. * if not found (default: `false`)
  2040. * @example
  2041. * _converse.api.waitUntil('roomsAutoJoined').then(() => {
  2042. * const create_if_not_found = true;
  2043. * _converse.api.rooms.get(
  2044. * 'group@muc.example.com',
  2045. * {'nick': 'dread-pirate-roberts'},
  2046. * create_if_not_found
  2047. * )
  2048. * });
  2049. */
  2050. get (jids, attrs, create) {
  2051. if (_.isString(attrs)) {
  2052. attrs = {'nick': attrs};
  2053. } else if (_.isUndefined(attrs)) {
  2054. attrs = {};
  2055. }
  2056. if (_.isUndefined(jids)) {
  2057. const result = [];
  2058. _converse.chatboxes.each(function (chatbox) {
  2059. if (chatbox.get('type') === _converse.CHATROOMS_TYPE) {
  2060. result.push(chatbox);
  2061. }
  2062. });
  2063. return result;
  2064. }
  2065. if (!attrs.nick) {
  2066. attrs.nick = Strophe.getNodeFromJid(_converse.bare_jid);
  2067. }
  2068. if (_.isString(jids)) {
  2069. return getChatRoom(jids, attrs);
  2070. }
  2071. return _.map(jids, _.partial(getChatRoom, _, attrs));
  2072. }
  2073. }
  2074. });
  2075. /************************ END API ************************/
  2076. }
  2077. });