· 4 years ago · Jul 03, 2021, 12:06 AM
1import ClientAppService from "./system/ClientAppService";
2
3// Data Models
4import XError from "./core/model/XError";
5import XRequest from "./core/model/net/XRequest";
6import XResponse from "./core/model/net/XResponse";
7import XObject from "./core/model/XObject";
8import XMObject from "./core/model/XMObject";
9import XResultList from "./core/model/util/XResultList";
10import XResultMap from "./core/model/util/XResultMap";
11import XBinaryData from "./core/model/XBinaryData";
12import {
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";
27import {XMFollows, XMFollowers} from "./core/model/social/XMFollow";
28import Util from "./core/Util";
29import axios from "axios";
30
31import XMWatchesPost from "./core/model/post/XMWatchesPost";
32import XMWatchedPost from "./core/model/post/XMWatchedPost";
33import XPostFeed from "./core/model/activity/XPostFeed";
34import XMPost from "./core/model/post/XMPost";
35import {AppMessages, SupportedLanguageList} from "./app/AppMessages";
36import XUserInfo from "./core/model/user/XUserInfo";
37import XCommentFeed from "./core/model/post/XCommentFeed";
38import XMComment from "./core/model/social/XMComment";
39import API from "./core/API";
40import ErrorCodes from "./core/ErrorCodes";
41import {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
46XError.CheckIn();
47XResponse.CheckIn();
48XRequest.CheckIn();
49XBinaryData.CheckIn();
50XResultList.CheckIn();
51XResultMap.CheckIn();
52
53XPostFeed.CheckIn();
54XMPost.CheckIn();
55
56// import numeral from 'numeral';
57
58const _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 */
68export 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
4646export default GetterService;
4647