/*!
* Converse.js (Web-based XMPP instant messaging client)
* http://conversejs.org
*
* Copyright (c) 2012, Jan-Carel Brand
* Licensed under the Mozilla Public License (MPL)
*/
// AMD/global registrations
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define("converse",
["converse-dependencies", "converse-templates"],
function (dependencies, templates) {
var otr = dependencies.otr;
if (typeof otr !== "undefined") {
return factory(
dependencies.jQuery,
_,
otr.OTR,
otr.DSA,
templates,
dependencies.moment,
dependencies.utils
);
} else {
return factory(
dependencies.jQuery,
_,
undefined,
undefined,
templates,
dependencies.moment,
dependencies.utils
);
}
}
);
} else {
root.converse = factory(jQuery, _, OTR, DSA, templates, moment, utils);
}
}(this, function ($, _, OTR, DSA, templates, moment, utils) {
// "use strict";
// Cannot use this due to Safari bug.
// See https://github.com/jcbrand/converse.js/issues/196
if (typeof console === "undefined" || typeof console.log === "undefined") {
console = { log: function () {}, error: function () {} };
}
// Configuration of underscore templates (this config is distict to the
// config of requirejs-tpl in main.js). This one is for normal inline
// templates.
// Use Mustache style syntax for variable interpolation
_.templateSettings = {
evaluate : /\{\[([\s\S]+?)\]\}/g,
interpolate : /\{\{([\s\S]+?)\}\}/g
};
var contains = function (attr, query) {
return function (item) {
if (typeof attr === 'object') {
var value = false;
_.each(attr, function (a) {
value = value || item.get(a).toLowerCase().indexOf(query.toLowerCase()) !== -1;
});
return value;
} else if (typeof attr === 'string') {
return item.get(attr).toLowerCase().indexOf(query.toLowerCase()) !== -1;
} else {
throw new Error('Wrong attribute type. Must be string or array.');
}
};
};
contains.not = function (attr, query) {
return function (item) {
return !(contains(attr, query)(item));
};
};
// XXX: these can perhaps be moved to src/polyfills.js
String.prototype.splitOnce = function (delimiter) {
var components = this.split(delimiter);
return [components.shift(), components.join(delimiter)];
};
$.fn.addEmoticons = function () {
if (converse.visible_toolbar_buttons.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(/;\-\)/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(/: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 playNotification = function () {
var audio;
if (converse.play_sounds && typeof Audio !== "undefined"){
audio = new Audio("sounds/msg_received.ogg");
if (audio.canPlayType('/audio/ogg')) {
audio.play();
} else {
audio = new Audio("/sounds/msg_received.mp3");
audio.play();
}
}
};
var converse = {
plugins: {},
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);
},
refreshWebkit: function () {
/* This works around a webkit bug. Refresh the browser's viewport,
* otherwise chatboxes are not moved along when one is closed.
*/
if ($.browser.webkit) {
var conversejs = document.getElementById('conversejs');
conversejs.style.display = 'none';
conversejs.offsetHeight = conversejs.offsetHeight;
conversejs.style.display = 'block';
}
}
};
converse.initialize = function (settings, callback) {
var converse = this;
// Logging
Strophe.log = function (level, msg) { converse.log(level+' '+msg, level); };
Strophe.error = function (msg) { converse.log(msg, 'error'); };
// Add Strophe Namespaces
Strophe.addNamespace('CHATSTATES', 'http://jabber.org/protocol/chatstates');
Strophe.addNamespace('REGISTER', 'jabber:iq:register');
Strophe.addNamespace('XFORM', 'jabber:x:data');
// Add Strophe Statuses
var i = 0;
Object.keys(Strophe.Status).forEach(function (key) {
i = Math.max(i, Strophe.Status[key]);
});
Strophe.Status.REGIFAIL = i + 1;
Strophe.Status.REGISTERED = i + 2;
Strophe.Status.CONFLICT = i + 3;
Strophe.Status.NOTACCEPTABLE = i + 5;
// Constants
// ---------
var UNENCRYPTED = 0;
var UNVERIFIED= 1;
var VERIFIED= 2;
var FINISHED = 3;
var KEY = {
ENTER: 13,
FORWARD_SLASH: 47
};
var STATUS_WEIGHTS = {
'offline': 6,
'unavailable': 5,
'xa': 4,
'away': 3,
'dnd': 2,
'online': 1
};
// XEP-0085 Chat states
// http://xmpp.org/extensions/xep-0085.html
var INACTIVE = 'inactive';
var ACTIVE = 'active';
var COMPOSING = 'composing';
var PAUSED = 'paused';
var GONE = 'gone';
this.TIMEOUTS = { // Set as module attr so that we can override in tests.
'PAUSED': 20000,
'INACTIVE': 90000
};
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")
);
var OPENED = 'opened';
var CLOSED = 'closed';
// Translation machinery
// ---------------------
this.i18n = settings.i18n ? settings.i18n : locales.en;
var __ = $.proxy(utils.__, this);
var ___ = utils.___;
// Default configuration values
// ----------------------------
var default_settings = {
allow_contact_requests: true,
allow_dragresize: true,
allow_logout: true,
allow_muc: true,
allow_otr: true,
allow_registration: true,
animate: true,
auto_list_rooms: false,
auto_reconnect: false,
auto_subscribe: false,
bosh_service_url: undefined, // The BOSH connection manager URL.
cache_otr_key: false,
debug: false,
domain_placeholder: __(" e.g. conversejs.org"), // Placeholder text shown in the domain input on the registration form
default_box_height: 400, // The default height, in pixels, for the control box, chat boxes and chatrooms.
expose_rid_and_sid: false,
forward_messages: false,
hide_muc_server: false,
hide_offline_users: false,
jid: undefined,
keepalive: false,
message_carbons: false,
no_trimming: false, // Set to true for phantomjs tests (where browser apparently has no width)
play_sounds: false,
prebind: false,
providers_link: 'https://xmpp.net/directory.php', // Link to XMPP providers shown on registration page
rid: undefined,
roster_groups: false,
show_controlbox_by_default: false,
show_only_online_users: false,
show_toolbar: true,
sid: undefined,
storage: 'session',
use_otr_by_default: false,
use_vcards: true,
visible_toolbar_buttons: {
'emoticons': true,
'call': false,
'clear': true,
'toggle_participants': true
},
xhr_custom_status: false,
xhr_custom_status_url: '',
xhr_user_search: false,
xhr_user_search_url: ''
};
_.extend(this, default_settings);
// Allow only whitelisted configuration attributes to be overwritten
_.extend(this, _.pick(settings, Object.keys(default_settings)));
if (settings.visible_toolbar_buttons) {
_.extend(
this.visible_toolbar_buttons,
_.pick(settings.visible_toolbar_buttons, [
'emoticons', 'call', 'clear', 'toggle_participants'
]
));
}
$.fx.off = !this.animate;
// Only allow OTR if we have the capability
this.allow_otr = this.allow_otr && HAS_CRYPTO;
// Only use OTR by default if allow OTR is enabled to begin with
this.use_otr_by_default = this.use_otr_by_default && this.allow_otr;
// 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')
};
var DESC_GROUP_TOGGLE = __('Click to hide these contacts');
var HEADER_CURRENT_CONTACTS = __('My contacts');
var HEADER_PENDING_CONTACTS = __('Pending contacts');
var HEADER_REQUESTING_CONTACTS = __('Contact requests');
var HEADER_UNGROUPED = __('Ungrouped');
var LABEL_CONTACTS = __('Contacts');
var LABEL_GROUPS = __('Groups');
var HEADER_WEIGHTS = {};
HEADER_WEIGHTS[HEADER_CURRENT_CONTACTS] = 0;
HEADER_WEIGHTS[HEADER_UNGROUPED] = 1;
HEADER_WEIGHTS[HEADER_REQUESTING_CONTACTS] = 2;
HEADER_WEIGHTS[HEADER_PENDING_CONTACTS] = 3;
// 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').each(function (idx, el) {
var $el = $(el);
$el.addClass('conn-feedback').text(message);
if (klass) {
$el.addClass(klass);
} else {
$el.removeClass('error');
}
});
};
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 contact = converse.roster.get(jid);
if (contact) {
fullname = _.isEmpty(fullname)? contact.get('fullname') || jid: fullname;
contact.save({
'fullname': fullname,
'image_type': img_type,
'image': img,
'url': url,
'vcard_updated': moment().format()
});
}
}
if (callback) {
callback(jid, fullname, img, img_type, url);
}
}, this),
jid,
function (iq) {
// Error callback
var contact = converse.roster.get(jid);
if (contact) {
contact.save({
'vcard_updated': moment().format()
});
}
if (errback) {
errback(jid, iq);
}
}
);
};
this.reconnect = function () {
converse.giveFeedback(__('Reconnecting'), 'error');
converse.emit('reconnect');
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.renderLoginPanel = function () {
converse._tearDown();
var view = converse.chatboxviews.get('controlbox');
view.model.set({connected:false});
view.renderLoginPanel();
};
this.onConnect = function (status, condition, reconnect) {
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) {
if (converse.auto_reconnect) {
converse.reconnect();
} else {
converse.renderLoginPanel();
}
} else if (status === Strophe.Status.Error) {
converse.giveFeedback(__('Error'), 'error');
} else if (status === Strophe.Status.CONNECTING) {
converse.giveFeedback(__('Connecting'));
} else if (status === Strophe.Status.AUTHENTICATING) {
converse.giveFeedback(__('Authenticating'));
} else if (status === Strophe.Status.AUTHFAIL) {
converse.giveFeedback(__('Authentication Failed'), 'error');
converse.connection.disconnect(__('Authentication Failed'));
} else if (status === Strophe.Status.DISCONNECTING) {
if (!converse.connection.connected) {
converse.renderLoginPanel();
}
if (condition) {
converse.giveFeedback(condition, 'error');
}
}
};
this.applyHeightResistance = function (height) {
/* This method applies some resistance/gravity around the
* "default_box_height". If "height" is close enough to
* default_box_height, then that is returned instead.
*/
if (typeof height === 'undefined') {
return converse.default_box_height;
}
var resistance = 10;
if ((height !== converse.default_box_height) &&
(Math.abs(height - converse.default_box_height) < resistance)) {
return converse.default_box_height;
}
return height;
};
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 = b64_sha1('converse.xmppstatus-'+converse.bare_jid);
this.xmppstatus.id = id; // Appears to be necessary for backbone.browserStorage
this.xmppstatus.browserStorage = new Backbone.BrowserStorage[converse.storage](id);
this.xmppstatus.fetch({success: callback, error: callback});
};
this.initSession = function () {
this.session = new this.BOSHSession();
var id = b64_sha1('converse.bosh-session');
this.session.id = id; // Appears to be necessary for backbone.browserStorage
this.session.browserStorage = new Backbone.BrowserStorage[converse.storage](id);
this.session.fetch();
$(window).on('beforeunload', $.proxy(function () {
if (converse.connection.authenticated) {
this.setSession();
} else {
this.clearSession();
}
}, this));
};
this.clearSession = function () {
this.roster.browserStorage._clear();
this.session.browserStorage._clear();
// XXX: this should perhaps go into the beforeunload handler
converse.chatboxes.get('controlbox').save({'connected': false});
};
this.setSession = function () {
if (this.keepalive) {
this.session.save({
jid: this.connection.jid,
rid: this.connection._proto.rid,
sid: this.connection._proto.sid
});
}
};
this.logOut = function () {
converse.chatboxviews.closeAllChatBoxes(false);
converse.clearSession();
converse.connection.disconnect();
};
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();
}
});
$(document).on('mousemove', $.proxy(function (ev) {
if (!this.resized_chatbox || !this.allow_dragresize) { return true; }
ev.preventDefault();
this.resized_chatbox.resizeChatBox(ev);
}, this));
$(document).on('mouseup', $.proxy(function (ev) {
if (!this.resized_chatbox || !this.allow_dragresize) { return true; }
ev.preventDefault();
var height = this.applyHeightResistance(this.resized_chatbox.height);
if (this.connection.connected) {
this.resized_chatbox.model.save({'height': height});
} else {
this.resized_chatbox.model.set({'height': height});
}
this.resized_chatbox = null;
}, this));
$(window).on("blur focus", $.proxy(function (ev) {
if ((this.windowState != ev.type) && (ev.type == 'focus')) {
converse.clearMsgCounter();
}
this.windowState = ev.type;
},this));
$(window).on("resize", _.debounce($.proxy(function (ev) {
this.chatboxviews.trimChats();
},this), 200));
};
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(__('Contacts'));
}, this));
};
this.enableCarbons = function () {
/* Ask the XMPP server to enable Message Carbons
* See XEP-0280 https://xmpp.org/extensions/xep-0280.html#enabling
*/
if (!this.message_carbons || this.session.get('carbons_enabled')) {
return;
}
var carbons_iq = new Strophe.Builder('iq', {
from: this.connection.jid,
id: 'enablecarbons',
type: 'set'
})
.c('enable', {xmlns: 'urn:xmpp:carbons:2'});
this.connection.addHandler($.proxy(function (iq) {
if ($(iq).find('error').length > 0) {
converse.log('ERROR: An error occured while trying to enable message carbons.');
} else {
this.session.save({carbons_enabled: true});
converse.log('Message carbons have been enabled.');
}
}, this), null, "iq", null, "enablecarbons");
this.connection.send(carbons_iq);
};
this.onConnected = function () {
// When reconnecting, there might be some open chat boxes. We don't
// know whether these boxes are of the same account or not, so we
// close them now.
this.chatboxviews.closeAllChatBoxes();
this.setSession();
this.jid = this.connection.jid;
this.bare_jid = Strophe.getBareJidFromJid(this.connection.jid);
this.domain = Strophe.getDomainFromJid(this.connection.jid);
this.minimized_chats = new converse.MinimizedChats({model: this.chatboxes});
this.features = new this.Features();
this.enableCarbons();
this.initStatus($.proxy(function () {
this.chatboxes.onConnected();
this.giveFeedback(__('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('ready');
};
// Backbone Models and Views
// -------------------------
this.OTR = Backbone.Model.extend({
// A model for managing OTR settings.
getSessionPassphrase: function () {
if (converse.prebind) {
var key = b64_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[b64_sha1(jid+'priv_key')] =
cipher.encrypt(CryptoJS.algo.AES, key.packPrivate(), pass).toString();
window.sessionStorage[b64_sha1(jid+'instance_tag')] = instance_tag;
window.sessionStorage[b64_sha1(jid+'pass_check')] =
cipher.encrypt(CryptoJS.algo.AES, 'match', pass).toString();
}
}
return key;
}
});
this.Message = Backbone.Model;
this.Messages = Backbone.Collection.extend({
model: converse.Message
});
this.ChatBox = Backbone.Model.extend({
initialize: function () {
var height = converse.applyHeightResistance(this.get('height'));
if (this.get('box_id') !== 'controlbox') {
this.messages = new converse.Messages();
this.messages.browserStorage = new Backbone.BrowserStorage[converse.storage](
b64_sha1('converse.messages'+this.get('jid')+converse.bare_jid));
this.save({
// The chat_state will be set to ACTIVE once the chat box is opened
// and we listen for change:chat_state, so shouldn't set it to ACTIVE here.
'chat_state': undefined,
'box_id' : b64_sha1(this.get('jid')),
'height': height,
'minimized': this.get('minimized') || false,
'num_unread': this.get('num_unread') || 0,
'otr_status': this.get('otr_status') || UNENCRYPTED,
'time_minimized': this.get('time_minimized') || moment(),
'time_opened': this.get('time_opened') || moment().valueOf(),
'url': '',
'user_id' : Strophe.getNodeFromJid(this.get('jid'))
});
} else {
this.set({
'height': height,
'time_opened': moment(0).valueOf(),
'num_unread': this.get('num_unread') || 0
});
}
},
maximize: function () {
this.save({
'minimized': false,
'time_opened': moment().valueOf()
});
},
minimize: function () {
this.save({
'minimized': true,
'time_minimized': moment().format()
});
},
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[b64_sha1(this.id+'instance_tag')];
saved_key = window.sessionStorage[b64_sha1(this.id+'priv_key')];
pass_check = window.sessionStorage[b64_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 chat contact 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 contact. 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 contact.')]);
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 body = $message.children('body').text(),
delayed = $message.find('delay').length > 0,
fullname = this.get('fullname'),
is_groupchat = $message.attr('type') === 'groupchat',
msgid = $message.attr('id'),
chat_state = $message.find(COMPOSING).length && COMPOSING ||
$message.find(PAUSED).length && PAUSED ||
$message.find(INACTIVE).length && INACTIVE ||
$message.find(ACTIVE).length && ACTIVE ||
$message.find(GONE).length && GONE,
stamp, time, sender, from, createMessage;
if (is_groupchat) {
from = Strophe.unescapeNode(Strophe.getResourceFromJid($message.attr('from')));
} else {
from = Strophe.getBareJidFromJid($message.attr('from'));
}
fullname = (_.isEmpty(fullname) ? from: fullname).split(' ')[0];
if (delayed) {
stamp = $message.find('delay').attr('stamp');
time = stamp;
} else {
time = moment().format();
}
if ((is_groupchat && from === this.get('nick')) || (!is_groupchat && from == converse.bare_jid)) {
sender = 'me';
} else {
sender = 'them';
}
if (!body) {
createMessage = this.messages.add;
} else {
createMessage = this.messages.create;
}
this.messages.create({
chat_state: chat_state,
delayed: delayed,
fullname: fullname,
message: body || undefined,
msgid: msgid,
sender: sender,
time: time
});
},
receiveMessage: 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': 'close',
'click .toggle-chatbox-button': 'minimize',
'keypress textarea.chat-textarea': 'keyPressed',
'focus textarea.chat-textarea': 'chatBoxFocused',
'blur textarea.chat-textarea': 'chatBoxBlurred',
'click .toggle-smiley': 'toggleEmoticonMenu',
'click .toggle-smiley ul li': 'insertEmoticon',
'click .toggle-clear': 'clearMessages',
'click .toggle-otr': 'toggleOTRMenu',
'click .start-otr': 'startOTRFromToolbar',
'click .end-otr': 'endOTR',
'click .auth-otr': 'authOTR',
'click .toggle-call': 'toggleCall',
'mousedown .dragresize-tm': 'onDragResizeStart'
},
initialize: function (){
this.model.messages.on('add', this.onMessageAdded, this);
this.model.on('show', this.show, this);
this.model.on('destroy', this.hide, this);
// TODO check for changed fullname as well
this.model.on('change:chat_state', this.sendChatState, this);
this.model.on('change:chat_status', this.onChatStatusChanged, this);
this.model.on('change:image', this.renderAvatar, this);
this.model.on('change:otr_status', this.onOTRStatusChanged, this);
this.model.on('change:minimized', this.onMinimizedChanged, this);
this.model.on('change:status', this.onStatusChanged, this);
this.model.on('showOTRError', this.showOTRError, this);
this.model.on('showHelpMessages', this.showHelpMessages, this);
this.model.on('sendMessageStanza', this.sendMessageStanza, this);
this.model.on('showSentOTRMessage', function (text) {
this.showMessage({'message': text, 'sender': 'me'});
}, this);
this.model.on('showReceivedOTRMessage', function (text) {
this.showMessage({'message': text, 'sender': 'them'});
}, this);
this.updateVCard();
this.$el.insertAfter(converse.chatboxviews.get("controlbox").$el);
this.render().model.messages.fetch({add: true});
if (this.model.get('minimized')) {
this.hide();
} else {
this.show();
}
if ((_.contains([UNVERIFIED, VERIFIED], this.model.get('otr_status'))) || converse.use_otr_by_default) {
this.model.initiateOTR();
}
},
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();
converse.emit('chatBoxOpened', this);
setTimeout(function () {
converse.refreshWebkit();
}, 50);
return this.showStatusMessage();
},
initDragResize: function () {
this.prev_pageY = 0; // To store last known mouse position
if (converse.connection.connected) {
this.height = this.model.get('height');
}
return this;
},
showStatusNotification: function (message, keep_old) {
var $chat_content = this.$el.find('.chat-content');
if (!keep_old) {
$chat_content.find('div.chat-event').remove();
}
$chat_content.append($('').text(message));
this.scrollDown();
},
clearChatRoomMessages: function (ev) {
if (typeof ev !== "undefined") { ev.stopPropagation(); }
var result = confirm(__("Are you sure you want to clear the messages from this room?"));
if (result === true) {
this.$el.find('.chat-content').empty();
}
return this;
},
showMessage: function (msg_dict) {
var $content = this.$el.find('.chat-content'),
msg_time = moment(msg_dict.time) || moment,
text = msg_dict.message,
match = text.match(/^\/(.*?)(?: (.*))?$/),
fullname = this.model.get('fullname') || msg_dict.fullname,
extra_classes = msg_dict.delayed && 'delayed' || '',
template, username;
if ((match) && (match[1] === 'me')) {
text = text.replace(/^\/me/, '');
template = converse.templates.action;
username = fullname;
} else {
template = converse.templates.message;
username = msg_dict.sender === 'me' && __('me') || fullname;
}
$content.find('div.chat-event').remove();
if (this.is_chatroom && msg_dict.sender == 'them' && (new RegExp("\\b"+this.model.get('nick')+"\\b")).test(text)) {
// Add special class to mark groupchat messages in which we
// are mentioned.
extra_classes += ' mentioned';
}
var message = template({
'sender': msg_dict.sender,
'time': msg_time.format('hh:mm'),
'username': username,
'message': '',
'extra_classes': extra_classes
});
$content.append($(message).children('.chat-message-content').first().text(text).addHyperlinks().addEmoticons().parent());
this.scrollDown();
},
showHelpMessages: function (msgs, type, spinner) {
var $chat_content = this.$el.find('.chat-content'), i,
msgs_length = msgs.length;
for (i=0; i'+msgs[i]+''));
}
if (spinner === true) {
$chat_content.append('');
} else if (spinner === false) {
$chat_content.find('span.spinner').remove();
}
return this.scrollDown();
},
onMessageAdded: function (message) {
var time = message.get('time'),
times = this.model.messages.pluck('time'),
previous_message, idx, this_date, prev_date, 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 = moment(previous_message.get('time'));
if (prev_date.isBefore(time, 'day')) {
this_date = moment(time);
this.$el.find('.chat-content').append(converse.templates.new_day({
isodate: this_date.format("YYYY-MM-DD"),
datestring: this_date.format("dddd MMM Do YYYY")
}));
}
}
if (!message.get('message')) {
if (message.get('chat_state') === COMPOSING) {
this.showStatusNotification(message.get('fullname')+' '+__('is typing'));
return;
} else if (message.get('chat_state') === PAUSED) {
this.showStatusNotification(message.get('fullname')+' '+__('has stopped typing'));
return;
} else if (_.contains([INACTIVE, ACTIVE], message.get('chat_state'))) {
this.$el.find('.chat-content div.chat-event').remove();
return;
} else if (message.get('chat_state') === GONE) {
this.showStatusNotification(message.get('fullname')+' '+__('has gone away'));
return;
}
} else {
this.showMessage(_.clone(message.attributes));
}
if ((message.get('sender') != 'me') && (converse.windowState == 'blur')) {
converse.incrementMsgCounter();
}
return this.scrollDown();
},
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': Strophe.NS.CHATSTATES});
converse.connection.send(message);
if (converse.forward_messages) {
// Forward the message, so that other connected resources are also aware of it.
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(forwarded);
}
},
sendMessage: function (text) {
var match = text.replace(/^\s*/, "").match(/^\/(.*)\s*$/), msgs;
if (match) {
if (match[1] === "clear") {
return this.clearMessages();
}
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: moment().format(),
message: text
});
this.sendMessageStanza(text);
}
},
sendChatState: function () {
/* Sends a message with the status of the user in this chat session
* as taken from the 'chat_state' attribute of the chat box.
* See XEP-0085 Chat State Notifications.
*/
converse.connection.send(
$msg({'to':this.model.get('jid'), 'type': 'chat'})
.c(this.model.get('chat_state'), {'xmlns': Strophe.NS.CHATSTATES})
);
},
setChatState: function (state, no_save) {
/* Mutator for setting the chat state of this chat session.
* Handles clearing of any chat state notification timeouts and
* setting new ones if necessary.
* Timeouts are set when the state being set is COMPOSING or PAUSED.
* After the timeout, COMPOSING will become PAUSED and PAUSED will become INACTIVE.
* See XEP-0085 Chat State Notifications.
*
* Parameters:
* (string) state - The chat state (consts ACTIVE, COMPOSING, PAUSED, INACTIVE, GONE)
* (no_save) no_save - Just do the cleanup or setup but don't actually save the state.
*/
if (_.contains([ACTIVE, INACTIVE, GONE], state)) {
if (typeof this.chat_state_timeout !== 'undefined') {
clearTimeout(this.chat_state_timeout);
delete this.chat_state_timeout;
}
} else if (state === COMPOSING) {
this.chat_state_timeout = setTimeout(
$.proxy(this.setChatState, this), converse.TIMEOUTS.PAUSED, PAUSED);
} else if (state === PAUSED) {
this.chat_state_timeout = setTimeout(
$.proxy(this.setChatState, this), converse.TIMEOUTS.INACTIVE, INACTIVE);
}
if (!no_save && this.model.get('chat_state') != state) {
this.model.set('chat_state', state);
}
return this;
},
keyPressed: function (ev) {
/* Event handler for when a key is pressed in a chat box textarea.
*/
var $textarea = $(ev.target), message;
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('messageSend', message);
}
this.setChatState(ACTIVE);
} else if (!this.model.get('chatroom')) { // chat state data is currently only for single user chat
// Set chat state to composing if keyCode is not a forward-slash
// (which would imply an internal command and not a message).
this.setChatState(COMPOSING, ev.keyCode==KEY.FORWARD_SLASH);
}
},
chatBoxFocused: function (ev) {
ev.preventDefault();
this.setChatState(ACTIVE);
},
chatBoxBlurred: function (ev) {
ev.preventDefault();
this.setChatState(INACTIVE);
},
onDragResizeStart: function (ev) {
if (!converse.allow_dragresize) { return true; }
// Record element attributes for mouseMove().
this.height = this.$el.children('.box-flyout').height();
converse.resized_chatbox = this;
this.prev_pageY = ev.pageY;
},
setChatBoxHeight: function (height) {
if (!this.model.get('minimized')) {
this.$el.children('.box-flyout')[0].style.height = converse.applyHeightResistance(height)+'px';
}
},
resizeChatBox: function (ev) {
var diff = ev.pageY - this.prev_pageY;
if (!diff) { return; }
this.height -= diff;
this.prev_pageY = ev.pageY;
this.setChatBoxHeight(this.height);
},
clearMessages: function (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); }
var result = confirm(__("Are you sure you want to clear the messages from this chat box?"));
if (result === true) {
this.$el.find('.chat-content').empty();
this.model.messages.reset();
this.model.messages.browserStorage._clear();
}
return this;
},
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);
},
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 contact will then be prompted the same question and if they type the exact same answer (case sensitive), their identity will be 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('callButtonClicked', {
connection: converse.connection,
model: this.model
});
},
onChatStatusChanged: function (item) {
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('contactStatusChanged', item.attributes, item.get('chat_status'));
// TODO: DEPRECATED AND SHOULD BE REMOVED IN 0.9.0
converse.emit('buddyStatusChanged', item.attributes, item.get('chat_status'));
},
onStatusChanged: function (item) {
this.showStatusMessage();
converse.emit('contactStatusMessageChanged', item.attributes, item.get('status'));
// TODO: DEPRECATED AND SHOULD BE REMOVED IN 0.9.0
converse.emit('buddyStatusMessageChanged', item.attributes, item.get('status'));
},
onOTRStatusChanged: function (item) {
this.renderToolbar().informOTRChange();
},
onMinimizedChanged: function (item) {
if (item.get('minimized')) {
this.hide();
} else {
this.maximize();
}
},
showStatusMessage: function (msg) {
msg = msg || this.model.get('status');
if (typeof msg === "string") {
this.$el.find('p.user-custom-message').text(msg).attr('title', msg);
}
return this;
},
close: function (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); }
if (converse.connection.connected) {
this.model.destroy();
} else {
this.model.trigger('hide');
}
this.setChatState(INACTIVE);
converse.emit('chatBoxClosed', this);
return this;
},
maximize: function () {
// Restores a minimized chat box
this.$el.insertAfter(converse.chatboxviews.get("controlbox").$el).show('fast', $.proxy(function () {
converse.refreshWebkit();
this.setChatState(ACTIVE).focus();
converse.emit('chatBoxMaximized', this);
}, this));
},
minimize: function (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); }
// Minimizes a chat box
this.setChatState(INACTIVE).model.minimize();
this.$el.hide('fast', converse.refreshwebkit);
converse.emit('chatBoxMinimized', this);
},
updateVCard: function () {
var jid = this.model.get('jid'),
contact = converse.roster.get(jid);
if ((contact) && (!contact.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 contact's identity has not been verified."));
} else if (data.otr_status == VERIFIED){
msgs.push(__("Your contact's identify has been verified."));
} else if (data.otr_status == FINISHED){
msgs.push(__("Your contact has ended encryption on their end, you should do the same."));
}
return this.showHelpMessages(msgs, 'info', false);
},
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 contact has not been verified.');
} else if (data.otr_status == VERIFIED){
data.otr_tooltip = __('Your messages are encrypted and your contact verified.');
} else if (data.otr_status == FINISHED){
data.otr_tooltip = __('Your contact 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_clear: __('Clear all messages'),
label_end_encrypted_conversation: __('End encrypted conversation'),
label_hide_participants: __('Hide the list of participants'),
label_refresh_encrypted_conversation: __('Refresh encrypted conversation'),
label_start_call: __('Start a call'),
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.visible_toolbar_buttons.call,
show_clear_button: converse.visible_toolbar_buttons.clear,
show_emoticons: converse.visible_toolbar_buttons.emoticons,
show_participants_toggle: this.is_chatroom && converse.visible_toolbar_buttons.toggle_participants
})
)
);
}
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();
converse.emit('chatBoxFocused', this);
return this;
},
hide: function () {
if (this.$el.is(':visible') && this.$el.css('opacity') == "1") {
this.$el.hide();
converse.refreshWebkit();
}
return this;
},
show: function (callback) {
if (this.$el.is(':visible') && this.$el.css('opacity') == "1") {
return this.focus();
}
this.$el.fadeIn(callback);
if (converse.connection.connected) {
// Without a connection, we haven't yet initialized
// localstorage
this.model.save();
this.initDragResize();
}
this.setChatState(ACTIVE);
return this;
},
scrollDown: function () {
var $content = this.$('.chat-content');
if ($content.is(':visible')) {
$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'),
label_logout: __('Log out'),
allow_logout: converse.allow_logout
});
this.$tabs.append(converse.templates.contacts_tab({label_contacts: LABEL_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);
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(converse.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',
className: 'controlbox-pane',
id: 'chatrooms',
events: {
'submit form.add-chatroom': 'createChatRoom',
'click input#show-rooms': 'showRooms',
'click a.open-room': 'createChatRoom',
'click a.room-info': 'showRoomInfo',
'change input[name=server]': 'setDomain',
'change input[name=nick]': 'setNick'
},
initialize: function (cfg) {
this.$parent = cfg.$parent;
this.model.on('change:muc_domain', this.onDomainChange, this);
this.model.on('change:nick', this.onNickChange, this);
},
render: function () {
this.$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 Room'),
'label_show_rooms': __('Show rooms')
})
).hide());
this.$tabs = this.$parent.parent().find('#controlbox-tabs');
this.$tabs.append(converse.templates.chatrooms_tab({label_rooms: __('Rooms')}));
return this;
},
onDomainChange: function (model) {
var $server = this.$el.find('input.new-chatroom-server');
$server.val(model.get('muc_domain'));
if (converse.auto_list_rooms) {
this.updateRoomsList();
}
},
onNickChange: function (model) {
var $nick = this.$el.find('input.new-chatroom-nick');
$nick.val(model.get('nick'));
},
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.model.get('muc_domain'))+'
');
$('input#show-rooms').show().siblings('span.spinner').remove();
},
updateRoomsList: function () {
converse.connection.muc.listRooms(
this.model.get('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.model.get('muc_domain'))+'
');
fragment = document.createDocumentFragment();
for (i=0; i');
this.model.save({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': $stanza.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.model.save({muc_domain: server});
} else {
if (!name) { $name.addClass('error'); }
if (!server) { $server.addClass('error'); }
return;
}
}
if (!nick) { return; }
chatroom = converse.chatboxviews.showChat({
'id': jid,
'jid': jid,
'name': Strophe.unescapeNode(Strophe.getNodeFromJid(jid)),
'nick': nick,
'chatroom': true,
'box_id' : b64_sha1(jid)
});
},
setDomain: function (ev) {
this.model.save({muc_domain: ev.target.value});
},
setNick: function (ev) {
this.model.save({nick: ev.target.value});
}
});
this.ControlBoxView = converse.ChatBoxView.extend({
tagName: 'div',
className: 'chatbox',
id: 'controlbox',
events: {
'click a.close-chatbox-button': 'close',
'click ul#controlbox-tabs li a': 'switchTab',
'mousedown .dragresize-tm': 'onDragResizeStart'
},
initialize: function () {
this.$el.insertAfter(converse.controlboxtoggle.$el);
this.model.on('change:connected', this.onConnected, this);
this.model.on('destroy', this.hide, this);
this.model.on('hide', this.hide, this);
this.model.on('show', this.show, this);
this.model.on('change:closed', this.ensureClosedState, this);
this.render();
if (this.model.get('connected')) {
this.initRoster();
}
if (!this.model.get('closed')) {
this.show();
} else {
this.hide();
}
},
giveFeedback: function (message, klass) {
var $el = this.$('.conn-feedback');
$el.addClass('conn-feedback').text(message);
if (klass) {
$el.addClass(klass);
}
},
onConnected: function () {
if (this.model.get('connected')) {
this.render().initRoster();
converse.features.off('add', this.featureAdded, this);
converse.features.on('add', 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);
}
}
},
initRoster: function () {
/* We initialize the roster, which will appear inside the
* Contacts Panel.
*/
converse.roster = new converse.RosterContacts();
converse.roster.browserStorage = new Backbone.BrowserStorage[converse.storage](
b64_sha1('converse.contacts-'+converse.bare_jid));
var rostergroups = new converse.RosterGroups();
rostergroups.browserStorage = new Backbone.BrowserStorage[converse.storage](
b64_sha1('converse.roster.groups'+converse.bare_jid));
converse.rosterview = new converse.RosterView({model: rostergroups});
this.contactspanel.$el.append(converse.rosterview.$el);
converse.rosterview.render().fetch().update();
return this;
},
render: function () {
if (!converse.connection.connected || !converse.connection.authenticated || converse.connection.disconnecting) {
// TODO: we might need to take prebinding into consideration here.
this.renderLoginPanel();
} else if (!this.contactspanel || !this.contactspanel.$el.is(':visible')) {
this.renderContactsPanel();
}
return this;
},
renderLoginPanel: function () {
var $feedback = this.$('.conn-feedback'); // we want to still show any existing feedback.
this.$el.html(converse.templates.controlbox(this.model.toJSON()));
var cfg = {'$parent': this.$el.find('.controlbox-panes'), 'model': this};
if (!this.loginpanel) {
this.loginpanel = new converse.LoginPanel(cfg);
if (converse.allow_registration) {
this.registerpanel = new converse.RegisterPanel(cfg);
}
} else {
this.loginpanel.delegateEvents().initialize(cfg);
if (converse.allow_registration) {
this.registerpanel.delegateEvents().initialize(cfg);
}
}
this.loginpanel.render();
if (converse.allow_registration) {
this.registerpanel.render().$el.hide();
}
this.initDragResize();
if ($feedback.length) {
this.$('.conn-feedback').replaceWith($feedback);
}
return this;
},
renderContactsPanel: function () {
var model;
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'),
'model': new (Backbone.Model.extend({
id: b64_sha1('converse.roomspanel'+converse.bare_jid), // Required by sessionStorage
browserStorage: new Backbone.BrowserStorage[converse.storage](
b64_sha1('converse.roomspanel'+converse.bare_jid))
}))()
});
this.roomspanel.render().model.fetch();
if (!this.roomspanel.model.get('nick')) {
this.roomspanel.model.save({nick: Strophe.getNodeFromJid(converse.bare_jid)});
}
}
this.initDragResize();
},
close: function (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); }
if (converse.connection.connected) {
this.model.save({'closed': true});
} else {
this.model.trigger('hide');
}
converse.emit('controlBoxClosed', this);
return this;
},
ensureClosedState: function () {
if (this.model.get('closed')) {
this.hide();
} else {
this.show();
}
},
hide: function (callback) {
this.$el.hide('fast', function () {
converse.refreshWebkit();
converse.emit('chatBoxClosed', this);
converse.controlboxtoggle.show(function () {
if (typeof callback === "function") {
callback();
}
});
});
return this;
},
show: function () {
converse.controlboxtoggle.hide($.proxy(function () {
this.$el.show('fast', function () {
if (converse.rosterview) {
converse.rosterview.update();
}
converse.refreshWebkit();
}.bind(this));
converse.emit('controlBoxOpened', this);
}, this));
return this;
},
featureAdded: function (feature) {
if ((feature.get('var') == 'http://jabber.org/protocol/muc') && (converse.allow_muc)) {
this.roomspanel.model.save({muc_domain: feature.get('from')});
var $server= this.$el.find('input.new-chatroom-server');
if (! $server.is(':focus')) {
$server.val(this.roomspanel.model.get('muc_domain'));
}
}
},
switchTab: function (ev) {
// TODO: automatically focus the relevant input
if (ev && ev.preventDefault) { ev.preventDefault(); }
var $tab = $(ev.target),
$sibling = $tab.parent().siblings('li').children('a'),
$tab_panel = $($tab.attr('href'));
$($sibling.attr('href')).hide();
$sibling.removeClass('current');
$tab.addClass('current');
$tab_panel.show();
return this;
},
showHelpMessages: function (msgs) {
// Override showHelpMessages in ChatBoxView, for now do nothing.
return;
}
});
this.ChatRoomOccupant = Backbone.Model;
this.ChatRoomOccupantView = Backbone.View.extend({
tagName: 'li',
initialize: function () {
this.model.on('change', this.render, this);
this.model.on('destroy', this.destroy, this);
},
render: function () {
var $new = converse.templates.occupant(
_.extend(
this.model.toJSON(), {
'desc_moderator': __('This user is a moderator'),
'desc_participant': __('This user can send messages in this room'),
'desc_visitor': __('This user can NOT send messages in this room')
})
);
this.$el.replaceWith($new);
this.setElement($new, true);
return this;
},
destroy: function () {
this.$el.remove();
}
});
this.ChatRoomOccupants = Backbone.Collection.extend({
model: converse.ChatRoomOccupant,
initialize: function (options) {
this.browserStorage = new Backbone.BrowserStorage[converse.storage](
b64_sha1('converse.occupants'+converse.bare_jid+options.nick));
}
});
this.ChatRoomOccupantsView = Backbone.Overview.extend({
tagName: 'div',
className: 'participants',
initialize: function () {
this.model.on("add", this.onOccupantAdded, this);
},
render: function () {
this.$el.html(
converse.templates.chatroom_sidebar({
'label_invitation': __('Invite...'),
'label_occupants': __('Occupants')
})
);
return this.initInviteWidget();
},
onOccupantAdded: function (item) {
var view = this.get(item.get('id'));
if (!view) {
view = this.add(item.get('id'), new converse.ChatRoomOccupantView({model: item}));
} else {
delete view.model; // Remove ref to old model to help garbage collection
view.model = item;
view.initialize();
}
this.$('.participant-list').append(view.render().$el);
},
onChatRoomRoster: function (roster, room) {
var roster_size = _.size(roster),
$participant_list = this.$('.participant-list'),
participants = [],
keys = _.keys(roster),
occupant, attrs, i, nick;
for (i=0; i{{value}}
')
}
});
$el.on('typeahead:selected', $.proxy(function (ev, suggestion, dname) {
var reason = prompt(
__(___('You are about to invite %1$s to the chat room "%2$s". '), suggestion.value, this.model.get('id')) +
__("You may optionally include a message, explaining the reason for the invitation.")
);
if (reason !== null) {
converse.connection.muc.rooms[this.chatroomview.model.get('id')].directInvite(suggestion.jid, reason);
converse.emit('roomInviteSent', this, suggestion.jid, reason);
}
$(ev.target).typeahead('val', '');
}, this));
return this;
}
});
this.ChatRoomView = converse.ChatBoxView.extend({
length: 300,
tagName: 'div',
className: 'chatroom',
events: {
'click .close-chatbox-button': 'close',
'click .toggle-chatbox-button': 'minimize',
'click .configure-chatroom-button': 'configureChatRoom',
'click .toggle-smiley': 'toggleEmoticonMenu',
'click .toggle-smiley ul li': 'insertEmoticon',
'click .toggle-clear': 'clearChatRoomMessages',
'click .toggle-participants a': 'toggleOccupants',
'keypress textarea.chat-textarea': 'keyPressed',
'mousedown .dragresize-tm': 'onDragResizeStart'
},
is_chatroom: true,
initialize: function () {
this.model.messages.on('add', this.onMessageAdded, this);
this.model.on('change:minimized', function (item) {
if (item.get('minimized')) {
this.hide();
} else {
this.maximize();
}
}, 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.occupantsview = new converse.ChatRoomOccupantsView({
model: new converse.ChatRoomOccupants({nick: this.model.get('nick')})
});
this.occupantsview.chatroomview = this;
this.render();
this.occupantsview.model.fetch({add:true});
this.connect(null);
converse.emit('chatRoomOpened', this);
this.$el.insertAfter(converse.chatboxviews.get("controlbox").$el);
this.model.messages.fetch({add: true});
if (this.model.get('minimized')) {
this.hide();
} else {
this.show();
}
},
render: function () {
this.$el.attr('id', this.model.get('box_id'))
.html(converse.templates.chatroom(this.model.toJSON()));
this.renderChatArea();
setTimeout(function () {
converse.refreshWebkit();
}, 50);
return this;
},
renderChatArea: function () {
if (!this.$('.chat-area').length) {
this.$('.chat-body').empty()
.append(
converse.templates.chatarea({
'show_toolbar': converse.show_toolbar,
'label_message': __('Message')
}))
.append(this.occupantsview.render().$el);
this.renderToolbar();
}
// XXX: This is a bit of a hack, to make sure that the
// sidebar's state is remembered.
this.model.set({hidden_occupants: !this.model.get('hidden_occupants')});
this.toggleOccupants();
return this;
},
toggleOccupants: function (ev) {
if (ev) {
ev.preventDefault();
ev.stopPropagation();
}
var $el = this.$('.icon-hide-users');
if (!this.model.get('hidden_occupants')) {
this.model.save({hidden_occupants: true});
$el.removeClass('icon-hide-users').addClass('icon-show-users');
this.$('form.sendXMPPMessage, .chat-area').animate({width: '100%'});
this.$('div.participants').animate({width: 0}, $.proxy(function () {
this.scrollDown();
}, this));
} else {
this.model.save({hidden_occupants: false});
$el.removeClass('icon-show-users').addClass('icon-hide-users');
this.$('.chat-area, form.sendXMPPMessage').css({width: ''});
this.$('div.participants').show().animate({width: 'auto'}, $.proxy(function () {
this.scrollDown();
}, this));
}
},
onCommandError: function (stanza) {
this.showStatusNotification(__("Error: could not execute the command"), true);
},
createChatRoomMessage: function (text) {
var fullname = converse.xmppstatus.get('fullname');
this.model.messages.create({
fullname: _.isEmpty(fullname)? converse.bare_jid: fullname,
sender: 'me',
time: moment().format(),
message: text,
msgid: converse.connection.muc.groupchat(this.model.get('jid'), text, undefined, String((new Date()).getTime()))
});
},
sendChatRoomMessage: function (text) {
var match = text.replace(/^\s*/, "").match(/^\/(.*?)(?: (.*))?$/) || [false], args;
switch (match[1]) {
case 'ban':
args = match[2].splitOnce(' ');
converse.connection.muc.ban(this.model.get('jid'), args[0], args[1], undefined, $.proxy(this.onCommandError, this));
break;
case 'clear':
this.clearChatRoomMessages();
break;
case 'deop':
args = match[2].splitOnce(' ');
converse.connection.muc.deop(this.model.get('jid'), args[0], args[1], undefined, $.proxy(this.onCommandError, this));
break;
case 'help':
this.showHelpMessages([
'/ban: ' +__('Ban user from room'),
'/clear: ' +__('Remove messages'),
'/help: ' +__('Show this menu'),
'/kick: ' +__('Kick user from room'),
'/me: ' +__('Write in 3rd person'),
'/mute: ' +__("Remove user's ability to post messages"),
'/nick: ' +__('Change your nickname'),
'/topic: ' +__('Set room topic'),
'/voice: ' +__('Allow muted user to post messages')
]);
break;
case 'kick':
args = match[2].splitOnce(' ');
converse.connection.muc.kick(this.model.get('jid'), args[0], args[1], undefined, $.proxy(this.onCommandError, this));
break;
case 'mute':
args = match[2].splitOnce(' ');
converse.connection.muc.mute(this.model.get('jid'), args[0], args[1], undefined, $.proxy(this.onCommandError, this));
break;
case 'nick':
converse.connection.muc.changeNick(this.model.get('jid'), match[2]);
break;
case 'op':
args = match[2].splitOnce(' ');
converse.connection.muc.op(this.model.get('jid'), args[0], args[1], undefined, $.proxy(this.onCommandError, this));
break;
case 'topic':
converse.connection.muc.setTopic(this.model.get('jid'), match[2]);
break;
case 'voice':
args = match[2].splitOnce(' ');
converse.connection.muc.voice(this.model.get('jid'), args[0], args[1], undefined, $.proxy(this.onCommandError, this));
break;
default:
this.createChatRoomMessage(text);
break;
}
},
connect: function (password) {
if (_.has(converse.connection.muc.rooms, this.model.get('jid'))) {
// If the room exists, it already has event listeners, so we
// don't 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);
}
},
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();
$form.find('span.spinner').remove();
$form.append($('