converse-core.js 87 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977
  1. // Converse.js (A browser based XMPP chat client)
  2. // http://conversejs.org
  3. //
  4. // Copyright (c) 2012-2016, Jan-Carel Brand <jc@opkode.com>
  5. // Licensed under the Mozilla Public License (MPLv2)
  6. //
  7. /*global Backbone, define, window, document, locales */
  8. (function (root, factory) {
  9. // Two modules are loaded as dependencies.
  10. //
  11. // * **converse-dependencies**: A list of dependencies converse.js depends on.
  12. // The path to this module is in main.js and the module itself can be overridden.
  13. // * **converse-templates**: The HTML templates used by converse.js.
  14. //
  15. // The dependencies are then split up and passed into the factory function,
  16. // which contains and instantiates converse.js.
  17. define("converse-core", [
  18. "jquery",
  19. "underscore",
  20. "polyfill",
  21. "utils",
  22. "moment_with_locales",
  23. "strophe",
  24. "pluggable",
  25. "tpl!chats_panel",
  26. "strophe.disco",
  27. "backbone.browserStorage",
  28. "backbone.overview",
  29. ], factory);
  30. }(this, function (
  31. $, _, dummy, utils, moment,
  32. Strophe, pluggable, tpl_chats_panel
  33. ) {
  34. /*
  35. * Cannot use this due to Safari bug.
  36. * See https://github.com/jcbrand/converse.js/issues/196
  37. */
  38. // "use strict";
  39. // Strophe globals
  40. var $build = Strophe.$build;
  41. var $iq = Strophe.$iq;
  42. var $pres = Strophe.$pres;
  43. var b64_sha1 = Strophe.SHA1.b64_sha1;
  44. Strophe = Strophe.Strophe;
  45. // Use Mustache style syntax for variable interpolation
  46. /* Configuration of underscore templates (this config is distinct to the
  47. * config of requirejs-tpl in main.js). This one is for normal inline templates.
  48. */
  49. _.templateSettings = {
  50. evaluate : /\{\[([\s\S]+?)\]\}/g,
  51. interpolate : /\{\{([\s\S]+?)\}\}/g
  52. };
  53. // We create an object to act as the "this" context for event handlers (as
  54. // defined below and accessible via converse_api.listen).
  55. // We don't want the inner converse object to be the context, since it
  56. // contains sensitive information, and we don't want it to be something in
  57. // the DOM or window, because then anyone can trigger converse events.
  58. var event_context = {};
  59. var converse = {
  60. templates: {'chats_panel': tpl_chats_panel},
  61. emit: function (evt, data) {
  62. $(event_context).trigger(evt, data);
  63. },
  64. once: function (evt, handler, context) {
  65. if (context) {
  66. handler = handler.bind(context);
  67. }
  68. $(event_context).one(evt, handler);
  69. },
  70. on: function (evt, handler, context) {
  71. if (_.contains(['ready', 'initialized'], evt)) {
  72. converse.log('Warning: The "'+evt+'" event has been deprecated and will be removed, please use "connected".');
  73. }
  74. if (context) {
  75. handler = handler.bind(context);
  76. }
  77. $(event_context).bind(evt, handler);
  78. },
  79. off: function (evt, handler) {
  80. $(event_context).unbind(evt, handler);
  81. }
  82. };
  83. // Make converse pluggable
  84. pluggable.enable(converse, 'converse', 'pluggable');
  85. // Module-level constants
  86. converse.STATUS_WEIGHTS = {
  87. 'offline': 6,
  88. 'unavailable': 5,
  89. 'xa': 4,
  90. 'away': 3,
  91. 'dnd': 2,
  92. 'chat': 1, // We currently don't differentiate between "chat" and "online"
  93. 'online': 1
  94. };
  95. converse.ANONYMOUS = "anonymous";
  96. converse.CLOSED = 'closed';
  97. converse.EXTERNAL = "external";
  98. converse.LOGIN = "login";
  99. converse.LOGOUT = "logout";
  100. converse.OPENED = 'opened';
  101. converse.PREBIND = "prebind";
  102. var PRETTY_CONNECTION_STATUS = {
  103. 0: 'ERROR',
  104. 1: 'CONNECTING',
  105. 2: 'CONNFAIL',
  106. 3: 'AUTHENTICATING',
  107. 4: 'AUTHFAIL',
  108. 5: 'CONNECTED',
  109. 6: 'DISCONNECTED',
  110. 7: 'DISCONNECTING',
  111. 8: 'ATTACHED',
  112. 9: 'REDIRECT'
  113. };
  114. converse.log = function (txt, level) {
  115. var logger;
  116. if (typeof console === "undefined" || typeof console.log === "undefined") {
  117. logger = { log: function () {}, error: function () {} };
  118. } else {
  119. logger = console;
  120. }
  121. if (converse.debug) {
  122. if (level === 'error') {
  123. logger.log('ERROR: '+txt);
  124. } else {
  125. logger.log(txt);
  126. }
  127. }
  128. };
  129. converse.initialize = function (settings, callback) {
  130. "use strict";
  131. settings = typeof settings !== "undefined" ? settings : {};
  132. var init_deferred = new $.Deferred();
  133. var converse = this;
  134. var unloadevent;
  135. if ('onpagehide' in window) {
  136. // Pagehide gets thrown in more cases than unload. Specifically it
  137. // gets thrown when the page is cached and not just
  138. // closed/destroyed. It's the only viable event on mobile Safari.
  139. // https://www.webkit.org/blog/516/webkit-page-cache-ii-the-unload-event/
  140. unloadevent = 'pagehide';
  141. } else if ('onbeforeunload' in window) {
  142. unloadevent = 'beforeunload';
  143. } else if ('onunload' in window) {
  144. unloadevent = 'unload';
  145. }
  146. // Logging
  147. Strophe.log = function (level, msg) { converse.log(level+' '+msg, level); };
  148. Strophe.error = function (msg) { converse.log(msg, 'error'); };
  149. // Add Strophe Namespaces
  150. Strophe.addNamespace('CARBONS', 'urn:xmpp:carbons:2');
  151. Strophe.addNamespace('CHATSTATES', 'http://jabber.org/protocol/chatstates');
  152. Strophe.addNamespace('CSI', 'urn:xmpp:csi:0');
  153. Strophe.addNamespace('ROSTERX', 'http://jabber.org/protocol/rosterx');
  154. Strophe.addNamespace('XFORM', 'jabber:x:data');
  155. Strophe.addNamespace('NICK', 'http://jabber.org/protocol/nick');
  156. Strophe.addNamespace('HINTS', 'urn:xmpp:hints');
  157. Strophe.addNamespace('PUBSUB', 'http://jabber.org/protocol/pubsub');
  158. // Instance level constants
  159. this.TIMEOUTS = { // Set as module attr so that we can override in tests.
  160. 'PAUSED': 10000,
  161. 'INACTIVE': 90000
  162. };
  163. // XEP-0085 Chat states
  164. // http://xmpp.org/extensions/xep-0085.html
  165. this.INACTIVE = 'inactive';
  166. this.ACTIVE = 'active';
  167. this.COMPOSING = 'composing';
  168. this.PAUSED = 'paused';
  169. this.GONE = 'gone';
  170. // Detect support for the user's locale
  171. // ------------------------------------
  172. this.isConverseLocale = function (locale) { return typeof locales[locale] !== "undefined"; };
  173. this.isMomentLocale = function (locale) { return moment.locale() !== moment.locale(locale); };
  174. this.user_settings = settings; // Save the user settings so that they can be used by plugins
  175. this.wrappedChatBox = function (chatbox) {
  176. /* Wrap a chatbox for outside consumption (i.e. so that it can be
  177. * returned via the API.
  178. */
  179. if (!chatbox) { return; }
  180. var view = converse.chatboxviews.get(chatbox.get('id'));
  181. return {
  182. 'close': view.close.bind(view),
  183. 'focus': view.focus.bind(view),
  184. 'get': chatbox.get.bind(chatbox),
  185. 'open': view.show.bind(view),
  186. 'set': chatbox.set.bind(chatbox)
  187. };
  188. };
  189. if (!moment.locale) { //moment.lang is deprecated after 2.8.1, use moment.locale instead
  190. moment.locale = moment.lang;
  191. }
  192. moment.locale(utils.detectLocale(this.isMomentLocale));
  193. this.i18n = settings.i18n ? settings.i18n : locales[utils.detectLocale(this.isConverseLocale)] || {};
  194. // Translation machinery
  195. // ---------------------
  196. var __ = utils.__.bind(this);
  197. var DESC_GROUP_TOGGLE = __('Click to hide these contacts');
  198. // Default configuration values
  199. // ----------------------------
  200. this.default_settings = {
  201. allow_contact_requests: true,
  202. animate: true,
  203. authentication: 'login', // Available values are "login", "prebind", "anonymous" and "external".
  204. auto_away: 0, // Seconds after which user status is set to 'away'
  205. auto_login: false, // Currently only used in connection with anonymous login
  206. auto_reconnect: false,
  207. auto_subscribe: false,
  208. auto_xa: 0, // Seconds after which user status is set to 'xa'
  209. bosh_service_url: undefined, // The BOSH connection manager URL.
  210. credentials_url: null, // URL from where login credentials can be fetched
  211. csi_waiting_time: 0, // Support for XEP-0352. Seconds before client is considered idle and CSI is sent out.
  212. debug: false,
  213. default_state: 'online',
  214. expose_rid_and_sid: false,
  215. filter_by_resource: false,
  216. forward_messages: false,
  217. hide_offline_users: false,
  218. include_offline_state: false,
  219. jid: undefined,
  220. keepalive: false,
  221. locked_domain: undefined,
  222. message_carbons: false, // Support for XEP-280
  223. password: undefined,
  224. prebind: false, // XXX: Deprecated, use "authentication" instead.
  225. prebind_url: null,
  226. rid: undefined,
  227. roster_groups: false,
  228. show_only_online_users: false,
  229. sid: undefined,
  230. storage: 'session',
  231. message_storage: 'session',
  232. strict_plugin_dependencies: false,
  233. synchronize_availability: true, // Set to false to not sync with other clients or with resource name of the particular client that it should synchronize with
  234. visible_toolbar_buttons: {
  235. 'emoticons': true,
  236. 'call': false,
  237. 'clear': true,
  238. 'toggle_occupants': true
  239. },
  240. websocket_url: undefined,
  241. xhr_custom_status: false,
  242. xhr_custom_status_url: '',
  243. };
  244. _.extend(this, this.default_settings);
  245. // Allow only whitelisted configuration attributes to be overwritten
  246. _.extend(this, _.pick(settings, Object.keys(this.default_settings)));
  247. // BBB
  248. if (this.prebind === true) { this.authentication = converse.PREBIND; }
  249. if (this.authentication === converse.ANONYMOUS) {
  250. if (this.auto_login && !this.jid) {
  251. throw new Error("Config Error: you need to provide the server's " +
  252. "domain via the 'jid' option when using anonymous " +
  253. "authentication with auto_login.");
  254. }
  255. }
  256. if (settings.visible_toolbar_buttons) {
  257. _.extend(
  258. this.visible_toolbar_buttons,
  259. _.pick(settings.visible_toolbar_buttons, [
  260. 'emoticons', 'call', 'clear', 'toggle_occupants'
  261. ]
  262. ));
  263. }
  264. $.fx.off = !this.animate;
  265. // Module-level variables
  266. // ----------------------
  267. this.callback = callback || function () {};
  268. /* When reloading the page:
  269. * For new sessions, we need to send out a presence stanza to notify
  270. * the server/network that we're online.
  271. * When re-attaching to an existing session (e.g. via the keepalive
  272. * option), we don't need to again send out a presence stanza, because
  273. * it's as if "we never left" (see onConnectStatusChanged).
  274. * https://github.com/jcbrand/converse.js/issues/521
  275. */
  276. this.send_initial_presence = true;
  277. this.msg_counter = 0;
  278. // Module-level functions
  279. // ----------------------
  280. this.generateResource = function () {
  281. return '/converse.js-' + Math.floor(Math.random()*139749825).toString();
  282. };
  283. this.sendCSI = function (stat) {
  284. /* Send out a Chat Status Notification (XEP-0352) */
  285. if (converse.features[Strophe.NS.CSI] || true) {
  286. converse.connection.send($build(stat, {xmlns: Strophe.NS.CSI}));
  287. converse.inactive = (stat === converse.INACTIVE) ? true : false;
  288. }
  289. };
  290. this.onUserActivity = function () {
  291. /* Resets counters and flags relating to CSI and auto_away/auto_xa */
  292. if (converse.idle_seconds > 0) {
  293. converse.idle_seconds = 0;
  294. }
  295. if (!converse.connection.authenticated) {
  296. // We can't send out any stanzas when there's no authenticated connection.
  297. // converse can happen when the connection reconnects.
  298. return;
  299. }
  300. if (converse.inactive) {
  301. converse.sendCSI(converse.ACTIVE);
  302. }
  303. if (converse.auto_changed_status === true) {
  304. converse.auto_changed_status = false;
  305. // XXX: we should really remember the original state here, and
  306. // then set it back to that...
  307. converse.xmppstatus.setStatus(converse.default_state);
  308. }
  309. };
  310. this.onEverySecond = function () {
  311. /* An interval handler running every second.
  312. * Used for CSI and the auto_away and auto_xa features.
  313. */
  314. if (!converse.connection.authenticated) {
  315. // We can't send out any stanzas when there's no authenticated connection.
  316. // This can happen when the connection reconnects.
  317. return;
  318. }
  319. var stat = converse.xmppstatus.getStatus();
  320. converse.idle_seconds++;
  321. if (converse.csi_waiting_time > 0 &&
  322. converse.idle_seconds > converse.csi_waiting_time &&
  323. !converse.inactive) {
  324. converse.sendCSI(converse.INACTIVE);
  325. }
  326. if (converse.auto_away > 0 &&
  327. converse.idle_seconds > converse.auto_away &&
  328. stat !== 'away' && stat !== 'xa') {
  329. converse.auto_changed_status = true;
  330. converse.xmppstatus.setStatus('away');
  331. } else if (converse.auto_xa > 0 &&
  332. converse.idle_seconds > converse.auto_xa && stat !== 'xa') {
  333. converse.auto_changed_status = true;
  334. converse.xmppstatus.setStatus('xa');
  335. }
  336. };
  337. this.registerIntervalHandler = function () {
  338. /* Set an interval of one second and register a handler for it.
  339. * Required for the auto_away, auto_xa and csi_waiting_time features.
  340. */
  341. if (converse.auto_away < 1 && converse.auto_xa < 1 && converse.csi_waiting_time < 1) {
  342. // Waiting time of less then one second means features aren't used.
  343. return;
  344. }
  345. converse.idle_seconds = 0;
  346. converse.auto_changed_status = false; // Was the user's status changed by converse.js?
  347. $(window).on('click mousemove keypress focus'+unloadevent, converse.onUserActivity);
  348. converse.everySecondTrigger = window.setInterval(converse.onEverySecond, 1000);
  349. };
  350. this.giveFeedback = function (subject, klass, message) {
  351. $('.conn-feedback').each(function (idx, el) {
  352. var $el = $(el);
  353. $el.addClass('conn-feedback').text(subject);
  354. if (klass) {
  355. $el.addClass(klass);
  356. } else {
  357. $el.removeClass('error');
  358. }
  359. });
  360. converse.emit('feedback', {
  361. 'klass': klass,
  362. 'message': message,
  363. 'subject': subject
  364. });
  365. };
  366. this.rejectPresenceSubscription = function (jid, message) {
  367. /* Reject or cancel another user's subscription to our presence updates.
  368. * Parameters:
  369. * (String) jid - The Jabber ID of the user whose subscription
  370. * is being canceled.
  371. * (String) message - An optional message to the user
  372. */
  373. var pres = $pres({to: jid, type: "unsubscribed"});
  374. if (message && message !== "") { pres.c("status").t(message); }
  375. converse.connection.send(pres);
  376. };
  377. this.reconnect = _.debounce(function (condition) {
  378. converse.connection.reconnecting = true;
  379. converse.connection.disconnect('re-connecting');
  380. converse.connection.reset();
  381. converse.log('The connection has dropped, attempting to reconnect.');
  382. converse.giveFeedback(
  383. __("Reconnecting"), 'warn', __('The connection has dropped, attempting to reconnect.')
  384. );
  385. converse.clearSession();
  386. converse._tearDown();
  387. if (converse.authentication !== "prebind") {
  388. converse.autoLogin();
  389. } else if (converse.prebind_url) {
  390. converse.startNewBOSHSession();
  391. }
  392. }, 1000);
  393. this.onDisconnected = function (condition) {
  394. if (converse.disconnection_cause !== converse.LOGOUT) {
  395. if (converse.disconnection_cause === Strophe.Status.CONNFAIL && converse.auto_reconnect) {
  396. converse.reconnect(condition);
  397. converse.log('RECONNECTING');
  398. return 'reconnecting';
  399. } else if (
  400. (converse.disconnection_cause === Strophe.Status.DISCONNECTING ||
  401. converse.disconnection_cause === Strophe.Status.DISCONNECTED) &&
  402. converse.auto_reconnect) {
  403. window.setTimeout(_.partial(converse.reconnect, condition), 3000);
  404. converse.log('RECONNECTING IN 3 SECONDS');
  405. return 'reconnecting';
  406. }
  407. }
  408. delete converse.connection.reconnecting;
  409. converse._tearDown();
  410. converse.emit('disconnected');
  411. converse.log('DISCONNECTED');
  412. return 'disconnected';
  413. };
  414. this.setDisconnectionCause = function (connection_status) {
  415. if (typeof converse.disconnection_cause === "undefined") {
  416. converse.disconnection_cause = connection_status;
  417. }
  418. };
  419. this.onConnectStatusChanged = function (status, condition) {
  420. converse.log("Status changed to: "+PRETTY_CONNECTION_STATUS[status]);
  421. if (status === Strophe.Status.CONNECTED || status === Strophe.Status.ATTACHED) {
  422. // By default we always want to send out an initial presence stanza.
  423. converse.send_initial_presence = true;
  424. delete converse.disconnection_cause;
  425. if (converse.connection.reconnecting) {
  426. converse.log(status === Strophe.Status.CONNECTED ? 'Reconnected' : 'Reattached');
  427. converse.onReconnected();
  428. } else {
  429. converse.log(status === Strophe.Status.CONNECTED ? 'Connected' : 'Attached');
  430. if (converse.connection.restored) {
  431. // No need to send an initial presence stanza when
  432. // we're restoring an existing session.
  433. converse.send_initial_presence = false;
  434. }
  435. converse.onConnected();
  436. }
  437. } else if (status === Strophe.Status.DISCONNECTED) {
  438. converse.setDisconnectionCause(status);
  439. converse.onDisconnected(condition);
  440. if (status === Strophe.Status.DISCONNECTING && condition) {
  441. converse.giveFeedback(
  442. __("Disconnected"), 'warn',
  443. __("The connection to the chat server has dropped")
  444. );
  445. }
  446. } else if (status === Strophe.Status.ERROR) {
  447. converse.giveFeedback(
  448. __('Connection error'), 'error',
  449. __('An error occurred while connecting to the chat server.')
  450. );
  451. } else if (status === Strophe.Status.CONNECTING) {
  452. converse.giveFeedback(__('Connecting'));
  453. } else if (status === Strophe.Status.AUTHENTICATING) {
  454. converse.giveFeedback(__('Authenticating'));
  455. } else if (status === Strophe.Status.AUTHFAIL) {
  456. converse.giveFeedback(__('Authentication failed.'), 'error');
  457. converse.connection.disconnect(__('Authentication Failed'));
  458. converse.disconnection_cause = Strophe.Status.AUTHFAIL;
  459. } else if (status === Strophe.Status.CONNFAIL ||
  460. status === Strophe.Status.DISCONNECTING) {
  461. converse.setDisconnectionCause(status);
  462. }
  463. };
  464. this.updateMsgCounter = function () {
  465. if (this.msg_counter > 0) {
  466. if (document.title.search(/^Messages \(\d+\) /) === -1) {
  467. document.title = "Messages (" + this.msg_counter + ") " + document.title;
  468. } else {
  469. document.title = document.title.replace(/^Messages \(\d+\) /, "Messages (" + this.msg_counter + ") ");
  470. }
  471. } else if (document.title.search(/^Messages \(\d+\) /) !== -1) {
  472. document.title = document.title.replace(/^Messages \(\d+\) /, "");
  473. }
  474. };
  475. this.incrementMsgCounter = function () {
  476. this.msg_counter += 1;
  477. this.updateMsgCounter();
  478. };
  479. this.clearMsgCounter = function () {
  480. this.msg_counter = 0;
  481. this.updateMsgCounter();
  482. };
  483. this.initStatus = function () {
  484. var deferred = new $.Deferred();
  485. this.xmppstatus = new this.XMPPStatus();
  486. var id = b64_sha1('converse.xmppstatus-'+converse.bare_jid);
  487. this.xmppstatus.id = id; // Appears to be necessary for backbone.browserStorage
  488. this.xmppstatus.browserStorage = new Backbone.BrowserStorage[converse.storage](id);
  489. this.xmppstatus.fetch({
  490. success: deferred.resolve,
  491. error: deferred.resolve
  492. });
  493. converse.emit('statusInitialized');
  494. return deferred.promise();
  495. };
  496. this.initSession = function () {
  497. this.session = new this.Session();
  498. var id = b64_sha1('converse.bosh-session');
  499. this.session.id = id; // Appears to be necessary for backbone.browserStorage
  500. this.session.browserStorage = new Backbone.BrowserStorage[converse.storage](id);
  501. this.session.fetch();
  502. };
  503. this.clearSession = function () {
  504. if (this.roster) {
  505. this.roster.browserStorage._clear();
  506. }
  507. this.session.browserStorage._clear();
  508. };
  509. this.logOut = function () {
  510. converse.disconnection_cause = converse.LOGOUT;
  511. converse.chatboxviews.closeAllChatBoxes();
  512. converse.clearSession();
  513. if (typeof converse.connection !== 'undefined') {
  514. converse.connection.disconnect();
  515. converse.connection.reset();
  516. }
  517. };
  518. this.saveWindowState = function (ev, hidden) {
  519. // XXX: eventually we should be able to just use
  520. // document.visibilityState (when we drop support for older
  521. // browsers).
  522. var state;
  523. var v = "visible", h = "hidden",
  524. event_map = {
  525. 'focus': v,
  526. 'focusin': v,
  527. 'pageshow': v,
  528. 'blur': h,
  529. 'focusout': h,
  530. 'pagehide': h
  531. };
  532. ev = ev || document.createEvent('Events');
  533. if (ev.type in event_map) {
  534. state = event_map[ev.type];
  535. } else {
  536. state = document[hidden] ? "hidden" : "visible";
  537. }
  538. if (state === 'visible') {
  539. converse.clearMsgCounter();
  540. }
  541. converse.windowState = state;
  542. };
  543. this.registerGlobalEventHandlers = function () {
  544. // Taken from:
  545. // http://stackoverflow.com/questions/1060008/is-there-a-way-to-detect-if-a-browser-window-is-not-currently-active
  546. var hidden = "hidden";
  547. // Standards:
  548. if (hidden in document) {
  549. document.addEventListener("visibilitychange", _.partial(converse.saveWindowState, _, hidden));
  550. } else if ((hidden = "mozHidden") in document) {
  551. document.addEventListener("mozvisibilitychange", _.partial(converse.saveWindowState, _, hidden));
  552. } else if ((hidden = "webkitHidden") in document) {
  553. document.addEventListener("webkitvisibilitychange", _.partial(converse.saveWindowState, _, hidden));
  554. } else if ((hidden = "msHidden") in document) {
  555. document.addEventListener("msvisibilitychange", _.partial(converse.saveWindowState, _, hidden));
  556. } else if ("onfocusin" in document) {
  557. // IE 9 and lower:
  558. document.onfocusin = document.onfocusout = _.partial(converse.saveWindowState, _, hidden);
  559. } else {
  560. // All others:
  561. window.onpageshow = window.onpagehide = window.onfocus = window.onblur = _.partial(converse.saveWindowState, _, hidden);
  562. }
  563. // set the initial state (but only if browser supports the Page Visibility API)
  564. if( document[hidden] !== undefined ) {
  565. _.partial(converse.saveWindowState, _, hidden)({type: document[hidden] ? "blur" : "focus"});
  566. }
  567. };
  568. this.afterReconnected = function () {
  569. this.registerPresenceHandler();
  570. this.chatboxes.registerMessageHandler();
  571. this.xmppstatus.sendPresence();
  572. this.giveFeedback(__('Contacts'));
  573. };
  574. this.onReconnected = function () {
  575. // We need to re-register all the event handlers on the newly
  576. // created connection.
  577. var deferred = new $.Deferred();
  578. converse.initStatus().done(function () {
  579. converse.afterReconnected();
  580. deferred.resolve();
  581. });
  582. converse.emit('reconnected');
  583. return deferred.promise();
  584. };
  585. this.enableCarbons = function () {
  586. /* Ask the XMPP server to enable Message Carbons
  587. * See XEP-0280 https://xmpp.org/extensions/xep-0280.html#enabling
  588. */
  589. if (!this.message_carbons || this.session.get('carbons_enabled')) {
  590. return;
  591. }
  592. var carbons_iq = new Strophe.Builder('iq', {
  593. from: this.connection.jid,
  594. id: 'enablecarbons',
  595. type: 'set'
  596. })
  597. .c('enable', {xmlns: Strophe.NS.CARBONS});
  598. this.connection.addHandler(function (iq) {
  599. if ($(iq).find('error').length > 0) {
  600. converse.log('ERROR: An error occured while trying to enable message carbons.');
  601. } else {
  602. this.session.save({carbons_enabled: true});
  603. converse.log('Message carbons have been enabled.');
  604. }
  605. }.bind(this), null, "iq", null, "enablecarbons");
  606. this.connection.send(carbons_iq);
  607. };
  608. this.initRoster = function () {
  609. converse.roster = new converse.RosterContacts();
  610. converse.roster.browserStorage = new Backbone.BrowserStorage.session(
  611. b64_sha1('converse.contacts-'+converse.bare_jid));
  612. converse.rostergroups = new converse.RosterGroups();
  613. converse.rostergroups.browserStorage = new Backbone.BrowserStorage.session(
  614. b64_sha1('converse.roster.groups'+converse.bare_jid));
  615. };
  616. this.populateRoster = function () {
  617. /* Fetch all the roster groups, and then the roster contacts.
  618. * Emit an event after fetching is done in each case.
  619. */
  620. converse.rostergroups.fetchRosterGroups().then(function () {
  621. converse.emit('rosterGroupsFetched');
  622. converse.roster.fetchRosterContacts().then(function () {
  623. converse.emit('rosterContactsFetched');
  624. converse.sendInitialPresence();
  625. });
  626. });
  627. };
  628. this.unregisterPresenceHandler = function () {
  629. if (typeof converse.presence_ref !== 'undefined') {
  630. converse.connection.deleteHandler(converse.presence_ref);
  631. delete converse.presence_ref;
  632. }
  633. };
  634. this.registerPresenceHandler = function () {
  635. converse.unregisterPresenceHandler();
  636. converse.presence_ref = converse.connection.addHandler(
  637. function (presence) {
  638. converse.roster.presenceHandler(presence);
  639. return true;
  640. }, null, 'presence', null);
  641. };
  642. this.sendInitialPresence = function () {
  643. if (converse.send_initial_presence) {
  644. converse.xmppstatus.sendPresence();
  645. }
  646. };
  647. this.onStatusInitialized = function () {
  648. this.registerIntervalHandler();
  649. this.initRoster();
  650. this.populateRoster();
  651. this.chatboxes.onConnected();
  652. this.registerPresenceHandler();
  653. this.giveFeedback(__('Contacts'));
  654. if (typeof this.callback === 'function') {
  655. // XXX: Deprecate in favor of init_deferred
  656. this.callback();
  657. }
  658. if (converse.connection.service === 'jasmine tests') {
  659. init_deferred.resolve(converse);
  660. } else {
  661. init_deferred.resolve();
  662. }
  663. converse.emit('initialized');
  664. };
  665. this.onConnected = function (callback) {
  666. // When reconnecting, there might be some open chat boxes. We don't
  667. // know whether these boxes are of the same account or not, so we
  668. // close them now.
  669. // XXX: ran into an issue where a returned PubSub BOSH response was
  670. // not received by the browser. The solution was to flush the
  671. // connection early on. I don't know what the underlying cause of
  672. // this issue is, and whether it's a Strophe.js or Prosody bug.
  673. // My suspicion is that Prosody replies to an invalid/expired
  674. // Request, which is why the browser then doesn't receive it.
  675. // In any case, flushing here (sending out a new BOSH request)
  676. // solves the problem.
  677. converse.connection.flush();
  678. /* Called as soon as a new connection has been established, either
  679. * by logging in or by attaching to an existing BOSH session.
  680. */
  681. this.chatboxviews.closeAllChatBoxes();
  682. this.jid = this.connection.jid;
  683. this.bare_jid = Strophe.getBareJidFromJid(this.connection.jid);
  684. this.resource = Strophe.getResourceFromJid(this.connection.jid);
  685. this.domain = Strophe.getDomainFromJid(this.connection.jid);
  686. this.features = new this.Features();
  687. this.enableCarbons();
  688. this.initStatus().done(_.bind(this.onStatusInitialized, this));
  689. converse.emit('connected');
  690. converse.emit('ready'); // BBB: Will be removed.
  691. };
  692. this.RosterContact = Backbone.Model.extend({
  693. initialize: function (attributes, options) {
  694. var jid = attributes.jid;
  695. var bare_jid = Strophe.getBareJidFromJid(jid);
  696. var resource = Strophe.getResourceFromJid(jid);
  697. attributes.jid = bare_jid;
  698. this.set(_.extend({
  699. 'id': bare_jid,
  700. 'jid': bare_jid,
  701. 'fullname': bare_jid,
  702. 'chat_status': 'offline',
  703. 'user_id': Strophe.getNodeFromJid(jid),
  704. 'resources': resource ? [resource] : [],
  705. 'groups': [],
  706. 'image_type': 'image/png',
  707. 'image': "iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAIAAABt+uBvAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gwHCy455JBsggAABkJJREFUeNrtnM1PE1sUwHvvTD8otWLHST/Gimi1CEgr6M6FEWuIBo2pujDVsNDEP8GN/4MbN7oxrlipG2OCgZgYlxAbkRYw1KqkIDRCSkM7nXvvW8x7vjyNeQ9m7p1p3z1LQk/v/Dhz7vkEXL161cHl9wI5Ag6IA+KAOCAOiAPigDggLhwQB2S+iNZ+PcYY/SWEEP2HAAAIoSAIoihCCP+ngDDGtVotGAz29/cfOXJEUZSOjg6n06lp2sbGRqlUWlhYyGazS0tLbrdbEASrzgksyeYJId3d3el0uqenRxRFAAAA4KdfIIRgjD9+/Pj8+fOpqSndslofEIQwHA6Pjo4mEon//qmFhYXHjx8vLi4ihBgDEnp7e9l8E0Jo165dQ0NDd+/eDYVC2/qsJElDQ0OEkKWlpa2tLZamxAhQo9EIBoOjo6MXL17csZLe3l5FUT59+lQul5l5JRaAVFWNRqN37tw5ceKEQVWRSOTw4cOFQuHbt2+iKLYCIISQLMu3b99OJpOmKAwEAgcPHszn8+vr6wzsiG6UQQhxuVyXLl0aGBgwUW0sFstkMl6v90fo1KyAMMYDAwPnzp0zXfPg4GAqlWo0Gk0MiBAiy/L58+edTqf5Aa4onj59OhaLYYybFRCEMBaL0fNxBw4cSCQStN0QRUBut3t4eJjq6U+dOiVJElVPRBFQIBDo6+ujCqirqyscDlONGykC2lYyYSR6pBoQQapHZwAoHo/TuARYAOrs7GQASFEUqn6aIiBJkhgA6ujooFpUo6iaTa7koFwnaoWadLNe81tbWwzoaJrWrICWl5cZAFpbW6OabVAEtLi4yABQsVjUNK0pAWWzWQaAcrlcswKanZ1VVZUqHYRQEwOq1Wpv3ryhCmh6erpcLjdrNl+v1ycnJ+l5UELI27dvv3//3qxxEADgy5cvExMT9Mznw4cPtFtAdAPFarU6Pj5eKpVM17yxsfHy5cvV1VXazXu62gVBKBQKT58+rdVqJqrFGL948eLdu3dU8/g/H4FBUaJYLAqC0NPTY9brMD4+PjY25mDSracOCABACJmZmXE6nUePHjWu8NWrV48ePSKEsGlAs7Agfd5nenq6Wq0mk0kjDzY2NvbkyRMIIbP2PLvhBUEQ8vl8NpuNx+M+n29bzhVjvLKycv/+/YmJCcazQuwA6YzW1tYmJyf1SY+2trZ/rRk1Go1SqfT69esHDx4UCgVmNaa/zZ/9ABUhRFXVYDB48uTJeDweiUQkSfL7/T9MA2NcqVTK5fLy8vL8/PzU1FSxWHS5XJaM4wGr9sUwxqqqer3eUCgkSZJuUBBCfTRvc3OzXC6vrKxUKhWn02nhCJ5lM4oQQo/HgxD6+vXr58+fHf8sDOp+HQDg8XgclorFU676dKLlo6yWRdItIBwQB8QBcUCtfosRQjRNQwhhjPUC4w46WXryBSHU1zgEQWBz99EFhDGu1+t+v//48ePxeFxRlD179ng8nh0Efgiher2+vr6ur3HMzMysrq7uTJVdACGEurq6Ll++nEgkPB7Pj9jPoDHqOxyqqubz+WfPnuVyuV9XPeyeagAAAoHArVu3BgcHab8CuVzu4cOHpVKJUnfA5GweY+xyuc6cOXPv3r1IJMLAR8iyPDw8XK/Xi8Wiqqqmm5KZgBBC7e3tN27cuHbtGuPVpf7+/lAoNDs7W61WzfVKpgHSSzw3b95MpVKW3MfRaDQSiczNzVUqFRMZmQOIEOL1eq9fv3727FlL1t50URRFluX5+flqtWpWEGAOIFEUU6nUlStXLKSjy759+xwOx9zcnKZpphzGHMzhcDiTydgk9r1w4YIp7RPTAAmCkMlk2FeLf/tIEKbTab/fbwtAhJBoNGrutpNx6e7uPnTokC1eMU3T0um0DZPMkZER6wERQnw+n/FFSxpy7Nix3bt3WwwIIcRgIWnHkkwmjecfRgGx7DtuV/r6+iwGhDHev3+/bQF1dnYaH6E2CkiWZdsC2rt3r8WAHA5HW1ubbQGZcjajgOwTH/4qNko1Wlg4IA6IA+KAOKBWBUQIsfNojyliKIoRRfH9+/dut9umf3wzpoUNNQ4BAJubmwz+ic+OxefzWWlBhJD29nbug7iT5sIBcUAcEAfEAXFAHBAHxOVn+QMrmWpuPZx12gAAAABJRU5ErkJggg==",
  708. 'status': ''
  709. }, attributes));
  710. this.on('destroy', function () { this.removeFromRoster(); }.bind(this));
  711. this.on('change:chat_status', function (item) {
  712. converse.emit('contactStatusChanged', item.attributes);
  713. });
  714. },
  715. subscribe: function (message) {
  716. /* Send a presence subscription request to this roster contact
  717. *
  718. * Parameters:
  719. * (String) message - An optional message to explain the
  720. * reason for the subscription request.
  721. */
  722. this.save('ask', "subscribe"); // ask === 'subscribe' Means we have ask to subscribe to them.
  723. var pres = $pres({to: this.get('jid'), type: "subscribe"});
  724. if (message && message !== "") {
  725. pres.c("status").t(message).up();
  726. }
  727. var nick = converse.xmppstatus.get('fullname');
  728. if (nick && nick !== "") {
  729. pres.c('nick', {'xmlns': Strophe.NS.NICK}).t(nick).up();
  730. }
  731. converse.connection.send(pres);
  732. return this;
  733. },
  734. ackSubscribe: function () {
  735. /* Upon receiving the presence stanza of type "subscribed",
  736. * the user SHOULD acknowledge receipt of that subscription
  737. * state notification by sending a presence stanza of type
  738. * "subscribe" to the contact
  739. */
  740. converse.connection.send($pres({
  741. 'type': 'subscribe',
  742. 'to': this.get('jid')
  743. }));
  744. },
  745. ackUnsubscribe: function (jid) {
  746. /* Upon receiving the presence stanza of type "unsubscribed",
  747. * the user SHOULD acknowledge receipt of that subscription state
  748. * notification by sending a presence stanza of type "unsubscribe"
  749. * this step lets the user's server know that it MUST no longer
  750. * send notification of the subscription state change to the user.
  751. * Parameters:
  752. * (String) jid - The Jabber ID of the user who is unsubscribing
  753. */
  754. converse.connection.send($pres({'type': 'unsubscribe', 'to': this.get('jid')}));
  755. this.destroy(); // Will cause removeFromRoster to be called.
  756. },
  757. unauthorize: function (message) {
  758. /* Unauthorize this contact's presence subscription
  759. * Parameters:
  760. * (String) message - Optional message to send to the person being unauthorized
  761. */
  762. converse.rejectPresenceSubscription(this.get('jid'), message);
  763. return this;
  764. },
  765. authorize: function (message) {
  766. /* Authorize presence subscription
  767. * Parameters:
  768. * (String) message - Optional message to send to the person being authorized
  769. */
  770. var pres = $pres({to: this.get('jid'), type: "subscribed"});
  771. if (message && message !== "") {
  772. pres.c("status").t(message);
  773. }
  774. converse.connection.send(pres);
  775. return this;
  776. },
  777. removeResource: function (resource) {
  778. var resources = this.get('resources'), idx;
  779. if (resource) {
  780. idx = _.indexOf(resources, resource);
  781. if (idx !== -1) {
  782. resources.splice(idx, 1);
  783. this.save({'resources': resources});
  784. }
  785. }
  786. else {
  787. // if there is no resource (resource is null), it probably
  788. // means that the user is now completely offline. To make sure
  789. // that there isn't any "ghost" resources left, we empty the array
  790. this.save({'resources': []});
  791. return 0;
  792. }
  793. return resources.length;
  794. },
  795. removeFromRoster: function (callback) {
  796. /* Instruct the XMPP server to remove this contact from our roster
  797. * Parameters:
  798. * (Function) callback
  799. */
  800. var iq = $iq({type: 'set'})
  801. .c('query', {xmlns: Strophe.NS.ROSTER})
  802. .c('item', {jid: this.get('jid'), subscription: "remove"});
  803. converse.connection.sendIQ(iq, callback, callback);
  804. return this;
  805. }
  806. });
  807. this.RosterContacts = Backbone.Collection.extend({
  808. model: converse.RosterContact,
  809. comparator: function (contact1, contact2) {
  810. var name1, name2;
  811. var status1 = contact1.get('chat_status') || 'offline';
  812. var status2 = contact2.get('chat_status') || 'offline';
  813. if (converse.STATUS_WEIGHTS[status1] === converse.STATUS_WEIGHTS[status2]) {
  814. name1 = contact1.get('fullname').toLowerCase();
  815. name2 = contact2.get('fullname').toLowerCase();
  816. return name1 < name2 ? -1 : (name1 > name2? 1 : 0);
  817. } else {
  818. return converse.STATUS_WEIGHTS[status1] < converse.STATUS_WEIGHTS[status2] ? -1 : 1;
  819. }
  820. },
  821. fetchRosterContacts: function () {
  822. /* Fetches the roster contacts, first by trying the
  823. * sessionStorage cache, and if that's empty, then by querying
  824. * the XMPP server.
  825. *
  826. * Returns a promise which resolves once the contacts have been
  827. * fetched.
  828. */
  829. var deferred = new $.Deferred();
  830. this.fetch({
  831. add: true,
  832. success: function (collection) {
  833. if (collection.length === 0) {
  834. /* We don't have any roster contacts stored in sessionStorage,
  835. * so lets fetch the roster from the XMPP server. We pass in
  836. * 'sendPresence' as callback method, because after initially
  837. * fetching the roster we are ready to receive presence
  838. * updates from our contacts.
  839. */
  840. converse.send_initial_presence = true;
  841. converse.roster.fetchFromServer(deferred.resolve);
  842. } else {
  843. converse.emit('cachedRoster', collection);
  844. deferred.resolve();
  845. }
  846. }
  847. });
  848. return deferred.promise();
  849. },
  850. subscribeToSuggestedItems: function (msg) {
  851. $(msg).find('item').each(function (i, items) {
  852. if (this.getAttribute('action') === 'add') {
  853. converse.roster.addAndSubscribe(
  854. this.getAttribute('jid'), null, converse.xmppstatus.get('fullname'));
  855. }
  856. });
  857. return true;
  858. },
  859. isSelf: function (jid) {
  860. return (Strophe.getBareJidFromJid(jid) === Strophe.getBareJidFromJid(converse.connection.jid));
  861. },
  862. addAndSubscribe: function (jid, name, groups, message, attributes) {
  863. /* Add a roster contact and then once we have confirmation from
  864. * the XMPP server we subscribe to that contact's presence updates.
  865. * Parameters:
  866. * (String) jid - The Jabber ID of the user being added and subscribed to.
  867. * (String) name - The name of that user
  868. * (Array of Strings) groups - Any roster groups the user might belong to
  869. * (String) message - An optional message to explain the
  870. * reason for the subscription request.
  871. * (Object) attributes - Any additional attributes to be stored on the user's model.
  872. */
  873. this.addContact(jid, name, groups, attributes).done(function (contact) {
  874. if (contact instanceof converse.RosterContact) {
  875. contact.subscribe(message);
  876. }
  877. });
  878. },
  879. sendContactAddIQ: function (jid, name, groups, callback, errback) {
  880. /* Send an IQ stanza to the XMPP server to add a new roster contact.
  881. *
  882. * Parameters:
  883. * (String) jid - The Jabber ID of the user being added
  884. * (String) name - The name of that user
  885. * (Array of Strings) groups - Any roster groups the user might belong to
  886. * (Function) callback - A function to call once the IQ is returned
  887. * (Function) errback - A function to call if an error occured
  888. */
  889. name = _.isEmpty(name)? jid: name;
  890. var iq = $iq({type: 'set'})
  891. .c('query', {xmlns: Strophe.NS.ROSTER})
  892. .c('item', { jid: jid, name: name });
  893. _.map(groups, function (group) { iq.c('group').t(group).up(); });
  894. converse.connection.sendIQ(iq, callback, errback);
  895. },
  896. addContact: function (jid, name, groups, attributes) {
  897. /* Adds a RosterContact instance to converse.roster and
  898. * registers the contact on the XMPP server.
  899. * Returns a promise which is resolved once the XMPP server has
  900. * responded.
  901. *
  902. * Parameters:
  903. * (String) jid - The Jabber ID of the user being added and subscribed to.
  904. * (String) name - The name of that user
  905. * (Array of Strings) groups - Any roster groups the user might belong to
  906. * (Object) attributes - Any additional attributes to be stored on the user's model.
  907. */
  908. var deferred = new $.Deferred();
  909. groups = groups || [];
  910. name = _.isEmpty(name)? jid: name;
  911. this.sendContactAddIQ(jid, name, groups,
  912. function (iq) {
  913. var contact = this.create(_.extend({
  914. ask: undefined,
  915. fullname: name,
  916. groups: groups,
  917. jid: jid,
  918. requesting: false,
  919. subscription: 'none'
  920. }, attributes), {sort: false});
  921. deferred.resolve(contact);
  922. }.bind(this),
  923. function (err) {
  924. alert(__("Sorry, there was an error while trying to add "+name+" as a contact."));
  925. converse.log(err);
  926. deferred.resolve(err);
  927. }
  928. );
  929. return deferred.promise();
  930. },
  931. addResource: function (bare_jid, resource) {
  932. var item = this.get(bare_jid),
  933. resources;
  934. if (item) {
  935. resources = item.get('resources');
  936. if (resources) {
  937. if (_.indexOf(resources, resource) === -1) {
  938. resources.push(resource);
  939. item.set({'resources': resources});
  940. }
  941. } else {
  942. item.set({'resources': [resource]});
  943. }
  944. }
  945. },
  946. subscribeBack: function (bare_jid) {
  947. var contact = this.get(bare_jid);
  948. if (contact instanceof converse.RosterContact) {
  949. contact.authorize().subscribe();
  950. } else {
  951. // Can happen when a subscription is retried or roster was deleted
  952. this.addContact(bare_jid, '', [], { 'subscription': 'from' }).done(function (contact) {
  953. if (contact instanceof converse.RosterContact) {
  954. contact.authorize().subscribe();
  955. }
  956. });
  957. }
  958. },
  959. getNumOnlineContacts: function () {
  960. var count = 0,
  961. ignored = ['offline', 'unavailable'],
  962. models = this.models,
  963. models_length = models.length,
  964. i;
  965. if (converse.show_only_online_users) {
  966. ignored = _.union(ignored, ['dnd', 'xa', 'away']);
  967. }
  968. for (i=0; i<models_length; i++) {
  969. if (_.indexOf(ignored, models[i].get('chat_status')) === -1) {
  970. count++;
  971. }
  972. }
  973. return count;
  974. },
  975. onRosterPush: function (iq) {
  976. /* Handle roster updates from the XMPP server.
  977. * See: https://xmpp.org/rfcs/rfc6121.html#roster-syntax-actions-push
  978. *
  979. * Parameters:
  980. * (XMLElement) IQ - The IQ stanza received from the XMPP server.
  981. */
  982. var id = iq.getAttribute('id');
  983. var from = iq.getAttribute('from');
  984. if (from && from !== "" && Strophe.getBareJidFromJid(from) !== converse.bare_jid) {
  985. // Receiving client MUST ignore stanza unless it has no from or from = user's bare JID.
  986. // XXX: Some naughty servers apparently send from a full
  987. // JID so we need to explicitly compare bare jids here.
  988. // https://github.com/jcbrand/converse.js/issues/493
  989. converse.connection.send(
  990. $iq({type: 'error', id: id, from: converse.connection.jid})
  991. .c('error', {'type': 'cancel'})
  992. .c('service-unavailable', {'xmlns': Strophe.NS.ROSTER })
  993. );
  994. return true;
  995. }
  996. converse.connection.send($iq({type: 'result', id: id, from: converse.connection.jid}));
  997. $(iq).children('query').find('item').each(function (idx, item) {
  998. this.updateContact(item);
  999. }.bind(this));
  1000. converse.emit('rosterPush', iq);
  1001. return true;
  1002. },
  1003. fetchFromServer: function (callback) {
  1004. /* Get the roster from the XMPP server */
  1005. var iq = $iq({type: 'get', 'id': converse.connection.getUniqueId('roster')})
  1006. .c('query', {xmlns: Strophe.NS.ROSTER});
  1007. return converse.connection.sendIQ(iq, function () {
  1008. this.onReceivedFromServer.apply(this, arguments);
  1009. callback.apply(this, arguments);
  1010. }.bind(this));
  1011. },
  1012. onReceivedFromServer: function (iq) {
  1013. /* An IQ stanza containing the roster has been received from
  1014. * the XMPP server.
  1015. */
  1016. $(iq).children('query').find('item').each(function (idx, item) {
  1017. this.updateContact(item);
  1018. }.bind(this));
  1019. converse.emit('roster', iq);
  1020. },
  1021. updateContact: function (item) {
  1022. /* Update or create RosterContact models based on items
  1023. * received in the IQ from the server.
  1024. */
  1025. var jid = item.getAttribute('jid');
  1026. if (this.isSelf(jid)) { return; }
  1027. var groups = [],
  1028. contact = this.get(jid),
  1029. ask = item.getAttribute("ask"),
  1030. subscription = item.getAttribute("subscription");
  1031. $.map(item.getElementsByTagName('group'), function (group) {
  1032. groups.push(Strophe.getText(group));
  1033. });
  1034. if (!contact) {
  1035. if ((subscription === "none" && ask === null) || (subscription === "remove")) {
  1036. return; // We're lazy when adding contacts.
  1037. }
  1038. this.create({
  1039. ask: ask,
  1040. fullname: item.getAttribute("name") || jid,
  1041. groups: groups,
  1042. jid: jid,
  1043. subscription: subscription
  1044. }, {sort: false});
  1045. } else {
  1046. if (subscription === "remove") {
  1047. return contact.destroy(); // will trigger removeFromRoster
  1048. }
  1049. // We only find out about requesting contacts via the
  1050. // presence handler, so if we receive a contact
  1051. // here, we know they aren't requesting anymore.
  1052. // see docs/DEVELOPER.rst
  1053. contact.save({
  1054. subscription: subscription,
  1055. ask: ask,
  1056. requesting: null,
  1057. groups: groups
  1058. });
  1059. }
  1060. },
  1061. createRequestingContact: function (presence) {
  1062. /* Creates a Requesting Contact.
  1063. *
  1064. * Note: this method gets completely overridden by converse-vcard.js
  1065. */
  1066. var bare_jid = Strophe.getBareJidFromJid(presence.getAttribute('from'));
  1067. var nick = $(presence).children('nick[xmlns='+Strophe.NS.NICK+']').text();
  1068. var user_data = {
  1069. jid: bare_jid,
  1070. subscription: 'none',
  1071. ask: null,
  1072. requesting: true,
  1073. fullname: nick || bare_jid,
  1074. };
  1075. this.create(user_data);
  1076. converse.emit('contactRequest', user_data);
  1077. },
  1078. handleIncomingSubscription: function (presence) {
  1079. var jid = presence.getAttribute('from');
  1080. var bare_jid = Strophe.getBareJidFromJid(jid);
  1081. var contact = this.get(bare_jid);
  1082. if (!converse.allow_contact_requests) {
  1083. converse.rejectPresenceSubscription(
  1084. jid,
  1085. __("This client does not allow presence subscriptions")
  1086. );
  1087. }
  1088. if (converse.auto_subscribe) {
  1089. if ((!contact) || (contact.get('subscription') !== 'to')) {
  1090. this.subscribeBack(bare_jid);
  1091. } else {
  1092. contact.authorize();
  1093. }
  1094. } else {
  1095. if (contact) {
  1096. if (contact.get('subscription') !== 'none') {
  1097. contact.authorize();
  1098. } else if (contact.get('ask') === "subscribe") {
  1099. contact.authorize();
  1100. }
  1101. } else if (!contact) {
  1102. this.createRequestingContact(presence);
  1103. }
  1104. }
  1105. },
  1106. presenceHandler: function (presence) {
  1107. var $presence = $(presence),
  1108. presence_type = presence.getAttribute('type');
  1109. if (presence_type === 'error') { return true; }
  1110. var jid = presence.getAttribute('from'),
  1111. bare_jid = Strophe.getBareJidFromJid(jid),
  1112. resource = Strophe.getResourceFromJid(jid),
  1113. chat_status = $presence.find('show').text() || 'online',
  1114. status_message = $presence.find('status'),
  1115. contact = this.get(bare_jid);
  1116. if (this.isSelf(bare_jid)) {
  1117. if ((converse.connection.jid !== jid) &&
  1118. (presence_type !== 'unavailable') &&
  1119. (converse.synchronize_availability === true ||
  1120. converse.synchronize_availability === resource)) {
  1121. // Another resource has changed its status and
  1122. // synchronize_availability option set to update,
  1123. // we'll update ours as well.
  1124. converse.xmppstatus.save({'status': chat_status});
  1125. if (status_message.length) {
  1126. converse.xmppstatus.save({
  1127. 'status_message': status_message.text()
  1128. });
  1129. }
  1130. }
  1131. return;
  1132. } else if (($presence.find('x').attr('xmlns') || '').indexOf(Strophe.NS.MUC) === 0) {
  1133. return; // Ignore MUC
  1134. }
  1135. if (contact && (status_message.text() !== contact.get('status'))) {
  1136. contact.save({'status': status_message.text()});
  1137. }
  1138. if (presence_type === 'subscribed' && contact) {
  1139. contact.ackSubscribe();
  1140. } else if (presence_type === 'unsubscribed' && contact) {
  1141. contact.ackUnsubscribe();
  1142. } else if (presence_type === 'unsubscribe') {
  1143. return;
  1144. } else if (presence_type === 'subscribe') {
  1145. this.handleIncomingSubscription(presence);
  1146. } else if (presence_type === 'unavailable' && contact) {
  1147. // Only set the user to offline if there aren't any
  1148. // other resources still available.
  1149. if (contact.removeResource(resource) === 0) {
  1150. contact.save({'chat_status': "offline"});
  1151. }
  1152. } else if (contact) { // presence_type is undefined
  1153. this.addResource(bare_jid, resource);
  1154. contact.save({'chat_status': chat_status});
  1155. }
  1156. }
  1157. });
  1158. this.RosterGroup = Backbone.Model.extend({
  1159. initialize: function (attributes, options) {
  1160. this.set(_.extend({
  1161. description: DESC_GROUP_TOGGLE,
  1162. state: converse.OPENED
  1163. }, attributes));
  1164. // Collection of contacts belonging to this group.
  1165. this.contacts = new converse.RosterContacts();
  1166. }
  1167. });
  1168. this.RosterGroups = Backbone.Collection.extend({
  1169. model: converse.RosterGroup,
  1170. fetchRosterGroups: function () {
  1171. /* Fetches all the roster groups from sessionStorage.
  1172. *
  1173. * Returns a promise which resolves once the groups have been
  1174. * returned.
  1175. */
  1176. var deferred = new $.Deferred();
  1177. this.fetch({
  1178. silent: true, // We need to first have all groups before
  1179. // we can start positioning them, so we set
  1180. // 'silent' to true.
  1181. success: deferred.resolve
  1182. });
  1183. return deferred.promise();
  1184. }
  1185. });
  1186. this.Message = Backbone.Model.extend({
  1187. defaults: function(){
  1188. return {
  1189. msgid: converse.connection.getUniqueId()
  1190. };
  1191. }
  1192. });
  1193. this.Messages = Backbone.Collection.extend({
  1194. model: converse.Message,
  1195. comparator: 'time'
  1196. });
  1197. this.ChatBox = Backbone.Model.extend({
  1198. initialize: function () {
  1199. this.messages = new converse.Messages();
  1200. this.messages.browserStorage = new Backbone.BrowserStorage[converse.message_storage](
  1201. b64_sha1('converse.messages'+this.get('jid')+converse.bare_jid));
  1202. this.save({
  1203. // The chat_state will be set to ACTIVE once the chat box is opened
  1204. // and we listen for change:chat_state, so shouldn't set it to ACTIVE here.
  1205. 'box_id' : b64_sha1(this.get('jid')),
  1206. 'chat_state': undefined,
  1207. 'num_unread': this.get('num_unread') || 0,
  1208. 'time_opened': this.get('time_opened') || moment().valueOf(),
  1209. 'url': '',
  1210. 'user_id' : Strophe.getNodeFromJid(this.get('jid'))
  1211. });
  1212. },
  1213. getMessageAttributes: function ($message, $delay, original_stanza) {
  1214. $delay = $delay || $message.find('delay');
  1215. var type = $message.attr('type'),
  1216. body, stamp, time, sender, from;
  1217. if (type === 'error') {
  1218. body = $message.find('error').children('text').text();
  1219. } else {
  1220. body = $message.children('body').text();
  1221. }
  1222. var delayed = $delay.length > 0,
  1223. fullname = this.get('fullname'),
  1224. is_groupchat = type === 'groupchat',
  1225. chat_state = $message.find(converse.COMPOSING).length && converse.COMPOSING ||
  1226. $message.find(converse.PAUSED).length && converse.PAUSED ||
  1227. $message.find(converse.INACTIVE).length && converse.INACTIVE ||
  1228. $message.find(converse.ACTIVE).length && converse.ACTIVE ||
  1229. $message.find(converse.GONE).length && converse.GONE;
  1230. if (is_groupchat) {
  1231. from = Strophe.unescapeNode(Strophe.getResourceFromJid($message.attr('from')));
  1232. } else {
  1233. from = Strophe.getBareJidFromJid($message.attr('from'));
  1234. }
  1235. if (_.isEmpty(fullname)) {
  1236. fullname = from;
  1237. }
  1238. if (delayed) {
  1239. stamp = $delay.attr('stamp');
  1240. time = stamp;
  1241. } else {
  1242. time = moment().format();
  1243. }
  1244. if ((is_groupchat && from === this.get('nick')) || (!is_groupchat && from === converse.bare_jid)) {
  1245. sender = 'me';
  1246. } else {
  1247. sender = 'them';
  1248. }
  1249. return {
  1250. 'type': type,
  1251. 'chat_state': chat_state,
  1252. 'delayed': delayed,
  1253. 'fullname': fullname,
  1254. 'message': body || undefined,
  1255. 'msgid': $message.attr('id'),
  1256. 'sender': sender,
  1257. 'time': time
  1258. };
  1259. },
  1260. createMessage: function ($message, $delay, original_stanza) {
  1261. return this.messages.create(this.getMessageAttributes.apply(this, arguments));
  1262. }
  1263. });
  1264. this.ChatBoxes = Backbone.Collection.extend({
  1265. model: converse.ChatBox,
  1266. comparator: 'time_opened',
  1267. registerMessageHandler: function () {
  1268. converse.connection.addHandler(this.onMessage.bind(this), null, 'message', 'chat');
  1269. converse.connection.addHandler(this.onErrorMessage.bind(this), null, 'message', 'error');
  1270. },
  1271. chatBoxMayBeShown: function (chatbox) {
  1272. return true;
  1273. },
  1274. onChatBoxesFetched: function (collection) {
  1275. /* Show chat boxes upon receiving them from sessionStorage
  1276. *
  1277. * This method gets overridden entirely in src/converse-controlbox.js
  1278. * if the controlbox plugin is active.
  1279. */
  1280. collection.each(function (chatbox) {
  1281. if (this.chatBoxMayBeShown(chatbox)) {
  1282. chatbox.trigger('show');
  1283. }
  1284. }.bind(this));
  1285. converse.emit('chatBoxesFetched');
  1286. },
  1287. onConnected: function () {
  1288. this.browserStorage = new Backbone.BrowserStorage[converse.storage](
  1289. b64_sha1('converse.chatboxes-'+converse.bare_jid));
  1290. this.registerMessageHandler();
  1291. this.fetch({
  1292. add: true,
  1293. success: this.onChatBoxesFetched.bind(this)
  1294. });
  1295. },
  1296. onErrorMessage: function (message) {
  1297. /* Handler method for all incoming error message stanzas
  1298. */
  1299. // TODO: we can likely just reuse "onMessage" below
  1300. var $message = $(message),
  1301. from_jid = Strophe.getBareJidFromJid($message.attr('from'));
  1302. if (from_jid === converse.bare_jid) {
  1303. return true;
  1304. }
  1305. // Get chat box, but only create a new one when the message has a body.
  1306. var chatbox = this.getChatBox(from_jid);
  1307. if (!chatbox) {
  1308. return true;
  1309. }
  1310. chatbox.createMessage($message, null, message);
  1311. return true;
  1312. },
  1313. onMessage: function (message) {
  1314. /* Handler method for all incoming single-user chat "message"
  1315. * stanzas.
  1316. */
  1317. var $message = $(message),
  1318. contact_jid, $forwarded, $delay, from_bare_jid,
  1319. from_resource, is_me, msgid,
  1320. chatbox, resource,
  1321. from_jid = $message.attr('from'),
  1322. to_jid = $message.attr('to'),
  1323. to_resource = Strophe.getResourceFromJid(to_jid);
  1324. if (converse.filter_by_resource && (to_resource && to_resource !== converse.resource)) {
  1325. converse.log(
  1326. 'onMessage: Ignoring incoming message intended for a different resource: '+to_jid,
  1327. 'info'
  1328. );
  1329. return true;
  1330. } else if (utils.isHeadlineMessage(message)) {
  1331. // XXX: Ideally we wouldn't have to check for headline
  1332. // messages, but Prosody sends headline messages with the
  1333. // wrong type ('chat'), so we need to filter them out here.
  1334. converse.log(
  1335. "onMessage: Ignoring incoming headline message sent with type 'chat' from JID: "+from_jid,
  1336. 'info'
  1337. );
  1338. return true;
  1339. }
  1340. $forwarded = $message.find('forwarded');
  1341. if ($forwarded.length) {
  1342. $message = $forwarded.children('message');
  1343. $delay = $forwarded.children('delay');
  1344. from_jid = $message.attr('from');
  1345. to_jid = $message.attr('to');
  1346. }
  1347. from_bare_jid = Strophe.getBareJidFromJid(from_jid);
  1348. from_resource = Strophe.getResourceFromJid(from_jid);
  1349. is_me = from_bare_jid === converse.bare_jid;
  1350. msgid = $message.attr('id');
  1351. if (is_me) {
  1352. // I am the sender, so this must be a forwarded message...
  1353. contact_jid = Strophe.getBareJidFromJid(to_jid);
  1354. resource = Strophe.getResourceFromJid(to_jid);
  1355. } else {
  1356. contact_jid = from_bare_jid;
  1357. resource = from_resource;
  1358. }
  1359. converse.emit('message', message);
  1360. // Get chat box, but only create a new one when the message has a body.
  1361. chatbox = this.getChatBox(contact_jid, $message.find('body').length > 0);
  1362. if (!chatbox) {
  1363. return true;
  1364. }
  1365. if (msgid && chatbox.messages.findWhere({msgid: msgid})) {
  1366. return true; // We already have this message stored.
  1367. }
  1368. chatbox.createMessage($message, $delay, message);
  1369. return true;
  1370. },
  1371. getChatBox: function (jid, create, attrs) {
  1372. /* Returns a chat box or optionally return a newly
  1373. * created one if one doesn't exist.
  1374. *
  1375. * Parameters:
  1376. * (String) jid - The JID of the user whose chat box we want
  1377. * (Boolean) create - Should a new chat box be created if none exists?
  1378. */
  1379. jid = jid.toLowerCase();
  1380. var bare_jid = Strophe.getBareJidFromJid(jid);
  1381. var chatbox = this.get(bare_jid);
  1382. if (!chatbox && create) {
  1383. var roster_item = converse.roster.get(bare_jid);
  1384. if (roster_item === undefined) {
  1385. converse.log('Could not get roster item for JID '+bare_jid, 'error');
  1386. return;
  1387. }
  1388. chatbox = this.create(_.extend({
  1389. 'id': bare_jid,
  1390. 'jid': bare_jid,
  1391. 'fullname': _.isEmpty(roster_item.get('fullname'))? jid: roster_item.get('fullname'),
  1392. 'image_type': roster_item.get('image_type'),
  1393. 'image': roster_item.get('image'),
  1394. 'url': roster_item.get('url')
  1395. }, attrs || {}));
  1396. }
  1397. return chatbox;
  1398. }
  1399. });
  1400. this.ChatBoxViews = Backbone.Overview.extend({
  1401. initialize: function () {
  1402. this.model.on("add", this.onChatBoxAdded, this);
  1403. this.model.on("destroy", this.removeChat, this);
  1404. },
  1405. _ensureElement: function () {
  1406. /* Override method from backbone.js
  1407. * If the #conversejs element doesn't exist, create it.
  1408. */
  1409. if (!this.el) {
  1410. var $el = $('#conversejs');
  1411. if (!$el.length) {
  1412. $el = $('<div id="conversejs">');
  1413. $('body').append($el);
  1414. }
  1415. $el.html(converse.templates.chats_panel());
  1416. this.setElement($el, false);
  1417. } else {
  1418. this.setElement(_.result(this, 'el'), false);
  1419. }
  1420. },
  1421. onChatBoxAdded: function (item) {
  1422. // Views aren't created here, since the core code doesn't
  1423. // contain any views. Instead, they're created in overrides in
  1424. // plugins, such as in converse-chatview.js and converse-muc.js
  1425. return this.get(item.get('id'));
  1426. },
  1427. removeChat: function (item) {
  1428. this.remove(item.get('id'));
  1429. },
  1430. closeAllChatBoxes: function () {
  1431. /* This method gets overridden in src/converse-controlbox.js if
  1432. * the controlbox plugin is active.
  1433. */
  1434. this.each(function (view) { view.close(); });
  1435. return this;
  1436. },
  1437. chatBoxMayBeShown: function (chatbox) {
  1438. return this.model.chatBoxMayBeShown(chatbox);
  1439. },
  1440. getChatBox: function (attrs, create) {
  1441. var chatbox = this.model.get(attrs.jid);
  1442. if (!chatbox && create) {
  1443. chatbox = this.model.create(attrs, {
  1444. 'error': function (model, response) {
  1445. converse.log(response.responseText);
  1446. }
  1447. });
  1448. }
  1449. return chatbox;
  1450. },
  1451. showChat: function (attrs) {
  1452. /* Find the chat box and show it (if it may be shown).
  1453. * If it doesn't exist, create it.
  1454. */
  1455. var chatbox = this.getChatBox(attrs, true);
  1456. if (this.chatBoxMayBeShown(chatbox)) {
  1457. chatbox.trigger('show', true);
  1458. }
  1459. return chatbox;
  1460. }
  1461. });
  1462. this.XMPPStatus = Backbone.Model.extend({
  1463. initialize: function () {
  1464. this.set({
  1465. 'status' : this.getStatus()
  1466. });
  1467. this.on('change', function (item) {
  1468. if (_.has(item.changed, 'status')) {
  1469. converse.emit('statusChanged', this.get('status'));
  1470. }
  1471. if (_.has(item.changed, 'status_message')) {
  1472. converse.emit('statusMessageChanged', this.get('status_message'));
  1473. }
  1474. }.bind(this));
  1475. },
  1476. constructPresence: function (type, status_message) {
  1477. var presence;
  1478. type = typeof type === 'string' ? type : (this.get('status') || converse.default_state);
  1479. status_message = typeof status_message === 'string' ? status_message : undefined;
  1480. // Most of these presence types are actually not explicitly sent,
  1481. // but I add all of them here for reference and future proofing.
  1482. if ((type === 'unavailable') ||
  1483. (type === 'probe') ||
  1484. (type === 'error') ||
  1485. (type === 'unsubscribe') ||
  1486. (type === 'unsubscribed') ||
  1487. (type === 'subscribe') ||
  1488. (type === 'subscribed')) {
  1489. presence = $pres({'type': type});
  1490. } else if (type === 'offline') {
  1491. presence = $pres({'type': 'unavailable'});
  1492. } else if (type === 'online') {
  1493. presence = $pres();
  1494. } else {
  1495. presence = $pres().c('show').t(type).up();
  1496. }
  1497. if (status_message) {
  1498. presence.c('status').t(status_message);
  1499. }
  1500. return presence;
  1501. },
  1502. sendPresence: function (type, status_message) {
  1503. converse.connection.send(this.constructPresence(type, status_message));
  1504. },
  1505. setStatus: function (value) {
  1506. this.sendPresence(value);
  1507. this.save({'status': value});
  1508. },
  1509. getStatus: function () {
  1510. return this.get('status') || converse.default_state;
  1511. },
  1512. setStatusMessage: function (status_message) {
  1513. this.sendPresence(this.getStatus(), status_message);
  1514. var prev_status = this.get('status_message');
  1515. this.save({'status_message': status_message});
  1516. if (this.xhr_custom_status) {
  1517. $.ajax({
  1518. url: this.xhr_custom_status_url,
  1519. type: 'POST',
  1520. data: {'msg': status_message}
  1521. });
  1522. }
  1523. if (prev_status === status_message) {
  1524. this.trigger("update-status-ui", this);
  1525. }
  1526. }
  1527. });
  1528. this.Session = Backbone.Model; // General session settings to be saved to sessionStorage.
  1529. this.Feature = Backbone.Model;
  1530. this.Features = Backbone.Collection.extend({
  1531. /* Service Discovery
  1532. * -----------------
  1533. * This collection stores Feature Models, representing features
  1534. * provided by available XMPP entities (e.g. servers)
  1535. * See XEP-0030 for more details: http://xmpp.org/extensions/xep-0030.html
  1536. * All features are shown here: http://xmpp.org/registrar/disco-features.html
  1537. */
  1538. model: converse.Feature,
  1539. initialize: function () {
  1540. this.addClientIdentities().addClientFeatures();
  1541. this.browserStorage = new Backbone.BrowserStorage[converse.storage](
  1542. b64_sha1('converse.features'+converse.bare_jid));
  1543. this.on('add', this.onFeatureAdded, this);
  1544. if (this.browserStorage.records.length === 0) {
  1545. // browserStorage is empty, so we've likely never queried this
  1546. // domain for features yet
  1547. converse.connection.disco.info(converse.domain, null, this.onInfo.bind(this));
  1548. converse.connection.disco.items(converse.domain, null, this.onItems.bind(this));
  1549. } else {
  1550. this.fetch({add:true});
  1551. }
  1552. },
  1553. onFeatureAdded: function (feature) {
  1554. converse.emit('serviceDiscovered', feature);
  1555. },
  1556. addClientIdentities: function () {
  1557. /* See http://xmpp.org/registrar/disco-categories.html
  1558. */
  1559. converse.connection.disco.addIdentity('client', 'web', 'Converse.js');
  1560. return this;
  1561. },
  1562. addClientFeatures: function () {
  1563. /* The strophe.disco.js plugin keeps a list of features which
  1564. * it will advertise to any #info queries made to it.
  1565. *
  1566. * See: http://xmpp.org/extensions/xep-0030.html#info
  1567. */
  1568. converse.connection.disco.addFeature(Strophe.NS.BOSH);
  1569. converse.connection.disco.addFeature(Strophe.NS.CHATSTATES);
  1570. converse.connection.disco.addFeature(Strophe.NS.DISCO_INFO);
  1571. converse.connection.disco.addFeature(Strophe.NS.ROSTERX); // Limited support
  1572. if (converse.message_carbons) {
  1573. converse.connection.disco.addFeature(Strophe.NS.CARBONS);
  1574. }
  1575. return this;
  1576. },
  1577. onItems: function (stanza) {
  1578. $(stanza).find('query item').each(function (idx, item) {
  1579. converse.connection.disco.info(
  1580. $(item).attr('jid'),
  1581. null,
  1582. this.onInfo.bind(this));
  1583. }.bind(this));
  1584. },
  1585. onInfo: function (stanza) {
  1586. var $stanza = $(stanza);
  1587. if (($stanza.find('identity[category=server][type=im]').length === 0) &&
  1588. ($stanza.find('identity[category=conference][type=text]').length === 0)) {
  1589. // This isn't an IM server component
  1590. return;
  1591. }
  1592. $stanza.find('feature').each(function (idx, feature) {
  1593. var namespace = $(feature).attr('var');
  1594. this[namespace] = true;
  1595. this.create({
  1596. 'var': namespace,
  1597. 'from': $stanza.attr('from')
  1598. });
  1599. }.bind(this));
  1600. }
  1601. });
  1602. this.setUpXMLLogging = function () {
  1603. Strophe.log = function (level, msg) {
  1604. converse.log(msg, level);
  1605. };
  1606. if (this.debug) {
  1607. this.connection.xmlInput = function (body) { converse.log(body.outerHTML); };
  1608. this.connection.xmlOutput = function (body) { converse.log(body.outerHTML); };
  1609. }
  1610. };
  1611. this.fetchLoginCredentials = function () {
  1612. var deferred = new $.Deferred();
  1613. $.ajax({
  1614. url: converse.credentials_url,
  1615. type: 'GET',
  1616. dataType: "json",
  1617. success: function (response) {
  1618. deferred.resolve({
  1619. 'jid': response.jid,
  1620. 'password': response.password
  1621. });
  1622. },
  1623. error: function (response) {
  1624. delete converse.connection;
  1625. converse.emit('noResumeableSession');
  1626. deferred.reject(response);
  1627. }
  1628. });
  1629. return deferred.promise();
  1630. };
  1631. this.startNewBOSHSession = function () {
  1632. $.ajax({
  1633. url: this.prebind_url,
  1634. type: 'GET',
  1635. dataType: "json",
  1636. success: function (response) {
  1637. this.connection.attach(
  1638. response.jid,
  1639. response.sid,
  1640. response.rid,
  1641. this.onConnectStatusChanged
  1642. );
  1643. }.bind(this),
  1644. error: function (response) {
  1645. delete this.connection;
  1646. this.emit('noResumeableSession');
  1647. }.bind(this)
  1648. });
  1649. };
  1650. this.attemptPreboundSession = function (tokens) {
  1651. /* Handle session resumption or initialization when prebind is being used.
  1652. */
  1653. if (this.jid && this.sid && this.rid) {
  1654. return this.connection.attach(this.jid, this.sid, this.rid, this.onConnectStatusChanged);
  1655. } else if (this.keepalive) {
  1656. if (!this.jid) {
  1657. throw new Error("attemptPreboundSession: when using 'keepalive' with 'prebind, "+
  1658. "you must supply the JID of the current user.");
  1659. }
  1660. try {
  1661. return this.connection.restore(this.jid, this.onConnectStatusChanged);
  1662. } catch (e) {
  1663. this.log("Could not restore session for jid: "+this.jid+" Error message: "+e.message);
  1664. this.clearSession(); // If there's a roster, we want to clear it (see #555)
  1665. }
  1666. } else {
  1667. throw new Error("attemptPreboundSession: If you use prebind and not keepalive, "+
  1668. "then you MUST supply JID, RID and SID values");
  1669. }
  1670. // We haven't been able to attach yet. Let's see if there
  1671. // is a prebind_url, otherwise there's nothing with which
  1672. // we can attach.
  1673. if (this.prebind_url) {
  1674. this.startNewBOSHSession();
  1675. } else {
  1676. delete this.connection;
  1677. this.emit('noResumeableSession');
  1678. }
  1679. };
  1680. this.autoLogin = function (credentials) {
  1681. if (credentials) {
  1682. // If passed in, then they come from credentials_url, so we
  1683. // set them on the converse object.
  1684. this.jid = credentials.jid;
  1685. this.password = credentials.password;
  1686. }
  1687. if (this.authentication === converse.ANONYMOUS) {
  1688. if (!this.jid) {
  1689. throw new Error("Config Error: when using anonymous login " +
  1690. "you need to provide the server's domain via the 'jid' option. " +
  1691. "Either when calling converse.initialize, or when calling " +
  1692. "converse.user.login.");
  1693. }
  1694. this.connection.connect(this.jid.toLowerCase(), null, this.onConnectStatusChanged);
  1695. } else if (this.authentication === converse.LOGIN) {
  1696. var password = converse.connection.pass || this.password;
  1697. if (!password) {
  1698. if (this.auto_login && !this.password) {
  1699. throw new Error("initConnection: If you use auto_login and "+
  1700. "authentication='login' then you also need to provide a password.");
  1701. }
  1702. converse.disconnection_cause = Strophe.Status.AUTHFAIL;
  1703. converse.onDisconnected();
  1704. converse.giveFeedback(''); // Wipe the feedback
  1705. }
  1706. var resource = Strophe.getResourceFromJid(this.jid);
  1707. if (!resource) {
  1708. this.jid = this.jid.toLowerCase() + converse.generateResource();
  1709. } else {
  1710. this.jid = Strophe.getBareJidFromJid(this.jid).toLowerCase()+'/'+resource;
  1711. }
  1712. this.connection.connect(this.jid, password, this.onConnectStatusChanged);
  1713. }
  1714. };
  1715. this.attemptNonPreboundSession = function () {
  1716. /* Handle session resumption or initialization when prebind is not being used.
  1717. *
  1718. * Two potential options exist and are handled in this method:
  1719. * 1. keepalive
  1720. * 2. auto_login
  1721. */
  1722. if (this.keepalive) {
  1723. try {
  1724. return this.connection.restore(this.jid, this.onConnectStatusChanged);
  1725. } catch (e) {
  1726. this.log("Could not restore session. Error message: "+e.message);
  1727. this.clearSession(); // If there's a roster, we want to clear it (see #555)
  1728. }
  1729. }
  1730. if (this.auto_login) {
  1731. if (this.credentials_url) {
  1732. this.fetchLoginCredentials().done(this.autoLogin.bind(this));
  1733. } else if (!this.jid) {
  1734. throw new Error(
  1735. "initConnection: If you use auto_login, you also need"+
  1736. "to give either a jid value (and if applicable a "+
  1737. "password) or you need to pass in a URL from where the "+
  1738. "username and password can be fetched (via credentials_url)."
  1739. );
  1740. } else {
  1741. this.autoLogin();
  1742. }
  1743. }
  1744. };
  1745. this.logIn = function (credentials) {
  1746. if (credentials || this.authentication === converse.ANONYMOUS) {
  1747. // When credentials are passed in, they override prebinding
  1748. // or credentials fetching via HTTP
  1749. this.autoLogin(credentials);
  1750. } else {
  1751. // We now try to resume or automatically set up a new session.
  1752. // Otherwise the user will be shown a login form.
  1753. if (this.authentication === converse.PREBIND) {
  1754. this.attemptPreboundSession();
  1755. } else {
  1756. this.attemptNonPreboundSession();
  1757. }
  1758. }
  1759. };
  1760. this.initConnection = function () {
  1761. if (this.connection) {
  1762. return;
  1763. }
  1764. if (!this.bosh_service_url && ! this.websocket_url) {
  1765. throw new Error("initConnection: you must supply a value for either the bosh_service_url or websocket_url or both.");
  1766. }
  1767. if (('WebSocket' in window || 'MozWebSocket' in window) && this.websocket_url) {
  1768. this.connection = new Strophe.Connection(this.websocket_url);
  1769. } else if (this.bosh_service_url) {
  1770. this.connection = new Strophe.Connection(this.bosh_service_url, {'keepalive': this.keepalive});
  1771. } else {
  1772. throw new Error("initConnection: this browser does not support websockets and bosh_service_url wasn't specified.");
  1773. }
  1774. };
  1775. this._tearDown = function () {
  1776. /* Remove those views which are only allowed with a valid
  1777. * connection.
  1778. */
  1779. this.unregisterPresenceHandler();
  1780. if (this.roster) {
  1781. this.roster.off().reset(); // Removes roster contacts
  1782. }
  1783. this.chatboxes.remove(); // Don't call off(), events won't get re-registered upon reconnect.
  1784. if (this.features) {
  1785. this.features.reset();
  1786. }
  1787. $(window).off('click mousemove keypress focus'+unloadevent, converse.onUserActivity);
  1788. window.clearInterval(converse.everySecondTrigger);
  1789. return this;
  1790. };
  1791. this._initialize = function () {
  1792. this.chatboxes = new this.ChatBoxes();
  1793. this.chatboxviews = new this.ChatBoxViews({model: this.chatboxes});
  1794. this.initSession();
  1795. this.initConnection();
  1796. this.setUpXMLLogging();
  1797. this.logIn();
  1798. return this;
  1799. };
  1800. // Initialization
  1801. // --------------
  1802. // This is the end of the initialize method.
  1803. if (settings.connection) {
  1804. this.connection = settings.connection;
  1805. }
  1806. var updateSettings = function (settings) {
  1807. /* Helper method which gets put on the plugin and allows it to
  1808. * add more user-facing config settings to converse.js.
  1809. */
  1810. _.extend(converse.default_settings, settings);
  1811. _.extend(converse, settings);
  1812. _.extend(converse, _.pick(converse.user_settings, Object.keys(settings)));
  1813. };
  1814. converse.pluggable.initializePlugins({
  1815. 'updateSettings': updateSettings,
  1816. 'converse': converse
  1817. });
  1818. converse.emit('pluginsInitialized');
  1819. converse._initialize();
  1820. converse.registerGlobalEventHandlers();
  1821. return init_deferred.promise();
  1822. };
  1823. return converse;
  1824. }));