sidechat.js 2.6.6 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/classes/SidechatAPIClient.d.ts +166 -11
  2. package/dist/classes/SidechatAPIClient.d.ts.map +1 -1
  3. package/dist/index.cjs +853 -311
  4. package/dist/index.mjs +853 -311
  5. package/dist/types/SidechatTypes.d.ts +260 -9
  6. package/dist/types/SidechatTypes.d.ts.map +1 -1
  7. package/docs/sidechat.js/2.7.0/SidechatAPIClient.html +3 -0
  8. package/docs/sidechat.js/2.7.0/SidechatAPIError.html +3 -0
  9. package/docs/sidechat.js/2.7.0/SidechatColorList.html +3 -0
  10. package/docs/sidechat.js/2.7.0/classes_SidechatAPIClient.js.html +1897 -0
  11. package/docs/sidechat.js/2.7.0/classes_SidechatAPIError.js.html +16 -0
  12. package/docs/sidechat.js/2.7.0/classes_SidechatColorList.js.html +79 -0
  13. package/docs/sidechat.js/2.7.0/data/search.json +1 -0
  14. package/docs/sidechat.js/2.7.0/fonts/Inconsolata-Regular.ttf +0 -0
  15. package/docs/sidechat.js/2.7.0/fonts/OpenSans-Regular.ttf +0 -0
  16. package/docs/sidechat.js/2.7.0/fonts/WorkSans-Bold.ttf +0 -0
  17. package/docs/sidechat.js/2.7.0/global.html +3 -0
  18. package/docs/sidechat.js/2.7.0/index.html +4 -0
  19. package/docs/sidechat.js/2.7.0/scripts/core.js +726 -0
  20. package/docs/sidechat.js/2.7.0/scripts/core.min.js +23 -0
  21. package/docs/sidechat.js/2.7.0/scripts/resize.js +90 -0
  22. package/docs/sidechat.js/2.7.0/scripts/search.js +265 -0
  23. package/docs/sidechat.js/2.7.0/scripts/search.min.js +6 -0
  24. package/docs/sidechat.js/2.7.0/scripts/third-party/Apache-License-2.0.txt +202 -0
  25. package/docs/sidechat.js/2.7.0/scripts/third-party/fuse.js +9 -0
  26. package/docs/sidechat.js/2.7.0/scripts/third-party/hljs-line-num-original.js +369 -0
  27. package/docs/sidechat.js/2.7.0/scripts/third-party/hljs-line-num.js +1 -0
  28. package/docs/sidechat.js/2.7.0/scripts/third-party/hljs-original.js +5171 -0
  29. package/docs/sidechat.js/2.7.0/scripts/third-party/hljs.js +1 -0
  30. package/docs/sidechat.js/2.7.0/scripts/third-party/popper.js +5 -0
  31. package/docs/sidechat.js/2.7.0/scripts/third-party/tippy.js +1 -0
  32. package/docs/sidechat.js/2.7.0/scripts/third-party/tocbot.js +672 -0
  33. package/docs/sidechat.js/2.7.0/scripts/third-party/tocbot.min.js +1 -0
  34. package/docs/sidechat.js/2.7.0/styles/clean-jsdoc-theme-base.css +1159 -0
  35. package/docs/sidechat.js/2.7.0/styles/clean-jsdoc-theme-dark.css +412 -0
  36. package/docs/sidechat.js/2.7.0/styles/clean-jsdoc-theme-light.css +482 -0
  37. package/docs/sidechat.js/2.7.0/styles/clean-jsdoc-theme-scrollbar.css +30 -0
  38. package/docs/sidechat.js/2.7.0/styles/clean-jsdoc-theme-without-scrollbar.min.css +1 -0
  39. package/docs/sidechat.js/2.7.0/styles/clean-jsdoc-theme.min.css +1 -0
  40. package/docs/sidechat.js/2.7.0/tutorial-Authentication.html +16 -0
  41. package/docs/sidechat.js/2.7.0/tutorial-Email Registration.html +6 -0
  42. package/docs/sidechat.js/2.7.0/tutorial-Feeds and Pagination.html +10 -0
  43. package/docs/sidechat.js/2.7.0/tutorial-Posting and Media.html +19 -0
  44. package/docs/sidechat.js/2.7.0/types_SidechatTypes.js.html +297 -0
  45. package/docs/tutorials/Authentication.md +63 -52
  46. package/docs/tutorials/Email Registration.md +19 -16
  47. package/docs/tutorials/Feeds and Pagination.md +45 -0
  48. package/docs/tutorials/Posting and Media.md +63 -0
  49. package/package.json +55 -55
  50. package/src/classes/SidechatAPIClient.js +477 -27
  51. package/src/types/SidechatTypes.js +294 -202
@@ -0,0 +1,1897 @@
1
+ <!DOCTYPE html><html lang="en" style="font-size:16px"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Source: classes/SidechatAPIClient.js</title><!--[if lt IE 9]>
2
+ <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
3
+ <![endif]--><script src="scripts/third-party/hljs.js" defer="defer"></script><script src="scripts/third-party/hljs-line-num.js" defer="defer"></script><script src="scripts/third-party/popper.js" defer="defer"></script><script src="scripts/third-party/tippy.js" defer="defer"></script><script src="scripts/third-party/tocbot.min.js"></script><script>var baseURL="/",locationPathname="";baseURL=(baseURL=(baseURL="https://micahlindley.com/sidechat.js/").replace(/https?:\/\//i,"")).substr(baseURL.indexOf("/"))</script><link rel="stylesheet" href="styles/clean-jsdoc-theme.min.css"><svg aria-hidden="true" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="display:none"><defs><symbol id="copy-icon" viewbox="0 0 488.3 488.3"><g><path d="M314.25,85.4h-227c-21.3,0-38.6,17.3-38.6,38.6v325.7c0,21.3,17.3,38.6,38.6,38.6h227c21.3,0,38.6-17.3,38.6-38.6V124 C352.75,102.7,335.45,85.4,314.25,85.4z M325.75,449.6c0,6.4-5.2,11.6-11.6,11.6h-227c-6.4,0-11.6-5.2-11.6-11.6V124 c0-6.4,5.2-11.6,11.6-11.6h227c6.4,0,11.6,5.2,11.6,11.6V449.6z"/><path d="M401.05,0h-227c-21.3,0-38.6,17.3-38.6,38.6c0,7.5,6,13.5,13.5,13.5s13.5-6,13.5-13.5c0-6.4,5.2-11.6,11.6-11.6h227 c6.4,0,11.6,5.2,11.6,11.6v325.7c0,6.4-5.2,11.6-11.6,11.6c-7.5,0-13.5,6-13.5,13.5s6,13.5,13.5,13.5c21.3,0,38.6-17.3,38.6-38.6 V38.6C439.65,17.3,422.35,0,401.05,0z"/></g></symbol><symbol id="search-icon" viewBox="0 0 512 512"><g><g><path d="M225.474,0C101.151,0,0,101.151,0,225.474c0,124.33,101.151,225.474,225.474,225.474 c124.33,0,225.474-101.144,225.474-225.474C450.948,101.151,349.804,0,225.474,0z M225.474,409.323 c-101.373,0-183.848-82.475-183.848-183.848S124.101,41.626,225.474,41.626s183.848,82.475,183.848,183.848 S326.847,409.323,225.474,409.323z"/></g></g><g><g><path d="M505.902,476.472L386.574,357.144c-8.131-8.131-21.299-8.131-29.43,0c-8.131,8.124-8.131,21.306,0,29.43l119.328,119.328 c4.065,4.065,9.387,6.098,14.715,6.098c5.321,0,10.649-2.033,14.715-6.098C514.033,497.778,514.033,484.596,505.902,476.472z"/></g></g></symbol><symbol id="font-size-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11.246 15H4.754l-2 5H.6L7 4h2l6.4 16h-2.154l-2-5zm-.8-2L8 6.885 5.554 13h4.892zM21 12.535V12h2v8h-2v-.535a4 4 0 1 1 0-6.93zM19 18a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></symbol><symbol id="add-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11 11V5h2v6h6v2h-6v6h-2v-6H5v-2z"/></symbol><symbol id="minus-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M5 11h14v2H5z"/></symbol><symbol id="dark-theme-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M10 7a7 7 0 0 0 12 4.9v.1c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2h.1A6.979 6.979 0 0 0 10 7zm-6 5a8 8 0 0 0 15.062 3.762A9 9 0 0 1 8.238 4.938 7.999 7.999 0 0 0 4 12z"/></symbol><symbol id="light-theme-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 18a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-2a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM11 1h2v3h-2V1zm0 19h2v3h-2v-3zM3.515 4.929l1.414-1.414L7.05 5.636 5.636 7.05 3.515 4.93zM16.95 18.364l1.414-1.414 2.121 2.121-1.414 1.414-2.121-2.121zm2.121-14.85l1.414 1.415-2.121 2.121-1.414-1.414 2.121-2.121zM5.636 16.95l1.414 1.414-2.121 2.121-1.414-1.414 2.121-2.121zM23 11v2h-3v-2h3zM4 11v2H1v-2h3z"/></symbol><symbol id="reset-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M18.537 19.567A9.961 9.961 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10c0 2.136-.67 4.116-1.81 5.74L17 12h3a8 8 0 1 0-2.46 5.772l.997 1.795z"/></symbol><symbol id="down-icon" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M12.7803 6.21967C13.0732 6.51256 13.0732 6.98744 12.7803 7.28033L8.53033 11.5303C8.23744 11.8232 7.76256 11.8232 7.46967 11.5303L3.21967 7.28033C2.92678 6.98744 2.92678 6.51256 3.21967 6.21967C3.51256 5.92678 3.98744 5.92678 4.28033 6.21967L8 9.93934L11.7197 6.21967C12.0126 5.92678 12.4874 5.92678 12.7803 6.21967Z"></path></symbol><symbol id="codepen-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M16.5 13.202L13 15.535v3.596L19.197 15 16.5 13.202zM14.697 12L12 10.202 9.303 12 12 13.798 14.697 12zM20 10.869L18.303 12 20 13.131V10.87zM19.197 9L13 4.869v3.596l3.5 2.333L19.197 9zM7.5 10.798L11 8.465V4.869L4.803 9 7.5 10.798zM4.803 15L11 19.131v-3.596l-3.5-2.333L4.803 15zM4 13.131L5.697 12 4 10.869v2.262zM2 9a1 1 0 0 1 .445-.832l9-6a1 1 0 0 1 1.11 0l9 6A1 1 0 0 1 22 9v6a1 1 0 0 1-.445.832l-9 6a1 1 0 0 1-1.11 0l-9-6A1 1 0 0 1 2 15V9z"/></symbol><symbol id="close-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z"/></symbol><symbol id="menu-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M3 4h18v2H3V4zm0 7h18v2H3v-2zm0 7h18v2H3v-2z"/></symbol></defs></svg></head><body data-theme="dark"><div class="sidebar-container"><div class="sidebar" id="sidebar"><a href="/" class="sidebar-title sidebar-title-anchor">sidechat.js</a><div class="sidebar-items-container"><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-tutorials"><div>Tutorials</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="tutorial-Authentication.html">Authentication</a></div><div class="sidebar-section-children"><a href="tutorial-Email Registration.html">Email Registration</a></div><div class="sidebar-section-children"><a href="tutorial-Feeds and Pagination.html">Feeds and Pagination</a></div><div class="sidebar-section-children"><a href="tutorial-Posting and Media.html">Posting and Media</a></div></div><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-classes"><div>Classes</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="SidechatAPIClient.html">SidechatAPIClient</a></div><div class="sidebar-section-children"><a href="SidechatAPIError.html">SidechatAPIError</a></div><div class="sidebar-section-children"><a href="SidechatColorList.html">SidechatColorList</a></div></div><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-global"><div>Global</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="global.html#SidechatActivityItem">SidechatActivityItem</a></div><div class="sidebar-section-children"><a href="global.html#SidechatActivityList">SidechatActivityList</a></div><div class="sidebar-section-children"><a href="global.html#SidechatAssetMetadata">SidechatAssetMetadata</a></div><div class="sidebar-section-children"><a href="global.html#SidechatAttachment">SidechatAttachment</a></div><div class="sidebar-section-children"><a href="global.html#SidechatAuthToken">SidechatAuthToken</a></div><div class="sidebar-section-children"><a href="global.html#SidechatColor">SidechatColor</a></div><div class="sidebar-section-children"><a href="global.html#SidechatCurrentUser">SidechatCurrentUser</a></div><div class="sidebar-section-children"><a href="global.html#SidechatCursorString">SidechatCursorString</a></div><div class="sidebar-section-children"><a href="global.html#SidechatDirectMessage">SidechatDirectMessage</a></div><div class="sidebar-section-children"><a href="global.html#SidechatDirectThread">SidechatDirectThread</a></div><div class="sidebar-section-children"><a href="global.html#SidechatFeed">SidechatFeed</a></div><div class="sidebar-section-children"><a href="global.html#SidechatFeedItem">SidechatFeedItem</a></div><div class="sidebar-section-children"><a href="global.html#SidechatGroup">SidechatGroup</a></div><div class="sidebar-section-children"><a href="global.html#SidechatIcon">SidechatIcon</a></div><div class="sidebar-section-children"><a href="global.html#SidechatIdentity">SidechatIdentity</a></div><div class="sidebar-section-children"><a href="global.html#SidechatKarma">SidechatKarma</a></div><div class="sidebar-section-children"><a href="global.html#SidechatLeaderboard">SidechatLeaderboard</a></div><div class="sidebar-section-children"><a href="global.html#SidechatLibraryAsset">SidechatLibraryAsset</a></div><div class="sidebar-section-children"><a href="global.html#SidechatMembership">SidechatMembership</a></div><div class="sidebar-section-children"><a href="global.html#SidechatNotificationPreferences">SidechatNotificationPreferences</a></div><div class="sidebar-section-children"><a href="global.html#SidechatPoll">SidechatPoll</a></div><div class="sidebar-section-children"><a href="global.html#SidechatPollChoice">SidechatPollChoice</a></div><div class="sidebar-section-children"><a href="global.html#SidechatPostOrComment">SidechatPostOrComment</a></div><div class="sidebar-section-children"><a href="global.html#SidechatPostsAndCursor">SidechatPostsAndCursor</a></div><div class="sidebar-section-children"><a href="global.html#SidechatPrivateAssetURL">SidechatPrivateAssetURL</a></div><div class="sidebar-section-children"><a href="global.html#SidechatProfile">SidechatProfile</a></div><div class="sidebar-section-children"><a href="global.html#SidechatReportReason">SidechatReportReason</a></div><div class="sidebar-section-children"><a href="global.html#SidechatSimpleAsset">SidechatSimpleAsset</a></div><div class="sidebar-section-children"><a href="global.html#SidechatVoteString">SidechatVoteString</a></div></div></div></div></div><div class="navbar-container" id="VuAckcnZhf"><nav class="navbar"><div class="navbar-left-items"><div class="navbar-item"><a id="" href="https://github.com/micahlt/sidechat.js" target="_blank">GitHub</a></div><div class="navbar-item"><a id="" href="https://www.npmjs.com/package/sidechat.js" target="_blank">NPM</a></div><div class="navbar-item"><a id="" href="https://offsides.micahlindley.com" target="_blank">Offsides (Sidechat for Android)</a></div></div><div class="navbar-right-items"><div class="navbar-right-item"><button class="icon-button search-button" aria-label="open-search"><svg><use xlink:href="#search-icon"></use></svg></button></div><div class="navbar-right-item"><button class="icon-button theme-toggle" aria-label="toggle-theme"><svg><use class="theme-svg-use" xlink:href="#light-theme-icon"></use></svg></button></div><div class="navbar-right-item"><button class="icon-button font-size" aria-label="change-font-size"><svg><use xlink:href="#font-size-icon"></use></svg></button></div></div><nav></nav></nav></div><div class="toc-container"><div class="toc-content"><span class="bold">On this page</span><div id="eed4d2a0bfd64539bb9df78095dec881"></div></div></div><div class="body-wrapper"><div class="main-content"><div class="main-wrapper"><section id="source-page" class="source-page"><header><h1 id="title" class="has-anchor">classes_SidechatAPIClient.js</h1></header><article><pre class="prettyprint source lang-js"><code>import SidechatAPIError from "../classes/SidechatAPIError.js";
4
+
5
+ /** @import * as types from "../types/SidechatTypes.js" */
6
+
7
+ /**
8
+ * API client class for making requests to Sidechat's private API. You'll need to [authenticate]{@tutorial Authentication} before using most of the methods.
9
+ * @class
10
+ * @since 2.0.0-alpha.0
11
+ */
12
+ class SidechatAPIClient {
13
+ /**
14
+ * User bearer token
15
+ * @type {types.SidechatAuthToken}
16
+ * */
17
+ userToken;
18
+
19
+ /**
20
+ * Default headers for every API request
21
+ * @type {Object}
22
+ * @static
23
+ * @constant
24
+ */
25
+ defaultHeaders = {
26
+ Accept: "application/json",
27
+ "Content-Type": "application/json",
28
+ "App-Version": "6.0.0",
29
+ Dnt: 1,
30
+ };
31
+
32
+ /**
33
+ * Root URL for every API request
34
+ * @type {String}
35
+ * @default "https://api.sidechat.lol"
36
+ */
37
+ apiRoot = "https://api.sidechat.lol";
38
+
39
+ /**
40
+ * Create a new instance of the API client
41
+ * @param {types.SidechatAuthToken} [token] - user bearer token
42
+ * @param {String} rootUrl - custom API root URL for mocking or using other server
43
+ */
44
+ constructor(token = "", rootUrl = "") {
45
+ if (token) {
46
+ this.userToken = token;
47
+ }
48
+ if (rootUrl) {
49
+ this.apiRoot = rootUrl;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Manually set the currently signed in user's token. Generally try to avoid this and instead either pass a token to the constructor or login automatically through the auth functions
55
+ * @method
56
+ * @param {types.SidechatAuthToken} token - user bearer token
57
+ */
58
+ setToken = (token) => {
59
+ this.userToken = token;
60
+ };
61
+
62
+ /**
63
+ * Manually set the root URL for all API requests. This can be used for mocking requests or redirecting them to a different server
64
+ * @method
65
+ * @param {String} url - new root URL to set
66
+ * @since 2.3.9
67
+ */
68
+ setAPIRoot = (url) => {
69
+ this.apiRoot = url;
70
+ };
71
+
72
+ /**
73
+ * Set an identifier for this device, which will be sent with every request in the `X-Device-ID` header. Should be stable across sessions on the same device.
74
+ * @method
75
+ * @param {String} deviceIdentifier - stable identifier for this device
76
+ * @since 2.7.0
77
+ */
78
+ setDeviceIdentifier = (deviceIdentifier) => {
79
+ this.defaultHeaders["X-Device-ID"] = deviceIdentifier;
80
+ };
81
+
82
+ /**
83
+ * Run an arbitrary API request using the current client's authentication
84
+ * @method
85
+ * @param {String} endpoint - API endpoint to request (e.g. "/v1/posts")
86
+ * @param {"GET"|"POST"|"PUT"|"DELETE"|"PATCH"|"OPTIONS"} [method] - HTTP method to use
87
+ * @param {Object} [body] - body to send with the request
88
+ * @param {Object} [headers] - custom headers to send with the request
89
+ * @param {Boolean} [stripHeaders] - remove the default headers from the request
90
+ * @since 2.4.9
91
+ */
92
+ sendRequest = (
93
+ endpoint,
94
+ method = "GET",
95
+ body = undefined,
96
+ headers = {},
97
+ stripHeaders = false,
98
+ ) => {
99
+ let requestHeaders = this.defaultHeaders;
100
+ if (stripHeaders) {
101
+ headers = {};
102
+ }
103
+ requestHeaders = { ...requestHeaders, ...headers };
104
+ return fetch(`${this.apiRoot}${endpoint}`, {
105
+ headers: { Authorization: `Bearer ${this.userToken}`, ...requestHeaders },
106
+ body: body,
107
+ method: method,
108
+ });
109
+ };
110
+
111
+ /**
112
+ * Initiate the login process with a phone number. Should be followed up with verifySMSCode(). If the response has an `error_code` of `"captcha_required"`, the server wants a captcha solved before it will send a code, and the request should be repeated with a captcha response. I haven't been able to recreate this yet, so I honestly don't know what captcha provider they want.
113
+ * @method
114
+ * @since 1.0.0
115
+ * @param {Number} phoneNumber - US phone number (WITHOUT +1) to send verification code to
116
+ * @param {Boolean} [useFallback] - send the code through the backup SMS route, which the official app offers when a code doesn't arrive
117
+ * @param {String} [captchaMethod] - type of captcha being answered
118
+ * @param {String} [captchaResponse] - solved captcha token
119
+ */
120
+ loginViaSMS = async (
121
+ phoneNumber,
122
+ useFallback = false,
123
+ captchaMethod = undefined,
124
+ captchaResponse = undefined,
125
+ ) => {
126
+ try {
127
+ const endpoint = useFallback
128
+ ? "/v1/login_register_sms_fallback"
129
+ : "/v1/login_register";
130
+ const res = await fetch(`${this.apiRoot}${endpoint}`, {
131
+ method: "POST",
132
+ headers: this.defaultHeaders,
133
+ body: JSON.stringify({
134
+ phone_number: `+1${phoneNumber}`,
135
+ version: 3,
136
+ captcha_method: captchaMethod,
137
+ captcha_response: captchaResponse,
138
+ }),
139
+ });
140
+ const json = await res.json();
141
+ return json;
142
+ } catch (err) {
143
+ console.error(err);
144
+ throw new SidechatAPIError("Failed to request SMS verification.");
145
+ }
146
+ };
147
+
148
+ /**
149
+ * Verify the code sent via SMS with loginViaSMS(). If this function succeeds, the user will be authenticated for future requests.
150
+ * @method
151
+ * @since 1.0.0
152
+ * @param {Number} phoneNumber - US phone number (WITHOUT +1) that verification code was sent to
153
+ * @param {String} code - the verification code
154
+ */
155
+ verifySMSCode = async (phoneNumber, code) => {
156
+ try {
157
+ const res = await fetch(`${this.apiRoot}/v1/verify_phone_number`, {
158
+ method: "POST",
159
+ headers: this.defaultHeaders,
160
+ body: JSON.stringify({
161
+ phone_number: `+1${phoneNumber}`,
162
+ code: code.toUpperCase(),
163
+ }),
164
+ });
165
+ const json = await res.json();
166
+ if (json?.logged_in_user?.token) {
167
+ this.userToken = json.logged_in_user.token;
168
+ }
169
+ return json;
170
+ } catch (err) {
171
+ console.error(err);
172
+ throw new SidechatAPIError("Failed verify this code.");
173
+ }
174
+ };
175
+
176
+ /**
177
+ * Set the user's age. If this function succeeds, the user will be authenticated for future requests.
178
+ * @method
179
+ * @since 1.0.0
180
+ * @param {Number} age - user's age in years
181
+ * @param {String} registrationID - the registration ID generated by verifySMSCode()
182
+ * @param {String} [timeZone] - time zone of the user (e.g. "America/Chicago"). Falls back to the system time zone.
183
+ * @param {String} [inviteLinkURL] - group invite link that the user signed up through
184
+ */
185
+ setAge = async (
186
+ age,
187
+ registrationID,
188
+ timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone,
189
+ inviteLinkURL = undefined,
190
+ ) => {
191
+ if (age &lt; 13) {
192
+ throw new SidechatAPIError("You're too young to use Offsides.");
193
+ }
194
+ try {
195
+ const res = await fetch(`${this.apiRoot}/v1/complete_registration`, {
196
+ method: "POST",
197
+ headers: this.defaultHeaders,
198
+ body: JSON.stringify({
199
+ age: Number(age),
200
+ registration_id: registrationID,
201
+ time_zone: timeZone,
202
+ invite_link_url: inviteLinkURL,
203
+ device_id: this.defaultHeaders["X-Device-ID"],
204
+ }),
205
+ });
206
+ const json = await res.json();
207
+ if (json.token) {
208
+ this.userToken = json.token;
209
+ }
210
+ return json;
211
+ } catch (err) {
212
+ console.error(err);
213
+ throw new SidechatAPIError("Failed verify this code.");
214
+ }
215
+ };
216
+
217
+ /**
218
+ * Initiate the email setup process. Should be followed up with checkEmailVerification().
219
+ * @method
220
+ * @since 1.0.0
221
+ * @param {String} email - school email address to send verification code to
222
+ * @tutorial Email Registration
223
+ */
224
+ registerEmail = async (email) => {
225
+ if (!this.userToken) {
226
+ throw new SidechatAPIError("User is not authenticated.");
227
+ }
228
+ try {
229
+ const res = await fetch(`${this.apiRoot}/v2/users/register_email`, {
230
+ method: "POST",
231
+ headers: {
232
+ ...this.defaultHeaders,
233
+ Authorization: `Bearer ${this.userToken}`,
234
+ },
235
+ body: JSON.stringify({
236
+ email: email,
237
+ }),
238
+ });
239
+ const json = await res.json();
240
+ if (json.message) {
241
+ throw new SidechatAPIError(json.message);
242
+ }
243
+ return json;
244
+ } catch (err) {
245
+ console.error(err);
246
+ throw new SidechatAPIError("Failed to request email verification.");
247
+ }
248
+ };
249
+
250
+ /**
251
+ * Check is the user's email is verified.
252
+ * @method
253
+ * @since 1.0.0
254
+ */
255
+ checkEmailVerification = async () => {
256
+ if (!this.userToken) {
257
+ throw new SidechatAPIError("User is not authenticated.");
258
+ }
259
+ try {
260
+ const res = await fetch(`${this.apiRoot}/v1/users/check_email_verified`, {
261
+ method: "GET",
262
+ headers: {
263
+ ...this.defaultHeaders,
264
+ Authorization: `Bearer ${this.userToken}`,
265
+ },
266
+ });
267
+ const json = await res.json();
268
+ if (json.verified_email_updates_response) {
269
+ return json.verified_email_updates_response;
270
+ } else if (json.changing_phone_number_verified_email_user) {
271
+ return json.changing_phone_number_verified_email_user;
272
+ } else {
273
+ throw new SidechatAPIError(json?.message || "Email is not verified.");
274
+ }
275
+ } catch (err) {
276
+ console.error(err);
277
+ throw new SidechatAPIError("Email is not verified.");
278
+ }
279
+ };
280
+
281
+ /**
282
+ * Set the device ID of the current user
283
+ * @method
284
+ * @since 1.0.0
285
+ * @param {String} deviceId - the device ID to set
286
+ * @param {"com.flowerave.sidechat"|"com.flowerave.yikyak"} [bundleID] - bundle ID of the app the push token belongs to
287
+ */
288
+ setDeviceID = async (deviceID, bundleID = "com.flowerave.sidechat") => {
289
+ if (!this.userToken) {
290
+ throw new SidechatAPIError("User is not authenticated.");
291
+ }
292
+ try {
293
+ const res = await fetch(`${this.apiRoot}/v1/register_device_token`, {
294
+ method: "POST",
295
+ headers: {
296
+ ...this.defaultHeaders,
297
+ Authorization: `Bearer ${this.userToken}`,
298
+ },
299
+ body: JSON.stringify({
300
+ build_type: "release",
301
+ bundle_id: bundleID,
302
+ device_token: deviceID,
303
+ }),
304
+ });
305
+ const json = await res.json();
306
+ return json;
307
+ } catch (err) {
308
+ console.error(err);
309
+ throw new SidechatAPIError("Failed verify this code.");
310
+ }
311
+ };
312
+
313
+ /**
314
+ * Get updated status for user and group
315
+ * @method
316
+ * @since 1.0.0
317
+ * @deprecated since 2.1.0, will be removed by 3.0.0. Please use `getUpdates` instead!
318
+ * @param {String} [groupID] - ID of a specific group to retrieve info from. Falls back to user's primary group.
319
+ */
320
+ getUserAndGroup = async (groupID = "") => {
321
+ const json = await this.getUpdates(groupID);
322
+ return json;
323
+ };
324
+
325
+ /**
326
+ * Get updated status for user and group
327
+ * @method
328
+ * @since 2.1.0
329
+ * @param {String} [groupID] - ID of a specific group to retrieve info from. Falls back to user's primary group.
330
+ */
331
+ getUpdates = async (groupID = "") => {
332
+ if (!this.userToken) {
333
+ throw new SidechatAPIError("User is not authenticated.");
334
+ }
335
+ try {
336
+ const res = await fetch(
337
+ `${this.apiRoot}/v1/updates?group_id=${groupID}`,
338
+ {
339
+ method: "GET",
340
+ headers: {
341
+ ...this.defaultHeaders,
342
+ Authorization: `Bearer ${this.userToken}`,
343
+ },
344
+ },
345
+ );
346
+ const json = await res.json();
347
+ return json;
348
+ } catch (err) {
349
+ console.error(err);
350
+ throw new SidechatAPIError(`Failed to get posts from group.`);
351
+ }
352
+ };
353
+
354
+ /**
355
+ * Fetches posts from the specified category in a group
356
+ * @method
357
+ * @since 1.0.0
358
+ * @param {String} groupID - group ID
359
+ * @param {"hot"|"recent"|"top"} category - category to filter posts
360
+ * @param {types.SidechatCursorString} [cursor] - cursor string
361
+ * @param {"day"|"week"|"month"|"year"|"all_time"} [period] - time period to pull top posts from (only if category is "top")
362
+ * @returns {Promise&lt;types.SidechatPostsAndCursor>} List of posts and cursor
363
+ */
364
+ getGroupPosts = async (groupID, category = "hot", cursor, period) => {
365
+ if (!this.userToken) {
366
+ throw new SidechatAPIError("User is not authenticated.");
367
+ }
368
+ try {
369
+ const res = await fetch(
370
+ `${this.apiRoot}/v1/posts?${
371
+ cursor ? "cursor=" + encodeURIComponent(cursor) + "&amp;" : ""
372
+ }${
373
+ period ? "period=" + period + "&amp;" : ""
374
+ }group_id=${groupID}&amp;type=${category}&amp;cacheBust=${Date.now()}`,
375
+ {
376
+ method: "GET",
377
+ headers: {
378
+ ...this.defaultHeaders,
379
+ Authorization: `Bearer ${this.userToken}`,
380
+ },
381
+ },
382
+ );
383
+ const json = await res.json();
384
+ return json;
385
+ } catch (err) {
386
+ console.error(err);
387
+ throw new SidechatAPIError(`Failed to get posts from group.`);
388
+ }
389
+ };
390
+
391
+ /**
392
+ * Upvote or downvote, or unvote a post
393
+ * @method
394
+ * @since 2.0.0-alpha.0
395
+ * @param {String} postID - post ID to vote on
396
+ * @param {types.SidechatVoteString} action - whether to upvote, downvote, or reset vote
397
+ */
398
+ setVote = async (postID, action) => {
399
+ if (!this.userToken) {
400
+ throw new SidechatAPIError("User is not authenticated.");
401
+ }
402
+ try {
403
+ const res = await fetch(`${this.apiRoot}/v1/posts/set_vote`, {
404
+ method: "POST",
405
+ headers: {
406
+ ...this.defaultHeaders,
407
+ Authorization: `Bearer ${this.userToken}`,
408
+ },
409
+ body: JSON.stringify({
410
+ post_id: postID,
411
+ vote_status: action,
412
+ }),
413
+ });
414
+ const json = await res.json();
415
+ return json;
416
+ } catch (err) {
417
+ console.error(err);
418
+ throw new SidechatAPIError(`Failed to change the vote on post.`);
419
+ }
420
+ };
421
+
422
+ /**
423
+ * Fetches a single post with just its ID
424
+ * @method
425
+ * @since 2.3.0
426
+ * @param {String} postID - ID of post to fetch
427
+ * @param {Boolean} includeDeleted - undocumented
428
+ * @returns {Promise&lt;types.SidechatPostOrComment>} post object
429
+ */
430
+ getPost = async (postID, includeDeleted = false) => {
431
+ if (!this.userToken) {
432
+ throw new SidechatAPIError("User is not authenticated.");
433
+ }
434
+ try {
435
+ const res = await fetch(
436
+ `${this.apiRoot}/v1/posts/get?include_deleted=${includeDeleted}&amp;post_id=${postID}&amp;cacheBust=${Date.now()}`,
437
+ {
438
+ method: "GET",
439
+ headers: {
440
+ ...this.defaultHeaders,
441
+ Authorization: `Bearer ${this.userToken}`,
442
+ },
443
+ },
444
+ );
445
+ const json = await res.json();
446
+ return json.post;
447
+ } catch (err) {
448
+ console.error(err);
449
+ throw new SidechatAPIError(`Failed to get post from ID.`);
450
+ }
451
+ };
452
+
453
+ /**
454
+ * Fetches the posts or comments that the user has created
455
+ * @method
456
+ * @since 2.3.5
457
+ * @param {"posts"|"comments"} contentType - type of user content to fetch
458
+ * @returns {Promise&lt;types.SidechatPostOrComment[]>} post object
459
+ */
460
+ getUserContent = async (contentType) => {
461
+ if (!this.userToken) {
462
+ throw new SidechatAPIError("User is not authenticated.");
463
+ }
464
+ if (contentType == "posts") {
465
+ contentType = "my_posts";
466
+ } else if (contentType == "comments") {
467
+ contentType = "my_comments";
468
+ }
469
+ try {
470
+ const res = await fetch(`${this.apiRoot}/v1/posts?type=${contentType}`, {
471
+ method: "GET",
472
+ headers: {
473
+ ...this.defaultHeaders,
474
+ Authorization: `Bearer ${this.userToken}`,
475
+ },
476
+ });
477
+ const json = await res.json();
478
+ return json.posts;
479
+ } catch (err) {
480
+ console.error(err);
481
+ throw new SidechatAPIError(`Failed to get content from user.`);
482
+ }
483
+ };
484
+
485
+ /**
486
+ * Get all the commments on a post
487
+ * @method
488
+ * @since 2.0.0-alpha.0
489
+ * @param {String} postID - post ID to get comments for
490
+ * @returns {Promise&lt;types.SidechatPostOrComment[]>} list of comments
491
+ */
492
+ getPostComments = async (postID) => {
493
+ if (!this.userToken) {
494
+ throw new SidechatAPIError("User is not authenticated.");
495
+ }
496
+ try {
497
+ const res = await fetch(
498
+ `${this.apiRoot}/v1/posts/comments/?post_id=${postID}&amp;cacheBust=${Date.now()}`,
499
+ {
500
+ method: "GET",
501
+ headers: {
502
+ ...this.defaultHeaders,
503
+ Authorization: `Bearer ${this.userToken}`,
504
+ },
505
+ },
506
+ );
507
+ const json = await res.json();
508
+ // Function to preprocess the comments and organize them into a nested structure
509
+ function preprocessComments(apiComments) {
510
+ // Map to store comments by their IDs for efficient lookup
511
+ const commentMap = new Map();
512
+ // List to store top-level comments
513
+ const topLevelComments = [];
514
+
515
+ // Iterate through the API comments
516
+ apiComments.forEach((comment) => {
517
+ // Store the comment in the map with its ID as the key
518
+ commentMap.set(comment.id, comment);
519
+ // Get the parent comment using the reply_post_id
520
+ const parentComment = commentMap.get(comment.reply_post_id);
521
+ // Check if the comment is a top-level comment
522
+ if (
523
+ !parentComment ||
524
+ comment.reply_post_id === comment.parent_post_id
525
+ ) {
526
+ // If it's a top-level comment, push it to the topLevelComments array
527
+ topLevelComments.push(comment);
528
+ } else {
529
+ // If it's a reply, add it to the parent comment's replies array
530
+ if (!parentComment.replies) parentComment.replies = [];
531
+ parentComment.replies.push(comment);
532
+ }
533
+ });
534
+
535
+ // Flatten the nested structure and return a single list of comments
536
+ return flattenComments(topLevelComments);
537
+ }
538
+
539
+ // Function to flatten nested comments into a single list
540
+ function flattenComments(comments) {
541
+ // Use reduce to flatten the nested comments array into a single list
542
+ return comments.reduce((flatComments, comment) => {
543
+ // Push the current comment to the flatComments array
544
+ flatComments.push(comment);
545
+ // If the current comment has replies, recursively flatten them and push to the flatComments array
546
+ if (comment.replies)
547
+ flatComments.push(...flattenComments(comment.replies));
548
+ // Return the flatComments array
549
+ return flatComments;
550
+ }, []);
551
+ }
552
+
553
+ const sortedComments = preprocessComments(json.posts);
554
+ return sortedComments;
555
+ } catch (err) {
556
+ console.error(err);
557
+ throw new SidechatAPIError(`Failed to get comments on post.`);
558
+ }
559
+ };
560
+
561
+ /**
562
+ * Gets groups to be displayed on the "Explore Groups" page
563
+ * @method
564
+ * @since 2.0.0-alpha.0
565
+ * @param {Boolean} onePage - whether or not results should be returned as a single page
566
+ * @returns {Promise&lt;types.SidechatGroup[]>}
567
+ */
568
+ getAvailableGroups = async (onePage = true) => {
569
+ if (!this.userToken) {
570
+ throw new SidechatAPIError("User is not authenticated.");
571
+ }
572
+ try {
573
+ const res = await fetch(`${this.apiRoot}/v1/groups/explore`, {
574
+ method: "GET",
575
+ headers: {
576
+ ...this.defaultHeaders,
577
+ "App-Version": onePage ? "0" : this.defaultHeaders["App-Version"],
578
+ Authorization: `Bearer ${this.userToken}`,
579
+ },
580
+ });
581
+ const json = await res.json();
582
+ return await json.groups;
583
+ } catch (err) {
584
+ console.error(err);
585
+ throw new SidechatAPIError(`Failed to get groups from explore.`);
586
+ }
587
+ };
588
+
589
+ /**
590
+ * Searches for new groups based on a query keyword
591
+ * @method
592
+ * @since 2.6.0
593
+ * @param {String} query - the string to search for. This will be encoded, so strings with spaces and special characters are okay.
594
+ * @returns {Promise&lt;types.SidechatGroup[]>}
595
+ */
596
+ searchAvailableGroups = async (query) => {
597
+ if (!this.userToken) {
598
+ throw new SidechatAPIError("User is not authenticated.");
599
+ }
600
+ try {
601
+ const res = await fetch(
602
+ `${this.apiRoot}/v1/groups/explore/search?term=${encodeURIComponent(query)}`,
603
+ {
604
+ method: "GET",
605
+ headers: {
606
+ ...this.defaultHeaders,
607
+ Authorization: `Bearer ${this.userToken}`,
608
+ },
609
+ },
610
+ );
611
+ const json = await res.json();
612
+ return await json.results;
613
+ } catch (err) {
614
+ console.error(err);
615
+ throw new SidechatAPIError(`Failed to search groups.`);
616
+ }
617
+ };
618
+
619
+ /**
620
+ * Retrieves the entire accessible asset library. Be warned that as of the time of this documentation, it's a 1.5MB JSON download and this request is very expensive.
621
+ * @method
622
+ * @since 2.0.6
623
+ * @returns {Promise&lt;types.SidechatLibraryAsset[]>}
624
+ */
625
+ getAssetLibrary = async () => {
626
+ if (!this.userToken) {
627
+ throw new SidechatAPIError("User is not authenticated.");
628
+ }
629
+ try {
630
+ const res = await fetch(`${this.apiRoot}/v1/assets/library`, {
631
+ method: "GET",
632
+ headers: {
633
+ ...this.defaultHeaders,
634
+ Authorization: `Bearer ${this.userToken}`,
635
+ },
636
+ });
637
+ const json = await res.json();
638
+ return await json.items;
639
+ } catch (err) {
640
+ console.error(err);
641
+ throw new SidechatAPIError(`Failed to get asset library.`);
642
+ }
643
+ };
644
+
645
+ /**
646
+ * Gets the current authenticated user and a list of the groups of which they are members.
647
+ * @method
648
+ * @since 2.1.0
649
+ * @returns {Promise&lt;types.SidechatCurrentUser>}
650
+ */
651
+ getCurrentUser = async () => {
652
+ if (!this.userToken) {
653
+ throw new SidechatAPIError("User is not authenticated.");
654
+ }
655
+ try {
656
+ const res = await fetch(`${this.apiRoot}/v1/users/me`, {
657
+ method: "GET",
658
+ headers: {
659
+ ...this.defaultHeaders,
660
+ Authorization: `Bearer ${this.userToken}`,
661
+ },
662
+ });
663
+ const json = await res.json();
664
+ return await json;
665
+ } catch (err) {
666
+ console.error(err);
667
+ throw new SidechatAPIError(`Failed to get asset library.`);
668
+ }
669
+ };
670
+
671
+ /**
672
+ * Gets the metadata of a group from its ID
673
+ * @method
674
+ * @since 2.1.0
675
+ * @param {String} [groupID] - alphanumeric ID of a group to get. Falls back to user's primary group.
676
+ * @returns {Promise&lt;types.SidechatGroup>}
677
+ */
678
+ getGroupMetadata = async (groupID = "") => {
679
+ if (!this.userToken) {
680
+ throw new SidechatAPIError("User is not authenticated.");
681
+ }
682
+ try {
683
+ const res = await fetch(`${this.apiRoot}/v1/groups/${groupID}`, {
684
+ method: "GET",
685
+ headers: {
686
+ ...this.defaultHeaders,
687
+ Authorization: `Bearer ${this.userToken}`,
688
+ },
689
+ });
690
+ const json = await res.json();
691
+ return await json.group;
692
+ } catch (err) {
693
+ console.error(err);
694
+ throw new SidechatAPIError(`Failed to get group metadata.`);
695
+ }
696
+ };
697
+
698
+ /**
699
+ * Joins or leaves a group
700
+ * @method
701
+ * @param {String} groupID - alphanumeric ID of group to join or leave
702
+ * @param {Boolean} isMember - whether or not the user should be a member of the group
703
+ * @since 2.3.8
704
+ */
705
+ setGroupMembership = async (groupID, isMember) => {
706
+ if (!this.userToken) {
707
+ throw new SidechatAPIError("User is not authenticated.");
708
+ }
709
+ try {
710
+ // Joining moved to the v2 endpoint, which takes a list of IDs. Leave still sends group_id by itself, no list.
711
+ const res = await fetch(
712
+ `${this.apiRoot}${isMember ? "/v2/groups/join" : "/v1/groups/leave"}`,
713
+ {
714
+ method: "POST",
715
+ headers: {
716
+ ...this.defaultHeaders,
717
+ Authorization: `Bearer ${this.userToken}`,
718
+ },
719
+ body: JSON.stringify({
720
+ group_id: isMember ? undefined : groupID,
721
+ group_ids: [groupID],
722
+ }),
723
+ },
724
+ );
725
+ const json = await res.json();
726
+ return await json;
727
+ } catch (err) {
728
+ console.error(err);
729
+ throw new SidechatAPIError(`Failed to modify group membership.`);
730
+ }
731
+ };
732
+
733
+ /**
734
+ * Creates a comment on a post
735
+ * @method
736
+ * @since 2.2.0
737
+ * @param {String} parentPostID - alphanumeric ID of post on which this comment is made
738
+ * @param {String} text - text content of comment
739
+ * @param {String} groupID - alphanumeric ID of group in which the parent post resides
740
+ * @param {String} [replyCommentID] - alphanumeric ID of comment to reply to. Falls back to parentPostID
741
+ * @param {String} [topLevelReplyID] - alphanumeric ID of the top-level comment to reply to. Used only when replying to replies. Falls back to parentPostID
742
+ * @param {types.SidechatSimpleAsset[]} [assetList] - list of assets to attach
743
+ * @param {Boolean} [disableDMs] - prevent direct messages being sent to comment's author
744
+ * @param {Boolean} [anonymous] - whether or not to hide user's name and icon on comment
745
+ * @returns {Promise&lt;types.SidechatPostOrComment>} created comment
746
+ */
747
+ createComment = async (
748
+ parentPostID,
749
+ text,
750
+ groupID,
751
+ replyCommentID,
752
+ topLevelReplyID,
753
+ assetList = [],
754
+ disableDMs = false,
755
+ anonymous = false,
756
+ ) => {
757
+ if (!this.userToken) {
758
+ throw new SidechatAPIError("User is not authenticated.");
759
+ }
760
+ try {
761
+ const res = await fetch(`${this.apiRoot}/v1/posts`, {
762
+ method: "POST",
763
+ headers: {
764
+ ...this.defaultHeaders,
765
+ Authorization: `Bearer ${this.userToken}`,
766
+ },
767
+ body: JSON.stringify({
768
+ type: "comment",
769
+ assets: assetList,
770
+ group_ids: [groupID],
771
+ text: text,
772
+ reply_post_id: topLevelReplyID || replyCommentID || parentPostID,
773
+ reply_comment_post_id: replyCommentID || parentPostID,
774
+ parent_post_id: parentPostID,
775
+ dms_disabled: disableDMs,
776
+ using_identity: !anonymous,
777
+ }),
778
+ });
779
+ const json = await res.json();
780
+ if (!res.ok) throw new SidechatAPIError(json.message);
781
+ return await json.comment;
782
+ } catch (err) {
783
+ console.error(err);
784
+ if (err instanceof SidechatAPIError &amp;&amp; err.message) throw err;
785
+ throw new SidechatAPIError(`Failed to post comment.`);
786
+ }
787
+ };
788
+
789
+ /**
790
+ * Creates a new post in the specified group. Users can only make two posts a minute; if a post is rejected, the thrown error contains the reason from the server.
791
+ * @method
792
+ * @since 2.2.0
793
+ * @param {String} text - text content of comment
794
+ * @param {String} groupID - alphanumeric ID of group in which the parent post resides
795
+ * @param {types.SidechatSimpleAsset[]} [assetList] - list of assets to attach.
796
+ * @param {Boolean} [disableDMs] - prevent direct messages from being sent to post's author
797
+ * @param {Boolean} [disableComments] - whether or not comments should be disabled on post
798
+ * @param {Boolean} [anonymous] - whether or not to hide user's name and icon on post
799
+ * @param {Array&lt;String>} [pollOptions] - List of poll options. If provided, a poll will be created with these options.
800
+ * @param {Array&lt;String>} [attachmentIDs] - IDs of link attachments created with createLinkAttachment()
801
+ * @returns {Promise&lt;types.SidechatPostOrComment>} the created post
802
+ */
803
+ createPost = async (
804
+ text,
805
+ groupID,
806
+ assetList = [],
807
+ disableDMs = false,
808
+ disableComments = false,
809
+ anonymous = false,
810
+ repostId = undefined,
811
+ pollOptions = undefined,
812
+ attachmentIDs = [],
813
+ ) => {
814
+ if (!this.userToken) {
815
+ throw new SidechatAPIError("User is not authenticated.");
816
+ }
817
+ try {
818
+ // Build the request body
819
+ const body = {
820
+ type: "post",
821
+ assets: assetList,
822
+ group_ids: [groupID],
823
+ text: text,
824
+ attachments: attachmentIDs,
825
+ dms_disabled: disableDMs,
826
+ comments_disabled: disableComments,
827
+ using_identity: !anonymous,
828
+ quote_post_id: repostId,
829
+ };
830
+ // If pollOptions is provided and is a non-empty array, add poll_request
831
+ if (Array.isArray(pollOptions) &amp;&amp; pollOptions.length > 0) {
832
+ body.poll_request = {
833
+ allows_view_results: true,
834
+ choices: pollOptions,
835
+ };
836
+ }
837
+ const res = await fetch(`${this.apiRoot}/v1/posts`, {
838
+ method: "POST",
839
+ headers: {
840
+ ...this.defaultHeaders,
841
+ Authorization: `Bearer ${this.userToken}`,
842
+ },
843
+ body: JSON.stringify(body),
844
+ });
845
+ const json = await res.json();
846
+ if (!res.ok) throw new SidechatAPIError(json.message);
847
+ return await json.posts[0];
848
+ } catch (err) {
849
+ console.error(err);
850
+ if (err instanceof SidechatAPIError &amp;&amp; err.message) throw err;
851
+ throw new SidechatAPIError(`Failed to make post.`);
852
+ }
853
+ };
854
+
855
+ /**
856
+ * Deletes a post or comment that the user created
857
+ * @method
858
+ * @since 2.2.0
859
+ * @param {String} postOrCommentID - alphanumeric ID of post to delete
860
+ */
861
+ deletePostOrComment = async (postOrCommentID) => {
862
+ if (!this.userToken) {
863
+ throw new SidechatAPIError("User is not authenticated.");
864
+ }
865
+ try {
866
+ const res = await fetch(`${this.apiRoot}/v1/posts/delete`, {
867
+ method: "POST",
868
+ headers: {
869
+ ...this.defaultHeaders,
870
+ Authorization: `Bearer ${this.userToken}`,
871
+ },
872
+ body: JSON.stringify({
873
+ post_id: postOrCommentID,
874
+ }),
875
+ });
876
+ const json = await res.json();
877
+ return await json;
878
+ } catch (err) {
879
+ console.error(err);
880
+ throw new SidechatAPIError(`Failed to delete post.`);
881
+ }
882
+ };
883
+
884
+ /**
885
+ * Votes on a poll attached to a post
886
+ * @method
887
+ * @param {String} pollId - alphanumeric ID of poll to vote on
888
+ * @param {Number} choiceIndex - index of the choice to vote on
889
+ * @since 2.5.4
890
+ */
891
+ voteOnPoll = async (pollId, choiceIndex) => {
892
+ if (!this.userToken) {
893
+ throw new SidechatAPIError("User is not authenticated.");
894
+ }
895
+ try {
896
+ const res = await fetch(`${this.apiRoot}/v1/polls/vote`, {
897
+ method: "POST",
898
+ headers: {
899
+ ...this.defaultHeaders,
900
+ Authorization: `Bearer ${this.userToken}`,
901
+ },
902
+ body: JSON.stringify({
903
+ poll_id: pollId,
904
+ choice: choiceIndex,
905
+ }),
906
+ });
907
+ const json = await res.json();
908
+ return await json;
909
+ } catch (err) {
910
+ console.error(err);
911
+ throw new SidechatAPIError(`Failed to vote on poll`);
912
+ }
913
+ };
914
+
915
+ /**
916
+ * Marks that the user has viewed results on a poll. Note that this does not actually return the results of the poll.
917
+ * @method
918
+ * @param {String} pollId - alphanumeric ID of poll to vote on
919
+ * @since 2.5.4
920
+ */
921
+ viewPollResults = async (pollId) => {
922
+ if (!this.userToken) {
923
+ throw new SidechatAPIError("User is not authenticated.");
924
+ }
925
+ try {
926
+ const res = await fetch(
927
+ `${this.apiRoot}/v1/polls/view_results?cacheBust=${Date.now()}`,
928
+ {
929
+ method: "POST",
930
+ headers: {
931
+ ...this.defaultHeaders,
932
+ Authorization: `Bearer ${this.userToken}`,
933
+ },
934
+ body: JSON.stringify({
935
+ poll_id: pollId,
936
+ }),
937
+ },
938
+ );
939
+ const json = await res.json();
940
+ return await json;
941
+ } catch (err) {
942
+ console.error(err);
943
+ throw new SidechatAPIError(`Failed to mark poll results as viewed.`);
944
+ }
945
+ };
946
+
947
+ /**
948
+ * Uploads an asset to AWS S3 for use in posts and comments. Supports images and videos (videos must be under 50MB and are processed after upload)
949
+ * @method
950
+ * @param {String} uri - URI of the asset to upload
951
+ * @param {String} mimeType - mimetype of the asset (e.g. "image/png" or "video/mp4")
952
+ * @param {String} [name] - filename of the asset
953
+ * @param {"post"|"post_vod_source"|"chat"|"chat_icon"|"group_icon"|"profile_picture_icon"} [context] - where the asset will be used. Videos default to "post_vod_source", which is required for them to be processed into a playable stream.
954
+ * @returns {Promise&lt;String>} URL of the uploaded asset
955
+ * @since 2.5.1
956
+ */
957
+ uploadAsset = async (uri, mimeType, name = "", context = undefined) => {
958
+ if (!this.userToken) {
959
+ throw new SidechatAPIError("User is not authenticated.");
960
+ }
961
+
962
+ let imageType = mimeType.split("/")[1];
963
+ if (imageType == "quicktime") imageType = "mov";
964
+ if (!["png", "jpeg", "gif", "webp", "mp4", "mov"].includes(imageType)) {
965
+ throw new SidechatAPIError("Unsupported file format.");
966
+ }
967
+ if (!context &amp;&amp; ["mp4", "mov"].includes(imageType)) {
968
+ context = "post_vod_source";
969
+ }
970
+
971
+ const data = new FormData();
972
+ data.append("image", {
973
+ name: name,
974
+ type: mimeType,
975
+ uri: uri,
976
+ });
977
+
978
+ const urlReq = await this.sendRequest(
979
+ `/v1/assets/upload_url?content_type=${imageType}${
980
+ context ? "&amp;context=" + context : ""
981
+ }`,
982
+ );
983
+ const urlJson = await urlReq.json();
984
+
985
+ try {
986
+ const uploadReq = await fetch(urlJson.upload_url, {
987
+ body: data.getAll("image")[0],
988
+ method: "PUT",
989
+ headers: {
990
+ "Content-Type": mimeType,
991
+ },
992
+ });
993
+ if (uploadReq.status == 200) {
994
+ return `${this.apiRoot}/v1/assets/library/${urlJson.asset_id}`;
995
+ } else {
996
+ throw new SidechatAPIError(
997
+ `Couldn't upload image - error ${uploadReq.status}`,
998
+ );
999
+ }
1000
+ } catch (e) {
1001
+ throw new SidechatAPIError(e.message);
1002
+ }
1003
+ };
1004
+
1005
+ /**
1006
+ * Sets the conversation icon of a user
1007
+ * @method
1008
+ * @since 2.2.1
1009
+ * @param {String} userID - alphanumeric ID of user
1010
+ * @param {String} emoji - emoji to set as icon
1011
+ * @param {String} primaryColor - hex string (including #) of primary color
1012
+ * @param {String} secondaryColor - hex string (including #) of secondary color
1013
+ */
1014
+ setUserIcon = async (userID, emoji, primaryColor, secondaryColor) => {
1015
+ if (!this.userToken) {
1016
+ throw new SidechatAPIError("User is not authenticated.");
1017
+ }
1018
+ try {
1019
+ const res = await fetch(`${this.apiRoot}/v1/users/${userID}`, {
1020
+ method: "PATCH",
1021
+ headers: {
1022
+ ...this.defaultHeaders,
1023
+ "App-Version": "0",
1024
+ Authorization: `Bearer ${this.userToken}`,
1025
+ },
1026
+ body: JSON.stringify({
1027
+ conversation_icon: {
1028
+ emoji: emoji,
1029
+ secondary_color: secondaryColor,
1030
+ is_migrated: true,
1031
+ color: primaryColor,
1032
+ },
1033
+ }),
1034
+ });
1035
+ const json = await res.json();
1036
+ return await json;
1037
+ } catch (err) {
1038
+ console.error(err);
1039
+ throw new SidechatAPIError(`Failed to set icon.`);
1040
+ }
1041
+ };
1042
+
1043
+ /**
1044
+ * Sets the bio text of a user
1045
+ * @method
1046
+ * @since 2.5.6
1047
+ * @param {String} userID - alphanumeric ID of user
1048
+ * @param {String} bio - text to set as bio
1049
+ */
1050
+ setUserBio = async (userID, bio) => {
1051
+ if (!this.userToken) {
1052
+ throw new SidechatAPIError("User is not authenticated.");
1053
+ }
1054
+ try {
1055
+ const res = await fetch(`${this.apiRoot}/v1/users/${userID}`, {
1056
+ method: "PATCH",
1057
+ headers: {
1058
+ ...this.defaultHeaders,
1059
+ "App-Version": "5.4.22",
1060
+ Authorization: `Bearer ${this.userToken}`,
1061
+ },
1062
+ body: JSON.stringify({
1063
+ bio: bio,
1064
+ }),
1065
+ });
1066
+ const json = await res.json();
1067
+ return await json;
1068
+ } catch (err) {
1069
+ console.error(err);
1070
+ throw new SidechatAPIError(`Failed to set bio.`);
1071
+ }
1072
+ };
1073
+
1074
+ /**
1075
+ * Checks if user can set their username to a string
1076
+ * @method
1077
+ * @since 2.3.6
1078
+ * @param {String} username - string to check
1079
+ * @returns {Promise&lt;Boolean>} whether or not username is valid and unused
1080
+ */
1081
+ checkUsername = async (username) => {
1082
+ if (!this.userToken) {
1083
+ throw new SidechatAPIError("User is not authenticated.");
1084
+ }
1085
+ try {
1086
+ const res = await fetch(
1087
+ `${this.apiRoot}/v1/users/username?username=${username}`,
1088
+ {
1089
+ method: "GET",
1090
+ headers: {
1091
+ ...this.defaultHeaders,
1092
+ Authorization: `Bearer ${this.userToken}`,
1093
+ },
1094
+ },
1095
+ );
1096
+ if (res?.status == 200 || res?.status == 204) {
1097
+ return true;
1098
+ } else {
1099
+ return false;
1100
+ }
1101
+ } catch (err) {
1102
+ console.error(err);
1103
+ throw new SidechatAPIError(`Failed to check username.`);
1104
+ }
1105
+ };
1106
+
1107
+ /**
1108
+ * Changes the username of the current user
1109
+ * @method
1110
+ * @since 2.3.6
1111
+ * @param {String} userID - alphanumeric ID of user
1112
+ * @param {String} username - new username to set
1113
+ */
1114
+ setUsername = async (userID, username) => {
1115
+ if (!this.userToken) {
1116
+ throw new SidechatAPIError("User is not authenticated.");
1117
+ }
1118
+ try {
1119
+ const res = await fetch(`${this.apiRoot}/v1/users/${userID}`, {
1120
+ method: "PATCH",
1121
+ headers: {
1122
+ ...this.defaultHeaders,
1123
+ Authorization: `Bearer ${this.userToken}`,
1124
+ },
1125
+ body: JSON.stringify({
1126
+ username: username,
1127
+ }),
1128
+ });
1129
+ const json = await res.json();
1130
+ return await json.user;
1131
+ } catch (err) {
1132
+ console.error(err);
1133
+ throw new SidechatAPIError(`Failed to set icon.`);
1134
+ }
1135
+ };
1136
+
1137
+ /**
1138
+ * Fetches a public user profile
1139
+ * @method
1140
+ * @since 2.6.0
1141
+ * @param {String} username - username of the user to fetch
1142
+ * @returns {Promise&lt;types.SidechatProfile>}
1143
+ */
1144
+ getUserProfile = async (username) => {
1145
+ if (!this.userToken) {
1146
+ throw new SidechatAPIError("User is not authenticated.");
1147
+ }
1148
+ try {
1149
+ const res = await fetch(
1150
+ `${this.apiRoot}/v1/groups/username?username=${username}&amp;cacheBust=${Date.now()}`,
1151
+ {
1152
+ method: "GET",
1153
+ headers: {
1154
+ ...this.defaultHeaders,
1155
+ Authorization: `Bearer ${this.userToken}`,
1156
+ },
1157
+ },
1158
+ );
1159
+ const json = await res.json();
1160
+ return await json.group;
1161
+ } catch (err) {
1162
+ console.error(err);
1163
+ throw new SidechatAPIError(`Failed to set icon.`);
1164
+ }
1165
+ };
1166
+
1167
+ /**
1168
+ * Fetches a public user's posts
1169
+ * @method
1170
+ * @since 2.6.0
1171
+ * @param {String} username - username of the user to fetch
1172
+ * @returns {Promise&lt;types.SidechatPostOrComment[]>}
1173
+ */
1174
+ getUserPosts = async (username) => {
1175
+ if (!this.userToken) {
1176
+ throw new SidechatAPIError("User is not authenticated.");
1177
+ }
1178
+ try {
1179
+ const res = await fetch(
1180
+ `${this.apiRoot}/v1/users/posts?username=${username}`,
1181
+ {
1182
+ method: "GET",
1183
+ headers: {
1184
+ ...this.defaultHeaders,
1185
+ Authorization: `Bearer ${this.userToken}`,
1186
+ },
1187
+ },
1188
+ );
1189
+ const json = await res.json();
1190
+ return await json.posts;
1191
+ } catch (err) {
1192
+ console.error(err);
1193
+ throw new SidechatAPIError(`Failed to set icon.`);
1194
+ }
1195
+ };
1196
+
1197
+ /**
1198
+ * Marks an activity item as read
1199
+ * @method
1200
+ * @since 2.3.2
1201
+ * @param {String} activityID - alphanumeric ID of activity object
1202
+ */
1203
+ readActivity = async (activityID) => {
1204
+ if (!this.userToken) {
1205
+ throw new SidechatAPIError("User is not authenticated.");
1206
+ }
1207
+ try {
1208
+ const res = await fetch(`${this.apiRoot}/v1/activity/seen`, {
1209
+ method: "POST",
1210
+ headers: {
1211
+ ...this.defaultHeaders,
1212
+ Authorization: `Bearer ${this.userToken}`,
1213
+ },
1214
+ body: JSON.stringify({
1215
+ ids: [activityID],
1216
+ }),
1217
+ });
1218
+ const json = await res.json();
1219
+ return await json;
1220
+ } catch (err) {
1221
+ console.error(err);
1222
+ throw new SidechatAPIError(`Failed to mark activity as read.`);
1223
+ }
1224
+ };
1225
+
1226
+ /**
1227
+ * Retrieves joinable group chats
1228
+ * @method
1229
+ * @since 2.3.5
1230
+ */
1231
+ getGroupChats = async () => {
1232
+ if (!this.userToken) {
1233
+ throw new SidechatAPIError("User is not authenticated.");
1234
+ }
1235
+ try {
1236
+ const res = await fetch(
1237
+ `${this.apiRoot}/v1/chats/explore?cacheBust=${Date.now()}`,
1238
+ {
1239
+ method: "GET",
1240
+ headers: {
1241
+ ...this.defaultHeaders,
1242
+ Authorization: `Bearer ${this.userToken}`,
1243
+ },
1244
+ },
1245
+ );
1246
+ const json = await res.json();
1247
+ return await json.chats;
1248
+ } catch (err) {
1249
+ console.error(err);
1250
+ throw new SidechatAPIError(`Failed to get groupchats.`);
1251
+ }
1252
+ };
1253
+
1254
+ /**
1255
+ * Joins a group chat. To mimic the official client, use the user's display name and icon by default.
1256
+ * @method
1257
+ * @param {String} groupChatID - alphanumeric ID of group chat to join
1258
+ * @param {String} displayName - display name to use in chat
1259
+ * @param {String} emoji - emoji to use as icon
1260
+ * @param {String} primaryColor - hex string of primary color
1261
+ * @param {String} secondaryColor - hex string of secondary color
1262
+ * @since 2.3.5
1263
+ */
1264
+ joinGroupChat = async (
1265
+ groupChatID,
1266
+ displayName,
1267
+ emoji,
1268
+ primaryColor,
1269
+ secondaryColor,
1270
+ ) => {
1271
+ if (!this.userToken) {
1272
+ throw new SidechatAPIError("User is not authenticated.");
1273
+ }
1274
+ try {
1275
+ const res = await fetch(`${this.apiRoot}/v1/chats/groups/join`, {
1276
+ method: "POST",
1277
+ headers: {
1278
+ ...this.defaultHeaders,
1279
+ Authorization: `Bearer ${this.userToken}`,
1280
+ },
1281
+ body: JSON.stringify({
1282
+ chat_id: groupChatID,
1283
+ identity: {
1284
+ display_name: displayName,
1285
+ emoji: emoji,
1286
+ secondary_color: secondaryColor,
1287
+ color: primaryColor,
1288
+ },
1289
+ }),
1290
+ });
1291
+ const json = await res.json();
1292
+ return await json;
1293
+ } catch (err) {
1294
+ console.error(err);
1295
+ throw new SidechatAPIError(`Failed to join groupchat.`);
1296
+ }
1297
+ };
1298
+
1299
+ /**
1300
+ * Gets a list of the user's direct messages
1301
+ * @method
1302
+ * @param {types.SidechatCursorString} [cursor] - cursor string
1303
+ * @returns {Promise&lt;types.SidechatDirectThread[]>}
1304
+ * @since 2.4.4
1305
+ */
1306
+ getDMs = async (cursor) => {
1307
+ if (!this.userToken) {
1308
+ throw new SidechatAPIError("User is not authenticated.");
1309
+ }
1310
+ try {
1311
+ const res = await fetch(
1312
+ `${this.apiRoot}/v1/chats${
1313
+ cursor ? "?cursor=" + encodeURIComponent(cursor) : ""
1314
+ }`,
1315
+ {
1316
+ method: "GET",
1317
+ headers: {
1318
+ ...this.defaultHeaders,
1319
+ Authorization: `Bearer ${this.userToken}`,
1320
+ },
1321
+ },
1322
+ );
1323
+ const json = await res.json();
1324
+ const list = await json.chats;
1325
+ let r = [];
1326
+ list.forEach((o) => {
1327
+ r.push(o.chat);
1328
+ });
1329
+ return await r;
1330
+ } catch (err) {
1331
+ console.error(err);
1332
+ throw new SidechatAPIError(`Failed to fetch DMs.`);
1333
+ }
1334
+ };
1335
+
1336
+ /**
1337
+ * Gets a single direct message thread
1338
+ * @method
1339
+ * @param {String} id - alphanumeric ID of the chat to fetch
1340
+ * @returns {Promise&lt;types.SidechatDirectThread>}
1341
+ * @since 2.4.4
1342
+ */
1343
+ getDMThread = async (id) => {
1344
+ if (!this.userToken) {
1345
+ throw new SidechatAPIError("User is not authenticated.");
1346
+ }
1347
+ try {
1348
+ const res = await fetch(
1349
+ `${this.apiRoot}/v1/chats/messages?chat_id=${id}`,
1350
+ {
1351
+ method: "GET",
1352
+ headers: {
1353
+ ...this.defaultHeaders,
1354
+ Authorization: `Bearer ${this.userToken}`,
1355
+ },
1356
+ },
1357
+ );
1358
+ const json = await res.json();
1359
+ return await json.chat;
1360
+ } catch (err) {
1361
+ console.error(err);
1362
+ throw new SidechatAPIError(`Failed to fetch DM thread.`);
1363
+ }
1364
+ };
1365
+
1366
+ /**
1367
+ * Sends a message to an existing direct message thread - note that you must first use startDM() to start a thread.
1368
+ * @method
1369
+ * @param {String} chatID - alphanumeric ID of the chat to send to
1370
+ * @param {String} text - text content of message
1371
+ * @param {String} clientID - alphanumeric device ID
1372
+ * @param {types.SidechatAsset[]} assets - array of assets to send
1373
+ * @param {Boolean} anonymous - whether the DM should be sent anonymously
1374
+ * @since 2.4.4
1375
+ */
1376
+ sendDM = async (chatID, text, clientID, assets = [], anonymous = false) => {
1377
+ if (!this.userToken) {
1378
+ throw new SidechatAPIError("User is not authenticated.");
1379
+ }
1380
+ try {
1381
+ const res = await fetch(`${this.apiRoot}/v1/chats/send`, {
1382
+ method: "POST",
1383
+ headers: {
1384
+ ...this.defaultHeaders,
1385
+ Authorization: `Bearer ${this.userToken}`,
1386
+ },
1387
+ body: JSON.stringify({
1388
+ chat_id: chatID,
1389
+ text: text,
1390
+ client_id: clientID,
1391
+ anonymous: anonymous,
1392
+ assets: assets,
1393
+ }),
1394
+ });
1395
+ const json = await res.json();
1396
+ return await json;
1397
+ } catch (err) {
1398
+ console.error(err);
1399
+ throw new SidechatAPIError(`Failed to send message.`);
1400
+ }
1401
+ };
1402
+
1403
+ /**
1404
+ * Creates a new direct message thread
1405
+ * @method
1406
+ * @param {String} text - text content of message
1407
+ * @param {String} clientID - alphanumeric ID of devide
1408
+ * @param {String} postID - alphanumeric ID of post or comment
1409
+ * @param {Boolean} anonymous - whether the DM should be sent anonymously
1410
+ * @param {"feed"} postContext - context of post (mostly undocumented, defaults to "feed")
1411
+ * @since 2.4.4
1412
+ */
1413
+ startDM = async (
1414
+ text,
1415
+ clientID,
1416
+ postID,
1417
+ anonymous = false,
1418
+ postContext = "feed",
1419
+ ) => {
1420
+ if (!this.userToken) {
1421
+ throw new SidechatAPIError("User is not authenticated.");
1422
+ }
1423
+ try {
1424
+ const res = await fetch(`${this.apiRoot}/v1/chats/start`, {
1425
+ method: "POST",
1426
+ headers: {
1427
+ ...this.defaultHeaders,
1428
+ Authorization: `Bearer ${this.userToken}`,
1429
+ },
1430
+ body: JSON.stringify({
1431
+ text: text,
1432
+ client_id: clientID,
1433
+ post_id: postID,
1434
+ anonymous: anonymous,
1435
+ post_context: postContext,
1436
+ }),
1437
+ });
1438
+ const json = await res.json();
1439
+ return await json;
1440
+ } catch (err) {
1441
+ console.error(err);
1442
+ throw new SidechatAPIError(`Failed to start DM.`);
1443
+ }
1444
+ };
1445
+
1446
+ /**
1447
+ * Hides posts from user
1448
+ * @method
1449
+ * @param {String} postID - alphanumeric ID of post to hide
1450
+ * @since 2.6.2
1451
+ * @deprecated since 2.7.0. Please use `blockUser` instead!
1452
+ */
1453
+ hidePostsFromUser = async (postID) => {
1454
+ if (!this.userToken) {
1455
+ throw new SidechatAPIError("User is not authenticated.");
1456
+ }
1457
+ try {
1458
+ const res = await fetch(`${this.apiRoot}/v1/posts/hide_posts_from_user`, {
1459
+ method: "POST",
1460
+ headers: {
1461
+ ...this.defaultHeaders,
1462
+ Authorization: `Bearer ${this.userToken}`,
1463
+ },
1464
+ body: JSON.stringify({
1465
+ post_id: postID,
1466
+ post_context: "feed",
1467
+ report: false,
1468
+ }),
1469
+ });
1470
+ const json = await res.json();
1471
+ return await json;
1472
+ } catch (err) {
1473
+ console.error(err);
1474
+ throw new SidechatAPIError(`Failed to hide post.`);
1475
+ }
1476
+ };
1477
+
1478
+ /**
1479
+ * Unhides all posts from all users
1480
+ * @method
1481
+ * @since 2.6.2
1482
+ * @deprecated since 2.7.0. No longer used by the official app and may stop working.
1483
+ */
1484
+ unhidePostsFromAllUsers = async () => {
1485
+ if (!this.userToken) {
1486
+ throw new SidechatAPIError("User is not authenticated.");
1487
+ }
1488
+ try {
1489
+ const res = await fetch(`${this.apiRoot}/v1/users/unhide_all`, {
1490
+ method: "POST",
1491
+ headers: {
1492
+ ...this.defaultHeaders,
1493
+ Authorization: `Bearer ${this.userToken}`,
1494
+ },
1495
+ body: JSON.stringify({}),
1496
+ });
1497
+ const json = await res.json();
1498
+ return await json;
1499
+ } catch (err) {
1500
+ console.error(err);
1501
+ throw new SidechatAPIError(`Failed to unhide post.`);
1502
+ }
1503
+ };
1504
+
1505
+ /**
1506
+ * Fetches the home feed, which mixes posts from all of the user's groups
1507
+ * @method
1508
+ * @param {"for_you"|"hot"|"recent"} [type] - how the feed should be sorted
1509
+ * @param {String} [schoolGroupID] - alphanumeric ID of the user's school group
1510
+ * @param {types.SidechatCursorString} [cursor] - cursor string
1511
+ * @returns {Promise&lt;types.SidechatFeed>} feed items and cursor
1512
+ * @since 2.7.0
1513
+ */
1514
+ getHomeFeed = async (type = "for_you", schoolGroupID, cursor) => {
1515
+ if (!this.userToken) {
1516
+ throw new SidechatAPIError("User is not authenticated.");
1517
+ }
1518
+ try {
1519
+ const params = new URLSearchParams({ type: type });
1520
+ if (schoolGroupID) params.set("school_group_id", schoolGroupID);
1521
+ if (cursor) params.set("cursor", cursor);
1522
+ const res = await this.sendRequest(`/v1/posts/home?${params}`);
1523
+ return await res.json();
1524
+ } catch (err) {
1525
+ console.error(err);
1526
+ throw new SidechatAPIError(`Failed to get home feed.`);
1527
+ }
1528
+ };
1529
+
1530
+ /**
1531
+ * Follows or unfollows a post. Followed posts send activity notifications when they get new comments.
1532
+ * @method
1533
+ * @param {String} postID - alphanumeric ID of post
1534
+ * @param {Boolean} following - whether or not the user should follow the post
1535
+ * @returns {Promise&lt;{post: types.SidechatPostOrComment}>} the updated post
1536
+ * @since 2.7.0
1537
+ */
1538
+ setPostFollowing = async (postID, following) => {
1539
+ if (!this.userToken) {
1540
+ throw new SidechatAPIError("User is not authenticated.");
1541
+ }
1542
+ try {
1543
+ const res = await this.sendRequest(
1544
+ `/v1/posts/${following ? "follow" : "unfollow"}`,
1545
+ "POST",
1546
+ JSON.stringify({ post_id: postID }),
1547
+ );
1548
+ return await res.json();
1549
+ } catch (err) {
1550
+ console.error(err);
1551
+ throw new SidechatAPIError(`Failed to change post follow status.`);
1552
+ }
1553
+ };
1554
+
1555
+ /**
1556
+ * Reports a post or comment to the group's moderators
1557
+ * @method
1558
+ * @param {String} postID - alphanumeric ID of post or comment to report
1559
+ * @param {types.SidechatReportReason} reason - why the content is being reported
1560
+ * @param {String} [note] - explanation to send along with the report
1561
+ * @since 2.7.0
1562
+ */
1563
+ flagPost = async (postID, reason, note = undefined) => {
1564
+ if (!this.userToken) {
1565
+ throw new SidechatAPIError("User is not authenticated.");
1566
+ }
1567
+ try {
1568
+ const res = await this.sendRequest(
1569
+ "/v1/posts/flag",
1570
+ "POST",
1571
+ JSON.stringify({ post_id: postID, reason: reason, note: note }),
1572
+ );
1573
+ return await res.json();
1574
+ } catch (err) {
1575
+ console.error(err);
1576
+ throw new SidechatAPIError(`Failed to report post.`);
1577
+ }
1578
+ };
1579
+
1580
+ /**
1581
+ * Blocks the author of a post or comment, hiding all of their content from the user. Replaces `hidePostsFromUser`.
1582
+ * @method
1583
+ * @param {String} postID - alphanumeric ID of a post or comment by the user to block
1584
+ * @param {Boolean} [report] - whether or not to also report the content
1585
+ * @param {"feed"|"chat"} [postContext] - where the content was seen
1586
+ * @since 2.7.0
1587
+ */
1588
+ blockUser = async (postID, report = false, postContext = "feed") => {
1589
+ if (!this.userToken) {
1590
+ throw new SidechatAPIError("User is not authenticated.");
1591
+ }
1592
+ try {
1593
+ const res = await this.sendRequest(
1594
+ "/v1/posts/block_user",
1595
+ "POST",
1596
+ JSON.stringify({
1597
+ post_id: postID,
1598
+ post_context: postContext,
1599
+ report: report,
1600
+ }),
1601
+ );
1602
+ return await res.json();
1603
+ } catch (err) {
1604
+ console.error(err);
1605
+ throw new SidechatAPIError(`Failed to block user.`);
1606
+ }
1607
+ };
1608
+
1609
+ /**
1610
+ * Creates a link attachment that can be added to a post with createPost(). YouTube links are recognized and shown as embedded videos.
1611
+ * @method
1612
+ * @param {String} url - URL to attach
1613
+ * @returns {Promise&lt;types.SidechatAttachment>} the created attachment
1614
+ * @since 2.7.0
1615
+ */
1616
+ createLinkAttachment = async (url) => {
1617
+ if (!this.userToken) {
1618
+ throw new SidechatAPIError("User is not authenticated.");
1619
+ }
1620
+ try {
1621
+ const res = await this.sendRequest(
1622
+ "/v1/attachments/upload_link",
1623
+ "POST",
1624
+ JSON.stringify({ link_url: url }),
1625
+ );
1626
+ return await res.json();
1627
+ } catch (err) {
1628
+ console.error(err);
1629
+ throw new SidechatAPIError(`Failed to create link attachment.`);
1630
+ }
1631
+ };
1632
+
1633
+ /**
1634
+ * Gets the user's activity feed (notifications for comments, votes, followers, etc.)
1635
+ * @method
1636
+ * @param {types.SidechatCursorString} [cursor] - cursor string
1637
+ * @returns {Promise&lt;types.SidechatActivityList>} activity items and cursor
1638
+ * @since 2.7.0
1639
+ */
1640
+ getActivity = async (cursor) => {
1641
+ if (!this.userToken) {
1642
+ throw new SidechatAPIError("User is not authenticated.");
1643
+ }
1644
+ try {
1645
+ const res = await this.sendRequest(
1646
+ `/v1/activity${cursor ? "?cursor=" + encodeURIComponent(cursor) : ""}`,
1647
+ );
1648
+ return await res.json();
1649
+ } catch (err) {
1650
+ console.error(err);
1651
+ throw new SidechatAPIError(`Failed to get activity.`);
1652
+ }
1653
+ };
1654
+
1655
+ /**
1656
+ * Gets the groups the user is a member of
1657
+ * @method
1658
+ * @returns {Promise&lt;types.SidechatGroup[]>}
1659
+ * @since 2.7.0
1660
+ */
1661
+ getGroups = async () => {
1662
+ if (!this.userToken) {
1663
+ throw new SidechatAPIError("User is not authenticated.");
1664
+ }
1665
+ try {
1666
+ const res = await this.sendRequest("/v1/groups");
1667
+ const json = await res.json();
1668
+ return json.groups;
1669
+ } catch (err) {
1670
+ console.error(err);
1671
+ throw new SidechatAPIError(`Failed to get groups.`);
1672
+ }
1673
+ };
1674
+
1675
+ /**
1676
+ * Gets groups suggested for the user to join
1677
+ * @method
1678
+ * @param {Number} [limit] - maximum number of groups to return
1679
+ * @returns {Promise&lt;types.SidechatGroup[]>}
1680
+ * @since 2.7.0
1681
+ */
1682
+ getSuggestedGroups = async (limit = 10) => {
1683
+ if (!this.userToken) {
1684
+ throw new SidechatAPIError("User is not authenticated.");
1685
+ }
1686
+ try {
1687
+ const res = await this.sendRequest(
1688
+ `/v1/groups/suggestions?limit=${limit}`,
1689
+ );
1690
+ const json = await res.json();
1691
+ return json.groups;
1692
+ } catch (err) {
1693
+ console.error(err);
1694
+ throw new SidechatAPIError(`Failed to get suggested groups.`);
1695
+ }
1696
+ };
1697
+
1698
+ /**
1699
+ * Gets a shareable invite link for a group. School groups don't have invite links and will throw an error.
1700
+ * @method
1701
+ * @param {String} groupID - alphanumeric ID of group
1702
+ * @returns {Promise&lt;String>} invite link URL
1703
+ * @since 2.7.0
1704
+ */
1705
+ getGroupInviteLink = async (groupID) => {
1706
+ if (!this.userToken) {
1707
+ throw new SidechatAPIError("User is not authenticated.");
1708
+ }
1709
+ try {
1710
+ const res = await this.sendRequest(
1711
+ `/v1/groups/invite_link?group_id=${groupID}`,
1712
+ );
1713
+ const json = await res.json();
1714
+ return json.link;
1715
+ } catch (err) {
1716
+ console.error(err);
1717
+ throw new SidechatAPIError(`Failed to get invite link.`);
1718
+ }
1719
+ };
1720
+
1721
+ /**
1722
+ * Gets the yakarma leaderboard for a group, along with the user's own karma
1723
+ * @method
1724
+ * @param {String} groupID - alphanumeric ID of group
1725
+ * @returns {Promise&lt;types.SidechatLeaderboard>}
1726
+ * @since 2.7.0
1727
+ */
1728
+ getLeaderboard = async (groupID) => {
1729
+ if (!this.userToken) {
1730
+ throw new SidechatAPIError("User is not authenticated.");
1731
+ }
1732
+ try {
1733
+ const res = await this.sendRequest(`/v1/leaderboard?group_id=${groupID}`);
1734
+ return await res.json();
1735
+ } catch (err) {
1736
+ console.error(err);
1737
+ throw new SidechatAPIError(`Failed to get leaderboard.`);
1738
+ }
1739
+ };
1740
+
1741
+ /**
1742
+ * Gets the existing direct message thread for a post or comment. Returns undefined if the user hasn't started one.
1743
+ * @method
1744
+ * @param {String} postID - alphanumeric ID of post or comment
1745
+ * @returns {Promise&lt;types.SidechatDirectThread>}
1746
+ * @since 2.7.0
1747
+ */
1748
+ getDMForPost = async (postID) => {
1749
+ if (!this.userToken) {
1750
+ throw new SidechatAPIError("User is not authenticated.");
1751
+ }
1752
+ try {
1753
+ const res = await this.sendRequest(`/v1/chats/by_post?post_id=${postID}`);
1754
+ const json = await res.json();
1755
+ return json.chat;
1756
+ } catch (err) {
1757
+ console.error(err);
1758
+ throw new SidechatAPIError(`Failed to get DM thread for post.`);
1759
+ }
1760
+ };
1761
+
1762
+ /**
1763
+ * Reports a direct message thread
1764
+ * @method
1765
+ * @param {String} chatID - alphanumeric ID of the thread to report
1766
+ * @param {types.SidechatReportReason} reason - why the thread is being reported
1767
+ * @since 2.7.0
1768
+ */
1769
+ reportDM = async (chatID, reason) => {
1770
+ if (!this.userToken) {
1771
+ throw new SidechatAPIError("User is not authenticated.");
1772
+ }
1773
+ try {
1774
+ const res = await this.sendRequest(
1775
+ "/v1/chats/report",
1776
+ "POST",
1777
+ JSON.stringify({ chat_id: chatID, reason: reason }),
1778
+ );
1779
+ return await res.json();
1780
+ } catch (err) {
1781
+ console.error(err);
1782
+ throw new SidechatAPIError(`Failed to report DM.`);
1783
+ }
1784
+ };
1785
+
1786
+ /**
1787
+ * Sets which push notifications the user receives
1788
+ * @method
1789
+ * @param {types.SidechatNotificationPreferences} preferences - notification types to enable or disable
1790
+ * @since 2.7.0
1791
+ */
1792
+ setNotificationPreferences = async (preferences) => {
1793
+ if (!this.userToken) {
1794
+ throw new SidechatAPIError("User is not authenticated.");
1795
+ }
1796
+ try {
1797
+ const res = await this.sendRequest(
1798
+ "/v1/users/set_notification_preferences",
1799
+ "POST",
1800
+ JSON.stringify({ notification_preferences: preferences }),
1801
+ );
1802
+ return await res.json();
1803
+ } catch (err) {
1804
+ console.error(err);
1805
+ throw new SidechatAPIError(`Failed to set notification preferences.`);
1806
+ }
1807
+ };
1808
+
1809
+ /**
1810
+ * Verifies the user's school email by signing in with Google or Microsoft instead of clicking an emailed link. Returns the same object as checkEmailVerification(). This was discovered from me decompiling the new Android app, I haven't tested this but based on the implementation it should work fine and might resolve the problem that a lot of people have with their accounts not being associated with their school automatically.
1811
+ * @method
1812
+ * @param {String} authToken - token from the Google or Microsoft sign-in
1813
+ * @param {"google"|"microsoft"} authType - provider the token came from
1814
+ * @param {Boolean} [isIncomingFreshman] - whether or not the user is an incoming freshman
1815
+ * @since 2.7.0
1816
+ * @tutorial Email Registration
1817
+ */
1818
+ verifyEmailWithThirdParty = async (
1819
+ authToken,
1820
+ authType,
1821
+ isIncomingFreshman = false,
1822
+ ) => {
1823
+ if (!this.userToken) {
1824
+ throw new SidechatAPIError("User is not authenticated.");
1825
+ }
1826
+ try {
1827
+ const res = await this.sendRequest(
1828
+ "/v1/users/third_party_email_verified",
1829
+ "POST",
1830
+ JSON.stringify({
1831
+ auth: authToken,
1832
+ auth_type: authType,
1833
+ is_incoming_freshman: isIncomingFreshman,
1834
+ }),
1835
+ );
1836
+ const json = await res.json();
1837
+ if (json.verified_email_updates_response) {
1838
+ return json.verified_email_updates_response;
1839
+ } else {
1840
+ throw new SidechatAPIError(json?.message || "Email is not verified.");
1841
+ }
1842
+ } catch (err) {
1843
+ console.error(err);
1844
+ throw new SidechatAPIError("Failed to verify email.");
1845
+ }
1846
+ };
1847
+
1848
+ /**
1849
+ * Authorizes a realtime (Pusher) connection to subscribe to a private channel, such as `private-chat-&lt;chatID>` for live direct messages. Pass the result to your Pusher client's channel authorizer. This hasn't been tested, and is mostly theoretical.
1850
+ * @method
1851
+ * @param {String} socketID - socket ID assigned by Pusher
1852
+ * @param {String} channelName - name of the private channel to subscribe to
1853
+ * @returns {Promise&lt;{auth: String}>} signed channel authorization
1854
+ * @since 2.7.0
1855
+ */
1856
+ authorizeRealtimeChannel = async (socketID, channelName) => {
1857
+ if (!this.userToken) {
1858
+ throw new SidechatAPIError("User is not authenticated.");
1859
+ }
1860
+ try {
1861
+ const res = await this.sendRequest(
1862
+ "/pusher/auth",
1863
+ "POST",
1864
+ JSON.stringify({ socket_id: socketID, channel_name: channelName }),
1865
+ );
1866
+ return await res.json();
1867
+ } catch (err) {
1868
+ console.error(err);
1869
+ throw new SidechatAPIError(`Failed to authorize realtime channel.`);
1870
+ }
1871
+ };
1872
+
1873
+ /**
1874
+ * Permanently deletes the current user's account. This cannot be undone!
1875
+ * @method
1876
+ * @since 2.7.0
1877
+ */
1878
+ deleteAccount = async () => {
1879
+ if (!this.userToken) {
1880
+ throw new SidechatAPIError("User is not authenticated.");
1881
+ }
1882
+ try {
1883
+ const res = await this.sendRequest(
1884
+ "/v1/users/delete",
1885
+ "POST",
1886
+ JSON.stringify({}),
1887
+ );
1888
+ return await res.json();
1889
+ } catch (err) {
1890
+ console.error(err);
1891
+ throw new SidechatAPIError(`Failed to delete account.`);
1892
+ }
1893
+ };
1894
+ }
1895
+
1896
+ export default SidechatAPIClient;
1897
+ </code></pre></article></section></div></div></div><div class="search-container" id="PkfLWpAbet" style="display:none"><div class="wrapper" id="iCxFxjkHbP"><button class="icon-button search-close-button" id="VjLlGakifb" aria-label="close search"><svg><use xlink:href="#close-icon"></use></svg></button><div class="search-box-c"><svg><use xlink:href="#search-icon"></use></svg> <input type="text" id="vpcKVYIppa" class="search-input" placeholder="Search..." autofocus></div><div class="search-result-c" id="fWwVHRuDuN"><span class="search-result-c-text">Type anything to view search result</span></div></div></div><div class="mobile-menu-icon-container"><button class="icon-button" id="mobile-menu" data-isopen="false" aria-label="menu"><svg><use xlink:href="#menu-icon"></use></svg></button></div><div id="mobile-sidebar" class="mobile-sidebar-container"><div class="mobile-sidebar-wrapper"><a href="/" class="sidebar-title sidebar-title-anchor">sidechat.js</a><div class="mobile-nav-links"><div class="navbar-item"><a id="" href="https://github.com/micahlt/sidechat.js" target="_blank">GitHub</a></div><div class="navbar-item"><a id="" href="https://www.npmjs.com/package/sidechat.js" target="_blank">NPM</a></div><div class="navbar-item"><a id="" href="https://offsides.micahlindley.com" target="_blank">Offsides (Sidechat for Android)</a></div></div><div class="mobile-sidebar-items-c"><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-tutorials"><div>Tutorials</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="tutorial-Authentication.html">Authentication</a></div><div class="sidebar-section-children"><a href="tutorial-Email Registration.html">Email Registration</a></div><div class="sidebar-section-children"><a href="tutorial-Feeds and Pagination.html">Feeds and Pagination</a></div><div class="sidebar-section-children"><a href="tutorial-Posting and Media.html">Posting and Media</a></div></div><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-classes"><div>Classes</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="SidechatAPIClient.html">SidechatAPIClient</a></div><div class="sidebar-section-children"><a href="SidechatAPIError.html">SidechatAPIError</a></div><div class="sidebar-section-children"><a href="SidechatColorList.html">SidechatColorList</a></div></div><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-global"><div>Global</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="global.html#SidechatActivityItem">SidechatActivityItem</a></div><div class="sidebar-section-children"><a href="global.html#SidechatActivityList">SidechatActivityList</a></div><div class="sidebar-section-children"><a href="global.html#SidechatAssetMetadata">SidechatAssetMetadata</a></div><div class="sidebar-section-children"><a href="global.html#SidechatAttachment">SidechatAttachment</a></div><div class="sidebar-section-children"><a href="global.html#SidechatAuthToken">SidechatAuthToken</a></div><div class="sidebar-section-children"><a href="global.html#SidechatColor">SidechatColor</a></div><div class="sidebar-section-children"><a href="global.html#SidechatCurrentUser">SidechatCurrentUser</a></div><div class="sidebar-section-children"><a href="global.html#SidechatCursorString">SidechatCursorString</a></div><div class="sidebar-section-children"><a href="global.html#SidechatDirectMessage">SidechatDirectMessage</a></div><div class="sidebar-section-children"><a href="global.html#SidechatDirectThread">SidechatDirectThread</a></div><div class="sidebar-section-children"><a href="global.html#SidechatFeed">SidechatFeed</a></div><div class="sidebar-section-children"><a href="global.html#SidechatFeedItem">SidechatFeedItem</a></div><div class="sidebar-section-children"><a href="global.html#SidechatGroup">SidechatGroup</a></div><div class="sidebar-section-children"><a href="global.html#SidechatIcon">SidechatIcon</a></div><div class="sidebar-section-children"><a href="global.html#SidechatIdentity">SidechatIdentity</a></div><div class="sidebar-section-children"><a href="global.html#SidechatKarma">SidechatKarma</a></div><div class="sidebar-section-children"><a href="global.html#SidechatLeaderboard">SidechatLeaderboard</a></div><div class="sidebar-section-children"><a href="global.html#SidechatLibraryAsset">SidechatLibraryAsset</a></div><div class="sidebar-section-children"><a href="global.html#SidechatMembership">SidechatMembership</a></div><div class="sidebar-section-children"><a href="global.html#SidechatNotificationPreferences">SidechatNotificationPreferences</a></div><div class="sidebar-section-children"><a href="global.html#SidechatPoll">SidechatPoll</a></div><div class="sidebar-section-children"><a href="global.html#SidechatPollChoice">SidechatPollChoice</a></div><div class="sidebar-section-children"><a href="global.html#SidechatPostOrComment">SidechatPostOrComment</a></div><div class="sidebar-section-children"><a href="global.html#SidechatPostsAndCursor">SidechatPostsAndCursor</a></div><div class="sidebar-section-children"><a href="global.html#SidechatPrivateAssetURL">SidechatPrivateAssetURL</a></div><div class="sidebar-section-children"><a href="global.html#SidechatProfile">SidechatProfile</a></div><div class="sidebar-section-children"><a href="global.html#SidechatReportReason">SidechatReportReason</a></div><div class="sidebar-section-children"><a href="global.html#SidechatSimpleAsset">SidechatSimpleAsset</a></div><div class="sidebar-section-children"><a href="global.html#SidechatVoteString">SidechatVoteString</a></div></div></div><div class="mobile-navbar-actions"><div class="navbar-right-item"><button class="icon-button search-button" aria-label="open-search"><svg><use xlink:href="#search-icon"></use></svg></button></div><div class="navbar-right-item"><button class="icon-button theme-toggle" aria-label="toggle-theme"><svg><use class="theme-svg-use" xlink:href="#light-theme-icon"></use></svg></button></div><div class="navbar-right-item"><button class="icon-button font-size" aria-label="change-font-size"><svg><use xlink:href="#font-size-icon"></use></svg></button></div></div></div></div><script type="text/javascript" src="scripts/core.min.js"></script><script src="scripts/search.min.js" defer="defer"></script><script src="scripts/third-party/fuse.js" defer="defer"></script><script type="text/javascript">var tocbotInstance=tocbot.init({tocSelector:"#eed4d2a0bfd64539bb9df78095dec881",contentSelector:".main-content",headingSelector:"h1, h2, h3",hasInnerContainers:!0,scrollContainer:".main-content",headingsOffset:130,onClick:bringLinkToView})</script></body></html>