/*! * 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", "converse-templates"], function(otr, templates) { if (typeof otr !== "undefined") { return factory(jQuery, _, otr.OTR, otr.DSA, console, templates); } else { return factory(jQuery, _, undefined, undefined, console, templates); } } ); } else { // Browser globals // FIXME _.templateSettings = { evaluate : /\{\[([\s\S]+?)\]\}/g, interpolate : /\{\{([\s\S]+?)\}\}/g }; root.converse = factory(jQuery, _, OTR, DSA, console || {log: function(){}}); } }(this, function ($, _, OTR, DSA, console, templates) { $.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 = { templates: templates, 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.refresh = function () { // TODO: only do this for webkit browsers var conversejs = document.getElementById('conversejs'); conversejs.style.display = 'none'; conversejs.offsetHeight; // no need to store this anywhere, the reference is enough conversejs.style.display = 'block'; }; 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_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.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_subscribe', 'bosh_service_url', 'connection', 'debug', 'fullname', 'hide_muc_server', 'i18n', 'jid', 'prebind', 'rid', 'show_controlbox_by_default', 'show_emoticons', 'show_only_online_users', 'show_toolbar', 'show_call_button', '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 // ---------------------- // TODO: REMOVE this.createLinks = 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, 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.onConnect = function (status) { var $button, $form; if (status === Strophe.Status.CONNECTED) { converse.log('Connected'); converse.onConnected(); } else if (status === Strophe.Status.DISCONNECTED) { $form = $('#converse-login'); $button = $form.find('input[type=submit]'); if ($button) { $button.show().siblings('span').remove(); } converse.giveFeedback(__('Disconnected'), 'error'); converse.connection.connect( converse.connection.jid, converse.connection.pass, converse.onConnect ); } else if (status === Strophe.Status.Error) { $form = $('#converse-login'); $button = $form.find('input[type=submit]'); 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'); } 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.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.initRoster = function () { // Set up the roster this.roster = new this.RosterItems(); this.roster.localStorage = new Backbone.LocalStorage( hex_sha1('converse.rosteritems-'+converse.bare_jid)); // Register callbacks that depend on the roster this.connection.roster.registerCallback( $.proxy(this.roster.rosterHandler, this.roster), null, 'presence', null); this.connection.addHandler( $.proxy(this.roster.subscribeToSuggestedItems, this.roster), 'http://jabber.org/protocol/rosterx', 'message', null); this.connection.addHandler( $.proxy(function (presence) { this.presenceHandler(presence); return true; }, this.roster), null, 'presence', null); // No create the view which will fetch roster items from // localStorage this.rosterview = new this.RosterView({'model':this.roster}); }; 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 () {}); $(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.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.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')+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 () { // XXX: sessionStorage is not supported in IE < 8. Perhaps a // user alert is required here... var saved_key = window.sessionStorage[hex_sha1(this.id+'priv_key')]; var instance_tag = window.sessionStorage[hex_sha1(this.id+'instance_tag')]; var cipher = CryptoJS.lib.PasswordBasedCipher; var pass = converse.connection.pass; var pass_check = this.get('pass_check'); var result, key; if (saved_key && instance_tag && typeof pass_check !== 'undefined') { var decrypted = cipher.decrypt(CryptoJS.algo.AES, saved_key, pass); 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 user's password is still the same this.trigger('showHelpMessages', [__('Re-establishing encrypted session')]); return { 'key': key, 'instance_tag': instance_tag }; } } // We need to generate a new key and instance tag result = alert(__('Your browser needs to generate a private key, which will be used in your encrypted chat session. This can take up to 30 seconds during which your browser might freeze and become unresponsive.')); instance_tag = OTR.makeInstanceTag(); key = new DSA(); // Encrypt the key and set in sessionStorage. Also store // instance tag window.sessionStorage[hex_sha1(this.id+'priv_key')] = cipher.encrypt(CryptoJS.algo.AES, key.packPrivate(), pass).toString(); window.sessionStorage[hex_sha1(this.id+'instance_tag')] = instance_tag; this.trigger('showHelpMessages', [__('Private key generated.')]); this.save({'pass_check': cipher.encrypt(CryptoJS.algo.AES, 'match', pass).toString()}); return { 'key': key, 'instance_tag': instance_tag }; }, 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(); 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)); if (query_msg) { this.otr.receiveMsg(query_msg); } else { this.otr.sendQueryMsg(); } }, 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 (_.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); } } } }); 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' }, initialize: function (){ this.model.messages.on('add', this.onMessageAdded, this); this.model.on('show', this.show, this); this.model.on('destroy', this.hide, this); this.model.on('change', this.onChange, this); this.model.on('showOTRError', this.showOTRError, this); this.model.on('buddyStartsOTR', this.buddyStartsOTR, this); this.model.on('showHelpMessages', this.showHelpMessages, this); this.model.on('sendMessageStanza', this.sendMessageStanza, this); this.model.on('showSentOTRMessage', function (text) { this.showOTRMessage(text, 'me'); }, this); this.model.on('showReceivedOTRMessage', function (text) { this.showOTRMessage(text, 'them'); }, this); this.updateVCard(); this.$el.appendTo(converse.chatboxesview.$el); this.render().show().model.messages.fetch({add: true}); if (this.model.get('status')) { this.showStatusMessage(this.model.get('status')); } }, render: function () { this.$el.attr('id', this.model.get('box_id')) .html( converse.templates.chatbox( _.extend( this.model.toJSON(), { show_toolbar: converse.show_toolbar, label_personal_message: __('Personal message') } ) ) ); this.renderToolbar().renderAvatar(); return this; }, showStatusNotification: function (message, replace) { var $chat_content = this.$el.find('.chat-content'); $chat_content.find('div.chat-event').remove().end() .append($('
').text(message)); this.scrollDown(); }, showMessage: function ($el, msg_dict) { var this_date = converse.parseISO8601(msg_dict.time), text = msg_dict.message, match = text.match(/^\/(.*?)(?: (.*))?$/), sender = msg_dict.sender, template, username; if ((match) && (match[1] === 'me')) { text = text.replace(/^\/me/, ''); template = converse.templates.action_template; username = msg_dict.fullname; } else { template = converse.templates.message; username = sender === 'me' && __('me') || msg_dict.fullname; } $el.find('div.chat-event').remove(); var message = template({ 'sender': sender, 'time': this_date.toTimeString().substring(0,5), 'username': username, 'message': '', 'extra_classes': msg_dict.delayed && 'delayed' || '' }); $el.append($(message).children('.chat-message-content').first().text(text).addHyperlinks().addEmoticons().parent()); return this.scrollDown(); }, showOTRMessage: function (text, sender) { /* "Off-the-record" messages are encrypted and not stored at all, * so we don't have a backbone converse.Message object to work with. */ var username = sender === 'me' && sender || this.model.get('fullname'); var $el = this.$el.find('.chat-content'); $el.find('div.chat-event').remove(); $el.append( converse.templates.message({ 'sender': sender, 'time': (new Date()).toTimeString().substring(0,5), 'message': text, 'username': username, 'extra_classes': '' })); return this.scrollDown(); }, showHelpMessages: function (msgs, type) { var $chat_content = this.$el.find('.chat-content'), i, msgs_length = msgs.length; for (i=0; i'+msgs[i]+'')); } return this.scrollDown(); }, onMessageAdded: function (message) { var time = message.get('time'), times = this.model.messages.pluck('time'), this_date = converse.parseISO8601(time), $chat_content = this.$el.find('.chat-content'), previous_message, idx, prev_date, isodate, text, match; // If this message is on a different day than the one received // prior, then indicate it on the chatbox. idx = _.indexOf(times, time)-1; if (idx >= 0) { previous_message = this.model.messages.at(idx); prev_date = converse.parseISO8601(previous_message.get('time')); isodate = new Date(this_date.getTime()); isodate.setUTCHours(0,0,0,0); isodate = converse.toISOString(isodate); if (this.isDifferentDay(prev_date, this_date)) { $chat_content.append(converse.templates.new_day({ isodate: isodate, datestring: this_date.toString().substring(0,15) })); } } if (message.get('composing')) { this.showStatusNotification(message.get('fullname')+' '+'is typing'); return; } else { this.showMessage($chat_content, _.clone(message.attributes)); } if ((message.get('sender') != 'me') && (converse.windowState == 'blur')) { converse.incrementMsgCounter(); } return this.scrollDown(); }, isDifferentDay: function (prev_date, next_date) { return ( (next_date.getDate() != prev_date.getDate()) || (next_date.getFullYear() != prev_date.getFullYear()) || (next_date.getMonth() != prev_date.getMonth())); }, sendMessageStanza: function (text) { /* * Sends the actual XML stanza to the XMPP server. */ // TODO: Look in ChatPartners to see what resources we have for the recipient. // if we have one resource, we sent to only that resources, if we have multiple // we send to the bare jid. var timestamp = (new Date()).getTime(); var bare_jid = this.model.get('jid'); var message = $msg({from: converse.connection.jid, to: bare_jid, type: 'chat', id: timestamp}) .c('body').t(text).up() .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}); // Forward the message, so that other connected resources are also aware of it. // TODO: Forward the message only to other connected resources (inside the browser) var forwarded = $msg({to:converse.bare_jid, type:'chat', id:timestamp}) .c('forwarded', {xmlns:'urn:xmpp:forward:0'}) .c('delay', {xmns:'urn:xmpp:delay',stamp:timestamp}).up() .cnode(message.tree()); converse.connection.send(message); converse.connection.send(forwarded); }, sendMessage: function (text) { var match = text.replace(/^\s*/, "").match(/^\/(.*)\s*$/), msgs; if (match) { if (match[1] === "clear") { this.$el.find('.chat-content').empty(); this.model.messages.reset().localStorage._clear(); return; } else if (match[1] === "help") { msgs = [ '/help:'+__('Show this menu')+'', '/me:'+__('Write in the third person')+'', '/clear:'+__('Remove messages')+'' ]; this.showHelpMessages(msgs); return; } else if ((converse.allow_otr) || (match[1] === "endotr")) { return this.endOTR(); } else if ((converse.allow_otr) || (match[1] === "otr")) { return this.model.initiateOTR(); } } if (_.contains([UNVERIFIED, VERIFIED], this.model.get('otr_status'))) { // Off-the-record encryption is active this.model.otr.sendMsg(text); this.model.trigger('showSentOTRMessage', text); } else { // We only save unencrypted messages. var fullname = converse.xmppstatus.get('fullname'); fullname = _.isEmpty(fullname)? converse.bare_jid: fullname; this.model.messages.create({ fullname: fullname, sender: 'me', time: converse.toISOString(new Date()), message: text }); this.sendMessageStanza(text); } }, keyPressed: function (ev) { var $textarea = $(ev.target), message, notify, composing; if(ev.keyCode == KEY.ENTER) { ev.preventDefault(); message = $textarea.val(); $textarea.val('').focus(); if (message !== '') { if (this.model.get('chatroom')) { this.sendChatRoomMessage(message); } else { this.sendMessage(message); } converse.emit('onMessageSend', message); } this.$el.data('composing', false); } else if (!this.model.get('chatroom')) { // composing data is only for single user chat composing = this.$el.data('composing'); if (!composing) { if (ev.keyCode != 47) { // We don't send composing messages if the message // starts with forward-slash. notify = $msg({'to':this.model.get('jid'), 'type': 'chat'}) .c('composing', {'xmlns':'http://jabber.org/protocol/chatstates'}); converse.connection.send(notify); } this.$el.data('composing', true); } } }, insertEmoticon: function (ev) { ev.stopPropagation(); this.$el.find('.toggle-smiley ul').slideToggle(200); var $textbox = this.$el.find('textarea.chat-textarea'); var value = $textbox.val(); var $target = $(ev.target); $target = $target.is('a') ? $target : $target.children('a'); if (value && (value[value.length-1] !== ' ')) { value = value + ' '; } $textbox.focus().val(value+$target.data('emoticon')+' '); }, toggleEmoticonMenu: function (ev) { ev.stopPropagation(); this.$el.find('.toggle-smiley ul').slideToggle(200); }, toggleOTRMenu: function (ev) { ev.stopPropagation(); this.$el.find('.toggle-otr ul').slideToggle(200); }, showOTRError: function (msg) { if (msg == 'Message cannot be sent at this time.') { this.showHelpMessages( [__('Your message could not be sent')], 'error'); } else if (msg == 'Received an unencrypted message.') { this.showHelpMessages( [__('We received an unencrypted message')], 'error'); } else if (msg == 'Received an unreadable encrypted message.') { this.showHelpMessages( [__('We received an unreadable encrypted message')], 'error'); } else { this.showHelpMessages(['Encryption error occured: '+msg], 'error'); } console.log("OTR ERROR:"+msg); }, buddyStartsOTR: function (ev) { this.showHelpMessages([__('This user has requested an encrypted session.')]); this.model.initiateOTR(); }, startOTRFromToolbar: function (ev) { $(ev.target).parent().parent().slideUp(); ev.stopPropagation(); this.model.initiateOTR(); }, endOTR: function (ev) { if (typeof ev !== "undefined") { ev.preventDefault(); ev.stopPropagation(); } this.model.endOTR(); }, authOTR: function (ev) { var scheme = $(ev.target).data().scheme; var result, question, answer; if (scheme === 'fingerprint') { result = confirm(__('Here are the fingerprints, please confirm them with %1$s, outside of this chat.\n\nFingerprint for you, %2$s: %3$s\n\nFingerprint for %1$s: %4$s\n\nIf you have confirmed that the fingerprints match, click OK, otherwise click Cancel.', [ this.model.get('fullname'), converse.xmppstatus.get('fullname')||converse.bare_jid, this.model.otr.priv.fingerprint(), this.model.otr.their_priv_pk.fingerprint() ] )); if (result === true) { this.model.save({'otr_status': VERIFIED}); } else { this.model.save({'otr_status': UNVERIFIED}); } } else if (scheme === 'smp') { alert(__('You will be prompted to provide a security question and then an answer to that question.\n\nYour buddy will then be prompted the same question and if they type the exact same answer (case sensitive), their identity will have been verified.')); question = prompt(__('What is your security question?')); if (question) { answer = prompt(__('What is the answer to the security question?')); this.model.otr.smpSecret(answer, question); } } else { this.showHelpMessages([__('Invalid authentication scheme provided')], 'error'); } }, toggleCall: function (ev) { ev.stopPropagation(); converse.emit('onCallButtonClicked', { connection: converse.connection, model: this.model }); }, onChange: function (item, changed) { if (_.has(item.changed, 'chat_status')) { var chat_status = item.get('chat_status'), fullname = item.get('fullname'); fullname = _.isEmpty(fullname)? item.get('jid'): fullname; if (this.$el.is(':visible')) { if (chat_status === 'offline') { this.showStatusNotification(fullname+' '+'has gone offline'); } else if (chat_status === 'away') { this.showStatusNotification(fullname+' '+'has gone away'); } else if ((chat_status === 'dnd')) { this.showStatusNotification(fullname+' '+'is busy'); } else if (chat_status === 'online') { this.$el.find('div.chat-event').remove(); } } converse.emit('onBuddyStatusChanged', item.attributes, item.get('chat_status')); } if (_.has(item.changed, 'status')) { this.showStatusMessage(item.get('status')); converse.emit('onBuddyStatusMessageChanged', item.attributes, item.get('status')); } if (_.has(item.changed, 'image')) { this.renderAvatar(); } if (_.has(item.changed, 'otr_status')) { this.renderToolbar().informOTRChange(); } // TODO check for changed fullname as well }, showStatusMessage: function (msg) { this.$el.find('p.user-custom-message').text(msg).attr('title', msg); }, closeChat: function () { if (converse.connection) { this.model.destroy(); } else { this.model.trigger('hide'); } }, updateVCard: function () { var jid = this.model.get('jid'), rosteritem = converse.roster.get(jid); if ((rosteritem) && (!rosteritem.get('vcard_updated'))) { converse.getVCard( jid, $.proxy(function (jid, fullname, image, image_type, url) { this.model.save({ 'fullname' : fullname || jid, 'url': url, 'image_type': image_type, 'image': image }); }, this), $.proxy(function (stanza) { converse.log("ChatBoxView.initialize: An error occured while fetching vcard"); }, this) ); } }, informOTRChange: function () { var data = this.model.toJSON(); var msgs = []; if (data.otr_status == UNENCRYPTED) { msgs.push(__("Your messages are not encrypted anymore")); } else if (data.otr_status == UNVERIFIED){ msgs.push(__("Your messages are now encrypted but your buddy's identity has not been verified.")); } else if (data.otr_status == VERIFIED){ msgs.push(__("Your buddy's identify has been verified.")); } else if (data.otr_status == FINISHED){ msgs.push(__("Your buddy has ended encryption on their end, you should do the same.")); } return this.showHelpMessages(msgs); }, renderToolbar: function () { if (converse.show_toolbar) { var data = this.model.toJSON(); if (data.otr_status == UNENCRYPTED) { data.otr_tooltip = __('Your messages are not encrypted. Click here to enable OTR encryption.'); } else if (data.otr_status == UNVERIFIED){ data.otr_tooltip = __('Your messages are encrypted, but your buddy has not been verified.'); } else if (data.otr_status == VERIFIED){ data.otr_tooltip = __('Your messages are encrypted and your buddy verified.'); } else if (data.otr_status == FINISHED){ data.otr_tooltip = __('Your buddy has closed their end of the private session, you should do the same'); } this.$el.find('.chat-toolbar').html( converse.templates.toolbar( _.extend(data, { FINISHED: FINISHED, UNENCRYPTED: UNENCRYPTED, UNVERIFIED: UNVERIFIED, VERIFIED: VERIFIED, allow_otr: converse.allow_otr && !this.is_chatroom, label_end_encrypted_conversation: __('End encrypted conversation'), label_refresh_encrypted_conversation: __('Refresh encrypted conversation'), label_start_encrypted_conversation: __('Start encrypted conversation'), label_verify_with_fingerprints: __('Verify with fingerprints'), label_verify_with_smp: __('Verify with SMP'), label_whats_this: __("What\'s this?"), otr_status_class: OTR_CLASS_MAPPING[data.otr_status], otr_translated_status: OTR_TRANSLATED_MAPPING[data.otr_status], show_call_button: converse.show_call_button, show_emoticons: converse.show_emoticons }) ) ); } return this; }, renderAvatar: function () { if (!this.model.get('image')) { return; } var img_src = 'data:'+this.model.get('image_type')+';base64,'+this.model.get('image'), canvas = $('').get(0); if (!(canvas.getContext && canvas.getContext('2d'))) { return this; } var ctx = canvas.getContext('2d'); var img = new Image(); // Create new Image object img.onload = function() { var ratio = img.width/img.height; ctx.drawImage(img, 0,0, 35*ratio, 35); }; img.src = img_src; this.$el.find('.chat-title').before(canvas); return this; }, focus: function () { this.$el.find('.chat-textarea').focus(); return this; }, hide: function () { if (this.$el.is(':visible') && this.$el.css('opacity') == "1") { if (converse.animate) { this.$el.hide('fast'); } else { this.$el.hide(); } converse.emit('onChatBoxClosed', this); } }, show: function (callback) { if (this.$el.is(':visible') && this.$el.css('opacity') == "1") { converse.emit('onChatBoxFocused', this); return this.focus(); } if (converse.animate) { this.$el.css({'opacity': 0, 'display': 'inline'}).animate({opacity: '1'}, 200, null, callback); } else { this.$el.css({'opacity': 1, 'display': 'inline'}); callback(); } if (converse.connection) { // Without a connection, we haven't yet initialized // localstorage this.model.save(); } converse.emit('onChatBoxOpened', this); return this; }, scrollDown: function () { var $content = this.$el.find('.chat-content'); $content.scrollTop($content[0].scrollHeight); return this; } }); this.ContactsPanel = Backbone.View.extend({ tagName: 'div', className: 'controlbox-pane', id: 'users', events: { 'click a.toggle-xmpp-contact-form': 'toggleContactForm', 'submit form.add-xmpp-contact': 'addContactFromForm', 'submit form.search-xmpp-contact': 'searchContacts', 'click a.subscribe-to-user': 'addContactFromList' }, initialize: function (cfg) { cfg.$parent.append(this.$el); this.$tabs = cfg.$parent.parent().find('#controlbox-tabs'); }, render: function () { var markup; var widgets = converse.templates.contacts_panel({ label_online: __('Online'), label_busy: __('Busy'), label_away: __('Away'), label_offline: __('Offline') }); this.$tabs.append(converse.templates.contacts_tab({label_contacts: __('Contacts')})); if (converse.xhr_user_search) { markup = converse.templates.search_contact({ label_contact_name: __('Contact name'), label_search: __('Search') }); } else { markup = converse.templates.add_contact_form({ label_contact_username: __('Contact username'), label_add: __('Add') }); } if (converse.allow_contact_requests) { widgets += converse.templates.add_contact_dropdown({ label_click_to_chat: __('Click to add new chat contacts'), label_add_contact: __('Add a contact') }); } this.$el.html(widgets); this.$el.find('.search-xmpp ul').append(markup); this.$el.append(converse.rosterview.$el); return this; }, toggleContactForm: function (ev) { ev.preventDefault(); this.$el.find('.search-xmpp').toggle('fast', function () { if ($(this).is(':visible')) { $(this).find('input.username').focus(); } }); }, searchContacts: function (ev) { ev.preventDefault(); $.getJSON(xhr_user_search_url+ "?q=" + $(ev.target).find('input.username').val(), function (data) { var $ul= $('.search-xmpp ul'); $ul.find('li.found-user').remove(); $ul.find('li.chat-info').remove(); if (!data.length) { $ul.append('
  • '+__('No users found')+'
  • '); } $(data).each(function (idx, obj) { $ul.append( $('
  • ') .append( $('') .attr('data-recipient', Strophe.escapeNode(obj.id)+'@'+converse.domain) .text(obj.fullname) ) ); }); }); }, addContactFromForm: function (ev) { ev.preventDefault(); var $input = $(ev.target).find('input'); var jid = $input.val(); if (! jid) { // this is not a valid JID $input.addClass('error'); return; } this.addContact(jid); $('.search-xmpp').hide(); }, addContactFromList: function (ev) { ev.preventDefault(); var $target = $(ev.target), jid = $target.attr('data-recipient'), name = $target.text(); this.addContact(jid, name); $target.parent().remove(); $('.search-xmpp').hide(); }, addContact: function (jid, name) { name = _.isEmpty(name)? jid: name; converse.connection.roster.add(jid, name, [], function (iq) { converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname')); }); } }); this.RoomsPanel = Backbone.View.extend({ tagName: 'div', id: 'chatrooms', events: { 'submit form.add-chatroom': 'createChatRoom', 'click input#show-rooms': 'showRooms', 'click a.open-room': 'createChatRoom', 'click a.room-info': 'showRoomInfo' }, initialize: function (cfg) { cfg.$parent.append( this.$el.html( converse.templates.room_panel({ 'server_input_type': converse.hide_muc_server && 'hidden' || 'text', 'label_room_name': __('Room name'), 'label_nickname': __('Nickname'), 'label_server': __('Server'), 'label_join': __('Join'), 'label_show_rooms': __('Show rooms') }) ).hide()); this.$tabs = cfg.$parent.parent().find('#controlbox-tabs'); this.on('update-rooms-list', function (ev) { this.updateRoomsList(); }); converse.xmppstatus.on("change", $.proxy(function (model) { if (!(_.has(model.changed, 'fullname'))) { return; } var $nick = this.$el.find('input.new-chatroom-nick'); if (! $nick.is(':focus')) { $nick.val(model.get('fullname')); } }, this)); }, render: function () { this.$tabs.append(converse.templates.chatrooms_tab({label_rooms: __('Rooms')})); return this; }, informNoRoomsFound: function () { var $available_chatrooms = this.$el.find('#available-chatrooms'); // # For translators: %1$s is a variable and will be replaced with the XMPP server name $available_chatrooms.html('
    '+__('No rooms on %1$s',this.muc_domain)+'
    '); $('input#show-rooms').show().siblings('span.spinner').remove(); }, updateRoomsList: function (domain) { converse.connection.muc.listRooms( this.muc_domain, $.proxy(function (iq) { // Success var name, jid, i, fragment, that = this, $available_chatrooms = this.$el.find('#available-chatrooms'); this.rooms = $(iq).find('query').find('item'); if (this.rooms.length) { // # For translators: %1$s is a variable and will be // # replaced with the XMPP server name $available_chatrooms.html('
    '+__('Rooms on %1$s',this.muc_domain)+'
    '); fragment = document.createDocumentFragment(); for (i=0; i'); this.muc_domain = server; this.updateRoomsList(); }, showRoomInfo: function (ev) { var target = ev.target, $dd = $(target).parent('dd'), $div = $dd.find('div.room-info'); if ($div.length) { $div.remove(); } else { $dd.find('span.spinner').remove(); $dd.append(''); converse.connection.disco.info( $(target).attr('data-room-jid'), null, $.proxy(function (stanza) { var $stanza = $(stanza); // All MUC features found here: http://xmpp.org/registrar/disco-features.html $dd.find('span.spinner').replaceWith( converse.templates.room_description({ 'desc': $stanza.find('field[var="muc#roominfo_description"] value').text(), 'occ': $stanza.find('field[var="muc#roominfo_occupants"] value').text(), 'hidden': $stanza.find('feature[var="muc_hidden"]').length, 'membersonly': $stanza.find('feature[var="muc_membersonly"]').length, 'moderated': $stanza.find('feature[var="muc_moderated"]').length, 'nonanonymous': $stanza.find('feature[var="muc_nonanonymous"]').length, 'open': $stanza.find('feature[var="muc_open"]').length, 'passwordprotected': $stanza.find('feature[var="muc_passwordprotected"]').length, 'persistent': $stanza.find('feature[var="muc_persistent"]').length, 'publicroom': $stanza.find('feature[var="muc_public"]').length, 'semianonymous': $stanza.find('feature[var="muc_semianonymous"]').length, 'temporary': $stanza.find('feature[var="muc_temporary"]').length, 'unmoderated': jstanza.find('feature[var="muc_unmoderated"]').length, 'label_desc': __('Description:'), 'label_occ': __('Occupants:'), 'label_features': __('Features:'), 'label_requires_auth': __('Requires authentication'), 'label_hidden': __('Hidden'), 'label_requires_invite': __('Requires an invitation'), 'label_moderated': __('Moderated'), 'label_non_anon': __('Non-anonymous'), 'label_open_room': __('Open room'), 'label_permanent_room': __('Permanent room'), 'label_public': __('Public'), 'label_semi_anon': _('Semi-anonymous'), 'label_temp_room': _('Temporary room'), 'label_unmoderated': __('Unmoderated') })); }, this)); } }, createChatRoom: function (ev) { ev.preventDefault(); var name, $name, server, $server, jid, $nick = this.$el.find('input.new-chatroom-nick'), nick = $nick.val(), chatroom; if (!nick) { $nick.addClass('error'); } else { $nick.removeClass('error'); } if (ev.type === 'click') { jid = $(ev.target).attr('data-room-jid'); } else { $name = this.$el.find('input.new-chatroom-name'); $server= this.$el.find('input.new-chatroom-server'); server = $server.val(); name = $name.val().trim().toLowerCase(); $name.val(''); // Clear the input if (name && server) { jid = Strophe.escapeNode(name) + '@' + server; $name.removeClass('error'); $server.removeClass('error'); this.muc_domain = server; } else { if (!name) { $name.addClass('error'); } if (!server) { $server.addClass('error'); } return; } } if (!nick) { return; } chatroom = converse.chatboxesview.showChatBox({ 'id': jid, 'jid': jid, 'name': Strophe.unescapeNode(Strophe.getNodeFromJid(jid)), 'nick': nick, 'chatroom': true, 'box_id' : hex_sha1(jid) }); if (!chatroom.get('connected')) { converse.chatboxesview.views[jid].connect(null); } } }); this.ControlBoxView = converse.ChatBoxView.extend({ tagName: 'div', className: 'chatbox', id: 'controlbox', events: { 'click a.close-chatbox-button': 'closeChat', 'click ul#controlbox-tabs li a': 'switchTab' }, initialize: function () { this.$el.appendTo(converse.chatboxesview.$el); this.model.on('change', $.proxy(function (item, changed) { var i; if (_.has(item.changed, 'connected')) { this.render(); converse.features.on('add', $.proxy(this.featureAdded, this)); // Features could have been added before the controlbox was // initialized. Currently we're only interested in MUC var feature = converse.features.findWhere({'var': 'http://jabber.org/protocol/muc'}); if (feature) { this.featureAdded(feature); } } if (_.has(item.changed, 'visible')) { if (item.changed.visible === true) { this.show(); } } }, this)); this.model.on('show', this.show, this); this.model.on('destroy', this.hide, this); this.model.on('hide', this.hide, this); if (this.model.get('visible')) { this.show(); } }, hide: function (callback) { this.$el.hide('fast', function () { converse.controlboxtoggle.show(function () { converse.refresh(); if (typeof callback === "function") { callback(); } }); }); }, show: function () { converse.controlboxtoggle.hide(); if (this.$el.is(':visible') && this.$el.css('opacity') == "1") { return; } if (converse.animate) { this.$el.css({'opacity': 0, 'display': 'inline'}).animate({opacity: '1'}, 200, null, function () { converse.refresh(); }); } else { this.$el.css({'opacity': 1, 'display': 'inline'}); converse.refresh(); } if (converse.connection) { // Without a connection, we haven't yet initialized // localstorage this.model.save(); } converse.emit('onControlBoxOpened', this); return this; }, featureAdded: function (feature) { if ((feature.get('var') == 'http://jabber.org/protocol/muc') && (converse.allow_muc)) { this.roomspanel.muc_domain = feature.get('from'); var $server= this.$el.find('input.new-chatroom-server'); if (! $server.is(':focus')) { $server.val(this.roomspanel.muc_domain); } if (converse.auto_list_rooms) { this.roomspanel.trigger('update-rooms-list'); } } }, switchTab: function (ev) { ev.preventDefault(); var $tab = $(ev.target), $sibling = $tab.parent().siblings('li').children('a'), $tab_panel = $($tab.attr('href')), $sibling_panel = $($sibling.attr('href')); $sibling_panel.fadeOut('fast', function () { $sibling.removeClass('current'); $tab.addClass('current'); $tab_panel.fadeIn('fast', function () { }); }); }, showHelpMessages: function (msgs) { // Override showHelpMessages in ChatBoxView, for now do nothing. return; }, render: function () { if ((!converse.prebind) && (!converse.connection)) { // Add login panel if the user still has to authenticate this.$el.html(converse.templates.controlbox(this.model.toJSON())); this.loginpanel = new converse.LoginPanel({'$parent': this.$el.find('.controlbox-panes'), 'model': this}); this.loginpanel.render(); } else if (!this.contactspanel) { this.$el.html(converse.templates.controlbox(this.model.toJSON())); this.contactspanel = new converse.ContactsPanel({'$parent': this.$el.find('.controlbox-panes')}); this.contactspanel.render(); converse.xmppstatusview = new converse.XMPPStatusView({'model': converse.xmppstatus}); converse.xmppstatusview.render(); if (converse.allow_muc) { this.roomspanel = new converse.RoomsPanel({'$parent': this.$el.find('.controlbox-panes')}); this.roomspanel.render(); } } return this; } }); this.ChatRoomView = converse.ChatBoxView.extend({ length: 300, tagName: 'div', className: 'chatroom', events: { 'click a.close-chatbox-button': 'closeChat', 'click a.configure-chatroom-button': 'configureChatRoom', 'click .toggle-smiley': 'toggleEmoticonMenu', 'click .toggle-smiley ul li': 'insertEmoticon', 'keypress textarea.chat-textarea': 'keyPressed' }, is_chatroom: true, sendChatRoomMessage: function (body) { var match = body.replace(/^\s*/, "").match(/^\/(.*?)(?: (.*))?$/) || [false], $chat_content; switch (match[1]) { case 'msg': // TODO: Private messages break; case 'clear': this.$el.find('.chat-content').empty(); break; case 'topic': converse.connection.muc.setTopic(this.model.get('jid'), match[2]); break; case 'kick': converse.connection.muc.kick(this.model.get('jid'), match[2]); break; case 'ban': converse.connection.muc.ban(this.model.get('jid'), match[2]); break; case 'op': converse.connection.muc.op(this.model.get('jid'), match[2]); break; case 'deop': converse.connection.muc.deop(this.model.get('jid'), match[2]); break; case 'help': $chat_content = this.$el.find('.chat-content'); msgs = [ '/help:'+__('Show this menu')+'', '/me:'+__('Write in the third person')+'', '/topic:'+__('Set chatroom topic')+'', '/kick:'+__('Kick user from chatroom')+'', '/ban:'+__('Ban user from chatroom')+'', '/clear:'+__('Remove messages')+'' ]; this.showHelpMessages(msgs); break; default: this.last_msgid = converse.connection.muc.groupchat(this.model.get('jid'), body); break; } }, render: function () { this.$el.attr('id', this.model.get('box_id')) .html(converse.templates.chatroom(this.model.toJSON())); return this; }, renderChatArea: function () { if (!this.$el.find('.chat-area').length) { this.$el.find('.chat-body').empty().append( converse.templates.chatarea({ 'show_toolbar': converse.show_toolbar, 'label_message': __('Message') }) ); this.renderToolbar(); } return this; }, connect: function (password) { if (_.has(converse.connection.muc.rooms, this.model.get('jid'))) { // If the room exists, it already has event listeners, so we // doing add them again. converse.connection.muc.join( this.model.get('jid'), this.model.get('nick'), null, null, null, password); } else { converse.connection.muc.join( this.model.get('jid'), this.model.get('nick'), $.proxy(this.onChatRoomMessage, this), $.proxy(this.onChatRoomPresence, this), $.proxy(this.onChatRoomRoster, this), password); } }, initialize: function () { this.connect(null); this.model.messages.on('add', this.onMessageAdded, this); this.model.on('destroy', function (model, response, options) { this.hide(); converse.connection.muc.leave( this.model.get('jid'), this.model.get('nick'), $.proxy(this.onLeave, this), undefined); }, this); this.$el.appendTo(converse.chatboxesview.$el); this.render().show().model.messages.fetch({add: true}); }, onLeave: function () { this.model.set('connected', false); }, renderConfigurationForm: function (stanza) { var $form= this.$el.find('form.chatroom-form'), $stanza = $(stanza), $fields = $stanza.find('field'), title = $stanza.find('title').text(), instructions = $stanza.find('instructions').text(), i, j, options=[]; var input_types = { 'text-private': 'password', 'text-single': 'textline', 'boolean': 'checkbox', 'hidden': 'hidden', 'list-single': 'dropdown' }; $form.find('span.spinner').remove(); $form.append($('').text(title)); if (instructions != title) { $form.append($('

    ').text(instructions)); } for (i=0; i<$fields.length; i++) { $field = $($fields[i]); if ($field.attr('type') == 'list-single') { options = []; $options = $field.find('option'); for (j=0; j<$options.length; j++) { options.push(converse.templates.select_option({ value: $($options[j]).find('value').text(), label: $($options[j]).attr('label') })); } $form.append(this.form_select_template({ name: $field.attr('var'), label: $field.attr('label'), options: options.join('') })); } else if ($field.attr('type') == 'boolean') { $form.append(converse.templates.form_checkbox({ name: $field.attr('var'), type: input_types[$field.attr('type')], label: $field.attr('label') || '', checked: $field.find('value').text() === "1" && 'checked="1"' || '' })); } else { $form.append(converse.templates.form_input({ name: $field.attr('var'), type: input_types[$field.attr('type')], label: $field.attr('label') || '', value: $field.find('value').text() })); } } $form.append(''); $form.append(''); $form.on('submit', $.proxy(this.saveConfiguration, this)); $form.find('input[type=button]').on('click', $.proxy(this.cancelConfiguration, this)); }, saveConfiguration: function (ev) { ev.preventDefault(); var that = this; var $inputs = $(ev.target).find(':input:not([type=button]):not([type=submit])'), count = $inputs.length, configArray = []; $inputs.each(function () { var $input = $(this), value; if ($input.is('[type=checkbox]')) { value = $input.is(':checked') && 1 || 0; } else { value = $input.val(); } var cnode = $(converse.templates.field({ name: $input.attr('name'), value: value }))[0]; configArray.push(cnode); if (!--count) { converse.connection.muc.saveConfiguration( that.model.get('jid'), configArray, $.proxy(that.onConfigSaved, that), $.proxy(that.onErrorConfigSaved, that) ); } }); this.$el.find('div.chatroom-form-container').hide( function () { $(this).remove(); that.$el.find('.chat-area').show(); that.$el.find('.participants').show(); }); }, onConfigSaved: function (stanza) { // XXX }, onErrorConfigSaved: function (stanza) { this.showStatusNotification(__("An error occurred while trying to save the form.")); }, cancelConfiguration: function (ev) { ev.preventDefault(); var that = this; this.$el.find('div.chatroom-form-container').hide( function () { $(this).remove(); that.$el.find('.chat-area').show(); that.$el.find('.participants').show(); }); }, configureChatRoom: function (ev) { ev.preventDefault(); if (this.$el.find('div.chatroom-form-container').length) { return; } this.$el.find('.chat-area').hide(); this.$el.find('.participants').hide(); this.$el.find('.chat-body').append( $('

    '+ '
    '+ ''+ ''+ '
    ')); converse.connection.muc.configure( this.model.get('jid'), $.proxy(this.renderConfigurationForm, this) ); }, submitPassword: function (ev) { ev.preventDefault(); var password = this.$el.find('.chatroom-form').find('input[type=password]').val(); this.$el.find('.chatroom-form-container').replaceWith( ''); this.connect(password); }, renderPasswordForm: function () { this.$el.find('span.centered.spinner').remove(); this.$el.find('.chat-body').append( $('
    '+ '
    '+ ''+__('This chatroom requires a password')+'' + '' + '%1$s has been banned"), 307: ___("%1$s has been kicked out"), 321: ___("%1$s has been removed because of an affiliation change"), 322: ___("%1$s has been removed for not being a member") }, disconnectMessages: { 301: __('You have been banned from this room'), 307: __('You have been kicked from this room'), 321: __("You have been removed from this room because of an affiliation change"), 322: __("You have been removed from this room because the room has changed to members-only and you're not a member"), 332: __("You have been removed from this room because the MUC (Multi-user chat) service is being shut down.") }, showStatusMessages: function ($el, is_self) { /* Check for status codes and communicate their purpose to the user * See: http://xmpp.org/registrar/mucstatus.html */ var $chat_content = this.$el.find('.chat-content'), $stats = $el.find('status'), disconnect_msgs = [], info_msgs = [], action_msgs = [], msgs, i; for (i=0; i<$stats.length; i++) { var stat = $stats[i].getAttribute('code'); if (is_self) { if (_.contains(_.keys(this.disconnectMessages), stat)) { disconnect_msgs.push(this.disconnectMessages[stat]); } } else { if (_.contains(_.keys(this.infoMessages), stat)) { info_msgs.push(this.infoMessages[stat]); } else if (_.contains(_.keys(this.actionInfoMessages), stat)) { action_msgs.push( __(this.actionInfoMessages[stat], Strophe.unescapeNode(Strophe.getResourceFromJid($el.attr('from')))) ); } } } if (disconnect_msgs.length > 0) { for (i=0; i 0, subject = $message.children('subject').text(), match, template, message_datetime, message_date, dates, isodate, stamp; if (delayed) { stamp = $message.find('delay').attr('stamp'); message_datetime = converse.parseISO8601(stamp); } else { message_datetime = new Date(); } // If this message is on a different day than the one received // prior, then indicate it on the chatbox. dates = $chat_content.find("time").map(function(){return $(this).attr("datetime");}).get(); message_date = new Date(message_datetime.getTime()); message_date.setUTCHours(0,0,0,0); isodate = converse.toISOString(message_date); if (_.indexOf(dates, isodate) == -1) { $chat_content.append(converse.templates.new_day({ isodate: isodate, datestring: message_date.toString().substring(0,15) })); } this.showStatusMessages($message); if (subject) { this.$el.find('.chatroom-topic').text(subject).attr('title', subject); // # For translators: the %1$s and %2$s parts will get replaced by the user and topic text respectively // # Example: Topic set by JC Brand to: Hello World! $chat_content.append( converse.templates.info({ 'message': __('Topic set by %1$s to: %2$s', sender, subject) })); } if (!body) { return true; } var display_sender = sender === this.model.get('nick') && 'me' || 'room'; this.showMessage($chat_content, { 'message': body, 'sender': display_sender, 'fullname': sender, 'time': converse.toISOString(message_datetime) }); if (display_sender === 'room') { // We only emit an event if it's not our own message converse.emit('onMessage', message); } return true; }, onChatRoomRoster: function (roster, room) { this.renderChatArea(); var controlboxview = converse.chatboxesview.views.controlbox, roster_size = _.size(roster), $participant_list = this.$el.find('.participant-list'), participants = [], keys = _.keys(roster), i; this.$el.find('.participant-list').empty(); for (i=0; i elements and add option values options.each(function(){ options_list.push(converse.templates.status_option({ 'value': $(this).val(), 'text': this.text })); }); $options_target = this.$el.find("#target dd ul").hide(); $options_target.append(options_list.join('')); $select.remove(); return this; } }); this.Feature = Backbone.Model.extend(); this.Features = Backbone.Collection.extend({ /* Service Discovery * ----------------- * This collection stores Feature Models, representing features * provided by available XMPP entities (e.g. servers) * See XEP-0030 for more details: http://xmpp.org/extensions/xep-0030.html * All features are shown here: http://xmpp.org/registrar/disco-features.html */ model: converse.Feature, initialize: function () { this.localStorage = new Backbone.LocalStorage( hex_sha1('converse.features'+converse.bare_jid)); if (this.localStorage.records.length === 0) { // localStorage is empty, so we've likely never queried this // domain for features yet converse.connection.disco.info(converse.domain, null, $.proxy(this.onInfo, this)); converse.connection.disco.items(converse.domain, null, $.proxy(this.onItems, this)); } else { this.fetch({add:true}); } }, onItems: function (stanza) { $(stanza).find('query item').each($.proxy(function (idx, item) { converse.connection.disco.info( $(item).attr('jid'), null, $.proxy(this.onInfo, this)); }, this)); }, onInfo: function (stanza) { var $stanza = $(stanza); if (($stanza.find('identity[category=server][type=im]').length === 0) && ($stanza.find('identity[category=conference][type=text]').length === 0)) { // This isn't an IM server component return; } $stanza.find('feature').each($.proxy(function (idx, feature) { this.create({ 'var': $(feature).attr('var'), 'from': $stanza.attr('from') }); }, this)); } }); this.LoginPanel = Backbone.View.extend({ tagName: 'div', id: "login-dialog", events: { 'submit form#converse-login': 'authenticate' }, connect: function ($form, jid, password) { if ($form) { $form.find('input[type=submit]').hide().after('