From 897bb0719ae3b5f6ab80aa2d894fd8f133db6f6a Mon Sep 17 00:00:00 2001 From: Riandy Rahman Nugraha Date: Fri, 4 Aug 2017 09:11:29 +0700 Subject: [PATCH] =?UTF-8?q?remove=20tinode.js=20from=20example=20app=20?= =?UTF-8?q?=E2=80=94>=20tinode.js=20already=20has=20its=20own=20repo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + tinode.js | 2735 ---------------------------------------------------- 2 files changed, 1 insertion(+), 2735 deletions(-) create mode 100644 .gitignore delete mode 100755 tinode.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..ac179a9b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +tinode.js diff --git a/tinode.js b/tinode.js deleted file mode 100755 index 19f80d06..00000000 --- a/tinode.js +++ /dev/null @@ -1,2735 +0,0 @@ -/** - * @file All the logic need to connect to Tinode chat server. Tinode is a single js - * file with no dependencies. Just include tinode.js into your project. - * It will add a singleton Tinode object to the top level object, usually window. - * See https://github.com/tinode/example-react-js for real-life usage. - * - * @copyright 2015-2017 Tinode - * @summary Javascript bindings for Tinode. - * @license Apache 2.0 - * @version 0.13 - * - * @example - * - * - * - * - * - * ... - * - * - */ - -/** - * Tinode object is automatically accessible in the top scope, usually window. - * DO NOT call the constructor. It's shown here due to a - * deficiency in jsdoc. - * - * @class Tinode - */ -(function(environment) { // closure for web browsers - 'use strict'; - - // Global constants - var PROTOVERSION = "0"; - var VERSION = "0.13"; - var LIBRARY = "tinodejs/" + VERSION; - - var TOPIC_NEW = "new"; - var TOPIC_ME = "me"; - var TOPIC_FND = "fnd"; - var USER_NEW = "new"; - - // Utility functions - - // RFC3339 formater of Date - var rfc3339DateString = function(d) { - function pad(n) { - return n < 10 ? '0' + n : n; - } - return d.getUTCFullYear() + '-' + pad(d.getUTCMonth() + 1) + '-' + pad(d.getUTCDate()) + - 'T' + pad(d.getUTCHours()) + ':' + pad(d.getUTCMinutes()) + ':' + pad(d.getUTCSeconds()) + 'Z'; - } - - // Copy src's own properties to dst. - var mergeObj = function(dst, src) { - for (var prop in src) { - if (src.hasOwnProperty(prop) && src[prop]) { - dst[prop] = src[prop]; - } - } - } - - // Update object stored in a cache. Returns updated value. - var mergeToCache = function(cache, key, newval) { - var oldval = cache[key]; - if (oldval) { - mergeObj(oldval, newval); - } else { - cache[key] = newval; - oldval = newval; - } - - return oldval; - } - - // Basic cross-domain requester. Supports normal browsers and IE8+ - var xdreq = function() { - var xdreq = null; - - // Detect browser support for CORS - if ('withCredentials' in new XMLHttpRequest()) { - // Support for standard cross-domain requests - xdreq = new XMLHttpRequest(); - } else if (typeof XDomainRequest !== "undefined") { - // IE-specific "CORS" with XDR - xdreq = new XDomainRequest(); - } else { - // Browser without CORS support, don't know how to handle - throw new Error("browser not supported"); - } - - return xdreq; - } - - // JSON stringify helper - pre-processor for JSON.stringify - var jsonBuildHelper = function(key, val) { - // strip out empty elements while serializing objects to JSON - if (val === undefined || val === null || val === false || val.length === 0 || - ((typeof val === "object") && (Object.keys(val).length === 0))) { - return undefined; - // Convert javascript Date objects to rfc3339 strings - } else if (val instanceof Date) { - val = rfc3339DateString(val); - } - return val; - } - - // Attempt to convert date strings to objects. - var jsonParseHelper = function(key, val) { - // Convert string timestamps with optional milliseconds to Date - // 2015-09-02T01:45:43[.123]Z - if (key === 'ts' && typeof val === 'string' && - val.length >= 20 && val.length <= 24) { - var date = new Date(val); - if (date) { - return date; - } - } - return val; - }; - - // Trims very long strings (encoded images) to make logged packets more readable. - var jsonLoggerHelper = function(key, val) { - if (typeof val === 'string' && val.length > 128) { - return "<" + val.length + ", bytes: " + val.substring(0, 12) + '...' + val.substring(val.length-12) + ">"; - } - return jsonBuildHelper(key, val); - }; - - /** - * Circular buffer: holds at most 'size' objects. Once the limit is reached, - * the new object overwrites the oldest one. - * - * @class CBuffer - * @memberof Tinode - * @protected - * - * @param {number} size - Maximum number of elements to hold in the buffer. - */ - var CBuffer = function(size) { - var base = 0, - buffer = [], - contains = 0; - - return { - /** - * Get an element at the given position. - * @memberof Tinode.CBuffer# - * @param {number} position - Position to fetch from. - * @returns {Object} Element at the given position or undefined - */ - getAt: function(at) { - if (at >= contains || at < 0) return undefined; - return buffer[(at + base) % size]; - }, - - /** Add new element(s) to the buffer. Variadic: takes one or more arguments. If an array is passed as a single - * argument, its elements are inserted individually. - * @memberof Tinode.CBuffer# - * - * @param {(Array|...Object)} - One or more objects to insert. - */ - put: function() { - var insert; - // inspect arguments: if array, insert its elements, if one or more arguments, insert them one by one - if (arguments.length === 1 && Array.isArray(arguments[0])) { - insert = arguments[0]; - } else { - insert = arguments; - } - for (var idx in insert) { - buffer[(base + contains) % size] = insert[idx]; - contains += 1; - if (contains > size) { - contains = size; - base = (base + 1) % size; - } - } - }, - - /** - * Return the maximum number of element the buffer can hold - * @memberof Tinode.CBuffer# - * @return {number} The size of the buffer. - */ - size: function() { - return size; - }, - - /** - * Return the number of elements the buffer holds - * @memberof Tinode.CBuffer# - * @returns {number} Number of elements in the buffer. - */ - contains: function() { - return contains; - }, - - /** - * Discard all elements and reset the buffer to the new size (maximum number of elements). - * @memberof Tinode.CBuffer# - * @param {number} newSize - New size of the buffer. - */ - reset: function(newSize) { - if (newSize) { - size = newSize; - } - base = 0; - contains = 0; - }, - - /** - * Callback for iterating contents of buffer. See {@link Tinode.CBuffer#forEach}. - * @callback ForEachCallbackType - * @memberof Tinode.CBuffer# - * @param {Object} elem - Element of the buffer. - * @param {number} index - Index of the current element. - */ - - /** - * Apply given function `callback` to all elements of the buffer. - * @memberof Tinode.CBuffer# - * - * @param {ForEachCallbackType} callback - Function to call for each element. - * @param {Object} context - calling context (i.e. value of 'this' in callback) - */ - forEach: function(callback, context) { - for (var i = 0; i < contains; i++) { - callback.call(context, buffer[(i + base) % size], i); - } - } - } - } - - // Helper function for creating an endpoint URL - function makeBaseUrl(host, protocol, apiKey) { - var url = null; - - if (protocol === 'http' || protocol === 'https' || protocol === 'ws' || protocol === 'wss') { - url = protocol + '://'; - url += host; - if (url.charAt(url.length - 1) !== '/') { - url += '/'; - } - url += "v" + PROTOVERSION + "/channels"; - if (protocol === "http" || protocol === "https") { - // Long polling endpoint end with "lp", i.e. - // '/v0/channels/lp' vs just '/v0/channels' - url += "/lp"; - } - url += "?apikey=" + apiKey; - } - - return url; - } - - /** - * An abstraction for a websocket or a long polling connection. - * - * @class Connection - * @memberof Tinode - * @protected - * - * @param {string} transport_ - network transport to use, either `ws`/`wss` for websocket or `lp` for long polling. - * @returns a connection object. - */ - var Connection = (function(transport_, autoreconnect_) { - var instance; - - var host; - var secure; - var apiKey; - - var autoreconnect = autoreconnect_; - - // Settings for exponential backoff - const _BOFF_BASE = 2000; // 2000 milliseconds, minimum delay between reconnects - const _BOFF_MAX_ITER = 10; // Maximum delay between reconnects 2^10 * 2000 ~ 34 minutes - const _BOFF_JITTER = 0.3; // Add random delay - var _boffTimer = null; - var _boffIteration = 0; - var _boffClosed = false; // Indicator if the socket was manually closed - don't autoreconnect if true. - - function log(text) { - if (instance.logger) { - instance.logger(text); - } - } - - // Reconnect after a timeout. - function reconnect() { - // Clear timer - window.clearTimeout(_boffTimer); - // Calculate when to fire the reconnect attempt - var timeout = _BOFF_BASE * (Math.pow(2, _boffIteration) * (1.0 +_BOFF_JITTER * Math.random())); - // Update iteration counter for future use - _boffIteration = (_boffIteration >= _BOFF_MAX_ITER ? _boffIteration : _boffIteration + 1); - _boffTimer = setTimeout(function() { - console.log("Reconnecting, iter=" + _boffIteration + ", timeout=" + timeout); - // Maybe the socket was closed while we waited for the timer? - if (!_boffClosed) { - instance.connect().catch(function(){/* do nothing */}); - } - }, timeout); - } - - // Initialization for Websocket - function init_ws() { - var _socket = null; - - return { - /** - * Initiate a new connection - * @memberof Tinode.Connection# - * @return {Promise} Promise resolved/rejected when the connection call completes, - resolution is called without parameters, rejection passes the {Error} as parameter. - */ - connect: function(host_) { - if (_socket && _socket.readyState === 1) { - return Promise.resolve(); - } - - if (host_) { - host = host_; - } - - return new Promise(function(resolve, reject) { - var url = makeBaseUrl(host, secure ? "wss" : "ws", apiKey); - - log("Connecting to: " + url); - - var conn = new WebSocket(url); - - conn.onopen = function(evt) { - _boffClosed = false; - - if (instance.onOpen) { - instance.onOpen(); - } - resolve(); - - if (autoreconnect) { - window.clearTimeout(_boffTimer); - _boffTimer = null; - _boffIteration = 0; - } - } - - conn.onclose = function(evt) { - _socket = null; - - if (instance.onDisconnect) { - instance.onDisconnect(null); - } - - if (!_boffClosed && autoreconnect) { - reconnect(); - } - } - - conn.onerror = function(err) { - reject(err); - } - - conn.onmessage = function(evt) { - if (instance.onMessage) { - instance.onMessage(evt.data); - } - } - _socket = conn; - }); - }, - - /** Terminate the network connection - * @memberof Tinode.Connection# - */ - disconnect: function() { - if (_socket) { - _boffClosed = true; - _socket.close(); - } - _socket = null; - }, - - /** - * Send a string to the server. - * @memberof Tinode.Connection# - * - * @param {string} msg - String to send. - * @throws Throws an exception if the underlying connection is not live. - */ - sendText: function(msg) { - if (_socket && (_socket.readyState == _socket.OPEN)) { - _socket.send(msg); - } else { - throw new Error("Websocket is not connected"); - } - }, - - /** - * Check if socket is alive. - * @memberof Tinode.Connection# - * @returns {boolean} true if connection is live, false otherwise - */ - isConnected: function() { - return (_socket && (_socket.readyState === 1)); - } - } - } - - // Initialization for long polling. - function init_lp() { - // Fully composed endpoint URL, with API key & SID - var _lpURL = null; - - var _poller = null; - var _sender = null; - - function lp_sender(url_) { - var sender = xdreq(); - sender.open('POST', url_, true); - - sender.onreadystatechange = function(evt) { - if (sender.readyState == 4 && sender.status >= 400) { - // Some sort of error response - throw new Error("LP sender failed, " + sender.status); - } - } - - return sender; - } - - function lp_poller(url_, resolve, reject) { - var poller = xdreq(); - poller.open('GET', url_, true); - - poller.onreadystatechange = function(evt) { - if (poller.readyState == 4) { // 4 == DONE - console.log("got response: " + poller.responseText); - if (poller.status == 201) { // 201 == HTTP.Created, get SID - var pkt = JSON.parse(poller.responseText); - var text = poller.responseText; - - _lpURL = url_ + "&sid=" + pkt.ctrl.params.sid - poller = lp_poller(_lpURL); - poller.send(null) - if (resolve) { - resolve(); - } - if (instance.onMessage) { - onMessage(text); - } - } else if (poller.status == 200) { // 200 = HTTP.OK - if (instance.onMessage) { - instance.onMessage(poller.responseText) - } - poller = lp_poller(_lpURL); - poller.send(null); - } else { - // Don't throw an error here, gracefully handle server errors - if (reject) { - reject(poller.responseText); - } - if (instance.onMessage) { - instance.onMessage(poller.responseText); - } - if (instance.onDisconnect) { - instance.onDisconnect(new Error("" + poller.status + " " + poller.responseText)); - } - } - } - } - - return poller; - } - - return { - connect: function(host_) { - if (host_) { - host = host_; - } - - return new Promise(function(resolve, reject){ - var url = makeBaseUrl(host, secure ? "https" : "http", apiKey); - log("Connecting to: " + url); - _poller = lp_poller(url, resolve, reject); - _poller.send(null) - }).catch(function() { - - }); - }, - disconnect: function() { - if (_sender) { - _sender.abort(); - _sender = null; - } - if (_poller) { - _poller.abort(); - _poller = null; - } - if (instance.onDisconnect) { - instance.onDisconnect(null); - } - // Ensure it's reconstructed - _lpURL = null; - }, - sendText: function(msg) { - _sender = lp_sender(_lpURL); - if (_sender && (_sender.readyState == 1)) { // 1 == OPENED - _sender.send(msg); - } else { - throw new Error("Long poller failed to connect"); - } - }, - isConnected: function() { - return (_poller && true); - } - }; - } - - if (transport_ === "lp") { - // explicit request to use long polling - instance = init_lp(); - } else if (transport_ === "ws") { - // explicit request to use web socket - // if websockets are not available, horrible things will happen - instance = init_ws(); - } else { - // Default transport selection - if (!window["WebSocket"]) { - // The browser has no websockets - instance = init_lp(); - } else { - // Using web sockets -- default - instance = init_ws(); - } - } - - instance.setup = function(host_, secure_, apiKey_) { - host = host_; - secure = secure_; - apiKey = apiKey_; - }; - - // Callbacks: - /** - * A callback to pass incoming messages to. See {@link Tinode.Connection#onMessage}. - * @callback Tinode.Connection.OnMessage - * @memberof Tinode.Connection - * @param {string} message - Message to process. - */ - /** - * A callback to pass incoming messages to. - * @type {Tinode.Connection.OnMessage} - * @memberof Tinode.Connection# - */ - instance.onMessage = undefined; - - /** - * A callback for reporting a dropped connection. - * @type {function} - * @memberof Tinode.Connection# - */ - instance.onDisconnect = undefined; - - /** - * A callback called when the connection is ready to be used for sending. For websockets it's socket open, - * for long polling it's readyState=1 (OPENED) - * @type {function} - * @memberof Tinode.Connection# - */ - instance.onOpen = undefined; - - /** - * A callback to log events from Connection. See {@link Tinode.Connection#logger}. - * @callback LoggerCallbackType - * @memberof Tinode.Connection - * @param {string} event - Event to log. - */ - /** - * A callback to report logging events. - * @memberof Tinode.Connection# - * @type {Tinode.Connection.LoggerCallbackType} - */ - instance.logger = undefined; - - return instance; - }); - - - // Core Tinode functionality. - var Tinode = (function() { - var instance; - - // Initialize Tinode instance - function init() { - // Private variables - - // Client-provided application name, format / - var _appName = "Undefined"; - var _platform = navigator.platform; - // Logging to console enabled - var _loggingEnabled = false; - // When logging, trip long strings (base64-encoded images) for readability - var _trimLongStrings = false; - // A connection object, see Connection above. - var _connection = null; - // UID of the currently authenticated user - var _myUID = null; - // Login used in the last successful basic authentication - var _login = null; - // Token which can be used for login instead of login/password. - var _loginToken = null; - // Counter of received packets - var _inPacketCount = 0; - // Counter for generating unique message IDs - var _messageId = 0; - - // Information about the server, if connected - var _serverInfo = null; - - // Generic cache, currently used for topics/users - var _cache = {}; - // Cache of pending promises - var _pendingPromises = {}; - - // Private methods - - // Console logger - function log(str) { - if (_loggingEnabled) { - var d = new Date() - var dateString = ('0' + d.getUTCHours()).slice(-2) + ':' + - ('0' + d.getUTCMinutes()).slice(-2) + ':' + - ('0' + d.getUTCSeconds()).slice(-2) + ':' + - ('0' + d.getUTCMilliseconds()).slice(-3); - - console.log('[' + dateString + '] ' + str); - } - } - - // Access to Tinode's cache of objects - function cachePut(type, name, obj) { - _cache[type + ":" + name] = obj; - } - - function cacheGet(type, name) { - return _cache[type + ":" + name]; - } - - function cacheDel(type, name) { - delete _cache[type + ":" + name]; - } - // Enumerate all items in cache, call func for each item. - // Enumeration stops if func returns true. - function cacheMap(func, context) { - for (var idx in _cache) { - if (func(_cache[idx], idx, context)) { - break; - } - } - } - - // Make limited cache management available to topic. - function attachCacheToTopic(topic) { - topic._cacheGetUser = function(uid) { - return cacheGet("user", uid); - }; - topic._cachePutUser = function(uid, user) { - return cachePut("user", uid, user); - }; - topic._cacheDelUser = function(uid) { - return cacheDel("user", uid); - }; - topic._cachePutSelf = function() { - return cachePut("topic", topic.name, topic); - } - topic._cacheDelSelf = function() { - return cacheDel("topic", topic.name); - } - } - - // Resolve or reject a pending promise. - // Unresolved promises are stored in _pendingPromises. - function execPromise(id, code, onOK, errorText) { - var callbacks = _pendingPromises[id]; - if (callbacks) { - delete _pendingPromises[id]; - if (code >= 200 && code < 400) { - if (callbacks.resolve) { - callbacks.resolve(onOK); - } - } else if (callbacks.reject) { - callbacks.reject(new Error("" + code + " " + errorText)); - } - } - } - - // Generator of default promises for sent packets - var makePromise = function(id) { - var promise = null; - if (id) { - var promise = new Promise(function(resolve, reject) { - // Stored callbacks will be called when the response packet with this Id arrives - _pendingPromises[id] = { - "resolve": resolve, - "reject": reject - }; - }) - } - return promise; - } - - // Generates unique message IDs - function getNextMessageId() { - return (_messageId != 0) ? '' + _messageId++ : undefined; - } - - // Get User Agent string - function getUserAgent() { - return _appName + " (" + _platform + ") " + LIBRARY; - } - - // Generator of packets stubs - function initPacket(type, topic) { - var pkt = null; - switch (type) { - case "hi": - return { - "hi": { - "id": getNextMessageId(), - "ver": VERSION, - "ua": getUserAgent(), - } - }; - - case "acc": - return { - "acc": { - "id": getNextMessageId(), - "user": null, - "scheme": null, - "secret": null, - "login": false, - "desc": {} - } - }; - - case "login": - return { - "login": { - "id": getNextMessageId(), - "scheme": null, - "secret": null - } - }; - - case "sub": - return { - "sub": { - "id": getNextMessageId(), - "topic": topic, - "set": {}, - "get": {} - } - }; - - case "leave": - return { - "leave": { - "id": getNextMessageId(), - "topic": topic, - "unsub": false - } - }; - - case "pub": - return { - "pub": { - "id": getNextMessageId(), - "topic": topic, - "params": {}, - "content": {} - } - }; - - case "get": - return { - "get": { - "id": getNextMessageId(), - "topic": topic, - "what": null, // data, sub, desc, space separated list; unknown strings are ignored - "desc": {}, - "sub": {}, - "data": {} - } - }; - - case "set": - return { - "set": { - "id": getNextMessageId(), - "topic": topic, - "desc": {}, - "sub": {} - } - }; - - case "del": - return { - "del": { - "id": getNextMessageId(), - "topic": topic - } - }; - - case "note": - return { - "note": { - // no id by design - "topic": topic, - "what": null, // one of "recv", "read", "kp" - "seq": undefined // the server-side message id aknowledged as received or read - } - }; - - default: - throw new Error("Unknown packet type requested: " + type); - } - } - - // Send a packet. - function sendBasic(pkt) { - var msg = JSON.stringify(pkt, jsonBuildHelper); - log("out: " + (_trimLongStrings ? JSON.stringify(pkt, jsonLoggerHelper) : msg)); - _connection.sendText(msg); - } - - // Send a packet returning a promise. - function sendWithPromise(pkt, id) { - var promise = makePromise(id); - var msg = JSON.stringify(pkt, jsonBuildHelper); - log("out: " + (_trimLongStrings ? JSON.stringify(pkt, jsonLoggerHelper) : msg)); - _connection.sendText(msg); - return promise; - } - - // The main message dispatcher. - function dispatchMessage(data) { - // Skip empty response. This happens when LP times out. - if (!data) return; - - _inPacketCount++; - - // Send raw message to listener - if (instance.onRawMessage) { - instance.onRawMessage(data); - } - - var pkt = JSON.parse(data, jsonParseHelper); - if (!pkt) { - log("in: " + data); - log("ERROR: failed to parse data"); - } else { - log("in: " + (_trimLongStrings ? JSON.stringify(pkt, jsonLoggerHelper) : data)); - - // Send complete packet to listener - if (instance.onMessage) { - instance.onMessage(pkt); - } - - if (pkt.ctrl) { - // Handling {ctrl} message - if (instance.onCtrlMessage) { - instance.onCtrlMessage(pkt.ctrl); - } - - // Resolve or reject a pending promise, if any - if (pkt.ctrl.id) { - execPromise(pkt.ctrl.id, pkt.ctrl.code, pkt.ctrl, pkt.ctrl.text); - } - } else if (pkt.meta) { - // Handling a {meta} message. - - // Preferred API: Route meta to topic, if one is registered - var topic = cacheGet("topic", pkt.meta.topic); - if (topic) { - topic._routeMeta(pkt.meta); - } - - // Secondary API: callback - if (instance.onMetaMessage) { - instance.onMetaMessage(pkt.meta); - } - } else if (pkt.data) { - // Handling {data} message - - // Preferred API: Route data to topic, if one is registered - var topic = cacheGet("topic", pkt.data.topic); - if (topic) { - topic._routeData(pkt.data); - } - - // Secondary API: Call callback - if (instance.onDataMessage) { - instance.onDataMessage(pkt.data); - } - } else if (pkt.pres) { - // Handling {pres} message - - // Preferred API: Route presence to topic, if one is registered - var topic = cacheGet("topic", pkt.pres.topic); - if (topic) { - topic._routePres(pkt.pres); - } - - // Secondary API - callback - if (instance.onPresMessage) { - instance.onPresMessage(pkt.pres); - } - } else if (pkt.info) { - // {info} message - read/received notifications and key presses - - // Preferred API: Route {info}} to topic, if one is registered - var topic = cacheGet("topic", pkt.info.topic); - if (topic) { - topic._routeInfo(pkt.info); - } - - // Secondary API - callback - if (instance.onInfoMessage) { - instance.onInfoMessage(pkt.info); - } - } else { - log("ERROR: Unknown packet received."); - } - } - } - - // Helper function to construct {get} and {sub.get} packet. - function parseGetParams(params) { - var what = []; - var result; - - if (params) { - result = {}; - ["data", "sub", "desc"].map(function(key) { - if (params.hasOwnProperty(key)) { - what.push(key); - result[key] = params[key]; - } - }); - } - - if (what.length > 0) { - result.what = what.join(" "); - } else { - result = undefined; - } - - return result; - } - - function handleReadyToSend() { - instance.hello(); - } - - function handleDisconnect(err) { - _inPacketCount = 0; - _myUID = null; - _serverInfo = null; - - cacheMap(function(obj, key) { - if (key.lastIndexOf("topic:", 0) === 0) { - obj._resetSub(); - } - }); - - if (instance.onDisconnect) { - instance.onDisconnect(err); - } - } - - function loginSuccessful(ctrl) { - // This is a response to a successful login, - // extract UID and security token, save it in Tinode module - _myUID = ctrl.params.user; - if (ctrl.params && ctrl.params.token && ctrl.params.expires) { - _loginToken = { - token: ctrl.params.token, - expires: new Date(ctrl.params.expires) - }; - } else { - _loginToken = null; - } - - if (instance.onLogin) { - instance.onLogin(ctrl.code, ctrl.text); - } - } - // Returning an initialized instance with public methods; - return { - - /** Instance configuration. Can be calle dmultiple times. - * @memberof Tinode# - * - * @param {string} appname - Name of the caliing application to be reported in User Agent. - * @param {string} host - Host name and port number to connect to. - * @param {string} apiKey - API key generated by keygen - * @param {string} transport - See {@link Tinode.Connection#transport}. - */ - setup: function(appname_, host_, apiKey_, transport_) { - // Initialize with a random id each time, to avoid confusing with a packet - // from a previous session. - _messageId = Math.floor((Math.random() * 0xFFFF) + 0xFFFF); - - if (appname_) { - _appName = appname_; - } - - if (_connection) { - _connection.disconnect(); - } - - _connection = Connection(transport_, true); - _connection.logger = log; - _connection.onMessage = dispatchMessage; - _connection.onDisconnect = handleDisconnect; - _connection.onOpen = handleReadyToSend; - _connection.setup(host_, false, apiKey_); - }, - - /** - * Connect to the server. - * @memberof Tinode# - * - * @return {Promise} Promise resolved/rejected when the connection call completes: - * resolve() is called without parameters, reject() receives the Error as a single parameter. - */ - connect: function(host_) { - return _connection.connect(host_); - }, - - /** - * Disconnect from the server. - * @memberof Tinode# - */ - disconnect: function() { - if (_connection) { - _connection.disconnect(); - } - }, - - /** - * Check for live connection to server - * @memberof Tinode# - * @returns {boolean} true if there is a live connection, false otherwise. - */ - isConnected: function() { - return _connection && _connection.isConnected(); - }, - - /** - * @typedef AccountCreationParams - * @memberof Tinode - * @type Object - * @property {string} login - Name of the scheme to use to authenticate the session with the newly created account. - * @property {Tinode.DefAcs=} defacs - Default access parameters for user's me topic. - * @property {Object=} public - Public application-defined data exposed on me topic. - * @property {Object=} private - Private application-defined data accessible on me topic. - */ - /** - * @typedef DefAcs - * @memberof Tinode - * @type Object - * @property {string=} auth - Access mode for me for authenticated users. - * @property {string=} anon - Access mode for me anonymous users. - */ - - /** - * Create or update an account. - * @memberof Tinode# - * - * @param {string} uid - User id to update - * @param {string} scheme - Authentication scheme; "basic" is the only currently supported scheme. - * @param {string} secret - Authentication secret, assumed to be already base64 encoded. - * @param {boolean} login - Use new account to authenticate current session - * @param {Tinode.AccountCreationParams} params - User data to pass to the server. - */ - account: function(uid, scheme, secret, login, params) { - var pkt = initPacket("acc"); - pkt.acc.user = uid; - pkt.acc.scheme = scheme; - pkt.acc.secret = secret; - // Log in to the new account using selected scheme - pkt.acc.login = login; - - if (params) { - pkt.acc.desc.defacs = params.defacs; - pkt.acc.desc.public = params.public; - pkt.acc.desc.private = params.private; - pkt.acc.tags = params.tags; - } - - return sendWithPromise(pkt, pkt.acc.id); - }, - - /** - * Create a new user. - * @memberof Tinode# - * - * @param {string} scheme - Authentication scheme; "basic" is the only currently supported scheme. - * @param {string} secret - Authentication. - * @param {boolean} login - Use new account to authenticate current session - * @param {Tinode.AccountCreationParams} params - User data to pass to the server. - */ - createAccount: function(scheme, secret, login, params) { - var promise = instance.account(USER_NEW, scheme, secret, login, params); - if (login) { - promise = promise.then(function(ctrl) { - loginSuccessful(ctrl); - }); - } - return promise; - }, - - /** - * Create user with 'basic' authentication scheme and immediately - * use it for authentication. - * - * @memberof Tinode# - */ - createAccountBasic: function(username, password, params) { - return instance.createAccount("basic", btoa(username + ":" + password), true, params); - }, - - /** - * Update user's credentials for 'basic' authentication scheme. - * - * @memberof Tinode# - */ - updateAccountBasic: function(uid, username, password) { - return instance.account(uid, "basic", btoa(username + ":" + password), false, null); - }, - - /** - * Send handshake to the server. - * @memberof Tinode# - * - * @returns {Promise} Promise which will be resolved/rejected when server reply is received. - */ - hello: function() { - var pkt = initPacket("hi"); - - return sendWithPromise(pkt, pkt.hi.id) - .then(function(ctrl) { - // Server response contains server protocol version, build, - // and session ID for long polling. Save them. - if (ctrl.params) { - _serverInfo = ctrl.params; - } - - if (instance.onConnect) { - instance.onConnect(); - } - }).catch(function(err) { - if (instance.onDisconnect) { - instance.onDisconnect(err); - } - }); - }, - - /** - * Authenticate current session. - * @memberof Tinode# - * - * @param {string} scheme - Authentication scheme; "basic" is the only currently supported scheme. - * @param {string} secret - Authentication secret, assumed to be already base64 encoded. - * @returns {Promise} Promise which will be resolved/rejected when server reply is received. - */ - login: function(scheme, secret) { - var pkt = initPacket("login"); - pkt.login.scheme = scheme; - pkt.login.secret = secret; - - return sendWithPromise(pkt, pkt.login.id) - .then(function(ctrl) { - loginSuccessful(ctrl); - }); - }, - - /** - * Wrapper for {@link Tinode#login} with basic authentication - * @memberof Tinode# - * - * @param {string} uname - User name. - * @param {string} password - Password. - * @returns {Promise} Promise which will be resolved/rejected on receiving server reply. - */ - loginBasic: function(uname, password) { - return instance.login("basic", btoa(uname + ":" + password)) - .then(function(ctrl) { - _login = uname; - return ctrl; - }); - }, - - /** - * Wrapper for {@link Tinode#login} with token authentication - * @memberof Tinode# - * - * @param {string} token - Token received in response to earlier login. - * @returns {Promise} Promise which will be resolved/rejected on receiving server reply. - */ - loginToken: function(token) { - return instance.login("token", token); - }, - - getLoginToken: function() { - if (_loginToken && (_loginToken.expires.getTime() > Date.now())) { - return _loginToken; - } else { - _loginToken = null; - } - return null; - }, - /** - * @typedef SetParams - * @memberof Tinode - * @property {Tinode.SetDesc=} desc - Topic initialization parameters when creating a new topic or a new subscription. - * @property {Tinode.SetSub=} sub - Subscription initialization parameters. - */ - /** - * @typedef SetDesc - * @memberof Tinode - * @property {Tinode.DefAcs=} defacs - Default access mode. - * @property {Object=} public - Free-form topic description, publically accessible. - * @property {Object=} private - Free-form topic descriptionaccessible only to the owner. - */ - /** - * @typedef SetSub - * @memberof Tinode - * @property {string=} user - UID of the user affected by the request. Default (empty) - current user. - * @property {string=} mode - User access mode, either requested or assigned dependent on context. - * @property {Object=} info - Free-form payload to pass to the invited user or topic manager. - */ - /** - * Parameters passed to {@link Tinode#subscribe} - * @typedef SubscriptionParams - * @memberof Tinode - * @property {Tinode.SetParams=} set - Parameters used to initialize topic - * @property {Tinode.GetQuery=} get - Query for fetching data from topic. - */ - - /** - * Send a topic subscription request. - * @memberof Tinode# - * - * @param {string} topic - Name of the topic to subscribe to. - * @param {Tinode.SubscriptionParams=} params - Optional subscription parameters: - * @returns {Promise} Promise which will be resolved/rejected on receiving server reply. - */ - subscribe: function(topicName, params) { - var pkt = initPacket("sub", topicName) - if (!topicName) { - topicName = TOPIC_NEW; - } - - if (params) { - pkt.sub.get = parseGetParams(params.get); - - if (params.set) { - if (params.set.sub) { - pkt.sub.set.sub = params.set.sub; - } - - if (topicName === TOPIC_NEW) { - // set.desc params are used for new topics only - if (params.set.desc) { - pkt.sub.set.desc = params.set.desc - } - } else { - // browse makes sense only in context of an existing topic - if (params.set.data) { - pkt.sub.set.data = params.set.data; - } - } - } - } - - return sendWithPromise(pkt, pkt.sub.id); - }, - - /** - * Detach and optionally unsubscribe from the topic - * @memberof Tinode# - * - * @param {string} topic - Topic to detach from. - * @param {boolean} unsub - If true, detach and unsubscribe, otherwise just detach. - * @returns {Promise} Promise which will be resolved/rejected on receiving server reply. - */ - leave: function(topic, unsub) { - var pkt = initPacket("leave", topic); - pkt.leave.unsub = unsub; - - return sendWithPromise(pkt, pkt.leave.id); - }, - - /** - * Publish {data} message to topic. - * @memberof Tinode# - * - * @param {string} topic - Name of the topic to publish to. - * @param {Object} data - Payload to publish. - * @param {boolean} noEcho - If true, do not echo the message to the original session. - * @returns {Promise} Promise which will be resolved/rejected on receiving server reply. - */ - publish: function(topic, data, noEcho) { - var pkt = initPacket("pub", topic); - pkt.pub.noecho = noEcho; - pkt.pub.content = data; - - return sendWithPromise(pkt, pkt.pub.id); - }, - - /** - * @typedef GetQuery - * @type Object - * @memberof Tinode - * @property {Tinode.GetOptsType} desc - If provided (even if empty), fetch topic description. - * @property {Tinode.GetOptsType} sub - If provided (even if empty), fetch topic subscriptions. - * @property {Tinode.GetDataType} data - If provided (even if empty), get messages. - */ - - /** - * @typedef GetOptsType - * @type Object - * @memberof Tinode - * @property {Date} ims - "If modified since", fetch data only it was was modified since stated date. - * @property {number} limit - Maximum number of results to return. Ignored when querying topic description. - */ - - /** - * @typedef GetDataType - * @type Object - * @memberof Tinode - * @property {number} since - Load messages with seq id equal or greater than this value. - * @property {number} before - Load messages with seq id lower than this number. - * @property {number} limit - Maximum number of results to return. - */ - - /** - * Request topic metadata - * @memberof Tinode# - * - * @param {string} topic - Name of the topic to query. - * @param {Tinode.GetQuery} params - Parameters of the query. - * @returns {Promise} Promise which will be resolved/rejected on receiving server reply. - */ - getMeta: function(topic, params) { - var pkt = initPacket("get", topic); - - mergeObj(pkt.get, parseGetParams(params)); - - return sendWithPromise(pkt, pkt.get.id); - }, - - /** - * Update topic's metadata: description (desc), subscribtions (sub). - * @memberof Tinode# - * - * @param {string} topic - Topic to update. - * @param {Object} desc - Update to topic description. - * @param {Object} sub - Update to a subscription. - * @returns {Promise} Promise which will be resolved/rejected on receiving server reply. - */ - setMeta: function(topic, params) { - var pkt = initPacket("set", topic); - var what = []; - - if (params) { - ["desc", "sub"].map(function(key){ - if (params.hasOwnProperty(key)) { - what.push(key); - pkt.set[key] = params[key]; - } - }); - } - - if (what.length == 0) { - throw new Error("Invalid {set} parameters."); - } - - return sendWithPromise(pkt, pkt.set.id); - }, - - /** - * Delete some or all messages in a topic. - * @memberof Tinode# - * - * @param {string} topic - Topic name to delete messages from. - * @param {Object} params - What messages to delete. - * @returns {Promise} Promise which will be resolved/rejected on receiving server reply. - */ - delMessages: function(topic, params) { - var pkt = initPacket("del", topic); - - pkt.del.what = "msg"; - pkt.del.before = params.before; - pkt.del.list = params.list; - pkt.del.hard = params.hard; - - return sendWithPromise(pkt, pkt.del.id); - }, - - /** - * Delete the topic alltogether. Requires Owner permission. - * @memberof Tinode# - * - * @param {string} topic - Name of the topic to delete - * @returns {Promise} Promise which will be resolved/rejected on receiving server reply. - */ - delTopic: function(topic) { - var pkt = initPacket("del", topic); - pkt.del.what = "topic"; - - return sendWithPromise(pkt, pkt.del.id).then(function(ctrl) { - cacheDel("topic", topic); - return ctrl; - }); - }, - - /** - * Delete subscription. Requires Share permission. - * @memberof Tinode# - * - * @param {string} topic - Name of the topic to delete - * @param {string} user - User ID to remove. - * @returns {Promise} Promise which will be resolved/rejected on receiving server reply. - */ - delSubscription: function(topic, user) { - var pkt = initPacket("del", topic); - pkt.del.what = "sub"; - pkt.del.user = user; - - return sendWithPromise(pkt, pkt.del.id); - }, - - /** - * Notify server that a message or messages were a read or received. - * @memberof Tinode# - * - * @param {string} topic - Name of the topic where the mesage is being aknowledged. - * @param {string} what - Action being aknowledged, either "read" or "recv". - * @param {number} seq - Maximum id of the message being acknowledged. - */ - note: function(topic, what, seq) { - if (seq <= 0) { - throw new Error("Invalid message id"); - } - var pkt = initPacket("note", topic); - pkt.note.what = what; - pkt.note.seq = seq; - sendBasic(pkt); - }, - - /** - * Broadcast a key-press notification to topic subscribers. Used to show - * typing notifications "user X is typing...". - * @memberof Tinode# - * - * @param {string} topic - Name of the topic to broadcast to. - */ - noteKeyPress: function(topic) { - var pkt = initPacket("note", topic); - pkt.note.what = "kp"; - sendBasic(pkt); - }, - - /** - * Get a named topic, either pull it from cache or create a new instance. - * There is a single instance of topic for each name. - * @memberof Tinode# - * - * @param {string} topic - Name of the topic to get. - * @returns {Tinode.Topic} Requested or newly created topic or undefined if topic name is invalid. - */ - getTopic: function(name) { - var topic = cacheGet("topic", name); - if (!topic && name) { - if (name === TOPIC_ME) { - topic = new TopicMe(); - } else { - topic = new Topic(name); - } - cachePut("topic", name, topic); - } - if (topic) { - attachCacheToTopic(topic); - } - return topic; - }, - - /** - * Instantiate a new unnamed topic. Name will be assigned by the server on {@link Tinode.Topic.subscribe}. - * @memberof Tinode# - * - * @param {Tinode.Callbacks} callbacks - Object with callbacks for various events. - * @returns {Tinode.Topic} Newly created topic. - */ - newTopic: function(callbacks) { - var topic = new Topic(undefined, callbacks); - attachCacheToTopic(topic); - return topic; - }, - - /** - * Instantiate a new P2P topic with a given peer. - * @memberof Tinode# - * - * @param {string} peer - UId of the peer to start topic with. - * @param {Tinode.Callbacks} callbacks - Object with callbacks for various events. - * @returns {Tinode.Topic} Newly created topic. - */ - newTopicWith: function(peer, callbacks) { - var topic = new Topic(peer, callbacks); - attachCacheToTopic(topic); - return topic; - }, - - - /** - * Instantiate 'me' topic or get it from cache. - * @memberof Tinode# - * - * @returns {Tinode.TopicMe} Instance of 'me' topic. - */ - getMeTopic: function() { - return instance.getTopic(TOPIC_ME); - }, - - /** - * Instantiate 'fnd' (find) topic or get it from cache. - * @memberof Tinode# - * - * @returns {Tinode.Topic} Instance of 'fnd' topic. - */ - getFndTopic: function() { - return instance.getTopic(TOPIC_FND); - }, - - /** - * Get the UID of the the current authenticated user. - * @memberof Tinode# - * @returns {string} UID of the current user or undefined if the session is not yet authenticated or if there is no session. - */ - getCurrentUserID: function() { - return _myUID; - }, - - /** - * Get login used for last successful authentication. - * @memberof Tinode# - * @returns {string} login last used successfully or undefined. - */ - getCurrentLogin: function() { - return _login; - }, - - /** - * Return information about the server: protocol version and build timestamp. - * @memberof Tinode# - * @returns {Object} build and version of the server or null if there is no connection or if the first server response has not been received yet. - */ - getServerInfo: function() { - return _serverInfo; - }, - - /** - * Return information about the current version of this Tinode client library. - * @memberof Tinode# - * @returns {string} current version in the MAJOR.MINOR format, e.g. '0.8'. - */ - getVersion: function() { - return VERSION; - }, - - /** - * Toggle console logging. Logging is off by default. - * @memberof Tinode# - * @param {boolean} enabled - Set to true to enable logging to console. - */ - enableLogging: function(enabled, trimLongStrings) { - _loggingEnabled = enabled; - _trimLongStrings = trimLongStrings; - }, - - /** - * Determine topic type from topic's name: grp, p2p, me, fnd. - * @memberof Tinode - * - * @param {string} name - Name of the topic to test. - * @returns {string} One of 'me', 'grp', 'p2p' or undefined. - */ - getTopicType: function(name) { - var tp = (typeof name === "string") ? name.substring(0, 3) : undefined; - if (tp === 'usr') return 'p2p'; - return (tp === "me" || tp === "fnd" || tp === "grp") ? tp : undefined; - }, - - /** - * Request server to aknowledge messages. Required for promises to function. Default "on". - * @memberof Tinode# - * - * @param {boolean} status - Turn aknowledgemens on or off. - * @deprecated - */ - wantAkn: function(status) { - if (status) { - _messageId = Math.floor((Math.random() * 0xFFFFFF) + 0xFFFFFF); - } else { - _messageId = 0; - } - }, - - // Callbacks: - /** - * Callback to report when the websocket is opened. The callback has no parameters. - * @memberof Tinode# - * @type {Tinode.onWebsocketOpen} - */ - onWebsocketOpen: undefined, - - /** - * @typedef Tinode.ServerParams - * @memberof Tinode - * @type Object - * @property {string} ver - Server version - * @property {string} build - Server build - * @property {string=} sid - Session ID, long polling connections only. - */ - - /** - * @callback Tinode.onConnect - * @param {number} code - Result code - * @param {string} text - Text epxplaining the completion, i.e "OK" or an error message. - * @param {Tinode.ServerParams} params - Parameters returned by the server. - */ - /** - * Callback to report when connection with Tinode server is established. - * @memberof Tinode# - * @type {Tinode.onConnect} - */ - onConnect: undefined, - - /** - * Callback to report when connection is lost. The callback has no parameters. - * @memberof Tinode# - * @type {Tinode.onDisconnect} - */ - onDisconnect: undefined, - - /** - * @callback Tinode.onLogin - * @param {number} code - NUmeric completion code, same as HTTP status codes. - * @param {string} text - Explanation of the completion code. - */ - /** - * Callback to report login completion. - * @memberof Tinode# - * @type {Tinode.onLogin} - */ - onLogin: undefined, - - /** - * Callback to receive {ctrl} (control) messages. - * @memberof Tinode# - * @type {Tinode.onCtrlMessage} - */ - onCtrlMessage: undefined, - - /** - * Callback to recieve {data} (content) messages. - * @memberof Tinode# - * @type {Tinode.onDataMessage} - */ - onDataMessage: undefined, - - /** - * Callback to receive {pres} (presence) messages. - * @memberof Tinode# - * @type {Tinode.onPresMessage} - */ - onPresMessage: undefined, - - /** - * Callback to receive all messages as objects. - * @memberof Tinode# - * @type {Tinode.onMessage} - */ - onMessage: undefined, - - /** - * Callback to receive all messages as unparsed text. - * @memberof Tinode# - * @type {Tinode.onRawMessage} - */ - onRawMessage: undefined - }; - } - - return { - // Get the Singleton instance if one exists or create one if it doesn't. - getInstance: function() { - if (!instance) { - instance = init(); - } - return instance; - } - }; - })(); - - var AccessMode = function(acs) { - if (acs != null) { - this.given = typeof acs.given === 'number' ? acs.given : AccessMode.decode(acs.given); - this.want = typeof acs.want === 'number' ? acs.want : AccessMode.decode(acs.want); - this.mode = typeof acs.mode === 'number' ? acs.mode : AccessMode.decode(acs.mode); - } - }; - - AccessMode._MODE_NONE = 0x00; - AccessMode._MODE_JOIN = 0x01; - AccessMode._MODE_READ = 0x02; - AccessMode._MODE_WRITE = 0x04; - AccessMode._MODE_PRES = 0x08; - AccessMode._MODE_APPROVE = 0x10; - AccessMode._MODE_SHARE = 0x20; - AccessMode._MODE_DELETE = 0x40; - AccessMode._MODE_OWNER = 0x80; - AccessMode._MODE_INVALID = 0x100000; - - /** - * Parse string into an access mode value. - * - * @memberof Tinode.AccessMode - * @static - * - * @param {string} mode - String representation of the access mode to parse. - * @returns {number} - Access mode as a numeric value. - */ - AccessMode.decode = function(str) { - if (!str) { - return null; - } else if (str === 'N' || str === 'n') { - return AccessMode._MODE_NONE; - } - - var bitmask = { - 'J': AccessMode._MODE_JOIN, - 'R': AccessMode._MODE_READ, - 'W': AccessMode._MODE_WRITE, - 'P': AccessMode._MODE_PRES, - 'A': AccessMode._MODE_APPROVE, - 'S': AccessMode._MODE_SHARE, - 'D': AccessMode._MODE_DELETE, - 'O': AccessMode._MODE_OWNER - }; - - var m0 = AccessMode._MODE_NONE; - - for (var i=0; itrue server will not echo message back to originating session. - * @returns {Promise} Promise to be resolved/rejected when the server responds to the request. - */ - publish: function(data, noEcho) { - if (!this._subscribed) { - throw new Error("Cannot publish on inactive topic"); - } - // Send data - return Tinode.getInstance().publish(this.name, data, noEcho); - }, - - /** - * Leave the topic, optionally unsibscribe. Leaving the topic means the topic will stop - * receiving updates from the server. Unsubscribing will terminate user's relationship with the topic. - * Wrapper for {@link Tinode#leave}. - * @memberof Tinode.Topic# - * - * @param {boolean} unsub - If true, unsubscribe, otherwise just leave. - * @returns {Promise} Promise to be resolved/rejected when the server responds to the request. - */ - leave: function(unsub) { - // FIXME(gene): It's possible to unsubscribe (unsub==true) from inactive topic. - if (!this._subscribed && !unsub) { - throw new Error("Cannot leave inactive topic"); - } - // Send a 'leave' message, handle async response - var topic = this; - return Tinode.getInstance().leave(this.name, unsub).then(function(ctrl) { - topic._resetSub(); - return ctrl; - }); - }, - - /** - * Request topic metadata from the server. - * @memberof Tinode.Topic# - * - * @param {Tinode.GetQuery} request parameters - * @returns {Promise} Promise to be resolved/rejected when the server responds to request. - */ - getMeta: function(params) { - if (!this._subscribed) { - throw new Error("Cannot query inactive topic"); - } - // Send {get} message, return promise. - return Tinode.getInstance().getMeta(this.name, params); - }, - - /** - * Update topic metadata. - * @memberof Tinode.Topic# - * - * @param {Object} parameters to update - * @returns {Promise} Promise to be resolved/rejected when the server responds to request. - */ - setMeta: function(params) { - var topic = this; - if (!this._subscribed) { - throw new Error("Cannot update inactive topic"); - } - - // Send Set message, handle async response. - return Tinode.getInstance().setMeta(this.name, params) - .then(function(ctrl) { - if (ctrl.params && ctrl.params.acs) { - topic.acs = new AccessMode(ctrl.params.acs); - } - return ctrl; - }); - }, - - /** - * @typedef Tinode.Topic.DelMessages - * @type Object - * @property {number} before - Delete messages with id less or equal to before. - * @property {boolean} hard - Hard-delete messages, i.e. delete from storage. Otherwise just hide them. Requires Owner permission. - */ - /** - * Delete messages. Hard-deleting messages requires Owner permission. Wrapper for {@link Tinode#delMessages}. - * @memberof Tinode.Topic# - * - * @param {Tinode.Topic.DelMessages} params - What messages to delete. - */ - delMessages: function(params) { - if (!this._subscribed) { - throw new Error("Cannot delete messages in inactive topic"); - } - // Send {del} message, return promise - return Tinode.getInstance().delMessages(this.name, params); - }, - - /** - * Delete all messages. Hard-deleting messages requires Owner permission. - * - * @memberof Tinode.Topic# - * - * @param {boolean} hardDel - true if messages should be hard-deleted. - */ - delMessagesAll: function(hardDel) { - var topic = this; - // Send {del} message, return promise - return this.delMessages({before: this._seq, hard: hardDel}) - .then(function(ctrl) { - topic._messages.reset(); - if (topic.onData) { - // Calling with no parameters to indicate the messages were deleted. - topic.onData(); - } - return ctrl; - }); - }, - - /** - * Delete all messages. Hard-deleting messages requires Owner permission. - * - * @memberof Tinode.Topic# - * - * @param {array} listToDelete - list of seq IDs to delete - * @param {boolean} hardDel - true if messages should be hard-deleted. - */ - delMessagesList: function(listToDelete, hardDel) { - var topic = this; - // Send {del} message, return promise - return this.delMessages({list: listToDelete, "hard": hardDel}) - .then(function(ctrl) { - // Remove from the buffer messages with matching ids: - // create an empty buffer and copy messages we want to to keep. - var messages = new CBuffer(100); - topic.messages(function(msg) { - if (listToDelete.indexOf(msg.seq) == -1) { - messages.put(msg); - } - }); - topic._messages = messages; - if (topic.onData) { - // Calling with no parameters to indicate that messages were deleted. - topic.onData(); - } - return ctrl; - }); - }, - - /** - * Delete topic. Requires Owner permission. Wrapper for {@link Tinode#delTopic}. - * @memberof Tinode.Topic# - * - * @returns {Promise} Promise to be resolved/rejected when the server responds to the request. - */ - delTopic: function() { - var topic = this; - var tinode = Tinode.getInstance(); - return tinode.delTopic(this.name).then(function(obj) { - topic._resetSub(); - - var me = tinode.getMeTopic(); - if (me) { - me._routePres({ - what: "gone", - topic: "me", - src: topic.name - }); - } - if (topic.onDeleteTopic) { - topic.onDeleteTopic(); - } - }); - }, - - /** - * Delete subscription. Requires Share permission. Wrapper for {@link Tinode#delSubscription}. - * @memberof Tinode.Topic# - * - * @param {String} user - ID of the user to remove subscription for. - */ - delSubscription: function(user) { - if (!this._subscribed) { - throw new Error("Cannot delete subscription in inactive topic"); - } - var topic = this; - // Send {del} message, return promise - return Tinode.getInstance().delSubscription(this.name, user).then(function(obj) { - // Remove the object from the subscription cache; - delete topic._users[user]; - }); - }, - - // Send a read/recv notification - note: function(what, seq) { - var tinode = Tinode.getInstance(); - var user = this._users[tinode.getCurrentUserID()]; - if (user) { - if (!user[what] || user[what] < seq) { - if (!this._subscribed) { - throw new Error("Cannot send notification in inactive topic"); - } - tinode.note(this.name, what, seq); - } - user[what] = seq; - } else { - console.log("note(): user not found " + tinode.getCurrentUserID()); - } - - // Update locally cached contact with the new count - var me = tinode.getMeTopic(); - if (me) { - me.setMsgReadRecv(this.name, what, seq); - } - }, - - /** - * Send a 'recv' receipt. Wrapper for {@link Tinode#noteRecv}. - * @memberof Tinode.Topic# - * - * @param {number} seq - ID of the message to aknowledge. - */ - noteRecv: function(seq) { - this.note("recv", seq); - }, - - /** - * Send a 'read' receipt. Wrapper for {@link Tinode#noteRead}. - * @memberof Tinode.Topic# - * - * @param {number} seq - ID of the message to aknowledge. - */ - noteRead: function(seq) { - this.note("read", seq); - }, - - /** - * Send a key-press notification. Wrapper for {@link Tinode#noteKeyPress}. - * @memberof Tinode.Topic# - */ - noteKeyPress: function() { - if (!this._subscribed) { - throw new Error("Cannot send notification in inactive topic"); - } - Tinode.getInstance().noteKeyPress(this.name); - }, - - // Get user description - userDesc: function(uid) { - // TODO(gene): handle asynchronous requests - - var user = this._cacheGetUser(uid); - if (user) { - return user; // Promise.resolve(user) - } - //return Tinode.getInstance().get(uid); - }, - - /** - * Iterate over cached subscribers. If callback is undefined, use this.onMetaSub. - * @memberof Tinode.Topic# - * - * @param {function} callback - Callback which will receive subscribers one by one. - * @param {Object} context - Value of `this` inside the `callback`. - */ - subscribers: function(callback, context) { - var cb = (callback || this.onMetaSub); - if (cb) { - for (var idx in this._users) { - cb.call(context, this._users[idx], idx, this._users); - } - } - }, - /** - * Iterate over cached messages. If callback is undefined, use this.onData. - * @memberof Tinode.Topic# - * - * @param {function} callback - Callback which will receive messages one by one. See {@link Tinode.CBuffer#forEach} - * @param {Object} context - Value of `this` inside the `callback`. - */ - messages: function(callback, context) { - var cb = (callback || this.onData); - if (cb) { - this._messages.forEach(cb, context); - } - }, - - // Get the number of topic subscribers who marked this message as either recv or read - // Current user is excluded from the count. - msgReceiptCount: function(what, seq) { - var count = 0; - var me = Tinode.getInstance().getCurrentUserID(); - if (seq > 0) { - for (var idx in this._users) { - var user = this._users[idx]; - if (user.user !== me && user[what] >= seq) { - count++; - } - } - } - return count; - }, - - /** - * Get the number of topic subscribers who marked this message (and all older messages) as read. - * The current user is excluded from the count. - * @memberof Tinode.Topic# - * - * @param {number} seq - Message id to check. - * @returns {number} Number of subscribers who claim to have received the message. - */ - msgReadCount: function(seq) { - return this.msgReceiptCount("read", seq); - }, - - /** - * Get the number of topic subscribers who marked this message (and all older messages) as received. - * The current user is excluded from the count. - * @memberof Tinode.Topic# - * - * @param {number} seq - Message id to check. - * @returns {number} Number of subscribers who claim to have received the message. - */ - msgRecvCount: function(seq) { - return this.msgReceiptCount("recv", seq); - }, - - /** - * Get type of the topic: me, p2p, grp, fnd... - * @memberof Tinode.Topic# - * - * @returns {string} One of 'me', 'p2p', 'grp', 'fnd' or undefined. - */ - getType: function() { - return Tinode.getInstance().getTopicType(this.name); - }, - - /** - * Get user's cumulative access mode of the topic. - * @memberof Tinode.Topic# - * - * @returns {Tinode.AccessMode} - user's access mode - */ - getAccessMode: function() { - return this.acs; - }, - - // Process data message - _routeData: function(data) { - this._lastUpdate = data.ts; - - if (Object.keys(this._users).indexOf(data.from) < 0) { - this._users[data.from] = {user: data.from}; - } - - this._messages.put(data); - - if (data.seq > this._seq) { - this._seq = data.seq; - } - - if (this.onData) { - this.onData(data); - } - - // Update locally cached contact with the new message count - var me = Tinode.getInstance().getMeTopic(); - if (me) { - me.setMsgReadRecv(this.name, "msg", data.seq); - } - }, - - // Process metadata message - _routeMeta: function(meta) { - this._lastUpdate = meta.ts; - - if (meta.desc) { - this._processMetaDesc(meta.desc); - } - if (meta.sub && meta.sub.length > 0) { - this._processMetaSub(meta.sub); - } - if (this.onMeta) { - this.onMeta(meta); - } - }, - - // Process presence change message - _routePres: function(pres) { - if (this.onPres) { - this.onPres(pres); - } - }, - - // Process {info} message - _routeInfo: function(info) { - if (info.what !== "kp") { - var user = this._users[info.from]; - if (user) { - user[info.what] = info.seq; - } - } - if (this.onInfo) { - this.onInfo(info); - } - }, - - // Called by Tinode when meta.desc packet is received. - _processMetaDesc: function(desc) { - // Copy parameters from desc object to this topic. - mergeObj(this, desc); - - if (typeof this.created === "string") { - this.created = new Date(this.created); - } - if (typeof this.updated === "string") { - this.updated = new Date(this.updated); - } - // Ensure that the acs is properly parsed. - this.acs = new AccessMode(this.acs); - - // Update relevant contact in the me topic, if available: - if (this.name !== 'me') { - var me = Tinode.getInstance().getMeTopic(); - if (me) { - me._processMetaSub([{ - topic: this.name, - updated: this.updated, - acs: this.acs, - public: this.public, - private: this.private - }]); - } - } - - if (this.onMetaDesc) { - this.onMetaDesc(desc); - } - }, - - // Called by Tinode when meta.sub is recived. - _processMetaSub: function(subs) { - for (var idx in subs) { - var sub = subs[idx]; - sub.acs = new AccessMode(sub.acs); - if (sub.user) { // Response to get.sub on 'me' topic does not have .user set - // Save the object to global cache. - sub.updated = new Date(sub.updated); - sub.deleted = sub.deleted ? new Date(sub.deleted) : null; - - var cached = null; - if (!sub.deleted) { - // Cache user in the topic - cached = mergeToCache(this._users, sub.user, sub); - this._cachePutUser(sub.user, cached); - } else { - // Subscription is deleted, remove it from topic (but leave in Users cache) - delete this._users[sub.user]; - cached = sub; - } - } - - if (this.onMetaSub) { - this.onMetaSub(cached); - } - } - - if (this.onSubsUpdated) { - this.onSubsUpdated(Object.keys(this._users)); - } - }, - - // Reset subscribed state - // TODO(gene): should it also clear the message cache? - _resetSub: function() { - this._subscribed = false; - }, - - /** - * Get topic's default access mode. - * @memberof Tinode.Topic# - * - * @returns {Object} - access mode, such as {auth: `RWP`, anon: `N`}. - */ - getDefaultAccess: function() { - return this.defacs; - } - }; - - /** - * @class TopicMe - special case of {@link Tinode.Topic} for receiving and confirming invitations, - * managing data of the current user, including contact list. - * @extends Tinode.Topic - * @memberof Tinode - * - * @param {TopicMe.Callbacks} callbacks - Callbacks to receive various events. - */ - var TopicMe = function(callbacks) { - Topic.call(this, TOPIC_ME, callbacks); - // List of contacts (topic_name -> Contact object) - this._contacts = {}; - - // me-specific callbacks - if (callbacks) { - this.onContactUpdate = callbacks.onContactUpdate; - } - }; - - // Inherit everyting from the generic Topic - TopicMe.prototype = Object.create(Topic.prototype, { - // Override the original Topic._processMetaSub - _processMetaSub: { - value: function(subs) { - var updateCount = 0; - for (var idx in subs) { - var sub = subs[idx]; - var topic = sub.topic; - // Don't show 'fnd' topic in the list of contacts - if (topic === TOPIC_FND) { - continue; - } - sub.updated = new Date(sub.updated); - sub.deleted = sub.deleted ? new Date(sub.deleted) : null; - - var cont = null; - if (!sub.deleted) { - sub.acs = new AccessMode(sub.acs); - if (sub.seen && sub.seen.when) { - sub.seen.when = new Date(sub.seen.when); - } - cont = mergeToCache(this._contacts, topic, sub); - } else { - cont = sub; - delete this._contacts[topic]; - } - - updateCount ++; - - if (this.onMetaSub) { - this.onMetaSub(cont); - } - } - - if (updateCount > 0 && this.onSubsUpdated) { - this.onSubsUpdated(Object.keys(this._contacts)); - } - }, - enumerable: true, - configurable: true, - writable: true - }, - - // Process presence change message - _routePres: { - value: function(pres) { - var cont = this._contacts[pres.src]; - if (cont) { - switch(pres.what) { - case "on": // topic came online - cont.online = true; - break; - case "off": // topic went offline - cont.online = false; - if (cont.seen) { - cont.seen.when = new Date(); - } else { - cont.seen = {when: new Date()}; - } - break; - case "msg": // new message received - cont.seq = pres.seq; - break; - case "upd": // desc updated - // TODO(gene): request updated description - break; - case "ua": // user agent changed - cont.seen = {when: new Date(), ua: pres.ua}; - break; - case "recv": // user's other session marked some messges as received - cont.recv = cont.recv ? Math.max(cont.recv, pres.seq) : pres.seq; - break; - case "read": // user's other session marked some messages as read - cont.read = cont.read ? Math.max(cont.read, pres.seq) : pres.seq; - break; - case "gone": // topic deleted or unsubscribed from - delete this._contacts[pres.src]; - break; - case "del": - // Handle message deletion - break; - } - if (this.onContactUpdate) { - this.onContactUpdate(pres.what, cont); - } - } - if (this.onPres) { - this.onPres(pres); - } - }, - enumerable: true, - configurable: true, - writable: true - }, - - /** - * Publishing to TopicMe is not supported. {@link Topic#publish} is overriden and thows an {Error} if called. - * @memberof Tinode.TopicMe# - * @throws {Error} Always throws an error. - */ - publish: { - value: function(data, noEcho) { - throw new Error("Publishing to 'me' is not supported"); - }, - enumerable: true, - configurable: true, - writable: true - }, - - /** - * Iterate over cached contacts. If callback is undefined, use {@link this.onMetaSub}. - * @function - * @memberof Tinode.TopicMe# - * @param {TopicMe.ContactCallback} callback - Callback to call for each contact. - * @param {Object} context - Context to use for calling the `callback`, i.e. the value of `this` inside the callback. - */ - contacts: { - value: function(callback, context) { - var cb = (callback || this.onMetaSub); - if (cb) { - for (var idx in this._contacts) { - cb.call(context, this._contacts[idx], idx, this._contacts); - } - } - }, - enumerable: true, - configurable: true, - writable: true - }, - - /** - * Update a cached contact with new read/received/message count. - * @function - * @memberof Tinode.TopicMe# - * - * @param {string} contactName - UID of contact to update. - * @param {string} what - Whach count to update, one of "read", "recv", "msg" - * @param {number} seq - New value of the count. - */ - setMsgReadRecv: { - value: function(contactName, what, seq) { - var cont = this._contacts[contactName]; - var oldVal, newVal; - var mode = null; - if (cont) { - if (what === "recv") { - oldVal = cont.recv; - cont.recv = cont.recv ? Math.max(cont.recv, seq) : seq; - newVal = cont.recv; - } else if (what === "read") { - oldVal = cont.read; - cont.read = cont.read ? Math.max(cont.read, seq) : seq; - newVal = cont.read; - } else if (what === "msg") { - oldVal = cont.seq; - cont.seq = cont.seq ? Math.max(cont.seq, seq) : seq; - newVal = cont.seq; - } - - if ((oldVal != newVal) && (!cont.acs || !cont.acs.isMuted()) && this.onContactUpdate) { - this.onContactUpdate(what, cont); - } - } - }, - enumerable: true, - configurable: true, - writable: true - }, - - /** - * Get a contact from cache. - * @memberof Tinode.TopicMe# - * - * @param {string} name - Name of the contact to get, either a UID (for p2p topics) or a topic name. - * @returns {Tinode.Contact} - Contact or `undefined`. - */ - getContact: { - value: function(name) { - return this._contacts[name]; - }, - enumerable: true, - configurable: true, - writable: true - }, - - /** - * Get access mode of a given contact. - * @memberof Tinode.TopicMe# - * - * @param {string} name - Name of the contact to get access mode for, aither a UID (for p2p topics) or a topic name. - * @returns {string} - access mode, such as `RWP`. - */ - getAccessMode: { - value: function(name) { - var cont = this._contacts[name]; - return cont ? cont.acs : null; - }, - enumerable: true, - configurable: true, - writable: true - } - }); - TopicMe.prototype.constructor = TopicMe; - - // Export for the window object or node; Check that is not already defined. - if (typeof(environment.Tinode) === 'undefined') { - environment.Tinode = Tinode.getInstance(); - } - -})(this); // 'this' will be window object for the browsers.