From c684105fed364349e4192c95c64430791962e45a Mon Sep 17 00:00:00 2001 From: JC Brand Date: Mon, 22 Oct 2018 14:44:54 +0200 Subject: [PATCH] New build of converse.js --- dist/converse.js | 29735 ++++++++++++++++++++++----------------------- 1 file changed, 14783 insertions(+), 14952 deletions(-) diff --git a/dist/converse.js b/dist/converse.js index ee9ac15e2..0f410f804 100644 --- a/dist/converse.js +++ b/dist/converse.js @@ -86,1119 +86,6 @@ /************************************************************************/ /******/ ({ -/***/ "./3rdparty/lodash.fp.js": -/*!*******************************!*\ - !*** ./3rdparty/lodash.fp.js ***! - \*******************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -(function webpackUniversalModuleDefinition(root, factory) { - if (true) module.exports = factory();else {} -})(this, function () { - return ( - /******/ - function (modules) { - // webpackBootstrap - - /******/ - // The module cache - - /******/ - var installedModules = {}; - /******/ - // The require function - - /******/ - - function __webpack_require__(moduleId) { - /******/ - // Check if module is in cache - - /******/ - if (installedModules[moduleId]) - /******/ - return installedModules[moduleId].exports; - /******/ - // Create a new module (and put it into the cache) - - /******/ - - var module = installedModules[moduleId] = { - /******/ - exports: {}, - - /******/ - id: moduleId, - - /******/ - loaded: false - /******/ - - }; - /******/ - // Execute the module function - - /******/ - - modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); - /******/ - // Flag the module as loaded - - /******/ - - module.loaded = true; - /******/ - // Return the exports of the module - - /******/ - - return module.exports; - /******/ - } - /******/ - // expose the modules object (__webpack_modules__) - - /******/ - - - __webpack_require__.m = modules; - /******/ - // expose the module cache - - /******/ - - __webpack_require__.c = installedModules; - /******/ - // __webpack_public_path__ - - /******/ - - __webpack_require__.p = ""; - /******/ - // Load entry module and return exports - - /******/ - - return __webpack_require__(0); - /******/ - }( - /************************************************************************/ - - /******/ - [ - /* 0 */ - - /***/ - function (module, exports, __webpack_require__) { - var baseConvert = __webpack_require__(1); - /** - * Converts `lodash` to an immutable auto-curried iteratee-first data-last - * version with conversion `options` applied. - * - * @param {Function} lodash The lodash function to convert. - * @param {Object} [options] The options object. See `baseConvert` for more details. - * @returns {Function} Returns the converted `lodash`. - */ - - - function browserConvert(lodash, options) { - return baseConvert(lodash, lodash, options); - } - - if (typeof _ == 'function' && typeof _.runInContext == 'function') { - // XXX: Customization in order to be able to run both _ and fp in the - // non-AMD usecase. - fp = browserConvert(_.runInContext()); - } - - module.exports = browserConvert; - /***/ - }, - /* 1 */ - - /***/ - function (module, exports, __webpack_require__) { - var mapping = __webpack_require__(2), - fallbackHolder = __webpack_require__(3); - /** Built-in value reference. */ - - - var push = Array.prototype.push; - /** - * Creates a function, with an arity of `n`, that invokes `func` with the - * arguments it receives. - * - * @private - * @param {Function} func The function to wrap. - * @param {number} n The arity of the new function. - * @returns {Function} Returns the new function. - */ - - function baseArity(func, n) { - return n == 2 ? function (a, b) { - return func.apply(undefined, arguments); - } : function (a) { - return func.apply(undefined, arguments); - }; - } - /** - * Creates a function that invokes `func`, with up to `n` arguments, ignoring - * any additional arguments. - * - * @private - * @param {Function} func The function to cap arguments for. - * @param {number} n The arity cap. - * @returns {Function} Returns the new function. - */ - - - function baseAry(func, n) { - return n == 2 ? function (a, b) { - return func(a, b); - } : function (a) { - return func(a); - }; - } - /** - * Creates a clone of `array`. - * - * @private - * @param {Array} array The array to clone. - * @returns {Array} Returns the cloned array. - */ - - - function cloneArray(array) { - var length = array ? array.length : 0, - result = Array(length); - - while (length--) { - result[length] = array[length]; - } - - return result; - } - /** - * Creates a function that clones a given object using the assignment `func`. - * - * @private - * @param {Function} func The assignment function. - * @returns {Function} Returns the new cloner function. - */ - - - function createCloner(func) { - return function (object) { - return func({}, object); - }; - } - /** - * A specialized version of `_.spread` which flattens the spread array into - * the arguments of the invoked `func`. - * - * @private - * @param {Function} func The function to spread arguments over. - * @param {number} start The start position of the spread. - * @returns {Function} Returns the new function. - */ - - - function flatSpread(func, start) { - return function () { - var length = arguments.length, - lastIndex = length - 1, - args = Array(length); - - while (length--) { - args[length] = arguments[length]; - } - - var array = args[start], - otherArgs = args.slice(0, start); - - if (array) { - push.apply(otherArgs, array); - } - - if (start != lastIndex) { - push.apply(otherArgs, args.slice(start + 1)); - } - - return func.apply(this, otherArgs); - }; - } - /** - * Creates a function that wraps `func` and uses `cloner` to clone the first - * argument it receives. - * - * @private - * @param {Function} func The function to wrap. - * @param {Function} cloner The function to clone arguments. - * @returns {Function} Returns the new immutable function. - */ - - - function wrapImmutable(func, cloner) { - return function () { - var length = arguments.length; - - if (!length) { - return; - } - - var args = Array(length); - - while (length--) { - args[length] = arguments[length]; - } - - var result = args[0] = cloner.apply(undefined, args); - func.apply(undefined, args); - return result; - }; - } - /** - * The base implementation of `convert` which accepts a `util` object of methods - * required to perform conversions. - * - * @param {Object} util The util object. - * @param {string} name The name of the function to convert. - * @param {Function} func The function to convert. - * @param {Object} [options] The options object. - * @param {boolean} [options.cap=true] Specify capping iteratee arguments. - * @param {boolean} [options.curry=true] Specify currying. - * @param {boolean} [options.fixed=true] Specify fixed arity. - * @param {boolean} [options.immutable=true] Specify immutable operations. - * @param {boolean} [options.rearg=true] Specify rearranging arguments. - * @returns {Function|Object} Returns the converted function or object. - */ - - - function baseConvert(util, name, func, options) { - var setPlaceholder, - isLib = typeof name == 'function', - isObj = name === Object(name); - - if (isObj) { - options = func; - func = name; - name = undefined; - } - - if (func == null) { - throw new TypeError(); - } - - options || (options = {}); - var config = { - 'cap': 'cap' in options ? options.cap : true, - 'curry': 'curry' in options ? options.curry : true, - 'fixed': 'fixed' in options ? options.fixed : true, - 'immutable': 'immutable' in options ? options.immutable : true, - 'rearg': 'rearg' in options ? options.rearg : true - }; - var forceCurry = 'curry' in options && options.curry, - forceFixed = 'fixed' in options && options.fixed, - forceRearg = 'rearg' in options && options.rearg, - placeholder = isLib ? func : fallbackHolder, - pristine = isLib ? func.runInContext() : undefined; - var helpers = isLib ? func : { - 'ary': util.ary, - 'assign': util.assign, - 'clone': util.clone, - 'curry': util.curry, - 'forEach': util.forEach, - 'isArray': util.isArray, - 'isFunction': util.isFunction, - 'iteratee': util.iteratee, - 'keys': util.keys, - 'rearg': util.rearg, - 'toInteger': util.toInteger, - 'toPath': util.toPath - }; - var ary = helpers.ary, - assign = helpers.assign, - clone = helpers.clone, - curry = helpers.curry, - each = helpers.forEach, - isArray = helpers.isArray, - isFunction = helpers.isFunction, - keys = helpers.keys, - rearg = helpers.rearg, - toInteger = helpers.toInteger, - toPath = helpers.toPath; - var aryMethodKeys = keys(mapping.aryMethod); - var wrappers = { - 'castArray': function castArray(_castArray) { - return function () { - var value = arguments[0]; - return isArray(value) ? _castArray(cloneArray(value)) : _castArray.apply(undefined, arguments); - }; - }, - 'iteratee': function iteratee(_iteratee) { - return function () { - var func = arguments[0], - arity = arguments[1], - result = _iteratee(func, arity), - length = result.length; - - if (config.cap && typeof arity == 'number') { - arity = arity > 2 ? arity - 2 : 1; - return length && length <= arity ? result : baseAry(result, arity); - } - - return result; - }; - }, - 'mixin': function mixin(_mixin) { - return function (source) { - var func = this; - - if (!isFunction(func)) { - return _mixin(func, Object(source)); - } - - var pairs = []; - each(keys(source), function (key) { - if (isFunction(source[key])) { - pairs.push([key, func.prototype[key]]); - } - }); - - _mixin(func, Object(source)); - - each(pairs, function (pair) { - var value = pair[1]; - - if (isFunction(value)) { - func.prototype[pair[0]] = value; - } else { - delete func.prototype[pair[0]]; - } - }); - return func; - }; - }, - 'nthArg': function nthArg(_nthArg) { - return function (n) { - var arity = n < 0 ? 1 : toInteger(n) + 1; - return curry(_nthArg(n), arity); - }; - }, - 'rearg': function rearg(_rearg) { - return function (func, indexes) { - var arity = indexes ? indexes.length : 0; - return curry(_rearg(func, indexes), arity); - }; - }, - 'runInContext': function runInContext(_runInContext) { - return function (context) { - return baseConvert(util, _runInContext(context), options); - }; - } - }; - /*--------------------------------------------------------------------------*/ - - /** - * Casts `func` to a function with an arity capped iteratee if needed. - * - * @private - * @param {string} name The name of the function to inspect. - * @param {Function} func The function to inspect. - * @returns {Function} Returns the cast function. - */ - - function castCap(name, func) { - if (config.cap) { - var indexes = mapping.iterateeRearg[name]; - - if (indexes) { - return iterateeRearg(func, indexes); - } - - var n = !isLib && mapping.iterateeAry[name]; - - if (n) { - return iterateeAry(func, n); - } - } - - return func; - } - /** - * Casts `func` to a curried function if needed. - * - * @private - * @param {string} name The name of the function to inspect. - * @param {Function} func The function to inspect. - * @param {number} n The arity of `func`. - * @returns {Function} Returns the cast function. - */ - - - function castCurry(name, func, n) { - return forceCurry || config.curry && n > 1 ? curry(func, n) : func; - } - /** - * Casts `func` to a fixed arity function if needed. - * - * @private - * @param {string} name The name of the function to inspect. - * @param {Function} func The function to inspect. - * @param {number} n The arity cap. - * @returns {Function} Returns the cast function. - */ - - - function castFixed(name, func, n) { - if (config.fixed && (forceFixed || !mapping.skipFixed[name])) { - var data = mapping.methodSpread[name], - start = data && data.start; - return start === undefined ? ary(func, n) : flatSpread(func, start); - } - - return func; - } - /** - * Casts `func` to an rearged function if needed. - * - * @private - * @param {string} name The name of the function to inspect. - * @param {Function} func The function to inspect. - * @param {number} n The arity of `func`. - * @returns {Function} Returns the cast function. - */ - - - function castRearg(name, func, n) { - return config.rearg && n > 1 && (forceRearg || !mapping.skipRearg[name]) ? rearg(func, mapping.methodRearg[name] || mapping.aryRearg[n]) : func; - } - /** - * Creates a clone of `object` by `path`. - * - * @private - * @param {Object} object The object to clone. - * @param {Array|string} path The path to clone by. - * @returns {Object} Returns the cloned object. - */ - - - function cloneByPath(object, path) { - path = toPath(path); - var index = -1, - length = path.length, - lastIndex = length - 1, - result = clone(Object(object)), - nested = result; - - while (nested != null && ++index < length) { - var key = path[index], - value = nested[key]; - - if (value != null) { - nested[path[index]] = clone(index == lastIndex ? value : Object(value)); - } - - nested = nested[key]; - } - - return result; - } - /** - * Converts `lodash` to an immutable auto-curried iteratee-first data-last - * version with conversion `options` applied. - * - * @param {Object} [options] The options object. See `baseConvert` for more details. - * @returns {Function} Returns the converted `lodash`. - */ - - - function convertLib(options) { - return _.runInContext.convert(options)(undefined); - } - /** - * Create a converter function for `func` of `name`. - * - * @param {string} name The name of the function to convert. - * @param {Function} func The function to convert. - * @returns {Function} Returns the new converter function. - */ - - - function createConverter(name, func) { - var realName = mapping.aliasToReal[name] || name, - methodName = mapping.remap[realName] || realName, - oldOptions = options; - return function (options) { - var newUtil = isLib ? pristine : helpers, - newFunc = isLib ? pristine[methodName] : func, - newOptions = assign(assign({}, oldOptions), options); - return baseConvert(newUtil, realName, newFunc, newOptions); - }; - } - /** - * Creates a function that wraps `func` to invoke its iteratee, with up to `n` - * arguments, ignoring any additional arguments. - * - * @private - * @param {Function} func The function to cap iteratee arguments for. - * @param {number} n The arity cap. - * @returns {Function} Returns the new function. - */ - - - function iterateeAry(func, n) { - return overArg(func, function (func) { - return typeof func == 'function' ? baseAry(func, n) : func; - }); - } - /** - * Creates a function that wraps `func` to invoke its iteratee with arguments - * arranged according to the specified `indexes` where the argument value at - * the first index is provided as the first argument, the argument value at - * the second index is provided as the second argument, and so on. - * - * @private - * @param {Function} func The function to rearrange iteratee arguments for. - * @param {number[]} indexes The arranged argument indexes. - * @returns {Function} Returns the new function. - */ - - - function iterateeRearg(func, indexes) { - return overArg(func, function (func) { - var n = indexes.length; - return baseArity(rearg(baseAry(func, n), indexes), n); - }); - } - /** - * Creates a function that invokes `func` with its first argument transformed. - * - * @private - * @param {Function} func The function to wrap. - * @param {Function} transform The argument transform. - * @returns {Function} Returns the new function. - */ - - - function overArg(func, transform) { - return function () { - var length = arguments.length; - - if (!length) { - return func(); - } - - var args = Array(length); - - while (length--) { - args[length] = arguments[length]; - } - - var index = config.rearg ? 0 : length - 1; - args[index] = transform(args[index]); - return func.apply(undefined, args); - }; - } - /** - * Creates a function that wraps `func` and applys the conversions - * rules by `name`. - * - * @private - * @param {string} name The name of the function to wrap. - * @param {Function} func The function to wrap. - * @returns {Function} Returns the converted function. - */ - - - function wrap(name, func) { - var result, - realName = mapping.aliasToReal[name] || name, - wrapped = func, - wrapper = wrappers[realName]; - - if (wrapper) { - wrapped = wrapper(func); - } else if (config.immutable) { - if (mapping.mutate.array[realName]) { - wrapped = wrapImmutable(func, cloneArray); - } else if (mapping.mutate.object[realName]) { - wrapped = wrapImmutable(func, createCloner(func)); - } else if (mapping.mutate.set[realName]) { - wrapped = wrapImmutable(func, cloneByPath); - } - } - - each(aryMethodKeys, function (aryKey) { - each(mapping.aryMethod[aryKey], function (otherName) { - if (realName == otherName) { - var data = mapping.methodSpread[realName], - afterRearg = data && data.afterRearg; - result = afterRearg ? castFixed(realName, castRearg(realName, wrapped, aryKey), aryKey) : castRearg(realName, castFixed(realName, wrapped, aryKey), aryKey); - result = castCap(realName, result); - result = castCurry(realName, result, aryKey); - return false; - } - }); - return !result; - }); - result || (result = wrapped); - - if (result == func) { - result = forceCurry ? curry(result, 1) : function () { - return func.apply(this, arguments); - }; - } - - result.convert = createConverter(realName, func); - - if (mapping.placeholder[realName]) { - setPlaceholder = true; - result.placeholder = func.placeholder = placeholder; - } - - return result; - } - /*--------------------------------------------------------------------------*/ - - - if (!isObj) { - return wrap(name, func); - } - - var _ = func; // Convert methods by ary cap. - - var pairs = []; - each(aryMethodKeys, function (aryKey) { - each(mapping.aryMethod[aryKey], function (key) { - var func = _[mapping.remap[key] || key]; - - if (func) { - pairs.push([key, wrap(key, func)]); - } - }); - }); // Convert remaining methods. - - each(keys(_), function (key) { - var func = _[key]; - - if (typeof func == 'function') { - var length = pairs.length; - - while (length--) { - if (pairs[length][0] == key) { - return; - } - } - - func.convert = createConverter(key, func); - pairs.push([key, func]); - } - }); // Assign to `_` leaving `_.prototype` unchanged to allow chaining. - - each(pairs, function (pair) { - _[pair[0]] = pair[1]; - }); - _.convert = convertLib; - - if (setPlaceholder) { - _.placeholder = placeholder; - } // Assign aliases. - - - each(keys(_), function (key) { - each(mapping.realToAlias[key] || [], function (alias) { - _[alias] = _[key]; - }); - }); - return _; - } - - module.exports = baseConvert; - /***/ - }, - /* 2 */ - - /***/ - function (module, exports) { - /** Used to map aliases to their real names. */ - exports.aliasToReal = { - // Lodash aliases. - 'each': 'forEach', - 'eachRight': 'forEachRight', - 'entries': 'toPairs', - 'entriesIn': 'toPairsIn', - 'extend': 'assignIn', - 'extendAll': 'assignInAll', - 'extendAllWith': 'assignInAllWith', - 'extendWith': 'assignInWith', - 'first': 'head', - // Methods that are curried variants of others. - 'conforms': 'conformsTo', - 'matches': 'isMatch', - 'property': 'get', - // Ramda aliases. - '__': 'placeholder', - 'F': 'stubFalse', - 'T': 'stubTrue', - 'all': 'every', - 'allPass': 'overEvery', - 'always': 'constant', - 'any': 'some', - 'anyPass': 'overSome', - 'apply': 'spread', - 'assoc': 'set', - 'assocPath': 'set', - 'complement': 'negate', - 'compose': 'flowRight', - 'contains': 'includes', - 'dissoc': 'unset', - 'dissocPath': 'unset', - 'dropLast': 'dropRight', - 'dropLastWhile': 'dropRightWhile', - 'equals': 'isEqual', - 'identical': 'eq', - 'indexBy': 'keyBy', - 'init': 'initial', - 'invertObj': 'invert', - 'juxt': 'over', - 'omitAll': 'omit', - 'nAry': 'ary', - 'path': 'get', - 'pathEq': 'matchesProperty', - 'pathOr': 'getOr', - 'paths': 'at', - 'pickAll': 'pick', - 'pipe': 'flow', - 'pluck': 'map', - 'prop': 'get', - 'propEq': 'matchesProperty', - 'propOr': 'getOr', - 'props': 'at', - 'symmetricDifference': 'xor', - 'symmetricDifferenceBy': 'xorBy', - 'symmetricDifferenceWith': 'xorWith', - 'takeLast': 'takeRight', - 'takeLastWhile': 'takeRightWhile', - 'unapply': 'rest', - 'unnest': 'flatten', - 'useWith': 'overArgs', - 'where': 'conformsTo', - 'whereEq': 'isMatch', - 'zipObj': 'zipObject' - }; - /** Used to map ary to method names. */ - - exports.aryMethod = { - '1': ['assignAll', 'assignInAll', 'attempt', 'castArray', 'ceil', 'create', 'curry', 'curryRight', 'defaultsAll', 'defaultsDeepAll', 'floor', 'flow', 'flowRight', 'fromPairs', 'invert', 'iteratee', 'memoize', 'method', 'mergeAll', 'methodOf', 'mixin', 'nthArg', 'over', 'overEvery', 'overSome', 'rest', 'reverse', 'round', 'runInContext', 'spread', 'template', 'trim', 'trimEnd', 'trimStart', 'uniqueId', 'words', 'zipAll'], - '2': ['add', 'after', 'ary', 'assign', 'assignAllWith', 'assignIn', 'assignInAllWith', 'at', 'before', 'bind', 'bindAll', 'bindKey', 'chunk', 'cloneDeepWith', 'cloneWith', 'concat', 'conformsTo', 'countBy', 'curryN', 'curryRightN', 'debounce', 'defaults', 'defaultsDeep', 'defaultTo', 'delay', 'difference', 'divide', 'drop', 'dropRight', 'dropRightWhile', 'dropWhile', 'endsWith', 'eq', 'every', 'filter', 'find', 'findIndex', 'findKey', 'findLast', 'findLastIndex', 'findLastKey', 'flatMap', 'flatMapDeep', 'flattenDepth', 'forEach', 'forEachRight', 'forIn', 'forInRight', 'forOwn', 'forOwnRight', 'get', 'groupBy', 'gt', 'gte', 'has', 'hasIn', 'includes', 'indexOf', 'intersection', 'invertBy', 'invoke', 'invokeMap', 'isEqual', 'isMatch', 'join', 'keyBy', 'lastIndexOf', 'lt', 'lte', 'map', 'mapKeys', 'mapValues', 'matchesProperty', 'maxBy', 'meanBy', 'merge', 'mergeAllWith', 'minBy', 'multiply', 'nth', 'omit', 'omitBy', 'overArgs', 'pad', 'padEnd', 'padStart', 'parseInt', 'partial', 'partialRight', 'partition', 'pick', 'pickBy', 'propertyOf', 'pull', 'pullAll', 'pullAt', 'random', 'range', 'rangeRight', 'rearg', 'reject', 'remove', 'repeat', 'restFrom', 'result', 'sampleSize', 'some', 'sortBy', 'sortedIndex', 'sortedIndexOf', 'sortedLastIndex', 'sortedLastIndexOf', 'sortedUniqBy', 'split', 'spreadFrom', 'startsWith', 'subtract', 'sumBy', 'take', 'takeRight', 'takeRightWhile', 'takeWhile', 'tap', 'throttle', 'thru', 'times', 'trimChars', 'trimCharsEnd', 'trimCharsStart', 'truncate', 'union', 'uniqBy', 'uniqWith', 'unset', 'unzipWith', 'without', 'wrap', 'xor', 'zip', 'zipObject', 'zipObjectDeep'], - '3': ['assignInWith', 'assignWith', 'clamp', 'differenceBy', 'differenceWith', 'findFrom', 'findIndexFrom', 'findLastFrom', 'findLastIndexFrom', 'getOr', 'includesFrom', 'indexOfFrom', 'inRange', 'intersectionBy', 'intersectionWith', 'invokeArgs', 'invokeArgsMap', 'isEqualWith', 'isMatchWith', 'flatMapDepth', 'lastIndexOfFrom', 'mergeWith', 'orderBy', 'padChars', 'padCharsEnd', 'padCharsStart', 'pullAllBy', 'pullAllWith', 'rangeStep', 'rangeStepRight', 'reduce', 'reduceRight', 'replace', 'set', 'slice', 'sortedIndexBy', 'sortedLastIndexBy', 'transform', 'unionBy', 'unionWith', 'update', 'xorBy', 'xorWith', 'zipWith'], - '4': ['fill', 'setWith', 'updateWith'] - }; - /** Used to map ary to rearg configs. */ - - exports.aryRearg = { - '2': [1, 0], - '3': [2, 0, 1], - '4': [3, 2, 0, 1] - }; - /** Used to map method names to their iteratee ary. */ - - exports.iterateeAry = { - 'dropRightWhile': 1, - 'dropWhile': 1, - 'every': 1, - 'filter': 1, - 'find': 1, - 'findFrom': 1, - 'findIndex': 1, - 'findIndexFrom': 1, - 'findKey': 1, - 'findLast': 1, - 'findLastFrom': 1, - 'findLastIndex': 1, - 'findLastIndexFrom': 1, - 'findLastKey': 1, - 'flatMap': 1, - 'flatMapDeep': 1, - 'flatMapDepth': 1, - 'forEach': 1, - 'forEachRight': 1, - 'forIn': 1, - 'forInRight': 1, - 'forOwn': 1, - 'forOwnRight': 1, - 'map': 1, - 'mapKeys': 1, - 'mapValues': 1, - 'partition': 1, - 'reduce': 2, - 'reduceRight': 2, - 'reject': 1, - 'remove': 1, - 'some': 1, - 'takeRightWhile': 1, - 'takeWhile': 1, - 'times': 1, - 'transform': 2 - }; - /** Used to map method names to iteratee rearg configs. */ - - exports.iterateeRearg = { - 'mapKeys': [1], - 'reduceRight': [1, 0] - }; - /** Used to map method names to rearg configs. */ - - exports.methodRearg = { - 'assignInAllWith': [1, 0], - 'assignInWith': [1, 2, 0], - 'assignAllWith': [1, 0], - 'assignWith': [1, 2, 0], - 'differenceBy': [1, 2, 0], - 'differenceWith': [1, 2, 0], - 'getOr': [2, 1, 0], - 'intersectionBy': [1, 2, 0], - 'intersectionWith': [1, 2, 0], - 'isEqualWith': [1, 2, 0], - 'isMatchWith': [2, 1, 0], - 'mergeAllWith': [1, 0], - 'mergeWith': [1, 2, 0], - 'padChars': [2, 1, 0], - 'padCharsEnd': [2, 1, 0], - 'padCharsStart': [2, 1, 0], - 'pullAllBy': [2, 1, 0], - 'pullAllWith': [2, 1, 0], - 'rangeStep': [1, 2, 0], - 'rangeStepRight': [1, 2, 0], - 'setWith': [3, 1, 2, 0], - 'sortedIndexBy': [2, 1, 0], - 'sortedLastIndexBy': [2, 1, 0], - 'unionBy': [1, 2, 0], - 'unionWith': [1, 2, 0], - 'updateWith': [3, 1, 2, 0], - 'xorBy': [1, 2, 0], - 'xorWith': [1, 2, 0], - 'zipWith': [1, 2, 0] - }; - /** Used to map method names to spread configs. */ - - exports.methodSpread = { - 'assignAll': { - 'start': 0 - }, - 'assignAllWith': { - 'start': 0 - }, - 'assignInAll': { - 'start': 0 - }, - 'assignInAllWith': { - 'start': 0 - }, - 'defaultsAll': { - 'start': 0 - }, - 'defaultsDeepAll': { - 'start': 0 - }, - 'invokeArgs': { - 'start': 2 - }, - 'invokeArgsMap': { - 'start': 2 - }, - 'mergeAll': { - 'start': 0 - }, - 'mergeAllWith': { - 'start': 0 - }, - 'partial': { - 'start': 1 - }, - 'partialRight': { - 'start': 1 - }, - 'without': { - 'start': 1 - }, - 'zipAll': { - 'start': 0 - } - }; - /** Used to identify methods which mutate arrays or objects. */ - - exports.mutate = { - 'array': { - 'fill': true, - 'pull': true, - 'pullAll': true, - 'pullAllBy': true, - 'pullAllWith': true, - 'pullAt': true, - 'remove': true, - 'reverse': true - }, - 'object': { - 'assign': true, - 'assignAll': true, - 'assignAllWith': true, - 'assignIn': true, - 'assignInAll': true, - 'assignInAllWith': true, - 'assignInWith': true, - 'assignWith': true, - 'defaults': true, - 'defaultsAll': true, - 'defaultsDeep': true, - 'defaultsDeepAll': true, - 'merge': true, - 'mergeAll': true, - 'mergeAllWith': true, - 'mergeWith': true - }, - 'set': { - 'set': true, - 'setWith': true, - 'unset': true, - 'update': true, - 'updateWith': true - } - }; - /** Used to track methods with placeholder support */ - - exports.placeholder = { - 'bind': true, - 'bindKey': true, - 'curry': true, - 'curryRight': true, - 'partial': true, - 'partialRight': true - }; - /** Used to map real names to their aliases. */ - - exports.realToAlias = function () { - var hasOwnProperty = Object.prototype.hasOwnProperty, - object = exports.aliasToReal, - result = {}; - - for (var key in object) { - var value = object[key]; - - if (hasOwnProperty.call(result, value)) { - result[value].push(key); - } else { - result[value] = [key]; - } - } - - return result; - }(); - /** Used to map method names to other names. */ - - - exports.remap = { - 'assignAll': 'assign', - 'assignAllWith': 'assignWith', - 'assignInAll': 'assignIn', - 'assignInAllWith': 'assignInWith', - 'curryN': 'curry', - 'curryRightN': 'curryRight', - 'defaultsAll': 'defaults', - 'defaultsDeepAll': 'defaultsDeep', - 'findFrom': 'find', - 'findIndexFrom': 'findIndex', - 'findLastFrom': 'findLast', - 'findLastIndexFrom': 'findLastIndex', - 'getOr': 'get', - 'includesFrom': 'includes', - 'indexOfFrom': 'indexOf', - 'invokeArgs': 'invoke', - 'invokeArgsMap': 'invokeMap', - 'lastIndexOfFrom': 'lastIndexOf', - 'mergeAll': 'merge', - 'mergeAllWith': 'mergeWith', - 'padChars': 'pad', - 'padCharsEnd': 'padEnd', - 'padCharsStart': 'padStart', - 'propertyOf': 'get', - 'rangeStep': 'range', - 'rangeStepRight': 'rangeRight', - 'restFrom': 'rest', - 'spreadFrom': 'spread', - 'trimChars': 'trim', - 'trimCharsEnd': 'trimEnd', - 'trimCharsStart': 'trimStart', - 'zipAll': 'zip' - }; - /** Used to track methods that skip fixing their arity. */ - - exports.skipFixed = { - 'castArray': true, - 'flow': true, - 'flowRight': true, - 'iteratee': true, - 'mixin': true, - 'rearg': true, - 'runInContext': true - }; - /** Used to track methods that skip rearranging arguments. */ - - exports.skipRearg = { - 'add': true, - 'assign': true, - 'assignIn': true, - 'bind': true, - 'bindKey': true, - 'concat': true, - 'difference': true, - 'divide': true, - 'eq': true, - 'gt': true, - 'gte': true, - 'isEqual': true, - 'lt': true, - 'lte': true, - 'matchesProperty': true, - 'merge': true, - 'multiply': true, - 'overArgs': true, - 'partial': true, - 'partialRight': true, - 'propertyOf': true, - 'random': true, - 'range': true, - 'rangeRight': true, - 'subtract': true, - 'zip': true, - 'zipObject': true, - 'zipObjectDeep': true - }; - /***/ - }, - /* 3 */ - - /***/ - function (module, exports) { - /** - * The default argument placeholder value for methods. - * - * @type {Object} - */ - module.exports = {}; - /***/ - } - /******/ - ]) - ); -}); - -; - -/***/ }), - /***/ "./node_modules/awesomplete-avoid-xss/awesomplete.js": /*!***********************************************************!*\ !*** ./node_modules/awesomplete-avoid-xss/awesomplete.js ***! @@ -7494,205 +6381,6 @@ return Promise$2; /***/ }), -/***/ "./node_modules/fast-text-encoding/text.js": -/*!*************************************************!*\ - !*** ./node_modules/fast-text-encoding/text.js ***! - \*************************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -/* WEBPACK VAR INJECTION */(function(global) {/* - * Copyright 2017 Sam Thorogood. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -/** - * @fileoverview Polyfill for TextEncoder and TextDecoder. - * - * You probably want `text.min.js`, and not this file directly. - */ - -(function(scope) { -'use strict'; - -// fail early -if (scope['TextEncoder'] && scope['TextDecoder']) { - return false; -} - -/** - * @constructor - * @param {string=} utfLabel - */ -function FastTextEncoder(utfLabel='utf-8') { - if (utfLabel !== 'utf-8') { - throw new RangeError( - `Failed to construct 'TextEncoder': The encoding label provided ('${utfLabel}') is invalid.`); - } -} - -Object.defineProperty(FastTextEncoder.prototype, 'encoding', {value: 'utf-8'}); - -/** - * @param {string} string - * @param {{stream: boolean}=} options - * @return {!Uint8Array} - */ -FastTextEncoder.prototype.encode = function(string, options={stream: false}) { - if (options.stream) { - throw new Error(`Failed to encode: the 'stream' option is unsupported.`); - } - - let pos = 0; - const len = string.length; - const out = []; - - let at = 0; // output position - let tlen = Math.max(32, len + (len >> 1) + 7); // 1.5x size - let target = new Uint8Array((tlen >> 3) << 3); // ... but at 8 byte offset - - while (pos < len) { - let value = string.charCodeAt(pos++); - if (value >= 0xd800 && value <= 0xdbff) { - // high surrogate - if (pos < len) { - const extra = string.charCodeAt(pos); - if ((extra & 0xfc00) === 0xdc00) { - ++pos; - value = ((value & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000; - } - } - if (value >= 0xd800 && value <= 0xdbff) { - continue; // drop lone surrogate - } - } - - // expand the buffer if we couldn't write 4 bytes - if (at + 4 > target.length) { - tlen += 8; // minimum extra - tlen *= (1.0 + (pos / string.length) * 2); // take 2x the remaining - tlen = (tlen >> 3) << 3; // 8 byte offset - - const update = new Uint8Array(tlen); - update.set(target); - target = update; - } - - if ((value & 0xffffff80) === 0) { // 1-byte - target[at++] = value; // ASCII - continue; - } else if ((value & 0xfffff800) === 0) { // 2-byte - target[at++] = ((value >> 6) & 0x1f) | 0xc0; - } else if ((value & 0xffff0000) === 0) { // 3-byte - target[at++] = ((value >> 12) & 0x0f) | 0xe0; - target[at++] = ((value >> 6) & 0x3f) | 0x80; - } else if ((value & 0xffe00000) === 0) { // 4-byte - target[at++] = ((value >> 18) & 0x07) | 0xf0; - target[at++] = ((value >> 12) & 0x3f) | 0x80; - target[at++] = ((value >> 6) & 0x3f) | 0x80; - } else { - // FIXME: do we care - continue; - } - - target[at++] = (value & 0x3f) | 0x80; - } - - return target.slice(0, at); -} - -/** - * @constructor - * @param {string=} utfLabel - * @param {{fatal: boolean}=} options - */ -function FastTextDecoder(utfLabel='utf-8', options={fatal: false}) { - if (utfLabel !== 'utf-8') { - throw new RangeError( - `Failed to construct 'TextDecoder': The encoding label provided ('${utfLabel}') is invalid.`); - } - if (options.fatal) { - throw new Error(`Failed to construct 'TextDecoder': the 'fatal' option is unsupported.`); - } -} - -Object.defineProperty(FastTextDecoder.prototype, 'encoding', {value: 'utf-8'}); - -Object.defineProperty(FastTextDecoder.prototype, 'fatal', {value: false}); - -Object.defineProperty(FastTextDecoder.prototype, 'ignoreBOM', {value: false}); - -/** - * @param {(!ArrayBuffer|!ArrayBufferView)} buffer - * @param {{stream: boolean}=} options - */ -FastTextDecoder.prototype.decode = function(buffer, options={stream: false}) { - if (options['stream']) { - throw new Error(`Failed to decode: the 'stream' option is unsupported.`); - } - - const bytes = new Uint8Array(buffer); - let pos = 0; - const len = bytes.length; - const out = []; - - while (pos < len) { - const byte1 = bytes[pos++]; - if (byte1 === 0) { - break; // NULL - } - - if ((byte1 & 0x80) === 0) { // 1-byte - out.push(byte1); - } else if ((byte1 & 0xe0) === 0xc0) { // 2-byte - const byte2 = bytes[pos++] & 0x3f; - out.push(((byte1 & 0x1f) << 6) | byte2); - } else if ((byte1 & 0xf0) === 0xe0) { - const byte2 = bytes[pos++] & 0x3f; - const byte3 = bytes[pos++] & 0x3f; - out.push(((byte1 & 0x1f) << 12) | (byte2 << 6) | byte3); - } else if ((byte1 & 0xf8) === 0xf0) { - const byte2 = bytes[pos++] & 0x3f; - const byte3 = bytes[pos++] & 0x3f; - const byte4 = bytes[pos++] & 0x3f; - - // this can be > 0xffff, so possibly generate surrogates - let codepoint = ((byte1 & 0x07) << 0x12) | (byte2 << 0x0c) | (byte3 << 0x06) | byte4; - if (codepoint > 0xffff) { - // codepoint &= ~0x10000; - codepoint -= 0x10000; - out.push((codepoint >>> 10) & 0x3ff | 0xd800) - codepoint = 0xdc00 | codepoint & 0x3ff; - } - out.push(codepoint); - } else { - // FIXME: we're ignoring this - } - } - - return String.fromCharCode.apply(null, out); -} - -scope['TextEncoder'] = FastTextEncoder; -scope['TextDecoder'] = FastTextDecoder; - -}(typeof window !== 'undefined' ? window : (typeof global !== 'undefined' ? global : this))); - -/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../webpack/buildin/global.js */ "./node_modules/webpack/buildin/global.js"))) - -/***/ }), - /***/ "./node_modules/filesize/lib/filesize.js": /*!***********************************************!*\ !*** ./node_modules/filesize/lib/filesize.js ***! @@ -43834,6 +42522,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ return; } if (_.isBoolean(plugin.enabled) && plugin.enabled || _.isFunction(plugin.enabled) && plugin.enabled(this.plugged) || _.isNil(plugin.enabled)) { + _.extend(plugin, this.properties); if (plugin.dependencies) { this.loadPluginDependencies(plugin); @@ -43907,7 +42596,6 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ //# sourceMappingURL=pluggable.js.map - /***/ }), /***/ "./node_modules/process/browser.js": @@ -58606,21 +57294,6 @@ exports["filterCSS"] = (filterCSS); /***/ }), -/***/ "./src/backbone.noconflict.js": -/*!************************************!*\ - !*** ./src/backbone.noconflict.js ***! - \************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*global define */ -!(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! backbone */ "./node_modules/backbone/backbone.js")], __WEBPACK_AMD_DEFINE_RESULT__ = (function (Backbone) { - return Backbone.noConflict(); -}).apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), - __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); - -/***/ }), - /***/ "./src/converse-autocomplete.js": /*!**************************************!*\ !*** ./src/converse-autocomplete.js ***! @@ -58636,7 +57309,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ // This plugin started as a fork of Lea Verou's Awesomplete // https://leaverou.github.io/awesomplete/ (function (root, factory) { - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! converse-core */ "./src/converse-core.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! @converse/headless/converse-core */ "./src/headless/converse-core.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); @@ -59092,7 +57765,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ * in XEP-0048. */ (function (root, factory) { - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! converse-core */ "./src/converse-core.js"), __webpack_require__(/*! converse-muc */ "./src/converse-muc.js"), __webpack_require__(/*! templates/chatroom_bookmark_form.html */ "./src/templates/chatroom_bookmark_form.html"), __webpack_require__(/*! templates/chatroom_bookmark_toggle.html */ "./src/templates/chatroom_bookmark_toggle.html"), __webpack_require__(/*! templates/bookmark.html */ "./src/templates/bookmark.html"), __webpack_require__(/*! templates/bookmarks_list.html */ "./src/templates/bookmarks_list.html")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! @converse/headless/converse-core */ "./src/headless/converse-core.js"), __webpack_require__(/*! @converse/headless/converse-muc */ "./src/headless/converse-muc.js"), __webpack_require__(/*! templates/chatroom_bookmark_form.html */ "./src/templates/chatroom_bookmark_form.html"), __webpack_require__(/*! templates/chatroom_bookmark_toggle.html */ "./src/templates/chatroom_bookmark_toggle.html"), __webpack_require__(/*! templates/bookmark.html */ "./src/templates/bookmark.html"), __webpack_require__(/*! templates/bookmarks_list.html */ "./src/templates/bookmarks_list.html")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); @@ -59117,7 +57790,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ * * NB: These plugins need to have already been loaded via require.js. */ - dependencies: ["converse-chatboxes", "converse-muc", "converse-muc-views"], + dependencies: ["converse-chatboxes", "@converse/headless/converse-muc", "converse-muc-views"], overrides: { // Overrides mentioned here will be picked up by converse.js's // plugin architecture they will replace existing methods on the @@ -59735,7 +58408,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ // Copyright (c) 2013-2018, the Converse.js developers // Licensed under the Mozilla Public License (MPLv2) (function (root, factory) { - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! converse-core */ "./src/converse-core.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! @converse/headless/converse-core */ "./src/headless/converse-core.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); @@ -59799,1066 +58472,6 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /***/ }), -/***/ "./src/converse-chatboxes.js": -/*!***********************************!*\ - !*** ./src/converse-chatboxes.js ***! - \***********************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;// Converse.js -// http://conversejs.org -// -// Copyright (c) 2012-2018, the Converse.js developers -// Licensed under the Mozilla Public License (MPLv2) -(function (root, factory) { - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! converse-core */ "./src/converse-core.js"), __webpack_require__(/*! filesize */ "./node_modules/filesize/lib/filesize.js"), __webpack_require__(/*! utils/form */ "./src/utils/form.js"), __webpack_require__(/*! utils/emoji */ "./src/utils/emoji.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), - __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? - (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), - __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); -})(this, function (converse, filesize) { - "use strict"; - - const _converse$env = converse.env, - $msg = _converse$env.$msg, - Backbone = _converse$env.Backbone, - Promise = _converse$env.Promise, - Strophe = _converse$env.Strophe, - b64_sha1 = _converse$env.b64_sha1, - moment = _converse$env.moment, - sizzle = _converse$env.sizzle, - utils = _converse$env.utils, - _ = _converse$env._; - const u = converse.env.utils; - Strophe.addNamespace('MESSAGE_CORRECT', 'urn:xmpp:message-correct:0'); - Strophe.addNamespace('REFERENCE', 'urn:xmpp:reference:0'); - converse.plugins.add('converse-chatboxes', { - dependencies: ["converse-roster", "converse-vcard"], - - initialize() { - /* The initialize function gets called as soon as the plugin is - * loaded by converse.js's plugin machinery. - */ - const _converse = this._converse, - __ = _converse.__; // Configuration values for this plugin - // ==================================== - // Refer to docs/source/configuration.rst for explanations of these - // configuration settings. - - _converse.api.settings.update({ - 'auto_join_private_chats': [], - 'filter_by_resource': false, - 'forward_messages': false, - 'send_chat_state_notifications': true - }); - - _converse.api.promises.add(['chatBoxesFetched', 'chatBoxesInitialized', 'privateChatsAutoJoined']); - - function openChat(jid) { - if (!utils.isValidJID(jid)) { - return _converse.log(`Invalid JID "${jid}" provided in URL fragment`, Strophe.LogLevel.WARN); - } - - _converse.api.chats.open(jid); - } - - _converse.router.route('converse/chat?jid=:jid', openChat); - - _converse.Message = Backbone.Model.extend({ - defaults() { - return { - 'msgid': _converse.connection.getUniqueId(), - 'time': moment().format() - }; - }, - - initialize() { - this.setVCard(); - - if (this.get('file')) { - this.on('change:put', this.uploadFile, this); - - if (!_.includes([_converse.SUCCESS, _converse.FAILURE], this.get('upload'))) { - this.getRequestSlotURL(); - } - } - - if (this.isOnlyChatStateNotification()) { - window.setTimeout(this.destroy.bind(this), 20000); - } - }, - - getVCardForChatroomOccupant() { - const chatbox = this.collection.chatbox, - nick = Strophe.getResourceFromJid(this.get('from')); - - if (chatbox.get('nick') === nick) { - return _converse.xmppstatus.vcard; - } else { - let vcard; - - if (this.get('vcard_jid')) { - vcard = _converse.vcards.findWhere({ - 'jid': this.get('vcard_jid') - }); - } - - if (!vcard) { - let jid; - const occupant = chatbox.occupants.findWhere({ - 'nick': nick - }); - - if (occupant && occupant.get('jid')) { - jid = occupant.get('jid'); - this.save({ - 'vcard_jid': jid - }, { - 'silent': true - }); - } else { - jid = this.get('from'); - } - - vcard = _converse.vcards.findWhere({ - 'jid': jid - }) || _converse.vcards.create({ - 'jid': jid - }); - } - - return vcard; - } - }, - - setVCard() { - if (this.get('type') === 'error') { - return; - } else if (this.get('type') === 'groupchat') { - this.vcard = this.getVCardForChatroomOccupant(); - } else { - const jid = this.get('from'); - this.vcard = _converse.vcards.findWhere({ - 'jid': jid - }) || _converse.vcards.create({ - 'jid': jid - }); - } - }, - - isOnlyChatStateNotification() { - return u.isOnlyChatStateNotification(this); - }, - - getDisplayName() { - if (this.get('type') === 'groupchat') { - return this.get('nick'); - } else { - return this.vcard.get('fullname') || this.get('from'); - } - }, - - sendSlotRequestStanza() { - /* Send out an IQ stanza to request a file upload slot. - * - * https://xmpp.org/extensions/xep-0363.html#request - */ - const file = this.get('file'); - return new Promise((resolve, reject) => { - const iq = converse.env.$iq({ - 'from': _converse.jid, - 'to': this.get('slot_request_url'), - 'type': 'get' - }).c('request', { - 'xmlns': Strophe.NS.HTTPUPLOAD, - 'filename': file.name, - 'size': file.size, - 'content-type': file.type - }); - - _converse.connection.sendIQ(iq, resolve, reject); - }); - }, - - getRequestSlotURL() { - this.sendSlotRequestStanza().then(stanza => { - const slot = stanza.querySelector('slot'); - - if (slot) { - this.save({ - 'get': slot.querySelector('get').getAttribute('url'), - 'put': slot.querySelector('put').getAttribute('url') - }); - } else { - return this.save({ - 'type': 'error', - 'message': __("Sorry, could not determine file upload URL.") - }); - } - }).catch(e => { - _converse.log(e, Strophe.LogLevel.ERROR); - - return this.save({ - 'type': 'error', - 'message': __("Sorry, could not determine upload URL.") - }); - }); - }, - - uploadFile() { - const xhr = new XMLHttpRequest(); - - xhr.onreadystatechange = () => { - if (xhr.readyState === XMLHttpRequest.DONE) { - _converse.log("Status: " + xhr.status, Strophe.LogLevel.INFO); - - if (xhr.status === 200 || xhr.status === 201) { - this.save({ - 'upload': _converse.SUCCESS, - 'oob_url': this.get('get'), - 'message': this.get('get') - }); - } else { - xhr.onerror(); - } - } - }; - - xhr.upload.addEventListener("progress", evt => { - if (evt.lengthComputable) { - this.set('progress', evt.loaded / evt.total); - } - }, false); - - xhr.onerror = () => { - let message; - - if (xhr.responseText) { - message = __('Sorry, could not succesfully upload your file. Your server’s response: "%1$s"', xhr.responseText); - } else { - message = __('Sorry, could not succesfully upload your file.'); - } - - this.save({ - 'type': 'error', - 'upload': _converse.FAILURE, - 'message': message - }); - }; - - xhr.open('PUT', this.get('put'), true); - xhr.setRequestHeader("Content-type", this.get('file').type); - xhr.send(this.get('file')); - } - - }); - _converse.Messages = Backbone.Collection.extend({ - model: _converse.Message, - comparator: 'time' - }); - _converse.ChatBox = _converse.ModelWithVCardAndPresence.extend({ - defaults() { - return { - 'bookmarked': false, - 'chat_state': undefined, - 'num_unread': 0, - 'type': _converse.PRIVATE_CHAT_TYPE, - 'message_type': 'chat', - 'url': '', - 'hidden': _.includes(['mobile', 'fullscreen'], _converse.view_mode) - }; - }, - - initialize() { - _converse.ModelWithVCardAndPresence.prototype.initialize.apply(this, arguments); - - _converse.api.waitUntil('rosterContactsFetched').then(() => { - this.addRelatedContact(_converse.roster.findWhere({ - 'jid': this.get('jid') - })); - }); - - this.messages = new _converse.Messages(); - - const storage = _converse.config.get('storage'); - - this.messages.browserStorage = new Backbone.BrowserStorage[storage](b64_sha1(`converse.messages${this.get('jid')}${_converse.bare_jid}`)); - this.messages.chatbox = this; - this.messages.on('change:upload', message => { - if (message.get('upload') === _converse.SUCCESS) { - this.sendMessageStanza(this.createMessageStanza(message)); - } - }); - this.on('change:chat_state', this.sendChatState, this); - 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. - 'box_id': b64_sha1(this.get('jid')), - 'time_opened': this.get('time_opened') || moment().valueOf(), - 'user_id': Strophe.getNodeFromJid(this.get('jid')) - }); - }, - - addRelatedContact(contact) { - if (!_.isUndefined(contact)) { - this.contact = contact; - this.trigger('contactAdded', contact); - } - }, - - getDisplayName() { - return this.vcard.get('fullname') || this.get('jid'); - }, - - handleMessageCorrection(stanza) { - const replace = sizzle(`replace[xmlns="${Strophe.NS.MESSAGE_CORRECT}"]`, stanza).pop(); - - if (replace) { - const msgid = replace && replace.getAttribute('id') || stanza.getAttribute('id'), - message = msgid && this.messages.findWhere({ - msgid - }); - - if (!message) { - // XXX: Looks like we received a correction for a - // non-existing message, probably due to MAM. - // Not clear what can be done about this... we'll - // just create it as a separate message for now. - return false; - } - - const older_versions = message.get('older_versions') || []; - older_versions.push(message.get('message')); - message.save({ - 'message': _converse.chatboxes.getMessageBody(stanza), - 'references': this.getReferencesFromStanza(stanza), - 'older_versions': older_versions, - 'edited': moment().format() - }); - return true; - } - - return false; - }, - - createMessageStanza(message) { - /* Given a _converse.Message Backbone.Model, return the XML - * stanza that represents it. - * - * Parameters: - * (Object) message - The Backbone.Model representing the message - */ - const stanza = $msg({ - 'from': _converse.connection.jid, - 'to': this.get('jid'), - 'type': this.get('message_type'), - 'id': message.get('edited') && _converse.connection.getUniqueId() || message.get('msgid') - }).c('body').t(message.get('message')).up().c(_converse.ACTIVE, { - 'xmlns': Strophe.NS.CHATSTATES - }).up(); - - if (message.get('is_spoiler')) { - if (message.get('spoiler_hint')) { - stanza.c('spoiler', { - 'xmlns': Strophe.NS.SPOILER - }, message.get('spoiler_hint')).up(); - } else { - stanza.c('spoiler', { - 'xmlns': Strophe.NS.SPOILER - }).up(); - } - } - - (message.get('references') || []).forEach(reference => { - const attrs = { - 'xmlns': Strophe.NS.REFERENCE, - 'begin': reference.begin, - 'end': reference.end, - 'type': reference.type - }; - - if (reference.uri) { - attrs.uri = reference.uri; - } - - stanza.c('reference', attrs).up(); - }); - - if (message.get('file')) { - stanza.c('x', { - 'xmlns': Strophe.NS.OUTOFBAND - }).c('url').t(message.get('message')).up(); - } - - if (message.get('edited')) { - stanza.c('replace', { - 'xmlns': Strophe.NS.MESSAGE_CORRECT, - 'id': message.get('msgid') - }).up(); - } - - return stanza; - }, - - sendMessageStanza(stanza) { - _converse.connection.send(stanza); - - if (_converse.forward_messages) { - // Forward the message, so that other connected resources are also aware of it. - _converse.connection.send($msg({ - 'to': _converse.bare_jid, - 'type': this.get('message_type') - }).c('forwarded', { - 'xmlns': Strophe.NS.FORWARD - }).c('delay', { - 'xmns': Strophe.NS.DELAY, - 'stamp': moment().format() - }).up().cnode(stanza.tree())); - } - }, - - getOutgoingMessageAttributes(text, spoiler_hint) { - const is_spoiler = this.get('composing_spoiler'); - return _.extend(this.toJSON(), { - 'id': _converse.connection.getUniqueId(), - 'fullname': _converse.xmppstatus.get('fullname'), - 'from': _converse.bare_jid, - 'sender': 'me', - 'time': moment().format(), - 'message': text ? u.httpToGeoUri(u.shortnameToUnicode(text), _converse) : undefined, - 'is_spoiler': is_spoiler, - 'spoiler_hint': is_spoiler ? spoiler_hint : undefined, - 'type': this.get('message_type') - }); - }, - - sendMessage(attrs) { - /* Responsible for sending off a text message. - * - * Parameters: - * (Message) message - The chat message - */ - let message = this.messages.findWhere('correcting'); - - if (message) { - const older_versions = message.get('older_versions') || []; - older_versions.push(message.get('message')); - message.save({ - 'correcting': false, - 'edited': moment().format(), - 'message': attrs.message, - 'older_versions': older_versions, - 'references': attrs.references - }); - } else { - message = this.messages.create(attrs); - } - - return this.sendMessageStanza(this.createMessageStanza(message)); - }, - - sendChatState() { - /* 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. - */ - if (_converse.send_chat_state_notifications) { - _converse.connection.send($msg({ - 'to': this.get('jid'), - 'type': 'chat' - }).c(this.get('chat_state'), { - 'xmlns': Strophe.NS.CHATSTATES - }).up().c('no-store', { - 'xmlns': Strophe.NS.HINTS - }).up().c('no-permanent-store', { - 'xmlns': Strophe.NS.HINTS - })); - } - }, - - sendFiles(files) { - _converse.api.disco.supports(Strophe.NS.HTTPUPLOAD, _converse.domain).then(result => { - const item = result.pop(), - data = item.dataforms.where({ - 'FORM_TYPE': { - 'value': Strophe.NS.HTTPUPLOAD, - 'type': "hidden" - } - }).pop(), - max_file_size = window.parseInt(_.get(data, 'attributes.max-file-size.value')), - slot_request_url = _.get(item, 'id'); - - if (!slot_request_url) { - this.messages.create({ - 'message': __("Sorry, looks like file upload is not supported by your server."), - 'type': 'error' - }); - return; - } - - _.each(files, file => { - if (!window.isNaN(max_file_size) && window.parseInt(file.size) > max_file_size) { - return this.messages.create({ - 'message': __('The size of your file, %1$s, exceeds the maximum allowed by your server, which is %2$s.', file.name, filesize(max_file_size)), - 'type': 'error' - }); - } else { - this.messages.create(_.extend(this.getOutgoingMessageAttributes(), { - 'file': file, - 'progress': 0, - 'slot_request_url': slot_request_url - })); - } - }); - }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); - }, - - getReferencesFromStanza(stanza) { - const text = _.propertyOf(stanza.querySelector('body'))('textContent'); - - return sizzle(`reference[xmlns="${Strophe.NS.REFERENCE}"]`, stanza).map(ref => { - const begin = ref.getAttribute('begin'), - end = ref.getAttribute('end'); - return { - 'begin': begin, - 'end': end, - 'type': ref.getAttribute('type'), - 'value': text.slice(begin, end), - 'uri': ref.getAttribute('uri') - }; - }); - }, - - getMessageAttributesFromStanza(stanza, original_stanza) { - /* Parses a passed in message stanza and returns an object - * of attributes. - * - * Parameters: - * (XMLElement) stanza - The message stanza - * (XMLElement) delay - The node from the - * stanza, if there was one. - * (XMLElement) original_stanza - The original stanza, - * that contains the message stanza, if it was - * contained, otherwise it's the message stanza itself. - */ - const archive = sizzle(`result[xmlns="${Strophe.NS.MAM}"]`, original_stanza).pop(), - spoiler = sizzle(`spoiler[xmlns="${Strophe.NS.SPOILER}"]`, original_stanza).pop(), - delay = sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, original_stanza).pop(), - chat_state = stanza.getElementsByTagName(_converse.COMPOSING).length && _converse.COMPOSING || stanza.getElementsByTagName(_converse.PAUSED).length && _converse.PAUSED || stanza.getElementsByTagName(_converse.INACTIVE).length && _converse.INACTIVE || stanza.getElementsByTagName(_converse.ACTIVE).length && _converse.ACTIVE || stanza.getElementsByTagName(_converse.GONE).length && _converse.GONE; - - const attrs = { - 'chat_state': chat_state, - 'is_archived': !_.isNil(archive), - 'is_delayed': !_.isNil(delay), - 'is_spoiler': !_.isNil(spoiler), - 'message': _converse.chatboxes.getMessageBody(stanza) || undefined, - 'references': this.getReferencesFromStanza(stanza), - 'msgid': stanza.getAttribute('id'), - 'time': delay ? delay.getAttribute('stamp') : moment().format(), - 'type': stanza.getAttribute('type') - }; - - if (attrs.type === 'groupchat') { - attrs.from = stanza.getAttribute('from'); - attrs.nick = Strophe.unescapeNode(Strophe.getResourceFromJid(attrs.from)); - attrs.sender = attrs.nick === this.get('nick') ? 'me' : 'them'; - } else { - attrs.from = Strophe.getBareJidFromJid(stanza.getAttribute('from')); - - if (attrs.from === _converse.bare_jid) { - attrs.sender = 'me'; - attrs.fullname = _converse.xmppstatus.get('fullname'); - } else { - attrs.sender = 'them'; - attrs.fullname = this.get('fullname'); - } - } - - _.each(sizzle(`x[xmlns="${Strophe.NS.OUTOFBAND}"]`, stanza), xform => { - attrs['oob_url'] = xform.querySelector('url').textContent; - attrs['oob_desc'] = xform.querySelector('url').textContent; - }); - - if (spoiler) { - attrs.spoiler_hint = spoiler.textContent.length > 0 ? spoiler.textContent : ''; - } - - return attrs; - }, - - createMessage(message, original_stanza) { - /* Create a Backbone.Message object inside this chat box - * based on the identified message stanza. - */ - const that = this; - - function _create(attrs) { - const is_csn = u.isOnlyChatStateNotification(attrs); - - if (is_csn && (attrs.is_delayed || attrs.type === 'groupchat' && Strophe.getResourceFromJid(attrs.from) == that.get('nick'))) { - // XXX: MUC leakage - // No need showing delayed or our own CSN messages - return; - } else if (!is_csn && !attrs.file && !attrs.plaintext && !attrs.message && !attrs.oob_url && attrs.type !== 'error') { - // TODO: handle messages (currently being done by ChatRoom) - return; - } else { - return that.messages.create(attrs); - } - } - - const result = this.getMessageAttributesFromStanza(message, original_stanza); - - if (typeof result.then === "function") { - return new Promise((resolve, reject) => result.then(attrs => resolve(_create(attrs)))); - } else { - const message = _create(result); - - return Promise.resolve(message); - } - }, - - isHidden() { - /* Returns a boolean to indicate whether a newly received - * message will be visible to the user or not. - */ - return this.get('hidden') || this.get('minimized') || this.isScrolledUp() || _converse.windowState === 'hidden'; - }, - - incrementUnreadMsgCounter(message) { - /* Given a newly received message, update the unread counter if - * necessary. - */ - if (!message) { - return; - } - - if (_.isNil(message.get('message'))) { - return; - } - - if (utils.isNewMessage(message) && this.isHidden()) { - this.save({ - 'num_unread': this.get('num_unread') + 1 - }); - - _converse.incrementMsgCounter(); - } - }, - - clearUnreadMsgCounter() { - u.safeSave(this, { - 'num_unread': 0 - }); - }, - - isScrolledUp() { - return this.get('scrolled', true); - } - - }); - _converse.ChatBoxes = Backbone.Collection.extend({ - comparator: 'time_opened', - - model(attrs, options) { - return new _converse.ChatBox(attrs, options); - }, - - registerMessageHandler() { - _converse.connection.addHandler(stanza => { - this.onMessage(stanza); - return true; - }, null, 'message', 'chat'); - - _converse.connection.addHandler(stanza => { - this.onErrorMessage(stanza); - return true; - }, null, 'message', 'error'); - }, - - chatBoxMayBeShown(chatbox) { - return true; - }, - - onChatBoxesFetched(collection) { - /* Show chat boxes upon receiving them from sessionStorage */ - collection.each(chatbox => { - if (this.chatBoxMayBeShown(chatbox)) { - chatbox.trigger('show'); - } - }); - - _converse.emit('chatBoxesFetched'); - }, - - onConnected() { - this.browserStorage = new Backbone.BrowserStorage.session(`converse.chatboxes-${_converse.bare_jid}`); - this.registerMessageHandler(); - this.fetch({ - 'add': true, - 'success': this.onChatBoxesFetched.bind(this) - }); - }, - - onErrorMessage(message) { - /* Handler method for all incoming error message stanzas - */ - const from_jid = Strophe.getBareJidFromJid(message.getAttribute('from')); - - if (utils.isSameBareJID(from_jid, _converse.bare_jid)) { - return true; - } - - const chatbox = this.getChatBox(from_jid); - - if (!chatbox) { - return true; - } - - chatbox.createMessage(message, message); - return true; - }, - - getMessageBody(stanza) { - /* Given a message stanza, return the text contained in its body. - */ - const type = stanza.getAttribute('type'); - - if (type === 'error') { - const error = stanza.querySelector('error'); - return _.propertyOf(error.querySelector('text'))('textContent') || __('Sorry, an error occurred:') + ' ' + error.innerHTML; - } else { - return _.propertyOf(stanza.querySelector('body'))('textContent'); - } - }, - - onMessage(stanza) { - /* Handler method for all incoming single-user chat "message" - * stanzas. - * - * Parameters: - * (XMLElement) stanza - The incoming message stanza - */ - let to_jid = stanza.getAttribute('to'); - const to_resource = Strophe.getResourceFromJid(to_jid); - - if (_converse.filter_by_resource && to_resource && to_resource !== _converse.resource) { - _converse.log(`onMessage: Ignoring incoming message intended for a different resource: ${to_jid}`, Strophe.LogLevel.INFO); - - return true; - } else if (utils.isHeadlineMessage(_converse, stanza)) { - // XXX: Ideally we wouldn't have to check for headline - // messages, but Prosody sends headline messages with the - // wrong type ('chat'), so we need to filter them out here. - _converse.log(`onMessage: Ignoring incoming headline message sent with type 'chat' from JID: ${stanza.getAttribute('from')}`, Strophe.LogLevel.INFO); - - return true; - } - - let from_jid = stanza.getAttribute('from'); - const forwarded = stanza.querySelector('forwarded'), - original_stanza = stanza; - - if (!_.isNull(forwarded)) { - const forwarded_message = forwarded.querySelector('message'), - forwarded_from = forwarded_message.getAttribute('from'), - is_carbon = !_.isNull(stanza.querySelector(`received[xmlns="${Strophe.NS.CARBONS}"]`)); - - if (is_carbon && Strophe.getBareJidFromJid(forwarded_from) !== from_jid) { - // Prevent message forging via carbons - // https://xmpp.org/extensions/xep-0280.html#security - return true; - } - - stanza = forwarded_message; - from_jid = stanza.getAttribute('from'); - to_jid = stanza.getAttribute('to'); - } - - const from_bare_jid = Strophe.getBareJidFromJid(from_jid), - from_resource = Strophe.getResourceFromJid(from_jid), - is_me = from_bare_jid === _converse.bare_jid; - let contact_jid; - - if (is_me) { - // I am the sender, so this must be a forwarded message... - if (_.isNull(to_jid)) { - return _converse.log(`Don't know how to handle message stanza without 'to' attribute. ${stanza.outerHTML}`, Strophe.LogLevel.ERROR); - } - - contact_jid = Strophe.getBareJidFromJid(to_jid); - } else { - contact_jid = from_bare_jid; - } - - const attrs = { - 'fullname': _.get(_converse.api.contacts.get(contact_jid), 'attributes.fullname') // Get chat box, but only create a new one when the message has a body. - - }; - const has_body = sizzle(`body, encrypted[xmlns="${Strophe.NS.OMEMO}`).length > 0; - const chatbox = this.getChatBox(contact_jid, attrs, has_body); - - if (chatbox && !chatbox.handleMessageCorrection(stanza)) { - const msgid = stanza.getAttribute('id'), - message = msgid && chatbox.messages.findWhere({ - msgid - }); - - if (!message) { - // Only create the message when we're sure it's not a duplicate - chatbox.createMessage(stanza, original_stanza).then(msg => chatbox.incrementUnreadMsgCounter(msg)).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); - } - } - - _converse.emit('message', { - 'stanza': original_stanza, - 'chatbox': chatbox - }); - - return true; - }, - - getChatBox(jid, attrs = {}, create) { - /* Returns a chat box or optionally return a newly - * created one if one doesn't exist. - * - * Parameters: - * (String) jid - The JID of the user whose chat box we want - * (Boolean) create - Should a new chat box be created if none exists? - * (Object) attrs - Optional chat box atributes. - */ - if (_.isObject(jid)) { - create = attrs; - attrs = jid; - jid = attrs.jid; - } - - jid = Strophe.getBareJidFromJid(jid.toLowerCase()); - let chatbox = this.get(Strophe.getBareJidFromJid(jid)); - - if (!chatbox && create) { - _.extend(attrs, { - 'jid': jid, - 'id': jid - }); - - chatbox = this.create(attrs, { - 'error'(model, response) { - _converse.log(response.responseText); - } - - }); - } - - return chatbox; - } - - }); - - function autoJoinChats() { - /* Automatically join private chats, based on the - * "auto_join_private_chats" configuration setting. - */ - _.each(_converse.auto_join_private_chats, function (jid) { - if (_converse.chatboxes.where({ - 'jid': jid - }).length) { - return; - } - - if (_.isString(jid)) { - _converse.api.chats.open(jid); - } else { - _converse.log('Invalid jid criteria specified for "auto_join_private_chats"', Strophe.LogLevel.ERROR); - } - }); - - _converse.emit('privateChatsAutoJoined'); - } - /************************ BEGIN Event Handlers ************************/ - - - _converse.on('chatBoxesFetched', autoJoinChats); - - _converse.api.waitUntil('rosterContactsFetched').then(() => { - _converse.roster.on('add', contact => { - /* When a new contact is added, check if we already have a - * chatbox open for it, and if so attach it to the chatbox. - */ - const chatbox = _converse.chatboxes.findWhere({ - 'jid': contact.get('jid') - }); - - if (chatbox) { - chatbox.addRelatedContact(contact); - } - }); - }); - - _converse.on('addClientFeatures', () => { - _converse.api.disco.own.features.add(Strophe.NS.MESSAGE_CORRECT); - - _converse.api.disco.own.features.add(Strophe.NS.HTTPUPLOAD); - - _converse.api.disco.own.features.add(Strophe.NS.OUTOFBAND); - }); - - _converse.api.listen.on('pluginsInitialized', () => { - _converse.chatboxes = new _converse.ChatBoxes(); - - _converse.emit('chatBoxesInitialized'); - }); - - _converse.api.listen.on('presencesInitialized', () => _converse.chatboxes.onConnected()); - /************************ END Event Handlers ************************/ - - /************************ BEGIN API ************************/ - - - _.extend(_converse.api, { - /** - * The "chats" namespace (used for one-on-one chats) - * - * @namespace _converse.api.chats - * @memberOf _converse.api - */ - 'chats': { - /** - * @method _converse.api.chats.create - * @param {string|string[]} jid|jids An jid or array of jids - * @param {object} attrs An object containing configuration attributes. - */ - 'create'(jids, attrs) { - if (_.isUndefined(jids)) { - _converse.log("chats.create: You need to provide at least one JID", Strophe.LogLevel.ERROR); - - return null; - } - - if (_.isString(jids)) { - if (attrs && !_.get(attrs, 'fullname')) { - attrs.fullname = _.get(_converse.api.contacts.get(jids), 'attributes.fullname'); - } - - const chatbox = _converse.chatboxes.getChatBox(jids, attrs, true); - - if (_.isNil(chatbox)) { - _converse.log("Could not open chatbox for JID: " + jids, Strophe.LogLevel.ERROR); - - return; - } - - return chatbox; - } - - return _.map(jids, jid => { - attrs.fullname = _.get(_converse.api.contacts.get(jid), 'attributes.fullname'); - return _converse.chatboxes.getChatBox(jid, attrs, true).trigger('show'); - }); - }, - - /** - * Opens a new one-on-one chat. - * - * @method _converse.api.chats.open - * @param {String|string[]} name - e.g. 'buddy@example.com' or ['buddy1@example.com', 'buddy2@example.com'] - * @returns {Promise} Promise which resolves with the Backbone.Model representing the chat. - * - * @example - * // To open a single chat, provide the JID of the contact you're chatting with in that chat: - * converse.plugins.add('myplugin', { - * initialize: function() { - * var _converse = this._converse; - * // Note, buddy@example.org must be in your contacts roster! - * _converse.api.chats.open('buddy@example.com').then((chat) => { - * // Now you can do something with the chat model - * }); - * } - * }); - * - * @example - * // To open an array of chats, provide an array of JIDs: - * converse.plugins.add('myplugin', { - * initialize: function () { - * var _converse = this._converse; - * // Note, these users must first be in your contacts roster! - * _converse.api.chats.open(['buddy1@example.com', 'buddy2@example.com']).then((chats) => { - * // Now you can do something with the chat models - * }); - * } - * }); - * - */ - 'open'(jids, attrs) { - return new Promise((resolve, reject) => { - Promise.all([_converse.api.waitUntil('rosterContactsFetched'), _converse.api.waitUntil('chatBoxesFetched')]).then(() => { - if (_.isUndefined(jids)) { - const err_msg = "chats.open: You need to provide at least one JID"; - - _converse.log(err_msg, Strophe.LogLevel.ERROR); - - reject(new Error(err_msg)); - } else if (_.isString(jids)) { - resolve(_converse.api.chats.create(jids, attrs).trigger('show')); - } else { - resolve(_.map(jids, jid => _converse.api.chats.create(jid, attrs).trigger('show'))); - } - }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); - }); - }, - - /** - * Returns a chat model. The chat should already be open. - * - * @method _converse.api.chats.get - * @param {String|string[]} name - e.g. 'buddy@example.com' or ['buddy1@example.com', 'buddy2@example.com'] - * @returns {Backbone.Model} - * - * @example - * // To return a single chat, provide the JID of the contact you're chatting with in that chat: - * const model = _converse.api.chats.get('buddy@example.com'); - * - * @example - * // To return an array of chats, provide an array of JIDs: - * const models = _converse.api.chats.get(['buddy1@example.com', 'buddy2@example.com']); - * - * @example - * // To return all open chats, call the method without any parameters:: - * const models = _converse.api.chats.get(); - * - */ - 'get'(jids) { - if (_.isUndefined(jids)) { - const result = []; - - _converse.chatboxes.each(function (chatbox) { - // FIXME: Leaky abstraction from MUC. We need to add a - // base type for chat boxes, and check for that. - if (chatbox.get('type') !== _converse.CHATROOMS_TYPE) { - result.push(chatbox); - } - }); - - return result; - } else if (_.isString(jids)) { - return _converse.chatboxes.getChatBox(jids); - } - - return _.map(jids, _.partial(_converse.chatboxes.getChatBox.bind(_converse.chatboxes), _, {}, true)); - } - - } - }); - /************************ END API ************************/ - - } - - }); - return converse; -}); - -/***/ }), - /***/ "./src/converse-chatboxviews.js": /*!**************************************!*\ !*** ./src/converse-chatboxviews.js ***! @@ -60872,7 +58485,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ // Copyright (c) 2012-2018, the Converse.js developers // Licensed under the Mozilla Public License (MPLv2) (function (root, factory) { - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! converse-core */ "./src/converse-core.js"), __webpack_require__(/*! templates/chatboxes.html */ "./src/templates/chatboxes.html"), __webpack_require__(/*! converse-chatboxes */ "./src/converse-chatboxes.js"), __webpack_require__(/*! backbone.overview */ "./node_modules/backbone.overview/backbone.overview.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! @converse/headless/converse-core */ "./src/headless/converse-core.js"), __webpack_require__(/*! templates/chatboxes.html */ "./src/templates/chatboxes.html"), __webpack_require__(/*! @converse/headless/converse-chatboxes */ "./src/headless/converse-chatboxes.js"), __webpack_require__(/*! backbone.nativeview */ "./node_modules/backbone.nativeview/backbone.nativeview.js"), __webpack_require__(/*! backbone.overview */ "./node_modules/backbone.overview/backbone.overview.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); @@ -61067,7 +58680,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ // Copyright (c) 2012-2018, the Converse.js developers // Licensed under the Mozilla Public License (MPLv2) (function (root, factory) { - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! utils/emoji */ "./src/utils/emoji.js"), __webpack_require__(/*! converse-core */ "./src/converse-core.js"), __webpack_require__(/*! bootstrap */ "./node_modules/bootstrap.native/dist/bootstrap-native-v4.js"), __webpack_require__(/*! twemoji */ "./node_modules/twemoji/2/esm.js"), __webpack_require__(/*! xss */ "./node_modules/xss/dist/xss.js"), __webpack_require__(/*! templates/chatbox.html */ "./src/templates/chatbox.html"), __webpack_require__(/*! templates/chatbox_head.html */ "./src/templates/chatbox_head.html"), __webpack_require__(/*! templates/chatbox_message_form.html */ "./src/templates/chatbox_message_form.html"), __webpack_require__(/*! templates/emojis.html */ "./src/templates/emojis.html"), __webpack_require__(/*! templates/error_message.html */ "./src/templates/error_message.html"), __webpack_require__(/*! templates/help_message.html */ "./src/templates/help_message.html"), __webpack_require__(/*! templates/info.html */ "./src/templates/info.html"), __webpack_require__(/*! templates/new_day.html */ "./src/templates/new_day.html"), __webpack_require__(/*! templates/user_details_modal.html */ "./src/templates/user_details_modal.html"), __webpack_require__(/*! templates/toolbar_fileupload.html */ "./src/templates/toolbar_fileupload.html"), __webpack_require__(/*! templates/spinner.html */ "./src/templates/spinner.html"), __webpack_require__(/*! templates/spoiler_button.html */ "./src/templates/spoiler_button.html"), __webpack_require__(/*! templates/status_message.html */ "./src/templates/status_message.html"), __webpack_require__(/*! templates/toolbar.html */ "./src/templates/toolbar.html"), __webpack_require__(/*! converse-modal */ "./src/converse-modal.js"), __webpack_require__(/*! converse-chatboxviews */ "./src/converse-chatboxviews.js"), __webpack_require__(/*! converse-message-view */ "./src/converse-message-view.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! utils/emoji */ "./src/headless/utils/emoji.js"), __webpack_require__(/*! @converse/headless/converse-core */ "./src/headless/converse-core.js"), __webpack_require__(/*! bootstrap */ "./node_modules/bootstrap.native/dist/bootstrap-native-v4.js"), __webpack_require__(/*! twemoji */ "./node_modules/twemoji/2/esm.js"), __webpack_require__(/*! xss */ "./node_modules/xss/dist/xss.js"), __webpack_require__(/*! templates/chatbox.html */ "./src/templates/chatbox.html"), __webpack_require__(/*! templates/chatbox_head.html */ "./src/templates/chatbox_head.html"), __webpack_require__(/*! templates/chatbox_message_form.html */ "./src/templates/chatbox_message_form.html"), __webpack_require__(/*! templates/emojis.html */ "./src/templates/emojis.html"), __webpack_require__(/*! templates/error_message.html */ "./src/templates/error_message.html"), __webpack_require__(/*! templates/help_message.html */ "./src/templates/help_message.html"), __webpack_require__(/*! templates/info.html */ "./src/templates/info.html"), __webpack_require__(/*! templates/new_day.html */ "./src/templates/new_day.html"), __webpack_require__(/*! templates/user_details_modal.html */ "./src/templates/user_details_modal.html"), __webpack_require__(/*! templates/toolbar_fileupload.html */ "./src/templates/toolbar_fileupload.html"), __webpack_require__(/*! templates/spinner.html */ "./src/templates/spinner.html"), __webpack_require__(/*! templates/spoiler_button.html */ "./src/templates/spoiler_button.html"), __webpack_require__(/*! templates/status_message.html */ "./src/templates/status_message.html"), __webpack_require__(/*! templates/toolbar.html */ "./src/templates/toolbar.html"), __webpack_require__(/*! converse-modal */ "./src/converse-modal.js"), __webpack_require__(/*! converse-chatboxviews */ "./src/converse-chatboxviews.js"), __webpack_require__(/*! converse-message-view */ "./src/converse-message-view.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); @@ -62404,11 +60017,11 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ * @method _converse.api.chatviews.get * @returns {ChatBoxView} A [Backbone.View](http://backbonejs.org/#View) instance. * The chat should already be open, otherwise `undefined` will be returned. - * + * * @example * // To return a single view, provide the JID of the contact: * _converse.api.chatviews.get('buddy@example.com') - * + * * @example * // To return an array of views, provide an array of JIDs: * _converse.api.chatviews.get(['buddy1@example.com', 'buddy2@example.com']) @@ -62455,7 +60068,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /*global define */ (function (root, factory) { - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! converse-core */ "./src/converse-core.js"), __webpack_require__(/*! bootstrap */ "./node_modules/bootstrap.native/dist/bootstrap-native-v4.js"), __webpack_require__(/*! formdata-polyfill */ "./node_modules/formdata-polyfill/FormData.js"), __webpack_require__(/*! lodash.fp */ "./src/lodash.fp.js"), __webpack_require__(/*! templates/converse_brand_heading.html */ "./src/templates/converse_brand_heading.html"), __webpack_require__(/*! templates/controlbox.html */ "./src/templates/controlbox.html"), __webpack_require__(/*! templates/controlbox_toggle.html */ "./src/templates/controlbox_toggle.html"), __webpack_require__(/*! templates/login_panel.html */ "./src/templates/login_panel.html"), __webpack_require__(/*! converse-chatview */ "./src/converse-chatview.js"), __webpack_require__(/*! converse-rosterview */ "./src/converse-rosterview.js"), __webpack_require__(/*! converse-profile */ "./src/converse-profile.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! @converse/headless/converse-core */ "./src/headless/converse-core.js"), __webpack_require__(/*! bootstrap */ "./node_modules/bootstrap.native/dist/bootstrap-native-v4.js"), __webpack_require__(/*! formdata-polyfill */ "./node_modules/formdata-polyfill/FormData.js"), __webpack_require__(/*! @converse/headless/lodash.fp */ "./src/headless/lodash.fp.js"), __webpack_require__(/*! templates/converse_brand_heading.html */ "./src/templates/converse_brand_heading.html"), __webpack_require__(/*! templates/controlbox.html */ "./src/templates/controlbox.html"), __webpack_require__(/*! templates/controlbox_toggle.html */ "./src/templates/controlbox_toggle.html"), __webpack_require__(/*! templates/login_panel.html */ "./src/templates/login_panel.html"), __webpack_require__(/*! converse-chatview */ "./src/converse-chatview.js"), __webpack_require__(/*! converse-rosterview */ "./src/converse-rosterview.js"), __webpack_require__(/*! converse-profile */ "./src/converse-profile.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); @@ -63109,2621 +60722,6 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /***/ }), -/***/ "./src/converse-core.js": -/*!******************************!*\ - !*** ./src/converse-core.js ***! - \******************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;// Converse.js -// https://conversejs.org -// -// Copyright (c) 2013-2018, the Converse.js developers -// Licensed under the Mozilla Public License (MPLv2) -(function (root, factory) { - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! sizzle */ "./node_modules/sizzle/dist/sizzle.js"), __webpack_require__(/*! es6-promise */ "./node_modules/es6-promise/dist/es6-promise.auto.js"), __webpack_require__(/*! lodash.noconflict */ "./src/lodash.noconflict.js"), __webpack_require__(/*! lodash.fp */ "./src/lodash.fp.js"), __webpack_require__(/*! polyfill */ "./src/polyfill.js"), __webpack_require__(/*! i18n */ "./src/i18n.js"), __webpack_require__(/*! utils/core */ "./src/utils/core.js"), __webpack_require__(/*! moment */ "./node_modules/moment/moment.js"), __webpack_require__(/*! strophe.js */ "./node_modules/strophe.js/dist/strophe.js"), __webpack_require__(/*! pluggable */ "./node_modules/pluggable.js/dist/pluggable.js"), __webpack_require__(/*! backbone.noconflict */ "./src/backbone.noconflict.js"), __webpack_require__(/*! backbone.nativeview */ "./node_modules/backbone.nativeview/backbone.nativeview.js"), __webpack_require__(/*! backbone.browserStorage */ "./node_modules/backbone.browserStorage/backbone.browserStorage.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), - __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? - (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), - __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); -})(this, function (sizzle, Promise, _, f, polyfill, i18n, u, moment, Strophe, pluggable, Backbone) { - "use strict"; // Strophe globals - - const _Strophe = Strophe, - $build = _Strophe.$build, - $iq = _Strophe.$iq, - $msg = _Strophe.$msg, - $pres = _Strophe.$pres; - const b64_sha1 = Strophe.SHA1.b64_sha1; - Strophe = Strophe.Strophe; // Add Strophe Namespaces - - Strophe.addNamespace('CARBONS', 'urn:xmpp:carbons:2'); - Strophe.addNamespace('CHATSTATES', 'http://jabber.org/protocol/chatstates'); - Strophe.addNamespace('CSI', 'urn:xmpp:csi:0'); - Strophe.addNamespace('DELAY', 'urn:xmpp:delay'); - Strophe.addNamespace('FORWARD', 'urn:xmpp:forward:0'); - Strophe.addNamespace('HINTS', 'urn:xmpp:hints'); - Strophe.addNamespace('HTTPUPLOAD', 'urn:xmpp:http:upload:0'); - Strophe.addNamespace('MAM', 'urn:xmpp:mam:2'); - Strophe.addNamespace('NICK', 'http://jabber.org/protocol/nick'); - Strophe.addNamespace('OMEMO', "eu.siacs.conversations.axolotl"); - Strophe.addNamespace('OUTOFBAND', 'jabber:x:oob'); - Strophe.addNamespace('PUBSUB', 'http://jabber.org/protocol/pubsub'); - Strophe.addNamespace('REGISTER', 'jabber:iq:register'); - Strophe.addNamespace('ROSTERX', 'http://jabber.org/protocol/rosterx'); - Strophe.addNamespace('RSM', 'http://jabber.org/protocol/rsm'); - Strophe.addNamespace('SID', 'urn:xmpp:sid:0'); - Strophe.addNamespace('SPOILER', 'urn:xmpp:spoiler:0'); - Strophe.addNamespace('VCARD', 'vcard-temp'); - Strophe.addNamespace('VCARDUPDATE', 'vcard-temp:x:update'); - Strophe.addNamespace('XFORM', 'jabber:x:data'); // Use Mustache style syntax for variable interpolation - - /* Configuration of Lodash templates (this config is distinct to the - * config of requirejs-tpl in main.js). This one is for normal inline templates. - */ - - _.templateSettings = { - 'escape': /\{\{\{([\s\S]+?)\}\}\}/g, - 'evaluate': /\{\[([\s\S]+?)\]\}/g, - 'interpolate': /\{\{([\s\S]+?)\}\}/g, - 'imports': { - '_': _ - } - }; - /** - * A private, closured object containing the private api (via `_converse.api`) - * as well as private methods and internal data-structures. - * - * @namespace _converse - */ - - const _converse = { - 'templates': {}, - 'promises': {} - }; - - _.extend(_converse, Backbone.Events); // Core plugins are whitelisted automatically - - - _converse.core_plugins = ['converse-autocomplete', 'converse-bookmarks', 'converse-caps', 'converse-chatboxes', 'converse-chatboxviews', 'converse-chatview', 'converse-controlbox', 'converse-core', 'converse-disco', 'converse-dragresize', 'converse-embedded', 'converse-fullscreen', 'converse-headline', 'converse-mam', 'converse-message-view', 'converse-minimize', 'converse-modal', 'converse-muc', 'converse-muc-views', 'converse-notification', 'converse-omemo', 'converse-ping', 'converse-profile', 'converse-push', 'converse-register', 'converse-roomslist', 'converse-roster', 'converse-rosterview', 'converse-singleton', 'converse-spoilers', 'converse-vcard']; // Setting wait to 59 instead of 60 to avoid timing conflicts with the - // webserver, which is often also set to 60 and might therefore sometimes - // return a 504 error page instead of passing through to the BOSH proxy. - - const BOSH_WAIT = 59; // Make converse pluggable - - pluggable.enable(_converse, '_converse', 'pluggable'); - _converse.keycodes = { - TAB: 9, - ENTER: 13, - SHIFT: 16, - CTRL: 17, - ALT: 18, - ESCAPE: 27, - UP_ARROW: 38, - DOWN_ARROW: 40, - FORWARD_SLASH: 47, - AT: 50, - META: 91, - META_RIGHT: 93 - }; // Module-level constants - - _converse.STATUS_WEIGHTS = { - 'offline': 6, - 'unavailable': 5, - 'xa': 4, - 'away': 3, - 'dnd': 2, - 'chat': 1, - // We currently don't differentiate between "chat" and "online" - 'online': 1 - }; - _converse.PRETTY_CHAT_STATUS = { - 'offline': 'Offline', - 'unavailable': 'Unavailable', - 'xa': 'Extended Away', - 'away': 'Away', - 'dnd': 'Do not disturb', - 'chat': 'Chattty', - 'online': 'Online' - }; - _converse.ANONYMOUS = "anonymous"; - _converse.CLOSED = 'closed'; - _converse.EXTERNAL = "external"; - _converse.LOGIN = "login"; - _converse.LOGOUT = "logout"; - _converse.OPENED = 'opened'; - _converse.PREBIND = "prebind"; - _converse.IQ_TIMEOUT = 20000; - _converse.CONNECTION_STATUS = { - 0: 'ERROR', - 1: 'CONNECTING', - 2: 'CONNFAIL', - 3: 'AUTHENTICATING', - 4: 'AUTHFAIL', - 5: 'CONNECTED', - 6: 'DISCONNECTED', - 7: 'DISCONNECTING', - 8: 'ATTACHED', - 9: 'REDIRECT', - 10: 'RECONNECTING' - }; - _converse.SUCCESS = 'success'; - _converse.FAILURE = 'failure'; - _converse.DEFAULT_IMAGE_TYPE = 'image/png'; - _converse.DEFAULT_IMAGE = "iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAIAAABt+uBvAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gwHCy455JBsggAABkJJREFUeNrtnM1PE1sUwHvvTD8otWLHST/Gimi1CEgr6M6FEWuIBo2pujDVsNDEP8GN/4MbN7oxrlipG2OCgZgYlxAbkRYw1KqkIDRCSkM7nXvvW8x7vjyNeQ9m7p1p3z1LQk/v/Dhz7vkEXL161cHl9wI5Ag6IA+KAOCAOiAPigDggLhwQB2S+iNZ+PcYY/SWEEP2HAAAIoSAIoihCCP+ngDDGtVotGAz29/cfOXJEUZSOjg6n06lp2sbGRqlUWlhYyGazS0tLbrdbEASrzgksyeYJId3d3el0uqenRxRFAAAA4KdfIIRgjD9+/Pj8+fOpqSndslofEIQwHA6Pjo4mEon//qmFhYXHjx8vLi4ihBgDEnp7e9l8E0Jo165dQ0NDd+/eDYVC2/qsJElDQ0OEkKWlpa2tLZamxAhQo9EIBoOjo6MXL17csZLe3l5FUT59+lQul5l5JRaAVFWNRqN37tw5ceKEQVWRSOTw4cOFQuHbt2+iKLYCIISQLMu3b99OJpOmKAwEAgcPHszn8+vr6wzsiG6UQQhxuVyXLl0aGBgwUW0sFstkMl6v90fo1KyAMMYDAwPnzp0zXfPg4GAqlWo0Gk0MiBAiy/L58+edTqf5Aa4onj59OhaLYYybFRCEMBaL0fNxBw4cSCQStN0QRUBut3t4eJjq6U+dOiVJElVPRBFQIBDo6+ujCqirqyscDlONGykC2lYyYSR6pBoQQapHZwAoHo/TuARYAOrs7GQASFEUqn6aIiBJkhgA6ujooFpUo6iaTa7koFwnaoWadLNe81tbWwzoaJrWrICWl5cZAFpbW6OabVAEtLi4yABQsVjUNK0pAWWzWQaAcrlcswKanZ1VVZUqHYRQEwOq1Wpv3ryhCmh6erpcLjdrNl+v1ycnJ+l5UELI27dvv3//3qxxEADgy5cvExMT9Mznw4cPtFtAdAPFarU6Pj5eKpVM17yxsfHy5cvV1VXazXu62gVBKBQKT58+rdVqJqrFGL948eLdu3dU8/g/H4FBUaJYLAqC0NPTY9brMD4+PjY25mDSracOCABACJmZmXE6nUePHjWu8NWrV48ePSKEsGlAs7Agfd5nenq6Wq0mk0kjDzY2NvbkyRMIIbP2PLvhBUEQ8vl8NpuNx+M+n29bzhVjvLKycv/+/YmJCcazQuwA6YzW1tYmJyf1SY+2trZ/rRk1Go1SqfT69esHDx4UCgVmNaa/zZ/9ABUhRFXVYDB48uTJeDweiUQkSfL7/T9MA2NcqVTK5fLy8vL8/PzU1FSxWHS5XJaM4wGr9sUwxqqqer3eUCgkSZJuUBBCfTRvc3OzXC6vrKxUKhWn02nhCJ5lM4oQQo/HgxD6+vXr58+fHf8sDOp+HQDg8XgclorFU676dKLlo6yWRdItIBwQB8QBcUCtfosRQjRNQwhhjPUC4w46WXryBSHU1zgEQWBz99EFhDGu1+t+v//48ePxeFxRlD179ng8nh0Efgiher2+vr6ur3HMzMysrq7uTJVdACGEurq6Ll++nEgkPB7Pj9jPoDHqOxyqqubz+WfPnuVyuV9XPeyeagAAAoHArVu3BgcHab8CuVzu4cOHpVKJUnfA5GweY+xyuc6cOXPv3r1IJMLAR8iyPDw8XK/Xi8Wiqqqmm5KZgBBC7e3tN27cuHbtGuPVpf7+/lAoNDs7W61WzfVKpgHSSzw3b95MpVKW3MfRaDQSiczNzVUqFRMZmQOIEOL1eq9fv3727FlL1t50URRFluX5+flqtWpWEGAOIFEUU6nUlStXLKSjy759+xwOx9zcnKZpphzGHMzhcDiTydgk9r1w4YIp7RPTAAmCkMlk2FeLf/tIEKbTab/fbwtAhJBoNGrutpNx6e7uPnTokC1eMU3T0um0DZPMkZER6wERQnw+n/FFSxpy7Nix3bt3WwwIIcRgIWnHkkwmjecfRgGx7DtuV/r6+iwGhDHev3+/bQF1dnYaH6E2CkiWZdsC2rt3r8WAHA5HW1ubbQGZcjajgOwTH/4qNko1Wlg4IA6IA+KAOKBWBUQIsfNojyliKIoRRfH9+/dut9umf3wzpoUNNQ4BAJubmwz+ic+OxefzWWlBhJD29nbug7iT5sIBcUAcEAfEAXFAHBAHxOVn+QMrmWpuPZx12gAAAABJRU5ErkJggg=="; - _converse.TIMEOUTS = { - // Set as module attr so that we can override in tests. - 'PAUSED': 10000, - 'INACTIVE': 90000 - }; // XEP-0085 Chat states - // http://xmpp.org/extensions/xep-0085.html - - _converse.INACTIVE = 'inactive'; - _converse.ACTIVE = 'active'; - _converse.COMPOSING = 'composing'; - _converse.PAUSED = 'paused'; - _converse.GONE = 'gone'; // Chat types - - _converse.PRIVATE_CHAT_TYPE = 'chatbox'; - _converse.CHATROOMS_TYPE = 'chatroom'; - _converse.HEADLINES_TYPE = 'headline'; - _converse.CONTROLBOX_TYPE = 'controlbox'; // Default configuration values - // ---------------------------- - - _converse.default_settings = { - allow_non_roster_messaging: false, - animate: true, - authentication: 'login', - // Available values are "login", "prebind", "anonymous" and "external". - auto_away: 0, - // Seconds after which user status is set to 'away' - auto_login: false, - // Currently only used in connection with anonymous login - auto_reconnect: true, - auto_xa: 0, - // Seconds after which user status is set to 'xa' - blacklisted_plugins: [], - bosh_service_url: undefined, - connection_options: {}, - credentials_url: null, - // URL from where login credentials can be fetched - csi_waiting_time: 0, - // Support for XEP-0352. Seconds before client is considered idle and CSI is sent out. - debug: false, - default_state: 'online', - expose_rid_and_sid: false, - geouri_regex: /https:\/\/www.openstreetmap.org\/.*#map=[0-9]+\/([\-0-9.]+)\/([\-0-9.]+)\S*/g, - geouri_replacement: 'https://www.openstreetmap.org/?mlat=$1&mlon=$2#map=18/$1/$2', - jid: undefined, - keepalive: true, - locales_url: 'locale/{{{locale}}}/LC_MESSAGES/converse.json', - locales: ['af', 'ar', 'bg', 'ca', 'cs', 'de', 'es', 'eu', 'en', 'fr', 'he', 'hi', 'hu', 'id', 'it', 'ja', 'nb', 'nl', 'pl', 'pt_BR', 'ro', 'ru', 'tr', 'uk', 'zh_CN', 'zh_TW'], - message_carbons: true, - nickname: undefined, - password: undefined, - prebind_url: null, - priority: 0, - rid: undefined, - root: window.document, - sid: undefined, - strict_plugin_dependencies: false, - trusted: true, - view_mode: 'overlayed', - // Choices are 'overlayed', 'fullscreen', 'mobile' - websocket_url: undefined, - whitelisted_plugins: [] - }; - - _converse.log = function (message, level, style = '') { - /* Logs messages to the browser's developer console. - * - * Parameters: - * (String) message - The message to be logged. - * (Integer) level - The loglevel which allows for filtering of log - * messages. - * - * Available loglevels are 0 for 'debug', 1 for 'info', 2 for 'warn', - * 3 for 'error' and 4 for 'fatal'. - * - * When using the 'error' or 'warn' loglevels, a full stacktrace will be - * logged as well. - */ - if (level === Strophe.LogLevel.ERROR || level === Strophe.LogLevel.FATAL) { - style = style || 'color: maroon'; - } - - if (message instanceof Error) { - message = message.stack; - } else if (_.isElement(message)) { - message = message.outerHTML; - } - - const prefix = style ? '%c' : ''; - - const logger = _.assign({ - 'debug': _.get(console, 'log') ? console.log.bind(console) : _.noop, - 'error': _.get(console, 'log') ? console.log.bind(console) : _.noop, - 'info': _.get(console, 'log') ? console.log.bind(console) : _.noop, - 'warn': _.get(console, 'log') ? console.log.bind(console) : _.noop - }, console); - - if (level === Strophe.LogLevel.ERROR) { - logger.error(`${prefix} ERROR: ${message}`, style); - } else if (level === Strophe.LogLevel.WARN) { - if (_converse.debug) { - logger.warn(`${prefix} ${moment().format()} WARNING: ${message}`, style); - } - } else if (level === Strophe.LogLevel.FATAL) { - logger.error(`${prefix} FATAL: ${message}`, style); - } else if (_converse.debug) { - if (level === Strophe.LogLevel.DEBUG) { - logger.debug(`${prefix} ${moment().format()} DEBUG: ${message}`, style); - } else { - logger.info(`${prefix} ${moment().format()} INFO: ${message}`, style); - } - } - }; - - Strophe.log = function (level, msg) { - _converse.log(level + ' ' + msg, level); - }; - - Strophe.error = function (msg) { - _converse.log(msg, Strophe.LogLevel.ERROR); - }; - - _converse.__ = function (str) { - /* Translate the given string based on the current locale. - * - * Parameters: - * (String) str - The string to translate. - */ - if (_.isUndefined(i18n)) { - return str; - } - - return i18n.translate.apply(i18n, arguments); - }; - - const __ = _converse.__; - const PROMISES = ['initialized', 'connectionInitialized', 'pluginsInitialized', 'statusInitialized']; - - function addPromise(promise) { - /* Private function, used to add a new promise to the ones already - * available via the `waitUntil` api method. - */ - _converse.promises[promise] = u.getResolveablePromise(); - } - - _converse.emit = function (name) { - /* Event emitter and promise resolver */ - _converse.trigger.apply(this, arguments); - - const promise = _converse.promises[name]; - - if (!_.isUndefined(promise)) { - promise.resolve(); - } - }; - - _converse.isSingleton = function () { - return _.includes(['mobile', 'fullscreen', 'embedded'], _converse.view_mode); - }; - - _converse.router = new Backbone.Router(); - - _converse.initialize = function (settings, callback) { - settings = !_.isUndefined(settings) ? settings : {}; - const init_promise = u.getResolveablePromise(); - - _.each(PROMISES, addPromise); - - if (!_.isUndefined(_converse.connection)) { - // Looks like _converse.initialized was called again without logging - // out or disconnecting in the previous session. - // This happens in tests. We therefore first clean up. - Backbone.history.stop(); - - _converse.chatboxviews.closeAllChatBoxes(); - - if (_converse.bookmarks) { - _converse.bookmarks.reset(); - } - - delete _converse.controlboxtoggle; - delete _converse.chatboxviews; - - _converse.connection.reset(); - - _converse.stopListening(); - - _converse.tearDown(); - - delete _converse.config; - - _converse.initClientConfig(); - - _converse.off(); - } - - if ('onpagehide' in window) { - // Pagehide gets thrown in more cases than unload. Specifically it - // gets thrown when the page is cached and not just - // closed/destroyed. It's the only viable event on mobile Safari. - // https://www.webkit.org/blog/516/webkit-page-cache-ii-the-unload-event/ - _converse.unloadevent = 'pagehide'; - } else if ('onbeforeunload' in window) { - _converse.unloadevent = 'beforeunload'; - } else if ('onunload' in window) { - _converse.unloadevent = 'unload'; - } - - _.assignIn(this, this.default_settings); // Allow only whitelisted configuration attributes to be overwritten - - - _.assignIn(this, _.pick(settings, _.keys(this.default_settings))); - - if (this.authentication === _converse.ANONYMOUS) { - if (this.auto_login && !this.jid) { - throw new Error("Config Error: you need to provide the server's " + "domain via the 'jid' option when using anonymous " + "authentication with auto_login."); - } - } - /* Localisation */ - - - if (!_.isUndefined(i18n)) { - i18n.setLocales(settings.i18n, _converse); - } else { - _converse.locale = 'en'; - } // Module-level variables - // ---------------------- - - - this.callback = callback || _.noop; - /* When reloading the page: - * For new sessions, we need to send out a presence stanza to notify - * the server/network that we're online. - * When re-attaching to an existing session (e.g. via the keepalive - * option), we don't need to again send out a presence stanza, because - * it's as if "we never left" (see onConnectStatusChanged). - * https://github.com/jcbrand/converse.js/issues/521 - */ - - this.send_initial_presence = true; - this.msg_counter = 0; - this.user_settings = settings; // Save the user settings so that they can be used by plugins - // Module-level functions - // ---------------------- - - this.generateResource = () => `/converse.js-${Math.floor(Math.random() * 139749528).toString()}`; - - this.sendCSI = function (stat) { - /* Send out a Chat Status Notification (XEP-0352) - * - * Parameters: - * (String) stat: The user's chat status - */ - - /* Send out a Chat Status Notification (XEP-0352) */ - // XXX if (converse.features[Strophe.NS.CSI] || true) { - _converse.connection.send($build(stat, { - xmlns: Strophe.NS.CSI - })); - - _converse.inactive = stat === _converse.INACTIVE ? true : false; - }; - - this.onUserActivity = function () { - /* Resets counters and flags relating to CSI and auto_away/auto_xa */ - if (_converse.idle_seconds > 0) { - _converse.idle_seconds = 0; - } - - if (!_converse.connection.authenticated) { - // We can't send out any stanzas when there's no authenticated connection. - // converse can happen when the connection reconnects. - return; - } - - if (_converse.inactive) { - _converse.sendCSI(_converse.ACTIVE); - } - - if (_converse.auto_changed_status === true) { - _converse.auto_changed_status = false; // XXX: we should really remember the original state here, and - // then set it back to that... - - _converse.xmppstatus.set('status', _converse.default_state); - } - }; - - this.onEverySecond = function () { - /* An interval handler running every second. - * Used for CSI and the auto_away and auto_xa features. - */ - if (!_converse.connection.authenticated) { - // We can't send out any stanzas when there's no authenticated connection. - // This can happen when the connection reconnects. - return; - } - - const stat = _converse.xmppstatus.get('status'); - - _converse.idle_seconds++; - - if (_converse.csi_waiting_time > 0 && _converse.idle_seconds > _converse.csi_waiting_time && !_converse.inactive) { - _converse.sendCSI(_converse.INACTIVE); - } - - if (_converse.auto_away > 0 && _converse.idle_seconds > _converse.auto_away && stat !== 'away' && stat !== 'xa' && stat !== 'dnd') { - _converse.auto_changed_status = true; - - _converse.xmppstatus.set('status', 'away'); - } else if (_converse.auto_xa > 0 && _converse.idle_seconds > _converse.auto_xa && stat !== 'xa' && stat !== 'dnd') { - _converse.auto_changed_status = true; - - _converse.xmppstatus.set('status', 'xa'); - } - }; - - this.registerIntervalHandler = function () { - /* Set an interval of one second and register a handler for it. - * Required for the auto_away, auto_xa and csi_waiting_time features. - */ - if (_converse.auto_away < 1 && _converse.auto_xa < 1 && _converse.csi_waiting_time < 1) { - // Waiting time of less then one second means features aren't used. - return; - } - - _converse.idle_seconds = 0; - _converse.auto_changed_status = false; // Was the user's status changed by _converse.js? - - window.addEventListener('click', _converse.onUserActivity); - window.addEventListener('focus', _converse.onUserActivity); - window.addEventListener('keypress', _converse.onUserActivity); - window.addEventListener('mousemove', _converse.onUserActivity); - const options = { - 'once': true, - 'passive': true - }; - window.addEventListener(_converse.unloadevent, _converse.onUserActivity, options); - _converse.everySecondTrigger = window.setInterval(_converse.onEverySecond, 1000); - }; - - this.setConnectionStatus = function (connection_status, message) { - _converse.connfeedback.set({ - 'connection_status': connection_status, - 'message': message - }); - }; - - this.rejectPresenceSubscription = function (jid, message) { - /* Reject or cancel another user's subscription to our presence updates. - * - * Parameters: - * (String) jid - The Jabber ID of the user whose subscription - * is being canceled. - * (String) message - An optional message to the user - */ - const pres = $pres({ - to: jid, - type: "unsubscribed" - }); - - if (message && message !== "") { - pres.c("status").t(message); - } - - _converse.connection.send(pres); - }; - - this.reconnect = _.debounce(function () { - _converse.log('RECONNECTING'); - - _converse.log('The connection has dropped, attempting to reconnect.'); - - _converse.setConnectionStatus(Strophe.Status.RECONNECTING, __('The connection has dropped, attempting to reconnect.')); - - _converse.connection.reconnecting = true; - - _converse.tearDown(); - - _converse.logIn(null, true); - }, 3000, { - 'leading': true - }); - - this.disconnect = function () { - _converse.log('DISCONNECTED'); - - delete _converse.connection.reconnecting; - - _converse.connection.reset(); - - _converse.tearDown(); - - _converse.clearSession(); - - _converse.emit('disconnected'); - }; - - this.onDisconnected = function () { - /* Gets called once strophe's status reaches Strophe.Status.DISCONNECTED. - * Will either start a teardown process for converse.js or attempt - * to reconnect. - */ - const reason = _converse.disconnection_reason; - - if (_converse.disconnection_cause === Strophe.Status.AUTHFAIL) { - if (_converse.credentials_url && _converse.auto_reconnect) { - /* In this case, we reconnect, because we might be receiving - * expirable tokens from the credentials_url. - */ - _converse.emit('will-reconnect'); - - return _converse.reconnect(); - } else { - return _converse.disconnect(); - } - } else if (_converse.disconnection_cause === _converse.LOGOUT || !_.isUndefined(reason) && reason === _.get(Strophe, 'ErrorCondition.NO_AUTH_MECH') || reason === "host-unknown" || reason === "remote-connection-failed" || !_converse.auto_reconnect) { - return _converse.disconnect(); - } - - _converse.emit('will-reconnect'); - - _converse.reconnect(); - }; - - this.setDisconnectionCause = function (cause, reason, override) { - /* Used to keep track of why we got disconnected, so that we can - * decide on what the next appropriate action is (in onDisconnected) - */ - if (_.isUndefined(cause)) { - delete _converse.disconnection_cause; - delete _converse.disconnection_reason; - } else if (_.isUndefined(_converse.disconnection_cause) || override) { - _converse.disconnection_cause = cause; - _converse.disconnection_reason = reason; - } - }; - - this.onConnectStatusChanged = function (status, message) { - /* Callback method called by Strophe as the Strophe.Connection goes - * through various states while establishing or tearing down a - * connection. - */ - _converse.log(`Status changed to: ${_converse.CONNECTION_STATUS[status]}`); - - if (status === Strophe.Status.CONNECTED || status === Strophe.Status.ATTACHED) { - _converse.setConnectionStatus(status); // By default we always want to send out an initial presence stanza. - - - _converse.send_initial_presence = true; - - _converse.setDisconnectionCause(); - - if (_converse.connection.reconnecting) { - _converse.log(status === Strophe.Status.CONNECTED ? 'Reconnected' : 'Reattached'); - - _converse.onConnected(true); - } else { - _converse.log(status === Strophe.Status.CONNECTED ? 'Connected' : 'Attached'); - - if (_converse.connection.restored) { - // No need to send an initial presence stanza when - // we're restoring an existing session. - _converse.send_initial_presence = false; - } - - _converse.onConnected(); - } - } else if (status === Strophe.Status.DISCONNECTED) { - _converse.setDisconnectionCause(status, message); - - _converse.onDisconnected(); - } else if (status === Strophe.Status.ERROR) { - _converse.setConnectionStatus(status, __('An error occurred while connecting to the chat server.')); - } else if (status === Strophe.Status.CONNECTING) { - _converse.setConnectionStatus(status); - } else if (status === Strophe.Status.AUTHENTICATING) { - _converse.setConnectionStatus(status); - } else if (status === Strophe.Status.AUTHFAIL) { - if (!message) { - message = __('Your Jabber ID and/or password is incorrect. Please try again.'); - } - - _converse.setConnectionStatus(status, message); - - _converse.setDisconnectionCause(status, message, true); - - _converse.onDisconnected(); - } else if (status === Strophe.Status.CONNFAIL) { - let feedback = message; - - if (message === "host-unknown" || message == "remote-connection-failed") { - feedback = __("Sorry, we could not connect to the XMPP host with domain: %1$s", `\"${Strophe.getDomainFromJid(_converse.connection.jid)}\"`); - } else if (!_.isUndefined(message) && message === _.get(Strophe, 'ErrorCondition.NO_AUTH_MECH')) { - feedback = __("The XMPP server did not offer a supported authentication mechanism"); - } - - _converse.setConnectionStatus(status, feedback); - - _converse.setDisconnectionCause(status, message); - } else if (status === Strophe.Status.DISCONNECTING) { - _converse.setDisconnectionCause(status, message); - } - }; - - this.incrementMsgCounter = function () { - this.msg_counter += 1; - const unreadMsgCount = this.msg_counter; - let title = document.title; - - if (_.isNil(title)) { - return; - } - - if (title.search(/^Messages \(\d+\) /) === -1) { - title = `Messages (${unreadMsgCount}) ${title}`; - } else { - title = title.replace(/^Messages \(\d+\) /, `Messages (${unreadMsgCount})`); - } - }; - - this.clearMsgCounter = function () { - this.msg_counter = 0; - let title = document.title; - - if (_.isNil(title)) { - return; - } - - if (title.search(/^Messages \(\d+\) /) !== -1) { - title = title.replace(/^Messages \(\d+\) /, ""); - } - }; - - this.initStatus = reconnecting => { - // If there's no xmppstatus obj, then we were never connected to - // begin with, so we set reconnecting to false. - reconnecting = _.isUndefined(_converse.xmppstatus) ? false : reconnecting; - - if (reconnecting) { - _converse.onStatusInitialized(reconnecting); - } else { - const id = `converse.xmppstatus-${_converse.bare_jid}`; - this.xmppstatus = new this.XMPPStatus({ - 'id': id - }); - this.xmppstatus.browserStorage = new Backbone.BrowserStorage.session(id); - this.xmppstatus.fetch({ - 'success': _.partial(_converse.onStatusInitialized, reconnecting), - 'error': _.partial(_converse.onStatusInitialized, reconnecting) - }); - } - }; - - this.initClientConfig = function () { - /* The client config refers to configuration of the client which is - * independent of any particular user. - * What this means is that config values need to persist across - * user sessions. - */ - const id = b64_sha1('converse.client-config'); - _converse.config = new Backbone.Model({ - 'id': id, - 'trusted': _converse.trusted && true || false, - 'storage': _converse.trusted ? 'local' : 'session' - }); - _converse.config.browserStorage = new Backbone.BrowserStorage.session(id); - - _converse.config.fetch(); - - _converse.emit('clientConfigInitialized'); - }; - - this.initSession = function () { - const id = b64_sha1('converse.bosh-session'); - _converse.session = new Backbone.Model({ - 'id': id - }); - _converse.session.browserStorage = new Backbone.BrowserStorage.session(id); - - _converse.session.fetch(); - - _converse.emit('sessionInitialized'); - }; - - this.clearSession = function () { - if (!_converse.config.get('trusted')) { - window.localStorage.clear(); - window.sessionStorage.clear(); - } else if (!_.isUndefined(this.session) && this.session.browserStorage) { - this.session.browserStorage._clear(); - } - - _converse.emit('clearSession'); - }; - - this.logOut = function () { - _converse.clearSession(); - - _converse.setDisconnectionCause(_converse.LOGOUT, undefined, true); - - if (!_.isUndefined(_converse.connection)) { - _converse.connection.disconnect(); - } else { - _converse.tearDown(); - } // Recreate all the promises - - - _.each(_.keys(_converse.promises), addPromise); - - _converse.emit('logout'); - }; - - this.saveWindowState = function (ev, hidden) { - // XXX: eventually we should be able to just use - // document.visibilityState (when we drop support for older - // browsers). - let state; - const event_map = { - 'focus': "visible", - 'focusin': "visible", - 'pageshow': "visible", - 'blur': "hidden", - 'focusout': "hidden", - 'pagehide': "hidden" - }; - ev = ev || document.createEvent('Events'); - - if (ev.type in event_map) { - state = event_map[ev.type]; - } else { - state = document[hidden] ? "hidden" : "visible"; - } - - if (state === 'visible') { - _converse.clearMsgCounter(); - } - - _converse.windowState = state; - - _converse.emit('windowStateChanged', { - state - }); - }; - - this.registerGlobalEventHandlers = function () { - // Taken from: - // http://stackoverflow.com/questions/1060008/is-there-a-way-to-detect-if-a-browser-window-is-not-currently-active - let hidden = "hidden"; // Standards: - - if (hidden in document) { - document.addEventListener("visibilitychange", _.partial(_converse.saveWindowState, _, hidden)); - } else if ((hidden = "mozHidden") in document) { - document.addEventListener("mozvisibilitychange", _.partial(_converse.saveWindowState, _, hidden)); - } else if ((hidden = "webkitHidden") in document) { - document.addEventListener("webkitvisibilitychange", _.partial(_converse.saveWindowState, _, hidden)); - } else if ((hidden = "msHidden") in document) { - document.addEventListener("msvisibilitychange", _.partial(_converse.saveWindowState, _, hidden)); - } else if ("onfocusin" in document) { - // IE 9 and lower: - document.onfocusin = document.onfocusout = _.partial(_converse.saveWindowState, _, hidden); - } else { - // All others: - window.onpageshow = window.onpagehide = window.onfocus = window.onblur = _.partial(_converse.saveWindowState, _, hidden); - } // set the initial state (but only if browser supports the Page Visibility API) - - - if (document[hidden] !== undefined) { - _.partial(_converse.saveWindowState, _, hidden)({ - type: document[hidden] ? "blur" : "focus" - }); - } - - _converse.emit('registeredGlobalEventHandlers'); - }; - - 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; - } - - const carbons_iq = new Strophe.Builder('iq', { - 'from': this.connection.jid, - 'id': 'enablecarbons', - 'type': 'set' - }).c('enable', { - xmlns: Strophe.NS.CARBONS - }); - this.connection.addHandler(iq => { - if (iq.querySelectorAll('error').length > 0) { - _converse.log('An error occurred while trying to enable message carbons.', Strophe.LogLevel.WARN); - } else { - this.session.save({ - 'carbons_enabled': true - }); - - _converse.log('Message carbons have been enabled.'); - } - }, null, "iq", null, "enablecarbons"); - this.connection.send(carbons_iq); - }; - - this.sendInitialPresence = function () { - if (_converse.send_initial_presence) { - _converse.xmppstatus.sendPresence(); - } - }; - - this.onStatusInitialized = function (reconnecting) { - _converse.emit('statusInitialized', reconnecting); - - if (reconnecting) { - _converse.emit('reconnected'); - } else { - init_promise.resolve(); - - _converse.emit('initialized'); - - _converse.emit('connected'); - } - }; - - this.setUserJID = function () { - _converse.jid = _converse.connection.jid; - _converse.bare_jid = Strophe.getBareJidFromJid(_converse.connection.jid); - _converse.resource = Strophe.getResourceFromJid(_converse.connection.jid); - _converse.domain = Strophe.getDomainFromJid(_converse.connection.jid); - - _converse.emit('setUserJID'); - }; - - this.onConnected = function (reconnecting) { - /* Called as soon as a new connection has been established, either - * by logging in or by attaching to an existing BOSH session. - */ - _converse.connection.flush(); // Solves problem of returned PubSub BOSH response not received by browser - - - _converse.setUserJID(); - - _converse.initSession(); - - _converse.enableCarbons(); - - _converse.initStatus(reconnecting); - }; - - this.ConnectionFeedback = Backbone.Model.extend({ - defaults: { - 'connection_status': Strophe.Status.DISCONNECTED, - 'message': '' - }, - - initialize() { - this.on('change', () => { - _converse.emit('connfeedback', _converse.connfeedback); - }); - } - - }); - this.connfeedback = new this.ConnectionFeedback(); - this.XMPPStatus = Backbone.Model.extend({ - defaults() { - return { - "jid": _converse.bare_jid, - "status": _converse.default_state - }; - }, - - initialize() { - this.vcard = _converse.vcards.findWhere({ - 'jid': this.get('jid') - }); - - if (_.isNil(this.vcard)) { - this.vcard = _converse.vcards.create({ - 'jid': this.get('jid') - }); - } - - this.on('change:status', item => { - const status = this.get('status'); - this.sendPresence(status); - - _converse.emit('statusChanged', status); - }); - this.on('change:status_message', () => { - const status_message = this.get('status_message'); - this.sendPresence(this.get('status'), status_message); - - _converse.emit('statusMessageChanged', status_message); - }); - }, - - constructPresence(type, status_message) { - let presence; - type = _.isString(type) ? type : this.get('status') || _converse.default_state; - status_message = _.isString(status_message) ? status_message : this.get('status_message'); // Most of these presence types are actually not explicitly sent, - // but I add all of them here for reference and future proofing. - - if (type === 'unavailable' || type === 'probe' || type === 'error' || type === 'unsubscribe' || type === 'unsubscribed' || type === 'subscribe' || type === 'subscribed') { - presence = $pres({ - 'type': type - }); - } else if (type === 'offline') { - presence = $pres({ - 'type': 'unavailable' - }); - } else if (type === 'online') { - presence = $pres(); - } else { - presence = $pres().c('show').t(type).up(); - } - - if (status_message) { - presence.c('status').t(status_message).up(); - } - - presence.c('priority').t(_.isNaN(Number(_converse.priority)) ? 0 : _converse.priority); - return presence; - }, - - sendPresence(type, status_message) { - _converse.connection.send(this.constructPresence(type, status_message)); - } - - }); - - this.setUpXMLLogging = function () { - Strophe.log = function (level, msg) { - _converse.log(msg, level); - }; - - if (this.debug) { - this.connection.xmlInput = function (body) { - _converse.log(body.outerHTML, Strophe.LogLevel.DEBUG, 'color: darkgoldenrod'); - }; - - this.connection.xmlOutput = function (body) { - _converse.log(body.outerHTML, Strophe.LogLevel.DEBUG, 'color: darkcyan'); - }; - } - }; - - this.fetchLoginCredentials = () => new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest(); - xhr.open('GET', _converse.credentials_url, true); - xhr.setRequestHeader('Accept', "application/json, text/javascript"); - - xhr.onload = function () { - if (xhr.status >= 200 && xhr.status < 400) { - const data = JSON.parse(xhr.responseText); - resolve({ - 'jid': data.jid, - 'password': data.password - }); - } else { - xhr.onerror(); - } - }; - - xhr.onerror = function () { - delete _converse.connection; - - _converse.emit('noResumeableSession', this); - - reject(xhr.responseText); - }; - - xhr.send(); - }); - - this.startNewBOSHSession = function () { - const xhr = new XMLHttpRequest(); - xhr.open('GET', _converse.prebind_url, true); - xhr.setRequestHeader('Accept', "application/json, text/javascript"); - - xhr.onload = function () { - if (xhr.status >= 200 && xhr.status < 400) { - const data = JSON.parse(xhr.responseText); - - _converse.connection.attach(data.jid, data.sid, data.rid, _converse.onConnectStatusChanged); - } else { - xhr.onerror(); - } - }; - - xhr.onerror = function () { - delete _converse.connection; - - _converse.emit('noResumeableSession', this); - }; - - xhr.send(); - }; - - this.restoreBOSHSession = function (jid_is_required) { - /* Tries to restore a cached BOSH session. */ - if (!this.jid) { - const msg = "restoreBOSHSession: tried to restore a \"keepalive\" session " + "but we don't have the JID for the user!"; - - if (jid_is_required) { - throw new Error(msg); - } else { - _converse.log(msg); - } - } - - try { - this.connection.restore(this.jid, this.onConnectStatusChanged); - return true; - } catch (e) { - _converse.log("Could not restore session for jid: " + this.jid + " Error message: " + e.message, Strophe.LogLevel.WARN); - - this.clearSession(); // We want to clear presences (see #555) - - return false; - } - }; - - this.attemptPreboundSession = function (reconnecting) { - /* Handle session resumption or initialization when prebind is - * being used. - */ - if (!reconnecting) { - if (this.keepalive && this.restoreBOSHSession(true)) { - return; - } // No keepalive, or session resumption has failed. - - - if (this.jid && this.sid && this.rid) { - return this.connection.attach(this.jid, this.sid, this.rid, this.onConnectStatusChanged); - } - } - - if (this.prebind_url) { - return this.startNewBOSHSession(); - } else { - throw new Error("attemptPreboundSession: If you use prebind and not keepalive, " + "then you MUST supply JID, RID and SID values or a prebind_url."); - } - }; - - this.attemptNonPreboundSession = function (credentials, reconnecting) { - /* Handle session resumption or initialization when prebind is not being used. - * - * Two potential options exist and are handled in this method: - * 1. keepalive - * 2. auto_login - */ - if (!reconnecting && this.keepalive && this.restoreBOSHSession()) { - return; - } - - if (credentials) { - // When credentials are passed in, they override prebinding - // or credentials fetching via HTTP - this.autoLogin(credentials); - } else if (this.auto_login) { - if (this.credentials_url) { - this.fetchLoginCredentials().then(this.autoLogin.bind(this), this.autoLogin.bind(this)); - } else if (!this.jid) { - throw new Error("attemptNonPreboundSession: If you use auto_login, " + "you also need to give either a jid value (and if " + "applicable a password) or you need to pass in a URL " + "from where the username and password can be fetched " + "(via credentials_url)."); - } else { - this.autoLogin(); // Could be ANONYMOUS or EXTERNAL - } - } else if (reconnecting) { - this.autoLogin(); - } - }; - - this.autoLogin = function (credentials) { - if (credentials) { - // If passed in, the credentials come from credentials_url, - // so we set them on the converse object. - this.jid = credentials.jid; - } - - if (this.authentication === _converse.ANONYMOUS || this.authentication === _converse.EXTERNAL) { - if (!this.jid) { - throw new Error("Config Error: when using anonymous login " + "you need to provide the server's domain via the 'jid' option. " + "Either when calling converse.initialize, or when calling " + "_converse.api.user.login."); - } - - if (!this.connection.reconnecting) { - this.connection.reset(); - } - - this.connection.connect(this.jid.toLowerCase(), null, this.onConnectStatusChanged, BOSH_WAIT); - } else if (this.authentication === _converse.LOGIN) { - const password = _.isNil(credentials) ? _converse.connection.pass || this.password : credentials.password; - - if (!password) { - if (this.auto_login) { - throw new Error("initConnection: If you use auto_login and " + "authentication='login' then you also need to provide a password."); - } - - _converse.setDisconnectionCause(Strophe.Status.AUTHFAIL, undefined, true); - - _converse.disconnect(); - - return; - } - - const resource = Strophe.getResourceFromJid(this.jid); - - if (!resource) { - this.jid = this.jid.toLowerCase() + _converse.generateResource(); - } else { - this.jid = Strophe.getBareJidFromJid(this.jid).toLowerCase() + '/' + resource; - } - - if (!this.connection.reconnecting) { - this.connection.reset(); - } - - this.connection.connect(this.jid, password, this.onConnectStatusChanged, BOSH_WAIT); - } - }; - - this.logIn = function (credentials, reconnecting) { - // We now try to resume or automatically set up a new session. - // Otherwise the user will be shown a login form. - if (this.authentication === _converse.PREBIND) { - this.attemptPreboundSession(reconnecting); - } else { - this.attemptNonPreboundSession(credentials, reconnecting); - } - }; - - this.initConnection = function () { - /* Creates a new Strophe.Connection instance if we don't already have one. - */ - if (!this.connection) { - if (!this.bosh_service_url && !this.websocket_url) { - throw new Error("initConnection: you must supply a value for either the bosh_service_url or websocket_url or both."); - } - - if (('WebSocket' in window || 'MozWebSocket' in window) && this.websocket_url) { - this.connection = new Strophe.Connection(this.websocket_url, this.connection_options); - } else if (this.bosh_service_url) { - this.connection = new Strophe.Connection(this.bosh_service_url, _.assignIn(this.connection_options, { - 'keepalive': this.keepalive - })); - } else { - throw new Error("initConnection: this browser does not support websockets and bosh_service_url wasn't specified."); - } - } - - _converse.emit('connectionInitialized'); - }; - - this.tearDown = function () { - /* Remove those views which are only allowed with a valid - * connection. - */ - _converse.emit('beforeTearDown'); - - if (!_.isUndefined(_converse.session)) { - _converse.session.destroy(); - } - - window.removeEventListener('click', _converse.onUserActivity); - window.removeEventListener('focus', _converse.onUserActivity); - window.removeEventListener('keypress', _converse.onUserActivity); - window.removeEventListener('mousemove', _converse.onUserActivity); - window.removeEventListener(_converse.unloadevent, _converse.onUserActivity); - window.clearInterval(_converse.everySecondTrigger); - - _converse.emit('afterTearDown'); - - return _converse; - }; - - this.initPlugins = function () { - // If initialize gets called a second time (e.g. during tests), then we - // need to re-apply all plugins (for a new converse instance), and we - // therefore need to clear this array that prevents plugins from being - // initialized twice. - // If initialize is called for the first time, then this array is empty - // in any case. - _converse.pluggable.initialized_plugins = []; - - const whitelist = _converse.core_plugins.concat(_converse.whitelisted_plugins); - - if (_converse.view_mode === 'embedded') { - _.forEach([// eslint-disable-line lodash/prefer-map - "converse-bookmarks", "converse-controlbox", "converse-headline", "converse-register"], name => { - _converse.blacklisted_plugins.push(name); - }); - } - - _converse.pluggable.initializePlugins({ - 'updateSettings'() { - _converse.log("(DEPRECATION) " + "The `updateSettings` method has been deprecated. " + "Please use `_converse.api.settings.update` instead.", Strophe.LogLevel.WARN); - - _converse.api.settings.update.apply(_converse, arguments); - }, - - '_converse': _converse - }, whitelist, _converse.blacklisted_plugins); - - _converse.emit('pluginsInitialized'); - }; // Initialization - // -------------- - // This is the end of the initialize method. - - - if (settings.connection) { - this.connection = settings.connection; - } - - function finishInitialization() { - _converse.initPlugins(); - - _converse.initClientConfig(); - - _converse.initConnection(); - - _converse.setUpXMLLogging(); - - _converse.logIn(); - - _converse.registerGlobalEventHandlers(); - - if (!Backbone.history.started) { - Backbone.history.start(); - } - } - - if (!_.isUndefined(_converse.connection) && _converse.connection.service === 'jasmine tests') { - finishInitialization(); - return _converse; - } else if (_.isUndefined(i18n)) { - finishInitialization(); - } else { - i18n.fetchTranslations(_converse.locale, _converse.locales, u.interpolate(_converse.locales_url, { - 'locale': _converse.locale - })).catch(e => _converse.log(e.message, Strophe.LogLevel.FATAL)).then(finishInitialization).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); - } - - return init_promise; - }; - /** - * ### The private API - * - * The private API methods are only accessible via the closured {@link _converse} - * object, which is only available to plugins. - * - * These methods are kept private (i.e. not global) because they may return - * sensitive data which should be kept off-limits to other 3rd-party scripts - * that might be running in the page. - * - * @namespace _converse.api - * @memberOf _converse - */ - - - _converse.api = { - /** - * This grouping collects API functions related to the XMPP connection. - * - * @namespace _converse.api.connection - * @memberOf _converse.api - */ - 'connection': { - /** - * @method _converse.api.connection.connected - * @memberOf _converse.api.connection - * @returns {boolean} Whether there is an established connection or not. - */ - 'connected'() { - return _converse.connection && _converse.connection.connected || false; - }, - - /** - * Terminates the connection. - * - * @method _converse.api.connection.disconnect - * @memberOf _converse.api.connection - */ - 'disconnect'() { - _converse.connection.disconnect(); - } - - }, - - /** - * Lets you emit (i.e. trigger) events, which can be listened to via - * {@link _converse.api.listen.on} or {@link _converse.api.listen.once} - * (see [_converse.api.listen](http://localhost:8000/docs/html/api/-_converse.api.listen.html)). - * - * @method _converse.api.emit - */ - 'emit'() { - _converse.emit.apply(_converse, arguments); - }, - - /** - * This grouping collects API functions related to the current logged in user. - * - * @namespace _converse.api.user - * @memberOf _converse.api - */ - 'user': { - /** - * @method _converse.api.user.jid - * @returns {string} The current user's full JID (Jabber ID) - * @example _converse.api.user.jid()) - */ - 'jid'() { - return _converse.connection.jid; - }, - - /** - * Logs the user in. - * - * If called without any parameters, Converse will try - * to log the user in by calling the `prebind_url` or `credentials_url` depending - * on whether prebinding is used or not. - * - * @method _converse.api.user.login - * @param {object} [credentials] An object with the credentials. - * @example - * converse.plugins.add('myplugin', { - * initialize: function () { - * - * this._converse.api.user.login({ - * 'jid': 'dummy@example.com', - * 'password': 'secret' - * }); - * - * } - * }); - */ - 'login'(credentials) { - _converse.logIn(credentials); - }, - - /** - * Logs the user out of the current XMPP session. - * - * @method _converse.api.user.logout - * @example _converse.api.user.logout(); - */ - 'logout'() { - _converse.logOut(); - }, - - /** - * Set and get the user's chat status, also called their *availability*. - * - * @namespace _converse.api.user.status - * @memberOf _converse.api.user - */ - 'status': { - /** Return the current user's availability status. - * - * @method _converse.api.user.status.get - * @example _converse.api.user.status.get(); - */ - 'get'() { - return _converse.xmppstatus.get('status'); - }, - - /** - * The user's status can be set to one of the following values: - * - * @method _converse.api.user.status.set - * @param {string} value The user's chat status (e.g. 'away', 'dnd', 'offline', 'online', 'unavailable' or 'xa') - * @param {string} [message] A custom status message - * - * @example this._converse.api.user.status.set('dnd'); - * @example this._converse.api.user.status.set('dnd', 'In a meeting'); - */ - 'set'(value, message) { - const data = { - 'status': value - }; - - if (!_.includes(_.keys(_converse.STATUS_WEIGHTS), value)) { - throw new Error('Invalid availability value. See https://xmpp.org/rfcs/rfc3921.html#rfc.section.2.2.2.1'); - } - - if (_.isString(message)) { - data.status_message = message; - } - - _converse.xmppstatus.sendPresence(value); - - _converse.xmppstatus.save(data); - }, - - /** - * Set and retrieve the user's custom status message. - * - * @namespace _converse.api.user.status.message - * @memberOf _converse.api.user.status - */ - 'message': { - /** - * @method _converse.api.user.status.message.get - * @returns {string} The status message - * @example const message = _converse.api.user.status.message.get() - */ - 'get'() { - return _converse.xmppstatus.get('status_message'); - }, - - /** - * @method _converse.api.user.status.message.set - * @param {string} status The status message - * @example _converse.api.user.status.message.set('In a meeting'); - */ - 'set'(status) { - _converse.xmppstatus.save({ - 'status_message': status - }); - } - - } - } - }, - - /** - * This grouping allows access to the - * [configuration settings](/docs/html/configuration.html#configuration-settings) - * of Converse. - * - * @namespace _converse.api.settings - * @memberOf _converse.api - */ - 'settings': { - /** - * Allows new configuration settings to be specified, or new default values for - * existing configuration settings to be specified. - * - * @method _converse.api.settings.update - * @param {object} settings The configuration settings - * @example - * _converse.api.settings.update({ - * 'enable_foo': true - * }); - * - * // The user can then override the default value of the configuration setting when - * // calling `converse.initialize`. - * converse.initialize({ - * 'enable_foo': false - * }); - */ - 'update'(settings) { - u.merge(_converse.default_settings, settings); - u.merge(_converse, settings); - u.applyUserSettings(_converse, settings, _converse.user_settings); - }, - - /** - * @method _converse.api.settings.get - * @returns {*} Value of the particular configuration setting. - * @example _converse.api.settings.get("play_sounds"); - */ - 'get'(key) { - if (_.includes(_.keys(_converse.default_settings), key)) { - return _converse[key]; - } - }, - - /** - * Set one or many configuration settings. - * - * Note, this is not an alternative to calling {@link converse.initialize}, which still needs - * to be called. Generally, you'd use this method after Converse is already - * running and you want to change the configuration on-the-fly. - * - * @method _converse.api.settings.set - * @param {Object} [settings] An object containing configuration settings. - * @param {string} [key] Alternatively to passing in an object, you can pass in a key and a value. - * @param {string} [value] - * @example _converse.api.settings.set("play_sounds", true); - * @example - * _converse.api.settings.set({ - * "play_sounds", true, - * "hide_offline_users" true - * }); - */ - 'set'(key, val) { - const o = {}; - - if (_.isObject(key)) { - _.assignIn(_converse, _.pick(key, _.keys(_converse.default_settings))); - } else if (_.isString("string")) { - o[key] = val; - - _.assignIn(_converse, _.pick(o, _.keys(_converse.default_settings))); - } - } - - }, - - /** - * Converse and its plugins emit various events which you can listen to via the - * {@link _converse.api.listen} namespace. - * - * Some of these events are also available as [ES2015 Promises](http://es6-features.org/#PromiseUsage) - * although not all of them could logically act as promises, since some events - * might be fired multpile times whereas promises are to be resolved (or - * rejected) only once. - * - * Events which are also promises include: - * - * * [cachedRoster](/docs/html/events.html#cachedroster) - * * [chatBoxesFetched](/docs/html/events.html#chatBoxesFetched) - * * [pluginsInitialized](/docs/html/events.html#pluginsInitialized) - * * [roster](/docs/html/events.html#roster) - * * [rosterContactsFetched](/docs/html/events.html#rosterContactsFetched) - * * [rosterGroupsFetched](/docs/html/events.html#rosterGroupsFetched) - * * [rosterInitialized](/docs/html/events.html#rosterInitialized) - * * [statusInitialized](/docs/html/events.html#statusInitialized) - * * [roomsPanelRendered](/docs/html/events.html#roomsPanelRendered) - * - * The various plugins might also provide promises, and they do this by using the - * `promises.add` api method. - * - * @namespace _converse.api.promises - * @memberOf _converse.api - */ - 'promises': { - /** - * By calling `promises.add`, a new [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) - * is made available for other code or plugins to depend on via the - * {@link _converse.api.waitUntil} method. - * - * Generally, it's the responsibility of the plugin which adds the promise to - * also resolve it. - * - * This is done by calling {@link _converse.api.emit}, which not only resolves the - * promise, but also emits an event with the same name (which can be listened to - * via {@link _converse.api.listen}). - * - * @method _converse.api.promises.add - * @param {string|array} [name|names] The name or an array of names for the promise(s) to be added - * @example _converse.api.promises.add('foo-completed'); - */ - 'add'(promises) { - promises = _.isArray(promises) ? promises : [promises]; - - _.each(promises, addPromise); - } - - }, - - /** - * This namespace lets you access the BOSH tokens - * - * @namespace _converse.api.tokens - * @memberOf _converse.api - */ - 'tokens': { - /** - * @method _converse.api.tokens.get - * @param {string} [id] The type of token to return ('rid' or 'sid'). - * @returns 'string' A token, either the RID or SID token depending on what's asked for. - * @example _converse.api.tokens.get('rid'); - */ - 'get'(id) { - if (!_converse.expose_rid_and_sid || _.isUndefined(_converse.connection)) { - return null; - } - - if (id.toLowerCase() === 'rid') { - return _converse.connection.rid || _converse.connection._proto.rid; - } else if (id.toLowerCase() === 'sid') { - return _converse.connection.sid || _converse.connection._proto.sid; - } - } - - }, - - /** - * Converse emits events to which you can subscribe to. - * - * The `listen` namespace exposes methods for creating event listeners - * (aka handlers) for these events. - * - * @namespace _converse.api.listen - * @memberOf _converse - */ - 'listen': { - /** - * Lets you listen to an event exactly once. - * - * @method _converse.api.listen.once - * @param {string} name The event's name - * @param {function} callback The callback method to be called when the event is emitted. - * @param {object} [context] The value of the `this` parameter for the callback. - * @example _converse.api.listen.once('message', function (messageXML) { ... }); - */ - 'once': _converse.once.bind(_converse), - - /** - * Lets you subscribe to an event. - * - * Every time the event fires, the callback method specified by `callback` will be called. - * - * @method _converse.api.listen.on - * @param {string} name The event's name - * @param {function} callback The callback method to be called when the event is emitted. - * @param {object} [context] The value of the `this` parameter for the callback. - * @example _converse.api.listen.on('message', function (messageXML) { ... }); - */ - 'on': _converse.on.bind(_converse), - - /** - * To stop listening to an event, you can use the `not` method. - * - * Every time the event fires, the callback method specified by `callback` will be called. - * - * @method _converse.api.listen.not - * @param {string} name The event's name - * @param {function} callback The callback method that is to no longer be called when the event fires - * @example _converse.api.listen.not('message', function (messageXML); - */ - 'not': _converse.off.bind(_converse), - - /** - * Subscribe to an incoming stanza - * - * Every a matched stanza is received, the callback method specified by `callback` will be called. - * - * @method _converse.api.listen.stanza - * @param {string} name The stanza's name - * @param {object} options Matching options - * (e.g. 'ns' for namespace, 'type' for stanza type, also 'id' and 'from'); - * @param {function} handler The callback method to be called when the stanza appears - */ - 'stanza'(name, options, handler) { - if (_.isFunction(options)) { - handler = options; - options = {}; - } else { - options = options || {}; - } - - _converse.connection.addHandler(handler, options.ns, name, options.type, options.id, options.from, options); - } - - }, - - /** - * Wait until a promise is resolved - * - * @method _converse.api.waitUntil - * @param {string} name The name of the promise - * @returns {Promise} - */ - 'waitUntil'(name) { - const promise = _converse.promises[name]; - - if (_.isUndefined(promise)) { - return null; - } - - return promise; - }, - - /** - * Allows you to send XML stanzas. - * - * @method _converse.api.send - * @example - * const msg = converse.env.$msg({ - * 'from': 'juliet@example.com/balcony', - * 'to': 'romeo@example.net', - * 'type':'chat' - * }); - * _converse.api.send(msg); - */ - 'send'(stanza) { - _converse.connection.send(stanza); - }, - - /** - * Send an IQ stanza and receive a promise - * - * @method _converse.api.sendIQ - * @returns {Promise} A promise which resolves when we receive a `result` stanza - * or is rejected when we receive an `error` stanza. - */ - 'sendIQ'(stanza) { - return new Promise((resolve, reject) => { - _converse.connection.sendIQ(stanza, resolve, reject, _converse.IQ_TIMEOUT); - }); - } - - }; - /** - * ### The Public API - * - * This namespace contains public API methods which are are - * accessible on the global `converse` object. - * They are public, because any JavaScript in the - * page can call them. Public methods therefore don’t expose any sensitive - * or closured data. To do that, you’ll need to create a plugin, which has - * access to the private API method. - * - * @namespace converse - */ - - const converse = { - /** - * Public API method which initializes Converse. - * This method must always be called when using Converse. - * - * @memberOf converse - * @method initialize - * @param {object} config A map of [configuration-settings](https://conversejs.org/docs/html/configuration.html#configuration-settings). - * - * @example - * converse.initialize({ - * allow_otr: true, - * auto_list_rooms: false, - * auto_subscribe: false, - * bosh_service_url: 'https://bind.example.com', - * hide_muc_server: false, - * i18n: locales['en'], - * keepalive: true, - * play_sounds: true, - * prebind: false, - * show_controlbox_by_default: true, - * debug: false, - * roster_groups: true - * }); - */ - 'initialize'(settings, callback) { - return _converse.initialize(settings, callback); - }, - - /** - * Exposes methods for adding and removing plugins. You'll need to write a plugin - * if you want to have access to the private API methods defined further down below. - * - * For more information on plugins, read the documentation on [writing a plugin](/docs/html/plugin_development.html). - * - * @namespace plugins - * @memberOf converse - */ - 'plugins': { - /** Registers a new plugin. - * - * @method converse.plugins.add - * @param {string} name The name of the plugin - * @param {object} plugin The plugin object - * - * @example - * - * const plugin = { - * initialize: function () { - * // Gets called as soon as the plugin has been loaded. - * - * // Inside this method, you have access to the private - * // API via `_covnerse.api`. - * - * // The private _converse object contains the core logic - * // and data-structures of Converse. - * } - * } - * converse.plugins.add('myplugin', plugin); - */ - 'add'(name, plugin) { - plugin.__name__ = name; - - if (!_.isUndefined(_converse.pluggable.plugins[name])) { - throw new TypeError(`Error: plugin with name "${name}" has already been ` + 'registered!'); - } else { - _converse.pluggable.plugins[name] = plugin; - } - } - - }, - - /** - * Utility methods and globals from bundled 3rd party libraries. - * @memberOf converse - * - * @property {function} converse.env.$build - Creates a Strophe.Builder, for creating stanza objects. - * @property {function} converse.env.$iq - Creates a Strophe.Builder with an element as the root. - * @property {function} converse.env.$msg - Creates a Strophe.Builder with an element as the root. - * @property {function} converse.env.$pres - Creates a Strophe.Builder with an element as the root. - * @property {object} converse.env.Backbone - The [Backbone](http://backbonejs.org) object used by Converse to create models and views. - * @property {function} converse.env.Promise - The Promise implementation used by Converse. - * @property {function} converse.env.Strophe - The [Strophe](http://strophe.im/strophejs) XMPP library used by Converse. - * @property {object} converse.env._ - The instance of [lodash](http://lodash.com) used by Converse. - * @property {function} converse.env.f - And instance of Lodash with its methods wrapped to produce immutable auto-curried iteratee-first data-last methods. - * @property {function} converse.env.b64_sha1 - Utility method from Strophe for creating base64 encoded sha1 hashes. - * @property {object} converse.env.moment - [Moment](https://momentjs.com) date manipulation library. - * @property {function} converse.env.sizzle - [Sizzle](https://sizzlejs.com) CSS selector engine. - * @property {object} converse.env.utils - Module containing common utility methods used by Converse. - */ - 'env': { - '$build': $build, - '$iq': $iq, - '$msg': $msg, - '$pres': $pres, - 'Backbone': Backbone, - 'Promise': Promise, - 'Strophe': Strophe, - '_': _, - 'f': f, - 'b64_sha1': b64_sha1, - 'moment': moment, - 'sizzle': sizzle, - 'utils': u - } - }; - window.converse = converse; - window.dispatchEvent(new CustomEvent('converse-loaded')); - return converse; -}); - -/***/ }), - -/***/ "./src/converse-disco.js": -/*!*******************************!*\ - !*** ./src/converse-disco.js ***! - \*******************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;// Converse.js -// http://conversejs.org -// -// Copyright (c) 2013-2018, the Converse developers -// Licensed under the Mozilla Public License (MPLv2) - -/* This is a Converse plugin which add support for XEP-0030: Service Discovery */ -(function (root, factory) { - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! converse-core */ "./src/converse-core.js"), __webpack_require__(/*! sizzle */ "./node_modules/sizzle/dist/sizzle.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), - __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? - (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), - __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); -})(this, function (converse, sizzle) { - const _converse$env = converse.env, - Backbone = _converse$env.Backbone, - Promise = _converse$env.Promise, - Strophe = _converse$env.Strophe, - $iq = _converse$env.$iq, - b64_sha1 = _converse$env.b64_sha1, - utils = _converse$env.utils, - _ = _converse$env._, - f = _converse$env.f; - converse.plugins.add('converse-disco', { - initialize() { - /* The initialize function gets called as soon as the plugin is - * loaded by converse.js's plugin machinery. - */ - const _converse = this._converse; // Promises exposed by this plugin - - _converse.api.promises.add('discoInitialized'); - - _converse.DiscoEntity = Backbone.Model.extend({ - /* A Disco Entity is a JID addressable entity that can be queried - * for features. - * - * See XEP-0030: https://xmpp.org/extensions/xep-0030.html - */ - idAttribute: 'jid', - - initialize() { - this.waitUntilFeaturesDiscovered = utils.getResolveablePromise(); - this.dataforms = new Backbone.Collection(); - this.dataforms.browserStorage = new Backbone.BrowserStorage.session(b64_sha1(`converse.dataforms-{this.get('jid')}`)); - this.features = new Backbone.Collection(); - this.features.browserStorage = new Backbone.BrowserStorage.session(b64_sha1(`converse.features-${this.get('jid')}`)); - this.features.on('add', this.onFeatureAdded, this); - this.fields = new Backbone.Collection(); - this.fields.browserStorage = new Backbone.BrowserStorage.session(b64_sha1(`converse.fields-${this.get('jid')}`)); - this.fields.on('add', this.onFieldAdded, this); - this.identities = new Backbone.Collection(); - this.identities.browserStorage = new Backbone.BrowserStorage.session(b64_sha1(`converse.identities-${this.get('jid')}`)); - this.fetchFeatures(); - this.items = new _converse.DiscoEntities(); - this.items.browserStorage = new Backbone.BrowserStorage.session(b64_sha1(`converse.disco-items-${this.get('jid')}`)); - this.items.fetch(); - }, - - getIdentity(category, type) { - /* Returns a Promise which resolves with a map indicating - * whether a given identity is provided. - * - * Parameters: - * (String) category - The identity category - * (String) type - The identity type - */ - const entity = this; - return new Promise((resolve, reject) => { - function fulfillPromise() { - const model = entity.identities.findWhere({ - 'category': category, - 'type': type - }); - resolve(model); - } - - entity.waitUntilFeaturesDiscovered.then(fulfillPromise).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); - }); - }, - - hasFeature(feature) { - /* Returns a Promise which resolves with a map indicating - * whether a given feature is supported. - * - * Parameters: - * (String) feature - The feature that might be supported. - */ - const entity = this; - return new Promise((resolve, reject) => { - function fulfillPromise() { - if (entity.features.findWhere({ - 'var': feature - })) { - resolve(entity); - } else { - resolve(); - } - } - - entity.waitUntilFeaturesDiscovered.then(fulfillPromise).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); - }); - }, - - onFeatureAdded(feature) { - feature.entity = this; - - _converse.emit('serviceDiscovered', feature); - }, - - onFieldAdded(field) { - field.entity = this; - - _converse.emit('discoExtensionFieldDiscovered', field); - }, - - fetchFeatures() { - if (this.features.browserStorage.records.length === 0) { - this.queryInfo(); - } else { - this.features.fetch({ - add: true, - success: () => { - this.waitUntilFeaturesDiscovered.resolve(this); - this.trigger('featuresDiscovered'); - } - }); - this.identities.fetch({ - add: true - }); - } - }, - - queryInfo() { - _converse.api.disco.info(this.get('jid'), null).then(stanza => this.onInfo(stanza)).catch(iq => { - this.waitUntilFeaturesDiscovered.resolve(this); - - _converse.log(iq, Strophe.LogLevel.ERROR); - }); - }, - - onDiscoItems(stanza) { - _.each(sizzle(`query[xmlns="${Strophe.NS.DISCO_ITEMS}"] item`, stanza), item => { - if (item.getAttribute("node")) { - // XXX: ignore nodes for now. - // See: https://xmpp.org/extensions/xep-0030.html#items-nodes - return; - } - - const jid = item.getAttribute('jid'); - - if (_.isUndefined(this.items.get(jid))) { - const entity = _converse.disco_entities.get(jid); - - if (entity) { - this.items.add(entity); - } else { - this.items.create({ - 'jid': jid - }); - } - } - }); - }, - - queryForItems() { - if (_.isEmpty(this.identities.where({ - 'category': 'server' - }))) { - // Don't fetch features and items if this is not a - // server or a conference component. - return; - } - - _converse.api.disco.items(this.get('jid')).then(stanza => this.onDiscoItems(stanza)); - }, - - onInfo(stanza) { - _.forEach(stanza.querySelectorAll('identity'), identity => { - this.identities.create({ - 'category': identity.getAttribute('category'), - 'type': identity.getAttribute('type'), - 'name': identity.getAttribute('name') - }); - }); - - _.each(sizzle(`x[type="result"][xmlns="${Strophe.NS.XFORM}"]`, stanza), form => { - const data = {}; - - _.each(form.querySelectorAll('field'), field => { - data[field.getAttribute('var')] = { - 'value': _.get(field.querySelector('value'), 'textContent'), - 'type': field.getAttribute('type') - }; - }); - - this.dataforms.create(data); - }); - - if (stanza.querySelector(`feature[var="${Strophe.NS.DISCO_ITEMS}"]`)) { - this.queryForItems(); - } - - _.forEach(stanza.querySelectorAll('feature'), feature => { - this.features.create({ - 'var': feature.getAttribute('var'), - 'from': stanza.getAttribute('from') - }); - }); // XEP-0128 Service Discovery Extensions - - - _.forEach(sizzle('x[type="result"][xmlns="jabber:x:data"] field', stanza), field => { - this.fields.create({ - 'var': field.getAttribute('var'), - 'value': _.get(field.querySelector('value'), 'textContent'), - 'from': stanza.getAttribute('from') - }); - }); - - this.waitUntilFeaturesDiscovered.resolve(this); - this.trigger('featuresDiscovered'); - } - - }); - _converse.DiscoEntities = Backbone.Collection.extend({ - model: _converse.DiscoEntity, - - fetchEntities() { - return new Promise((resolve, reject) => { - this.fetch({ - add: true, - success: resolve, - - error() { - reject(new Error("Could not fetch disco entities")); - } - - }); - }); - } - - }); - - function addClientFeatures() { - // See http://xmpp.org/registrar/disco-categories.html - _converse.api.disco.own.identities.add('client', 'web', 'Converse'); - - _converse.api.disco.own.features.add(Strophe.NS.BOSH); - - _converse.api.disco.own.features.add(Strophe.NS.CHATSTATES); - - _converse.api.disco.own.features.add(Strophe.NS.DISCO_INFO); - - _converse.api.disco.own.features.add(Strophe.NS.ROSTERX); // Limited support - - - if (_converse.message_carbons) { - _converse.api.disco.own.features.add(Strophe.NS.CARBONS); - } - - _converse.emit('addClientFeatures'); - - return this; - } - - function initStreamFeatures() { - _converse.stream_features = new Backbone.Collection(); - _converse.stream_features.browserStorage = new Backbone.BrowserStorage.session(b64_sha1(`converse.stream-features-${_converse.bare_jid}`)); - - _converse.stream_features.fetch({ - success(collection) { - if (collection.length === 0 && _converse.connection.features) { - _.forEach(_converse.connection.features.childNodes, feature => { - _converse.stream_features.create({ - 'name': feature.nodeName, - 'xmlns': feature.getAttribute('xmlns') - }); - }); - } - } - - }); - - _converse.emit('streamFeaturesAdded'); - } - - function initializeDisco() { - addClientFeatures(); - - _converse.connection.addHandler(onDiscoInfoRequest, Strophe.NS.DISCO_INFO, 'iq', 'get', null, null); - - _converse.disco_entities = new _converse.DiscoEntities(); - _converse.disco_entities.browserStorage = new Backbone.BrowserStorage.session(b64_sha1(`converse.disco-entities-${_converse.bare_jid}`)); - - _converse.disco_entities.fetchEntities().then(collection => { - if (collection.length === 0 || !collection.get(_converse.domain)) { - // If we don't have an entity for our own XMPP server, - // create one. - _converse.disco_entities.create({ - 'jid': _converse.domain - }); - } - - _converse.emit('discoInitialized'); - }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); - } - - _converse.api.listen.on('sessionInitialized', initStreamFeatures); - - _converse.api.listen.on('reconnected', initializeDisco); - - _converse.api.listen.on('connected', initializeDisco); - - _converse.api.listen.on('beforeTearDown', () => { - if (_converse.disco_entities) { - _converse.disco_entities.each(entity => { - entity.features.reset(); - - entity.features.browserStorage._clear(); - }); - - _converse.disco_entities.reset(); - - _converse.disco_entities.browserStorage._clear(); - } - }); - - const plugin = this; - plugin._identities = []; - plugin._features = []; - - function onDiscoInfoRequest(stanza) { - const node = stanza.getElementsByTagName('query')[0].getAttribute('node'); - const attrs = { - xmlns: Strophe.NS.DISCO_INFO - }; - - if (node) { - attrs.node = node; - } - - const iqresult = $iq({ - 'type': 'result', - 'id': stanza.getAttribute('id') - }); - const from = stanza.getAttribute('from'); - - if (from !== null) { - iqresult.attrs({ - 'to': from - }); - } - - iqresult.c('query', attrs); - - _.each(plugin._identities, identity => { - const attrs = { - 'category': identity.category, - 'type': identity.type - }; - - if (identity.name) { - attrs.name = identity.name; - } - - if (identity.lang) { - attrs['xml:lang'] = identity.lang; - } - - iqresult.c('identity', attrs).up(); - }); - - _.each(plugin._features, feature => { - iqresult.c('feature', { - 'var': feature - }).up(); - }); - - _converse.connection.send(iqresult.tree()); - - return true; - } - - _.extend(_converse.api, { - /** - * The XEP-0030 service discovery API - * - * This API lets you discover information about entities on the - * XMPP network. - * - * @namespace _converse.api.disco - * @memberOf _converse.api - */ - 'disco': { - /** - * @namespace _converse.api.disco.stream - * @memberOf _converse.api.disco - */ - 'stream': { - /** - * @method _converse.api.disco.stream.getFeature - * @param {String} name The feature name - * @param {String} xmlns The XML namespace - * @example _converse.api.disco.stream.getFeature('ver', 'urn:xmpp:features:rosterver') - */ - 'getFeature': function getFeature(name, xmlns) { - if (_.isNil(name) || _.isNil(xmlns)) { - throw new Error("name and xmlns need to be provided when calling disco.stream.getFeature"); - } - - return _converse.stream_features.findWhere({ - 'name': name, - 'xmlns': xmlns - }); - } - }, - - /** - * @namespace _converse.api.disco.own - * @memberOf _converse.api.disco - */ - 'own': { - /** - * @namespace _converse.api.disco.own.identities - * @memberOf _converse.api.disco.own - */ - 'identities': { - /** - * Lets you add new identities for this client (i.e. instance of Converse) - * @method _converse.api.disco.own.identities.add - * - * @param {String} category - server, client, gateway, directory, etc. - * @param {String} type - phone, pc, web, etc. - * @param {String} name - "Converse" - * @param {String} lang - en, el, de, etc. - * - * @example _converse.api.disco.own.identities.clear(); - */ - add(category, type, name, lang) { - for (var i = 0; i < plugin._identities.length; i++) { - if (plugin._identities[i].category == category && plugin._identities[i].type == type && plugin._identities[i].name == name && plugin._identities[i].lang == lang) { - return false; - } - } - - plugin._identities.push({ - category: category, - type: type, - name: name, - lang: lang - }); - }, - - /** - * Clears all previously registered identities. - * @method _converse.api.disco.own.identities.clear - * @example _converse.api.disco.own.identities.clear(); - */ - clear() { - plugin._identities = []; - }, - - /** - * Returns all of the identities registered for this client - * (i.e. instance of Converse). - * @method _converse.api.disco.identities.get - * @example const identities = _converse.api.disco.own.identities.get(); - */ - get() { - return plugin._identities; - } - - }, - - /** - * @namespace _converse.api.disco.own.features - * @memberOf _converse.api.disco.own - */ - 'features': { - /** - * Lets you register new disco features for this client (i.e. instance of Converse) - * @method _converse.api.disco.own.features.add - * @param {String} name - e.g. http://jabber.org/protocol/caps - * @example _converse.api.disco.own.features.add("http://jabber.org/protocol/caps"); - */ - add(name) { - for (var i = 0; i < plugin._features.length; i++) { - if (plugin._features[i] == name) { - return false; - } - } - - plugin._features.push(name); - }, - - /** - * Clears all previously registered features. - * @method _converse.api.disco.own.features.clear - * @example _converse.api.disco.own.features.clear(); - */ - clear() { - plugin._features = []; - }, - - /** - * Returns all of the features registered for this client (i.e. instance of Converse). - * @method _converse.api.disco.own.features.get - * @example const features = _converse.api.disco.own.features.get(); - */ - get() { - return plugin._features; - } - - } - }, - - /** - * Query for information about an XMPP entity - * - * @method _converse.api.disco.info - * @param {string} jid The Jabber ID of the entity to query - * @param {string} [node] A specific node identifier associated with the JID - * @returns {promise} Promise which resolves once we have a result from the server. - */ - 'info'(jid, node) { - const attrs = { - xmlns: Strophe.NS.DISCO_INFO - }; - - if (node) { - attrs.node = node; - } - - const info = $iq({ - 'from': _converse.connection.jid, - 'to': jid, - 'type': 'get' - }).c('query', attrs); - return _converse.api.sendIQ(info); - }, - - /** - * Query for items associated with an XMPP entity - * - * @method _converse.api.disco.items - * @param {string} jid The Jabber ID of the entity to query for items - * @param {string} [node] A specific node identifier associated with the JID - * @returns {promise} Promise which resolves once we have a result from the server. - */ - 'items'(jid, node) { - const attrs = { - 'xmlns': Strophe.NS.DISCO_ITEMS - }; - - if (node) { - attrs.node = node; - } - - return _converse.api.sendIQ($iq({ - 'from': _converse.connection.jid, - 'to': jid, - 'type': 'get' - }).c('query', attrs)); - }, - - /** - * Namespace for methods associated with disco entities - * - * @namespace _converse.api.disco.entities - * @memberOf _converse.api.disco - */ - 'entities': { - /** - * Get the the corresponding `DiscoEntity` instance. - * - * @method _converse.api.disco.entities.get - * @param {string} jid The Jabber ID of the entity - * @param {boolean} [create] Whether the entity should be created if it doesn't exist. - * @example _converse.api.disco.entities.get(jid); - */ - 'get'(jid, create = false) { - return _converse.api.waitUntil('discoInitialized').then(() => { - if (_.isNil(jid)) { - return _converse.disco_entities; - } - - const entity = _converse.disco_entities.get(jid); - - if (entity || !create) { - return entity; - } - - return _converse.disco_entities.create({ - 'jid': jid - }); - }); - } - - }, - - /** - * Used to determine whether an entity supports a given feature. - * - * @method _converse.api.disco.supports - * @param {string} feature The feature that might be - * supported. In the XML stanza, this is the `var` - * attribute of the `` element. For - * example: `http://jabber.org/protocol/muc` - * @param {string} jid The JID of the entity - * (and its associated items) which should be queried - * @returns {promise} A promise which resolves with a list containing - * _converse.Entity instances representing the entity - * itself or those items associated with the entity if - * they support the given feature. - * - * @example - * _converse.api.disco.supports(Strophe.NS.MAM, _converse.bare_jid) - * .then(value => { - * // `value` is a map with two keys, `supported` and `feature`. - * if (value.supported) { - * // The feature is supported - * } else { - * // The feature is not supported - * } - * }).catch(() => { - * _converse.log( - * "Error or timeout while checking for feature support", - * Strophe.LogLevel.ERROR - * ); - * }); - */ - 'supports'(feature, jid) { - if (_.isNil(jid)) { - throw new TypeError('api.disco.supports: You need to provide an entity JID'); - } - - return _converse.api.waitUntil('discoInitialized').then(() => _converse.api.disco.entities.get(jid, true)).then(entity => entity.waitUntilFeaturesDiscovered).then(entity => { - const promises = _.concat(entity.items.map(item => item.hasFeature(feature)), entity.hasFeature(feature)); - - return Promise.all(promises); - }).then(result => f.filter(f.isObject, result)); - }, - - /** - * Refresh the features (and fields and identities) associated with a - * disco entity by refetching them from the server - * - * @method _converse.api.disco.refreshFeatures - * @param {string} jid The JID of the entity whose features are refreshed. - * @returns {promise} A promise which resolves once the features have been refreshed - * @example - * await _converse.api.disco.refreshFeatures('room@conference.example.org'); - */ - 'refreshFeatures'(jid) { - if (_.isNil(jid)) { - throw new TypeError('api.disco.refreshFeatures: You need to provide an entity JID'); - } - - return _converse.api.waitUntil('discoInitialized').then(() => _converse.api.disco.entities.get(jid, true)).then(entity => { - entity.features.reset(); - entity.fields.reset(); - entity.identities.reset(); - entity.waitUntilFeaturesDiscovered = utils.getResolveablePromise(); - entity.queryInfo(); - return entity.waitUntilFeaturesDiscovered; - }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); - }, - - /** - * Return all the features associated with a disco entity - * - * @method _converse.api.disco.getFeatures - * @param {string} jid The JID of the entity whose features are returned. - * @returns {promise} A promise which resolves with the returned features - * @example - * const features = await _converse.api.disco.getFeatures('room@conference.example.org'); - */ - 'getFeatures'(jid) { - if (_.isNil(jid)) { - throw new TypeError('api.disco.getFeatures: You need to provide an entity JID'); - } - - return _converse.api.waitUntil('discoInitialized').then(() => _converse.api.disco.entities.get(jid, true)).then(entity => entity.waitUntilFeaturesDiscovered).then(entity => entity.features).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); - }, - - /** - * Return all the service discovery extensions fields - * associated with an entity. - * - * See [XEP-0129: Service Discovery Extensions](https://xmpp.org/extensions/xep-0128.html) - * - * @method _converse.api.disco.getFields - * @param {string} jid The JID of the entity whose fields are returned. - * @example - * const fields = await _converse.api.disco.getFields('room@conference.example.org'); - */ - 'getFields'(jid) { - if (_.isNil(jid)) { - throw new TypeError('api.disco.getFields: You need to provide an entity JID'); - } - - return _converse.api.waitUntil('discoInitialized').then(() => _converse.api.disco.entities.get(jid, true)).then(entity => entity.waitUntilFeaturesDiscovered).then(entity => entity.fields).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); - }, - - /** - * Get the identity (with the given category and type) for a given disco entity. - * - * For example, when determining support for PEP (personal eventing protocol), you - * want to know whether the user's own JID has an identity with - * `category='pubsub'` and `type='pep'` as explained in this section of - * XEP-0163: https://xmpp.org/extensions/xep-0163.html#support - * - * @method _converse.api.disco.getIdentity - * @param {string} The identity category. - * In the XML stanza, this is the `category` - * attribute of the `` element. - * For example: 'pubsub' - * @param {string} type The identity type. - * In the XML stanza, this is the `type` - * attribute of the `` element. - * For example: 'pep' - * @param {string} jid The JID of the entity which might have the identity - * @returns {promise} A promise which resolves with a map indicating - * whether an identity with a given type is provided by the entity. - * @example - * _converse.api.disco.getIdentity('pubsub', 'pep', _converse.bare_jid).then( - * function (identity) { - * if (_.isNil(identity)) { - * // The entity DOES NOT have this identity - * } else { - * // The entity DOES have this identity - * } - * } - * ).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); - */ - 'getIdentity'(category, type, jid) { - return _converse.api.disco.entities.get(jid, true).then(e => e.getIdentity(category, type)); - } - - } - }); - } - - }); -}); - -/***/ }), - /***/ "./src/converse-dragresize.js": /*!************************************!*\ !*** ./src/converse-dragresize.js ***! @@ -65740,7 +60738,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /*global define, window, document */ (function (root, factory) { - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! converse-core */ "./src/converse-core.js"), __webpack_require__(/*! templates/dragresize.html */ "./src/templates/dragresize.html"), __webpack_require__(/*! converse-chatview */ "./src/converse-chatview.js"), __webpack_require__(/*! converse-controlbox */ "./src/converse-controlbox.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! @converse/headless/converse-core */ "./src/headless/converse-core.js"), __webpack_require__(/*! templates/dragresize.html */ "./src/templates/dragresize.html"), __webpack_require__(/*! converse-chatview */ "./src/converse-chatview.js"), __webpack_require__(/*! converse-controlbox */ "./src/converse-controlbox.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); @@ -66159,7 +61157,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ // Copyright (c) 2012-2018, the Converse.js developers // Licensed under the Mozilla Public License (MPLv2) (function (root, factory) { - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! converse-core */ "./src/converse-core.js"), __webpack_require__(/*! converse-muc */ "./src/converse-muc.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! @converse/headless/converse-core */ "./src/headless/converse-core.js"), __webpack_require__(/*! @converse/headless/converse-muc */ "./src/headless/converse-muc.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); @@ -66219,7 +61217,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /*global define */ (function (root, factory) { - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! converse-core */ "./src/converse-core.js"), __webpack_require__(/*! templates/inverse_brand_heading.html */ "./src/templates/inverse_brand_heading.html"), __webpack_require__(/*! converse-chatview */ "./src/converse-chatview.js"), __webpack_require__(/*! converse-controlbox */ "./src/converse-controlbox.js"), __webpack_require__(/*! converse-muc */ "./src/converse-muc.js"), __webpack_require__(/*! converse-singleton */ "./src/converse-singleton.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! @converse/headless/converse-core */ "./src/headless/converse-core.js"), __webpack_require__(/*! templates/inverse_brand_heading.html */ "./src/templates/inverse_brand_heading.html"), __webpack_require__(/*! converse-chatview */ "./src/converse-chatview.js"), __webpack_require__(/*! converse-controlbox */ "./src/converse-controlbox.js"), __webpack_require__(/*! @converse/headless/converse-muc */ "./src/headless/converse-muc.js"), __webpack_require__(/*! converse-singleton */ "./src/converse-singleton.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); @@ -66287,7 +61285,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /*global define */ (function (root, factory) { - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! converse-core */ "./src/converse-core.js"), __webpack_require__(/*! templates/chatbox.html */ "./src/templates/chatbox.html"), __webpack_require__(/*! converse-chatview */ "./src/converse-chatview.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! @converse/headless/converse-core */ "./src/headless/converse-core.js"), __webpack_require__(/*! templates/chatbox.html */ "./src/templates/chatbox.html"), __webpack_require__(/*! converse-chatview */ "./src/converse-chatview.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); @@ -66444,680 +61442,6 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /***/ }), -/***/ "./src/converse-mam.js": -/*!*****************************!*\ - !*** ./src/converse-mam.js ***! - \*****************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;// Converse.js (A browser based XMPP chat client) -// http://conversejs.org -// -// Copyright (c) 2012-2017, Jan-Carel Brand -// Licensed under the Mozilla Public License (MPLv2) -// - -/*global define */ -// XEP-0059 Result Set Management -(function (root, factory) { - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! sizzle */ "./node_modules/sizzle/dist/sizzle.js"), __webpack_require__(/*! converse-core */ "./src/converse-core.js"), __webpack_require__(/*! converse-disco */ "./src/converse-disco.js"), __webpack_require__(/*! strophejs-plugin-rsm */ "./node_modules/strophejs-plugin-rsm/lib/strophe.rsm.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), - __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? - (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), - __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); -})(this, function (sizzle, converse) { - "use strict"; - - const CHATROOMS_TYPE = 'chatroom'; - const _converse$env = converse.env, - Promise = _converse$env.Promise, - Strophe = _converse$env.Strophe, - $iq = _converse$env.$iq, - _ = _converse$env._, - moment = _converse$env.moment; - const u = converse.env.utils; - const RSM_ATTRIBUTES = ['max', 'first', 'last', 'after', 'before', 'index', 'count']; // XEP-0313 Message Archive Management - - const MAM_ATTRIBUTES = ['with', 'start', 'end']; - - function getMessageArchiveID(stanza) { - // See https://xmpp.org/extensions/xep-0313.html#results - // - // The result messages MUST contain a element with an 'id' - // attribute that gives the current message's archive UID - const result = sizzle(`result[xmlns="${Strophe.NS.MAM}"]`, stanza).pop(); - - if (!_.isUndefined(result)) { - return result.getAttribute('id'); - } // See: https://xmpp.org/extensions/xep-0313.html#archives_id - - - const stanza_id = sizzle(`stanza-id[xmlns="${Strophe.NS.SID}"]`, stanza).pop(); - - if (!_.isUndefined(stanza_id)) { - return stanza_id.getAttribute('id'); - } - } - - function queryForArchivedMessages(_converse, options, callback, errback) { - /* Internal function, called by the "archive.query" API method. - */ - let date; - - if (_.isFunction(options)) { - callback = options; - errback = callback; - options = null; - } - - const queryid = _converse.connection.getUniqueId(); - - const attrs = { - 'type': 'set' - }; - - if (options && options.groupchat) { - if (!options['with']) { - // eslint-disable-line dot-notation - throw new Error('You need to specify a "with" value containing ' + 'the chat room JID, when querying groupchat messages.'); - } - - attrs.to = options['with']; // eslint-disable-line dot-notation - } - - const stanza = $iq(attrs).c('query', { - 'xmlns': Strophe.NS.MAM, - 'queryid': queryid - }); - - if (options) { - stanza.c('x', { - 'xmlns': Strophe.NS.XFORM, - 'type': 'submit' - }).c('field', { - 'var': 'FORM_TYPE', - 'type': 'hidden' - }).c('value').t(Strophe.NS.MAM).up().up(); - - if (options['with'] && !options.groupchat) { - // eslint-disable-line dot-notation - stanza.c('field', { - 'var': 'with' - }).c('value').t(options['with']).up().up(); // eslint-disable-line dot-notation - } - - _.each(['start', 'end'], function (t) { - if (options[t]) { - date = moment(options[t]); - - if (date.isValid()) { - stanza.c('field', { - 'var': t - }).c('value').t(date.format()).up().up(); - } else { - throw new TypeError(`archive.query: invalid date provided for: ${t}`); - } - } - }); - - stanza.up(); - - if (options instanceof Strophe.RSM) { - stanza.cnode(options.toXML()); - } else if (_.intersection(RSM_ATTRIBUTES, _.keys(options)).length) { - stanza.cnode(new Strophe.RSM(options).toXML()); - } - } - - const messages = []; - - const message_handler = _converse.connection.addHandler(message => { - if (options.groupchat && message.getAttribute('from') !== options['with']) { - // eslint-disable-line dot-notation - return true; - } - - const result = message.querySelector('result'); - - if (!_.isNull(result) && result.getAttribute('queryid') === queryid) { - messages.push(message); - } - - return true; - }, Strophe.NS.MAM); - - _converse.connection.sendIQ(stanza, function (iq) { - _converse.connection.deleteHandler(message_handler); - - if (_.isFunction(callback)) { - const set = iq.querySelector('set'); - let rsm; - - if (!_.isUndefined(set)) { - rsm = new Strophe.RSM({ - xml: set - }); - - _.extend(rsm, _.pick(options, _.concat(MAM_ATTRIBUTES, ['max']))); - } - - callback(messages, rsm); - } - }, function () { - _converse.connection.deleteHandler(message_handler); - - if (_.isFunction(errback)) { - errback.apply(this, arguments); - } - }, _converse.message_archiving_timeout); - } - - converse.plugins.add('converse-mam', { - dependencies: ['converse-chatview', 'converse-muc', 'converse-muc-views'], - overrides: { - // Overrides mentioned here will be picked up by converse.js's - // plugin architecture they will replace existing methods on the - // relevant objects or classes. - // - // New functions which don't exist yet can also be added. - ChatBox: { - getMessageAttributesFromStanza(message, original_stanza) { - function _process(attrs) { - const archive_id = getMessageArchiveID(original_stanza); - - if (archive_id) { - attrs.archive_id = archive_id; - } - - return attrs; - } - - const result = this.__super__.getMessageAttributesFromStanza.apply(this, arguments); - - if (result instanceof Promise) { - return new Promise((resolve, reject) => result.then(attrs => resolve(_process(attrs))).catch(reject)); - } else { - return _process(result); - } - } - - }, - ChatBoxView: { - render() { - const result = this.__super__.render.apply(this, arguments); - - if (!this.disable_mam) { - this.content.addEventListener('scroll', _.debounce(this.onScroll.bind(this), 100)); - } - - return result; - }, - - fetchNewestMessages() { - /* Fetches messages that might have been archived *after* - * the last archived message in our local cache. - */ - if (this.disable_mam) { - return; - } - - const _converse = this.__super__._converse, - most_recent_msg = u.getMostRecentMessage(this.model); - - if (_.isNil(most_recent_msg)) { - this.fetchArchivedMessages(); - } else { - const archive_id = most_recent_msg.get('archive_id'); - - if (archive_id) { - this.fetchArchivedMessages({ - 'after': most_recent_msg.get('archive_id') - }); - } else { - this.fetchArchivedMessages({ - 'start': most_recent_msg.get('time') - }); - } - } - }, - - fetchArchivedMessagesIfNecessary() { - /* Check if archived messages should be fetched, and if so, do so. */ - if (this.disable_mam || this.model.get('mam_initialized')) { - return; - } - - const _converse = this.__super__._converse; - - _converse.api.disco.supports(Strophe.NS.MAM, _converse.bare_jid).then(result => { - // Success - if (result.length) { - this.fetchArchivedMessages(); - } - - this.model.save({ - 'mam_initialized': true - }); - }, () => { - // Error - _converse.log("Error or timeout while checking for MAM support", Strophe.LogLevel.ERROR); - }).catch(msg => { - this.clearSpinner(); - - _converse.log(msg, Strophe.LogLevel.FATAL); - }); - }, - - fetchArchivedMessages(options) { - const _converse = this.__super__._converse; - - if (this.disable_mam) { - return; - } - - const is_groupchat = this.model.get('type') === CHATROOMS_TYPE; - let mam_jid, message_handler; - - if (is_groupchat) { - mam_jid = this.model.get('jid'); - message_handler = this.model.onMessage.bind(this.model); - } else { - mam_jid = _converse.bare_jid; - message_handler = _converse.chatboxes.onMessage.bind(_converse.chatboxes); - } - - _converse.api.disco.supports(Strophe.NS.MAM, mam_jid).then(results => { - // Success - if (!results.length) { - return; - } - - this.addSpinner(); - - _converse.api.archive.query(_.extend({ - 'groupchat': is_groupchat, - 'before': '', - // Page backwards from the most recent message - 'max': _converse.archived_messages_page_size, - 'with': this.model.get('jid') - }, options), messages => { - // Success - this.clearSpinner(); - - _.each(messages, message_handler); - }, () => { - // Error - this.clearSpinner(); - - _converse.log("Error or timeout while trying to fetch " + "archived messages", Strophe.LogLevel.ERROR); - }); - }, () => { - // Error - _converse.log("Error or timeout while checking for MAM support", Strophe.LogLevel.ERROR); - }).catch(msg => { - this.clearSpinner(); - - _converse.log(msg, Strophe.LogLevel.FATAL); - }); - }, - - onScroll(ev) { - const _converse = this.__super__._converse; - - if (this.content.scrollTop === 0 && this.model.messages.length) { - const oldest_message = this.model.messages.at(0); - const archive_id = oldest_message.get('archive_id'); - - if (archive_id) { - this.fetchArchivedMessages({ - 'before': archive_id - }); - } else { - this.fetchArchivedMessages({ - 'end': oldest_message.get('time') - }); - } - } - } - - }, - ChatRoom: { - isDuplicate(message, original_stanza) { - const result = this.__super__.isDuplicate.apply(this, arguments); - - if (result) { - return result; - } - - const archive_id = getMessageArchiveID(original_stanza); - - if (archive_id) { - return this.messages.filter({ - 'archive_id': archive_id - }).length > 0; - } - } - - }, - ChatRoomView: { - initialize() { - const _converse = this.__super__._converse; - - this.__super__.initialize.apply(this, arguments); - - this.model.on('change:mam_enabled', this.fetchArchivedMessagesIfNecessary, this); - this.model.on('change:connection_status', this.fetchArchivedMessagesIfNecessary, this); - }, - - renderChatArea() { - const result = this.__super__.renderChatArea.apply(this, arguments); - - if (!this.disable_mam) { - this.content.addEventListener('scroll', _.debounce(this.onScroll.bind(this), 100)); - } - - return result; - }, - - fetchArchivedMessagesIfNecessary() { - if (this.model.get('connection_status') !== converse.ROOMSTATUS.ENTERED || !this.model.get('mam_enabled') || this.model.get('mam_initialized')) { - return; - } - - this.fetchArchivedMessages(); - this.model.save({ - 'mam_initialized': true - }); - } - - } - }, - - initialize() { - /* The initialize function gets called as soon as the plugin is - * loaded by Converse.js's plugin machinery. - */ - const _converse = this._converse; - - _converse.api.settings.update({ - archived_messages_page_size: '50', - message_archiving: undefined, - // Supported values are 'always', 'never', 'roster' (https://xmpp.org/extensions/xep-0313.html#prefs) - message_archiving_timeout: 8000 // Time (in milliseconds) to wait before aborting MAM request - - }); - - _converse.onMAMError = function (model, iq) { - if (iq.querySelectorAll('feature-not-implemented').length) { - _converse.log("Message Archive Management (XEP-0313) not supported by this server", Strophe.LogLevel.WARN); - } else { - _converse.log("An error occured while trying to set archiving preferences.", Strophe.LogLevel.ERROR); - - _converse.log(iq); - } - }; - - _converse.onMAMPreferences = function (feature, iq) { - /* Handle returned IQ stanza containing Message Archive - * Management (XEP-0313) preferences. - * - * XXX: For now we only handle the global default preference. - * The XEP also provides for per-JID preferences, which is - * currently not supported in converse.js. - * - * Per JID preferences will be set in chat boxes, so it'll - * probbaly be handled elsewhere in any case. - */ - const preference = sizzle(`prefs[xmlns="${Strophe.NS.MAM}"]`, iq).pop(); - const default_pref = preference.getAttribute('default'); - - if (default_pref !== _converse.message_archiving) { - const stanza = $iq({ - 'type': 'set' - }).c('prefs', { - 'xmlns': Strophe.NS.MAM, - 'default': _converse.message_archiving - }); - - _.each(preference.children, function (child) { - stanza.cnode(child).up(); - }); - - _converse.connection.sendIQ(stanza, _.partial(function (feature, iq) { - // XXX: Strictly speaking, the server should respond with the updated prefs - // (see example 18: https://xmpp.org/extensions/xep-0313.html#config) - // but Prosody doesn't do this, so we don't rely on it. - feature.save({ - 'preferences': { - 'default': _converse.message_archiving - } - }); - }, feature), _converse.onMAMError); - } else { - feature.save({ - 'preferences': { - 'default': _converse.message_archiving - } - }); - } - }; - /* Event handlers */ - - - _converse.on('serviceDiscovered', feature => { - const prefs = feature.get('preferences') || {}; - - if (feature.get('var') === Strophe.NS.MAM && prefs['default'] !== _converse.message_archiving && // eslint-disable-line dot-notation - !_.isUndefined(_converse.message_archiving)) { - // Ask the server for archiving preferences - _converse.connection.sendIQ($iq({ - 'type': 'get' - }).c('prefs', { - 'xmlns': Strophe.NS.MAM - }), _.partial(_converse.onMAMPreferences, feature), _.partial(_converse.onMAMError, feature)); - } - }); - - _converse.on('addClientFeatures', () => { - _converse.api.disco.own.features.add(Strophe.NS.MAM); - }); - - _converse.on('afterMessagesFetched', chatboxview => { - chatboxview.fetchNewestMessages(); - }); - - _converse.on('reconnected', () => { - const private_chats = _converse.chatboxviews.filter(view => _.at(view, 'model.attributes.type')[0] === 'chatbox'); - - _.each(private_chats, view => view.fetchNewestMessages()); - }); - - _.extend(_converse.api, { - /** - * The [XEP-0313](https://xmpp.org/extensions/xep-0313.html) Message Archive Management API - * - * Enables you to query an XMPP server for archived messages. - * - * See also the [message-archiving](/docs/html/configuration.html#message-archiving) - * option in the configuration settings section, which you'll - * usually want to use in conjunction with this API. - * - * @namespace _converse.api.archive - * @memberOf _converse.api - */ - 'archive': { - /** - * Query for archived messages. - * - * The options parameter can also be an instance of - * Strophe.RSM to enable easy querying between results pages. - * - * @method _converse.api.archive.query - * @param {(Object|Strophe.RSM)} options Query parameters, either - * MAM-specific or also for Result Set Management. - * Can be either an object or an instance of Strophe.RSM. - * Valid query parameters are: - * * `with` - * * `start` - * * `end` - * * `first` - * * `last` - * * `after` - * * `before` - * * `index` - * * `count` - * @param {Function} callback A function to call whenever - * we receive query-relevant stanza. - * When the callback is called, a Strophe.RSM object is - * returned on which "next" or "previous" can be called - * before passing it in again to this method, to - * get the next or previous page in the result set. - * @param {Function} errback A function to call when an - * error stanza is received, for example when it - * doesn't support message archiving. - * - * @example - * // Requesting all archived messages - * // ================================ - * // - * // The simplest query that can be made is to simply not pass in any parameters. - * // Such a query will return all archived messages for the current user. - * // - * // Generally, you'll however always want to pass in a callback method, to receive - * // the returned messages. - * - * this._converse.api.archive.query( - * (messages) => { - * // Do something with the messages, like showing them in your webpage. - * }, - * (iq) => { - * // The query was not successful, perhaps inform the user? - * // The IQ stanza returned by the XMPP server is passed in, so that you - * // may inspect it and determine what the problem was. - * } - * ) - * @example - * // Waiting until server support has been determined - * // ================================================ - * // - * // The query method will only work if Converse has been able to determine that - * // the server supports MAM queries, otherwise the following error will be raised: - * // - * // "This server does not support XEP-0313, Message Archive Management" - * // - * // The very first time Converse loads in a browser tab, if you call the query - * // API too quickly, the above error might appear because service discovery has not - * // yet been completed. - * // - * // To work solve this problem, you can first listen for the `serviceDiscovered` event, - * // through which you can be informed once support for MAM has been determined. - * - * _converse.api.listen.on('serviceDiscovered', function (feature) { - * if (feature.get('var') === converse.env.Strophe.NS.MAM) { - * _converse.api.archive.query() - * } - * }); - * - * @example - * // Requesting all archived messages for a particular contact or room - * // ================================================================= - * // - * // To query for messages sent between the current user and another user or room, - * // the query options need to contain the the JID (Jabber ID) of the user or - * // room under the `with` key. - * - * // For a particular user - * this._converse.api.archive.query({'with': 'john@doe.net'}, callback, errback);) - * - * // For a particular room - * this._converse.api.archive.query({'with': 'discuss@conference.doglovers.net'}, callback, errback);) - * - * @example - * // Requesting all archived messages before or after a certain date - * // =============================================================== - * // - * // The `start` and `end` parameters are used to query for messages - * // within a certain timeframe. The passed in date values may either be ISO8601 - * // formatted date strings, or JavaScript Date objects. - * - * const options = { - * 'with': 'john@doe.net', - * 'start': '2010-06-07T00:00:00Z', - * 'end': '2010-07-07T13:23:54Z' - * }; - * this._converse.api.archive.query(options, callback, errback); - * - * @example - * // Limiting the amount of messages returned - * // ======================================== - * // - * // The amount of returned messages may be limited with the `max` parameter. - * // By default, the messages are returned from oldest to newest. - * - * // Return maximum 10 archived messages - * this._converse.api.archive.query({'with': 'john@doe.net', 'max':10}, callback, errback); - * - * @example - * // Paging forwards through a set of archived messages - * // ================================================== - * // - * // When limiting the amount of messages returned per query, you might want to - * // repeatedly make a further query to fetch the next batch of messages. - * // - * // To simplify this usecase for you, the callback method receives not only an array - * // with the returned archived messages, but also a special RSM (*Result Set - * // Management*) object which contains the query parameters you passed in, as well - * // as two utility methods `next`, and `previous`. - * // - * // When you call one of these utility methods on the returned RSM object, and then - * // pass the result into a new query, you'll receive the next or previous batch of - * // archived messages. Please note, when calling these methods, pass in an integer - * // to limit your results. - * - * const callback = function (messages, rsm) { - * // Do something with the messages, like showing them in your webpage. - * // ... - * // You can now use the returned "rsm" object, to fetch the next batch of messages: - * _converse.api.archive.query(rsm.next(10), callback, errback)) - * - * } - * _converse.api.archive.query({'with': 'john@doe.net', 'max':10}, callback, errback); - * - * @example - * // Paging backwards through a set of archived messages - * // =================================================== - * // - * // To page backwards through the archive, you need to know the UID of the message - * // which you'd like to page backwards from and then pass that as value for the - * // `before` parameter. If you simply want to page backwards from the most recent - * // message, pass in the `before` parameter with an empty string value `''`. - * - * _converse.api.archive.query({'before': '', 'max':5}, function (message, rsm) { - * // Do something with the messages, like showing them in your webpage. - * // ... - * // You can now use the returned "rsm" object, to fetch the previous batch of messages: - * rsm.previous(5); // Call previous method, to update the object's parameters, - * // passing in a limit value of 5. - * // Now we query again, to get the previous batch. - * _converse.api.archive.query(rsm, callback, errback); - * } - */ - 'query': function query(options, callback, errback) { - if (!_converse.api.connection.connected()) { - throw new Error('Can\'t call `api.archive.query` before having established an XMPP session'); - } - - return queryForArchivedMessages(_converse, options, callback, errback); - } - } - }); - } - - }); -}); - -/***/ }), - /***/ "./src/converse-message-view.js": /*!**************************************!*\ !*** ./src/converse-message-view.js ***! @@ -67131,11 +61455,11 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ // Copyright (c) 2013-2018, the Converse.js developers // Licensed under the Mozilla Public License (MPLv2) (function (root, factory) { - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! utils/emoji */ "./src/utils/emoji.js"), __webpack_require__(/*! converse-core */ "./src/converse-core.js"), __webpack_require__(/*! xss */ "./node_modules/xss/dist/xss.js"), __webpack_require__(/*! filesize */ "./node_modules/filesize/lib/filesize.js"), __webpack_require__(/*! templates/csn.html */ "./src/templates/csn.html"), __webpack_require__(/*! templates/file_progress.html */ "./src/templates/file_progress.html"), __webpack_require__(/*! templates/info.html */ "./src/templates/info.html"), __webpack_require__(/*! templates/message.html */ "./src/templates/message.html"), __webpack_require__(/*! templates/message_versions_modal.html */ "./src/templates/message_versions_modal.html")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! ./utils/html */ "./src/utils/html.js"), __webpack_require__(/*! utils/emoji */ "./src/headless/utils/emoji.js"), __webpack_require__(/*! @converse/headless/converse-core */ "./src/headless/converse-core.js"), __webpack_require__(/*! xss */ "./node_modules/xss/dist/xss.js"), __webpack_require__(/*! filesize */ "./node_modules/filesize/lib/filesize.js"), __webpack_require__(/*! templates/csn.html */ "./src/templates/csn.html"), __webpack_require__(/*! templates/file_progress.html */ "./src/templates/file_progress.html"), __webpack_require__(/*! templates/info.html */ "./src/templates/info.html"), __webpack_require__(/*! templates/message.html */ "./src/templates/message.html"), __webpack_require__(/*! templates/message_versions_modal.html */ "./src/templates/message_versions_modal.html")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); -})(this, function (u, converse, xss, filesize, tpl_csn, tpl_file_progress, tpl_info, tpl_message, tpl_message_versions_modal) { +})(this, function (html, u, converse, xss, filesize, tpl_csn, tpl_file_progress, tpl_info, tpl_message, tpl_message_versions_modal) { "use strict"; const _converse$env = converse.env, @@ -67407,7 +61731,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /*global define, window, document */ (function (root, factory) { - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! converse-core */ "./src/converse-core.js"), __webpack_require__(/*! templates/chatbox_minimize.html */ "./src/templates/chatbox_minimize.html"), __webpack_require__(/*! templates/toggle_chats.html */ "./src/templates/toggle_chats.html"), __webpack_require__(/*! templates/trimmed_chat.html */ "./src/templates/trimmed_chat.html"), __webpack_require__(/*! templates/chats_panel.html */ "./src/templates/chats_panel.html"), __webpack_require__(/*! converse-chatview */ "./src/converse-chatview.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! @converse/headless/converse-core */ "./src/headless/converse-core.js"), __webpack_require__(/*! templates/chatbox_minimize.html */ "./src/templates/chatbox_minimize.html"), __webpack_require__(/*! templates/toggle_chats.html */ "./src/templates/toggle_chats.html"), __webpack_require__(/*! templates/trimmed_chat.html */ "./src/templates/trimmed_chat.html"), __webpack_require__(/*! templates/chats_panel.html */ "./src/templates/chats_panel.html"), __webpack_require__(/*! converse-chatview */ "./src/converse-chatview.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); @@ -67435,7 +61759,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ * * NB: These plugins need to have already been loaded via require.js. */ - dependencies: ["converse-chatview", "converse-controlbox", "converse-muc", "converse-muc-views", "converse-headline"], + dependencies: ["converse-chatview", "converse-controlbox", "@converse/headless/converse-muc", "converse-muc-views", "converse-headline"], enabled(_converse) { return _converse.view_mode == 'overlayed'; @@ -68020,7 +62344,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ // Licensed under the Mozilla Public License (MPLv2) (function (root, factory) { if (true) { - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! converse-core */ "./src/converse-core.js"), __webpack_require__(/*! templates/alert_modal.html */ "./src/templates/alert_modal.html"), __webpack_require__(/*! bootstrap */ "./node_modules/bootstrap.native/dist/bootstrap-native-v4.js"), __webpack_require__(/*! backbone.vdomview */ "./node_modules/backbone.vdomview/backbone.vdomview.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! @converse/headless/converse-core */ "./src/headless/converse-core.js"), __webpack_require__(/*! templates/alert_modal.html */ "./src/templates/alert_modal.html"), __webpack_require__(/*! bootstrap */ "./node_modules/bootstrap.native/dist/bootstrap-native-v4.js"), __webpack_require__(/*! backbone.vdomview */ "./node_modules/backbone.vdomview/backbone.vdomview.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); @@ -68153,7 +62477,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ // Copyright (c) 2013-2018, the Converse.js developers // Licensed under the Mozilla Public License (MPLv2) (function (root, factory) { - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! converse-core */ "./src/converse-core.js"), __webpack_require__(/*! formdata-polyfill */ "./node_modules/formdata-polyfill/FormData.js"), __webpack_require__(/*! utils/muc */ "./src/utils/muc.js"), __webpack_require__(/*! xss */ "./node_modules/xss/dist/xss.js"), __webpack_require__(/*! templates/add_chatroom_modal.html */ "./src/templates/add_chatroom_modal.html"), __webpack_require__(/*! templates/chatarea.html */ "./src/templates/chatarea.html"), __webpack_require__(/*! templates/chatroom.html */ "./src/templates/chatroom.html"), __webpack_require__(/*! templates/chatroom_details_modal.html */ "./src/templates/chatroom_details_modal.html"), __webpack_require__(/*! templates/chatroom_destroyed.html */ "./src/templates/chatroom_destroyed.html"), __webpack_require__(/*! templates/chatroom_disconnect.html */ "./src/templates/chatroom_disconnect.html"), __webpack_require__(/*! templates/chatroom_features.html */ "./src/templates/chatroom_features.html"), __webpack_require__(/*! templates/chatroom_form.html */ "./src/templates/chatroom_form.html"), __webpack_require__(/*! templates/chatroom_head.html */ "./src/templates/chatroom_head.html"), __webpack_require__(/*! templates/chatroom_invite.html */ "./src/templates/chatroom_invite.html"), __webpack_require__(/*! templates/chatroom_nickname_form.html */ "./src/templates/chatroom_nickname_form.html"), __webpack_require__(/*! templates/chatroom_password_form.html */ "./src/templates/chatroom_password_form.html"), __webpack_require__(/*! templates/chatroom_sidebar.html */ "./src/templates/chatroom_sidebar.html"), __webpack_require__(/*! templates/info.html */ "./src/templates/info.html"), __webpack_require__(/*! templates/list_chatrooms_modal.html */ "./src/templates/list_chatrooms_modal.html"), __webpack_require__(/*! templates/occupant.html */ "./src/templates/occupant.html"), __webpack_require__(/*! templates/room_description.html */ "./src/templates/room_description.html"), __webpack_require__(/*! templates/room_item.html */ "./src/templates/room_item.html"), __webpack_require__(/*! templates/room_panel.html */ "./src/templates/room_panel.html"), __webpack_require__(/*! templates/rooms_results.html */ "./src/templates/rooms_results.html"), __webpack_require__(/*! templates/spinner.html */ "./src/templates/spinner.html"), __webpack_require__(/*! awesomplete */ "./node_modules/awesomplete-avoid-xss/awesomplete.js"), __webpack_require__(/*! converse-modal */ "./src/converse-modal.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! @converse/headless/converse-core */ "./src/headless/converse-core.js"), __webpack_require__(/*! formdata-polyfill */ "./node_modules/formdata-polyfill/FormData.js"), __webpack_require__(/*! utils/muc */ "./src/headless/utils/muc.js"), __webpack_require__(/*! xss */ "./node_modules/xss/dist/xss.js"), __webpack_require__(/*! templates/add_chatroom_modal.html */ "./src/templates/add_chatroom_modal.html"), __webpack_require__(/*! templates/chatarea.html */ "./src/templates/chatarea.html"), __webpack_require__(/*! templates/chatroom.html */ "./src/templates/chatroom.html"), __webpack_require__(/*! templates/chatroom_details_modal.html */ "./src/templates/chatroom_details_modal.html"), __webpack_require__(/*! templates/chatroom_destroyed.html */ "./src/templates/chatroom_destroyed.html"), __webpack_require__(/*! templates/chatroom_disconnect.html */ "./src/templates/chatroom_disconnect.html"), __webpack_require__(/*! templates/chatroom_features.html */ "./src/templates/chatroom_features.html"), __webpack_require__(/*! templates/chatroom_form.html */ "./src/templates/chatroom_form.html"), __webpack_require__(/*! templates/chatroom_head.html */ "./src/templates/chatroom_head.html"), __webpack_require__(/*! templates/chatroom_invite.html */ "./src/templates/chatroom_invite.html"), __webpack_require__(/*! templates/chatroom_nickname_form.html */ "./src/templates/chatroom_nickname_form.html"), __webpack_require__(/*! templates/chatroom_password_form.html */ "./src/templates/chatroom_password_form.html"), __webpack_require__(/*! templates/chatroom_sidebar.html */ "./src/templates/chatroom_sidebar.html"), __webpack_require__(/*! templates/info.html */ "./src/templates/info.html"), __webpack_require__(/*! templates/list_chatrooms_modal.html */ "./src/templates/list_chatrooms_modal.html"), __webpack_require__(/*! templates/occupant.html */ "./src/templates/occupant.html"), __webpack_require__(/*! templates/room_description.html */ "./src/templates/room_description.html"), __webpack_require__(/*! templates/room_item.html */ "./src/templates/room_item.html"), __webpack_require__(/*! templates/room_panel.html */ "./src/templates/room_panel.html"), __webpack_require__(/*! templates/rooms_results.html */ "./src/templates/rooms_results.html"), __webpack_require__(/*! templates/spinner.html */ "./src/templates/spinner.html"), __webpack_require__(/*! awesomplete */ "./node_modules/awesomplete-avoid-xss/awesomplete.js"), __webpack_require__(/*! converse-modal */ "./src/converse-modal.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); @@ -70385,10 +64709,10860 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /***/ }), -/***/ "./src/converse-muc.js": -/*!*****************************!*\ - !*** ./src/converse-muc.js ***! - \*****************************/ +/***/ "./src/converse-notification.js": +/*!**************************************!*\ + !*** ./src/converse-notification.js ***! + \**************************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;// Converse.js (A browser based XMPP chat client) +// http://conversejs.org +// +// Copyright (c) 2013-2018, JC Brand +// Licensed under the Mozilla Public License (MPLv2) +// + +/*global define */ +(function (root, factory) { + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! @converse/headless/converse-core */ "./src/headless/converse-core.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), + __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? + (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), + __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); +})(this, function (converse) { + "use strict"; + + const _converse$env = converse.env, + Strophe = _converse$env.Strophe, + _ = _converse$env._, + sizzle = _converse$env.sizzle, + u = converse.env.utils; + converse.plugins.add('converse-notification', { + dependencies: ["converse-chatboxes"], + + initialize() { + /* The initialize function gets called as soon as the plugin is + * loaded by converse.js's plugin machinery. + */ + const _converse = this._converse; + const __ = _converse.__; + _converse.supports_html5_notification = "Notification" in window; + + _converse.api.settings.update({ + notify_all_room_messages: false, + show_desktop_notifications: true, + show_chatstate_notifications: false, + chatstate_notification_blacklist: [], + // ^ a list of JIDs to ignore concerning chat state notifications + play_sounds: true, + sounds_path: '/sounds/', + notification_icon: '/logo/conversejs-filled.svg' + }); + + _converse.isOnlyChatStateNotification = msg => // See XEP-0085 Chat State Notification + _.isNull(msg.querySelector('body')) && (_.isNull(msg.querySelector(_converse.ACTIVE)) || _.isNull(msg.querySelector(_converse.COMPOSING)) || _.isNull(msg.querySelector(_converse.INACTIVE)) || _.isNull(msg.querySelector(_converse.PAUSED)) || _.isNull(msg.querySelector(_converse.GONE))); + + _converse.shouldNotifyOfGroupMessage = function (message) { + /* Is this a group message worthy of notification? + */ + let notify_all = _converse.notify_all_room_messages; + const jid = message.getAttribute('from'), + resource = Strophe.getResourceFromJid(jid), + room_jid = Strophe.getBareJidFromJid(jid), + sender = resource && Strophe.unescapeNode(resource) || ''; + + if (sender === '' || message.querySelectorAll('delay').length > 0) { + return false; + } + + const room = _converse.chatboxes.get(room_jid); + + const body = message.querySelector('body'); + + if (_.isNull(body)) { + return false; + } + + const mentioned = new RegExp(`\\b${room.get('nick')}\\b`).test(body.textContent); + notify_all = notify_all === true || _.isArray(notify_all) && _.includes(notify_all, room_jid); + + if (sender === room.get('nick') || !notify_all && !mentioned) { + return false; + } + + return true; + }; + + _converse.isMessageToHiddenChat = function (message) { + if (_.includes(['mobile', 'fullscreen', 'embedded'], _converse.view_mode)) { + const jid = Strophe.getBareJidFromJid(message.getAttribute('from')), + view = _converse.chatboxviews.get(jid); + + if (!_.isNil(view)) { + return view.model.get('hidden') || _converse.windowState === 'hidden' || !u.isVisible(view.el); + } + + return true; + } + + return _converse.windowState === 'hidden'; + }; + + _converse.shouldNotifyOfMessage = function (message) { + const forwarded = message.querySelector('forwarded'); + + if (!_.isNull(forwarded)) { + return false; + } else if (message.getAttribute('type') === 'groupchat') { + return _converse.shouldNotifyOfGroupMessage(message); + } else if (u.isHeadlineMessage(_converse, message)) { + // We want to show notifications for headline messages. + return _converse.isMessageToHiddenChat(message); + } + + const is_me = Strophe.getBareJidFromJid(message.getAttribute('from')) === _converse.bare_jid; + + return !_converse.isOnlyChatStateNotification(message) && !is_me && _converse.isMessageToHiddenChat(message); + }; + + _converse.playSoundNotification = function () { + /* Plays a sound to notify that a new message was recieved. + */ + // XXX Eventually this can be refactored to use Notification's sound + // feature, but no browser currently supports it. + // https://developer.mozilla.org/en-US/docs/Web/API/notification/sound + let audio; + + if (_converse.play_sounds && !_.isUndefined(window.Audio)) { + audio = new Audio(_converse.sounds_path + "msg_received.ogg"); + + if (audio.canPlayType('audio/ogg')) { + audio.play(); + } else { + audio = new Audio(_converse.sounds_path + "msg_received.mp3"); + + if (audio.canPlayType('audio/mp3')) { + audio.play(); + } + } + } + }; + + _converse.areDesktopNotificationsEnabled = function () { + return _converse.supports_html5_notification && _converse.show_desktop_notifications && Notification.permission === "granted"; + }; + + _converse.showMessageNotification = function (message) { + /* Shows an HTML5 Notification to indicate that a new chat + * message was received. + */ + let title, roster_item; + const full_from_jid = message.getAttribute('from'), + from_jid = Strophe.getBareJidFromJid(full_from_jid); + + if (message.getAttribute('type') === 'headline') { + if (!_.includes(from_jid, '@') || _converse.allow_non_roster_messaging) { + title = __("Notification from %1$s", from_jid); + } else { + return; + } + } else if (!_.includes(from_jid, '@')) { + // workaround for Prosody which doesn't give type "headline" + title = __("Notification from %1$s", from_jid); + } else if (message.getAttribute('type') === 'groupchat') { + title = __("%1$s says", Strophe.getResourceFromJid(full_from_jid)); + } else { + if (_.isUndefined(_converse.roster)) { + _converse.log("Could not send notification, because roster is undefined", Strophe.LogLevel.ERROR); + + return; + } + + roster_item = _converse.roster.get(from_jid); + + if (!_.isUndefined(roster_item)) { + title = __("%1$s says", roster_item.getDisplayName()); + } else { + if (_converse.allow_non_roster_messaging) { + title = __("%1$s says", from_jid); + } else { + return; + } + } + } // TODO: we should suppress notifications if we cannot decrypt + // the message... + + + const body = sizzle(`encrypted[xmlns="${Strophe.NS.OMEMO}"]`, message).length ? __('OMEMO Message received') : _.get(message.querySelector('body'), 'textContent'); + + if (!body) { + return; + } + + const n = new Notification(title, { + 'body': body, + 'lang': _converse.locale, + 'icon': _converse.notification_icon + }); + setTimeout(n.close.bind(n), 5000); + }; + + _converse.showChatStateNotification = function (contact) { + /* Creates an HTML5 Notification to inform of a change in a + * contact's chat state. + */ + if (_.includes(_converse.chatstate_notification_blacklist, contact.jid)) { + // Don't notify if the user is being ignored. + return; + } + + const chat_state = contact.chat_status; + let message = null; + + if (chat_state === 'offline') { + message = __('has gone offline'); + } else if (chat_state === 'away') { + message = __('has gone away'); + } else if (chat_state === 'dnd') { + message = __('is busy'); + } else if (chat_state === 'online') { + message = __('has come online'); + } + + if (message === null) { + return; + } + + const n = new Notification(contact.getDisplayName(), { + body: message, + lang: _converse.locale, + icon: _converse.notification_icon + }); + setTimeout(n.close.bind(n), 5000); + }; + + _converse.showContactRequestNotification = function (contact) { + const n = new Notification(contact.getDisplayName(), { + body: __('wants to be your contact'), + lang: _converse.locale, + icon: _converse.notification_icon + }); + setTimeout(n.close.bind(n), 5000); + }; + + _converse.showFeedbackNotification = function (data) { + if (data.klass === 'error' || data.klass === 'warn') { + const n = new Notification(data.subject, { + body: data.message, + lang: _converse.locale, + icon: _converse.notification_icon + }); + setTimeout(n.close.bind(n), 5000); + } + }; + + _converse.handleChatStateNotification = function (contact) { + /* Event handler for on('contactPresenceChanged'). + * Will show an HTML5 notification to indicate that the chat + * status has changed. + */ + if (_converse.areDesktopNotificationsEnabled() && _converse.show_chatstate_notifications) { + _converse.showChatStateNotification(contact); + } + }; + + _converse.handleMessageNotification = function (data) { + /* Event handler for the on('message') event. Will call methods + * to play sounds and show HTML5 notifications. + */ + const message = data.stanza; + + if (!_converse.shouldNotifyOfMessage(message)) { + return false; + } + + _converse.playSoundNotification(); + + if (_converse.areDesktopNotificationsEnabled()) { + _converse.showMessageNotification(message); + } + }; + + _converse.handleContactRequestNotification = function (contact) { + if (_converse.areDesktopNotificationsEnabled(true)) { + _converse.showContactRequestNotification(contact); + } + }; + + _converse.handleFeedback = function (data) { + if (_converse.areDesktopNotificationsEnabled(true)) { + _converse.showFeedbackNotification(data); + } + }; + + _converse.requestPermission = function () { + if (_converse.supports_html5_notification && !_.includes(['denied', 'granted'], Notification.permission)) { + // Ask user to enable HTML5 notifications + Notification.requestPermission(); + } + }; + + _converse.on('pluginsInitialized', function () { + // We only register event handlers after all plugins are + // registered, because other plugins might override some of our + // handlers. + _converse.on('contactRequest', _converse.handleContactRequestNotification); + + _converse.on('contactPresenceChanged', _converse.handleChatStateNotification); + + _converse.on('message', _converse.handleMessageNotification); + + _converse.on('feedback', _converse.handleFeedback); + + _converse.on('connected', _converse.requestPermission); + }); + } + + }); +}); + +/***/ }), + +/***/ "./src/converse-omemo.js": +/*!*******************************!*\ + !*** ./src/converse-omemo.js ***! + \*******************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;// Converse.js +// http://conversejs.org +// +// Copyright (c) 2013-2018, the Converse.js developers +// Licensed under the Mozilla Public License (MPLv2) + +/* global libsignal, ArrayBuffer, parseInt, crypto */ +(function (root, factory) { + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! @converse/headless/converse-core */ "./src/headless/converse-core.js"), __webpack_require__(/*! templates/toolbar_omemo.html */ "./src/templates/toolbar_omemo.html")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), + __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? + (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), + __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); +})(this, function (converse, tpl_toolbar_omemo) { + const _converse$env = converse.env, + Backbone = _converse$env.Backbone, + Promise = _converse$env.Promise, + Strophe = _converse$env.Strophe, + moment = _converse$env.moment, + sizzle = _converse$env.sizzle, + $iq = _converse$env.$iq, + $msg = _converse$env.$msg, + _ = _converse$env._, + f = _converse$env.f, + b64_sha1 = _converse$env.b64_sha1; + const u = converse.env.utils; + Strophe.addNamespace('OMEMO_DEVICELIST', Strophe.NS.OMEMO + ".devicelist"); + Strophe.addNamespace('OMEMO_VERIFICATION', Strophe.NS.OMEMO + ".verification"); + Strophe.addNamespace('OMEMO_WHITELISTED', Strophe.NS.OMEMO + ".whitelisted"); + Strophe.addNamespace('OMEMO_BUNDLES', Strophe.NS.OMEMO + ".bundles"); + const UNDECIDED = 0; + const TRUSTED = 1; + const UNTRUSTED = -1; + const TAG_LENGTH = 128; + const KEY_ALGO = { + 'name': "AES-GCM", + 'length': 128 + }; + + function parseBundle(bundle_el) { + /* Given an XML element representing a user's OMEMO bundle, parse it + * and return a map. + */ + const signed_prekey_public_el = bundle_el.querySelector('signedPreKeyPublic'), + signed_prekey_signature_el = bundle_el.querySelector('signedPreKeySignature'), + identity_key_el = bundle_el.querySelector('identityKey'); + + const prekeys = _.map(sizzle(`prekeys > preKeyPublic`, bundle_el), el => { + return { + 'id': parseInt(el.getAttribute('preKeyId'), 10), + 'key': el.textContent + }; + }); + + return { + 'identity_key': bundle_el.querySelector('identityKey').textContent.trim(), + 'signed_prekey': { + 'id': parseInt(signed_prekey_public_el.getAttribute('signedPreKeyId'), 10), + 'public_key': signed_prekey_public_el.textContent, + 'signature': signed_prekey_signature_el.textContent + }, + 'prekeys': prekeys + }; + } + + converse.plugins.add('converse-omemo', { + enabled(_converse) { + return !_.isNil(window.libsignal) && !f.includes('converse-omemo', _converse.blacklisted_plugins); + }, + + dependencies: ["converse-chatview"], + overrides: { + ProfileModal: { + events: { + 'change input.select-all': 'selectAll', + 'submit .fingerprint-removal': 'removeSelectedFingerprints' + }, + + initialize() { + const _converse = this.__super__._converse; + this.debouncedRender = _.debounce(this.render, 50); + this.devicelist = _converse.devicelists.get(_converse.bare_jid); + this.devicelist.devices.on('change:bundle', this.debouncedRender, this); + this.devicelist.devices.on('reset', this.debouncedRender, this); + this.devicelist.devices.on('remove', this.debouncedRender, this); + this.devicelist.devices.on('add', this.debouncedRender, this); + return this.__super__.initialize.apply(this, arguments); + }, + + beforeRender() { + const _converse = this.__super__._converse, + device_id = _converse.omemo_store.get('device_id'); + + this.current_device = this.devicelist.devices.get(device_id); + this.other_devices = this.devicelist.devices.filter(d => d.get('id') !== device_id); + + if (this.__super__.beforeRender) { + return this.__super__.beforeRender.apply(this, arguments); + } + }, + + selectAll(ev) { + let sibling = u.ancestor(ev.target, 'li'); + + while (sibling) { + sibling.querySelector('input[type="checkbox"]').checked = ev.target.checked; + sibling = sibling.nextElementSibling; + } + }, + + removeSelectedFingerprints(ev) { + ev.preventDefault(); + ev.stopPropagation(); + ev.target.querySelector('.select-all').checked = false; + + const checkboxes = ev.target.querySelectorAll('.fingerprint-removal-item input[type="checkbox"]:checked'), + device_ids = _.map(checkboxes, 'value'); + + this.devicelist.removeOwnDevices(device_ids).then(this.modal.hide).catch(err => { + const _converse = this.__super__._converse, + __ = _converse.__; + + _converse.log(err, Strophe.LogLevel.ERROR); + + _converse.api.alert.show(Strophe.LogLevel.ERROR, __('Error'), [__('Sorry, an error occurred while trying to remove the devices.')]); + }); + } + + }, + UserDetailsModal: { + events: { + 'click .fingerprint-trust .btn input': 'toggleDeviceTrust' + }, + + initialize() { + const _converse = this.__super__._converse; + const jid = this.model.get('jid'); + this.devicelist = _converse.devicelists.get(jid) || _converse.devicelists.create({ + 'jid': jid + }); + this.devicelist.devices.on('change:bundle', this.render, this); + this.devicelist.devices.on('change:trusted', this.render, this); + this.devicelist.devices.on('remove', this.render, this); + this.devicelist.devices.on('add', this.render, this); + this.devicelist.devices.on('reset', this.render, this); + return this.__super__.initialize.apply(this, arguments); + }, + + toggleDeviceTrust(ev) { + const radio = ev.target; + const device = this.devicelist.devices.get(radio.getAttribute('name')); + device.save('trusted', parseInt(radio.value, 10)); + } + + }, + ChatBox: { + getBundlesAndBuildSessions() { + const _converse = this.__super__._converse; + let devices; + return _converse.getDevicesForContact(this.get('jid')).then(their_devices => { + const device_id = _converse.omemo_store.get('device_id'), + devicelist = _converse.devicelists.get(_converse.bare_jid), + own_devices = devicelist.devices.filter(device => device.get('id') !== device_id); + + devices = _.concat(own_devices, their_devices.models); + return Promise.all(devices.map(device => device.getBundle())); + }).then(() => this.buildSessions(devices)); + }, + + buildSession(device) { + const _converse = this.__super__._converse, + address = new libsignal.SignalProtocolAddress(device.get('jid'), device.get('id')), + sessionBuilder = new libsignal.SessionBuilder(_converse.omemo_store, address), + prekey = device.getRandomPreKey(); + return device.getBundle().then(bundle => { + return sessionBuilder.processPreKey({ + 'registrationId': parseInt(device.get('id'), 10), + 'identityKey': u.base64ToArrayBuffer(bundle.identity_key), + 'signedPreKey': { + 'keyId': bundle.signed_prekey.id, + // + 'publicKey': u.base64ToArrayBuffer(bundle.signed_prekey.public_key), + 'signature': u.base64ToArrayBuffer(bundle.signed_prekey.signature) + }, + 'preKey': { + 'keyId': prekey.id, + // + 'publicKey': u.base64ToArrayBuffer(prekey.key) + } + }); + }); + }, + + getSession(device) { + const _converse = this.__super__._converse, + address = new libsignal.SignalProtocolAddress(device.get('jid'), device.get('id')); + return _converse.omemo_store.loadSession(address.toString()).then(session => { + if (session) { + return Promise.resolve(); + } else { + return this.buildSession(device); + } + }); + }, + + async encryptMessage(plaintext) { + // The client MUST use fresh, randomly generated key/IV pairs + // with AES-128 in Galois/Counter Mode (GCM). + // For GCM a 12 byte IV is strongly suggested as other IV lengths + // will require additional calculations. In principle any IV size + // can be used as long as the IV doesn't ever repeat. NIST however + // suggests that only an IV size of 12 bytes needs to be supported + // by implementations. + // + // https://crypto.stackexchange.com/questions/26783/ciphertext-and-tag-size-and-iv-transmission-with-aes-in-gcm-mode + const iv = crypto.getRandomValues(new window.Uint8Array(12)), + key = await crypto.subtle.generateKey(KEY_ALGO, true, ["encrypt", "decrypt"]), + algo = { + 'name': 'AES-GCM', + 'iv': iv, + 'tagLength': TAG_LENGTH + }, + encrypted = await crypto.subtle.encrypt(algo, key, u.stringToArrayBuffer(plaintext)), + length = encrypted.byteLength - (128 + 7 >> 3), + ciphertext = encrypted.slice(0, length), + tag = encrypted.slice(length), + exported_key = await crypto.subtle.exportKey("raw", key); + return Promise.resolve({ + 'key': exported_key, + 'tag': tag, + 'key_and_tag': u.appendArrayBuffer(exported_key, tag), + 'payload': u.arrayBufferToBase64(ciphertext), + 'iv': u.arrayBufferToBase64(iv) + }); + }, + + async decryptMessage(obj) { + const key_obj = await crypto.subtle.importKey('raw', obj.key, KEY_ALGO, true, ['encrypt', 'decrypt']), + cipher = u.appendArrayBuffer(u.base64ToArrayBuffer(obj.payload), obj.tag), + algo = { + 'name': "AES-GCM", + 'iv': u.base64ToArrayBuffer(obj.iv), + 'tagLength': TAG_LENGTH + }; + return u.arrayBufferToString((await crypto.subtle.decrypt(algo, key_obj, cipher))); + }, + + reportDecryptionError(e) { + const _converse = this.__super__._converse; + + if (_converse.debug) { + const __ = _converse.__; + this.messages.create({ + 'message': __("Sorry, could not decrypt a received OMEMO message due to an error.") + ` ${e.name} ${e.message}`, + 'type': 'error' + }); + } + + _converse.log(`${e.name} ${e.message}`, Strophe.LogLevel.ERROR); + }, + + decrypt(attrs) { + const _converse = this.__super__._converse, + session_cipher = this.getSessionCipher(attrs.from, parseInt(attrs.encrypted.device_id, 10)); // https://xmpp.org/extensions/xep-0384.html#usecases-receiving + + if (attrs.encrypted.prekey === 'true') { + let plaintext; + return session_cipher.decryptPreKeyWhisperMessage(u.base64ToArrayBuffer(attrs.encrypted.key), 'binary').then(key_and_tag => { + if (attrs.encrypted.payload) { + const key = key_and_tag.slice(0, 16), + tag = key_and_tag.slice(16); + return this.decryptMessage(_.extend(attrs.encrypted, { + 'key': key, + 'tag': tag + })); + } + + return Promise.resolve(); + }).then(pt => { + plaintext = pt; + return _converse.omemo_store.generateMissingPreKeys(); + }).then(() => _converse.omemo_store.publishBundle()).then(() => { + if (plaintext) { + return _.extend(attrs, { + 'plaintext': plaintext + }); + } else { + return _.extend(attrs, { + 'is_only_key': true + }); + } + }).catch(e => { + this.reportDecryptionError(e); + return attrs; + }); + } else { + return session_cipher.decryptWhisperMessage(u.base64ToArrayBuffer(attrs.encrypted.key), 'binary').then(key_and_tag => { + const key = key_and_tag.slice(0, 16), + tag = key_and_tag.slice(16); + return this.decryptMessage(_.extend(attrs.encrypted, { + 'key': key, + 'tag': tag + })); + }).then(plaintext => _.extend(attrs, { + 'plaintext': plaintext + })).catch(e => { + this.reportDecryptionError(e); + return attrs; + }); + } + }, + + getEncryptionAttributesfromStanza(stanza, original_stanza, attrs) { + const _converse = this.__super__._converse, + encrypted = sizzle(`encrypted[xmlns="${Strophe.NS.OMEMO}"]`, original_stanza).pop(), + header = encrypted.querySelector('header'), + key = sizzle(`key[rid="${_converse.omemo_store.get('device_id')}"]`, encrypted).pop(); + + if (key) { + attrs['is_encrypted'] = true; + attrs['encrypted'] = { + 'device_id': header.getAttribute('sid'), + 'iv': header.querySelector('iv').textContent, + 'key': key.textContent, + 'payload': _.get(encrypted.querySelector('payload'), 'textContent', null), + 'prekey': key.getAttribute('prekey') + }; + return this.decrypt(attrs); + } else { + return Promise.resolve(attrs); + } + }, + + getMessageAttributesFromStanza(stanza, original_stanza) { + const _converse = this.__super__._converse, + encrypted = sizzle(`encrypted[xmlns="${Strophe.NS.OMEMO}"]`, original_stanza).pop(), + attrs = this.__super__.getMessageAttributesFromStanza.apply(this, arguments); + + if (!encrypted || !_converse.config.get('trusted')) { + return attrs; + } else { + return this.getEncryptionAttributesfromStanza(stanza, original_stanza, attrs); + } + }, + + buildSessions(devices) { + return Promise.all(devices.map(device => this.getSession(device))).then(() => devices); + }, + + getSessionCipher(jid, id) { + const _converse = this.__super__._converse, + address = new libsignal.SignalProtocolAddress(jid, id); + this.session_cipher = new window.libsignal.SessionCipher(_converse.omemo_store, address); + return this.session_cipher; + }, + + encryptKey(plaintext, device) { + return this.getSessionCipher(device.get('jid'), device.get('id')).encrypt(plaintext).then(payload => ({ + 'payload': payload, + 'device': device + })); + }, + + addKeysToMessageStanza(stanza, dicts, iv) { + for (var i in dicts) { + if (Object.prototype.hasOwnProperty.call(dicts, i)) { + const payload = dicts[i].payload, + device = dicts[i].device, + prekey = 3 == parseInt(payload.type, 10); + stanza.c('key', { + 'rid': device.get('id') + }).t(btoa(payload.body)); + + if (prekey) { + stanza.attrs({ + 'prekey': prekey + }); + } + + stanza.up(); + + if (i == dicts.length - 1) { + stanza.c('iv').t(iv).up().up(); + } + } + } + + return Promise.resolve(stanza); + }, + + createOMEMOMessageStanza(message, devices) { + const _converse = this.__super__._converse, + __ = _converse.__; + + const body = __("This is an OMEMO encrypted message which your client doesn’t seem to support. " + "Find more information on https://conversations.im/omemo"); + + if (!message.get('message')) { + throw new Error("No message body to encrypt!"); + } + + const stanza = $msg({ + 'from': _converse.connection.jid, + 'to': this.get('jid'), + 'type': this.get('message_type'), + 'id': message.get('msgid') + }).c('body').t(body).up() // An encrypted header is added to the message for + // each device that is supposed to receive it. + // These headers simply contain the key that the + // payload message is encrypted with, + // and they are separately encrypted using the + // session corresponding to the counterpart device. + .c('encrypted', { + 'xmlns': Strophe.NS.OMEMO + }).c('header', { + 'sid': _converse.omemo_store.get('device_id') + }); + return this.encryptMessage(message.get('message')).then(obj => { + // The 16 bytes key and the GCM authentication tag (The tag + // SHOULD have at least 128 bit) are concatenated and for each + // intended recipient device, i.e. both own devices as well as + // devices associated with the contact, the result of this + // concatenation is encrypted using the corresponding + // long-standing SignalProtocol session. + const promises = devices.filter(device => device.get('trusted') != UNTRUSTED).map(device => this.encryptKey(obj.key_and_tag, device)); + return Promise.all(promises).then(dicts => this.addKeysToMessageStanza(stanza, dicts, obj.iv)).then(stanza => { + stanza.c('payload').t(obj.payload).up().up(); + stanza.c('store', { + 'xmlns': Strophe.NS.HINTS + }); + return stanza; + }); + }); + }, + + sendMessage(attrs) { + const _converse = this.__super__._converse, + __ = _converse.__; + + if (this.get('omemo_active') && attrs.message) { + attrs['is_encrypted'] = true; + attrs['plaintext'] = attrs.message; + const message = this.messages.create(attrs); + this.getBundlesAndBuildSessions().then(devices => this.createOMEMOMessageStanza(message, devices)).then(stanza => this.sendMessageStanza(stanza)).catch(e => { + this.messages.create({ + 'message': __("Sorry, could not send the message due to an error.") + ` ${e.message}`, + 'type': 'error' + }); + + _converse.log(e, Strophe.LogLevel.ERROR); + }); + } else { + return this.__super__.sendMessage.apply(this, arguments); + } + } + + }, + ChatBoxView: { + events: { + 'click .toggle-omemo': 'toggleOMEMO' + }, + + showMessage(message) { + // We don't show a message if it's only keying material + if (!message.get('is_only_key')) { + return this.__super__.showMessage.apply(this, arguments); + } + }, + + async renderOMEMOToolbarButton() { + const _converse = this.__super__._converse, + __ = _converse.__; + const support = await _converse.contactHasOMEMOSupport(this.model.get('jid')); + + if (support) { + const icon = this.el.querySelector('.toggle-omemo'), + html = tpl_toolbar_omemo(_.extend(this.model.toJSON(), { + '__': __ + })); + + if (icon) { + icon.outerHTML = html; + } else { + this.el.querySelector('.chat-toolbar').insertAdjacentHTML('beforeend', html); + } + } + }, + + toggleOMEMO(ev) { + ev.preventDefault(); + this.model.save({ + 'omemo_active': !this.model.get('omemo_active') + }); + this.renderOMEMOToolbarButton(); + } + + } + }, + + initialize() { + /* The initialize function gets called as soon as the plugin is + * loaded by Converse.js's plugin machinery. + */ + const _converse = this._converse; + + _converse.api.promises.add(['OMEMOInitialized']); + + _converse.NUM_PREKEYS = 100; // Set here so that tests can override + + function generateFingerprint(device) { + if (_.get(device.get('bundle'), 'fingerprint')) { + return; + } + + return device.getBundle().then(bundle => { + bundle['fingerprint'] = u.arrayBufferToHex(u.base64ToArrayBuffer(bundle['identity_key'])); + device.save('bundle', bundle); + device.trigger('change:bundle'); // Doesn't get triggered automatically due to pass-by-reference + }); + } + + _converse.generateFingerprints = function (jid) { + return _converse.getDevicesForContact(jid).then(devices => Promise.all(devices.map(d => generateFingerprint(d)))); + }; + + _converse.getDeviceForContact = function (jid, device_id) { + return _converse.getDevicesForContact(jid).then(devices => devices.get(device_id)); + }; + + _converse.getDevicesForContact = function (jid) { + let devicelist; + return _converse.api.waitUntil('OMEMOInitialized').then(() => { + devicelist = _converse.devicelists.get(jid) || _converse.devicelists.create({ + 'jid': jid + }); + return devicelist.fetchDevices(); + }).then(() => devicelist.devices); + }; + + _converse.contactHasOMEMOSupport = function (jid) { + /* Checks whether the contact advertises any OMEMO-compatible devices. */ + return new Promise((resolve, reject) => { + _converse.getDevicesForContact(jid).then(devices => resolve(devices.length > 0)).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); + }); + }; + + function generateDeviceID() { + /* Generates a device ID, making sure that it's unique */ + const existing_ids = _converse.devicelists.get(_converse.bare_jid).devices.pluck('id'); + + let device_id = libsignal.KeyHelper.generateRegistrationId(); + let i = 0; + + while (_.includes(existing_ids, device_id)) { + device_id = libsignal.KeyHelper.generateRegistrationId(); + i++; + + if (i == 10) { + throw new Error("Unable to generate a unique device ID"); + } + } + + return device_id.toString(); + } + + _converse.OMEMOStore = Backbone.Model.extend({ + Direction: { + SENDING: 1, + RECEIVING: 2 + }, + + getIdentityKeyPair() { + const keypair = this.get('identity_keypair'); + return Promise.resolve({ + 'privKey': u.base64ToArrayBuffer(keypair.privKey), + 'pubKey': u.base64ToArrayBuffer(keypair.pubKey) + }); + }, + + getLocalRegistrationId() { + return Promise.resolve(parseInt(this.get('device_id'), 10)); + }, + + isTrustedIdentity(identifier, identity_key, direction) { + if (_.isNil(identifier)) { + throw new Error("Can't check identity key for invalid key"); + } + + if (!(identity_key instanceof ArrayBuffer)) { + throw new Error("Expected identity_key to be an ArrayBuffer"); + } + + const trusted = this.get('identity_key' + identifier); + + if (trusted === undefined) { + return Promise.resolve(true); + } + + return Promise.resolve(u.arrayBufferToBase64(identity_key) === trusted); + }, + + loadIdentityKey(identifier) { + if (_.isNil(identifier)) { + throw new Error("Can't load identity_key for invalid identifier"); + } + + return Promise.resolve(u.base64ToArrayBuffer(this.get('identity_key' + identifier))); + }, + + saveIdentity(identifier, identity_key) { + if (_.isNil(identifier)) { + throw new Error("Can't save identity_key for invalid identifier"); + } + + const address = new libsignal.SignalProtocolAddress.fromString(identifier), + existing = this.get('identity_key' + address.getName()); + const b64_idkey = u.arrayBufferToBase64(identity_key); + this.save('identity_key' + address.getName(), b64_idkey); + + if (existing && b64_idkey !== existing) { + return Promise.resolve(true); + } else { + return Promise.resolve(false); + } + }, + + getPreKeys() { + return this.get('prekeys') || {}; + }, + + loadPreKey(key_id) { + const res = this.getPreKeys()[key_id]; + + if (res) { + return Promise.resolve({ + 'privKey': u.base64ToArrayBuffer(res.privKey), + 'pubKey': u.base64ToArrayBuffer(res.pubKey) + }); + } + + return Promise.resolve(); + }, + + storePreKey(key_id, key_pair) { + const prekey = {}; + prekey[key_id] = { + 'pubKey': u.arrayBufferToBase64(key_pair.pubKey), + 'privKey': u.arrayBufferToBase64(key_pair.privKey) + }; + this.save('prekeys', _.extend(this.getPreKeys(), prekey)); + return Promise.resolve(); + }, + + removePreKey(key_id) { + this.save('prekeys', _.omit(this.getPreKeys(), key_id)); + return Promise.resolve(); + }, + + loadSignedPreKey(keyId) { + const res = this.get('signed_prekey'); + + if (res) { + return Promise.resolve({ + 'privKey': u.base64ToArrayBuffer(res.privKey), + 'pubKey': u.base64ToArrayBuffer(res.pubKey) + }); + } + + return Promise.resolve(); + }, + + storeSignedPreKey(spk) { + if (typeof spk !== "object") { + // XXX: We've changed the signature of this method from the + // example given in InMemorySignalProtocolStore. + // Should be fine because the libsignal code doesn't + // actually call this method. + throw new Error("storeSignedPreKey: expected an object"); + } + + this.save('signed_prekey', { + 'id': spk.keyId, + 'privKey': u.arrayBufferToBase64(spk.keyPair.privKey), + 'pubKey': u.arrayBufferToBase64(spk.keyPair.pubKey), + // XXX: The InMemorySignalProtocolStore does not pass + // in or store the signature, but we need it when we + // publish out bundle and this method isn't called from + // within libsignal code, so we modify it to also store + // the signature. + 'signature': u.arrayBufferToBase64(spk.signature) + }); + return Promise.resolve(); + }, + + removeSignedPreKey(key_id) { + if (this.get('signed_prekey')['id'] === key_id) { + this.unset('signed_prekey'); + this.save(); + } + + return Promise.resolve(); + }, + + loadSession(identifier) { + return Promise.resolve(this.get('session' + identifier)); + }, + + storeSession(identifier, record) { + return Promise.resolve(this.save('session' + identifier, record)); + }, + + removeSession(identifier) { + return Promise.resolve(this.unset('session' + identifier)); + }, + + removeAllSessions(identifier) { + const keys = _.filter(_.keys(this.attributes), key => { + if (key.startsWith('session' + identifier)) { + return key; + } + }); + + const attrs = {}; + + _.forEach(keys, key => { + attrs[key] = undefined; + }); + + this.save(attrs); + return Promise.resolve(); + }, + + publishBundle() { + const signed_prekey = this.get('signed_prekey'); + const stanza = $iq({ + 'from': _converse.bare_jid, + 'type': 'set' + }).c('pubsub', { + 'xmlns': Strophe.NS.PUBSUB + }).c('publish', { + 'node': `${Strophe.NS.OMEMO_BUNDLES}:${this.get('device_id')}` + }).c('item').c('bundle', { + 'xmlns': Strophe.NS.OMEMO + }).c('signedPreKeyPublic', { + 'signedPreKeyId': signed_prekey.id + }).t(signed_prekey.pubKey).up().c('signedPreKeySignature').t(signed_prekey.signature).up().c('identityKey').t(this.get('identity_keypair').pubKey).up().c('prekeys'); + + _.forEach(this.get('prekeys'), (prekey, id) => stanza.c('preKeyPublic', { + 'preKeyId': id + }).t(prekey.pubKey).up()); + + return _converse.api.sendIQ(stanza); + }, + + generateMissingPreKeys() { + const current_keys = this.getPreKeys(), + missing_keys = _.difference(_.invokeMap(_.range(0, _converse.NUM_PREKEYS), Number.prototype.toString), _.keys(current_keys)); + + if (missing_keys.length < 1) { + _converse.log("No missing prekeys to generate for our own device", Strophe.LogLevel.WARN); + + return Promise.resolve(); + } + + return Promise.all(_.map(missing_keys, id => libsignal.KeyHelper.generatePreKey(parseInt(id, 10)))).then(keys => { + _.forEach(keys, k => this.storePreKey(k.keyId, k.keyPair)); + + const marshalled_keys = _.map(this.getPreKeys(), k => ({ + 'id': k.keyId, + 'key': u.arrayBufferToBase64(k.pubKey) + })), + devicelist = _converse.devicelists.get(_converse.bare_jid), + device = devicelist.devices.get(this.get('device_id')); + + return device.getBundle().then(bundle => device.save('bundle', _.extend(bundle, { + 'prekeys': marshalled_keys + }))); + }); + }, + + async generateBundle() { + /* The first thing that needs to happen if a client wants to + * start using OMEMO is they need to generate an IdentityKey + * and a Device ID. The IdentityKey is a Curve25519 [6] + * public/private Key pair. The Device ID is a randomly + * generated integer between 1 and 2^31 - 1. + */ + const identity_keypair = await libsignal.KeyHelper.generateIdentityKeyPair(); + const bundle = {}, + identity_key = u.arrayBufferToBase64(identity_keypair.pubKey), + device_id = generateDeviceID(); + bundle['identity_key'] = identity_key; + bundle['device_id'] = device_id; + this.save({ + 'device_id': device_id, + 'identity_keypair': { + 'privKey': u.arrayBufferToBase64(identity_keypair.privKey), + 'pubKey': identity_key + }, + 'identity_key': identity_key + }); + const signed_prekey = await libsignal.KeyHelper.generateSignedPreKey(identity_keypair, 0); + + _converse.omemo_store.storeSignedPreKey(signed_prekey); + + bundle['signed_prekey'] = { + 'id': signed_prekey.keyId, + 'public_key': u.arrayBufferToBase64(signed_prekey.keyPair.privKey), + 'signature': u.arrayBufferToBase64(signed_prekey.signature) + }; + const keys = await Promise.all(_.map(_.range(0, _converse.NUM_PREKEYS), id => libsignal.KeyHelper.generatePreKey(id))); + + _.forEach(keys, k => _converse.omemo_store.storePreKey(k.keyId, k.keyPair)); + + const devicelist = _converse.devicelists.get(_converse.bare_jid), + device = devicelist.devices.create({ + 'id': bundle.device_id, + 'jid': _converse.bare_jid + }), + marshalled_keys = _.map(keys, k => ({ + 'id': k.keyId, + 'key': u.arrayBufferToBase64(k.keyPair.pubKey) + })); + + bundle['prekeys'] = marshalled_keys; + device.save('bundle', bundle); + }, + + fetchSession() { + if (_.isUndefined(this._setup_promise)) { + this._setup_promise = new Promise((resolve, reject) => { + this.fetch({ + 'success': () => { + if (!_converse.omemo_store.get('device_id')) { + this.generateBundle().then(resolve).catch(resolve); + } else { + resolve(); + } + }, + 'error': () => { + this.generateBundle().then(resolve).catch(resolve); + } + }); + }); + } + + return this._setup_promise; + } + + }); + _converse.Device = Backbone.Model.extend({ + defaults: { + 'trusted': UNDECIDED + }, + + getRandomPreKey() { + // XXX: assumes that the bundle has already been fetched + const bundle = this.get('bundle'); + return bundle.prekeys[u.getRandomInt(bundle.prekeys.length)]; + }, + + fetchBundleFromServer() { + const stanza = $iq({ + 'type': 'get', + 'from': _converse.bare_jid, + 'to': this.get('jid') + }).c('pubsub', { + 'xmlns': Strophe.NS.PUBSUB + }).c('items', { + 'node': `${Strophe.NS.OMEMO_BUNDLES}:${this.get('id')}` + }); + return _converse.api.sendIQ(stanza).then(iq => { + const publish_el = sizzle(`items[node="${Strophe.NS.OMEMO_BUNDLES}:${this.get('id')}"]`, iq).pop(), + bundle_el = sizzle(`bundle[xmlns="${Strophe.NS.OMEMO}"]`, publish_el).pop(), + bundle = parseBundle(bundle_el); + this.save('bundle', bundle); + return bundle; + }).catch(iq => { + _converse.log(iq.outerHTML, Strophe.LogLevel.ERROR); + }); + }, + + getBundle() { + /* Fetch and save the bundle information associated with + * this device, if the information is not at hand already. + */ + if (this.get('bundle')) { + return Promise.resolve(this.get('bundle'), this); + } else { + return this.fetchBundleFromServer(); + } + } + + }); + _converse.Devices = Backbone.Collection.extend({ + model: _converse.Device + }); + _converse.DeviceList = Backbone.Model.extend({ + idAttribute: 'jid', + + initialize() { + this.devices = new _converse.Devices(); + const id = `converse.devicelist-${_converse.bare_jid}-${this.get('jid')}`; + this.devices.browserStorage = new Backbone.BrowserStorage.session(id); + this.fetchDevices(); + }, + + fetchDevices() { + if (_.isUndefined(this._devices_promise)) { + this._devices_promise = new Promise((resolve, reject) => { + this.devices.fetch({ + 'success': collection => { + if (collection.length === 0) { + this.fetchDevicesFromServer().then(ids => this.publishCurrentDevice(ids)).finally(resolve); + } else { + resolve(); + } + } + }); + }); + } + + return this._devices_promise; + }, + + async publishCurrentDevice(device_ids) { + if (this.get('jid') !== _converse.bare_jid) { + // We only publish for ourselves. + return; + } + + await restoreOMEMOSession(); + + let device_id = _converse.omemo_store.get('device_id'); + + if (!this.devices.findWhere({ + 'id': device_id + })) { + // Generate a new bundle if we cannot find our device + await _converse.omemo_store.generateBundle(); + device_id = _converse.omemo_store.get('device_id'); + } + + if (!_.includes(device_ids, device_id)) { + return this.publishDevices(); + } + }, + + fetchDevicesFromServer() { + const stanza = $iq({ + 'type': 'get', + 'from': _converse.bare_jid, + 'to': this.get('jid') + }).c('pubsub', { + 'xmlns': Strophe.NS.PUBSUB + }).c('items', { + 'node': Strophe.NS.OMEMO_DEVICELIST + }); + return _converse.api.sendIQ(stanza).then(iq => { + const device_ids = _.map(sizzle(`list[xmlns="${Strophe.NS.OMEMO}"] device`, iq), dev => dev.getAttribute('id')); + + _.forEach(device_ids, id => this.devices.create({ + 'id': id, + 'jid': this.get('jid') + })); + + return device_ids; + }); + }, + + publishDevices() { + const stanza = $iq({ + 'from': _converse.bare_jid, + 'type': 'set' + }).c('pubsub', { + 'xmlns': Strophe.NS.PUBSUB + }).c('publish', { + 'node': Strophe.NS.OMEMO_DEVICELIST + }).c('item').c('list', { + 'xmlns': Strophe.NS.OMEMO + }); + this.devices.each(device => stanza.c('device', { + 'id': device.get('id') + }).up()); + return _converse.api.sendIQ(stanza); + }, + + removeOwnDevices(device_ids) { + if (this.get('jid') !== _converse.bare_jid) { + throw new Error("Cannot remove devices from someone else's device list"); + } + + _.forEach(device_ids, device_id => this.devices.get(device_id).destroy()); + + return this.publishDevices(); + } + + }); + _converse.DeviceLists = Backbone.Collection.extend({ + model: _converse.DeviceList + }); + + function fetchDeviceLists() { + return new Promise((resolve, reject) => _converse.devicelists.fetch({ + 'success': resolve + })); + } + + function fetchOwnDevices() { + return fetchDeviceLists().then(() => { + let own_devicelist = _converse.devicelists.get(_converse.bare_jid); + + if (_.isNil(own_devicelist)) { + own_devicelist = _converse.devicelists.create({ + 'jid': _converse.bare_jid + }); + } + + return own_devicelist.fetchDevices(); + }); + } + + function updateBundleFromStanza(stanza) { + const items_el = sizzle(`items`, stanza).pop(); + + if (!items_el || !items_el.getAttribute('node').startsWith(Strophe.NS.OMEMO_BUNDLES)) { + return; + } + + const device_id = items_el.getAttribute('node').split(':')[1], + jid = stanza.getAttribute('from'), + bundle_el = sizzle(`item > bundle`, items_el).pop(), + devicelist = _converse.devicelists.get(jid) || _converse.devicelists.create({ + 'jid': jid + }), + device = devicelist.devices.get(device_id) || devicelist.devices.create({ + 'id': device_id, + 'jid': jid + }); + + device.save({ + 'bundle': parseBundle(bundle_el) + }); + } + + function updateDevicesFromStanza(stanza) { + const items_el = sizzle(`items[node="${Strophe.NS.OMEMO_DEVICELIST}"]`, stanza).pop(); + + if (!items_el) { + return; + } + + const device_ids = _.map(sizzle(`item list[xmlns="${Strophe.NS.OMEMO}"] device`, items_el), device => device.getAttribute('id')); + + const jid = stanza.getAttribute('from'), + devicelist = _converse.devicelists.get(jid) || _converse.devicelists.create({ + 'jid': jid + }), + devices = devicelist.devices, + removed_ids = _.difference(devices.pluck('id'), device_ids); + + _.forEach(removed_ids, id => { + if (jid === _converse.bare_jid && id === _converse.omemo_store.get('device_id')) { + // We don't remove the current device + return; + } + + devices.get(id).destroy(); + }); + + _.forEach(device_ids, device_id => { + if (!devices.get(device_id)) { + devices.create({ + 'id': device_id, + 'jid': jid + }); + } + }); + + if (Strophe.getBareJidFromJid(jid) === _converse.bare_jid) { + // Make sure our own device is on the list (i.e. if it was + // removed, add it again. + _converse.devicelists.get(_converse.bare_jid).publishCurrentDevice(device_ids); + } + } + + function registerPEPPushHandler() { + // Add a handler for devices pushed from other connected clients + _converse.connection.addHandler(message => { + try { + if (sizzle(`event[xmlns="${Strophe.NS.PUBSUB}#event"]`, message).length) { + updateDevicesFromStanza(message); + updateBundleFromStanza(message); + } + } catch (e) { + _converse.log(e.message, Strophe.LogLevel.ERROR); + } + + return true; + }, null, 'message', 'headline'); + } + + function restoreOMEMOSession() { + if (_.isUndefined(_converse.omemo_store)) { + const storage = _converse.config.get('storage'), + id = `converse.omemosession-${_converse.bare_jid}`; + + _converse.omemo_store = new _converse.OMEMOStore({ + 'id': id + }); + _converse.omemo_store.browserStorage = new Backbone.BrowserStorage[storage](id); + } + + return _converse.omemo_store.fetchSession(); + } + + function initOMEMO() { + if (!_converse.config.get('trusted')) { + return; + } + + _converse.devicelists = new _converse.DeviceLists(); + + const storage = _converse.config.get('storage'), + id = `converse.devicelists-${_converse.bare_jid}`; + + _converse.devicelists.browserStorage = new Backbone.BrowserStorage[storage](id); + fetchOwnDevices().then(() => restoreOMEMOSession()).then(() => _converse.omemo_store.publishBundle()).then(() => _converse.emit('OMEMOInitialized')).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); + } + + _converse.api.listen.on('afterTearDown', () => { + if (_converse.devicelists) { + _converse.devicelists.reset(); + } + + delete _converse.omemo_store; + }); + + _converse.api.listen.on('connected', registerPEPPushHandler); + + _converse.api.listen.on('renderToolbar', view => view.renderOMEMOToolbarButton()); + + _converse.api.listen.on('statusInitialized', initOMEMO); + + _converse.api.listen.on('addClientFeatures', () => _converse.api.disco.own.features.add(`${Strophe.NS.OMEMO_DEVICELIST}+notify`)); + + _converse.api.listen.on('userDetailsModalInitialized', contact => { + const jid = contact.get('jid'); + + _converse.generateFingerprints(jid).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); + }); + + _converse.api.listen.on('profileModalInitialized', contact => { + _converse.generateFingerprints(_converse.bare_jid).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); + }); + } + + }); +}); + +/***/ }), + +/***/ "./src/converse-profile.js": +/*!*********************************!*\ + !*** ./src/converse-profile.js ***! + \*********************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;// Converse.js (A browser based XMPP chat client) +// http://conversejs.org +// +// Copyright (c) 2013-2017, Jan-Carel Brand +// Licensed under the Mozilla Public License (MPLv2) +// + +/*global define */ +(function (root, factory) { + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! @converse/headless/converse-core */ "./src/headless/converse-core.js"), __webpack_require__(/*! bootstrap */ "./node_modules/bootstrap.native/dist/bootstrap-native-v4.js"), __webpack_require__(/*! formdata-polyfill */ "./node_modules/formdata-polyfill/FormData.js"), __webpack_require__(/*! templates/alert.html */ "./src/templates/alert.html"), __webpack_require__(/*! templates/chat_status_modal.html */ "./src/templates/chat_status_modal.html"), __webpack_require__(/*! templates/profile_modal.html */ "./src/templates/profile_modal.html"), __webpack_require__(/*! templates/profile_view.html */ "./src/templates/profile_view.html"), __webpack_require__(/*! templates/status_option.html */ "./src/templates/status_option.html"), __webpack_require__(/*! @converse/headless/converse-vcard */ "./src/headless/converse-vcard.js"), __webpack_require__(/*! converse-modal */ "./src/converse-modal.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), + __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? + (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), + __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); +})(this, function (converse, bootstrap, _FormData, tpl_alert, tpl_chat_status_modal, tpl_profile_modal, tpl_profile_view, tpl_status_option) { + "use strict"; + + const _converse$env = converse.env, + Strophe = _converse$env.Strophe, + Backbone = _converse$env.Backbone, + Promise = _converse$env.Promise, + utils = _converse$env.utils, + _ = _converse$env._, + moment = _converse$env.moment; + const u = converse.env.utils; + converse.plugins.add('converse-profile', { + dependencies: ["converse-modal", "converse-vcard", "converse-chatboxviews"], + + initialize() { + /* The initialize function gets called as soon as the plugin is + * loaded by converse.js's plugin machinery. + */ + const _converse = this._converse, + __ = _converse.__; + _converse.ProfileModal = _converse.BootstrapModal.extend({ + events: { + 'click .change-avatar': "openFileSelection", + 'change input[type="file"': "updateFilePreview", + 'submit .profile-form': 'onFormSubmitted' + }, + + initialize() { + this.model.on('change', this.render, this); + + _converse.BootstrapModal.prototype.initialize.apply(this, arguments); + + _converse.emit('profileModalInitialized', this.model); + }, + + toHTML() { + return tpl_profile_modal(_.extend(this.model.toJSON(), this.model.vcard.toJSON(), { + '_': _, + '__': __, + '_converse': _converse, + 'alt_avatar': __('Your avatar image'), + 'heading_profile': __('Your Profile'), + 'label_close': __('Close'), + 'label_email': __('Email'), + 'label_fullname': __('Full Name'), + 'label_jid': __('XMPP Address (JID)'), + 'label_nickname': __('Nickname'), + 'label_role': __('Role'), + 'label_role_help': __('Use commas to separate multiple roles. Your roles are shown next to your name on your chat messages.'), + 'label_url': __('URL'), + 'utils': u, + 'view': this + })); + }, + + afterRender() { + this.tabs = _.map(this.el.querySelectorAll('.nav-item'), tab => new bootstrap.Tab(tab)); + }, + + openFileSelection(ev) { + ev.preventDefault(); + this.el.querySelector('input[type="file"]').click(); + }, + + updateFilePreview(ev) { + const file = ev.target.files[0], + reader = new FileReader(); + + reader.onloadend = () => { + this.el.querySelector('.avatar').setAttribute('src', reader.result); + }; + + reader.readAsDataURL(file); + }, + + setVCard(data) { + _converse.api.vcard.set(_converse.bare_jid, data).then(() => _converse.api.vcard.update(this.model.vcard, true)).catch(err => { + _converse.log(err, Strophe.LogLevel.FATAL); + + _converse.api.alert.show(Strophe.LogLevel.ERROR, __('Error'), [__("Sorry, an error happened while trying to save your profile data."), __("You can check your browser's developer console for any error output.")]); + }); + + this.modal.hide(); + }, + + onFormSubmitted(ev) { + ev.preventDefault(); + const reader = new FileReader(), + form_data = new FormData(ev.target), + image_file = form_data.get('image'); + const data = { + 'fn': form_data.get('fn'), + 'nickname': form_data.get('nickname'), + 'role': form_data.get('role'), + 'email': form_data.get('email'), + 'url': form_data.get('url') + }; + + if (!image_file.size) { + _.extend(data, { + 'image': this.model.vcard.get('image'), + 'image_type': this.model.vcard.get('image_type') + }); + + this.setVCard(data); + } else { + reader.onloadend = () => { + _.extend(data, { + 'image': btoa(reader.result), + 'image_type': image_file.type + }); + + this.setVCard(data); + }; + + reader.readAsBinaryString(image_file); + } + } + + }); + _converse.ChatStatusModal = _converse.BootstrapModal.extend({ + events: { + "submit form#set-xmpp-status": "onFormSubmitted", + "click .clear-input": "clearStatusMessage" + }, + + toHTML() { + return tpl_chat_status_modal(_.extend(this.model.toJSON(), this.model.vcard.toJSON(), { + 'label_away': __('Away'), + 'label_close': __('Close'), + 'label_busy': __('Busy'), + 'label_cancel': __('Cancel'), + 'label_custom_status': __('Custom status'), + 'label_offline': __('Offline'), + 'label_online': __('Online'), + 'label_save': __('Save'), + 'label_xa': __('Away for long'), + 'modal_title': __('Change chat status'), + 'placeholder_status_message': __('Personal status message') + })); + }, + + afterRender() { + this.el.addEventListener('shown.bs.modal', () => { + this.el.querySelector('input[name="status_message"]').focus(); + }, false); + }, + + clearStatusMessage(ev) { + if (ev && ev.preventDefault) { + ev.preventDefault(); + u.hideElement(this.el.querySelector('.clear-input')); + } + + const roster_filter = this.el.querySelector('input[name="status_message"]'); + roster_filter.value = ''; + }, + + onFormSubmitted(ev) { + ev.preventDefault(); + const data = new FormData(ev.target); + this.model.save({ + 'status_message': data.get('status_message'), + 'status': data.get('chat_status') + }); + this.modal.hide(); + } + + }); + _converse.XMPPStatusView = _converse.VDOMViewWithAvatar.extend({ + tagName: "div", + events: { + "click a.show-profile": "showProfileModal", + "click a.change-status": "showStatusChangeModal", + "click .logout": "logOut" + }, + + initialize() { + this.model.on("change", this.render, this); + this.model.vcard.on("change", this.render, this); + }, + + toHTML() { + const chat_status = this.model.get('status') || 'offline'; + return tpl_profile_view(_.extend(this.model.toJSON(), this.model.vcard.toJSON(), { + '__': __, + 'fullname': this.model.vcard.get('fullname') || _converse.bare_jid, + 'status_message': this.model.get('status_message') || __("I am %1$s", this.getPrettyStatus(chat_status)), + 'chat_status': chat_status, + '_converse': _converse, + 'title_change_settings': __('Change settings'), + 'title_change_status': __('Click to change your chat status'), + 'title_log_out': __('Log out'), + 'title_your_profile': __('Your profile') + })); + }, + + afterRender() { + this.renderAvatar(); + }, + + showProfileModal(ev) { + if (_.isUndefined(this.profile_modal)) { + this.profile_modal = new _converse.ProfileModal({ + model: this.model + }); + } + + this.profile_modal.show(ev); + }, + + showStatusChangeModal(ev) { + if (_.isUndefined(this.status_modal)) { + this.status_modal = new _converse.ChatStatusModal({ + model: this.model + }); + } + + this.status_modal.show(ev); + }, + + logOut(ev) { + ev.preventDefault(); + const result = confirm(__("Are you sure you want to log out?")); + + if (result === true) { + _converse.logOut(); + } + }, + + getPrettyStatus(stat) { + if (stat === 'chat') { + return __('online'); + } else if (stat === 'dnd') { + return __('busy'); + } else if (stat === 'xa') { + return __('away for long'); + } else if (stat === 'away') { + return __('away'); + } else if (stat === 'offline') { + return __('offline'); + } else { + return __(stat) || __('online'); + } + } + + }); + } + + }); +}); + +/***/ }), + +/***/ "./src/converse-push.js": +/*!******************************!*\ + !*** ./src/converse-push.js ***! + \******************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;// Converse.js +// https://conversejs.org +// +// Copyright (c) 2013-2018, the Converse.js developers +// Licensed under the Mozilla Public License (MPLv2) + +/* This is a Converse.js plugin which add support for registering + * an "App Server" as defined in XEP-0357 + */ +(function (root, factory) { + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! @converse/headless/converse-core */ "./src/headless/converse-core.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), + __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? + (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), + __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); +})(this, function (converse) { + "use strict"; + + const _converse$env = converse.env, + Strophe = _converse$env.Strophe, + $iq = _converse$env.$iq, + _ = _converse$env._; + Strophe.addNamespace('PUSH', 'urn:xmpp:push:0'); + converse.plugins.add('converse-push', { + initialize() { + /* The initialize function gets called as soon as the plugin is + * loaded by converse.js's plugin machinery. + */ + const _converse = this._converse, + __ = _converse.__; + + _converse.api.settings.update({ + 'push_app_servers': [], + 'enable_muc_push': false + }); + + async function disablePushAppServer(domain, push_app_server) { + if (!push_app_server.jid) { + return; + } + + const result = await _converse.api.disco.supports(Strophe.NS.PUSH, domain || _converse.bare_jid); + + if (!result.length) { + return _converse.log(`Not disabling push app server "${push_app_server.jid}", no disco support from your server.`, Strophe.LogLevel.WARN); + } + + const stanza = $iq({ + 'type': 'set' + }); + + if (domain !== _converse.bare_jid) { + stanza.attrs({ + 'to': domain + }); + } + + stanza.c('disable', { + 'xmlns': Strophe.NS.PUSH, + 'jid': push_app_server.jid + }); + + if (push_app_server.node) { + stanza.attrs({ + 'node': push_app_server.node + }); + } + + _converse.api.sendIQ(stanza).catch(e => { + _converse.log(`Could not disable push app server for ${push_app_server.jid}`, Strophe.LogLevel.ERROR); + + _converse.log(e, Strophe.LogLevel.ERROR); + }); + } + + async function enablePushAppServer(domain, push_app_server) { + if (!push_app_server.jid || !push_app_server.node) { + return; + } + + const identity = await _converse.api.disco.getIdentity('pubsub', 'push', push_app_server.jid); + + if (!identity) { + return _converse.log(`Not enabling push the service "${push_app_server.jid}", it doesn't have the right disco identtiy.`, Strophe.LogLevel.WARN); + } + + const result = await Promise.all([_converse.api.disco.supports(Strophe.NS.PUSH, push_app_server.jid), _converse.api.disco.supports(Strophe.NS.PUSH, domain)]); + + if (!result[0].length && !result[1].length) { + return _converse.log(`Not enabling push app server "${push_app_server.jid}", no disco support from your server.`, Strophe.LogLevel.WARN); + } + + const stanza = $iq({ + 'type': 'set' + }); + + if (domain !== _converse.bare_jid) { + stanza.attrs({ + 'to': domain + }); + } + + stanza.c('enable', { + 'xmlns': Strophe.NS.PUSH, + 'jid': push_app_server.jid, + 'node': push_app_server.node + }); + + if (push_app_server.secret) { + stanza.c('x', { + 'xmlns': Strophe.NS.XFORM, + 'type': 'submit' + }).c('field', { + 'var': 'FORM_TYPE' + }).c('value').t(`${Strophe.NS.PUBSUB}#publish-options`).up().up().c('field', { + 'var': 'secret' + }).c('value').t(push_app_server.secret); + } + + return _converse.api.sendIQ(stanza); + } + + async function enablePush(domain) { + domain = domain || _converse.bare_jid; + const push_enabled = _converse.session.get('push_enabled') || []; + + if (_.includes(push_enabled, domain)) { + return; + } + + const enabled_services = _.reject(_converse.push_app_servers, 'disable'); + + try { + await Promise.all(_.map(enabled_services, _.partial(enablePushAppServer, domain))); + } catch (e) { + _converse.log('Could not enable push App Server', Strophe.LogLevel.ERROR); + + if (e) _converse.log(e, Strophe.LogLevel.ERROR); + } finally { + push_enabled.push(domain); + } + + const disabled_services = _.filter(_converse.push_app_servers, 'disable'); + + _.each(disabled_services, _.partial(disablePushAppServer, domain)); + + _converse.session.save('push_enabled', push_enabled); + } + + _converse.api.listen.on('statusInitialized', () => enablePush()); + + function onChatBoxAdded(model) { + if (model.get('type') == _converse.CHATROOMS_TYPE) { + enablePush(Strophe.getDomainFromJid(model.get('jid'))); + } + } + + if (_converse.enable_muc_push) { + _converse.api.listen.on('chatBoxesInitialized', () => _converse.chatboxes.on('add', onChatBoxAdded)); + } + } + + }); +}); + +/***/ }), + +/***/ "./src/converse-register.js": +/*!**********************************!*\ + !*** ./src/converse-register.js ***! + \**********************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;// Converse.js (A browser based XMPP chat client) +// http://conversejs.org +// +// Copyright (c) 2012-2017, Jan-Carel Brand +// Licensed under the Mozilla Public License (MPLv2) +// + +/*global define */ + +/* This is a Converse.js plugin which add support for in-band registration + * as specified in XEP-0077. + */ +(function (root, factory) { + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! utils/form */ "./src/headless/utils/form.js"), __webpack_require__(/*! @converse/headless/converse-core */ "./src/headless/converse-core.js"), __webpack_require__(/*! templates/form_username.html */ "./src/templates/form_username.html"), __webpack_require__(/*! templates/register_link.html */ "./src/templates/register_link.html"), __webpack_require__(/*! templates/register_panel.html */ "./src/templates/register_panel.html"), __webpack_require__(/*! templates/registration_form.html */ "./src/templates/registration_form.html"), __webpack_require__(/*! templates/registration_request.html */ "./src/templates/registration_request.html"), __webpack_require__(/*! templates/form_input.html */ "./src/templates/form_input.html"), __webpack_require__(/*! templates/spinner.html */ "./src/templates/spinner.html"), __webpack_require__(/*! converse-controlbox */ "./src/converse-controlbox.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), + __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? + (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), + __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); +})(this, function (utils, converse, tpl_form_username, tpl_register_link, tpl_register_panel, tpl_registration_form, tpl_registration_request, tpl_form_input, tpl_spinner) { + "use strict"; // Strophe methods for building stanzas + + const _converse$env = converse.env, + Strophe = _converse$env.Strophe, + Backbone = _converse$env.Backbone, + sizzle = _converse$env.sizzle, + $iq = _converse$env.$iq, + _ = _converse$env._; // Add Strophe Namespaces + + Strophe.addNamespace('REGISTER', 'jabber:iq:register'); // Add Strophe Statuses + + let i = 0; + + _.each(_.keys(Strophe.Status), 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; + converse.plugins.add('converse-register', { + 'overrides': { + // Overrides mentioned here will be picked up by converse.js's + // plugin architecture they will replace existing methods on the + // relevant objects or classes. + // + // New functions which don't exist yet can also be added. + LoginPanel: { + insertRegisterLink() { + const _converse = this.__super__._converse; + + if (_.isUndefined(this.registerlinkview)) { + this.registerlinkview = new _converse.RegisterLinkView({ + 'model': this.model + }); + this.registerlinkview.render(); + this.el.querySelector('.buttons').insertAdjacentElement('afterend', this.registerlinkview.el); + } + + this.registerlinkview.render(); + }, + + render(cfg) { + const _converse = this.__super__._converse; + + this.__super__.render.apply(this, arguments); + + if (_converse.allow_registration && !_converse.auto_login) { + this.insertRegisterLink(); + } + + return this; + } + + }, + ControlBoxView: { + initialize() { + this.__super__.initialize.apply(this, arguments); + + this.model.on('change:active-form', this.showLoginOrRegisterForm.bind(this)); + }, + + showLoginOrRegisterForm() { + const _converse = this.__super__._converse; + + if (_.isNil(this.registerpanel)) { + return; + } + + if (this.model.get('active-form') == "register") { + this.loginpanel.el.classList.add('hidden'); + this.registerpanel.el.classList.remove('hidden'); + } else { + this.loginpanel.el.classList.remove('hidden'); + this.registerpanel.el.classList.add('hidden'); + } + }, + + renderRegistrationPanel() { + const _converse = this.__super__._converse; + + if (_converse.allow_registration) { + this.registerpanel = new _converse.RegisterPanel({ + 'model': this.model + }); + this.registerpanel.render(); + this.registerpanel.el.classList.add('hidden'); + this.el.querySelector('#converse-login-panel').insertAdjacentElement('afterend', this.registerpanel.el); + this.showLoginOrRegisterForm(); + } + + return this; + }, + + renderLoginPanel() { + /* Also render a registration panel, when rendering the + * login panel. + */ + this.__super__.renderLoginPanel.apply(this, arguments); + + this.renderRegistrationPanel(); + return this; + } + + } + }, + + initialize() { + /* The initialize function gets called as soon as the plugin is + * loaded by converse.js's plugin machinery. + */ + const _converse = this._converse, + __ = _converse.__; + _converse.CONNECTION_STATUS[Strophe.Status.REGIFAIL] = 'REGIFAIL'; + _converse.CONNECTION_STATUS[Strophe.Status.REGISTERED] = 'REGISTERED'; + _converse.CONNECTION_STATUS[Strophe.Status.CONFLICT] = 'CONFLICT'; + _converse.CONNECTION_STATUS[Strophe.Status.NOTACCEPTABLE] = 'NOTACCEPTABLE'; + + _converse.api.settings.update({ + 'allow_registration': true, + 'domain_placeholder': __(" e.g. conversejs.org"), + // Placeholder text shown in the domain input on the registration form + 'providers_link': 'https://compliance.conversations.im/', + // Link to XMPP providers shown on registration page + 'registration_domain': '' + }); + + function setActiveForm(value) { + _converse.api.waitUntil('controlboxInitialized').then(() => { + const controlbox = _converse.chatboxes.get('controlbox'); + + controlbox.set({ + 'active-form': value + }); + }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); + } + + _converse.router.route('converse/login', _.partial(setActiveForm, 'login')); + + _converse.router.route('converse/register', _.partial(setActiveForm, 'register')); + + _converse.RegisterLinkView = Backbone.VDOMView.extend({ + toHTML() { + return tpl_register_link(_.extend(this.model.toJSON(), { + '__': _converse.__, + '_converse': _converse, + 'connection_status': _converse.connfeedback.get('connection_status') + })); + } + + }); + _converse.RegisterPanel = Backbone.NativeView.extend({ + tagName: 'div', + id: "converse-register-panel", + className: 'controlbox-pane fade-in', + events: { + 'submit form#converse-register': 'onFormSubmission', + 'click .button-cancel': 'renderProviderChoiceForm' + }, + + initialize(cfg) { + this.reset(); + this.registerHooks(); + }, + + render() { + this.model.set('registration_form_rendered', false); + this.el.innerHTML = tpl_register_panel({ + '__': __, + 'default_domain': _converse.registration_domain, + 'label_register': __('Fetch registration form'), + 'help_providers': __('Tip: A list of public XMPP providers is available'), + 'help_providers_link': __('here'), + 'href_providers': _converse.providers_link, + 'domain_placeholder': _converse.domain_placeholder + }); + + if (_converse.registration_domain) { + this.fetchRegistrationForm(_converse.registration_domain); + } + + return this; + }, + + registerHooks() { + /* Hook into Strophe's _connect_cb, so that we can send an IQ + * requesting the registration fields. + */ + const conn = _converse.connection; + + const connect_cb = conn._connect_cb.bind(conn); + + conn._connect_cb = (req, callback, raw) => { + if (!this._registering) { + connect_cb(req, callback, raw); + } else { + if (this.getRegistrationFields(req, callback, raw)) { + this._registering = false; + } + } + }; + }, + + getRegistrationFields(req, _callback, raw) { + /* Send an IQ stanza to the XMPP server asking for the + * registration fields. + * Parameters: + * (Strophe.Request) req - The current request + * (Function) callback + */ + const conn = _converse.connection; + conn.connected = true; + + const body = conn._proto._reqToData(req); + + if (!body) { + return; + } + + if (conn._proto._connect_cb(body) === Strophe.Status.CONNFAIL) { + this.showValidationError(__("Sorry, we're unable to connect to your chosen provider.")); + return false; + } + + const register = body.getElementsByTagName("register"); + const mechanisms = body.getElementsByTagName("mechanism"); + + if (register.length === 0 && mechanisms.length === 0) { + conn._proto._no_auth_received(_callback); + + return false; + } + + if (register.length === 0) { + conn._changeConnectStatus(Strophe.Status.REGIFAIL); + + this.showValidationError(__("Sorry, the given provider does not support in " + "band account registration. Please try with a " + "different provider.")); + return true; + } // Send an IQ stanza to get all required data fields + + + conn._addSysHandler(this.onRegistrationFields.bind(this), null, "iq", null, null); + + const stanza = $iq({ + type: "get" + }).c("query", { + xmlns: Strophe.NS.REGISTER + }).tree(); + stanza.setAttribute("id", conn.getUniqueId("sendIQ")); + conn.send(stanza); + conn.connected = false; + return true; + }, + + onRegistrationFields(stanza) { + /* Handler for Registration Fields Request. + * + * Parameters: + * (XMLElement) elem - The query stanza. + */ + if (stanza.getAttribute("type") === "error") { + _converse.connection._changeConnectStatus(Strophe.Status.REGIFAIL, __('Something went wrong while establishing a connection with "%1$s". ' + 'Are you sure it exists?', this.domain)); + + return false; + } + + if (stanza.getElementsByTagName("query").length !== 1) { + _converse.connection._changeConnectStatus(Strophe.Status.REGIFAIL, "unknown"); + + return false; + } + + this.setFields(stanza); + + if (!this.model.get('registration_form_rendered')) { + this.renderRegistrationForm(stanza); + } + + return false; + }, + + reset(settings) { + const defaults = { + fields: {}, + urls: [], + title: "", + instructions: "", + registered: false, + _registering: false, + domain: null, + form_type: null + }; + + _.extend(this, defaults); + + if (settings) { + _.extend(this, _.pick(settings, _.keys(defaults))); + } + }, + + onFormSubmission(ev) { + /* Event handler when the #converse-register form is + * submitted. + * + * Depending on the available input fields, we delegate to + * other methods. + */ + if (ev && ev.preventDefault) { + ev.preventDefault(); + } + + if (_.isNull(ev.target.querySelector('input[name=domain]'))) { + this.submitRegistrationForm(ev.target); + } else { + this.onProviderChosen(ev.target); + } + }, + + onProviderChosen(form) { + /* Callback method that gets called when the user has chosen an + * XMPP provider. + * + * Parameters: + * (HTMLElement) form - The form that was submitted + */ + const domain_input = form.querySelector('input[name=domain]'), + domain = _.get(domain_input, 'value'); + + if (!domain) { + // TODO: add validation message + domain_input.classList.add('error'); + return; + } + + form.querySelector('input[type=submit]').classList.add('hidden'); + this.fetchRegistrationForm(domain.trim()); + }, + + fetchRegistrationForm(domain_name) { + /* This is called with a domain name based on which, it fetches a + * registration form from the requested domain. + * + * Parameters: + * (String) domain_name - XMPP server domain + */ + if (!this.model.get('registration_form_rendered')) { + this.renderRegistrationRequest(); + } + + this.reset({ + 'domain': Strophe.getDomainFromJid(domain_name), + '_registering': true + }); + + _converse.connection.connect(this.domain, "", this.onConnectStatusChanged.bind(this)); + + return false; + }, + + renderRegistrationRequest() { + /* Clear the form and inform the user that the registration + * form is being fetched. + */ + this.clearRegistrationForm().insertAdjacentHTML('beforeend', tpl_registration_request({ + '__': _converse.__, + 'cancel': _converse.registration_domain + })); + }, + + giveFeedback(message, klass) { + let feedback = this.el.querySelector('.reg-feedback'); + + if (!_.isNull(feedback)) { + feedback.parentNode.removeChild(feedback); + } + + const form = this.el.querySelector('form'); + form.insertAdjacentHTML('afterbegin', ''); + feedback = form.querySelector('.reg-feedback'); + feedback.textContent = message; + + if (klass) { + feedback.classList.add(klass); + } + }, + + clearRegistrationForm() { + const form = this.el.querySelector('form'); + form.innerHTML = ''; + this.model.set('registration_form_rendered', false); + return form; + }, + + showSpinner() { + const form = this.el.querySelector('form'); + form.innerHTML = tpl_spinner(); + this.model.set('registration_form_rendered', false); + return this; + }, + + onConnectStatusChanged(status_code) { + /* Callback function called by Strophe whenever the + * connection status changes. + * + * Passed to Strophe specifically during a registration + * attempt. + * + * Parameters: + * (Integer) status_code - The Stroph.Status status code + */ + _converse.log('converse-register: onConnectStatusChanged'); + + if (_.includes([Strophe.Status.DISCONNECTED, Strophe.Status.CONNFAIL, Strophe.Status.REGIFAIL, Strophe.Status.NOTACCEPTABLE, Strophe.Status.CONFLICT], status_code)) { + _converse.log(`Problem during registration: Strophe.Status is ${_converse.CONNECTION_STATUS[status_code]}`, Strophe.LogLevel.ERROR); + + this.abortRegistration(); + } else if (status_code === Strophe.Status.REGISTERED) { + _converse.log("Registered successfully."); + + _converse.connection.reset(); + + this.showSpinner(); + + if (_.includes(["converse/login", "converse/register"], Backbone.history.getFragment())) { + _converse.router.navigate('', { + 'replace': true + }); + } + + if (this.fields.password && this.fields.username) { + // automatically log the user in + _converse.connection.connect(this.fields.username.toLowerCase() + '@' + this.domain.toLowerCase(), this.fields.password, _converse.onConnectStatusChanged); + + this.giveFeedback(__('Now logging you in'), 'info'); + } else { + _converse.chatboxviews.get('controlbox').renderLoginPanel(); + + _converse.giveFeedback(__('Registered successfully')); + } + + this.reset(); + } + }, + + renderLegacyRegistrationForm(form) { + _.each(_.keys(this.fields), key => { + if (key === "username") { + form.insertAdjacentHTML('beforeend', tpl_form_username({ + 'domain': ` @${this.domain}`, + 'name': key, + 'type': "text", + 'label': key, + 'value': '', + 'required': true + })); + } else { + form.insertAdjacentHTML('beforeend', tpl_form_input({ + 'label': key, + 'name': key, + 'placeholder': key, + 'required': true, + 'type': key === 'password' || key === 'email' ? key : "text", + 'value': '' + })); + } + }); // Show urls + + + _.each(this.urls, url => { + form.insertAdjacentHTML('afterend', '' + url + ''); + }); + }, + + renderRegistrationForm(stanza) { + /* Renders the registration form based on the XForm fields + * received from the XMPP server. + * + * Parameters: + * (XMLElement) stanza - The IQ stanza received from the XMPP server. + */ + const form = this.el.querySelector('form'); + form.innerHTML = tpl_registration_form({ + '__': _converse.__, + 'domain': this.domain, + 'title': this.title, + 'instructions': this.instructions, + 'registration_domain': _converse.registration_domain + }); + const buttons = form.querySelector('fieldset.buttons'); + + if (this.form_type === 'xform') { + _.each(stanza.querySelectorAll('field'), field => { + buttons.insertAdjacentHTML('beforebegin', utils.xForm2webForm(field, stanza, this.domain)); + }); + } else { + this.renderLegacyRegistrationForm(form); + } + + if (!this.fields) { + form.querySelector('.button-primary').classList.add('hidden'); + } + + form.classList.remove('hidden'); + this.model.set('registration_form_rendered', true); + }, + + showValidationError(message) { + const form = this.el.querySelector('form'); + let flash = form.querySelector('.form-errors'); + + if (_.isNull(flash)) { + flash = ''; + const instructions = form.querySelector('p.instructions'); + + if (_.isNull(instructions)) { + form.insertAdjacentHTML('afterbegin', flash); + } else { + instructions.insertAdjacentHTML('afterend', flash); + } + + flash = form.querySelector('.form-errors'); + } else { + flash.innerHTML = ''; + } + + flash.insertAdjacentHTML('beforeend', '

' + message + '

'); + flash.classList.remove('hidden'); + }, + + reportErrors(stanza) { + /* Report back to the user any error messages received from the + * XMPP server after attempted registration. + * + * Parameters: + * (XMLElement) stanza - The IQ stanza received from the + * XMPP server. + */ + const errors = stanza.querySelectorAll('error'); + + _.each(errors, error => { + this.showValidationError(error.textContent); + }); + + if (!errors.length) { + const message = __('The provider rejected your registration attempt. ' + 'Please check the values you entered for correctness.'); + + this.showValidationError(message); + } + }, + + renderProviderChoiceForm(ev) { + if (ev && ev.preventDefault) { + ev.preventDefault(); + } + + _converse.connection._proto._abortAllRequests(); + + _converse.connection.reset(); + + this.render(); + }, + + abortRegistration() { + _converse.connection._proto._abortAllRequests(); + + _converse.connection.reset(); + + if (this.model.get('registration_form_rendered')) { + if (_converse.registration_domain && this.model.get('registration_form_rendered')) { + this.fetchRegistrationForm(_converse.registration_domain); + } + } else { + this.render(); + } + }, + + submitRegistrationForm(form) { + /* Handler, when the user submits the registration form. + * Provides form error feedback or starts the registration + * process. + * + * Parameters: + * (HTMLElement) form - The HTML form that was submitted + */ + const has_empty_inputs = _.reduce(this.el.querySelectorAll('input.required'), function (result, input) { + if (input.value === '') { + input.classList.add('error'); + return result + 1; + } + + return result; + }, 0); + + if (has_empty_inputs) { + return; + } + + const inputs = sizzle(':input:not([type=button]):not([type=submit])', form), + iq = $iq({ + 'type': 'set', + 'id': _converse.connection.getUniqueId() + }).c("query", { + xmlns: Strophe.NS.REGISTER + }); + + if (this.form_type === 'xform') { + iq.c("x", { + xmlns: Strophe.NS.XFORM, + type: 'submit' + }); + + _.each(inputs, input => { + iq.cnode(utils.webForm2xForm(input)).up(); + }); + } else { + _.each(inputs, input => { + iq.c(input.getAttribute('name'), {}, input.value); + }); + } + + _converse.connection._addSysHandler(this._onRegisterIQ.bind(this), null, "iq", null, null); + + _converse.connection.send(iq); + + this.setFields(iq.tree()); + }, + + setFields(stanza) { + /* Stores the values that will be sent to the XMPP server + * during attempted registration. + * + * Parameters: + * (XMLElement) stanza - the IQ stanza that will be sent to the XMPP server. + */ + const query = stanza.querySelector('query'); + const xform = sizzle(`x[xmlns="${Strophe.NS.XFORM}"]`, query); + + if (xform.length > 0) { + this._setFieldsFromXForm(xform.pop()); + } else { + this._setFieldsFromLegacy(query); + } + }, + + _setFieldsFromLegacy(query) { + _.each(query.children, field => { + if (field.tagName.toLowerCase() === 'instructions') { + this.instructions = Strophe.getText(field); + return; + } else if (field.tagName.toLowerCase() === 'x') { + if (field.getAttribute('xmlns') === 'jabber:x:oob') { + this.urls.concat(_.map(field.querySelectorAll('url'), 'textContent')); + } + + return; + } + + this.fields[field.tagName.toLowerCase()] = Strophe.getText(field); + }); + + this.form_type = 'legacy'; + }, + + _setFieldsFromXForm(xform) { + this.title = _.get(xform.querySelector('title'), 'textContent'); + this.instructions = _.get(xform.querySelector('instructions'), 'textContent'); + + _.each(xform.querySelectorAll('field'), field => { + const _var = field.getAttribute('var'); + + if (_var) { + this.fields[_var.toLowerCase()] = _.get(field.querySelector('value'), 'textContent', ''); + } else { + // TODO: other option seems to be type="fixed" + _converse.log("Found field we couldn't parse", Strophe.LogLevel.WARN); + } + }); + + this.form_type = 'xform'; + }, + + _onRegisterIQ(stanza) { + /* Callback method that gets called when a return IQ stanza + * is received from the XMPP server, after attempting to + * register a new user. + * + * Parameters: + * (XMLElement) stanza - The IQ stanza. + */ + if (stanza.getAttribute("type") === "error") { + _converse.log("Registration failed.", Strophe.LogLevel.ERROR); + + this.reportErrors(stanza); + let error = stanza.getElementsByTagName("error"); + + if (error.length !== 1) { + _converse.connection._changeConnectStatus(Strophe.Status.REGIFAIL, "unknown"); + + return false; + } + + error = error[0].firstChild.tagName.toLowerCase(); + + if (error === 'conflict') { + _converse.connection._changeConnectStatus(Strophe.Status.CONFLICT, error); + } else if (error === 'not-acceptable') { + _converse.connection._changeConnectStatus(Strophe.Status.NOTACCEPTABLE, error); + } else { + _converse.connection._changeConnectStatus(Strophe.Status.REGIFAIL, error); + } + } else { + _converse.connection._changeConnectStatus(Strophe.Status.REGISTERED, null); + } + + return false; + } + + }); + } + + }); +}); + +/***/ }), + +/***/ "./src/converse-roomslist.js": +/*!***********************************!*\ + !*** ./src/converse-roomslist.js ***! + \***********************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;// Converse.js (A browser based XMPP chat client) +// http://conversejs.org +// +// Copyright (c) 2012-2017, Jan-Carel Brand +// Licensed under the Mozilla Public License (MPLv2) +// + +/*global define */ + +/* This is a non-core Converse.js plugin which shows a list of currently open + * rooms in the "Rooms Panel" of the ControlBox. + */ +(function (root, factory) { + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! @converse/headless/converse-core */ "./src/headless/converse-core.js"), __webpack_require__(/*! @converse/headless/converse-muc */ "./src/headless/converse-muc.js"), __webpack_require__(/*! templates/rooms_list.html */ "./src/templates/rooms_list.html"), __webpack_require__(/*! templates/rooms_list_item.html */ "./src/templates/rooms_list_item.html")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), + __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? + (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), + __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); +})(this, function (converse, muc, tpl_rooms_list, tpl_rooms_list_item) { + const _converse$env = converse.env, + Backbone = _converse$env.Backbone, + Promise = _converse$env.Promise, + Strophe = _converse$env.Strophe, + b64_sha1 = _converse$env.b64_sha1, + sizzle = _converse$env.sizzle, + _ = _converse$env._; + const u = converse.env.utils; + converse.plugins.add('converse-roomslist', { + /* Optional dependencies are other plugins which might be + * overridden or relied upon, and therefore need to be loaded before + * this plugin. They are called "optional" because they might not be + * available, in which case any overrides applicable to them will be + * ignored. + * + * It's possible however to make optional dependencies non-optional. + * If the setting "strict_plugin_dependencies" is set to true, + * an error will be raised if the plugin is not found. + * + * NB: These plugins need to have already been loaded via require.js. + */ + dependencies: ["converse-singleton", "converse-controlbox", "@converse/headless/converse-muc", "converse-bookmarks"], + + initialize() { + /* The initialize function gets called as soon as the plugin is + * loaded by converse.js's plugin machinery. + */ + const _converse = this._converse, + __ = _converse.__; + _converse.OpenRooms = Backbone.Collection.extend({ + comparator(room) { + if (room.get('bookmarked')) { + const bookmark = _.head(_converse.bookmarksview.model.where({ + 'jid': room.get('jid') + })); + + return bookmark.get('name'); + } else { + return room.get('name'); + } + }, + + initialize() { + _converse.chatboxes.on('add', this.onChatBoxAdded, this); + + _converse.chatboxes.on('change:hidden', this.onChatBoxChanged, this); + + _converse.chatboxes.on('change:bookmarked', this.onChatBoxChanged, this); + + _converse.chatboxes.on('change:name', this.onChatBoxChanged, this); + + _converse.chatboxes.on('change:num_unread', this.onChatBoxChanged, this); + + _converse.chatboxes.on('change:num_unread_general', this.onChatBoxChanged, this); + + _converse.chatboxes.on('remove', this.onChatBoxRemoved, this); + + this.reset(_.map(_converse.chatboxes.where({ + 'type': 'chatroom' + }), 'attributes')); + }, + + onChatBoxAdded(item) { + if (item.get('type') === 'chatroom') { + this.create(item.attributes); + } + }, + + onChatBoxChanged(item) { + if (item.get('type') === 'chatroom') { + const room = this.get(item.get('jid')); + + if (!_.isNil(room)) { + room.set(item.attributes); + } + } + }, + + onChatBoxRemoved(item) { + if (item.get('type') === 'chatroom') { + const room = this.get(item.get('jid')); + this.remove(room); + } + } + + }); + _converse.RoomsList = Backbone.Model.extend({ + defaults: { + "toggle-state": _converse.OPENED + } + }); + _converse.RoomsListElementView = Backbone.VDOMView.extend({ + events: { + 'click .room-info': 'showRoomDetailsModal' + }, + + initialize() { + this.model.on('destroy', this.remove, this); + this.model.on('remove', this.remove, this); + this.model.on('change:bookmarked', this.render, this); + this.model.on('change:hidden', this.render, this); + this.model.on('change:name', this.render, this); + this.model.on('change:num_unread', this.render, this); + this.model.on('change:num_unread_general', this.render, this); + }, + + toHTML() { + return tpl_rooms_list_item(_.extend(this.model.toJSON(), { + // XXX: By the time this renders, the _converse.bookmarks + // collection should already exist if bookmarks are + // supported by the XMPP server. So we can use it + // as a check for support (other ways of checking are async). + 'allow_bookmarks': _converse.allow_bookmarks && _converse.bookmarks, + 'currently_open': _converse.isSingleton() && !this.model.get('hidden'), + 'info_leave_room': __('Leave this groupchat'), + 'info_remove_bookmark': __('Unbookmark this groupchat'), + 'info_add_bookmark': __('Bookmark this groupchat'), + 'info_title': __('Show more information on this groupchat'), + 'name': this.getRoomsListElementName(), + 'open_title': __('Click to open this groupchat') + })); + }, + + showRoomDetailsModal(ev) { + const room = _converse.chatboxes.get(this.model.get('jid')); + + ev.preventDefault(); + + if (_.isUndefined(room.room_details_modal)) { + room.room_details_modal = new _converse.RoomDetailsModal({ + 'model': room + }); + } + + room.room_details_modal.show(ev); + }, + + getRoomsListElementName() { + if (this.model.get('bookmarked') && _converse.bookmarksview) { + const bookmark = _.head(_converse.bookmarksview.model.where({ + 'jid': this.model.get('jid') + })); + + return bookmark.get('name'); + } else { + return this.model.get('name'); + } + } + + }); + _converse.RoomsListView = Backbone.OrderedListView.extend({ + tagName: 'div', + className: 'open-rooms-list list-container rooms-list-container', + events: { + 'click .add-bookmark': 'addBookmark', + 'click .close-room': 'closeRoom', + 'click .list-toggle': 'toggleRoomsList', + 'click .remove-bookmark': 'removeBookmark', + 'click .open-room': 'openRoom' + }, + listSelector: '.rooms-list', + ItemView: _converse.RoomsListElementView, + subviewIndex: 'jid', + + initialize() { + Backbone.OrderedListView.prototype.initialize.apply(this, arguments); + this.model.on('add', this.showOrHide, this); + this.model.on('remove', this.showOrHide, this); + + const storage = _converse.config.get('storage'), + id = b64_sha1(`converse.roomslist${_converse.bare_jid}`); + + this.list_model = new _converse.RoomsList({ + 'id': id + }); + this.list_model.browserStorage = new Backbone.BrowserStorage[storage](id); + this.list_model.fetch(); + this.render(); + this.sortAndPositionAllItems(); + }, + + render() { + this.el.innerHTML = tpl_rooms_list({ + 'toggle_state': this.list_model.get('toggle-state'), + 'desc_rooms': __('Click to toggle the list of open groupchats'), + 'label_rooms': __('Open Groupchats'), + '_converse': _converse + }); + + if (this.list_model.get('toggle-state') !== _converse.OPENED) { + this.el.querySelector('.open-rooms-list').classList.add('collapsed'); + } + + this.showOrHide(); + this.insertIntoControlBox(); + return this; + }, + + insertIntoControlBox() { + const controlboxview = _converse.chatboxviews.get('controlbox'); + + if (!_.isUndefined(controlboxview) && !u.rootContains(_converse.root, this.el)) { + const el = controlboxview.el.querySelector('.open-rooms-list'); + + if (!_.isNull(el)) { + el.parentNode.replaceChild(this.el, el); + } + } + }, + + hide() { + u.hideElement(this.el); + }, + + show() { + u.showElement(this.el); + }, + + openRoom(ev) { + ev.preventDefault(); + const name = ev.target.textContent; + const jid = ev.target.getAttribute('data-room-jid'); + const data = { + 'name': name || Strophe.unescapeNode(Strophe.getNodeFromJid(jid)) || jid + }; + + _converse.api.rooms.open(jid, data); + }, + + closeRoom(ev) { + ev.preventDefault(); + const name = ev.target.getAttribute('data-room-name'); + const jid = ev.target.getAttribute('data-room-jid'); + + if (confirm(__("Are you sure you want to leave the groupchat %1$s?", name))) { + // TODO: replace with API call + _converse.chatboxviews.get(jid).close(); + } + }, + + showOrHide(item) { + if (!this.model.models.length) { + u.hideElement(this.el); + } else { + u.showElement(this.el); + } + }, + + removeBookmark: _converse.removeBookmarkViaEvent, + addBookmark: _converse.addBookmarkViaEvent, + + toggleRoomsList(ev) { + if (ev && ev.preventDefault) { + ev.preventDefault(); + } + + const icon_el = ev.target.querySelector('.fa'); + + if (icon_el.classList.contains("fa-caret-down")) { + u.slideIn(this.el.querySelector('.open-rooms-list')).then(() => { + this.list_model.save({ + 'toggle-state': _converse.CLOSED + }); + icon_el.classList.remove("fa-caret-down"); + icon_el.classList.add("fa-caret-right"); + }); + } else { + u.slideOut(this.el.querySelector('.open-rooms-list')).then(() => { + this.list_model.save({ + 'toggle-state': _converse.OPENED + }); + icon_el.classList.remove("fa-caret-right"); + icon_el.classList.add("fa-caret-down"); + }); + } + } + + }); + + const initRoomsListView = function initRoomsListView() { + const storage = _converse.config.get('storage'), + id = b64_sha1(`converse.open-rooms-{_converse.bare_jid}`), + model = new _converse.OpenRooms(); + + model.browserStorage = new Backbone.BrowserStorage[storage](id); + _converse.rooms_list_view = new _converse.RoomsListView({ + 'model': model + }); + }; + + if (_converse.allow_bookmarks) { + u.onMultipleEvents([{ + 'object': _converse, + 'event': 'chatBoxesFetched' + }, { + 'object': _converse, + 'event': 'roomsPanelRendered' + }, { + 'object': _converse, + 'event': 'bookmarksInitialized' + }], initRoomsListView); + } else { + u.onMultipleEvents([{ + 'object': _converse, + 'event': 'chatBoxesFetched' + }, { + 'object': _converse, + 'event': 'roomsPanelRendered' + }], initRoomsListView); + } + + _converse.api.listen.on('reconnected', initRoomsListView); + } + + }); +}); + +/***/ }), + +/***/ "./src/converse-roster.js": +/*!********************************!*\ + !*** ./src/converse-roster.js ***! + \********************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;// Converse.js +// http://conversejs.org +// +// Copyright (c) 2012-2018, the Converse.js developers +// Licensed under the Mozilla Public License (MPLv2) +(function (root, factory) { + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! @converse/headless/converse-core */ "./src/headless/converse-core.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), + __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? + (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), + __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); +})(this, function (converse) { + "use strict"; + + const _converse$env = converse.env, + Backbone = _converse$env.Backbone, + Promise = _converse$env.Promise, + Strophe = _converse$env.Strophe, + $iq = _converse$env.$iq, + $pres = _converse$env.$pres, + b64_sha1 = _converse$env.b64_sha1, + moment = _converse$env.moment, + sizzle = _converse$env.sizzle, + _ = _converse$env._; + const u = converse.env.utils; + converse.plugins.add('converse-roster', { + dependencies: ["converse-vcard"], + + initialize() { + /* The initialize function gets called as soon as the plugin is + * loaded by converse.js's plugin machinery. + */ + const _converse = this._converse, + __ = _converse.__; + + _converse.api.settings.update({ + 'allow_contact_requests': true, + 'auto_subscribe': false, + 'synchronize_availability': true + }); + + _converse.api.promises.add(['cachedRoster', 'roster', 'rosterContactsFetched', 'rosterGroupsFetched', 'rosterInitialized']); + + _converse.registerPresenceHandler = function () { + _converse.unregisterPresenceHandler(); + + _converse.presence_ref = _converse.connection.addHandler(function (presence) { + _converse.roster.presenceHandler(presence); + + return true; + }, null, 'presence', null); + }; + + _converse.initRoster = function () { + /* Initialize the Bakcbone collections that represent the contats + * roster and the roster groups. + */ + const storage = _converse.config.get('storage'); + + _converse.roster = new _converse.RosterContacts(); + _converse.roster.browserStorage = new Backbone.BrowserStorage[storage](b64_sha1(`converse.contacts-${_converse.bare_jid}`)); + _converse.roster.data = new Backbone.Model(); + const id = b64_sha1(`converse-roster-model-${_converse.bare_jid}`); + _converse.roster.data.id = id; + _converse.roster.data.browserStorage = new Backbone.BrowserStorage[storage](id); + + _converse.roster.data.fetch(); + + _converse.rostergroups = new _converse.RosterGroups(); + _converse.rostergroups.browserStorage = new Backbone.BrowserStorage[storage](b64_sha1(`converse.roster.groups${_converse.bare_jid}`)); + + _converse.emit('rosterInitialized'); + }; + + _converse.populateRoster = function (ignore_cache = false) { + /* Fetch all the roster groups, and then the roster contacts. + * Emit an event after fetching is done in each case. + * + * Parameters: + * (Bool) ignore_cache - If set to to true, the local cache + * will be ignored it's guaranteed that the XMPP server + * will be queried for the roster. + */ + if (ignore_cache) { + _converse.send_initial_presence = true; + + _converse.roster.fetchFromServer().then(() => { + _converse.emit('rosterContactsFetched'); + + _converse.sendInitialPresence(); + }).catch(reason => { + _converse.log(reason, Strophe.LogLevel.ERROR); + + _converse.sendInitialPresence(); + }); + } else { + _converse.rostergroups.fetchRosterGroups().then(() => { + _converse.emit('rosterGroupsFetched'); + + return _converse.roster.fetchRosterContacts(); + }).then(() => { + _converse.emit('rosterContactsFetched'); + + _converse.sendInitialPresence(); + }).catch(reason => { + _converse.log(reason, Strophe.LogLevel.ERROR); + + _converse.sendInitialPresence(); + }); + } + }; + + _converse.Presence = Backbone.Model.extend({ + defaults() { + return { + 'show': 'offline', + 'resources': {} + }; + }, + + getHighestPriorityResource() { + /* Return the resource with the highest priority. + * + * If multiple resources have the same priority, take the + * latest one. + */ + const resources = this.get('resources'); + + if (_.isObject(resources) && _.size(resources)) { + const val = _.flow(_.values, _.partial(_.sortBy, _, ['priority', 'timestamp']), _.reverse)(resources)[0]; + + if (!_.isUndefined(val)) { + return val; + } + } + }, + + addResource(presence) { + /* Adds a new resource and it's associated attributes as taken + * from the passed in presence stanza. + * + * Also updates the presence if the resource has higher priority (and is newer). + */ + const jid = presence.getAttribute('from'), + show = _.propertyOf(presence.querySelector('show'))('textContent') || 'online', + resource = Strophe.getResourceFromJid(jid), + delay = sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, presence).pop(), + timestamp = _.isNil(delay) ? moment().format() : moment(delay.getAttribute('stamp')).format(); + let priority = _.propertyOf(presence.querySelector('priority'))('textContent') || 0; + priority = _.isNaN(parseInt(priority, 10)) ? 0 : parseInt(priority, 10); + const resources = _.isObject(this.get('resources')) ? this.get('resources') : {}; + resources[resource] = { + 'name': resource, + 'priority': priority, + 'show': show, + 'timestamp': timestamp + }; + const changed = { + 'resources': resources + }; + const hpr = this.getHighestPriorityResource(); + + if (priority == hpr.priority && timestamp == hpr.timestamp) { + // Only set the "global" presence if this is the newest resource + // with the highest priority + changed.show = show; + } + + this.save(changed); + return resources; + }, + + removeResource(resource) { + /* Remove the passed in resource from the resources map. + * + * Also redetermines the presence given that there's one less + * resource. + */ + let resources = this.get('resources'); + + if (!_.isObject(resources)) { + resources = {}; + } else { + delete resources[resource]; + } + + this.save({ + 'resources': resources, + 'show': _.propertyOf(this.getHighestPriorityResource())('show') || 'offline' + }); + } + + }); + _converse.Presences = Backbone.Collection.extend({ + model: _converse.Presence + }); + _converse.ModelWithVCardAndPresence = Backbone.Model.extend({ + initialize() { + this.setVCard(); + this.setPresence(); + }, + + setVCard() { + const jid = this.get('jid'); + this.vcard = _converse.vcards.findWhere({ + 'jid': jid + }) || _converse.vcards.create({ + 'jid': jid + }); + }, + + setPresence() { + const jid = this.get('jid'); + this.presence = _converse.presences.findWhere({ + 'jid': jid + }) || _converse.presences.create({ + 'jid': jid + }); + } + + }); + _converse.RosterContact = _converse.ModelWithVCardAndPresence.extend({ + defaults: { + 'chat_state': undefined, + 'image': _converse.DEFAULT_IMAGE, + 'image_type': _converse.DEFAULT_IMAGE_TYPE, + 'num_unread': 0, + 'status': '' + }, + + initialize(attributes) { + _converse.ModelWithVCardAndPresence.prototype.initialize.apply(this, arguments); + + const jid = attributes.jid, + bare_jid = Strophe.getBareJidFromJid(jid).toLowerCase(), + resource = Strophe.getResourceFromJid(jid); + attributes.jid = bare_jid; + this.set(_.assignIn({ + 'groups': [], + 'id': bare_jid, + 'jid': bare_jid, + 'user_id': Strophe.getNodeFromJid(jid) + }, attributes)); + this.setChatBox(); + this.presence.on('change:show', () => _converse.emit('contactPresenceChanged', this)); + this.presence.on('change:show', () => this.trigger('presenceChanged')); + }, + + setChatBox(chatbox = null) { + chatbox = chatbox || _converse.chatboxes.get(this.get('jid')); + + if (chatbox) { + this.chatbox = chatbox; + this.chatbox.on('change:hidden', this.render, this); + } + }, + + getDisplayName() { + return this.get('nickname') || this.vcard.get('nickname') || this.vcard.get('fullname') || this.get('jid'); + }, + + getFullname() { + return this.vcard.get('fullname'); + }, + + subscribe(message) { + /* Send a presence subscription request to this roster contact + * + * Parameters: + * (String) message - An optional message to explain the + * reason for the subscription request. + */ + const pres = $pres({ + to: this.get('jid'), + type: "subscribe" + }); + + if (message && message !== "") { + pres.c("status").t(message).up(); + } + + const nick = _converse.xmppstatus.vcard.get('nickname') || _converse.xmppstatus.vcard.get('fullname'); + + if (nick) { + pres.c('nick', { + 'xmlns': Strophe.NS.NICK + }).t(nick).up(); + } + + _converse.connection.send(pres); + + this.save('ask', "subscribe"); // ask === 'subscribe' Means we have asked to subscribe to them. + + return this; + }, + + ackSubscribe() { + /* Upon receiving the presence stanza of type "subscribed", + * the user SHOULD acknowledge receipt of that subscription + * state notification by sending a presence stanza of type + * "subscribe" to the contact + */ + _converse.connection.send($pres({ + 'type': 'subscribe', + 'to': this.get('jid') + })); + }, + + ackUnsubscribe() { + /* Upon receiving the presence stanza of type "unsubscribed", + * the user SHOULD acknowledge receipt of that subscription state + * notification by sending a presence stanza of type "unsubscribe" + * this step lets the user's server know that it MUST no longer + * send notification of the subscription state change to the user. + * Parameters: + * (String) jid - The Jabber ID of the user who is unsubscribing + */ + _converse.connection.send($pres({ + 'type': 'unsubscribe', + 'to': this.get('jid') + })); + + this.removeFromRoster(); + this.destroy(); + }, + + unauthorize(message) { + /* Unauthorize this contact's presence subscription + * Parameters: + * (String) message - Optional message to send to the person being unauthorized + */ + _converse.rejectPresenceSubscription(this.get('jid'), message); + + return this; + }, + + authorize(message) { + /* Authorize presence subscription + * Parameters: + * (String) message - Optional message to send to the person being authorized + */ + const pres = $pres({ + 'to': this.get('jid'), + 'type': "subscribed" + }); + + if (message && message !== "") { + pres.c("status").t(message); + } + + _converse.connection.send(pres); + + return this; + }, + + removeFromRoster(callback, errback) { + /* Instruct the XMPP server to remove this contact from our roster + * Parameters: + * (Function) callback + */ + const iq = $iq({ + type: 'set' + }).c('query', { + xmlns: Strophe.NS.ROSTER + }).c('item', { + jid: this.get('jid'), + subscription: "remove" + }); + + _converse.connection.sendIQ(iq, callback, errback); + + return this; + } + + }); + _converse.RosterContacts = Backbone.Collection.extend({ + model: _converse.RosterContact, + + comparator(contact1, contact2) { + const status1 = contact1.presence.get('show') || 'offline'; + const status2 = contact2.presence.get('show') || 'offline'; + + if (_converse.STATUS_WEIGHTS[status1] === _converse.STATUS_WEIGHTS[status2]) { + const name1 = contact1.getDisplayName().toLowerCase(); + const name2 = contact2.getDisplayName().toLowerCase(); + return name1 < name2 ? -1 : name1 > name2 ? 1 : 0; + } else { + return _converse.STATUS_WEIGHTS[status1] < _converse.STATUS_WEIGHTS[status2] ? -1 : 1; + } + }, + + onConnected() { + /* Called as soon as the connection has been established + * (either after initial login, or after reconnection). + * + * Use the opportunity to register stanza handlers. + */ + this.registerRosterHandler(); + this.registerRosterXHandler(); + }, + + registerRosterHandler() { + /* Register a handler for roster IQ "set" stanzas, which update + * roster contacts. + */ + _converse.connection.addHandler(iq => { + _converse.roster.onRosterPush(iq); + + return true; + }, Strophe.NS.ROSTER, 'iq', "set"); + }, + + registerRosterXHandler() { + /* Register a handler for RosterX message stanzas, which are + * used to suggest roster contacts to a user. + */ + let t = 0; + + _converse.connection.addHandler(function (msg) { + window.setTimeout(function () { + _converse.connection.flush(); + + _converse.roster.subscribeToSuggestedItems.bind(_converse.roster)(msg); + }, t); + t += msg.querySelectorAll('item').length * 250; + return true; + }, Strophe.NS.ROSTERX, 'message', null); + }, + + fetchRosterContacts() { + /* Fetches the roster contacts, first by trying the + * sessionStorage cache, and if that's empty, then by querying + * the XMPP server. + * + * Returns a promise which resolves once the contacts have been + * fetched. + */ + const that = this; + return new Promise((resolve, reject) => { + this.fetch({ + 'add': true, + 'silent': true, + + success(collection) { + if (collection.length === 0 || that.rosterVersioningSupported() && !_converse.session.get('roster_fetched')) { + _converse.send_initial_presence = true; + + _converse.roster.fetchFromServer().then(resolve).catch(reject); + } else { + _converse.emit('cachedRoster', collection); + + resolve(); + } + } + + }); + }); + }, + + subscribeToSuggestedItems(msg) { + _.each(msg.querySelectorAll('item'), function (item) { + if (item.getAttribute('action') === 'add') { + _converse.roster.addAndSubscribe(item.getAttribute('jid'), _converse.xmppstatus.vcard.get('nickname') || _converse.xmppstatus.vcard.get('fullname')); + } + }); + + return true; + }, + + isSelf(jid) { + return u.isSameBareJID(jid, _converse.connection.jid); + }, + + addAndSubscribe(jid, name, groups, message, attributes) { + /* Add a roster contact and then once we have confirmation from + * the XMPP server we subscribe to that contact's presence updates. + * Parameters: + * (String) jid - The Jabber ID of the user being added and subscribed to. + * (String) name - The name of that user + * (Array of Strings) groups - Any roster groups the user might belong to + * (String) message - An optional message to explain the + * reason for the subscription request. + * (Object) attributes - Any additional attributes to be stored on the user's model. + */ + const handler = contact => { + if (contact instanceof _converse.RosterContact) { + contact.subscribe(message); + } + }; + + this.addContactToRoster(jid, name, groups, attributes).then(handler, handler); + }, + + sendContactAddIQ(jid, name, groups, callback, errback) { + /* Send an IQ stanza to the XMPP server to add a new roster contact. + * + * Parameters: + * (String) jid - The Jabber ID of the user being added + * (String) name - The name of that user + * (Array of Strings) groups - Any roster groups the user might belong to + * (Function) callback - A function to call once the IQ is returned + * (Function) errback - A function to call if an error occurred + */ + name = _.isEmpty(name) ? jid : name; + const iq = $iq({ + type: 'set' + }).c('query', { + xmlns: Strophe.NS.ROSTER + }).c('item', { + jid, + name + }); + + _.each(groups, function (group) { + iq.c('group').t(group).up(); + }); + + _converse.connection.sendIQ(iq, callback, errback); + }, + + addContactToRoster(jid, name, groups, attributes) { + /* Adds a RosterContact instance to _converse.roster and + * registers the contact on the XMPP server. + * Returns a promise which is resolved once the XMPP server has + * responded. + * + * Parameters: + * (String) jid - The Jabber ID of the user being added and subscribed to. + * (String) name - The name of that user + * (Array of Strings) groups - Any roster groups the user might belong to + * (Object) attributes - Any additional attributes to be stored on the user's model. + */ + return new Promise((resolve, reject) => { + groups = groups || []; + this.sendContactAddIQ(jid, name, groups, () => { + const contact = this.create(_.assignIn({ + 'ask': undefined, + 'nickname': name, + groups, + jid, + 'requesting': false, + 'subscription': 'none' + }, attributes), { + sort: false + }); + resolve(contact); + }, function (err) { + alert(__('Sorry, there was an error while trying to add %1$s as a contact.', name)); + + _converse.log(err, Strophe.LogLevel.ERROR); + + resolve(err); + }); + }); + }, + + subscribeBack(bare_jid, presence) { + const contact = this.get(bare_jid); + + if (contact instanceof _converse.RosterContact) { + contact.authorize().subscribe(); + } else { + // Can happen when a subscription is retried or roster was deleted + const handler = contact => { + if (contact instanceof _converse.RosterContact) { + contact.authorize().subscribe(); + } + }; + + const nickname = _.get(sizzle(`nick[xmlns="${Strophe.NS.NICK}"]`, presence).pop(), 'textContent', null); + + this.addContactToRoster(bare_jid, nickname, [], { + 'subscription': 'from' + }).then(handler, handler); + } + }, + + getNumOnlineContacts() { + let ignored = ['offline', 'unavailable']; + + if (_converse.show_only_online_users) { + ignored = _.union(ignored, ['dnd', 'xa', 'away']); + } + + return _.sum(this.models.filter(model => !_.includes(ignored, model.presence.get('show')))); + }, + + onRosterPush(iq) { + /* Handle roster updates from the XMPP server. + * See: https://xmpp.org/rfcs/rfc6121.html#roster-syntax-actions-push + * + * Parameters: + * (XMLElement) IQ - The IQ stanza received from the XMPP server. + */ + const id = iq.getAttribute('id'); + const from = iq.getAttribute('from'); + + if (from && from !== _converse.bare_jid) { + // https://tools.ietf.org/html/rfc6121#page-15 + // + // A receiving client MUST ignore the stanza unless it has no 'from' + // attribute (i.e., implicitly from the bare JID of the user's + // account) or it has a 'from' attribute whose value matches the + // user's bare JID . + return; + } + + _converse.connection.send($iq({ + type: 'result', + id, + from: _converse.connection.jid + })); + + const query = sizzle(`query[xmlns="${Strophe.NS.ROSTER}"]`, iq).pop(); + this.data.save('version', query.getAttribute('ver')); + const items = sizzle(`item`, query); + + if (items.length > 1) { + _converse.log(iq, Strophe.LogLevel.ERROR); + + throw new Error('Roster push query may not contain more than one "item" element.'); + } + + if (items.length === 0) { + _converse.log(iq, Strophe.LogLevel.WARN); + + _converse.log('Received a roster push stanza without an "item" element.', Strophe.LogLevel.WARN); + + return; + } + + this.updateContact(items.pop()); + + _converse.emit('rosterPush', iq); + + return; + }, + + rosterVersioningSupported() { + return _converse.api.disco.stream.getFeature('ver', 'urn:xmpp:features:rosterver') && this.data.get('version'); + }, + + fetchFromServer() { + /* Fetch the roster from the XMPP server */ + return new Promise((resolve, reject) => { + const iq = $iq({ + 'type': 'get', + 'id': _converse.connection.getUniqueId('roster') + }).c('query', { + xmlns: Strophe.NS.ROSTER + }); + + if (this.rosterVersioningSupported()) { + iq.attrs({ + 'ver': this.data.get('version') + }); + } + + const callback = _.flow(this.onReceivedFromServer.bind(this), resolve); + + const errback = function errback(iq) { + const errmsg = "Error while trying to fetch roster from the server"; + + _converse.log(errmsg, Strophe.LogLevel.ERROR); + + reject(new Error(errmsg)); + }; + + return _converse.connection.sendIQ(iq, callback, errback); + }); + }, + + onReceivedFromServer(iq) { + /* An IQ stanza containing the roster has been received from + * the XMPP server. + */ + const query = sizzle(`query[xmlns="${Strophe.NS.ROSTER}"]`, iq).pop(); + + if (query) { + const items = sizzle(`item`, query); + + _.each(items, item => this.updateContact(item)); + + this.data.save('version', query.getAttribute('ver')); + + _converse.session.save('roster_fetched', true); + } + + _converse.emit('roster', iq); + }, + + updateContact(item) { + /* Update or create RosterContact models based on items + * received in the IQ from the server. + */ + const jid = item.getAttribute('jid'); + + if (this.isSelf(jid)) { + return; + } + + const contact = this.get(jid), + subscription = item.getAttribute("subscription"), + ask = item.getAttribute("ask"), + groups = _.map(item.getElementsByTagName('group'), Strophe.getText); + + if (!contact) { + if (subscription === "none" && ask === null || subscription === "remove") { + return; // We're lazy when adding contacts. + } + + this.create({ + 'ask': ask, + 'nickname': item.getAttribute("name"), + 'groups': groups, + 'jid': jid, + 'subscription': subscription + }, { + sort: false + }); + } else { + if (subscription === "remove") { + return contact.destroy(); + } // We only find out about requesting contacts via the + // presence handler, so if we receive a contact + // here, we know they aren't requesting anymore. + // see docs/DEVELOPER.rst + + + contact.save({ + 'subscription': subscription, + 'ask': ask, + 'requesting': null, + 'groups': groups + }); + } + }, + + createRequestingContact(presence) { + const bare_jid = Strophe.getBareJidFromJid(presence.getAttribute('from')), + nickname = _.get(sizzle(`nick[xmlns="${Strophe.NS.NICK}"]`, presence).pop(), 'textContent', null); + + const user_data = { + 'jid': bare_jid, + 'subscription': 'none', + 'ask': null, + 'requesting': true, + 'nickname': nickname + }; + + _converse.emit('contactRequest', this.create(user_data)); + }, + + handleIncomingSubscription(presence) { + const jid = presence.getAttribute('from'), + bare_jid = Strophe.getBareJidFromJid(jid), + contact = this.get(bare_jid); + + if (!_converse.allow_contact_requests) { + _converse.rejectPresenceSubscription(jid, __("This client does not allow presence subscriptions")); + } + + if (_converse.auto_subscribe) { + if (!contact || contact.get('subscription') !== 'to') { + this.subscribeBack(bare_jid, presence); + } else { + contact.authorize(); + } + } else { + if (contact) { + if (contact.get('subscription') !== 'none') { + contact.authorize(); + } else if (contact.get('ask') === "subscribe") { + contact.authorize(); + } + } else { + this.createRequestingContact(presence); + } + } + }, + + handleOwnPresence(presence) { + const jid = presence.getAttribute('from'), + resource = Strophe.getResourceFromJid(jid), + presence_type = presence.getAttribute('type'); + + if (_converse.connection.jid !== jid && presence_type !== 'unavailable' && (_converse.synchronize_availability === true || _converse.synchronize_availability === resource)) { + // Another resource has changed its status and + // synchronize_availability option set to update, + // we'll update ours as well. + const show = _.propertyOf(presence.querySelector('show'))('textContent') || 'online'; + + _converse.xmppstatus.save({ + 'status': show + }, { + 'silent': true + }); + + const status_message = _.propertyOf(presence.querySelector('status'))('textContent'); + + if (status_message) { + _converse.xmppstatus.save({ + 'status_message': status_message + }); + } + } + + if (_converse.jid === jid && presence_type === 'unavailable') { + // XXX: We've received an "unavailable" presence from our + // own resource. Apparently this happens due to a + // Prosody bug, whereby we send an IQ stanza to remove + // a roster contact, and Prosody then sends + // "unavailable" globally, instead of directed to the + // particular user that's removed. + // + // Here is the bug report: https://prosody.im/issues/1121 + // + // I'm not sure whether this might legitimately happen + // in other cases. + // + // As a workaround for now we simply send our presence again, + // otherwise we're treated as offline. + _converse.xmppstatus.sendPresence(); + } + }, + + presenceHandler(presence) { + const presence_type = presence.getAttribute('type'); + + if (presence_type === 'error') { + return true; + } + + const jid = presence.getAttribute('from'), + bare_jid = Strophe.getBareJidFromJid(jid); + + if (this.isSelf(bare_jid)) { + return this.handleOwnPresence(presence); + } else if (sizzle(`query[xmlns="${Strophe.NS.MUC}"]`, presence).length) { + return; // Ignore MUC + } + + const status_message = _.propertyOf(presence.querySelector('status'))('textContent'), + contact = this.get(bare_jid); + + if (contact && status_message !== contact.get('status')) { + contact.save({ + 'status': status_message + }); + } + + if (presence_type === 'subscribed' && contact) { + contact.ackSubscribe(); + } else if (presence_type === 'unsubscribed' && contact) { + contact.ackUnsubscribe(); + } else if (presence_type === 'unsubscribe') { + return; + } else if (presence_type === 'subscribe') { + this.handleIncomingSubscription(presence); + } else if (presence_type === 'unavailable' && contact) { + const resource = Strophe.getResourceFromJid(jid); + contact.presence.removeResource(resource); + } else if (contact) { + // presence_type is undefined + contact.presence.addResource(presence); + } + } + + }); + _converse.RosterGroup = Backbone.Model.extend({ + initialize(attributes) { + this.set(_.assignIn({ + description: __('Click to hide these contacts'), + state: _converse.OPENED + }, attributes)); // Collection of contacts belonging to this group. + + this.contacts = new _converse.RosterContacts(); + } + + }); + _converse.RosterGroups = Backbone.Collection.extend({ + model: _converse.RosterGroup, + + fetchRosterGroups() { + /* Fetches all the roster groups from sessionStorage. + * + * Returns a promise which resolves once the groups have been + * returned. + */ + return new Promise((resolve, reject) => { + this.fetch({ + silent: true, + // We need to first have all groups before + // we can start positioning them, so we set + // 'silent' to true. + success: resolve + }); + }); + } + + }); + + _converse.unregisterPresenceHandler = function () { + if (!_.isUndefined(_converse.presence_ref)) { + _converse.connection.deleteHandler(_converse.presence_ref); + + delete _converse.presence_ref; + } + }; + /********** Event Handlers *************/ + + + function updateUnreadCounter(chatbox) { + const contact = _converse.roster.findWhere({ + 'jid': chatbox.get('jid') + }); + + if (!_.isUndefined(contact)) { + contact.save({ + 'num_unread': chatbox.get('num_unread') + }); + } + } + + _converse.api.listen.on('chatBoxesInitialized', () => { + _converse.chatboxes.on('change:num_unread', updateUnreadCounter); + }); + + _converse.api.listen.on('beforeTearDown', _converse.unregisterPresenceHandler()); + + _converse.api.listen.on('afterTearDown', () => { + if (_converse.presences) { + _converse.presences.off().reset(); // Remove presences + + } + }); + + _converse.api.listen.on('clearSession', () => { + if (_converse.presences) { + _converse.presences.browserStorage._clear(); + } + }); + + _converse.api.listen.on('statusInitialized', reconnecting => { + if (!reconnecting) { + _converse.presences = new _converse.Presences(); + _converse.presences.browserStorage = new Backbone.BrowserStorage.session(b64_sha1(`converse.presences-${_converse.bare_jid}`)); + + _converse.presences.fetch(); + } + + _converse.emit('presencesInitialized', reconnecting); + }); + + _converse.api.listen.on('presencesInitialized', reconnecting => { + if (reconnecting) { + // No need to recreate the roster, otherwise we lose our + // cached data. However we still emit an event, to give + // event handlers a chance to register views for the + // roster and its groups, before we start populating. + _converse.emit('rosterReadyAfterReconnection'); + } else { + _converse.registerIntervalHandler(); + + _converse.initRoster(); + } + + _converse.roster.onConnected(); + + _converse.populateRoster(reconnecting); + + _converse.registerPresenceHandler(); + }); + /************************ API ************************/ + // API methods only available to plugins + + + _.extend(_converse.api, { + /** + * @namespace _converse.api.contacts + * @memberOf _converse.api + */ + 'contacts': { + /** + * This method is used to retrieve roster contacts. + * + * @method _converse.api.contacts.get + * @params {(string[]|string)} jid|jids The JID or JIDs of + * the contacts to be returned. + * @returns {(RosterContact[]|RosterContact)} [Backbone.Model](http://backbonejs.org/#Model) + * (or an array of them) representing the contact. + * + * @example + * // Fetch a single contact + * _converse.api.listen.on('rosterContactsFetched', function () { + * const contact = _converse.api.contacts.get('buddy@example.com') + * // ... + * }); + * + * @example + * // To get multiple contacts, pass in an array of JIDs: + * _converse.api.listen.on('rosterContactsFetched', function () { + * const contacts = _converse.api.contacts.get( + * ['buddy1@example.com', 'buddy2@example.com'] + * ) + * // ... + * }); + * + * @example + * // To return all contacts, simply call ``get`` without any parameters: + * _converse.api.listen.on('rosterContactsFetched', function () { + * const contacts = _converse.api.contacts.get(); + * // ... + * }); + */ + 'get'(jids) { + const _getter = function _getter(jid) { + return _converse.roster.get(Strophe.getBareJidFromJid(jid)) || null; + }; + + if (_.isUndefined(jids)) { + jids = _converse.roster.pluck('jid'); + } else if (_.isString(jids)) { + return _getter(jids); + } + + return _.map(jids, _getter); + }, + + /** + * Add a contact. + * + * @method _converse.api.contacts.add + * @param {string} jid The JID of the contact to be added + * @param {string} [name] A custom name to show the user by + * in the roster. + * @example + * _converse.api.contacts.add('buddy@example.com') + * @example + * _converse.api.contacts.add('buddy@example.com', 'Buddy') + */ + 'add'(jid, name) { + if (!_.isString(jid) || !_.includes(jid, '@')) { + throw new TypeError('contacts.add: invalid jid'); + } + + _converse.roster.addAndSubscribe(jid, _.isEmpty(name) ? jid : name); + } + + } + }); + } + + }); +}); + +/***/ }), + +/***/ "./src/converse-rosterview.js": +/*!************************************!*\ + !*** ./src/converse-rosterview.js ***! + \************************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;// Converse.js +// http://conversejs.org +// +// Copyright (c) 2012-2018, the Converse.js developers +// Licensed under the Mozilla Public License (MPLv2) +(function (root, factory) { + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! @converse/headless/converse-core */ "./src/headless/converse-core.js"), __webpack_require__(/*! formdata-polyfill */ "./node_modules/formdata-polyfill/FormData.js"), __webpack_require__(/*! templates/add_contact_modal.html */ "./src/templates/add_contact_modal.html"), __webpack_require__(/*! templates/group_header.html */ "./src/templates/group_header.html"), __webpack_require__(/*! templates/pending_contact.html */ "./src/templates/pending_contact.html"), __webpack_require__(/*! templates/requesting_contact.html */ "./src/templates/requesting_contact.html"), __webpack_require__(/*! templates/roster.html */ "./src/templates/roster.html"), __webpack_require__(/*! templates/roster_filter.html */ "./src/templates/roster_filter.html"), __webpack_require__(/*! templates/roster_item.html */ "./src/templates/roster_item.html"), __webpack_require__(/*! templates/search_contact.html */ "./src/templates/search_contact.html"), __webpack_require__(/*! awesomplete */ "./node_modules/awesomplete-avoid-xss/awesomplete.js"), __webpack_require__(/*! @converse/headless/converse-chatboxes */ "./src/headless/converse-chatboxes.js"), __webpack_require__(/*! converse-modal */ "./src/converse-modal.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), + __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? + (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), + __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); +})(this, function (converse, _FormData, tpl_add_contact_modal, tpl_group_header, tpl_pending_contact, tpl_requesting_contact, tpl_roster, tpl_roster_filter, tpl_roster_item, tpl_search_contact, Awesomplete) { + "use strict"; + + const _converse$env = converse.env, + Backbone = _converse$env.Backbone, + Strophe = _converse$env.Strophe, + $iq = _converse$env.$iq, + b64_sha1 = _converse$env.b64_sha1, + sizzle = _converse$env.sizzle, + _ = _converse$env._; + const u = converse.env.utils; + converse.plugins.add('converse-rosterview', { + dependencies: ["converse-roster", "converse-modal"], + overrides: { + // Overrides mentioned here will be picked up by converse.js's + // plugin architecture they will replace existing methods on the + // relevant objects or classes. + // + // New functions which don't exist yet can also be added. + afterReconnected() { + this.__super__.afterReconnected.apply(this, arguments); + }, + + tearDown() { + /* Remove the rosterview when tearing down. It gets created + * anew when reconnecting or logging in. + */ + this.__super__.tearDown.apply(this, arguments); + + if (!_.isUndefined(this.rosterview)) { + this.rosterview.remove(); + } + }, + + RosterGroups: { + comparator() { + // RosterGroupsComparator only gets set later (once i18n is + // set up), so we need to wrap it in this nameless function. + const _converse = this.__super__._converse; + return _converse.RosterGroupsComparator.apply(this, arguments); + } + + } + }, + + initialize() { + /* The initialize function gets called as soon as the plugin is + * loaded by converse.js's plugin machinery. + */ + const _converse = this._converse, + __ = _converse.__; + + _converse.api.settings.update({ + 'allow_chat_pending_contacts': true, + 'allow_contact_removal': true, + 'hide_offline_users': false, + 'roster_groups': true, + 'show_only_online_users': false, + 'show_toolbar': true, + 'xhr_user_search_url': null + }); + + _converse.api.promises.add('rosterViewInitialized'); + + const 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') + }; + + const LABEL_GROUPS = __('Groups'); + + const HEADER_CURRENT_CONTACTS = __('My contacts'); + + const HEADER_PENDING_CONTACTS = __('Pending contacts'); + + const HEADER_REQUESTING_CONTACTS = __('Contact requests'); + + const HEADER_UNGROUPED = __('Ungrouped'); + + const HEADER_WEIGHTS = {}; + HEADER_WEIGHTS[HEADER_REQUESTING_CONTACTS] = 0; + HEADER_WEIGHTS[HEADER_CURRENT_CONTACTS] = 1; + HEADER_WEIGHTS[HEADER_UNGROUPED] = 2; + HEADER_WEIGHTS[HEADER_PENDING_CONTACTS] = 3; + + _converse.RosterGroupsComparator = function (a, b) { + /* Groups are sorted alphabetically, ignoring case. + * However, Ungrouped, Requesting Contacts and Pending Contacts + * appear last and in that order. + */ + a = a.get('name'); + b = b.get('name'); + + const special_groups = _.keys(HEADER_WEIGHTS); + + const a_is_special = _.includes(special_groups, a); + + const b_is_special = _.includes(special_groups, b); + + if (!a_is_special && !b_is_special) { + return a.toLowerCase() < b.toLowerCase() ? -1 : a.toLowerCase() > b.toLowerCase() ? 1 : 0; + } else if (a_is_special && b_is_special) { + return HEADER_WEIGHTS[a] < HEADER_WEIGHTS[b] ? -1 : HEADER_WEIGHTS[a] > HEADER_WEIGHTS[b] ? 1 : 0; + } else if (!a_is_special && b_is_special) { + return b === HEADER_REQUESTING_CONTACTS ? 1 : -1; + } else if (a_is_special && !b_is_special) { + return a === HEADER_REQUESTING_CONTACTS ? -1 : 1; + } + }; + + _converse.AddContactModal = _converse.BootstrapModal.extend({ + events: { + 'submit form': 'addContactFromForm' + }, + + initialize() { + _converse.BootstrapModal.prototype.initialize.apply(this, arguments); + + this.model.on('change', this.render, this); + }, + + toHTML() { + const label_nickname = _converse.xhr_user_search_url ? __('Contact name') : __('Optional nickname'); + return tpl_add_contact_modal(_.extend(this.model.toJSON(), { + '_converse': _converse, + 'heading_new_contact': __('Add a Contact'), + 'label_xmpp_address': __('XMPP Address'), + 'label_nickname': label_nickname, + 'contact_placeholder': __('name@example.org'), + 'label_add': __('Add'), + 'error_message': __('Please enter a valid XMPP address') + })); + }, + + afterRender() { + if (_converse.xhr_user_search_url && _.isString(_converse.xhr_user_search_url)) { + this.initXHRAutoComplete(this.el); + } else { + this.initJIDAutoComplete(this.el); + } + + const jid_input = this.el.querySelector('input[name="jid"]'); + this.el.addEventListener('shown.bs.modal', () => { + jid_input.focus(); + }, false); + }, + + initJIDAutoComplete(root) { + const jid_input = root.querySelector('input[name="jid"]'); + + const list = _.uniq(_converse.roster.map(item => Strophe.getDomainFromJid(item.get('jid')))); + + new Awesomplete(jid_input, { + 'list': list, + 'data': function data(text, input) { + return input.slice(0, input.indexOf("@")) + "@" + text; + }, + 'filter': Awesomplete.FILTER_STARTSWITH + }); + }, + + initXHRAutoComplete(root) { + const name_input = this.el.querySelector('input[name="name"]'); + const jid_input = this.el.querySelector('input[name="jid"]'); + const awesomplete = new Awesomplete(name_input, { + 'minChars': 1, + 'list': [] + }); + const xhr = new window.XMLHttpRequest(); // `open` must be called after `onload` for mock/testing purposes. + + xhr.onload = function () { + if (xhr.responseText) { + awesomplete.list = JSON.parse(xhr.responseText).map(i => { + //eslint-disable-line arrow-body-style + return { + 'label': i.fullname || i.jid, + 'value': i.jid + }; + }); + awesomplete.evaluate(); + } + }; + + name_input.addEventListener('input', _.debounce(() => { + xhr.open("GET", `${_converse.xhr_user_search_url}q=${name_input.value}`, true); + xhr.send(); + }, 300)); + this.el.addEventListener('awesomplete-selectcomplete', ev => { + jid_input.value = ev.text.value; + name_input.value = ev.text.label; + }); + }, + + addContactFromForm(ev) { + ev.preventDefault(); + const data = new FormData(ev.target), + jid = data.get('jid'), + name = data.get('name'); + + if (!jid || _.compact(jid.split('@')).length < 2) { + // XXX: we have to do this manually, instead of via + // toHTML because Awesomplete messes things up and + // confuses Snabbdom + u.addClass('is-invalid', this.el.querySelector('input[name="jid"]')); + u.addClass('d-block', this.el.querySelector('.invalid-feedback')); + } else { + ev.target.reset(); + + _converse.roster.addAndSubscribe(jid, name); + + this.model.clear(); + this.modal.hide(); + } + } + + }); + _converse.RosterFilter = Backbone.Model.extend({ + initialize() { + this.set({ + 'filter_text': '', + 'filter_type': 'contacts', + 'chat_state': '' + }); + } + + }); + _converse.RosterFilterView = Backbone.VDOMView.extend({ + tagName: 'form', + className: 'roster-filter-form', + events: { + "keydown .roster-filter": "liveFilter", + "submit form.roster-filter-form": "submitFilter", + "click .clear-input": "clearFilter", + "click .filter-by span": "changeTypeFilter", + "change .state-type": "changeChatStateFilter" + }, + + initialize() { + this.model.on('change:filter_type', this.render, this); + this.model.on('change:filter_text', this.render, this); + }, + + toHTML() { + return tpl_roster_filter(_.extend(this.model.toJSON(), { + visible: this.shouldBeVisible(), + placeholder: __('Filter'), + title_contact_filter: __('Filter by contact name'), + title_group_filter: __('Filter by group name'), + title_status_filter: __('Filter by status'), + label_any: __('Any'), + label_unread_messages: __('Unread'), + label_online: __('Online'), + label_chatty: __('Chatty'), + label_busy: __('Busy'), + label_away: __('Away'), + label_xa: __('Extended Away'), + label_offline: __('Offline') + })); + }, + + changeChatStateFilter(ev) { + if (ev && ev.preventDefault) { + ev.preventDefault(); + } + + this.model.save({ + 'chat_state': this.el.querySelector('.state-type').value + }); + }, + + changeTypeFilter(ev) { + if (ev && ev.preventDefault) { + ev.preventDefault(); + } + + const type = ev.target.dataset.type; + + if (type === 'state') { + this.model.save({ + 'filter_type': type, + 'chat_state': this.el.querySelector('.state-type').value + }); + } else { + this.model.save({ + 'filter_type': type, + 'filter_text': this.el.querySelector('.roster-filter').value + }); + } + }, + + liveFilter: _.debounce(function (ev) { + this.model.save({ + 'filter_text': this.el.querySelector('.roster-filter').value + }); + }, 250), + + submitFilter(ev) { + if (ev && ev.preventDefault) { + ev.preventDefault(); + } + + this.liveFilter(); + this.render(); + }, + + isActive() { + /* Returns true if the filter is enabled (i.e. if the user + * has added values to the filter). + */ + if (this.model.get('filter_type') === 'state' || this.model.get('filter_text')) { + return true; + } + + return false; + }, + + shouldBeVisible() { + return _converse.roster.length >= 5 || this.isActive(); + }, + + showOrHide() { + if (this.shouldBeVisible()) { + this.show(); + } else { + this.hide(); + } + }, + + show() { + if (u.isVisible(this.el)) { + return this; + } + + this.el.classList.add('fade-in'); + this.el.classList.remove('hidden'); + return this; + }, + + hide() { + if (!u.isVisible(this.el)) { + return this; + } + + this.model.save({ + 'filter_text': '', + 'chat_state': '' + }); + this.el.classList.add('hidden'); + return this; + }, + + clearFilter(ev) { + if (ev && ev.preventDefault) { + ev.preventDefault(); + u.hideElement(this.el.querySelector('.clear-input')); + } + + const roster_filter = this.el.querySelector('.roster-filter'); + roster_filter.value = ''; + this.model.save({ + 'filter_text': '' + }); + } + + }); + _converse.RosterContactView = Backbone.NativeView.extend({ + tagName: 'li', + className: 'list-item d-flex hidden controlbox-padded', + events: { + "click .accept-xmpp-request": "acceptRequest", + "click .decline-xmpp-request": "declineRequest", + "click .open-chat": "openChat", + "click .remove-xmpp-contact": "removeContact" + }, + + initialize() { + this.model.on("change", this.render, this); + this.model.on("highlight", this.highlight, this); + this.model.on("destroy", this.remove, this); + this.model.on("open", this.openChat, this); + this.model.on("remove", this.remove, this); + this.model.presence.on("change:show", this.render, this); + this.model.vcard.on('change:fullname', this.render, this); + }, + + render() { + const that = this; + + if (!this.mayBeShown()) { + u.hideElement(this.el); + return this; + } + + const ask = this.model.get('ask'), + show = this.model.presence.get('show'), + requesting = this.model.get('requesting'), + subscription = this.model.get('subscription'); + const classes_to_remove = ['current-xmpp-contact', 'pending-xmpp-contact', 'requesting-xmpp-contact'].concat(_.keys(STATUSES)); + + _.each(classes_to_remove, function (cls) { + if (_.includes(that.el.className, cls)) { + that.el.classList.remove(cls); + } + }); + + this.el.classList.add(show); + this.el.setAttribute('data-status', show); + this.highlight(); + + if (_converse.isSingleton()) { + const chatbox = _converse.chatboxes.get(this.model.get('jid')); + + if (chatbox) { + if (chatbox.get('hidden')) { + this.el.classList.remove('open'); + } else { + this.el.classList.add('open'); + } + } + } + + if (ask === 'subscribe' || subscription === 'from') { + /* ask === 'subscribe' + * Means we have asked to subscribe to them. + * + * subscription === 'from' + * They are subscribed to use, but not vice versa. + * We assume that there is a pending subscription + * from us to them (otherwise we're in a state not + * supported by converse.js). + * + * So in both cases the user is a "pending" contact. + */ + const display_name = this.model.getDisplayName(); + this.el.classList.add('pending-xmpp-contact'); + this.el.innerHTML = tpl_pending_contact(_.extend(this.model.toJSON(), { + 'display_name': display_name, + 'desc_remove': __('Click to remove %1$s as a contact', display_name), + 'allow_chat_pending_contacts': _converse.allow_chat_pending_contacts + })); + } else if (requesting === true) { + const display_name = this.model.getDisplayName(); + this.el.classList.add('requesting-xmpp-contact'); + this.el.innerHTML = tpl_requesting_contact(_.extend(this.model.toJSON(), { + 'display_name': display_name, + 'desc_accept': __("Click to accept the contact request from %1$s", display_name), + 'desc_decline': __("Click to decline the contact request from %1$s", display_name), + 'allow_chat_pending_contacts': _converse.allow_chat_pending_contacts + })); + } else if (subscription === 'both' || subscription === 'to') { + this.el.classList.add('current-xmpp-contact'); + this.el.classList.remove(_.without(['both', 'to'], subscription)[0]); + this.el.classList.add(subscription); + this.renderRosterItem(this.model); + } + + return this; + }, + + highlight() { + /* If appropriate, highlight the contact (by adding the 'open' class). + */ + if (_converse.isSingleton()) { + const chatbox = _converse.chatboxes.get(this.model.get('jid')); + + if (chatbox) { + if (chatbox.get('hidden')) { + this.el.classList.remove('open'); + } else { + this.el.classList.add('open'); + } + } + } + }, + + renderRosterItem(item) { + let status_icon = 'fa fa-times-circle'; + const show = item.presence.get('show') || 'offline'; + + if (show === 'online') { + status_icon = 'fa fa-circle chat-status chat-status--online'; + } else if (show === 'away') { + status_icon = 'fa fa-circle chat-status chat-status--away'; + } else if (show === 'xa') { + status_icon = 'far fa-circle chat-status'; + } else if (show === 'dnd') { + status_icon = 'fa fa-minus-circle chat-status chat-status--busy'; + } + + const display_name = item.getDisplayName(); + this.el.innerHTML = tpl_roster_item(_.extend(item.toJSON(), { + 'display_name': display_name, + 'desc_status': STATUSES[show], + 'status_icon': status_icon, + 'desc_chat': __('Click to chat with %1$s (JID: %2$s)', display_name, item.get('jid')), + 'desc_remove': __('Click to remove %1$s as a contact', display_name), + 'allow_contact_removal': _converse.allow_contact_removal, + 'num_unread': item.get('num_unread') || 0 + })); + return this; + }, + + mayBeShown() { + /* Return a boolean indicating whether this contact should + * generally be visible in the roster. + * + * It doesn't check for the more specific case of whether + * the group it's in is collapsed. + */ + const chatStatus = this.model.presence.get('show'); + + if (_converse.show_only_online_users && chatStatus !== 'online' || _converse.hide_offline_users && chatStatus === 'offline') { + // If pending or requesting, show + if (this.model.get('ask') === 'subscribe' || this.model.get('subscription') === 'from' || this.model.get('requesting') === true) { + return true; + } + + return false; + } + + return true; + }, + + openChat(ev) { + if (ev && ev.preventDefault) { + ev.preventDefault(); + } + + const attrs = this.model.attributes; + + _converse.api.chats.open(attrs.jid, attrs); + }, + + removeContact(ev) { + if (ev && ev.preventDefault) { + ev.preventDefault(); + } + + if (!_converse.allow_contact_removal) { + return; + } + + const result = confirm(__("Are you sure you want to remove this contact?")); + + if (result === true) { + this.model.removeFromRoster(iq => { + this.model.destroy(); + this.remove(); + }, function (err) { + alert(__('Sorry, there was an error while trying to remove %1$s as a contact.', name)); + + _converse.log(err, Strophe.LogLevel.ERROR); + }); + } + }, + + acceptRequest(ev) { + if (ev && ev.preventDefault) { + ev.preventDefault(); + } + + _converse.roster.sendContactAddIQ(this.model.get('jid'), this.model.getFullname(), [], () => { + this.model.authorize().subscribe(); + }); + }, + + declineRequest(ev) { + if (ev && ev.preventDefault) { + ev.preventDefault(); + } + + const result = confirm(__("Are you sure you want to decline this contact request?")); + + if (result === true) { + this.model.unauthorize().destroy(); + } + + return this; + } + + }); + _converse.RosterGroupView = Backbone.OrderedListView.extend({ + tagName: 'div', + className: 'roster-group hidden', + events: { + "click a.group-toggle": "toggle" + }, + ItemView: _converse.RosterContactView, + listItems: 'model.contacts', + listSelector: '.roster-group-contacts', + sortEvent: 'presenceChanged', + + initialize() { + Backbone.OrderedListView.prototype.initialize.apply(this, arguments); + this.model.contacts.on("change:subscription", this.onContactSubscriptionChange, this); + this.model.contacts.on("change:requesting", this.onContactRequestChange, this); + this.model.contacts.on("remove", this.onRemove, this); + + _converse.roster.on('change:groups', this.onContactGroupChange, this); // This event gets triggered once *all* contacts (i.e. not + // just this group's) have been fetched from browser + // storage or the XMPP server and once they've been + // assigned to their various groups. + + + _converse.rosterview.on('rosterContactsFetchedAndProcessed', this.sortAndPositionAllItems.bind(this)); + }, + + render() { + this.el.setAttribute('data-group', this.model.get('name')); + this.el.innerHTML = tpl_group_header({ + 'label_group': this.model.get('name'), + 'desc_group_toggle': this.model.get('description'), + 'toggle_state': this.model.get('state'), + '_converse': _converse + }); + this.contacts_el = this.el.querySelector('.roster-group-contacts'); + return this; + }, + + show() { + u.showElement(this.el); + + _.each(this.getAll(), contact_view => { + if (contact_view.mayBeShown() && this.model.get('state') === _converse.OPENED) { + u.showElement(contact_view.el); + } + }); + + return this; + }, + + collapse() { + return u.slideIn(this.contacts_el); + }, + + filterOutContacts(contacts = []) { + /* Given a list of contacts, make sure they're filtered out + * (aka hidden) and that all other contacts are visible. + * + * If all contacts are hidden, then also hide the group + * title. + */ + let shown = 0; + const all_contact_views = this.getAll(); + + _.each(this.model.contacts.models, contact => { + const contact_view = this.get(contact.get('id')); + + if (_.includes(contacts, contact)) { + u.hideElement(contact_view.el); + } else if (contact_view.mayBeShown()) { + u.showElement(contact_view.el); + shown += 1; + } + }); + + if (shown) { + u.showElement(this.el); + } else { + u.hideElement(this.el); + } + }, + + getFilterMatches(q, type) { + /* Given the filter query "q" and the filter type "type", + * return a list of contacts that need to be filtered out. + */ + if (q.length === 0) { + return []; + } + + let matches; + q = q.toLowerCase(); + + if (type === 'state') { + if (this.model.get('name') === HEADER_REQUESTING_CONTACTS) { + // When filtering by chat state, we still want to + // show requesting contacts, even though they don't + // have the state in question. + matches = this.model.contacts.filter(contact => !_.includes(contact.presence.get('show'), q) && !contact.get('requesting')); + } else if (q === 'unread_messages') { + matches = this.model.contacts.filter({ + 'num_unread': 0 + }); + } else { + matches = this.model.contacts.filter(contact => !_.includes(contact.presence.get('show'), q)); + } + } else { + matches = this.model.contacts.filter(contact => { + return !_.includes(contact.getDisplayName().toLowerCase(), q.toLowerCase()); + }); + } + + return matches; + }, + + filter(q, type) { + /* Filter the group's contacts based on the query "q". + * + * If all contacts are filtered out (i.e. hidden), then the + * group must be filtered out as well. + */ + if (_.isNil(q)) { + type = type || _converse.rosterview.filter_view.model.get('filter_type'); + + if (type === 'state') { + q = _converse.rosterview.filter_view.model.get('chat_state'); + } else { + q = _converse.rosterview.filter_view.model.get('filter_text'); + } + } + + this.filterOutContacts(this.getFilterMatches(q, type)); + }, + + toggle(ev) { + if (ev && ev.preventDefault) { + ev.preventDefault(); + } + + const icon_el = ev.target.querySelector('.fa'); + + if (_.includes(icon_el.classList, "fa-caret-down")) { + this.model.save({ + state: _converse.CLOSED + }); + this.collapse().then(() => { + icon_el.classList.remove("fa-caret-down"); + icon_el.classList.add("fa-caret-right"); + }); + } else { + icon_el.classList.remove("fa-caret-right"); + icon_el.classList.add("fa-caret-down"); + this.model.save({ + state: _converse.OPENED + }); + this.filter(); + u.showElement(this.el); + u.slideOut(this.contacts_el); + } + }, + + onContactGroupChange(contact) { + const in_this_group = _.includes(contact.get('groups'), this.model.get('name')); + + const cid = contact.get('id'); + const in_this_overview = !this.get(cid); + + if (in_this_group && !in_this_overview) { + this.items.trigger('add', contact); + } else if (!in_this_group) { + this.removeContact(contact); + } + }, + + onContactSubscriptionChange(contact) { + if (this.model.get('name') === HEADER_PENDING_CONTACTS && contact.get('subscription') !== 'from') { + this.removeContact(contact); + } + }, + + onContactRequestChange(contact) { + if (this.model.get('name') === HEADER_REQUESTING_CONTACTS && !contact.get('requesting')) { + this.removeContact(contact); + } + }, + + removeContact(contact) { + // We suppress events, otherwise the remove event will + // also cause the contact's view to be removed from the + // "Pending Contacts" group. + this.model.contacts.remove(contact, { + 'silent': true + }); + this.onRemove(contact); + }, + + onRemove(contact) { + this.remove(contact.get('jid')); + + if (this.model.contacts.length === 0) { + this.remove(); + } + } + + }); + _converse.RosterView = Backbone.OrderedListView.extend({ + tagName: 'div', + id: 'converse-roster', + className: 'controlbox-section', + ItemView: _converse.RosterGroupView, + listItems: 'model', + listSelector: '.roster-contacts', + sortEvent: null, + // Groups are immutable, so they don't get re-sorted + subviewIndex: 'name', + events: { + 'click a.chatbox-btn.add-contact': 'showAddContactModal' + }, + + initialize() { + Backbone.OrderedListView.prototype.initialize.apply(this, arguments); + + _converse.roster.on("add", this.onContactAdded, this); + + _converse.roster.on('change:groups', this.onContactAdded, this); + + _converse.roster.on('change', this.onContactChange, this); + + _converse.roster.on("destroy", this.update, this); + + _converse.roster.on("remove", this.update, this); + + _converse.presences.on('change:show', () => { + this.update(); + this.updateFilter(); + }); + + this.model.on("reset", this.reset, this); // This event gets triggered once *all* contacts (i.e. not + // just this group's) have been fetched from browser + // storage or the XMPP server and once they've been + // assigned to their various groups. + + _converse.on('rosterGroupsFetched', this.sortAndPositionAllItems.bind(this)); + + _converse.on('rosterContactsFetched', () => { + _converse.roster.each(contact => this.addRosterContact(contact, { + 'silent': true + })); + + this.update(); + this.updateFilter(); + this.trigger('rosterContactsFetchedAndProcessed'); + }); + + this.createRosterFilter(); + }, + + render() { + this.el.innerHTML = tpl_roster({ + 'allow_contact_requests': _converse.allow_contact_requests, + 'heading_contacts': __('Contacts'), + 'title_add_contact': __('Add a contact') + }); + const form = this.el.querySelector('.roster-filter-form'); + this.el.replaceChild(this.filter_view.render().el, form); + this.roster_el = this.el.querySelector('.roster-contacts'); + return this; + }, + + showAddContactModal(ev) { + if (_.isUndefined(this.add_contact_modal)) { + this.add_contact_modal = new _converse.AddContactModal({ + 'model': new Backbone.Model() + }); + } + + this.add_contact_modal.show(ev); + }, + + createRosterFilter() { + // Create a model on which we can store filter properties + const model = new _converse.RosterFilter(); + model.id = b64_sha1(`_converse.rosterfilter${_converse.bare_jid}`); + model.browserStorage = new Backbone.BrowserStorage.local(this.filter.id); + this.filter_view = new _converse.RosterFilterView({ + 'model': model + }); + this.filter_view.model.on('change', this.updateFilter, this); + this.filter_view.model.fetch(); + }, + + updateFilter: _.debounce(function () { + /* Filter the roster again. + * Called whenever the filter settings have been changed or + * when contacts have been added, removed or changed. + * + * Debounced so that it doesn't get called for every + * contact fetched from browser storage. + */ + const type = this.filter_view.model.get('filter_type'); + + if (type === 'state') { + this.filter(this.filter_view.model.get('chat_state'), type); + } else { + this.filter(this.filter_view.model.get('filter_text'), type); + } + }, 100), + update: _.debounce(function () { + if (!u.isVisible(this.roster_el)) { + u.showElement(this.roster_el); + } + + this.filter_view.showOrHide(); + return this; + }, _converse.animate ? 100 : 0), + + filter(query, type) { + // First we make sure the filter is restored to its + // original state + _.each(this.getAll(), function (view) { + if (view.model.contacts.length > 0) { + view.show().filter(''); + } + }); // Now we can filter + + + query = query.toLowerCase(); + + if (type === 'groups') { + _.each(this.getAll(), function (view, idx) { + if (!_.includes(view.model.get('name').toLowerCase(), query.toLowerCase())) { + u.slideIn(view.el); + } else if (view.model.contacts.length > 0) { + u.slideOut(view.el); + } + }); + } else { + _.each(this.getAll(), function (view) { + view.filter(query, type); + }); + } + }, + + reset() { + _converse.roster.reset(); + + this.removeAll(); + this.render().update(); + return this; + }, + + onContactAdded(contact) { + this.addRosterContact(contact); + this.update(); + this.updateFilter(); + }, + + onContactChange(contact) { + this.updateChatBox(contact); + this.update(); + + if (_.has(contact.changed, 'subscription')) { + if (contact.changed.subscription === 'from') { + this.addContactToGroup(contact, HEADER_PENDING_CONTACTS); + } else if (_.includes(['both', 'to'], contact.get('subscription'))) { + this.addExistingContact(contact); + } + } + + if (_.has(contact.changed, 'ask') && contact.changed.ask === 'subscribe') { + this.addContactToGroup(contact, HEADER_PENDING_CONTACTS); + } + + if (_.has(contact.changed, 'subscription') && contact.changed.requesting === 'true') { + this.addContactToGroup(contact, HEADER_REQUESTING_CONTACTS); + } + + this.updateFilter(); + }, + + updateChatBox(contact) { + if (!this.model.chatbox) { + return this; + } + + const changes = {}; + + if (_.has(contact.changed, 'status')) { + changes.status = contact.get('status'); + } + + this.model.chatbox.save(changes); + return this; + }, + + getGroup(name) { + /* Returns the group as specified by name. + * Creates the group if it doesn't exist. + */ + const view = this.get(name); + + if (view) { + return view.model; + } + + return this.model.create({ + name, + id: b64_sha1(name) + }); + }, + + addContactToGroup(contact, name, options) { + this.getGroup(name).contacts.add(contact, options); + this.sortAndPositionAllItems(); + }, + + addExistingContact(contact, options) { + let groups; + + if (_converse.roster_groups) { + groups = contact.get('groups'); + + if (groups.length === 0) { + groups = [HEADER_UNGROUPED]; + } + } else { + groups = [HEADER_CURRENT_CONTACTS]; + } + + _.each(groups, _.bind(this.addContactToGroup, this, contact, _, options)); + }, + + addRosterContact(contact, options) { + if (contact.get('subscription') === 'both' || contact.get('subscription') === 'to') { + this.addExistingContact(contact, options); + } else { + if (!_converse.allow_contact_requests) { + _converse.log(`Not adding requesting or pending contact ${contact.get('jid')} ` + `because allow_contact_requests is false`, Strophe.LogLevel.DEBUG); + + return; + } + + if (contact.get('ask') === 'subscribe' || contact.get('subscription') === 'from') { + this.addContactToGroup(contact, HEADER_PENDING_CONTACTS, options); + } else if (contact.get('requesting') === true) { + this.addContactToGroup(contact, HEADER_REQUESTING_CONTACTS, options); + } + } + + return this; + } + + }); + /* -------- Event Handlers ----------- */ + + _converse.api.listen.on('chatBoxesInitialized', () => { + _converse.chatboxes.on('change:hidden', chatbox => { + const contact = _converse.roster.findWhere({ + 'jid': chatbox.get('jid') + }); + + if (!_.isUndefined(contact)) { + contact.trigger('highlight', contact); + } + }); + }); + + function initRoster() { + /* Create an instance of RosterView once the RosterGroups + * collection has been created (in @converse/headless/converse-core.js) + */ + if (_converse.authentication === _converse.ANONYMOUS) { + return; + } + + _converse.rosterview = new _converse.RosterView({ + 'model': _converse.rostergroups + }); + + _converse.rosterview.render(); + + _converse.emit('rosterViewInitialized'); + } + + _converse.api.listen.on('rosterInitialized', initRoster); + + _converse.api.listen.on('rosterReadyAfterReconnection', initRoster); + } + + }); +}); + +/***/ }), + +/***/ "./src/converse-singleton.js": +/*!***********************************!*\ + !*** ./src/converse-singleton.js ***! + \***********************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;// Converse.js +// http://conversejs.org +// +// Copyright (c) 2012-2018, the Converse.js developers +// Licensed under the Mozilla Public License (MPLv2) + +/* converse-singleton + * ****************** + * + * A plugin which ensures that only one chat (private or groupchat) is + * visible at any one time. All other ongoing chats are hidden and kept in the + * background. + * + * This plugin makes sense in mobile or fullscreen chat environments (as + * configured by the `view_mode` setting). + * + */ +(function (root, factory) { + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! @converse/headless/converse-core */ "./src/headless/converse-core.js"), __webpack_require__(/*! converse-chatview */ "./src/converse-chatview.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), + __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? + (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), + __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); +})(this, function (converse) { + "use strict"; + + const _converse$env = converse.env, + _ = _converse$env._, + Strophe = _converse$env.Strophe; + const u = converse.env.utils; + + function hideChat(view) { + if (view.model.get('id') === 'controlbox') { + return; + } + + u.safeSave(view.model, { + 'hidden': true + }); + view.hide(); + } + + converse.plugins.add('converse-singleton', { + // It's possible however to make optional dependencies non-optional. + // If the setting "strict_plugin_dependencies" is set to true, + // an error will be raised if the plugin is not found. + // + // NB: These plugins need to have already been loaded via require.js. + dependencies: ['converse-chatboxes', 'converse-muc', 'converse-muc-views', 'converse-controlbox', 'converse-rosterview'], + overrides: { + // overrides mentioned here will be picked up by converse.js's + // plugin architecture they will replace existing methods on the + // relevant objects or classes. + // + // new functions which don't exist yet can also be added. + ChatBoxes: { + chatBoxMayBeShown(chatbox) { + const _converse = this.__super__._converse; + + if (chatbox.get('id') === 'controlbox') { + return true; + } + + if (_converse.isSingleton()) { + const any_chats_visible = _converse.chatboxes.filter(cb => cb.get('id') != 'controlbox').filter(cb => !cb.get('hidden')).length > 0; + + if (any_chats_visible) { + return !chatbox.get('hidden'); + } else { + return true; + } + } else { + return this.__super__.chatBoxMayBeShown.apply(this, arguments); + } + }, + + createChatBox(jid, attrs) { + /* Make sure new chat boxes are hidden by default. */ + const _converse = this.__super__._converse; + + if (_converse.isSingleton()) { + attrs = attrs || {}; + attrs.hidden = true; + } + + return this.__super__.createChatBox.call(this, jid, attrs); + } + + }, + ChatBoxView: { + shouldShowOnTextMessage() { + const _converse = this.__super__._converse; + + if (_converse.isSingleton()) { + return false; + } else { + return this.__super__.shouldShowOnTextMessage.apply(this, arguments); + } + }, + + _show(focus) { + /* We only have one chat visible at any one + * time. So before opening a chat, we make sure all other + * chats are hidden. + */ + const _converse = this.__super__._converse; + + if (_converse.isSingleton()) { + _.each(this.__super__._converse.chatboxviews.xget(this.model.get('id')), hideChat); + + u.safeSave(this.model, { + 'hidden': false + }); + } + + return this.__super__._show.apply(this, arguments); + } + + }, + ChatRoomView: { + show(focus) { + const _converse = this.__super__._converse; + + if (_converse.isSingleton()) { + _.each(this.__super__._converse.chatboxviews.xget(this.model.get('id')), hideChat); + + u.safeSave(this.model, { + 'hidden': false + }); + } + + return this.__super__.show.apply(this, arguments); + } + + } + } + }); +}); + +/***/ }), + +/***/ "./src/converse.js": +/*!*************************!*\ + !*** ./src/converse.js ***! + \*************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*global define */ +if (true) { + // The section below determines which plugins will be included in a build + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! @converse/headless/converse-core */ "./src/headless/converse-core.js"), + /* START: Removable components + * -------------------- + * Any of the following components may be removed if they're not needed. + */ + __webpack_require__(/*! converse-autocomplete */ "./src/converse-autocomplete.js"), __webpack_require__(/*! converse-bookmarks */ "./src/converse-bookmarks.js"), // XEP-0048 Bookmarks + __webpack_require__(/*! converse-caps */ "./src/converse-caps.js"), // XEP-0115 Entity Capabilities + __webpack_require__(/*! converse-chatview */ "./src/converse-chatview.js"), // Renders standalone chat boxes for single user chat + __webpack_require__(/*! converse-controlbox */ "./src/converse-controlbox.js"), // The control box + __webpack_require__(/*! converse-dragresize */ "./src/converse-dragresize.js"), // Allows chat boxes to be resized by dragging them + __webpack_require__(/*! converse-embedded */ "./src/converse-embedded.js"), __webpack_require__(/*! converse-fullscreen */ "./src/converse-fullscreen.js"), __webpack_require__(/*! converse-push */ "./src/converse-push.js"), // XEP-0357 Push Notifications + __webpack_require__(/*! converse-headline */ "./src/converse-headline.js"), // Support for headline messages + __webpack_require__(/*! @converse/headless/converse-mam */ "./src/headless/converse-mam.js"), // XEP-0313 Message Archive Management + __webpack_require__(/*! converse-minimize */ "./src/converse-minimize.js"), // Allows chat boxes to be minimized + __webpack_require__(/*! @converse/headless/converse-muc */ "./src/headless/converse-muc.js"), // XEP-0045 Multi-user chat + __webpack_require__(/*! converse-muc-views */ "./src/converse-muc-views.js"), // Views related to MUC + __webpack_require__(/*! converse-notification */ "./src/converse-notification.js"), // HTML5 Notifications + __webpack_require__(/*! converse-omemo */ "./src/converse-omemo.js"), __webpack_require__(/*! @converse/headless/converse-ping */ "./src/headless/converse-ping.js"), // XEP-0199 XMPP Ping + __webpack_require__(/*! converse-register */ "./src/converse-register.js"), // XEP-0077 In-band registration + __webpack_require__(/*! converse-roomslist */ "./src/converse-roomslist.js"), // Show currently open chat rooms + __webpack_require__(/*! converse-roster */ "./src/converse-roster.js"), __webpack_require__(/*! @converse/headless/converse-vcard */ "./src/headless/converse-vcard.js")], __WEBPACK_AMD_DEFINE_RESULT__ = (function (converse) { + return converse; + }).apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), + __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); +} + +/***/ }), + +/***/ "./src/headless/3rdparty/lodash.fp.js": +/*!********************************************!*\ + !*** ./src/headless/3rdparty/lodash.fp.js ***! + \********************************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +(function webpackUniversalModuleDefinition(root, factory) { + if (true) module.exports = factory();else {} +})(this, function () { + return ( + /******/ + function (modules) { + // webpackBootstrap + + /******/ + // The module cache + + /******/ + var installedModules = {}; + /******/ + // The require function + + /******/ + + function __webpack_require__(moduleId) { + /******/ + // Check if module is in cache + + /******/ + if (installedModules[moduleId]) + /******/ + return installedModules[moduleId].exports; + /******/ + // Create a new module (and put it into the cache) + + /******/ + + var module = installedModules[moduleId] = { + /******/ + exports: {}, + + /******/ + id: moduleId, + + /******/ + loaded: false + /******/ + + }; + /******/ + // Execute the module function + + /******/ + + modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); + /******/ + // Flag the module as loaded + + /******/ + + module.loaded = true; + /******/ + // Return the exports of the module + + /******/ + + return module.exports; + /******/ + } + /******/ + // expose the modules object (__webpack_modules__) + + /******/ + + + __webpack_require__.m = modules; + /******/ + // expose the module cache + + /******/ + + __webpack_require__.c = installedModules; + /******/ + // __webpack_public_path__ + + /******/ + + __webpack_require__.p = ""; + /******/ + // Load entry module and return exports + + /******/ + + return __webpack_require__(0); + /******/ + }( + /************************************************************************/ + + /******/ + [ + /* 0 */ + + /***/ + function (module, exports, __webpack_require__) { + var baseConvert = __webpack_require__(1); + /** + * Converts `lodash` to an immutable auto-curried iteratee-first data-last + * version with conversion `options` applied. + * + * @param {Function} lodash The lodash function to convert. + * @param {Object} [options] The options object. See `baseConvert` for more details. + * @returns {Function} Returns the converted `lodash`. + */ + + + function browserConvert(lodash, options) { + return baseConvert(lodash, lodash, options); + } + + if (typeof _ == 'function' && typeof _.runInContext == 'function') { + // XXX: Customization in order to be able to run both _ and fp in the + // non-AMD usecase. + fp = browserConvert(_.runInContext()); + } + + module.exports = browserConvert; + /***/ + }, + /* 1 */ + + /***/ + function (module, exports, __webpack_require__) { + var mapping = __webpack_require__(2), + fallbackHolder = __webpack_require__(3); + /** Built-in value reference. */ + + + var push = Array.prototype.push; + /** + * Creates a function, with an arity of `n`, that invokes `func` with the + * arguments it receives. + * + * @private + * @param {Function} func The function to wrap. + * @param {number} n The arity of the new function. + * @returns {Function} Returns the new function. + */ + + function baseArity(func, n) { + return n == 2 ? function (a, b) { + return func.apply(undefined, arguments); + } : function (a) { + return func.apply(undefined, arguments); + }; + } + /** + * Creates a function that invokes `func`, with up to `n` arguments, ignoring + * any additional arguments. + * + * @private + * @param {Function} func The function to cap arguments for. + * @param {number} n The arity cap. + * @returns {Function} Returns the new function. + */ + + + function baseAry(func, n) { + return n == 2 ? function (a, b) { + return func(a, b); + } : function (a) { + return func(a); + }; + } + /** + * Creates a clone of `array`. + * + * @private + * @param {Array} array The array to clone. + * @returns {Array} Returns the cloned array. + */ + + + function cloneArray(array) { + var length = array ? array.length : 0, + result = Array(length); + + while (length--) { + result[length] = array[length]; + } + + return result; + } + /** + * Creates a function that clones a given object using the assignment `func`. + * + * @private + * @param {Function} func The assignment function. + * @returns {Function} Returns the new cloner function. + */ + + + function createCloner(func) { + return function (object) { + return func({}, object); + }; + } + /** + * A specialized version of `_.spread` which flattens the spread array into + * the arguments of the invoked `func`. + * + * @private + * @param {Function} func The function to spread arguments over. + * @param {number} start The start position of the spread. + * @returns {Function} Returns the new function. + */ + + + function flatSpread(func, start) { + return function () { + var length = arguments.length, + lastIndex = length - 1, + args = Array(length); + + while (length--) { + args[length] = arguments[length]; + } + + var array = args[start], + otherArgs = args.slice(0, start); + + if (array) { + push.apply(otherArgs, array); + } + + if (start != lastIndex) { + push.apply(otherArgs, args.slice(start + 1)); + } + + return func.apply(this, otherArgs); + }; + } + /** + * Creates a function that wraps `func` and uses `cloner` to clone the first + * argument it receives. + * + * @private + * @param {Function} func The function to wrap. + * @param {Function} cloner The function to clone arguments. + * @returns {Function} Returns the new immutable function. + */ + + + function wrapImmutable(func, cloner) { + return function () { + var length = arguments.length; + + if (!length) { + return; + } + + var args = Array(length); + + while (length--) { + args[length] = arguments[length]; + } + + var result = args[0] = cloner.apply(undefined, args); + func.apply(undefined, args); + return result; + }; + } + /** + * The base implementation of `convert` which accepts a `util` object of methods + * required to perform conversions. + * + * @param {Object} util The util object. + * @param {string} name The name of the function to convert. + * @param {Function} func The function to convert. + * @param {Object} [options] The options object. + * @param {boolean} [options.cap=true] Specify capping iteratee arguments. + * @param {boolean} [options.curry=true] Specify currying. + * @param {boolean} [options.fixed=true] Specify fixed arity. + * @param {boolean} [options.immutable=true] Specify immutable operations. + * @param {boolean} [options.rearg=true] Specify rearranging arguments. + * @returns {Function|Object} Returns the converted function or object. + */ + + + function baseConvert(util, name, func, options) { + var setPlaceholder, + isLib = typeof name == 'function', + isObj = name === Object(name); + + if (isObj) { + options = func; + func = name; + name = undefined; + } + + if (func == null) { + throw new TypeError(); + } + + options || (options = {}); + var config = { + 'cap': 'cap' in options ? options.cap : true, + 'curry': 'curry' in options ? options.curry : true, + 'fixed': 'fixed' in options ? options.fixed : true, + 'immutable': 'immutable' in options ? options.immutable : true, + 'rearg': 'rearg' in options ? options.rearg : true + }; + var forceCurry = 'curry' in options && options.curry, + forceFixed = 'fixed' in options && options.fixed, + forceRearg = 'rearg' in options && options.rearg, + placeholder = isLib ? func : fallbackHolder, + pristine = isLib ? func.runInContext() : undefined; + var helpers = isLib ? func : { + 'ary': util.ary, + 'assign': util.assign, + 'clone': util.clone, + 'curry': util.curry, + 'forEach': util.forEach, + 'isArray': util.isArray, + 'isFunction': util.isFunction, + 'iteratee': util.iteratee, + 'keys': util.keys, + 'rearg': util.rearg, + 'toInteger': util.toInteger, + 'toPath': util.toPath + }; + var ary = helpers.ary, + assign = helpers.assign, + clone = helpers.clone, + curry = helpers.curry, + each = helpers.forEach, + isArray = helpers.isArray, + isFunction = helpers.isFunction, + keys = helpers.keys, + rearg = helpers.rearg, + toInteger = helpers.toInteger, + toPath = helpers.toPath; + var aryMethodKeys = keys(mapping.aryMethod); + var wrappers = { + 'castArray': function castArray(_castArray) { + return function () { + var value = arguments[0]; + return isArray(value) ? _castArray(cloneArray(value)) : _castArray.apply(undefined, arguments); + }; + }, + 'iteratee': function iteratee(_iteratee) { + return function () { + var func = arguments[0], + arity = arguments[1], + result = _iteratee(func, arity), + length = result.length; + + if (config.cap && typeof arity == 'number') { + arity = arity > 2 ? arity - 2 : 1; + return length && length <= arity ? result : baseAry(result, arity); + } + + return result; + }; + }, + 'mixin': function mixin(_mixin) { + return function (source) { + var func = this; + + if (!isFunction(func)) { + return _mixin(func, Object(source)); + } + + var pairs = []; + each(keys(source), function (key) { + if (isFunction(source[key])) { + pairs.push([key, func.prototype[key]]); + } + }); + + _mixin(func, Object(source)); + + each(pairs, function (pair) { + var value = pair[1]; + + if (isFunction(value)) { + func.prototype[pair[0]] = value; + } else { + delete func.prototype[pair[0]]; + } + }); + return func; + }; + }, + 'nthArg': function nthArg(_nthArg) { + return function (n) { + var arity = n < 0 ? 1 : toInteger(n) + 1; + return curry(_nthArg(n), arity); + }; + }, + 'rearg': function rearg(_rearg) { + return function (func, indexes) { + var arity = indexes ? indexes.length : 0; + return curry(_rearg(func, indexes), arity); + }; + }, + 'runInContext': function runInContext(_runInContext) { + return function (context) { + return baseConvert(util, _runInContext(context), options); + }; + } + }; + /*--------------------------------------------------------------------------*/ + + /** + * Casts `func` to a function with an arity capped iteratee if needed. + * + * @private + * @param {string} name The name of the function to inspect. + * @param {Function} func The function to inspect. + * @returns {Function} Returns the cast function. + */ + + function castCap(name, func) { + if (config.cap) { + var indexes = mapping.iterateeRearg[name]; + + if (indexes) { + return iterateeRearg(func, indexes); + } + + var n = !isLib && mapping.iterateeAry[name]; + + if (n) { + return iterateeAry(func, n); + } + } + + return func; + } + /** + * Casts `func` to a curried function if needed. + * + * @private + * @param {string} name The name of the function to inspect. + * @param {Function} func The function to inspect. + * @param {number} n The arity of `func`. + * @returns {Function} Returns the cast function. + */ + + + function castCurry(name, func, n) { + return forceCurry || config.curry && n > 1 ? curry(func, n) : func; + } + /** + * Casts `func` to a fixed arity function if needed. + * + * @private + * @param {string} name The name of the function to inspect. + * @param {Function} func The function to inspect. + * @param {number} n The arity cap. + * @returns {Function} Returns the cast function. + */ + + + function castFixed(name, func, n) { + if (config.fixed && (forceFixed || !mapping.skipFixed[name])) { + var data = mapping.methodSpread[name], + start = data && data.start; + return start === undefined ? ary(func, n) : flatSpread(func, start); + } + + return func; + } + /** + * Casts `func` to an rearged function if needed. + * + * @private + * @param {string} name The name of the function to inspect. + * @param {Function} func The function to inspect. + * @param {number} n The arity of `func`. + * @returns {Function} Returns the cast function. + */ + + + function castRearg(name, func, n) { + return config.rearg && n > 1 && (forceRearg || !mapping.skipRearg[name]) ? rearg(func, mapping.methodRearg[name] || mapping.aryRearg[n]) : func; + } + /** + * Creates a clone of `object` by `path`. + * + * @private + * @param {Object} object The object to clone. + * @param {Array|string} path The path to clone by. + * @returns {Object} Returns the cloned object. + */ + + + function cloneByPath(object, path) { + path = toPath(path); + var index = -1, + length = path.length, + lastIndex = length - 1, + result = clone(Object(object)), + nested = result; + + while (nested != null && ++index < length) { + var key = path[index], + value = nested[key]; + + if (value != null) { + nested[path[index]] = clone(index == lastIndex ? value : Object(value)); + } + + nested = nested[key]; + } + + return result; + } + /** + * Converts `lodash` to an immutable auto-curried iteratee-first data-last + * version with conversion `options` applied. + * + * @param {Object} [options] The options object. See `baseConvert` for more details. + * @returns {Function} Returns the converted `lodash`. + */ + + + function convertLib(options) { + return _.runInContext.convert(options)(undefined); + } + /** + * Create a converter function for `func` of `name`. + * + * @param {string} name The name of the function to convert. + * @param {Function} func The function to convert. + * @returns {Function} Returns the new converter function. + */ + + + function createConverter(name, func) { + var realName = mapping.aliasToReal[name] || name, + methodName = mapping.remap[realName] || realName, + oldOptions = options; + return function (options) { + var newUtil = isLib ? pristine : helpers, + newFunc = isLib ? pristine[methodName] : func, + newOptions = assign(assign({}, oldOptions), options); + return baseConvert(newUtil, realName, newFunc, newOptions); + }; + } + /** + * Creates a function that wraps `func` to invoke its iteratee, with up to `n` + * arguments, ignoring any additional arguments. + * + * @private + * @param {Function} func The function to cap iteratee arguments for. + * @param {number} n The arity cap. + * @returns {Function} Returns the new function. + */ + + + function iterateeAry(func, n) { + return overArg(func, function (func) { + return typeof func == 'function' ? baseAry(func, n) : func; + }); + } + /** + * Creates a function that wraps `func` to invoke its iteratee with arguments + * arranged according to the specified `indexes` where the argument value at + * the first index is provided as the first argument, the argument value at + * the second index is provided as the second argument, and so on. + * + * @private + * @param {Function} func The function to rearrange iteratee arguments for. + * @param {number[]} indexes The arranged argument indexes. + * @returns {Function} Returns the new function. + */ + + + function iterateeRearg(func, indexes) { + return overArg(func, function (func) { + var n = indexes.length; + return baseArity(rearg(baseAry(func, n), indexes), n); + }); + } + /** + * Creates a function that invokes `func` with its first argument transformed. + * + * @private + * @param {Function} func The function to wrap. + * @param {Function} transform The argument transform. + * @returns {Function} Returns the new function. + */ + + + function overArg(func, transform) { + return function () { + var length = arguments.length; + + if (!length) { + return func(); + } + + var args = Array(length); + + while (length--) { + args[length] = arguments[length]; + } + + var index = config.rearg ? 0 : length - 1; + args[index] = transform(args[index]); + return func.apply(undefined, args); + }; + } + /** + * Creates a function that wraps `func` and applys the conversions + * rules by `name`. + * + * @private + * @param {string} name The name of the function to wrap. + * @param {Function} func The function to wrap. + * @returns {Function} Returns the converted function. + */ + + + function wrap(name, func) { + var result, + realName = mapping.aliasToReal[name] || name, + wrapped = func, + wrapper = wrappers[realName]; + + if (wrapper) { + wrapped = wrapper(func); + } else if (config.immutable) { + if (mapping.mutate.array[realName]) { + wrapped = wrapImmutable(func, cloneArray); + } else if (mapping.mutate.object[realName]) { + wrapped = wrapImmutable(func, createCloner(func)); + } else if (mapping.mutate.set[realName]) { + wrapped = wrapImmutable(func, cloneByPath); + } + } + + each(aryMethodKeys, function (aryKey) { + each(mapping.aryMethod[aryKey], function (otherName) { + if (realName == otherName) { + var data = mapping.methodSpread[realName], + afterRearg = data && data.afterRearg; + result = afterRearg ? castFixed(realName, castRearg(realName, wrapped, aryKey), aryKey) : castRearg(realName, castFixed(realName, wrapped, aryKey), aryKey); + result = castCap(realName, result); + result = castCurry(realName, result, aryKey); + return false; + } + }); + return !result; + }); + result || (result = wrapped); + + if (result == func) { + result = forceCurry ? curry(result, 1) : function () { + return func.apply(this, arguments); + }; + } + + result.convert = createConverter(realName, func); + + if (mapping.placeholder[realName]) { + setPlaceholder = true; + result.placeholder = func.placeholder = placeholder; + } + + return result; + } + /*--------------------------------------------------------------------------*/ + + + if (!isObj) { + return wrap(name, func); + } + + var _ = func; // Convert methods by ary cap. + + var pairs = []; + each(aryMethodKeys, function (aryKey) { + each(mapping.aryMethod[aryKey], function (key) { + var func = _[mapping.remap[key] || key]; + + if (func) { + pairs.push([key, wrap(key, func)]); + } + }); + }); // Convert remaining methods. + + each(keys(_), function (key) { + var func = _[key]; + + if (typeof func == 'function') { + var length = pairs.length; + + while (length--) { + if (pairs[length][0] == key) { + return; + } + } + + func.convert = createConverter(key, func); + pairs.push([key, func]); + } + }); // Assign to `_` leaving `_.prototype` unchanged to allow chaining. + + each(pairs, function (pair) { + _[pair[0]] = pair[1]; + }); + _.convert = convertLib; + + if (setPlaceholder) { + _.placeholder = placeholder; + } // Assign aliases. + + + each(keys(_), function (key) { + each(mapping.realToAlias[key] || [], function (alias) { + _[alias] = _[key]; + }); + }); + return _; + } + + module.exports = baseConvert; + /***/ + }, + /* 2 */ + + /***/ + function (module, exports) { + /** Used to map aliases to their real names. */ + exports.aliasToReal = { + // Lodash aliases. + 'each': 'forEach', + 'eachRight': 'forEachRight', + 'entries': 'toPairs', + 'entriesIn': 'toPairsIn', + 'extend': 'assignIn', + 'extendAll': 'assignInAll', + 'extendAllWith': 'assignInAllWith', + 'extendWith': 'assignInWith', + 'first': 'head', + // Methods that are curried variants of others. + 'conforms': 'conformsTo', + 'matches': 'isMatch', + 'property': 'get', + // Ramda aliases. + '__': 'placeholder', + 'F': 'stubFalse', + 'T': 'stubTrue', + 'all': 'every', + 'allPass': 'overEvery', + 'always': 'constant', + 'any': 'some', + 'anyPass': 'overSome', + 'apply': 'spread', + 'assoc': 'set', + 'assocPath': 'set', + 'complement': 'negate', + 'compose': 'flowRight', + 'contains': 'includes', + 'dissoc': 'unset', + 'dissocPath': 'unset', + 'dropLast': 'dropRight', + 'dropLastWhile': 'dropRightWhile', + 'equals': 'isEqual', + 'identical': 'eq', + 'indexBy': 'keyBy', + 'init': 'initial', + 'invertObj': 'invert', + 'juxt': 'over', + 'omitAll': 'omit', + 'nAry': 'ary', + 'path': 'get', + 'pathEq': 'matchesProperty', + 'pathOr': 'getOr', + 'paths': 'at', + 'pickAll': 'pick', + 'pipe': 'flow', + 'pluck': 'map', + 'prop': 'get', + 'propEq': 'matchesProperty', + 'propOr': 'getOr', + 'props': 'at', + 'symmetricDifference': 'xor', + 'symmetricDifferenceBy': 'xorBy', + 'symmetricDifferenceWith': 'xorWith', + 'takeLast': 'takeRight', + 'takeLastWhile': 'takeRightWhile', + 'unapply': 'rest', + 'unnest': 'flatten', + 'useWith': 'overArgs', + 'where': 'conformsTo', + 'whereEq': 'isMatch', + 'zipObj': 'zipObject' + }; + /** Used to map ary to method names. */ + + exports.aryMethod = { + '1': ['assignAll', 'assignInAll', 'attempt', 'castArray', 'ceil', 'create', 'curry', 'curryRight', 'defaultsAll', 'defaultsDeepAll', 'floor', 'flow', 'flowRight', 'fromPairs', 'invert', 'iteratee', 'memoize', 'method', 'mergeAll', 'methodOf', 'mixin', 'nthArg', 'over', 'overEvery', 'overSome', 'rest', 'reverse', 'round', 'runInContext', 'spread', 'template', 'trim', 'trimEnd', 'trimStart', 'uniqueId', 'words', 'zipAll'], + '2': ['add', 'after', 'ary', 'assign', 'assignAllWith', 'assignIn', 'assignInAllWith', 'at', 'before', 'bind', 'bindAll', 'bindKey', 'chunk', 'cloneDeepWith', 'cloneWith', 'concat', 'conformsTo', 'countBy', 'curryN', 'curryRightN', 'debounce', 'defaults', 'defaultsDeep', 'defaultTo', 'delay', 'difference', 'divide', 'drop', 'dropRight', 'dropRightWhile', 'dropWhile', 'endsWith', 'eq', 'every', 'filter', 'find', 'findIndex', 'findKey', 'findLast', 'findLastIndex', 'findLastKey', 'flatMap', 'flatMapDeep', 'flattenDepth', 'forEach', 'forEachRight', 'forIn', 'forInRight', 'forOwn', 'forOwnRight', 'get', 'groupBy', 'gt', 'gte', 'has', 'hasIn', 'includes', 'indexOf', 'intersection', 'invertBy', 'invoke', 'invokeMap', 'isEqual', 'isMatch', 'join', 'keyBy', 'lastIndexOf', 'lt', 'lte', 'map', 'mapKeys', 'mapValues', 'matchesProperty', 'maxBy', 'meanBy', 'merge', 'mergeAllWith', 'minBy', 'multiply', 'nth', 'omit', 'omitBy', 'overArgs', 'pad', 'padEnd', 'padStart', 'parseInt', 'partial', 'partialRight', 'partition', 'pick', 'pickBy', 'propertyOf', 'pull', 'pullAll', 'pullAt', 'random', 'range', 'rangeRight', 'rearg', 'reject', 'remove', 'repeat', 'restFrom', 'result', 'sampleSize', 'some', 'sortBy', 'sortedIndex', 'sortedIndexOf', 'sortedLastIndex', 'sortedLastIndexOf', 'sortedUniqBy', 'split', 'spreadFrom', 'startsWith', 'subtract', 'sumBy', 'take', 'takeRight', 'takeRightWhile', 'takeWhile', 'tap', 'throttle', 'thru', 'times', 'trimChars', 'trimCharsEnd', 'trimCharsStart', 'truncate', 'union', 'uniqBy', 'uniqWith', 'unset', 'unzipWith', 'without', 'wrap', 'xor', 'zip', 'zipObject', 'zipObjectDeep'], + '3': ['assignInWith', 'assignWith', 'clamp', 'differenceBy', 'differenceWith', 'findFrom', 'findIndexFrom', 'findLastFrom', 'findLastIndexFrom', 'getOr', 'includesFrom', 'indexOfFrom', 'inRange', 'intersectionBy', 'intersectionWith', 'invokeArgs', 'invokeArgsMap', 'isEqualWith', 'isMatchWith', 'flatMapDepth', 'lastIndexOfFrom', 'mergeWith', 'orderBy', 'padChars', 'padCharsEnd', 'padCharsStart', 'pullAllBy', 'pullAllWith', 'rangeStep', 'rangeStepRight', 'reduce', 'reduceRight', 'replace', 'set', 'slice', 'sortedIndexBy', 'sortedLastIndexBy', 'transform', 'unionBy', 'unionWith', 'update', 'xorBy', 'xorWith', 'zipWith'], + '4': ['fill', 'setWith', 'updateWith'] + }; + /** Used to map ary to rearg configs. */ + + exports.aryRearg = { + '2': [1, 0], + '3': [2, 0, 1], + '4': [3, 2, 0, 1] + }; + /** Used to map method names to their iteratee ary. */ + + exports.iterateeAry = { + 'dropRightWhile': 1, + 'dropWhile': 1, + 'every': 1, + 'filter': 1, + 'find': 1, + 'findFrom': 1, + 'findIndex': 1, + 'findIndexFrom': 1, + 'findKey': 1, + 'findLast': 1, + 'findLastFrom': 1, + 'findLastIndex': 1, + 'findLastIndexFrom': 1, + 'findLastKey': 1, + 'flatMap': 1, + 'flatMapDeep': 1, + 'flatMapDepth': 1, + 'forEach': 1, + 'forEachRight': 1, + 'forIn': 1, + 'forInRight': 1, + 'forOwn': 1, + 'forOwnRight': 1, + 'map': 1, + 'mapKeys': 1, + 'mapValues': 1, + 'partition': 1, + 'reduce': 2, + 'reduceRight': 2, + 'reject': 1, + 'remove': 1, + 'some': 1, + 'takeRightWhile': 1, + 'takeWhile': 1, + 'times': 1, + 'transform': 2 + }; + /** Used to map method names to iteratee rearg configs. */ + + exports.iterateeRearg = { + 'mapKeys': [1], + 'reduceRight': [1, 0] + }; + /** Used to map method names to rearg configs. */ + + exports.methodRearg = { + 'assignInAllWith': [1, 0], + 'assignInWith': [1, 2, 0], + 'assignAllWith': [1, 0], + 'assignWith': [1, 2, 0], + 'differenceBy': [1, 2, 0], + 'differenceWith': [1, 2, 0], + 'getOr': [2, 1, 0], + 'intersectionBy': [1, 2, 0], + 'intersectionWith': [1, 2, 0], + 'isEqualWith': [1, 2, 0], + 'isMatchWith': [2, 1, 0], + 'mergeAllWith': [1, 0], + 'mergeWith': [1, 2, 0], + 'padChars': [2, 1, 0], + 'padCharsEnd': [2, 1, 0], + 'padCharsStart': [2, 1, 0], + 'pullAllBy': [2, 1, 0], + 'pullAllWith': [2, 1, 0], + 'rangeStep': [1, 2, 0], + 'rangeStepRight': [1, 2, 0], + 'setWith': [3, 1, 2, 0], + 'sortedIndexBy': [2, 1, 0], + 'sortedLastIndexBy': [2, 1, 0], + 'unionBy': [1, 2, 0], + 'unionWith': [1, 2, 0], + 'updateWith': [3, 1, 2, 0], + 'xorBy': [1, 2, 0], + 'xorWith': [1, 2, 0], + 'zipWith': [1, 2, 0] + }; + /** Used to map method names to spread configs. */ + + exports.methodSpread = { + 'assignAll': { + 'start': 0 + }, + 'assignAllWith': { + 'start': 0 + }, + 'assignInAll': { + 'start': 0 + }, + 'assignInAllWith': { + 'start': 0 + }, + 'defaultsAll': { + 'start': 0 + }, + 'defaultsDeepAll': { + 'start': 0 + }, + 'invokeArgs': { + 'start': 2 + }, + 'invokeArgsMap': { + 'start': 2 + }, + 'mergeAll': { + 'start': 0 + }, + 'mergeAllWith': { + 'start': 0 + }, + 'partial': { + 'start': 1 + }, + 'partialRight': { + 'start': 1 + }, + 'without': { + 'start': 1 + }, + 'zipAll': { + 'start': 0 + } + }; + /** Used to identify methods which mutate arrays or objects. */ + + exports.mutate = { + 'array': { + 'fill': true, + 'pull': true, + 'pullAll': true, + 'pullAllBy': true, + 'pullAllWith': true, + 'pullAt': true, + 'remove': true, + 'reverse': true + }, + 'object': { + 'assign': true, + 'assignAll': true, + 'assignAllWith': true, + 'assignIn': true, + 'assignInAll': true, + 'assignInAllWith': true, + 'assignInWith': true, + 'assignWith': true, + 'defaults': true, + 'defaultsAll': true, + 'defaultsDeep': true, + 'defaultsDeepAll': true, + 'merge': true, + 'mergeAll': true, + 'mergeAllWith': true, + 'mergeWith': true + }, + 'set': { + 'set': true, + 'setWith': true, + 'unset': true, + 'update': true, + 'updateWith': true + } + }; + /** Used to track methods with placeholder support */ + + exports.placeholder = { + 'bind': true, + 'bindKey': true, + 'curry': true, + 'curryRight': true, + 'partial': true, + 'partialRight': true + }; + /** Used to map real names to their aliases. */ + + exports.realToAlias = function () { + var hasOwnProperty = Object.prototype.hasOwnProperty, + object = exports.aliasToReal, + result = {}; + + for (var key in object) { + var value = object[key]; + + if (hasOwnProperty.call(result, value)) { + result[value].push(key); + } else { + result[value] = [key]; + } + } + + return result; + }(); + /** Used to map method names to other names. */ + + + exports.remap = { + 'assignAll': 'assign', + 'assignAllWith': 'assignWith', + 'assignInAll': 'assignIn', + 'assignInAllWith': 'assignInWith', + 'curryN': 'curry', + 'curryRightN': 'curryRight', + 'defaultsAll': 'defaults', + 'defaultsDeepAll': 'defaultsDeep', + 'findFrom': 'find', + 'findIndexFrom': 'findIndex', + 'findLastFrom': 'findLast', + 'findLastIndexFrom': 'findLastIndex', + 'getOr': 'get', + 'includesFrom': 'includes', + 'indexOfFrom': 'indexOf', + 'invokeArgs': 'invoke', + 'invokeArgsMap': 'invokeMap', + 'lastIndexOfFrom': 'lastIndexOf', + 'mergeAll': 'merge', + 'mergeAllWith': 'mergeWith', + 'padChars': 'pad', + 'padCharsEnd': 'padEnd', + 'padCharsStart': 'padStart', + 'propertyOf': 'get', + 'rangeStep': 'range', + 'rangeStepRight': 'rangeRight', + 'restFrom': 'rest', + 'spreadFrom': 'spread', + 'trimChars': 'trim', + 'trimCharsEnd': 'trimEnd', + 'trimCharsStart': 'trimStart', + 'zipAll': 'zip' + }; + /** Used to track methods that skip fixing their arity. */ + + exports.skipFixed = { + 'castArray': true, + 'flow': true, + 'flowRight': true, + 'iteratee': true, + 'mixin': true, + 'rearg': true, + 'runInContext': true + }; + /** Used to track methods that skip rearranging arguments. */ + + exports.skipRearg = { + 'add': true, + 'assign': true, + 'assignIn': true, + 'bind': true, + 'bindKey': true, + 'concat': true, + 'difference': true, + 'divide': true, + 'eq': true, + 'gt': true, + 'gte': true, + 'isEqual': true, + 'lt': true, + 'lte': true, + 'matchesProperty': true, + 'merge': true, + 'multiply': true, + 'overArgs': true, + 'partial': true, + 'partialRight': true, + 'propertyOf': true, + 'random': true, + 'range': true, + 'rangeRight': true, + 'subtract': true, + 'zip': true, + 'zipObject': true, + 'zipObjectDeep': true + }; + /***/ + }, + /* 3 */ + + /***/ + function (module, exports) { + /** + * The default argument placeholder value for methods. + * + * @type {Object} + */ + module.exports = {}; + /***/ + } + /******/ + ]) + ); +}); + +; + +/***/ }), + +/***/ "./src/headless/backbone.noconflict.js": +/*!*********************************************!*\ + !*** ./src/headless/backbone.noconflict.js ***! + \*********************************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*global define */ +!(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! backbone */ "./node_modules/backbone/backbone.js")], __WEBPACK_AMD_DEFINE_RESULT__ = (function (Backbone) { + return Backbone.noConflict(); +}).apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), + __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + +/***/ }), + +/***/ "./src/headless/converse-chatboxes.js": +/*!********************************************!*\ + !*** ./src/headless/converse-chatboxes.js ***! + \********************************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;// Converse.js +// http://conversejs.org +// +// Copyright (c) 2012-2018, the Converse.js developers +// Licensed under the Mozilla Public License (MPLv2) +(function (root, factory) { + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! ./converse-core */ "./src/headless/converse-core.js"), __webpack_require__(/*! filesize */ "./node_modules/filesize/lib/filesize.js"), __webpack_require__(/*! ./utils/form */ "./src/headless/utils/form.js"), __webpack_require__(/*! ./utils/emoji */ "./src/headless/utils/emoji.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), + __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? + (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), + __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); +})(this, function (converse, filesize) { + "use strict"; + + const _converse$env = converse.env, + $msg = _converse$env.$msg, + Backbone = _converse$env.Backbone, + Promise = _converse$env.Promise, + Strophe = _converse$env.Strophe, + b64_sha1 = _converse$env.b64_sha1, + moment = _converse$env.moment, + sizzle = _converse$env.sizzle, + utils = _converse$env.utils, + _ = _converse$env._; + const u = converse.env.utils; + Strophe.addNamespace('MESSAGE_CORRECT', 'urn:xmpp:message-correct:0'); + Strophe.addNamespace('REFERENCE', 'urn:xmpp:reference:0'); + converse.plugins.add('converse-chatboxes', { + dependencies: ["converse-roster", "converse-vcard"], + + initialize() { + /* The initialize function gets called as soon as the plugin is + * loaded by converse.js's plugin machinery. + */ + const _converse = this._converse, + __ = _converse.__; // Configuration values for this plugin + // ==================================== + // Refer to docs/source/configuration.rst for explanations of these + // configuration settings. + + _converse.api.settings.update({ + 'auto_join_private_chats': [], + 'filter_by_resource': false, + 'forward_messages': false, + 'send_chat_state_notifications': true + }); + + _converse.api.promises.add(['chatBoxesFetched', 'chatBoxesInitialized', 'privateChatsAutoJoined']); + + function openChat(jid) { + if (!utils.isValidJID(jid)) { + return _converse.log(`Invalid JID "${jid}" provided in URL fragment`, Strophe.LogLevel.WARN); + } + + _converse.api.chats.open(jid); + } + + _converse.router.route('converse/chat?jid=:jid', openChat); + + _converse.Message = Backbone.Model.extend({ + defaults() { + return { + 'msgid': _converse.connection.getUniqueId(), + 'time': moment().format() + }; + }, + + initialize() { + this.setVCard(); + + if (this.get('file')) { + this.on('change:put', this.uploadFile, this); + + if (!_.includes([_converse.SUCCESS, _converse.FAILURE], this.get('upload'))) { + this.getRequestSlotURL(); + } + } + + if (this.isOnlyChatStateNotification()) { + window.setTimeout(this.destroy.bind(this), 20000); + } + }, + + getVCardForChatroomOccupant() { + const chatbox = this.collection.chatbox, + nick = Strophe.getResourceFromJid(this.get('from')); + + if (chatbox.get('nick') === nick) { + return _converse.xmppstatus.vcard; + } else { + let vcard; + + if (this.get('vcard_jid')) { + vcard = _converse.vcards.findWhere({ + 'jid': this.get('vcard_jid') + }); + } + + if (!vcard) { + let jid; + const occupant = chatbox.occupants.findWhere({ + 'nick': nick + }); + + if (occupant && occupant.get('jid')) { + jid = occupant.get('jid'); + this.save({ + 'vcard_jid': jid + }, { + 'silent': true + }); + } else { + jid = this.get('from'); + } + + vcard = _converse.vcards.findWhere({ + 'jid': jid + }) || _converse.vcards.create({ + 'jid': jid + }); + } + + return vcard; + } + }, + + setVCard() { + if (this.get('type') === 'error') { + return; + } else if (this.get('type') === 'groupchat') { + this.vcard = this.getVCardForChatroomOccupant(); + } else { + const jid = this.get('from'); + this.vcard = _converse.vcards.findWhere({ + 'jid': jid + }) || _converse.vcards.create({ + 'jid': jid + }); + } + }, + + isOnlyChatStateNotification() { + return u.isOnlyChatStateNotification(this); + }, + + getDisplayName() { + if (this.get('type') === 'groupchat') { + return this.get('nick'); + } else { + return this.vcard.get('fullname') || this.get('from'); + } + }, + + sendSlotRequestStanza() { + /* Send out an IQ stanza to request a file upload slot. + * + * https://xmpp.org/extensions/xep-0363.html#request + */ + const file = this.get('file'); + return new Promise((resolve, reject) => { + const iq = converse.env.$iq({ + 'from': _converse.jid, + 'to': this.get('slot_request_url'), + 'type': 'get' + }).c('request', { + 'xmlns': Strophe.NS.HTTPUPLOAD, + 'filename': file.name, + 'size': file.size, + 'content-type': file.type + }); + + _converse.connection.sendIQ(iq, resolve, reject); + }); + }, + + getRequestSlotURL() { + this.sendSlotRequestStanza().then(stanza => { + const slot = stanza.querySelector('slot'); + + if (slot) { + this.save({ + 'get': slot.querySelector('get').getAttribute('url'), + 'put': slot.querySelector('put').getAttribute('url') + }); + } else { + return this.save({ + 'type': 'error', + 'message': __("Sorry, could not determine file upload URL.") + }); + } + }).catch(e => { + _converse.log(e, Strophe.LogLevel.ERROR); + + return this.save({ + 'type': 'error', + 'message': __("Sorry, could not determine upload URL.") + }); + }); + }, + + uploadFile() { + const xhr = new XMLHttpRequest(); + + xhr.onreadystatechange = () => { + if (xhr.readyState === XMLHttpRequest.DONE) { + _converse.log("Status: " + xhr.status, Strophe.LogLevel.INFO); + + if (xhr.status === 200 || xhr.status === 201) { + this.save({ + 'upload': _converse.SUCCESS, + 'oob_url': this.get('get'), + 'message': this.get('get') + }); + } else { + xhr.onerror(); + } + } + }; + + xhr.upload.addEventListener("progress", evt => { + if (evt.lengthComputable) { + this.set('progress', evt.loaded / evt.total); + } + }, false); + + xhr.onerror = () => { + let message; + + if (xhr.responseText) { + message = __('Sorry, could not succesfully upload your file. Your server’s response: "%1$s"', xhr.responseText); + } else { + message = __('Sorry, could not succesfully upload your file.'); + } + + this.save({ + 'type': 'error', + 'upload': _converse.FAILURE, + 'message': message + }); + }; + + xhr.open('PUT', this.get('put'), true); + xhr.setRequestHeader("Content-type", this.get('file').type); + xhr.send(this.get('file')); + } + + }); + _converse.Messages = Backbone.Collection.extend({ + model: _converse.Message, + comparator: 'time' + }); + _converse.ChatBox = _converse.ModelWithVCardAndPresence.extend({ + defaults() { + return { + 'bookmarked': false, + 'chat_state': undefined, + 'num_unread': 0, + 'type': _converse.PRIVATE_CHAT_TYPE, + 'message_type': 'chat', + 'url': '', + 'hidden': _.includes(['mobile', 'fullscreen'], _converse.view_mode) + }; + }, + + initialize() { + _converse.ModelWithVCardAndPresence.prototype.initialize.apply(this, arguments); + + _converse.api.waitUntil('rosterContactsFetched').then(() => { + this.addRelatedContact(_converse.roster.findWhere({ + 'jid': this.get('jid') + })); + }); + + this.messages = new _converse.Messages(); + + const storage = _converse.config.get('storage'); + + this.messages.browserStorage = new Backbone.BrowserStorage[storage](b64_sha1(`converse.messages${this.get('jid')}${_converse.bare_jid}`)); + this.messages.chatbox = this; + this.messages.on('change:upload', message => { + if (message.get('upload') === _converse.SUCCESS) { + this.sendMessageStanza(this.createMessageStanza(message)); + } + }); + this.on('change:chat_state', this.sendChatState, this); + 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. + 'box_id': b64_sha1(this.get('jid')), + 'time_opened': this.get('time_opened') || moment().valueOf(), + 'user_id': Strophe.getNodeFromJid(this.get('jid')) + }); + }, + + addRelatedContact(contact) { + if (!_.isUndefined(contact)) { + this.contact = contact; + this.trigger('contactAdded', contact); + } + }, + + getDisplayName() { + return this.vcard.get('fullname') || this.get('jid'); + }, + + handleMessageCorrection(stanza) { + const replace = sizzle(`replace[xmlns="${Strophe.NS.MESSAGE_CORRECT}"]`, stanza).pop(); + + if (replace) { + const msgid = replace && replace.getAttribute('id') || stanza.getAttribute('id'), + message = msgid && this.messages.findWhere({ + msgid + }); + + if (!message) { + // XXX: Looks like we received a correction for a + // non-existing message, probably due to MAM. + // Not clear what can be done about this... we'll + // just create it as a separate message for now. + return false; + } + + const older_versions = message.get('older_versions') || []; + older_versions.push(message.get('message')); + message.save({ + 'message': _converse.chatboxes.getMessageBody(stanza), + 'references': this.getReferencesFromStanza(stanza), + 'older_versions': older_versions, + 'edited': moment().format() + }); + return true; + } + + return false; + }, + + createMessageStanza(message) { + /* Given a _converse.Message Backbone.Model, return the XML + * stanza that represents it. + * + * Parameters: + * (Object) message - The Backbone.Model representing the message + */ + const stanza = $msg({ + 'from': _converse.connection.jid, + 'to': this.get('jid'), + 'type': this.get('message_type'), + 'id': message.get('edited') && _converse.connection.getUniqueId() || message.get('msgid') + }).c('body').t(message.get('message')).up().c(_converse.ACTIVE, { + 'xmlns': Strophe.NS.CHATSTATES + }).up(); + + if (message.get('is_spoiler')) { + if (message.get('spoiler_hint')) { + stanza.c('spoiler', { + 'xmlns': Strophe.NS.SPOILER + }, message.get('spoiler_hint')).up(); + } else { + stanza.c('spoiler', { + 'xmlns': Strophe.NS.SPOILER + }).up(); + } + } + + (message.get('references') || []).forEach(reference => { + const attrs = { + 'xmlns': Strophe.NS.REFERENCE, + 'begin': reference.begin, + 'end': reference.end, + 'type': reference.type + }; + + if (reference.uri) { + attrs.uri = reference.uri; + } + + stanza.c('reference', attrs).up(); + }); + + if (message.get('file')) { + stanza.c('x', { + 'xmlns': Strophe.NS.OUTOFBAND + }).c('url').t(message.get('message')).up(); + } + + if (message.get('edited')) { + stanza.c('replace', { + 'xmlns': Strophe.NS.MESSAGE_CORRECT, + 'id': message.get('msgid') + }).up(); + } + + return stanza; + }, + + sendMessageStanza(stanza) { + _converse.connection.send(stanza); + + if (_converse.forward_messages) { + // Forward the message, so that other connected resources are also aware of it. + _converse.connection.send($msg({ + 'to': _converse.bare_jid, + 'type': this.get('message_type') + }).c('forwarded', { + 'xmlns': Strophe.NS.FORWARD + }).c('delay', { + 'xmns': Strophe.NS.DELAY, + 'stamp': moment().format() + }).up().cnode(stanza.tree())); + } + }, + + getOutgoingMessageAttributes(text, spoiler_hint) { + const is_spoiler = this.get('composing_spoiler'); + return _.extend(this.toJSON(), { + 'id': _converse.connection.getUniqueId(), + 'fullname': _converse.xmppstatus.get('fullname'), + 'from': _converse.bare_jid, + 'sender': 'me', + 'time': moment().format(), + 'message': text ? u.httpToGeoUri(u.shortnameToUnicode(text), _converse) : undefined, + 'is_spoiler': is_spoiler, + 'spoiler_hint': is_spoiler ? spoiler_hint : undefined, + 'type': this.get('message_type') + }); + }, + + sendMessage(attrs) { + /* Responsible for sending off a text message. + * + * Parameters: + * (Message) message - The chat message + */ + let message = this.messages.findWhere('correcting'); + + if (message) { + const older_versions = message.get('older_versions') || []; + older_versions.push(message.get('message')); + message.save({ + 'correcting': false, + 'edited': moment().format(), + 'message': attrs.message, + 'older_versions': older_versions, + 'references': attrs.references + }); + } else { + message = this.messages.create(attrs); + } + + return this.sendMessageStanza(this.createMessageStanza(message)); + }, + + sendChatState() { + /* 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. + */ + if (_converse.send_chat_state_notifications) { + _converse.connection.send($msg({ + 'to': this.get('jid'), + 'type': 'chat' + }).c(this.get('chat_state'), { + 'xmlns': Strophe.NS.CHATSTATES + }).up().c('no-store', { + 'xmlns': Strophe.NS.HINTS + }).up().c('no-permanent-store', { + 'xmlns': Strophe.NS.HINTS + })); + } + }, + + sendFiles(files) { + _converse.api.disco.supports(Strophe.NS.HTTPUPLOAD, _converse.domain).then(result => { + const item = result.pop(), + data = item.dataforms.where({ + 'FORM_TYPE': { + 'value': Strophe.NS.HTTPUPLOAD, + 'type': "hidden" + } + }).pop(), + max_file_size = window.parseInt(_.get(data, 'attributes.max-file-size.value')), + slot_request_url = _.get(item, 'id'); + + if (!slot_request_url) { + this.messages.create({ + 'message': __("Sorry, looks like file upload is not supported by your server."), + 'type': 'error' + }); + return; + } + + _.each(files, file => { + if (!window.isNaN(max_file_size) && window.parseInt(file.size) > max_file_size) { + return this.messages.create({ + 'message': __('The size of your file, %1$s, exceeds the maximum allowed by your server, which is %2$s.', file.name, filesize(max_file_size)), + 'type': 'error' + }); + } else { + this.messages.create(_.extend(this.getOutgoingMessageAttributes(), { + 'file': file, + 'progress': 0, + 'slot_request_url': slot_request_url + })); + } + }); + }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); + }, + + getReferencesFromStanza(stanza) { + const text = _.propertyOf(stanza.querySelector('body'))('textContent'); + + return sizzle(`reference[xmlns="${Strophe.NS.REFERENCE}"]`, stanza).map(ref => { + const begin = ref.getAttribute('begin'), + end = ref.getAttribute('end'); + return { + 'begin': begin, + 'end': end, + 'type': ref.getAttribute('type'), + 'value': text.slice(begin, end), + 'uri': ref.getAttribute('uri') + }; + }); + }, + + getMessageAttributesFromStanza(stanza, original_stanza) { + /* Parses a passed in message stanza and returns an object + * of attributes. + * + * Parameters: + * (XMLElement) stanza - The message stanza + * (XMLElement) delay - The node from the + * stanza, if there was one. + * (XMLElement) original_stanza - The original stanza, + * that contains the message stanza, if it was + * contained, otherwise it's the message stanza itself. + */ + const archive = sizzle(`result[xmlns="${Strophe.NS.MAM}"]`, original_stanza).pop(), + spoiler = sizzle(`spoiler[xmlns="${Strophe.NS.SPOILER}"]`, original_stanza).pop(), + delay = sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, original_stanza).pop(), + chat_state = stanza.getElementsByTagName(_converse.COMPOSING).length && _converse.COMPOSING || stanza.getElementsByTagName(_converse.PAUSED).length && _converse.PAUSED || stanza.getElementsByTagName(_converse.INACTIVE).length && _converse.INACTIVE || stanza.getElementsByTagName(_converse.ACTIVE).length && _converse.ACTIVE || stanza.getElementsByTagName(_converse.GONE).length && _converse.GONE; + + const attrs = { + 'chat_state': chat_state, + 'is_archived': !_.isNil(archive), + 'is_delayed': !_.isNil(delay), + 'is_spoiler': !_.isNil(spoiler), + 'message': _converse.chatboxes.getMessageBody(stanza) || undefined, + 'references': this.getReferencesFromStanza(stanza), + 'msgid': stanza.getAttribute('id'), + 'time': delay ? delay.getAttribute('stamp') : moment().format(), + 'type': stanza.getAttribute('type') + }; + + if (attrs.type === 'groupchat') { + attrs.from = stanza.getAttribute('from'); + attrs.nick = Strophe.unescapeNode(Strophe.getResourceFromJid(attrs.from)); + attrs.sender = attrs.nick === this.get('nick') ? 'me' : 'them'; + } else { + attrs.from = Strophe.getBareJidFromJid(stanza.getAttribute('from')); + + if (attrs.from === _converse.bare_jid) { + attrs.sender = 'me'; + attrs.fullname = _converse.xmppstatus.get('fullname'); + } else { + attrs.sender = 'them'; + attrs.fullname = this.get('fullname'); + } + } + + _.each(sizzle(`x[xmlns="${Strophe.NS.OUTOFBAND}"]`, stanza), xform => { + attrs['oob_url'] = xform.querySelector('url').textContent; + attrs['oob_desc'] = xform.querySelector('url').textContent; + }); + + if (spoiler) { + attrs.spoiler_hint = spoiler.textContent.length > 0 ? spoiler.textContent : ''; + } + + return attrs; + }, + + createMessage(message, original_stanza) { + /* Create a Backbone.Message object inside this chat box + * based on the identified message stanza. + */ + const that = this; + + function _create(attrs) { + const is_csn = u.isOnlyChatStateNotification(attrs); + + if (is_csn && (attrs.is_delayed || attrs.type === 'groupchat' && Strophe.getResourceFromJid(attrs.from) == that.get('nick'))) { + // XXX: MUC leakage + // No need showing delayed or our own CSN messages + return; + } else if (!is_csn && !attrs.file && !attrs.plaintext && !attrs.message && !attrs.oob_url && attrs.type !== 'error') { + // TODO: handle messages (currently being done by ChatRoom) + return; + } else { + return that.messages.create(attrs); + } + } + + const result = this.getMessageAttributesFromStanza(message, original_stanza); + + if (typeof result.then === "function") { + return new Promise((resolve, reject) => result.then(attrs => resolve(_create(attrs)))); + } else { + const message = _create(result); + + return Promise.resolve(message); + } + }, + + isHidden() { + /* Returns a boolean to indicate whether a newly received + * message will be visible to the user or not. + */ + return this.get('hidden') || this.get('minimized') || this.isScrolledUp() || _converse.windowState === 'hidden'; + }, + + incrementUnreadMsgCounter(message) { + /* Given a newly received message, update the unread counter if + * necessary. + */ + if (!message) { + return; + } + + if (_.isNil(message.get('message'))) { + return; + } + + if (utils.isNewMessage(message) && this.isHidden()) { + this.save({ + 'num_unread': this.get('num_unread') + 1 + }); + + _converse.incrementMsgCounter(); + } + }, + + clearUnreadMsgCounter() { + u.safeSave(this, { + 'num_unread': 0 + }); + }, + + isScrolledUp() { + return this.get('scrolled', true); + } + + }); + _converse.ChatBoxes = Backbone.Collection.extend({ + comparator: 'time_opened', + + model(attrs, options) { + return new _converse.ChatBox(attrs, options); + }, + + registerMessageHandler() { + _converse.connection.addHandler(stanza => { + this.onMessage(stanza); + return true; + }, null, 'message', 'chat'); + + _converse.connection.addHandler(stanza => { + this.onErrorMessage(stanza); + return true; + }, null, 'message', 'error'); + }, + + chatBoxMayBeShown(chatbox) { + return true; + }, + + onChatBoxesFetched(collection) { + /* Show chat boxes upon receiving them from sessionStorage */ + collection.each(chatbox => { + if (this.chatBoxMayBeShown(chatbox)) { + chatbox.trigger('show'); + } + }); + + _converse.emit('chatBoxesFetched'); + }, + + onConnected() { + this.browserStorage = new Backbone.BrowserStorage.session(`converse.chatboxes-${_converse.bare_jid}`); + this.registerMessageHandler(); + this.fetch({ + 'add': true, + 'success': this.onChatBoxesFetched.bind(this) + }); + }, + + onErrorMessage(message) { + /* Handler method for all incoming error message stanzas + */ + const from_jid = Strophe.getBareJidFromJid(message.getAttribute('from')); + + if (utils.isSameBareJID(from_jid, _converse.bare_jid)) { + return true; + } + + const chatbox = this.getChatBox(from_jid); + + if (!chatbox) { + return true; + } + + chatbox.createMessage(message, message); + return true; + }, + + getMessageBody(stanza) { + /* Given a message stanza, return the text contained in its body. + */ + const type = stanza.getAttribute('type'); + + if (type === 'error') { + const error = stanza.querySelector('error'); + return _.propertyOf(error.querySelector('text'))('textContent') || __('Sorry, an error occurred:') + ' ' + error.innerHTML; + } else { + return _.propertyOf(stanza.querySelector('body'))('textContent'); + } + }, + + onMessage(stanza) { + /* Handler method for all incoming single-user chat "message" + * stanzas. + * + * Parameters: + * (XMLElement) stanza - The incoming message stanza + */ + let to_jid = stanza.getAttribute('to'); + const to_resource = Strophe.getResourceFromJid(to_jid); + + if (_converse.filter_by_resource && to_resource && to_resource !== _converse.resource) { + _converse.log(`onMessage: Ignoring incoming message intended for a different resource: ${to_jid}`, Strophe.LogLevel.INFO); + + return true; + } else if (utils.isHeadlineMessage(_converse, stanza)) { + // XXX: Ideally we wouldn't have to check for headline + // messages, but Prosody sends headline messages with the + // wrong type ('chat'), so we need to filter them out here. + _converse.log(`onMessage: Ignoring incoming headline message sent with type 'chat' from JID: ${stanza.getAttribute('from')}`, Strophe.LogLevel.INFO); + + return true; + } + + let from_jid = stanza.getAttribute('from'); + const forwarded = stanza.querySelector('forwarded'), + original_stanza = stanza; + + if (!_.isNull(forwarded)) { + const forwarded_message = forwarded.querySelector('message'), + forwarded_from = forwarded_message.getAttribute('from'), + is_carbon = !_.isNull(stanza.querySelector(`received[xmlns="${Strophe.NS.CARBONS}"]`)); + + if (is_carbon && Strophe.getBareJidFromJid(forwarded_from) !== from_jid) { + // Prevent message forging via carbons + // https://xmpp.org/extensions/xep-0280.html#security + return true; + } + + stanza = forwarded_message; + from_jid = stanza.getAttribute('from'); + to_jid = stanza.getAttribute('to'); + } + + const from_bare_jid = Strophe.getBareJidFromJid(from_jid), + from_resource = Strophe.getResourceFromJid(from_jid), + is_me = from_bare_jid === _converse.bare_jid; + let contact_jid; + + if (is_me) { + // I am the sender, so this must be a forwarded message... + if (_.isNull(to_jid)) { + return _converse.log(`Don't know how to handle message stanza without 'to' attribute. ${stanza.outerHTML}`, Strophe.LogLevel.ERROR); + } + + contact_jid = Strophe.getBareJidFromJid(to_jid); + } else { + contact_jid = from_bare_jid; + } + + const attrs = { + 'fullname': _.get(_converse.api.contacts.get(contact_jid), 'attributes.fullname') // Get chat box, but only create a new one when the message has a body. + + }; + const has_body = sizzle(`body, encrypted[xmlns="${Strophe.NS.OMEMO}`).length > 0; + const chatbox = this.getChatBox(contact_jid, attrs, has_body); + + if (chatbox && !chatbox.handleMessageCorrection(stanza)) { + const msgid = stanza.getAttribute('id'), + message = msgid && chatbox.messages.findWhere({ + msgid + }); + + if (!message) { + // Only create the message when we're sure it's not a duplicate + chatbox.createMessage(stanza, original_stanza).then(msg => chatbox.incrementUnreadMsgCounter(msg)).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); + } + } + + _converse.emit('message', { + 'stanza': original_stanza, + 'chatbox': chatbox + }); + + return true; + }, + + getChatBox(jid, attrs = {}, create) { + /* Returns a chat box or optionally return a newly + * created one if one doesn't exist. + * + * Parameters: + * (String) jid - The JID of the user whose chat box we want + * (Boolean) create - Should a new chat box be created if none exists? + * (Object) attrs - Optional chat box atributes. + */ + if (_.isObject(jid)) { + create = attrs; + attrs = jid; + jid = attrs.jid; + } + + jid = Strophe.getBareJidFromJid(jid.toLowerCase()); + let chatbox = this.get(Strophe.getBareJidFromJid(jid)); + + if (!chatbox && create) { + _.extend(attrs, { + 'jid': jid, + 'id': jid + }); + + chatbox = this.create(attrs, { + 'error'(model, response) { + _converse.log(response.responseText); + } + + }); + } + + return chatbox; + } + + }); + + function autoJoinChats() { + /* Automatically join private chats, based on the + * "auto_join_private_chats" configuration setting. + */ + _.each(_converse.auto_join_private_chats, function (jid) { + if (_converse.chatboxes.where({ + 'jid': jid + }).length) { + return; + } + + if (_.isString(jid)) { + _converse.api.chats.open(jid); + } else { + _converse.log('Invalid jid criteria specified for "auto_join_private_chats"', Strophe.LogLevel.ERROR); + } + }); + + _converse.emit('privateChatsAutoJoined'); + } + /************************ BEGIN Event Handlers ************************/ + + + _converse.on('chatBoxesFetched', autoJoinChats); + + _converse.api.waitUntil('rosterContactsFetched').then(() => { + _converse.roster.on('add', contact => { + /* When a new contact is added, check if we already have a + * chatbox open for it, and if so attach it to the chatbox. + */ + const chatbox = _converse.chatboxes.findWhere({ + 'jid': contact.get('jid') + }); + + if (chatbox) { + chatbox.addRelatedContact(contact); + } + }); + }); + + _converse.on('addClientFeatures', () => { + _converse.api.disco.own.features.add(Strophe.NS.MESSAGE_CORRECT); + + _converse.api.disco.own.features.add(Strophe.NS.HTTPUPLOAD); + + _converse.api.disco.own.features.add(Strophe.NS.OUTOFBAND); + }); + + _converse.api.listen.on('pluginsInitialized', () => { + _converse.chatboxes = new _converse.ChatBoxes(); + + _converse.emit('chatBoxesInitialized'); + }); + + _converse.api.listen.on('presencesInitialized', () => _converse.chatboxes.onConnected()); + /************************ END Event Handlers ************************/ + + /************************ BEGIN API ************************/ + + + _.extend(_converse.api, { + /** + * The "chats" namespace (used for one-on-one chats) + * + * @namespace _converse.api.chats + * @memberOf _converse.api + */ + 'chats': { + /** + * @method _converse.api.chats.create + * @param {string|string[]} jid|jids An jid or array of jids + * @param {object} attrs An object containing configuration attributes. + */ + 'create'(jids, attrs) { + if (_.isUndefined(jids)) { + _converse.log("chats.create: You need to provide at least one JID", Strophe.LogLevel.ERROR); + + return null; + } + + if (_.isString(jids)) { + if (attrs && !_.get(attrs, 'fullname')) { + attrs.fullname = _.get(_converse.api.contacts.get(jids), 'attributes.fullname'); + } + + const chatbox = _converse.chatboxes.getChatBox(jids, attrs, true); + + if (_.isNil(chatbox)) { + _converse.log("Could not open chatbox for JID: " + jids, Strophe.LogLevel.ERROR); + + return; + } + + return chatbox; + } + + return _.map(jids, jid => { + attrs.fullname = _.get(_converse.api.contacts.get(jid), 'attributes.fullname'); + return _converse.chatboxes.getChatBox(jid, attrs, true).trigger('show'); + }); + }, + + /** + * Opens a new one-on-one chat. + * + * @method _converse.api.chats.open + * @param {String|string[]} name - e.g. 'buddy@example.com' or ['buddy1@example.com', 'buddy2@example.com'] + * @returns {Promise} Promise which resolves with the Backbone.Model representing the chat. + * + * @example + * // To open a single chat, provide the JID of the contact you're chatting with in that chat: + * converse.plugins.add('myplugin', { + * initialize: function() { + * var _converse = this._converse; + * // Note, buddy@example.org must be in your contacts roster! + * _converse.api.chats.open('buddy@example.com').then((chat) => { + * // Now you can do something with the chat model + * }); + * } + * }); + * + * @example + * // To open an array of chats, provide an array of JIDs: + * converse.plugins.add('myplugin', { + * initialize: function () { + * var _converse = this._converse; + * // Note, these users must first be in your contacts roster! + * _converse.api.chats.open(['buddy1@example.com', 'buddy2@example.com']).then((chats) => { + * // Now you can do something with the chat models + * }); + * } + * }); + * + */ + 'open'(jids, attrs) { + return new Promise((resolve, reject) => { + Promise.all([_converse.api.waitUntil('rosterContactsFetched'), _converse.api.waitUntil('chatBoxesFetched')]).then(() => { + if (_.isUndefined(jids)) { + const err_msg = "chats.open: You need to provide at least one JID"; + + _converse.log(err_msg, Strophe.LogLevel.ERROR); + + reject(new Error(err_msg)); + } else if (_.isString(jids)) { + resolve(_converse.api.chats.create(jids, attrs).trigger('show')); + } else { + resolve(_.map(jids, jid => _converse.api.chats.create(jid, attrs).trigger('show'))); + } + }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); + }); + }, + + /** + * Returns a chat model. The chat should already be open. + * + * @method _converse.api.chats.get + * @param {String|string[]} name - e.g. 'buddy@example.com' or ['buddy1@example.com', 'buddy2@example.com'] + * @returns {Backbone.Model} + * + * @example + * // To return a single chat, provide the JID of the contact you're chatting with in that chat: + * const model = _converse.api.chats.get('buddy@example.com'); + * + * @example + * // To return an array of chats, provide an array of JIDs: + * const models = _converse.api.chats.get(['buddy1@example.com', 'buddy2@example.com']); + * + * @example + * // To return all open chats, call the method without any parameters:: + * const models = _converse.api.chats.get(); + * + */ + 'get'(jids) { + if (_.isUndefined(jids)) { + const result = []; + + _converse.chatboxes.each(function (chatbox) { + // FIXME: Leaky abstraction from MUC. We need to add a + // base type for chat boxes, and check for that. + if (chatbox.get('type') !== _converse.CHATROOMS_TYPE) { + result.push(chatbox); + } + }); + + return result; + } else if (_.isString(jids)) { + return _converse.chatboxes.getChatBox(jids); + } + + return _.map(jids, _.partial(_converse.chatboxes.getChatBox.bind(_converse.chatboxes), _, {}, true)); + } + + } + }); + /************************ END API ************************/ + + } + + }); + return converse; +}); + +/***/ }), + +/***/ "./src/headless/converse-core.js": +/*!***************************************!*\ + !*** ./src/headless/converse-core.js ***! + \***************************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;// Converse.js +// https://conversejs.org +// +// Copyright (c) 2013-2018, the Converse.js developers +// Licensed under the Mozilla Public License (MPLv2) +(function (root, factory) { + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! sizzle */ "./node_modules/sizzle/dist/sizzle.js"), __webpack_require__(/*! es6-promise/dist/es6-promise.auto */ "./node_modules/es6-promise/dist/es6-promise.auto.js"), __webpack_require__(/*! ./lodash.noconflict */ "./src/headless/lodash.noconflict.js"), __webpack_require__(/*! ./lodash.fp */ "./src/headless/lodash.fp.js"), __webpack_require__(/*! ./polyfill */ "./src/headless/polyfill.js"), __webpack_require__(/*! ./i18n */ "./src/headless/i18n.js"), __webpack_require__(/*! ./utils/core */ "./src/headless/utils/core.js"), __webpack_require__(/*! moment */ "./node_modules/moment/moment.js"), __webpack_require__(/*! strophe.js */ "./node_modules/strophe.js/dist/strophe.js"), __webpack_require__(/*! pluggable.js/dist/pluggable */ "./node_modules/pluggable.js/dist/pluggable.js"), __webpack_require__(/*! ./backbone.noconflict */ "./src/headless/backbone.noconflict.js"), __webpack_require__(/*! backbone.browserStorage */ "./node_modules/backbone.browserStorage/backbone.browserStorage.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), + __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? + (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), + __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); +})(this, function (sizzle, Promise, _, f, polyfill, i18n, u, moment, Strophe, pluggable, Backbone) { + "use strict"; // Strophe globals + + const _Strophe = Strophe, + $build = _Strophe.$build, + $iq = _Strophe.$iq, + $msg = _Strophe.$msg, + $pres = _Strophe.$pres; + const b64_sha1 = Strophe.SHA1.b64_sha1; + Strophe = Strophe.Strophe; // Add Strophe Namespaces + + Strophe.addNamespace('CARBONS', 'urn:xmpp:carbons:2'); + Strophe.addNamespace('CHATSTATES', 'http://jabber.org/protocol/chatstates'); + Strophe.addNamespace('CSI', 'urn:xmpp:csi:0'); + Strophe.addNamespace('DELAY', 'urn:xmpp:delay'); + Strophe.addNamespace('FORWARD', 'urn:xmpp:forward:0'); + Strophe.addNamespace('HINTS', 'urn:xmpp:hints'); + Strophe.addNamespace('HTTPUPLOAD', 'urn:xmpp:http:upload:0'); + Strophe.addNamespace('MAM', 'urn:xmpp:mam:2'); + Strophe.addNamespace('NICK', 'http://jabber.org/protocol/nick'); + Strophe.addNamespace('OMEMO', "eu.siacs.conversations.axolotl"); + Strophe.addNamespace('OUTOFBAND', 'jabber:x:oob'); + Strophe.addNamespace('PUBSUB', 'http://jabber.org/protocol/pubsub'); + Strophe.addNamespace('REGISTER', 'jabber:iq:register'); + Strophe.addNamespace('ROSTERX', 'http://jabber.org/protocol/rosterx'); + Strophe.addNamespace('RSM', 'http://jabber.org/protocol/rsm'); + Strophe.addNamespace('SID', 'urn:xmpp:sid:0'); + Strophe.addNamespace('SPOILER', 'urn:xmpp:spoiler:0'); + Strophe.addNamespace('VCARD', 'vcard-temp'); + Strophe.addNamespace('VCARDUPDATE', 'vcard-temp:x:update'); + Strophe.addNamespace('XFORM', 'jabber:x:data'); // Use Mustache style syntax for variable interpolation + + /* Configuration of Lodash templates (this config is distinct to the + * config of requirejs-tpl in main.js). This one is for normal inline templates. + */ + + _.templateSettings = { + 'escape': /\{\{\{([\s\S]+?)\}\}\}/g, + 'evaluate': /\{\[([\s\S]+?)\]\}/g, + 'interpolate': /\{\{([\s\S]+?)\}\}/g, + 'imports': { + '_': _ + } + }; + /** + * A private, closured object containing the private api (via `_converse.api`) + * as well as private methods and internal data-structures. + * + * @namespace _converse + */ + + const _converse = { + 'templates': {}, + 'promises': {} + }; + + _.extend(_converse, Backbone.Events); // Core plugins are whitelisted automatically + + + _converse.core_plugins = ['converse-autocomplete', 'converse-bookmarks', 'converse-caps', 'converse-chatboxes', 'converse-chatboxviews', 'converse-chatview', 'converse-controlbox', 'converse-core', 'converse-disco', 'converse-dragresize', 'converse-embedded', 'converse-fullscreen', 'converse-headline', 'converse-mam', 'converse-message-view', 'converse-minimize', 'converse-modal', 'converse-muc', 'converse-muc-views', 'converse-notification', 'converse-omemo', 'converse-ping', 'converse-profile', 'converse-push', 'converse-register', 'converse-roomslist', 'converse-roster', 'converse-rosterview', 'converse-singleton', 'converse-spoilers', 'converse-vcard']; // Setting wait to 59 instead of 60 to avoid timing conflicts with the + // webserver, which is often also set to 60 and might therefore sometimes + // return a 504 error page instead of passing through to the BOSH proxy. + + const BOSH_WAIT = 59; // Make converse pluggable + + pluggable.enable(_converse, '_converse', 'pluggable'); + _converse.keycodes = { + TAB: 9, + ENTER: 13, + SHIFT: 16, + CTRL: 17, + ALT: 18, + ESCAPE: 27, + UP_ARROW: 38, + DOWN_ARROW: 40, + FORWARD_SLASH: 47, + AT: 50, + META: 91, + META_RIGHT: 93 + }; // Module-level constants + + _converse.STATUS_WEIGHTS = { + 'offline': 6, + 'unavailable': 5, + 'xa': 4, + 'away': 3, + 'dnd': 2, + 'chat': 1, + // We currently don't differentiate between "chat" and "online" + 'online': 1 + }; + _converse.PRETTY_CHAT_STATUS = { + 'offline': 'Offline', + 'unavailable': 'Unavailable', + 'xa': 'Extended Away', + 'away': 'Away', + 'dnd': 'Do not disturb', + 'chat': 'Chattty', + 'online': 'Online' + }; + _converse.ANONYMOUS = "anonymous"; + _converse.CLOSED = 'closed'; + _converse.EXTERNAL = "external"; + _converse.LOGIN = "login"; + _converse.LOGOUT = "logout"; + _converse.OPENED = 'opened'; + _converse.PREBIND = "prebind"; + _converse.IQ_TIMEOUT = 20000; + _converse.CONNECTION_STATUS = { + 0: 'ERROR', + 1: 'CONNECTING', + 2: 'CONNFAIL', + 3: 'AUTHENTICATING', + 4: 'AUTHFAIL', + 5: 'CONNECTED', + 6: 'DISCONNECTED', + 7: 'DISCONNECTING', + 8: 'ATTACHED', + 9: 'REDIRECT', + 10: 'RECONNECTING' + }; + _converse.SUCCESS = 'success'; + _converse.FAILURE = 'failure'; + _converse.DEFAULT_IMAGE_TYPE = 'image/png'; + _converse.DEFAULT_IMAGE = "iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAIAAABt+uBvAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gwHCy455JBsggAABkJJREFUeNrtnM1PE1sUwHvvTD8otWLHST/Gimi1CEgr6M6FEWuIBo2pujDVsNDEP8GN/4MbN7oxrlipG2OCgZgYlxAbkRYw1KqkIDRCSkM7nXvvW8x7vjyNeQ9m7p1p3z1LQk/v/Dhz7vkEXL161cHl9wI5Ag6IA+KAOCAOiAPigDggLhwQB2S+iNZ+PcYY/SWEEP2HAAAIoSAIoihCCP+ngDDGtVotGAz29/cfOXJEUZSOjg6n06lp2sbGRqlUWlhYyGazS0tLbrdbEASrzgksyeYJId3d3el0uqenRxRFAAAA4KdfIIRgjD9+/Pj8+fOpqSndslofEIQwHA6Pjo4mEon//qmFhYXHjx8vLi4ihBgDEnp7e9l8E0Jo165dQ0NDd+/eDYVC2/qsJElDQ0OEkKWlpa2tLZamxAhQo9EIBoOjo6MXL17csZLe3l5FUT59+lQul5l5JRaAVFWNRqN37tw5ceKEQVWRSOTw4cOFQuHbt2+iKLYCIISQLMu3b99OJpOmKAwEAgcPHszn8+vr6wzsiG6UQQhxuVyXLl0aGBgwUW0sFstkMl6v90fo1KyAMMYDAwPnzp0zXfPg4GAqlWo0Gk0MiBAiy/L58+edTqf5Aa4onj59OhaLYYybFRCEMBaL0fNxBw4cSCQStN0QRUBut3t4eJjq6U+dOiVJElVPRBFQIBDo6+ujCqirqyscDlONGykC2lYyYSR6pBoQQapHZwAoHo/TuARYAOrs7GQASFEUqn6aIiBJkhgA6ujooFpUo6iaTa7koFwnaoWadLNe81tbWwzoaJrWrICWl5cZAFpbW6OabVAEtLi4yABQsVjUNK0pAWWzWQaAcrlcswKanZ1VVZUqHYRQEwOq1Wpv3ryhCmh6erpcLjdrNl+v1ycnJ+l5UELI27dvv3//3qxxEADgy5cvExMT9Mznw4cPtFtAdAPFarU6Pj5eKpVM17yxsfHy5cvV1VXazXu62gVBKBQKT58+rdVqJqrFGL948eLdu3dU8/g/H4FBUaJYLAqC0NPTY9brMD4+PjY25mDSracOCABACJmZmXE6nUePHjWu8NWrV48ePSKEsGlAs7Agfd5nenq6Wq0mk0kjDzY2NvbkyRMIIbP2PLvhBUEQ8vl8NpuNx+M+n29bzhVjvLKycv/+/YmJCcazQuwA6YzW1tYmJyf1SY+2trZ/rRk1Go1SqfT69esHDx4UCgVmNaa/zZ/9ABUhRFXVYDB48uTJeDweiUQkSfL7/T9MA2NcqVTK5fLy8vL8/PzU1FSxWHS5XJaM4wGr9sUwxqqqer3eUCgkSZJuUBBCfTRvc3OzXC6vrKxUKhWn02nhCJ5lM4oQQo/HgxD6+vXr58+fHf8sDOp+HQDg8XgclorFU676dKLlo6yWRdItIBwQB8QBcUCtfosRQjRNQwhhjPUC4w46WXryBSHU1zgEQWBz99EFhDGu1+t+v//48ePxeFxRlD179ng8nh0Efgiher2+vr6ur3HMzMysrq7uTJVdACGEurq6Ll++nEgkPB7Pj9jPoDHqOxyqqubz+WfPnuVyuV9XPeyeagAAAoHArVu3BgcHab8CuVzu4cOHpVKJUnfA5GweY+xyuc6cOXPv3r1IJMLAR8iyPDw8XK/Xi8Wiqqqmm5KZgBBC7e3tN27cuHbtGuPVpf7+/lAoNDs7W61WzfVKpgHSSzw3b95MpVKW3MfRaDQSiczNzVUqFRMZmQOIEOL1eq9fv3727FlL1t50URRFluX5+flqtWpWEGAOIFEUU6nUlStXLKSjy759+xwOx9zcnKZpphzGHMzhcDiTydgk9r1w4YIp7RPTAAmCkMlk2FeLf/tIEKbTab/fbwtAhJBoNGrutpNx6e7uPnTokC1eMU3T0um0DZPMkZER6wERQnw+n/FFSxpy7Nix3bt3WwwIIcRgIWnHkkwmjecfRgGx7DtuV/r6+iwGhDHev3+/bQF1dnYaH6E2CkiWZdsC2rt3r8WAHA5HW1ubbQGZcjajgOwTH/4qNko1Wlg4IA6IA+KAOKBWBUQIsfNojyliKIoRRfH9+/dut9umf3wzpoUNNQ4BAJubmwz+ic+OxefzWWlBhJD29nbug7iT5sIBcUAcEAfEAXFAHBAHxOVn+QMrmWpuPZx12gAAAABJRU5ErkJggg=="; + _converse.TIMEOUTS = { + // Set as module attr so that we can override in tests. + 'PAUSED': 10000, + 'INACTIVE': 90000 + }; // XEP-0085 Chat states + // http://xmpp.org/extensions/xep-0085.html + + _converse.INACTIVE = 'inactive'; + _converse.ACTIVE = 'active'; + _converse.COMPOSING = 'composing'; + _converse.PAUSED = 'paused'; + _converse.GONE = 'gone'; // Chat types + + _converse.PRIVATE_CHAT_TYPE = 'chatbox'; + _converse.CHATROOMS_TYPE = 'chatroom'; + _converse.HEADLINES_TYPE = 'headline'; + _converse.CONTROLBOX_TYPE = 'controlbox'; // Default configuration values + // ---------------------------- + + _converse.default_settings = { + allow_non_roster_messaging: false, + animate: true, + authentication: 'login', + // Available values are "login", "prebind", "anonymous" and "external". + auto_away: 0, + // Seconds after which user status is set to 'away' + auto_login: false, + // Currently only used in connection with anonymous login + auto_reconnect: true, + auto_xa: 0, + // Seconds after which user status is set to 'xa' + blacklisted_plugins: [], + bosh_service_url: undefined, + connection_options: {}, + credentials_url: null, + // URL from where login credentials can be fetched + csi_waiting_time: 0, + // Support for XEP-0352. Seconds before client is considered idle and CSI is sent out. + debug: false, + default_state: 'online', + expose_rid_and_sid: false, + geouri_regex: /https:\/\/www.openstreetmap.org\/.*#map=[0-9]+\/([\-0-9.]+)\/([\-0-9.]+)\S*/g, + geouri_replacement: 'https://www.openstreetmap.org/?mlat=$1&mlon=$2#map=18/$1/$2', + jid: undefined, + keepalive: true, + locales_url: 'locale/{{{locale}}}/LC_MESSAGES/converse.json', + locales: ['af', 'ar', 'bg', 'ca', 'cs', 'de', 'es', 'eu', 'en', 'fr', 'he', 'hi', 'hu', 'id', 'it', 'ja', 'nb', 'nl', 'pl', 'pt_BR', 'ro', 'ru', 'tr', 'uk', 'zh_CN', 'zh_TW'], + message_carbons: true, + nickname: undefined, + password: undefined, + prebind_url: null, + priority: 0, + rid: undefined, + root: window.document, + sid: undefined, + strict_plugin_dependencies: false, + trusted: true, + view_mode: 'overlayed', + // Choices are 'overlayed', 'fullscreen', 'mobile' + websocket_url: undefined, + whitelisted_plugins: [] + }; + + _converse.log = function (message, level, style = '') { + /* Logs messages to the browser's developer console. + * + * Parameters: + * (String) message - The message to be logged. + * (Integer) level - The loglevel which allows for filtering of log + * messages. + * + * Available loglevels are 0 for 'debug', 1 for 'info', 2 for 'warn', + * 3 for 'error' and 4 for 'fatal'. + * + * When using the 'error' or 'warn' loglevels, a full stacktrace will be + * logged as well. + */ + if (level === Strophe.LogLevel.ERROR || level === Strophe.LogLevel.FATAL) { + style = style || 'color: maroon'; + } + + if (message instanceof Error) { + message = message.stack; + } else if (_.isElement(message)) { + message = message.outerHTML; + } + + const prefix = style ? '%c' : ''; + + const logger = _.assign({ + 'debug': _.get(console, 'log') ? console.log.bind(console) : _.noop, + 'error': _.get(console, 'log') ? console.log.bind(console) : _.noop, + 'info': _.get(console, 'log') ? console.log.bind(console) : _.noop, + 'warn': _.get(console, 'log') ? console.log.bind(console) : _.noop + }, console); + + if (level === Strophe.LogLevel.ERROR) { + logger.error(`${prefix} ERROR: ${message}`, style); + } else if (level === Strophe.LogLevel.WARN) { + if (_converse.debug) { + logger.warn(`${prefix} ${moment().format()} WARNING: ${message}`, style); + } + } else if (level === Strophe.LogLevel.FATAL) { + logger.error(`${prefix} FATAL: ${message}`, style); + } else if (_converse.debug) { + if (level === Strophe.LogLevel.DEBUG) { + logger.debug(`${prefix} ${moment().format()} DEBUG: ${message}`, style); + } else { + logger.info(`${prefix} ${moment().format()} INFO: ${message}`, style); + } + } + }; + + Strophe.log = function (level, msg) { + _converse.log(level + ' ' + msg, level); + }; + + Strophe.error = function (msg) { + _converse.log(msg, Strophe.LogLevel.ERROR); + }; + + _converse.__ = function (str) { + /* Translate the given string based on the current locale. + * + * Parameters: + * (String) str - The string to translate. + */ + if (_.isUndefined(i18n)) { + return str; + } + + return i18n.translate.apply(i18n, arguments); + }; + + const __ = _converse.__; + const PROMISES = ['initialized', 'connectionInitialized', 'pluginsInitialized', 'statusInitialized']; + + function addPromise(promise) { + /* Private function, used to add a new promise to the ones already + * available via the `waitUntil` api method. + */ + _converse.promises[promise] = u.getResolveablePromise(); + } + + _converse.emit = function (name) { + /* Event emitter and promise resolver */ + _converse.trigger.apply(this, arguments); + + const promise = _converse.promises[name]; + + if (!_.isUndefined(promise)) { + promise.resolve(); + } + }; + + _converse.isSingleton = function () { + return _.includes(['mobile', 'fullscreen', 'embedded'], _converse.view_mode); + }; + + _converse.router = new Backbone.Router(); + + _converse.initialize = function (settings, callback) { + settings = !_.isUndefined(settings) ? settings : {}; + const init_promise = u.getResolveablePromise(); + + _.each(PROMISES, addPromise); + + if (!_.isUndefined(_converse.connection)) { + // Looks like _converse.initialized was called again without logging + // out or disconnecting in the previous session. + // This happens in tests. We therefore first clean up. + Backbone.history.stop(); + + _converse.chatboxviews.closeAllChatBoxes(); + + if (_converse.bookmarks) { + _converse.bookmarks.reset(); + } + + delete _converse.controlboxtoggle; + delete _converse.chatboxviews; + + _converse.connection.reset(); + + _converse.stopListening(); + + _converse.tearDown(); + + delete _converse.config; + + _converse.initClientConfig(); + + _converse.off(); + } + + if ('onpagehide' in window) { + // Pagehide gets thrown in more cases than unload. Specifically it + // gets thrown when the page is cached and not just + // closed/destroyed. It's the only viable event on mobile Safari. + // https://www.webkit.org/blog/516/webkit-page-cache-ii-the-unload-event/ + _converse.unloadevent = 'pagehide'; + } else if ('onbeforeunload' in window) { + _converse.unloadevent = 'beforeunload'; + } else if ('onunload' in window) { + _converse.unloadevent = 'unload'; + } + + _.assignIn(this, this.default_settings); // Allow only whitelisted configuration attributes to be overwritten + + + _.assignIn(this, _.pick(settings, _.keys(this.default_settings))); + + if (this.authentication === _converse.ANONYMOUS) { + if (this.auto_login && !this.jid) { + throw new Error("Config Error: you need to provide the server's " + "domain via the 'jid' option when using anonymous " + "authentication with auto_login."); + } + } + /* Localisation */ + + + if (!_.isUndefined(i18n)) { + i18n.setLocales(settings.i18n, _converse); + } else { + _converse.locale = 'en'; + } // Module-level variables + // ---------------------- + + + this.callback = callback || _.noop; + /* When reloading the page: + * For new sessions, we need to send out a presence stanza to notify + * the server/network that we're online. + * When re-attaching to an existing session (e.g. via the keepalive + * option), we don't need to again send out a presence stanza, because + * it's as if "we never left" (see onConnectStatusChanged). + * https://github.com/jcbrand/converse.js/issues/521 + */ + + this.send_initial_presence = true; + this.msg_counter = 0; + this.user_settings = settings; // Save the user settings so that they can be used by plugins + // Module-level functions + // ---------------------- + + this.generateResource = () => `/converse.js-${Math.floor(Math.random() * 139749528).toString()}`; + + this.sendCSI = function (stat) { + /* Send out a Chat Status Notification (XEP-0352) + * + * Parameters: + * (String) stat: The user's chat status + */ + + /* Send out a Chat Status Notification (XEP-0352) */ + // XXX if (converse.features[Strophe.NS.CSI] || true) { + _converse.connection.send($build(stat, { + xmlns: Strophe.NS.CSI + })); + + _converse.inactive = stat === _converse.INACTIVE ? true : false; + }; + + this.onUserActivity = function () { + /* Resets counters and flags relating to CSI and auto_away/auto_xa */ + if (_converse.idle_seconds > 0) { + _converse.idle_seconds = 0; + } + + if (!_converse.connection.authenticated) { + // We can't send out any stanzas when there's no authenticated connection. + // converse can happen when the connection reconnects. + return; + } + + if (_converse.inactive) { + _converse.sendCSI(_converse.ACTIVE); + } + + if (_converse.auto_changed_status === true) { + _converse.auto_changed_status = false; // XXX: we should really remember the original state here, and + // then set it back to that... + + _converse.xmppstatus.set('status', _converse.default_state); + } + }; + + this.onEverySecond = function () { + /* An interval handler running every second. + * Used for CSI and the auto_away and auto_xa features. + */ + if (!_converse.connection.authenticated) { + // We can't send out any stanzas when there's no authenticated connection. + // This can happen when the connection reconnects. + return; + } + + const stat = _converse.xmppstatus.get('status'); + + _converse.idle_seconds++; + + if (_converse.csi_waiting_time > 0 && _converse.idle_seconds > _converse.csi_waiting_time && !_converse.inactive) { + _converse.sendCSI(_converse.INACTIVE); + } + + if (_converse.auto_away > 0 && _converse.idle_seconds > _converse.auto_away && stat !== 'away' && stat !== 'xa' && stat !== 'dnd') { + _converse.auto_changed_status = true; + + _converse.xmppstatus.set('status', 'away'); + } else if (_converse.auto_xa > 0 && _converse.idle_seconds > _converse.auto_xa && stat !== 'xa' && stat !== 'dnd') { + _converse.auto_changed_status = true; + + _converse.xmppstatus.set('status', 'xa'); + } + }; + + this.registerIntervalHandler = function () { + /* Set an interval of one second and register a handler for it. + * Required for the auto_away, auto_xa and csi_waiting_time features. + */ + if (_converse.auto_away < 1 && _converse.auto_xa < 1 && _converse.csi_waiting_time < 1) { + // Waiting time of less then one second means features aren't used. + return; + } + + _converse.idle_seconds = 0; + _converse.auto_changed_status = false; // Was the user's status changed by _converse.js? + + window.addEventListener('click', _converse.onUserActivity); + window.addEventListener('focus', _converse.onUserActivity); + window.addEventListener('keypress', _converse.onUserActivity); + window.addEventListener('mousemove', _converse.onUserActivity); + const options = { + 'once': true, + 'passive': true + }; + window.addEventListener(_converse.unloadevent, _converse.onUserActivity, options); + _converse.everySecondTrigger = window.setInterval(_converse.onEverySecond, 1000); + }; + + this.setConnectionStatus = function (connection_status, message) { + _converse.connfeedback.set({ + 'connection_status': connection_status, + 'message': message + }); + }; + + this.rejectPresenceSubscription = function (jid, message) { + /* Reject or cancel another user's subscription to our presence updates. + * + * Parameters: + * (String) jid - The Jabber ID of the user whose subscription + * is being canceled. + * (String) message - An optional message to the user + */ + const pres = $pres({ + to: jid, + type: "unsubscribed" + }); + + if (message && message !== "") { + pres.c("status").t(message); + } + + _converse.connection.send(pres); + }; + + this.reconnect = _.debounce(function () { + _converse.log('RECONNECTING'); + + _converse.log('The connection has dropped, attempting to reconnect.'); + + _converse.setConnectionStatus(Strophe.Status.RECONNECTING, __('The connection has dropped, attempting to reconnect.')); + + _converse.connection.reconnecting = true; + + _converse.tearDown(); + + _converse.logIn(null, true); + }, 3000, { + 'leading': true + }); + + this.disconnect = function () { + _converse.log('DISCONNECTED'); + + delete _converse.connection.reconnecting; + + _converse.connection.reset(); + + _converse.tearDown(); + + _converse.clearSession(); + + _converse.emit('disconnected'); + }; + + this.onDisconnected = function () { + /* Gets called once strophe's status reaches Strophe.Status.DISCONNECTED. + * Will either start a teardown process for converse.js or attempt + * to reconnect. + */ + const reason = _converse.disconnection_reason; + + if (_converse.disconnection_cause === Strophe.Status.AUTHFAIL) { + if (_converse.credentials_url && _converse.auto_reconnect) { + /* In this case, we reconnect, because we might be receiving + * expirable tokens from the credentials_url. + */ + _converse.emit('will-reconnect'); + + return _converse.reconnect(); + } else { + return _converse.disconnect(); + } + } else if (_converse.disconnection_cause === _converse.LOGOUT || !_.isUndefined(reason) && reason === _.get(Strophe, 'ErrorCondition.NO_AUTH_MECH') || reason === "host-unknown" || reason === "remote-connection-failed" || !_converse.auto_reconnect) { + return _converse.disconnect(); + } + + _converse.emit('will-reconnect'); + + _converse.reconnect(); + }; + + this.setDisconnectionCause = function (cause, reason, override) { + /* Used to keep track of why we got disconnected, so that we can + * decide on what the next appropriate action is (in onDisconnected) + */ + if (_.isUndefined(cause)) { + delete _converse.disconnection_cause; + delete _converse.disconnection_reason; + } else if (_.isUndefined(_converse.disconnection_cause) || override) { + _converse.disconnection_cause = cause; + _converse.disconnection_reason = reason; + } + }; + + this.onConnectStatusChanged = function (status, message) { + /* Callback method called by Strophe as the Strophe.Connection goes + * through various states while establishing or tearing down a + * connection. + */ + _converse.log(`Status changed to: ${_converse.CONNECTION_STATUS[status]}`); + + if (status === Strophe.Status.CONNECTED || status === Strophe.Status.ATTACHED) { + _converse.setConnectionStatus(status); // By default we always want to send out an initial presence stanza. + + + _converse.send_initial_presence = true; + + _converse.setDisconnectionCause(); + + if (_converse.connection.reconnecting) { + _converse.log(status === Strophe.Status.CONNECTED ? 'Reconnected' : 'Reattached'); + + _converse.onConnected(true); + } else { + _converse.log(status === Strophe.Status.CONNECTED ? 'Connected' : 'Attached'); + + if (_converse.connection.restored) { + // No need to send an initial presence stanza when + // we're restoring an existing session. + _converse.send_initial_presence = false; + } + + _converse.onConnected(); + } + } else if (status === Strophe.Status.DISCONNECTED) { + _converse.setDisconnectionCause(status, message); + + _converse.onDisconnected(); + } else if (status === Strophe.Status.ERROR) { + _converse.setConnectionStatus(status, __('An error occurred while connecting to the chat server.')); + } else if (status === Strophe.Status.CONNECTING) { + _converse.setConnectionStatus(status); + } else if (status === Strophe.Status.AUTHENTICATING) { + _converse.setConnectionStatus(status); + } else if (status === Strophe.Status.AUTHFAIL) { + if (!message) { + message = __('Your Jabber ID and/or password is incorrect. Please try again.'); + } + + _converse.setConnectionStatus(status, message); + + _converse.setDisconnectionCause(status, message, true); + + _converse.onDisconnected(); + } else if (status === Strophe.Status.CONNFAIL) { + let feedback = message; + + if (message === "host-unknown" || message == "remote-connection-failed") { + feedback = __("Sorry, we could not connect to the XMPP host with domain: %1$s", `\"${Strophe.getDomainFromJid(_converse.connection.jid)}\"`); + } else if (!_.isUndefined(message) && message === _.get(Strophe, 'ErrorCondition.NO_AUTH_MECH')) { + feedback = __("The XMPP server did not offer a supported authentication mechanism"); + } + + _converse.setConnectionStatus(status, feedback); + + _converse.setDisconnectionCause(status, message); + } else if (status === Strophe.Status.DISCONNECTING) { + _converse.setDisconnectionCause(status, message); + } + }; + + this.incrementMsgCounter = function () { + this.msg_counter += 1; + const unreadMsgCount = this.msg_counter; + let title = document.title; + + if (_.isNil(title)) { + return; + } + + if (title.search(/^Messages \(\d+\) /) === -1) { + title = `Messages (${unreadMsgCount}) ${title}`; + } else { + title = title.replace(/^Messages \(\d+\) /, `Messages (${unreadMsgCount})`); + } + }; + + this.clearMsgCounter = function () { + this.msg_counter = 0; + let title = document.title; + + if (_.isNil(title)) { + return; + } + + if (title.search(/^Messages \(\d+\) /) !== -1) { + title = title.replace(/^Messages \(\d+\) /, ""); + } + }; + + this.initStatus = reconnecting => { + // If there's no xmppstatus obj, then we were never connected to + // begin with, so we set reconnecting to false. + reconnecting = _.isUndefined(_converse.xmppstatus) ? false : reconnecting; + + if (reconnecting) { + _converse.onStatusInitialized(reconnecting); + } else { + const id = `converse.xmppstatus-${_converse.bare_jid}`; + this.xmppstatus = new this.XMPPStatus({ + 'id': id + }); + this.xmppstatus.browserStorage = new Backbone.BrowserStorage.session(id); + this.xmppstatus.fetch({ + 'success': _.partial(_converse.onStatusInitialized, reconnecting), + 'error': _.partial(_converse.onStatusInitialized, reconnecting) + }); + } + }; + + this.initClientConfig = function () { + /* The client config refers to configuration of the client which is + * independent of any particular user. + * What this means is that config values need to persist across + * user sessions. + */ + const id = b64_sha1('converse.client-config'); + _converse.config = new Backbone.Model({ + 'id': id, + 'trusted': _converse.trusted && true || false, + 'storage': _converse.trusted ? 'local' : 'session' + }); + _converse.config.browserStorage = new Backbone.BrowserStorage.session(id); + + _converse.config.fetch(); + + _converse.emit('clientConfigInitialized'); + }; + + this.initSession = function () { + const id = b64_sha1('converse.bosh-session'); + _converse.session = new Backbone.Model({ + 'id': id + }); + _converse.session.browserStorage = new Backbone.BrowserStorage.session(id); + + _converse.session.fetch(); + + _converse.emit('sessionInitialized'); + }; + + this.clearSession = function () { + if (!_converse.config.get('trusted')) { + window.localStorage.clear(); + window.sessionStorage.clear(); + } else if (!_.isUndefined(this.session) && this.session.browserStorage) { + this.session.browserStorage._clear(); + } + + _converse.emit('clearSession'); + }; + + this.logOut = function () { + _converse.clearSession(); + + _converse.setDisconnectionCause(_converse.LOGOUT, undefined, true); + + if (!_.isUndefined(_converse.connection)) { + _converse.connection.disconnect(); + } else { + _converse.tearDown(); + } // Recreate all the promises + + + _.each(_.keys(_converse.promises), addPromise); + + _converse.emit('logout'); + }; + + this.saveWindowState = function (ev, hidden) { + // XXX: eventually we should be able to just use + // document.visibilityState (when we drop support for older + // browsers). + let state; + const event_map = { + 'focus': "visible", + 'focusin': "visible", + 'pageshow': "visible", + 'blur': "hidden", + 'focusout': "hidden", + 'pagehide': "hidden" + }; + ev = ev || document.createEvent('Events'); + + if (ev.type in event_map) { + state = event_map[ev.type]; + } else { + state = document[hidden] ? "hidden" : "visible"; + } + + if (state === 'visible') { + _converse.clearMsgCounter(); + } + + _converse.windowState = state; + + _converse.emit('windowStateChanged', { + state + }); + }; + + this.registerGlobalEventHandlers = function () { + // Taken from: + // http://stackoverflow.com/questions/1060008/is-there-a-way-to-detect-if-a-browser-window-is-not-currently-active + let hidden = "hidden"; // Standards: + + if (hidden in document) { + document.addEventListener("visibilitychange", _.partial(_converse.saveWindowState, _, hidden)); + } else if ((hidden = "mozHidden") in document) { + document.addEventListener("mozvisibilitychange", _.partial(_converse.saveWindowState, _, hidden)); + } else if ((hidden = "webkitHidden") in document) { + document.addEventListener("webkitvisibilitychange", _.partial(_converse.saveWindowState, _, hidden)); + } else if ((hidden = "msHidden") in document) { + document.addEventListener("msvisibilitychange", _.partial(_converse.saveWindowState, _, hidden)); + } else if ("onfocusin" in document) { + // IE 9 and lower: + document.onfocusin = document.onfocusout = _.partial(_converse.saveWindowState, _, hidden); + } else { + // All others: + window.onpageshow = window.onpagehide = window.onfocus = window.onblur = _.partial(_converse.saveWindowState, _, hidden); + } // set the initial state (but only if browser supports the Page Visibility API) + + + if (document[hidden] !== undefined) { + _.partial(_converse.saveWindowState, _, hidden)({ + type: document[hidden] ? "blur" : "focus" + }); + } + + _converse.emit('registeredGlobalEventHandlers'); + }; + + 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; + } + + const carbons_iq = new Strophe.Builder('iq', { + 'from': this.connection.jid, + 'id': 'enablecarbons', + 'type': 'set' + }).c('enable', { + xmlns: Strophe.NS.CARBONS + }); + this.connection.addHandler(iq => { + if (iq.querySelectorAll('error').length > 0) { + _converse.log('An error occurred while trying to enable message carbons.', Strophe.LogLevel.WARN); + } else { + this.session.save({ + 'carbons_enabled': true + }); + + _converse.log('Message carbons have been enabled.'); + } + }, null, "iq", null, "enablecarbons"); + this.connection.send(carbons_iq); + }; + + this.sendInitialPresence = function () { + if (_converse.send_initial_presence) { + _converse.xmppstatus.sendPresence(); + } + }; + + this.onStatusInitialized = function (reconnecting) { + _converse.emit('statusInitialized', reconnecting); + + if (reconnecting) { + _converse.emit('reconnected'); + } else { + init_promise.resolve(); + + _converse.emit('initialized'); + + _converse.emit('connected'); + } + }; + + this.setUserJID = function () { + _converse.jid = _converse.connection.jid; + _converse.bare_jid = Strophe.getBareJidFromJid(_converse.connection.jid); + _converse.resource = Strophe.getResourceFromJid(_converse.connection.jid); + _converse.domain = Strophe.getDomainFromJid(_converse.connection.jid); + + _converse.emit('setUserJID'); + }; + + this.onConnected = function (reconnecting) { + /* Called as soon as a new connection has been established, either + * by logging in or by attaching to an existing BOSH session. + */ + _converse.connection.flush(); // Solves problem of returned PubSub BOSH response not received by browser + + + _converse.setUserJID(); + + _converse.initSession(); + + _converse.enableCarbons(); + + _converse.initStatus(reconnecting); + }; + + this.ConnectionFeedback = Backbone.Model.extend({ + defaults: { + 'connection_status': Strophe.Status.DISCONNECTED, + 'message': '' + }, + + initialize() { + this.on('change', () => { + _converse.emit('connfeedback', _converse.connfeedback); + }); + } + + }); + this.connfeedback = new this.ConnectionFeedback(); + this.XMPPStatus = Backbone.Model.extend({ + defaults() { + return { + "jid": _converse.bare_jid, + "status": _converse.default_state + }; + }, + + initialize() { + this.vcard = _converse.vcards.findWhere({ + 'jid': this.get('jid') + }); + + if (_.isNil(this.vcard)) { + this.vcard = _converse.vcards.create({ + 'jid': this.get('jid') + }); + } + + this.on('change:status', item => { + const status = this.get('status'); + this.sendPresence(status); + + _converse.emit('statusChanged', status); + }); + this.on('change:status_message', () => { + const status_message = this.get('status_message'); + this.sendPresence(this.get('status'), status_message); + + _converse.emit('statusMessageChanged', status_message); + }); + }, + + constructPresence(type, status_message) { + let presence; + type = _.isString(type) ? type : this.get('status') || _converse.default_state; + status_message = _.isString(status_message) ? status_message : this.get('status_message'); // Most of these presence types are actually not explicitly sent, + // but I add all of them here for reference and future proofing. + + if (type === 'unavailable' || type === 'probe' || type === 'error' || type === 'unsubscribe' || type === 'unsubscribed' || type === 'subscribe' || type === 'subscribed') { + presence = $pres({ + 'type': type + }); + } else if (type === 'offline') { + presence = $pres({ + 'type': 'unavailable' + }); + } else if (type === 'online') { + presence = $pres(); + } else { + presence = $pres().c('show').t(type).up(); + } + + if (status_message) { + presence.c('status').t(status_message).up(); + } + + presence.c('priority').t(_.isNaN(Number(_converse.priority)) ? 0 : _converse.priority); + return presence; + }, + + sendPresence(type, status_message) { + _converse.connection.send(this.constructPresence(type, status_message)); + } + + }); + + this.setUpXMLLogging = function () { + Strophe.log = function (level, msg) { + _converse.log(msg, level); + }; + + if (this.debug) { + this.connection.xmlInput = function (body) { + _converse.log(body.outerHTML, Strophe.LogLevel.DEBUG, 'color: darkgoldenrod'); + }; + + this.connection.xmlOutput = function (body) { + _converse.log(body.outerHTML, Strophe.LogLevel.DEBUG, 'color: darkcyan'); + }; + } + }; + + this.fetchLoginCredentials = () => new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('GET', _converse.credentials_url, true); + xhr.setRequestHeader('Accept', "application/json, text/javascript"); + + xhr.onload = function () { + if (xhr.status >= 200 && xhr.status < 400) { + const data = JSON.parse(xhr.responseText); + resolve({ + 'jid': data.jid, + 'password': data.password + }); + } else { + xhr.onerror(); + } + }; + + xhr.onerror = function () { + delete _converse.connection; + + _converse.emit('noResumeableSession', this); + + reject(xhr.responseText); + }; + + xhr.send(); + }); + + this.startNewBOSHSession = function () { + const xhr = new XMLHttpRequest(); + xhr.open('GET', _converse.prebind_url, true); + xhr.setRequestHeader('Accept', "application/json, text/javascript"); + + xhr.onload = function () { + if (xhr.status >= 200 && xhr.status < 400) { + const data = JSON.parse(xhr.responseText); + + _converse.connection.attach(data.jid, data.sid, data.rid, _converse.onConnectStatusChanged); + } else { + xhr.onerror(); + } + }; + + xhr.onerror = function () { + delete _converse.connection; + + _converse.emit('noResumeableSession', this); + }; + + xhr.send(); + }; + + this.restoreBOSHSession = function (jid_is_required) { + /* Tries to restore a cached BOSH session. */ + if (!this.jid) { + const msg = "restoreBOSHSession: tried to restore a \"keepalive\" session " + "but we don't have the JID for the user!"; + + if (jid_is_required) { + throw new Error(msg); + } else { + _converse.log(msg); + } + } + + try { + this.connection.restore(this.jid, this.onConnectStatusChanged); + return true; + } catch (e) { + _converse.log("Could not restore session for jid: " + this.jid + " Error message: " + e.message, Strophe.LogLevel.WARN); + + this.clearSession(); // We want to clear presences (see #555) + + return false; + } + }; + + this.attemptPreboundSession = function (reconnecting) { + /* Handle session resumption or initialization when prebind is + * being used. + */ + if (!reconnecting) { + if (this.keepalive && this.restoreBOSHSession(true)) { + return; + } // No keepalive, or session resumption has failed. + + + if (this.jid && this.sid && this.rid) { + return this.connection.attach(this.jid, this.sid, this.rid, this.onConnectStatusChanged); + } + } + + if (this.prebind_url) { + return this.startNewBOSHSession(); + } else { + throw new Error("attemptPreboundSession: If you use prebind and not keepalive, " + "then you MUST supply JID, RID and SID values or a prebind_url."); + } + }; + + this.attemptNonPreboundSession = function (credentials, reconnecting) { + /* Handle session resumption or initialization when prebind is not being used. + * + * Two potential options exist and are handled in this method: + * 1. keepalive + * 2. auto_login + */ + if (!reconnecting && this.keepalive && this.restoreBOSHSession()) { + return; + } + + if (credentials) { + // When credentials are passed in, they override prebinding + // or credentials fetching via HTTP + this.autoLogin(credentials); + } else if (this.auto_login) { + if (this.credentials_url) { + this.fetchLoginCredentials().then(this.autoLogin.bind(this), this.autoLogin.bind(this)); + } else if (!this.jid) { + throw new Error("attemptNonPreboundSession: If you use auto_login, " + "you also need to give either a jid value (and if " + "applicable a password) or you need to pass in a URL " + "from where the username and password can be fetched " + "(via credentials_url)."); + } else { + this.autoLogin(); // Could be ANONYMOUS or EXTERNAL + } + } else if (reconnecting) { + this.autoLogin(); + } + }; + + this.autoLogin = function (credentials) { + if (credentials) { + // If passed in, the credentials come from credentials_url, + // so we set them on the converse object. + this.jid = credentials.jid; + } + + if (this.authentication === _converse.ANONYMOUS || this.authentication === _converse.EXTERNAL) { + if (!this.jid) { + throw new Error("Config Error: when using anonymous login " + "you need to provide the server's domain via the 'jid' option. " + "Either when calling converse.initialize, or when calling " + "_converse.api.user.login."); + } + + if (!this.connection.reconnecting) { + this.connection.reset(); + } + + this.connection.connect(this.jid.toLowerCase(), null, this.onConnectStatusChanged, BOSH_WAIT); + } else if (this.authentication === _converse.LOGIN) { + const password = _.isNil(credentials) ? _converse.connection.pass || this.password : credentials.password; + + if (!password) { + if (this.auto_login) { + throw new Error("initConnection: If you use auto_login and " + "authentication='login' then you also need to provide a password."); + } + + _converse.setDisconnectionCause(Strophe.Status.AUTHFAIL, undefined, true); + + _converse.disconnect(); + + return; + } + + const resource = Strophe.getResourceFromJid(this.jid); + + if (!resource) { + this.jid = this.jid.toLowerCase() + _converse.generateResource(); + } else { + this.jid = Strophe.getBareJidFromJid(this.jid).toLowerCase() + '/' + resource; + } + + if (!this.connection.reconnecting) { + this.connection.reset(); + } + + this.connection.connect(this.jid, password, this.onConnectStatusChanged, BOSH_WAIT); + } + }; + + this.logIn = function (credentials, reconnecting) { + // We now try to resume or automatically set up a new session. + // Otherwise the user will be shown a login form. + if (this.authentication === _converse.PREBIND) { + this.attemptPreboundSession(reconnecting); + } else { + this.attemptNonPreboundSession(credentials, reconnecting); + } + }; + + this.initConnection = function () { + /* Creates a new Strophe.Connection instance if we don't already have one. + */ + if (!this.connection) { + if (!this.bosh_service_url && !this.websocket_url) { + throw new Error("initConnection: you must supply a value for either the bosh_service_url or websocket_url or both."); + } + + if (('WebSocket' in window || 'MozWebSocket' in window) && this.websocket_url) { + this.connection = new Strophe.Connection(this.websocket_url, this.connection_options); + } else if (this.bosh_service_url) { + this.connection = new Strophe.Connection(this.bosh_service_url, _.assignIn(this.connection_options, { + 'keepalive': this.keepalive + })); + } else { + throw new Error("initConnection: this browser does not support websockets and bosh_service_url wasn't specified."); + } + } + + _converse.emit('connectionInitialized'); + }; + + this.tearDown = function () { + /* Remove those views which are only allowed with a valid + * connection. + */ + _converse.emit('beforeTearDown'); + + if (!_.isUndefined(_converse.session)) { + _converse.session.destroy(); + } + + window.removeEventListener('click', _converse.onUserActivity); + window.removeEventListener('focus', _converse.onUserActivity); + window.removeEventListener('keypress', _converse.onUserActivity); + window.removeEventListener('mousemove', _converse.onUserActivity); + window.removeEventListener(_converse.unloadevent, _converse.onUserActivity); + window.clearInterval(_converse.everySecondTrigger); + + _converse.emit('afterTearDown'); + + return _converse; + }; + + this.initPlugins = function () { + // If initialize gets called a second time (e.g. during tests), then we + // need to re-apply all plugins (for a new converse instance), and we + // therefore need to clear this array that prevents plugins from being + // initialized twice. + // If initialize is called for the first time, then this array is empty + // in any case. + _converse.pluggable.initialized_plugins = []; + + const whitelist = _converse.core_plugins.concat(_converse.whitelisted_plugins); + + if (_converse.view_mode === 'embedded') { + _.forEach([// eslint-disable-line lodash/prefer-map + "converse-bookmarks", "converse-controlbox", "converse-headline", "converse-register"], name => { + _converse.blacklisted_plugins.push(name); + }); + } + + _converse.pluggable.initializePlugins({ + 'updateSettings'() { + _converse.log("(DEPRECATION) " + "The `updateSettings` method has been deprecated. " + "Please use `_converse.api.settings.update` instead.", Strophe.LogLevel.WARN); + + _converse.api.settings.update.apply(_converse, arguments); + }, + + '_converse': _converse + }, whitelist, _converse.blacklisted_plugins); + + _converse.emit('pluginsInitialized'); + }; // Initialization + // -------------- + // This is the end of the initialize method. + + + if (settings.connection) { + this.connection = settings.connection; + } + + function finishInitialization() { + _converse.initPlugins(); + + _converse.initClientConfig(); + + _converse.initConnection(); + + _converse.setUpXMLLogging(); + + _converse.logIn(); + + _converse.registerGlobalEventHandlers(); + + if (!Backbone.history.started) { + Backbone.history.start(); + } + } + + if (!_.isUndefined(_converse.connection) && _converse.connection.service === 'jasmine tests') { + finishInitialization(); + return _converse; + } else if (_.isUndefined(i18n)) { + finishInitialization(); + } else { + i18n.fetchTranslations(_converse.locale, _converse.locales, u.interpolate(_converse.locales_url, { + 'locale': _converse.locale + })).catch(e => _converse.log(e.message, Strophe.LogLevel.FATAL)).then(finishInitialization).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); + } + + return init_promise; + }; + /** + * ### The private API + * + * The private API methods are only accessible via the closured {@link _converse} + * object, which is only available to plugins. + * + * These methods are kept private (i.e. not global) because they may return + * sensitive data which should be kept off-limits to other 3rd-party scripts + * that might be running in the page. + * + * @namespace _converse.api + * @memberOf _converse + */ + + + _converse.api = { + /** + * This grouping collects API functions related to the XMPP connection. + * + * @namespace _converse.api.connection + * @memberOf _converse.api + */ + 'connection': { + /** + * @method _converse.api.connection.connected + * @memberOf _converse.api.connection + * @returns {boolean} Whether there is an established connection or not. + */ + 'connected'() { + return _converse.connection && _converse.connection.connected || false; + }, + + /** + * Terminates the connection. + * + * @method _converse.api.connection.disconnect + * @memberOf _converse.api.connection + */ + 'disconnect'() { + _converse.connection.disconnect(); + } + + }, + + /** + * Lets you emit (i.e. trigger) events, which can be listened to via + * {@link _converse.api.listen.on} or {@link _converse.api.listen.once} + * (see [_converse.api.listen](http://localhost:8000/docs/html/api/-_converse.api.listen.html)). + * + * @method _converse.api.emit + */ + 'emit'() { + _converse.emit.apply(_converse, arguments); + }, + + /** + * This grouping collects API functions related to the current logged in user. + * + * @namespace _converse.api.user + * @memberOf _converse.api + */ + 'user': { + /** + * @method _converse.api.user.jid + * @returns {string} The current user's full JID (Jabber ID) + * @example _converse.api.user.jid()) + */ + 'jid'() { + return _converse.connection.jid; + }, + + /** + * Logs the user in. + * + * If called without any parameters, Converse will try + * to log the user in by calling the `prebind_url` or `credentials_url` depending + * on whether prebinding is used or not. + * + * @method _converse.api.user.login + * @param {object} [credentials] An object with the credentials. + * @example + * converse.plugins.add('myplugin', { + * initialize: function () { + * + * this._converse.api.user.login({ + * 'jid': 'dummy@example.com', + * 'password': 'secret' + * }); + * + * } + * }); + */ + 'login'(credentials) { + _converse.logIn(credentials); + }, + + /** + * Logs the user out of the current XMPP session. + * + * @method _converse.api.user.logout + * @example _converse.api.user.logout(); + */ + 'logout'() { + _converse.logOut(); + }, + + /** + * Set and get the user's chat status, also called their *availability*. + * + * @namespace _converse.api.user.status + * @memberOf _converse.api.user + */ + 'status': { + /** Return the current user's availability status. + * + * @method _converse.api.user.status.get + * @example _converse.api.user.status.get(); + */ + 'get'() { + return _converse.xmppstatus.get('status'); + }, + + /** + * The user's status can be set to one of the following values: + * + * @method _converse.api.user.status.set + * @param {string} value The user's chat status (e.g. 'away', 'dnd', 'offline', 'online', 'unavailable' or 'xa') + * @param {string} [message] A custom status message + * + * @example this._converse.api.user.status.set('dnd'); + * @example this._converse.api.user.status.set('dnd', 'In a meeting'); + */ + 'set'(value, message) { + const data = { + 'status': value + }; + + if (!_.includes(_.keys(_converse.STATUS_WEIGHTS), value)) { + throw new Error('Invalid availability value. See https://xmpp.org/rfcs/rfc3921.html#rfc.section.2.2.2.1'); + } + + if (_.isString(message)) { + data.status_message = message; + } + + _converse.xmppstatus.sendPresence(value); + + _converse.xmppstatus.save(data); + }, + + /** + * Set and retrieve the user's custom status message. + * + * @namespace _converse.api.user.status.message + * @memberOf _converse.api.user.status + */ + 'message': { + /** + * @method _converse.api.user.status.message.get + * @returns {string} The status message + * @example const message = _converse.api.user.status.message.get() + */ + 'get'() { + return _converse.xmppstatus.get('status_message'); + }, + + /** + * @method _converse.api.user.status.message.set + * @param {string} status The status message + * @example _converse.api.user.status.message.set('In a meeting'); + */ + 'set'(status) { + _converse.xmppstatus.save({ + 'status_message': status + }); + } + + } + } + }, + + /** + * This grouping allows access to the + * [configuration settings](/docs/html/configuration.html#configuration-settings) + * of Converse. + * + * @namespace _converse.api.settings + * @memberOf _converse.api + */ + 'settings': { + /** + * Allows new configuration settings to be specified, or new default values for + * existing configuration settings to be specified. + * + * @method _converse.api.settings.update + * @param {object} settings The configuration settings + * @example + * _converse.api.settings.update({ + * 'enable_foo': true + * }); + * + * // The user can then override the default value of the configuration setting when + * // calling `converse.initialize`. + * converse.initialize({ + * 'enable_foo': false + * }); + */ + 'update'(settings) { + u.merge(_converse.default_settings, settings); + u.merge(_converse, settings); + u.applyUserSettings(_converse, settings, _converse.user_settings); + }, + + /** + * @method _converse.api.settings.get + * @returns {*} Value of the particular configuration setting. + * @example _converse.api.settings.get("play_sounds"); + */ + 'get'(key) { + if (_.includes(_.keys(_converse.default_settings), key)) { + return _converse[key]; + } + }, + + /** + * Set one or many configuration settings. + * + * Note, this is not an alternative to calling {@link converse.initialize}, which still needs + * to be called. Generally, you'd use this method after Converse is already + * running and you want to change the configuration on-the-fly. + * + * @method _converse.api.settings.set + * @param {Object} [settings] An object containing configuration settings. + * @param {string} [key] Alternatively to passing in an object, you can pass in a key and a value. + * @param {string} [value] + * @example _converse.api.settings.set("play_sounds", true); + * @example + * _converse.api.settings.set({ + * "play_sounds", true, + * "hide_offline_users" true + * }); + */ + 'set'(key, val) { + const o = {}; + + if (_.isObject(key)) { + _.assignIn(_converse, _.pick(key, _.keys(_converse.default_settings))); + } else if (_.isString("string")) { + o[key] = val; + + _.assignIn(_converse, _.pick(o, _.keys(_converse.default_settings))); + } + } + + }, + + /** + * Converse and its plugins emit various events which you can listen to via the + * {@link _converse.api.listen} namespace. + * + * Some of these events are also available as [ES2015 Promises](http://es6-features.org/#PromiseUsage) + * although not all of them could logically act as promises, since some events + * might be fired multpile times whereas promises are to be resolved (or + * rejected) only once. + * + * Events which are also promises include: + * + * * [cachedRoster](/docs/html/events.html#cachedroster) + * * [chatBoxesFetched](/docs/html/events.html#chatBoxesFetched) + * * [pluginsInitialized](/docs/html/events.html#pluginsInitialized) + * * [roster](/docs/html/events.html#roster) + * * [rosterContactsFetched](/docs/html/events.html#rosterContactsFetched) + * * [rosterGroupsFetched](/docs/html/events.html#rosterGroupsFetched) + * * [rosterInitialized](/docs/html/events.html#rosterInitialized) + * * [statusInitialized](/docs/html/events.html#statusInitialized) + * * [roomsPanelRendered](/docs/html/events.html#roomsPanelRendered) + * + * The various plugins might also provide promises, and they do this by using the + * `promises.add` api method. + * + * @namespace _converse.api.promises + * @memberOf _converse.api + */ + 'promises': { + /** + * By calling `promises.add`, a new [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) + * is made available for other code or plugins to depend on via the + * {@link _converse.api.waitUntil} method. + * + * Generally, it's the responsibility of the plugin which adds the promise to + * also resolve it. + * + * This is done by calling {@link _converse.api.emit}, which not only resolves the + * promise, but also emits an event with the same name (which can be listened to + * via {@link _converse.api.listen}). + * + * @method _converse.api.promises.add + * @param {string|array} [name|names] The name or an array of names for the promise(s) to be added + * @example _converse.api.promises.add('foo-completed'); + */ + 'add'(promises) { + promises = _.isArray(promises) ? promises : [promises]; + + _.each(promises, addPromise); + } + + }, + + /** + * This namespace lets you access the BOSH tokens + * + * @namespace _converse.api.tokens + * @memberOf _converse.api + */ + 'tokens': { + /** + * @method _converse.api.tokens.get + * @param {string} [id] The type of token to return ('rid' or 'sid'). + * @returns 'string' A token, either the RID or SID token depending on what's asked for. + * @example _converse.api.tokens.get('rid'); + */ + 'get'(id) { + if (!_converse.expose_rid_and_sid || _.isUndefined(_converse.connection)) { + return null; + } + + if (id.toLowerCase() === 'rid') { + return _converse.connection.rid || _converse.connection._proto.rid; + } else if (id.toLowerCase() === 'sid') { + return _converse.connection.sid || _converse.connection._proto.sid; + } + } + + }, + + /** + * Converse emits events to which you can subscribe to. + * + * The `listen` namespace exposes methods for creating event listeners + * (aka handlers) for these events. + * + * @namespace _converse.api.listen + * @memberOf _converse + */ + 'listen': { + /** + * Lets you listen to an event exactly once. + * + * @method _converse.api.listen.once + * @param {string} name The event's name + * @param {function} callback The callback method to be called when the event is emitted. + * @param {object} [context] The value of the `this` parameter for the callback. + * @example _converse.api.listen.once('message', function (messageXML) { ... }); + */ + 'once': _converse.once.bind(_converse), + + /** + * Lets you subscribe to an event. + * + * Every time the event fires, the callback method specified by `callback` will be called. + * + * @method _converse.api.listen.on + * @param {string} name The event's name + * @param {function} callback The callback method to be called when the event is emitted. + * @param {object} [context] The value of the `this` parameter for the callback. + * @example _converse.api.listen.on('message', function (messageXML) { ... }); + */ + 'on': _converse.on.bind(_converse), + + /** + * To stop listening to an event, you can use the `not` method. + * + * Every time the event fires, the callback method specified by `callback` will be called. + * + * @method _converse.api.listen.not + * @param {string} name The event's name + * @param {function} callback The callback method that is to no longer be called when the event fires + * @example _converse.api.listen.not('message', function (messageXML); + */ + 'not': _converse.off.bind(_converse), + + /** + * Subscribe to an incoming stanza + * + * Every a matched stanza is received, the callback method specified by `callback` will be called. + * + * @method _converse.api.listen.stanza + * @param {string} name The stanza's name + * @param {object} options Matching options + * (e.g. 'ns' for namespace, 'type' for stanza type, also 'id' and 'from'); + * @param {function} handler The callback method to be called when the stanza appears + */ + 'stanza'(name, options, handler) { + if (_.isFunction(options)) { + handler = options; + options = {}; + } else { + options = options || {}; + } + + _converse.connection.addHandler(handler, options.ns, name, options.type, options.id, options.from, options); + } + + }, + + /** + * Wait until a promise is resolved + * + * @method _converse.api.waitUntil + * @param {string} name The name of the promise + * @returns {Promise} + */ + 'waitUntil'(name) { + const promise = _converse.promises[name]; + + if (_.isUndefined(promise)) { + return null; + } + + return promise; + }, + + /** + * Allows you to send XML stanzas. + * + * @method _converse.api.send + * @example + * const msg = converse.env.$msg({ + * 'from': 'juliet@example.com/balcony', + * 'to': 'romeo@example.net', + * 'type':'chat' + * }); + * _converse.api.send(msg); + */ + 'send'(stanza) { + _converse.connection.send(stanza); + }, + + /** + * Send an IQ stanza and receive a promise + * + * @method _converse.api.sendIQ + * @returns {Promise} A promise which resolves when we receive a `result` stanza + * or is rejected when we receive an `error` stanza. + */ + 'sendIQ'(stanza) { + return new Promise((resolve, reject) => { + _converse.connection.sendIQ(stanza, resolve, reject, _converse.IQ_TIMEOUT); + }); + } + + }; + /** + * ### The Public API + * + * This namespace contains public API methods which are are + * accessible on the global `converse` object. + * They are public, because any JavaScript in the + * page can call them. Public methods therefore don’t expose any sensitive + * or closured data. To do that, you’ll need to create a plugin, which has + * access to the private API method. + * + * @namespace converse + */ + + const converse = { + /** + * Public API method which initializes Converse. + * This method must always be called when using Converse. + * + * @memberOf converse + * @method initialize + * @param {object} config A map of [configuration-settings](https://conversejs.org/docs/html/configuration.html#configuration-settings). + * + * @example + * converse.initialize({ + * allow_otr: true, + * auto_list_rooms: false, + * auto_subscribe: false, + * bosh_service_url: 'https://bind.example.com', + * hide_muc_server: false, + * i18n: locales['en'], + * keepalive: true, + * play_sounds: true, + * prebind: false, + * show_controlbox_by_default: true, + * debug: false, + * roster_groups: true + * }); + */ + 'initialize'(settings, callback) { + return _converse.initialize(settings, callback); + }, + + /** + * Exposes methods for adding and removing plugins. You'll need to write a plugin + * if you want to have access to the private API methods defined further down below. + * + * For more information on plugins, read the documentation on [writing a plugin](/docs/html/plugin_development.html). + * + * @namespace plugins + * @memberOf converse + */ + 'plugins': { + /** Registers a new plugin. + * + * @method converse.plugins.add + * @param {string} name The name of the plugin + * @param {object} plugin The plugin object + * + * @example + * + * const plugin = { + * initialize: function () { + * // Gets called as soon as the plugin has been loaded. + * + * // Inside this method, you have access to the private + * // API via `_covnerse.api`. + * + * // The private _converse object contains the core logic + * // and data-structures of Converse. + * } + * } + * converse.plugins.add('myplugin', plugin); + */ + 'add'(name, plugin) { + plugin.__name__ = name; + + if (!_.isUndefined(_converse.pluggable.plugins[name])) { + throw new TypeError(`Error: plugin with name "${name}" has already been ` + 'registered!'); + } else { + _converse.pluggable.plugins[name] = plugin; + } + } + + }, + + /** + * Utility methods and globals from bundled 3rd party libraries. + * @memberOf converse + * + * @property {function} converse.env.$build - Creates a Strophe.Builder, for creating stanza objects. + * @property {function} converse.env.$iq - Creates a Strophe.Builder with an element as the root. + * @property {function} converse.env.$msg - Creates a Strophe.Builder with an element as the root. + * @property {function} converse.env.$pres - Creates a Strophe.Builder with an element as the root. + * @property {object} converse.env.Backbone - The [Backbone](http://backbonejs.org) object used by Converse to create models and views. + * @property {function} converse.env.Promise - The Promise implementation used by Converse. + * @property {function} converse.env.Strophe - The [Strophe](http://strophe.im/strophejs) XMPP library used by Converse. + * @property {object} converse.env._ - The instance of [lodash](http://lodash.com) used by Converse. + * @property {function} converse.env.f - And instance of Lodash with its methods wrapped to produce immutable auto-curried iteratee-first data-last methods. + * @property {function} converse.env.b64_sha1 - Utility method from Strophe for creating base64 encoded sha1 hashes. + * @property {object} converse.env.moment - [Moment](https://momentjs.com) date manipulation library. + * @property {function} converse.env.sizzle - [Sizzle](https://sizzlejs.com) CSS selector engine. + * @property {object} converse.env.utils - Module containing common utility methods used by Converse. + */ + 'env': { + '$build': $build, + '$iq': $iq, + '$msg': $msg, + '$pres': $pres, + 'Backbone': Backbone, + 'Promise': Promise, + 'Strophe': Strophe, + '_': _, + 'f': f, + 'b64_sha1': b64_sha1, + 'moment': moment, + 'sizzle': sizzle, + 'utils': u + } + }; + window.converse = converse; + window.dispatchEvent(new CustomEvent('converse-loaded')); + return converse; +}); + +/***/ }), + +/***/ "./src/headless/converse-disco.js": +/*!****************************************!*\ + !*** ./src/headless/converse-disco.js ***! + \****************************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;// Converse.js +// http://conversejs.org +// +// Copyright (c) 2013-2018, the Converse developers +// Licensed under the Mozilla Public License (MPLv2) + +/* This is a Converse plugin which add support for XEP-0030: Service Discovery */ +(function (root, factory) { + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! ./converse-core */ "./src/headless/converse-core.js"), __webpack_require__(/*! sizzle */ "./node_modules/sizzle/dist/sizzle.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), + __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? + (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), + __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); +})(this, function (converse, sizzle) { + const _converse$env = converse.env, + Backbone = _converse$env.Backbone, + Promise = _converse$env.Promise, + Strophe = _converse$env.Strophe, + $iq = _converse$env.$iq, + b64_sha1 = _converse$env.b64_sha1, + utils = _converse$env.utils, + _ = _converse$env._, + f = _converse$env.f; + converse.plugins.add('converse-disco', { + initialize() { + /* The initialize function gets called as soon as the plugin is + * loaded by converse.js's plugin machinery. + */ + const _converse = this._converse; // Promises exposed by this plugin + + _converse.api.promises.add('discoInitialized'); + + _converse.DiscoEntity = Backbone.Model.extend({ + /* A Disco Entity is a JID addressable entity that can be queried + * for features. + * + * See XEP-0030: https://xmpp.org/extensions/xep-0030.html + */ + idAttribute: 'jid', + + initialize() { + this.waitUntilFeaturesDiscovered = utils.getResolveablePromise(); + this.dataforms = new Backbone.Collection(); + this.dataforms.browserStorage = new Backbone.BrowserStorage.session(b64_sha1(`converse.dataforms-{this.get('jid')}`)); + this.features = new Backbone.Collection(); + this.features.browserStorage = new Backbone.BrowserStorage.session(b64_sha1(`converse.features-${this.get('jid')}`)); + this.features.on('add', this.onFeatureAdded, this); + this.fields = new Backbone.Collection(); + this.fields.browserStorage = new Backbone.BrowserStorage.session(b64_sha1(`converse.fields-${this.get('jid')}`)); + this.fields.on('add', this.onFieldAdded, this); + this.identities = new Backbone.Collection(); + this.identities.browserStorage = new Backbone.BrowserStorage.session(b64_sha1(`converse.identities-${this.get('jid')}`)); + this.fetchFeatures(); + this.items = new _converse.DiscoEntities(); + this.items.browserStorage = new Backbone.BrowserStorage.session(b64_sha1(`converse.disco-items-${this.get('jid')}`)); + this.items.fetch(); + }, + + getIdentity(category, type) { + /* Returns a Promise which resolves with a map indicating + * whether a given identity is provided. + * + * Parameters: + * (String) category - The identity category + * (String) type - The identity type + */ + const entity = this; + return new Promise((resolve, reject) => { + function fulfillPromise() { + const model = entity.identities.findWhere({ + 'category': category, + 'type': type + }); + resolve(model); + } + + entity.waitUntilFeaturesDiscovered.then(fulfillPromise).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); + }); + }, + + hasFeature(feature) { + /* Returns a Promise which resolves with a map indicating + * whether a given feature is supported. + * + * Parameters: + * (String) feature - The feature that might be supported. + */ + const entity = this; + return new Promise((resolve, reject) => { + function fulfillPromise() { + if (entity.features.findWhere({ + 'var': feature + })) { + resolve(entity); + } else { + resolve(); + } + } + + entity.waitUntilFeaturesDiscovered.then(fulfillPromise).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); + }); + }, + + onFeatureAdded(feature) { + feature.entity = this; + + _converse.emit('serviceDiscovered', feature); + }, + + onFieldAdded(field) { + field.entity = this; + + _converse.emit('discoExtensionFieldDiscovered', field); + }, + + fetchFeatures() { + if (this.features.browserStorage.records.length === 0) { + this.queryInfo(); + } else { + this.features.fetch({ + add: true, + success: () => { + this.waitUntilFeaturesDiscovered.resolve(this); + this.trigger('featuresDiscovered'); + } + }); + this.identities.fetch({ + add: true + }); + } + }, + + queryInfo() { + _converse.api.disco.info(this.get('jid'), null).then(stanza => this.onInfo(stanza)).catch(iq => { + this.waitUntilFeaturesDiscovered.resolve(this); + + _converse.log(iq, Strophe.LogLevel.ERROR); + }); + }, + + onDiscoItems(stanza) { + _.each(sizzle(`query[xmlns="${Strophe.NS.DISCO_ITEMS}"] item`, stanza), item => { + if (item.getAttribute("node")) { + // XXX: ignore nodes for now. + // See: https://xmpp.org/extensions/xep-0030.html#items-nodes + return; + } + + const jid = item.getAttribute('jid'); + + if (_.isUndefined(this.items.get(jid))) { + const entity = _converse.disco_entities.get(jid); + + if (entity) { + this.items.add(entity); + } else { + this.items.create({ + 'jid': jid + }); + } + } + }); + }, + + queryForItems() { + if (_.isEmpty(this.identities.where({ + 'category': 'server' + }))) { + // Don't fetch features and items if this is not a + // server or a conference component. + return; + } + + _converse.api.disco.items(this.get('jid')).then(stanza => this.onDiscoItems(stanza)); + }, + + onInfo(stanza) { + _.forEach(stanza.querySelectorAll('identity'), identity => { + this.identities.create({ + 'category': identity.getAttribute('category'), + 'type': identity.getAttribute('type'), + 'name': identity.getAttribute('name') + }); + }); + + _.each(sizzle(`x[type="result"][xmlns="${Strophe.NS.XFORM}"]`, stanza), form => { + const data = {}; + + _.each(form.querySelectorAll('field'), field => { + data[field.getAttribute('var')] = { + 'value': _.get(field.querySelector('value'), 'textContent'), + 'type': field.getAttribute('type') + }; + }); + + this.dataforms.create(data); + }); + + if (stanza.querySelector(`feature[var="${Strophe.NS.DISCO_ITEMS}"]`)) { + this.queryForItems(); + } + + _.forEach(stanza.querySelectorAll('feature'), feature => { + this.features.create({ + 'var': feature.getAttribute('var'), + 'from': stanza.getAttribute('from') + }); + }); // XEP-0128 Service Discovery Extensions + + + _.forEach(sizzle('x[type="result"][xmlns="jabber:x:data"] field', stanza), field => { + this.fields.create({ + 'var': field.getAttribute('var'), + 'value': _.get(field.querySelector('value'), 'textContent'), + 'from': stanza.getAttribute('from') + }); + }); + + this.waitUntilFeaturesDiscovered.resolve(this); + this.trigger('featuresDiscovered'); + } + + }); + _converse.DiscoEntities = Backbone.Collection.extend({ + model: _converse.DiscoEntity, + + fetchEntities() { + return new Promise((resolve, reject) => { + this.fetch({ + add: true, + success: resolve, + + error() { + reject(new Error("Could not fetch disco entities")); + } + + }); + }); + } + + }); + + function addClientFeatures() { + // See http://xmpp.org/registrar/disco-categories.html + _converse.api.disco.own.identities.add('client', 'web', 'Converse'); + + _converse.api.disco.own.features.add(Strophe.NS.BOSH); + + _converse.api.disco.own.features.add(Strophe.NS.CHATSTATES); + + _converse.api.disco.own.features.add(Strophe.NS.DISCO_INFO); + + _converse.api.disco.own.features.add(Strophe.NS.ROSTERX); // Limited support + + + if (_converse.message_carbons) { + _converse.api.disco.own.features.add(Strophe.NS.CARBONS); + } + + _converse.emit('addClientFeatures'); + + return this; + } + + function initStreamFeatures() { + _converse.stream_features = new Backbone.Collection(); + _converse.stream_features.browserStorage = new Backbone.BrowserStorage.session(b64_sha1(`converse.stream-features-${_converse.bare_jid}`)); + + _converse.stream_features.fetch({ + success(collection) { + if (collection.length === 0 && _converse.connection.features) { + _.forEach(_converse.connection.features.childNodes, feature => { + _converse.stream_features.create({ + 'name': feature.nodeName, + 'xmlns': feature.getAttribute('xmlns') + }); + }); + } + } + + }); + + _converse.emit('streamFeaturesAdded'); + } + + function initializeDisco() { + addClientFeatures(); + + _converse.connection.addHandler(onDiscoInfoRequest, Strophe.NS.DISCO_INFO, 'iq', 'get', null, null); + + _converse.disco_entities = new _converse.DiscoEntities(); + _converse.disco_entities.browserStorage = new Backbone.BrowserStorage.session(b64_sha1(`converse.disco-entities-${_converse.bare_jid}`)); + + _converse.disco_entities.fetchEntities().then(collection => { + if (collection.length === 0 || !collection.get(_converse.domain)) { + // If we don't have an entity for our own XMPP server, + // create one. + _converse.disco_entities.create({ + 'jid': _converse.domain + }); + } + + _converse.emit('discoInitialized'); + }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); + } + + _converse.api.listen.on('sessionInitialized', initStreamFeatures); + + _converse.api.listen.on('reconnected', initializeDisco); + + _converse.api.listen.on('connected', initializeDisco); + + _converse.api.listen.on('beforeTearDown', () => { + if (_converse.disco_entities) { + _converse.disco_entities.each(entity => { + entity.features.reset(); + + entity.features.browserStorage._clear(); + }); + + _converse.disco_entities.reset(); + + _converse.disco_entities.browserStorage._clear(); + } + }); + + const plugin = this; + plugin._identities = []; + plugin._features = []; + + function onDiscoInfoRequest(stanza) { + const node = stanza.getElementsByTagName('query')[0].getAttribute('node'); + const attrs = { + xmlns: Strophe.NS.DISCO_INFO + }; + + if (node) { + attrs.node = node; + } + + const iqresult = $iq({ + 'type': 'result', + 'id': stanza.getAttribute('id') + }); + const from = stanza.getAttribute('from'); + + if (from !== null) { + iqresult.attrs({ + 'to': from + }); + } + + iqresult.c('query', attrs); + + _.each(plugin._identities, identity => { + const attrs = { + 'category': identity.category, + 'type': identity.type + }; + + if (identity.name) { + attrs.name = identity.name; + } + + if (identity.lang) { + attrs['xml:lang'] = identity.lang; + } + + iqresult.c('identity', attrs).up(); + }); + + _.each(plugin._features, feature => { + iqresult.c('feature', { + 'var': feature + }).up(); + }); + + _converse.connection.send(iqresult.tree()); + + return true; + } + + _.extend(_converse.api, { + /** + * The XEP-0030 service discovery API + * + * This API lets you discover information about entities on the + * XMPP network. + * + * @namespace _converse.api.disco + * @memberOf _converse.api + */ + 'disco': { + /** + * @namespace _converse.api.disco.stream + * @memberOf _converse.api.disco + */ + 'stream': { + /** + * @method _converse.api.disco.stream.getFeature + * @param {String} name The feature name + * @param {String} xmlns The XML namespace + * @example _converse.api.disco.stream.getFeature('ver', 'urn:xmpp:features:rosterver') + */ + 'getFeature': function getFeature(name, xmlns) { + if (_.isNil(name) || _.isNil(xmlns)) { + throw new Error("name and xmlns need to be provided when calling disco.stream.getFeature"); + } + + return _converse.stream_features.findWhere({ + 'name': name, + 'xmlns': xmlns + }); + } + }, + + /** + * @namespace _converse.api.disco.own + * @memberOf _converse.api.disco + */ + 'own': { + /** + * @namespace _converse.api.disco.own.identities + * @memberOf _converse.api.disco.own + */ + 'identities': { + /** + * Lets you add new identities for this client (i.e. instance of Converse) + * @method _converse.api.disco.own.identities.add + * + * @param {String} category - server, client, gateway, directory, etc. + * @param {String} type - phone, pc, web, etc. + * @param {String} name - "Converse" + * @param {String} lang - en, el, de, etc. + * + * @example _converse.api.disco.own.identities.clear(); + */ + add(category, type, name, lang) { + for (var i = 0; i < plugin._identities.length; i++) { + if (plugin._identities[i].category == category && plugin._identities[i].type == type && plugin._identities[i].name == name && plugin._identities[i].lang == lang) { + return false; + } + } + + plugin._identities.push({ + category: category, + type: type, + name: name, + lang: lang + }); + }, + + /** + * Clears all previously registered identities. + * @method _converse.api.disco.own.identities.clear + * @example _converse.api.disco.own.identities.clear(); + */ + clear() { + plugin._identities = []; + }, + + /** + * Returns all of the identities registered for this client + * (i.e. instance of Converse). + * @method _converse.api.disco.identities.get + * @example const identities = _converse.api.disco.own.identities.get(); + */ + get() { + return plugin._identities; + } + + }, + + /** + * @namespace _converse.api.disco.own.features + * @memberOf _converse.api.disco.own + */ + 'features': { + /** + * Lets you register new disco features for this client (i.e. instance of Converse) + * @method _converse.api.disco.own.features.add + * @param {String} name - e.g. http://jabber.org/protocol/caps + * @example _converse.api.disco.own.features.add("http://jabber.org/protocol/caps"); + */ + add(name) { + for (var i = 0; i < plugin._features.length; i++) { + if (plugin._features[i] == name) { + return false; + } + } + + plugin._features.push(name); + }, + + /** + * Clears all previously registered features. + * @method _converse.api.disco.own.features.clear + * @example _converse.api.disco.own.features.clear(); + */ + clear() { + plugin._features = []; + }, + + /** + * Returns all of the features registered for this client (i.e. instance of Converse). + * @method _converse.api.disco.own.features.get + * @example const features = _converse.api.disco.own.features.get(); + */ + get() { + return plugin._features; + } + + } + }, + + /** + * Query for information about an XMPP entity + * + * @method _converse.api.disco.info + * @param {string} jid The Jabber ID of the entity to query + * @param {string} [node] A specific node identifier associated with the JID + * @returns {promise} Promise which resolves once we have a result from the server. + */ + 'info'(jid, node) { + const attrs = { + xmlns: Strophe.NS.DISCO_INFO + }; + + if (node) { + attrs.node = node; + } + + const info = $iq({ + 'from': _converse.connection.jid, + 'to': jid, + 'type': 'get' + }).c('query', attrs); + return _converse.api.sendIQ(info); + }, + + /** + * Query for items associated with an XMPP entity + * + * @method _converse.api.disco.items + * @param {string} jid The Jabber ID of the entity to query for items + * @param {string} [node] A specific node identifier associated with the JID + * @returns {promise} Promise which resolves once we have a result from the server. + */ + 'items'(jid, node) { + const attrs = { + 'xmlns': Strophe.NS.DISCO_ITEMS + }; + + if (node) { + attrs.node = node; + } + + return _converse.api.sendIQ($iq({ + 'from': _converse.connection.jid, + 'to': jid, + 'type': 'get' + }).c('query', attrs)); + }, + + /** + * Namespace for methods associated with disco entities + * + * @namespace _converse.api.disco.entities + * @memberOf _converse.api.disco + */ + 'entities': { + /** + * Get the the corresponding `DiscoEntity` instance. + * + * @method _converse.api.disco.entities.get + * @param {string} jid The Jabber ID of the entity + * @param {boolean} [create] Whether the entity should be created if it doesn't exist. + * @example _converse.api.disco.entities.get(jid); + */ + 'get'(jid, create = false) { + return _converse.api.waitUntil('discoInitialized').then(() => { + if (_.isNil(jid)) { + return _converse.disco_entities; + } + + const entity = _converse.disco_entities.get(jid); + + if (entity || !create) { + return entity; + } + + return _converse.disco_entities.create({ + 'jid': jid + }); + }); + } + + }, + + /** + * Used to determine whether an entity supports a given feature. + * + * @method _converse.api.disco.supports + * @param {string} feature The feature that might be + * supported. In the XML stanza, this is the `var` + * attribute of the `` element. For + * example: `http://jabber.org/protocol/muc` + * @param {string} jid The JID of the entity + * (and its associated items) which should be queried + * @returns {promise} A promise which resolves with a list containing + * _converse.Entity instances representing the entity + * itself or those items associated with the entity if + * they support the given feature. + * + * @example + * _converse.api.disco.supports(Strophe.NS.MAM, _converse.bare_jid) + * .then(value => { + * // `value` is a map with two keys, `supported` and `feature`. + * if (value.supported) { + * // The feature is supported + * } else { + * // The feature is not supported + * } + * }).catch(() => { + * _converse.log( + * "Error or timeout while checking for feature support", + * Strophe.LogLevel.ERROR + * ); + * }); + */ + 'supports'(feature, jid) { + if (_.isNil(jid)) { + throw new TypeError('api.disco.supports: You need to provide an entity JID'); + } + + return _converse.api.waitUntil('discoInitialized').then(() => _converse.api.disco.entities.get(jid, true)).then(entity => entity.waitUntilFeaturesDiscovered).then(entity => { + const promises = _.concat(entity.items.map(item => item.hasFeature(feature)), entity.hasFeature(feature)); + + return Promise.all(promises); + }).then(result => f.filter(f.isObject, result)); + }, + + /** + * Refresh the features (and fields and identities) associated with a + * disco entity by refetching them from the server + * + * @method _converse.api.disco.refreshFeatures + * @param {string} jid The JID of the entity whose features are refreshed. + * @returns {promise} A promise which resolves once the features have been refreshed + * @example + * await _converse.api.disco.refreshFeatures('room@conference.example.org'); + */ + 'refreshFeatures'(jid) { + if (_.isNil(jid)) { + throw new TypeError('api.disco.refreshFeatures: You need to provide an entity JID'); + } + + return _converse.api.waitUntil('discoInitialized').then(() => _converse.api.disco.entities.get(jid, true)).then(entity => { + entity.features.reset(); + entity.fields.reset(); + entity.identities.reset(); + entity.waitUntilFeaturesDiscovered = utils.getResolveablePromise(); + entity.queryInfo(); + return entity.waitUntilFeaturesDiscovered; + }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); + }, + + /** + * Return all the features associated with a disco entity + * + * @method _converse.api.disco.getFeatures + * @param {string} jid The JID of the entity whose features are returned. + * @returns {promise} A promise which resolves with the returned features + * @example + * const features = await _converse.api.disco.getFeatures('room@conference.example.org'); + */ + 'getFeatures'(jid) { + if (_.isNil(jid)) { + throw new TypeError('api.disco.getFeatures: You need to provide an entity JID'); + } + + return _converse.api.waitUntil('discoInitialized').then(() => _converse.api.disco.entities.get(jid, true)).then(entity => entity.waitUntilFeaturesDiscovered).then(entity => entity.features).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); + }, + + /** + * Return all the service discovery extensions fields + * associated with an entity. + * + * See [XEP-0129: Service Discovery Extensions](https://xmpp.org/extensions/xep-0128.html) + * + * @method _converse.api.disco.getFields + * @param {string} jid The JID of the entity whose fields are returned. + * @example + * const fields = await _converse.api.disco.getFields('room@conference.example.org'); + */ + 'getFields'(jid) { + if (_.isNil(jid)) { + throw new TypeError('api.disco.getFields: You need to provide an entity JID'); + } + + return _converse.api.waitUntil('discoInitialized').then(() => _converse.api.disco.entities.get(jid, true)).then(entity => entity.waitUntilFeaturesDiscovered).then(entity => entity.fields).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); + }, + + /** + * Get the identity (with the given category and type) for a given disco entity. + * + * For example, when determining support for PEP (personal eventing protocol), you + * want to know whether the user's own JID has an identity with + * `category='pubsub'` and `type='pep'` as explained in this section of + * XEP-0163: https://xmpp.org/extensions/xep-0163.html#support + * + * @method _converse.api.disco.getIdentity + * @param {string} The identity category. + * In the XML stanza, this is the `category` + * attribute of the `` element. + * For example: 'pubsub' + * @param {string} type The identity type. + * In the XML stanza, this is the `type` + * attribute of the `` element. + * For example: 'pep' + * @param {string} jid The JID of the entity which might have the identity + * @returns {promise} A promise which resolves with a map indicating + * whether an identity with a given type is provided by the entity. + * @example + * _converse.api.disco.getIdentity('pubsub', 'pep', _converse.bare_jid).then( + * function (identity) { + * if (_.isNil(identity)) { + * // The entity DOES NOT have this identity + * } else { + * // The entity DOES have this identity + * } + * } + * ).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); + */ + 'getIdentity'(category, type, jid) { + return _converse.api.disco.entities.get(jid, true).then(e => e.getIdentity(category, type)); + } + + } + }); + } + + }); +}); + +/***/ }), + +/***/ "./src/headless/converse-mam.js": +/*!**************************************!*\ + !*** ./src/headless/converse-mam.js ***! + \**************************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;// Converse.js (A browser based XMPP chat client) +// http://conversejs.org +// +// Copyright (c) 2012-2017, Jan-Carel Brand +// Licensed under the Mozilla Public License (MPLv2) +// + +/*global define */ +// XEP-0059 Result Set Management +(function (root, factory) { + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! sizzle */ "./node_modules/sizzle/dist/sizzle.js"), __webpack_require__(/*! ./converse-core */ "./src/headless/converse-core.js"), __webpack_require__(/*! ./converse-disco */ "./src/headless/converse-disco.js"), __webpack_require__(/*! strophejs-plugin-rsm */ "./node_modules/strophejs-plugin-rsm/lib/strophe.rsm.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), + __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? + (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), + __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); +})(this, function (sizzle, converse) { + "use strict"; + + const CHATROOMS_TYPE = 'chatroom'; + const _converse$env = converse.env, + Promise = _converse$env.Promise, + Strophe = _converse$env.Strophe, + $iq = _converse$env.$iq, + _ = _converse$env._, + moment = _converse$env.moment; + const u = converse.env.utils; + const RSM_ATTRIBUTES = ['max', 'first', 'last', 'after', 'before', 'index', 'count']; // XEP-0313 Message Archive Management + + const MAM_ATTRIBUTES = ['with', 'start', 'end']; + + function getMessageArchiveID(stanza) { + // See https://xmpp.org/extensions/xep-0313.html#results + // + // The result messages MUST contain a element with an 'id' + // attribute that gives the current message's archive UID + const result = sizzle(`result[xmlns="${Strophe.NS.MAM}"]`, stanza).pop(); + + if (!_.isUndefined(result)) { + return result.getAttribute('id'); + } // See: https://xmpp.org/extensions/xep-0313.html#archives_id + + + const stanza_id = sizzle(`stanza-id[xmlns="${Strophe.NS.SID}"]`, stanza).pop(); + + if (!_.isUndefined(stanza_id)) { + return stanza_id.getAttribute('id'); + } + } + + function queryForArchivedMessages(_converse, options, callback, errback) { + /* Internal function, called by the "archive.query" API method. + */ + let date; + + if (_.isFunction(options)) { + callback = options; + errback = callback; + options = null; + } + + const queryid = _converse.connection.getUniqueId(); + + const attrs = { + 'type': 'set' + }; + + if (options && options.groupchat) { + if (!options['with']) { + // eslint-disable-line dot-notation + throw new Error('You need to specify a "with" value containing ' + 'the chat room JID, when querying groupchat messages.'); + } + + attrs.to = options['with']; // eslint-disable-line dot-notation + } + + const stanza = $iq(attrs).c('query', { + 'xmlns': Strophe.NS.MAM, + 'queryid': queryid + }); + + if (options) { + stanza.c('x', { + 'xmlns': Strophe.NS.XFORM, + 'type': 'submit' + }).c('field', { + 'var': 'FORM_TYPE', + 'type': 'hidden' + }).c('value').t(Strophe.NS.MAM).up().up(); + + if (options['with'] && !options.groupchat) { + // eslint-disable-line dot-notation + stanza.c('field', { + 'var': 'with' + }).c('value').t(options['with']).up().up(); // eslint-disable-line dot-notation + } + + _.each(['start', 'end'], function (t) { + if (options[t]) { + date = moment(options[t]); + + if (date.isValid()) { + stanza.c('field', { + 'var': t + }).c('value').t(date.format()).up().up(); + } else { + throw new TypeError(`archive.query: invalid date provided for: ${t}`); + } + } + }); + + stanza.up(); + + if (options instanceof Strophe.RSM) { + stanza.cnode(options.toXML()); + } else if (_.intersection(RSM_ATTRIBUTES, _.keys(options)).length) { + stanza.cnode(new Strophe.RSM(options).toXML()); + } + } + + const messages = []; + + const message_handler = _converse.connection.addHandler(message => { + if (options.groupchat && message.getAttribute('from') !== options['with']) { + // eslint-disable-line dot-notation + return true; + } + + const result = message.querySelector('result'); + + if (!_.isNull(result) && result.getAttribute('queryid') === queryid) { + messages.push(message); + } + + return true; + }, Strophe.NS.MAM); + + _converse.connection.sendIQ(stanza, function (iq) { + _converse.connection.deleteHandler(message_handler); + + if (_.isFunction(callback)) { + const set = iq.querySelector('set'); + let rsm; + + if (!_.isUndefined(set)) { + rsm = new Strophe.RSM({ + xml: set + }); + + _.extend(rsm, _.pick(options, _.concat(MAM_ATTRIBUTES, ['max']))); + } + + callback(messages, rsm); + } + }, function () { + _converse.connection.deleteHandler(message_handler); + + if (_.isFunction(errback)) { + errback.apply(this, arguments); + } + }, _converse.message_archiving_timeout); + } + + converse.plugins.add('converse-mam', { + dependencies: ['converse-chatview', 'converse-muc', 'converse-muc-views'], + overrides: { + // Overrides mentioned here will be picked up by converse.js's + // plugin architecture they will replace existing methods on the + // relevant objects or classes. + // + // New functions which don't exist yet can also be added. + ChatBox: { + getMessageAttributesFromStanza(message, original_stanza) { + function _process(attrs) { + const archive_id = getMessageArchiveID(original_stanza); + + if (archive_id) { + attrs.archive_id = archive_id; + } + + return attrs; + } + + const result = this.__super__.getMessageAttributesFromStanza.apply(this, arguments); + + if (result instanceof Promise) { + return new Promise((resolve, reject) => result.then(attrs => resolve(_process(attrs))).catch(reject)); + } else { + return _process(result); + } + } + + }, + ChatBoxView: { + render() { + const result = this.__super__.render.apply(this, arguments); + + if (!this.disable_mam) { + this.content.addEventListener('scroll', _.debounce(this.onScroll.bind(this), 100)); + } + + return result; + }, + + fetchNewestMessages() { + /* Fetches messages that might have been archived *after* + * the last archived message in our local cache. + */ + if (this.disable_mam) { + return; + } + + const _converse = this.__super__._converse, + most_recent_msg = u.getMostRecentMessage(this.model); + + if (_.isNil(most_recent_msg)) { + this.fetchArchivedMessages(); + } else { + const archive_id = most_recent_msg.get('archive_id'); + + if (archive_id) { + this.fetchArchivedMessages({ + 'after': most_recent_msg.get('archive_id') + }); + } else { + this.fetchArchivedMessages({ + 'start': most_recent_msg.get('time') + }); + } + } + }, + + fetchArchivedMessagesIfNecessary() { + /* Check if archived messages should be fetched, and if so, do so. */ + if (this.disable_mam || this.model.get('mam_initialized')) { + return; + } + + const _converse = this.__super__._converse; + + _converse.api.disco.supports(Strophe.NS.MAM, _converse.bare_jid).then(result => { + // Success + if (result.length) { + this.fetchArchivedMessages(); + } + + this.model.save({ + 'mam_initialized': true + }); + }, () => { + // Error + _converse.log("Error or timeout while checking for MAM support", Strophe.LogLevel.ERROR); + }).catch(msg => { + this.clearSpinner(); + + _converse.log(msg, Strophe.LogLevel.FATAL); + }); + }, + + fetchArchivedMessages(options) { + const _converse = this.__super__._converse; + + if (this.disable_mam) { + return; + } + + const is_groupchat = this.model.get('type') === CHATROOMS_TYPE; + let mam_jid, message_handler; + + if (is_groupchat) { + mam_jid = this.model.get('jid'); + message_handler = this.model.onMessage.bind(this.model); + } else { + mam_jid = _converse.bare_jid; + message_handler = _converse.chatboxes.onMessage.bind(_converse.chatboxes); + } + + _converse.api.disco.supports(Strophe.NS.MAM, mam_jid).then(results => { + // Success + if (!results.length) { + return; + } + + this.addSpinner(); + + _converse.api.archive.query(_.extend({ + 'groupchat': is_groupchat, + 'before': '', + // Page backwards from the most recent message + 'max': _converse.archived_messages_page_size, + 'with': this.model.get('jid') + }, options), messages => { + // Success + this.clearSpinner(); + + _.each(messages, message_handler); + }, () => { + // Error + this.clearSpinner(); + + _converse.log("Error or timeout while trying to fetch " + "archived messages", Strophe.LogLevel.ERROR); + }); + }, () => { + // Error + _converse.log("Error or timeout while checking for MAM support", Strophe.LogLevel.ERROR); + }).catch(msg => { + this.clearSpinner(); + + _converse.log(msg, Strophe.LogLevel.FATAL); + }); + }, + + onScroll(ev) { + const _converse = this.__super__._converse; + + if (this.content.scrollTop === 0 && this.model.messages.length) { + const oldest_message = this.model.messages.at(0); + const archive_id = oldest_message.get('archive_id'); + + if (archive_id) { + this.fetchArchivedMessages({ + 'before': archive_id + }); + } else { + this.fetchArchivedMessages({ + 'end': oldest_message.get('time') + }); + } + } + } + + }, + ChatRoom: { + isDuplicate(message, original_stanza) { + const result = this.__super__.isDuplicate.apply(this, arguments); + + if (result) { + return result; + } + + const archive_id = getMessageArchiveID(original_stanza); + + if (archive_id) { + return this.messages.filter({ + 'archive_id': archive_id + }).length > 0; + } + } + + }, + ChatRoomView: { + initialize() { + const _converse = this.__super__._converse; + + this.__super__.initialize.apply(this, arguments); + + this.model.on('change:mam_enabled', this.fetchArchivedMessagesIfNecessary, this); + this.model.on('change:connection_status', this.fetchArchivedMessagesIfNecessary, this); + }, + + renderChatArea() { + const result = this.__super__.renderChatArea.apply(this, arguments); + + if (!this.disable_mam) { + this.content.addEventListener('scroll', _.debounce(this.onScroll.bind(this), 100)); + } + + return result; + }, + + fetchArchivedMessagesIfNecessary() { + if (this.model.get('connection_status') !== converse.ROOMSTATUS.ENTERED || !this.model.get('mam_enabled') || this.model.get('mam_initialized')) { + return; + } + + this.fetchArchivedMessages(); + this.model.save({ + 'mam_initialized': true + }); + } + + } + }, + + initialize() { + /* The initialize function gets called as soon as the plugin is + * loaded by Converse.js's plugin machinery. + */ + const _converse = this._converse; + + _converse.api.settings.update({ + archived_messages_page_size: '50', + message_archiving: undefined, + // Supported values are 'always', 'never', 'roster' (https://xmpp.org/extensions/xep-0313.html#prefs) + message_archiving_timeout: 8000 // Time (in milliseconds) to wait before aborting MAM request + + }); + + _converse.onMAMError = function (model, iq) { + if (iq.querySelectorAll('feature-not-implemented').length) { + _converse.log("Message Archive Management (XEP-0313) not supported by this server", Strophe.LogLevel.WARN); + } else { + _converse.log("An error occured while trying to set archiving preferences.", Strophe.LogLevel.ERROR); + + _converse.log(iq); + } + }; + + _converse.onMAMPreferences = function (feature, iq) { + /* Handle returned IQ stanza containing Message Archive + * Management (XEP-0313) preferences. + * + * XXX: For now we only handle the global default preference. + * The XEP also provides for per-JID preferences, which is + * currently not supported in converse.js. + * + * Per JID preferences will be set in chat boxes, so it'll + * probbaly be handled elsewhere in any case. + */ + const preference = sizzle(`prefs[xmlns="${Strophe.NS.MAM}"]`, iq).pop(); + const default_pref = preference.getAttribute('default'); + + if (default_pref !== _converse.message_archiving) { + const stanza = $iq({ + 'type': 'set' + }).c('prefs', { + 'xmlns': Strophe.NS.MAM, + 'default': _converse.message_archiving + }); + + _.each(preference.children, function (child) { + stanza.cnode(child).up(); + }); + + _converse.connection.sendIQ(stanza, _.partial(function (feature, iq) { + // XXX: Strictly speaking, the server should respond with the updated prefs + // (see example 18: https://xmpp.org/extensions/xep-0313.html#config) + // but Prosody doesn't do this, so we don't rely on it. + feature.save({ + 'preferences': { + 'default': _converse.message_archiving + } + }); + }, feature), _converse.onMAMError); + } else { + feature.save({ + 'preferences': { + 'default': _converse.message_archiving + } + }); + } + }; + /* Event handlers */ + + + _converse.on('serviceDiscovered', feature => { + const prefs = feature.get('preferences') || {}; + + if (feature.get('var') === Strophe.NS.MAM && prefs['default'] !== _converse.message_archiving && // eslint-disable-line dot-notation + !_.isUndefined(_converse.message_archiving)) { + // Ask the server for archiving preferences + _converse.connection.sendIQ($iq({ + 'type': 'get' + }).c('prefs', { + 'xmlns': Strophe.NS.MAM + }), _.partial(_converse.onMAMPreferences, feature), _.partial(_converse.onMAMError, feature)); + } + }); + + _converse.on('addClientFeatures', () => { + _converse.api.disco.own.features.add(Strophe.NS.MAM); + }); + + _converse.on('afterMessagesFetched', chatboxview => { + chatboxview.fetchNewestMessages(); + }); + + _converse.on('reconnected', () => { + const private_chats = _converse.chatboxviews.filter(view => _.at(view, 'model.attributes.type')[0] === 'chatbox'); + + _.each(private_chats, view => view.fetchNewestMessages()); + }); + + _.extend(_converse.api, { + /** + * The [XEP-0313](https://xmpp.org/extensions/xep-0313.html) Message Archive Management API + * + * Enables you to query an XMPP server for archived messages. + * + * See also the [message-archiving](/docs/html/configuration.html#message-archiving) + * option in the configuration settings section, which you'll + * usually want to use in conjunction with this API. + * + * @namespace _converse.api.archive + * @memberOf _converse.api + */ + 'archive': { + /** + * Query for archived messages. + * + * The options parameter can also be an instance of + * Strophe.RSM to enable easy querying between results pages. + * + * @method _converse.api.archive.query + * @param {(Object|Strophe.RSM)} options Query parameters, either + * MAM-specific or also for Result Set Management. + * Can be either an object or an instance of Strophe.RSM. + * Valid query parameters are: + * * `with` + * * `start` + * * `end` + * * `first` + * * `last` + * * `after` + * * `before` + * * `index` + * * `count` + * @param {Function} callback A function to call whenever + * we receive query-relevant stanza. + * When the callback is called, a Strophe.RSM object is + * returned on which "next" or "previous" can be called + * before passing it in again to this method, to + * get the next or previous page in the result set. + * @param {Function} errback A function to call when an + * error stanza is received, for example when it + * doesn't support message archiving. + * + * @example + * // Requesting all archived messages + * // ================================ + * // + * // The simplest query that can be made is to simply not pass in any parameters. + * // Such a query will return all archived messages for the current user. + * // + * // Generally, you'll however always want to pass in a callback method, to receive + * // the returned messages. + * + * this._converse.api.archive.query( + * (messages) => { + * // Do something with the messages, like showing them in your webpage. + * }, + * (iq) => { + * // The query was not successful, perhaps inform the user? + * // The IQ stanza returned by the XMPP server is passed in, so that you + * // may inspect it and determine what the problem was. + * } + * ) + * @example + * // Waiting until server support has been determined + * // ================================================ + * // + * // The query method will only work if Converse has been able to determine that + * // the server supports MAM queries, otherwise the following error will be raised: + * // + * // "This server does not support XEP-0313, Message Archive Management" + * // + * // The very first time Converse loads in a browser tab, if you call the query + * // API too quickly, the above error might appear because service discovery has not + * // yet been completed. + * // + * // To work solve this problem, you can first listen for the `serviceDiscovered` event, + * // through which you can be informed once support for MAM has been determined. + * + * _converse.api.listen.on('serviceDiscovered', function (feature) { + * if (feature.get('var') === converse.env.Strophe.NS.MAM) { + * _converse.api.archive.query() + * } + * }); + * + * @example + * // Requesting all archived messages for a particular contact or room + * // ================================================================= + * // + * // To query for messages sent between the current user and another user or room, + * // the query options need to contain the the JID (Jabber ID) of the user or + * // room under the `with` key. + * + * // For a particular user + * this._converse.api.archive.query({'with': 'john@doe.net'}, callback, errback);) + * + * // For a particular room + * this._converse.api.archive.query({'with': 'discuss@conference.doglovers.net'}, callback, errback);) + * + * @example + * // Requesting all archived messages before or after a certain date + * // =============================================================== + * // + * // The `start` and `end` parameters are used to query for messages + * // within a certain timeframe. The passed in date values may either be ISO8601 + * // formatted date strings, or JavaScript Date objects. + * + * const options = { + * 'with': 'john@doe.net', + * 'start': '2010-06-07T00:00:00Z', + * 'end': '2010-07-07T13:23:54Z' + * }; + * this._converse.api.archive.query(options, callback, errback); + * + * @example + * // Limiting the amount of messages returned + * // ======================================== + * // + * // The amount of returned messages may be limited with the `max` parameter. + * // By default, the messages are returned from oldest to newest. + * + * // Return maximum 10 archived messages + * this._converse.api.archive.query({'with': 'john@doe.net', 'max':10}, callback, errback); + * + * @example + * // Paging forwards through a set of archived messages + * // ================================================== + * // + * // When limiting the amount of messages returned per query, you might want to + * // repeatedly make a further query to fetch the next batch of messages. + * // + * // To simplify this usecase for you, the callback method receives not only an array + * // with the returned archived messages, but also a special RSM (*Result Set + * // Management*) object which contains the query parameters you passed in, as well + * // as two utility methods `next`, and `previous`. + * // + * // When you call one of these utility methods on the returned RSM object, and then + * // pass the result into a new query, you'll receive the next or previous batch of + * // archived messages. Please note, when calling these methods, pass in an integer + * // to limit your results. + * + * const callback = function (messages, rsm) { + * // Do something with the messages, like showing them in your webpage. + * // ... + * // You can now use the returned "rsm" object, to fetch the next batch of messages: + * _converse.api.archive.query(rsm.next(10), callback, errback)) + * + * } + * _converse.api.archive.query({'with': 'john@doe.net', 'max':10}, callback, errback); + * + * @example + * // Paging backwards through a set of archived messages + * // =================================================== + * // + * // To page backwards through the archive, you need to know the UID of the message + * // which you'd like to page backwards from and then pass that as value for the + * // `before` parameter. If you simply want to page backwards from the most recent + * // message, pass in the `before` parameter with an empty string value `''`. + * + * _converse.api.archive.query({'before': '', 'max':5}, function (message, rsm) { + * // Do something with the messages, like showing them in your webpage. + * // ... + * // You can now use the returned "rsm" object, to fetch the previous batch of messages: + * rsm.previous(5); // Call previous method, to update the object's parameters, + * // passing in a limit value of 5. + * // Now we query again, to get the previous batch. + * _converse.api.archive.query(rsm, callback, errback); + * } + */ + 'query': function query(options, callback, errback) { + if (!_converse.api.connection.connected()) { + throw new Error('Can\'t call `api.archive.query` before having established an XMPP session'); + } + + return queryForArchivedMessages(_converse, options, callback, errback); + } + } + }); + } + + }); +}); + +/***/ }), + +/***/ "./src/headless/converse-muc.js": +/*!**************************************!*\ + !*** ./src/headless/converse-muc.js ***! + \**************************************/ /*! no static exports found */ /***/ (function(module, exports, __webpack_require__) { @@ -70406,7 +75580,7 @@ function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } // Copyright (c) 2013-2018, the Converse.js developers // Licensed under the Mozilla Public License (MPLv2) (function (root, factory) { - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! utils/form */ "./src/utils/form.js"), __webpack_require__(/*! converse-core */ "./src/converse-core.js"), __webpack_require__(/*! converse-disco */ "./src/converse-disco.js"), __webpack_require__(/*! backbone.overview */ "./node_modules/backbone.overview/backbone.overview.js"), __webpack_require__(/*! backbone.orderedlistview */ "./node_modules/backbone.overview/backbone.orderedlistview.js"), __webpack_require__(/*! backbone.vdomview */ "./node_modules/backbone.vdomview/backbone.vdomview.js"), __webpack_require__(/*! utils/muc */ "./src/utils/muc.js"), __webpack_require__(/*! utils/emoji */ "./src/utils/emoji.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! ./utils/form */ "./src/headless/utils/form.js"), __webpack_require__(/*! ./converse-core */ "./src/headless/converse-core.js"), __webpack_require__(/*! ./converse-disco */ "./src/headless/converse-disco.js"), __webpack_require__(/*! backbone.overview/backbone.overview */ "./node_modules/backbone.overview/backbone.overview.js"), __webpack_require__(/*! backbone.overview/backbone.orderedlistview */ "./node_modules/backbone.overview/backbone.orderedlistview.js"), __webpack_require__(/*! backbone.vdomview */ "./node_modules/backbone.vdomview/backbone.vdomview.js"), __webpack_require__(/*! ./utils/muc */ "./src/headless/utils/muc.js"), __webpack_require__(/*! ./utils/emoji */ "./src/headless/utils/emoji.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); @@ -72120,1496 +77294,10 @@ function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } /***/ }), -/***/ "./src/converse-notification.js": -/*!**************************************!*\ - !*** ./src/converse-notification.js ***! - \**************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;// Converse.js (A browser based XMPP chat client) -// http://conversejs.org -// -// Copyright (c) 2013-2018, JC Brand -// Licensed under the Mozilla Public License (MPLv2) -// - -/*global define */ -(function (root, factory) { - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! converse-core */ "./src/converse-core.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), - __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? - (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), - __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); -})(this, function (converse) { - "use strict"; - - const _converse$env = converse.env, - Strophe = _converse$env.Strophe, - _ = _converse$env._, - sizzle = _converse$env.sizzle, - u = converse.env.utils; - converse.plugins.add('converse-notification', { - dependencies: ["converse-chatboxes"], - - initialize() { - /* The initialize function gets called as soon as the plugin is - * loaded by converse.js's plugin machinery. - */ - const _converse = this._converse; - const __ = _converse.__; - _converse.supports_html5_notification = "Notification" in window; - - _converse.api.settings.update({ - notify_all_room_messages: false, - show_desktop_notifications: true, - show_chatstate_notifications: false, - chatstate_notification_blacklist: [], - // ^ a list of JIDs to ignore concerning chat state notifications - play_sounds: true, - sounds_path: '/sounds/', - notification_icon: '/logo/conversejs-filled.svg' - }); - - _converse.isOnlyChatStateNotification = msg => // See XEP-0085 Chat State Notification - _.isNull(msg.querySelector('body')) && (_.isNull(msg.querySelector(_converse.ACTIVE)) || _.isNull(msg.querySelector(_converse.COMPOSING)) || _.isNull(msg.querySelector(_converse.INACTIVE)) || _.isNull(msg.querySelector(_converse.PAUSED)) || _.isNull(msg.querySelector(_converse.GONE))); - - _converse.shouldNotifyOfGroupMessage = function (message) { - /* Is this a group message worthy of notification? - */ - let notify_all = _converse.notify_all_room_messages; - const jid = message.getAttribute('from'), - resource = Strophe.getResourceFromJid(jid), - room_jid = Strophe.getBareJidFromJid(jid), - sender = resource && Strophe.unescapeNode(resource) || ''; - - if (sender === '' || message.querySelectorAll('delay').length > 0) { - return false; - } - - const room = _converse.chatboxes.get(room_jid); - - const body = message.querySelector('body'); - - if (_.isNull(body)) { - return false; - } - - const mentioned = new RegExp(`\\b${room.get('nick')}\\b`).test(body.textContent); - notify_all = notify_all === true || _.isArray(notify_all) && _.includes(notify_all, room_jid); - - if (sender === room.get('nick') || !notify_all && !mentioned) { - return false; - } - - return true; - }; - - _converse.isMessageToHiddenChat = function (message) { - if (_.includes(['mobile', 'fullscreen', 'embedded'], _converse.view_mode)) { - const jid = Strophe.getBareJidFromJid(message.getAttribute('from')), - view = _converse.chatboxviews.get(jid); - - if (!_.isNil(view)) { - return view.model.get('hidden') || _converse.windowState === 'hidden' || !u.isVisible(view.el); - } - - return true; - } - - return _converse.windowState === 'hidden'; - }; - - _converse.shouldNotifyOfMessage = function (message) { - const forwarded = message.querySelector('forwarded'); - - if (!_.isNull(forwarded)) { - return false; - } else if (message.getAttribute('type') === 'groupchat') { - return _converse.shouldNotifyOfGroupMessage(message); - } else if (u.isHeadlineMessage(_converse, message)) { - // We want to show notifications for headline messages. - return _converse.isMessageToHiddenChat(message); - } - - const is_me = Strophe.getBareJidFromJid(message.getAttribute('from')) === _converse.bare_jid; - - return !_converse.isOnlyChatStateNotification(message) && !is_me && _converse.isMessageToHiddenChat(message); - }; - - _converse.playSoundNotification = function () { - /* Plays a sound to notify that a new message was recieved. - */ - // XXX Eventually this can be refactored to use Notification's sound - // feature, but no browser currently supports it. - // https://developer.mozilla.org/en-US/docs/Web/API/notification/sound - let audio; - - if (_converse.play_sounds && !_.isUndefined(window.Audio)) { - audio = new Audio(_converse.sounds_path + "msg_received.ogg"); - - if (audio.canPlayType('audio/ogg')) { - audio.play(); - } else { - audio = new Audio(_converse.sounds_path + "msg_received.mp3"); - - if (audio.canPlayType('audio/mp3')) { - audio.play(); - } - } - } - }; - - _converse.areDesktopNotificationsEnabled = function () { - return _converse.supports_html5_notification && _converse.show_desktop_notifications && Notification.permission === "granted"; - }; - - _converse.showMessageNotification = function (message) { - /* Shows an HTML5 Notification to indicate that a new chat - * message was received. - */ - let title, roster_item; - const full_from_jid = message.getAttribute('from'), - from_jid = Strophe.getBareJidFromJid(full_from_jid); - - if (message.getAttribute('type') === 'headline') { - if (!_.includes(from_jid, '@') || _converse.allow_non_roster_messaging) { - title = __("Notification from %1$s", from_jid); - } else { - return; - } - } else if (!_.includes(from_jid, '@')) { - // workaround for Prosody which doesn't give type "headline" - title = __("Notification from %1$s", from_jid); - } else if (message.getAttribute('type') === 'groupchat') { - title = __("%1$s says", Strophe.getResourceFromJid(full_from_jid)); - } else { - if (_.isUndefined(_converse.roster)) { - _converse.log("Could not send notification, because roster is undefined", Strophe.LogLevel.ERROR); - - return; - } - - roster_item = _converse.roster.get(from_jid); - - if (!_.isUndefined(roster_item)) { - title = __("%1$s says", roster_item.getDisplayName()); - } else { - if (_converse.allow_non_roster_messaging) { - title = __("%1$s says", from_jid); - } else { - return; - } - } - } // TODO: we should suppress notifications if we cannot decrypt - // the message... - - - const body = sizzle(`encrypted[xmlns="${Strophe.NS.OMEMO}"]`, message).length ? __('OMEMO Message received') : _.get(message.querySelector('body'), 'textContent'); - - if (!body) { - return; - } - - const n = new Notification(title, { - 'body': body, - 'lang': _converse.locale, - 'icon': _converse.notification_icon - }); - setTimeout(n.close.bind(n), 5000); - }; - - _converse.showChatStateNotification = function (contact) { - /* Creates an HTML5 Notification to inform of a change in a - * contact's chat state. - */ - if (_.includes(_converse.chatstate_notification_blacklist, contact.jid)) { - // Don't notify if the user is being ignored. - return; - } - - const chat_state = contact.chat_status; - let message = null; - - if (chat_state === 'offline') { - message = __('has gone offline'); - } else if (chat_state === 'away') { - message = __('has gone away'); - } else if (chat_state === 'dnd') { - message = __('is busy'); - } else if (chat_state === 'online') { - message = __('has come online'); - } - - if (message === null) { - return; - } - - const n = new Notification(contact.getDisplayName(), { - body: message, - lang: _converse.locale, - icon: _converse.notification_icon - }); - setTimeout(n.close.bind(n), 5000); - }; - - _converse.showContactRequestNotification = function (contact) { - const n = new Notification(contact.getDisplayName(), { - body: __('wants to be your contact'), - lang: _converse.locale, - icon: _converse.notification_icon - }); - setTimeout(n.close.bind(n), 5000); - }; - - _converse.showFeedbackNotification = function (data) { - if (data.klass === 'error' || data.klass === 'warn') { - const n = new Notification(data.subject, { - body: data.message, - lang: _converse.locale, - icon: _converse.notification_icon - }); - setTimeout(n.close.bind(n), 5000); - } - }; - - _converse.handleChatStateNotification = function (contact) { - /* Event handler for on('contactPresenceChanged'). - * Will show an HTML5 notification to indicate that the chat - * status has changed. - */ - if (_converse.areDesktopNotificationsEnabled() && _converse.show_chatstate_notifications) { - _converse.showChatStateNotification(contact); - } - }; - - _converse.handleMessageNotification = function (data) { - /* Event handler for the on('message') event. Will call methods - * to play sounds and show HTML5 notifications. - */ - const message = data.stanza; - - if (!_converse.shouldNotifyOfMessage(message)) { - return false; - } - - _converse.playSoundNotification(); - - if (_converse.areDesktopNotificationsEnabled()) { - _converse.showMessageNotification(message); - } - }; - - _converse.handleContactRequestNotification = function (contact) { - if (_converse.areDesktopNotificationsEnabled(true)) { - _converse.showContactRequestNotification(contact); - } - }; - - _converse.handleFeedback = function (data) { - if (_converse.areDesktopNotificationsEnabled(true)) { - _converse.showFeedbackNotification(data); - } - }; - - _converse.requestPermission = function () { - if (_converse.supports_html5_notification && !_.includes(['denied', 'granted'], Notification.permission)) { - // Ask user to enable HTML5 notifications - Notification.requestPermission(); - } - }; - - _converse.on('pluginsInitialized', function () { - // We only register event handlers after all plugins are - // registered, because other plugins might override some of our - // handlers. - _converse.on('contactRequest', _converse.handleContactRequestNotification); - - _converse.on('contactPresenceChanged', _converse.handleChatStateNotification); - - _converse.on('message', _converse.handleMessageNotification); - - _converse.on('feedback', _converse.handleFeedback); - - _converse.on('connected', _converse.requestPermission); - }); - } - - }); -}); - -/***/ }), - -/***/ "./src/converse-omemo.js": -/*!*******************************!*\ - !*** ./src/converse-omemo.js ***! - \*******************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;// Converse.js -// http://conversejs.org -// -// Copyright (c) 2013-2018, the Converse.js developers -// Licensed under the Mozilla Public License (MPLv2) - -/* global libsignal, ArrayBuffer, parseInt, crypto */ -(function (root, factory) { - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! converse-core */ "./src/converse-core.js"), __webpack_require__(/*! templates/toolbar_omemo.html */ "./src/templates/toolbar_omemo.html")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), - __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? - (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), - __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); -})(this, function (converse, tpl_toolbar_omemo) { - const _converse$env = converse.env, - Backbone = _converse$env.Backbone, - Promise = _converse$env.Promise, - Strophe = _converse$env.Strophe, - moment = _converse$env.moment, - sizzle = _converse$env.sizzle, - $iq = _converse$env.$iq, - $msg = _converse$env.$msg, - _ = _converse$env._, - f = _converse$env.f, - b64_sha1 = _converse$env.b64_sha1; - const u = converse.env.utils; - Strophe.addNamespace('OMEMO_DEVICELIST', Strophe.NS.OMEMO + ".devicelist"); - Strophe.addNamespace('OMEMO_VERIFICATION', Strophe.NS.OMEMO + ".verification"); - Strophe.addNamespace('OMEMO_WHITELISTED', Strophe.NS.OMEMO + ".whitelisted"); - Strophe.addNamespace('OMEMO_BUNDLES', Strophe.NS.OMEMO + ".bundles"); - const UNDECIDED = 0; - const TRUSTED = 1; - const UNTRUSTED = -1; - const TAG_LENGTH = 128; - const KEY_ALGO = { - 'name': "AES-GCM", - 'length': 128 - }; - - function parseBundle(bundle_el) { - /* Given an XML element representing a user's OMEMO bundle, parse it - * and return a map. - */ - const signed_prekey_public_el = bundle_el.querySelector('signedPreKeyPublic'), - signed_prekey_signature_el = bundle_el.querySelector('signedPreKeySignature'), - identity_key_el = bundle_el.querySelector('identityKey'); - - const prekeys = _.map(sizzle(`prekeys > preKeyPublic`, bundle_el), el => { - return { - 'id': parseInt(el.getAttribute('preKeyId'), 10), - 'key': el.textContent - }; - }); - - return { - 'identity_key': bundle_el.querySelector('identityKey').textContent.trim(), - 'signed_prekey': { - 'id': parseInt(signed_prekey_public_el.getAttribute('signedPreKeyId'), 10), - 'public_key': signed_prekey_public_el.textContent, - 'signature': signed_prekey_signature_el.textContent - }, - 'prekeys': prekeys - }; - } - - converse.plugins.add('converse-omemo', { - enabled(_converse) { - return !_.isNil(window.libsignal) && !f.includes('converse-omemo', _converse.blacklisted_plugins); - }, - - dependencies: ["converse-chatview"], - overrides: { - ProfileModal: { - events: { - 'change input.select-all': 'selectAll', - 'submit .fingerprint-removal': 'removeSelectedFingerprints' - }, - - initialize() { - const _converse = this.__super__._converse; - this.debouncedRender = _.debounce(this.render, 50); - this.devicelist = _converse.devicelists.get(_converse.bare_jid); - this.devicelist.devices.on('change:bundle', this.debouncedRender, this); - this.devicelist.devices.on('reset', this.debouncedRender, this); - this.devicelist.devices.on('remove', this.debouncedRender, this); - this.devicelist.devices.on('add', this.debouncedRender, this); - return this.__super__.initialize.apply(this, arguments); - }, - - beforeRender() { - const _converse = this.__super__._converse, - device_id = _converse.omemo_store.get('device_id'); - - this.current_device = this.devicelist.devices.get(device_id); - this.other_devices = this.devicelist.devices.filter(d => d.get('id') !== device_id); - - if (this.__super__.beforeRender) { - return this.__super__.beforeRender.apply(this, arguments); - } - }, - - selectAll(ev) { - let sibling = u.ancestor(ev.target, 'li'); - - while (sibling) { - sibling.querySelector('input[type="checkbox"]').checked = ev.target.checked; - sibling = sibling.nextElementSibling; - } - }, - - removeSelectedFingerprints(ev) { - ev.preventDefault(); - ev.stopPropagation(); - ev.target.querySelector('.select-all').checked = false; - - const checkboxes = ev.target.querySelectorAll('.fingerprint-removal-item input[type="checkbox"]:checked'), - device_ids = _.map(checkboxes, 'value'); - - this.devicelist.removeOwnDevices(device_ids).then(this.modal.hide).catch(err => { - const _converse = this.__super__._converse, - __ = _converse.__; - - _converse.log(err, Strophe.LogLevel.ERROR); - - _converse.api.alert.show(Strophe.LogLevel.ERROR, __('Error'), [__('Sorry, an error occurred while trying to remove the devices.')]); - }); - } - - }, - UserDetailsModal: { - events: { - 'click .fingerprint-trust .btn input': 'toggleDeviceTrust' - }, - - initialize() { - const _converse = this.__super__._converse; - const jid = this.model.get('jid'); - this.devicelist = _converse.devicelists.get(jid) || _converse.devicelists.create({ - 'jid': jid - }); - this.devicelist.devices.on('change:bundle', this.render, this); - this.devicelist.devices.on('change:trusted', this.render, this); - this.devicelist.devices.on('remove', this.render, this); - this.devicelist.devices.on('add', this.render, this); - this.devicelist.devices.on('reset', this.render, this); - return this.__super__.initialize.apply(this, arguments); - }, - - toggleDeviceTrust(ev) { - const radio = ev.target; - const device = this.devicelist.devices.get(radio.getAttribute('name')); - device.save('trusted', parseInt(radio.value, 10)); - } - - }, - ChatBox: { - getBundlesAndBuildSessions() { - const _converse = this.__super__._converse; - let devices; - return _converse.getDevicesForContact(this.get('jid')).then(their_devices => { - const device_id = _converse.omemo_store.get('device_id'), - devicelist = _converse.devicelists.get(_converse.bare_jid), - own_devices = devicelist.devices.filter(device => device.get('id') !== device_id); - - devices = _.concat(own_devices, their_devices.models); - return Promise.all(devices.map(device => device.getBundle())); - }).then(() => this.buildSessions(devices)); - }, - - buildSession(device) { - const _converse = this.__super__._converse, - address = new libsignal.SignalProtocolAddress(device.get('jid'), device.get('id')), - sessionBuilder = new libsignal.SessionBuilder(_converse.omemo_store, address), - prekey = device.getRandomPreKey(); - return device.getBundle().then(bundle => { - return sessionBuilder.processPreKey({ - 'registrationId': parseInt(device.get('id'), 10), - 'identityKey': u.base64ToArrayBuffer(bundle.identity_key), - 'signedPreKey': { - 'keyId': bundle.signed_prekey.id, - // - 'publicKey': u.base64ToArrayBuffer(bundle.signed_prekey.public_key), - 'signature': u.base64ToArrayBuffer(bundle.signed_prekey.signature) - }, - 'preKey': { - 'keyId': prekey.id, - // - 'publicKey': u.base64ToArrayBuffer(prekey.key) - } - }); - }); - }, - - getSession(device) { - const _converse = this.__super__._converse, - address = new libsignal.SignalProtocolAddress(device.get('jid'), device.get('id')); - return _converse.omemo_store.loadSession(address.toString()).then(session => { - if (session) { - return Promise.resolve(); - } else { - return this.buildSession(device); - } - }); - }, - - async encryptMessage(plaintext) { - // The client MUST use fresh, randomly generated key/IV pairs - // with AES-128 in Galois/Counter Mode (GCM). - // For GCM a 12 byte IV is strongly suggested as other IV lengths - // will require additional calculations. In principle any IV size - // can be used as long as the IV doesn't ever repeat. NIST however - // suggests that only an IV size of 12 bytes needs to be supported - // by implementations. - // - // https://crypto.stackexchange.com/questions/26783/ciphertext-and-tag-size-and-iv-transmission-with-aes-in-gcm-mode - const iv = crypto.getRandomValues(new window.Uint8Array(12)), - key = await crypto.subtle.generateKey(KEY_ALGO, true, ["encrypt", "decrypt"]), - algo = { - 'name': 'AES-GCM', - 'iv': iv, - 'tagLength': TAG_LENGTH - }, - encrypted = await crypto.subtle.encrypt(algo, key, u.stringToArrayBuffer(plaintext)), - length = encrypted.byteLength - (128 + 7 >> 3), - ciphertext = encrypted.slice(0, length), - tag = encrypted.slice(length), - exported_key = await crypto.subtle.exportKey("raw", key); - return Promise.resolve({ - 'key': exported_key, - 'tag': tag, - 'key_and_tag': u.appendArrayBuffer(exported_key, tag), - 'payload': u.arrayBufferToBase64(ciphertext), - 'iv': u.arrayBufferToBase64(iv) - }); - }, - - async decryptMessage(obj) { - const key_obj = await crypto.subtle.importKey('raw', obj.key, KEY_ALGO, true, ['encrypt', 'decrypt']), - cipher = u.appendArrayBuffer(u.base64ToArrayBuffer(obj.payload), obj.tag), - algo = { - 'name': "AES-GCM", - 'iv': u.base64ToArrayBuffer(obj.iv), - 'tagLength': TAG_LENGTH - }; - return u.arrayBufferToString((await crypto.subtle.decrypt(algo, key_obj, cipher))); - }, - - reportDecryptionError(e) { - const _converse = this.__super__._converse; - - if (_converse.debug) { - const __ = _converse.__; - this.messages.create({ - 'message': __("Sorry, could not decrypt a received OMEMO message due to an error.") + ` ${e.name} ${e.message}`, - 'type': 'error' - }); - } - - _converse.log(`${e.name} ${e.message}`, Strophe.LogLevel.ERROR); - }, - - decrypt(attrs) { - const _converse = this.__super__._converse, - session_cipher = this.getSessionCipher(attrs.from, parseInt(attrs.encrypted.device_id, 10)); // https://xmpp.org/extensions/xep-0384.html#usecases-receiving - - if (attrs.encrypted.prekey === 'true') { - let plaintext; - return session_cipher.decryptPreKeyWhisperMessage(u.base64ToArrayBuffer(attrs.encrypted.key), 'binary').then(key_and_tag => { - if (attrs.encrypted.payload) { - const key = key_and_tag.slice(0, 16), - tag = key_and_tag.slice(16); - return this.decryptMessage(_.extend(attrs.encrypted, { - 'key': key, - 'tag': tag - })); - } - - return Promise.resolve(); - }).then(pt => { - plaintext = pt; - return _converse.omemo_store.generateMissingPreKeys(); - }).then(() => _converse.omemo_store.publishBundle()).then(() => { - if (plaintext) { - return _.extend(attrs, { - 'plaintext': plaintext - }); - } else { - return _.extend(attrs, { - 'is_only_key': true - }); - } - }).catch(e => { - this.reportDecryptionError(e); - return attrs; - }); - } else { - return session_cipher.decryptWhisperMessage(u.base64ToArrayBuffer(attrs.encrypted.key), 'binary').then(key_and_tag => { - const key = key_and_tag.slice(0, 16), - tag = key_and_tag.slice(16); - return this.decryptMessage(_.extend(attrs.encrypted, { - 'key': key, - 'tag': tag - })); - }).then(plaintext => _.extend(attrs, { - 'plaintext': plaintext - })).catch(e => { - this.reportDecryptionError(e); - return attrs; - }); - } - }, - - getEncryptionAttributesfromStanza(stanza, original_stanza, attrs) { - const _converse = this.__super__._converse, - encrypted = sizzle(`encrypted[xmlns="${Strophe.NS.OMEMO}"]`, original_stanza).pop(), - header = encrypted.querySelector('header'), - key = sizzle(`key[rid="${_converse.omemo_store.get('device_id')}"]`, encrypted).pop(); - - if (key) { - attrs['is_encrypted'] = true; - attrs['encrypted'] = { - 'device_id': header.getAttribute('sid'), - 'iv': header.querySelector('iv').textContent, - 'key': key.textContent, - 'payload': _.get(encrypted.querySelector('payload'), 'textContent', null), - 'prekey': key.getAttribute('prekey') - }; - return this.decrypt(attrs); - } else { - return Promise.resolve(attrs); - } - }, - - getMessageAttributesFromStanza(stanza, original_stanza) { - const _converse = this.__super__._converse, - encrypted = sizzle(`encrypted[xmlns="${Strophe.NS.OMEMO}"]`, original_stanza).pop(), - attrs = this.__super__.getMessageAttributesFromStanza.apply(this, arguments); - - if (!encrypted || !_converse.config.get('trusted')) { - return attrs; - } else { - return this.getEncryptionAttributesfromStanza(stanza, original_stanza, attrs); - } - }, - - buildSessions(devices) { - return Promise.all(devices.map(device => this.getSession(device))).then(() => devices); - }, - - getSessionCipher(jid, id) { - const _converse = this.__super__._converse, - address = new libsignal.SignalProtocolAddress(jid, id); - this.session_cipher = new window.libsignal.SessionCipher(_converse.omemo_store, address); - return this.session_cipher; - }, - - encryptKey(plaintext, device) { - return this.getSessionCipher(device.get('jid'), device.get('id')).encrypt(plaintext).then(payload => ({ - 'payload': payload, - 'device': device - })); - }, - - addKeysToMessageStanza(stanza, dicts, iv) { - for (var i in dicts) { - if (Object.prototype.hasOwnProperty.call(dicts, i)) { - const payload = dicts[i].payload, - device = dicts[i].device, - prekey = 3 == parseInt(payload.type, 10); - stanza.c('key', { - 'rid': device.get('id') - }).t(btoa(payload.body)); - - if (prekey) { - stanza.attrs({ - 'prekey': prekey - }); - } - - stanza.up(); - - if (i == dicts.length - 1) { - stanza.c('iv').t(iv).up().up(); - } - } - } - - return Promise.resolve(stanza); - }, - - createOMEMOMessageStanza(message, devices) { - const _converse = this.__super__._converse, - __ = _converse.__; - - const body = __("This is an OMEMO encrypted message which your client doesn’t seem to support. " + "Find more information on https://conversations.im/omemo"); - - if (!message.get('message')) { - throw new Error("No message body to encrypt!"); - } - - const stanza = $msg({ - 'from': _converse.connection.jid, - 'to': this.get('jid'), - 'type': this.get('message_type'), - 'id': message.get('msgid') - }).c('body').t(body).up() // An encrypted header is added to the message for - // each device that is supposed to receive it. - // These headers simply contain the key that the - // payload message is encrypted with, - // and they are separately encrypted using the - // session corresponding to the counterpart device. - .c('encrypted', { - 'xmlns': Strophe.NS.OMEMO - }).c('header', { - 'sid': _converse.omemo_store.get('device_id') - }); - return this.encryptMessage(message.get('message')).then(obj => { - // The 16 bytes key and the GCM authentication tag (The tag - // SHOULD have at least 128 bit) are concatenated and for each - // intended recipient device, i.e. both own devices as well as - // devices associated with the contact, the result of this - // concatenation is encrypted using the corresponding - // long-standing SignalProtocol session. - const promises = devices.filter(device => device.get('trusted') != UNTRUSTED).map(device => this.encryptKey(obj.key_and_tag, device)); - return Promise.all(promises).then(dicts => this.addKeysToMessageStanza(stanza, dicts, obj.iv)).then(stanza => { - stanza.c('payload').t(obj.payload).up().up(); - stanza.c('store', { - 'xmlns': Strophe.NS.HINTS - }); - return stanza; - }); - }); - }, - - sendMessage(attrs) { - const _converse = this.__super__._converse, - __ = _converse.__; - - if (this.get('omemo_active') && attrs.message) { - attrs['is_encrypted'] = true; - attrs['plaintext'] = attrs.message; - const message = this.messages.create(attrs); - this.getBundlesAndBuildSessions().then(devices => this.createOMEMOMessageStanza(message, devices)).then(stanza => this.sendMessageStanza(stanza)).catch(e => { - this.messages.create({ - 'message': __("Sorry, could not send the message due to an error.") + ` ${e.message}`, - 'type': 'error' - }); - - _converse.log(e, Strophe.LogLevel.ERROR); - }); - } else { - return this.__super__.sendMessage.apply(this, arguments); - } - } - - }, - ChatBoxView: { - events: { - 'click .toggle-omemo': 'toggleOMEMO' - }, - - showMessage(message) { - // We don't show a message if it's only keying material - if (!message.get('is_only_key')) { - return this.__super__.showMessage.apply(this, arguments); - } - }, - - async renderOMEMOToolbarButton() { - const _converse = this.__super__._converse, - __ = _converse.__; - const support = await _converse.contactHasOMEMOSupport(this.model.get('jid')); - - if (support) { - const icon = this.el.querySelector('.toggle-omemo'), - html = tpl_toolbar_omemo(_.extend(this.model.toJSON(), { - '__': __ - })); - - if (icon) { - icon.outerHTML = html; - } else { - this.el.querySelector('.chat-toolbar').insertAdjacentHTML('beforeend', html); - } - } - }, - - toggleOMEMO(ev) { - ev.preventDefault(); - this.model.save({ - 'omemo_active': !this.model.get('omemo_active') - }); - this.renderOMEMOToolbarButton(); - } - - } - }, - - initialize() { - /* The initialize function gets called as soon as the plugin is - * loaded by Converse.js's plugin machinery. - */ - const _converse = this._converse; - - _converse.api.promises.add(['OMEMOInitialized']); - - _converse.NUM_PREKEYS = 100; // Set here so that tests can override - - function generateFingerprint(device) { - if (_.get(device.get('bundle'), 'fingerprint')) { - return; - } - - return device.getBundle().then(bundle => { - bundle['fingerprint'] = u.arrayBufferToHex(u.base64ToArrayBuffer(bundle['identity_key'])); - device.save('bundle', bundle); - device.trigger('change:bundle'); // Doesn't get triggered automatically due to pass-by-reference - }); - } - - _converse.generateFingerprints = function (jid) { - return _converse.getDevicesForContact(jid).then(devices => Promise.all(devices.map(d => generateFingerprint(d)))); - }; - - _converse.getDeviceForContact = function (jid, device_id) { - return _converse.getDevicesForContact(jid).then(devices => devices.get(device_id)); - }; - - _converse.getDevicesForContact = function (jid) { - let devicelist; - return _converse.api.waitUntil('OMEMOInitialized').then(() => { - devicelist = _converse.devicelists.get(jid) || _converse.devicelists.create({ - 'jid': jid - }); - return devicelist.fetchDevices(); - }).then(() => devicelist.devices); - }; - - _converse.contactHasOMEMOSupport = function (jid) { - /* Checks whether the contact advertises any OMEMO-compatible devices. */ - return new Promise((resolve, reject) => { - _converse.getDevicesForContact(jid).then(devices => resolve(devices.length > 0)).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); - }); - }; - - function generateDeviceID() { - /* Generates a device ID, making sure that it's unique */ - const existing_ids = _converse.devicelists.get(_converse.bare_jid).devices.pluck('id'); - - let device_id = libsignal.KeyHelper.generateRegistrationId(); - let i = 0; - - while (_.includes(existing_ids, device_id)) { - device_id = libsignal.KeyHelper.generateRegistrationId(); - i++; - - if (i == 10) { - throw new Error("Unable to generate a unique device ID"); - } - } - - return device_id.toString(); - } - - _converse.OMEMOStore = Backbone.Model.extend({ - Direction: { - SENDING: 1, - RECEIVING: 2 - }, - - getIdentityKeyPair() { - const keypair = this.get('identity_keypair'); - return Promise.resolve({ - 'privKey': u.base64ToArrayBuffer(keypair.privKey), - 'pubKey': u.base64ToArrayBuffer(keypair.pubKey) - }); - }, - - getLocalRegistrationId() { - return Promise.resolve(parseInt(this.get('device_id'), 10)); - }, - - isTrustedIdentity(identifier, identity_key, direction) { - if (_.isNil(identifier)) { - throw new Error("Can't check identity key for invalid key"); - } - - if (!(identity_key instanceof ArrayBuffer)) { - throw new Error("Expected identity_key to be an ArrayBuffer"); - } - - const trusted = this.get('identity_key' + identifier); - - if (trusted === undefined) { - return Promise.resolve(true); - } - - return Promise.resolve(u.arrayBufferToBase64(identity_key) === trusted); - }, - - loadIdentityKey(identifier) { - if (_.isNil(identifier)) { - throw new Error("Can't load identity_key for invalid identifier"); - } - - return Promise.resolve(u.base64ToArrayBuffer(this.get('identity_key' + identifier))); - }, - - saveIdentity(identifier, identity_key) { - if (_.isNil(identifier)) { - throw new Error("Can't save identity_key for invalid identifier"); - } - - const address = new libsignal.SignalProtocolAddress.fromString(identifier), - existing = this.get('identity_key' + address.getName()); - const b64_idkey = u.arrayBufferToBase64(identity_key); - this.save('identity_key' + address.getName(), b64_idkey); - - if (existing && b64_idkey !== existing) { - return Promise.resolve(true); - } else { - return Promise.resolve(false); - } - }, - - getPreKeys() { - return this.get('prekeys') || {}; - }, - - loadPreKey(key_id) { - const res = this.getPreKeys()[key_id]; - - if (res) { - return Promise.resolve({ - 'privKey': u.base64ToArrayBuffer(res.privKey), - 'pubKey': u.base64ToArrayBuffer(res.pubKey) - }); - } - - return Promise.resolve(); - }, - - storePreKey(key_id, key_pair) { - const prekey = {}; - prekey[key_id] = { - 'pubKey': u.arrayBufferToBase64(key_pair.pubKey), - 'privKey': u.arrayBufferToBase64(key_pair.privKey) - }; - this.save('prekeys', _.extend(this.getPreKeys(), prekey)); - return Promise.resolve(); - }, - - removePreKey(key_id) { - this.save('prekeys', _.omit(this.getPreKeys(), key_id)); - return Promise.resolve(); - }, - - loadSignedPreKey(keyId) { - const res = this.get('signed_prekey'); - - if (res) { - return Promise.resolve({ - 'privKey': u.base64ToArrayBuffer(res.privKey), - 'pubKey': u.base64ToArrayBuffer(res.pubKey) - }); - } - - return Promise.resolve(); - }, - - storeSignedPreKey(spk) { - if (typeof spk !== "object") { - // XXX: We've changed the signature of this method from the - // example given in InMemorySignalProtocolStore. - // Should be fine because the libsignal code doesn't - // actually call this method. - throw new Error("storeSignedPreKey: expected an object"); - } - - this.save('signed_prekey', { - 'id': spk.keyId, - 'privKey': u.arrayBufferToBase64(spk.keyPair.privKey), - 'pubKey': u.arrayBufferToBase64(spk.keyPair.pubKey), - // XXX: The InMemorySignalProtocolStore does not pass - // in or store the signature, but we need it when we - // publish out bundle and this method isn't called from - // within libsignal code, so we modify it to also store - // the signature. - 'signature': u.arrayBufferToBase64(spk.signature) - }); - return Promise.resolve(); - }, - - removeSignedPreKey(key_id) { - if (this.get('signed_prekey')['id'] === key_id) { - this.unset('signed_prekey'); - this.save(); - } - - return Promise.resolve(); - }, - - loadSession(identifier) { - return Promise.resolve(this.get('session' + identifier)); - }, - - storeSession(identifier, record) { - return Promise.resolve(this.save('session' + identifier, record)); - }, - - removeSession(identifier) { - return Promise.resolve(this.unset('session' + identifier)); - }, - - removeAllSessions(identifier) { - const keys = _.filter(_.keys(this.attributes), key => { - if (key.startsWith('session' + identifier)) { - return key; - } - }); - - const attrs = {}; - - _.forEach(keys, key => { - attrs[key] = undefined; - }); - - this.save(attrs); - return Promise.resolve(); - }, - - publishBundle() { - const signed_prekey = this.get('signed_prekey'); - const stanza = $iq({ - 'from': _converse.bare_jid, - 'type': 'set' - }).c('pubsub', { - 'xmlns': Strophe.NS.PUBSUB - }).c('publish', { - 'node': `${Strophe.NS.OMEMO_BUNDLES}:${this.get('device_id')}` - }).c('item').c('bundle', { - 'xmlns': Strophe.NS.OMEMO - }).c('signedPreKeyPublic', { - 'signedPreKeyId': signed_prekey.id - }).t(signed_prekey.pubKey).up().c('signedPreKeySignature').t(signed_prekey.signature).up().c('identityKey').t(this.get('identity_keypair').pubKey).up().c('prekeys'); - - _.forEach(this.get('prekeys'), (prekey, id) => stanza.c('preKeyPublic', { - 'preKeyId': id - }).t(prekey.pubKey).up()); - - return _converse.api.sendIQ(stanza); - }, - - generateMissingPreKeys() { - const current_keys = this.getPreKeys(), - missing_keys = _.difference(_.invokeMap(_.range(0, _converse.NUM_PREKEYS), Number.prototype.toString), _.keys(current_keys)); - - if (missing_keys.length < 1) { - _converse.log("No missing prekeys to generate for our own device", Strophe.LogLevel.WARN); - - return Promise.resolve(); - } - - return Promise.all(_.map(missing_keys, id => libsignal.KeyHelper.generatePreKey(parseInt(id, 10)))).then(keys => { - _.forEach(keys, k => this.storePreKey(k.keyId, k.keyPair)); - - const marshalled_keys = _.map(this.getPreKeys(), k => ({ - 'id': k.keyId, - 'key': u.arrayBufferToBase64(k.pubKey) - })), - devicelist = _converse.devicelists.get(_converse.bare_jid), - device = devicelist.devices.get(this.get('device_id')); - - return device.getBundle().then(bundle => device.save('bundle', _.extend(bundle, { - 'prekeys': marshalled_keys - }))); - }); - }, - - async generateBundle() { - /* The first thing that needs to happen if a client wants to - * start using OMEMO is they need to generate an IdentityKey - * and a Device ID. The IdentityKey is a Curve25519 [6] - * public/private Key pair. The Device ID is a randomly - * generated integer between 1 and 2^31 - 1. - */ - const identity_keypair = await libsignal.KeyHelper.generateIdentityKeyPair(); - const bundle = {}, - identity_key = u.arrayBufferToBase64(identity_keypair.pubKey), - device_id = generateDeviceID(); - bundle['identity_key'] = identity_key; - bundle['device_id'] = device_id; - this.save({ - 'device_id': device_id, - 'identity_keypair': { - 'privKey': u.arrayBufferToBase64(identity_keypair.privKey), - 'pubKey': identity_key - }, - 'identity_key': identity_key - }); - const signed_prekey = await libsignal.KeyHelper.generateSignedPreKey(identity_keypair, 0); - - _converse.omemo_store.storeSignedPreKey(signed_prekey); - - bundle['signed_prekey'] = { - 'id': signed_prekey.keyId, - 'public_key': u.arrayBufferToBase64(signed_prekey.keyPair.privKey), - 'signature': u.arrayBufferToBase64(signed_prekey.signature) - }; - const keys = await Promise.all(_.map(_.range(0, _converse.NUM_PREKEYS), id => libsignal.KeyHelper.generatePreKey(id))); - - _.forEach(keys, k => _converse.omemo_store.storePreKey(k.keyId, k.keyPair)); - - const devicelist = _converse.devicelists.get(_converse.bare_jid), - device = devicelist.devices.create({ - 'id': bundle.device_id, - 'jid': _converse.bare_jid - }), - marshalled_keys = _.map(keys, k => ({ - 'id': k.keyId, - 'key': u.arrayBufferToBase64(k.keyPair.pubKey) - })); - - bundle['prekeys'] = marshalled_keys; - device.save('bundle', bundle); - }, - - fetchSession() { - if (_.isUndefined(this._setup_promise)) { - this._setup_promise = new Promise((resolve, reject) => { - this.fetch({ - 'success': () => { - if (!_converse.omemo_store.get('device_id')) { - this.generateBundle().then(resolve).catch(resolve); - } else { - resolve(); - } - }, - 'error': () => { - this.generateBundle().then(resolve).catch(resolve); - } - }); - }); - } - - return this._setup_promise; - } - - }); - _converse.Device = Backbone.Model.extend({ - defaults: { - 'trusted': UNDECIDED - }, - - getRandomPreKey() { - // XXX: assumes that the bundle has already been fetched - const bundle = this.get('bundle'); - return bundle.prekeys[u.getRandomInt(bundle.prekeys.length)]; - }, - - fetchBundleFromServer() { - const stanza = $iq({ - 'type': 'get', - 'from': _converse.bare_jid, - 'to': this.get('jid') - }).c('pubsub', { - 'xmlns': Strophe.NS.PUBSUB - }).c('items', { - 'node': `${Strophe.NS.OMEMO_BUNDLES}:${this.get('id')}` - }); - return _converse.api.sendIQ(stanza).then(iq => { - const publish_el = sizzle(`items[node="${Strophe.NS.OMEMO_BUNDLES}:${this.get('id')}"]`, iq).pop(), - bundle_el = sizzle(`bundle[xmlns="${Strophe.NS.OMEMO}"]`, publish_el).pop(), - bundle = parseBundle(bundle_el); - this.save('bundle', bundle); - return bundle; - }).catch(iq => { - _converse.log(iq.outerHTML, Strophe.LogLevel.ERROR); - }); - }, - - getBundle() { - /* Fetch and save the bundle information associated with - * this device, if the information is not at hand already. - */ - if (this.get('bundle')) { - return Promise.resolve(this.get('bundle'), this); - } else { - return this.fetchBundleFromServer(); - } - } - - }); - _converse.Devices = Backbone.Collection.extend({ - model: _converse.Device - }); - _converse.DeviceList = Backbone.Model.extend({ - idAttribute: 'jid', - - initialize() { - this.devices = new _converse.Devices(); - const id = `converse.devicelist-${_converse.bare_jid}-${this.get('jid')}`; - this.devices.browserStorage = new Backbone.BrowserStorage.session(id); - this.fetchDevices(); - }, - - fetchDevices() { - if (_.isUndefined(this._devices_promise)) { - this._devices_promise = new Promise((resolve, reject) => { - this.devices.fetch({ - 'success': collection => { - if (collection.length === 0) { - this.fetchDevicesFromServer().then(ids => this.publishCurrentDevice(ids)).finally(resolve); - } else { - resolve(); - } - } - }); - }); - } - - return this._devices_promise; - }, - - async publishCurrentDevice(device_ids) { - if (this.get('jid') !== _converse.bare_jid) { - // We only publish for ourselves. - return; - } - - await restoreOMEMOSession(); - - let device_id = _converse.omemo_store.get('device_id'); - - if (!this.devices.findWhere({ - 'id': device_id - })) { - // Generate a new bundle if we cannot find our device - await _converse.omemo_store.generateBundle(); - device_id = _converse.omemo_store.get('device_id'); - } - - if (!_.includes(device_ids, device_id)) { - return this.publishDevices(); - } - }, - - fetchDevicesFromServer() { - const stanza = $iq({ - 'type': 'get', - 'from': _converse.bare_jid, - 'to': this.get('jid') - }).c('pubsub', { - 'xmlns': Strophe.NS.PUBSUB - }).c('items', { - 'node': Strophe.NS.OMEMO_DEVICELIST - }); - return _converse.api.sendIQ(stanza).then(iq => { - const device_ids = _.map(sizzle(`list[xmlns="${Strophe.NS.OMEMO}"] device`, iq), dev => dev.getAttribute('id')); - - _.forEach(device_ids, id => this.devices.create({ - 'id': id, - 'jid': this.get('jid') - })); - - return device_ids; - }); - }, - - publishDevices() { - const stanza = $iq({ - 'from': _converse.bare_jid, - 'type': 'set' - }).c('pubsub', { - 'xmlns': Strophe.NS.PUBSUB - }).c('publish', { - 'node': Strophe.NS.OMEMO_DEVICELIST - }).c('item').c('list', { - 'xmlns': Strophe.NS.OMEMO - }); - this.devices.each(device => stanza.c('device', { - 'id': device.get('id') - }).up()); - return _converse.api.sendIQ(stanza); - }, - - removeOwnDevices(device_ids) { - if (this.get('jid') !== _converse.bare_jid) { - throw new Error("Cannot remove devices from someone else's device list"); - } - - _.forEach(device_ids, device_id => this.devices.get(device_id).destroy()); - - return this.publishDevices(); - } - - }); - _converse.DeviceLists = Backbone.Collection.extend({ - model: _converse.DeviceList - }); - - function fetchDeviceLists() { - return new Promise((resolve, reject) => _converse.devicelists.fetch({ - 'success': resolve - })); - } - - function fetchOwnDevices() { - return fetchDeviceLists().then(() => { - let own_devicelist = _converse.devicelists.get(_converse.bare_jid); - - if (_.isNil(own_devicelist)) { - own_devicelist = _converse.devicelists.create({ - 'jid': _converse.bare_jid - }); - } - - return own_devicelist.fetchDevices(); - }); - } - - function updateBundleFromStanza(stanza) { - const items_el = sizzle(`items`, stanza).pop(); - - if (!items_el || !items_el.getAttribute('node').startsWith(Strophe.NS.OMEMO_BUNDLES)) { - return; - } - - const device_id = items_el.getAttribute('node').split(':')[1], - jid = stanza.getAttribute('from'), - bundle_el = sizzle(`item > bundle`, items_el).pop(), - devicelist = _converse.devicelists.get(jid) || _converse.devicelists.create({ - 'jid': jid - }), - device = devicelist.devices.get(device_id) || devicelist.devices.create({ - 'id': device_id, - 'jid': jid - }); - - device.save({ - 'bundle': parseBundle(bundle_el) - }); - } - - function updateDevicesFromStanza(stanza) { - const items_el = sizzle(`items[node="${Strophe.NS.OMEMO_DEVICELIST}"]`, stanza).pop(); - - if (!items_el) { - return; - } - - const device_ids = _.map(sizzle(`item list[xmlns="${Strophe.NS.OMEMO}"] device`, items_el), device => device.getAttribute('id')); - - const jid = stanza.getAttribute('from'), - devicelist = _converse.devicelists.get(jid) || _converse.devicelists.create({ - 'jid': jid - }), - devices = devicelist.devices, - removed_ids = _.difference(devices.pluck('id'), device_ids); - - _.forEach(removed_ids, id => { - if (jid === _converse.bare_jid && id === _converse.omemo_store.get('device_id')) { - // We don't remove the current device - return; - } - - devices.get(id).destroy(); - }); - - _.forEach(device_ids, device_id => { - if (!devices.get(device_id)) { - devices.create({ - 'id': device_id, - 'jid': jid - }); - } - }); - - if (Strophe.getBareJidFromJid(jid) === _converse.bare_jid) { - // Make sure our own device is on the list (i.e. if it was - // removed, add it again. - _converse.devicelists.get(_converse.bare_jid).publishCurrentDevice(device_ids); - } - } - - function registerPEPPushHandler() { - // Add a handler for devices pushed from other connected clients - _converse.connection.addHandler(message => { - try { - if (sizzle(`event[xmlns="${Strophe.NS.PUBSUB}#event"]`, message).length) { - updateDevicesFromStanza(message); - updateBundleFromStanza(message); - } - } catch (e) { - _converse.log(e.message, Strophe.LogLevel.ERROR); - } - - return true; - }, null, 'message', 'headline'); - } - - function restoreOMEMOSession() { - if (_.isUndefined(_converse.omemo_store)) { - const storage = _converse.config.get('storage'), - id = `converse.omemosession-${_converse.bare_jid}`; - - _converse.omemo_store = new _converse.OMEMOStore({ - 'id': id - }); - _converse.omemo_store.browserStorage = new Backbone.BrowserStorage[storage](id); - } - - return _converse.omemo_store.fetchSession(); - } - - function initOMEMO() { - if (!_converse.config.get('trusted')) { - return; - } - - _converse.devicelists = new _converse.DeviceLists(); - - const storage = _converse.config.get('storage'), - id = `converse.devicelists-${_converse.bare_jid}`; - - _converse.devicelists.browserStorage = new Backbone.BrowserStorage[storage](id); - fetchOwnDevices().then(() => restoreOMEMOSession()).then(() => _converse.omemo_store.publishBundle()).then(() => _converse.emit('OMEMOInitialized')).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); - } - - _converse.api.listen.on('afterTearDown', () => { - if (_converse.devicelists) { - _converse.devicelists.reset(); - } - - delete _converse.omemo_store; - }); - - _converse.api.listen.on('connected', registerPEPPushHandler); - - _converse.api.listen.on('renderToolbar', view => view.renderOMEMOToolbarButton()); - - _converse.api.listen.on('statusInitialized', initOMEMO); - - _converse.api.listen.on('addClientFeatures', () => _converse.api.disco.own.features.add(`${Strophe.NS.OMEMO_DEVICELIST}+notify`)); - - _converse.api.listen.on('userDetailsModalInitialized', contact => { - const jid = contact.get('jid'); - - _converse.generateFingerprints(jid).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); - }); - - _converse.api.listen.on('profileModalInitialized', contact => { - _converse.generateFingerprints(_converse.bare_jid).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); - }); - } - - }); -}); - -/***/ }), - -/***/ "./src/converse-ping.js": -/*!******************************!*\ - !*** ./src/converse-ping.js ***! - \******************************/ +/***/ "./src/headless/converse-ping.js": +/*!***************************************!*\ + !*** ./src/headless/converse-ping.js ***! + \***************************************/ /*! no static exports found */ /***/ (function(module, exports, __webpack_require__) { @@ -73623,7 +77311,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ * as specified in XEP-0199 XMPP Ping. */ (function (root, factory) { - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! converse-core */ "./src/converse-core.js"), __webpack_require__(/*! strophejs-plugin-ping */ "./node_modules/strophejs-plugin-ping/strophe.ping.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! ./converse-core */ "./src/headless/converse-core.js"), __webpack_require__(/*! strophejs-plugin-ping */ "./node_modules/strophejs-plugin-ping/strophe.ping.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); @@ -73738,3859 +77426,10 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /***/ }), -/***/ "./src/converse-profile.js": -/*!*********************************!*\ - !*** ./src/converse-profile.js ***! - \*********************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;// Converse.js (A browser based XMPP chat client) -// http://conversejs.org -// -// Copyright (c) 2013-2017, Jan-Carel Brand -// Licensed under the Mozilla Public License (MPLv2) -// - -/*global define */ -(function (root, factory) { - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! converse-core */ "./src/converse-core.js"), __webpack_require__(/*! bootstrap */ "./node_modules/bootstrap.native/dist/bootstrap-native-v4.js"), __webpack_require__(/*! formdata-polyfill */ "./node_modules/formdata-polyfill/FormData.js"), __webpack_require__(/*! templates/alert.html */ "./src/templates/alert.html"), __webpack_require__(/*! templates/chat_status_modal.html */ "./src/templates/chat_status_modal.html"), __webpack_require__(/*! templates/profile_modal.html */ "./src/templates/profile_modal.html"), __webpack_require__(/*! templates/profile_view.html */ "./src/templates/profile_view.html"), __webpack_require__(/*! templates/status_option.html */ "./src/templates/status_option.html"), __webpack_require__(/*! converse-vcard */ "./src/converse-vcard.js"), __webpack_require__(/*! converse-modal */ "./src/converse-modal.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), - __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? - (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), - __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); -})(this, function (converse, bootstrap, _FormData, tpl_alert, tpl_chat_status_modal, tpl_profile_modal, tpl_profile_view, tpl_status_option) { - "use strict"; - - const _converse$env = converse.env, - Strophe = _converse$env.Strophe, - Backbone = _converse$env.Backbone, - Promise = _converse$env.Promise, - utils = _converse$env.utils, - _ = _converse$env._, - moment = _converse$env.moment; - const u = converse.env.utils; - converse.plugins.add('converse-profile', { - dependencies: ["converse-modal", "converse-vcard", "converse-chatboxviews"], - - initialize() { - /* The initialize function gets called as soon as the plugin is - * loaded by converse.js's plugin machinery. - */ - const _converse = this._converse, - __ = _converse.__; - _converse.ProfileModal = _converse.BootstrapModal.extend({ - events: { - 'click .change-avatar': "openFileSelection", - 'change input[type="file"': "updateFilePreview", - 'submit .profile-form': 'onFormSubmitted' - }, - - initialize() { - this.model.on('change', this.render, this); - - _converse.BootstrapModal.prototype.initialize.apply(this, arguments); - - _converse.emit('profileModalInitialized', this.model); - }, - - toHTML() { - return tpl_profile_modal(_.extend(this.model.toJSON(), this.model.vcard.toJSON(), { - '_': _, - '__': __, - '_converse': _converse, - 'alt_avatar': __('Your avatar image'), - 'heading_profile': __('Your Profile'), - 'label_close': __('Close'), - 'label_email': __('Email'), - 'label_fullname': __('Full Name'), - 'label_jid': __('XMPP Address (JID)'), - 'label_nickname': __('Nickname'), - 'label_role': __('Role'), - 'label_role_help': __('Use commas to separate multiple roles. Your roles are shown next to your name on your chat messages.'), - 'label_url': __('URL'), - 'utils': u, - 'view': this - })); - }, - - afterRender() { - this.tabs = _.map(this.el.querySelectorAll('.nav-item'), tab => new bootstrap.Tab(tab)); - }, - - openFileSelection(ev) { - ev.preventDefault(); - this.el.querySelector('input[type="file"]').click(); - }, - - updateFilePreview(ev) { - const file = ev.target.files[0], - reader = new FileReader(); - - reader.onloadend = () => { - this.el.querySelector('.avatar').setAttribute('src', reader.result); - }; - - reader.readAsDataURL(file); - }, - - setVCard(data) { - _converse.api.vcard.set(_converse.bare_jid, data).then(() => _converse.api.vcard.update(this.model.vcard, true)).catch(err => { - _converse.log(err, Strophe.LogLevel.FATAL); - - _converse.api.alert.show(Strophe.LogLevel.ERROR, __('Error'), [__("Sorry, an error happened while trying to save your profile data."), __("You can check your browser's developer console for any error output.")]); - }); - - this.modal.hide(); - }, - - onFormSubmitted(ev) { - ev.preventDefault(); - const reader = new FileReader(), - form_data = new FormData(ev.target), - image_file = form_data.get('image'); - const data = { - 'fn': form_data.get('fn'), - 'nickname': form_data.get('nickname'), - 'role': form_data.get('role'), - 'email': form_data.get('email'), - 'url': form_data.get('url') - }; - - if (!image_file.size) { - _.extend(data, { - 'image': this.model.vcard.get('image'), - 'image_type': this.model.vcard.get('image_type') - }); - - this.setVCard(data); - } else { - reader.onloadend = () => { - _.extend(data, { - 'image': btoa(reader.result), - 'image_type': image_file.type - }); - - this.setVCard(data); - }; - - reader.readAsBinaryString(image_file); - } - } - - }); - _converse.ChatStatusModal = _converse.BootstrapModal.extend({ - events: { - "submit form#set-xmpp-status": "onFormSubmitted", - "click .clear-input": "clearStatusMessage" - }, - - toHTML() { - return tpl_chat_status_modal(_.extend(this.model.toJSON(), this.model.vcard.toJSON(), { - 'label_away': __('Away'), - 'label_close': __('Close'), - 'label_busy': __('Busy'), - 'label_cancel': __('Cancel'), - 'label_custom_status': __('Custom status'), - 'label_offline': __('Offline'), - 'label_online': __('Online'), - 'label_save': __('Save'), - 'label_xa': __('Away for long'), - 'modal_title': __('Change chat status'), - 'placeholder_status_message': __('Personal status message') - })); - }, - - afterRender() { - this.el.addEventListener('shown.bs.modal', () => { - this.el.querySelector('input[name="status_message"]').focus(); - }, false); - }, - - clearStatusMessage(ev) { - if (ev && ev.preventDefault) { - ev.preventDefault(); - u.hideElement(this.el.querySelector('.clear-input')); - } - - const roster_filter = this.el.querySelector('input[name="status_message"]'); - roster_filter.value = ''; - }, - - onFormSubmitted(ev) { - ev.preventDefault(); - const data = new FormData(ev.target); - this.model.save({ - 'status_message': data.get('status_message'), - 'status': data.get('chat_status') - }); - this.modal.hide(); - } - - }); - _converse.XMPPStatusView = _converse.VDOMViewWithAvatar.extend({ - tagName: "div", - events: { - "click a.show-profile": "showProfileModal", - "click a.change-status": "showStatusChangeModal", - "click .logout": "logOut" - }, - - initialize() { - this.model.on("change", this.render, this); - this.model.vcard.on("change", this.render, this); - }, - - toHTML() { - const chat_status = this.model.get('status') || 'offline'; - return tpl_profile_view(_.extend(this.model.toJSON(), this.model.vcard.toJSON(), { - '__': __, - 'fullname': this.model.vcard.get('fullname') || _converse.bare_jid, - 'status_message': this.model.get('status_message') || __("I am %1$s", this.getPrettyStatus(chat_status)), - 'chat_status': chat_status, - '_converse': _converse, - 'title_change_settings': __('Change settings'), - 'title_change_status': __('Click to change your chat status'), - 'title_log_out': __('Log out'), - 'title_your_profile': __('Your profile') - })); - }, - - afterRender() { - this.renderAvatar(); - }, - - showProfileModal(ev) { - if (_.isUndefined(this.profile_modal)) { - this.profile_modal = new _converse.ProfileModal({ - model: this.model - }); - } - - this.profile_modal.show(ev); - }, - - showStatusChangeModal(ev) { - if (_.isUndefined(this.status_modal)) { - this.status_modal = new _converse.ChatStatusModal({ - model: this.model - }); - } - - this.status_modal.show(ev); - }, - - logOut(ev) { - ev.preventDefault(); - const result = confirm(__("Are you sure you want to log out?")); - - if (result === true) { - _converse.logOut(); - } - }, - - getPrettyStatus(stat) { - if (stat === 'chat') { - return __('online'); - } else if (stat === 'dnd') { - return __('busy'); - } else if (stat === 'xa') { - return __('away for long'); - } else if (stat === 'away') { - return __('away'); - } else if (stat === 'offline') { - return __('offline'); - } else { - return __(stat) || __('online'); - } - } - - }); - } - - }); -}); - -/***/ }), - -/***/ "./src/converse-push.js": -/*!******************************!*\ - !*** ./src/converse-push.js ***! - \******************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;// Converse.js -// https://conversejs.org -// -// Copyright (c) 2013-2018, the Converse.js developers -// Licensed under the Mozilla Public License (MPLv2) - -/* This is a Converse.js plugin which add support for registering - * an "App Server" as defined in XEP-0357 - */ -(function (root, factory) { - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! converse-core */ "./src/converse-core.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), - __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? - (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), - __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); -})(this, function (converse) { - "use strict"; - - const _converse$env = converse.env, - Strophe = _converse$env.Strophe, - $iq = _converse$env.$iq, - _ = _converse$env._; - Strophe.addNamespace('PUSH', 'urn:xmpp:push:0'); - converse.plugins.add('converse-push', { - initialize() { - /* The initialize function gets called as soon as the plugin is - * loaded by converse.js's plugin machinery. - */ - const _converse = this._converse, - __ = _converse.__; - - _converse.api.settings.update({ - 'push_app_servers': [], - 'enable_muc_push': false - }); - - async function disablePushAppServer(domain, push_app_server) { - if (!push_app_server.jid) { - return; - } - - const result = await _converse.api.disco.supports(Strophe.NS.PUSH, domain || _converse.bare_jid); - - if (!result.length) { - return _converse.log(`Not disabling push app server "${push_app_server.jid}", no disco support from your server.`, Strophe.LogLevel.WARN); - } - - const stanza = $iq({ - 'type': 'set' - }); - - if (domain !== _converse.bare_jid) { - stanza.attrs({ - 'to': domain - }); - } - - stanza.c('disable', { - 'xmlns': Strophe.NS.PUSH, - 'jid': push_app_server.jid - }); - - if (push_app_server.node) { - stanza.attrs({ - 'node': push_app_server.node - }); - } - - _converse.api.sendIQ(stanza).catch(e => { - _converse.log(`Could not disable push app server for ${push_app_server.jid}`, Strophe.LogLevel.ERROR); - - _converse.log(e, Strophe.LogLevel.ERROR); - }); - } - - async function enablePushAppServer(domain, push_app_server) { - if (!push_app_server.jid || !push_app_server.node) { - return; - } - - const identity = await _converse.api.disco.getIdentity('pubsub', 'push', push_app_server.jid); - - if (!identity) { - return _converse.log(`Not enabling push the service "${push_app_server.jid}", it doesn't have the right disco identtiy.`, Strophe.LogLevel.WARN); - } - - const result = await Promise.all([_converse.api.disco.supports(Strophe.NS.PUSH, push_app_server.jid), _converse.api.disco.supports(Strophe.NS.PUSH, domain)]); - - if (!result[0].length && !result[1].length) { - return _converse.log(`Not enabling push app server "${push_app_server.jid}", no disco support from your server.`, Strophe.LogLevel.WARN); - } - - const stanza = $iq({ - 'type': 'set' - }); - - if (domain !== _converse.bare_jid) { - stanza.attrs({ - 'to': domain - }); - } - - stanza.c('enable', { - 'xmlns': Strophe.NS.PUSH, - 'jid': push_app_server.jid, - 'node': push_app_server.node - }); - - if (push_app_server.secret) { - stanza.c('x', { - 'xmlns': Strophe.NS.XFORM, - 'type': 'submit' - }).c('field', { - 'var': 'FORM_TYPE' - }).c('value').t(`${Strophe.NS.PUBSUB}#publish-options`).up().up().c('field', { - 'var': 'secret' - }).c('value').t(push_app_server.secret); - } - - return _converse.api.sendIQ(stanza); - } - - async function enablePush(domain) { - domain = domain || _converse.bare_jid; - const push_enabled = _converse.session.get('push_enabled') || []; - - if (_.includes(push_enabled, domain)) { - return; - } - - const enabled_services = _.reject(_converse.push_app_servers, 'disable'); - - try { - await Promise.all(_.map(enabled_services, _.partial(enablePushAppServer, domain))); - } catch (e) { - _converse.log('Could not enable push App Server', Strophe.LogLevel.ERROR); - - if (e) _converse.log(e, Strophe.LogLevel.ERROR); - } finally { - push_enabled.push(domain); - } - - const disabled_services = _.filter(_converse.push_app_servers, 'disable'); - - _.each(disabled_services, _.partial(disablePushAppServer, domain)); - - _converse.session.save('push_enabled', push_enabled); - } - - _converse.api.listen.on('statusInitialized', () => enablePush()); - - function onChatBoxAdded(model) { - if (model.get('type') == _converse.CHATROOMS_TYPE) { - enablePush(Strophe.getDomainFromJid(model.get('jid'))); - } - } - - if (_converse.enable_muc_push) { - _converse.api.listen.on('chatBoxesInitialized', () => _converse.chatboxes.on('add', onChatBoxAdded)); - } - } - - }); -}); - -/***/ }), - -/***/ "./src/converse-register.js": -/*!**********************************!*\ - !*** ./src/converse-register.js ***! - \**********************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;// Converse.js (A browser based XMPP chat client) -// http://conversejs.org -// -// Copyright (c) 2012-2017, Jan-Carel Brand -// Licensed under the Mozilla Public License (MPLv2) -// - -/*global define */ - -/* This is a Converse.js plugin which add support for in-band registration - * as specified in XEP-0077. - */ -(function (root, factory) { - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! utils/form */ "./src/utils/form.js"), __webpack_require__(/*! converse-core */ "./src/converse-core.js"), __webpack_require__(/*! templates/form_username.html */ "./src/templates/form_username.html"), __webpack_require__(/*! templates/register_link.html */ "./src/templates/register_link.html"), __webpack_require__(/*! templates/register_panel.html */ "./src/templates/register_panel.html"), __webpack_require__(/*! templates/registration_form.html */ "./src/templates/registration_form.html"), __webpack_require__(/*! templates/registration_request.html */ "./src/templates/registration_request.html"), __webpack_require__(/*! templates/form_input.html */ "./src/templates/form_input.html"), __webpack_require__(/*! templates/spinner.html */ "./src/templates/spinner.html"), __webpack_require__(/*! converse-controlbox */ "./src/converse-controlbox.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), - __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? - (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), - __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); -})(this, function (utils, converse, tpl_form_username, tpl_register_link, tpl_register_panel, tpl_registration_form, tpl_registration_request, tpl_form_input, tpl_spinner) { - "use strict"; // Strophe methods for building stanzas - - const _converse$env = converse.env, - Strophe = _converse$env.Strophe, - Backbone = _converse$env.Backbone, - sizzle = _converse$env.sizzle, - $iq = _converse$env.$iq, - _ = _converse$env._; // Add Strophe Namespaces - - Strophe.addNamespace('REGISTER', 'jabber:iq:register'); // Add Strophe Statuses - - let i = 0; - - _.each(_.keys(Strophe.Status), 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; - converse.plugins.add('converse-register', { - 'overrides': { - // Overrides mentioned here will be picked up by converse.js's - // plugin architecture they will replace existing methods on the - // relevant objects or classes. - // - // New functions which don't exist yet can also be added. - LoginPanel: { - insertRegisterLink() { - const _converse = this.__super__._converse; - - if (_.isUndefined(this.registerlinkview)) { - this.registerlinkview = new _converse.RegisterLinkView({ - 'model': this.model - }); - this.registerlinkview.render(); - this.el.querySelector('.buttons').insertAdjacentElement('afterend', this.registerlinkview.el); - } - - this.registerlinkview.render(); - }, - - render(cfg) { - const _converse = this.__super__._converse; - - this.__super__.render.apply(this, arguments); - - if (_converse.allow_registration && !_converse.auto_login) { - this.insertRegisterLink(); - } - - return this; - } - - }, - ControlBoxView: { - initialize() { - this.__super__.initialize.apply(this, arguments); - - this.model.on('change:active-form', this.showLoginOrRegisterForm.bind(this)); - }, - - showLoginOrRegisterForm() { - const _converse = this.__super__._converse; - - if (_.isNil(this.registerpanel)) { - return; - } - - if (this.model.get('active-form') == "register") { - this.loginpanel.el.classList.add('hidden'); - this.registerpanel.el.classList.remove('hidden'); - } else { - this.loginpanel.el.classList.remove('hidden'); - this.registerpanel.el.classList.add('hidden'); - } - }, - - renderRegistrationPanel() { - const _converse = this.__super__._converse; - - if (_converse.allow_registration) { - this.registerpanel = new _converse.RegisterPanel({ - 'model': this.model - }); - this.registerpanel.render(); - this.registerpanel.el.classList.add('hidden'); - this.el.querySelector('#converse-login-panel').insertAdjacentElement('afterend', this.registerpanel.el); - this.showLoginOrRegisterForm(); - } - - return this; - }, - - renderLoginPanel() { - /* Also render a registration panel, when rendering the - * login panel. - */ - this.__super__.renderLoginPanel.apply(this, arguments); - - this.renderRegistrationPanel(); - return this; - } - - } - }, - - initialize() { - /* The initialize function gets called as soon as the plugin is - * loaded by converse.js's plugin machinery. - */ - const _converse = this._converse, - __ = _converse.__; - _converse.CONNECTION_STATUS[Strophe.Status.REGIFAIL] = 'REGIFAIL'; - _converse.CONNECTION_STATUS[Strophe.Status.REGISTERED] = 'REGISTERED'; - _converse.CONNECTION_STATUS[Strophe.Status.CONFLICT] = 'CONFLICT'; - _converse.CONNECTION_STATUS[Strophe.Status.NOTACCEPTABLE] = 'NOTACCEPTABLE'; - - _converse.api.settings.update({ - 'allow_registration': true, - 'domain_placeholder': __(" e.g. conversejs.org"), - // Placeholder text shown in the domain input on the registration form - 'providers_link': 'https://compliance.conversations.im/', - // Link to XMPP providers shown on registration page - 'registration_domain': '' - }); - - function setActiveForm(value) { - _converse.api.waitUntil('controlboxInitialized').then(() => { - const controlbox = _converse.chatboxes.get('controlbox'); - - controlbox.set({ - 'active-form': value - }); - }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); - } - - _converse.router.route('converse/login', _.partial(setActiveForm, 'login')); - - _converse.router.route('converse/register', _.partial(setActiveForm, 'register')); - - _converse.RegisterLinkView = Backbone.VDOMView.extend({ - toHTML() { - return tpl_register_link(_.extend(this.model.toJSON(), { - '__': _converse.__, - '_converse': _converse, - 'connection_status': _converse.connfeedback.get('connection_status') - })); - } - - }); - _converse.RegisterPanel = Backbone.NativeView.extend({ - tagName: 'div', - id: "converse-register-panel", - className: 'controlbox-pane fade-in', - events: { - 'submit form#converse-register': 'onFormSubmission', - 'click .button-cancel': 'renderProviderChoiceForm' - }, - - initialize(cfg) { - this.reset(); - this.registerHooks(); - }, - - render() { - this.model.set('registration_form_rendered', false); - this.el.innerHTML = tpl_register_panel({ - '__': __, - 'default_domain': _converse.registration_domain, - 'label_register': __('Fetch registration form'), - 'help_providers': __('Tip: A list of public XMPP providers is available'), - 'help_providers_link': __('here'), - 'href_providers': _converse.providers_link, - 'domain_placeholder': _converse.domain_placeholder - }); - - if (_converse.registration_domain) { - this.fetchRegistrationForm(_converse.registration_domain); - } - - return this; - }, - - registerHooks() { - /* Hook into Strophe's _connect_cb, so that we can send an IQ - * requesting the registration fields. - */ - const conn = _converse.connection; - - const connect_cb = conn._connect_cb.bind(conn); - - conn._connect_cb = (req, callback, raw) => { - if (!this._registering) { - connect_cb(req, callback, raw); - } else { - if (this.getRegistrationFields(req, callback, raw)) { - this._registering = false; - } - } - }; - }, - - getRegistrationFields(req, _callback, raw) { - /* Send an IQ stanza to the XMPP server asking for the - * registration fields. - * Parameters: - * (Strophe.Request) req - The current request - * (Function) callback - */ - const conn = _converse.connection; - conn.connected = true; - - const body = conn._proto._reqToData(req); - - if (!body) { - return; - } - - if (conn._proto._connect_cb(body) === Strophe.Status.CONNFAIL) { - this.showValidationError(__("Sorry, we're unable to connect to your chosen provider.")); - return false; - } - - const register = body.getElementsByTagName("register"); - const mechanisms = body.getElementsByTagName("mechanism"); - - if (register.length === 0 && mechanisms.length === 0) { - conn._proto._no_auth_received(_callback); - - return false; - } - - if (register.length === 0) { - conn._changeConnectStatus(Strophe.Status.REGIFAIL); - - this.showValidationError(__("Sorry, the given provider does not support in " + "band account registration. Please try with a " + "different provider.")); - return true; - } // Send an IQ stanza to get all required data fields - - - conn._addSysHandler(this.onRegistrationFields.bind(this), null, "iq", null, null); - - const stanza = $iq({ - type: "get" - }).c("query", { - xmlns: Strophe.NS.REGISTER - }).tree(); - stanza.setAttribute("id", conn.getUniqueId("sendIQ")); - conn.send(stanza); - conn.connected = false; - return true; - }, - - onRegistrationFields(stanza) { - /* Handler for Registration Fields Request. - * - * Parameters: - * (XMLElement) elem - The query stanza. - */ - if (stanza.getAttribute("type") === "error") { - _converse.connection._changeConnectStatus(Strophe.Status.REGIFAIL, __('Something went wrong while establishing a connection with "%1$s". ' + 'Are you sure it exists?', this.domain)); - - return false; - } - - if (stanza.getElementsByTagName("query").length !== 1) { - _converse.connection._changeConnectStatus(Strophe.Status.REGIFAIL, "unknown"); - - return false; - } - - this.setFields(stanza); - - if (!this.model.get('registration_form_rendered')) { - this.renderRegistrationForm(stanza); - } - - return false; - }, - - reset(settings) { - const defaults = { - fields: {}, - urls: [], - title: "", - instructions: "", - registered: false, - _registering: false, - domain: null, - form_type: null - }; - - _.extend(this, defaults); - - if (settings) { - _.extend(this, _.pick(settings, _.keys(defaults))); - } - }, - - onFormSubmission(ev) { - /* Event handler when the #converse-register form is - * submitted. - * - * Depending on the available input fields, we delegate to - * other methods. - */ - if (ev && ev.preventDefault) { - ev.preventDefault(); - } - - if (_.isNull(ev.target.querySelector('input[name=domain]'))) { - this.submitRegistrationForm(ev.target); - } else { - this.onProviderChosen(ev.target); - } - }, - - onProviderChosen(form) { - /* Callback method that gets called when the user has chosen an - * XMPP provider. - * - * Parameters: - * (HTMLElement) form - The form that was submitted - */ - const domain_input = form.querySelector('input[name=domain]'), - domain = _.get(domain_input, 'value'); - - if (!domain) { - // TODO: add validation message - domain_input.classList.add('error'); - return; - } - - form.querySelector('input[type=submit]').classList.add('hidden'); - this.fetchRegistrationForm(domain.trim()); - }, - - fetchRegistrationForm(domain_name) { - /* This is called with a domain name based on which, it fetches a - * registration form from the requested domain. - * - * Parameters: - * (String) domain_name - XMPP server domain - */ - if (!this.model.get('registration_form_rendered')) { - this.renderRegistrationRequest(); - } - - this.reset({ - 'domain': Strophe.getDomainFromJid(domain_name), - '_registering': true - }); - - _converse.connection.connect(this.domain, "", this.onConnectStatusChanged.bind(this)); - - return false; - }, - - renderRegistrationRequest() { - /* Clear the form and inform the user that the registration - * form is being fetched. - */ - this.clearRegistrationForm().insertAdjacentHTML('beforeend', tpl_registration_request({ - '__': _converse.__, - 'cancel': _converse.registration_domain - })); - }, - - giveFeedback(message, klass) { - let feedback = this.el.querySelector('.reg-feedback'); - - if (!_.isNull(feedback)) { - feedback.parentNode.removeChild(feedback); - } - - const form = this.el.querySelector('form'); - form.insertAdjacentHTML('afterbegin', ''); - feedback = form.querySelector('.reg-feedback'); - feedback.textContent = message; - - if (klass) { - feedback.classList.add(klass); - } - }, - - clearRegistrationForm() { - const form = this.el.querySelector('form'); - form.innerHTML = ''; - this.model.set('registration_form_rendered', false); - return form; - }, - - showSpinner() { - const form = this.el.querySelector('form'); - form.innerHTML = tpl_spinner(); - this.model.set('registration_form_rendered', false); - return this; - }, - - onConnectStatusChanged(status_code) { - /* Callback function called by Strophe whenever the - * connection status changes. - * - * Passed to Strophe specifically during a registration - * attempt. - * - * Parameters: - * (Integer) status_code - The Stroph.Status status code - */ - _converse.log('converse-register: onConnectStatusChanged'); - - if (_.includes([Strophe.Status.DISCONNECTED, Strophe.Status.CONNFAIL, Strophe.Status.REGIFAIL, Strophe.Status.NOTACCEPTABLE, Strophe.Status.CONFLICT], status_code)) { - _converse.log(`Problem during registration: Strophe.Status is ${_converse.CONNECTION_STATUS[status_code]}`, Strophe.LogLevel.ERROR); - - this.abortRegistration(); - } else if (status_code === Strophe.Status.REGISTERED) { - _converse.log("Registered successfully."); - - _converse.connection.reset(); - - this.showSpinner(); - - if (_.includes(["converse/login", "converse/register"], Backbone.history.getFragment())) { - _converse.router.navigate('', { - 'replace': true - }); - } - - if (this.fields.password && this.fields.username) { - // automatically log the user in - _converse.connection.connect(this.fields.username.toLowerCase() + '@' + this.domain.toLowerCase(), this.fields.password, _converse.onConnectStatusChanged); - - this.giveFeedback(__('Now logging you in'), 'info'); - } else { - _converse.chatboxviews.get('controlbox').renderLoginPanel(); - - _converse.giveFeedback(__('Registered successfully')); - } - - this.reset(); - } - }, - - renderLegacyRegistrationForm(form) { - _.each(_.keys(this.fields), key => { - if (key === "username") { - form.insertAdjacentHTML('beforeend', tpl_form_username({ - 'domain': ` @${this.domain}`, - 'name': key, - 'type': "text", - 'label': key, - 'value': '', - 'required': true - })); - } else { - form.insertAdjacentHTML('beforeend', tpl_form_input({ - 'label': key, - 'name': key, - 'placeholder': key, - 'required': true, - 'type': key === 'password' || key === 'email' ? key : "text", - 'value': '' - })); - } - }); // Show urls - - - _.each(this.urls, url => { - form.insertAdjacentHTML('afterend', '' + url + ''); - }); - }, - - renderRegistrationForm(stanza) { - /* Renders the registration form based on the XForm fields - * received from the XMPP server. - * - * Parameters: - * (XMLElement) stanza - The IQ stanza received from the XMPP server. - */ - const form = this.el.querySelector('form'); - form.innerHTML = tpl_registration_form({ - '__': _converse.__, - 'domain': this.domain, - 'title': this.title, - 'instructions': this.instructions, - 'registration_domain': _converse.registration_domain - }); - const buttons = form.querySelector('fieldset.buttons'); - - if (this.form_type === 'xform') { - _.each(stanza.querySelectorAll('field'), field => { - buttons.insertAdjacentHTML('beforebegin', utils.xForm2webForm(field, stanza, this.domain)); - }); - } else { - this.renderLegacyRegistrationForm(form); - } - - if (!this.fields) { - form.querySelector('.button-primary').classList.add('hidden'); - } - - form.classList.remove('hidden'); - this.model.set('registration_form_rendered', true); - }, - - showValidationError(message) { - const form = this.el.querySelector('form'); - let flash = form.querySelector('.form-errors'); - - if (_.isNull(flash)) { - flash = ''; - const instructions = form.querySelector('p.instructions'); - - if (_.isNull(instructions)) { - form.insertAdjacentHTML('afterbegin', flash); - } else { - instructions.insertAdjacentHTML('afterend', flash); - } - - flash = form.querySelector('.form-errors'); - } else { - flash.innerHTML = ''; - } - - flash.insertAdjacentHTML('beforeend', '

' + message + '

'); - flash.classList.remove('hidden'); - }, - - reportErrors(stanza) { - /* Report back to the user any error messages received from the - * XMPP server after attempted registration. - * - * Parameters: - * (XMLElement) stanza - The IQ stanza received from the - * XMPP server. - */ - const errors = stanza.querySelectorAll('error'); - - _.each(errors, error => { - this.showValidationError(error.textContent); - }); - - if (!errors.length) { - const message = __('The provider rejected your registration attempt. ' + 'Please check the values you entered for correctness.'); - - this.showValidationError(message); - } - }, - - renderProviderChoiceForm(ev) { - if (ev && ev.preventDefault) { - ev.preventDefault(); - } - - _converse.connection._proto._abortAllRequests(); - - _converse.connection.reset(); - - this.render(); - }, - - abortRegistration() { - _converse.connection._proto._abortAllRequests(); - - _converse.connection.reset(); - - if (this.model.get('registration_form_rendered')) { - if (_converse.registration_domain && this.model.get('registration_form_rendered')) { - this.fetchRegistrationForm(_converse.registration_domain); - } - } else { - this.render(); - } - }, - - submitRegistrationForm(form) { - /* Handler, when the user submits the registration form. - * Provides form error feedback or starts the registration - * process. - * - * Parameters: - * (HTMLElement) form - The HTML form that was submitted - */ - const has_empty_inputs = _.reduce(this.el.querySelectorAll('input.required'), function (result, input) { - if (input.value === '') { - input.classList.add('error'); - return result + 1; - } - - return result; - }, 0); - - if (has_empty_inputs) { - return; - } - - const inputs = sizzle(':input:not([type=button]):not([type=submit])', form), - iq = $iq({ - 'type': 'set', - 'id': _converse.connection.getUniqueId() - }).c("query", { - xmlns: Strophe.NS.REGISTER - }); - - if (this.form_type === 'xform') { - iq.c("x", { - xmlns: Strophe.NS.XFORM, - type: 'submit' - }); - - _.each(inputs, input => { - iq.cnode(utils.webForm2xForm(input)).up(); - }); - } else { - _.each(inputs, input => { - iq.c(input.getAttribute('name'), {}, input.value); - }); - } - - _converse.connection._addSysHandler(this._onRegisterIQ.bind(this), null, "iq", null, null); - - _converse.connection.send(iq); - - this.setFields(iq.tree()); - }, - - setFields(stanza) { - /* Stores the values that will be sent to the XMPP server - * during attempted registration. - * - * Parameters: - * (XMLElement) stanza - the IQ stanza that will be sent to the XMPP server. - */ - const query = stanza.querySelector('query'); - const xform = sizzle(`x[xmlns="${Strophe.NS.XFORM}"]`, query); - - if (xform.length > 0) { - this._setFieldsFromXForm(xform.pop()); - } else { - this._setFieldsFromLegacy(query); - } - }, - - _setFieldsFromLegacy(query) { - _.each(query.children, field => { - if (field.tagName.toLowerCase() === 'instructions') { - this.instructions = Strophe.getText(field); - return; - } else if (field.tagName.toLowerCase() === 'x') { - if (field.getAttribute('xmlns') === 'jabber:x:oob') { - this.urls.concat(_.map(field.querySelectorAll('url'), 'textContent')); - } - - return; - } - - this.fields[field.tagName.toLowerCase()] = Strophe.getText(field); - }); - - this.form_type = 'legacy'; - }, - - _setFieldsFromXForm(xform) { - this.title = _.get(xform.querySelector('title'), 'textContent'); - this.instructions = _.get(xform.querySelector('instructions'), 'textContent'); - - _.each(xform.querySelectorAll('field'), field => { - const _var = field.getAttribute('var'); - - if (_var) { - this.fields[_var.toLowerCase()] = _.get(field.querySelector('value'), 'textContent', ''); - } else { - // TODO: other option seems to be type="fixed" - _converse.log("Found field we couldn't parse", Strophe.LogLevel.WARN); - } - }); - - this.form_type = 'xform'; - }, - - _onRegisterIQ(stanza) { - /* Callback method that gets called when a return IQ stanza - * is received from the XMPP server, after attempting to - * register a new user. - * - * Parameters: - * (XMLElement) stanza - The IQ stanza. - */ - if (stanza.getAttribute("type") === "error") { - _converse.log("Registration failed.", Strophe.LogLevel.ERROR); - - this.reportErrors(stanza); - let error = stanza.getElementsByTagName("error"); - - if (error.length !== 1) { - _converse.connection._changeConnectStatus(Strophe.Status.REGIFAIL, "unknown"); - - return false; - } - - error = error[0].firstChild.tagName.toLowerCase(); - - if (error === 'conflict') { - _converse.connection._changeConnectStatus(Strophe.Status.CONFLICT, error); - } else if (error === 'not-acceptable') { - _converse.connection._changeConnectStatus(Strophe.Status.NOTACCEPTABLE, error); - } else { - _converse.connection._changeConnectStatus(Strophe.Status.REGIFAIL, error); - } - } else { - _converse.connection._changeConnectStatus(Strophe.Status.REGISTERED, null); - } - - return false; - } - - }); - } - - }); -}); - -/***/ }), - -/***/ "./src/converse-roomslist.js": -/*!***********************************!*\ - !*** ./src/converse-roomslist.js ***! - \***********************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;// Converse.js (A browser based XMPP chat client) -// http://conversejs.org -// -// Copyright (c) 2012-2017, Jan-Carel Brand -// Licensed under the Mozilla Public License (MPLv2) -// - -/*global define */ - -/* This is a non-core Converse.js plugin which shows a list of currently open - * rooms in the "Rooms Panel" of the ControlBox. - */ -(function (root, factory) { - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! converse-core */ "./src/converse-core.js"), __webpack_require__(/*! converse-muc */ "./src/converse-muc.js"), __webpack_require__(/*! templates/rooms_list.html */ "./src/templates/rooms_list.html"), __webpack_require__(/*! templates/rooms_list_item.html */ "./src/templates/rooms_list_item.html")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), - __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? - (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), - __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); -})(this, function (converse, muc, tpl_rooms_list, tpl_rooms_list_item) { - const _converse$env = converse.env, - Backbone = _converse$env.Backbone, - Promise = _converse$env.Promise, - Strophe = _converse$env.Strophe, - b64_sha1 = _converse$env.b64_sha1, - sizzle = _converse$env.sizzle, - _ = _converse$env._; - const u = converse.env.utils; - converse.plugins.add('converse-roomslist', { - /* Optional dependencies are other plugins which might be - * overridden or relied upon, and therefore need to be loaded before - * this plugin. They are called "optional" because they might not be - * available, in which case any overrides applicable to them will be - * ignored. - * - * It's possible however to make optional dependencies non-optional. - * If the setting "strict_plugin_dependencies" is set to true, - * an error will be raised if the plugin is not found. - * - * NB: These plugins need to have already been loaded via require.js. - */ - dependencies: ["converse-singleton", "converse-controlbox", "converse-muc", "converse-bookmarks"], - - initialize() { - /* The initialize function gets called as soon as the plugin is - * loaded by converse.js's plugin machinery. - */ - const _converse = this._converse, - __ = _converse.__; - _converse.OpenRooms = Backbone.Collection.extend({ - comparator(room) { - if (room.get('bookmarked')) { - const bookmark = _.head(_converse.bookmarksview.model.where({ - 'jid': room.get('jid') - })); - - return bookmark.get('name'); - } else { - return room.get('name'); - } - }, - - initialize() { - _converse.chatboxes.on('add', this.onChatBoxAdded, this); - - _converse.chatboxes.on('change:hidden', this.onChatBoxChanged, this); - - _converse.chatboxes.on('change:bookmarked', this.onChatBoxChanged, this); - - _converse.chatboxes.on('change:name', this.onChatBoxChanged, this); - - _converse.chatboxes.on('change:num_unread', this.onChatBoxChanged, this); - - _converse.chatboxes.on('change:num_unread_general', this.onChatBoxChanged, this); - - _converse.chatboxes.on('remove', this.onChatBoxRemoved, this); - - this.reset(_.map(_converse.chatboxes.where({ - 'type': 'chatroom' - }), 'attributes')); - }, - - onChatBoxAdded(item) { - if (item.get('type') === 'chatroom') { - this.create(item.attributes); - } - }, - - onChatBoxChanged(item) { - if (item.get('type') === 'chatroom') { - const room = this.get(item.get('jid')); - - if (!_.isNil(room)) { - room.set(item.attributes); - } - } - }, - - onChatBoxRemoved(item) { - if (item.get('type') === 'chatroom') { - const room = this.get(item.get('jid')); - this.remove(room); - } - } - - }); - _converse.RoomsList = Backbone.Model.extend({ - defaults: { - "toggle-state": _converse.OPENED - } - }); - _converse.RoomsListElementView = Backbone.VDOMView.extend({ - events: { - 'click .room-info': 'showRoomDetailsModal' - }, - - initialize() { - this.model.on('destroy', this.remove, this); - this.model.on('remove', this.remove, this); - this.model.on('change:bookmarked', this.render, this); - this.model.on('change:hidden', this.render, this); - this.model.on('change:name', this.render, this); - this.model.on('change:num_unread', this.render, this); - this.model.on('change:num_unread_general', this.render, this); - }, - - toHTML() { - return tpl_rooms_list_item(_.extend(this.model.toJSON(), { - // XXX: By the time this renders, the _converse.bookmarks - // collection should already exist if bookmarks are - // supported by the XMPP server. So we can use it - // as a check for support (other ways of checking are async). - 'allow_bookmarks': _converse.allow_bookmarks && _converse.bookmarks, - 'currently_open': _converse.isSingleton() && !this.model.get('hidden'), - 'info_leave_room': __('Leave this groupchat'), - 'info_remove_bookmark': __('Unbookmark this groupchat'), - 'info_add_bookmark': __('Bookmark this groupchat'), - 'info_title': __('Show more information on this groupchat'), - 'name': this.getRoomsListElementName(), - 'open_title': __('Click to open this groupchat') - })); - }, - - showRoomDetailsModal(ev) { - const room = _converse.chatboxes.get(this.model.get('jid')); - - ev.preventDefault(); - - if (_.isUndefined(room.room_details_modal)) { - room.room_details_modal = new _converse.RoomDetailsModal({ - 'model': room - }); - } - - room.room_details_modal.show(ev); - }, - - getRoomsListElementName() { - if (this.model.get('bookmarked') && _converse.bookmarksview) { - const bookmark = _.head(_converse.bookmarksview.model.where({ - 'jid': this.model.get('jid') - })); - - return bookmark.get('name'); - } else { - return this.model.get('name'); - } - } - - }); - _converse.RoomsListView = Backbone.OrderedListView.extend({ - tagName: 'div', - className: 'open-rooms-list list-container rooms-list-container', - events: { - 'click .add-bookmark': 'addBookmark', - 'click .close-room': 'closeRoom', - 'click .list-toggle': 'toggleRoomsList', - 'click .remove-bookmark': 'removeBookmark', - 'click .open-room': 'openRoom' - }, - listSelector: '.rooms-list', - ItemView: _converse.RoomsListElementView, - subviewIndex: 'jid', - - initialize() { - Backbone.OrderedListView.prototype.initialize.apply(this, arguments); - this.model.on('add', this.showOrHide, this); - this.model.on('remove', this.showOrHide, this); - - const storage = _converse.config.get('storage'), - id = b64_sha1(`converse.roomslist${_converse.bare_jid}`); - - this.list_model = new _converse.RoomsList({ - 'id': id - }); - this.list_model.browserStorage = new Backbone.BrowserStorage[storage](id); - this.list_model.fetch(); - this.render(); - this.sortAndPositionAllItems(); - }, - - render() { - this.el.innerHTML = tpl_rooms_list({ - 'toggle_state': this.list_model.get('toggle-state'), - 'desc_rooms': __('Click to toggle the list of open groupchats'), - 'label_rooms': __('Open Groupchats'), - '_converse': _converse - }); - - if (this.list_model.get('toggle-state') !== _converse.OPENED) { - this.el.querySelector('.open-rooms-list').classList.add('collapsed'); - } - - this.showOrHide(); - this.insertIntoControlBox(); - return this; - }, - - insertIntoControlBox() { - const controlboxview = _converse.chatboxviews.get('controlbox'); - - if (!_.isUndefined(controlboxview) && !u.rootContains(_converse.root, this.el)) { - const el = controlboxview.el.querySelector('.open-rooms-list'); - - if (!_.isNull(el)) { - el.parentNode.replaceChild(this.el, el); - } - } - }, - - hide() { - u.hideElement(this.el); - }, - - show() { - u.showElement(this.el); - }, - - openRoom(ev) { - ev.preventDefault(); - const name = ev.target.textContent; - const jid = ev.target.getAttribute('data-room-jid'); - const data = { - 'name': name || Strophe.unescapeNode(Strophe.getNodeFromJid(jid)) || jid - }; - - _converse.api.rooms.open(jid, data); - }, - - closeRoom(ev) { - ev.preventDefault(); - const name = ev.target.getAttribute('data-room-name'); - const jid = ev.target.getAttribute('data-room-jid'); - - if (confirm(__("Are you sure you want to leave the groupchat %1$s?", name))) { - // TODO: replace with API call - _converse.chatboxviews.get(jid).close(); - } - }, - - showOrHide(item) { - if (!this.model.models.length) { - u.hideElement(this.el); - } else { - u.showElement(this.el); - } - }, - - removeBookmark: _converse.removeBookmarkViaEvent, - addBookmark: _converse.addBookmarkViaEvent, - - toggleRoomsList(ev) { - if (ev && ev.preventDefault) { - ev.preventDefault(); - } - - const icon_el = ev.target.querySelector('.fa'); - - if (icon_el.classList.contains("fa-caret-down")) { - u.slideIn(this.el.querySelector('.open-rooms-list')).then(() => { - this.list_model.save({ - 'toggle-state': _converse.CLOSED - }); - icon_el.classList.remove("fa-caret-down"); - icon_el.classList.add("fa-caret-right"); - }); - } else { - u.slideOut(this.el.querySelector('.open-rooms-list')).then(() => { - this.list_model.save({ - 'toggle-state': _converse.OPENED - }); - icon_el.classList.remove("fa-caret-right"); - icon_el.classList.add("fa-caret-down"); - }); - } - } - - }); - - const initRoomsListView = function initRoomsListView() { - const storage = _converse.config.get('storage'), - id = b64_sha1(`converse.open-rooms-{_converse.bare_jid}`), - model = new _converse.OpenRooms(); - - model.browserStorage = new Backbone.BrowserStorage[storage](id); - _converse.rooms_list_view = new _converse.RoomsListView({ - 'model': model - }); - }; - - if (_converse.allow_bookmarks) { - u.onMultipleEvents([{ - 'object': _converse, - 'event': 'chatBoxesFetched' - }, { - 'object': _converse, - 'event': 'roomsPanelRendered' - }, { - 'object': _converse, - 'event': 'bookmarksInitialized' - }], initRoomsListView); - } else { - u.onMultipleEvents([{ - 'object': _converse, - 'event': 'chatBoxesFetched' - }, { - 'object': _converse, - 'event': 'roomsPanelRendered' - }], initRoomsListView); - } - - _converse.api.listen.on('reconnected', initRoomsListView); - } - - }); -}); - -/***/ }), - -/***/ "./src/converse-roster.js": -/*!********************************!*\ - !*** ./src/converse-roster.js ***! - \********************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;// Converse.js -// http://conversejs.org -// -// Copyright (c) 2012-2018, the Converse.js developers -// Licensed under the Mozilla Public License (MPLv2) -(function (root, factory) { - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! converse-core */ "./src/converse-core.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), - __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? - (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), - __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); -})(this, function (converse) { - "use strict"; - - const _converse$env = converse.env, - Backbone = _converse$env.Backbone, - Promise = _converse$env.Promise, - Strophe = _converse$env.Strophe, - $iq = _converse$env.$iq, - $pres = _converse$env.$pres, - b64_sha1 = _converse$env.b64_sha1, - moment = _converse$env.moment, - sizzle = _converse$env.sizzle, - _ = _converse$env._; - const u = converse.env.utils; - converse.plugins.add('converse-roster', { - dependencies: ["converse-vcard"], - - initialize() { - /* The initialize function gets called as soon as the plugin is - * loaded by converse.js's plugin machinery. - */ - const _converse = this._converse, - __ = _converse.__; - - _converse.api.settings.update({ - 'allow_contact_requests': true, - 'auto_subscribe': false, - 'synchronize_availability': true - }); - - _converse.api.promises.add(['cachedRoster', 'roster', 'rosterContactsFetched', 'rosterGroupsFetched', 'rosterInitialized']); - - _converse.registerPresenceHandler = function () { - _converse.unregisterPresenceHandler(); - - _converse.presence_ref = _converse.connection.addHandler(function (presence) { - _converse.roster.presenceHandler(presence); - - return true; - }, null, 'presence', null); - }; - - _converse.initRoster = function () { - /* Initialize the Bakcbone collections that represent the contats - * roster and the roster groups. - */ - const storage = _converse.config.get('storage'); - - _converse.roster = new _converse.RosterContacts(); - _converse.roster.browserStorage = new Backbone.BrowserStorage[storage](b64_sha1(`converse.contacts-${_converse.bare_jid}`)); - _converse.roster.data = new Backbone.Model(); - const id = b64_sha1(`converse-roster-model-${_converse.bare_jid}`); - _converse.roster.data.id = id; - _converse.roster.data.browserStorage = new Backbone.BrowserStorage[storage](id); - - _converse.roster.data.fetch(); - - _converse.rostergroups = new _converse.RosterGroups(); - _converse.rostergroups.browserStorage = new Backbone.BrowserStorage[storage](b64_sha1(`converse.roster.groups${_converse.bare_jid}`)); - - _converse.emit('rosterInitialized'); - }; - - _converse.populateRoster = function (ignore_cache = false) { - /* Fetch all the roster groups, and then the roster contacts. - * Emit an event after fetching is done in each case. - * - * Parameters: - * (Bool) ignore_cache - If set to to true, the local cache - * will be ignored it's guaranteed that the XMPP server - * will be queried for the roster. - */ - if (ignore_cache) { - _converse.send_initial_presence = true; - - _converse.roster.fetchFromServer().then(() => { - _converse.emit('rosterContactsFetched'); - - _converse.sendInitialPresence(); - }).catch(reason => { - _converse.log(reason, Strophe.LogLevel.ERROR); - - _converse.sendInitialPresence(); - }); - } else { - _converse.rostergroups.fetchRosterGroups().then(() => { - _converse.emit('rosterGroupsFetched'); - - return _converse.roster.fetchRosterContacts(); - }).then(() => { - _converse.emit('rosterContactsFetched'); - - _converse.sendInitialPresence(); - }).catch(reason => { - _converse.log(reason, Strophe.LogLevel.ERROR); - - _converse.sendInitialPresence(); - }); - } - }; - - _converse.Presence = Backbone.Model.extend({ - defaults() { - return { - 'show': 'offline', - 'resources': {} - }; - }, - - getHighestPriorityResource() { - /* Return the resource with the highest priority. - * - * If multiple resources have the same priority, take the - * latest one. - */ - const resources = this.get('resources'); - - if (_.isObject(resources) && _.size(resources)) { - const val = _.flow(_.values, _.partial(_.sortBy, _, ['priority', 'timestamp']), _.reverse)(resources)[0]; - - if (!_.isUndefined(val)) { - return val; - } - } - }, - - addResource(presence) { - /* Adds a new resource and it's associated attributes as taken - * from the passed in presence stanza. - * - * Also updates the presence if the resource has higher priority (and is newer). - */ - const jid = presence.getAttribute('from'), - show = _.propertyOf(presence.querySelector('show'))('textContent') || 'online', - resource = Strophe.getResourceFromJid(jid), - delay = sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, presence).pop(), - timestamp = _.isNil(delay) ? moment().format() : moment(delay.getAttribute('stamp')).format(); - let priority = _.propertyOf(presence.querySelector('priority'))('textContent') || 0; - priority = _.isNaN(parseInt(priority, 10)) ? 0 : parseInt(priority, 10); - const resources = _.isObject(this.get('resources')) ? this.get('resources') : {}; - resources[resource] = { - 'name': resource, - 'priority': priority, - 'show': show, - 'timestamp': timestamp - }; - const changed = { - 'resources': resources - }; - const hpr = this.getHighestPriorityResource(); - - if (priority == hpr.priority && timestamp == hpr.timestamp) { - // Only set the "global" presence if this is the newest resource - // with the highest priority - changed.show = show; - } - - this.save(changed); - return resources; - }, - - removeResource(resource) { - /* Remove the passed in resource from the resources map. - * - * Also redetermines the presence given that there's one less - * resource. - */ - let resources = this.get('resources'); - - if (!_.isObject(resources)) { - resources = {}; - } else { - delete resources[resource]; - } - - this.save({ - 'resources': resources, - 'show': _.propertyOf(this.getHighestPriorityResource())('show') || 'offline' - }); - } - - }); - _converse.Presences = Backbone.Collection.extend({ - model: _converse.Presence - }); - _converse.ModelWithVCardAndPresence = Backbone.Model.extend({ - initialize() { - this.setVCard(); - this.setPresence(); - }, - - setVCard() { - const jid = this.get('jid'); - this.vcard = _converse.vcards.findWhere({ - 'jid': jid - }) || _converse.vcards.create({ - 'jid': jid - }); - }, - - setPresence() { - const jid = this.get('jid'); - this.presence = _converse.presences.findWhere({ - 'jid': jid - }) || _converse.presences.create({ - 'jid': jid - }); - } - - }); - _converse.RosterContact = _converse.ModelWithVCardAndPresence.extend({ - defaults: { - 'chat_state': undefined, - 'image': _converse.DEFAULT_IMAGE, - 'image_type': _converse.DEFAULT_IMAGE_TYPE, - 'num_unread': 0, - 'status': '' - }, - - initialize(attributes) { - _converse.ModelWithVCardAndPresence.prototype.initialize.apply(this, arguments); - - const jid = attributes.jid, - bare_jid = Strophe.getBareJidFromJid(jid).toLowerCase(), - resource = Strophe.getResourceFromJid(jid); - attributes.jid = bare_jid; - this.set(_.assignIn({ - 'groups': [], - 'id': bare_jid, - 'jid': bare_jid, - 'user_id': Strophe.getNodeFromJid(jid) - }, attributes)); - this.setChatBox(); - this.presence.on('change:show', () => _converse.emit('contactPresenceChanged', this)); - this.presence.on('change:show', () => this.trigger('presenceChanged')); - }, - - setChatBox(chatbox = null) { - chatbox = chatbox || _converse.chatboxes.get(this.get('jid')); - - if (chatbox) { - this.chatbox = chatbox; - this.chatbox.on('change:hidden', this.render, this); - } - }, - - getDisplayName() { - return this.get('nickname') || this.vcard.get('nickname') || this.vcard.get('fullname') || this.get('jid'); - }, - - getFullname() { - return this.vcard.get('fullname'); - }, - - subscribe(message) { - /* Send a presence subscription request to this roster contact - * - * Parameters: - * (String) message - An optional message to explain the - * reason for the subscription request. - */ - const pres = $pres({ - to: this.get('jid'), - type: "subscribe" - }); - - if (message && message !== "") { - pres.c("status").t(message).up(); - } - - const nick = _converse.xmppstatus.vcard.get('nickname') || _converse.xmppstatus.vcard.get('fullname'); - - if (nick) { - pres.c('nick', { - 'xmlns': Strophe.NS.NICK - }).t(nick).up(); - } - - _converse.connection.send(pres); - - this.save('ask', "subscribe"); // ask === 'subscribe' Means we have asked to subscribe to them. - - return this; - }, - - ackSubscribe() { - /* Upon receiving the presence stanza of type "subscribed", - * the user SHOULD acknowledge receipt of that subscription - * state notification by sending a presence stanza of type - * "subscribe" to the contact - */ - _converse.connection.send($pres({ - 'type': 'subscribe', - 'to': this.get('jid') - })); - }, - - ackUnsubscribe() { - /* Upon receiving the presence stanza of type "unsubscribed", - * the user SHOULD acknowledge receipt of that subscription state - * notification by sending a presence stanza of type "unsubscribe" - * this step lets the user's server know that it MUST no longer - * send notification of the subscription state change to the user. - * Parameters: - * (String) jid - The Jabber ID of the user who is unsubscribing - */ - _converse.connection.send($pres({ - 'type': 'unsubscribe', - 'to': this.get('jid') - })); - - this.removeFromRoster(); - this.destroy(); - }, - - unauthorize(message) { - /* Unauthorize this contact's presence subscription - * Parameters: - * (String) message - Optional message to send to the person being unauthorized - */ - _converse.rejectPresenceSubscription(this.get('jid'), message); - - return this; - }, - - authorize(message) { - /* Authorize presence subscription - * Parameters: - * (String) message - Optional message to send to the person being authorized - */ - const pres = $pres({ - 'to': this.get('jid'), - 'type': "subscribed" - }); - - if (message && message !== "") { - pres.c("status").t(message); - } - - _converse.connection.send(pres); - - return this; - }, - - removeFromRoster(callback, errback) { - /* Instruct the XMPP server to remove this contact from our roster - * Parameters: - * (Function) callback - */ - const iq = $iq({ - type: 'set' - }).c('query', { - xmlns: Strophe.NS.ROSTER - }).c('item', { - jid: this.get('jid'), - subscription: "remove" - }); - - _converse.connection.sendIQ(iq, callback, errback); - - return this; - } - - }); - _converse.RosterContacts = Backbone.Collection.extend({ - model: _converse.RosterContact, - - comparator(contact1, contact2) { - const status1 = contact1.presence.get('show') || 'offline'; - const status2 = contact2.presence.get('show') || 'offline'; - - if (_converse.STATUS_WEIGHTS[status1] === _converse.STATUS_WEIGHTS[status2]) { - const name1 = contact1.getDisplayName().toLowerCase(); - const name2 = contact2.getDisplayName().toLowerCase(); - return name1 < name2 ? -1 : name1 > name2 ? 1 : 0; - } else { - return _converse.STATUS_WEIGHTS[status1] < _converse.STATUS_WEIGHTS[status2] ? -1 : 1; - } - }, - - onConnected() { - /* Called as soon as the connection has been established - * (either after initial login, or after reconnection). - * - * Use the opportunity to register stanza handlers. - */ - this.registerRosterHandler(); - this.registerRosterXHandler(); - }, - - registerRosterHandler() { - /* Register a handler for roster IQ "set" stanzas, which update - * roster contacts. - */ - _converse.connection.addHandler(iq => { - _converse.roster.onRosterPush(iq); - - return true; - }, Strophe.NS.ROSTER, 'iq', "set"); - }, - - registerRosterXHandler() { - /* Register a handler for RosterX message stanzas, which are - * used to suggest roster contacts to a user. - */ - let t = 0; - - _converse.connection.addHandler(function (msg) { - window.setTimeout(function () { - _converse.connection.flush(); - - _converse.roster.subscribeToSuggestedItems.bind(_converse.roster)(msg); - }, t); - t += msg.querySelectorAll('item').length * 250; - return true; - }, Strophe.NS.ROSTERX, 'message', null); - }, - - fetchRosterContacts() { - /* Fetches the roster contacts, first by trying the - * sessionStorage cache, and if that's empty, then by querying - * the XMPP server. - * - * Returns a promise which resolves once the contacts have been - * fetched. - */ - const that = this; - return new Promise((resolve, reject) => { - this.fetch({ - 'add': true, - 'silent': true, - - success(collection) { - if (collection.length === 0 || that.rosterVersioningSupported() && !_converse.session.get('roster_fetched')) { - _converse.send_initial_presence = true; - - _converse.roster.fetchFromServer().then(resolve).catch(reject); - } else { - _converse.emit('cachedRoster', collection); - - resolve(); - } - } - - }); - }); - }, - - subscribeToSuggestedItems(msg) { - _.each(msg.querySelectorAll('item'), function (item) { - if (item.getAttribute('action') === 'add') { - _converse.roster.addAndSubscribe(item.getAttribute('jid'), _converse.xmppstatus.vcard.get('nickname') || _converse.xmppstatus.vcard.get('fullname')); - } - }); - - return true; - }, - - isSelf(jid) { - return u.isSameBareJID(jid, _converse.connection.jid); - }, - - addAndSubscribe(jid, name, groups, message, attributes) { - /* Add a roster contact and then once we have confirmation from - * the XMPP server we subscribe to that contact's presence updates. - * Parameters: - * (String) jid - The Jabber ID of the user being added and subscribed to. - * (String) name - The name of that user - * (Array of Strings) groups - Any roster groups the user might belong to - * (String) message - An optional message to explain the - * reason for the subscription request. - * (Object) attributes - Any additional attributes to be stored on the user's model. - */ - const handler = contact => { - if (contact instanceof _converse.RosterContact) { - contact.subscribe(message); - } - }; - - this.addContactToRoster(jid, name, groups, attributes).then(handler, handler); - }, - - sendContactAddIQ(jid, name, groups, callback, errback) { - /* Send an IQ stanza to the XMPP server to add a new roster contact. - * - * Parameters: - * (String) jid - The Jabber ID of the user being added - * (String) name - The name of that user - * (Array of Strings) groups - Any roster groups the user might belong to - * (Function) callback - A function to call once the IQ is returned - * (Function) errback - A function to call if an error occurred - */ - name = _.isEmpty(name) ? jid : name; - const iq = $iq({ - type: 'set' - }).c('query', { - xmlns: Strophe.NS.ROSTER - }).c('item', { - jid, - name - }); - - _.each(groups, function (group) { - iq.c('group').t(group).up(); - }); - - _converse.connection.sendIQ(iq, callback, errback); - }, - - addContactToRoster(jid, name, groups, attributes) { - /* Adds a RosterContact instance to _converse.roster and - * registers the contact on the XMPP server. - * Returns a promise which is resolved once the XMPP server has - * responded. - * - * Parameters: - * (String) jid - The Jabber ID of the user being added and subscribed to. - * (String) name - The name of that user - * (Array of Strings) groups - Any roster groups the user might belong to - * (Object) attributes - Any additional attributes to be stored on the user's model. - */ - return new Promise((resolve, reject) => { - groups = groups || []; - this.sendContactAddIQ(jid, name, groups, () => { - const contact = this.create(_.assignIn({ - 'ask': undefined, - 'nickname': name, - groups, - jid, - 'requesting': false, - 'subscription': 'none' - }, attributes), { - sort: false - }); - resolve(contact); - }, function (err) { - alert(__('Sorry, there was an error while trying to add %1$s as a contact.', name)); - - _converse.log(err, Strophe.LogLevel.ERROR); - - resolve(err); - }); - }); - }, - - subscribeBack(bare_jid, presence) { - const contact = this.get(bare_jid); - - if (contact instanceof _converse.RosterContact) { - contact.authorize().subscribe(); - } else { - // Can happen when a subscription is retried or roster was deleted - const handler = contact => { - if (contact instanceof _converse.RosterContact) { - contact.authorize().subscribe(); - } - }; - - const nickname = _.get(sizzle(`nick[xmlns="${Strophe.NS.NICK}"]`, presence).pop(), 'textContent', null); - - this.addContactToRoster(bare_jid, nickname, [], { - 'subscription': 'from' - }).then(handler, handler); - } - }, - - getNumOnlineContacts() { - let ignored = ['offline', 'unavailable']; - - if (_converse.show_only_online_users) { - ignored = _.union(ignored, ['dnd', 'xa', 'away']); - } - - return _.sum(this.models.filter(model => !_.includes(ignored, model.presence.get('show')))); - }, - - onRosterPush(iq) { - /* Handle roster updates from the XMPP server. - * See: https://xmpp.org/rfcs/rfc6121.html#roster-syntax-actions-push - * - * Parameters: - * (XMLElement) IQ - The IQ stanza received from the XMPP server. - */ - const id = iq.getAttribute('id'); - const from = iq.getAttribute('from'); - - if (from && from !== _converse.bare_jid) { - // https://tools.ietf.org/html/rfc6121#page-15 - // - // A receiving client MUST ignore the stanza unless it has no 'from' - // attribute (i.e., implicitly from the bare JID of the user's - // account) or it has a 'from' attribute whose value matches the - // user's bare JID . - return; - } - - _converse.connection.send($iq({ - type: 'result', - id, - from: _converse.connection.jid - })); - - const query = sizzle(`query[xmlns="${Strophe.NS.ROSTER}"]`, iq).pop(); - this.data.save('version', query.getAttribute('ver')); - const items = sizzle(`item`, query); - - if (items.length > 1) { - _converse.log(iq, Strophe.LogLevel.ERROR); - - throw new Error('Roster push query may not contain more than one "item" element.'); - } - - if (items.length === 0) { - _converse.log(iq, Strophe.LogLevel.WARN); - - _converse.log('Received a roster push stanza without an "item" element.', Strophe.LogLevel.WARN); - - return; - } - - this.updateContact(items.pop()); - - _converse.emit('rosterPush', iq); - - return; - }, - - rosterVersioningSupported() { - return _converse.api.disco.stream.getFeature('ver', 'urn:xmpp:features:rosterver') && this.data.get('version'); - }, - - fetchFromServer() { - /* Fetch the roster from the XMPP server */ - return new Promise((resolve, reject) => { - const iq = $iq({ - 'type': 'get', - 'id': _converse.connection.getUniqueId('roster') - }).c('query', { - xmlns: Strophe.NS.ROSTER - }); - - if (this.rosterVersioningSupported()) { - iq.attrs({ - 'ver': this.data.get('version') - }); - } - - const callback = _.flow(this.onReceivedFromServer.bind(this), resolve); - - const errback = function errback(iq) { - const errmsg = "Error while trying to fetch roster from the server"; - - _converse.log(errmsg, Strophe.LogLevel.ERROR); - - reject(new Error(errmsg)); - }; - - return _converse.connection.sendIQ(iq, callback, errback); - }); - }, - - onReceivedFromServer(iq) { - /* An IQ stanza containing the roster has been received from - * the XMPP server. - */ - const query = sizzle(`query[xmlns="${Strophe.NS.ROSTER}"]`, iq).pop(); - - if (query) { - const items = sizzle(`item`, query); - - _.each(items, item => this.updateContact(item)); - - this.data.save('version', query.getAttribute('ver')); - - _converse.session.save('roster_fetched', true); - } - - _converse.emit('roster', iq); - }, - - updateContact(item) { - /* Update or create RosterContact models based on items - * received in the IQ from the server. - */ - const jid = item.getAttribute('jid'); - - if (this.isSelf(jid)) { - return; - } - - const contact = this.get(jid), - subscription = item.getAttribute("subscription"), - ask = item.getAttribute("ask"), - groups = _.map(item.getElementsByTagName('group'), Strophe.getText); - - if (!contact) { - if (subscription === "none" && ask === null || subscription === "remove") { - return; // We're lazy when adding contacts. - } - - this.create({ - 'ask': ask, - 'nickname': item.getAttribute("name"), - 'groups': groups, - 'jid': jid, - 'subscription': subscription - }, { - sort: false - }); - } else { - if (subscription === "remove") { - return contact.destroy(); - } // We only find out about requesting contacts via the - // presence handler, so if we receive a contact - // here, we know they aren't requesting anymore. - // see docs/DEVELOPER.rst - - - contact.save({ - 'subscription': subscription, - 'ask': ask, - 'requesting': null, - 'groups': groups - }); - } - }, - - createRequestingContact(presence) { - const bare_jid = Strophe.getBareJidFromJid(presence.getAttribute('from')), - nickname = _.get(sizzle(`nick[xmlns="${Strophe.NS.NICK}"]`, presence).pop(), 'textContent', null); - - const user_data = { - 'jid': bare_jid, - 'subscription': 'none', - 'ask': null, - 'requesting': true, - 'nickname': nickname - }; - - _converse.emit('contactRequest', this.create(user_data)); - }, - - handleIncomingSubscription(presence) { - const jid = presence.getAttribute('from'), - bare_jid = Strophe.getBareJidFromJid(jid), - contact = this.get(bare_jid); - - if (!_converse.allow_contact_requests) { - _converse.rejectPresenceSubscription(jid, __("This client does not allow presence subscriptions")); - } - - if (_converse.auto_subscribe) { - if (!contact || contact.get('subscription') !== 'to') { - this.subscribeBack(bare_jid, presence); - } else { - contact.authorize(); - } - } else { - if (contact) { - if (contact.get('subscription') !== 'none') { - contact.authorize(); - } else if (contact.get('ask') === "subscribe") { - contact.authorize(); - } - } else { - this.createRequestingContact(presence); - } - } - }, - - handleOwnPresence(presence) { - const jid = presence.getAttribute('from'), - resource = Strophe.getResourceFromJid(jid), - presence_type = presence.getAttribute('type'); - - if (_converse.connection.jid !== jid && presence_type !== 'unavailable' && (_converse.synchronize_availability === true || _converse.synchronize_availability === resource)) { - // Another resource has changed its status and - // synchronize_availability option set to update, - // we'll update ours as well. - const show = _.propertyOf(presence.querySelector('show'))('textContent') || 'online'; - - _converse.xmppstatus.save({ - 'status': show - }, { - 'silent': true - }); - - const status_message = _.propertyOf(presence.querySelector('status'))('textContent'); - - if (status_message) { - _converse.xmppstatus.save({ - 'status_message': status_message - }); - } - } - - if (_converse.jid === jid && presence_type === 'unavailable') { - // XXX: We've received an "unavailable" presence from our - // own resource. Apparently this happens due to a - // Prosody bug, whereby we send an IQ stanza to remove - // a roster contact, and Prosody then sends - // "unavailable" globally, instead of directed to the - // particular user that's removed. - // - // Here is the bug report: https://prosody.im/issues/1121 - // - // I'm not sure whether this might legitimately happen - // in other cases. - // - // As a workaround for now we simply send our presence again, - // otherwise we're treated as offline. - _converse.xmppstatus.sendPresence(); - } - }, - - presenceHandler(presence) { - const presence_type = presence.getAttribute('type'); - - if (presence_type === 'error') { - return true; - } - - const jid = presence.getAttribute('from'), - bare_jid = Strophe.getBareJidFromJid(jid); - - if (this.isSelf(bare_jid)) { - return this.handleOwnPresence(presence); - } else if (sizzle(`query[xmlns="${Strophe.NS.MUC}"]`, presence).length) { - return; // Ignore MUC - } - - const status_message = _.propertyOf(presence.querySelector('status'))('textContent'), - contact = this.get(bare_jid); - - if (contact && status_message !== contact.get('status')) { - contact.save({ - 'status': status_message - }); - } - - if (presence_type === 'subscribed' && contact) { - contact.ackSubscribe(); - } else if (presence_type === 'unsubscribed' && contact) { - contact.ackUnsubscribe(); - } else if (presence_type === 'unsubscribe') { - return; - } else if (presence_type === 'subscribe') { - this.handleIncomingSubscription(presence); - } else if (presence_type === 'unavailable' && contact) { - const resource = Strophe.getResourceFromJid(jid); - contact.presence.removeResource(resource); - } else if (contact) { - // presence_type is undefined - contact.presence.addResource(presence); - } - } - - }); - _converse.RosterGroup = Backbone.Model.extend({ - initialize(attributes) { - this.set(_.assignIn({ - description: __('Click to hide these contacts'), - state: _converse.OPENED - }, attributes)); // Collection of contacts belonging to this group. - - this.contacts = new _converse.RosterContacts(); - } - - }); - _converse.RosterGroups = Backbone.Collection.extend({ - model: _converse.RosterGroup, - - fetchRosterGroups() { - /* Fetches all the roster groups from sessionStorage. - * - * Returns a promise which resolves once the groups have been - * returned. - */ - return new Promise((resolve, reject) => { - this.fetch({ - silent: true, - // We need to first have all groups before - // we can start positioning them, so we set - // 'silent' to true. - success: resolve - }); - }); - } - - }); - - _converse.unregisterPresenceHandler = function () { - if (!_.isUndefined(_converse.presence_ref)) { - _converse.connection.deleteHandler(_converse.presence_ref); - - delete _converse.presence_ref; - } - }; - /********** Event Handlers *************/ - - - function updateUnreadCounter(chatbox) { - const contact = _converse.roster.findWhere({ - 'jid': chatbox.get('jid') - }); - - if (!_.isUndefined(contact)) { - contact.save({ - 'num_unread': chatbox.get('num_unread') - }); - } - } - - _converse.api.listen.on('chatBoxesInitialized', () => { - _converse.chatboxes.on('change:num_unread', updateUnreadCounter); - }); - - _converse.api.listen.on('beforeTearDown', _converse.unregisterPresenceHandler()); - - _converse.api.listen.on('afterTearDown', () => { - if (_converse.presences) { - _converse.presences.off().reset(); // Remove presences - - } - }); - - _converse.api.listen.on('clearSession', () => { - if (_converse.presences) { - _converse.presences.browserStorage._clear(); - } - }); - - _converse.api.listen.on('statusInitialized', reconnecting => { - if (!reconnecting) { - _converse.presences = new _converse.Presences(); - _converse.presences.browserStorage = new Backbone.BrowserStorage.session(b64_sha1(`converse.presences-${_converse.bare_jid}`)); - - _converse.presences.fetch(); - } - - _converse.emit('presencesInitialized', reconnecting); - }); - - _converse.api.listen.on('presencesInitialized', reconnecting => { - if (reconnecting) { - // No need to recreate the roster, otherwise we lose our - // cached data. However we still emit an event, to give - // event handlers a chance to register views for the - // roster and its groups, before we start populating. - _converse.emit('rosterReadyAfterReconnection'); - } else { - _converse.registerIntervalHandler(); - - _converse.initRoster(); - } - - _converse.roster.onConnected(); - - _converse.populateRoster(reconnecting); - - _converse.registerPresenceHandler(); - }); - /************************ API ************************/ - // API methods only available to plugins - - - _.extend(_converse.api, { - /** - * @namespace _converse.api.contacts - * @memberOf _converse.api - */ - 'contacts': { - /** - * This method is used to retrieve roster contacts. - * - * @method _converse.api.contacts.get - * @params {(string[]|string)} jid|jids The JID or JIDs of - * the contacts to be returned. - * @returns {(RosterContact[]|RosterContact)} [Backbone.Model](http://backbonejs.org/#Model) - * (or an array of them) representing the contact. - * - * @example - * // Fetch a single contact - * _converse.api.listen.on('rosterContactsFetched', function () { - * const contact = _converse.api.contacts.get('buddy@example.com') - * // ... - * }); - * - * @example - * // To get multiple contacts, pass in an array of JIDs: - * _converse.api.listen.on('rosterContactsFetched', function () { - * const contacts = _converse.api.contacts.get( - * ['buddy1@example.com', 'buddy2@example.com'] - * ) - * // ... - * }); - * - * @example - * // To return all contacts, simply call ``get`` without any parameters: - * _converse.api.listen.on('rosterContactsFetched', function () { - * const contacts = _converse.api.contacts.get(); - * // ... - * }); - */ - 'get'(jids) { - const _getter = function _getter(jid) { - return _converse.roster.get(Strophe.getBareJidFromJid(jid)) || null; - }; - - if (_.isUndefined(jids)) { - jids = _converse.roster.pluck('jid'); - } else if (_.isString(jids)) { - return _getter(jids); - } - - return _.map(jids, _getter); - }, - - /** - * Add a contact. - * - * @method _converse.api.contacts.add - * @param {string} jid The JID of the contact to be added - * @param {string} [name] A custom name to show the user by - * in the roster. - * @example - * _converse.api.contacts.add('buddy@example.com') - * @example - * _converse.api.contacts.add('buddy@example.com', 'Buddy') - */ - 'add'(jid, name) { - if (!_.isString(jid) || !_.includes(jid, '@')) { - throw new TypeError('contacts.add: invalid jid'); - } - - _converse.roster.addAndSubscribe(jid, _.isEmpty(name) ? jid : name); - } - - } - }); - } - - }); -}); - -/***/ }), - -/***/ "./src/converse-rosterview.js": -/*!************************************!*\ - !*** ./src/converse-rosterview.js ***! - \************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;// Converse.js -// http://conversejs.org -// -// Copyright (c) 2012-2018, the Converse.js developers -// Licensed under the Mozilla Public License (MPLv2) -(function (root, factory) { - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! converse-core */ "./src/converse-core.js"), __webpack_require__(/*! formdata-polyfill */ "./node_modules/formdata-polyfill/FormData.js"), __webpack_require__(/*! templates/add_contact_modal.html */ "./src/templates/add_contact_modal.html"), __webpack_require__(/*! templates/group_header.html */ "./src/templates/group_header.html"), __webpack_require__(/*! templates/pending_contact.html */ "./src/templates/pending_contact.html"), __webpack_require__(/*! templates/requesting_contact.html */ "./src/templates/requesting_contact.html"), __webpack_require__(/*! templates/roster.html */ "./src/templates/roster.html"), __webpack_require__(/*! templates/roster_filter.html */ "./src/templates/roster_filter.html"), __webpack_require__(/*! templates/roster_item.html */ "./src/templates/roster_item.html"), __webpack_require__(/*! templates/search_contact.html */ "./src/templates/search_contact.html"), __webpack_require__(/*! awesomplete */ "./node_modules/awesomplete-avoid-xss/awesomplete.js"), __webpack_require__(/*! converse-chatboxes */ "./src/converse-chatboxes.js"), __webpack_require__(/*! converse-modal */ "./src/converse-modal.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), - __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? - (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), - __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); -})(this, function (converse, _FormData, tpl_add_contact_modal, tpl_group_header, tpl_pending_contact, tpl_requesting_contact, tpl_roster, tpl_roster_filter, tpl_roster_item, tpl_search_contact, Awesomplete) { - "use strict"; - - const _converse$env = converse.env, - Backbone = _converse$env.Backbone, - Strophe = _converse$env.Strophe, - $iq = _converse$env.$iq, - b64_sha1 = _converse$env.b64_sha1, - sizzle = _converse$env.sizzle, - _ = _converse$env._; - const u = converse.env.utils; - converse.plugins.add('converse-rosterview', { - dependencies: ["converse-roster", "converse-modal"], - overrides: { - // Overrides mentioned here will be picked up by converse.js's - // plugin architecture they will replace existing methods on the - // relevant objects or classes. - // - // New functions which don't exist yet can also be added. - afterReconnected() { - this.__super__.afterReconnected.apply(this, arguments); - }, - - tearDown() { - /* Remove the rosterview when tearing down. It gets created - * anew when reconnecting or logging in. - */ - this.__super__.tearDown.apply(this, arguments); - - if (!_.isUndefined(this.rosterview)) { - this.rosterview.remove(); - } - }, - - RosterGroups: { - comparator() { - // RosterGroupsComparator only gets set later (once i18n is - // set up), so we need to wrap it in this nameless function. - const _converse = this.__super__._converse; - return _converse.RosterGroupsComparator.apply(this, arguments); - } - - } - }, - - initialize() { - /* The initialize function gets called as soon as the plugin is - * loaded by converse.js's plugin machinery. - */ - const _converse = this._converse, - __ = _converse.__; - - _converse.api.settings.update({ - 'allow_chat_pending_contacts': true, - 'allow_contact_removal': true, - 'hide_offline_users': false, - 'roster_groups': true, - 'show_only_online_users': false, - 'show_toolbar': true, - 'xhr_user_search_url': null - }); - - _converse.api.promises.add('rosterViewInitialized'); - - const 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') - }; - - const LABEL_GROUPS = __('Groups'); - - const HEADER_CURRENT_CONTACTS = __('My contacts'); - - const HEADER_PENDING_CONTACTS = __('Pending contacts'); - - const HEADER_REQUESTING_CONTACTS = __('Contact requests'); - - const HEADER_UNGROUPED = __('Ungrouped'); - - const HEADER_WEIGHTS = {}; - HEADER_WEIGHTS[HEADER_REQUESTING_CONTACTS] = 0; - HEADER_WEIGHTS[HEADER_CURRENT_CONTACTS] = 1; - HEADER_WEIGHTS[HEADER_UNGROUPED] = 2; - HEADER_WEIGHTS[HEADER_PENDING_CONTACTS] = 3; - - _converse.RosterGroupsComparator = function (a, b) { - /* Groups are sorted alphabetically, ignoring case. - * However, Ungrouped, Requesting Contacts and Pending Contacts - * appear last and in that order. - */ - a = a.get('name'); - b = b.get('name'); - - const special_groups = _.keys(HEADER_WEIGHTS); - - const a_is_special = _.includes(special_groups, a); - - const b_is_special = _.includes(special_groups, b); - - if (!a_is_special && !b_is_special) { - return a.toLowerCase() < b.toLowerCase() ? -1 : a.toLowerCase() > b.toLowerCase() ? 1 : 0; - } else if (a_is_special && b_is_special) { - return HEADER_WEIGHTS[a] < HEADER_WEIGHTS[b] ? -1 : HEADER_WEIGHTS[a] > HEADER_WEIGHTS[b] ? 1 : 0; - } else if (!a_is_special && b_is_special) { - return b === HEADER_REQUESTING_CONTACTS ? 1 : -1; - } else if (a_is_special && !b_is_special) { - return a === HEADER_REQUESTING_CONTACTS ? -1 : 1; - } - }; - - _converse.AddContactModal = _converse.BootstrapModal.extend({ - events: { - 'submit form': 'addContactFromForm' - }, - - initialize() { - _converse.BootstrapModal.prototype.initialize.apply(this, arguments); - - this.model.on('change', this.render, this); - }, - - toHTML() { - const label_nickname = _converse.xhr_user_search_url ? __('Contact name') : __('Optional nickname'); - return tpl_add_contact_modal(_.extend(this.model.toJSON(), { - '_converse': _converse, - 'heading_new_contact': __('Add a Contact'), - 'label_xmpp_address': __('XMPP Address'), - 'label_nickname': label_nickname, - 'contact_placeholder': __('name@example.org'), - 'label_add': __('Add'), - 'error_message': __('Please enter a valid XMPP address') - })); - }, - - afterRender() { - if (_converse.xhr_user_search_url && _.isString(_converse.xhr_user_search_url)) { - this.initXHRAutoComplete(this.el); - } else { - this.initJIDAutoComplete(this.el); - } - - const jid_input = this.el.querySelector('input[name="jid"]'); - this.el.addEventListener('shown.bs.modal', () => { - jid_input.focus(); - }, false); - }, - - initJIDAutoComplete(root) { - const jid_input = root.querySelector('input[name="jid"]'); - - const list = _.uniq(_converse.roster.map(item => Strophe.getDomainFromJid(item.get('jid')))); - - new Awesomplete(jid_input, { - 'list': list, - 'data': function data(text, input) { - return input.slice(0, input.indexOf("@")) + "@" + text; - }, - 'filter': Awesomplete.FILTER_STARTSWITH - }); - }, - - initXHRAutoComplete(root) { - const name_input = this.el.querySelector('input[name="name"]'); - const jid_input = this.el.querySelector('input[name="jid"]'); - const awesomplete = new Awesomplete(name_input, { - 'minChars': 1, - 'list': [] - }); - const xhr = new window.XMLHttpRequest(); // `open` must be called after `onload` for mock/testing purposes. - - xhr.onload = function () { - if (xhr.responseText) { - awesomplete.list = JSON.parse(xhr.responseText).map(i => { - //eslint-disable-line arrow-body-style - return { - 'label': i.fullname || i.jid, - 'value': i.jid - }; - }); - awesomplete.evaluate(); - } - }; - - name_input.addEventListener('input', _.debounce(() => { - xhr.open("GET", `${_converse.xhr_user_search_url}q=${name_input.value}`, true); - xhr.send(); - }, 300)); - this.el.addEventListener('awesomplete-selectcomplete', ev => { - jid_input.value = ev.text.value; - name_input.value = ev.text.label; - }); - }, - - addContactFromForm(ev) { - ev.preventDefault(); - const data = new FormData(ev.target), - jid = data.get('jid'), - name = data.get('name'); - - if (!jid || _.compact(jid.split('@')).length < 2) { - // XXX: we have to do this manually, instead of via - // toHTML because Awesomplete messes things up and - // confuses Snabbdom - u.addClass('is-invalid', this.el.querySelector('input[name="jid"]')); - u.addClass('d-block', this.el.querySelector('.invalid-feedback')); - } else { - ev.target.reset(); - - _converse.roster.addAndSubscribe(jid, name); - - this.model.clear(); - this.modal.hide(); - } - } - - }); - _converse.RosterFilter = Backbone.Model.extend({ - initialize() { - this.set({ - 'filter_text': '', - 'filter_type': 'contacts', - 'chat_state': '' - }); - } - - }); - _converse.RosterFilterView = Backbone.VDOMView.extend({ - tagName: 'form', - className: 'roster-filter-form', - events: { - "keydown .roster-filter": "liveFilter", - "submit form.roster-filter-form": "submitFilter", - "click .clear-input": "clearFilter", - "click .filter-by span": "changeTypeFilter", - "change .state-type": "changeChatStateFilter" - }, - - initialize() { - this.model.on('change:filter_type', this.render, this); - this.model.on('change:filter_text', this.render, this); - }, - - toHTML() { - return tpl_roster_filter(_.extend(this.model.toJSON(), { - visible: this.shouldBeVisible(), - placeholder: __('Filter'), - title_contact_filter: __('Filter by contact name'), - title_group_filter: __('Filter by group name'), - title_status_filter: __('Filter by status'), - label_any: __('Any'), - label_unread_messages: __('Unread'), - label_online: __('Online'), - label_chatty: __('Chatty'), - label_busy: __('Busy'), - label_away: __('Away'), - label_xa: __('Extended Away'), - label_offline: __('Offline') - })); - }, - - changeChatStateFilter(ev) { - if (ev && ev.preventDefault) { - ev.preventDefault(); - } - - this.model.save({ - 'chat_state': this.el.querySelector('.state-type').value - }); - }, - - changeTypeFilter(ev) { - if (ev && ev.preventDefault) { - ev.preventDefault(); - } - - const type = ev.target.dataset.type; - - if (type === 'state') { - this.model.save({ - 'filter_type': type, - 'chat_state': this.el.querySelector('.state-type').value - }); - } else { - this.model.save({ - 'filter_type': type, - 'filter_text': this.el.querySelector('.roster-filter').value - }); - } - }, - - liveFilter: _.debounce(function (ev) { - this.model.save({ - 'filter_text': this.el.querySelector('.roster-filter').value - }); - }, 250), - - submitFilter(ev) { - if (ev && ev.preventDefault) { - ev.preventDefault(); - } - - this.liveFilter(); - this.render(); - }, - - isActive() { - /* Returns true if the filter is enabled (i.e. if the user - * has added values to the filter). - */ - if (this.model.get('filter_type') === 'state' || this.model.get('filter_text')) { - return true; - } - - return false; - }, - - shouldBeVisible() { - return _converse.roster.length >= 5 || this.isActive(); - }, - - showOrHide() { - if (this.shouldBeVisible()) { - this.show(); - } else { - this.hide(); - } - }, - - show() { - if (u.isVisible(this.el)) { - return this; - } - - this.el.classList.add('fade-in'); - this.el.classList.remove('hidden'); - return this; - }, - - hide() { - if (!u.isVisible(this.el)) { - return this; - } - - this.model.save({ - 'filter_text': '', - 'chat_state': '' - }); - this.el.classList.add('hidden'); - return this; - }, - - clearFilter(ev) { - if (ev && ev.preventDefault) { - ev.preventDefault(); - u.hideElement(this.el.querySelector('.clear-input')); - } - - const roster_filter = this.el.querySelector('.roster-filter'); - roster_filter.value = ''; - this.model.save({ - 'filter_text': '' - }); - } - - }); - _converse.RosterContactView = Backbone.NativeView.extend({ - tagName: 'li', - className: 'list-item d-flex hidden controlbox-padded', - events: { - "click .accept-xmpp-request": "acceptRequest", - "click .decline-xmpp-request": "declineRequest", - "click .open-chat": "openChat", - "click .remove-xmpp-contact": "removeContact" - }, - - initialize() { - this.model.on("change", this.render, this); - this.model.on("highlight", this.highlight, this); - this.model.on("destroy", this.remove, this); - this.model.on("open", this.openChat, this); - this.model.on("remove", this.remove, this); - this.model.presence.on("change:show", this.render, this); - this.model.vcard.on('change:fullname', this.render, this); - }, - - render() { - const that = this; - - if (!this.mayBeShown()) { - u.hideElement(this.el); - return this; - } - - const ask = this.model.get('ask'), - show = this.model.presence.get('show'), - requesting = this.model.get('requesting'), - subscription = this.model.get('subscription'); - const classes_to_remove = ['current-xmpp-contact', 'pending-xmpp-contact', 'requesting-xmpp-contact'].concat(_.keys(STATUSES)); - - _.each(classes_to_remove, function (cls) { - if (_.includes(that.el.className, cls)) { - that.el.classList.remove(cls); - } - }); - - this.el.classList.add(show); - this.el.setAttribute('data-status', show); - this.highlight(); - - if (_converse.isSingleton()) { - const chatbox = _converse.chatboxes.get(this.model.get('jid')); - - if (chatbox) { - if (chatbox.get('hidden')) { - this.el.classList.remove('open'); - } else { - this.el.classList.add('open'); - } - } - } - - if (ask === 'subscribe' || subscription === 'from') { - /* ask === 'subscribe' - * Means we have asked to subscribe to them. - * - * subscription === 'from' - * They are subscribed to use, but not vice versa. - * We assume that there is a pending subscription - * from us to them (otherwise we're in a state not - * supported by converse.js). - * - * So in both cases the user is a "pending" contact. - */ - const display_name = this.model.getDisplayName(); - this.el.classList.add('pending-xmpp-contact'); - this.el.innerHTML = tpl_pending_contact(_.extend(this.model.toJSON(), { - 'display_name': display_name, - 'desc_remove': __('Click to remove %1$s as a contact', display_name), - 'allow_chat_pending_contacts': _converse.allow_chat_pending_contacts - })); - } else if (requesting === true) { - const display_name = this.model.getDisplayName(); - this.el.classList.add('requesting-xmpp-contact'); - this.el.innerHTML = tpl_requesting_contact(_.extend(this.model.toJSON(), { - 'display_name': display_name, - 'desc_accept': __("Click to accept the contact request from %1$s", display_name), - 'desc_decline': __("Click to decline the contact request from %1$s", display_name), - 'allow_chat_pending_contacts': _converse.allow_chat_pending_contacts - })); - } else if (subscription === 'both' || subscription === 'to') { - this.el.classList.add('current-xmpp-contact'); - this.el.classList.remove(_.without(['both', 'to'], subscription)[0]); - this.el.classList.add(subscription); - this.renderRosterItem(this.model); - } - - return this; - }, - - highlight() { - /* If appropriate, highlight the contact (by adding the 'open' class). - */ - if (_converse.isSingleton()) { - const chatbox = _converse.chatboxes.get(this.model.get('jid')); - - if (chatbox) { - if (chatbox.get('hidden')) { - this.el.classList.remove('open'); - } else { - this.el.classList.add('open'); - } - } - } - }, - - renderRosterItem(item) { - let status_icon = 'fa fa-times-circle'; - const show = item.presence.get('show') || 'offline'; - - if (show === 'online') { - status_icon = 'fa fa-circle chat-status chat-status--online'; - } else if (show === 'away') { - status_icon = 'fa fa-circle chat-status chat-status--away'; - } else if (show === 'xa') { - status_icon = 'far fa-circle chat-status'; - } else if (show === 'dnd') { - status_icon = 'fa fa-minus-circle chat-status chat-status--busy'; - } - - const display_name = item.getDisplayName(); - this.el.innerHTML = tpl_roster_item(_.extend(item.toJSON(), { - 'display_name': display_name, - 'desc_status': STATUSES[show], - 'status_icon': status_icon, - 'desc_chat': __('Click to chat with %1$s (JID: %2$s)', display_name, item.get('jid')), - 'desc_remove': __('Click to remove %1$s as a contact', display_name), - 'allow_contact_removal': _converse.allow_contact_removal, - 'num_unread': item.get('num_unread') || 0 - })); - return this; - }, - - mayBeShown() { - /* Return a boolean indicating whether this contact should - * generally be visible in the roster. - * - * It doesn't check for the more specific case of whether - * the group it's in is collapsed. - */ - const chatStatus = this.model.presence.get('show'); - - if (_converse.show_only_online_users && chatStatus !== 'online' || _converse.hide_offline_users && chatStatus === 'offline') { - // If pending or requesting, show - if (this.model.get('ask') === 'subscribe' || this.model.get('subscription') === 'from' || this.model.get('requesting') === true) { - return true; - } - - return false; - } - - return true; - }, - - openChat(ev) { - if (ev && ev.preventDefault) { - ev.preventDefault(); - } - - const attrs = this.model.attributes; - - _converse.api.chats.open(attrs.jid, attrs); - }, - - removeContact(ev) { - if (ev && ev.preventDefault) { - ev.preventDefault(); - } - - if (!_converse.allow_contact_removal) { - return; - } - - const result = confirm(__("Are you sure you want to remove this contact?")); - - if (result === true) { - this.model.removeFromRoster(iq => { - this.model.destroy(); - this.remove(); - }, function (err) { - alert(__('Sorry, there was an error while trying to remove %1$s as a contact.', name)); - - _converse.log(err, Strophe.LogLevel.ERROR); - }); - } - }, - - acceptRequest(ev) { - if (ev && ev.preventDefault) { - ev.preventDefault(); - } - - _converse.roster.sendContactAddIQ(this.model.get('jid'), this.model.getFullname(), [], () => { - this.model.authorize().subscribe(); - }); - }, - - declineRequest(ev) { - if (ev && ev.preventDefault) { - ev.preventDefault(); - } - - const result = confirm(__("Are you sure you want to decline this contact request?")); - - if (result === true) { - this.model.unauthorize().destroy(); - } - - return this; - } - - }); - _converse.RosterGroupView = Backbone.OrderedListView.extend({ - tagName: 'div', - className: 'roster-group hidden', - events: { - "click a.group-toggle": "toggle" - }, - ItemView: _converse.RosterContactView, - listItems: 'model.contacts', - listSelector: '.roster-group-contacts', - sortEvent: 'presenceChanged', - - initialize() { - Backbone.OrderedListView.prototype.initialize.apply(this, arguments); - this.model.contacts.on("change:subscription", this.onContactSubscriptionChange, this); - this.model.contacts.on("change:requesting", this.onContactRequestChange, this); - this.model.contacts.on("remove", this.onRemove, this); - - _converse.roster.on('change:groups', this.onContactGroupChange, this); // This event gets triggered once *all* contacts (i.e. not - // just this group's) have been fetched from browser - // storage or the XMPP server and once they've been - // assigned to their various groups. - - - _converse.rosterview.on('rosterContactsFetchedAndProcessed', this.sortAndPositionAllItems.bind(this)); - }, - - render() { - this.el.setAttribute('data-group', this.model.get('name')); - this.el.innerHTML = tpl_group_header({ - 'label_group': this.model.get('name'), - 'desc_group_toggle': this.model.get('description'), - 'toggle_state': this.model.get('state'), - '_converse': _converse - }); - this.contacts_el = this.el.querySelector('.roster-group-contacts'); - return this; - }, - - show() { - u.showElement(this.el); - - _.each(this.getAll(), contact_view => { - if (contact_view.mayBeShown() && this.model.get('state') === _converse.OPENED) { - u.showElement(contact_view.el); - } - }); - - return this; - }, - - collapse() { - return u.slideIn(this.contacts_el); - }, - - filterOutContacts(contacts = []) { - /* Given a list of contacts, make sure they're filtered out - * (aka hidden) and that all other contacts are visible. - * - * If all contacts are hidden, then also hide the group - * title. - */ - let shown = 0; - const all_contact_views = this.getAll(); - - _.each(this.model.contacts.models, contact => { - const contact_view = this.get(contact.get('id')); - - if (_.includes(contacts, contact)) { - u.hideElement(contact_view.el); - } else if (contact_view.mayBeShown()) { - u.showElement(contact_view.el); - shown += 1; - } - }); - - if (shown) { - u.showElement(this.el); - } else { - u.hideElement(this.el); - } - }, - - getFilterMatches(q, type) { - /* Given the filter query "q" and the filter type "type", - * return a list of contacts that need to be filtered out. - */ - if (q.length === 0) { - return []; - } - - let matches; - q = q.toLowerCase(); - - if (type === 'state') { - if (this.model.get('name') === HEADER_REQUESTING_CONTACTS) { - // When filtering by chat state, we still want to - // show requesting contacts, even though they don't - // have the state in question. - matches = this.model.contacts.filter(contact => !_.includes(contact.presence.get('show'), q) && !contact.get('requesting')); - } else if (q === 'unread_messages') { - matches = this.model.contacts.filter({ - 'num_unread': 0 - }); - } else { - matches = this.model.contacts.filter(contact => !_.includes(contact.presence.get('show'), q)); - } - } else { - matches = this.model.contacts.filter(contact => { - return !_.includes(contact.getDisplayName().toLowerCase(), q.toLowerCase()); - }); - } - - return matches; - }, - - filter(q, type) { - /* Filter the group's contacts based on the query "q". - * - * If all contacts are filtered out (i.e. hidden), then the - * group must be filtered out as well. - */ - if (_.isNil(q)) { - type = type || _converse.rosterview.filter_view.model.get('filter_type'); - - if (type === 'state') { - q = _converse.rosterview.filter_view.model.get('chat_state'); - } else { - q = _converse.rosterview.filter_view.model.get('filter_text'); - } - } - - this.filterOutContacts(this.getFilterMatches(q, type)); - }, - - toggle(ev) { - if (ev && ev.preventDefault) { - ev.preventDefault(); - } - - const icon_el = ev.target.querySelector('.fa'); - - if (_.includes(icon_el.classList, "fa-caret-down")) { - this.model.save({ - state: _converse.CLOSED - }); - this.collapse().then(() => { - icon_el.classList.remove("fa-caret-down"); - icon_el.classList.add("fa-caret-right"); - }); - } else { - icon_el.classList.remove("fa-caret-right"); - icon_el.classList.add("fa-caret-down"); - this.model.save({ - state: _converse.OPENED - }); - this.filter(); - u.showElement(this.el); - u.slideOut(this.contacts_el); - } - }, - - onContactGroupChange(contact) { - const in_this_group = _.includes(contact.get('groups'), this.model.get('name')); - - const cid = contact.get('id'); - const in_this_overview = !this.get(cid); - - if (in_this_group && !in_this_overview) { - this.items.trigger('add', contact); - } else if (!in_this_group) { - this.removeContact(contact); - } - }, - - onContactSubscriptionChange(contact) { - if (this.model.get('name') === HEADER_PENDING_CONTACTS && contact.get('subscription') !== 'from') { - this.removeContact(contact); - } - }, - - onContactRequestChange(contact) { - if (this.model.get('name') === HEADER_REQUESTING_CONTACTS && !contact.get('requesting')) { - this.removeContact(contact); - } - }, - - removeContact(contact) { - // We suppress events, otherwise the remove event will - // also cause the contact's view to be removed from the - // "Pending Contacts" group. - this.model.contacts.remove(contact, { - 'silent': true - }); - this.onRemove(contact); - }, - - onRemove(contact) { - this.remove(contact.get('jid')); - - if (this.model.contacts.length === 0) { - this.remove(); - } - } - - }); - _converse.RosterView = Backbone.OrderedListView.extend({ - tagName: 'div', - id: 'converse-roster', - className: 'controlbox-section', - ItemView: _converse.RosterGroupView, - listItems: 'model', - listSelector: '.roster-contacts', - sortEvent: null, - // Groups are immutable, so they don't get re-sorted - subviewIndex: 'name', - events: { - 'click a.chatbox-btn.add-contact': 'showAddContactModal' - }, - - initialize() { - Backbone.OrderedListView.prototype.initialize.apply(this, arguments); - - _converse.roster.on("add", this.onContactAdded, this); - - _converse.roster.on('change:groups', this.onContactAdded, this); - - _converse.roster.on('change', this.onContactChange, this); - - _converse.roster.on("destroy", this.update, this); - - _converse.roster.on("remove", this.update, this); - - _converse.presences.on('change:show', () => { - this.update(); - this.updateFilter(); - }); - - this.model.on("reset", this.reset, this); // This event gets triggered once *all* contacts (i.e. not - // just this group's) have been fetched from browser - // storage or the XMPP server and once they've been - // assigned to their various groups. - - _converse.on('rosterGroupsFetched', this.sortAndPositionAllItems.bind(this)); - - _converse.on('rosterContactsFetched', () => { - _converse.roster.each(contact => this.addRosterContact(contact, { - 'silent': true - })); - - this.update(); - this.updateFilter(); - this.trigger('rosterContactsFetchedAndProcessed'); - }); - - this.createRosterFilter(); - }, - - render() { - this.el.innerHTML = tpl_roster({ - 'allow_contact_requests': _converse.allow_contact_requests, - 'heading_contacts': __('Contacts'), - 'title_add_contact': __('Add a contact') - }); - const form = this.el.querySelector('.roster-filter-form'); - this.el.replaceChild(this.filter_view.render().el, form); - this.roster_el = this.el.querySelector('.roster-contacts'); - return this; - }, - - showAddContactModal(ev) { - if (_.isUndefined(this.add_contact_modal)) { - this.add_contact_modal = new _converse.AddContactModal({ - 'model': new Backbone.Model() - }); - } - - this.add_contact_modal.show(ev); - }, - - createRosterFilter() { - // Create a model on which we can store filter properties - const model = new _converse.RosterFilter(); - model.id = b64_sha1(`_converse.rosterfilter${_converse.bare_jid}`); - model.browserStorage = new Backbone.BrowserStorage.local(this.filter.id); - this.filter_view = new _converse.RosterFilterView({ - 'model': model - }); - this.filter_view.model.on('change', this.updateFilter, this); - this.filter_view.model.fetch(); - }, - - updateFilter: _.debounce(function () { - /* Filter the roster again. - * Called whenever the filter settings have been changed or - * when contacts have been added, removed or changed. - * - * Debounced so that it doesn't get called for every - * contact fetched from browser storage. - */ - const type = this.filter_view.model.get('filter_type'); - - if (type === 'state') { - this.filter(this.filter_view.model.get('chat_state'), type); - } else { - this.filter(this.filter_view.model.get('filter_text'), type); - } - }, 100), - update: _.debounce(function () { - if (!u.isVisible(this.roster_el)) { - u.showElement(this.roster_el); - } - - this.filter_view.showOrHide(); - return this; - }, _converse.animate ? 100 : 0), - - filter(query, type) { - // First we make sure the filter is restored to its - // original state - _.each(this.getAll(), function (view) { - if (view.model.contacts.length > 0) { - view.show().filter(''); - } - }); // Now we can filter - - - query = query.toLowerCase(); - - if (type === 'groups') { - _.each(this.getAll(), function (view, idx) { - if (!_.includes(view.model.get('name').toLowerCase(), query.toLowerCase())) { - u.slideIn(view.el); - } else if (view.model.contacts.length > 0) { - u.slideOut(view.el); - } - }); - } else { - _.each(this.getAll(), function (view) { - view.filter(query, type); - }); - } - }, - - reset() { - _converse.roster.reset(); - - this.removeAll(); - this.render().update(); - return this; - }, - - onContactAdded(contact) { - this.addRosterContact(contact); - this.update(); - this.updateFilter(); - }, - - onContactChange(contact) { - this.updateChatBox(contact); - this.update(); - - if (_.has(contact.changed, 'subscription')) { - if (contact.changed.subscription === 'from') { - this.addContactToGroup(contact, HEADER_PENDING_CONTACTS); - } else if (_.includes(['both', 'to'], contact.get('subscription'))) { - this.addExistingContact(contact); - } - } - - if (_.has(contact.changed, 'ask') && contact.changed.ask === 'subscribe') { - this.addContactToGroup(contact, HEADER_PENDING_CONTACTS); - } - - if (_.has(contact.changed, 'subscription') && contact.changed.requesting === 'true') { - this.addContactToGroup(contact, HEADER_REQUESTING_CONTACTS); - } - - this.updateFilter(); - }, - - updateChatBox(contact) { - if (!this.model.chatbox) { - return this; - } - - const changes = {}; - - if (_.has(contact.changed, 'status')) { - changes.status = contact.get('status'); - } - - this.model.chatbox.save(changes); - return this; - }, - - getGroup(name) { - /* Returns the group as specified by name. - * Creates the group if it doesn't exist. - */ - const view = this.get(name); - - if (view) { - return view.model; - } - - return this.model.create({ - name, - id: b64_sha1(name) - }); - }, - - addContactToGroup(contact, name, options) { - this.getGroup(name).contacts.add(contact, options); - this.sortAndPositionAllItems(); - }, - - addExistingContact(contact, options) { - let groups; - - if (_converse.roster_groups) { - groups = contact.get('groups'); - - if (groups.length === 0) { - groups = [HEADER_UNGROUPED]; - } - } else { - groups = [HEADER_CURRENT_CONTACTS]; - } - - _.each(groups, _.bind(this.addContactToGroup, this, contact, _, options)); - }, - - addRosterContact(contact, options) { - if (contact.get('subscription') === 'both' || contact.get('subscription') === 'to') { - this.addExistingContact(contact, options); - } else { - if (!_converse.allow_contact_requests) { - _converse.log(`Not adding requesting or pending contact ${contact.get('jid')} ` + `because allow_contact_requests is false`, Strophe.LogLevel.DEBUG); - - return; - } - - if (contact.get('ask') === 'subscribe' || contact.get('subscription') === 'from') { - this.addContactToGroup(contact, HEADER_PENDING_CONTACTS, options); - } else if (contact.get('requesting') === true) { - this.addContactToGroup(contact, HEADER_REQUESTING_CONTACTS, options); - } - } - - return this; - } - - }); - /* -------- Event Handlers ----------- */ - - _converse.api.listen.on('chatBoxesInitialized', () => { - _converse.chatboxes.on('change:hidden', chatbox => { - const contact = _converse.roster.findWhere({ - 'jid': chatbox.get('jid') - }); - - if (!_.isUndefined(contact)) { - contact.trigger('highlight', contact); - } - }); - }); - - function initRoster() { - /* Create an instance of RosterView once the RosterGroups - * collection has been created (in converse-core.js) - */ - if (_converse.authentication === _converse.ANONYMOUS) { - return; - } - - _converse.rosterview = new _converse.RosterView({ - 'model': _converse.rostergroups - }); - - _converse.rosterview.render(); - - _converse.emit('rosterViewInitialized'); - } - - _converse.api.listen.on('rosterInitialized', initRoster); - - _converse.api.listen.on('rosterReadyAfterReconnection', initRoster); - } - - }); -}); - -/***/ }), - -/***/ "./src/converse-singleton.js": -/*!***********************************!*\ - !*** ./src/converse-singleton.js ***! - \***********************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;// Converse.js -// http://conversejs.org -// -// Copyright (c) 2012-2018, the Converse.js developers -// Licensed under the Mozilla Public License (MPLv2) - -/* converse-singleton - * ****************** - * - * A plugin which ensures that only one chat (private or groupchat) is - * visible at any one time. All other ongoing chats are hidden and kept in the - * background. - * - * This plugin makes sense in mobile or fullscreen chat environments (as - * configured by the `view_mode` setting). - * - */ -(function (root, factory) { - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! converse-core */ "./src/converse-core.js"), __webpack_require__(/*! converse-chatview */ "./src/converse-chatview.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), - __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? - (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), - __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); -})(this, function (converse) { - "use strict"; - - const _converse$env = converse.env, - _ = _converse$env._, - Strophe = _converse$env.Strophe; - const u = converse.env.utils; - - function hideChat(view) { - if (view.model.get('id') === 'controlbox') { - return; - } - - u.safeSave(view.model, { - 'hidden': true - }); - view.hide(); - } - - converse.plugins.add('converse-singleton', { - // It's possible however to make optional dependencies non-optional. - // If the setting "strict_plugin_dependencies" is set to true, - // an error will be raised if the plugin is not found. - // - // NB: These plugins need to have already been loaded via require.js. - dependencies: ['converse-chatboxes', 'converse-muc', 'converse-muc-views', 'converse-controlbox', 'converse-rosterview'], - overrides: { - // overrides mentioned here will be picked up by converse.js's - // plugin architecture they will replace existing methods on the - // relevant objects or classes. - // - // new functions which don't exist yet can also be added. - ChatBoxes: { - chatBoxMayBeShown(chatbox) { - const _converse = this.__super__._converse; - - if (chatbox.get('id') === 'controlbox') { - return true; - } - - if (_converse.isSingleton()) { - const any_chats_visible = _converse.chatboxes.filter(cb => cb.get('id') != 'controlbox').filter(cb => !cb.get('hidden')).length > 0; - - if (any_chats_visible) { - return !chatbox.get('hidden'); - } else { - return true; - } - } else { - return this.__super__.chatBoxMayBeShown.apply(this, arguments); - } - }, - - createChatBox(jid, attrs) { - /* Make sure new chat boxes are hidden by default. */ - const _converse = this.__super__._converse; - - if (_converse.isSingleton()) { - attrs = attrs || {}; - attrs.hidden = true; - } - - return this.__super__.createChatBox.call(this, jid, attrs); - } - - }, - ChatBoxView: { - shouldShowOnTextMessage() { - const _converse = this.__super__._converse; - - if (_converse.isSingleton()) { - return false; - } else { - return this.__super__.shouldShowOnTextMessage.apply(this, arguments); - } - }, - - _show(focus) { - /* We only have one chat visible at any one - * time. So before opening a chat, we make sure all other - * chats are hidden. - */ - const _converse = this.__super__._converse; - - if (_converse.isSingleton()) { - _.each(this.__super__._converse.chatboxviews.xget(this.model.get('id')), hideChat); - - u.safeSave(this.model, { - 'hidden': false - }); - } - - return this.__super__._show.apply(this, arguments); - } - - }, - ChatRoomView: { - show(focus) { - const _converse = this.__super__._converse; - - if (_converse.isSingleton()) { - _.each(this.__super__._converse.chatboxviews.xget(this.model.get('id')), hideChat); - - u.safeSave(this.model, { - 'hidden': false - }); - } - - return this.__super__.show.apply(this, arguments); - } - - } - } - }); -}); - -/***/ }), - -/***/ "./src/converse-vcard.js": -/*!*******************************!*\ - !*** ./src/converse-vcard.js ***! - \*******************************/ +/***/ "./src/headless/converse-vcard.js": +/*!****************************************!*\ + !*** ./src/headless/converse-vcard.js ***! + \****************************************/ /*! no static exports found */ /***/ (function(module, exports, __webpack_require__) { @@ -77600,7 +77439,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ // Copyright (c) 2013-2018, the Converse.js developers // Licensed under the Mozilla Public License (MPLv2) (function (root, factory) { - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! converse-core */ "./src/converse-core.js"), __webpack_require__(/*! templates/vcard.html */ "./src/templates/vcard.html")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! ./converse-core */ "./src/headless/converse-core.js"), __webpack_require__(/*! ./templates/vcard.html */ "./src/headless/templates/vcard.html")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); @@ -77857,48 +77696,10 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /***/ }), -/***/ "./src/converse.js": -/*!*************************!*\ - !*** ./src/converse.js ***! - \*************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*global define */ -if (true) { - // The section below determines which plugins will be included in a build - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! converse-core */ "./src/converse-core.js"), - /* START: Removable components - * -------------------- - * Any of the following components may be removed if they're not needed. - */ - __webpack_require__(/*! converse-autocomplete */ "./src/converse-autocomplete.js"), __webpack_require__(/*! converse-bookmarks */ "./src/converse-bookmarks.js"), // XEP-0048 Bookmarks - __webpack_require__(/*! converse-caps */ "./src/converse-caps.js"), // XEP-0115 Entity Capabilities - __webpack_require__(/*! converse-chatview */ "./src/converse-chatview.js"), // Renders standalone chat boxes for single user chat - __webpack_require__(/*! converse-controlbox */ "./src/converse-controlbox.js"), // The control box - __webpack_require__(/*! converse-dragresize */ "./src/converse-dragresize.js"), // Allows chat boxes to be resized by dragging them - __webpack_require__(/*! converse-embedded */ "./src/converse-embedded.js"), __webpack_require__(/*! converse-fullscreen */ "./src/converse-fullscreen.js"), __webpack_require__(/*! converse-push */ "./src/converse-push.js"), // XEP-0357 Push Notifications - __webpack_require__(/*! converse-headline */ "./src/converse-headline.js"), // Support for headline messages - __webpack_require__(/*! converse-mam */ "./src/converse-mam.js"), // XEP-0313 Message Archive Management - __webpack_require__(/*! converse-minimize */ "./src/converse-minimize.js"), // Allows chat boxes to be minimized - __webpack_require__(/*! converse-muc */ "./src/converse-muc.js"), // XEP-0045 Multi-user chat - __webpack_require__(/*! converse-muc-views */ "./src/converse-muc-views.js"), // Views related to MUC - __webpack_require__(/*! converse-notification */ "./src/converse-notification.js"), // HTML5 Notifications - __webpack_require__(/*! converse-omemo */ "./src/converse-omemo.js"), __webpack_require__(/*! converse-ping */ "./src/converse-ping.js"), // XEP-0199 XMPP Ping - __webpack_require__(/*! converse-register */ "./src/converse-register.js"), // XEP-0077 In-band registration - __webpack_require__(/*! converse-roomslist */ "./src/converse-roomslist.js"), // Show currently open chat rooms - __webpack_require__(/*! converse-roster */ "./src/converse-roster.js"), __webpack_require__(/*! converse-vcard */ "./src/converse-vcard.js")], __WEBPACK_AMD_DEFINE_RESULT__ = (function (converse) { - return converse; - }).apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), - __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); -} - -/***/ }), - -/***/ "./src/i18n.js": -/*!*********************!*\ - !*** ./src/i18n.js ***! - \*********************/ +/***/ "./src/headless/i18n.js": +/*!******************************!*\ + !*** ./src/headless/i18n.js ***! + \******************************/ /*! no static exports found */ /***/ (function(module, exports, __webpack_require__) { @@ -77913,7 +77714,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /*global define */ (function (root, factory) { - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! es6-promise */ "./node_modules/es6-promise/dist/es6-promise.auto.js"), __webpack_require__(/*! jed */ "./node_modules/jed/jed.js"), __webpack_require__(/*! lodash.noconflict */ "./src/lodash.noconflict.js"), __webpack_require__(/*! moment */ "./node_modules/moment/moment.js"), __webpack_require__(/*! moment/locale/af */ "./node_modules/moment/locale/af.js"), __webpack_require__(/*! moment/locale/ar */ "./node_modules/moment/locale/ar.js"), __webpack_require__(/*! moment/locale/bg */ "./node_modules/moment/locale/bg.js"), __webpack_require__(/*! moment/locale/ca */ "./node_modules/moment/locale/ca.js"), __webpack_require__(/*! moment/locale/cs */ "./node_modules/moment/locale/cs.js"), __webpack_require__(/*! moment/locale/de */ "./node_modules/moment/locale/de.js"), __webpack_require__(/*! moment/locale/es */ "./node_modules/moment/locale/es.js"), __webpack_require__(/*! moment/locale/eu */ "./node_modules/moment/locale/eu.js"), __webpack_require__(/*! moment/locale/fr */ "./node_modules/moment/locale/fr.js"), __webpack_require__(/*! moment/locale/he */ "./node_modules/moment/locale/he.js"), __webpack_require__(/*! moment/locale/hi */ "./node_modules/moment/locale/hi.js"), __webpack_require__(/*! moment/locale/hu */ "./node_modules/moment/locale/hu.js"), __webpack_require__(/*! moment/locale/id */ "./node_modules/moment/locale/id.js"), __webpack_require__(/*! moment/locale/it */ "./node_modules/moment/locale/it.js"), __webpack_require__(/*! moment/locale/ja */ "./node_modules/moment/locale/ja.js"), __webpack_require__(/*! moment/locale/nb */ "./node_modules/moment/locale/nb.js"), __webpack_require__(/*! moment/locale/nl */ "./node_modules/moment/locale/nl.js"), __webpack_require__(/*! moment/locale/pl */ "./node_modules/moment/locale/pl.js"), __webpack_require__(/*! moment/locale/pt-br */ "./node_modules/moment/locale/pt-br.js"), __webpack_require__(/*! moment/locale/ro */ "./node_modules/moment/locale/ro.js"), __webpack_require__(/*! moment/locale/ru */ "./node_modules/moment/locale/ru.js"), __webpack_require__(/*! moment/locale/tr */ "./node_modules/moment/locale/tr.js"), __webpack_require__(/*! moment/locale/uk */ "./node_modules/moment/locale/uk.js"), __webpack_require__(/*! moment/locale/zh-cn */ "./node_modules/moment/locale/zh-cn.js"), __webpack_require__(/*! moment/locale/zh-tw */ "./node_modules/moment/locale/zh-tw.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! es6-promise/dist/es6-promise.auto */ "./node_modules/es6-promise/dist/es6-promise.auto.js"), __webpack_require__(/*! jed */ "./node_modules/jed/jed.js"), __webpack_require__(/*! ./lodash.noconflict */ "./src/headless/lodash.noconflict.js"), __webpack_require__(/*! moment */ "./node_modules/moment/moment.js"), __webpack_require__(/*! moment/locale/af */ "./node_modules/moment/locale/af.js"), __webpack_require__(/*! moment/locale/ar */ "./node_modules/moment/locale/ar.js"), __webpack_require__(/*! moment/locale/bg */ "./node_modules/moment/locale/bg.js"), __webpack_require__(/*! moment/locale/ca */ "./node_modules/moment/locale/ca.js"), __webpack_require__(/*! moment/locale/cs */ "./node_modules/moment/locale/cs.js"), __webpack_require__(/*! moment/locale/de */ "./node_modules/moment/locale/de.js"), __webpack_require__(/*! moment/locale/es */ "./node_modules/moment/locale/es.js"), __webpack_require__(/*! moment/locale/eu */ "./node_modules/moment/locale/eu.js"), __webpack_require__(/*! moment/locale/fr */ "./node_modules/moment/locale/fr.js"), __webpack_require__(/*! moment/locale/he */ "./node_modules/moment/locale/he.js"), __webpack_require__(/*! moment/locale/hi */ "./node_modules/moment/locale/hi.js"), __webpack_require__(/*! moment/locale/hu */ "./node_modules/moment/locale/hu.js"), __webpack_require__(/*! moment/locale/id */ "./node_modules/moment/locale/id.js"), __webpack_require__(/*! moment/locale/it */ "./node_modules/moment/locale/it.js"), __webpack_require__(/*! moment/locale/ja */ "./node_modules/moment/locale/ja.js"), __webpack_require__(/*! moment/locale/nb */ "./node_modules/moment/locale/nb.js"), __webpack_require__(/*! moment/locale/nl */ "./node_modules/moment/locale/nl.js"), __webpack_require__(/*! moment/locale/pl */ "./node_modules/moment/locale/pl.js"), __webpack_require__(/*! moment/locale/pt-br */ "./node_modules/moment/locale/pt-br.js"), __webpack_require__(/*! moment/locale/ro */ "./node_modules/moment/locale/ro.js"), __webpack_require__(/*! moment/locale/ru */ "./node_modules/moment/locale/ru.js"), __webpack_require__(/*! moment/locale/tr */ "./node_modules/moment/locale/tr.js"), __webpack_require__(/*! moment/locale/uk */ "./node_modules/moment/locale/uk.js"), __webpack_require__(/*! moment/locale/zh-cn */ "./node_modules/moment/locale/zh-cn.js"), __webpack_require__(/*! moment/locale/zh-tw */ "./node_modules/moment/locale/zh-tw.js")], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); @@ -78058,29 +77859,14 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /***/ }), -/***/ "./src/jquery-stub.js": -/*!****************************!*\ - !*** ./src/jquery-stub.js ***! - \****************************/ +/***/ "./src/headless/lodash.fp.js": +/*!***********************************!*\ + !*** ./src/headless/lodash.fp.js ***! + \***********************************/ /*! no static exports found */ /***/ (function(module, exports, __webpack_require__) { -var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*global define */ -!(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_RESULT__ = (function () { - return Object; -}).apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), - __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); - -/***/ }), - -/***/ "./src/lodash.fp.js": -/*!**************************!*\ - !*** ./src/lodash.fp.js ***! - \**************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! lodash */ "./node_modules/lodash/lodash.js"), __webpack_require__(/*! lodash.converter */ "./3rdparty/lodash.fp.js")], __WEBPACK_AMD_DEFINE_RESULT__ = (function (_, lodashConverter) { +var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! lodash */ "./node_modules/lodash/lodash.js"), __webpack_require__(/*! ./3rdparty/lodash.fp */ "./src/headless/3rdparty/lodash.fp.js")], __WEBPACK_AMD_DEFINE_RESULT__ = (function (_, lodashConverter) { var fp = lodashConverter(_.runInContext()); return fp; }).apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), @@ -78088,10 +77874,10 @@ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_ /***/ }), -/***/ "./src/lodash.noconflict.js": -/*!**********************************!*\ - !*** ./src/lodash.noconflict.js ***! - \**********************************/ +/***/ "./src/headless/lodash.noconflict.js": +/*!*******************************************!*\ + !*** ./src/headless/lodash.noconflict.js ***! + \*******************************************/ /*! no static exports found */ /***/ (function(module, exports, __webpack_require__) { @@ -78103,10 +77889,10 @@ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*global define /***/ }), -/***/ "./src/polyfill.js": -/*!*************************!*\ - !*** ./src/polyfill.js ***! - \*************************/ +/***/ "./src/headless/polyfill.js": +/*!**********************************!*\ + !*** ./src/headless/polyfill.js ***! + \**********************************/ /*! no static exports found */ /***/ (function(module, exports) { @@ -78178,184 +77964,9 @@ if (!String.prototype.trim) { /***/ }), -/***/ "./src/templates/add_chatroom_modal.html": -/*!***********************************************!*\ - !*** ./src/templates/add_chatroom_modal.html ***! - \***********************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape; -__p += '\n\n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/add_contact_modal.html": -/*!**********************************************!*\ - !*** ./src/templates/add_contact_modal.html ***! - \**********************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n\n\n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/alert.html": -/*!**********************************!*\ - !*** ./src/templates/alert.html ***! - \**********************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape; -__p += '\n\n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/alert_modal.html": -/*!****************************************!*\ - !*** ./src/templates/alert_modal.html ***! - \****************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n\n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/audio.html": -/*!**********************************!*\ - !*** ./src/templates/audio.html ***! - \**********************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape; -__p += '\n\n' + -__e(o.label_download) + -'\n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/bookmark.html": -/*!*************************************!*\ - !*** ./src/templates/bookmark.html ***! - \*************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n\n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/bookmarks_list.html": +/***/ "./src/headless/templates/field.html": /*!*******************************************!*\ - !*** ./src/templates/bookmarks_list.html ***! + !*** ./src/headless/templates/field.html ***! \*******************************************/ /*! no static exports found */ /***/ (function(module, exports, __webpack_require__) { @@ -78364,1010 +77975,7 @@ var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./no module.exports = function(o) { var __t, __p = '', __e = _.escape, __j = Array.prototype.join; function print() { __p += __j.call(arguments, '') } -__p += '\n\n \n ' + -__e(o.label_bookmarks) + -'\n\n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/chat_status_modal.html": -/*!**********************************************!*\ - !*** ./src/templates/chat_status_modal.html ***! - \**********************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n\n\n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/chatarea.html": -/*!*************************************!*\ - !*** ./src/templates/chatarea.html ***! - \*************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n
\n
\n
\n
\n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/chatbox.html": -/*!************************************!*\ - !*** ./src/templates/chatbox.html ***! - \************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n
\n
\n
\n
\n
\n
\n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/chatbox_head.html": -/*!*****************************************!*\ - !*** ./src/templates/chatbox_head.html ***! - \*****************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n
\n
\n
\n
\n \n
\n '; - if (o.url) { ; -__p += '\n \n '; - } ; -__p += '\n ' + -__e( o.nickname || o.fullname || o.jid ) + -'\n '; - if (o.url) { ; -__p += '\n \n '; - } ; -__p += '\n

' + -__e( o.status ) + -'

\n
\n
\n
\n
\n \n \n
\n
\n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/chatbox_message_form.html": -/*!*************************************************!*\ - !*** ./src/templates/chatbox_message_form.html ***! - \*************************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n
\n\n
\n '; - if (o.show_toolbar) { ; -__p += '\n
    \n '; - } ; -__p += '\n \n\n
    \n \n ' + -((__t = ( o.message_value )) == null ? '' : __t) + -'\n \n\n '; - if (o.show_send_button) { ; -__p += '\n \n '; - } ; -__p += '\n
    \n
    \n
    \n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/chatbox_minimize.html": -/*!*********************************************!*\ - !*** ./src/templates/chatbox_minimize.html ***! - \*********************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape; -__p += '\n\n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/chatboxes.html": -/*!**************************************!*\ - !*** ./src/templates/chatboxes.html ***! - \**************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = ''; -__p += '\n
    \n
    \n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/chatroom.html": -/*!*************************************!*\ - !*** ./src/templates/chatroom.html ***! - \*************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = ''; -__p += '\n
    \n
    \n
    \n \n
    \n
    \n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/chatroom_bookmark_form.html": -/*!***************************************************!*\ - !*** ./src/templates/chatroom_bookmark_form.html ***! - \***************************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape; -__p += '\n
    \n
    \n ' + -__e(o.heading) + -'\n
    \n \n \n
    \n
    \n \n \n
    \n
    \n \n \n
    \n
    \n \n \n
    \n
    \n
    \n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/chatroom_bookmark_toggle.html": -/*!*****************************************************!*\ - !*** ./src/templates/chatroom_bookmark_toggle.html ***! - \*****************************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n\n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/chatroom_destroyed.html": -/*!***********************************************!*\ - !*** ./src/templates/chatroom_destroyed.html ***! - \***********************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n
    \n

    ' + -__e(o.__('This room no longer exists')) + -'

    \n\n

    ' + -__e(o.reason) + -'

    \n\n '; - if (o.jid) { ; -__p += '\n

    \n ' + -__e(o.__('The conversation has moved. Click below to enter.') ) + -'\n

    \n \n '; - } ; -__p += '\n
    \n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/chatroom_details_modal.html": -/*!***************************************************!*\ - !*** ./src/templates/chatroom_details_modal.html ***! - \***************************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n\n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/chatroom_disconnect.html": -/*!************************************************!*\ - !*** ./src/templates/chatroom_disconnect.html ***! - \************************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n
    \n

    ' + -__e(o.disconnect_messages[0]) + -'

    \n\n '; - o._.forEach(o.disconnect_messages.slice(1), function (msg) { ; -__p += '\n

    ' + -__e(msg) + -'

    \n '; - }); ; -__p += '\n
    \n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/chatroom_features.html": -/*!**********************************************!*\ - !*** ./src/templates/chatroom_features.html ***! - \**********************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n'; - if (o.has_features) { ; -__p += '\n

    ' + -__e(o.__('Features')) + -'

    \n'; - } ; -__p += '\n
      \n'; - if (o.passwordprotected) { ; -__p += '\n
    • ' + -__e( o.__('Password protected') ) + -'
    • \n'; - } ; -__p += '\n'; - if (o.unsecured) { ; -__p += '\n
    • ' + -__e( o.__('No password') ) + -'
    • \n'; - } ; -__p += '\n'; - if (o.hidden) { ; -__p += '\n
    • ' + -__e( o.__('Hidden') ) + -'
    • \n'; - } ; -__p += '\n'; - if (o.public_room) { ; -__p += '\n
    • ' + -__e( o.__('Public') ) + -'
    • \n'; - } ; -__p += '\n'; - if (o.membersonly) { ; -__p += '\n
    • ' + -__e( o.__('Members only') ) + -'
    • \n'; - } ; -__p += '\n'; - if (o.open) { ; -__p += '\n
    • ' + -__e( o.__('Open') ) + -'
    • \n'; - } ; -__p += '\n'; - if (o.persistent) { ; -__p += '\n
    • ' + -__e( o.__('Persistent') ) + -'
    • \n'; - } ; -__p += '\n'; - if (o.temporary) { ; -__p += '\n
    • ' + -__e( o.__('Temporary') ) + -'
    • \n'; - } ; -__p += '\n'; - if (o.nonanonymous) { ; -__p += '\n
    • ' + -__e( o.__('Not anonymous') ) + -'
    • \n'; - } ; -__p += '\n'; - if (o.semianonymous) { ; -__p += '\n
    • ' + -__e( o.__('Semi-anonymous') ) + -'
    • \n'; - } ; -__p += '\n'; - if (o.moderated) { ; -__p += '\n
    • ' + -__e( o.__('Moderated') ) + -'
    • \n'; - } ; -__p += '\n'; - if (o.unmoderated) { ; -__p += '\n
    • ' + -__e( o.__('Not moderated') ) + -'
    • \n'; - } ; -__p += '\n'; - if (o.mam_enabled) { ; -__p += '\n
    • ' + -__e( o.__('Message archiving') ) + -'
    • \n'; - } ; -__p += '\n
    \n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/chatroom_form.html": -/*!******************************************!*\ - !*** ./src/templates/chatroom_form.html ***! - \******************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = ''; -__p += '\n
    \n
    \n
    \n \n
    \n
    \n
    \n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/chatroom_head.html": -/*!******************************************!*\ - !*** ./src/templates/chatroom_head.html ***! - \******************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n
    \n
    \n
    \n '; - if (o.name && o.name !== o.Strophe.getNodeFromJid(o.jid)) { ; -__p += '\n ' + -__e( o.name ) + -'\n '; - } else { ; -__p += '\n ' + -__e( o.Strophe.getNodeFromJid(o.jid) ) + -'@' + -__e( o.Strophe.getDomainFromJid(o.jid) ) + -'\n '; - } ; -__p += '\n
    \n \n

    ' + -((__t = (o.description)) == null ? '' : __t) + -'

    \n
    \n
    \n \n '; - if (o.affiliation == 'owner') { ; -__p += '\n \n '; - } ; -__p += '\n \n
    \n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/chatroom_invite.html": -/*!********************************************!*\ - !*** ./src/templates/chatroom_invite.html ***! - \********************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n
    \n '; - if (o.error_message) { ; -__p += '\n ' + -__e(o.error_message) + -'\n '; - } ; -__p += '\n \n
    \n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/chatroom_nickname_form.html": -/*!***************************************************!*\ - !*** ./src/templates/chatroom_nickname_form.html ***! - \***************************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape; -__p += '\n
    \n
    \n
    \n \n

    ' + -__e(o.validation_message) + -'

    \n \n
    \n \n
    \n
    \n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/chatroom_password_form.html": -/*!***************************************************!*\ - !*** ./src/templates/chatroom_password_form.html ***! - \***************************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape; -__p += '\n
    \n
    \n
    \n ' + -__e(o.heading) + -'\n \n \n
    \n \n
    \n
    \n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/chatroom_sidebar.html": -/*!*********************************************!*\ - !*** ./src/templates/chatroom_sidebar.html ***! - \*********************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape; -__p += '\n\n
    \n \n

    ' + -__e(o.label_occupants) + -'

    \n
    \n
      \n
      \n\n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/chats_panel.html": -/*!****************************************!*\ - !*** ./src/templates/chats_panel.html ***! - \****************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = ''; -__p += '\n\n
      \n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/controlbox.html": -/*!***************************************!*\ - !*** ./src/templates/controlbox.html ***! - \***************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n
      \n
      \n '; - if (!o.sticky_controlbox) { ; -__p += '\n \n '; - } ; -__p += '\n
      \n
      \n
      \n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/controlbox_toggle.html": -/*!**********************************************!*\ - !*** ./src/templates/controlbox_toggle.html ***! - \**********************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape; -__p += '\n' + -__e(o.label_toggle) + -'\n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/converse_brand_heading.html": -/*!***************************************************!*\ - !*** ./src/templates/converse_brand_heading.html ***! - \***************************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = ''; -__p += '\n\n \n\n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/csn.html": -/*!********************************!*\ - !*** ./src/templates/csn.html ***! - \********************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape; -__p += '\n
      ' + -__e(o.message) + -'
      \n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/dragresize.html": -/*!***************************************!*\ - !*** ./src/templates/dragresize.html ***! - \***************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = ''; -__p += '\n
      \n
      \n
      \n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/emojis.html": -/*!***********************************!*\ - !*** ./src/templates/emojis.html ***! - \***********************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n
      \n'; - o._.forEach(o.emojis_by_category, function (obj, category) { ; -__p += '\n \n'; - }); ; -__p += '\n\n
      \n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/error_message.html": -/*!******************************************!*\ - !*** ./src/templates/error_message.html ***! - \******************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape; -__p += '\n
      ' + -__e(o.message) + -'
      \n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/field.html": -/*!**********************************!*\ - !*** ./src/templates/field.html ***! - \**********************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n\n'; if (o.value.constructor === Array) { ; @@ -79389,1595 +77997,9 @@ return __p /***/ }), -/***/ "./src/templates/file.html": -/*!*********************************!*\ - !*** ./src/templates/file.html ***! - \*********************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape; -__p += '\n' + -__e(o.label_download) + -'\n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/file_progress.html": -/*!******************************************!*\ - !*** ./src/templates/file_progress.html ***! - \******************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape; -__p += '\n
      \n \n
      \n Uploading file: ' + -__e(o.file.name) + -', ' + -__e(o.filesize) + -'\n \n
      \n
      \n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/form_captcha.html": -/*!*****************************************!*\ - !*** ./src/templates/form_captcha.html ***! - \*****************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n'; - if (o.label) { ; -__p += '\n\n'; - } ; -__p += '\n\n' + -__e(o.label) + -'\n
      \n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/form_input.html": -/*!***************************************!*\ - !*** ./src/templates/form_input.html ***! - \***************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n
      \n '; - if (o.type !== 'hidden') { ; -__p += '\n \n '; - } ; -__p += '\n ' + -__e(o.label) + -'\n \n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/form_url.html": -/*!*************************************!*\ - !*** ./src/templates/form_url.html ***! - \*************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape; -__p += '\n\n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/form_username.html": -/*!******************************************!*\ - !*** ./src/templates/form_username.html ***! - \******************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n
      \n '; - if (o.label) { ; -__p += '\n \n '; - } ; -__p += '\n
      \n
      \n ' + -__e(o.domain) + -'
      \n
      \n
      \n
      \n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/group_header.html": -/*!*****************************************!*\ - !*** ./src/templates/group_header.html ***! - \*****************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n\n \n ' + -__e(o.label_group) + -'\n\n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/help_message.html": -/*!*****************************************!*\ - !*** ./src/templates/help_message.html ***! - \*****************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n
      ' + -((__t = (o.message)) == null ? '' : __t) + -'
      \n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/image.html": -/*!**********************************!*\ - !*** ./src/templates/image.html ***! - \**********************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape; -__p += '\n\n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/info.html": -/*!*********************************!*\ - !*** ./src/templates/info.html ***! - \*********************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n'; - if (o.render_message) { ; -__p += '\n \n
      \n'; - } else { ; -__p += '\n
      \n'; - } ; -__p += '\n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/inverse_brand_heading.html": -/*!**************************************************!*\ - !*** ./src/templates/inverse_brand_heading.html ***! - \**************************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = ''; -__p += '\n
      \n
      \n

      Converse

      \n

      Open Source XMPP chat client brought to you by Opkode

      \n

      Translate it into your own language

      \n
      \n
      \n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/list_chatrooms_modal.html": -/*!*************************************************!*\ - !*** ./src/templates/list_chatrooms_modal.html ***! - \*************************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape; -__p += '\n\n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/login_panel.html": -/*!****************************************!*\ - !*** ./src/templates/login_panel.html ***! - \****************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n
      \n
      \n \n '; - if (o.auto_login || o._converse.CONNECTION_STATUS[o.connection_status] === 'CONNECTING') { ; -__p += '\n \n '; - } else { ; -__p += '\n '; - if (o.authentication == o.LOGIN || o.authentication == o.EXTERNAL) { ; -__p += '\n
      \n \n \n
      \n '; - if (o.authentication !== o.EXTERNAL) { ; -__p += '\n
      \n \n \n
      \n '; - } ; -__p += '\n \n\n
      \n \n
      \n '; - } ; -__p += '\n '; - if (o.authentication == o.ANONYMOUS) { ; -__p += '\n \n '; - } ; -__p += '\n '; - if (o.authentication == o.PREBIND) { ; -__p += '\n

      Disconnected.

      \n '; - } ; -__p += '\n '; - } ; -__p += '\n \n
      \n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/message.html": -/*!************************************!*\ - !*** ./src/templates/message.html ***! - \************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n
      \n '; - if (o.type !== 'headline' && !o.is_me_message) { ; -__p += '\n \n '; - } ; -__p += '\n
      \n \n '; - if (o.is_me_message) { ; -__p += ''; - } ; -__p += '\n '; - if (o.is_me_message) { ; -__p += '**'; - }; ; -__p += -__e(o.username) + -'\n '; -o.roles.forEach(function (role) { ; -__p += ' ' + -__e(role) + -' '; - }); ; -__p += '\n '; - if (!o.is_me_message) { ; -__p += ''; - } ; -__p += '\n '; - if (o.is_encrypted) { ; -__p += ''; - } ; -__p += '\n \n '; - if (!o.is_me_message) { ; -__p += '
      '; - } ; -__p += '\n '; - if (o.edited) { ; -__p += ' '; - } ; -__p += '\n '; - if (!o.is_me_message) { ; -__p += '
      '; - } ; -__p += '\n '; - if (o.is_spoiler) { ; -__p += '\n
      \n ' + -__e(o.spoiler_hint) + -'\n ' + -__e(o.label_show) + -'\n
      \n '; - } ; -__p += '\n
      \n
      \n '; - if (!o.is_me_message) { ; -__p += '
      '; - } ; -__p += '\n '; - if (o.type !== 'headline' && !o.is_me_message && o.sender === 'me') { ; -__p += '\n
      \n \n
      \n '; - } ; -__p += '\n\n '; - if (!o.is_me_message) { ; -__p += '
      '; - } ; -__p += '\n
      \n
      \n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/message_versions_modal.html": -/*!***************************************************!*\ - !*** ./src/templates/message_versions_modal.html ***! - \***************************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n\n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/new_day.html": -/*!************************************!*\ - !*** ./src/templates/new_day.html ***! - \************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape; -__p += '\n
      \n
      \n \n
      \n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/occupant.html": -/*!*************************************!*\ - !*** ./src/templates/occupant.html ***! - \*************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n
    • \n
      \n
      \n ' + -__e(o.nick || o.jid) + -'\n '; - if (o.affiliation === "owner") { ; -__p += '\n ' + -__e(o.label_owner) + -'\n '; - } ; -__p += '\n '; - if (o.affiliation === "admin") { ; -__p += '\n ' + -__e(o.label_admin) + -'\n '; - } ; -__p += '\n '; - if (o.affiliation === "member") { ; -__p += '\n ' + -__e(o.label_member) + -'\n '; - } ; -__p += '\n\n '; - if (o.role === "moderator") { ; -__p += '\n ' + -__e(o.label_moderator) + -'\n '; - } ; -__p += '\n '; - if (o.role === "visitor") { ; -__p += '\n ' + -__e(o.label_visitor) + -'\n '; - } ; -__p += '\n
      \n
      \n\n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/pending_contact.html": -/*!********************************************!*\ - !*** ./src/templates/pending_contact.html ***! - \********************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n'; - if (o.allow_chat_pending_contacts) { ; -__p += ''; - } ; -__p += '\n' + -__e(o.display_name) + -' \n'; - if (o.allow_chat_pending_contacts) { ; -__p += ''; - } ; -__p += '\n\n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/profile_modal.html": -/*!******************************************!*\ - !*** ./src/templates/profile_modal.html ***! - \******************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n\n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/profile_view.html": -/*!*****************************************!*\ - !*** ./src/templates/profile_view.html ***! - \*****************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n
      \n
      \n \n \n \n ' + -__e(o.fullname) + -'\n \n \n '; - if (o._converse.allow_logout) { ; -__p += '\n \n '; - } ; -__p += '\n
      \n
      \n \n ' + -__e(o.status_message) + -'\n \n
      \n
      \n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/register_link.html": -/*!******************************************!*\ - !*** ./src/templates/register_link.html ***! - \******************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n
      \n '; - if (!o._converse.auto_login && o._converse.CONNECTION_STATUS[o.connection_status] !== 'CONNECTING') { ; -__p += '\n

      ' + -__e( o.__("Don't have a chat account?") ) + -'

      \n

      \n '; - } ; -__p += '\n
      \n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/register_panel.html": +/***/ "./src/headless/templates/vcard.html": /*!*******************************************!*\ - !*** ./src/templates/register_panel.html ***! - \*******************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n
      \n
      \n ' + -__e(o.__("Create your account")) + -'\n\n
      \n \n \n\n '; - if (o.default_domain) { ; -__p += '\n ' + -__e(o.default_domain) + -'\n
      \n '; - } ; -__p += '\n '; - if (!o.default_domain) { ; -__p += '\n \n

      ' + -__e(o.help_providers) + -' ' + -__e(o.help_providers_link) + -'.

      \n
      \n
      \n \n
      \n

      ' + -__e( o.__("Already have a chat account?") ) + -'

      \n

      \n
      \n
      \n '; - } ; -__p += '\n
      \n \n
      \n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/registration_form.html": -/*!**********************************************!*\ - !*** ./src/templates/registration_form.html ***! - \**********************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n' + -__e(o.__("Account Registration:")) + -' ' + -__e(o.domain) + -'\n

      ' + -__e(o.title) + -'

      \n

      ' + -__e(o.instructions) + -'

      \n\n\n
      \n \n '; - if (!o.registration_domain) { ; -__p += '\n \n '; - } ; -__p += '\n
      \n

      ' + -__e( o.__("Already have a chat account?") ) + -'

      \n

      \n
      \n
      \n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/registration_request.html": -/*!*************************************************!*\ - !*** ./src/templates/registration_request.html ***! - \*************************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n\n

      ' + -__e(o.__("Hold tight, we're fetching the registration form…")) + -'

      \n'; - if (o.cancel) { ; -__p += '\n \n'; - } ; -__p += '\n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/requesting_contact.html": -/*!***********************************************!*\ - !*** ./src/templates/requesting_contact.html ***! - \***********************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n'; - if (o.allow_chat_pending_contacts) { ; -__p += '\n\n'; - } ; -__p += '\n' + -__e(o.display_name) + -'\n'; - if (o.allow_chat_pending_contacts) { ; -__p += '\n\n'; - } ; -__p += '\n\n\n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/room_description.html": -/*!*********************************************!*\ - !*** ./src/templates/room_description.html ***! - \*********************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n\n
      \n

      ' + -__e(o.label_jid) + -' ' + -__e(o.jid) + -'

      \n

      ' + -__e(o.label_desc) + -' ' + -__e(o.desc) + -'

      \n

      ' + -__e(o.label_occ) + -' ' + -__e(o.occ) + -'

      \n

      ' + -__e(o.label_features) + -'\n

        \n '; - if (o.passwordprotected) { ; -__p += '\n
      • ' + -__e(o.label_requires_auth) + -'
      • \n '; - } ; -__p += '\n '; - if (o.hidden) { ; -__p += '\n
      • ' + -__e(o.label_hidden) + -'
      • \n '; - } ; -__p += '\n '; - if (o.membersonly) { ; -__p += '\n
      • ' + -__e(o.label_requires_invite) + -'
      • \n '; - } ; -__p += '\n '; - if (o.moderated) { ; -__p += '\n
      • ' + -__e(o.label_moderated) + -'
      • \n '; - } ; -__p += '\n '; - if (o.nonanonymous) { ; -__p += '\n
      • ' + -__e(o.label_non_anon) + -'
      • \n '; - } ; -__p += '\n '; - if (o.open) { ; -__p += '\n
      • ' + -__e(o.label_open_room) + -'
      • \n '; - } ; -__p += '\n '; - if (o.persistent) { ; -__p += '\n
      • ' + -__e(o.label_permanent_room) + -'
      • \n '; - } ; -__p += '\n '; - if (o.publicroom) { ; -__p += '\n
      • ' + -__e(o.label_public) + -'
      • \n '; - } ; -__p += '\n '; - if (o.semianonymous) { ; -__p += '\n
      • ' + -__e(o.label_semi_anon) + -'
      • \n '; - } ; -__p += '\n '; - if (o.temporary) { ; -__p += '\n
      • ' + -__e(o.label_temp_room) + -'
      • \n '; - } ; -__p += '\n '; - if (o.unmoderated) { ; -__p += '\n
      • ' + -__e(o.label_unmoderated) + -'
      • \n '; - } ; -__p += '\n
      \n

      \n
      \n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/room_item.html": -/*!**************************************!*\ - !*** ./src/templates/room_item.html ***! - \**************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape; -__p += '\n
    • \n \n
    • \n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/room_panel.html": -/*!***************************************!*\ - !*** ./src/templates/room_panel.html ***! - \***************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape; -__p += '\n\n
      \n ' + -__e(o.heading_chatrooms) + -'\n \n \n
      \n
      \n
      \n\n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/rooms_list.html": -/*!***************************************!*\ - !*** ./src/templates/rooms_list.html ***! - \***************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n\n \n ' + -__e(o.label_rooms) + -'\n
      \n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/rooms_list_item.html": -/*!********************************************!*\ - !*** ./src/templates/rooms_list_item.html ***! - \********************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n
      \n'; - if (o.num_unread) { ; -__p += '\n ' + -__e( o.num_unread ) + -'\n'; - } ; -__p += '\n' + -__e(o.name || o.jid) + -'\n\n'; - if (o.allow_bookmarks) { ; -__p += '\n \n'; - } ; -__p += '\n\n \n\n \n\n
      \n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/rooms_results.html": -/*!******************************************!*\ - !*** ./src/templates/rooms_results.html ***! - \******************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape; -__p += '\n
    • ' + -__e( o.feedback_text ) + -'\n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/roster.html": -/*!***********************************!*\ - !*** ./src/templates/roster.html ***! - \***********************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n
      \n ' + -__e(o.heading_contacts) + -'\n '; - if (o.allow_contact_requests) { ; -__p += '\n \n '; - } ; -__p += '\n
      \n\n
      \n\n
      \n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/roster_filter.html": -/*!******************************************!*\ - !*** ./src/templates/roster_filter.html ***! - \******************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n\n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/roster_item.html": -/*!****************************************!*\ - !*** ./src/templates/roster_item.html ***! - \****************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n\n \n '; - if (o.num_unread) { ; -__p += '\n ' + -__e( o.num_unread ) + -'\n '; - } ; -__p += '\n ' + -__e(o.display_name) + -'\n'; - if (o.allow_contact_removal) { ; -__p += '\n\n'; - } ; -__p += '\n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/search_contact.html": -/*!*******************************************!*\ - !*** ./src/templates/search_contact.html ***! + !*** ./src/headless/templates/vcard.html ***! \*******************************************/ /*! no static exports found */ /***/ (function(module, exports, __webpack_require__) { @@ -80985,414 +78007,7 @@ return __p var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; module.exports = function(o) { var __t, __p = '', __e = _.escape; -__p += '\n
    • \n
      \n \n \n
      \n
    • \n'; -return __p -}; - -/***/ }), - -/***/ "./src/templates/select_option.html": -/*!******************************************!*\ - !*** ./src/templates/select_option.html ***! - \******************************************/ -/*! no static exports found */ -/***/ (function(module, exports, __webpack_require__) { - -var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./node_modules/lodash/escape.js")}; -module.exports = function(o) { -var __t, __p = '', __e = _.escape, __j = Array.prototype.join; -function print() { __p += __j.call(arguments, '') } -__p += '\n