sidechat.js 2.6.5 → 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 (87) 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 +925 -365
  4. package/dist/index.mjs +925 -365
  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.6.6/SidechatAPIClient.html +3 -0
  8. package/docs/sidechat.js/2.6.6/SidechatAPIError.html +3 -0
  9. package/docs/sidechat.js/2.6.6/SidechatColorList.html +3 -0
  10. package/docs/sidechat.js/2.6.6/classes_SidechatAPIClient.js.html +1447 -0
  11. package/docs/sidechat.js/2.6.6/classes_SidechatAPIError.js.html +16 -0
  12. package/docs/sidechat.js/2.6.6/classes_SidechatColorList.js.html +79 -0
  13. package/docs/sidechat.js/2.6.6/data/search.json +1 -0
  14. package/docs/sidechat.js/2.6.6/fonts/Inconsolata-Regular.ttf +0 -0
  15. package/docs/sidechat.js/2.6.6/fonts/OpenSans-Regular.ttf +0 -0
  16. package/docs/sidechat.js/2.6.6/fonts/WorkSans-Bold.ttf +0 -0
  17. package/docs/sidechat.js/2.6.6/global.html +3 -0
  18. package/docs/sidechat.js/2.6.6/index.html +4 -0
  19. package/docs/sidechat.js/2.6.6/scripts/core.js +726 -0
  20. package/docs/sidechat.js/2.6.6/scripts/core.min.js +23 -0
  21. package/docs/sidechat.js/2.6.6/scripts/resize.js +90 -0
  22. package/docs/sidechat.js/2.6.6/scripts/search.js +265 -0
  23. package/docs/sidechat.js/2.6.6/scripts/search.min.js +6 -0
  24. package/docs/sidechat.js/2.6.6/scripts/third-party/Apache-License-2.0.txt +202 -0
  25. package/docs/sidechat.js/2.6.6/scripts/third-party/fuse.js +9 -0
  26. package/docs/sidechat.js/2.6.6/scripts/third-party/hljs-line-num-original.js +369 -0
  27. package/docs/sidechat.js/2.6.6/scripts/third-party/hljs-line-num.js +1 -0
  28. package/docs/sidechat.js/2.6.6/scripts/third-party/hljs-original.js +5171 -0
  29. package/docs/sidechat.js/2.6.6/scripts/third-party/hljs.js +1 -0
  30. package/docs/sidechat.js/2.6.6/scripts/third-party/popper.js +5 -0
  31. package/docs/sidechat.js/2.6.6/scripts/third-party/tippy.js +1 -0
  32. package/docs/sidechat.js/2.6.6/scripts/third-party/tocbot.js +672 -0
  33. package/docs/sidechat.js/2.6.6/scripts/third-party/tocbot.min.js +1 -0
  34. package/docs/sidechat.js/2.6.6/styles/clean-jsdoc-theme-base.css +1159 -0
  35. package/docs/sidechat.js/2.6.6/styles/clean-jsdoc-theme-dark.css +412 -0
  36. package/docs/sidechat.js/2.6.6/styles/clean-jsdoc-theme-light.css +482 -0
  37. package/docs/sidechat.js/2.6.6/styles/clean-jsdoc-theme-scrollbar.css +30 -0
  38. package/docs/sidechat.js/2.6.6/styles/clean-jsdoc-theme-without-scrollbar.min.css +1 -0
  39. package/docs/sidechat.js/2.6.6/styles/clean-jsdoc-theme.min.css +1 -0
  40. package/docs/sidechat.js/2.6.6/tutorial-Authentication.html +14 -0
  41. package/docs/sidechat.js/2.6.6/tutorial-Email Registration.html +6 -0
  42. package/docs/sidechat.js/2.6.6/types_SidechatTypes.js.html +204 -0
  43. package/docs/sidechat.js/2.7.0/SidechatAPIClient.html +3 -0
  44. package/docs/sidechat.js/2.7.0/SidechatAPIError.html +3 -0
  45. package/docs/sidechat.js/2.7.0/SidechatColorList.html +3 -0
  46. package/docs/sidechat.js/2.7.0/classes_SidechatAPIClient.js.html +1897 -0
  47. package/docs/sidechat.js/2.7.0/classes_SidechatAPIError.js.html +16 -0
  48. package/docs/sidechat.js/2.7.0/classes_SidechatColorList.js.html +79 -0
  49. package/docs/sidechat.js/2.7.0/data/search.json +1 -0
  50. package/docs/sidechat.js/2.7.0/fonts/Inconsolata-Regular.ttf +0 -0
  51. package/docs/sidechat.js/2.7.0/fonts/OpenSans-Regular.ttf +0 -0
  52. package/docs/sidechat.js/2.7.0/fonts/WorkSans-Bold.ttf +0 -0
  53. package/docs/sidechat.js/2.7.0/global.html +3 -0
  54. package/docs/sidechat.js/2.7.0/index.html +4 -0
  55. package/docs/sidechat.js/2.7.0/scripts/core.js +726 -0
  56. package/docs/sidechat.js/2.7.0/scripts/core.min.js +23 -0
  57. package/docs/sidechat.js/2.7.0/scripts/resize.js +90 -0
  58. package/docs/sidechat.js/2.7.0/scripts/search.js +265 -0
  59. package/docs/sidechat.js/2.7.0/scripts/search.min.js +6 -0
  60. package/docs/sidechat.js/2.7.0/scripts/third-party/Apache-License-2.0.txt +202 -0
  61. package/docs/sidechat.js/2.7.0/scripts/third-party/fuse.js +9 -0
  62. package/docs/sidechat.js/2.7.0/scripts/third-party/hljs-line-num-original.js +369 -0
  63. package/docs/sidechat.js/2.7.0/scripts/third-party/hljs-line-num.js +1 -0
  64. package/docs/sidechat.js/2.7.0/scripts/third-party/hljs-original.js +5171 -0
  65. package/docs/sidechat.js/2.7.0/scripts/third-party/hljs.js +1 -0
  66. package/docs/sidechat.js/2.7.0/scripts/third-party/popper.js +5 -0
  67. package/docs/sidechat.js/2.7.0/scripts/third-party/tippy.js +1 -0
  68. package/docs/sidechat.js/2.7.0/scripts/third-party/tocbot.js +672 -0
  69. package/docs/sidechat.js/2.7.0/scripts/third-party/tocbot.min.js +1 -0
  70. package/docs/sidechat.js/2.7.0/styles/clean-jsdoc-theme-base.css +1159 -0
  71. package/docs/sidechat.js/2.7.0/styles/clean-jsdoc-theme-dark.css +412 -0
  72. package/docs/sidechat.js/2.7.0/styles/clean-jsdoc-theme-light.css +482 -0
  73. package/docs/sidechat.js/2.7.0/styles/clean-jsdoc-theme-scrollbar.css +30 -0
  74. package/docs/sidechat.js/2.7.0/styles/clean-jsdoc-theme-without-scrollbar.min.css +1 -0
  75. package/docs/sidechat.js/2.7.0/styles/clean-jsdoc-theme.min.css +1 -0
  76. package/docs/sidechat.js/2.7.0/tutorial-Authentication.html +16 -0
  77. package/docs/sidechat.js/2.7.0/tutorial-Email Registration.html +6 -0
  78. package/docs/sidechat.js/2.7.0/tutorial-Feeds and Pagination.html +10 -0
  79. package/docs/sidechat.js/2.7.0/tutorial-Posting and Media.html +19 -0
  80. package/docs/sidechat.js/2.7.0/types_SidechatTypes.js.html +297 -0
  81. package/docs/tutorials/Authentication.md +63 -52
  82. package/docs/tutorials/Email Registration.md +19 -16
  83. package/docs/tutorials/Feeds and Pagination.md +45 -0
  84. package/docs/tutorials/Posting and Media.md +63 -0
  85. package/package.json +55 -55
  86. package/src/classes/SidechatAPIClient.js +549 -81
  87. package/src/types/SidechatTypes.js +294 -202
@@ -67,6 +67,16 @@ class SidechatAPIClient {
67
67
  this.apiRoot = url;
68
68
  };
69
69
 
70
+ /**
71
+ * 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.
72
+ * @method
73
+ * @param {String} deviceIdentifier - stable identifier for this device
74
+ * @since 2.7.0
75
+ */
76
+ setDeviceIdentifier = (deviceIdentifier) => {
77
+ this.defaultHeaders["X-Device-ID"] = deviceIdentifier;
78
+ };
79
+
70
80
  /**
71
81
  * Run an arbitrary API request using the current client's authentication
72
82
  * @method
@@ -82,7 +92,7 @@ class SidechatAPIClient {
82
92
  method = "GET",
83
93
  body = undefined,
84
94
  headers = {},
85
- stripHeaders = false
95
+ stripHeaders = false,
86
96
  ) => {
87
97
  let requestHeaders = this.defaultHeaders;
88
98
  if (stripHeaders) {
@@ -97,19 +107,32 @@ class SidechatAPIClient {
97
107
  };
98
108
 
99
109
  /**
100
- * Initiate the login process with a phone number. Should be followed up with verifySMSCode().
110
+ * 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.
101
111
  * @method
102
112
  * @since 1.0.0
103
113
  * @param {Number} phoneNumber - US phone number (WITHOUT +1) to send verification code to
114
+ * @param {Boolean} [useFallback] - send the code through the backup SMS route, which the official app offers when a code doesn't arrive
115
+ * @param {String} [captchaMethod] - type of captcha being answered
116
+ * @param {String} [captchaResponse] - solved captcha token
104
117
  */
105
- loginViaSMS = async (phoneNumber) => {
118
+ loginViaSMS = async (
119
+ phoneNumber,
120
+ useFallback = false,
121
+ captchaMethod = undefined,
122
+ captchaResponse = undefined,
123
+ ) => {
106
124
  try {
107
- const res = await fetch(`${this.apiRoot}/v1/login_register`, {
125
+ const endpoint = useFallback
126
+ ? "/v1/login_register_sms_fallback"
127
+ : "/v1/login_register";
128
+ const res = await fetch(`${this.apiRoot}${endpoint}`, {
108
129
  method: "POST",
109
130
  headers: this.defaultHeaders,
110
131
  body: JSON.stringify({
111
132
  phone_number: `+1${phoneNumber}`,
112
133
  version: 3,
134
+ captcha_method: captchaMethod,
135
+ captcha_response: captchaResponse,
113
136
  }),
114
137
  });
115
138
  const json = await res.json();
@@ -154,8 +177,15 @@ class SidechatAPIClient {
154
177
  * @since 1.0.0
155
178
  * @param {Number} age - user's age in years
156
179
  * @param {String} registrationID - the registration ID generated by verifySMSCode()
180
+ * @param {String} [timeZone] - time zone of the user (e.g. "America/Chicago"). Falls back to the system time zone.
181
+ * @param {String} [inviteLinkURL] - group invite link that the user signed up through
157
182
  */
158
- setAge = async (age, registrationID) => {
183
+ setAge = async (
184
+ age,
185
+ registrationID,
186
+ timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone,
187
+ inviteLinkURL = undefined,
188
+ ) => {
159
189
  if (age < 13) {
160
190
  throw new SidechatAPIError("You're too young to use Offsides.");
161
191
  }
@@ -166,6 +196,9 @@ class SidechatAPIClient {
166
196
  body: JSON.stringify({
167
197
  age: Number(age),
168
198
  registration_id: registrationID,
199
+ time_zone: timeZone,
200
+ invite_link_url: inviteLinkURL,
201
+ device_id: this.defaultHeaders["X-Device-ID"],
169
202
  }),
170
203
  });
171
204
  const json = await res.json();
@@ -248,8 +281,9 @@ class SidechatAPIClient {
248
281
  * @method
249
282
  * @since 1.0.0
250
283
  * @param {String} deviceId - the device ID to set
284
+ * @param {"com.flowerave.sidechat"|"com.flowerave.yikyak"} [bundleID] - bundle ID of the app the push token belongs to
251
285
  */
252
- setDeviceID = async (deviceID) => {
286
+ setDeviceID = async (deviceID, bundleID = "com.flowerave.sidechat") => {
253
287
  if (!this.userToken) {
254
288
  throw new SidechatAPIError("User is not authenticated.");
255
289
  }
@@ -262,7 +296,7 @@ class SidechatAPIClient {
262
296
  },
263
297
  body: JSON.stringify({
264
298
  build_type: "release",
265
- bundle_id: "com.flowerave.sidechat",
299
+ bundle_id: bundleID,
266
300
  device_token: deviceID,
267
301
  }),
268
302
  });
@@ -305,7 +339,7 @@ class SidechatAPIClient {
305
339
  ...this.defaultHeaders,
306
340
  Authorization: `Bearer ${this.userToken}`,
307
341
  },
308
- }
342
+ },
309
343
  );
310
344
  const json = await res.json();
311
345
  return json;
@@ -322,23 +356,27 @@ class SidechatAPIClient {
322
356
  * @param {String} groupID - group ID
323
357
  * @param {"hot"|"recent"|"top"} category - category to filter posts
324
358
  * @param {types.SidechatCursorString} [cursor] - cursor string
359
+ * @param {"day"|"week"|"month"|"year"|"all_time"} [period] - time period to pull top posts from (only if category is "top")
325
360
  * @returns {Promise<types.SidechatPostsAndCursor>} List of posts and cursor
326
361
  */
327
- getGroupPosts = async (groupID, category = "hot", cursor) => {
362
+ getGroupPosts = async (groupID, category = "hot", cursor, period) => {
328
363
  if (!this.userToken) {
329
364
  throw new SidechatAPIError("User is not authenticated.");
330
365
  }
331
366
  try {
332
367
  const res = await fetch(
333
- `${this.apiRoot}/v1/posts?${cursor ? "cursor=" + encodeURIComponent(cursor) + "&" : ""
334
- }group_id=${groupID}&type=${category}`,
368
+ `${this.apiRoot}/v1/posts?${
369
+ cursor ? "cursor=" + encodeURIComponent(cursor) + "&" : ""
370
+ }${
371
+ period ? "period=" + period + "&" : ""
372
+ }group_id=${groupID}&type=${category}&cacheBust=${Date.now()}`,
335
373
  {
336
374
  method: "GET",
337
375
  headers: {
338
376
  ...this.defaultHeaders,
339
377
  Authorization: `Bearer ${this.userToken}`,
340
378
  },
341
- }
379
+ },
342
380
  );
343
381
  const json = await res.json();
344
382
  return json;
@@ -393,14 +431,14 @@ class SidechatAPIClient {
393
431
  }
394
432
  try {
395
433
  const res = await fetch(
396
- `${this.apiRoot}/v1/posts/get?include_deleted=${includeDeleted}&post_id=${postID}`,
434
+ `${this.apiRoot}/v1/posts/get?include_deleted=${includeDeleted}&post_id=${postID}&cacheBust=${Date.now()}`,
397
435
  {
398
436
  method: "GET",
399
437
  headers: {
400
438
  ...this.defaultHeaders,
401
439
  Authorization: `Bearer ${this.userToken}`,
402
440
  },
403
- }
441
+ },
404
442
  );
405
443
  const json = await res.json();
406
444
  return json.post;
@@ -427,7 +465,7 @@ class SidechatAPIClient {
427
465
  contentType = "my_comments";
428
466
  }
429
467
  try {
430
- const res = await fetch(`${this.apiRoot}/v1/posts&type=${contentType}`, {
468
+ const res = await fetch(`${this.apiRoot}/v1/posts?type=${contentType}`, {
431
469
  method: "GET",
432
470
  headers: {
433
471
  ...this.defaultHeaders,
@@ -455,14 +493,14 @@ class SidechatAPIClient {
455
493
  }
456
494
  try {
457
495
  const res = await fetch(
458
- `${this.apiRoot}/v1/posts/comments/?post_id=${postID}`,
496
+ `${this.apiRoot}/v1/posts/comments/?post_id=${postID}&cacheBust=${Date.now()}`,
459
497
  {
460
498
  method: "GET",
461
499
  headers: {
462
500
  ...this.defaultHeaders,
463
501
  Authorization: `Bearer ${this.userToken}`,
464
502
  },
465
- }
503
+ },
466
504
  );
467
505
  const json = await res.json();
468
506
  // Function to preprocess the comments and organize them into a nested structure
@@ -558,13 +596,16 @@ class SidechatAPIClient {
558
596
  throw new SidechatAPIError("User is not authenticated.");
559
597
  }
560
598
  try {
561
- const res = await fetch(`${this.apiRoot}/v1/groups/explore/search?term=${encodeURIComponent(query)}`, {
562
- method: "GET",
563
- headers: {
564
- ...this.defaultHeaders,
565
- Authorization: `Bearer ${this.userToken}`,
599
+ const res = await fetch(
600
+ `${this.apiRoot}/v1/groups/explore/search?term=${encodeURIComponent(query)}`,
601
+ {
602
+ method: "GET",
603
+ headers: {
604
+ ...this.defaultHeaders,
605
+ Authorization: `Bearer ${this.userToken}`,
606
+ },
566
607
  },
567
- });
608
+ );
568
609
  const json = await res.json();
569
610
  return await json.results;
570
611
  } catch (err) {
@@ -664,8 +705,9 @@ class SidechatAPIClient {
664
705
  throw new SidechatAPIError("User is not authenticated.");
665
706
  }
666
707
  try {
708
+ // Joining moved to the v2 endpoint, which takes a list of IDs. Leave still sends group_id by itself, no list.
667
709
  const res = await fetch(
668
- `${this.apiRoot}/v1/groups/${isMember ? "join" : "leave"}`,
710
+ `${this.apiRoot}${isMember ? "/v2/groups/join" : "/v1/groups/leave"}`,
669
711
  {
670
712
  method: "POST",
671
713
  headers: {
@@ -673,9 +715,10 @@ class SidechatAPIClient {
673
715
  Authorization: `Bearer ${this.userToken}`,
674
716
  },
675
717
  body: JSON.stringify({
676
- group_id: groupID,
718
+ group_id: isMember ? undefined : groupID,
719
+ group_ids: [groupID],
677
720
  }),
678
- }
721
+ },
679
722
  );
680
723
  const json = await res.json();
681
724
  return await json;
@@ -707,7 +750,7 @@ class SidechatAPIClient {
707
750
  topLevelReplyID,
708
751
  assetList = [],
709
752
  disableDMs = false,
710
- anonymous = false
753
+ anonymous = false,
711
754
  ) => {
712
755
  if (!this.userToken) {
713
756
  throw new SidechatAPIError("User is not authenticated.");
@@ -732,15 +775,17 @@ class SidechatAPIClient {
732
775
  }),
733
776
  });
734
777
  const json = await res.json();
778
+ if (!res.ok) throw new SidechatAPIError(json.message);
735
779
  return await json.comment;
736
780
  } catch (err) {
737
781
  console.error(err);
782
+ if (err instanceof SidechatAPIError && err.message) throw err;
738
783
  throw new SidechatAPIError(`Failed to post comment.`);
739
784
  }
740
785
  };
741
786
 
742
787
  /**
743
- * Creates a new post in the specified group
788
+ * 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.
744
789
  * @method
745
790
  * @since 2.2.0
746
791
  * @param {String} text - text content of comment
@@ -750,6 +795,7 @@ class SidechatAPIClient {
750
795
  * @param {Boolean} [disableComments] - whether or not comments should be disabled on post
751
796
  * @param {Boolean} [anonymous] - whether or not to hide user's name and icon on post
752
797
  * @param {Array<String>} [pollOptions] - List of poll options. If provided, a poll will be created with these options.
798
+ * @param {Array<String>} [attachmentIDs] - IDs of link attachments created with createLinkAttachment()
753
799
  * @returns {Promise<types.SidechatPostOrComment>} the created post
754
800
  */
755
801
  createPost = async (
@@ -760,7 +806,8 @@ class SidechatAPIClient {
760
806
  disableComments = false,
761
807
  anonymous = false,
762
808
  repostId = undefined,
763
- pollOptions = undefined
809
+ pollOptions = undefined,
810
+ attachmentIDs = [],
764
811
  ) => {
765
812
  if (!this.userToken) {
766
813
  throw new SidechatAPIError("User is not authenticated.");
@@ -772,17 +819,17 @@ class SidechatAPIClient {
772
819
  assets: assetList,
773
820
  group_ids: [groupID],
774
821
  text: text,
775
- attachments: [],
822
+ attachments: attachmentIDs,
776
823
  dms_disabled: disableDMs,
777
824
  comments_disabled: disableComments,
778
825
  using_identity: !anonymous,
779
- quote_post_id: repostId
826
+ quote_post_id: repostId,
780
827
  };
781
828
  // If pollOptions is provided and is a non-empty array, add poll_request
782
829
  if (Array.isArray(pollOptions) && pollOptions.length > 0) {
783
830
  body.poll_request = {
784
831
  allows_view_results: true,
785
- choices: pollOptions
832
+ choices: pollOptions,
786
833
  };
787
834
  }
788
835
  const res = await fetch(`${this.apiRoot}/v1/posts`, {
@@ -794,9 +841,11 @@ class SidechatAPIClient {
794
841
  body: JSON.stringify(body),
795
842
  });
796
843
  const json = await res.json();
844
+ if (!res.ok) throw new SidechatAPIError(json.message);
797
845
  return await json.posts[0];
798
846
  } catch (err) {
799
847
  console.error(err);
848
+ if (err instanceof SidechatAPIError && err.message) throw err;
800
849
  throw new SidechatAPIError(`Failed to make post.`);
801
850
  }
802
851
  };
@@ -872,16 +921,19 @@ class SidechatAPIClient {
872
921
  throw new SidechatAPIError("User is not authenticated.");
873
922
  }
874
923
  try {
875
- const res = await fetch(`${this.apiRoot}/v1/polls/view_results`, {
876
- method: "POST",
877
- headers: {
878
- ...this.defaultHeaders,
879
- Authorization: `Bearer ${this.userToken}`,
924
+ const res = await fetch(
925
+ `${this.apiRoot}/v1/polls/view_results?cacheBust=${Date.now()}`,
926
+ {
927
+ method: "POST",
928
+ headers: {
929
+ ...this.defaultHeaders,
930
+ Authorization: `Bearer ${this.userToken}`,
931
+ },
932
+ body: JSON.stringify({
933
+ poll_id: pollId,
934
+ }),
880
935
  },
881
- body: JSON.stringify({
882
- poll_id: pollId,
883
- }),
884
- });
936
+ );
885
937
  const json = await res.json();
886
938
  return await json;
887
939
  } catch (err) {
@@ -891,22 +943,27 @@ class SidechatAPIClient {
891
943
  };
892
944
 
893
945
  /**
894
- * Uploads an asset to AWS S3 for use in posts and comments. Currently photos only
946
+ * 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)
895
947
  * @method
896
948
  * @param {String} uri - URI of the asset to upload
897
- * @param {String} mimeType - mimetype of the asset (e.g. "image/png")
949
+ * @param {String} mimeType - mimetype of the asset (e.g. "image/png" or "video/mp4")
898
950
  * @param {String} [name] - filename of the asset
951
+ * @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.
899
952
  * @returns {Promise<String>} URL of the uploaded asset
900
953
  * @since 2.5.1
901
954
  */
902
- uploadAsset = async (uri, mimeType, name = "") => {
955
+ uploadAsset = async (uri, mimeType, name = "", context = undefined) => {
903
956
  if (!this.userToken) {
904
957
  throw new SidechatAPIError("User is not authenticated.");
905
958
  }
906
959
 
907
960
  let imageType = mimeType.split("/")[1];
908
- if (!["png", "jpeg", "gif"].includes(imageType)) {
909
- throw new SidechatAPIError("Unsupported image format.");
961
+ if (imageType == "quicktime") imageType = "mov";
962
+ if (!["png", "jpeg", "gif", "webp", "mp4", "mov"].includes(imageType)) {
963
+ throw new SidechatAPIError("Unsupported file format.");
964
+ }
965
+ if (!context && ["mp4", "mov"].includes(imageType)) {
966
+ context = "post_vod_source";
910
967
  }
911
968
 
912
969
  const data = new FormData();
@@ -916,7 +973,11 @@ class SidechatAPIClient {
916
973
  uri: uri,
917
974
  });
918
975
 
919
- const urlReq = await this.sendRequest(`/v1/assets/upload_url?content_type=${imageType}`);
976
+ const urlReq = await this.sendRequest(
977
+ `/v1/assets/upload_url?content_type=${imageType}${
978
+ context ? "&context=" + context : ""
979
+ }`,
980
+ );
920
981
  const urlJson = await urlReq.json();
921
982
 
922
983
  try {
@@ -931,7 +992,7 @@ class SidechatAPIClient {
931
992
  return `${this.apiRoot}/v1/assets/library/${urlJson.asset_id}`;
932
993
  } else {
933
994
  throw new SidechatAPIError(
934
- `Couldn't upload image - error ${uploadReq.status}`
995
+ `Couldn't upload image - error ${uploadReq.status}`,
935
996
  );
936
997
  }
937
998
  } catch (e) {
@@ -997,7 +1058,7 @@ class SidechatAPIClient {
997
1058
  Authorization: `Bearer ${this.userToken}`,
998
1059
  },
999
1060
  body: JSON.stringify({
1000
- bio: bio
1061
+ bio: bio,
1001
1062
  }),
1002
1063
  });
1003
1064
  const json = await res.json();
@@ -1028,7 +1089,7 @@ class SidechatAPIClient {
1028
1089
  ...this.defaultHeaders,
1029
1090
  Authorization: `Bearer ${this.userToken}`,
1030
1091
  },
1031
- }
1092
+ },
1032
1093
  );
1033
1094
  if (res?.status == 200 || res?.status == 204) {
1034
1095
  return true;
@@ -1083,13 +1144,16 @@ class SidechatAPIClient {
1083
1144
  throw new SidechatAPIError("User is not authenticated.");
1084
1145
  }
1085
1146
  try {
1086
- const res = await fetch(`${this.apiRoot}/v1/groups/username?username=${username}`, {
1087
- method: "GET",
1088
- headers: {
1089
- ...this.defaultHeaders,
1090
- Authorization: `Bearer ${this.userToken}`,
1091
- }
1092
- });
1147
+ const res = await fetch(
1148
+ `${this.apiRoot}/v1/groups/username?username=${username}&cacheBust=${Date.now()}`,
1149
+ {
1150
+ method: "GET",
1151
+ headers: {
1152
+ ...this.defaultHeaders,
1153
+ Authorization: `Bearer ${this.userToken}`,
1154
+ },
1155
+ },
1156
+ );
1093
1157
  const json = await res.json();
1094
1158
  return await json.group;
1095
1159
  } catch (err) {
@@ -1110,13 +1174,16 @@ class SidechatAPIClient {
1110
1174
  throw new SidechatAPIError("User is not authenticated.");
1111
1175
  }
1112
1176
  try {
1113
- const res = await fetch(`${this.apiRoot}/v1/users/posts?username=${username}`, {
1114
- method: "GET",
1115
- headers: {
1116
- ...this.defaultHeaders,
1117
- Authorization: `Bearer ${this.userToken}`,
1118
- }
1119
- });
1177
+ const res = await fetch(
1178
+ `${this.apiRoot}/v1/users/posts?username=${username}`,
1179
+ {
1180
+ method: "GET",
1181
+ headers: {
1182
+ ...this.defaultHeaders,
1183
+ Authorization: `Bearer ${this.userToken}`,
1184
+ },
1185
+ },
1186
+ );
1120
1187
  const json = await res.json();
1121
1188
  return await json.posts;
1122
1189
  } catch (err) {
@@ -1164,13 +1231,16 @@ class SidechatAPIClient {
1164
1231
  throw new SidechatAPIError("User is not authenticated.");
1165
1232
  }
1166
1233
  try {
1167
- const res = await fetch(`${this.apiRoot}/v1/chats/explore`, {
1168
- method: "GET",
1169
- headers: {
1170
- ...this.defaultHeaders,
1171
- Authorization: `Bearer ${this.userToken}`,
1234
+ const res = await fetch(
1235
+ `${this.apiRoot}/v1/chats/explore?cacheBust=${Date.now()}`,
1236
+ {
1237
+ method: "GET",
1238
+ headers: {
1239
+ ...this.defaultHeaders,
1240
+ Authorization: `Bearer ${this.userToken}`,
1241
+ },
1172
1242
  },
1173
- });
1243
+ );
1174
1244
  const json = await res.json();
1175
1245
  return await json.chats;
1176
1246
  } catch (err) {
@@ -1194,7 +1264,7 @@ class SidechatAPIClient {
1194
1264
  displayName,
1195
1265
  emoji,
1196
1266
  primaryColor,
1197
- secondaryColor
1267
+ secondaryColor,
1198
1268
  ) => {
1199
1269
  if (!this.userToken) {
1200
1270
  throw new SidechatAPIError("User is not authenticated.");
@@ -1227,21 +1297,27 @@ class SidechatAPIClient {
1227
1297
  /**
1228
1298
  * Gets a list of the user's direct messages
1229
1299
  * @method
1300
+ * @param {types.SidechatCursorString} [cursor] - cursor string
1230
1301
  * @returns {Promise<types.SidechatDirectThread[]>}
1231
1302
  * @since 2.4.4
1232
1303
  */
1233
- getDMs = async () => {
1304
+ getDMs = async (cursor) => {
1234
1305
  if (!this.userToken) {
1235
1306
  throw new SidechatAPIError("User is not authenticated.");
1236
1307
  }
1237
1308
  try {
1238
- const res = await fetch(`${this.apiRoot}/v1/chats`, {
1239
- method: "GET",
1240
- headers: {
1241
- ...this.defaultHeaders,
1242
- Authorization: `Bearer ${this.userToken}`,
1309
+ const res = await fetch(
1310
+ `${this.apiRoot}/v1/chats${
1311
+ cursor ? "?cursor=" + encodeURIComponent(cursor) : ""
1312
+ }`,
1313
+ {
1314
+ method: "GET",
1315
+ headers: {
1316
+ ...this.defaultHeaders,
1317
+ Authorization: `Bearer ${this.userToken}`,
1318
+ },
1243
1319
  },
1244
- });
1320
+ );
1245
1321
  const json = await res.json();
1246
1322
  const list = await json.chats;
1247
1323
  let r = [];
@@ -1275,7 +1351,7 @@ class SidechatAPIClient {
1275
1351
  ...this.defaultHeaders,
1276
1352
  Authorization: `Bearer ${this.userToken}`,
1277
1353
  },
1278
- }
1354
+ },
1279
1355
  );
1280
1356
  const json = await res.json();
1281
1357
  return await json.chat;
@@ -1337,7 +1413,7 @@ class SidechatAPIClient {
1337
1413
  clientID,
1338
1414
  postID,
1339
1415
  anonymous = false,
1340
- postContext = "feed"
1416
+ postContext = "feed",
1341
1417
  ) => {
1342
1418
  if (!this.userToken) {
1343
1419
  throw new SidechatAPIError("User is not authenticated.");
@@ -1370,6 +1446,7 @@ class SidechatAPIClient {
1370
1446
  * @method
1371
1447
  * @param {String} postID - alphanumeric ID of post to hide
1372
1448
  * @since 2.6.2
1449
+ * @deprecated since 2.7.0. Please use `blockUser` instead!
1373
1450
  */
1374
1451
  hidePostsFromUser = async (postID) => {
1375
1452
  if (!this.userToken) {
@@ -1385,7 +1462,7 @@ class SidechatAPIClient {
1385
1462
  body: JSON.stringify({
1386
1463
  post_id: postID,
1387
1464
  post_context: "feed",
1388
- report: false
1465
+ report: false,
1389
1466
  }),
1390
1467
  });
1391
1468
  const json = await res.json();
@@ -1400,6 +1477,7 @@ class SidechatAPIClient {
1400
1477
  * Unhides all posts from all users
1401
1478
  * @method
1402
1479
  * @since 2.6.2
1480
+ * @deprecated since 2.7.0. No longer used by the official app and may stop working.
1403
1481
  */
1404
1482
  unhidePostsFromAllUsers = async () => {
1405
1483
  if (!this.userToken) {
@@ -1421,6 +1499,396 @@ class SidechatAPIClient {
1421
1499
  throw new SidechatAPIError(`Failed to unhide post.`);
1422
1500
  }
1423
1501
  };
1502
+
1503
+ /**
1504
+ * Fetches the home feed, which mixes posts from all of the user's groups
1505
+ * @method
1506
+ * @param {"for_you"|"hot"|"recent"} [type] - how the feed should be sorted
1507
+ * @param {String} [schoolGroupID] - alphanumeric ID of the user's school group
1508
+ * @param {types.SidechatCursorString} [cursor] - cursor string
1509
+ * @returns {Promise<types.SidechatFeed>} feed items and cursor
1510
+ * @since 2.7.0
1511
+ */
1512
+ getHomeFeed = async (type = "for_you", schoolGroupID, cursor) => {
1513
+ if (!this.userToken) {
1514
+ throw new SidechatAPIError("User is not authenticated.");
1515
+ }
1516
+ try {
1517
+ const params = new URLSearchParams({ type: type });
1518
+ if (schoolGroupID) params.set("school_group_id", schoolGroupID);
1519
+ if (cursor) params.set("cursor", cursor);
1520
+ const res = await this.sendRequest(`/v1/posts/home?${params}`);
1521
+ return await res.json();
1522
+ } catch (err) {
1523
+ console.error(err);
1524
+ throw new SidechatAPIError(`Failed to get home feed.`);
1525
+ }
1526
+ };
1527
+
1528
+ /**
1529
+ * Follows or unfollows a post. Followed posts send activity notifications when they get new comments.
1530
+ * @method
1531
+ * @param {String} postID - alphanumeric ID of post
1532
+ * @param {Boolean} following - whether or not the user should follow the post
1533
+ * @returns {Promise<{post: types.SidechatPostOrComment}>} the updated post
1534
+ * @since 2.7.0
1535
+ */
1536
+ setPostFollowing = async (postID, following) => {
1537
+ if (!this.userToken) {
1538
+ throw new SidechatAPIError("User is not authenticated.");
1539
+ }
1540
+ try {
1541
+ const res = await this.sendRequest(
1542
+ `/v1/posts/${following ? "follow" : "unfollow"}`,
1543
+ "POST",
1544
+ JSON.stringify({ post_id: postID }),
1545
+ );
1546
+ return await res.json();
1547
+ } catch (err) {
1548
+ console.error(err);
1549
+ throw new SidechatAPIError(`Failed to change post follow status.`);
1550
+ }
1551
+ };
1552
+
1553
+ /**
1554
+ * Reports a post or comment to the group's moderators
1555
+ * @method
1556
+ * @param {String} postID - alphanumeric ID of post or comment to report
1557
+ * @param {types.SidechatReportReason} reason - why the content is being reported
1558
+ * @param {String} [note] - explanation to send along with the report
1559
+ * @since 2.7.0
1560
+ */
1561
+ flagPost = async (postID, reason, note = undefined) => {
1562
+ if (!this.userToken) {
1563
+ throw new SidechatAPIError("User is not authenticated.");
1564
+ }
1565
+ try {
1566
+ const res = await this.sendRequest(
1567
+ "/v1/posts/flag",
1568
+ "POST",
1569
+ JSON.stringify({ post_id: postID, reason: reason, note: note }),
1570
+ );
1571
+ return await res.json();
1572
+ } catch (err) {
1573
+ console.error(err);
1574
+ throw new SidechatAPIError(`Failed to report post.`);
1575
+ }
1576
+ };
1577
+
1578
+ /**
1579
+ * Blocks the author of a post or comment, hiding all of their content from the user. Replaces `hidePostsFromUser`.
1580
+ * @method
1581
+ * @param {String} postID - alphanumeric ID of a post or comment by the user to block
1582
+ * @param {Boolean} [report] - whether or not to also report the content
1583
+ * @param {"feed"|"chat"} [postContext] - where the content was seen
1584
+ * @since 2.7.0
1585
+ */
1586
+ blockUser = async (postID, report = false, postContext = "feed") => {
1587
+ if (!this.userToken) {
1588
+ throw new SidechatAPIError("User is not authenticated.");
1589
+ }
1590
+ try {
1591
+ const res = await this.sendRequest(
1592
+ "/v1/posts/block_user",
1593
+ "POST",
1594
+ JSON.stringify({
1595
+ post_id: postID,
1596
+ post_context: postContext,
1597
+ report: report,
1598
+ }),
1599
+ );
1600
+ return await res.json();
1601
+ } catch (err) {
1602
+ console.error(err);
1603
+ throw new SidechatAPIError(`Failed to block user.`);
1604
+ }
1605
+ };
1606
+
1607
+ /**
1608
+ * Creates a link attachment that can be added to a post with createPost(). YouTube links are recognized and shown as embedded videos.
1609
+ * @method
1610
+ * @param {String} url - URL to attach
1611
+ * @returns {Promise<types.SidechatAttachment>} the created attachment
1612
+ * @since 2.7.0
1613
+ */
1614
+ createLinkAttachment = async (url) => {
1615
+ if (!this.userToken) {
1616
+ throw new SidechatAPIError("User is not authenticated.");
1617
+ }
1618
+ try {
1619
+ const res = await this.sendRequest(
1620
+ "/v1/attachments/upload_link",
1621
+ "POST",
1622
+ JSON.stringify({ link_url: url }),
1623
+ );
1624
+ return await res.json();
1625
+ } catch (err) {
1626
+ console.error(err);
1627
+ throw new SidechatAPIError(`Failed to create link attachment.`);
1628
+ }
1629
+ };
1630
+
1631
+ /**
1632
+ * Gets the user's activity feed (notifications for comments, votes, followers, etc.)
1633
+ * @method
1634
+ * @param {types.SidechatCursorString} [cursor] - cursor string
1635
+ * @returns {Promise<types.SidechatActivityList>} activity items and cursor
1636
+ * @since 2.7.0
1637
+ */
1638
+ getActivity = async (cursor) => {
1639
+ if (!this.userToken) {
1640
+ throw new SidechatAPIError("User is not authenticated.");
1641
+ }
1642
+ try {
1643
+ const res = await this.sendRequest(
1644
+ `/v1/activity${cursor ? "?cursor=" + encodeURIComponent(cursor) : ""}`,
1645
+ );
1646
+ return await res.json();
1647
+ } catch (err) {
1648
+ console.error(err);
1649
+ throw new SidechatAPIError(`Failed to get activity.`);
1650
+ }
1651
+ };
1652
+
1653
+ /**
1654
+ * Gets the groups the user is a member of
1655
+ * @method
1656
+ * @returns {Promise<types.SidechatGroup[]>}
1657
+ * @since 2.7.0
1658
+ */
1659
+ getGroups = async () => {
1660
+ if (!this.userToken) {
1661
+ throw new SidechatAPIError("User is not authenticated.");
1662
+ }
1663
+ try {
1664
+ const res = await this.sendRequest("/v1/groups");
1665
+ const json = await res.json();
1666
+ return json.groups;
1667
+ } catch (err) {
1668
+ console.error(err);
1669
+ throw new SidechatAPIError(`Failed to get groups.`);
1670
+ }
1671
+ };
1672
+
1673
+ /**
1674
+ * Gets groups suggested for the user to join
1675
+ * @method
1676
+ * @param {Number} [limit] - maximum number of groups to return
1677
+ * @returns {Promise<types.SidechatGroup[]>}
1678
+ * @since 2.7.0
1679
+ */
1680
+ getSuggestedGroups = async (limit = 10) => {
1681
+ if (!this.userToken) {
1682
+ throw new SidechatAPIError("User is not authenticated.");
1683
+ }
1684
+ try {
1685
+ const res = await this.sendRequest(
1686
+ `/v1/groups/suggestions?limit=${limit}`,
1687
+ );
1688
+ const json = await res.json();
1689
+ return json.groups;
1690
+ } catch (err) {
1691
+ console.error(err);
1692
+ throw new SidechatAPIError(`Failed to get suggested groups.`);
1693
+ }
1694
+ };
1695
+
1696
+ /**
1697
+ * Gets a shareable invite link for a group. School groups don't have invite links and will throw an error.
1698
+ * @method
1699
+ * @param {String} groupID - alphanumeric ID of group
1700
+ * @returns {Promise<String>} invite link URL
1701
+ * @since 2.7.0
1702
+ */
1703
+ getGroupInviteLink = async (groupID) => {
1704
+ if (!this.userToken) {
1705
+ throw new SidechatAPIError("User is not authenticated.");
1706
+ }
1707
+ try {
1708
+ const res = await this.sendRequest(
1709
+ `/v1/groups/invite_link?group_id=${groupID}`,
1710
+ );
1711
+ const json = await res.json();
1712
+ return json.link;
1713
+ } catch (err) {
1714
+ console.error(err);
1715
+ throw new SidechatAPIError(`Failed to get invite link.`);
1716
+ }
1717
+ };
1718
+
1719
+ /**
1720
+ * Gets the yakarma leaderboard for a group, along with the user's own karma
1721
+ * @method
1722
+ * @param {String} groupID - alphanumeric ID of group
1723
+ * @returns {Promise<types.SidechatLeaderboard>}
1724
+ * @since 2.7.0
1725
+ */
1726
+ getLeaderboard = async (groupID) => {
1727
+ if (!this.userToken) {
1728
+ throw new SidechatAPIError("User is not authenticated.");
1729
+ }
1730
+ try {
1731
+ const res = await this.sendRequest(`/v1/leaderboard?group_id=${groupID}`);
1732
+ return await res.json();
1733
+ } catch (err) {
1734
+ console.error(err);
1735
+ throw new SidechatAPIError(`Failed to get leaderboard.`);
1736
+ }
1737
+ };
1738
+
1739
+ /**
1740
+ * Gets the existing direct message thread for a post or comment. Returns undefined if the user hasn't started one.
1741
+ * @method
1742
+ * @param {String} postID - alphanumeric ID of post or comment
1743
+ * @returns {Promise<types.SidechatDirectThread>}
1744
+ * @since 2.7.0
1745
+ */
1746
+ getDMForPost = async (postID) => {
1747
+ if (!this.userToken) {
1748
+ throw new SidechatAPIError("User is not authenticated.");
1749
+ }
1750
+ try {
1751
+ const res = await this.sendRequest(`/v1/chats/by_post?post_id=${postID}`);
1752
+ const json = await res.json();
1753
+ return json.chat;
1754
+ } catch (err) {
1755
+ console.error(err);
1756
+ throw new SidechatAPIError(`Failed to get DM thread for post.`);
1757
+ }
1758
+ };
1759
+
1760
+ /**
1761
+ * Reports a direct message thread
1762
+ * @method
1763
+ * @param {String} chatID - alphanumeric ID of the thread to report
1764
+ * @param {types.SidechatReportReason} reason - why the thread is being reported
1765
+ * @since 2.7.0
1766
+ */
1767
+ reportDM = async (chatID, reason) => {
1768
+ if (!this.userToken) {
1769
+ throw new SidechatAPIError("User is not authenticated.");
1770
+ }
1771
+ try {
1772
+ const res = await this.sendRequest(
1773
+ "/v1/chats/report",
1774
+ "POST",
1775
+ JSON.stringify({ chat_id: chatID, reason: reason }),
1776
+ );
1777
+ return await res.json();
1778
+ } catch (err) {
1779
+ console.error(err);
1780
+ throw new SidechatAPIError(`Failed to report DM.`);
1781
+ }
1782
+ };
1783
+
1784
+ /**
1785
+ * Sets which push notifications the user receives
1786
+ * @method
1787
+ * @param {types.SidechatNotificationPreferences} preferences - notification types to enable or disable
1788
+ * @since 2.7.0
1789
+ */
1790
+ setNotificationPreferences = async (preferences) => {
1791
+ if (!this.userToken) {
1792
+ throw new SidechatAPIError("User is not authenticated.");
1793
+ }
1794
+ try {
1795
+ const res = await this.sendRequest(
1796
+ "/v1/users/set_notification_preferences",
1797
+ "POST",
1798
+ JSON.stringify({ notification_preferences: preferences }),
1799
+ );
1800
+ return await res.json();
1801
+ } catch (err) {
1802
+ console.error(err);
1803
+ throw new SidechatAPIError(`Failed to set notification preferences.`);
1804
+ }
1805
+ };
1806
+
1807
+ /**
1808
+ * 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.
1809
+ * @method
1810
+ * @param {String} authToken - token from the Google or Microsoft sign-in
1811
+ * @param {"google"|"microsoft"} authType - provider the token came from
1812
+ * @param {Boolean} [isIncomingFreshman] - whether or not the user is an incoming freshman
1813
+ * @since 2.7.0
1814
+ * @tutorial Email Registration
1815
+ */
1816
+ verifyEmailWithThirdParty = async (
1817
+ authToken,
1818
+ authType,
1819
+ isIncomingFreshman = false,
1820
+ ) => {
1821
+ if (!this.userToken) {
1822
+ throw new SidechatAPIError("User is not authenticated.");
1823
+ }
1824
+ try {
1825
+ const res = await this.sendRequest(
1826
+ "/v1/users/third_party_email_verified",
1827
+ "POST",
1828
+ JSON.stringify({
1829
+ auth: authToken,
1830
+ auth_type: authType,
1831
+ is_incoming_freshman: isIncomingFreshman,
1832
+ }),
1833
+ );
1834
+ const json = await res.json();
1835
+ if (json.verified_email_updates_response) {
1836
+ return json.verified_email_updates_response;
1837
+ } else {
1838
+ throw new SidechatAPIError(json?.message || "Email is not verified.");
1839
+ }
1840
+ } catch (err) {
1841
+ console.error(err);
1842
+ throw new SidechatAPIError("Failed to verify email.");
1843
+ }
1844
+ };
1845
+
1846
+ /**
1847
+ * Authorizes a realtime (Pusher) connection to subscribe to a private channel, such as `private-chat-<chatID>` for live direct messages. Pass the result to your Pusher client's channel authorizer. This hasn't been tested, and is mostly theoretical.
1848
+ * @method
1849
+ * @param {String} socketID - socket ID assigned by Pusher
1850
+ * @param {String} channelName - name of the private channel to subscribe to
1851
+ * @returns {Promise<{auth: String}>} signed channel authorization
1852
+ * @since 2.7.0
1853
+ */
1854
+ authorizeRealtimeChannel = async (socketID, channelName) => {
1855
+ if (!this.userToken) {
1856
+ throw new SidechatAPIError("User is not authenticated.");
1857
+ }
1858
+ try {
1859
+ const res = await this.sendRequest(
1860
+ "/pusher/auth",
1861
+ "POST",
1862
+ JSON.stringify({ socket_id: socketID, channel_name: channelName }),
1863
+ );
1864
+ return await res.json();
1865
+ } catch (err) {
1866
+ console.error(err);
1867
+ throw new SidechatAPIError(`Failed to authorize realtime channel.`);
1868
+ }
1869
+ };
1870
+
1871
+ /**
1872
+ * Permanently deletes the current user's account. This cannot be undone!
1873
+ * @method
1874
+ * @since 2.7.0
1875
+ */
1876
+ deleteAccount = async () => {
1877
+ if (!this.userToken) {
1878
+ throw new SidechatAPIError("User is not authenticated.");
1879
+ }
1880
+ try {
1881
+ const res = await this.sendRequest(
1882
+ "/v1/users/delete",
1883
+ "POST",
1884
+ JSON.stringify({}),
1885
+ );
1886
+ return await res.json();
1887
+ } catch (err) {
1888
+ console.error(err);
1889
+ throw new SidechatAPIError(`Failed to delete account.`);
1890
+ }
1891
+ };
1424
1892
  }
1425
1893
 
1426
1894
  export default SidechatAPIClient;