/*! * Converse.js (XMPP-based instant messaging with Strophe.js and backbone.js) * http://opkode.com * * Copyright (c) 2012 Jan-Carel Brand (jc@opkode.com) * Dual licensed under the MIT and GPL Licenses */ /* The following line defines global variables defined elsewhere. */ /*globals jQuery, portal_url*/ // AMD/global registrations (function (root, factory) { if (console===undefined || console.log===undefined) { console = { log: function () {}, error: function () {} }; } if (typeof define === 'function' && define.amd) { require.config({ // paths: { // "patterns": "Libraries/Patterns" // }, // define module dependencies for modules not using define shim: { 'Libraries/backbone': { //These script dependencies should be loaded before loading //backbone.js deps: [ 'Libraries/underscore', 'jquery'], //Once loaded, use the global 'Backbone' as the //module value. exports: 'Backbone' }, 'Libraries/strophe.muc': { deps: ['Libraries/strophe', 'jquery'] }, 'Libraries/strophe.roster': { deps: ['Libraries/strophe', 'jquery'] }, 'Libraries/strophe.vcard': { deps: ['Libraries/strophe', 'jquery'] } } }); define([ "Libraries/burry.js/burry", "Libraries/jquery.tinysort", "Libraries/jquery-ui-1.9.1.custom", "Libraries/sjcl", "Libraries/backbone", "Libraries/strophe.muc", "Libraries/strophe.roster", "Libraries/strophe.vcard" ], function (Burry, _s) { var store = new Burry.Store('collective.xmpp.chat'); // Init underscore.str _.str = _s; // Use Mustache style syntax for variable interpolation _.templateSettings = { evaluate : /\{\[([\s\S]+?)\]\}/g, interpolate : /\{\{([\s\S]+?)\}\}/g }; return factory(jQuery, store, _, console); } ); } else { // Browser globals var store = new Burry.Store('collective.xmpp.chat'); _.templateSettings = { evaluate : /\{\[([\s\S]+?)\]\}/g, interpolate : /\{\{([\s\S]+?)\}\}/g }; root.xmppchat = factory(jQuery, store, _, console || {log: function(){}}); } }(this, function ($, store, _, console) { var xmppchat = {}; xmppchat.msg_counter = 0; var strinclude = function(str, needle){ if (needle === '') { return true; } if (str === null) { return false; } return String(str).indexOf(needle) !== -1; }; xmppchat.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'; } }; xmppchat.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; for (var i = 0, k; (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])); }; xmppchat.updateMsgCounter = function () { this.msg_counter += 1; 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(/^\(\d\) /) !== -1) { document.title = document.title.replace(/^Messages \(\d\) /, ""); } }; xmppchat.collections = { /* FIXME: XEP-0136 specifies 'urn:xmpp:archive' but the mod_archive_odbc * add-on for ejabberd wants the URL below. This might break for other * Jabber servers. */ 'URI': 'http://www.xmpp.org/extensions/xep-0136.html#ns' }; xmppchat.collections.getLastCollection = function (jid, callback) { var bare_jid = Strophe.getBareJidFromJid(jid), iq = $iq({'type':'get'}) .c('list', {'xmlns': this.URI, 'with': bare_jid }) .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'}) .c('before').up() .c('max') .t('1'); xmppchat.connection.sendIQ(iq, callback, function () { console.log('Error while retrieving collections'); }); }; xmppchat.collections.getLastMessages = function (jid, callback) { var that = this; this.getLastCollection(jid, function (result) { // Retrieve the last page of a collection (max 30 elements). var $collection = $(result).find('chat'), jid = $collection.attr('with'), start = $collection.attr('start'), iq = $iq({'type':'get'}) .c('retrieve', {'start': start, 'xmlns': that.URI, 'with': jid }) .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'}) .c('max') .t('30'); xmppchat.connection.sendIQ(iq, callback); }); }; xmppchat.ClientStorage = Backbone.Model.extend({ initialize: function (own_jid) { this.set({ 'own_jid' : own_jid }); }, addMessage: function (jid, msg, direction) { var bare_jid = Strophe.getBareJidFromJid(jid), now = xmppchat.toISOString(new Date()), msgs = store.get(hex_sha1(this.get('own_jid')+bare_jid)) || []; if (msgs.length >= 30) { msgs.shift(); } msgs.push(sjcl.encrypt(hex_sha1(this.get('own_jid')), now+' '+direction+' '+msg)); store.set(hex_sha1(this.get('own_jid')+bare_jid), msgs); }, getMessages: function (jid) { var bare_jid = Strophe.getBareJidFromJid(jid), decrypted_msgs = [], i; var msgs = store.get(hex_sha1(this.get('own_jid')+bare_jid)) || [], msgs_length = msgs.length; for (i=0; i' + '{{time}} {{username}}: ' + '{{message}}' + ''), action_template: _.template( '
' + '{{time}}: ' + '{{message}}' + '
'), autoLink: function (text) { // Convert URLs into hyperlinks var re = /((http|https|ftp):\/\/[\w?=&.\/\-;#~%\-]+(?![\w\s?&.\/;#~%"=\-]*>))/g; return text.replace(re, '$1'); }, appendMessage: function (message) { var now = new Date(), time = now.toLocaleTimeString().substring(0,5), minutes = now.getMinutes().toString(), $chat_content = this.$el.find('.chat-content'); var msg = xmppchat.storage.getLastMessage(this.model.get('jid')); if (typeof msg !== 'undefined') { var prev_date = new Date(Date(msg.split(' ', 2)[0])); if (this.isDifferentDay(prev_date, now)) { $chat_content.append($('
 
')); $chat_content.append($('
').text(now.toString().substring(0,15))); } } message = this.autoLink(message); // TODO use minutes logic or remove it if (minutes.length==1) {minutes = '0'+minutes;} $chat_content.find('div.chat-event').remove(); $chat_content.append(this.message_template({ 'sender': 'me', 'time': time, 'message': message, 'username': 'me', 'extra_classes': '' })); $chat_content.scrollTop($chat_content[0].scrollHeight); }, insertStatusNotification: function (message, replace) { var $chat_content = this.$el.find('.chat-content'); $chat_content.find('div.chat-event').remove().end() .append($('
').text(message)); $chat_content.scrollTop($chat_content[0].scrollHeight); }, messageReceived: function (message) { /* XXX: event.mtype should be 'xhtml' for XHTML-IM messages, but I only seem to get 'text'. */ var $message = $(message); var body = this.autoLink($message.children('body').text()), from = Strophe.getBareJidFromJid($message.attr('from')), to = $message.attr('to'), composing = $message.find('composing'), $chat_content = this.$el.find('.chat-content'), delayed = $message.find('delay').length > 0, fullname = this.model.get('fullname'), time, stamp, username, sender; if (xmppchat.xmppstatus.getStatus() === 'offline') { // only update the UI if the user is not offline return; } if (!body) { if (composing.length) { this.insertStatusNotification(fullname+' '+'is typing'); return; } } else { if (from == xmppchat.connection.bare_jid) { // I am the sender, so this must be a forwarded message... $chat_content.find('div.chat-event').remove(); username = 'me'; sender = 'me'; } else { xmppchat.storage.addMessage(from, body, 'from'); $chat_content.find('div.chat-event').remove(); username = fullname.split(' ')[0]; sender = 'them'; } if (delayed) { // XXX: Test properly (for really old messages we somehow need to show // their date as well) stamp = $message.find('delay').attr('stamp'); time = (new Date(stamp)).toLocaleTimeString().substring(0,5); } else { time = (new Date()).toLocaleTimeString().substring(0,5); } $chat_content.append( this.message_template({ 'sender': sender, 'time': time, 'message': body, 'username': username, 'extra_classes': delayed && 'delayed' || '' })); $chat_content.scrollTop($chat_content[0].scrollHeight); } xmppchat.updateMsgCounter(); }, 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())); }, insertClientStoredMessages: function () { var msgs = xmppchat.storage.getMessages(this.model.get('jid')), msgs_length = msgs.length, $content = this.$el.find('.chat-content'), prev_date, this_date, i; for (i=0; i').text(this_date.toString().substring(0,15))); } } else { prev_date = this_date; this_date = new Date(Date(date)); if (this.isDifferentDay(prev_date, this_date)) { $content.append($('
 
')); $content.append($('
').text(this_date.toString().substring(0,15))); } } msg = this.autoLink(String(msg).replace(/(.*?\s.*?\s)/, '')); if (msg_array[1] == 'to') { $content.append( this.message_template({ 'sender': 'me', 'time': this_date.toLocaleTimeString().substring(0,5), 'message': msg, 'username': 'me', 'extra_classes': 'delayed' })); } else { $content.append( this.message_template({ 'sender': 'them', 'time': this_date.toLocaleTimeString().substring(0,5), 'message': msg, 'username': this.model.get('fullname').split(' ')[0], 'extra_classes': 'delayed' })); } } }, addHelpMessages: function (msgs) { var $chat_content = this.$el.find('.chat-content'), i, msgs_length = msgs.length; for (i=0; i'+msgs[i]+'')); } this.scrollDown(); }, sendMessage: function (text) { // 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(), bare_jid = this.model.get('jid'), match = text.replace(/^\s*/, "").match(/^\/(.*)\s*$/), msgs; if (match) { if (match[1] === "clear") { this.$el.find('.chat-content').empty(); xmppchat.storage.clearMessages(bare_jid); return; } else if (match[1] === "help") { msgs = [ '/help: Show this menu', '/clear: Remove messages' ]; this.addHelpMessages(msgs); return; } } var message = $msg({from: xmppchat.connection.bare_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:xmppchat.connection.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()); xmppchat.connection.send(message); xmppchat.connection.send(forwarded); this.appendMessage(text); xmppchat.storage.addMessage(bare_jid, text, 'to'); }, keyPressed: function (ev) { var $textarea = $(ev.target), message, notify, composing; if(ev.keyCode == 13) { message = $textarea.val(); $textarea.val('').focus(); if (message !== '') { this.sendMessage(message); } this.$el.data('composing', false); } else { 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'}); xmppchat.connection.send(notify); } this.$el.data('composing', true); } } }, saveChatToStorage: function () { xmppchat.storage.addOpenChat(this.model.get('jid')); }, removeChatFromStorage: function () { xmppchat.storage.removeOpenChat(this.model.get('jid')); }, closeChat: function () { var that = this; $('#'+this.model.get('box_id')).hide('fast', function () { that.removeChatFromStorage(that.model.get('id')); }); }, initialize: function (){ $('body').append(this.$el.hide()); xmppchat.roster.on('change', function (item, changed) { var fullname = this.model.get('fullname'), chat_status = item.get('chat_status'); if (item.get('jid') === this.model.get('jid')) { if (_.has(changed.changes, 'chat_status')) { if (this.$el.is(':visible')) { if (chat_status === 'offline') { this.insertStatusNotification(fullname+' '+'has gone offline'); } else if (chat_status === 'away') { this.insertStatusNotification(fullname+' '+'has gone away'); } else if ((chat_status === 'dnd')) { this.insertStatusNotification(fullname+' '+'is busy'); } else if (chat_status === 'online') { this.$el.find('div.chat-event').remove(); } } } else if (_.has(changed.changes, 'status')) { this.$el.find('p.user-custom-message').text(item.get('status')).attr('title', item.get('status')); } } }, this); }, template: _.template( '' + '
' + '
' + '