View difference between Paste ID: anvJEn54 and CMuUhESw
SHOW: | | - or go back to the newest paste.
1
import ClientAppService from "./system/ClientAppService";
2
3
// Data Models
4
import XError from "./core/model/XError";
5
import XRequest from "./core/model/net/XRequest";
6
import XResponse from "./core/model/net/XResponse";
7
import XObject from "./core/model/XObject";
8
import XMObject from "./core/model/XMObject";
9
import XResultList from "./core/model/util/XResultList";
10
import XResultMap from "./core/model/util/XResultMap";
11
import XBinaryData from "./core/model/XBinaryData";
12
import {
13
  ModelType,
14
  MessageProps,
15
  UserProps,
16
  LanguageCodes,
17
  SocialProps,
18
  PREFIX_COMMENT_ID,
19
  TOPIC_CATEGORIES,
20
  FEATURE_LIKE,
21
  FEATURE_FOLLOW_USER,
22
  FEATURE_REPLY_POST,
23
  FEATURE_REPLY_COMMENT,
24
  FEATURE_REPOST,
25
  FEATURE_SUBMIT_POST,
26
} from "./core/model/ModelConsts";
27
import {XMFollows, XMFollowers} from "./core/model/social/XMFollow";
28
import Util from "./core/Util";
29
import axios from "axios";
30
31
import XMWatchesPost from "./core/model/post/XMWatchesPost";
32
import XMWatchedPost from "./core/model/post/XMWatchedPost";
33
import XPostFeed from "./core/model/activity/XPostFeed";
34
import XMPost from "./core/model/post/XMPost";
35
import {AppMessages, SupportedLanguageList} from "./app/AppMessages";
36
import XUserInfo from "./core/model/user/XUserInfo";
37
import XCommentFeed from "./core/model/post/XCommentFeed";
38
import XMComment from "./core/model/social/XMComment";
39
import API from "./core/API";
40
import ErrorCodes from "./core/ErrorCodes";
41
import {fileToMd5} from "src/util/file";
42
43
// These are classes that we have instances but not direct type
44
// reference. They are good for IDE/JSDoc and these are used to eliminate
45
// the complaints
46
XError.CheckIn();
47
XResponse.CheckIn();
48
XRequest.CheckIn();
49
XBinaryData.CheckIn();
50
XResultList.CheckIn();
51
XResultMap.CheckIn();
52
53
XPostFeed.CheckIn();
54
XMPost.CheckIn();
55
56
// import numeral from 'numeral';
57
58
const _CLSNAME = "GetterService";
59
60
/**
61
 * Application context / helper (controller, api requestor)
62
 * for the web app.
63
 *
64
 * There should be one instance per app type per user. With
65
 * SSR configuration, there will be one instance in the browser code,
66
 * and one on the server side.
67
 */
68
export class GetterService extends ClientAppService {
69
  /**
70
   *
71
   * @constructor
72
   * @param {object} props outside (configuration) properties to use
73
   */
74
  constructor(props) {
75
    super(_CLSNAME, props);
76
77
    this.applyConfig(this.props);
78
  }
79
80
  applyConfig(props) {
81
    // const _m = "applyConfig";
82
    super.applyConfig(props);
83
84
    // let isBrowser = Global.IsBrowser();
85
    // this.log("applyConfig", "parameters: ", props);
86
87
    this.urlPrefix = props.urlPrefix ? props.urlPrefix : "/";
88
    this.appUrl = props.appUrl ? props.appUrl : null;
89
    this.appPrefix = this.appUrl ? this.appUrl + "/" : null;
90
91
    // this.trace(_m, `**** APP URL: ${this.appUrl} Is Browser: ${isBrowser} ****`);
92
93
    this.urlHost = props.apiHost ? props.apiHost : "http://255.255.255.255:999";
94
95
    this.title = props.title ? props.title : "Untitled App";
96
97
    this.urlTagInfo = this.getURL(this.urlHost, "/s/taginfo");
98
    this.urlActivityLog = this.getURL(this.urlHost, "/log/activity/");
99
    this.urlLogMessage = this.getURL(this.urlHost, "/log/msg");
100
  }
101
102
  /**
103
   * Initializer user info. This is called by Portal upon login.
104
   */
105
  initUser(userInfo) {
106
    // re-intialize analytics tracking
107
    this._trackingUser();
108
  }
109
110
  assertXMObject(value) {
111
    return this.assertType(value, XMObject);
112
  }
113
114
  /**
115
   * Reset user information. This is called by Portal upon logout.
116
   */
117
  resetUser() {
118
    // clear session?
119
  }
120
121
  /**
122
   * Return the application's official URL, as set in the environment
123
   * variable *_APP_URL. This is optional and only used for user click
124
   * backs.
125
   *
126
   * @param {*} defaultVal
127
   * @return {string=} application's url with protocol and port
128
   */
129
  getAppUrl(defaultVal = null) {
130
    return this.appUrl ? this.appUrl : defaultVal;
131
  }
132
133
  /**
134
   * Return the SPA's url prefix.
135
   */
136
  getUrlPrefix() {
137
    return this.urlPrefix;
138
  }
139
140
  /**
141
   * Prefix of this web application's URL, or a
142
   * complete URL if given path
143
   *
144
   * @param {string} path path to add to prefix
145
   * @return {string} either prefix, or complete URL if given path
146
   */
147
  getAppPrefix(path) {
148
    return path ? this.urlPrefix + path : this.urlPrefix;
149
  }
150
151
  /**
152
   * Prefix of this app's service URL to make API calls.
153
   *
154
   * @param {string} path path to add to prefix
155
   * @return {string} either prefix, or complete URL if given path
156
   */
157
  getServicePrefix(path) {
158
    return path ? new URL(path, this.urlHost).toString() : this.urlHost;
159
  }
160
161
  /**
162
   *
163
   * @param {string} phrase search phrase
164
   */
165
  getUrlSearchResults(phrase) {
166
    let url = this.getAppPrefix("search");
167
    if (!Util.StringIsEmpty(url)) url += `?q=` + encodeURIComponent(phrase);
168
    return url;
169
  }
170
171
  getUrlHashtagPage(hashtag) {
172
    if (hashtag[0] !== "#") {
173
      hashtag = "/hashtag/" + encodeURIComponent("#" + hashtag);
174
    } else {
175
      hashtag = "/hashtag/" + encodeURIComponent(hashtag);
176
    }
177
    return hashtag;
178
  }
179
180
  getUrlUsertagPage(userId) {
181
    if (userId[0] === "@") userId = userId.substring(1);
182
    return `/user/${userId}`;
183
  }
184
185
  getUrlPostPage(postId) {
186
    return this.getAppPrefix(`post/${postId}`);
187
  }
188
189
  getUrlCommentPage(commentId) {
190
    return this.getAppPrefix(`comment/${commentId}`);
191
  }
192
193
  getUrlUserProfilePage(username) {
194
    return this.getAppPrefix(`user/${username}`);
195
  }
196
197
  getUrlNotificationsAll() {
198
    return this.getAppPrefix(`notifications`);
199
  }
200
201
  getUrlNotificationsMentions() {
202
    return this.getAppPrefix(`notifications/mentions`);
203
  }
204
205
  /**
206
   * Explore URL: /explore or /explore/topic/:topic
207
   *
208
   * @param {string} topic topic to get results from server
209
   */
210
  getUrlExplore(topic) {
211
    let url = Util.StringIsEmpty(topic) ? "explore" : "explore/topic/" + topic;
212
    return this.getAppPrefix(url);
213
  }
214
215
  getUrlHome() {
216
    return this.getAppPrefix("");
217
  }
218
219
  getUrlLogin() {
220
    return this.getAppPrefix("login");
221
  }
222
223
  getUrlLogout() {
224
    return this.getAppPrefix("logout");
225
  }
226
227
  getUrlSignup() {
228
    return this.getAppPrefix("signup");
229
  }
230
231
  getUrlDashboard() {
232
    return this.getAppPrefix("");
233
  }
234
235
  getUrlWelcome() {
236
    return this.getAppPrefix("welcome");
237
  }
238
239
  getUrlNotFound() {
240
    return this.getAppPrefix("notfound");
241
  }
242
243
  // ----------------------- API URL Constructions ------------------------------
244
245
  /**
246
   * Construct API endpoint url for retrieving stats on an object
247
   *
248
   * @param {string} type object type. This may not match ModelType constants,
249
   * so best to look up the endpoint. For example, ModelType.COMMENT is "cm"
250
   * while the endpoint uses "comment" as in /u/comment/:commentId/...
251
   * @param {string[]} objectId
252
   * @param {boolean} inclObj true to also fetch and return the XObject instance
253
   * @return {string} derived URL
254
   */
255
  apiGetObjectStats(type, objectId, inclObj = false) {
256
    if (!objectId) {
257
      this.error("apiGetObjectStats", "no tagnames given");
258
      return;
259
    }
260
261
    let query = this.getURL(this.urlHost, `/s/${type}/${objectId}/stats/`);
262
    if (inclObj === true) query += `?${API.INCL_OBJ}=true`;
263
264
    return query;
265
  }
266
267
  /**
268
   * Construct API URL for GetUserSettings
269
   *
270
   * @param {string[]} userId in array or delimited by comma
271
   * @param {string[]} props field names in array or delimited by comma
272
   */
273
  apiGetUserSettings(userId, section, props = null) {
274
    if (!userId || !section) {
275
      console.error("apiGUS: ?null");
276
      return;
277
    }
278
279
    if (props != null) props = Array.isArray(props) ? props.join(",") : props;
280
    let query = this.getURL(
281
      this.urlHost,
282
      `/u/user/${userId}/settings/${section}`,
283
    );
284
    if (props) query += "?props=" + JSON.stringify(props);
285
286
    return query;
287
  }
288
289
  // ------------------------- LANGUAGE SUPPORT --------------------------
290
291
  getSupportedLanguageCodes() {
292
    return [LanguageCodes.ENGLISH, LanguageCodes.CHINESE_SIMPLIFIED];
293
  }
294
295
  /**
296
   * Return entire language list of supported languages, or just the
297
   * record of the desired language code
298
   *
299
   * @param {string} langCode
300
   * @param {string=} defaultVal optional backup language. Default will be English
301
   * @return {object[]} either full language map keyed off code, or the record
302
   * for the given code
303
   */
304
  getSupportedLanguageList(langCode = null, defaultVal = null) {
305
    let langList = SupportedLanguageList;
306
    let result;
307
    if (langCode) {
308
      result = Util.GetObjectFromArrayByValue(langList, "code", langCode);
309
      if (result == null) {
310
        if (defaultVal == null) defaultVal = LanguageCodes.ENGLISH;
311
        result = Util.GetObjectFromArrayByValue(langList, "code", defaultVal);
312
      }
313
    } else result = langList;
314
    return result;
315
  }
316
317
  /**
318
   * Return current locale (language + country)
319
   *
320
   * @param {*} defaultVal
321
   * @return {string} "en" for now
322
   */
323
  getLanguagePref(defaultVal = "en") {
324
    let lang = this.getSessionVar(UserProps.LANGUAGE, null);
325
    if (lang == null) {
326
      let xUserInfo = this.getXUserInfo();
327
      lang = xUserInfo ? xUserInfo.getLanguagePref(defaultVal) : defaultVal;
328
    }
329
    return lang;
330
  }
331
332
  /**
333
   *
334
   * @param {string} langCode language code
335
   * @return {boolean} true if set, false if something happened
336
   */
337
  setLanguagePref(langCode, sessionOnly = true) {
338
    if (langCode == null) return false;
339
    let prevCode = this.setSessionVar(UserProps.LANGUAGE, langCode);
340
    if (prevCode !== langCode) {
341
      let xUserInfo = new XUserInfo();
342
      xUserInfo.setLanguagePref(langCode);
343
      this.updateUserInfo(xUserInfo);
344
    }
345
    return true;
346
  }
347
348
  // ------------------------- NOTIFICATION -----------------------------
349
350
  /**
351
   * Construct API URL for user alert count (/u/user/:userId/count/alerts/:targetId)
352
   *
353
   * @param {string} userId user of the alerts (logged in currently)
354
   * @param {string} field field to retrieve (default is unread)
355
   * @return {string} fully qualified URL
356
   */
357
  apiUserAlertCount(userId, field = "unread", props = null) {
358
    if (!userId) {
359
      this.error("apiUserAlertCount", "no userId or targetId given");
360
      return false;
361
    }
362
    props = Array.isArray(props) ? props.join(",") : props;
363
364
    let query = this.getURL(
365
      this.urlHost,
366
      `/u/user/${userId}/count/alerts/${field}`,
367
    );
368
    if (props) query += "&props=" + JSON.stringify(props);
369
370
    return query;
371
  } // apiUserAlertCount
372
373
  /**
374
   * Construct API URL for user alerts (/uuser/:userId/alerts
375
   *
376
   * @param {string} userId user for alerts (logged in currently)
377
   * @param {string} field field to retrieve (default is unread)
378
   * @param {number} max maximum number of alerts to retrieve
379
   * @return {string} fully qualified URL
380
   */
381
  apiUserAlerts(userId, field = "", max = 20, props = null) {
382
    if (!userId) {
383
      this.error("apiUserAlert", "no userId or targetId given");
384
      return false;
385
    }
386
387
    if (props == null) props = {};
388
    props[API.BATCH_SIZE] = max;
389
390
    props = Array.isArray(props) ? props.join(",") : props;
391
392
    let query = this.getURL(this.urlHost, `/u/user/${userId}/alerts/${field}`);
393
    if (props) query += "?props=" + JSON.stringify(props);
394
395
    return query;
396
  } // apiUserAlerts
397
398
  /**
399
   * Construct API URL for user alerts status (/u/user/:userId/alerts/status/)
400
   *
401
   * @param {string} userId alerts for user (logged in currently)
402
   * @param {string[]} alertIds array if Ids to check/get. Note this
403
   * is mostly likely in the body using POST
404
   * @return {string} fully qualified URL
405
   */
406
  apiUserAlertsStatus(userId, alertIds = null) {
407
    if (!userId) {
408
      return false;
409
    }
410
    let idString = alertIds ? alertIds.join(",") : null;
411
412
    let query = this.getURL(this.urlHost, `/u/user/${userId}/alerts/status/`);
413
    if (idString) query += "?ids=" + idString;
414
415
    return query;
416
  } // apiUserAlertsStats
417
418
  /**
419
   * Construct API URL for confirmation by Id
420
   *
421
   * @param {string} confirmId alerts for user (logged in currently)
422
   * @param {string} sourceId array if Ids to check/get. Note this
423
   * is mostly likely in the body using POST
424
   * @return {string} fully qualified URL
425
   */
426
  apiConfirmById(confirmId, sourceId) {
427
    if (!confirmId) {
428
      this.error("apiCBI");
429
      return false;
430
    }
431
    let query = this.getURL(this.urlHost, `/s/confirm/${confirmId}`);
432
    if (sourceId) query = `${query}/src/${sourceId}`;
433
434
    return query;
435
  } // apiConfirmById
436
437
  // ---------------------------- FEED -----------------------------------
438
439
  /**
440
   * Add parameters to given URL related to batch fetching.
441
   *
442
   * @param {string} url
443
   * @param {number} offset if null then set to zero
444
   * @param {number} max maximum to return in this batch size
445
   * @param {number} startTime point in time as starting point for fetch in either direction
446
   * @param {string} direction fetch direction. Either API.DIRECTION_FORWARD or API.DIRECTION_BACKWARD
447
   * @param {string=} starter default to '?' and assume no other params already (dumb, I know)
448
   */
449
  appendFetchParams(
450
    url,
451
    offset,
452
    max,
453
    startTime,
454
    direction,
455
    starter,
456
    isComment = false,
457
  ) {
458
    if (offset == null) offset = 0;
459
460
    if (starter == null) starter = "?";
461
    url += `?${API.OFFSET}=${offset}`;
462
    if (max) url += `&${API.BATCH_SIZE}=${max}`;
463
    if (startTime) url += `&${API.START_TS}=${startTime}`;
464
    if (direction) url += `&${API.DIRECTION}=${direction}`;
465
    url += `&incl=posts|stats|userinfo|shared|liked`;
466
    return url;
467
  }
468
469
  // ------------------------ FOLLOW API URL -----------------------------
470
471
  /**
472
   * Construct API URL for user follows (/u/:userId/follows/:targetId)
473
   *
474
   * @param {string} userId ID of following user (logged in currently)
475
   * @param {string[]} props field names in array or delimited by comma
476
   * @return {string} fully qualified URL
477
   */
478
  apiAddFollows(userId, targetId, props = null) {
479
    if (!targetId) {
480
      this.error("apiAddFollows", "no userId or targetId given");
481
      return false;
482
    }
483
    props = Array.isArray(props) ? props.join(",") : props;
484
485
    let query = this.getURL(
486
      this.urlHost,
487
      `/u/user/${userId}/follows/${targetId}`,
488
    );
489
    if (props) query += "&props=" + props;
490
491
    return query;
492
  } // addFollows
493
494
  /**
495
   * Construct API URL for user follows (/u/:userId/follows/:targetId)
496
   *
497
   * @param {string} userId ID of following user (logged in currently)
498
   * @param {string[]} props field names in array or delimited by comma
499
   * @return {string} fully qualified URL
500
   */
501
  apiUserFollowStatus(userId, targetId, props = null) {
502
    if (!targetId || !userId) {
503
      return false;
504
    }
505
    props = Array.isArray(props) ? props.join(",") : props;
506
507
    let query = this.getURL(
508
      this.urlHost,
509
      `/u/user/${userId}/follows/${targetId}`,
510
    );
511
    if (props) query += "&props=" + props;
512
513
    return query;
514
  } // apiGetUserFollowStatus
515
516
  /**
517
   * Construct API URL for user followers (/u/:userId/follows/:targetId)
518
   *
519
   * @param {string} userId ID of following user (logged in currently)
520
   * @param {string[]} props field names in array or delimited by comma
521
   * @return {string} fully qualified URL
522
   */
523
  apiUserFollowerStatus(userId, followerId, props = null) {
524
    if (!userId && !followerId) {
525
      return false;
526
    }
527
    props = Array.isArray(props) ? props.join(",") : props;
528
529
    let query = this.getURL(
530
      this.urlHost,
531
      `/u/user/${followerId}/follows/${userId}`,
532
    );
533
    if (props) query += "&props=" + props;
534
535
    return query;
536
  } // apiGetUserFollowStatus
537
538
  /**
539
   * Construct API URL for retrieving follows (/u/:userId/follows/)
540
   *
541
   * @param {string} userId ID of users follows currently logged in
542
   * @param {string[]} props field names in array or delimited by comma
543
   * @return {string} fully qualified URL
544
   */
545
  apiGetFollows(userId, props = null) {
546
    props = Array.isArray(props) ? props.join(",") : props;
547
548
    let query = this.getURL(this.urlHost, `/u/user/${userId}/followings/`);
549
    if (props) query += "&props=" + props;
550
551
    return query;
552
  } // getFollows
553
554
  /**
555
   * Construct API URL for retrieving follows (/u/:userId/followers/)
556
   *
557
   * @param {string} userId ID to retrieve followers for
558
   * @param {string[]} props field names in array or delimited by comma
559
   * @return {string} fully qualified URL
560
   */
561
  apiGetFollowers(userId, props = null) {
562
    props = Array.isArray(props) ? props.join(",") : props;
563
564
    let query = this.getURL(this.urlHost, `/u/user/${userId}/followers/`);
565
    if (props) query += "&props=" + props;
566
567
    return query;
568
  } // getFollows
569
570
  /**
571
   * Construct API URL for user follows (/u/:userId/follows/:targetId)
572
   *
573
   * @param {string} userId ID of following user (logged in currently)
574
   * @param {string[]} props field names in array or delimited by comma
575
   * @return {string} fully qualified URL
576
   *
577
   * @see #apiAddFollows
578
   * @see #apiGetFollows
579
   * @see #apiRemoveFollows
580
   */
581
  apiRemoveFollows(userId, targetId, props = null) {
582
    if (!targetId) {
583
      this.error("apiRemoveFollows", "no userId or targetId given");
584
      return false;
585
    }
586
    props = Array.isArray(props) ? props.join(",") : props;
587
588
    let query = this.getURL(
589
      this.urlHost,
590
      `/u/user/${userId}/unfollows/${targetId}`,
591
    );
592
    if (props) query += "&props=" + props;
593
594
    return query;
595
  } // apiRemoveFollows
596
597
  /**
598
   * Construct API URL for user follows (/u/:userId/blocks/:targetId)
599
   *
600
   * @param {string} userId ID to block follower user (logged in currently)
601
   * @param {string} followerId follower to block
602
   * @param {string[]} props field names in array or delimited by comma
603
   * @return {string} fully qualified URL
604
   */
605
  apiBlockFollower(userId, followerId, props = null) {
606
    if (!followerId) {
607
      this.error("apiBlockFollower", "no userId or followerId given");
608
      return false;
609
    }
610
    props = Array.isArray(props) ? props.join(",") : props;
611
612
    let query = this.getURL(
613
      this.urlHost,
614
      `/u/user/${userId}/blocks/${followerId}`,
615
    );
616
    if (props) query += "&props=" + props;
617
618
    return query;
619
  } // apiBlockFollower
620
621
  /**
622
   * Construct API URL for user follows (/u/:userId/unblocks/:targetId)
623
   *
624
   * @param {string} userId ID to block follower user (logged in currently)
625
   * @param {string} followerId follower to block
626
   * @param {string[]} props field names in array or delimited by comma
627
   * @return {string} fully qualified URL
628
   */
629
  apiUnblockFollower(userId, followerId, props = null) {
630
    if (!followerId) {
631
      this.error("apiUnblockFollower", "no userId or followerId given");
632
      return false;
633
    }
634
    props = Array.isArray(props) ? props.join(",") : props;
635
636
    let query = this.getURL(
637
      this.urlHost,
638
      `/u/user/${userId}/unblocks/${followerId}`,
639
    );
640
    if (props) query += "&props=" + props;
641
642
    return query;
643
  } // apiUnblockFollower
644
645
  /**
646
   * Construct API URL to mute (/u/:userId/mutes/:targetId)
647
   *
648
   * @param {string} userId ID to mute user (logged in currently)
649
   * @param {string} followerId user to mute
650
   * @param {string[]} props field names in array or delimited by comma
651
   * @return {string} fully qualified URL
652
   */
653
  apiMuteFollower(userId, followerId, props = null) {
654
    if (!followerId) {
655
      this.error("apiMuteFollower", "no userId or followerId given");
656
      return false;
657
    }
658
    props = Array.isArray(props) ? props.join(",") : props;
659
660
    let query = this.getURL(
661
      this.urlHost,
662
      `/u/user/${userId}/mutes/${followerId}`,
663
    );
664
    if (props) query += "&props=" + props;
665
666
    return query;
667
  } // apiMuteFollower
668
669
  /**
670
   * Construct API URL to unmute (/u/:userId/unmutes/:targetId)
671
   *
672
   * @param {string} userId ID to unmute user (logged in currently)
673
   * @param {string} followerId user to unmute
674
   * @param {string[]} props field names in array or delimited by comma
675
   * @return {string} fully qualified URL
676
   */
677
  apiUnmuteFollower(userId, followerId, props = null) {
678
    if (!followerId) {
679
      this.error("apiUnmuteFollower", "no userId or followerId given");
680
      return false;
681
    }
682
    props = Array.isArray(props) ? props.join(",") : props;
683
684
    let query = this.getURL(
685
      this.urlHost,
686
      `/u/user/${userId}/unmutes/${followerId}`,
687
    );
688
    if (props) query += "&props=" + props;
689
690
    return query;
691
  } // apiUnmuteFollower
692
693
  // ------------------------ LIKE OBJECT API URL -----------------------------
694
695
  /**
696
   * Construct API URL for user like (/u/user/:userId/likes/{type}/:objectId)
697
   *
698
   *
699
   * @param {string} userId
700
   * @param {string} type object type (see ModelType)
701
   * @param {string} objectId
702
   * @param {string[]} props field names in array or delimited by comma
703
   * @return {string} fully qualified URL
704
   */
705
  apiAddLikeObject(userId, type, objectId, props = null) {
706
    if (!objectId) {
707
      this.error("apiAddLikeObj", "no userId or objectId given");
708
      return false;
709
    }
710
    props = Array.isArray(props) ? props.join(",") : props;
711
712
    let query = this.getURL(
713
      this.urlHost,
714
      `/u/user/${userId}/likes/${type}/${objectId}`,
715
    );
716
    if (props) query += "&props=" + props;
717
718
    return query;
719
  } // apiAddLikeObject
720
721
  /**
722
   * Construct API URL for user likes (/u/user/:userId/likes/{type}/:objectId)
723
   *e
724
   * @param {string} userId ID of liking user (logged in currently)
725
   * @param {string[]} props field names in array or delimited by comma
726
   * @return {string} fully qualified URL
727
   */
728
  apiUserLikeObjectStatus(userId, type, objectId, props = null) {
729
    if (!objectId || !userId) {
730
      return false;
731
    }
732
    props = Array.isArray(props) ? props.join(",") : props;
733
734
    let query = this.getURL(
735
      this.urlHost,
736
      `/u/user/${userId}/likes/${type}/${objectId}`,
737
    );
738
    if (props) query += "&props=" + props;
739
740
    return query;
741
  } // apiGetUserLikeObjectStatus
742
743
  /**
744
   * Construct API URL for retrieving follows (/u/:userId/likes/rl)
745
   *
746
   * @param {string} userId ID of users follows currently logged in
747
   * @param {string} type object type (see ModelType)
748
   * @param {string[]} props field names in array or delimited by comma
749
   * @return {string} fully qualified URL
750
   */
751
  apiGetLikesObject(userId, type, props = null) {
752
    props = Array.isArray(props) ? props.join(",") : props;
753
754
    let query = this.getURL(this.urlHost, `/u/user/${userId}/likes/${type}/`);
755
    if (props) query += "&props=" + props;
756
757
    return query;
758
  } // apiGetLikesObject
759
760
  /**
761
   * Construct API URL for retrieving follows (/u/:type/:objectId/liked/)
762
   *
763
   * @param {string} type object type (see ModelType)
764
   * @param {string} objectId ID to retrieve likes for
765
   * @param {string[]} props field names in array or delimited by comma
766
   * @return {string} fully qualified URL
767
   */
768
  apiGetLikedObject(type, objectId, props = null) {
769
    props = Array.isArray(props) ? props.join(",") : props;
770
771
    let query = this.getURL(this.urlHost, `/u/${type}/${objectId}/liked/`);
772
    if (props) query += "&props=" + props;
773
774
    return query;
775
  } // apiGetLikedPost
776
777
  /**
778
   * Construct API URL for user follows (/u/:userId/unlike/:type/:objectId)
779
   *
780
   * @param {string} userId ID of following user (logged in currently)
781
   * @param {string[]} props field names in array or delimited by comma
782
   * @return {string} fully qualified URL
783
   *
784
   * @see #apiAddLikesRL
785
   * @see #apiGetLikesRL
786
   */
787
  apiRemoveLikeObject(userId, type, objectId, props = null) {
788
    if (!objectId) {
789
      this.error("apiRmLikeObject", "no userId or objectId given");
790
      return false;
791
    }
792
    props = Array.isArray(props) ? props.join(",") : props;
793
794
    let query = this.getURL(
795
      this.urlHost,
796
      `/u/user/${userId}/unlike/${type}/${objectId}`,
797
    );
798
    if (props) query += "&props=" + props;
799
800
    return query;
801
  } // apiRemoveLikeObject
802
803
  // ------------------------ LIKE POST API URL -----------------------------
804
805
  /**
806
   * Construct API URL for user follows (/u/user/:userId/likes/rl/:postId)
807
   *
808
   * @param {string} userId ID of following user (logged in currently)
809
   * @param {string[]} props field names in array or delimited by comma
810
   * @return {string} fully qualified URL
811
   */
812
  apiAddLikePost(userId, postId, props = null) {
813
    return this.apiAddLikeObject(userId, ModelType.POST, postId, props);
814
    // if (!postId) {
815
    //   this.error("apiAddLikePost", "no userId or postId given");
816
    //   return false;
817
    // }
818
    // props = Array.isArray(props) ? props.join(",") : props;
819
820
    // let query = this.getURL( this.urlHost, `/u/user/${userId}/likes/post/${postId}`);
821
    // if (props) query += "&props=" + props;
822
823
    // return query;
824
  } // apiAddLikePost
825
826
  /**
827
   * Construct API URL for user likes (/u/user/:userId/likes/post/:postId)
828
   *
829
   * @param {string} userId ID of liking user (logged in currently)
830
   * @param {string[]} props field names in array or delimited by comma
831
   * @return {string} fully qualified URL
832
   */
833
  apiUserLikePostStatus(userId, postId, props = null) {
834
    if (!postId || !userId) {
835
      return false;
836
    }
837
    props = Array.isArray(props) ? props.join(",") : props;
838
839
    let query = this.getURL(
840
      this.urlHost,
841
      `/u/user/${userId}/likes/post/${postId}`,
842
    );
843
    if (props) query += "&props=" + props;
844
845
    return query;
846
  } // apiGetUserLikePostStatus
847
848
  /**
849
   * Construct API URL for retrieving follows (/u/:userId/likes/post)
850
   *
851
   * @param {string} userId ID of users follows currently logged in
852
   * @param {string[]} props field names in array or delimited by comma
853
   * @return {string} fully qualified URL
854
   */
855
  apiGetLikesPost(userId, props = null) {
856
    props = Array.isArray(props) ? props.join(",") : props;
857
858
    let query = this.getURL(this.urlHost, `/u/user/${userId}/likes/post/`);
859
    if (props) query += "&props=" + props;
860
861
    return query;
862
  } // apiGetLikesPost
863
864
  /**
865
   * Construct API URL for retrieving follows (/u/:userId/followers/)
866
   *
867
   * @param {string} postId ID to retrieve post
868
   * @param {string[]} props field names in array or delimited by comma
869
   * @return {string} fully qualified URL
870
   */
871
  apiGetLikedPost(postId, props = null) {
872
    props = Array.isArray(props) ? props.join(",") : props;
873
874
    let query = this.getURL(this.urlHost, `/u/post/${postId}/liked/`);
875
    if (props) query += "&props=" + props;
876
877
    return query;
878
  } // apiGetLikedRL
879
880
  /**
881
   * Construct API URL for user follows (/u/:userId/unlike/rl/:postId)
882
   *
883
   * @param {string} userId ID doing the like removal
884
   * @param {string[]} props field names in array or delimited by comma
885
   * @return {string} fully qualified URL
886
   *
887
   * @see #apiAddLikesPost
888
   * @see #apiGetLikesPost
889
   */
890
  apiRemoveLikePost(userId, postId, props = null) {
891
    if (!postId) {
892
      this.error("apiRemoveLikePost", "no userId or post given");
893
      return false;
894
    }
895
    props = Array.isArray(props) ? props.join(",") : props;
896
897
    let query = this.getURL(
898
      this.urlHost,
899
      `/u/user/${userId}/unlike/post/${postId}`,
900
    );
901
    if (props) query += "&props=" + props;
902
903
    return query;
904
  } // apiRemoveLikePost
905
906
  // ------------------------- SHARE API -------------------------------
907
908
  /**
909
   * Construct API URL for user shares (/u/user/:userId/likes/:type/:objectId)
910
   *
911
   * @param {string} userId ID of sharing user (logged in currently)
912
   * @param {string[]} props field names in array or delimited by comma
913
   * @return {string} fully qualified URL
914
   */
915
  apiUserShareObjectStatus(userId, type, objectId, props = null) {
916
    if (!objectId || !userId) {
917
      return false;
918
    }
919
    if (type == null) {
920
      this.error("apiGetUserShareStatus", "null type");
921
      this.trace();
922
    }
923
    props = Array.isArray(props) ? props.join(",") : props;
924
925
    let query = this.getURL(
926
      this.urlHost,
927
      `/u/user/${userId}/shares/${type}/${objectId}`, // deprecated - remove in 2021
928
      // `/u/user/${userId}/shares/${type}/${objectId}`,  // use this in 2021
929
    );
930
    if (props) query += "&props=" + props;
931
932
    return query;
933
  } // apiGetUserShareObjectStatus
934
935
  /**
936
   *
937
   * @param {XMObject} xmObject
938
   *
939
   * @return {boolean}
940
   */
941
  userCanShare(xmObject) {
942
    // let userId = this.getUserId();
943
944
    if (xmObject.hasACL() === false) return true;
945
946
    return true;
947
  }
948
949
  // ------------------------- USER SHARE SERVICES ------------------------------
950
951
  /**
952
   * Submit sharing of a post. "Sharing" in this csae is basically
953
   * a "repost" without any added content. This mean it is
954
   * literally a share. Standard POST is used.
955
   *
956
   * For reposting with user's own content, use userRepost()
957
   *
958
   * @param {string} postId post to share by logged in user
959
   * @param {string} text additional text from reposter
960
   *
961
   * @return {string} updated share status "y" or "n"
962
   *
963
   * @see ~SubmitRepost
964
   */
965
  async userSharesPost(postId, text, callback) {
966
    const _m = "userSharesPost";
967
968
    let loggedInUserId = this.getUserId();
969
    let shareStatus;
970
    let error = null;
971
    try {
972
      let getUrl = this.getURL(
973
        this.urlHost,
974
        `/u/user/${loggedInUserId}/shares/post/${postId}`,
975
      );
976
      let content = {
977
        text: text,
978
      };
979
      shareStatus = await this.requestPOST(getUrl, content);
980
    } catch (e) {
981
      this.error(_m, e);
982
      error = e;
983
      shareStatus = null;
984
    }
985
986
    return callback ? callback(error, shareStatus) : shareStatus;
987
  } // userSharesPost
988
989
  /**
990
   * remove shares.
991
   *
992
   *
993
   * @param {string} postId  postItem id
994
   *
995
   * @return {string} updated share status "y" or "n"
996
   *
997
   * @see ~SubmitRepost
998
   */
999
  async userUnshares(postId, action, callback) {
1000
    const _m = "userUnsharesPost";
1001
1002
    let loggedInUserId = this.getUserId();
1003
    let unshareStatus;
1004
    let error = null;
1005
    try {
1006
      let getUrl = this.getURL(
1007
        this.urlHost,
1008
        `/u/user/${loggedInUserId}/shares/${
1009
          action === "p" ? "post" : "comment"
1010
        }/${postId}`,
1011
      );
1012
      unshareStatus = await this.requestDELETE(getUrl);
1013
    } catch (e) {
1014
      this.error(_m, e);
1015
      error = e;
1016
      unshareStatus = null;
1017
    }
1018
1019
    return callback ? callback(error, unshareStatus) : unshareStatus;
1020
  } // userUnshares
1021
1022
  /**
1023
   * Submit sharing of a comment. "Sharing" in this csae is basically
1024
   * a "repost" without any added content. This mean it is
1025
   * literally a share. Standard POST is used.
1026
   *
1027
   * For reposting with user's own content, use submitRepost()
1028
   *
1029
   * @param {string} commentId post to share by logged in user
1030
   * @param {string} text additional text from reposter
1031
   *
1032
   * @return {string} updated share status "y" or "n"
1033
   *
1034
   * @see ~SubmitRepost
1035
   */
1036
  async userSharesComment(commentId, callback) {
1037
    const _m = "userSharesComment";
1038
1039
    let loggedInUserId = this.getUserId();
1040
    let shareStatus;
1041
    let error = null;
1042
    try {
1043
      let getUrl = this.getURL(
1044
        this.urlHost,
1045
        `/u/user/${loggedInUserId}/shares/comment/${commentId}`,
1046
      );
1047
      let content = null;
1048
      shareStatus = await this.requestPOST(getUrl, content);
1049
    } catch (e) {
1050
      this.error(_m, e);
1051
      error = e;
1052
      shareStatus = null;
1053
    }
1054
1055
    return callback ? callback(error, shareStatus) : shareStatus;
1056
  } // userSharesComment
1057
1058
  /**
1059
   * Retrieve answer to whether the logged in user is sharing post
1060
   *
1061
   * @param {string[]} postId
1062
   * @param {string[]} props properties to include (array or comma delimited string)
1063
   * Null to include defaults which is title only.
1064
   *
1065
   * @return {string} "y" or "no"
1066
   */
1067
  async userSharePostStatus(postId, props = null, callback) {
1068
    const _m = "userSharePostStatus";
1069
1070
    let statusValue = null;
1071
    let error = null;
1072
    let userId = this.getUserId();
1073
    if (!postId || !userId) {
1074
      return "no";
1075
    }
1076
    try {
1077
      let url = this.apiUserShareObjectStatus(userId, "post", postId, props);
1078
      statusValue = await this.requestGET(url, null);
1079
    } catch (e) {
1080
      this.error(_m, "server returned error:", e);
1081
      error = e;
1082
      statusValue = null;
1083
    }
1084
    return callback ? callback(error, statusValue) : statusValue;
1085
  } // userSharePostStatus
1086
1087
  /**
1088
   * Retrieve answer to whether the logged in user is sharing post
1089
   *
1090
   * @param {string[]} commentId
1091
   * @param {string[]} props properties to include (array or comma delimited string)
1092
   * Null to include defaults which is title only.
1093
   *
1094
   * @return {string} "y" or "no"
1095
   */
1096
  async userShareCommentStatus(commentId, props = null, callback) {
1097
    const _m = "userShareCommentStatus";
1098
1099
    let statusValue = null;
1100
    let error = null;
1101
    let userId = this.getUserId();
1102
    if (!commentId || !userId) {
1103
      return "no";
1104
    }
1105
    try {
1106
      let url = this.apiUserShareObjectStatus(
1107
        userId,
1108
        "comment",
1109
        commentId,
1110
        props,
1111
      );
1112
      statusValue = await this.requestGET(url, null);
1113
    } catch (e) {
1114
      this.error(_m, "server returned error:", e);
1115
      error = e;
1116
      statusValue = null;
1117
    }
1118
    return callback ? callback(error, statusValue) : statusValue;
1119
  } // userShareCommentStatus
1120
1121
  // ------------------------ WATCH OBJECT API URLs -----------------------------
1122
1123
  /**
1124
   * Construct API URL for user watching an object. The URL for this
1125
   * API should be (POST): /u/user/:userId/watch/{type}/:objId
1126
   *
1127
   * @param {string} type object's type name (tag, rl, etc)
1128
   * @param {string} userId ID of following user (logged in currently)
1129
   * @param {string} objectId ID of object to watch
1130
   * @param {string[]} props field names in array or delimited by comma
1131
   * @return {string} fully qualified URL
1132
   */
1133
  apiAddWatchObject(type, userId, objectId, props = null) {
1134
    if (!objectId && !objectId) {
1135
      this.error("apiAddWatchObj", "no userId or objId given");
1136
      return false;
1137
    }
1138
    props = Array.isArray(props) ? props.join(",") : props;
1139
1140
    let query = this.getURL(
1141
      this.urlHost,
1142
      `/u/user/${userId}/watch/${type}/${objectId}`,
1143
    );
1144
    if (props) query += "&props=" + props;
1145
1146
    return query;
1147
  }
1148
1149
  /**
1150
   * Construct API URL for status of the object the user may be watching. The
1151
   * URL for this API should be (GET): /u/user/:userId/watch/{type}/:objId
1152
   *
1153
   * @param {string} type object's type name (tag, rl, etc)
1154
   * @param {string} userId ID of the user
1155
   * @param {string} objectId ID of the object watched
1156
   * @param {string[]} props field names in array or delimited by comma
1157
   * @return {string} fully qualified URL
1158
   */
1159
  apiUserWatchObjectStatus(type, userId, objectId, props = null) {
1160
    if (!objectId || !type || !userId) {
1161
      return false;
1162
    }
1163
    props = Array.isArray(props) ? props.join(",") : props;
1164
1165
    let query = this.getURL(
1166
      this.urlHost,
1167
      `/u/user/${userId}/watch/${type}/${objectId}`,
1168
    );
1169
    if (props) query += "&props=" + props;
1170
1171
    return query;
1172
  }
1173
1174
  /**
1175
   * Construct API URL for retrieving follows (/u/:userId/watch/rl)
1176
   *
1177
   * @param {string} userId ID of users follows currently logged in
1178
   * @param {string[]} props field names in array or delimited by comma
1179
   * @return {string} fully qualified URL
1180
   */
1181
  apiGetWatchesObject(type, userId, props = null) {
1182
    props = Array.isArray(props) ? props.join(",") : props;
1183
1184
    let query = this.getURL(this.urlHost, `/u/user/${userId}/watch/${type}/`);
1185
    if (props) query += "&props=" + props;
1186
1187
    return query;
1188
  }
1189
1190
  /**
1191
   * Construct API URL for retrieving watchers (/u/:type/:objectId/watched/)
1192
   *
1193
   * @param {string} type object's type name (tag, rl, etc)
1194
   * @param {string} objectId ID to retrieve followers for
1195
   * @param {string[]} props field names in array or delimited by comma
1196
   * @return {string} fully qualified URL
1197
   */
1198
  apiGetObjectWatchers(type, objectId, props = null) {
1199
    props = Array.isArray(props) ? props.join(",") : props;
1200
1201
    let query = this.getURL(this.urlHost, `/u/${type}/${objectId}/watched/`);
1202
    if (props) query += "&props=" + props;
1203
1204
    return query;
1205
  }
1206
1207
  /**
1208
   * Construct API URL for user follows (/u/:userId/unlike/:type/:objectId)
1209
   *
1210
   * @param {string} type object's type name (tag, rl, etc)
1211
   * @param {string} userId ID of following user (logged in currently)
1212
   * @param {string[]} props field names in array or delimited by comma
1213
   * @return {string} fully qualified URL
1214
   *
1215
   */
1216
  apiRemoveWatchObject(type, userId, objectId, props = null) {
1217
    if (!objectId) {
1218
      this.error("apiRmWatchObject", "no userId or objectId given");
1219
      return false;
1220
    }
1221
    props = Array.isArray(props) ? props.join(",") : props;
1222
1223
    let query = this.getURL(
1224
      this.urlHost,
1225
      `/u/user/${userId}/unwatch/${type}/${objectId}`,
1226
    );
1227
    if (props) query += "&props=" + props;
1228
1229
    return query;
1230
  }
1231
1232
  // -------------------------- Privilege Check ---------------------------
1233
1234
  userIsGod() {
1235
    return this.portal.userIsGod();
1236
  }
1237
1238
  /**
1239
   * @return {boolean}
1240
   */
1241
  userHasAdminRole() {
1242
    return this.portal.userHasAdminRole();
1243
  }
1244
1245
  /**
1246
   * @return {boolean}
1247
   */
1248
  userHasSysAdminRole() {
1249
    return this.portal.userHasSysAdminRole();
1250
  }
1251
1252
  userHasModeratorRole() {
1253
    return this.portal.userHasModeratorRole();
1254
  }
1255
1256
  /**
1257
   * Not Used
1258
   *
1259
   * @return {boolean}
1260
   */
1261
  userHasPreviewFeatures() {
1262
    return this.portal.userHasPreviewFeatures();
1263
  }
1264
1265
  /**
1266
   * @deprecated
1267
   *
1268
   * @return {boolean}
1269
   */
1270
  userHasSocialFeatures() {
1271
    return true;
1272
  }
1273
1274
  /**
1275
   * @return {boolean}
1276
   */
1277
  userHasFeature(featureId) {
1278
    return this.getSession().userHasFeature(featureId);
1279
  }
1280
1281
  /**
1282
   * @return {boolean}
1283
   */
1284
  userHasFeatureDisabled(featureId) {
1285
    return this.getSession().userHasFeatureDisabled(featureId);
1286
  }
1287
1288
  /**
1289
   * @return {boolean}
1290
   */
1291
  userCanFollow() {
1292
    return this.userHasFeature(FEATURE_FOLLOW_USER);
1293
  }
1294
1295
  /**
1296
   * @return {boolean}
1297
   */
1298
  userCanPost() {
1299
    return this.userHasFeature(FEATURE_SUBMIT_POST);
1300
  }
1301
1302
  /**
1303
   * @return {boolean}
1304
   */
1305
  userCanRepost() {
1306
    return this.userHasFeature(FEATURE_REPOST);
1307
  }
1308
1309
  /**
1310
   * @return {boolean}
1311
   */
1312
  userCanReplyPost() {
1313
    return this.userHasFeature(FEATURE_REPLY_POST);
1314
  }
1315
1316
  /**
1317
   * @return {boolean}
1318
   */
1319
  userCanReplyComment() {
1320
    return this.userHasFeature(FEATURE_REPLY_COMMENT);
1321
  }
1322
1323
  /**
1324
   * @return {boolean}
1325
   */
1326
  userCanLike() {
1327
    return this.userHasFeature(FEATURE_LIKE);
1328
  }
1329
1330
  /**
1331
   * Determine if the given resource can be edited
1332
   * by current logged in user.
1333
   *
1334
   * @param {XMObject} instance of XMObject subclass
1335
   *
1336
   * @return {boolean}
1337
   */
1338
  canEditResource(xmObject) {
1339
    let isOwner = this.userIsOwner(xmObject);
1340
    if (isOwner) return true;
1341
1342
    if (this.userHasModeratorRole()) return true;
1343
1344
    // ACL check - Future/TBD
1345
1346
    return false;
1347
  } // canEditResource
1348
1349
  canShareResource(xmObject) {
1350
    return true;
1351
  }
1352
1353
  canDeleteResource(xmObject) {
1354
    return this.canEditResource(xmObject);
1355
  }
1356
1357
  canSubmitPost() {
1358
    return this.userHasFeature(FEATURE_SUBMIT_POST);
1359
  }
1360
1361
  // --------------------------------------------------------------------------
1362
1363
  /**
1364
   * Determine if the given object's owner is the
1365
   * logged in user.
1366
   *
1367
   * @param {XMObject} xmObject
1368
   *
1369
   * @return {booleaan}
1370
   */
1371
  userIsOwner(xmObject) {
1372
    let loggedInUserId = this.getUserId();
1373
    if (loggedInUserId == null) return false;
1374
1375
    let objectOwnerId = xmObject.getOwnerId();
1376
1377
    // simple check for now
1378
    return loggedInUserId === objectOwnerId;
1379
  } // userIsOwner
1380
1381
  trackContainer(container) {
1382
    this.appContainer = container;
1383
  }
1384
1385
  async refreshContainer() {
1386
    if (this.appContainer) this.appContainer.refreshView(true);
1387
  }
1388
1389
  /**
1390
   * Retrieve stats on an object
1391
   *
1392
   * @param {string} type object type
1393
   * @param {string[]} objectId
1394
   * @param {boolean} inclObj also return the XObject for which the stats are for
1395
   *
1396
   * @return {XObjectStat, [XObject, XObjectStat]}
1397
   */
1398
  async fetchObjectStats(type, objectId, inclObj = false, callback) {
1399
    const _m = "fetchObjectStatus";
1400
1401
    let result;
1402
    let error = null;
1403
    try {
1404
      let apiUrl = this.apiGetObjectStats(type, objectId, inclObj);
1405
      result = await this.requestGET(apiUrl, null);
1406
    } catch (e) {
1407
      this.error(_m, e);
1408
      error = e;
1409
      result = null;
1410
    }
1411
    return callback ? callback(error, result) : result;
1412
  } // fetchObjectStats
1413
1414
  // ----------------------- USER NEWS FEED --------------------------------
1415
1416
  /**
1417
   * Fetch user timline using /u/user/:userId/timeline. Timeline includes
1418
   * all user followings posts.
1419
   *
1420
   * @param {string} userId which user to retrieve posts for?
1421
   * @param {number} offset offset into start position of the expected query
1422
   * @param {number} size batch size
1423
   * @param {number} startTS timestamp where the posts streaming should start
1424
   * @callback (err, PostList)
1425
   * @return {PostList}
1426
   */
1427
  async fetchUserTimeline(
1428
    userId,
1429
    offset = null,
1430
    size = null,
1431
    startTS,
1432
    direction,
1433
    callback,
1434
  ) {
1435
    let _m = `getUserTimeline(${userId})`;
1436
    this.log(_m, `Fetching: offset=${offset}, max=${size}`);
1437
    let err = null;
1438
    let feedList;
1439
    try {
1440
      let url = this.getURL(this.urlHost, `/u/user/${userId}/timeline`);
1441
      url = this.appendFetchParams(url, offset, size, startTS, direction);
1442
      let result = await this.requestGET(url, null);
1443
      feedList = XPostFeed.Wrap(result);
1444
    } catch (e) {
1445
      this.log(_m, e);
1446
      if (callback == null) throw e;
1447
    }
1448
1449
    return callback ? callback(err, feedList) : feedList;
1450
  } // fetchUserTimeline
1451
1452
  async fetchUserPostsFeed(userId, offset, size, startTS, direction, callback) {
1453
    return this.fetchUserTimeline(
1454
      userId,
1455
      offset,
1456
      size,
1457
      startTS,
1458
      direction,
1459
      callback,
1460
    );
1461
  }
1462
1463
  /**
1464
   * Fetch user posts /u/user/:userId/posts. This
1465
   * must be called on behalf of the user request (therefore /u).
1466
   *
1467
   * @param {string} userId which user to retrieve posts for?
1468
   * @param {number} offset offset into start position of the expected query
1469
   * @param {number} size batch size
1470
   * @param {number} startTS timestamp where the posts streaming should start
1471
   * @callback (err, PostList)
1472
   * @return {PostList}
1473
   */
1474
  async fetchUserPosts(
1475
    userId,
1476
    offset = null,
1477
    size = null,
1478
    startTS,
1479
    direction,
1480
    callback,
1481
    options = null,
1482
    medias = null,
1483
  ) {
1484
    let _m = `getUserPosts(${userId})`;
1485
    this.log(_m, `Fetching: offset=${offset}, max=${size}`);
1486
    let err = null;
1487
    let feedList;
1488
    try {
1489
      let url = this.getURL(this.urlHost, `/u/user/${userId}/posts`);
1490
      url = this.appendFetchParams(
1491
        url,
1492
        offset,
1493
        size,
1494
        startTS,
1495
        direction,
1496
        null,
1497
        options === "c" || options === "l" ? true : false,
1498
      );
1499
      let result = await this.requestGET(
1500
        url +
1501
          (medias ? "&fp=f_um" : "&fp=f_u") +
1502
          (options ? options : medias ? "" : "o"),
1503
        null,
1504
      );
1505
      feedList = XPostFeed.Wrap(result);
1506
    } catch (e) {
1507
      this.log(_m, e);
1508
      if (callback == null) throw e;
1509
    }
1510
1511
    return callback ? callback(err, feedList) : feedList;
1512
  } // fetchUserPostFeed
1513
1514
  async getSearchByPhrase(phrase, offset, max, callback) {
1515
    let _m = `getSearchByPhrase(${phrase})`;
1516
    let err = null;
1517
    let feedList;
1518
1519
    try {
1520
      let encodedPhrase = "#" + phrase.slice(3);
1521
      let url = this.getURL(this.urlHost, `/u/posts/srch/phrase`);
1522
      let result = await this.requestPOST(url, {
1523
        q: encodedPhrase,
1524
        offset,
1525
        max,
1526
      });
1527
      feedList = XPostFeed.Wrap(result);
1528
    } catch (e) {
1529
      this.log(_m, e);
1530
      if (callback === null) throw e;
1531
    }
1532
    return callback ? callback(err, feedList) : feedList;
1533
  }
1534
1535
  async getTopSearchResult(phrase, offset, max, callback) {
1536
    let _m = `getSearchByPhrase(${phrase})`;
1537
1538
    let feedList;
1539
    let err;
1540
1541
    try {
1542
      let url = this.getURL(this.urlHost, `/u/posts/srch/phrase`);
1543
      let result = await this.requestPOST(url, {
1544
        q: phrase,
1545
        offset,
1546
        max,
1547
      });
1548
1549
      feedList = XPostFeed.Wrap(result);
1550
    } catch (e) {
1551
      err = e;
1552
      this.log(_m, e);
1553
      if (callback === null) throw e;
1554
    }
1555
    return callback ? callback(feedList || err) : feedList || err;
1556
  }
1557
1558
  /**
1559
   * Execute search of posts by phrase and return a batch of posts using the
1560
   * feed format.
1561
   *
1562
   * @param {string} phrase a string that user typed
1563
   * @param {number} offset offset into start position of the expected query
1564
   * @param {number} size batch size
1565
   * @param {number} startTS timestamp where the posts streaming should start
1566
   * @param {function} callback
1567
   * @return {XPostFeed} collection of XPostItems, which wrap
1568
   */
1569
  async getSearchPostsFeed(
1570
    phrase,
1571
    offset = null,
1572
    size = null,
1573
    startTS,
1574
    direction,
1575
    callback,
1576
  ) {
1577
    let _m = "";
1578
    // this.log(_m, `Fetching for userId: ${userId}`);
1579
    let err = null;
1580
    let url = this.getURL(this.urlHost, `/u/posts/srch/phrase`); // @depcrecated
1581
    // let url = this.getURL(this.urlHost, `/u/posts/srch/phrase`); // to use with 12/3/2020 checkin but need to be pushed to cloud
1582
1583
    url = this.appendFetchParams(url, offset, size, startTS, direction);
1584
    let params = {
1585
      q: phrase,
1586
    };
1587
    let feedList;
1588
    try {
1589
      let result = await this.requestPOST(url, params);
1590
      feedList = XPostFeed.Wrap(result);
1591
    } catch (e) {
1592
      this.log(_m, e);
1593
      throw e;
1594
    }
1595
1596
    return callback ? callback(err, feedList) : feedList;
1597
  } // getSearchPostsFeed
1598
1599
  /**
1600
   * Return a feed of posts that system identify as trendy / news. This
1601
   * is more suitable as a home page.
1602
   *
1603
   * @param {string} topics
1604
   * @param {number} offset
1605
   * @param {number} size
1606
   * @callback
1607
   * @return {XPostFeed}
1608
   */
1609
  async getTrendsPostsFeed(
1610
    topics,
1611
    offset = null,
1612
    size = null,
1613
    startTs,
1614
    lang,
1615
    callback = null,
1616
  ) {
1617
    if (!lang) lang = "en";
1618
    const _m = "gTPF";
1619
    let err;
1620
    let url = this.getURL(this.urlHost, `/u/posts/trends`);
1621
    url = this.appendFetchParams(url, offset, size, startTs);
1622
    if (lang) url += `&${API.FILTER_LANGUAGE_PREF}=${lang}`;
1623
    let params = topics
1624
      ? {
1625
          [API.FILTER_LANGUAGE_PREF]: topics,
1626
          [API.FILTER_TOPICS]: lang,
1627
        }
1628
      : null;
1629
    let feedList;
1630
    try {
1631
      let result = await this.requestGET(url, params);
1632
      feedList = XPostFeed.Wrap(result);
1633
    } catch (e) {
1634
      this.log(_m, e);
1635
      err = e;
1636
      if (!callback) throw e;
1637
    }
1638
    return callback ? callback(err, feedList) : feedList;
1639
  }
1640
1641
  /**
1642
   * Fetch post with post stats and userinfo as piggybacked data
1643
   * in aux fields.
1644
   *
1645
   * @param {string} commentId
1646
   * @callback
1647
   * @return {XMPost} comment object with aux data
1648
   * keyed by ModelType.COMMENT_STATS and ModelType.USERINFO
1649
   */
1650
  fetchPostWithStats_UserInfo(commentId, callback) {
1651
    let inclOptions = API.INCL_POSTSTATS + "|" + API.INCL_USERINFO;
1652
    return this.fetchPost(commentId, inclOptions, false, callback);
1653
  }
1654
1655
  /**
1656
   * Fetch a post object
1657
   *
1658
   * @param {string} postId ID for post
1659
   * @param {string} inclOptions API.INCL_POSTSTATS|API.INCL_USERINFO
1660
   * @param {boolean} cache true to ask ObjectManager to track it
1661
   * @return {XMPost}
1662
   * @callback {XError, XMPost}
1663
   *
1664
   * @see ~fetchPostWithStatsAndUser
1665
   */
1666
  fetchPost(postId, inclOptions = null, cache = false, callback) {
1667
    let _m = `fetchPost(${postId})`;
1668
    let p = new Promise((resolve, reject) => {
1669
      let processResults = (err, postObj) => {
1670
        if (err) {
1671
          this.error(_m, `Error post ${postId}`);
1672
          console.error(err);
1673
          return callback ? callback(err, null) : reject(err);
1674
        }
1675
        if (callback) callback(null, postObj);
1676
        resolve(postObj);
1677
      };
1678
      let params = inclOptions ? {[API.PARAM_INCL]: inclOptions} : null;
1679
      this.user_getResource(
1680
        postId,
1681
        ModelType.POST,
1682
        params,
1683
        cache,
1684
        processResults,
1685
      );
1686
    });
1687
1688
    return p;
1689
  }
1690
1691
  /**
1692
   * Retrieve stats for a post
1693
   *
1694
   * @param {string[]} postId
1695
   * @param {boolean} inclObj include XMPost object in the return result
1696
   *
1697
   * @return {XPostStat | [XMPost,XPostStat]} either single object, or two objects
1698
   */
1699
  async fetchPostStats(postId, inclObj = false, callback) {
1700
    const _m = "fetchPostStats";
1701
1702
    let result;
1703
    let error = null;
1704
1705
    try {
1706
      let apiUrl = this.apiGetObjectStats(ModelType.POST, postId, inclObj);
1707
      result = await this.requestGET(apiUrl, null);
1708
    } catch (e) {
1709
      this.error(_m, e);
1710
      error = e;
1711
      result = null;
1712
    }
1713
    return callback ? callback(error, result) : result;
1714
  }
1715
1716
  // ----------------------------- COMMENTS ------------------------------
1717
1718
  /**
1719
   * Fetch comments of a post, or as replies of a comment.  Currently
1720
   * we determine which by the prefix of the Id
1721
   *
1722
   * @param {string} parentId either postId or a commentId.
1723
   * @param {number} offset offset into start position of the expected query
1724
   * @param {number} size batch size
1725
   * @param {number} startTS timestamp where the posts streaming should start
1726
   * @callback (err, XCommentFeed)
1727
   * @return {XCommentFeed}
1728
   */
1729
  async fetchComments(
1730
    parentId,
1731
    offset = null,
1732
    size = null,
1733
    startTS,
1734
    direction,
1735
    callback,
1736
  ) {
1737
    let isComment = parentId.startsWith(PREFIX_COMMENT_ID) ? true : false;
1738
    let _m = `fetchComments(${isComment ? "comment" : "post"}:${parentId})`;
1739
    this.log(_m, `Fetching: offset=${offset}, max=${size}`);
1740
    let err = null;
1741
1742
    let endpoint = isComment
1743
      ? `/u/comment/${parentId}/comments`
1744
      : `/u/post/${parentId}/comments`;
1745
1746
    let feedList;
1747
    try {
1748
      let url = this.getURL(this.urlHost, endpoint);
1749
      url = this.appendFetchParams(
1750
        url,
1751
        offset,
1752
        size,
1753
        startTS,
1754
        direction,
1755
        null,
1756
        true,
1757
      );
1758
      let result = await this.requestGET(url, null);
1759
      feedList = XCommentFeed.Wrap(result); // should already been wrapped...
1760
    } catch (e) {
1761
      this.log(_m, e);
1762
      if (callback == null) throw e;
1763
    }
1764
1765
    return callback ? callback(err, feedList) : feedList;
1766
  } // fetchComments
1767
1768
  /**
1769
   * Fetch a comment object with piggybacked comment stats,
1770
   * user info, associated post object, and its post stats
1771
   *
1772
   * @param {string} commentId
1773
   * @callback
1774
   *
1775
   * @return {XMComment} comment object with aux object
1776
   * keyed by ModelType.COMMENT_STATS and ModelType.USERINFO,
1777
   * ModelType.POST, and ModelType.POST_STATS
1778
   */
1779
  fetchCommentWithStats_UserInfo_Post(commentId, callback) {
1780
    // API.STATS will be honored by all objects, but in this case we don't
1781
    // want UserInfo to come back with UserStats
1782
    let inclOptions = `${API.INCL_COMMENTSTATS}|${API.INCL_USERINFO}|${API.INCL_POSTS}|${API.INCL_POSTSTATS}`;
1783
    return this.fetchComment(commentId, inclOptions, false, callback);
1784
  }
1785
1786
  /**
1787
   * Fetch a comment object with piggybacked comment stats and userinfo
1788
   *
1789
   * @param {string} commentId
1790
   * @callback
1791
   *
1792
   * @return {XMComment} comment object with aux object
1793
   * keyed by ModelType.COMMENT_STATS and ModelType.USERINFO
1794
   */
1795
  fetchCommentWithStats_UserInfo(commentId, callback) {
1796
    let inclOptions = API.INCL_COMMENTSTATS + "|" + API.INCL_USERINFO;
1797
    return this.fetchComment(commentId, inclOptions, false, callback);
1798
  }
1799
1800
  /**
1801
   * Fetch a comment object from server
1802
   *
1803
   * @param {string} commentId ID for post
1804
   * @param {string} inclOptions INCL_COMMENTSTATS|INCL_USERINFO
1805
   * @param {boolean} cache true to ask ObjectManager to track it
1806
   * @return {XMComment}
1807
   * @callback {XError, XMComment}
1808
   *
1809
   * @see ~fetchComment
1810
   */
1811
  fetchComment(commentId, inclOptions, cache = false, callback) {
1812
    let _m = `fetchComment(${commentId})`;
1813
    let p = new Promise((resolve, reject) => {
1814
      let processResults = (err, commentObj) => {
1815
        if (err) {
1816
          this.error(_m, `Error comment ${commentId}`);
1817
          console.error(err);
1818
          return callback ? callback(err, null) : reject(err);
1819
        }
1820
        if (callback) callback(null, commentObj);
1821
        resolve(commentObj);
1822
      };
1823
      let params = inclOptions ? {[API.PARAM_INCL]: inclOptions} : null;
1824
      this.user_getResource(
1825
        commentId,
1826
        "comment",
1827
        params,
1828
        cache,
1829
        processResults,
1830
      );
1831
    });
1832
1833
    return p;
1834
  }
1835
1836
  /**
1837
   * Retrieve comment stats object from server
1838
   *
1839
   * @param {string[]} commentId
1840
   * @param {boolean} inclObj include XMPost object in the return result
1841
   *
1842
   * @return {XMCommentStat | [XMComment,XMCommentStats]} either single object, or two objects
1843
   */
1844
  async fetchCommentStats(commentId, inclObj = false, callback) {
1845
    const _m = "fetchCommentStats";
1846
1847
    let result;
1848
    let error = null;
1849
1850
    try {
1851
      let apiUrl = this.apiGetObjectStats("comment", commentId, inclObj);
1852
      result = await this.requestGET(apiUrl, null);
1853
    } catch (e) {
1854
      this.error(_m, e);
1855
      error = e;
1856
      result = null;
1857
    }
1858
    return callback ? callback(error, result) : result;
1859
  }
1860
1861
  // --------------------------- USER SERVICES -----------------------------
1862
1863
  /**
1864
   * Check whether a user exists.
1865
   *
1866
   * NOTE: currently, it does not return user status, which includes suspended/inactive.
1867
   * So it's mainly used for use during sign-up
1868
   *
1869
   * @param {string[]} userId
1870
   * @param {string[]} props properties to include (array or comma delimited string)
1871
   * Null to include defaults which is title only.
1872
   *
1873
   * @return {object} map with tagname is key, and requested props: {title: <text>} as default
1874
   */
1875
  async checkUserExists(userId, callback) {
1876
    const _m = "chkUID";
1877
    let verdict = null;
1878
    let error = null;
1879
    try {
1880
      let checkUserUrl = this.getURL(this.urlHost, `/u/user/${userId}/exists`);
1881
      verdict = await this.requestGET(checkUserUrl, null);
1882
    } catch (e) {
1883
      this.error(_m, e);
1884
      error = e;
1885
    }
1886
    return callback ? callback(error, verdict) : verdict;
1887
  } // checkUserExists
1888
1889
  /**
1890
   * Check whether an email exists.
1891
   *
1892
1893
   * @param {string[]} email
1894
   *
1895
   * @return {object} map with tagname is key, and requested props: {title: <text>} as default
1896
   */
1897
  async checkEmailExists(email, callback) {
1898
    const _m = "chkEm";
1899
    let verdict = false;
1900
    let error = null;
1901
    if (Util.EmailIsValid(email)) {
1902
      try {
1903
        const encoded = encodeURIComponent(email);
1904
        let checkUserUrl = this.getURL(
1905
          this.urlHost,
1906
          `/s/email/exists?email=${email}`,
1907
        );
1908
        verdict = await this.requestGET(checkUserUrl, null);
1909
      } catch (e) {
1910
        this.error(_m, e);
1911
        error = e;
1912
      }
1913
    } else {
1914
      error = new XError.New(ErrorCodes.USER_BAD_INPUT, "Invalid Email");
1915
    }
1916
1917
    return callback ? callback(error, verdict) : verdict;
1918
  } // checkEmailExists
1919
1920
  /**
1921
   * Get user's status with the system
1922
   *
1923
   * @param {string[]} userId
1924
   *
1925
   * @return {string} user status (see UserProps.STATUS)
1926
   */
1927
  async getUserStatus(userId, callback) {
1928
    const _m = "getUserStatus";
1929
    let verdict = null;
1930
    let error = null;
1931
    try {
1932
      let checkUserUrl = this.getURL(this.urlHost, `/s/user/${userId}/status`);
1933
      // debugger;
1934
      verdict = await this.requestGET(checkUserUrl, null);
1935
    } catch (e) {
1936
      this.error(_m, e);
1937
      error = e;
1938
    }
1939
    return callback ? callback(error, verdict) : verdict;
1940
  } // checkUserStatus
1941
1942
  /**
1943
   * Retrieve nickname from userID. This convenient
1944
   * method assumes user object is already in cache.
1945
   *
1946
   * @param {string} userId user ID to lookup
1947
   */
1948
  getUserNickname(userId, defaultVal = null) {
1949
    let om = this.getObjectManager();
1950
    let usrObj = om.getFromCache(userId, null, null);
1951
1952
    return usrObj ? usrObj.getNickname() : defaultVal;
1953
  }
1954
1955
  async updateUserProfile(objId, data, userId) {
1956
    let _m = "updateUserProfile";
1957
    let token = this.portal.getUserToken();
1958
    let error = null;
1959
    let result = null;
1960
    try {
1961
      let formData = new FormData();
1962
1963
      for (let key in data) {
1964
        if (key === "username") {
1965
          formData.append("nickname", data.username);
1966
        } else if (key === "bio") {
1967
          formData.append("dsc", data.bio);
1968
        } else if (data[key] === "ico") {
1969
          formData.append(key, data[key]);
1970
        } else if (data[key] === "bgimg") {
1971
          formData.append(key, data[key]);
1972
        } else {
1973
          formData.append(key, data[key]);
1974
        }
1975
      }
1976
      let postUrl = this.getURL(this.urlHost, `/u/user/${userId}/profile`);
1977
1978
      let xAuth =
1979
        userId === null
1980
          ? `{"user": null, "token": null}`
1981
          : `{"user": "${userId}", "token": "${token}"}`;
1982
      let config = {
1983
        headers: {
1984
          "Content-Type": "multipart/form-data",
1985
          "x-app-auth": xAuth,
1986
        },
1987
      };
1988
      result = await axios({
1989
        url: postUrl,
1990
        method: "post",
1991
        data: formData,
1992
        ...config,
1993
      });
1994
    } catch (e) {
1995
      this.error(_m, e);
1996
      error = e;
1997
    }
1998
    return result;
1999
  }
2000
2001
  /**
2002
   * Fetch a user info record from server
2003
   *
2004
   * @param {string} userId use user ID
2005
   * @param params any arguments or filters
2006
   * @callback
2007
   * @return {XUserInfo}
2008
   */
2009
  async fetchUserInfo(userId, params, callback) {
2010
    let _m = "fetchUserInfo";
2011
    let p = new Promise((resolve, reject) => {
2012
      let processResults = (err, userInfo) => {
2013
        // this.log(_m, "user info retrieved:", userInfo);
2014
        if (err) {
2015
          this.error(_m, err);
2016
          if (callback) callback(err, null);
2017
          reject(err);
2018
        } else {
2019
          if (callback) callback(null, userInfo);
2020
          resolve(userInfo);
2021
        }
2022
      }; // processResults
2023
      this.getResource(
2024
        userId,
2025
        ModelType.USER_INFO,
2026
        null,
2027
        false,
2028
        processResults,
2029
      );
2030
    });
2031
2032
    return p;
2033
  } // fetchuserInfo
2034
2035
  /**
2036
   * Force refresh of user info by reading from server and
2037
   * then update session/cookie
2038
   *
2039
   * @param {function} callback in case not using promise
2040
   */
2041
  async refreshUserInfo(callback) {
2042
    let userInfo;
2043
    let errObj;
2044
    try {
2045
      let userId = this.getUserId();
2046
      userInfo = await this.fetchUserInfo(userId);
2047
      if (userInfo) {
2048
        // this.log("refreshUserInfo", "info", userInfo);
2049
        this.updateUserInfo(userInfo);
2050
      }
2051
    } catch (err) {
2052
      if (callback == null) throw err;
2053
    }
2054
    return callback ? callback(errObj, userInfo) : userInfo;
2055
  } // refreshUserInfo
2056
2057
  /**
2058
   * Invalidate a settings group and force re-retrieve.
2059
   *
2060
   * @param {string} section one of UserProps.SETTINGS_*
2061
   * @param {{}} params pass to server
2062
   * @param {function} callback in case not using promise
2063
   */
2064
  async invalidateSettings(section, params, callback) {
2065
    // for now, we only allow refreshing the profile section
2066
    if (UserProps.SETTINGS_PROFILE !== section) return;
2067
2068
    return this.refreshUserInfo(callback);
2069
  } // invalidateSettings
2070
2071
  /**
2072
   * Fetch a user settings record from server
2073
   *
2074
   * @param {string} userId use user ID
2075
   * @param {string} section one of UserProps.SETTINGS_*
2076
   * @param {{}} params any arguments or filters
2077
   * @return {XUserInfo} subclass of it which is basically specific
2078
   * settings like XAccountSettings, XProfileSettings, etc.
2079
   */
2080
  async fetchUserSettings(userId, section, params, callback) {
2081
    const _m = "fetchUserSettings";
2082
    let settingsObj;
2083
    let error = null;
2084
    try {
2085
      let getSettingsUrl = this.apiGetUserSettings(userId, section, params);
2086
      settingsObj = await this.requestGET(getSettingsUrl, null);
2087
      settingsObj = XMObject.Wrap(settingsObj);
2088
    } catch (e) {
2089
      this.error(_m, "server returned error:", e);
2090
      error = e;
2091
      settingsObj = null;
2092
    }
2093
    return callback ? callback(error, settingsObj) : settingsObj;
2094
  } // fetchUserSettings
2095
2096
  /**Update a user settings (delta) record to server
2097
   *
2098
   * @param {string} userId use user ID
2099
   * @param {string} section settings type defined in UserProps.SETTINGS_*
2100
   * @param {XDeepDiff} settingsChanges changes to update
2101
   * @param {{}} params any arguments or filters
2102
   * @return {XUserInfo} updated settings object of types like
2103
   * XAccountSettings, XProfileSettings, etc.
2104
   */
2105
  async updateUserSettings(userId, section, settingsChanges, params, callback) {
2106
    const _m = "uUrSt";
2107
    let settingsObj;
2108
    let settingsData = XMObject.Unwrap(settingsChanges);
2109
    let encryptedData = Util.EncryptJSON(settingsData);
2110
    let error = null;
2111
    try {
2112
      let updateSettingsUrl = this.apiGetUserSettings(userId, section, params);
2113
      settingsObj = await this.requestPOST(updateSettingsUrl, encryptedData);
2114
      settingsObj = XMObject.Wrap(settingsObj);
2115
    } catch (e) {
2116
      this.error(_m, e);
2117
      error = e;
2118
      settingsObj = null;
2119
    }
2120
2121
    // If settings is profile, then we need to update user info
2122
    if (this.isLoggedInUser(userId)) this.invalidateSettings(section);
2123
2124
    return callback ? callback(error, settingsObj) : settingsObj;
2125
  } // updateUserSettings
2126
2127
  /**
2128
   * Initiate an user stat update from server, which will
2129
   * update in session variables.
2130
   *
2131
   * @param {{}} props control what stats to update
2132
   *
2133
   * @callback
2134
   */
2135
  async updateUserStats(props, callback) {
2136
    let userId = this.getUserId();
2137
    let statsObj = await this.fetchUserStats(userId, props);
2138
    // Update session
2139
    if (statsObj) {
2140
      this.getSession().updateUserStats(statsObj);
2141
    }
2142
  }
2143
2144
  /**
2145
   * Retrieve latest in stats of (current) user and update
2146
   * the profile
2147
   *
2148
   * @param {string[]} userId
2149
   * @param {string[]} props properties to include (array or comma delimited string)
2150
   * Null to include defaults which is title only.
2151
   *
2152
   * @return {object} map with tagname is key, and requested props: {title: <text>} as default
2153
   */
2154
  async fetchUserStats(userId, props = null, callback) {
2155
    const _m = "fetchUserStats";
2156
    let statsObj;
2157
    let error = null;
2158
    try {
2159
      let url = this.getURL(this.urlHost, `/s/user/${userId}/stats/`);
2160
      if (props) {
2161
        // props = Array.isArray(props) ? props.join(",") : props;
2162
        url += "?props=" + JSON.stringify(props);
2163
      }
2164
      statsObj = await this.requestGET(url, null);
2165
    } catch (e) {
2166
      this.error(_m, e);
2167
      error = e;
2168
      statsObj = null;
2169
    }
2170
    return callback ? callback(error, statsObj) : statsObj;
2171
  } // fetchUserStats
2172
2173
  /**
2174
   * Retrieve tag stats
2175
   *
2176
   * @param {string[]} tagId
2177
   * @param {string[]} props properties to include (array or comma delimited string)
2178
   * Null to include defaults which is title only.
2179
   *
2180
   * @return {object} map with tagname is key, and requested props: {title: <text>} as default
2181
   */
2182
  async fetchTagStats(tagId, props = null, callback) {
2183
    const _m = "fetchTagStats";
2184
2185
    let statsObj;
2186
    let error = null;
2187
    try {
2188
      let apiUrl = this.apiGetCategoryStats(tagId, props);
2189
      statsObj = await this.requestGET(apiUrl, null);
2190
    } catch (e) {
2191
      this.error(_m, e);
2192
      error = e;
2193
      statsObj = null;
2194
    }
2195
    return callback ? callback(error, statsObj) : statsObj;
2196
  } // fetchTagStats
2197
2198
  // ----------------------- TOPIC CATEGORIES -----------------------------
2199
2200
  /**
2201
   * Submit update of interested topic categories for a user
2202
   *
2203
   * @param {string[]} topicIds array of topic category identifiers as
2204
   * specified in ModelConst.CATEGORY_*. If one, still submit as a string
2205
   *
2206
   *
2207
   * @return {string[]} all user interested topics
2208
   */
2209
  async submitTopics(topicIds, callback) {
2210
    const _m = "setTopics";
2211
    let error = null;
2212
    let userTopics = null;
2213
    try {
2214
      const userId = this.getUserId();
2215
      let submitTopicsUrl = this.getURL(
2216
        this.urlHost,
2217
        `/s/user/${userId}/topics`,
2218
      );
2219
      let data = {
2220
        [API.PARAM_CATEGORIES]: API.CreateOptions(topicIds),
2221
      };
2222
      userTopics = await this.requestPOST(submitTopicsUrl, data);
2223
    } catch (e) {
2224
      this.error(_m, e);
2225
      error = e;
2226
    }
2227
    return callback ? callback(error, userTopics) : userTopics;
2228
  } // submitTopics
2229
2230
  /**
2231
   * Return all available topic (IDs). The Ids should be mappable
2232
   * to translations.
2233
   *
2234
   * @return {string[]} topicIds array of topic category identifiers as
2235
   * specified in ModelConst.CATEGORY_*. If one, still submit as a string
2236
   *
2237
   * @return {string[]} all user interested topics
2238
   */
2239
  async fetchAvailableTopics(callback) {
2240
    const _m = "gTopics";
2241
    let error = null;
2242
2243
    // We have these topic Ids in constants, so no need to
2244
    // make a call to server for now.
2245
    let topicIds = TOPIC_CATEGORIES;
2246
    // try {
2247
    //   const userId = this.getUserId();
2248
    //   let getTopicsUrl = this.getURL(this.urlHost, `/s/topics`);
2249
    //   topicIds = await this.requestGET(getTopicsUrl);
2250
    // } catch (e) {
2251
    //   this.error(_m, e);
2252
    //   error = e;
2253
    // }
2254
    return callback ? callback(error, topicIds) : topicIds;
2255
  } // fetchAvailableTopics
2256
2257
  // --------------------- ALERT SERVICE ----------------------
2258
2259
  /**
2260
   * Get alert count
2261
   *
2262
   * @param {string} userId user to get count, or null for logged in user
2263
   * @param {{}} props future
2264
   * @param {*} defaultVal if no value is retrieved.
2265
   *
2266
   * @return {number} count
2267
   */
2268
  async fetchAlertCount(
2269
    userId = null,
2270
    props = null,
2271
    defaultVal = -1,
2272
    callback,
2273
  ) {
2274
    const _m = "fetchAlertCount";
2275
2276
    let loggedInUserId = userId ? userId : this.getUserId();
2277
    let alertCount;
2278
    let field = "unread";
2279
    let error = null;
2280
    try {
2281
      let getUrl = this.apiUserAlertCount(loggedInUserId, field, props);
2282
      let result = await this.requestGET(getUrl);
2283
      if (result) alertCount = result[field];
2284
    } catch (e) {
2285
      this.error(_m, e);
2286
      error = e;
2287
      alertCount = defaultVal;
2288
    }
2289
2290
    return callback ? callback(error, alertCount) : alertCount;
2291
  } // fetchAlertCount
2292
2293
  /**
2294
   * Fetch User alerts
2295
   * @param {string} userId
2296
   * @param {*} props
2297
   * @param {*} defaultVal
2298
   * @callback
2299
   * @return {XMUserAlerts} wrapper to XUserAlert instances and XVarData
2300
   */
2301
  async fetchAlerts(userId = null, props = null, defaultVal = null, callback) {
2302
    const _m = "fetchAlerts";
2303
2304
    let loggedInUserId = userId ? userId : this.getUserId();
2305
2306
    /** @type {XMUserAlert} */
2307
    let retval;
2308
    let field = "";
2309
    let max = 20;
2310
    let error = null;
2311
    try {
2312
      let getUrl = this.apiUserAlerts(loggedInUserId, field, max, props);
2313
      retval = await this.requestGET(getUrl);
2314
    } catch (e) {
2315
      this.error(_m, e);
2316
      error = e;
2317
      retval = defaultVal;
2318
    }
2319
2320
    return callback ? callback(error, retval) : retval;
2321
  } // fetchAlerts
2322
2323
  /**
2324
   * Get alert count
2325
   *
2326
   * @param {string} userId user to get count, or null for logged in user
2327
   * @param {{}} props future
2328
   * @param {*} defaultVal if no value is retrieved.
2329
   *
2330
   * @return {number} count
2331
   */
2332
  async markAlertsRead(alertIds, props = null, callback) {
2333
    const _m = "markAlertsRead";
2334
2335
    let loggedInUserId = this.getUserId();
2336
    let returnVal;
2337
    if (props == null) props = {};
2338
2339
    // for now...
2340
    props[API.ALERT_IDS] = alertIds;
2341
    props[API.READ_TS] = Date.now();
2342
    props[API.READ_MEDIUM] = MessageProps.MEDIUM_APP;
2343
    let error = null;
2344
    try {
2345
      let getUrl = this.apiUserAlertsStatus(loggedInUserId, null, null);
2346
      returnVal = await this.requestPOST(getUrl, props);
2347
    } catch (e) {
2348
      this.error(_m, e);
2349
      error = e;
2350
      returnVal = null;
2351
    }
2352
2353
    return callback ? callback(error, returnVal) : returnVal;
2354
  } // fetchAlertCount
2355
2356
  // --------------------- FOLLOWS SERVICE ----------------------
2357
2358
  /**
2359
   * Add a "userId follows :anotherUserId"
2360
   *
2361
   * @param {string} targetUserId user to follow
2362
   *
2363
   * @return {XUserInfo} upated user info with new follow
2364
   */
2365
  async userFollows(targetUserId, props = null, callback) {
2366
    const _m = "userFollows";
2367
2368
    let loggedInUserId = this.getUserId();
2369
    let followStatus;
2370
    let error = null;
2371
    try {
2372
      let getUrl = this.apiAddFollows(loggedInUserId, targetUserId, props);
2373
      followStatus = await this.requestPOST(getUrl);
2374
    } catch (e) {
2375
      // response object in here is not an xResObj
2376
      this.error(_m, e);
2377
      error = e;
2378
      followStatus = null;
2379
    }
2380
    return callback ? callback(error, followStatus) : followStatus;
2381
  } // userFollows
2382
2383
  /**
2384
   * Retrieve answer to whether the logged in user is following
2385
   * a given user.
2386
   *
2387
   * @param {string[]} userId
2388
   * @param {string[]} props properties to include (array or comma delimited string)
2389
   * Null to include defaults which is title only.
2390
   *
2391
   * @return {object} map with tagname is key, and requested props: {title: <text>} as default
2392
   */
2393
  async userFollowStatus(targetUserId, props = null, callback) {
2394
    const _m = "userFollowStatus";
2395
2396
    let statusValue = null;
2397
    let error = null;
2398
    let userId = this.getUserId();
2399
    if (!targetUserId || !userId) {
2400
      return false;
2401
    }
2402
    try {
2403
      let getUserStatsUrl = this.apiUserFollowStatus(
2404
        userId,
2405
        targetUserId,
2406
        props,
2407
      );
2408
      statusValue = await this.requestGET(getUserStatsUrl, null);
2409
    } catch (e) {
2410
      this.error(_m, e);
2411
      error = e;
2412
      statusValue = SocialProps.STATUS_UNKNOWN;
2413
    }
2414
    return callback ? callback(error, statusValue) : statusValue;
2415
  } // userFollowStatus
2416
2417
  /**
2418
   * Retrieve answer to whether the logged in user is following
2419
   * a given user (or pending or blocked).
2420
   *
2421
   * @param {string[]} followerId userId for the follower of logged in user
2422
   * @param {string[]} props properties to include (array or comma delimited string)
2423
   * Null to include defaults which is title only.
2424
   *
2425
   * @return {string} status PROP_ACCEPTED, PROP_PENDING, PROP_BLOCKED or null
2426
   */
2427
  async userFollowerStatus(followerId, props = null, callback) {
2428
    const _m = "userFollowerStatus";
2429
2430
    let statusValue = null;
2431
    let error = null;
2432
    let userId = this.getUserId();
2433
    if (!followerId || !userId) {
2434
      return null;
2435
    }
2436
    try {
2437
      let getUserStatsUrl = this.apiUserFollowerStatus(
2438
        userId,
2439
        followerId,
2440
        props,
2441
      );
2442
      statusValue = await this.requestGET(getUserStatsUrl, null);
2443
    } catch (e) {
2444
      this.error(_m, e);
2445
      error = e;
2446
      statusValue = null;
2447
    }
2448
    return callback ? callback(error, statusValue) : statusValue;
2449
  } // userFollowStatus
2450
2451
  /**
2452
   * Add a "userId unfollows :anotherUserId"
2453
   *
2454
   * @param {string} targetUserId user to unfollow
2455
   *
2456
   * @return {XUserInfo} updated user info with follow removed
2457
   */
2458
  async userUnfollows(targetUserId, props = null, callback) {
2459
    const _m = "userUnfollows";
2460
2461
    let loggedInUserId = this.getUserId();
2462
    let followStatus;
2463
    let error = null;
2464
    try {
2465
      let getUrl = this.apiRemoveFollows(loggedInUserId, targetUserId, props);
2466
      followStatus = await this.requestPOST(getUrl);
2467
    } catch (e) {
2468
      this.error(_m, e);
2469
      error = e;
2470
      followStatus = null;
2471
    }
2472
2473
    return callback ? callback(error, followStatus) : followStatus;
2474
  } // userUnfollows
2475
2476
  /**
2477
   * Request blocking of a user from logged in user
2478
   *
2479
   * @param {string} targetUserId user to block
2480
   *
2481
   * @return {XUserInfo} updted user info with follow removed
2482
   */
2483
  async userBlocksFollower(targetUserId, props = null, callback) {
2484
    const _m = "userBlocksFollower";
2485
2486
    let loggedInUserId = this.getUserId();
2487
    let followStatus;
2488
    let error = null;
2489
    try {
2490
      let getUrl = this.apiBlockFollower(loggedInUserId, targetUserId, props);
2491
      followStatus = await this.requestPOST(getUrl);
2492
    } catch (e) {
2493
      this.error(_m, e);
2494
      error = e;
2495
      followStatus = null;
2496
    }
2497
2498
    return callback ? callback(error, followStatus) : followStatus;
2499
  } // userBlocksUser
2500
2501
  /**
2502
   * Request unblocking of a user from logged in user
2503
   *
2504
   * @param {string} targetUserId user to block
2505
   *
2506
   * @return {XUserInfo} updted user info with follow removed
2507
   */
2508
  async userUnblocksFollower(targetUserId, props = null, callback) {
2509
    const _m = "userUnblocksFollower";
2510
2511
    let loggedInUserId = this.getUserId();
2512
    let followStatus;
2513
    let error = null;
2514
    try {
2515
      let getUrl = this.apiUnblockFollower(loggedInUserId, targetUserId, props);
2516
      followStatus = await this.requestPOST(getUrl);
2517
    } catch (e) {
2518
      this.error(_m, e);
2519
      error = e;
2520
      followStatus = null;
2521
    }
2522
2523
    return callback ? callback(error, followStatus) : followStatus;
2524
  } // userBlocksUser
2525
2526
  /**
2527
   * Request mute a user for logged in user
2528
   *
2529
   * @param {string} targetUserId user to mute
2530
   *
2531
   * @return {XUserInfo} updted user info with follow removed
2532
   */
2533
  async userMutesFollower(targetUserId, props = null, callback) {
2534
    const _m = "userMutesFollower";
2535
2536
    let loggedInUserId = this.getUserId();
2537
    let followStatus;
2538
    let error = null;
2539
    try {
2540
      let getUrl = this.apiMuteFollower(loggedInUserId, targetUserId, props);
2541
      followStatus = await this.requestPOST(getUrl);
2542
    } catch (e) {
2543
      this.error(_m, e);
2544
      error = e;
2545
      followStatus = null;
2546
    }
2547
2548
    return callback ? callback(error, followStatus) : followStatus;
2549
  } // userMutesUser
2550
2551
  /**
2552
   * Request unmute a user for logged in user
2553
   *
2554
   * @param {string} targetUserId user to mute
2555
   *
2556
   * @return {XUserInfo} updted user info with follow removed
2557
   */
2558
  async userUnmutesFollower(targetUserId, props = null, callback) {
2559
    const _m = "userUnmutesFollower";
2560
2561
    let loggedInUserId = this.getUserId();
2562
    let followStatus;
2563
    let error = null;
2564
    try {
2565
      let getUrl = this.apiUnmuteFollower(loggedInUserId, targetUserId, props);
2566
      followStatus = await this.requestPOST(getUrl);
2567
    } catch (e) {
2568
      this.error(_m, e);
2569
      error = e;
2570
      followStatus = null;
2571
    }
2572
2573
    return callback ? callback(error, followStatus) : followStatus;
2574
  } // userUnmutesUser
2575
2576
  /**
2577
   * Fetch a user follows instance, which contains user IDs
2578
   * that this user is following.
2579
   *
2580
   * @param {string} userId poll configuration ID
2581
   * @param {{}} params API.INCL_REFOBJS, API.INCL_STATS
2582
   * @callback
2583
   *
2584
   * @see ~fetchUserFollowsInclRefs
2585
   */
2586
  async fetchUserFollows(userId, params, callback) {
2587
    let _m = `fetchUserFollows(${userId})`;
2588
    let loggedInUserId = userId ? userId : this.getUserId();
2589
    let followObj;
2590
    let error = null;
2591
    try {
2592
      let getUrl = this.apiGetFollows(loggedInUserId, params);
2593
      followObj = await this.requestGET(getUrl);
2594
      followObj = XMFollows.Wrap(followObj);
2595
    } catch (e) {
2596
      this.error(_m, e);
2597
      error = e;
2598
      followObj = null;
2599
    }
2600
2601
    return callback ? callback(error, followObj) : followObj;
2602
  } // fetchUserFollows
2603
2604
  /**
2605
   *
2606
   * @param {string} userId
2607
   * @param {{}} params
2608
   * @param {boolean} cache true to ask ObjectManager to track it
2609
   * @callback
2610
   */
2611
  fetchUserFollowsInclRefs(userId, params, cache = false, callback) {
2612
    if (params == null) params = {};
2613
2614
    // The content is not evaluated at the server; only that
2615
    // this parameter is passed (as of 3/2019)
2616
    params[API.INCL_REFOBJS] = {
2617
      [XObject.PROP_TAGS]: {
2618
        [XObject.PROP_TITLE]: "en_us",
2619
      },
2620
    };
2621
2622
    return this.fetchUserFollows(userId, params, cache, callback);
2623
  }
2624
2625
  /**
2626
   * Fetch a user follows instance, which contains user IDs
2627
   * that this user is following.
2628
   *
2629
   * @param {string} userId poll configuration ID
2630
   * @param {{}} params API.INCL_REFOBJS, API.INCL_STATS
2631
   * @callback
2632
   */
2633
  async fetchUserFollowers(userId, params, callback) {
2634
    let _m = `fUF(${userId})`;
2635
    let loggedInUserId = userId ? userId : this.getUserId();
2636
    let followerObj;
2637
    let error = null;
2638
    try {
2639
      let getUrl = this.apiGetFollowers(loggedInUserId, params);
2640
      followerObj = await this.requestGET(getUrl);
2641
      followerObj = XMFollowers.Wrap(followerObj);
2642
    } catch (e) {
2643
      this.error(_m, "server returned error:", e);
2644
      error = e;
2645
      followerObj = null;
2646
    }
2647
2648
    return callback ? callback(error, followerObj) : followerObj;
2649
  } // fetchUserFollowers
2650
2651
  /**
2652
   *
2653
   * @param {string} userId
2654
   * @param {{}} params
2655
   * @param {boolean} cache true to ask ObjectManager to track it
2656
   * @callback
2657
   */
2658
  fetchUserFollowersInclRefs(userId, params, cache = false, callback) {
2659
    if (params == null) params = {};
2660
2661
    // The content is not evaluated at the server; only that
2662
    // this parameter is passed (as of 3/2019)
2663
    params[API.INCL_REFOBJS] = {
2664
      [XObject.PROP_TAGS]: {
2665
        [XObject.PROP_TITLE]: "en_us",
2666
      },
2667
    };
2668
2669
    return this.fetchUserFollowers(userId, params, cache, callback);
2670
  }
2671
2672
  // ------------------- WATCHES/WATCHED POSTS -----------------
2673
2674
  /**
2675
   * Fetch all tags that a user is watching
2676
   *
2677
   * @param {string} userId
2678
   * @param {{}} params API.INCL_REFOBJS, API.INCL_STATS
2679
   * @callback
2680
   * @return {XMWatchesPost}
2681
   */
2682
  async fetchWatchesPost(userId, params, callback) {
2683
    let _m = `fetchWatchesPost(${userId})`;
2684
    let loggedInUserId = userId ? userId : this.getUserId();
2685
    let xWatches;
2686
    let error = null;
2687
    try {
2688
      let getUrl = this.apiGetWatchesObject(
2689
        ModelType.POST,
2690
        loggedInUserId,
2691
        params,
2692
      );
2693
      xWatches = await this.requestGET(getUrl);
2694
      xWatches = XMWatchesPost.Wrap(xWatches);
2695
    } catch (e) {
2696
      this.error(_m, e);
2697
      error = e;
2698
      xWatches = null;
2699
    }
2700
    return callback ? callback(error, xWatches) : xWatches;
2701
  }
2702
2703
  /**
2704
   * Fetch a user follows instance, which contains user IDs
2705
   * that this user is following.
2706
   *
2707
   * @param {string} postId poll configuration ID
2708
   * @param {{}} params API.INCL_REFOBJS, API.INCL_STATS
2709
   * @callback
2710
   */
2711
  async fetchPostWatchers(postId, params, callback) {
2712
    let _m = `fetchPostWatchers(${postId})`;
2713
    let loggedInUserId = postId ? postId : this.getUserId();
2714
    let xWatchers;
2715
    let error = null;
2716
    try {
2717
      let getUrl = this.apiGetObjectWatchers(
2718
        ModelType.POST,
2719
        loggedInUserId,
2720
        params,
2721
      );
2722
      xWatchers = await this.requestGET(getUrl);
2723
2724
      xWatchers = XMWatchedPost.Wrap(xWatchers);
2725
    } catch (e) {
2726
      this.error(_m, e);
2727
      error = e;
2728
      xWatchers = null;
2729
    }
2730
    return callback ? callback(error, xWatchers) : xWatchers;
2731
  }
2732
2733
  // --------------------- LIKES POST SERVICE ----------------------
2734
2735
  /**
2736
   * Add a "userId likes :postId"
2737
   *
2738
   * @param {string} postId user to follow
2739
   *
2740
   * @return {string} updated like status "y" or "n"
2741
   */
2742
  async userLikesPost(postId, props = null, callback) {
2743
    const _m = "userLikesPost";
2744
2745
    let loggedInUserId = this.getUserId();
2746
    let likeStatus;
2747
    let error = null;
2748
    try {
2749
      let getUrl = this.apiAddLikeObject(
2750
        loggedInUserId,
2751
        ModelType.POST,
2752
        postId,
2753
        props,
2754
      );
2755
      likeStatus = await this.requestPOST(getUrl);
2756
    } catch (e) {
2757
      this.error(_m, e);
2758
      error = e;
2759
      likeStatus = null;
2760
    }
2761
2762
    return callback ? callback(error, likeStatus) : likeStatus;
2763
  } // userLikesPost
2764
2765
  /**
2766
   * Request to unlike a post
2767
   *
2768
   * @param {string} postId user to unfollow
2769
   *
2770
   * @return {XUserInfo} upated user info with follow removed
2771
   */
2772
  async userUnlikesPost(postId, props = null, callback) {
2773
    const _m = "userUnlikesPost";
2774
2775
    let loggedInUserId = this.getUserId();
2776
    let likeStatus;
2777
    let error = null;
2778
    try {
2779
      let getUrl = this.apiRemoveLikeObject(
2780
        loggedInUserId,
2781
        ModelType.POST,
2782
        postId,
2783
        props,
2784
      );
2785
      likeStatus = await this.requestPOST(getUrl);
2786
    } catch (e) {
2787
      this.error(_m, e);
2788
      error = e;
2789
      likeStatus = null;
2790
    }
2791
2792
    return callback ? callback(error, likeStatus) : likeStatus;
2793
  } // userUnlikesPost
2794
2795
  /**
2796
   * Retrieve answer to whether the logged in user has liked
2797
   * the given post.
2798
   *
2799
   * @param {string[]} postId
2800
   * @param {string[]} props properties to include (array or comma delimited string)
2801
   * Null to include defaults which is title only.
2802
   *
2803
   * @return "y" or "no"
2804
   */
2805
  async userLikePostStatus(postId, props = null, callback) {
2806
    const _m = "fULPS";
2807
2808
    let statusValue = null;
2809
    let error = null;
2810
    let userId = this.getUserId();
2811
    if (!postId || !userId) {
2812
      return "no";
2813
    }
2814
    try {
2815
      let url = this.apiUserLikeObjectStatus(
2816
        userId,
2817
        ModelType.POST,
2818
        postId,
2819
        props,
2820
      );
2821
      statusValue = await this.requestGET(url, null);
2822
    } catch (e) {
2823
      this.error(_m, e);
2824
      error = e;
2825
      statusValue = null;
2826
    }
2827
    return callback ? callback(error, statusValue) : statusValue;
2828
  } // userLikePostStatus
2829
2830
  // --------------------- LIKES COMMENT SERVICE ----------------------
2831
2832
  /**
2833
   * Add a "userId likes :commentId"
2834
   *
2835
   * @param {string} commentId user to follow
2836
   *
2837
   * @return {string} updated like status "y" or "n"
2838
   */
2839
  async userLikesComment(commentId, props = null, callback) {
2840
    const _m = "userLikesPost";
2841
2842
    let loggedInUserId = this.getUserId();
2843
    let likeStatus;
2844
    let error = null;
2845
    try {
2846
      let getUrl = this.apiAddLikeObject(
2847
        loggedInUserId,
2848
        "comment",
2849
        commentId,
2850
        props,
2851
      );
2852
      likeStatus = await this.requestPOST(getUrl);
2853
    } catch (e) {
2854
      this.error(_m, e);
2855
      error = e;
2856
      likeStatus = null;
2857
    }
2858
2859
    return callback ? callback(error, likeStatus) : likeStatus;
2860
  } // userLikesPost
2861
2862
  /**
2863
   * Request to unlike a post
2864
   *
2865
   * @param {string} commentId user to unfollow
2866
   *
2867
   * @return {XUserInfo} upated user info with follow removed
2868
   */
2869
  async userUnlikesComment(commentId, props = null, callback) {
2870
    const _m = "fUUCS";
2871
2872
    let loggedInUserId = this.getUserId();
2873
    let likeStatus;
2874
    let error = null;
2875
    try {
2876
      let getUrl = this.apiRemoveLikeObject(
2877
        loggedInUserId,
2878
        "comment",
2879
        commentId,
2880
        props,
2881
      );
2882
      likeStatus = await this.requestPOST(getUrl);
2883
    } catch (e) {
2884
      this.error(_m, e);
2885
      error = e;
2886
      likeStatus = null;
2887
    }
2888
2889
    return callback ? callback(error, likeStatus) : likeStatus;
2890
  } // userUnlikesComment
2891
2892
  /**
2893
   * Retrieve answer to whether the logged in user has liked
2894
   * the given post.
2895
   *
2896
   * @param {string[]} commentId
2897
   * @param {string[]} props properties to include (array or comma delimited string)
2898
   * Null to include defaults which is title only.
2899
   *
2900
   * @return "y" or "no"
2901
   */
2902
  async userLikeCommentStatus(commentId, props = null, callback) {
2903
    const _m = "fULCS";
2904
2905
    let statusValue = null;
2906
    let error = null;
2907
    let userId = this.getUserId();
2908
    if (!commentId || !userId) {
2909
      return "no";
2910
    }
2911
    try {
2912
      let url = this.apiUserLikeObjectStatus(
2913
        userId,
2914
        "comment",
2915
        commentId,
2916
        props,
2917
      );
2918
      statusValue = await this.requestGET(url, null);
2919
    } catch (e) {
2920
      this.error(_m, e);
2921
      error = e;
2922
      statusValue = null;
2923
    }
2924
    return callback ? callback(error, statusValue) : statusValue;
2925
  } // userLikeCommentStatus
2926
2927
  // --------------------- WATCH OBJECT SERVICE ----------------------
2928
2929
  /**
2930
   * Add a "userId watches :objectId"
2931
   *
2932
   * @param {string} objectId object to watch
2933
   *
2934
   * @return {string} updated like status "y" or "n"
2935
   */
2936
  async userWatchesObject(type, objectId, props = null, callback) {
2937
    const _m = "userWatchesObject";
2938
2939
    let loggedInUserId = this.getUserId();
2940
    let watchStatus;
2941
    let error = null;
2942
    try {
2943
      let url = this.apiAddWatchObject(type, loggedInUserId, objectId, props);
2944
      watchStatus = await this.requestPOST(url);
2945
    } catch (e) {
2946
      this.error(_m, e);
2947
      error = e;
2948
      watchStatus = null;
2949
    }
2950
2951
    return callback ? callback(error, watchStatus) : watchStatus;
2952
  }
2953
2954
  /**
2955
   * Request to unwatch an object
2956
   *
2957
   * @param {string} objectId object to unwatch
2958
   *
2959
   * @return {XUserInfo} upated user info with follow removed
2960
   */
2961
  async userUnwatchesObject(type, objectId, props = null, callback) {
2962
    const _m = "userUnwatchesObject";
2963
2964
    let loggedInUserId = this.getUserId();
2965
    let watchStatus;
2966
    let error = null;
2967
    try {
2968
      let url = this.apiRemoveWatchObject(
2969
        type,
2970
        loggedInUserId,
2971
        objectId,
2972
        props,
2973
      );
2974
      watchStatus = await this.requestPOST(url);
2975
    } catch (e) {
2976
      this.error(_m, "server returned error:", e);
2977
      error = e;
2978
      watchStatus = null;
2979
    }
2980
2981
    return callback ? callback(error, watchStatus) : watchStatus;
2982
  }
2983
2984
  /**
2985
   * Get watch status on the specified object type/id
2986
   *
2987
   * @param {string[]} objectId
2988
   * @param {string[]} props properties to include (array or comma delimited string)
2989
   * Null to include defaults which is title only.
2990
   *
2991
   * @return "y" or "no"
2992
   */
2993
  async userWatchObjectStatus(type, objectId, props = null, callback) {
2994
    const _m = "userWatchObjectStatus";
2995
2996
    let statusValue = null;
2997
    let error = null;
2998
    let userId = this.getUserId();
2999
    if (!objectId || !userId) {
3000
      return "no";
3001
    }
3002
    try {
3003
      let url = this.apiUserWatchObjectStatus(type, userId, objectId, props);
3004
      statusValue = await this.requestGET(url, null);
3005
    } catch (e) {
3006
      this.error(_m, "server returned error:", e);
3007
      error = e;
3008
      statusValue = null;
3009
    }
3010
    return callback ? callback(error, statusValue) : statusValue;
3011
  }
3012
3013
  // --------------------- (PASSWORD) CHANGE REQUEST -------------------------
3014
3015
  /**
3016
   * Fetch existing XMUserRequest instance.
3017
   *
3018
   * @param {string} requestId
3019
   * @callback
3020
   */
3021
  fetchUserRequest(requestId, cache = false, callback) {
3022
    // let _m = `fetchUserRequest(${templateId})`;
3023
3024
    let processResults = (err, rlObj) => {
3025
      if (err) {
3026
        console.log(err);
3027
        return callback ? callback(err, null) : null;
3028
      }
3029
3030
      if (callback) callback(null, rlObj);
3031
      else return rlObj;
3032
    };
3033
3034
    this.getResource(
3035
      requestId,
3036
      ModelType.USER_REQUEST,
3037
      null,
3038
      cache,
3039
      processResults,
3040
    );
3041
  } // fetchUserRequest
3042
3043
  /**
3044
   * Send password change request to verified contact path (e.g., email)
3045
   *
3046
   * @param {XUserInfo} userInfo
3047
   * @param {XAuthInfo} authInfo
3048
   * @param {string} requestType either REQUEST_EMAIL or REQUEST_SMS as wish
3049
   *
3050
   * @return {XMUserConfirm} confirmation object
3051
   */
3052
  async initiatePasswordChange(userInfo, authInfo, requestType, callback) {
3053
    const _m = "initPwdChange";
3054
3055
    let content = {};
3056
    if (userInfo) content["userinfo"] = XObject.Unwrap(userInfo);
3057
    if (authInfo) content["authinfo"] = XObject.Unwrap(authInfo);
3058
3059
    let encrypted = Util.EncryptJSON(content);
3060
3061
    let confirmObj;
3062
    let error = null;
3063
3064
    // type may become an option to allow user to choose how to send request (by email or sms)
3065
    // let type = (confirmType === UserProps.CONFIRM_SMS) ? UserProps.SMS : UserProps.EMAIL;
3066
    try {
3067
      let apiUrl = this.getURL(this.urlHost, `/s/request/pwdchg`);
3068
      confirmObj = await this.requestPOST(apiUrl, encrypted);
3069
    } catch (e) {
3070
      this.error(_m, "server error:", e);
3071
      error = e;
3072
      confirmObj = null;
3073
    }
3074
    return callback ? callback(error, confirmObj) : confirmObj;
3075
  }
3076
3077
  /**
3078
   * Resend new confirmation based on the given (expired) confirmation.
3079
   *
3080
   * @param {string} requestId
3081
   * @param {string} confirmType either CONFIRM_EMAIL or CONFIRM_SMS
3082
   *
3083
   * @return {XMUserConfirm} new confirmation object with different ID
3084
   */
3085
  async resendPasswordChangeRequest(requestId, callback) {
3086
    const _m = "resentPwdChangeReq";
3087
3088
    let xRequest;
3089
    let error = null;
3090
    try {
3091
      let apiUrl = this.getURL(this.urlHost, `/s/request/${requestId}/resend`);
3092
      xRequest = await this.requestGET(apiUrl, null);
3093
    } catch (e) {
3094
      this.error(_m, "server error:", e);
3095
      error = e;
3096
      xRequest = null;
3097
    }
3098
    return callback ? callback(error, xRequest) : xRequest;
3099
  }
3100
3101
  /**
3102
   * Submit a password change request to server.
3103
   * This is a pass-thru from
3104
   * the web app URL: /chgpwd/:requestId when responding to
3105
   * an email request to change password
3106
   *
3107
   * @param {string} curPassword current pasword (clear)
3108
   * @param {string} newPassword new password (clear)
3109
   *
3110
   * @return {string} status PROP_ACCEPTED, PROP_PENDING, PROP_BLOCKED or null
3111
   *
3112
   * @see PasswordChangeRoute.submitRequest (route-changepwd.js)
3113
   */
3114
  async submitPasswordChange(curPassword, newPassword, callback) {
3115
    const _m = "submitPwdChange";
3116
3117
    let result;
3118
    let error = null;
3119
    try {
3120
      let userId = this.getUserId();
3121
      let data = {
3122
        [API.CURRENT_PASSWORD]: curPassword,
3123
        [API.NEW_PASSWORD]: newPassword,
3124
      };
3125
3126
      let query = this.getURL(
3127
        this.urlHost,
3128
        `/u/user/${userId}/pwdchg?clear=true`,
3129
      );
3130
      // if (sourceId) query = `${query}/src/${sourceId}`;
3131
3132
      result = await this.requestPOST(query, data);
3133
    } catch (e) {
3134
      this.error(_m, "server:", e);
3135
      error = e;
3136
      result = null;
3137
    }
3138
    return callback ? callback(error, result) : result;
3139
  } // submitPasswordChange
3140
3141
  /**
3142
   * Change password
3143
   *
3144
   * @param {string} curPassword current pasword (clear)
3145
   * @param {string} newPassword new password (clear)
3146
   *
3147
   * @return {boolean} true to indicate success, or E_AUTH error
3148
   *
3149
   */
3150
  async changePassword(curPassword, newPassword, callback) {
3151
    const _m = "chgPwd";
3152
3153
    let result;
3154
    let error = null;
3155
    let username = this.getUsername();
3156
    try {
3157
      let query = this.getURL(this.urlHost, `/u/${username}/pwdchg`);
3158
3159
      let curpwd = Util.EncryptPwd(curPassword);
3160
      let newpwd = Util.EncryptPwd(newPassword);
3161
3162
      let data = {
3163
        [API.CURRENT_PASSWORD]: curpwd,
3164
        [API.NEW_PASSWORD]: newpwd,
3165
      };
3166
3167
      result = await this.requestPOST(query, data);
3168
    } catch (e) {
3169
      this.error(_m, "server:", e);
3170
      error = e;
3171
      result = null;
3172
    }
3173
    return callback ? callback(error, result) : result;
3174
  } // changePassword
3175
3176
  // --------------------- CONFIRMATION / VERIFICATION STATUS -------------------------
3177
3178
  /**
3179
   * Send verification email/text to user to confirm account (contact method)
3180
   *
3181
   * @param {string} userId
3182
   * @param {string} confirmType either CONFIRM_EMAIL or CONFIRM_SMS
3183
   *
3184
   * @return {XMUserConfirm} confirmation object
3185
   */
3186
  async verifyContact(userId, confirmType, callback) {
3187
    const _m = "verifyContact";
3188
3189
    let confirmObj;
3190
    let error = null;
3191
    let type =
3192
      confirmType === UserProps.CONFIRM_SMS ? UserProps.SMS : UserProps.EMAIL;
3193
    try {
3194
      let apiUrl = this.getURL(
3195
        this.urlHost,
3196
        `/s/user/${userId}/verify/${type}`,
3197
      );
3198
      confirmObj = await this.requestGET(apiUrl, null);
3199
    } catch (e) {
3200
      this.error(_m, "server error:", e);
3201
      error = e;
3202
      confirmObj = null;
3203
    }
3204
    return callback ? callback(error, confirmObj) : confirmObj;
3205
  }
3206
3207
  /**
3208
   * Resend new confirmation based on the given (expired) confirmation.
3209
   *
3210
   * @param {string} userId
3211
   * @param {string} confirmType either CONFIRM_EMAIL or CONFIRM_SMS
3212
   *
3213
   * @return {XMUserConfirm} new confirmation object with different ID
3214
   */
3215
  async resendConfirmation(confirmId, callback) {
3216
    const _m = "resendConfirm";
3217
3218
    let xConfirm;
3219
    let error = null;
3220
    try {
3221
      let apiUrl = this.getURL(this.urlHost, `/s/confirm/${confirmId}/resend`);
3222
      xConfirm = await this.requestGET(apiUrl, null);
3223
    } catch (e) {
3224
      this.error(_m, "server error:", e);
3225
      error = e;
3226
      xConfirm = null;
3227
    }
3228
    return callback ? callback(error, xConfirm) : xConfirm;
3229
  }
3230
3231
  /**
3232
   * Send confirmation by an Id to server and get results back.
3233
   *
3234
   * @param {string} confirmId confirmation identifier
3235
   * @param {string} sourceId identifier of source. If null, we'll look up IP
3236
   *
3237
   * @return {string} status PROP_ACCEPTED, PROP_PENDING, PROP_BLOCKED or null
3238
   */
3239
  async confirmById(confirmId, sourceId, callback) {
3240
    const _m = "confirmById";
3241
3242
    let confirmObj;
3243
    let error = null;
3244
    try {
3245
      let apiUrl = this.apiConfirmById(confirmId, sourceId);
3246
      confirmObj = await this.requestGET(apiUrl, null);
3247
      if (confirmObj && confirmObj.isConfirmed(false))
3248
        this.refreshUserInfo(null);
3249
    } catch (e) {
3250
      this.error(_m, "server:", e);
3251
      error = e;
3252
      confirmObj = null;
3253
    }
3254
    return callback ? callback(error, confirmObj) : confirmObj;
3255
  } // confirmById
3256
3257
  /**
3258
   * Submit a User Feedback
3259
   *
3260
   * @param {string} requestId confirmation identifier
3261
   * @param {string} sourceId identifier of source. If null, we'll look up IP
3262
   *
3263
   * @return {string} status PROP_ACCEPTED, PROP_PENDING, PROP_BLOCKED or null
3264
   */
3265
  async submitFeedback(xUserFeedback, callback) {
3266
    const _m = "submitFeedback";
3267
3268
    let result;
3269
    let error = null;
3270
    try {
3271
      let query = this.getURL(this.urlHost, `/s/submit/feedback`);
3272
3273
      result = await this.requestPOST(query, xUserFeedback);
3274
    } catch (e) {
3275
      this.error(_m, "server:", e);
3276
      error = e;
3277
      result = null;
3278
    }
3279
    return callback ? callback(error, result) : result;
3280
  } // submitPasswordChange
3281
3282
  // -------------------------------
3283
3284
  /**
3285
   * Check if given instance of XMObject can
3286
   * be updated by the currently logged in user.
3287
   *
3288
   * @param {XMObject} xmobject
3289
   */
3290
  validateWrite(xmobject) {
3291
    let loggedInUserId = this.getUserId();
3292
    let ownerId = xmobject ? xmobject.getOwnerId() : null;
3293
    if (ownerId == null || loggedInUserId == null) return false;
3294
3295
    if (ownerId !== loggedInUserId) return false;
3296
3297
    return true;
3298
  } // validateWrite
3299
3300
  // ------------------- HASHTAG / USERTAG RELATED SERVICES ---------------------
3301
3302
  /**
3303
   *
3304
   * @param {number} max
3305
   * @return {XResultList}
3306
   * @callback
3307
   */
3308
  async fetchSuggestedHashtags(offset = null, max = null, callback) {
3309
    if (!max) max = 20;
3310
3311
    let url = this.getURL(this.urlHost, "/s/hashtag/suggest");
3312
    url += "?max=" + max;
3313
    if (offset) url += "&offset=" + offset;
3314
    return this.requestGET(url, null, callback);
3315
  }
3316
3317
  /**
3318
   *
3319
   * @param {number} max maximum number of userIds to retrieve
3320
   * @param {number} offset starting position, if different from zero
3321
   * @return {XResultList}
3322
   * @callback
3323
   */
3324
  async fetchSuggestedUsertags(offset = null, max = null, callback) {
3325
    if (!max) max = 20;
3326
    let url = this.getURL(this.urlHost, "/s/usertag/suggest");
3327
    url += "?max=" + max;
3328
    if (offset) url += "&offset=" + offset;
3329
    url += "&incl=userinfo|followings";
3330
3331
    let resultList = await this.requestGET(url, null);
3332
3333
    if (callback) return callback(resultList);
3334
    else return resultList;
3335
  }
3336
3337
  // --------------- POST-RELATED SEARCHES / FETCHES ----------------------------
3338
3339
  /**
3340
   * Fetch matching keywords delmited by spaces. hashtags and mentions will give
3341
   * priority in results, follow by generaal results (eventually).
3342
   *
3343
   * @param {string} keywords delimited by space
3344
   * @param {boolean} inclSelf include own posts?
3345
   * @param {array} field names to include in the result (catObj for whole object)
3346
   * @param {number} max
3347
   * @param {number} min
3348
   * @return {XResultMap}
3349
   */
3350
  async fetchSearchChoices(keywords, max = null, min = null, callback) {
3351
    // const postProcess = function (err, resultMap) {
3352
    //   if (err) return callback(err, null);
3353
    //   // no filter processing
3354
    //   callback(null, resultMap);
3355
    // }; // postProcess
3356
3357
    let urlKwdTags = this.getURL(this.urlHost, "/u/posts/srch/choices");
3358
    let result = await this.searchPostPhrase(
3359
      urlKwdTags,
3360
      keywords,
3361
      false,
3362
      false,
3363
      max,
3364
      min,
3365
      //postProcess,
3366
    );
3367
3368
    return result;
3369
  } // fetchKwd2Tags
3370
3371
  async fetchSearchResultChoices(
3372
    type,
3373
    phrase,
3374
    offset = 10,
3375
    max = null,
3376
    callback,
3377
  ) {
3378
    let url = this.getURL(this.urlHost, "/u/posts/srch/choices");
3379
3380
    let apiUrl = url + `?phrase=${encodeURIComponent(type)}${phrase}`;
3381
    if (offset) apiUrl += `&offset=${offset}`;
3382
    if (max) apiUrl += `&max=${max}`;
3383
    if (type !== "#") apiUrl += "&incl=userinfo|followings";
3384
3385
    let resultList = await this.requestGET(apiUrl, null);
3386
3387
    if (callback) return callback(resultList);
3388
    else return resultList;
3389
  }
3390
3391
  async searchUserResult(phrase, offset, max, callback) {
3392
    let resultList;
3393
    try {
3394
      let url = this.getURL(this.urlHost, `/u/users/srch/phrase`);
3395
      resultList = await this.requestPOST(url, {
3396
        incl: "userinfo|followings|followers",
3397
        q: phrase,
3398
        offset,
3399
        max,
3400
      });
3401
    } catch (e) {
3402
      console.error(e);
3403
    }
3404
3405
    if (callback) {
3406
      return callback(resultList);
3407
    } else {
3408
      return resultList;
3409
    }
3410
  }
3411
3412
  /**
3413
   * Fetch matching hashtags and suggestions.
3414
   *
3415
   * @param {string} keywords delimited by space
3416
   * @param {boolean} inclSelf include own posts?
3417
   * @param {array} field names to include in the result (catObj for whole object)
3418
   * @param {number} max
3419
   * @param {number} min
3420
   * @return {XResultMap}
3421
   */
3422
  async fetchHashtagChoices(keywords, max = null, min = null, callback) {
3423
    const postProcess = function (err, resultMap) {
3424
      if (err) return callback(err, null);
3425
      // no filter processing
3426
      callback(null, resultMap);
3427
    }; // postProcess
3428
3429
    let urlKwdTags = this.getURL(this.urlHost, "/u/posts/srch/choices");
3430
    let result = this.searchPostPhrase(
3431
      urlKwdTags,
3432
      "#" + keywords,
3433
      false,
3434
      false,
3435
      max,
3436
      min,
3437
      postProcess,
3438
    );
3439
3440
    return result;
3441
  } // fetchHashtags
3442
3443
  /**
3444
   * Fetch matching mentions and suggestions.
3445
   *
3446
   * @param {string} keywords delimited by space
3447
   * @param {boolean} inclSelf include own posts?
3448
   * @param {array} field names to include in the result (catObj for whole object)
3449
   * @param {number} max
3450
   * @param {number} min
3451
   * @return {XResultMap}
3452
   */
3453
  async fetchMentionChoices(keywords, max = null, min = null, callback) {
3454
    const postProcess = function (err, resultMap) {
3455
      if (err) return callback(err, null);
3456
      // no filter processing
3457
      callback(null, resultMap);
3458
    }; // postProcess
3459
3460
    let urlKwdTags = this.getURL(this.urlHost, "/u/posts/srch/choices");
3461
    let result = this.searchPostPhrase(
3462
      urlKwdTags,
3463
      "@" + keywords,
3464
      false,
3465
      false,
3466
      max,
3467
      min,
3468
      postProcess,
3469
    );
3470
3471
    return result;
3472
  } // fetchMentions
3473
3474
  /**
3475
   * Fetch a list of categories by their IDs and cache them
3476
   *
3477
   * @param {string} url searc h API's URL to use
3478
   * @param {string} phrase delimited by
3479
   * @param {string} inclFields INCL_TAGINFO for now
3480
   * @param {string} expanded true to include expanded tags
3481
   * @param {string} max max entries
3482
   * @param {string} min entries, which means proceed with partial search if initial
3483
   * result is below this number
3484
   * @param params any arguments or filters
3485
   * @return {XResultList}
3486
   */
3487
  async searchPostPhrase(
3488
    url,
3489
    phrase,
3490
    inclFields = null,
3491
    expanded = null,
3492
    max = null,
3493
    min = null,
3494
    callback,
3495
  ) {
3496
    const _m = "shP";
3497
3498
    let apiUrl = url + "?phrase=" + encodeURIComponent(phrase);
3499
    if (inclFields) apiUrl += `&fields=${inclFields}`;
3500
    if (expanded) apiUrl += `&expanded=${String(expanded)}`;
3501
    if (max) apiUrl += `&max=${max}`;
3502
    if (min) apiUrl += `&min=${min}`;
3503
    apiUrl += "&incl=userinfo";
3504
    //let response = null;
3505
    let error = null;
3506
    let resultList;
3507
    // debugger;
3508
    try {
3509
      resultList = await this.requestGET(apiUrl, null);
3510
    } catch (e) {
3511
      this.error(_m, e);
3512
      error = e;
3513
    }
3514
    if (callback) return callback(error, resultList);
3515
    else return resultList;
3516
  } // searchPostPhrase
3517
3518
  // ------------------------------- POST SERVICES -----------------------------------
3519
3520
  /**
3521
   * Submit new post (create) to server, with specs
3522
   * for pictures
3523
   *
3524
   * THIS IS WORK IN PROGRESS
3525
   *
3526
   * @param {XMPost} newPost
3527
   * @param {File[]} files array of local files needed
3528
   * to trigger upload
3529
   * @param {{}} params TBD
3530
   * @callback callback
3531
   */
3532
  async submitPost(newPost, files, params, callback) {
3533
    const _m = "subPost";
3534
3535
    // let listId = newPost.getId();
3536
    // this.log(_m, "list to create/save ", newList);
3537
    if (newPost.getOwnerId() == null) newPost.setOwnerId(this.getUserId());
3538
    if (!this.validateWrite(newPost)) {
3539
      if (callback != null) callback("Unable to Submit Post, null");
3540
      // to-do: dee proper error object
3541
      else return false;
3542
    }
3543
3544
    let formData = new FormData();
3545
    if (!newPost.hasOwner()) newPost.setOwnerId(this.getUserId());
3546
    // formData.append("post", newPost.toJSONString()); // moved to content
3547
    if (files && files[0]?.m3u8) {
3548
      newPost.setVideoUrl(files[0].m3u8);
3549
      files[0].ori && newPost.setOriginalVideoUrl(files[0].ori);
3550
      files[0].screen && newPost.setMainImageURL(files[0].screen);
3551
      files[0].duration &&
3552
        newPost.setVideoDuration(parseInt(files[0].duration));
3553
      files[0].width && newPost.setVideoWidth(parseInt(files[0].width));
3554
      files[0].height && newPost.setVideoHeight(parseInt(files[0].height));
3555
    } else if (files) {
3556
      const imageFiles = [];
3557
      const imageMeta = [];
3558
      for (const file of files) {
3559
        imageFiles.push(file.ori);
3560
        const {heads, width: wid, height: hgt} = file;
3561
        if (heads && wid && hgt) {
3562
          imageMeta.push({wid, hgt, meta: {heads}});
3563
        }
3564
      }
3565
      if (imageMeta.length) {
3566
        newPost.setImageMeta(imageMeta);
3567
      }
3568
      newPost.setImageURLs(imageFiles);
3569
    }
3570
3571
    let savedPost;
3572
    let apiError;
3573
    try {
3574
      let url = this.getURL(this.urlHost, "/u/post");
3575
      savedPost = await this.requestPOST_FormData(url, newPost, formData);
3576
    } catch (e) {
3577
      this.error(_m, e);
3578
      apiError = e;
3579
    }
3580
    if (callback) callback(apiError, savedPost);
3581
    else return savedPost;
3582
  } // submitPost
3583
3584
  /**
3585
   * Submit a repost (create) to server, with specs
3586
   * for pictures. Repost differs from sharing a post,
3587
   * in that it is a real post that references another
3588
   * post.
3589
   *
3590
   * THIS IS WORK IN PROGRESS
3591
   *
3592
   * @param {XMPost} newPost
3593
   * @param {string} refPostId referenced post
3594
   * @param {File[]} imageFiles array of local files needed
3595
   * to trigger upload
3596
   * @param {{}} params TBD
3597
   * @callback callback
3598
   *
3599
   * @see ~SharesXXX
3600
   */
3601
  async submitRepost(newPost, files, params, callback) {
3602
    const _m = "subRepost";
3603
3604
    if (newPost.getOwnerId() == null) newPost.setOwnerId(this.getUserId());
3605
    if (!this.validateWrite(newPost)) {
3606
      if (callback != null) callback("Unable to Submit Repost, null");
3607
      // to-do: dee proper error object
3608
      else return false;
3609
    }
3610
3611
    let formData = new FormData();
3612
    if (!newPost.hasOwner()) newPost.setOwnerId(this.getUserId());
3613
    // formData.append("post", newPost.toJSONString()); // moved to content
3614
    if (files && files[0]?.m3u8) {
3615
      newPost.setOriginalVideoUrl(files[0]);
3616
    } else if (files) {
3617
      const imageFiles = [];
3618
      for (let i in files) {
3619
        imageFiles.push(files[i].ori);
3620
      }
3621
      newPost.setImageURLs(imageFiles);
3622
    }
3623
3624
    let savedPost;
3625
    let apiError;
3626
    try {
3627
      let url = this.getURL(this.urlHost, "/u/repost");
3628
      savedPost = await this.requestPOST_FormData(url, newPost, formData);
3629
    } catch (e) {
3630
      this.error(_m, e);
3631
      apiError = e;
3632
    }
3633
    if (callback) callback(apiError, savedPost);
3634
    else return savedPost;
3635
  } // submitRepost
3636
3637
  /**
3638
   * Delete a post by the owner
3639
   *
3640
   * @param {string} userId use user ID
3641
   * @param {string} section one of UserProps.SETTINGS_*
3642
   * @param {{}} params any arguments or filters
3643
   * @return {XUserInfo} subclass of it which is basically specific
3644
   * settings like XAccountSettings, XProfileSettings, etc.
3645
   */
3646
  async deletePost(postId, callback) {
3647
    const _m = "delPost";
3648
    let error = null;
3649
    let result = null;
3650
    try {
3651
      let url = this.getURL(this.urlHost, `/u/post/${postId}`);
3652
      result = await this.requestDELETE(url, null);
3653
      return callback ? callback(null, result) : result;
3654
    } catch (e) {
3655
      this.error(_m, e);
3656
      if (callback) callback(error, null);
3657
      else throw e;
3658
    }
3659
  } // deletePost
3660
3661
  // ------------------------------- POST COMMMENTS -----------------------------------
3662
3663
  /**
3664
   * Submit new comment to the server. If the comment object has
3665
   * a parent comment Id, then this comment is a reply to that
3666
   * parent comment. if parent comment Id is null, then this
3667
   * comment is a reply to the post.
3668
   *
3669
   *
3670
   * @param {string} postId post the comment is associated with, but
3671
   * does not have to be immediate reply! can be nested.
3672
   * @param {XMComment} newComment
3673
   * @param {File[]} files array of local files needed
3674
   * to trigger upload
3675
   * @param {{}} params TBD
3676
   * @callback callback
3677
   */
3678
  async submitComment(postId, newComment, files, params, callback) {
3679
    const _m = "subPost";
3680
3681
    // let listId = newPost.getId();
3682
    // this.log(_m, "list to create/save ", newList);
3683
    if (postId == null) postId = newComment.getPostId();
3684
    let parentCommentId = newComment.getParentCommentId();
3685
    let forPost = parentCommentId == null;
3686
    if (newComment.getOwnerId() == null)
3687
      newComment.setOwnerId(this.getUserId());
3688
    if (!this.validateWrite(newComment)) {
3689
      if (callback != null) callback("Unable to Submit Comment");
3690
      // to-do: dee proper error object
3691
      else return false;
3692
    }
3693
3694
    let formData = new FormData();
3695
    if (!newComment.hasOwner()) newComment.setOwnerId(this.getUserId());
3696
    // formData.append("post", newPost.toJSONString()); // moved to content
3697
    if (files && files[0]?.m3u8) {
3698
      newComment.setVideoUrl(files[0].m3u8);
3699
      files[0].ori && newComment.setOriginalVideoUrl(files[0].ori);
3700
      files[0].screen && newComment.setMainImageURL(files[0].screen);
3701
      files[0].duration &&
3702
        newComment.setVideoDuration(parseInt(files[0].duration));
3703
      files[0].width && newComment.setVideoWidth(parseInt(files[0].width));
3704
      files[0].height && newComment.setVideoHeight(parseInt(files[0].height));
3705
    } else if (files) {
3706
      const imageFiles = [];
3707
      for (let i in files) {
3708
        imageFiles.push(files[i].ori);
3709
      }
3710
      newComment.setImageURLs(imageFiles);
3711
    }
3712
    // formData.append("images", files);
3713
    let savedComment;
3714
    let apiError;
3715
    try {
3716
      let endpoint = forPost
3717
        ? `/u/post/${postId}/comment`
3718
        : `/u/comment/${parentCommentId}/comment`;
3719
      let url = this.getURL(this.urlHost, endpoint);
3720
      savedComment = await this.requestPOST_FormData(url, newComment, formData);
3721
    } catch (e) {
3722
      this.error(_m, e);
3723
      apiError = e;
3724
    }
3725
    if (callback) callback(apiError, savedComment);
3726
    else return savedComment;
3727
  } // submitPost
3728
3729
  /**
3730
   * Delete a post comment by the comment owner
3731
   *
3732
   * @param {string} postId
3733
   * @param {string} commentId
3734
   * @param {{}} params any arguments or filters
3735
   * @return {XUserInfo} subclass of it which is basically specific
3736
   * settings like XAccountSettings, XProfileSettings, etc.
3737
   */
3738
  async deletePostComment(commentId, callback) {
3739
    const _m = "delPost";
3740
    let error = null;
3741
    let result = null;
3742
    try {
3743
      let url = this.getURL(this.urlHost, `/u/comment/${commentId}`);
3744
      result = await this.requestDELETE(url, null);
3745
      return callback ? callback(null, result) : result;
3746
    } catch (e) {
3747
      this.error(_m, e);
3748
      if (callback) callback(e, null);
3749
      else throw e;
3750
    }
3751
  } // deletePostComment
3752
3753
  // -------------------------- LOG SERVICES ----------------------------
3754
3755
  /**
3756
   * Send a log record to server
3757
   *
3758
   * @param {XMActivityLog} activityLog constructed log
3759
   *
3760
   * @return {boolean} should be true if no issue. Sending is async
3761
   */
3762
  async transmitLog(activityLog, props = null) {
3763
    const _m = "tlog";
3764
    try {
3765
      let logId = activityLog.getDerivedID();
3766
      let url = this.urlActivityLog + logId;
3767
      await this.requestPOST(url, activityLog);
3768
    } catch (e) {
3769
      // probably should keep silent in the web browser
3770
      this.warn(_m, e);
3771
    }
3772
    return true;
3773
  } // transmitLog
3774
3775
  /**
3776
   * Log a message on the server side. This is useful for mobile
3777
   * debugging..for now
3778
   *
3779
   * @param {string=} m method name (optional)
3780
   * @param {string} msg
3781
   */
3782
  async logMessageServer(m, msg) {
3783
    const _m = "logms";
3784
    try {
3785
      msg = m ? m + ": " + msg : msg;
3786
      console.log(`${_m}: ${msg}`);
3787
      await this.requestPOST(this.urlLogMessage, {msg: msg});
3788
    } catch (e) {
3789
      // probably should keep silent in the web browser
3790
      this.warn(_m, e);
3791
    }
3792
    return true;
3793
  }
3794
3795
  // ----------------------- USER SIGN-UP / AUTH ------------------------
3796
3797
  /**
3798
   * User signup
3799
   *
3800
   * @param {string} userInfo user to follow
3801
   *
3802
   * @return {XUserInfo} upated user info with new follow
3803
   */
3804
  async signupUser(userInfo, authInfo, callback) {
3805
    const _m = "userFollows";
3806
    let newUser;
3807
    let error = null;
3808
    try {
3809
      let content = {
3810
        userinfo: userInfo.getData(),
3811
        authinfo: authInfo.getData(),
3812
      };
3813
3814
      let encrypted = Util.EncryptJSON(content);
3815
3816
      let url = this.getURL(this.urlHost, `/s/signup`);
3817
      newUser = await this.requestPOST(url, encrypted);
3818
    } catch (e) {
3819
      error = XError.FromRequestError(e);
3820
      this.error(_m, e);
3821
3822
      newUser = null;
3823
    }
3824
3825
    return callback ? callback(error, newUser) : newUser;
3826
  } // signupUser
3827
3828
  // ----------------------- LOAD SPECIAL RESOURCES ---------------------
3829
3830
  /**
3831
   * Fetch help file in MD format.
3832
   *
3833
   * @param {string=} helpId null to retreive latest template. Give a
3834
   * user ID will retrieve user's confirmed version.
3835
   * @param {string} locale language requirement. default is "en"
3836
   * @param {*} callback
3837
   */
3838
  async fetchHelpFile(helpId, locale, callback) {
3839
    if (Util.StringIsEmpty(helpId)) return "No Help??";
3840
3841
    if (locale == null) locale = this.getLanguagePref();
3842
    let url = `/doc/md/help/${locale}/${helpId}`;
3843
    return this.fetchBinaryData(url, callback);
3844
  }
3845
3846
  /**
3847
   * Fetch the About Us markdown text that is suitable to display publicly.
3848
   * @param {string} locale language. default is "en"
3849
   * @callback
3850
   * @return {XBinaryData} wrapper containing marked down TOS text.
3851
   */
3852
  async fetchAboutUs(locale, callback) {
3853
    if (locale == null) locale = "en";
3854
    return this.fetchBinaryData(`/doc/md/legal/${locale}/aboutus`, callback);
3855
  }
3856
3857
  /**
3858
   * Fetch TOS for user to confirm, or what user has already confirmed.
3859
   *
3860
   * @param {string=} userId null to retreive latest template. Give a
3861
   * user ID will retrieve user's confirmed version.
3862
   * @param {*} locale
3863
   * @param {*} callback
3864
   */
3865
  async fetchUserTOS(userId, locale, callback) {
3866
    if (locale == null) locale = "en";
3867
    let url = userId
3868
      ? `/doc/md/legal/${locale}/tos/user/${userId}`
3869
      : `/doc/md/legal/${locale}/tos/user`;
3870
    return this.fetchBinaryData(url, callback);
3871
  }
3872
3873
  /**
3874
   * Fetch the TOS markdown text that is suitable to display publicly.
3875
   * @param {string} locale language. default is "en"
3876
   * @callback
3877
   * @return {XBinaryData} wrapper containing marked down TOS text.
3878
   */
3879
  async fetchPublicTOS(locale, callback) {
3880
    if (locale == null) locale = "en";
3881
    return this.fetchBinaryData(`/doc/md/legal/${locale}/tos/public`, callback);
3882
  }
3883
3884
  /**
3885
   * Fetch the DMCA notice markdown text that is suitable to display publicly.
3886
   *
3887
   * @param {string} locale language. default is "en"
3888
   * @callback
3889
   * @return {XBinaryData} wrapper containing marked down TOS text.
3890
   */
3891
  async fetchDMCA(locale, callback) {
3892
    if (locale == null) locale = "en";
3893
    return this.fetchBinaryData(`/doc/md/legal/${locale}/dmca`, callback);
3894
  }
3895
3896
  /**
3897
   * Fetch the user guidelines markdown text that is suitable to display publicly.
3898
   *
3899
   * @param {string} locale language. default is "en"
3900
   * @callback
3901
   * @return {XBinaryData} wrapper containing marked down TOS text.
3902
   */
3903
  async fetchUserGuidelines(locale, callback) {
3904
    if (locale == null) locale = "en";
3905
    return this.fetchBinaryData(
3906
      `/doc/md/legal/${locale}/user_guidelines`,
3907
      callback,
3908
    );
3909
  }
3910
3911
  /**
3912
   * Fetch the legal guidelines markdown text that is suitable to display publicly.
3913
   *
3914
   * @param {string} locale language. default is "en"
3915
   * @callback
3916
   * @return {XBinaryData} wrapper containing marked down TOS text.
3917
   */
3918
  async fetchLegalGuidelines(locale, callback) {
3919
    if (locale == null) locale = "en";
3920
    return this.fetchBinaryData(
3921
      `/doc/md/legal/${locale}/legal_guidelines`,
3922
      callback,
3923
    );
3924
  }
3925
3926
  disableTranslation(flag) {
3927
    if (flag === true) {
3928
      this["xlate"] = false;
3929
      console.warn("Translation Disabled");
3930
    }
3931
  }
3932
3933
  translationDisabled() {
3934
    return this["xlate"] === false;
3935
  }
3936
3937
  async translateText(fromLang, toLang, text, callback) {
3938
    if (this.translationDisbled()) return callback(null);
3939
3940
    const API_KEY = this.getPortal().getGoogleAPIKey();
3941
3942
    let url = `https://translation.googleapis.com/language/translate/v2?key=${API_KEY}`;
3943
    url += "&q=" + encodeURI(text);
3944
    url += `&source=${fromLang}`;
3945
    url += `&target=${toLang}`;
3946
3947
    fetch(url, {
3948
      method: "GET",
3949
      headers: {
3950
        "Content-Type": "application/json",
3951
        Accept: "application/json",
3952
      },
3953
    })
3954
      .then((res) => res.json())
3955
      .then(callback)
3956
      .catch((error) => {
3957
        console.log("There was an error with the translation request: ", error);
3958
      });
3959
  }
3960
3961
  /**
3962
   *
3963
   * @param {string} fromLang
3964
   * @param {string} toLang
3965
   * @param {string} text
3966
   * @param {function(XError, *)} callback error and result
3967
   */
3968
  async translateText_notworking(fromLang, toLang, text, callback) {
3969
    if (this.translationDisbled()) return callback(null);
3970
3971
    const API_KEY = this.getPortal().getGoogleAPIKey();
3972
3973
    let url = `https://translation.googleapis.com/language/translate/v2?key=${API_KEY}`;
3974
    url += "&q=" + encodeURI(text);
3975
    url += `&source=${fromLang}`;
3976
    url += `&target=${toLang}`;
3977
    return this.requestExternalGET(url, null, callback);
3978
  }
3979
3980
  async detectLanguage(text, callback) {
3981
    const API_KEY = this.getPortal().getGoogleAPIKey();
3982
3983
    let url = `https://translation.googleapis.com/language/translate/v2/detect?key=${API_KEY}`;
3984
    url += "&q=" + encodeURI(text);
3985
3986
    fetch(url, {
3987
      method: "POST",
3988
      headers: {
3989
        "Content-Type": "application/json",
3990
        Accept: "application/json",
3991
      },
3992
      body: JSON.stringify({q: text}),
3993
    })
3994
      .then((res) => res.json())
3995
      .then(callback)
3996
      .catch((error) => {
3997
        console.log("There was an error with the translation request: ", error);
3998
      });
3999
  }
4000
4001
  /**
4002
   *
4003
   * @param {string} text
4004
   * @param {function(XError, *)} callback  error and result
4005
   */
4006
  async detectLanguage_notworking(text, callback) {
4007
    // let _m = "detectLang";
4008
    let apiKey = this.getPortal().getGoogleAPIKey();
4009
    let url = `https://translation.googleapis.com/language/translate/v2/detect?key=${apiKey}`;
4010
    url += "&q=" + encodeURI(text);
4011
4012
    let p = this.requestExternalPOST(url, null, (err, result) => {
4013
      this._processResult(err, result, callback);
4014
    });
4015
    return p;
4016
  }
4017
4018
  // ------------------- VERIFICATION CODE DELIVERY ---------------------
4019
4020
  /**
4021
   * Request server to delivery a verification code to an email address
4022
   *
4023
   * @param {string} code clear text to show in an email
4024
   * @param {string} email
4025
   * @return {boolean} true if no errors, false if not delivered for any reason
4026
   */
4027
  async sendVerificationCode(code, email, callback) {
4028
    const _m = "pre";
4029
4030
    let url = this.getURL(this.urlHost, "/s/pre");
4031
    let error = null;
4032
    let result = false;
4033
    try {
4034
      let content = {
4035
        code: code,
4036
        email: email,
4037
      };
4038
      result = await this.requestPOST(url, content);
4039
    } catch (e) {
4040
      this.error(_m, e);
4041
      error = e;
4042
    }
4043
4044
    return callback ? callback(error, result) : result;
4045
  }
4046
4047
  /**
4048
   * remove exif from image
4049
   * @param {File} file
4050
   */
4051
  _removeExif(file) {
4052
    return new Promise((resolve, reject) => {
4053
      if (file.type !== "image/jpeg" && file.type !== "image/png") {
4054
        return reject("Wrong file type");
4055
      }
4056
      const fileURL = URL.createObjectURL(file);
4057
      const canvas = document.createElement("canvas");
4058
      const canvasContext = canvas.getContext("2d");
4059
      const image = new Image();
4060
      image.src = fileURL;
4061
      image.onload = (e) => {
4062
        canvas.width = image.naturalWidth;
4063
        canvas.height = image.naturalHeight;
4064
        canvasContext.drawImage(
4065
          e.target,
4066
          0,
4067
          0,
4068
          image.naturalWidth,
4069
          image.naturalHeight,
4070
        );
4071
        canvas.toBlob((blob) => {
4072
          URL.revokeObjectURL(fileURL);
4073
          blob.lastModifiedDate = new Date();
4074
          blob.name = file.name;
4075
          return resolve(blob);
4076
        }, file.type);
4077
      };
4078
      image.onerror = () => {
4079
        return reject("Failed to remove exif");
4080
      };
4081
    });
4082
  }
4083
4084
  /**
4085
   * upload file
4086
   * @param {File} file
4087
   * @param {Function} callback
4088
   */
4089
  async uploadFile(file, hasExif, callback, path = "/media/upload") {
4090
    const fileSize = file.size;
4091
    let md5 = null;
4092
4093
    if (fileSize <= 300000000) {
4094
      // 300 MB
4095
      try {
4096
        md5 = await fileToMd5(file);
4097
      } catch (error) {}
4098
    }
4099
4100
    const _m = "uploadFile";
4101
    const url = this.getURL(process.env.REACT_APP_MEDIA_UPLOAD, path);
4102
    const formData = new FormData();
4103
    const userInfo = this.getXUserInfo();
4104
    const lv = userInfo.getInfluencerLevel();
4105
    // const lv = 5;
4106
    let result;
4107
    let error;
4108
    if (hasExif) {
4109
      try {
4110
        const blob = await this._removeExif(file);
4111
        if (blob) {
4112
          file = blob;
4113
        }
4114
      } catch (err) {
4115
        console.log(err);
4116
      }
4117
    }
4118
4119
    formData.append("file", file, file.name);
4120
    formData.append("user_id", userInfo.data._id);
4121
    // formData.append("auth_token", this.portal.getUserToken());
4122
    // try {
4123
    //   this.requestPOST_FormData(url, null, formData, callback);
4124
    // } catch (e) {
4125
    //   this.error(_m, e);
4126
    //   error = e;
4127
    // }
4128
    // return callback ? callback(error, result) : result;
4129
    // const auth =
4130
    //   "Basic " + Buffer.from("getterupload:getterupload").toString("base64");
4131
    let config = {
4132
      // withCredentials: true,
4133
      headers: {
4134
        // "Access-Control-Allow-Origin": "*",
4135
        "Content-Type": "multipart/form-data",
4136
        authorization: this.portal.getUserToken(),
4137
        userid: this.portal.getUserId(),
4138
        filename: file.name,
4139
        // lossless: hasExif ? 0 : 1,
4140
        lossless: 1,
4141
        lv,
4142
        env: process.env.REACT_APP_GETTER_ENV,
4143
      },
4144
    };
4145
4146
    if (md5) {
4147
      config.headers.md5 = md5;
4148
    }
4149
    const cancelTokenSource = axios.CancelToken.source();
4150
    const cancelUpload = cancelTokenSource.cancel;
4151
    let complete = false;
4152
    let timeout = false;
4153
    let timeoutInterval = setInterval(() => {
4154
      if (!timeout) {
4155
        timeout = true;
4156
      } else if (!complete) {
4157
        cancelUpload();
4158
      }
4159
    }, 20000);
4160
    axios({
4161
      url,
4162
      method: "post",
4163
      data: formData,
4164
      ...config,
4165
      onUploadProgress: (p) => {
4166
        timeout = false;
4167
        if (p.loaded == p.total) {
4168
          complete = true;
4169
        }
4170
        callback(null, null, 100 * (p.loaded / p.total));
4171
      },
4172
      cancelToken: cancelTokenSource.token,
4173
    })
4174
      .then((response) => {
4175
        result = response.data;
4176
        if (result.ori) {
4177
          callback(null, result);
4178
        } else {
4179
          error = result.error;
4180
          callback(error);
4181
        }
4182
      })
4183
      .catch((e) => {
4184
        error = e;
4185
        callback(error);
4186
      })
4187
      .finally(() => {
4188
        clearInterval(timeoutInterval);
4189
      });
4190
  }
4191
4192
  /**
4193
   * Construct API URL to report content(post) (/u/user/:userId/report/post/:postId/:reasonId)
4194
   *
4195
   * @param {string} userId
4196
   * @param {string} postId
4197
   * @param {string} reasonId
4198
   * @return {string} fully qualified URL
4199
   */
4200
  apiReportPost(userId, postId, reasonId) {
4201
    let query = this.getURL(
4202
      this.urlHost,
4203
      `/u/user/${userId}/report/post/${postId}/${reasonId}`,
4204
    );
4205
4206
    return query;
4207
  } // apiReportPost
4208
4209
  /**
4210
   * Construct API URL to report user (/u/user/:userId/report/user/:targetId/:reasonId)
4211
   *
4212
   * @param {string} userId
4213
   * @param {string} targetId
4214
   * @param {string} reasonId
4215
   * @return {string} fully qualified URL
4216
   */
4217
  apiReportUser(userId, targetId, reasonId) {
4218
    let query = this.getURL(
4219
      this.urlHost,
4220
      `/u/user/${userId}/report/user/${targetId}/${reasonId}`,
4221
    );
4222
4223
    return query;
4224
  } // apiReportUser
4225
4226
  /**
4227
   * Report content(post) by postId and reasonId
4228
   *
4229
   * @param {string} postId
4230
   * @param {number} reasonId
4231
   */
4232
  async reportPost(postId, reasonId, callback) {
4233
    const _m = "reportPost";
4234
    const loggedInUserId = this.getUserId();
4235
    const reasonIdPrefix = "rsn";
4236
    let result;
4237
    let error = null;
4238
    try {
4239
      let getUrl = this.apiReportPost(
4240
        loggedInUserId,
4241
        postId,
4242
        reasonIdPrefix + reasonId,
4243
      );
4244
      result = await this.requestPOST(getUrl);
4245
    } catch (e) {
4246
      this.error(_m, e);
4247
      error = e;
4248
      result = null;
4249
    }
4250
4251
    return callback ? callback(error, result) : result;
4252
  } // reportPost
4253
4254
  /**
4255
   * Report user by target userId and reasonId
4256
   *
4257
   * @param {string} targetId target userId
4258
   * @param {number} reasonId
4259
   */
4260
  async reportUser(targetId, reasonId, callback) {
4261
    const _m = "reportUser";
4262
    const loggedInUserId = this.getUserId();
4263
    const reasonIdPrefix = "rsn";
4264
    let result;
4265
    let error = null;
4266
    try {
4267
      let getUrl = this.apiReportUser(
4268
        loggedInUserId,
4269
        targetId,
4270
        reasonIdPrefix + reasonId,
4271
      );
4272
      result = await this.requestPOST(getUrl);
4273
    } catch (e) {
4274
      this.error(_m, e);
4275
      error = e;
4276
      result = null;
4277
    }
4278
4279
    return callback ? callback(error, result) : result;
4280
  } // reportUser
4281
4282
  /**
4283
   * Construct API URL to get muted users (/u/user/:userId/mutes/?offset=0&max=5&incl=userstats|userinfo)
4284
   *
4285
   * @param {string} userId
4286
   * @param {number} max
4287
   * @return {string} fully qualified URL
4288
   */
4289
  apiGetMutedUsers(userId, max) {
4290
    let query = this.getURL(
4291
      this.urlHost,
4292
      `/u/user/${userId}/mutes/?offset=0&max=${max}&incl=userstats|userinfo`,
4293
    );
4294
4295
    return query;
4296
  } // apiGetMutedUsers
4297
4298
  /**
4299
   * Get muted users
4300
   *
4301
   * @param {number} max
4302
   * @param {(error, result) => void} callback
4303
   */
4304
  async getMutedUsers(max = 5, callback) {
4305
    const _m = "getMutedUsers";
4306
    const loggedInUserId = this.getUserId();
4307
    let result;
4308
    let error = null;
4309
    try {
4310
      let getUrl = this.apiGetMutedUsers(loggedInUserId, max);
4311
      result = await this.requestGET(getUrl);
4312
    } catch (e) {
4313
      this.error(_m, e);
4314
      error = e;
4315
      result = null;
4316
    }
4317
4318
    return callback ? callback(error, result) : result;
4319
  } // getMutedUsers
4320
4321
  /**
4322
   * Construct API URL to get users who I follow (/u/user/:userId/followings/?offset=0&max=5&incl=userstats|userinfo)
4323
   *
4324
   * @param {string} userId
4325
   * @param {number} max
4326
   * @return {string} fully qualified URL
4327
   */
4328
  apiGetFollowingUsers(userId, max) {
4329
    let query = this.getURL(
4330
      this.urlHost,
4331
      `/u/user/${userId}/followings/?offset=0&max=${max}&incl=userstats|userinfo`,
4332
    );
4333
4334
    return query;
4335
  } // apiGetFollowingUsers
4336
4337
  /**
4338
   * Get following users
4339
   *
4340
   * @param {string} userId
4341
   * @param {number} max
4342
   * @param {(error, result) => void} callback
4343
   */
4344
  async getFollowingUsers(userId, max = 5, callback) {
4345
    const _m = "getFollowingUsers";
4346
    let result;
4347
    let error = null;
4348
    try {
4349
      let getUrl = this.apiGetFollowingUsers(userId, max);
4350
      result = await this.requestGET(getUrl);
4351
    } catch (e) {
4352
      this.error(_m, e);
4353
      error = e;
4354
      result = null;
4355
    }
4356
4357
    return callback ? callback(error, result) : result;
4358
  } // getFollowingUsers
4359
4360
  /**
4361
   * Construct API URL to get blocked users (/u/user/:userId/blockers/?offset=0&max=5&incl=userstats|userinfo)
4362
   *
4363
   * @param {string} userId
4364
   * @param {number} max
4365
   * @return {string} fully qualified URL
4366
   */
4367
  apiGetBlockedUsers(userId, max) {
4368
    let query = this.getURL(
4369
      this.urlHost,
4370
      `/u/user/${userId}/blockers/?offset=0&max=${max}&incl=userstats|userinfo`,
4371
    );
4372
4373
    return query;
4374
  } // apiGetBlockedUsers
4375
4376
  /**
4377
   * Get blocked users
4378
   *
4379
   * @param {string} userId
4380
   * @param {number} max
4381
   * @param {(error, result) => void} callback
4382
   */
4383
  async getBlockedUsers(userId, max = 5, callback) {
4384
    const _m = "getBlockedUsers";
4385
    let result;
4386
    let error = null;
4387
    try {
4388
      let getUrl = this.apiGetBlockedUsers(userId, max);
4389
      result = await this.requestGET(getUrl);
4390
    } catch (e) {
4391
      this.error(_m, e);
4392
      error = e;
4393
      result = null;
4394
    }
4395
4396
    return callback ? callback(error, result) : result;
4397
  } // getFollowing
4398
4399
  /**
4400
   * Construct API URL to mute user (/u/user/:userId/mutes/:targetId)
4401
   *
4402
   * @param {string} userId
4403
   * @param {string} targetUserId
4404
   * @return {string} fully qualified URL
4405
   */
4406
  apiMuteUser(userId, targetUserId) {
4407
    let query = this.getURL(
4408
      this.urlHost,
4409
      `/u/user/${userId}/mutes/${targetUserId}`,
4410
    );
4411
4412
    return query;
4413
  } // apiMuteUser
4414
4415
  /**
4416
   * Mute user
4417
   *
4418
   * @param {string} targetUserId
4419
   * @param {(error, result) => void} callback
4420
   */
4421
  async muteUser(targetUserId, callback) {
4422
    const _m = "muteUser";
4423
    const loggedInUserId = this.getUserId();
4424
    let result;
4425
    let error = null;
4426
    try {
4427
      let getUrl = this.apiMuteUser(loggedInUserId, targetUserId);
4428
      result = await this.requestPOST(getUrl);
4429
    } catch (e) {
4430
      this.error(_m, e);
4431
      error = e;
4432
      result = null;
4433
    }
4434
4435
    return callback ? callback(error, result) : result;
4436
  } // muteUser
4437
4438
  /**
4439
   * Unmute user
4440
   *
4441
   * @param {string} targetUserId
4442
   * @param {(error, result) => void} callback
4443
   */
4444
  async unmuteUser(targetUserId, callback) {
4445
    const _m = "unmuteUser";
4446
    const loggedInUserId = this.getUserId();
4447
    let result;
4448
    let error = null;
4449
    try {
4450
      let getUrl = this.apiMuteUser(loggedInUserId, targetUserId);
4451
      result = await this.requestDELETE(getUrl);
4452
    } catch (e) {
4453
      this.error(_m, e);
4454
      error = e;
4455
      result = null;
4456
    }
4457
4458
    return callback ? callback(error, result) : result;
4459
  } // unmuteUser
4460
4461
  /**
4462
   * Construct API URL to follow user (/u/user/:userId/follows/:targetId)
4463
   *
4464
   * @param {string} userId
4465
   * @param {string} targetUserId
4466
   * @return {string} fully qualified URL
4467
   */
4468
  apiFollowUser(userId, targetUserId) {
4469
    let query = this.getURL(
4470
      this.urlHost,
4471
      `/u/user/${userId}/follows/${targetUserId}`,
4472
    );
4473
4474
    return query;
4475
  } // apiFollowUser
4476
4477
  /**
4478
   * Follow user
4479
   *
4480
   * @param {string} targetUserId
4481
   * @param {(error, result) => void} callback
4482
   */
4483
  async followUser(targetUserId, callback) {
4484
    const _m = "followUser";
4485
    const loggedInUserId = this.getUserId();
4486
    let result;
4487
    let error = null;
4488
    try {
4489
      let getUrl = this.apiFollowUser(loggedInUserId, targetUserId);
4490
      result = await this.requestPOST(getUrl);
4491
    } catch (e) {
4492
      this.error(_m, e);
4493
      error = e;
4494
      result = null;
4495
    }
4496
4497
    return callback ? callback(error, result) : result;
4498
  } // followUser
4499
4500
  /**
4501
   * Unfollow user
4502
   *
4503
   * @param {string} targetUserId
4504
   * @param {(error, result) => void} callback
4505
   */
4506
  async unfollowUser(targetUserId, callback) {
4507
    const _m = "unfollowUser";
4508
    const loggedInUserId = this.getUserId();
4509
    let result;
4510
    let error = null;
4511
    try {
4512
      let getUrl = this.apiFollowUser(loggedInUserId, targetUserId);
4513
      result = await this.requestDELETE(getUrl);
4514
    } catch (e) {
4515
      this.error(_m, e);
4516
      error = e;
4517
      result = null;
4518
    }
4519
4520
    return callback ? callback(error, result) : result;
4521
  } // unfollowUser
4522
4523
  /**
4524
   * Construct API URL to block user (/u/user/:userId/blocks/:targetId)
4525
   *
4526
   * @param {string} userId
4527
   * @param {string} targetUserId
4528
   * @return {string} fully qualified URL
4529
   */
4530
  apiBlockUser(userId, targetUserId) {
4531
    let query = this.getURL(
4532
      this.urlHost,
4533
      `/u/user/${userId}/blocks/${targetUserId}`,
4534
    );
4535
4536
    return query;
4537
  } // apiBlockUser
4538
4539
  /**
4540
   * Block user
4541
   *
4542
   * @param {string} targetUserId
4543
   * @param {(error, result) => void} callback
4544
   */
4545
  async blockUser(targetUserId, callback) {
4546
    const _m = "blockUser";
4547
    const loggedInUserId = this.getUserId();
4548
    let result;
4549
    let error = null;
4550
    try {
4551
      let getUrl = this.apiBlockUser(loggedInUserId, targetUserId);
4552
      result = await this.requestPOST(getUrl);
4553
    } catch (e) {
4554
      this.error(_m, e);
4555
      error = e;
4556
      result = null;
4557
    }
4558
4559
    return callback ? callback(error, result) : result;
4560
  } // blockUser
4561
4562
  /**
4563
   * Unblock user
4564
   *
4565
   * @param {string} targetUserId
4566
   * @param {(error, result) => void} callback
4567
   */
4568
  async unblockUser(targetUserId, callback) {
4569
    const _m = "unblockUser";
4570
    const loggedInUserId = this.getUserId();
4571
    let result;
4572
    let error = null;
4573
    try {
4574
      let getUrl = this.apiBlockUser(loggedInUserId, targetUserId);
4575
      result = await this.requestDELETE(getUrl);
4576
    } catch (e) {
4577
      this.error(_m, e);
4578
      error = e;
4579
      result = null;
4580
    }
4581
4582
    return callback ? callback(error, result) : result;
4583
  } // unblockUser
4584
4585
  /**
4586
   * Construct API URL to block user (/u/user/:userId/blocks/:targetId)
4587
   *
4588
   * @param {string} userId
4589
   * @param {string} targetUserId
4590
   * @return {string} fully qualified URL
4591
   */
4592
  apiSuspendUser(targetUserId) {
4593
    let query = this.getURL(
4594
      this.urlHost,
4595
      `/admin/user/${targetUserId}/suspend`,
4596
    );
4597
4598
    return query;
4599
  } // apiSuspendUser
4600
4601
  /**
4602
   * Block user
4603
   *
4604
   * @param {string} targetUserId
4605
   * @param {(error, result) => void} callback
4606
   */
4607
  async suspendUser(targetUserId, callback) {
4608
    const _m = "suspendUser";
4609
    let result;
4610
    let error = null;
4611
    try {
4612
      let getUrl = this.apiSuspendUser(targetUserId);
4613
      result = await this.requestPOST(getUrl);
4614
    } catch (e) {
4615
      this.error(_m, e);
4616
      error = e;
4617
      result = null;
4618
    }
4619
4620
    return callback ? callback(error, result) : result;
4621
  } // suspendUser
4622
4623
  /**
4624
   * Unblock user
4625
   *
4626
   * @param {string} targetUserId
4627
   * @param {(error, result) => void} callback
4628
   */
4629
  async unSuspendUser(targetUserId, callback) {
4630
    const _m = "unSuspendUser";
4631
    let result;
4632
    let error = null;
4633
    try {
4634
      let getUrl = this.apiUnSuspendUser(targetUserId);
4635
      result = await this.requestDELETE(getUrl);
4636
    } catch (e) {
4637
      this.error(_m, e);
4638
      error = e;
4639
      result = null;
4640
    }
4641
4642
    return callback ? callback(error, result) : result;
4643
  } // unSuspendUser
4644
} // class
4645
4646
export default GetterService;
4647