@roeehrl/tinode-sdk 0.25.1-sqlite.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +47 -0
- package/package.json +76 -0
- package/src/access-mode.js +567 -0
- package/src/cbuffer.js +244 -0
- package/src/cbuffer.test.js +107 -0
- package/src/comm-error.js +14 -0
- package/src/config.js +71 -0
- package/src/connection.js +537 -0
- package/src/db.js +1021 -0
- package/src/drafty.js +2758 -0
- package/src/drafty.test.js +1600 -0
- package/src/fnd-topic.js +123 -0
- package/src/index.js +29 -0
- package/src/index.native.js +35 -0
- package/src/large-file.js +325 -0
- package/src/me-topic.js +480 -0
- package/src/meta-builder.js +283 -0
- package/src/storage-sqlite.js +1081 -0
- package/src/tinode.js +2382 -0
- package/src/topic.js +2160 -0
- package/src/utils.js +309 -0
- package/src/utils.test.js +456 -0
- package/types/index.d.ts +1227 -0
- package/umd/tinode.dev.js +6856 -0
- package/umd/tinode.dev.js.map +1 -0
- package/umd/tinode.prod.js +2 -0
- package/umd/tinode.prod.js.map +1 -0
package/src/tinode.js
ADDED
|
@@ -0,0 +1,2382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module tinode-sdk
|
|
3
|
+
*
|
|
4
|
+
* @copyright 2015-2025 Tinode LLC.
|
|
5
|
+
* @summary Javascript bindings for Tinode.
|
|
6
|
+
* @license Apache 2.0
|
|
7
|
+
* @version 0.25
|
|
8
|
+
*
|
|
9
|
+
* See <a href="https://github.com/tinode/webapp">https://github.com/tinode/webapp</a> for real-life usage.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* <head>
|
|
13
|
+
* <script src=".../tinode.js"></script>
|
|
14
|
+
* </head>
|
|
15
|
+
*
|
|
16
|
+
* <body>
|
|
17
|
+
* ...
|
|
18
|
+
* <script>
|
|
19
|
+
* // Instantiate tinode.
|
|
20
|
+
* const tinode = new Tinode(config, _ => {
|
|
21
|
+
* // Called on init completion.
|
|
22
|
+
* });
|
|
23
|
+
* tinode.enableLogging(true);
|
|
24
|
+
* tinode.onDisconnect = err => {
|
|
25
|
+
* // Handle disconnect.
|
|
26
|
+
* };
|
|
27
|
+
* // Connect to the server.
|
|
28
|
+
* tinode.connect('https://example.com/').then(_ => {
|
|
29
|
+
* // Connected. Login now.
|
|
30
|
+
* return tinode.loginBasic(login, password);
|
|
31
|
+
* }).then(ctrl => {
|
|
32
|
+
* // Logged in fine, attach callbacks, subscribe to 'me'.
|
|
33
|
+
* const me = tinode.getMeTopic();
|
|
34
|
+
* me.onMetaDesc = function(meta) { ... };
|
|
35
|
+
* // Subscribe, fetch topic description and the list of contacts.
|
|
36
|
+
* me.subscribe({get: {desc: {}, sub: {}}});
|
|
37
|
+
* }).catch(err => {
|
|
38
|
+
* // Login or subscription failed, do something.
|
|
39
|
+
* ...
|
|
40
|
+
* });
|
|
41
|
+
* ...
|
|
42
|
+
* </script>
|
|
43
|
+
* </body>
|
|
44
|
+
*/
|
|
45
|
+
'use strict';
|
|
46
|
+
|
|
47
|
+
// NOTE TO DEVELOPERS:
|
|
48
|
+
// Localizable strings should be double quoted "строка на другом языке",
|
|
49
|
+
// non-localizable strings should be single quoted 'non-localized'.
|
|
50
|
+
|
|
51
|
+
import AccessMode from './access-mode.js';
|
|
52
|
+
import * as Const from './config.js';
|
|
53
|
+
import CommError from './comm-error.js';
|
|
54
|
+
import Connection from './connection.js';
|
|
55
|
+
import DBCache from './db.js';
|
|
56
|
+
import Drafty from './drafty.js';
|
|
57
|
+
import LargeFileHelper from './large-file.js';
|
|
58
|
+
import MetaGetBuilder from './meta-builder.js';
|
|
59
|
+
import Topic from './topic.js';
|
|
60
|
+
import TopicFnd from './fnd-topic.js';
|
|
61
|
+
import TopicMe from './me-topic.js';
|
|
62
|
+
|
|
63
|
+
import {
|
|
64
|
+
isUrlRelative,
|
|
65
|
+
jsonParseHelper,
|
|
66
|
+
mergeObj,
|
|
67
|
+
rfc3339DateString,
|
|
68
|
+
simplify
|
|
69
|
+
} from './utils.js';
|
|
70
|
+
|
|
71
|
+
// Re-export AccessMode and DB
|
|
72
|
+
export {
|
|
73
|
+
AccessMode,
|
|
74
|
+
DBCache as DB
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
let WebSocketProvider;
|
|
78
|
+
if (typeof WebSocket != 'undefined') {
|
|
79
|
+
WebSocketProvider = WebSocket;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let XHRProvider;
|
|
83
|
+
if (typeof XMLHttpRequest != 'undefined') {
|
|
84
|
+
XHRProvider = XMLHttpRequest;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let IndexedDBProvider;
|
|
88
|
+
if (typeof indexedDB != 'undefined') {
|
|
89
|
+
IndexedDBProvider = indexedDB;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Re-export Drafty.
|
|
93
|
+
export {
|
|
94
|
+
Drafty
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
initForNonBrowserApp();
|
|
98
|
+
|
|
99
|
+
// Utility functions
|
|
100
|
+
|
|
101
|
+
// Polyfill for non-browser context, e.g. NodeJs.
|
|
102
|
+
function initForNonBrowserApp() {
|
|
103
|
+
// Tinode requirement in native mode because react native doesn't provide Base64 method
|
|
104
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
|
|
105
|
+
|
|
106
|
+
if (typeof btoa == 'undefined') {
|
|
107
|
+
global.btoa = function(input = '') {
|
|
108
|
+
let str = input;
|
|
109
|
+
let output = '';
|
|
110
|
+
|
|
111
|
+
for (let block = 0, charCode, i = 0, map = chars; str.charAt(i | 0) || (map = '=', i % 1); output += map.charAt(63 & block >> 8 - i % 1 * 8)) {
|
|
112
|
+
|
|
113
|
+
charCode = str.charCodeAt(i += 3 / 4);
|
|
114
|
+
|
|
115
|
+
if (charCode > 0xFF) {
|
|
116
|
+
throw new Error("'btoa' failed: The string to be encoded contains characters outside of the Latin1 range.");
|
|
117
|
+
}
|
|
118
|
+
block = block << 8 | charCode;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return output;
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (typeof atob == 'undefined') {
|
|
126
|
+
global.atob = function(input = '') {
|
|
127
|
+
let str = input.replace(/=+$/, '');
|
|
128
|
+
let output = '';
|
|
129
|
+
|
|
130
|
+
if (str.length % 4 == 1) {
|
|
131
|
+
throw new Error("'atob' failed: The string to be decoded is not correctly encoded.");
|
|
132
|
+
}
|
|
133
|
+
for (let bc = 0, bs = 0, buffer, i = 0; buffer = str.charAt(i++);
|
|
134
|
+
|
|
135
|
+
~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer,
|
|
136
|
+
bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0
|
|
137
|
+
) {
|
|
138
|
+
buffer = chars.indexOf(buffer);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return output;
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (typeof window == 'undefined') {
|
|
146
|
+
global.window = {
|
|
147
|
+
WebSocket: WebSocketProvider,
|
|
148
|
+
XMLHttpRequest: XHRProvider,
|
|
149
|
+
indexedDB: IndexedDBProvider,
|
|
150
|
+
URL: {
|
|
151
|
+
createObjectURL: function() {
|
|
152
|
+
throw new Error("Unable to use URL.createObjectURL in a non-browser application");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
Connection.setNetworkProviders(WebSocketProvider, XHRProvider);
|
|
159
|
+
LargeFileHelper.setNetworkProvider(XHRProvider);
|
|
160
|
+
DBCache.setDatabaseProvider(IndexedDBProvider);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Detect find most useful network transport.
|
|
164
|
+
function detectTransport() {
|
|
165
|
+
if (typeof window == 'object') {
|
|
166
|
+
if (window['WebSocket']) {
|
|
167
|
+
return 'ws';
|
|
168
|
+
} else if (window['XMLHttpRequest']) {
|
|
169
|
+
// The browser or node has no websockets, using long polling.
|
|
170
|
+
return 'lp';
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// btoa replacement. Stock btoa fails on on non-Latin1 strings.
|
|
177
|
+
function b64EncodeUnicode(str) {
|
|
178
|
+
// The encodeURIComponent percent-encodes UTF-8 string,
|
|
179
|
+
// then the percent encoding is converted into raw bytes which
|
|
180
|
+
// can be fed into btoa.
|
|
181
|
+
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
|
|
182
|
+
function toSolidBytes(match, p1) {
|
|
183
|
+
return String.fromCharCode('0x' + p1);
|
|
184
|
+
}));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// JSON stringify helper - pre-processor for JSON.stringify
|
|
188
|
+
function jsonBuildHelper(key, val) {
|
|
189
|
+
if (val instanceof Date) {
|
|
190
|
+
// Convert javascript Date objects to rfc3339 strings
|
|
191
|
+
val = rfc3339DateString(val);
|
|
192
|
+
} else if (val instanceof AccessMode) {
|
|
193
|
+
val = val.jsonHelper();
|
|
194
|
+
} else if (val === undefined || val === null || val === false ||
|
|
195
|
+
(Array.isArray(val) && val.length == 0) ||
|
|
196
|
+
((typeof val == 'object') && (Object.keys(val).length == 0))) {
|
|
197
|
+
// strip out empty elements while serializing objects to JSON
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return val;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// Trims very long strings (encoded images) to make logged packets more readable.
|
|
205
|
+
function jsonLoggerHelper(key, val) {
|
|
206
|
+
if (typeof val == 'string' && val.length > 128) {
|
|
207
|
+
return '<' + val.length + ', bytes: ' + val.substring(0, 12) + '...' + val.substring(val.length - 12) + '>';
|
|
208
|
+
}
|
|
209
|
+
return jsonBuildHelper(key, val);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// Parse browser user agent to extract browser name and version.
|
|
213
|
+
function getBrowserInfo(ua, product) {
|
|
214
|
+
ua = ua || '';
|
|
215
|
+
let reactnative = '';
|
|
216
|
+
// Check if this is a ReactNative app.
|
|
217
|
+
if (/reactnative/i.test(product)) {
|
|
218
|
+
reactnative = 'ReactNative; ';
|
|
219
|
+
}
|
|
220
|
+
let result;
|
|
221
|
+
// Remove useless string.
|
|
222
|
+
ua = ua.replace(' (KHTML, like Gecko)', '');
|
|
223
|
+
// Test for WebKit-based browser.
|
|
224
|
+
let m = ua.match(/(AppleWebKit\/[.\d]+)/i);
|
|
225
|
+
if (m) {
|
|
226
|
+
// List of common strings, from more useful to less useful.
|
|
227
|
+
// All unknown strings get the highest (-1) priority.
|
|
228
|
+
const priority = ['edg', 'chrome', 'safari', 'mobile', 'version'];
|
|
229
|
+
let tmp = ua.substr(m.index + m[0].length).split(' ');
|
|
230
|
+
let tokens = [];
|
|
231
|
+
let version; // 1.0 in Version/1.0 or undefined;
|
|
232
|
+
// Split string like 'Name/0.0.0' into ['Name', '0.0.0', 3] where the last element is the priority.
|
|
233
|
+
for (let i = 0; i < tmp.length; i++) {
|
|
234
|
+
let m2 = /([\w.]+)[\/]([\.\d]+)/.exec(tmp[i]);
|
|
235
|
+
if (m2) {
|
|
236
|
+
// Unknown values are highest priority (-1).
|
|
237
|
+
tokens.push([m2[1], m2[2], priority.findIndex((e) => {
|
|
238
|
+
return m2[1].toLowerCase().startsWith(e);
|
|
239
|
+
})]);
|
|
240
|
+
if (m2[1] == 'Version') {
|
|
241
|
+
version = m2[2];
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// Sort by priority: more interesting is earlier than less interesting.
|
|
246
|
+
tokens.sort((a, b) => {
|
|
247
|
+
return a[2] - b[2];
|
|
248
|
+
});
|
|
249
|
+
if (tokens.length > 0) {
|
|
250
|
+
// Return the least common browser string and version.
|
|
251
|
+
if (tokens[0][0].toLowerCase().startsWith('edg')) {
|
|
252
|
+
tokens[0][0] = 'Edge';
|
|
253
|
+
} else if (tokens[0][0] == 'OPR') {
|
|
254
|
+
tokens[0][0] = 'Opera';
|
|
255
|
+
} else if (tokens[0][0] == 'Safari' && version) {
|
|
256
|
+
tokens[0][1] = version;
|
|
257
|
+
}
|
|
258
|
+
result = tokens[0][0] + '/' + tokens[0][1];
|
|
259
|
+
} else {
|
|
260
|
+
// Failed to ID the browser. Return the webkit version.
|
|
261
|
+
result = m[1];
|
|
262
|
+
}
|
|
263
|
+
} else if (/firefox/i.test(ua)) {
|
|
264
|
+
m = /Firefox\/([.\d]+)/g.exec(ua);
|
|
265
|
+
if (m) {
|
|
266
|
+
result = 'Firefox/' + m[1];
|
|
267
|
+
} else {
|
|
268
|
+
result = 'Firefox/?';
|
|
269
|
+
}
|
|
270
|
+
} else {
|
|
271
|
+
// Neither AppleWebKit nor Firefox. Try the last resort.
|
|
272
|
+
m = /([\w.]+)\/([.\d]+)/.exec(ua);
|
|
273
|
+
if (m) {
|
|
274
|
+
result = m[1] + '/' + m[2];
|
|
275
|
+
} else {
|
|
276
|
+
m = ua.split(' ');
|
|
277
|
+
result = m[0];
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Shorten the version to one dot 'a.bb.ccc.d -> a.bb' at most.
|
|
282
|
+
m = result.split('/');
|
|
283
|
+
if (m.length > 1) {
|
|
284
|
+
const v = m[1].split('.');
|
|
285
|
+
const minor = v[1] ? '.' + v[1].substr(0, 2) : '';
|
|
286
|
+
result = `${m[0]}/${v[0]}${minor}`;
|
|
287
|
+
}
|
|
288
|
+
return reactnative + result;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* The main class for interacting with Tinode server.
|
|
293
|
+
*/
|
|
294
|
+
export class Tinode {
|
|
295
|
+
_host;
|
|
296
|
+
_secure;
|
|
297
|
+
|
|
298
|
+
_appName;
|
|
299
|
+
|
|
300
|
+
// API Key.
|
|
301
|
+
_apiKey;
|
|
302
|
+
|
|
303
|
+
// Name and version of the browser.
|
|
304
|
+
_browser = '';
|
|
305
|
+
_platform;
|
|
306
|
+
// Hardware
|
|
307
|
+
_hwos = 'undefined';
|
|
308
|
+
_humanLanguage = 'xx';
|
|
309
|
+
|
|
310
|
+
// Logging to console enabled
|
|
311
|
+
_loggingEnabled = false;
|
|
312
|
+
// When logging, trip long strings (base64-encoded images) for readability
|
|
313
|
+
_trimLongStrings = false;
|
|
314
|
+
// UID of the currently authenticated user.
|
|
315
|
+
_myUID = null;
|
|
316
|
+
// Status of connection: authenticated or not.
|
|
317
|
+
_authenticated = false;
|
|
318
|
+
// Login used in the last successful basic authentication
|
|
319
|
+
_login = null;
|
|
320
|
+
// Token which can be used for login instead of login/password.
|
|
321
|
+
_authToken = null;
|
|
322
|
+
// Counter of received packets
|
|
323
|
+
_inPacketCount = 0;
|
|
324
|
+
// Counter for generating unique message IDs
|
|
325
|
+
_messageId = Math.floor((Math.random() * 0xFFFF) + 0xFFFF);
|
|
326
|
+
// Information about the server, if connected
|
|
327
|
+
_serverInfo = null;
|
|
328
|
+
// Push notification token. Called deviceToken for consistency with the Android SDK.
|
|
329
|
+
_deviceToken = null;
|
|
330
|
+
|
|
331
|
+
// Cache of pending promises by message id.
|
|
332
|
+
_pendingPromises = {};
|
|
333
|
+
// The Timeout object returned by the reject expired promises setInterval.
|
|
334
|
+
_expirePromises = null;
|
|
335
|
+
|
|
336
|
+
// Websocket or long polling connection.
|
|
337
|
+
_connection = null;
|
|
338
|
+
|
|
339
|
+
// Use indexDB for caching topics and messages.
|
|
340
|
+
_persist = false;
|
|
341
|
+
// IndexedDB wrapper object.
|
|
342
|
+
_db = null;
|
|
343
|
+
|
|
344
|
+
// Tinode's cache of objects
|
|
345
|
+
_cache = {};
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Create Tinode object.
|
|
349
|
+
*
|
|
350
|
+
* @param {Object} config - configuration parameters.
|
|
351
|
+
* @param {string} config.appName - Name of the calling application to be reported in the User Agent.
|
|
352
|
+
* @param {string} config.host - Host name and optional port number to connect to.
|
|
353
|
+
* @param {string} config.apiKey - API key generated by <code>keygen</code>.
|
|
354
|
+
* @param {string} config.transport - See {@link Tinode.Connection#transport}.
|
|
355
|
+
* @param {boolean} config.secure - Use Secure WebSocket if <code>true</code>.
|
|
356
|
+
* @param {string} config.platform - Optional platform identifier, one of <code>"ios"</code>, <code>"web"</code>, <code>"android"</code>.
|
|
357
|
+
* @param {boolen} config.persist - Use IndexedDB persistent storage.
|
|
358
|
+
* @param {function} onComplete - callback to call when initialization is completed.
|
|
359
|
+
*/
|
|
360
|
+
constructor(config, onComplete) {
|
|
361
|
+
this._host = config.host;
|
|
362
|
+
this._secure = config.secure;
|
|
363
|
+
|
|
364
|
+
// Client-provided application name, format <Name>/<version number>
|
|
365
|
+
this._appName = config.appName || "Undefined";
|
|
366
|
+
|
|
367
|
+
// API Key.
|
|
368
|
+
this._apiKey = config.apiKey;
|
|
369
|
+
|
|
370
|
+
// Name and version of the browser.
|
|
371
|
+
this._platform = config.platform || 'web';
|
|
372
|
+
// Underlying OS.
|
|
373
|
+
if (typeof navigator != 'undefined') {
|
|
374
|
+
this._browser = getBrowserInfo(navigator.userAgent, navigator.product);
|
|
375
|
+
this._hwos = navigator.platform;
|
|
376
|
+
// This is the default language. It could be changed by client.
|
|
377
|
+
this._humanLanguage = navigator.language || 'en-US';
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
Connection.logger = this.logger;
|
|
381
|
+
Drafty.logger = this.logger;
|
|
382
|
+
|
|
383
|
+
// WebSocket or long polling network connection.
|
|
384
|
+
if (config.transport != 'lp' && config.transport != 'ws') {
|
|
385
|
+
config.transport = detectTransport();
|
|
386
|
+
}
|
|
387
|
+
this._connection = new Connection(config, Const.PROTOCOL_VERSION, /* autoreconnect */ true);
|
|
388
|
+
this._connection.onMessage = (data) => {
|
|
389
|
+
// Call the main message dispatcher.
|
|
390
|
+
this.#dispatchMessage(data);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Ready to start sending.
|
|
394
|
+
this._connection.onOpen = _ => this.#connectionOpen();
|
|
395
|
+
this._connection.onDisconnect = (err, code) => this.#disconnected(err, code);
|
|
396
|
+
|
|
397
|
+
// Wrapper for the reconnect iterator callback.
|
|
398
|
+
this._connection.onAutoreconnectIteration = (timeout, promise) => {
|
|
399
|
+
if (this.onAutoreconnectIteration) {
|
|
400
|
+
this.onAutoreconnectIteration(timeout, promise);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
this._persist = config.persist;
|
|
405
|
+
// Initialize object regardless. It simplifies the code.
|
|
406
|
+
this._db = new DBCache(this.logger, this.logger);
|
|
407
|
+
|
|
408
|
+
if (this._persist) {
|
|
409
|
+
// Create the persistent cache.
|
|
410
|
+
// Store promises to be resolved when messages load into memory.
|
|
411
|
+
const prom = [];
|
|
412
|
+
this._db.initDatabase().then(_ => {
|
|
413
|
+
// First load topics into memory.
|
|
414
|
+
return this._db.mapTopics(data => {
|
|
415
|
+
let topic = this.#cacheGet('topic', data.name);
|
|
416
|
+
if (topic) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (data.name == Const.TOPIC_ME) {
|
|
420
|
+
topic = new TopicMe();
|
|
421
|
+
} else if (data.name == Const.TOPIC_FND) {
|
|
422
|
+
topic = new TopicFnd();
|
|
423
|
+
} else {
|
|
424
|
+
topic = new Topic(data.name);
|
|
425
|
+
}
|
|
426
|
+
this._db.deserializeTopic(topic, data);
|
|
427
|
+
this.#attachCacheToTopic(topic);
|
|
428
|
+
topic._cachePutSelf();
|
|
429
|
+
this._db.maxDelId(topic.name).then(clear => {
|
|
430
|
+
topic._maxDel = Math.max(topic._maxDel, clear || 0);
|
|
431
|
+
});
|
|
432
|
+
// Topic loaded from DB is not new.
|
|
433
|
+
delete topic._new;
|
|
434
|
+
// Request to load messages and save the promise.
|
|
435
|
+
prom.push(topic._loadMessages(this._db));
|
|
436
|
+
});
|
|
437
|
+
}).then(_ => {
|
|
438
|
+
// Then load users.
|
|
439
|
+
return this._db.mapUsers((data) => {
|
|
440
|
+
this.#cachePut('user', data.uid, mergeObj({}, data.public));
|
|
441
|
+
});
|
|
442
|
+
}).then(_ => {
|
|
443
|
+
// Now wait for all messages to finish loading.
|
|
444
|
+
return Promise.all(prom);
|
|
445
|
+
}).then(_ => {
|
|
446
|
+
if (onComplete) {
|
|
447
|
+
onComplete();
|
|
448
|
+
}
|
|
449
|
+
this.logger("Persistent cache initialized.");
|
|
450
|
+
}).catch(err => {
|
|
451
|
+
if (onComplete) {
|
|
452
|
+
onComplete(err);
|
|
453
|
+
}
|
|
454
|
+
this.logger("Failed to initialize persistent cache:", err);
|
|
455
|
+
});
|
|
456
|
+
} else {
|
|
457
|
+
this._db.deleteDatabase().then(_ => {
|
|
458
|
+
if (onComplete) {
|
|
459
|
+
onComplete();
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Private methods.
|
|
466
|
+
|
|
467
|
+
// Console logger. Babel somehow fails to parse '...rest' parameter.
|
|
468
|
+
logger(str, ...args) {
|
|
469
|
+
if (this._loggingEnabled) {
|
|
470
|
+
const d = new Date();
|
|
471
|
+
const dateString = ('0' + d.getUTCHours()).slice(-2) + ':' +
|
|
472
|
+
('0' + d.getUTCMinutes()).slice(-2) + ':' +
|
|
473
|
+
('0' + d.getUTCSeconds()).slice(-2) + '.' +
|
|
474
|
+
('00' + d.getUTCMilliseconds()).slice(-3);
|
|
475
|
+
|
|
476
|
+
console.log('[' + dateString + ']', str, args.join(' '));
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Generator of default promises for sent packets.
|
|
481
|
+
#makePromise(id) {
|
|
482
|
+
let promise = null;
|
|
483
|
+
if (id) {
|
|
484
|
+
promise = new Promise((resolve, reject) => {
|
|
485
|
+
// Stored callbacks will be called when the response packet with this Id arrives
|
|
486
|
+
this._pendingPromises[id] = {
|
|
487
|
+
'resolve': resolve,
|
|
488
|
+
'reject': reject,
|
|
489
|
+
'ts': new Date()
|
|
490
|
+
};
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
return promise;
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
// Resolve or reject a pending promise.
|
|
497
|
+
// Unresolved promises are stored in _pendingPromises.
|
|
498
|
+
#execPromise(id, code, onOK, errorText) {
|
|
499
|
+
const callbacks = this._pendingPromises[id];
|
|
500
|
+
if (callbacks) {
|
|
501
|
+
delete this._pendingPromises[id];
|
|
502
|
+
if (code >= 200 && code < 400) {
|
|
503
|
+
if (callbacks.resolve) {
|
|
504
|
+
callbacks.resolve(onOK);
|
|
505
|
+
}
|
|
506
|
+
} else if (callbacks.reject) {
|
|
507
|
+
callbacks.reject(new CommError(errorText, code));
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Send a packet. If packet id is provided return a promise.
|
|
513
|
+
#send(pkt, id) {
|
|
514
|
+
let promise;
|
|
515
|
+
if (id) {
|
|
516
|
+
promise = this.#makePromise(id);
|
|
517
|
+
}
|
|
518
|
+
pkt = simplify(pkt);
|
|
519
|
+
let msg = JSON.stringify(pkt);
|
|
520
|
+
this.logger("out: " + (this._trimLongStrings ? JSON.stringify(pkt, jsonLoggerHelper) : msg));
|
|
521
|
+
try {
|
|
522
|
+
this._connection.sendText(msg);
|
|
523
|
+
} catch (err) {
|
|
524
|
+
// If sendText throws, wrap the error in a promise or rethrow.
|
|
525
|
+
if (id) {
|
|
526
|
+
this.#execPromise(id, Connection.NETWORK_ERROR, null, err.message);
|
|
527
|
+
} else {
|
|
528
|
+
throw err;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return promise;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// The main message dispatcher.
|
|
535
|
+
#dispatchMessage(data) {
|
|
536
|
+
// Skip empty response. This happens when LP times out.
|
|
537
|
+
if (!data)
|
|
538
|
+
return;
|
|
539
|
+
|
|
540
|
+
this._inPacketCount++;
|
|
541
|
+
|
|
542
|
+
// Send raw message to listener
|
|
543
|
+
if (this.onRawMessage) {
|
|
544
|
+
this.onRawMessage(data);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (data === '0') {
|
|
548
|
+
// Server response to a network probe.
|
|
549
|
+
if (this.onNetworkProbe) {
|
|
550
|
+
this.onNetworkProbe();
|
|
551
|
+
}
|
|
552
|
+
// No processing is necessary.
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
let pkt = JSON.parse(data, jsonParseHelper);
|
|
557
|
+
if (!pkt) {
|
|
558
|
+
this.logger("in: " + data);
|
|
559
|
+
this.logger("ERROR: failed to parse data");
|
|
560
|
+
} else {
|
|
561
|
+
this.logger("in: " + (this._trimLongStrings ? JSON.stringify(pkt, jsonLoggerHelper) : data));
|
|
562
|
+
|
|
563
|
+
// Send complete packet to listener
|
|
564
|
+
if (this.onMessage) {
|
|
565
|
+
this.onMessage(pkt);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (pkt.ctrl) {
|
|
569
|
+
// Handling {ctrl} message
|
|
570
|
+
if (this.onCtrlMessage) {
|
|
571
|
+
this.onCtrlMessage(pkt.ctrl);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Resolve or reject a pending promise, if any
|
|
575
|
+
if (pkt.ctrl.id) {
|
|
576
|
+
this.#execPromise(pkt.ctrl.id, pkt.ctrl.code, pkt.ctrl, pkt.ctrl.text);
|
|
577
|
+
}
|
|
578
|
+
setTimeout(_ => {
|
|
579
|
+
if (pkt.ctrl.code == 205 && pkt.ctrl.text == 'evicted') {
|
|
580
|
+
// User evicted from topic.
|
|
581
|
+
const topic = this.#cacheGet('topic', pkt.ctrl.topic);
|
|
582
|
+
if (topic) {
|
|
583
|
+
topic._resetSub();
|
|
584
|
+
if (pkt.ctrl.params && pkt.ctrl.params.unsub) {
|
|
585
|
+
topic._gone();
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
} else if (pkt.ctrl.code < 300 && pkt.ctrl.params) {
|
|
589
|
+
if (pkt.ctrl.params.what == 'data') {
|
|
590
|
+
// code=208, all messages received: "params":{"count":11,"what":"data"},
|
|
591
|
+
const topic = this.#cacheGet('topic', pkt.ctrl.topic);
|
|
592
|
+
if (topic) {
|
|
593
|
+
topic._allMessagesReceived(pkt.ctrl.params.count);
|
|
594
|
+
}
|
|
595
|
+
} else if (pkt.ctrl.params.what == 'sub') {
|
|
596
|
+
// code=204, the topic has no (refreshed) subscriptions.
|
|
597
|
+
const topic = this.#cacheGet('topic', pkt.ctrl.topic);
|
|
598
|
+
if (topic) {
|
|
599
|
+
// Trigger topic.onSubsUpdated.
|
|
600
|
+
topic._processMetaSubs([]);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}, 0);
|
|
605
|
+
} else {
|
|
606
|
+
setTimeout(_ => {
|
|
607
|
+
if (pkt.meta) {
|
|
608
|
+
// Handling a {meta} message.
|
|
609
|
+
// Preferred API: Route meta to topic, if one is registered
|
|
610
|
+
const topic = this.#cacheGet('topic', pkt.meta.topic);
|
|
611
|
+
if (topic) {
|
|
612
|
+
topic._routeMeta(pkt.meta);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (pkt.meta.id) {
|
|
616
|
+
this.#execPromise(pkt.meta.id, 200, pkt.meta, 'META');
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Secondary API: callback
|
|
620
|
+
if (this.onMetaMessage) {
|
|
621
|
+
this.onMetaMessage(pkt.meta);
|
|
622
|
+
}
|
|
623
|
+
} else if (pkt.data) {
|
|
624
|
+
// Handling {data} message
|
|
625
|
+
// Preferred API: Route data to topic, if one is registered
|
|
626
|
+
const topic = this.#cacheGet('topic', pkt.data.topic);
|
|
627
|
+
if (topic) {
|
|
628
|
+
topic._routeData(pkt.data);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Secondary API: Call callback
|
|
632
|
+
if (this.onDataMessage) {
|
|
633
|
+
this.onDataMessage(pkt.data);
|
|
634
|
+
}
|
|
635
|
+
} else if (pkt.pres) {
|
|
636
|
+
// Handling {pres} message
|
|
637
|
+
// Preferred API: Route presence to topic, if one is registered
|
|
638
|
+
const topic = this.#cacheGet('topic', pkt.pres.topic);
|
|
639
|
+
if (topic) {
|
|
640
|
+
topic._routePres(pkt.pres);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Secondary API - callback
|
|
644
|
+
if (this.onPresMessage) {
|
|
645
|
+
this.onPresMessage(pkt.pres);
|
|
646
|
+
}
|
|
647
|
+
} else if (pkt.info) {
|
|
648
|
+
// {info} message - read/received notifications and key presses
|
|
649
|
+
// Preferred API: Route {info}} to topic, if one is registered
|
|
650
|
+
const topic = this.#cacheGet('topic', pkt.info.topic);
|
|
651
|
+
if (topic) {
|
|
652
|
+
topic._routeInfo(pkt.info);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Secondary API - callback
|
|
656
|
+
if (this.onInfoMessage) {
|
|
657
|
+
this.onInfoMessage(pkt.info);
|
|
658
|
+
}
|
|
659
|
+
} else {
|
|
660
|
+
this.logger("ERROR: Unknown packet received.");
|
|
661
|
+
}
|
|
662
|
+
}, 0);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Connection open, ready to start sending.
|
|
668
|
+
#connectionOpen() {
|
|
669
|
+
if (!this._expirePromises) {
|
|
670
|
+
// Reject promises which have not been resolved for too long.
|
|
671
|
+
this._expirePromises = setInterval(_ => {
|
|
672
|
+
const err = new CommError("timeout", 504);
|
|
673
|
+
const expires = new Date(new Date().getTime() - Const.EXPIRE_PROMISES_TIMEOUT);
|
|
674
|
+
for (let id in this._pendingPromises) {
|
|
675
|
+
let callbacks = this._pendingPromises[id];
|
|
676
|
+
if (callbacks && callbacks.ts < expires) {
|
|
677
|
+
this.logger("Promise expired", id);
|
|
678
|
+
delete this._pendingPromises[id];
|
|
679
|
+
if (callbacks.reject) {
|
|
680
|
+
callbacks.reject(err);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}, Const.EXPIRE_PROMISES_PERIOD);
|
|
685
|
+
}
|
|
686
|
+
this.hello();
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
#disconnected(err, code) {
|
|
690
|
+
this._inPacketCount = 0;
|
|
691
|
+
this._serverInfo = null;
|
|
692
|
+
this._authenticated = false;
|
|
693
|
+
|
|
694
|
+
if (this._expirePromises) {
|
|
695
|
+
clearInterval(this._expirePromises);
|
|
696
|
+
this._expirePromises = null;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Mark all topics as unsubscribed
|
|
700
|
+
this.#cacheMap('topic', (topic, key) => {
|
|
701
|
+
topic._resetSub();
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
// Reject all pending promises
|
|
705
|
+
for (let key in this._pendingPromises) {
|
|
706
|
+
const callbacks = this._pendingPromises[key];
|
|
707
|
+
if (callbacks && callbacks.reject) {
|
|
708
|
+
callbacks.reject(err);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
this._pendingPromises = {};
|
|
712
|
+
|
|
713
|
+
if (this.onDisconnect) {
|
|
714
|
+
this.onDisconnect(err);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Get User Agent string
|
|
719
|
+
#getUserAgent() {
|
|
720
|
+
return this._appName + ' (' + (this._browser ? this._browser + '; ' : '') + this._hwos + '); ' + Const.LIBRARY;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Generator of packets stubs
|
|
724
|
+
#initPacket(type, topic) {
|
|
725
|
+
switch (type) {
|
|
726
|
+
case 'hi':
|
|
727
|
+
return {
|
|
728
|
+
'hi': {
|
|
729
|
+
'id': this.getNextUniqueId(),
|
|
730
|
+
'ver': Const.VERSION,
|
|
731
|
+
'ua': this.#getUserAgent(),
|
|
732
|
+
'dev': this._deviceToken,
|
|
733
|
+
'lang': this._humanLanguage,
|
|
734
|
+
'platf': this._platform
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
case 'acc':
|
|
739
|
+
return {
|
|
740
|
+
'acc': {
|
|
741
|
+
'id': this.getNextUniqueId(),
|
|
742
|
+
'user': null,
|
|
743
|
+
'scheme': null,
|
|
744
|
+
'secret': null,
|
|
745
|
+
'tmpscheme': null,
|
|
746
|
+
'tmpsecret': null,
|
|
747
|
+
'login': false,
|
|
748
|
+
'tags': null,
|
|
749
|
+
'desc': {},
|
|
750
|
+
'cred': {}
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
case 'login':
|
|
755
|
+
return {
|
|
756
|
+
'login': {
|
|
757
|
+
'id': this.getNextUniqueId(),
|
|
758
|
+
'scheme': null,
|
|
759
|
+
'secret': null
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
case 'sub':
|
|
764
|
+
return {
|
|
765
|
+
'sub': {
|
|
766
|
+
'id': this.getNextUniqueId(),
|
|
767
|
+
'topic': topic,
|
|
768
|
+
'set': {},
|
|
769
|
+
'get': {}
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
case 'leave':
|
|
774
|
+
return {
|
|
775
|
+
'leave': {
|
|
776
|
+
'id': this.getNextUniqueId(),
|
|
777
|
+
'topic': topic,
|
|
778
|
+
'unsub': false
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
|
|
782
|
+
case 'pub':
|
|
783
|
+
return {
|
|
784
|
+
'pub': {
|
|
785
|
+
'id': this.getNextUniqueId(),
|
|
786
|
+
'topic': topic,
|
|
787
|
+
'noecho': false,
|
|
788
|
+
'head': null,
|
|
789
|
+
'content': {}
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
case 'get':
|
|
794
|
+
return {
|
|
795
|
+
'get': {
|
|
796
|
+
'id': this.getNextUniqueId(),
|
|
797
|
+
'topic': topic,
|
|
798
|
+
'what': null,
|
|
799
|
+
'desc': {},
|
|
800
|
+
'sub': {},
|
|
801
|
+
'data': {}
|
|
802
|
+
}
|
|
803
|
+
};
|
|
804
|
+
|
|
805
|
+
case 'set':
|
|
806
|
+
return {
|
|
807
|
+
'set': {
|
|
808
|
+
'id': this.getNextUniqueId(),
|
|
809
|
+
'topic': topic,
|
|
810
|
+
'desc': {},
|
|
811
|
+
'sub': {},
|
|
812
|
+
'tags': [],
|
|
813
|
+
'aux': {}
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
case 'del':
|
|
818
|
+
return {
|
|
819
|
+
'del': {
|
|
820
|
+
'id': this.getNextUniqueId(),
|
|
821
|
+
'topic': topic,
|
|
822
|
+
'what': null,
|
|
823
|
+
'delseq': null,
|
|
824
|
+
'user': null,
|
|
825
|
+
'hard': false
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
case 'note':
|
|
830
|
+
return {
|
|
831
|
+
'note': {
|
|
832
|
+
// no id by design (except calls).
|
|
833
|
+
'topic': topic,
|
|
834
|
+
'what': null, // one of "recv", "read", "kp", "call"
|
|
835
|
+
'seq': undefined // the server-side message id acknowledged as received or read.
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
|
|
839
|
+
default:
|
|
840
|
+
throw new Error(`Unknown packet type requested: ${type}`);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Cache management
|
|
845
|
+
#cachePut(type, name, obj) {
|
|
846
|
+
this._cache[type + ':' + name] = obj;
|
|
847
|
+
}
|
|
848
|
+
#cacheGet(type, name) {
|
|
849
|
+
return this._cache[type + ':' + name];
|
|
850
|
+
}
|
|
851
|
+
#cacheDel(type, name) {
|
|
852
|
+
delete this._cache[type + ':' + name];
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// Enumerate all items in cache, call func for each item.
|
|
856
|
+
// Enumeration stops if func returns true.
|
|
857
|
+
#cacheMap(type, func, context) {
|
|
858
|
+
const key = type ? type + ':' : undefined;
|
|
859
|
+
for (let idx in this._cache) {
|
|
860
|
+
if (!key || idx.indexOf(key) == 0) {
|
|
861
|
+
if (func.call(context, this._cache[idx], idx)) {
|
|
862
|
+
break;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// Make limited cache management available to topic.
|
|
869
|
+
// Caching user.public only. Everything else is per-topic.
|
|
870
|
+
#attachCacheToTopic(topic) {
|
|
871
|
+
topic._tinode = this;
|
|
872
|
+
|
|
873
|
+
topic._cacheGetUser = (uid) => {
|
|
874
|
+
const pub = this.#cacheGet('user', uid);
|
|
875
|
+
if (pub) {
|
|
876
|
+
return {
|
|
877
|
+
user: uid,
|
|
878
|
+
public: mergeObj({}, pub)
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
return undefined;
|
|
882
|
+
};
|
|
883
|
+
topic._cachePutUser = (uid, user) => {
|
|
884
|
+
this.#cachePut('user', uid, mergeObj({}, user.public));
|
|
885
|
+
};
|
|
886
|
+
topic._cacheDelUser = (uid) => {
|
|
887
|
+
this.#cacheDel('user', uid);
|
|
888
|
+
};
|
|
889
|
+
topic._cachePutSelf = _ => {
|
|
890
|
+
this.#cachePut('topic', topic.name, topic);
|
|
891
|
+
};
|
|
892
|
+
topic._cacheDelSelf = _ => {
|
|
893
|
+
this.#cacheDel('topic', topic.name);
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// On successful login save server-provided data.
|
|
898
|
+
#loginSuccessful(ctrl) {
|
|
899
|
+
if (!ctrl.params || !ctrl.params.user) {
|
|
900
|
+
return ctrl;
|
|
901
|
+
}
|
|
902
|
+
// This is a response to a successful login,
|
|
903
|
+
// extract UID and security token, save it in Tinode module
|
|
904
|
+
this._myUID = ctrl.params.user;
|
|
905
|
+
this._authenticated = (ctrl && ctrl.code >= 200 && ctrl.code < 300);
|
|
906
|
+
if (ctrl.params && ctrl.params.token && ctrl.params.expires) {
|
|
907
|
+
this._authToken = {
|
|
908
|
+
token: ctrl.params.token,
|
|
909
|
+
expires: ctrl.params.expires
|
|
910
|
+
};
|
|
911
|
+
} else {
|
|
912
|
+
this._authToken = null;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
if (this.onLogin) {
|
|
916
|
+
this.onLogin(ctrl.code, ctrl.text);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
return ctrl;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// Static methods.
|
|
923
|
+
/**
|
|
924
|
+
* Helper method to package account credential.
|
|
925
|
+
*
|
|
926
|
+
* @param {string | Credential} meth - validation method or object with validation data.
|
|
927
|
+
* @param {string=} val - validation value (e.g. email or phone number).
|
|
928
|
+
* @param {Object=} params - validation parameters.
|
|
929
|
+
* @param {string=} resp - validation response.
|
|
930
|
+
*
|
|
931
|
+
* @returns {Array.<Credential>} array with a single credential or <code>null</code> if no valid credentials were given.
|
|
932
|
+
*/
|
|
933
|
+
static credential(meth, val, params, resp) {
|
|
934
|
+
if (typeof meth == 'object') {
|
|
935
|
+
({
|
|
936
|
+
val,
|
|
937
|
+
params,
|
|
938
|
+
resp,
|
|
939
|
+
meth
|
|
940
|
+
} = meth);
|
|
941
|
+
}
|
|
942
|
+
if (meth && (val || resp)) {
|
|
943
|
+
return [{
|
|
944
|
+
'meth': meth,
|
|
945
|
+
'val': val,
|
|
946
|
+
'resp': resp,
|
|
947
|
+
'params': params
|
|
948
|
+
}];
|
|
949
|
+
}
|
|
950
|
+
return null;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Determine topic type from topic's name: grp, p2p, me, fnd, sys.
|
|
955
|
+
* @param {string} name - Name of the topic to test.
|
|
956
|
+
* @returns {string} One of <code>"me"</code>, <code>"fnd"</code>, <code>"sys"</code>, <code>"grp"</code>,
|
|
957
|
+
* <code>"p2p"</code> or <code>undefined</code>.
|
|
958
|
+
*/
|
|
959
|
+
static topicType(name) {
|
|
960
|
+
return Topic.topicType(name);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* Check if the given topic name is a name of a 'me' topic.
|
|
965
|
+
* @param {string} name - Name of the topic to test.
|
|
966
|
+
* @returns {boolean} <code>true</code> if the name is a name of a 'me' topic, <code>false</code> otherwise.
|
|
967
|
+
*/
|
|
968
|
+
static isMeTopicName(name) {
|
|
969
|
+
return Topic.isMeTopicName(name);
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Check if the given topic name is a name of a 'slf' topic.
|
|
973
|
+
* @param {string} name - Name of the topic to test.
|
|
974
|
+
* @returns {boolean} <code>true</code> if the name is a name of a 'slf' topic, <code>false</code> otherwise.
|
|
975
|
+
*/
|
|
976
|
+
static isSelfTopicName(name) {
|
|
977
|
+
return Topic.isSelfTopicName(name);
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Check if the given topic name is a name of a group topic.
|
|
981
|
+
* @param {string} name - Name of the topic to test.
|
|
982
|
+
* @returns {boolean} <code>true</code> if the name is a name of a group topic, <code>false</code> otherwise.
|
|
983
|
+
*/
|
|
984
|
+
static isGroupTopicName(name) {
|
|
985
|
+
return Topic.isGroupTopicName(name);
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* Check if the given topic name is a name of a p2p topic.
|
|
989
|
+
* @param {string} name - Name of the topic to test.
|
|
990
|
+
* @returns {boolean} <code>true</code> if the name is a name of a p2p topic, <code>false</code> otherwise.
|
|
991
|
+
*/
|
|
992
|
+
static isP2PTopicName(name) {
|
|
993
|
+
return Topic.isP2PTopicName(name);
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Check if the given topic name is a name of a communication topic, i.e. P2P or group.
|
|
997
|
+
* @param {string} name - Name of the topic to test.
|
|
998
|
+
* @returns {boolean} <code>true</code> if the name is a name of a p2p or group topic, <code>false</code> otherwise.
|
|
999
|
+
*/
|
|
1000
|
+
static isCommTopicName(name) {
|
|
1001
|
+
return Topic.isCommTopicName(name);
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Check if the topic name is a name of a new topic.
|
|
1005
|
+
* @param {string} name - topic name to check.
|
|
1006
|
+
* @returns {boolean} <code>true</code> if the name is a name of a new topic, <code>false</code> otherwise.
|
|
1007
|
+
*/
|
|
1008
|
+
static isNewGroupTopicName(name) {
|
|
1009
|
+
return Topic.isNewGroupTopicName(name);
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Check if the topic name is a name of a channel.
|
|
1013
|
+
* @param {string} name - topic name to check.
|
|
1014
|
+
* @returns {boolean} <code>true</code> if the name is a name of a channel, <code>false</code> otherwise.
|
|
1015
|
+
*/
|
|
1016
|
+
static isChannelTopicName(name) {
|
|
1017
|
+
return Topic.isChannelTopicName(name);
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Get information about the current version of this Tinode client library.
|
|
1021
|
+
* @returns {string} semantic version of the library, e.g. <code>"0.15.5-rc1"</code>.
|
|
1022
|
+
*/
|
|
1023
|
+
static getVersion() {
|
|
1024
|
+
return Const.VERSION;
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* To use Tinode in a non browser context, supply WebSocket and XMLHttpRequest providers.
|
|
1028
|
+
* @static
|
|
1029
|
+
*
|
|
1030
|
+
* @param wsProvider <code>WebSocket</code> provider, e.g. for nodeJS , <code>require('ws')</code>.
|
|
1031
|
+
* @param xhrProvider <code>XMLHttpRequest</code> provider, e.g. for node <code>require('xhr')</code>.
|
|
1032
|
+
*/
|
|
1033
|
+
static setNetworkProviders(wsProvider, xhrProvider) {
|
|
1034
|
+
WebSocketProvider = wsProvider;
|
|
1035
|
+
XHRProvider = xhrProvider;
|
|
1036
|
+
|
|
1037
|
+
Connection.setNetworkProviders(WebSocketProvider, XHRProvider);
|
|
1038
|
+
LargeFileHelper.setNetworkProvider(XHRProvider);
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* To use Tinode in a non browser context, supply <code>indexedDB</code> provider.
|
|
1042
|
+
* @static
|
|
1043
|
+
*
|
|
1044
|
+
* @param idbProvider <code>indexedDB</code> provider, e.g. for nodeJS , <code>require('fake-indexeddb')</code>.
|
|
1045
|
+
*/
|
|
1046
|
+
static setDatabaseProvider(idbProvider) {
|
|
1047
|
+
IndexedDBProvider = idbProvider;
|
|
1048
|
+
|
|
1049
|
+
DBCache.setDatabaseProvider(IndexedDBProvider);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* Set a custom storage provider (e.g., SQLiteStorage for React Native).
|
|
1054
|
+
* Must be called BEFORE creating Tinode instance with persist: true.
|
|
1055
|
+
* @static
|
|
1056
|
+
*
|
|
1057
|
+
* @param {Object} storage - Storage implementation with the same interface as DB class.
|
|
1058
|
+
*/
|
|
1059
|
+
static setStorageProvider(storage) {
|
|
1060
|
+
DBCache.setStorageProvider(storage);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/**
|
|
1064
|
+
* Return information about the current name and version of this Tinode library.
|
|
1065
|
+
* @static
|
|
1066
|
+
*
|
|
1067
|
+
* @returns {string} the name of the library and it's version.
|
|
1068
|
+
*/
|
|
1069
|
+
static getLibrary() {
|
|
1070
|
+
return Const.LIBRARY;
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Check if the given string represents <code>NULL</code> value as defined by Tinode (<code>'\u2421'</code>).
|
|
1074
|
+
* @param {string} str - string to check for <code>NULL</code> value.
|
|
1075
|
+
* @returns {boolean} <code>true</code> if string represents <code>NULL</code> value, <code>false</code> otherwise.
|
|
1076
|
+
*/
|
|
1077
|
+
static isNullValue(str) {
|
|
1078
|
+
return str === Const.DEL_CHAR;
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Check if the given seq ID is likely to be issued by the server as oppisite to being temporary locally assigned ID.
|
|
1082
|
+
* @param {int} seq - seq ID to check.
|
|
1083
|
+
* @returns {boolean} <code>true</code> if seq is likely server-issued, <code>false</code> otherwise.
|
|
1084
|
+
*/
|
|
1085
|
+
static isServerAssignedSeq(seq) {
|
|
1086
|
+
return seq > 0 && seq < Const.LOCAL_SEQID;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
/**
|
|
1090
|
+
* Check if the given string is a valid tag value.
|
|
1091
|
+
* @param {string} tag - string to check.
|
|
1092
|
+
* @returns {boolean} <code>true</code> if the string is a valid tag value, <code>false</code> otherwise.
|
|
1093
|
+
*/
|
|
1094
|
+
static isValidTagValue(tag) {
|
|
1095
|
+
// 4-24 characters, starting with letter or digit, then letters, digits, hyphen, underscore.
|
|
1096
|
+
const ALIAS_REGEX = /^[a-z0-9][a-z0-9_\-]{3,23}$/i;
|
|
1097
|
+
return tag && typeof tag == 'string' && tag.length > 3 && tag.length < 24 && ALIAS_REGEX.test(tag);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* Split fully-qualified tag into prefix and value.
|
|
1102
|
+
*/
|
|
1103
|
+
static tagSplit(tag) {
|
|
1104
|
+
if (!tag) {
|
|
1105
|
+
return null;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
tag = tag.trim();
|
|
1109
|
+
|
|
1110
|
+
const splitAt = tag.indexOf(':');
|
|
1111
|
+
if (splitAt <= 0) {
|
|
1112
|
+
// Invalid syntax.
|
|
1113
|
+
return null;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
const value = tag.substring(splitAt + 1);
|
|
1117
|
+
if (!value) {
|
|
1118
|
+
return null;
|
|
1119
|
+
}
|
|
1120
|
+
return {
|
|
1121
|
+
prefix: tag.substring(0, splitAt),
|
|
1122
|
+
value: value
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
/**
|
|
1127
|
+
* Set a unique namespace tag.
|
|
1128
|
+
* If the tag with this namespace is already present then it's replaced with the new tag.
|
|
1129
|
+
* @param uniqueTag tag to add, must be fully-qualified; if null or empty, no action is taken.
|
|
1130
|
+
*/
|
|
1131
|
+
static setUniqueTag(tags, uniqueTag) {
|
|
1132
|
+
if (!tags || tags.length == 0) {
|
|
1133
|
+
// No tags, just add the new one.
|
|
1134
|
+
return [uniqueTag];
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
const parts = Tinode.tagSplit(uniqueTag)
|
|
1138
|
+
if (!parts) {
|
|
1139
|
+
// Invalid tag.
|
|
1140
|
+
return tags;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// Remove the old tag with the same prefix.
|
|
1144
|
+
tags = tags.filter(tag => tag && !tag.startsWith(parts.prefix));
|
|
1145
|
+
// Add the new tag.
|
|
1146
|
+
tags.push(uniqueTag);
|
|
1147
|
+
return tags;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* Remove a unique tag with the given prefix.
|
|
1152
|
+
* @param prefix prefix to remove
|
|
1153
|
+
*/
|
|
1154
|
+
static clearTagPrefix(tags, prefix) {
|
|
1155
|
+
if (!tags || tags.length == 0) {
|
|
1156
|
+
return [];
|
|
1157
|
+
}
|
|
1158
|
+
return tags.filter(tag => tag && !tag.startsWith(prefix));
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
/**
|
|
1162
|
+
* Find the first tag with the given prefix.
|
|
1163
|
+
* @param prefix prefix to search for.
|
|
1164
|
+
* @return the first tag with the given prefix if found or <code>undefined</code>.
|
|
1165
|
+
*/
|
|
1166
|
+
static tagByPrefix(tags, prefix) {
|
|
1167
|
+
if (!tags) {
|
|
1168
|
+
return undefined;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
return tags.find(tag => tag && tag.startsWith(prefix));
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// Instance methods.
|
|
1175
|
+
|
|
1176
|
+
// Generates unique message IDs
|
|
1177
|
+
getNextUniqueId() {
|
|
1178
|
+
return (this._messageId != 0) ? '' + this._messageId++ : undefined;
|
|
1179
|
+
};
|
|
1180
|
+
|
|
1181
|
+
/**
|
|
1182
|
+
* Connect to the server.
|
|
1183
|
+
*
|
|
1184
|
+
* @param {string} host_ - name of the host to connect to.
|
|
1185
|
+
* @return {Promise} Promise resolved/rejected when the connection call completes:
|
|
1186
|
+
* <code>resolve()</code> is called without parameters, <code>reject()</code> receives the
|
|
1187
|
+
* <code>Error</code> as a single parameter.
|
|
1188
|
+
*/
|
|
1189
|
+
connect(host_) {
|
|
1190
|
+
return this._connection.connect(host_);
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
/**
|
|
1194
|
+
* Attempt to reconnect to the server immediately.
|
|
1195
|
+
*
|
|
1196
|
+
* @param {string} force - if <code>true</code>, reconnect even if there is a connection already.
|
|
1197
|
+
*/
|
|
1198
|
+
reconnect(force) {
|
|
1199
|
+
this._connection.reconnect(force);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
/**
|
|
1203
|
+
* Disconnect from the server.
|
|
1204
|
+
*/
|
|
1205
|
+
disconnect() {
|
|
1206
|
+
this._connection.disconnect();
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
/**
|
|
1210
|
+
* Clear persistent cache: remove IndexedDB.
|
|
1211
|
+
*
|
|
1212
|
+
* @return {Promise} Promise resolved/rejected when the operation is completed.
|
|
1213
|
+
*/
|
|
1214
|
+
clearStorage() {
|
|
1215
|
+
if (this._db.isReady()) {
|
|
1216
|
+
return this._db.deleteDatabase();
|
|
1217
|
+
}
|
|
1218
|
+
return Promise.resolve();
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
/**
|
|
1222
|
+
* Initialize persistent cache: create IndexedDB cache.
|
|
1223
|
+
*
|
|
1224
|
+
* @return {Promise} Promise resolved/rejected when the operation is completed.
|
|
1225
|
+
*/
|
|
1226
|
+
initStorage() {
|
|
1227
|
+
if (!this._db.isReady()) {
|
|
1228
|
+
return this._db.initDatabase();
|
|
1229
|
+
}
|
|
1230
|
+
return Promise.resolve();
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
/**
|
|
1234
|
+
* Send a network probe message to make sure the connection is alive.
|
|
1235
|
+
*/
|
|
1236
|
+
networkProbe() {
|
|
1237
|
+
this._connection.probe();
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
/**
|
|
1241
|
+
* Check for live connection to server.
|
|
1242
|
+
*
|
|
1243
|
+
* @returns {boolean} <code>true</code> if there is a live connection, <code>false</code> otherwise.
|
|
1244
|
+
*/
|
|
1245
|
+
isConnected() {
|
|
1246
|
+
return this._connection.isConnected();
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
/**
|
|
1250
|
+
* Check if connection is authenticated (last login was successful).
|
|
1251
|
+
*
|
|
1252
|
+
* @returns {boolean} <code>true</code> if authenticated, <code>false</code> otherwise.
|
|
1253
|
+
*/
|
|
1254
|
+
isAuthenticated() {
|
|
1255
|
+
return this._authenticated;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
/**
|
|
1259
|
+
* Add API key and auth token to the relative URL making it usable for getting data
|
|
1260
|
+
* from the server in a simple <code>HTTP GET</code> request.
|
|
1261
|
+
*
|
|
1262
|
+
* @param {string} URL - URL to wrap.
|
|
1263
|
+
* @returns {string} URL with appended API key and token, if valid token is present.
|
|
1264
|
+
*/
|
|
1265
|
+
authorizeURL(url) {
|
|
1266
|
+
if (typeof url != 'string') {
|
|
1267
|
+
return url;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
if (isUrlRelative(url)) {
|
|
1271
|
+
// Fake base to make the relative URL parseable.
|
|
1272
|
+
const base = 'scheme://host/';
|
|
1273
|
+
const parsed = new URL(url, base);
|
|
1274
|
+
if (this._apiKey) {
|
|
1275
|
+
parsed.searchParams.append('apikey', this._apiKey);
|
|
1276
|
+
}
|
|
1277
|
+
if (this._authToken && this._authToken.token) {
|
|
1278
|
+
parsed.searchParams.append('auth', 'token');
|
|
1279
|
+
parsed.searchParams.append('secret', this._authToken.token);
|
|
1280
|
+
}
|
|
1281
|
+
// Convert back to string and strip fake base URL except for the root slash.
|
|
1282
|
+
url = parsed.toString().substring(base.length - 1);
|
|
1283
|
+
}
|
|
1284
|
+
return url;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
/**
|
|
1288
|
+
* @typedef AccountParams
|
|
1289
|
+
* @type {Object}
|
|
1290
|
+
* @property {DefAcs=} defacs - Default access parameters for user's <code>me</code> topic.
|
|
1291
|
+
* @property {Object=} public - Public application-defined data exposed on <code>me</code> topic.
|
|
1292
|
+
* @property {Object=} private - Private application-defined data accessible on <code>me</code> topic.
|
|
1293
|
+
* @property {Object=} trusted - Trusted user data which can be set by a root user only.
|
|
1294
|
+
* @property {Array.<string>} tags - array of string tags for user discovery.
|
|
1295
|
+
* @property {string} scheme - Temporary authentication scheme for password reset.
|
|
1296
|
+
* @property {string} secret - Temporary authentication secret for password reset.
|
|
1297
|
+
* @property {Array.<string>=} attachments - Array of references to out of band attachments used in account description.
|
|
1298
|
+
*/
|
|
1299
|
+
/**
|
|
1300
|
+
* @typedef DefAcs
|
|
1301
|
+
* @type {Object}
|
|
1302
|
+
* @property {string=} auth - Access mode for <code>me</code> for authenticated users.
|
|
1303
|
+
* @property {string=} anon - Access mode for <code>me</code> for anonymous users.
|
|
1304
|
+
*/
|
|
1305
|
+
|
|
1306
|
+
/**
|
|
1307
|
+
* Create or update an account.
|
|
1308
|
+
*
|
|
1309
|
+
* @param {string} uid - User id to update
|
|
1310
|
+
* @param {string} scheme - Authentication scheme; <code>"basic"</code> and <code>"anonymous"</code> are the currently supported schemes.
|
|
1311
|
+
* @param {string} secret - Authentication secret, assumed to be already base64 encoded.
|
|
1312
|
+
* @param {boolean=} login - Use new account to authenticate current session
|
|
1313
|
+
* @param {AccountParams=} params - User data to pass to the server.
|
|
1314
|
+
*
|
|
1315
|
+
* @returns {Promise} Promise which will be resolved/rejected when server reply is received.
|
|
1316
|
+
*/
|
|
1317
|
+
account(uid, scheme, secret, login, params) {
|
|
1318
|
+
const pkt = this.#initPacket('acc');
|
|
1319
|
+
pkt.acc.user = uid;
|
|
1320
|
+
pkt.acc.scheme = scheme;
|
|
1321
|
+
pkt.acc.secret = secret;
|
|
1322
|
+
// Log in to the new account using selected scheme
|
|
1323
|
+
pkt.acc.login = login;
|
|
1324
|
+
|
|
1325
|
+
if (params) {
|
|
1326
|
+
pkt.acc.desc.defacs = params.defacs;
|
|
1327
|
+
pkt.acc.desc.public = params.public;
|
|
1328
|
+
pkt.acc.desc.private = params.private;
|
|
1329
|
+
pkt.acc.desc.trusted = params.trusted;
|
|
1330
|
+
|
|
1331
|
+
pkt.acc.tags = params.tags;
|
|
1332
|
+
pkt.acc.cred = params.cred;
|
|
1333
|
+
|
|
1334
|
+
pkt.acc.tmpscheme = params.scheme;
|
|
1335
|
+
pkt.acc.tmpsecret = params.secret;
|
|
1336
|
+
|
|
1337
|
+
if (Array.isArray(params.attachments) && params.attachments.length > 0) {
|
|
1338
|
+
pkt.extra = {
|
|
1339
|
+
attachments: params.attachments.filter(ref => isUrlRelative(ref))
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
return this.#send(pkt, pkt.acc.id);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
/**
|
|
1348
|
+
* Create a new user. Wrapper for {@link Tinode#account}.
|
|
1349
|
+
*
|
|
1350
|
+
* @param {string} scheme - Authentication scheme; <code>"basic"</code> is the only currently supported scheme.
|
|
1351
|
+
* @param {string} secret - Authentication.
|
|
1352
|
+
* @param {boolean=} login - Use new account to authenticate current session
|
|
1353
|
+
* @param {AccountParams=} params - User data to pass to the server.
|
|
1354
|
+
*
|
|
1355
|
+
* @returns {Promise} Promise which will be resolved/rejected when server reply is received.
|
|
1356
|
+
*/
|
|
1357
|
+
createAccount(scheme, secret, login, params) {
|
|
1358
|
+
let promise = this.account(Const.USER_NEW, scheme, secret, login, params);
|
|
1359
|
+
if (login) {
|
|
1360
|
+
promise = promise.then(ctrl => this.#loginSuccessful(ctrl));
|
|
1361
|
+
}
|
|
1362
|
+
return promise;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
/**
|
|
1366
|
+
* Create user with <code>'basic'</code> authentication scheme and immediately
|
|
1367
|
+
* use it for authentication. Wrapper for {@link Tinode#account}.
|
|
1368
|
+
*
|
|
1369
|
+
* @param {string} username - Login to use for the new account.
|
|
1370
|
+
* @param {string} password - User's password.
|
|
1371
|
+
* @param {AccountParams=} params - User data to pass to the server.
|
|
1372
|
+
*
|
|
1373
|
+
* @returns {Promise} Promise which will be resolved/rejected when server reply is received.
|
|
1374
|
+
*/
|
|
1375
|
+
createAccountBasic(username, password, params) {
|
|
1376
|
+
// Make sure we are not using 'null' or 'undefined';
|
|
1377
|
+
username = username || '';
|
|
1378
|
+
password = password || '';
|
|
1379
|
+
return this.createAccount('basic',
|
|
1380
|
+
b64EncodeUnicode(username + ':' + password), true, params);
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
/**
|
|
1384
|
+
* Update user's credentials for <code>'basic'</code> authentication scheme. Wrapper for {@link Tinode#account}.
|
|
1385
|
+
*
|
|
1386
|
+
* @param {string} uid - User ID to update.
|
|
1387
|
+
* @param {string} username - Login to use for the new account.
|
|
1388
|
+
* @param {string} password - User's password.
|
|
1389
|
+
* @param {AccountParams=} params - data to pass to the server.
|
|
1390
|
+
*
|
|
1391
|
+
* @returns {Promise} Promise which will be resolved/rejected when server reply is received.
|
|
1392
|
+
*/
|
|
1393
|
+
updateAccountBasic(uid, username, password, params) {
|
|
1394
|
+
// Make sure we are not using 'null' or 'undefined';
|
|
1395
|
+
username = username || '';
|
|
1396
|
+
password = password || '';
|
|
1397
|
+
return this.account(uid, 'basic',
|
|
1398
|
+
b64EncodeUnicode(username + ':' + password), false, params);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
/**
|
|
1402
|
+
* Send handshake to the server.
|
|
1403
|
+
*
|
|
1404
|
+
* @returns {Promise} Promise which will be resolved/rejected when server reply is received.
|
|
1405
|
+
*/
|
|
1406
|
+
hello() {
|
|
1407
|
+
const pkt = this.#initPacket('hi');
|
|
1408
|
+
|
|
1409
|
+
return this.#send(pkt, pkt.hi.id)
|
|
1410
|
+
.then(ctrl => {
|
|
1411
|
+
// Reset backoff counter on successful connection.
|
|
1412
|
+
this._connection.backoffReset();
|
|
1413
|
+
|
|
1414
|
+
// Server response contains server protocol version, build, constraints,
|
|
1415
|
+
// session ID for long polling. Save them.
|
|
1416
|
+
if (ctrl.params) {
|
|
1417
|
+
this._serverInfo = ctrl.params;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
if (this.onConnect) {
|
|
1421
|
+
this.onConnect();
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
return ctrl;
|
|
1425
|
+
}).catch(err => {
|
|
1426
|
+
this._connection.reconnect(true);
|
|
1427
|
+
|
|
1428
|
+
if (this.onDisconnect) {
|
|
1429
|
+
this.onDisconnect(err);
|
|
1430
|
+
}
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
/**
|
|
1435
|
+
* Set or refresh the push notifications/device token. If the client is connected,
|
|
1436
|
+
* the deviceToken can be sent to the server.
|
|
1437
|
+
*
|
|
1438
|
+
* @param {string} dt - token obtained from the provider or <code>false</code>,
|
|
1439
|
+
* <code>null</code> or <code>undefined</code> to clear the token.
|
|
1440
|
+
*
|
|
1441
|
+
* @returns <code>true</code> if attempt was made to send the update to the server.
|
|
1442
|
+
*/
|
|
1443
|
+
setDeviceToken(dt) {
|
|
1444
|
+
let sent = false;
|
|
1445
|
+
// Convert any falsish value to null.
|
|
1446
|
+
dt = dt || null;
|
|
1447
|
+
if (dt != this._deviceToken) {
|
|
1448
|
+
this._deviceToken = dt;
|
|
1449
|
+
if (this.isConnected() && this.isAuthenticated()) {
|
|
1450
|
+
this.#send({
|
|
1451
|
+
'hi': {
|
|
1452
|
+
'dev': dt || Tinode.DEL_CHAR
|
|
1453
|
+
}
|
|
1454
|
+
});
|
|
1455
|
+
sent = true;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
return sent;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
/**
|
|
1462
|
+
* @typedef Credential
|
|
1463
|
+
* @type {Object}
|
|
1464
|
+
* @property {string} meth - validation method.
|
|
1465
|
+
* @property {string} val - value to validate (e.g. email or phone number).
|
|
1466
|
+
* @property {string} resp - validation response.
|
|
1467
|
+
* @property {Object} params - validation parameters.
|
|
1468
|
+
*/
|
|
1469
|
+
/**
|
|
1470
|
+
* Authenticate current session.
|
|
1471
|
+
*
|
|
1472
|
+
* @param {string} scheme - Authentication scheme; <code>"basic"</code> is the only currently supported scheme.
|
|
1473
|
+
* @param {string} secret - Authentication secret, assumed to be already base64 encoded.
|
|
1474
|
+
* @param {Credential=} cred - credential confirmation, if required.
|
|
1475
|
+
*
|
|
1476
|
+
* @returns {Promise} Promise which will be resolved/rejected when server reply is received.
|
|
1477
|
+
*/
|
|
1478
|
+
login(scheme, secret, cred) {
|
|
1479
|
+
const pkt = this.#initPacket('login');
|
|
1480
|
+
pkt.login.scheme = scheme;
|
|
1481
|
+
pkt.login.secret = secret;
|
|
1482
|
+
pkt.login.cred = cred;
|
|
1483
|
+
|
|
1484
|
+
return this.#send(pkt, pkt.login.id)
|
|
1485
|
+
.then(ctrl => this.#loginSuccessful(ctrl));
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
/**
|
|
1489
|
+
* Wrapper for {@link Tinode#login} with basic authentication
|
|
1490
|
+
*
|
|
1491
|
+
* @param {string} uname - User name.
|
|
1492
|
+
* @param {string} password - Password.
|
|
1493
|
+
* @param {Credential=} cred - credential confirmation, if required.
|
|
1494
|
+
*
|
|
1495
|
+
* @returns {Promise} Promise which will be resolved/rejected on receiving server reply.
|
|
1496
|
+
*/
|
|
1497
|
+
loginBasic(uname, password, cred) {
|
|
1498
|
+
return this.login('basic', b64EncodeUnicode(uname + ':' + password), cred)
|
|
1499
|
+
.then(ctrl => {
|
|
1500
|
+
this._login = uname;
|
|
1501
|
+
return ctrl;
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
/**
|
|
1506
|
+
* Wrapper for {@link Tinode#login} with token authentication
|
|
1507
|
+
*
|
|
1508
|
+
* @param {string} token - Token received in response to earlier login.
|
|
1509
|
+
* @param {Credential=} cred - credential confirmation, if required.
|
|
1510
|
+
*
|
|
1511
|
+
* @returns {Promise} Promise which will be resolved/rejected on receiving server reply.
|
|
1512
|
+
*/
|
|
1513
|
+
loginToken(token, cred) {
|
|
1514
|
+
return this.login('token', token, cred);
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
/**
|
|
1518
|
+
* Send a request for resetting an authentication secret.
|
|
1519
|
+
*
|
|
1520
|
+
* @param {string} scheme - authentication scheme to reset.
|
|
1521
|
+
* @param {string} method - method to use for resetting the secret, such as "email" or "tel".
|
|
1522
|
+
* @param {string} value - value of the credential to use, a specific email address or a phone number.
|
|
1523
|
+
*
|
|
1524
|
+
* @returns {Promise} Promise which will be resolved/rejected on receiving the server reply.
|
|
1525
|
+
*/
|
|
1526
|
+
requestResetAuthSecret(scheme, method, value) {
|
|
1527
|
+
return this.login('reset', b64EncodeUnicode(scheme + ':' + method + ':' + value));
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
/**
|
|
1531
|
+
* @typedef AuthToken
|
|
1532
|
+
* @type {Object}
|
|
1533
|
+
* @property {string} token - Token value.
|
|
1534
|
+
* @property {Date} expires - Token expiration time.
|
|
1535
|
+
*/
|
|
1536
|
+
/**
|
|
1537
|
+
* Get stored authentication token.
|
|
1538
|
+
*
|
|
1539
|
+
* @returns {AuthToken} authentication token.
|
|
1540
|
+
*/
|
|
1541
|
+
getAuthToken() {
|
|
1542
|
+
if (this._authToken && (this._authToken.expires.getTime() > Date.now())) {
|
|
1543
|
+
return this._authToken;
|
|
1544
|
+
} else {
|
|
1545
|
+
this._authToken = null;
|
|
1546
|
+
}
|
|
1547
|
+
return null;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
/**
|
|
1551
|
+
* Application may provide a saved authentication token.
|
|
1552
|
+
*
|
|
1553
|
+
* @param {AuthToken} token - authentication token.
|
|
1554
|
+
*/
|
|
1555
|
+
setAuthToken(token) {
|
|
1556
|
+
this._authToken = token;
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
/**
|
|
1560
|
+
* @typedef SetParams
|
|
1561
|
+
* @type {Object}
|
|
1562
|
+
* @property {SetDesc=} desc - Topic initialization parameters when creating a new topic or a new subscription.
|
|
1563
|
+
* @property {SetSub=} sub - Subscription initialization parameters.
|
|
1564
|
+
* @property {Array.<string>=} tags - Search tags.
|
|
1565
|
+
* @property {Object} aux - Auxiliary topic data.
|
|
1566
|
+
* @property {Array.<string>=} attachments - URLs of out of band attachments used in parameters.
|
|
1567
|
+
*/
|
|
1568
|
+
/**
|
|
1569
|
+
* @typedef SetDesc
|
|
1570
|
+
* @type {Object}
|
|
1571
|
+
* @property {DefAcs=} defacs - Default access mode.
|
|
1572
|
+
* @property {Object=} public - Free-form topic description, publically accessible.
|
|
1573
|
+
* @property {Object=} private - Free-form topic description accessible only to the owner.
|
|
1574
|
+
* @property {Object=} trusted - Trusted user data which can be set by a root user only.
|
|
1575
|
+
*/
|
|
1576
|
+
/**
|
|
1577
|
+
* @typedef SetSub
|
|
1578
|
+
* @type {Object}
|
|
1579
|
+
* @property {string=} user - UID of the user affected by the request. Default (empty) - current user.
|
|
1580
|
+
* @property {string=} mode - User access mode, either requested or assigned dependent on context.
|
|
1581
|
+
*/
|
|
1582
|
+
/**
|
|
1583
|
+
* Send a topic subscription request.
|
|
1584
|
+
*
|
|
1585
|
+
* @param {string} topic - Name of the topic to subscribe to.
|
|
1586
|
+
* @param {GetQuery=} getParams - Optional subscription metadata query
|
|
1587
|
+
* @param {SetParams=} setParams - Optional initialization parameters
|
|
1588
|
+
*
|
|
1589
|
+
* @returns {Promise} Promise which will be resolved/rejected on receiving server reply.
|
|
1590
|
+
*/
|
|
1591
|
+
subscribe(topicName, getParams, setParams) {
|
|
1592
|
+
const pkt = this.#initPacket('sub', topicName)
|
|
1593
|
+
if (!topicName) {
|
|
1594
|
+
topicName = Const.TOPIC_NEW;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
pkt.sub.get = getParams;
|
|
1598
|
+
|
|
1599
|
+
if (setParams) {
|
|
1600
|
+
if (setParams.sub) {
|
|
1601
|
+
pkt.sub.set.sub = setParams.sub;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
if (setParams.desc) {
|
|
1605
|
+
const desc = setParams.desc;
|
|
1606
|
+
if (Tinode.isNewGroupTopicName(topicName)) {
|
|
1607
|
+
// Full set.desc params are used for new topics only
|
|
1608
|
+
pkt.sub.set.desc = desc;
|
|
1609
|
+
} else if (Tinode.isP2PTopicName(topicName) && desc.defacs) {
|
|
1610
|
+
// Use optional default permissions only.
|
|
1611
|
+
pkt.sub.set.desc = {
|
|
1612
|
+
defacs: desc.defacs
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
// See if external objects were used in topic description.
|
|
1618
|
+
if (Array.isArray(setParams.attachments) && setParams.attachments.length > 0) {
|
|
1619
|
+
pkt.extra = {
|
|
1620
|
+
attachments: setParams.attachments.filter(ref => isUrlRelative(ref))
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
if (setParams.tags) {
|
|
1625
|
+
pkt.sub.set.tags = setParams.tags;
|
|
1626
|
+
}
|
|
1627
|
+
if (setParams.aux) {
|
|
1628
|
+
pkt.sub.set.aux = setParams.aux;
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
return this.#send(pkt, pkt.sub.id);
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
/**
|
|
1635
|
+
* Detach and optionally unsubscribe from the topic
|
|
1636
|
+
*
|
|
1637
|
+
* @param {string} topic - Topic to detach from.
|
|
1638
|
+
* @param {boolean} unsub - If <code>true</code>, detach and unsubscribe, otherwise just detach.
|
|
1639
|
+
*
|
|
1640
|
+
* @returns {Promise} Promise which will be resolved/rejected on receiving server reply.
|
|
1641
|
+
*/
|
|
1642
|
+
leave(topic, unsub) {
|
|
1643
|
+
const pkt = this.#initPacket('leave', topic);
|
|
1644
|
+
pkt.leave.unsub = unsub;
|
|
1645
|
+
|
|
1646
|
+
return this.#send(pkt, pkt.leave.id);
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
/**
|
|
1650
|
+
* Create message draft without sending it to the server.
|
|
1651
|
+
*
|
|
1652
|
+
* @param {string} topic - Name of the topic to publish to.
|
|
1653
|
+
* @param {Object} content - Payload to publish.
|
|
1654
|
+
* @param {boolean=} noEcho - If <code>true</code>, tell the server not to echo the message to the original session.
|
|
1655
|
+
*
|
|
1656
|
+
* @returns {Object} new message which can be sent to the server or otherwise used.
|
|
1657
|
+
*/
|
|
1658
|
+
createMessage(topic, content, noEcho) {
|
|
1659
|
+
const pkt = this.#initPacket('pub', topic);
|
|
1660
|
+
|
|
1661
|
+
let dft = typeof content == 'string' ? Drafty.parse(content) : content;
|
|
1662
|
+
if (dft && !Drafty.isPlainText(dft)) {
|
|
1663
|
+
pkt.pub.head = {
|
|
1664
|
+
mime: Drafty.getContentType()
|
|
1665
|
+
};
|
|
1666
|
+
content = dft;
|
|
1667
|
+
}
|
|
1668
|
+
pkt.pub.noecho = noEcho;
|
|
1669
|
+
pkt.pub.content = content;
|
|
1670
|
+
|
|
1671
|
+
return pkt.pub;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
/**
|
|
1675
|
+
* Publish {data} message to topic.
|
|
1676
|
+
*
|
|
1677
|
+
* @param {string} topicName - Name of the topic to publish to.
|
|
1678
|
+
* @param {Object} content - Payload to publish.
|
|
1679
|
+
* @param {boolean=} noEcho - If <code>true</code>, tell the server not to echo the message to the original session.
|
|
1680
|
+
*
|
|
1681
|
+
* @returns {Promise} Promise which will be resolved/rejected on receiving server reply.
|
|
1682
|
+
*/
|
|
1683
|
+
publish(topicName, content, noEcho) {
|
|
1684
|
+
return this.publishMessage(
|
|
1685
|
+
this.createMessage(topicName, content, noEcho)
|
|
1686
|
+
);
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
/**
|
|
1690
|
+
* Publish message to topic. The message should be created by {@link Tinode#createMessage}.
|
|
1691
|
+
*
|
|
1692
|
+
* @param {Object} pub - Message to publish.
|
|
1693
|
+
* @param {Array.<string>=} attachments - array of URLs with attachments.
|
|
1694
|
+
*
|
|
1695
|
+
* @returns {Promise} Promise which will be resolved/rejected on receiving server reply.
|
|
1696
|
+
*/
|
|
1697
|
+
publishMessage(pub, attachments) {
|
|
1698
|
+
// Make a shallow copy. Needed in order to clear locally-assigned temp values;
|
|
1699
|
+
pub = Object.assign({}, pub);
|
|
1700
|
+
pub.seq = undefined;
|
|
1701
|
+
pub.from = undefined;
|
|
1702
|
+
pub.ts = undefined;
|
|
1703
|
+
const msg = {
|
|
1704
|
+
pub: pub,
|
|
1705
|
+
};
|
|
1706
|
+
if (attachments) {
|
|
1707
|
+
msg.extra = {
|
|
1708
|
+
attachments: attachments.filter(ref => isUrlRelative(ref))
|
|
1709
|
+
};
|
|
1710
|
+
}
|
|
1711
|
+
return this.#send(msg, pub.id);
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
/**
|
|
1715
|
+
* Out of band notification: notify topic that an external (push) notification was recived by the client.
|
|
1716
|
+
*
|
|
1717
|
+
* @param {object} data - notification payload.
|
|
1718
|
+
* @param {string} data.what - notification type, 'msg', 'read', 'sub'.
|
|
1719
|
+
* @param {string} data.topic - name of the updated topic.
|
|
1720
|
+
* @param {number=} data.seq - seq ID of the affected message.
|
|
1721
|
+
* @param {string=} data.xfrom - UID of the sender.
|
|
1722
|
+
* @param {object=} data.given - new subscription 'given', e.g. 'ASWP...'.
|
|
1723
|
+
* @param {object=} data.want - new subscription 'want', e.g. 'RWJ...'.
|
|
1724
|
+
*/
|
|
1725
|
+
oobNotification(data) {
|
|
1726
|
+
this.logger('oob: ' + (this._trimLongStrings ? JSON.stringify(data, jsonLoggerHelper) : data));
|
|
1727
|
+
|
|
1728
|
+
switch (data.what) {
|
|
1729
|
+
case 'msg':
|
|
1730
|
+
if (!data.seq || data.seq < 1 || !data.topic) {
|
|
1731
|
+
// Server sent invalid data.
|
|
1732
|
+
break;
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
if (!this.isConnected()) {
|
|
1736
|
+
// Let's ignore the message if there is no connection: no connection means there are no open
|
|
1737
|
+
// tabs with Tinode.
|
|
1738
|
+
break;
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
const topic = this.#cacheGet('topic', data.topic);
|
|
1742
|
+
if (!topic) {
|
|
1743
|
+
// TODO: check if there is a case when a message can arrive from an unknown topic.
|
|
1744
|
+
break;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
if (topic.isSubscribed()) {
|
|
1748
|
+
// No need to fetch: topic is already subscribed and got data through normal channel.
|
|
1749
|
+
break;
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
if (topic.maxMsgSeq() < data.seq) {
|
|
1753
|
+
if (topic.isChannelType()) {
|
|
1754
|
+
topic._updateReceived(data.seq, 'fake-uid');
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
// New message.
|
|
1758
|
+
if (data.xfrom && !this.#cacheGet('user', data.xfrom)) {
|
|
1759
|
+
// Message from unknown sender, fetch description from the server.
|
|
1760
|
+
// Sending asynchronously without a subscription.
|
|
1761
|
+
this.getMeta(data.xfrom, new MetaGetBuilder().withDesc().build()).catch(err => {
|
|
1762
|
+
this.logger("Failed to get the name of a new sender", err);
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
topic.subscribe(null).then(_ => {
|
|
1767
|
+
return topic.getMeta(new MetaGetBuilder(topic).withLaterData(24).withLaterDel(24).build());
|
|
1768
|
+
}).then(_ => {
|
|
1769
|
+
// Allow data fetch to complete and get processed successfully.
|
|
1770
|
+
topic.leaveDelayed(false, 1000);
|
|
1771
|
+
}).catch(err => {
|
|
1772
|
+
this.logger("On push data fetch failed", err);
|
|
1773
|
+
}).finally(_ => {
|
|
1774
|
+
this.getMeTopic()._refreshContact('msg', topic);
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
break;
|
|
1778
|
+
|
|
1779
|
+
case 'read':
|
|
1780
|
+
this.getMeTopic()._routePres({
|
|
1781
|
+
what: 'read',
|
|
1782
|
+
seq: data.seq
|
|
1783
|
+
});
|
|
1784
|
+
break;
|
|
1785
|
+
|
|
1786
|
+
case 'sub':
|
|
1787
|
+
if (!this.isMe(data.xfrom)) {
|
|
1788
|
+
// TODO: handle updates from other users.
|
|
1789
|
+
break;
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
const mode = {
|
|
1793
|
+
given: data.modeGiven,
|
|
1794
|
+
want: data.modeWant
|
|
1795
|
+
};
|
|
1796
|
+
const acs = new AccessMode(mode);
|
|
1797
|
+
const pres = (!acs.mode || acs.mode == AccessMode._NONE) ?
|
|
1798
|
+
// Subscription deleted.
|
|
1799
|
+
{
|
|
1800
|
+
what: 'gone',
|
|
1801
|
+
src: data.topic
|
|
1802
|
+
} :
|
|
1803
|
+
// New subscription or subscription updated.
|
|
1804
|
+
{
|
|
1805
|
+
what: 'acs',
|
|
1806
|
+
src: data.topic,
|
|
1807
|
+
dacs: mode
|
|
1808
|
+
};
|
|
1809
|
+
this.getMeTopic()._routePres(pres);
|
|
1810
|
+
break;
|
|
1811
|
+
|
|
1812
|
+
default:
|
|
1813
|
+
this.logger("Unknown push type ignored", data.what);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
/**
|
|
1818
|
+
* @typedef GetOptsType
|
|
1819
|
+
* @type {Object}
|
|
1820
|
+
* @property {Date=} ims - "If modified since", fetch data only it was was modified since stated date.
|
|
1821
|
+
* @property {number=} limit - Maximum number of results to return. Ignored when querying topic description.
|
|
1822
|
+
*/
|
|
1823
|
+
|
|
1824
|
+
/**
|
|
1825
|
+
* @typedef GetDataType
|
|
1826
|
+
* @type {Object}
|
|
1827
|
+
* @property {number=} since - Load messages with seq ID equal or greater than this value.
|
|
1828
|
+
* @property {number=} before - Load messages with seq ID lower than this number.
|
|
1829
|
+
* @property {number=} limit - Maximum number of results to return.
|
|
1830
|
+
* @property {Array.<SeqRange>=} range - Ranges of seq IDs to fetch.
|
|
1831
|
+
*/
|
|
1832
|
+
|
|
1833
|
+
/**
|
|
1834
|
+
* @typedef GetQuery
|
|
1835
|
+
* @type {Object}
|
|
1836
|
+
* @property {GetOptsType=} desc - If provided (even if empty), fetch topic description.
|
|
1837
|
+
* @property {GetOptsType=} sub - If provided (even if empty), fetch topic subscriptions.
|
|
1838
|
+
* @property {GetDataType=} data - If provided (even if empty), get messages.
|
|
1839
|
+
*/
|
|
1840
|
+
|
|
1841
|
+
/**
|
|
1842
|
+
* Request topic metadata
|
|
1843
|
+
*
|
|
1844
|
+
* @param {string} topic - Name of the topic to query.
|
|
1845
|
+
* @param {GetQuery} params - Parameters of the query. Use {@link Tinode.MetaGetBuilder} to generate.
|
|
1846
|
+
*
|
|
1847
|
+
* @returns {Promise} Promise which will be resolved/rejected on receiving server reply.
|
|
1848
|
+
*/
|
|
1849
|
+
getMeta(topic, params) {
|
|
1850
|
+
const pkt = this.#initPacket('get', topic);
|
|
1851
|
+
|
|
1852
|
+
pkt.get = mergeObj(pkt.get, params);
|
|
1853
|
+
|
|
1854
|
+
return this.#send(pkt, pkt.get.id);
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
/**
|
|
1858
|
+
* Update topic's metadata: description, subscribtions.
|
|
1859
|
+
*
|
|
1860
|
+
* @param {string} topic - Topic to update.
|
|
1861
|
+
* @param {SetParams} params - topic metadata to update.
|
|
1862
|
+
* @returns {Promise} Promise which will be resolved/rejected on receiving server reply.
|
|
1863
|
+
*/
|
|
1864
|
+
setMeta(topic, params) {
|
|
1865
|
+
const pkt = this.#initPacket('set', topic);
|
|
1866
|
+
const what = [];
|
|
1867
|
+
|
|
1868
|
+
if (params) {
|
|
1869
|
+
['desc', 'sub', 'tags', 'cred', 'aux'].forEach(key => {
|
|
1870
|
+
if (params.hasOwnProperty(key)) {
|
|
1871
|
+
what.push(key);
|
|
1872
|
+
pkt.set[key] = params[key];
|
|
1873
|
+
}
|
|
1874
|
+
});
|
|
1875
|
+
|
|
1876
|
+
if (Array.isArray(params.attachments) && params.attachments.length > 0) {
|
|
1877
|
+
pkt.extra = {
|
|
1878
|
+
attachments: params.attachments.filter(ref => isUrlRelative(ref))
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
if (what.length == 0) {
|
|
1884
|
+
return Promise.reject(new Error("Invalid {set} parameters"));
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
return this.#send(pkt, pkt.set.id);
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
/**
|
|
1891
|
+
* Range of message IDs.
|
|
1892
|
+
*
|
|
1893
|
+
* @typedef SeqRange
|
|
1894
|
+
* @type {Object}
|
|
1895
|
+
* @property {number} low - low end of the range, inclusive (closed).
|
|
1896
|
+
* @property {number=} hi - high end of the range, exclusive (open).
|
|
1897
|
+
*/
|
|
1898
|
+
/**
|
|
1899
|
+
* Delete some or all messages in a topic.
|
|
1900
|
+
*
|
|
1901
|
+
* @param {string} topic - Topic name to delete messages from.
|
|
1902
|
+
* @param {Array.<SeqRange>} list - Ranges of message IDs to delete.
|
|
1903
|
+
* @param {boolean=} hard - Hard or soft delete
|
|
1904
|
+
*
|
|
1905
|
+
* @returns {Promise} Promise which will be resolved/rejected on receiving server reply.
|
|
1906
|
+
*/
|
|
1907
|
+
delMessages(topic, ranges, hard) {
|
|
1908
|
+
const pkt = this.#initPacket('del', topic);
|
|
1909
|
+
|
|
1910
|
+
pkt.del.what = 'msg';
|
|
1911
|
+
pkt.del.delseq = ranges;
|
|
1912
|
+
pkt.del.hard = hard;
|
|
1913
|
+
|
|
1914
|
+
return this.#send(pkt, pkt.del.id);
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
/**
|
|
1918
|
+
* Delete the topic alltogether. Requires Owner permission.
|
|
1919
|
+
*
|
|
1920
|
+
* @param {string} topicName - Name of the topic to delete
|
|
1921
|
+
* @param {boolean} hard - hard-delete topic.
|
|
1922
|
+
* @returns {Promise} Promise which will be resolved/rejected on receiving server reply.
|
|
1923
|
+
*/
|
|
1924
|
+
delTopic(topicName, hard) {
|
|
1925
|
+
const pkt = this.#initPacket('del', topicName);
|
|
1926
|
+
pkt.del.what = 'topic';
|
|
1927
|
+
pkt.del.hard = hard;
|
|
1928
|
+
|
|
1929
|
+
return this.#send(pkt, pkt.del.id);
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
/**
|
|
1933
|
+
* Delete subscription. Requires Share permission.
|
|
1934
|
+
*
|
|
1935
|
+
* @param {string} topicName - Name of the topic to delete
|
|
1936
|
+
* @param {string} user - User ID to remove.
|
|
1937
|
+
* @returns {Promise} Promise which will be resolved/rejected on receiving server reply.
|
|
1938
|
+
*/
|
|
1939
|
+
delSubscription(topicName, user) {
|
|
1940
|
+
const pkt = this.#initPacket('del', topicName);
|
|
1941
|
+
pkt.del.what = 'sub';
|
|
1942
|
+
pkt.del.user = user;
|
|
1943
|
+
|
|
1944
|
+
return this.#send(pkt, pkt.del.id);
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
/**
|
|
1948
|
+
* Delete credential. Always sent on <code>'me'</code> topic.
|
|
1949
|
+
*
|
|
1950
|
+
* @param {string} method - validation method such as <code>'email'</code> or <code>'tel'</code>.
|
|
1951
|
+
* @param {string} value - validation value, i.e. <code>'alice@example.com'</code>.
|
|
1952
|
+
* @returns {Promise} Promise which will be resolved/rejected on receiving server reply.
|
|
1953
|
+
*/
|
|
1954
|
+
delCredential(method, value) {
|
|
1955
|
+
const pkt = this.#initPacket('del', Const.TOPIC_ME);
|
|
1956
|
+
pkt.del.what = 'cred';
|
|
1957
|
+
pkt.del.cred = {
|
|
1958
|
+
meth: method,
|
|
1959
|
+
val: value
|
|
1960
|
+
};
|
|
1961
|
+
|
|
1962
|
+
return this.#send(pkt, pkt.del.id);
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
/**
|
|
1966
|
+
* Request to delete account of the current user.
|
|
1967
|
+
*
|
|
1968
|
+
* @param {boolean} hard - hard-delete user.
|
|
1969
|
+
* @returns {Promise} Promise which will be resolved/rejected on receiving server reply.
|
|
1970
|
+
*/
|
|
1971
|
+
delCurrentUser(hard) {
|
|
1972
|
+
const pkt = this.#initPacket('del', null);
|
|
1973
|
+
pkt.del.what = 'user';
|
|
1974
|
+
pkt.del.hard = hard;
|
|
1975
|
+
|
|
1976
|
+
return this.#send(pkt, pkt.del.id).then(_ => {
|
|
1977
|
+
this._myUID = null;
|
|
1978
|
+
});
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
/**
|
|
1982
|
+
* Notify server that a message or messages were read or received. Does NOT return promise.
|
|
1983
|
+
*
|
|
1984
|
+
* @param {string} topicName - Name of the topic where the mesage is being aknowledged.
|
|
1985
|
+
* @param {string} what - Action being aknowledged, either <code>"read"</code> or <code>"recv"</code>.
|
|
1986
|
+
* @param {number} seq - Maximum id of the message being acknowledged.
|
|
1987
|
+
* @throws {Error} if <code>seq</code> is invalid.
|
|
1988
|
+
*/
|
|
1989
|
+
note(topicName, what, seq) {
|
|
1990
|
+
if (seq <= 0 || seq >= Const.LOCAL_SEQID) {
|
|
1991
|
+
throw new Error(`Invalid message id ${seq}`);
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
const pkt = this.#initPacket('note', topicName);
|
|
1995
|
+
pkt.note.what = what;
|
|
1996
|
+
pkt.note.seq = seq;
|
|
1997
|
+
this.#send(pkt);
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
/**
|
|
2001
|
+
* Broadcast a key-press notification to topic subscribers. Used to show
|
|
2002
|
+
* typing notifications "user X is typing...".
|
|
2003
|
+
*
|
|
2004
|
+
* @param {string} topicName - Name of the topic to broadcast to.
|
|
2005
|
+
* @param {string=} type - notification to send, default is 'kp'.
|
|
2006
|
+
*/
|
|
2007
|
+
noteKeyPress(topicName, type) {
|
|
2008
|
+
const pkt = this.#initPacket('note', topicName);
|
|
2009
|
+
pkt.note.what = type || 'kp';
|
|
2010
|
+
this.#send(pkt);
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
/**
|
|
2014
|
+
* Send a video call notification to topic subscribers (including dialing,
|
|
2015
|
+
* hangup, etc.).
|
|
2016
|
+
*
|
|
2017
|
+
* @param {string} topicName - Name of the topic to broadcast to.
|
|
2018
|
+
* @param {int} seq - ID of the call message the event pertains to.
|
|
2019
|
+
* @param {string} evt - Call event.
|
|
2020
|
+
* @param {string} payload - Payload associated with this event (e.g. SDP string).
|
|
2021
|
+
*
|
|
2022
|
+
* @returns {Promise} Promise (for some call events) which will
|
|
2023
|
+
* be resolved/rejected on receiving server reply
|
|
2024
|
+
*/
|
|
2025
|
+
videoCall(topicName, seq, evt, payload) {
|
|
2026
|
+
const pkt = this.#initPacket('note', topicName);
|
|
2027
|
+
pkt.note.seq = seq;
|
|
2028
|
+
pkt.note.what = 'call';
|
|
2029
|
+
pkt.note.event = evt;
|
|
2030
|
+
pkt.note.payload = payload;
|
|
2031
|
+
this.#send(pkt, pkt.note.id);
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
/**
|
|
2035
|
+
* Get a named topic, either pull it from cache or create a new instance.
|
|
2036
|
+
* There is a single instance of topic for each name.
|
|
2037
|
+
*
|
|
2038
|
+
* @param {string} topicName - Name of the topic to get.
|
|
2039
|
+
*
|
|
2040
|
+
* @returns {Topic} Requested or newly created topic or <code>undefined</code> if topic name is invalid.
|
|
2041
|
+
*/
|
|
2042
|
+
getTopic(topicName) {
|
|
2043
|
+
let topic = this.#cacheGet('topic', topicName);
|
|
2044
|
+
if (!topic && topicName) {
|
|
2045
|
+
if (topicName == Const.TOPIC_ME) {
|
|
2046
|
+
topic = new TopicMe();
|
|
2047
|
+
} else if (topicName == Const.TOPIC_FND) {
|
|
2048
|
+
topic = new TopicFnd();
|
|
2049
|
+
} else {
|
|
2050
|
+
topic = new Topic(topicName);
|
|
2051
|
+
}
|
|
2052
|
+
// Cache management.
|
|
2053
|
+
this.#attachCacheToTopic(topic);
|
|
2054
|
+
topic._cachePutSelf();
|
|
2055
|
+
// Don't save to DB here: a record will be added when the topic is subscribed.
|
|
2056
|
+
}
|
|
2057
|
+
return topic;
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
/**
|
|
2061
|
+
* Get a named topic from cache.
|
|
2062
|
+
*
|
|
2063
|
+
* @param {string} topicName - Name of the topic to get.
|
|
2064
|
+
*
|
|
2065
|
+
* @returns {Topic} Requested topic or <code>undefined</code> if topic is not found in cache.
|
|
2066
|
+
*/
|
|
2067
|
+
cacheGetTopic(topicName) {
|
|
2068
|
+
return this.#cacheGet('topic', topicName);
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
/**
|
|
2072
|
+
* Remove named topic from cache.
|
|
2073
|
+
*
|
|
2074
|
+
* @param {string} topicName - Name of the topic to remove from cache.
|
|
2075
|
+
*/
|
|
2076
|
+
cacheRemTopic(topicName) {
|
|
2077
|
+
this.#cacheDel('topic', topicName);
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
/**
|
|
2081
|
+
* Iterate over cached topics.
|
|
2082
|
+
*
|
|
2083
|
+
* @param {Function} func - callback to call for each topic.
|
|
2084
|
+
* @param {Object} context - 'this' inside the 'func'.
|
|
2085
|
+
*/
|
|
2086
|
+
mapTopics(func, context) {
|
|
2087
|
+
this.#cacheMap('topic', func, context);
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
/**
|
|
2091
|
+
* Check if named topic is already present in cache.
|
|
2092
|
+
*
|
|
2093
|
+
* @param {string} topicName - Name of the topic to check.
|
|
2094
|
+
* @returns {boolean} true if topic is found in cache, false otherwise.
|
|
2095
|
+
*/
|
|
2096
|
+
isTopicCached(topicName) {
|
|
2097
|
+
return !!this.#cacheGet('topic', topicName);
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
/**
|
|
2101
|
+
* Generate unique name like <code>'new123456'</code> suitable for creating a new group topic.
|
|
2102
|
+
*
|
|
2103
|
+
* @param {boolean} isChan - if the topic is channel-enabled.
|
|
2104
|
+
* @returns {string} name which can be used for creating a new group topic.
|
|
2105
|
+
*/
|
|
2106
|
+
newGroupTopicName(isChan) {
|
|
2107
|
+
return (isChan ? Const.TOPIC_NEW_CHAN : Const.TOPIC_NEW) + this.getNextUniqueId();
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
/**
|
|
2111
|
+
* Instantiate <code>'me'</code> topic or get it from cache.
|
|
2112
|
+
*
|
|
2113
|
+
* @returns {TopicMe} Instance of <code>'me'</code> topic.
|
|
2114
|
+
*/
|
|
2115
|
+
getMeTopic() {
|
|
2116
|
+
return this.getTopic(Const.TOPIC_ME);
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
/**
|
|
2120
|
+
* Instantiate <code>'fnd'</code> (find) topic or get it from cache.
|
|
2121
|
+
*
|
|
2122
|
+
* @returns {Topic} Instance of <code>'fnd'</code> topic.
|
|
2123
|
+
*/
|
|
2124
|
+
getFndTopic() {
|
|
2125
|
+
return this.getTopic(Const.TOPIC_FND);
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
/**
|
|
2129
|
+
* Create a new {@link LargeFileHelper} instance
|
|
2130
|
+
*
|
|
2131
|
+
* @returns {LargeFileHelper} instance of a {@link Tinode.LargeFileHelper}.
|
|
2132
|
+
*/
|
|
2133
|
+
getLargeFileHelper() {
|
|
2134
|
+
return new LargeFileHelper(this, Const.PROTOCOL_VERSION);
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
/**
|
|
2138
|
+
* Get the UID of the the current authenticated user.
|
|
2139
|
+
*
|
|
2140
|
+
* @returns {string} UID of the current user or <code>undefined</code> if the session is not yet
|
|
2141
|
+
* authenticated or if there is no session.
|
|
2142
|
+
*/
|
|
2143
|
+
getCurrentUserID() {
|
|
2144
|
+
return this._myUID;
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
/**
|
|
2148
|
+
* Check if the given user ID is equal to the current user's UID.
|
|
2149
|
+
*
|
|
2150
|
+
* @param {string} uid - UID to check.
|
|
2151
|
+
*
|
|
2152
|
+
* @returns {boolean} true if the given UID belongs to the current logged in user.
|
|
2153
|
+
*/
|
|
2154
|
+
isMe(uid) {
|
|
2155
|
+
return this._myUID === uid;
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
/**
|
|
2159
|
+
* Get login used for last successful authentication.
|
|
2160
|
+
*
|
|
2161
|
+
* @returns {string} login last used successfully or <code>undefined</code>.
|
|
2162
|
+
*/
|
|
2163
|
+
getCurrentLogin() {
|
|
2164
|
+
return this._login;
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
/**
|
|
2168
|
+
* Return information about the server: protocol version and build timestamp.
|
|
2169
|
+
*
|
|
2170
|
+
* @returns {Object} build and version of the server or <code>null</code> if there is no connection or
|
|
2171
|
+
* if the first server response has not been received yet.
|
|
2172
|
+
*/
|
|
2173
|
+
getServerInfo() {
|
|
2174
|
+
return this._serverInfo;
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
/**
|
|
2178
|
+
* Report a topic for abuse. Wrapper for {@link Tinode#publish}.
|
|
2179
|
+
*
|
|
2180
|
+
* @param {string} action - the only supported action is 'report'.
|
|
2181
|
+
* @param {string} target - name of the topic being reported.
|
|
2182
|
+
*
|
|
2183
|
+
* @returns {Promise} Promise to be resolved/rejected when the server responds to request.
|
|
2184
|
+
*/
|
|
2185
|
+
report(action, target) {
|
|
2186
|
+
return this.publish(Const.TOPIC_SYS, Drafty.attachJSON(null, {
|
|
2187
|
+
'action': action,
|
|
2188
|
+
'target': target
|
|
2189
|
+
}));
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
/**
|
|
2193
|
+
* Return server-provided configuration value.
|
|
2194
|
+
*
|
|
2195
|
+
* @param {string} name of the value to return.
|
|
2196
|
+
* @param {Object} defaultValue to return in case the parameter is not set or not found.
|
|
2197
|
+
*
|
|
2198
|
+
* @returns {Object} named value.
|
|
2199
|
+
*/
|
|
2200
|
+
getServerParam(name, defaultValue) {
|
|
2201
|
+
return this._serverInfo && this._serverInfo[name] || defaultValue;
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
/**
|
|
2205
|
+
* Toggle console logging. Logging is off by default.
|
|
2206
|
+
*
|
|
2207
|
+
* @param {boolean} enabled - Set to <code>true</code> to enable logging to console.
|
|
2208
|
+
* @param {boolean} trimLongStrings - Set to <code>true</code> to trim long strings.
|
|
2209
|
+
*/
|
|
2210
|
+
enableLogging(enabled, trimLongStrings) {
|
|
2211
|
+
this._loggingEnabled = enabled;
|
|
2212
|
+
this._trimLongStrings = enabled && trimLongStrings;
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
/**
|
|
2216
|
+
* Set UI language to report to the server. Must be called before <code>'hi'</code> is sent, otherwise it will not be used.
|
|
2217
|
+
*
|
|
2218
|
+
* @param {string} hl - human (UI) language, like <code>"en_US"</code> or <code>"zh-Hans"</code>.
|
|
2219
|
+
*/
|
|
2220
|
+
setHumanLanguage(hl) {
|
|
2221
|
+
if (hl) {
|
|
2222
|
+
this._humanLanguage = hl;
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
/**
|
|
2227
|
+
* Check if given topic is online.
|
|
2228
|
+
*
|
|
2229
|
+
* @param {string} name of the topic to test.
|
|
2230
|
+
* @returns {boolean} true if topic is online, false otherwise.
|
|
2231
|
+
*/
|
|
2232
|
+
isTopicOnline(name) {
|
|
2233
|
+
const topic = this.#cacheGet('topic', name);
|
|
2234
|
+
return topic && topic.online;
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
/**
|
|
2238
|
+
* Get access mode for the given contact.
|
|
2239
|
+
*
|
|
2240
|
+
* @param {string} name of the topic to query.
|
|
2241
|
+
* @returns {AccessMode} access mode if topic is found, null otherwise.
|
|
2242
|
+
*/
|
|
2243
|
+
getTopicAccessMode(name) {
|
|
2244
|
+
const topic = this.#cacheGet('topic', name);
|
|
2245
|
+
return topic ? topic.acs : null;
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
/**
|
|
2249
|
+
* Include message ID into all subsequest messages to server instructin it to send aknowledgemens.
|
|
2250
|
+
* Required for promises to function. Default is <code>"on"</code>.
|
|
2251
|
+
*
|
|
2252
|
+
* @param {boolean} status - Turn aknowledgemens on or off.
|
|
2253
|
+
* @deprecated
|
|
2254
|
+
*/
|
|
2255
|
+
wantAkn(status) {
|
|
2256
|
+
if (status) {
|
|
2257
|
+
this._messageId = Math.floor((Math.random() * 0xFFFFFF) + 0xFFFFFF);
|
|
2258
|
+
} else {
|
|
2259
|
+
this._messageId = 0;
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
// Callbacks:
|
|
2264
|
+
/**
|
|
2265
|
+
* Callback to report when the websocket is opened. The callback has no parameters.
|
|
2266
|
+
*
|
|
2267
|
+
* @type {onWebsocketOpen}
|
|
2268
|
+
*/
|
|
2269
|
+
onWebsocketOpen = undefined;
|
|
2270
|
+
|
|
2271
|
+
/**
|
|
2272
|
+
* @typedef ServerParams
|
|
2273
|
+
*
|
|
2274
|
+
* @type {Object}
|
|
2275
|
+
* @property {string} ver - Server version
|
|
2276
|
+
* @property {string} build - Server build
|
|
2277
|
+
* @property {string=} sid - Session ID, long polling connections only.
|
|
2278
|
+
*/
|
|
2279
|
+
|
|
2280
|
+
/**
|
|
2281
|
+
* @callback onConnect
|
|
2282
|
+
* @param {number} code - Result code
|
|
2283
|
+
* @param {string} text - Text epxplaining the completion, i.e "OK" or an error message.
|
|
2284
|
+
* @param {ServerParams} params - Parameters returned by the server.
|
|
2285
|
+
*/
|
|
2286
|
+
/**
|
|
2287
|
+
* Callback to report when connection with Tinode server is established.
|
|
2288
|
+
* @type {onConnect}
|
|
2289
|
+
*/
|
|
2290
|
+
onConnect = undefined;
|
|
2291
|
+
|
|
2292
|
+
/**
|
|
2293
|
+
* Callback to report when connection is lost. The callback has no parameters.
|
|
2294
|
+
* @type {onDisconnect}
|
|
2295
|
+
*/
|
|
2296
|
+
onDisconnect = undefined;
|
|
2297
|
+
|
|
2298
|
+
/**
|
|
2299
|
+
* @callback onLogin
|
|
2300
|
+
* @param {number} code - NUmeric completion code, same as HTTP status codes.
|
|
2301
|
+
* @param {string} text - Explanation of the completion code.
|
|
2302
|
+
*/
|
|
2303
|
+
/**
|
|
2304
|
+
* Callback to report login completion.
|
|
2305
|
+
* @type {onLogin}
|
|
2306
|
+
*/
|
|
2307
|
+
onLogin = undefined;
|
|
2308
|
+
|
|
2309
|
+
/**
|
|
2310
|
+
* Callback to receive <code>{ctrl}</code> (control) messages.
|
|
2311
|
+
* @type {onCtrlMessage}
|
|
2312
|
+
*/
|
|
2313
|
+
onCtrlMessage = undefined;
|
|
2314
|
+
|
|
2315
|
+
/**
|
|
2316
|
+
* Callback to recieve <code>{data}</code> (content) messages.
|
|
2317
|
+
* @type {onDataMessage}
|
|
2318
|
+
*/
|
|
2319
|
+
onDataMessage = undefined;
|
|
2320
|
+
|
|
2321
|
+
/**
|
|
2322
|
+
* Callback to receive <code>{pres}</code> (presence) messages.
|
|
2323
|
+
* @type {onPresMessage}
|
|
2324
|
+
*/
|
|
2325
|
+
onPresMessage = undefined;
|
|
2326
|
+
|
|
2327
|
+
/**
|
|
2328
|
+
* Callback to receive all messages as objects.
|
|
2329
|
+
* @type {onMessage}
|
|
2330
|
+
*/
|
|
2331
|
+
onMessage = undefined;
|
|
2332
|
+
|
|
2333
|
+
/**
|
|
2334
|
+
* Callback to receive all messages as unparsed text.
|
|
2335
|
+
* @type {onRawMessage}
|
|
2336
|
+
*/
|
|
2337
|
+
onRawMessage = undefined;
|
|
2338
|
+
|
|
2339
|
+
/**
|
|
2340
|
+
* Callback to receive server responses to network probes. See {@link Tinode#networkProbe}
|
|
2341
|
+
* @type {onNetworkProbe}
|
|
2342
|
+
*/
|
|
2343
|
+
onNetworkProbe = undefined;
|
|
2344
|
+
|
|
2345
|
+
/**
|
|
2346
|
+
* Callback to be notified when exponential backoff is iterating.
|
|
2347
|
+
* @type {onAutoreconnectIteration}
|
|
2348
|
+
*/
|
|
2349
|
+
onAutoreconnectIteration = undefined;
|
|
2350
|
+
};
|
|
2351
|
+
|
|
2352
|
+
// Exported constants
|
|
2353
|
+
Tinode.MESSAGE_STATUS_NONE = Const.MESSAGE_STATUS_NONE;
|
|
2354
|
+
Tinode.MESSAGE_STATUS_QUEUED = Const.MESSAGE_STATUS_QUEUED;
|
|
2355
|
+
Tinode.MESSAGE_STATUS_SENDING = Const.MESSAGE_STATUS_SENDING;
|
|
2356
|
+
Tinode.MESSAGE_STATUS_FAILED = Const.MESSAGE_STATUS_FAILED;
|
|
2357
|
+
Tinode.MESSAGE_STATUS_FATAL = Const.MESSAGE_STATUS_FATAL;
|
|
2358
|
+
Tinode.MESSAGE_STATUS_SENT = Const.MESSAGE_STATUS_SENT;
|
|
2359
|
+
Tinode.MESSAGE_STATUS_RECEIVED = Const.MESSAGE_STATUS_RECEIVED;
|
|
2360
|
+
Tinode.MESSAGE_STATUS_READ = Const.MESSAGE_STATUS_READ;
|
|
2361
|
+
Tinode.MESSAGE_STATUS_TO_ME = Const.MESSAGE_STATUS_TO_ME;
|
|
2362
|
+
|
|
2363
|
+
// Unicode [del] symbol.
|
|
2364
|
+
Tinode.DEL_CHAR = Const.DEL_CHAR;
|
|
2365
|
+
|
|
2366
|
+
// Names of keys to server-provided configuration limits.
|
|
2367
|
+
Tinode.MAX_MESSAGE_SIZE = 'maxMessageSize';
|
|
2368
|
+
Tinode.MAX_SUBSCRIBER_COUNT = 'maxSubscriberCount';
|
|
2369
|
+
Tinode.MIN_TAG_LENGTH = 'minTagLength';
|
|
2370
|
+
Tinode.MAX_TAG_LENGTH = 'maxTagLength';
|
|
2371
|
+
Tinode.MAX_TAG_COUNT = 'maxTagCount';
|
|
2372
|
+
Tinode.MAX_FILE_UPLOAD_SIZE = 'maxFileUploadSize';
|
|
2373
|
+
Tinode.REQ_CRED_VALIDATORS = 'reqCred';
|
|
2374
|
+
Tinode.MSG_DELETE_AGE = 'msgDelAge';
|
|
2375
|
+
|
|
2376
|
+
// Tinode URI topic ID prefix, 'scheme:path/'.
|
|
2377
|
+
Tinode.URI_TOPIC_ID_PREFIX = 'tinode:topic/';
|
|
2378
|
+
|
|
2379
|
+
// Tag prefixes for alias, email, phone.
|
|
2380
|
+
Tinode.TAG_ALIAS = Const.TAG_ALIAS;
|
|
2381
|
+
Tinode.TAG_EMAIL = Const.TAG_EMAIL;
|
|
2382
|
+
Tinode.TAG_PHONE = Const.TAG_PHONE;
|