hollerith

gettr

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