@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.
@@ -0,0 +1,480 @@
1
+ /**
2
+ * @file Definition of 'me' topic.
3
+ *
4
+ * @copyright 2015-2025 Tinode LLC.
5
+ */
6
+ 'use strict';
7
+
8
+ import AccessMode from './access-mode.js';
9
+ import * as Const from './config.js';
10
+ import Topic from './topic.js';
11
+ import {
12
+ mergeObj
13
+ } from './utils.js';
14
+
15
+ /**
16
+ * @class TopicMe - special case of {@link Tinode.Topic} for
17
+ * managing data of the current user, including contact list.
18
+ * @extends Tinode.Topic
19
+ * @memberof Tinode
20
+ *
21
+ * @param {TopicMe.Callbacks} callbacks - Callbacks to receive various events.
22
+ */
23
+ export default class TopicMe extends Topic {
24
+ onContactUpdate;
25
+
26
+ constructor(callbacks) {
27
+ super(Const.TOPIC_ME, callbacks);
28
+
29
+ // me-specific callbacks
30
+ if (callbacks) {
31
+ this.onContactUpdate = callbacks.onContactUpdate;
32
+ }
33
+ }
34
+
35
+ // Override the original Topic._processMetaDesc.
36
+ _processMetaDesc(desc) {
37
+ // Check if online contacts need to be turned off because P permission was removed.
38
+ const turnOff = (desc.acs && !desc.acs.isPresencer()) && (this.acs && this.acs.isPresencer());
39
+
40
+ // Copy parameters from desc object to this topic.
41
+ mergeObj(this, desc);
42
+ this._tinode._db.updTopic(this);
43
+ // Update current user's record in the global cache.
44
+ this._updateCachedUser(this._tinode._myUID, desc);
45
+
46
+ // 'P' permission was removed. All topics are offline now.
47
+ if (turnOff) {
48
+ this._tinode.mapTopics((cont) => {
49
+ if (cont.online) {
50
+ cont.online = false;
51
+ cont.seen = Object.assign(cont.seen || {}, {
52
+ when: new Date()
53
+ });
54
+ this._refreshContact('off', cont);
55
+ }
56
+ });
57
+ }
58
+
59
+ if (this.onMetaDesc) {
60
+ this.onMetaDesc(this);
61
+ }
62
+ }
63
+
64
+ // Override the original Topic._processMetaSubs
65
+ _processMetaSubs(subs) {
66
+ let updateCount = 0;
67
+ subs.forEach((sub) => {
68
+ const topicName = sub.topic;
69
+ // Don't show 'me' and 'fnd' topics in the list of contacts.
70
+ if (topicName == Const.TOPIC_FND || topicName == Const.TOPIC_ME) {
71
+ return;
72
+ }
73
+ sub.online = !!sub.online;
74
+
75
+ let cont = null;
76
+ if (sub.deleted) {
77
+ cont = sub;
78
+ this._tinode.cacheRemTopic(topicName);
79
+ this._tinode._db.remTopic(topicName);
80
+ } else {
81
+ // Ensure the values are defined and are integers.
82
+ if (typeof sub.seq != 'undefined') {
83
+ sub.seq = sub.seq | 0;
84
+ sub.recv = sub.recv | 0;
85
+ sub.read = sub.read | 0;
86
+ sub.unread = sub.seq - sub.read;
87
+ }
88
+
89
+ const topic = this._tinode.getTopic(topicName);
90
+ if (topic._new) {
91
+ delete topic._new;
92
+ }
93
+
94
+ cont = mergeObj(topic, sub);
95
+ this._tinode._db.updTopic(cont);
96
+
97
+ if (Topic.isP2PTopicName(topicName)) {
98
+ this._cachePutUser(topicName, cont);
99
+ this._tinode._db.updUser(topicName, cont.public);
100
+ }
101
+ // Notify topic of the update if it's an external update.
102
+ if (!sub._noForwarding && topic) {
103
+ sub._noForwarding = true;
104
+ topic._processMetaDesc(sub);
105
+ }
106
+ }
107
+
108
+ updateCount++;
109
+
110
+ if (this.onMetaSub) {
111
+ this.onMetaSub(cont);
112
+ }
113
+ });
114
+
115
+ if (this.onSubsUpdated && updateCount > 0) {
116
+ const keys = [];
117
+ subs.forEach((s) => {
118
+ keys.push(s.topic);
119
+ });
120
+ this.onSubsUpdated(keys, updateCount);
121
+ }
122
+ }
123
+
124
+ // Called by Tinode when meta.sub is recived.
125
+ _processMetaCreds(creds, upd) {
126
+ if (creds.length == 1 && creds[0] == Const.DEL_CHAR) {
127
+ creds = [];
128
+ }
129
+ if (upd) {
130
+ creds.forEach((cr) => {
131
+ if (cr.val) {
132
+ // Adding a credential.
133
+ let idx = this._credentials.findIndex((el) => {
134
+ return el.meth == cr.meth && el.val == cr.val;
135
+ });
136
+ if (idx < 0) {
137
+ // Not found.
138
+ if (!cr.done) {
139
+ // Unconfirmed credential replaces previous unconfirmed credential of the same method.
140
+ idx = this._credentials.findIndex((el) => {
141
+ return el.meth == cr.meth && !el.done;
142
+ });
143
+ if (idx >= 0) {
144
+ // Remove previous unconfirmed credential.
145
+ this._credentials.splice(idx, 1);
146
+ }
147
+ }
148
+ this._credentials.push(cr);
149
+ } else {
150
+ // Found. Maybe change 'done' status.
151
+ this._credentials[idx].done = cr.done;
152
+ }
153
+ } else if (cr.resp) {
154
+ // Handle credential confirmation.
155
+ const idx = this._credentials.findIndex((el) => {
156
+ return el.meth == cr.meth && !el.done;
157
+ });
158
+ if (idx >= 0) {
159
+ this._credentials[idx].done = true;
160
+ }
161
+ }
162
+ });
163
+ } else {
164
+ this._credentials = creds;
165
+ }
166
+ if (this.onCredsUpdated) {
167
+ this.onCredsUpdated(this._credentials);
168
+ }
169
+ }
170
+
171
+ // Process presence change message
172
+ _routePres(pres) {
173
+ if (pres.what == 'term') {
174
+ // The 'me' topic itself is detached. Mark as unsubscribed.
175
+ this._resetSub();
176
+ return;
177
+ }
178
+
179
+ if (pres.what == 'upd' && pres.src == Const.TOPIC_ME) {
180
+ // Update to me's description. Request updated value.
181
+ this.getMeta(this.startMetaQuery().withDesc().build());
182
+ return;
183
+ }
184
+
185
+ const cont = this._tinode.cacheGetTopic(pres.src);
186
+ if (cont) {
187
+ switch (pres.what) {
188
+ case 'on': // topic came online
189
+ cont.online = true;
190
+ break;
191
+ case 'off': // topic went offline
192
+ if (cont.online) {
193
+ cont.online = false;
194
+ cont.seen = Object.assign(cont.seen || {}, {
195
+ when: new Date()
196
+ });
197
+ }
198
+ break;
199
+ case 'msg': // new message received
200
+ cont._updateReceived(pres.seq, pres.act);
201
+ break;
202
+ case 'upd': // desc updated
203
+ // Request updated subscription.
204
+ this.getMeta(this.startMetaQuery().withLaterOneSub(pres.src).build());
205
+ break;
206
+ case 'acs': // access mode changed
207
+ // If 'tgt' is not set then this is an update to the permissions of the current user.
208
+ // Otherwise it's an update to group topic subscriber permissions while the topic is offline.
209
+ // Just gnore it then.
210
+ if (!pres.tgt) {
211
+ if (cont.acs) {
212
+ cont.acs.updateAll(pres.dacs);
213
+ } else {
214
+ cont.acs = new AccessMode().updateAll(pres.dacs);
215
+ }
216
+ cont.touched = new Date();
217
+ }
218
+ break;
219
+ case 'ua':
220
+ // user agent changed.
221
+ cont.seen = {
222
+ when: new Date(),
223
+ ua: pres.ua
224
+ };
225
+ break;
226
+ case 'recv':
227
+ // user's other session marked some messges as received.
228
+ pres.seq = pres.seq | 0;
229
+ cont.recv = cont.recv ? Math.max(cont.recv, pres.seq) : pres.seq;
230
+ break;
231
+ case 'read':
232
+ // user's other session marked some messages as read.
233
+ pres.seq = pres.seq | 0;
234
+ cont.read = cont.read ? Math.max(cont.read, pres.seq) : pres.seq;
235
+ cont.recv = cont.recv ? Math.max(cont.read, cont.recv) : cont.recv;
236
+ cont.unread = cont.seq - cont.read;
237
+ break;
238
+ case 'gone':
239
+ // topic deleted or unsubscribed from.
240
+ this._tinode.cacheRemTopic(pres.src);
241
+ if (!cont._deleted) {
242
+ cont._deleted = true;
243
+ cont._attached = false;
244
+ this._tinode._db.markTopicAsDeleted(pres.src, true);
245
+ } else {
246
+ this._tinode._db.remTopic(pres.src);
247
+ }
248
+ break;
249
+ case 'del':
250
+ // Update topic.del value.
251
+ break;
252
+ default:
253
+ this._tinode.logger("INFO: Unsupported presence update in 'me'", pres.what);
254
+ }
255
+
256
+ this._refreshContact(pres.what, cont);
257
+ } else {
258
+ if (pres.what == 'acs') {
259
+ // New subscriptions and deleted/banned subscriptions have full
260
+ // access mode (no + or - in the dacs string). Changes to known subscriptions are sent as
261
+ // deltas, but they should not happen here.
262
+ const acs = new AccessMode(pres.dacs);
263
+ if (!acs || acs.mode == AccessMode._INVALID) {
264
+ this._tinode.logger("ERROR: Invalid access mode update", pres.src, pres.dacs);
265
+ return;
266
+ } else if (acs.mode == AccessMode._NONE) {
267
+ this._tinode.logger("WARNING: Removing non-existent subscription", pres.src, pres.dacs);
268
+ return;
269
+ } else {
270
+ // New subscription. Send request for the full description.
271
+ // Using .withOneSub (not .withLaterOneSub) to make sure IfModifiedSince is not set.
272
+ this.getMeta(this.startMetaQuery().withOneSub(undefined, pres.src).build());
273
+ // Create a dummy entry to catch online status update.
274
+ const dummy = this._tinode.getTopic(pres.src);
275
+ dummy.topic = pres.src;
276
+ dummy.online = false;
277
+ dummy.acs = acs;
278
+ this._tinode._db.updTopic(dummy);
279
+ }
280
+ } else if (pres.what == 'tags') {
281
+ this.getMeta(this.startMetaQuery().withTags().build());
282
+ } else if (pres.what == 'msg') {
283
+ // Message received for un unknown (previously deleted) topic.
284
+ this.getMeta(this.startMetaQuery().withOneSub(undefined, pres.src).build());
285
+ // Create an entry to catch updates and messages.
286
+ const dummy = this._tinode.getTopic(pres.src);
287
+ dummy._deleted = false;
288
+ this._tinode._db.updTopic(dummy);
289
+ }
290
+
291
+ this._refreshContact(pres.what, cont);
292
+ }
293
+
294
+ if (this.onPres) {
295
+ this.onPres(pres);
296
+ }
297
+ }
298
+
299
+ // Contact is updated, execute callbacks.
300
+ _refreshContact(what, cont) {
301
+ if (this.onContactUpdate) {
302
+ this.onContactUpdate(what, cont);
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Publishing to TopicMe is not supported. {@link Topic#publish} is overriden and thows an {Error} if called.
308
+ * @memberof Tinode.TopicMe#
309
+ * @throws {Error} Always throws an error.
310
+ */
311
+ publish() {
312
+ return Promise.reject(new Error("Publishing to 'me' is not supported"));
313
+ }
314
+
315
+ /**
316
+ * Delete validation credential.
317
+ * @memberof Tinode.TopicMe#
318
+ *
319
+ * @param {string} topic - Name of the topic to delete
320
+ * @param {string} user - User ID to remove.
321
+ * @returns {Promise} Promise which will be resolved/rejected on receiving server reply.
322
+ */
323
+ delCredential(method, value) {
324
+ if (!this._attached) {
325
+ return Promise.reject(new Error("Cannot delete credential in inactive 'me' topic"));
326
+ }
327
+ // Send {del} message, return promise
328
+ return this._tinode.delCredential(method, value).then(ctrl => {
329
+ // Remove deleted credential from the cache.
330
+ const index = this._credentials.findIndex((el) => {
331
+ return el.meth == method && el.val == value;
332
+ });
333
+ if (index > -1) {
334
+ this._credentials.splice(index, 1);
335
+ }
336
+ // Notify listeners
337
+ if (this.onCredsUpdated) {
338
+ this.onCredsUpdated(this._credentials);
339
+ }
340
+ return ctrl;
341
+ });
342
+ }
343
+
344
+ /**
345
+ * @callback contactFilter
346
+ * @param {Object} contact to check for inclusion.
347
+ * @returns {boolean} <code>true</code> if contact should be processed, <code>false</code> to exclude it.
348
+ */
349
+ /**
350
+ * Iterate over cached contacts.
351
+ *
352
+ * @function
353
+ * @memberof Tinode.TopicMe#
354
+ * @param {TopicMe.ContactCallback} callback - Callback to call for each contact.
355
+ * @param {contactFilter=} filter - Optionally filter contacts; include all if filter is false-ish, otherwise
356
+ * include those for which filter returns true-ish.
357
+ * @param {Object=} context - Context to use for calling the `callback`, i.e. the value of `this` inside the callback.
358
+ */
359
+ contacts(callback, filter, context) {
360
+ this._tinode.mapTopics((c, idx) => {
361
+ if (c.isCommType() && (!filter || filter(c))) {
362
+ callback.call(context, c, idx);
363
+ }
364
+ });
365
+ }
366
+
367
+ /**
368
+ * Get a contact from cache.
369
+ * @memberof Tinode.TopicMe#
370
+ *
371
+ * @param {string} name - Name of the contact to get, either a UID (for p2p topics) or a topic name.
372
+ * @returns {Tinode.Contact} - Contact or `undefined`.
373
+ */
374
+ getContact(name) {
375
+ return this._tinode.cacheGetTopic(name);
376
+ }
377
+
378
+ /**
379
+ * Get access mode of a given contact from cache.
380
+ * @memberof Tinode.TopicMe#
381
+ *
382
+ * @param {string} name - Name of the contact to get access mode for, either a UID (for p2p topics)
383
+ * or a topic name; if missing, access mode for the 'me' topic itself.
384
+ * @returns {string} - access mode, such as `RWP`.
385
+ */
386
+ getAccessMode(name) {
387
+ if (name) {
388
+ const cont = this._tinode.cacheGetTopic(name);
389
+ return cont ? cont.acs : null;
390
+ }
391
+ return this.acs;
392
+ }
393
+
394
+ /**
395
+ * Check if contact is archived, i.e. contact.private.arch == true.
396
+ * @memberof Tinode.TopicMe#
397
+ *
398
+ * @param {string} name - Name of the contact to check archived status, either a UID (for p2p topics) or a topic name.
399
+ * @returns {boolean} - true if contact is archived, false otherwise.
400
+ */
401
+ isArchived(name) {
402
+ const cont = this._tinode.cacheGetTopic(name);
403
+ return cont && cont.private && !!cont.private.arch;
404
+ }
405
+
406
+ /**
407
+ * @typedef Tinode.Credential
408
+ * @memberof Tinode
409
+ * @type Object
410
+ * @property {string} meth - validation method such as 'email' or 'tel'.
411
+ * @property {string} val - credential value, i.e. 'jdoe@example.com' or '+17025551234'
412
+ * @property {boolean} done - true if credential is validated.
413
+ */
414
+ /**
415
+ * Get the user's credentials: email, phone, etc.
416
+ * @memberof Tinode.TopicMe#
417
+ *
418
+ * @returns {Tinode.Credential[]} - array of credentials.
419
+ */
420
+ getCredentials() {
421
+ return this._credentials;
422
+ }
423
+
424
+ /**
425
+ * Pin topic to the top of the contact list.
426
+ * @memberof Tinode.TopicMe#
427
+ *
428
+ * @param {string} topic - Name of the topic to pin.
429
+ * @param {boolean} [pin=false] - If true, pin the topic, otherwise unpin.
430
+ *
431
+ * @returns {Promise} Promise to be resolved/rejected when the server responds to request.
432
+ */
433
+ pinTopic(topic, pin) {
434
+ if (!this._attached) {
435
+ return Promise.reject(new Error("Cannot pin topic in inactive 'me' topic"));
436
+ }
437
+ if (!Topic.isCommTopicName(topic)) {
438
+ return Promise.reject(new Error("Invalid topic to pin"));
439
+ }
440
+
441
+ const tpin = Array.isArray(this.private && this.private.tpin) ? this.private.tpin : [];
442
+ const found = tpin.includes(topic);
443
+ if ((pin && found) || (!pin && !found)) {
444
+ // Nothing to do.
445
+ return Promise.resolve(tpin);
446
+ }
447
+
448
+ if (pin) {
449
+ // Add topic to the top of the pinned list.
450
+ tpin.unshift(topic);
451
+ } else {
452
+ // Remove topic from the pinned list.
453
+ tpin.splice(tpin.indexOf(topic), 1);
454
+ }
455
+
456
+ return this.setMeta({
457
+ desc: {
458
+ private: {
459
+ tpin: tpin.length > 0 ? tpin : Const.DEL_CHAR
460
+ }
461
+ }
462
+ });
463
+ }
464
+
465
+ /**
466
+ * Get the rank of the pinned topic.
467
+ * @memberof Tinode.TopicMe#
468
+ * @param {string} topic - Name of the topic to check.
469
+ *
470
+ * @returns {number} numeric rank of the pinned topic in the range 1..N (N being the top,
471
+ * N - the number of pinned topics) or 0 if not pinned.
472
+ */
473
+ pinnedTopicRank(topic) {
474
+ if (!this.private || !this.private.tpin) {
475
+ return 0;
476
+ }
477
+ const idx = this.private.tpin.indexOf(topic);
478
+ return idx < 0 ? 0 : this.private.tpin.length - idx;
479
+ }
480
+ }