@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/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;