/*! * jsxc v3.1.0-beta - 2017-01-23 * * Copyright (c) 2017 Klaus Herberth
* Released under the MIT license * * Please see http://www.jsxc.org/ * * @author Klaus Herberth * @version 3.1.0-beta * @license MIT */ /*! This file is concatenated for the browser. */ var jsxc = null, RTC = null, RTCPeerconnection = null; (function($) { "use strict"; /** * JavaScript Xmpp Chat namespace * * @namespace jsxc */ jsxc = { /** Version of jsxc */ version: '3.1.0-beta', /** True if i'm the master */ master: false, /** True if the role allocation is finished */ role_allocation: false, /** Timeout for keepalive */ to: [], /** Timeout after normal keepalive starts */ toBusy: null, /** Timeout for notification */ toNotification: null, /** Timeout delay for notification */ toNotificationDelay: 500, /** Interval for keep-alive */ keepaliveInterval: null, /** True if jid, sid and rid was used to connect */ reconnect: false, /** True if restore is complete */ restoreCompleted: false, /** True if login through box */ triggeredFromBox: false, /** True if logout through element click */ triggeredFromElement: false, /** True if logout through logout click */ triggeredFromLogout: false, /** last values which we wrote into localstorage (IE workaround) */ ls: [], /** * storage event is even fired if I write something into storage (IE * workaround) 0: conform, 1: not conform, 2: not shure */ storageNotConform: null, /** Timeout for storageNotConform test */ toSNC: null, /** My bar id */ bid: null, /** Current state */ currentState: null, /** Current UI state */ currentUIState: null, /** Some constants */ CONST: { NOTIFICATION_DEFAULT: 'default', NOTIFICATION_GRANTED: 'granted', NOTIFICATION_DENIED: 'denied', STATUS: ['offline', 'dnd', 'xa', 'away', 'chat', 'online'], SOUNDS: { MSG: 'incomingMessage.wav', CALL: 'Rotary-Phone6.mp3', NOTICE: 'Ping1.mp3' }, REGEX: { JID: new RegExp('\\b[^"&\'\\/:<>@\\s]+@[\\w-_.]+\\b', 'ig'), URL: new RegExp(/(https?:\/\/|www\.)[^\s<>'"]+/gi) }, NS: { CARBONS: 'urn:xmpp:carbons:2', FORWARD: 'urn:xmpp:forward:0' }, HIDDEN: 'hidden', SHOWN: 'shown', STATE: { INITIATING: 0, PREVCONFOUND: 1, SUSPEND: 2, TRYTOINTERCEPT: 3, INTERCEPTED: 4, ESTABLISHING: 5, READY: 6 }, UISTATE: { INITIATING: 0, READY: 1 } }, /** * Parse a unix timestamp and return a formatted time string * * @memberOf jsxc * @param {Object} unixtime * @returns time of day and/or date */ getFormattedTime: function(unixtime) { var msgDate = new Date(parseInt(unixtime)); var day = ('0' + msgDate.getDate()).slice(-2); var month = ('0' + (msgDate.getMonth() + 1)).slice(-2); var year = msgDate.getFullYear(); var hours = ('0' + msgDate.getHours()).slice(-2); var minutes = ('0' + msgDate.getMinutes()).slice(-2); var dateNow = new Date(); var date = (typeof msgDate.toLocaleDateString === 'function') ? msgDate.toLocaleDateString() : day + '.' + month + '.' + year; var time = (typeof msgDate.toLocaleTimeString === 'function') ? msgDate.toLocaleTimeString() : hours + ':' + minutes; // compare dates only dateNow.setHours(0, 0, 0, 0); msgDate.setHours(0, 0, 0, 0); if (dateNow.getTime() !== msgDate.getTime()) { return date + ' ' + time; } return time; }, /** * Write debug message to console and to log. * * @memberOf jsxc * @param {String} msg Debug message * @param {Object} data * @param {String} Could be warn|error|null */ debug: function(msg, data, level) { if (level) { msg = '[' + level + '] ' + msg; } if (data) { if (jsxc.storage.getItem('debug') === true) { console.log(msg, data); } // try to convert data to string var d; try { // clone html snippet d = $("").prepend($(data).clone()).html(); } catch (err) { try { d = JSON.stringify(data); } catch (err2) { d = 'see js console'; } } jsxc.log = jsxc.log + '$ ' + msg + ': ' + d + '\n'; } else { console.log(msg); jsxc.log = jsxc.log + '$ ' + msg + '\n'; } }, /** * Write warn message. * * @memberOf jsxc * @param {String} msg Warn message * @param {Object} data */ warn: function(msg, data) { jsxc.debug(msg, data, 'WARN'); }, /** * Write error message. * * @memberOf jsxc * @param {String} msg Error message * @param {Object} data */ error: function(msg, data) { jsxc.debug(msg, data, 'ERROR'); }, /** debug log */ log: '', /** * This function initializes important core functions and event handlers. * Afterwards it performs the following actions in the given order: * *
    *
  1. If (loginForm.ifFound = 'force' and form was found) or (jid or rid or * sid was not found) intercept form, and listen for credentials.
  2. *
  3. Attach with jid, rid and sid from storage, if no form was found or * loginForm.ifFound = 'attach'
  4. *
  5. Attach with jid, rid and sid from options.xmpp, if no form was found or * loginForm.ifFound = 'attach'
  6. *
* * @memberOf jsxc * @param {object} options See {@link jsxc.options} */ init: function(options) { jsxc.changeState(jsxc.CONST.STATE.INITIATING); if (options && options.loginForm && typeof options.loginForm.attachIfFound === 'boolean' && !options.loginForm.ifFound) { // translate deprated option attachIfFound found to new ifFound options.loginForm.ifFound = (options.loginForm.attachIfFound) ? 'attach' : 'pause'; } if (options) { // override default options $.extend(true, jsxc.options, options); } // Check localStorage if (typeof(localStorage) === 'undefined') { jsxc.warn("Browser doesn't support localStorage."); return; } /** * Getter method for options. Saved options will override default one. * * @param {string} key option key * @returns default or saved option value */ jsxc.options.get = function(key) { if (jsxc.bid) { var local = jsxc.storage.getUserItem('options') || {}; return (typeof local[key] !== 'undefined') ? local[key] : jsxc.options[key]; } return jsxc.options[key]; }; /** * Setter method for options. Will write into localstorage. * * @param {string} key option key * @param {object} value option value */ jsxc.options.set = function(key, value) { jsxc.storage.updateItem('options', key, value, true); }; jsxc.storageNotConform = jsxc.storage.getItem('storageNotConform'); if (jsxc.storageNotConform === null) { jsxc.storageNotConform = 2; } // detect language var lang; if (jsxc.storage.getItem('lang') !== null) { lang = jsxc.storage.getItem('lang'); } else if (jsxc.options.autoLang && navigator.languages && navigator.languages.length > 0) { lang = navigator.languages[0].substr(0, 2); } else if (jsxc.options.autoLang && navigator.language) { lang = navigator.language.substr(0, 2); } else { lang = jsxc.options.defaultLang; } // initialize i18next translator window.i18next.init({ lng: lang, fallbackLng: 'en', resources: I18next, debug: jsxc.storage.getItem('debug') === true, interpolation: { prefix: '__', suffix: '__' } }, function() { window.jqueryI18next.init(window.i18next, $, { tName: 't', i18nName: 'i18next', handleName: 'localize', }); }); if (jsxc.storage.getItem('debug') === true) { jsxc.options.otr.debug = true; } // Register event listener for the storage event window.addEventListener('storage', jsxc.storage.onStorage, false); $(document).on('attached.jsxc', jsxc.registerLogout); var isStorageAttachParameters = jsxc.storage.getItem('rid') && jsxc.storage.getItem('sid') && jsxc.storage.getItem('jid'); var isOptionsAttachParameters = jsxc.options.xmpp.rid && jsxc.options.xmpp.sid && jsxc.options.xmpp.jid; var isForceLoginForm = jsxc.options.loginForm && jsxc.options.loginForm.ifFound === 'force' && jsxc.isLoginForm(); // Check if we have to establish a new connection if ((!isStorageAttachParameters && !isOptionsAttachParameters) || isForceLoginForm) { // clean up rid and sid jsxc.storage.removeItem('rid'); jsxc.storage.removeItem('sid'); // Looking for a login form if (!jsxc.isLoginForm()) { jsxc.changeState(jsxc.CONST.STATE.SUSPEND); if (jsxc.options.displayRosterMinimized()) { // Show minimized roster jsxc.storage.setUserItem('roster', 'hidden'); jsxc.gui.roster.init(); jsxc.gui.roster.noConnection(); } return; } jsxc.changeState(jsxc.CONST.STATE.TRYTOINTERCEPT); if (typeof jsxc.options.formFound === 'function') { jsxc.options.formFound.call(); } // create jquery object var form = jsxc.options.loginForm.form = $(jsxc.options.loginForm.form); var events = form.data('events') || { submit: [] }; var submits = []; // save attached submit events and remove them. Will be reattached // in jsxc.submitLoginForm $.each(events.submit, function(index, val) { submits.push(val.handler); }); form.data('submits', submits); form.off('submit'); // Add jsxc login action to form form.submit(function(ev) { ev.preventDefault(); jsxc.prepareLogin(function(settings) { if (settings !== false) { // settings.xmpp.onlogin is deprecated since v2.1.0 var enabled = (settings.loginForm && settings.loginForm.enable) || (settings.xmpp && settings.xmpp.onlogin); enabled = enabled === "true" || enabled === true; if (enabled) { jsxc.options.loginForm.triggered = true; jsxc.xmpp.login(jsxc.options.xmpp.jid, jsxc.options.xmpp.password); } } else { jsxc.submitLoginForm(); } }); // Trigger submit in jsxc.xmpp.connected() return false; }); jsxc.changeState(jsxc.CONST.STATE.INTERCEPTED); } else if (!jsxc.isLoginForm() || (jsxc.options.loginForm && jsxc.options.loginForm.ifFound === 'attach')) { // Restore old connection jsxc.changeState(jsxc.CONST.STATE.PREVCONFOUND); if (typeof jsxc.storage.getItem('alive') === 'undefined') { jsxc.onMaster(); } else { jsxc.checkMaster(); } } }, /** * Attach to previous session if jid, sid and rid are available * in storage or options (default behaviour also for {@link jsxc.init}). * * @memberOf jsxc */ /** * Start new chat session with given jid and password. * * @memberOf jsxc * @param {string} jid Jabber Id * @param {string} password Jabber password */ /** * Attach to new chat session with jid, sid and rid. * * @memberOf jsxc * @param {string} jid Jabber Id * @param {string} sid Session Id * @param {string} rid Request Id */ start: function() { var args = arguments; if (jsxc.role_allocation && !jsxc.master) { jsxc.debug('There is an other master tab'); return false; } if (jsxc.xmpp.conn && jsxc.xmpp.connected) { jsxc.debug('We are already connected'); return false; } if (args.length === 3) { $(document).one('attached.jsxc', function() { // save rid after first attachment jsxc.xmpp.onRidChange(jsxc.xmpp.conn._proto.rid); jsxc.onMaster(); }); } jsxc.checkMaster(function() { jsxc.xmpp.login.apply(this, args); }); }, registerLogout: function() { // Looking for logout element if (jsxc.options.logoutElement !== null && $(jsxc.options.logoutElement).length > 0) { var logout = function(ev) { ev.stopPropagation(); ev.preventDefault(); jsxc.options.logoutElement = $(this); jsxc.triggeredFromLogout = true; jsxc.xmpp.logout(); }; jsxc.options.logoutElement = $(jsxc.options.logoutElement); jsxc.options.logoutElement.off('click', null, logout).one('click', logout); } }, /** * Returns true if login form is found. * * @memberOf jsxc * @returns {boolean} True if login form was found. */ isLoginForm: function() { return jsxc.options.loginForm.form && jsxc.el_exists(jsxc.options.loginForm.form) && jsxc.el_exists(jsxc.options.loginForm.jid) && jsxc.el_exists(jsxc.options.loginForm.pass); }, /** * Load settings and prepare jid. * * @memberOf jsxc * @param {string} username * @param {string} password * @param {function} cb Called after login is prepared with result as param */ prepareLogin: function(username, password, cb) { if (typeof username === 'function') { cb = username; username = null; } username = username || $(jsxc.options.loginForm.jid).val(); password = password || $(jsxc.options.loginForm.pass).val(); if (!jsxc.triggeredFromBox && (jsxc.options.loginForm.onConnecting === 'dialog' || typeof jsxc.options.loginForm.onConnecting === 'undefined')) { jsxc.gui.showWaitAlert($.t('Logging_in')); } var settings; if (typeof jsxc.options.loadSettings === 'function') { settings = jsxc.options.loadSettings.call(this, username, password, function(s) { jsxc._prepareLogin(username, password, cb, s); }); if (typeof settings !== 'undefined') { jsxc._prepareLogin(username, password, cb, settings); } } else { jsxc._prepareLogin(username, password, cb); } }, /** * Process xmpp settings and save loaded settings. * * @private * @memberOf jsxc * @param {string} username * @param {string} password * @param {function} cb Called after login is prepared with result as param * @param {object} [loadedSettings] additonal options */ _prepareLogin: function(username, password, cb, loadedSettings) { if (loadedSettings === false) { jsxc.warn('No settings provided'); cb(false); return; } // prevent to modify the original object var settings = $.extend(true, {}, jsxc.options); if (loadedSettings) { // overwrite current options with loaded settings; settings = $.extend(true, settings, loadedSettings); } else { loadedSettings = {}; } if (typeof settings.xmpp.username === 'string') { username = settings.xmpp.username; } var resource = (settings.xmpp.resource) ? '/' + settings.xmpp.resource : ''; var domain = settings.xmpp.domain; var jid; if (username.match(/@(.*)$/)) { jid = (username.match(/\/(.*)$/)) ? username : username + resource; } else { jid = username + '@' + domain + resource; } if (typeof jsxc.options.loginForm.preJid === 'function') { jid = jsxc.options.loginForm.preJid(jid); } jsxc.bid = jsxc.jidToBid(jid); settings.xmpp.username = jid.split('@')[0]; settings.xmpp.domain = jid.split('@')[1].split('/')[0]; settings.xmpp.resource = jid.split('@')[1].split('/')[1] || ""; if (!loadedSettings.xmpp) { // force xmpp settings to be saved to storage loadedSettings.xmpp = {}; } // save loaded settings to storage $.each(loadedSettings, function(key) { var old = jsxc.options.get(key); var val = settings[key]; val = $.extend(true, old, val); jsxc.options.set(key, val); }); jsxc.options.xmpp.jid = jid; jsxc.options.xmpp.password = password; cb(settings); }, /** * Called if the script is a slave */ onSlave: function() { jsxc.debug('I am the slave.'); jsxc.role_allocation = true; jsxc.bid = jsxc.jidToBid(jsxc.storage.getItem('jid')); jsxc.gui.init(); $('#jsxc_roster').removeClass('jsxc_noConnection'); jsxc.registerLogout(); jsxc.gui.updateAvatar($('#jsxc_roster > .jsxc_bottom'), jsxc.jidToBid(jsxc.storage.getItem('jid')), 'own'); jsxc.gui.restore(); }, /** * Called if the script is the master */ onMaster: function() { jsxc.debug('I am master.'); jsxc.master = true; // Init local storage jsxc.storage.setItem('alive', 0); jsxc.storage.setItem('alive_busy', 0); // Sending keepalive signal jsxc.startKeepAlive(); jsxc.role_allocation = true; jsxc.xmpp.login(); }, /** * Checks if there is a master * * @param {function} [cb] Called if no master was found. */ checkMaster: function(cb) { jsxc.debug('check master'); cb = (cb && typeof cb === 'function') ? cb : jsxc.onMaster; if (typeof jsxc.storage.getItem('alive') === 'undefined') { cb.call(); } else { jsxc.to.push(window.setTimeout(cb, 1000)); jsxc.keepAlive('slave'); } }, masterActions: function() { if (!jsxc.xmpp.conn || !jsxc.xmpp.conn.authenticated) { return; } //prepare notifications var noti = jsxc.storage.getUserItem('notification'); noti = (typeof noti === 'number') ? noti : 2; if (jsxc.options.notification && noti > 0 && jsxc.notification.hasSupport()) { if (jsxc.notification.hasPermission()) { jsxc.notification.init(); } else { jsxc.notification.prepareRequest(); } } else { // No support => disable jsxc.options.notification = false; } if (jsxc.options.get('otr').enable) { // create or load DSA key jsxc.otr.createDSA(); } jsxc.gui.updateAvatar($('#jsxc_roster > .jsxc_bottom'), jsxc.jidToBid(jsxc.storage.getItem('jid')), 'own'); }, /** * Start sending keep-alive signal */ startKeepAlive: function() { jsxc.keepaliveInterval = window.setInterval(jsxc.keepAlive, jsxc.options.timeout - 1000); }, /** * Sends the keep-alive signal to signal that the master is still there. */ keepAlive: function(role) { var next = parseInt(jsxc.storage.getItem('alive')) + 1; role = role || 'master'; jsxc.storage.setItem('alive', next + ':' + role); }, /** * Send one keep-alive signal with higher timeout, and than resume with * normal signal */ keepBusyAlive: function() { if (jsxc.toBusy) { window.clearTimeout(jsxc.toBusy); } if (jsxc.keepaliveInterval) { window.clearInterval(jsxc.keepaliveInterval); } jsxc.storage.ink('alive_busy'); jsxc.toBusy = window.setTimeout(jsxc.startKeepAlive, jsxc.options.busyTimeout - 1000); }, /** * Generates a random integer number between 0 and max * * @param {Integer} max * @return {Integer} random integer between 0 and max */ random: function(max) { return Math.floor(Math.random() * max); }, /** * Checks if there is a element with the given selector * * @param {String} selector jQuery selector * @return {Boolean} */ el_exists: function(selector) { return $(selector).length > 0; }, /** * Creates a CSS compatible string from a JID * * @param {type} jid Valid Jabber ID * @returns {String} css Compatible string */ jidToCid: function(jid) { jsxc.warn('jsxc.jidToCid is deprecated!'); var cid = Strophe.getBareJidFromJid(jid).replace('@', '-').replace(/\./g, '-').toLowerCase(); return cid; }, /** * Create comparable bar jid. * * @memberOf jsxc * @param jid * @returns comparable bar jid */ jidToBid: function(jid) { return Strophe.unescapeNode(Strophe.getBareJidFromJid(jid).toLowerCase()); }, /** * Restore roster */ restoreRoster: function() { var buddies = jsxc.storage.getUserItem('buddylist'); if (!buddies || buddies.length === 0) { jsxc.debug('No saved buddylist.'); jsxc.gui.roster.empty(); return; } $.each(buddies, function(index, value) { jsxc.gui.roster.add(value); }); jsxc.gui.roster.loaded = true; $(document).trigger('cloaded.roster.jsxc'); }, /** * Restore all windows */ restoreWindows: function() { var windows = jsxc.storage.getUserItem('windowlist'); if (windows === null) { return; } $.each(windows, function(index, bid) { var win = jsxc.storage.getUserItem('window', bid); if (!win) { jsxc.debug('Associated window-element is missing: ' + bid); return true; } jsxc.gui.window.init(bid); if (!win.minimize) { jsxc.gui.window.show(bid); } else { jsxc.gui.window.hide(bid); } jsxc.gui.window.setText(bid, win.text); }); }, /** * This method submits the specified login form. */ submitLoginForm: function() { var form = $(jsxc.options.loginForm.form).off('submit'); // Attach original events var submits = form.data('submits') || []; $.each(submits, function(index, val) { form.submit(val); }); if (form.find('#submit').length > 0) { form.find('#submit').click(); } else if (form.get(0) && typeof form.get(0).submit === 'function') { form.submit(); } else if (form.find('[type="submit"]').length > 0) { form.find('[type="submit"]').click(); } else { jsxc.warn('Could not submit login form.'); } }, /** * Escapes some characters to HTML character */ escapeHTML: function(text) { text = text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); return text.replace(/&/g, '&').replace(//g, '>'); }, /** * Removes all html tags. * * @memberOf jsxc * @param text * @returns stripped text */ removeHTML: function(text) { return $('').html(text).text(); }, /** * Executes only one of the given events * * @param {string} obj.key event name * @param {function} obj.value function to execute * @returns {string} namespace of all events */ switchEvents: function(obj) { var ns = Math.random().toString(36).substr(2, 12); var self = this; $.each(obj, function(key, val) { $(document).one(key + '.' + ns, function() { $(document).off('.' + ns); val.apply(self, arguments); }); }); return ns; }, /** * Checks if tab is hidden. * * @returns {boolean} True if tab is hidden */ isHidden: function() { var hidden = false; if (typeof document.hidden !== 'undefined') { hidden = document.hidden; } else if (typeof document.webkitHidden !== 'undefined') { hidden = document.webkitHidden; } else if (typeof document.mozHidden !== 'undefined') { hidden = document.mozHidden; } else if (typeof document.msHidden !== 'undefined') { hidden = document.msHidden; } // handle multiple tabs if (hidden && jsxc.master) { jsxc.storage.ink('hidden', 0); } else if (!hidden && !jsxc.master) { jsxc.storage.ink('hidden'); } return hidden; }, /** * Checks if tab has focus. * * @returns {boolean} True if tabs has focus */ hasFocus: function() { var focus = true; if (typeof document.hasFocus === 'function') { focus = document.hasFocus(); } if (!focus && jsxc.master) { jsxc.storage.ink('focus', 0); } else if (focus && !jsxc.master) { jsxc.storage.ink('focus'); } return focus; }, /** * Executes the given function in jsxc namespace. * * @memberOf jsxc * @param {string} fnName Function name * @param {array} fnParams Function parameters * @returns Function return value */ exec: function(fnName, fnParams) { var fnList = fnName.split('.'); var fn = jsxc[fnList[0]]; var i; for (i = 1; i < fnList.length; i++) { fn = fn[fnList[i]]; } if (typeof fn === 'function') { return fn.apply(null, fnParams); } }, /** * Hash string into 32-bit signed integer. * * @memberOf jsxc * @param {string} str input string * @returns {integer} 32-bit signed integer */ hashStr: function(str) { var hash = 0, i; if (str.length === 0) { return hash; } for (i = 0; i < str.length; i++) { hash = ((hash << 5) - hash) + str.charCodeAt(i); hash |= 0; // Convert to 32bit integer } return hash; }, isExtraSmallDevice: function() { return $(window).width() < 500; }, changeState: function(state) { jsxc.currentState = state; jsxc.debug('State changed to ' + Object.keys(jsxc.CONST.STATE)[state]); $(document).trigger('stateChange.jsxc', state); }, changeUIState: function(state) { jsxc.currentUIState = state; jsxc.debug('UI State changed to ' + Object.keys(jsxc.CONST.UISTATE)[state]); $(document).trigger('stateUIChange.jsxc', state); } }; /** * Handle XMPP stuff. * * @namespace jsxc.xmpp */ jsxc.xmpp = { conn: null, // connection /** * Create new connection or attach to old * * @name login * @memberOf jsxc.xmpp * @private */ /** * Create new connection with given parameters. * * @name login^2 * @param {string} jid * @param {string} password * @memberOf jsxc.xmpp * @private */ /** * Attach connection with given parameters. * * @name login^3 * @param {string} jid * @param {string} sid * @param {string} rid * @memberOf jsxc.xmpp * @private */ login: function() { if (jsxc.xmpp.conn && jsxc.xmpp.conn.authenticated) { jsxc.debug('Connection already authenticated.'); return; } var jid = null, password = null, sid = null, rid = null; switch (arguments.length) { case 2: jid = arguments[0]; password = arguments[1]; break; case 3: jid = arguments[0]; sid = arguments[1]; rid = arguments[2]; break; default: sid = jsxc.storage.getItem('sid'); rid = jsxc.storage.getItem('rid'); if (sid !== null && rid !== null) { jid = jsxc.storage.getItem('jid'); } else { sid = jsxc.options.xmpp.sid || null; rid = jsxc.options.xmpp.rid || null; jid = jsxc.options.xmpp.jid; } } if (!jid) { jsxc.warn('Jid required for login'); return; } if (!jsxc.bid) { jsxc.bid = jsxc.jidToBid(jid); } var url = jsxc.options.get('xmpp').url; if (!url) { jsxc.warn('xmpp.url required for login'); return; } if (!(jsxc.xmpp.conn && jsxc.xmpp.conn.connected)) { // Register eventlistener $(document).on('connected.jsxc', jsxc.xmpp.connected); $(document).on('attached.jsxc', jsxc.xmpp.attached); $(document).on('disconnected.jsxc', jsxc.xmpp.disconnected); $(document).on('connfail.jsxc', jsxc.xmpp.onConnfail); $(document).on('authfail.jsxc', jsxc.xmpp.onAuthFail); Strophe.addNamespace('RECEIPTS', 'urn:xmpp:receipts'); } // Create new connection (no login) jsxc.xmpp.conn = new Strophe.Connection(url); if (jsxc.storage.getItem('debug') === true) { jsxc.xmpp.conn.xmlInput = function(data) { console.log('<', data); }; jsxc.xmpp.conn.xmlOutput = function(data) { console.log('>', data); }; } jsxc.xmpp.conn.nextValidRid = jsxc.xmpp.onRidChange; var callback = function(status, condition) { jsxc.debug(Object.getOwnPropertyNames(Strophe.Status)[status] + ': ' + condition); switch (status) { case Strophe.Status.CONNECTING: $(document).trigger('connecting.jsxc'); break; case Strophe.Status.CONNECTED: jsxc.bid = jsxc.jidToBid(jsxc.xmpp.conn.jid.toLowerCase()); $(document).trigger('connected.jsxc'); break; case Strophe.Status.ATTACHED: $(document).trigger('attached.jsxc'); break; case Strophe.Status.DISCONNECTED: $(document).trigger('disconnected.jsxc'); break; case Strophe.Status.CONNFAIL: $(document).trigger('connfail.jsxc'); break; case Strophe.Status.AUTHFAIL: $(document).trigger('authfail.jsxc'); break; } }; if (jsxc.xmpp.conn.caps) { jsxc.xmpp.conn.caps.node = 'http://jsxc.org/'; } jsxc.changeState(jsxc.CONST.STATE.ESTABLISHING); if (sid && rid) { jsxc.debug('Try to attach'); jsxc.debug('SID: ' + sid); jsxc.reconnect = true; jsxc.xmpp.conn.attach(jid, sid, rid, callback); } else { jsxc.debug('New connection'); if (jsxc.xmpp.conn.caps) { // Add system handler, because user handler isn't called before // we are authenticated jsxc.xmpp.conn._addSysHandler(function(stanza) { var from = jsxc.xmpp.conn.domain, c = stanza.querySelector('c'), ver = c.getAttribute('ver'), node = c.getAttribute('node'); var _jidNodeIndex = JSON.parse(localStorage.getItem('strophe.caps._jidNodeIndex')) || {}; jsxc.xmpp.conn.caps._jidVerIndex[from] = ver; _jidNodeIndex[from] = node; localStorage.setItem('strophe.caps._jidVerIndex', JSON.stringify(jsxc.xmpp.conn.caps._jidVerIndex)); localStorage.setItem('strophe.caps._jidNodeIndex', JSON.stringify(_jidNodeIndex)); }, Strophe.NS.CAPS); } jsxc.xmpp.conn.connect(jid, password || jsxc.options.xmpp.password, callback); } }, /** * Logs user out of his xmpp session and does some clean up. * * @param {boolean} complete If set to false, roster will not be removed * @returns {Boolean} */ logout: function(complete) { jsxc.triggeredFromElement = (typeof complete === 'boolean') ? complete : true; if (!jsxc.master) { // instruct master jsxc.storage.removeItem('sid'); // jsxc.xmpp.disconnected is called if master deletes alive after logout return true; } // REVIEW: this should maybe moved to xmpp.disconnected // clean up jsxc.storage.removeUserItem('buddylist'); jsxc.storage.removeUserItem('windowlist'); jsxc.storage.removeUserItem('unreadMsg'); // Hide dropdown menu $('body').click(); if (!jsxc.xmpp.conn || !jsxc.xmpp.conn.authenticated) { return true; } // restore all otr objects $.each(jsxc.storage.getUserItem('otrlist') || {}, function(i, val) { jsxc.otr.create(val); }); var numOtr = Object.keys(jsxc.otr.objects || {}).length + 1; var disReady = function() { if (--numOtr <= 0) { jsxc.xmpp.conn.flush(); setTimeout(function() { jsxc.xmpp.conn.disconnect(); }, 600); } }; // end all private conversations $.each(jsxc.otr.objects || {}, function(key, obj) { if (obj.msgstate === OTR.CONST.MSGSTATE_ENCRYPTED) { obj.endOtr.call(obj, function() { obj.init.call(obj); jsxc.otr.backup(key); disReady(); }); } else { disReady(); } }); disReady(); // Trigger real logout in jsxc.xmpp.disconnected() return false; }, /** * Triggered if connection is established * * @private */ connected: function() { jsxc.xmpp.conn.pause(); jsxc.xmpp.initNewConnection(); jsxc.xmpp.saveSessionParameter(); if (jsxc.options.loginForm.triggered) { switch (jsxc.options.loginForm.onConnected || 'submit') { case 'submit': jsxc.submitLoginForm(); return; case false: return; } } // start chat jsxc.gui.dialog.close(); jsxc.xmpp.conn.resume(); jsxc.onMaster(); jsxc.changeState(jsxc.CONST.STATE.READY); $(document).trigger('attached.jsxc'); }, /** * Triggered if connection is attached * * @private */ attached: function() { $('#jsxc_roster').removeClass('jsxc_noConnection'); jsxc.xmpp.conn.addHandler(jsxc.xmpp.onRosterChanged, 'jabber:iq:roster', 'iq', 'set'); jsxc.xmpp.conn.addHandler(jsxc.xmpp.onChatMessage, null, 'message', 'chat'); jsxc.xmpp.conn.addHandler(jsxc.xmpp.onHeadlineMessage, null, 'message', 'headline'); jsxc.xmpp.conn.addHandler(jsxc.xmpp.onReceived, null, 'message'); jsxc.xmpp.conn.addHandler(jsxc.xmpp.onPresence, null, 'presence'); jsxc.gui.init(); var caps = jsxc.xmpp.conn.caps; var domain = jsxc.xmpp.conn.domain; if (caps) { var conditionalEnable = function() {}; if (jsxc.options.get('carbons').enable) { conditionalEnable = function() { if (jsxc.xmpp.conn.caps.hasFeatureByJid(domain, jsxc.CONST.NS.CARBONS)) { jsxc.xmpp.carbons.enable(); } }; $(document).on('caps.strophe', function onCaps(ev, from) { if (from !== domain) { return; } conditionalEnable(); $(document).off('caps.strophe', onCaps); }); } if (typeof caps._knownCapabilities[caps._jidVerIndex[domain]] === 'undefined') { var _jidNodeIndex = JSON.parse(localStorage.getItem('strophe.caps._jidNodeIndex')) || {}; jsxc.debug('Request server capabilities'); caps._requestCapabilities(jsxc.xmpp.conn.domain, _jidNodeIndex[domain], caps._jidVerIndex[domain]); } else { // We know server caps conditionalEnable(); } } // Only load roaster if necessary if (!jsxc.reconnect || !jsxc.storage.getUserItem('buddylist')) { // in order to not overide existing presence information, we send // pres first after roster is ready $(document).one('cloaded.roster.jsxc', jsxc.xmpp.sendPres); $('#jsxc_roster > p:first').remove(); var iq = $iq({ type: 'get' }).c('query', { xmlns: 'jabber:iq:roster' }); jsxc.xmpp.conn.sendIQ(iq, jsxc.xmpp.onRoster); } else { jsxc.xmpp.sendPres(); if (!jsxc.restoreCompleted) { jsxc.gui.restore(); } } jsxc.xmpp.saveSessionParameter(); jsxc.masterActions(); jsxc.changeState(jsxc.CONST.STATE.READY); }, saveSessionParameter: function() { var nomJid = Strophe.getBareJidFromJid(jsxc.xmpp.conn.jid).toLowerCase() + '/' + Strophe.getResourceFromJid(jsxc.xmpp.conn.jid); // Save sid and jid jsxc.storage.setItem('sid', jsxc.xmpp.conn._proto.sid); jsxc.storage.setItem('jid', nomJid); }, initNewConnection: function() { // make shure roster will be reloaded jsxc.storage.removeUserItem('buddylist'); jsxc.storage.removeUserItem('windowlist'); jsxc.storage.removeUserItem('own'); jsxc.storage.removeUserItem('avatar', 'own'); jsxc.storage.removeUserItem('otrlist'); jsxc.storage.removeUserItem('unreadMsg'); // reset user options jsxc.storage.removeUserElement('options', 'RTCPeerConfig'); }, /** * Sends presence stanza to server. */ sendPres: function() { // disco stuff if (jsxc.xmpp.conn.disco) { jsxc.xmpp.conn.disco.addIdentity('client', 'web', 'JSXC'); jsxc.xmpp.conn.disco.addFeature(Strophe.NS.DISCO_INFO); jsxc.xmpp.conn.disco.addFeature(Strophe.NS.RECEIPTS); } // create presence stanza var pres = $pres(); if (jsxc.xmpp.conn.caps) { // attach caps pres.c('c', jsxc.xmpp.conn.caps.generateCapsAttrs()).up(); } var presState = jsxc.storage.getUserItem('presence') || 'online'; if (presState !== 'online') { pres.c('show').t(presState).up(); } var priority = jsxc.options.get('priority'); if (priority && typeof priority[presState] !== 'undefined' && parseInt(priority[presState]) !== 0) { pres.c('priority').t(priority[presState]).up(); } jsxc.debug('Send presence', pres.toString()); jsxc.xmpp.conn.send(pres); }, /** * Triggered if lost connection * * @private */ disconnected: function() { jsxc.debug('disconnected'); jsxc.storage.removeItem('jid'); jsxc.storage.removeItem('sid'); jsxc.storage.removeItem('rid'); jsxc.storage.removeItem('hidden'); jsxc.storage.removeUserItem('avatar', 'own'); jsxc.storage.removeUserItem('otrlist'); $(document).off('connected.jsxc', jsxc.xmpp.connected); $(document).off('attached.jsxc', jsxc.xmpp.attached); $(document).off('disconnected.jsxc', jsxc.xmpp.disconnected); $(document).off('connfail.jsxc', jsxc.xmpp.onConnfail); $(document).off('authfail.jsxc', jsxc.xmpp.onAuthFail); jsxc.xmpp.conn = null; $('#jsxc_windowList').remove(); if (jsxc.triggeredFromElement) { $(document).trigger('toggle.roster.jsxc', ['hidden', 0]); jsxc.gui.roster.ready = false; $('#jsxc_roster').remove(); // REVIEW: logoutElement without href attribute? if (jsxc.triggeredFromLogout) { window.location = jsxc.options.logoutElement.attr('href'); } } else { jsxc.gui.roster.noConnection(); } window.clearInterval(jsxc.keepaliveInterval); jsxc.role_allocation = false; jsxc.master = false; jsxc.storage.removeItem('alive'); jsxc.changeState(jsxc.CONST.STATE.SUSPEND); }, /** * Triggered on connection fault * * @param {String} condition information why we lost the connection * @private */ onConnfail: function(ev, condition) { jsxc.debug('XMPP connection failed: ' + condition); if (jsxc.options.loginForm.triggered) { jsxc.submitLoginForm(); } }, /** * Triggered on auth fail. * * @private */ onAuthFail: function() { if (jsxc.options.loginForm.triggered) { switch (jsxc.options.loginForm.onAuthFail || 'ask') { case 'ask': jsxc.gui.showAuthFail(); break; case 'submit': jsxc.submitLoginForm(); break; case 'quiet': case false: return; } } }, /** * Triggered on initial roster load * * @param {dom} iq * @private */ onRoster: function(iq) { /* * ... */ jsxc.debug('Load roster', iq); var buddies = []; $(iq).find('item').each(function() { var jid = $(this).attr('jid'); var name = $(this).attr('name') || jid; var bid = jsxc.jidToBid(jid); var sub = $(this).attr('subscription'); buddies.push(bid); jsxc.storage.removeUserItem('res', bid); jsxc.storage.saveBuddy(bid, { jid: jid, name: name, status: 0, sub: sub, res: [], rnd: Math.random() // force storage event }); jsxc.gui.roster.add(bid); }); if (buddies.length === 0) { jsxc.gui.roster.empty(); } jsxc.storage.setUserItem('buddylist', buddies); // load bookmarks jsxc.xmpp.bookmarks.load(); jsxc.gui.roster.loaded = true; jsxc.debug('Roster loaded'); $(document).trigger('cloaded.roster.jsxc'); jsxc.changeUIState(jsxc.CONST.UISTATE.READY); }, /** * Triggerd on roster changes * * @param {dom} iq * @returns {Boolean} True to preserve handler * @private */ onRosterChanged: function(iq) { /* * */ jsxc.debug('onRosterChanged', iq); $(iq).find('item').each(function() { var jid = $(this).attr('jid'); var name = $(this).attr('name') || jid; var bid = jsxc.jidToBid(jid); var sub = $(this).attr('subscription'); // var ask = $(this).attr('ask'); if (sub === 'remove') { jsxc.gui.roster.purge(bid); } else { var bl = jsxc.storage.getUserItem('buddylist'); if (bl.indexOf(bid) < 0) { bl.push(bid); // (INFO) push returns the new length jsxc.storage.setUserItem('buddylist', bl); } var temp = jsxc.storage.saveBuddy(bid, { jid: jid, name: name, sub: sub }); if (temp === 'updated') { jsxc.gui.update(bid); jsxc.gui.roster.reorder(bid); } else { jsxc.gui.roster.add(bid); } } // Remove pending friendship request from notice list if (sub === 'from' || sub === 'both') { var notices = jsxc.storage.getUserItem('notices'); var noticeKey = null, notice; for (noticeKey in notices) { notice = notices[noticeKey]; if (notice.fnName === 'gui.showApproveDialog' && notice.fnParams[0] === jid) { jsxc.debug('Remove notice with key ' + noticeKey); jsxc.notice.remove(noticeKey); } } } }); if (!jsxc.storage.getUserItem('buddylist') || jsxc.storage.getUserItem('buddylist').length === 0) { jsxc.gui.roster.empty(); } else { $('#jsxc_roster > p:first').remove(); } // preserve handler return true; }, /** * Triggered on incoming presence stanzas * * @param {dom} presence * @private */ onPresence: function(presence) { /* * * * 5 * * * chat * 5 */ jsxc.debug('onPresence', presence); var ptype = $(presence).attr('type'); var from = $(presence).attr('from'); var jid = Strophe.getBareJidFromJid(from).toLowerCase(); var r = Strophe.getResourceFromJid(from); var bid = jsxc.jidToBid(jid); var data = jsxc.storage.getUserItem('buddy', bid) || {}; var res = jsxc.storage.getUserItem('res', bid) || {}; var status = null; var xVCard = $(presence).find('x[xmlns="vcard-temp:x:update"]'); if (jid === Strophe.getBareJidFromJid(jsxc.storage.getItem("jid"))) { return true; } if (ptype === 'error') { $(document).trigger('error.presence.jsxc', [from, presence]); var error = $(presence).find('error'); //TODO display error message jsxc.error('[XMPP] ' + error.attr('code') + ' ' + error.find(">:first-child").prop('tagName')); return true; } // incoming friendship request if (ptype === 'subscribe') { var bl = jsxc.storage.getUserItem('buddylist'); if (bl.indexOf(bid) > -1) { jsxc.debug('Auto approve contact request, because he is already in our contact list.'); jsxc.xmpp.resFriendReq(jid, true); if (data.sub !== 'to') { jsxc.xmpp.addBuddy(jid, data.name); } return true; } jsxc.storage.setUserItem('friendReq', { jid: jid, approve: -1 }); jsxc.notice.add({ msg: $.t('Friendship_request'), description: $.t('from') + ' ' + jid, type: 'contact' }, 'gui.showApproveDialog', [jid]); return true; } else if (ptype === 'unavailable' || ptype === 'unsubscribed') { status = jsxc.CONST.STATUS.indexOf('offline'); } else { var show = $(presence).find('show').text(); if (show === '') { status = jsxc.CONST.STATUS.indexOf('online'); } else { status = jsxc.CONST.STATUS.indexOf(show); } } if (status === 0) { delete res[r]; } else if (r) { res[r] = status; } var maxVal = []; var max = 0, prop = null; for (prop in res) { if (res.hasOwnProperty(prop)) { if (max <= res[prop]) { if (max !== res[prop]) { maxVal = []; max = res[prop]; } maxVal.push(prop); } } } if (data.status === 0 && max > 0) { // buddy has come online jsxc.notification.notify({ title: data.name, msg: $.t('has_come_online'), source: bid }); } if (data.type === 'groupchat') { data.status = status; } else { data.status = max; } data.res = maxVal; data.jid = jid; // Looking for avatar if (xVCard.length > 0 && data.type !== 'groupchat') { var photo = xVCard.find('photo'); if (photo.length > 0 && photo.text() !== data.avatar) { jsxc.storage.removeUserItem('avatar', data.avatar); data.avatar = photo.text(); } } // Reset jid if (jsxc.gui.window.get(bid).length > 0) { jsxc.gui.window.get(bid).data('jid', jid); } jsxc.storage.setUserItem('buddy', bid, data); jsxc.storage.setUserItem('res', bid, res); jsxc.debug('Presence (' + from + '): ' + jsxc.CONST.STATUS[status]); jsxc.gui.update(bid); jsxc.gui.roster.reorder(bid); $(document).trigger('presence.jsxc', [from, status, presence]); // preserve handler return true; }, /** * Triggered on incoming message stanzas * * @param {dom} presence * @returns {Boolean} * @private */ onChatMessage: function(stanza) { var forwarded = $(stanza).find('forwarded[xmlns="' + jsxc.CONST.NS.FORWARD + '"]'); var message, carbon; if (forwarded.length > 0) { message = forwarded.find('> message'); forwarded = true; carbon = $(stanza).find('> [xmlns="' + jsxc.CONST.NS.CARBONS + '"]'); if (carbon.length === 0) { carbon = false; } jsxc.debug('Incoming forwarded message', message); } else { message = stanza; forwarded = false; carbon = false; jsxc.debug('Incoming message', message); } var body = $(message).find('body:first').text(); var htmlBody = $(message).find('body[xmlns="' + Strophe.NS.XHTML + '"]'); if (!body || (body.match(/\?OTR/i) && forwarded)) { return true; } var type = $(message).attr('type'); var from = $(message).attr('from'); var mid = $(message).attr('id'); var bid; var delay = $(message).find('delay[xmlns="urn:xmpp:delay"]'); var stamp = (delay.length > 0) ? new Date(delay.attr('stamp')) : new Date(); stamp = stamp.getTime(); if (carbon) { var direction = (carbon.prop("tagName") === 'sent') ? jsxc.Message.OUT : jsxc.Message.IN; bid = jsxc.jidToBid((direction === 'out') ? $(message).attr('to') : from); jsxc.gui.window.postMessage({ bid: bid, direction: direction, msg: body, encrypted: false, forwarded: forwarded, stamp: stamp }); return true; } else if (forwarded) { // Someone forwarded a message to us body = from + ' ' + $.t('to') + ' ' + $(stanza).attr('to') + '"' + body + '"'; from = $(stanza).attr('from'); } var jid = Strophe.getBareJidFromJid(from); bid = jsxc.jidToBid(jid); var data = jsxc.storage.getUserItem('buddy', bid); var request = $(message).find("request[xmlns='urn:xmpp:receipts']"); if (data === null) { // jid not in roster var chat = jsxc.storage.getUserItem('chat', bid) || []; if (chat.length === 0) { jsxc.notice.add({ msg: $.t('Unknown_sender'), description: $.t('You_received_a_message_from_an_unknown_sender') + ' (' + bid + ').' }, 'gui.showUnknownSender', [bid]); } var msg = jsxc.removeHTML(body); msg = jsxc.escapeHTML(msg); jsxc.storage.saveMessage(bid, 'in', msg, false, forwarded, stamp); return true; } var win = jsxc.gui.window.init(bid); // If we now the full jid, we use it if (type === 'chat') { win.data('jid', from); jsxc.storage.updateUserItem('buddy', bid, { jid: from }); } $(document).trigger('message.jsxc', [from, body]); // create related otr object if (jsxc.master && !jsxc.otr.objects[bid]) { jsxc.otr.create(bid); } if (!forwarded && mid !== null && request.length && data !== null && (data.sub === 'both' || data.sub === 'from') && type === 'chat') { // Send received according to XEP-0184 jsxc.xmpp.conn.send($msg({ to: from }).c('received', { xmlns: 'urn:xmpp:receipts', id: mid })); } var attachment; if (htmlBody.length === 1) { var httpUploadElement = htmlBody.find('a[data-type][data-name][data-size]'); if (httpUploadElement.length === 1) { attachment = { type: httpUploadElement.attr('data-type'), name: httpUploadElement.attr('data-name'), size: httpUploadElement.attr('data-size'), }; if (httpUploadElement.attr('data-thumbnail') && httpUploadElement.attr('data-thumbnail').match(/^\s*data:[a-z]+\/[a-z0-9-+.*]+;base64,[a-z0-9=+/]+$/i)) { attachment.thumbnail = httpUploadElement.attr('data-thumbnail'); } if (httpUploadElement.attr('href') && httpUploadElement.attr('href').match(/^https:\/\//)) { attachment.data = httpUploadElement.attr('href'); body = null; } if (!attachment.type.match(/^[a-z]+\/[a-z0-9-+.*]+$/i) || !attachment.name.match(/^[\s\w.,-]+$/i) || !attachment.size.match(/^\d+$/i)) { attachment = undefined; jsxc.warn('Invalid file type, name or size.'); } } } if (jsxc.otr.objects.hasOwnProperty(bid) && body) { // @TODO check for file upload url after decryption jsxc.otr.objects[bid].receiveMsg(body, { stamp: stamp, forwarded: forwarded, attachment: attachment }); } else { jsxc.gui.window.postMessage({ bid: bid, direction: jsxc.Message.IN, msg: body, encrypted: false, forwarded: forwarded, stamp: stamp, attachment: attachment }); } // preserve handler return true; }, /** * Process message stanzas of type headline. * * @param {String} stanza Message stanza of type headline * @return {Boolean} */ onHeadlineMessage: function(stanza) { stanza = $(stanza); var from = stanza.attr('from'); var domain = Strophe.getDomainFromJid(from); if (domain !== from) { if (!jsxc.storage.getUserItem('buddy', jsxc.jidToBid(from))) { return true; } } else if (domain !== Strophe.getDomainFromJid(jsxc.xmpp.conn.jid)) { return true; } var subject = stanza.find('subject:first').text() || $.t('Notification'); var body = stanza.find('body:first').text(); jsxc.notice.add({ msg: subject, description: body, type: (domain === from) ? 'announcement' : null }, 'gui.showNotification', [subject, body, from]); return true; }, /** * Triggerd if the rid changed * * @param {integer} rid next valid request id * @private */ onRidChange: function(rid) { jsxc.storage.setItem('rid', rid); }, /** * response to friendship request * * @param {string} from jid from original friendship req * @param {boolean} approve */ resFriendReq: function(from, approve) { if (jsxc.master) { jsxc.xmpp.conn.send($pres({ to: from, type: (approve) ? 'subscribed' : 'unsubscribed' })); jsxc.storage.removeUserItem('friendReq'); jsxc.gui.dialog.close(); } else { jsxc.storage.updateUserItem('friendReq', 'approve', approve); } }, /** * Add buddy to my friends * * @param {string} username jid * @param {string} alias */ addBuddy: function(username, alias) { var bid = jsxc.jidToBid(username); if (jsxc.master) { // add buddy to roster (trigger onRosterChanged) var iq = $iq({ type: 'set' }).c('query', { xmlns: 'jabber:iq:roster' }).c('item', { jid: username, name: alias || '' }); jsxc.xmpp.conn.sendIQ(iq); // send subscription request to buddy (trigger onRosterChanged) jsxc.xmpp.conn.send($pres({ to: username, type: 'subscribe' })); jsxc.storage.removeUserItem('add_' + bid); } else { jsxc.storage.setUserItem('add_' + bid, { username: username, alias: alias || null }); } }, /** * Remove buddy from my friends * * @param {type} jid */ removeBuddy: function(jid) { var bid = jsxc.jidToBid(jid); // Shortcut to remove buddy from roster and cancle all subscriptions var iq = $iq({ type: 'set' }).c('query', { xmlns: 'jabber:iq:roster' }).c('item', { jid: Strophe.getBareJidFromJid(jid), subscription: 'remove' }); jsxc.xmpp.conn.sendIQ(iq); jsxc.gui.roster.purge(bid); }, onReceived: function(stanza) { var received = $(stanza).find("received[xmlns='urn:xmpp:receipts']"); if (received.length) { var receivedId = received.attr('id'); var message = new jsxc.Message(receivedId); message.received(); } return true; }, /** * Public function to send message. * * @memberOf jsxc.xmpp * @param bid css jid of user * @param msg message * @param uid unique id */ sendMessage: function(message) { var bid = message.bid; var msg = message.htmlMsg; var mucRoomNames = (jsxc.xmpp.conn.muc && jsxc.xmpp.conn.muc.roomNames) ? jsxc.xmpp.conn.muc.roomNames : []; var isMucBid = mucRoomNames.indexOf(bid) >= 0; if (jsxc.otr.objects.hasOwnProperty(bid) && !isMucBid) { jsxc.otr.objects[bid].sendMsg(msg, message); } else { jsxc.xmpp._sendMessage(jsxc.gui.window.get(bid).data('jid'), msg, message); } }, /** * Create message stanza and send it. * * @memberOf jsxc.xmpp * @param jid Jabber id * @param msg Message * @param uid unique id * @private */ _sendMessage: function(jid, msg, message) { // @TODO put jid into message object var data = jsxc.storage.getUserItem('buddy', jsxc.jidToBid(jid)) || {}; var isBar = (Strophe.getBareJidFromJid(jid) === jid); var type = data.type || 'chat'; message = message || {}; var xmlMsg = $msg({ to: jid, type: type, id: message._uid }); if (message.type === jsxc.Message.HTML) { xmlMsg.c("html", { xmlns: Strophe.NS.XHTML_IM }); // Omit StropheJS XEP-0071 limitations var body = Strophe.xmlElement("body", { xmlns: Strophe.NS.XHTML }); body.innerHTML = msg; xmlMsg.node.appendChild(body); } else { xmlMsg.c('body').t(msg); } if (jsxc.xmpp.carbons.enabled && msg.match(/^\?OTR/)) { xmlMsg.up().c("private", { xmlns: jsxc.CONST.NS.CARBONS }); } if (type === 'chat' && (isBar || jsxc.xmpp.conn.caps.hasFeatureByJid(jid, Strophe.NS.RECEIPTS))) { // Add request according to XEP-0184 xmlMsg.up().c('request', { xmlns: 'urn:xmpp:receipts' }); } if (jsxc.xmpp.conn.chatstates && !jsxc.xmpp.chatState.isDisabled()) { // send active event (XEP-0085) xmlMsg.up().c('active', { xmlns: Strophe.NS.CHATSTATES }); } jsxc.xmpp.conn.send(xmlMsg); }, /** * This function loads a vcard. * * @memberOf jsxc.xmpp * @param bid * @param cb * @param error_cb */ loadVcard: function(bid, cb, error_cb) { if (jsxc.master) { jsxc.xmpp.conn.vcard.get(cb, bid, error_cb); } else { jsxc.storage.setUserItem('vcard', bid, 'request:' + (new Date()).getTime()); $(document).one('loaded.vcard.jsxc', function(ev, result) { if (result && result.state === 'success') { cb($(result.data).get(0)); } else { error_cb(); } }); } }, /** * Retrieves capabilities. * * @memberOf jsxc.xmpp * @param jid * @returns List of known capabilities */ getCapabilitiesByJid: function(jid) { if (jsxc.xmpp.conn) { return jsxc.xmpp.conn.caps.getCapabilitiesByJid(jid); } var jidVerIndex = JSON.parse(localStorage.getItem('strophe.caps._jidVerIndex')) || {}; var knownCapabilities = JSON.parse(localStorage.getItem('strophe.caps._knownCapabilities')) || {}; if (jidVerIndex[jid]) { return knownCapabilities[jidVerIndex[jid]]; } return null; }, /** * Test if jid has given features * * @param {string} jid Jabber id * @param {string[]} feature Single feature or list of features * @param {Function} cb Called with the result as first param. * @return {boolean} True, if jid has all given features. Null, if we do not know it currently. */ hasFeatureByJid: function(jid, feature, cb) { var conn = jsxc.xmpp.conn; cb = cb || function() {}; if (!feature) { return false; } if (!$.isArray(feature)) { feature = $.makeArray(feature); } var check = function(knownCapabilities) { if (!knownCapabilities) { return null; } var i; for (i = 0; i < feature.length; i++) { if (knownCapabilities['features'].indexOf(feature[i]) < 0) { return false; } } return true; }; if (conn.caps._jidVerIndex[jid] && conn.caps._knownCapabilities[conn.caps._jidVerIndex[jid]]) { var hasFeature = check(conn.caps._knownCapabilities[conn.caps._jidVerIndex[jid]]); cb(hasFeature); return hasFeature; } $(document).on('strophe.caps', function(ev, j, capabilities) { if (j === jid) { cb(check(capabilities)); $(document).off(ev); } }); return null; } }; /** * Handle carbons (XEP-0280); * * @namespace jsxc.xmpp.carbons */ jsxc.xmpp.carbons = { enabled: false, /** * Enable carbons. * * @memberOf jsxc.xmpp.carbons * @param cb callback */ enable: function(cb) { var iq = $iq({ type: 'set' }).c('enable', { xmlns: jsxc.CONST.NS.CARBONS }); jsxc.xmpp.conn.sendIQ(iq, function() { jsxc.xmpp.carbons.enabled = true; jsxc.debug('Carbons enabled'); if (cb) { cb.call(this); } }, function(stanza) { jsxc.warn('Could not enable carbons', stanza); }); }, /** * Disable carbons. * * @memberOf jsxc.xmpp.carbons * @param cb callback */ disable: function(cb) { var iq = $iq({ type: 'set' }).c('disable', { xmlns: jsxc.CONST.NS.CARBONS }); jsxc.xmpp.conn.sendIQ(iq, function() { jsxc.xmpp.carbons.enabled = false; jsxc.debug('Carbons disabled'); if (cb) { cb.call(this); } }, function(stanza) { jsxc.warn('Could not disable carbons', stanza); }); }, /** * Enable/Disable carbons depending on options key. * * @memberOf jsxc.xmpp.carbons * @param err error message */ refresh: function(err) { if (err === false) { return; } if (jsxc.options.get('carbons').enable) { return jsxc.xmpp.carbons.enable(); } return jsxc.xmpp.carbons.disable(); } }; /** * @namespace jsxc.fileTransfer * @type {Object} */ jsxc.fileTransfer = {}; /** * Make bytes more human readable. * * @memberOf jsxc.fileTransfer * @param {Integer} byte * @return {String} */ jsxc.fileTransfer.formatByte = function(byte) { var s = ['', 'KB', 'MB', 'GB', 'TB']; var i; for (i = 1; i < s.length; i++) { if (byte < 1024) { break; } byte /= 1024; } return (Math.round(byte * 10) / 10) + s[i - 1]; }; /** * Start file transfer dialog. * * @memberOf jsxc.fileTransfer * @param {String} jid */ jsxc.fileTransfer.startGuiAction = function(jid) { var bid = jsxc.jidToBid(jid); var res = Strophe.getResourceFromJid(jid); if (!res && !jsxc.xmpp.httpUpload.ready) { jsxc.fileTransfer.selectResource(bid, jsxc.fileTransfer.startGuiAction); return; } jsxc.fileTransfer.showFileSelection(jid); }; /** * Show select dialog for file transfer capable resources. * * @memberOf jsxc.fileTransfer * @param {String} bid * @param {Function} success_cb Called if user selects resource * @param {Function} error_cb Called if no resource was found or selected */ jsxc.fileTransfer.selectResource = function(bid, success_cb, error_cb) { var win = jsxc.gui.window.get(bid); var jid = win.data('jid'); var res = Strophe.getResourceFromJid(jid); var fileCapableRes = jsxc.webrtc.getCapableRes(jid, jsxc.webrtc.reqFileFeatures); var resources = Object.keys(jsxc.storage.getUserItem('res', bid)) || []; if (res === null && resources.length === 1 && fileCapableRes.length === 1) { // only one resource is available and this resource is also capable to receive files res = fileCapableRes[0]; jid = bid + '/' + res; success_cb(jid); } else if (fileCapableRes.indexOf(res) >= 0) { // currently used resource is capable to receive files success_cb(bid + '/' + res); } else if (fileCapableRes.indexOf(res) < 0) { // show selection dialog jsxc.gui.window.selectResource(bid, $.t('Your_contact_uses_multiple_clients_'), function(data) { if (data.status === 'unavailable') { jsxc.gui.window.hideOverlay(bid); if (typeof error_cb === 'function') { error_cb(); } } else if (data.status === 'selected') { success_cb(bid + '/' + data.result); } }, fileCapableRes); } }; /** * Show file selector. * * @memberOf jsxc.fileTransfer * @param {String} jid */ jsxc.fileTransfer.showFileSelection = function(jid) { var bid = jsxc.jidToBid(jid); var msg = $('
'); msg.addClass('jsxc_chatmessage'); jsxc.gui.window.showOverlay(bid, msg, true); // open file selection for user msg.find('label').click(); msg.find('[type="file"]').change(function(ev) { var file = ev.target.files[0]; // FileList object if (!file) { return; } jsxc.fileTransfer.fileSelected(jid, msg, file); }); }; /** * Callback for file selector. * * @memberOf jsxc.fileTransfer * @param {String} jid * @param {jQuery} msg jQuery object of temporary file message * @param {File} file selected file */ jsxc.fileTransfer.fileSelected = function(jid, msg, file) { var bid = jsxc.jidToBid(jid); if (file.transportMethod !== 'webrtc' && jsxc.xmpp.httpUpload.ready && file.size > jsxc.options.get('httpUpload').maxSize) { jsxc.debug('File too large for http upload.'); file.transportMethod = 'webrtc'; jsxc.fileTransfer.selectResource(bid, function(jid) { jsxc.fileTransfer.fileSelected(jid, msg, file); }, function() { var maxSize = jsxc.fileTransfer.formatByte(jsxc.options.get('httpUpload').maxSize); var fileSize = jsxc.fileTransfer.formatByte(file.size); jsxc.gui.window.postMessage({ bid: bid, direction: jsxc.Message.SYS, msg: $.t('File_too_large') + ' (' + fileSize + ' > ' + maxSize + ')' }); jsxc.gui.window.hideOverlay(bid); }); return; } else if (!jsxc.xmpp.httpUpload.ready && Strophe.getResourceFromJid(jid)) { // http upload not available file.transportMethod = 'webrtc'; } var attachment = $('
'); attachment.addClass('jsxc_attachment'); attachment.addClass('jsxc_' + file.type.replace(/\//, '-')); attachment.addClass('jsxc_' + file.type.replace(/^([^/]+)\/.*/, '$1')); msg.empty().append(attachment); if (FileReader && file.type.match(/^image\//)) { // show image preview var img = $('preview').attr('title', file.name); img.attr('src', jsxc.options.get('root') + '/img/loading.gif'); img.appendTo(attachment); var reader = new FileReader(); reader.onload = function() { img.attr('src', reader.result); }; reader.readAsDataURL(file); } else { attachment.text(file.name + ' (' + file.size + ' byte)'); } $('