/*! * Converse.js (Web-based XMPP instant messaging client) * http://conversejs.org * * Copyright (c) 2012, Jan-Carel Brand * Dual licensed under the MIT and GPL Licenses */ // AMD/global registrations (function (root, factory) { if (typeof console === "undefined" || typeof console.log === "undefined") { console = { log: function () {}, error: function () {} }; } if (typeof define === 'function' && define.amd) { define("converse", ["converse-dependencies"], function(otr) { // Use Mustache style syntax for variable interpolation _.templateSettings = { evaluate : /\{\[([\s\S]+?)\]\}/g, interpolate : /\{\{([\s\S]+?)\}\}/g }; if (typeof otr !== "undefined") { return factory(jQuery, _, otr.OTR, otr.DSA, console); } else { return factory(jQuery, _, undefined, undefined, console); } }); } else { // Browser globals _.templateSettings = { evaluate : /\{\[([\s\S]+?)\]\}/g, interpolate : /\{\{([\s\S]+?)\}\}/g }; if ((typeof OTR !== "undefined") && (typeof DSA !== "undefined")) { root.converse = factory(jQuery, _, OTR, DSA, console); } else { root.converse = factory(jQuery, _, undefined, undefined, console); } } }(this, function ($, _, OTR, DSA, console) { $.fn.addHyperlinks = function() { if (this.length > 0) { this.each(function(i, obj) { var x = $(obj).html(); var list = x.match(/\b(https?:\/\/|www\.|https?:\/\/www\.)[^\s<]{2,200}\b/g ); if (list) { for (i=0; i"+ list[i] + "" ); } } $(obj).html(x); }); } return this; }; $.fn.addEmoticons = function() { if (converse.show_emoticons) { if (this.length > 0) { this.each(function(i, obj) { var text = $(obj).html(); text = text.replace(/:\)/g, ''); text = text.replace(/:\-\)/g, ''); text = text.replace(/;\)/g, ''); text = text.replace(/;\-\)/g, ''); text = text.replace(/:D/g, ''); text = text.replace(/:\-D/g, ''); text = text.replace(/:P/g, ''); text = text.replace(/:\-P/g, ''); text = text.replace(/:p/g, ''); text = text.replace(/:\-p/g, ''); text = text.replace(/8\)/g, ''); text = text.replace(/>:\)/g, ''); text = text.replace(/:S/g, ''); text = text.replace(/:\\/g, ''); text = text.replace(/:\/ /g, ''); text = text.replace(/>:\(/g, ''); text = text.replace(/:\(/g, ''); text = text.replace(/:\-\(/g, ''); text = text.replace(/:O/g, ''); text = text.replace(/:\-O/g, ''); text = text.replace(/\=\-O/g, ''); text = text.replace(/\(\^.\^\)b/g, ''); text = text.replace(/<3/g, ''); $(obj).html(text); }); } } return this; }; var converse = { emit: function(evt, data) { $(this).trigger(evt, data); }, once: function(evt, handler) { $(this).one(evt, handler); }, on: function(evt, handler) { $(this).bind(evt, handler); }, off: function(evt, handler) { $(this).unbind(evt, handler); } }; converse.initialize = function (settings, callback) { var converse = this; // Constants // --------- var UNENCRYPTED = 0; var UNVERIFIED= 1; var VERIFIED= 2; var FINISHED = 3; var KEY = { ENTER: 13 }; var HAS_CSPRNG = ((typeof crypto !== 'undefined') && ((typeof crypto.randomBytes === 'function') || (typeof crypto.getRandomValues === 'function') )); var HAS_CRYPTO = HAS_CSPRNG && ( (typeof CryptoJS !== "undefined") && (typeof OTR !== "undefined") && (typeof DSA !== "undefined") ); // Default configuration values // ---------------------------- this.allow_contact_requests = true; this.allow_muc = true; this.allow_otr = true; this.animate = true; this.auto_list_rooms = false; this.auto_reconnect = true; this.auto_subscribe = false; this.bosh_service_url = undefined; // The BOSH connection manager URL. this.cache_otr_key = false; this.debug = false; this.expose_rid_and_sid = false; this.hide_muc_server = false; this.i18n = locales.en; this.prebind = false; this.show_controlbox_by_default = false; this.show_only_online_users = false; this.show_call_button = false; this.show_emoticons = true; this.show_toolbar = true; this.use_vcards = true; this.xhr_custom_status = false; this.xhr_custom_status_url = ''; this.xhr_user_search = false; this.xhr_user_search_url = ''; // Allow only whitelisted configuration attributes to be overwritten _.extend(this, _.pick(settings, [ 'allow_contact_requests', 'allow_muc', 'allow_otr', 'animate', 'auto_list_rooms', 'auto_reconnect', 'auto_subscribe', 'bosh_service_url', 'cache_otr_key', 'connection', 'debug', 'expose_rid_and_sid', 'fullname', 'hide_muc_server', 'i18n', 'jid', 'prebind', 'rid', 'show_call_button', 'show_controlbox_by_default', 'show_emoticons', 'show_only_online_users', 'show_toolbar', 'sid', 'use_vcards', 'xhr_custom_status', 'xhr_custom_status_url', 'xhr_user_search', 'xhr_user_search_url' ])); // Only allow OTR if we have the capability this.allow_otr = this.allow_otr && HAS_CRYPTO; // Translation machinery // --------------------- var __ = $.proxy(function (str) { // Translation factory if (this.i18n === undefined) { this.i18n = locales.en; } var t = this.i18n.translate(str); if (arguments.length>1) { return t.fetch.apply(t, [].slice.call(arguments,1)); } else { return t.fetch(); } }, this); var ___ = function (str) { /* XXX: This is part of a hack to get gettext to scan strings to be * translated. Strings we cannot send to the function above because * they require variable interpolation and we don't yet have the * variables at scan time. * * See actionInfoMessages */ return str; }; // Translation aware constants // --------------------------- var OTR_CLASS_MAPPING = {}; OTR_CLASS_MAPPING[UNENCRYPTED] = 'unencrypted'; OTR_CLASS_MAPPING[UNVERIFIED] = 'unverified'; OTR_CLASS_MAPPING[VERIFIED] = 'verified'; OTR_CLASS_MAPPING[FINISHED] = 'finished'; var OTR_TRANSLATED_MAPPING = {}; OTR_TRANSLATED_MAPPING[UNENCRYPTED] = __('unencrypted'); OTR_TRANSLATED_MAPPING[UNVERIFIED] = __('unverified'); OTR_TRANSLATED_MAPPING[VERIFIED] = __('verified'); OTR_TRANSLATED_MAPPING[FINISHED] = __('finished'); var STATUSES = { 'dnd': __('This contact is busy'), 'online': __('This contact is online'), 'offline': __('This contact is offline'), 'unavailable': __('This contact is unavailable'), 'xa': __('This contact is away for an extended period'), 'away': __('This contact is away') }; // Module-level variables // ---------------------- this.callback = callback || function () {}; this.initial_presence_sent = 0; this.msg_counter = 0; // Module-level functions // ---------------------- this.giveFeedback = function (message, klass) { $('.conn-feedback').text(message); $('.conn-feedback').attr('class', 'conn-feedback'); if (klass) { $('.conn-feedback').addClass(klass); } }; this.log = function (txt, level) { if (this.debug) { if (level == 'error') { console.log('ERROR: '+txt); } else { console.log(txt); } } }; this.getVCard = function (jid, callback, errback) { if (!this.use_vcards) { if (callback) { callback(jid, jid); } return; } converse.connection.vcard.get( $.proxy(function (iq) { // Successful callback var $vcard = $(iq).find('vCard'); var fullname = $vcard.find('FN').text(), img = $vcard.find('BINVAL').text(), img_type = $vcard.find('TYPE').text(), url = $vcard.find('URL').text(); if (jid) { var rosteritem = converse.roster.get(jid); if (rosteritem) { fullname = _.isEmpty(fullname)? rosteritem.get('fullname') || jid: fullname; rosteritem.save({ 'fullname': fullname, 'image_type': img_type, 'image': img, 'url': url, 'vcard_updated': converse.toISOString(new Date()) }); } } if (callback) { callback(jid, fullname, img, img_type, url); } }, this), jid, function (iq) { // Error callback var rosteritem = converse.roster.get(jid); if (rosteritem) { rosteritem.save({ 'vcard_updated': converse.toISOString(new Date()) }); } if (errback) { errback(iq); } } ); }; this.reconnect = function () { converse.giveFeedback(__('Reconnecting'), 'error'); if (!converse.prebind) { this.connection.connect( this.connection.jid, this.connection.pass, function (status, condition) { converse.onConnect(status, condition, true); }, this.connection.wait, this.connection.hold, this.connection.route ); } }; this.showLoginButton = function () { var view = converse.chatboxesview.views.controlbox; if (typeof view.loginpanel !== 'undefined') { view.loginpanel.showLoginButton(); } }; this.onConnect = function (status, condition, reconnect) { var $button, $form; if ((status === Strophe.Status.CONNECTED) || (status === Strophe.Status.ATTACHED)) { if ((typeof reconnect !== 'undefined') && (reconnect)) { converse.log(status === Strophe.Status.CONNECTED ? 'Reconnected' : 'Reattached'); converse.onReconnected(); } else { converse.log(status === Strophe.Status.CONNECTED ? 'Connected' : 'Attached'); converse.onConnected(); } } else if (status === Strophe.Status.DISCONNECTED) { // TODO: Handle case where user manually logs out... converse.giveFeedback(__('Disconnected'), 'error'); if (converse.auto_reconnect) { converse.reconnect(); } else { converse.showLoginButton(); } } else if (status === Strophe.Status.Error) { converse.showLoginButton(); converse.giveFeedback(__('Error'), 'error'); } else if (status === Strophe.Status.CONNECTING) { converse.giveFeedback(__('Connecting')); } else if (status === Strophe.Status.CONNFAIL) { converse.showLoginButton(); converse.giveFeedback(__('Connection Failed'), 'error'); } else if (status === Strophe.Status.AUTHENTICATING) { converse.giveFeedback(__('Authenticating')); } else if (status === Strophe.Status.AUTHFAIL) { converse.showLoginButton(); converse.giveFeedback(__('Authentication Failed'), 'error'); } else if (status === Strophe.Status.DISCONNECTING) { converse.giveFeedback(__('Disconnecting'), 'error'); } }; this.toISOString = function (date) { var pad; if (typeof date.toISOString !== 'undefined') { return date.toISOString(); } else { // IE <= 8 Doesn't have toISOStringMethod pad = function (num) { return (num < 10) ? '0' + num : '' + num; }; return date.getUTCFullYear() + '-' + pad(date.getUTCMonth() + 1) + '-' + pad(date.getUTCDate()) + 'T' + pad(date.getUTCHours()) + ':' + pad(date.getUTCMinutes()) + ':' + pad(date.getUTCSeconds()) + '.000Z'; } }; this.parseISO8601 = function (datestr) { /* Parses string formatted as 2013-02-14T11:27:08.268Z to a Date obj. */ var numericKeys = [1, 4, 5, 6, 7, 10, 11], struct = /^\s*(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}\.?\d*)Z\s*$/.exec(datestr), minutesOffset = 0, i, k; for (i = 0; (k = numericKeys[i]); ++i) { struct[k] = +struct[k] || 0; } // allow undefined days and months struct[2] = (+struct[2] || 1) - 1; struct[3] = +struct[3] || 1; if (struct[8] !== 'Z' && struct[9] !== undefined) { minutesOffset = struct[10] * 60 + struct[11]; if (struct[9] === '+') { minutesOffset = 0 - minutesOffset; } } return new Date(Date.UTC(struct[1], struct[2], struct[3], struct[4], struct[5] + minutesOffset, struct[6], struct[7])); }; this.updateMsgCounter = function () { if (this.msg_counter > 0) { if (document.title.search(/^Messages \(\d+\) /) == -1) { document.title = "Messages (" + this.msg_counter + ") " + document.title; } else { document.title = document.title.replace(/^Messages \(\d+\) /, "Messages (" + this.msg_counter + ") "); } window.blur(); window.focus(); } else if (document.title.search(/^Messages \(\d+\) /) != -1) { document.title = document.title.replace(/^Messages \(\d+\) /, ""); } }; this.incrementMsgCounter = function () { this.msg_counter += 1; this.updateMsgCounter(); }; this.clearMsgCounter = function () { this.msg_counter = 0; this.updateMsgCounter(); }; this.initStatus = function (callback) { this.xmppstatus = new this.XMPPStatus(); var id = hex_sha1('converse.xmppstatus-'+this.bare_jid); this.xmppstatus.id = id; // This appears to be necessary for backbone.localStorage this.xmppstatus.localStorage = new Backbone.LocalStorage(id); this.xmppstatus.fetch({success: callback, error: callback}); }; this.registerRosterHandler = function () { // Register handlers that depend on the roster this.connection.roster.registerCallback( $.proxy(this.roster.rosterHandler, this.roster), null, 'presence', null); }; this.registerRosterXHandler = function () { this.connection.addHandler( $.proxy(this.roster.subscribeToSuggestedItems, this.roster), 'http://jabber.org/protocol/rosterx', 'message', null); }; this.registerPresenceHandler = function () { this.connection.addHandler( $.proxy(function (presence) { this.presenceHandler(presence); return true; }, this.roster), null, 'presence', null); }; this.initRoster = function () { // Set up the roster this.roster = new this.RosterItems(); this.roster.localStorage = new Backbone.LocalStorage( hex_sha1('converse.rosteritems-'+converse.bare_jid)); this.registerRosterHandler(); this.registerRosterXHandler(); this.registerPresenceHandler(); // No create the view which will fetch roster items from // localStorage this.rosterview = new this.RosterView({'model':this.roster}); }; this.registerGlobalEventHandlers = function () { $(document).click(function() { if ($('.toggle-otr ul').is(':visible')) { $('.toggle-otr ul', this).slideUp(); } if ($('.toggle-smiley ul').is(':visible')) { $('.toggle-smiley ul', this).slideUp(); } }); $(window).on("blur focus", $.proxy(function(e) { if ((this.windowState != e.type) && (e.type == 'focus')) { converse.clearMsgCounter(); } this.windowState = e.type; },this)); }; this.onReconnected = function () { // We need to re-register all the event handlers on the newly // created connection. this.initStatus($.proxy(function () { this.registerRosterXHandler(); this.registerPresenceHandler(); this.chatboxes.registerMessageHandler(); converse.xmppstatus.sendPresence(); this.giveFeedback(__('Online Contacts')); }, this)); }; this.onConnected = function () { if (this.debug) { this.connection.xmlInput = function (body) { console.log(body); }; this.connection.xmlOutput = function (body) { console.log(body); }; Strophe.log = function (level, msg) { console.log(level+' '+msg); }; Strophe.error = function (msg) { console.log('ERROR: '+msg); }; } this.bare_jid = Strophe.getBareJidFromJid(this.connection.jid); this.domain = Strophe.getDomainFromJid(this.connection.jid); this.features = new this.Features(); this.initStatus($.proxy(function () { this.initRoster(); this.chatboxes.onConnected(); this.connection.roster.get(function () {}); this.registerGlobalEventHandlers(); this.giveFeedback(__('Online Contacts')); if (this.callback) { if (this.connection.service === 'jasmine tests') { // XXX: Call back with the internal converse object. This // object should never be exposed to production systems. // 'jasmine tests' is an invalid http bind service value, // so we're sure that this is just for tests. // // TODO: We might need to consider websockets, which // probably won't use the 'service' attr. Current // strophe.js version used by converse.js doesn't support // websockets. this.callback(this); } else { this.callback(); } } }, this)); converse.emit('onReady'); }; // Backbone Models and Views // ------------------------- this.OTR = Backbone.Model.extend({ // A model for managing OTR settings. getSessionPassphrase: function () { if (converse.prebind) { var key = hex_sha1(converse.connection.jid), pass = window.sessionStorage[key]; if (typeof pass === 'undefined') { pass = Math.floor(Math.random()*4294967295).toString(); window.sessionStorage[key] = pass; } return pass; } else { return converse.connection.pass; } }, generatePrivateKey: function () { var key = new DSA(); var jid = converse.connection.jid; if (converse.cache_otr_key) { var cipher = CryptoJS.lib.PasswordBasedCipher; var pass = this.getSessionPassphrase(); if (typeof pass !== "undefined") { // Encrypt the key and set in sessionStorage. Also store instance tag. window.sessionStorage[hex_sha1(jid+'priv_key')] = cipher.encrypt(CryptoJS.algo.AES, key.packPrivate(), pass).toString(); window.sessionStorage[hex_sha1(jid+'instance_tag')] = instance_tag; window.sessionStorage[hex_sha1(jid+'pass_check')] = cipher.encrypt(CryptoJS.algo.AES, 'match', pass).toString(); } } return key; } }); this.Message = Backbone.Model.extend(); this.Messages = Backbone.Collection.extend({ model: converse.Message }); this.ChatBox = Backbone.Model.extend({ initialize: function () { if (this.get('box_id') !== 'controlbox') { this.messages = new converse.Messages(); this.messages.localStorage = new Backbone.LocalStorage( hex_sha1('converse.messages'+this.get('jid')+converse.bare_jid)); this.set({ 'user_id' : Strophe.getNodeFromJid(this.get('jid')), 'box_id' : hex_sha1(this.get('jid')), 'otr_status': this.get('otr_status') || UNENCRYPTED }); } }, getSession: function (callback) { var cipher = CryptoJS.lib.PasswordBasedCipher; var result, pass, instance_tag, saved_key, pass_check; if (converse.cache_otr_key) { pass = converse.otr.getSessionPassphrase(); if (typeof pass !== "undefined") { instance_tag = window.sessionStorage[hex_sha1(this.id+'instance_tag')]; saved_key = window.sessionStorage[hex_sha1(this.id+'priv_key')]; pass_check = window.sessionStorage[hex_sha1(this.connection.jid+'pass_check')]; if (saved_key && instance_tag && typeof pass_check !== 'undefined') { var decrypted = cipher.decrypt(CryptoJS.algo.AES, saved_key, pass); var key = DSA.parsePrivate(decrypted.toString(CryptoJS.enc.Latin1)); if (cipher.decrypt(CryptoJS.algo.AES, pass_check, pass).toString(CryptoJS.enc.Latin1) === 'match') { // Verified that the passphrase is still the same this.trigger('showHelpMessages', [__('Re-establishing encrypted session')]); callback({ 'key': key, 'instance_tag': instance_tag }); return; // Our work is done here } } } } // We need to generate a new key and instance tag this.trigger('showHelpMessages', [ __('Generating private key.'), __('Your browser might become unresponsive.')], null, true // show spinner ); setTimeout(function () { callback({ 'key': converse.otr.generatePrivateKey.apply(this), 'instance_tag': OTR.makeInstanceTag() }); }, 500); }, updateOTRStatus: function (state) { switch (state) { case OTR.CONST.STATUS_AKE_SUCCESS: if (this.otr.msgstate === OTR.CONST.MSGSTATE_ENCRYPTED) { this.save({'otr_status': UNVERIFIED}); } break; case OTR.CONST.STATUS_END_OTR: if (this.otr.msgstate === OTR.CONST.MSGSTATE_FINISHED) { this.save({'otr_status': FINISHED}); } else if (this.otr.msgstate === OTR.CONST.MSGSTATE_PLAINTEXT) { this.save({'otr_status': UNENCRYPTED}); } break; } }, onSMP: function (type, data) { // Event handler for SMP (Socialist's Millionaire Protocol) // used by OTR (off-the-record). switch (type) { case 'question': this.otr.smpSecret(prompt(__( 'Authentication request from %1$s\n\nYour buddy is attempting to verify your identity, by asking you the question below.\n\n%2$s', [this.get('fullname'), data]))); break; case 'trust': if (data === true) { this.save({'otr_status': VERIFIED}); } else { this.trigger( 'showHelpMessages', [__("Could not verify this user's identify.")], 'error'); this.save({'otr_status': UNVERIFIED}); } break; default: throw new Error('Unknown type.'); } }, initiateOTR: function (query_msg) { // Sets up an OTR object through which we can send and receive // encrypted messages. // // If 'query_msg' is passed in, it means there is an alread incoming // query message from our buddy. Otherwise, it is us who will // send the query message to them. this.save({'otr_status': UNENCRYPTED}); var session = this.getSession($.proxy(function (session) { this.otr = new OTR({ fragment_size: 140, send_interval: 200, priv: session.key, instance_tag: session.instance_tag, debug: this.debug }); this.otr.on('status', $.proxy(this.updateOTRStatus, this)); this.otr.on('smp', $.proxy(this.onSMP, this)); this.otr.on('ui', $.proxy(function (msg) { this.trigger('showReceivedOTRMessage', msg); }, this)); this.otr.on('io', $.proxy(function (msg) { this.trigger('sendMessageStanza', msg); }, this)); this.otr.on('error', $.proxy(function (msg) { this.trigger('showOTRError', msg); }, this)); this.trigger('showHelpMessages', [__('Exchanging private key with buddy.')]); if (query_msg) { this.otr.receiveMsg(query_msg); } else { this.otr.sendQueryMsg(); } }, this)); }, endOTR: function () { if (this.otr) { this.otr.endOtr(); } this.save({'otr_status': UNENCRYPTED}); }, createMessage: function (message) { var $message = $(message), body = $message.children('body').text(), from = Strophe.getBareJidFromJid($message.attr('from')), composing = $message.find('composing'), delayed = $message.find('delay').length > 0, fullname = this.get('fullname'), stamp, time, sender; fullname = (_.isEmpty(fullname)? from: fullname).split(' ')[0]; if (!body) { if (composing.length) { this.messages.add({ fullname: fullname, sender: 'them', delayed: delayed, time: converse.toISOString(new Date()), composing: composing.length }); } } else { if (delayed) { stamp = $message.find('delay').attr('stamp'); time = stamp; } else { time = converse.toISOString(new Date()); } if (from == converse.bare_jid) { sender = 'me'; } else { sender = 'them'; } this.messages.create({ fullname: fullname, sender: sender, delayed: delayed, time: time, message: body }); } }, messageReceived: function (message) { var $body = $(message).children('body'); var text = ($body.length > 0 ? $body.text() : undefined); if ((!text) || (!converse.allow_otr)) { return this.createMessage(message); } if (text.match(/^\?OTRv23?/)) { this.initiateOTR(text); } else { if (_.contains([UNVERIFIED, VERIFIED], this.get('otr_status'))) { this.otr.receiveMsg(text); } else { if (text.match(/^\?OTR/)) { if (!this.otr) { this.initiateOTR(text); } else { this.otr.receiveMsg(text); } } else { // Normal unencrypted message. this.createMessage(message); } } } } }); this.ChatBoxView = Backbone.View.extend({ length: 200, tagName: 'div', className: 'chatbox', is_chatroom: false, // This is not a multi-user chatroom events: { 'click .close-chatbox-button': 'closeChat', 'keypress textarea.chat-textarea': 'keyPressed', 'click .toggle-smiley': 'toggleEmoticonMenu', 'click .toggle-smiley ul li': 'insertEmoticon', 'click .toggle-otr': 'toggleOTRMenu', 'click .start-otr': 'startOTRFromToolbar', 'click .end-otr': 'endOTR', 'click .auth-otr': 'authOTR', 'click .toggle-call': 'toggleCall' }, template: _.template( '
' + '' + '' + '
{{ fullname }}
' + '
' + '

' + '

' + '
' + '
' + '{[ if ('+converse.show_toolbar+') { ]}' + '
    '+ '{[ } ]}' + '