From 578df160d1eba7bdf41fa9c28fd72c9fddc1fa6a Mon Sep 17 00:00:00 2001 From: or-else Date: Sun, 23 Jul 2017 18:23:35 +0300 Subject: [PATCH 1/3] cosmetic change --- css/base.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/css/base.css b/css/base.css index 275f6f8a..7ab5950d 100644 --- a/css/base.css +++ b/css/base.css @@ -558,7 +558,8 @@ ul.permission-editor > li { .contact-box .contact-comment { text-overflow: ellipsis; - overflow: hidden + overflow: hidden; + font-weight: lighter; } /* The down arrow head - trigger for context menu in contacts list */ From d051c84737bfb35ced930c013f4971b8761b31bf Mon Sep 17 00:00:00 2001 From: Riandy Rahman Nugraha Date: Thu, 3 Aug 2017 09:15:31 +0700 Subject: [PATCH 2/3] =?UTF-8?q?explicitly=20set=20defacs=20for=20newly=20c?= =?UTF-8?q?reated=20account=20to=20=E2=80=9CJRWPS=E2=80=9D;=20add=20email?= =?UTF-8?q?=20as=20tags=20in=20newly=20created=20account,=20so=20it=20coul?= =?UTF-8?q?d=20be=20discovered=20by=20others;?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 28 +- tinode.js | 2735 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 2759 insertions(+), 4 deletions(-) create mode 100755 tinode.js diff --git a/index.html b/index.html index e6e35c3b..caf87972 100644 --- a/index.html +++ b/index.html @@ -116,6 +116,15 @@ return card; } + // construct email params + function email(emailAddress) { + var tags = []; + if(emailAddress) { + tags.push("email:" + emailAddress); + } + return tags + } + /* BEGIN Popup/dropdown menu */ var ContextMenu = React.createClass({ componentWillUnmount: function() { @@ -650,6 +659,7 @@ login: '', password: '', password2: '', + email: '', fn: '', // full/formatted name imageDataUrl: null, errorCleared: false @@ -664,6 +674,9 @@ handlePassword2Change: function(e) { this.setState({password2: e.target.value}); }, + handleEmailChange: function(e) { + this.setState({email: e.target.value}) + }, handleFnChange: function(e) { this.setState({fn: e.target.value}); }, @@ -679,7 +692,9 @@ this.props.onCreateAccount( this.state.login.trim(), this.state.password.trim(), - vcard(this.state.fn, this.state.imageDataUrl)); + vcard(this.state.fn, this.state.imageDataUrl), + null, + email(this.state.email)); } }, render: function() { @@ -700,9 +715,13 @@ +
+ +
+ value={this.state.fn} onChange={this.handleFnChange} required/>
@@ -2502,12 +2521,13 @@ this.setState({sidePanelSelected: 'register'}); }, // Actual registration of a new account. - handleNewAccountRequest: function(login_, password_, public_, private_) { + handleNewAccountRequest: function(login_, password_, public_, private_, tags_) { var instance = this; + tags_ = tags_ || []; // init tags_ value just in case undefined Tinode.connect(this.state.serverAddress) .then(function() { return Tinode.createAccountBasic(login_, password_, - {login: true, public: public_, private: private_}); + {defacs: {auth: "JRWPS", anon: "N"}, public: public_, private: private_, tags: tags_}); }) .then(function() { instance.handleLoginSuccessful(instance); diff --git a/tinode.js b/tinode.js new file mode 100755 index 00000000..19f80d06 --- /dev/null +++ b/tinode.js @@ -0,0 +1,2735 @@ +/** + * @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. From 897bb0719ae3b5f6ab80aa2d894fd8f133db6f6a Mon Sep 17 00:00:00 2001 From: Riandy Rahman Nugraha Date: Fri, 4 Aug 2017 09:11:29 +0700 Subject: [PATCH 3/3] =?UTF-8?q?remove=20tinode.js=20from=20example=20app?= =?UTF-8?q?=20=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.