sidechat.js 1.0.0 → 2.0.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 (90) hide show
  1. package/.github/workflows/static.yml +51 -0
  2. package/README.md +15 -1
  3. package/dist/sidechat.cjs +1 -1
  4. package/dist/sidechat.cjs.map +1 -1
  5. package/dist/sidechat.modern.js +1 -1
  6. package/dist/sidechat.modern.js.map +1 -1
  7. package/dist/sidechat.module.js +1 -1
  8. package/dist/sidechat.module.js.map +1 -1
  9. package/dist/sidechat.umd.js +1 -1
  10. package/dist/sidechat.umd.js.map +1 -1
  11. package/docs/sidechat.js/1.0.0/global.html +1 -1
  12. package/docs/sidechat.js/1.0.0/index.html +2 -1
  13. package/docs/sidechat.js/1.0.0/index.js.html +1 -3
  14. package/docs/sidechat.js/1.0.0/types_SidechatTypes.js.html +1 -1
  15. package/docs/sidechat.js/2.0.0/SidechatAPIClient.html +3 -0
  16. package/docs/sidechat.js/2.0.0/SidechatAPIError.html +3 -0
  17. package/docs/sidechat.js/2.0.0/classes_SidechatAPIClient.js.html +425 -0
  18. package/docs/sidechat.js/2.0.0/classes_SidechatAPIError.js.html +16 -0
  19. package/docs/sidechat.js/2.0.0/data/search.json +1 -0
  20. package/docs/sidechat.js/2.0.0/fonts/Inconsolata-Regular.ttf +0 -0
  21. package/docs/sidechat.js/2.0.0/fonts/OpenSans-Regular.ttf +0 -0
  22. package/docs/sidechat.js/2.0.0/fonts/WorkSans-Bold.ttf +0 -0
  23. package/docs/sidechat.js/2.0.0/global.html +3 -0
  24. package/docs/sidechat.js/2.0.0/index.html +4 -0
  25. package/docs/sidechat.js/2.0.0/scripts/core.js +702 -0
  26. package/docs/sidechat.js/2.0.0/scripts/core.min.js +23 -0
  27. package/docs/sidechat.js/2.0.0/scripts/resize.js +90 -0
  28. package/docs/sidechat.js/2.0.0/scripts/search.js +265 -0
  29. package/docs/sidechat.js/2.0.0/scripts/search.min.js +6 -0
  30. package/docs/sidechat.js/2.0.0/scripts/third-party/Apache-License-2.0.txt +202 -0
  31. package/docs/sidechat.js/2.0.0/scripts/third-party/fuse.js +9 -0
  32. package/docs/sidechat.js/2.0.0/scripts/third-party/hljs-line-num-original.js +369 -0
  33. package/docs/sidechat.js/2.0.0/scripts/third-party/hljs-line-num.js +1 -0
  34. package/docs/sidechat.js/2.0.0/scripts/third-party/hljs-original.js +5171 -0
  35. package/docs/sidechat.js/2.0.0/scripts/third-party/hljs.js +1 -0
  36. package/docs/sidechat.js/2.0.0/scripts/third-party/popper.js +5 -0
  37. package/docs/sidechat.js/2.0.0/scripts/third-party/tippy.js +1 -0
  38. package/docs/sidechat.js/2.0.0/scripts/third-party/tocbot.js +672 -0
  39. package/docs/sidechat.js/2.0.0/scripts/third-party/tocbot.min.js +1 -0
  40. package/docs/sidechat.js/2.0.0/styles/clean-jsdoc-theme-base.css +975 -0
  41. package/docs/sidechat.js/2.0.0/styles/clean-jsdoc-theme-dark.css +407 -0
  42. package/docs/sidechat.js/2.0.0/styles/clean-jsdoc-theme-light.css +388 -0
  43. package/docs/sidechat.js/2.0.0/styles/clean-jsdoc-theme.min.css +1 -0
  44. package/docs/sidechat.js/2.0.0/tutorial-Authentication.html +13 -0
  45. package/docs/sidechat.js/2.0.0/tutorial-Email Registration.html +6 -0
  46. package/docs/sidechat.js/2.0.0/types_SidechatTypes.js.html +98 -0
  47. package/docs/sidechat.js/2.0.0-alpha.0/SidechatAPIClient.html +3 -0
  48. package/docs/sidechat.js/2.0.0-alpha.0/SidechatAPIError.html +3 -0
  49. package/docs/sidechat.js/2.0.0-alpha.0/classes_SidechatAPIClient.js.html +418 -0
  50. package/docs/sidechat.js/2.0.0-alpha.0/classes_SidechatAPIError.js.html +16 -0
  51. package/docs/sidechat.js/2.0.0-alpha.0/data/search.json +1 -0
  52. package/docs/sidechat.js/2.0.0-alpha.0/fonts/Inconsolata-Regular.ttf +0 -0
  53. package/docs/sidechat.js/2.0.0-alpha.0/fonts/OpenSans-Regular.ttf +0 -0
  54. package/docs/sidechat.js/2.0.0-alpha.0/fonts/WorkSans-Bold.ttf +0 -0
  55. package/docs/sidechat.js/2.0.0-alpha.0/global.html +3 -0
  56. package/docs/sidechat.js/2.0.0-alpha.0/index.html +4 -0
  57. package/docs/sidechat.js/2.0.0-alpha.0/scripts/core.js +702 -0
  58. package/docs/sidechat.js/2.0.0-alpha.0/scripts/core.min.js +23 -0
  59. package/docs/sidechat.js/2.0.0-alpha.0/scripts/resize.js +90 -0
  60. package/docs/sidechat.js/2.0.0-alpha.0/scripts/search.js +265 -0
  61. package/docs/sidechat.js/2.0.0-alpha.0/scripts/search.min.js +6 -0
  62. package/docs/sidechat.js/2.0.0-alpha.0/scripts/third-party/Apache-License-2.0.txt +202 -0
  63. package/docs/sidechat.js/2.0.0-alpha.0/scripts/third-party/fuse.js +9 -0
  64. package/docs/sidechat.js/2.0.0-alpha.0/scripts/third-party/hljs-line-num-original.js +369 -0
  65. package/docs/sidechat.js/2.0.0-alpha.0/scripts/third-party/hljs-line-num.js +1 -0
  66. package/docs/sidechat.js/2.0.0-alpha.0/scripts/third-party/hljs-original.js +5171 -0
  67. package/docs/sidechat.js/2.0.0-alpha.0/scripts/third-party/hljs.js +1 -0
  68. package/docs/sidechat.js/2.0.0-alpha.0/scripts/third-party/popper.js +5 -0
  69. package/docs/sidechat.js/2.0.0-alpha.0/scripts/third-party/tippy.js +1 -0
  70. package/docs/sidechat.js/2.0.0-alpha.0/scripts/third-party/tocbot.js +672 -0
  71. package/docs/sidechat.js/2.0.0-alpha.0/scripts/third-party/tocbot.min.js +1 -0
  72. package/docs/sidechat.js/2.0.0-alpha.0/styles/clean-jsdoc-theme-base.css +975 -0
  73. package/docs/sidechat.js/2.0.0-alpha.0/styles/clean-jsdoc-theme-dark.css +407 -0
  74. package/docs/sidechat.js/2.0.0-alpha.0/styles/clean-jsdoc-theme-light.css +388 -0
  75. package/docs/sidechat.js/2.0.0-alpha.0/styles/clean-jsdoc-theme.min.css +1 -0
  76. package/docs/sidechat.js/2.0.0-alpha.0/tutorial-Authentication.html +13 -0
  77. package/docs/sidechat.js/2.0.0-alpha.0/tutorial-Email Registration.html +6 -0
  78. package/docs/sidechat.js/2.0.0-alpha.0/tutorial-Email Verification.html +6 -0
  79. package/docs/sidechat.js/2.0.0-alpha.0/types_SidechatTypes.js.html +98 -0
  80. package/docs/tutorials/Authentication.md +46 -0
  81. package/docs/tutorials/Email Registration.md +16 -0
  82. package/jsdoc.json +8 -4
  83. package/package.json +5 -4
  84. package/src/classes/SidechatAPIClient.js +422 -0
  85. package/{classes → src/classes}/SidechatAPIError.js +5 -0
  86. package/src/index.js +4 -0
  87. package/src/tests/basicTest.js +19 -0
  88. package/{types → src/types}/SidechatTypes.js +17 -1
  89. package/index.js +0 -362
  90. package/tests/basicTest.js +0 -8
@@ -0,0 +1,422 @@
1
+ import "../types/SidechatTypes.js";
2
+ import SidechatAPIError from "../classes/SidechatAPIError.js";
3
+
4
+ /**
5
+ * API client class for making requests to Sidechat's private API. You'll need to [authenticate]{@tutorial Authentication} before using most of the methods.
6
+ * @class
7
+ * @since 2.0.0-alpha.0
8
+ */
9
+ class SidechatAPIClient {
10
+ /**
11
+ * User bearer token
12
+ * @type {SidechatAuthToken}
13
+ * */
14
+ userToken;
15
+ /**
16
+ * Default headers for every API request
17
+ * @type {Object}
18
+ * @static
19
+ */
20
+ defaultHeaders = {
21
+ Accept: "application/json",
22
+ "Content-Type": "application/json",
23
+ };
24
+
25
+ /**
26
+ * Create a new instance of the API client
27
+ * @param {SidechatAuthToken} [token] - user bearer token
28
+ */
29
+ constructor(token = "") {
30
+ if (token) {
31
+ this.userToken = token;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * 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
37
+ * @method
38
+ * @param {SidechatAuthToken} token - user bearer token
39
+ */
40
+ setToken = (token) => {
41
+ this.userToken = token;
42
+ };
43
+
44
+ /**
45
+ * Initiate the login process with a phone number. Should be followed up with verifySMSCode().
46
+ * @method
47
+ * @param {Number} phoneNumber - US phone number (WITHOUT +1) to send verification code to
48
+ */
49
+ loginViaSMS = async (phoneNumber) => {
50
+ try {
51
+ const res = await fetch(`https://api.sidechat.lol/v1/login_register`, {
52
+ method: "POST",
53
+ headers: this.defaultHeaders,
54
+ body: JSON.stringify({
55
+ phone_number: `+1${phoneNumber}`,
56
+ version: 3,
57
+ }),
58
+ });
59
+ const json = await res.json();
60
+ return json;
61
+ } catch (err) {
62
+ console.error(err);
63
+ throw new SidechatAPIError("Failed to request SMS verification.");
64
+ }
65
+ };
66
+
67
+ /**
68
+ * Verify the code sent via SMS with loginViaSMS().
69
+ * @method
70
+ * @param {Number} phoneNumber - US phone number (WITHOUT +1) that verification code was sent to
71
+ * @param {String} code - the verification code
72
+ */
73
+ verifySMSCode = async (phoneNumber, code) => {
74
+ try {
75
+ const res = await fetch(
76
+ `https://api.sidechat.lol/v1/verify_phone_number`,
77
+ {
78
+ method: "POST",
79
+ headers: this.defaultHeaders,
80
+ body: JSON.stringify({
81
+ phone_number: `+1${phoneNumber}`,
82
+ code: code.toUpperCase(),
83
+ }),
84
+ }
85
+ );
86
+ const json = await res.json();
87
+ if (json?.logged_in_user?.token) {
88
+ this.userToken = json.logged_in_user.token;
89
+ }
90
+ return json;
91
+ } catch (err) {
92
+ console.error(err);
93
+ throw new SidechatAPIError("Failed verify this code.");
94
+ }
95
+ };
96
+
97
+ /**
98
+ * Set the user's age
99
+ * @method
100
+ * @param {Number} age - user's age in years
101
+ * @param {String} registrationID - the registration ID generated by verifySMSCode()
102
+ */
103
+ setAge = async (age, registrationID) => {
104
+ if (age < 13) {
105
+ throw new SidechatAPIError("You're too young to use Offsides.");
106
+ }
107
+ try {
108
+ const res = await fetch(
109
+ `https://api.sidechat.lol/v1/complete_registration`,
110
+ {
111
+ method: "POST",
112
+ headers: this.defaultHeaders,
113
+ body: JSON.stringify({
114
+ age: Number(age),
115
+ registration_id: registrationID,
116
+ }),
117
+ }
118
+ );
119
+ const json = await res.json();
120
+ if (json.token) {
121
+ this.userToken = json.token;
122
+ }
123
+ return json;
124
+ } catch (err) {
125
+ console.error(err);
126
+ throw new SidechatAPIError("Failed verify this code.");
127
+ }
128
+ };
129
+
130
+ /**
131
+ * Initiate the email setup process. Should be followed up with checkEmailVerification().
132
+ * @method
133
+ * @param {String} email - school email address to send verification code to
134
+ * @tutorial Email Registration
135
+ */
136
+ registerEmail = async (email) => {
137
+ if (!this.userToken) {
138
+ throw new SidechatAPIError("User is not authenticated.");
139
+ }
140
+ try {
141
+ const res = await fetch(
142
+ `https://api.sidechat.lol/v2/users/register_email`,
143
+ {
144
+ method: "POST",
145
+ headers: {
146
+ ...this.defaultHeaders,
147
+ Authorization: `Bearer ${this.userToken}`,
148
+ },
149
+ body: JSON.stringify({
150
+ email: email,
151
+ }),
152
+ }
153
+ );
154
+ const json = await res.json();
155
+ if (json.message) {
156
+ throw new SidechatAPIError(json.message);
157
+ }
158
+ return json;
159
+ } catch (err) {
160
+ console.error(error);
161
+ throw new SidechatAPIError("Failed to request email verification.");
162
+ }
163
+ };
164
+
165
+ /**
166
+ * Check is the user's email is verified.
167
+ * @method
168
+ */
169
+ checkEmailVerification = async () => {
170
+ if (!this.userToken) {
171
+ throw new SidechatAPIError("User is not authenticated.");
172
+ }
173
+ try {
174
+ const res = await fetch(
175
+ `https://api.sidechat.lol/v1/users/check_email_verified`,
176
+ {
177
+ method: "GET",
178
+ headers: {
179
+ ...this.defaultHeaders,
180
+ Authorization: `Bearer ${this.userToken}`,
181
+ },
182
+ }
183
+ );
184
+ const json = await res.json();
185
+ if (json.verified_email_updates_response) {
186
+ return json.verified_email_updates_response;
187
+ } else {
188
+ throw new SidechatAPIError(json?.message || "Email is not verified.");
189
+ }
190
+ } catch (err) {
191
+ console.error(error);
192
+ throw new SidechatAPIError("Email is not verified.");
193
+ }
194
+ };
195
+
196
+ /**
197
+ * Set the device ID of the current user
198
+ * @method
199
+ * @param {String} deviceId - the device ID to set
200
+ */
201
+ setDeviceID = async (deviceID) => {
202
+ if (!this.userToken) {
203
+ throw new SidechatAPIError("User is not authenticated.");
204
+ }
205
+ try {
206
+ const res = await fetch(
207
+ `https://api.sidechat.lol/v1/register_device_token`,
208
+ {
209
+ method: "POST",
210
+ headers: {
211
+ ...this.defaultHeaders,
212
+ Authorization: `Bearer ${this.userToken}`,
213
+ },
214
+ body: JSON.stringify({
215
+ build_type: "release",
216
+ bundle_id: "com.flowerave.sidechat",
217
+ device_token: deviceID,
218
+ }),
219
+ }
220
+ );
221
+ const json = await res.json();
222
+ await AsyncStorage.setItem("deviceID", deviceID);
223
+ return json;
224
+ } catch (err) {
225
+ console.error(err);
226
+ throw new SidechatAPIError("Failed verify this code.");
227
+ }
228
+ };
229
+
230
+ /**
231
+ * Get updated status for user and group
232
+ * @method
233
+ * @param {String} [groupID] - ID of a specific group to retrieve info from
234
+ */
235
+ getUserAndGroup = async (groupID = "") => {
236
+ if (!this.userToken) {
237
+ throw new SidechatAPIError("User is not authenticated.");
238
+ }
239
+ try {
240
+ const res = await fetch(
241
+ `https://api.sidechat.lol/v1/updates?group_id=${groupID}`,
242
+ {
243
+ method: "GET",
244
+ headers: {
245
+ ...this.defaultHeaders,
246
+ Authorization: `Bearer ${this.userToken}`,
247
+ },
248
+ }
249
+ );
250
+ const json = await res.json();
251
+ return json;
252
+ } catch (err) {
253
+ console.error(err);
254
+ throw new SidechatAPIError(`Failed to get posts from group.`);
255
+ }
256
+ };
257
+
258
+ /**
259
+ * Fetches posts from the specified category in a group
260
+ * @method
261
+ * @param {String} groupID - group ID
262
+ * @param {"hot"|"recent"|"top"} category - category to filter posts
263
+ * @param {SidechatCursorString} [cursor] - cursor string
264
+ * @returns {SidechatPostsAndCursor} List of posts and cursor
265
+ */
266
+ getGroupPosts = async (groupID, category = "hot", cursor) => {
267
+ if (!this.userToken) {
268
+ throw new SidechatAPIError("User is not authenticated.");
269
+ }
270
+ try {
271
+ const res = await fetch(
272
+ `https://api.sidechat.lol/v1/posts?group_id=${groupID}&type=${category}${
273
+ cursor ? "&cursor=" + cursor : ""
274
+ }`,
275
+ {
276
+ method: "GET",
277
+ headers: {
278
+ ...this.defaultHeaders,
279
+ Authorization: `Bearer ${this.userToken}`,
280
+ },
281
+ }
282
+ );
283
+ const json = await res.json();
284
+ return json;
285
+ } catch (err) {
286
+ console.error(err);
287
+ throw new SidechatAPIError(`Failed to get posts from group.`);
288
+ }
289
+ };
290
+
291
+ /**
292
+ * Upvote or downvote, or unvote a post
293
+ * @method
294
+ * @param {String} postID - post ID to vote on
295
+ * @param {SidechatVoteString} action - whether to upvote, downvote, or reset vote
296
+ */
297
+ setVote = async (postID, action) => {
298
+ if (!this.userToken) {
299
+ throw new SidechatAPIError("User is not authenticated.");
300
+ }
301
+ try {
302
+ const res = await fetch(`https://api.sidechat.lol/v1/posts/set_vote`, {
303
+ method: "POST",
304
+ headers: {
305
+ ...this.defaultHeaders,
306
+ Authorization: `Bearer ${this.userToken}`,
307
+ },
308
+ body: JSON.stringify({
309
+ post_id: postID,
310
+ vote_status: action,
311
+ }),
312
+ });
313
+ const json = await res.json();
314
+ return json;
315
+ } catch (err) {
316
+ console.error(err);
317
+ throw new SidechatAPIError(`Failed to change the vote on post.`);
318
+ }
319
+ };
320
+
321
+ /**
322
+ * Get all the commments on a post
323
+ * @method
324
+ * @param {String} postID - post ID to get comments for
325
+ * @returns {SidechatPostOrComment[]} list of comments
326
+ */
327
+ getPostComments = async (postID) => {
328
+ if (!this.userToken) {
329
+ throw new SidechatAPIError("User is not authenticated.");
330
+ }
331
+ try {
332
+ const res = await fetch(
333
+ `https://api.sidechat.lol/v1/posts/comments/?post_id=${postID}`,
334
+ {
335
+ method: "GET",
336
+ headers: {
337
+ ...this.defaultHeaders,
338
+ Authorization: `Bearer ${this.userToken}`,
339
+ },
340
+ }
341
+ );
342
+ const json = await res.json();
343
+ // Function to preprocess the comments and organize them into a nested structure
344
+ function preprocessComments(apiComments) {
345
+ // Map to store comments by their IDs for efficient lookup
346
+ const commentMap = new Map();
347
+ // List to store top-level comments
348
+ const topLevelComments = [];
349
+
350
+ // Iterate through the API comments
351
+ apiComments.forEach((comment) => {
352
+ // Store the comment in the map with its ID as the key
353
+ commentMap.set(comment.id, comment);
354
+ // Get the parent comment using the reply_post_id
355
+ const parentComment = commentMap.get(comment.reply_post_id);
356
+ // Check if the comment is a top-level comment
357
+ if (
358
+ !parentComment ||
359
+ comment.reply_post_id === comment.parent_post_id
360
+ ) {
361
+ // If it's a top-level comment, push it to the topLevelComments array
362
+ topLevelComments.push(comment);
363
+ } else {
364
+ // If it's a reply, add it to the parent comment's replies array
365
+ if (!parentComment.replies) parentComment.replies = [];
366
+ parentComment.replies.push(comment);
367
+ }
368
+ });
369
+
370
+ // Flatten the nested structure and return a single list of comments
371
+ return flattenComments(topLevelComments);
372
+ }
373
+
374
+ // Function to flatten nested comments into a single list
375
+ function flattenComments(comments) {
376
+ // Use reduce to flatten the nested comments array into a single list
377
+ return comments.reduce((flatComments, comment) => {
378
+ // Push the current comment to the flatComments array
379
+ flatComments.push(comment);
380
+ // If the current comment has replies, recursively flatten them and push to the flatComments array
381
+ if (comment.replies)
382
+ flatComments.push(...flattenComments(comment.replies));
383
+ // Return the flatComments array
384
+ return flatComments;
385
+ }, []);
386
+ }
387
+
388
+ const sortedComments = preprocessComments(json.posts);
389
+ return sortedComments;
390
+ } catch (err) {
391
+ console.error(err);
392
+ throw new SidechatAPIError(`Failed to get comments on post.`);
393
+ }
394
+ };
395
+
396
+ /**
397
+ * Gets groups to be displayed on the "Explore Groups" page
398
+ * @method
399
+ * @returns {SidechatGroup[]}
400
+ */
401
+ getAvailableGroups = async () => {
402
+ if (!this.userToken) {
403
+ throw new SidechatAPIError("User is not authenticated.");
404
+ }
405
+ try {
406
+ const res = await fetch(`https://api.sidechat.lol/v1/groups/explore`, {
407
+ method: "GET",
408
+ headers: {
409
+ ...this.defaultHeaders,
410
+ Authorization: `Bearer ${this.userToken}`,
411
+ },
412
+ });
413
+ const json = await res.json();
414
+ return await json.groups;
415
+ } catch (err) {
416
+ console.error(err);
417
+ throw new SidechatAPIError(`Failed to get groups from explore.`);
418
+ }
419
+ };
420
+ }
421
+
422
+ export default SidechatAPIClient;
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Throwable error caused by the Sidechat API
3
+ * @class
4
+ * @extends Error
5
+ */
1
6
  class SidechatAPIError extends Error {
2
7
  constructor(message) {
3
8
  super(message);
package/src/index.js ADDED
@@ -0,0 +1,4 @@
1
+ import SidechatAPIClient from "./classes/SidechatAPIClient.js";
2
+ import SidechatAPIError from "./classes/SidechatAPIError.js";
3
+
4
+ export { SidechatAPIClient, SidechatAPIError };
@@ -0,0 +1,19 @@
1
+ import { SidechatAPIClient } from "../index.js";
2
+ import PromptSync from "prompt-sync";
3
+ const prompt = new PromptSync();
4
+
5
+ async function main() {
6
+ const API = new SidechatAPIClient(
7
+ "eyJhbGciOiJIUzI1NiJ9.ZTViNjFiMzktM2UwNi00NGU1LTg1M2YtMTg2MDNhOWQzOWM5.gOl2_HPyPzcyNpTXqSQEuJ3ZUmZi7Zq_vSBoRfbKA1A"
8
+ );
9
+ if (API.userToken) {
10
+ const phoneNum = prompt("Phone number: ");
11
+ await API.loginViaSMS(phoneNum);
12
+ const code = prompt("Code: ");
13
+ await API.verifySMSCode(phoneNum, code);
14
+ console.log("User auth token:", API.userToken);
15
+ } else {
16
+ }
17
+ }
18
+
19
+ main();
@@ -74,6 +74,22 @@
74
74
  */
75
75
 
76
76
  /**
77
- * Represents a vote on a comment or post
77
+ * A vote on a comment or post
78
78
  * @typedef {"upvote"|"downvote"|"none"} SidechatVoteString
79
79
  */
80
+
81
+ /**
82
+ * A user's bearer authentication token needed for most requests
83
+ * @typedef {String} SidechatAuthToken
84
+ */
85
+
86
+ /**
87
+ * A list of posts and the associated cursor
88
+ * @typedef {Object} SidechatPostsAndCursor
89
+ * @prop {SidechatPostOrComment[]} posts
90
+ * @prop {SidechatCursorString} cursor
91
+ */
92
+
93
+ /**
94
+ * @tutorial Hello world
95
+ */