/*! * 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 (console===undefined || console.log===undefined) { console = { log: function () {}, error: function () {} }; } if (typeof define === 'function' && define.amd) { define("converse", [ "components/otr/build/otr", "crypto.aes", "locales", "backbone.localStorage", "jquery.tinysort", "strophe", "strophe.muc", "strophe.roster", "strophe.vcard", "strophe.disco" ], function(otr, crypto) { // Use Mustache style syntax for variable interpolation _.templateSettings = { evaluate : /\{\[([\s\S]+?)\]\}/g, interpolate : /\{\{([\s\S]+?)\}\}/g }; return factory(jQuery, _, crypto, otr, console); } ); } else { // Browser globals _.templateSettings = { evaluate : /\{\[([\s\S]+?)\]\}/g, interpolate : /\{\{([\s\S]+?)\}\}/g }; root.converse = factory(jQuery, _, crypto, otr, console || {log: function(){}}); } }(this, function ($, _, crypto, otr, console) { var converse = {}; converse.initialize = function (settings, callback) { // Default values var converse = this; this.animate = true; this.auto_list_rooms = false; this.auto_subscribe = false; this.bosh_service_url = undefined; // The BOSH connection manager URL. this.debug = false; this.hide_muc_server = false; this.i18n = locales.en; this.prebind = false; this.show_controlbox_by_default = false; this.xhr_user_search = false; this.xhr_custom_status = false; this.testing = false; // Exposes sensitive data for testing. Never set to true in production systems! this.callback = callback || function () {}; var UNENCRYPTED = 'unencrypted'; var UNVERIFIED= 'unverified'; var VERIFIED= 'verified'; var FINISHED = 'finished'; // Allow only the whitelisted settings attributes to be overwritten, // nothing else. whitelist = [ 'animate', 'auto_list_rooms', 'auto_subscribe', 'bosh_service_url', 'fullname', 'debug', 'hide_muc_server', 'i18n', 'prebind', 'show_controlbox_by_default', 'xhr_user_search', 'xhr_custom_status', 'connection', 'testing', 'jid', 'sid', 'rid' ]; _.extend(this, _.pick(settings, whitelist)); 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; }; this.msg_counter = 0; this.autoLink = function (text) { // Convert URLs into hyperlinks var re = /((http|https|ftp):\/\/[\w?=&.\/\-;#~%\-]+(?![\w\s?&.\/;#~%"=\-]*>))/g; return text.replace(re, '$1'); }; 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) { if (this.debug) { console.log(txt); } }; this.onConnect = function (status) { if (status === Strophe.Status.CONNECTED) { converse.log('Connected'); converse.onConnected(); } else if (status === Strophe.Status.DISCONNECTED) { //if ($button) { $button.show().siblings('span').remove(); } converse.giveFeedback(__('Disconnected'), 'error'); //converse.connection.connect(connection.jid, connection.pass, converse.onConnect); } else if (status === Strophe.Status.Error) { if ($button) { $button.show().siblings('span').remove(); } converse.giveFeedback(__('Error'), 'error'); } else if (status === Strophe.Status.CONNECTING) { converse.giveFeedback(__('Connecting')); } else if (status === Strophe.Status.CONNFAIL) { converse.chatboxesview.views.controlbox.trigger('connection-fail'); converse.giveFeedback(__('Connection Failed'), 'error'); } else if (status === Strophe.Status.AUTHENTICATING) { converse.giveFeedback(__('Authenticating')); } else if (status === Strophe.Status.AUTHFAIL) { converse.chatboxesview.views.controlbox.trigger('auth-fail'); converse.giveFeedback(__('Authentication Failed'), 'error'); } else if (status === Strophe.Status.DISCONNECTING) { converse.giveFeedback(__('Disconnecting'), 'error'); converse.connection.connect(connection.jid, connection.pass, converse.onConnect); } else if (status === Strophe.Status.ATTACHED) { converse.log('Attached'); converse.onConnected(); } }; 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.collections = { /* FIXME: XEP-0136 specifies 'urn:xmpp:archive' but the mod_archive_odbc * add-on for ejabberd wants the URL below. This might break for other * Jabber servers. */ 'URI': 'http://www.xmpp.org/extensions/xep-0136.html#ns' }; this.collections.getLastCollection = function (jid, callback) { var bare_jid = Strophe.getBareJidFromJid(jid), iq = $iq({'type':'get'}) .c('list', {'xmlns': this.URI, 'with': bare_jid }) .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'}) .c('before').up() .c('max') .t('1'); converse.connection.sendIQ(iq, callback, function () { converse.log('Error while retrieving collections'); }); }; this.collections.getLastMessages = function (jid, callback) { var that = this; this.getLastCollection(jid, function (result) { // Retrieve the last page of a collection (max 30 elements). var $collection = $(result).find('chat'), jid = $collection.attr('with'), start = $collection.attr('start'), iq = $iq({'type':'get'}) .c('retrieve', {'start': start, 'xmlns': that.URI, 'with': jid }) .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'}) .c('max') .t('30'); converse.connection.sendIQ(iq, callback); }); }; 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') { if (_.contains([UNVERIFIED, VERIFIED], this.get('otr_status'))) { this.initiateOTR(); } this.messages = new converse.Messages(); this.messages.localStorage = new Backbone.LocalStorage( hex_sha1('converse.messages'+this.get('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 saved_key = this.get('priv_key'); var cipher = crypto.lib.PasswordBasedCipher; var pass = converse.connection.pass; if (saved_key) { var decrypted = cipher.decrypt(crypto.algo.AES, saved_key, pass); var key = otr.DSA.parsePrivate(decrypted.toString(crypto.enc.Latin1)); if (cipher.decrypt(crypto.algo.AES, this.get('pass_check'), pass).toString(crypto.enc.Latin1) === 'match') { // Verified that the user's password is still the same return callback(key, this.get('instance_tag')); } } this.trigger('showHelpMessages', [__('Please wait, generating private key...')]); setTimeout($.proxy(function () { // Couldn't get stored key, generate a new one. var key = new otr.DSA(); var instance_tag = otr.OTR.makeInstanceTag(); this.trigger('showHelpMessages', [__('Private key generated.')]); this.save({ 'priv_key': cipher.encrypt(crypto.algo.AES, key.packPrivate(), pass).toString(), 'pass_check': cipher.encrypt(crypto.algo.AES, 'match', pass).toString(), 'instance_tag': instance_tag }); return callback(key, instance_tag); }, this)); }, updateOTRStatus: function (state) { switch (state) { case otr.OTR.CONST.STATUS_AKE_SUCCESS: if (this.otr.msgstate === otr.OTR.CONST.MSGSTATE_ENCRYPTED) { this.save({'otr_status': UNVERIFIED}); } break; case otr.OTR.CONST.STATUS_END_OTR: if (this.otr.msgstate === otr.OTR.CONST.MSGSTATE_FINISHED) { this.save({'otr_status': FINISHED}); } else if (this.otr.msgstate === otr.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\n'+ 'Your 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 (this.otr.trust === true) { this.save({'otr_status': VERIFIED}); } else { this.trigger( 'showHelpMessages', [__("Could not verify identify via the Socialist's Millionaire Protocol.")], '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.getSession($.proxy(function (key, instance_tag) { this.otr = new otr.OTR({ fragment_size: 140, send_interval: 200, priv: key, instance_tag: 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)); if (query_msg) { this.otr.receiveMsg(query_msg); } else { this.otr.sendQueryMsg(); } }, this)); }, createMessage: function (message) { var $message = $(message), body = converse.autoLink($message.children('body').text()), from = Strophe.getBareJidFromJid($message.attr('from')), composing = $message.find('composing'), delayed = $message.find('delay').length > 0, fullname = (this.get('fullname')||'').split(' ')[0], stamp, time, sender; 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 ? converse.autoLink($body.text()) : undefined); if (text) { if (_.contains([UNVERIFIED, VERIFIED], this.get('otr_status'))) { this.otr.receiveMsg(text); } else { if (text.match(/^\?OTR/)) { // They want to initiate OTR if (!this.otr) { this.initiateOTR(text); } else { this.otr.receiveMsg(text); } } else { // Normal unencrypted message. this.createMessage(message); } } } else { // No , so probably typing notification this.createMessage(message); } } }); this.ChatBoxView = Backbone.View.extend({ length: 200, tagName: 'div', className: 'chatbox', events: { 'click .close-chatbox-button': 'closeChat', 'keypress textarea.chat-textarea': 'keyPressed', 'click .toggle-otr': 'toggleOTRMenu', 'click .start-otr': 'startOTRFromToolbar', 'click .end-otr': 'endOTR', 'click .auth-otr': 'authOTR' }, template: _.template( '
' + '' + '' + '
{{ fullname }}
' + '
' + '

' + '

' + '
' + '
' + ''+ '