@ichinese/open-sdk 1.0.0-rc.2 → 1.0.0-rc.4

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.
package/dist/index.js CHANGED
@@ -28,6 +28,13 @@ var OpenApiError = class extends Error {
28
28
  get neverAsk() {
29
29
  return this.details.never_ask === true;
30
30
  }
31
+ /**
32
+ * Where the person agrees to show their name on posts (409 `consent_required`): send them there, an
33
+ * app cannot agree for them.
34
+ */
35
+ get consentUrl() {
36
+ return typeof this.details.consent_url === "string" ? this.details.consent_url : null;
37
+ }
31
38
  /** A refused, not yet asked or undeclared scope (403): degrade gracefully. */
32
39
  get isScopeError() {
33
40
  return this.status === 403 && ["scope_denied", "scope_pending", "scope_not_declared", "gold_only"].includes(String(this.reason));
@@ -36,7 +43,7 @@ var OpenApiError = class extends Error {
36
43
  var isOpenApiError = (e) => e instanceof OpenApiError;
37
44
 
38
45
  // src/gen/meta.ts
39
- var VERSION = "1.0.0-rc.2";
46
+ var VERSION = "1.0.0-rc.4";
40
47
  var DEFAULT_BASE_URL = "https://open.ichinese.cn";
41
48
  var API_PREFIX = "/open-apis/v1";
42
49
  var SCOPES = [
@@ -63,11 +70,21 @@ var SCOPES = [
63
70
  "job:candidate:readonly",
64
71
  "trial",
65
72
  "trade:order:readonly",
73
+ "trade:booking:readonly",
74
+ "venue:readonly",
75
+ "venue",
76
+ "opening:readonly",
66
77
  "review",
67
78
  "profile:interest:readonly",
68
79
  "profile:interest",
69
80
  "profile:learning:readonly",
70
- "report:create"
81
+ "report:create",
82
+ "community:post",
83
+ "me:schedule:readonly",
84
+ "me:learning:readonly",
85
+ "me:words:readonly",
86
+ "me:words",
87
+ "insights:readonly"
71
88
  ];
72
89
  var EVENT_TYPES = [
73
90
  "app.installed",
@@ -81,7 +98,18 @@ var EVENT_TYPES = [
81
98
  "trade.order_paid",
82
99
  "review.created",
83
100
  "trial.booked",
84
- "im.message_received"
101
+ "im.message_received",
102
+ "class.member_changed",
103
+ "course.completed",
104
+ "exam.results_released",
105
+ "event.checked_in",
106
+ "live.recording_ready",
107
+ "trade.order_refunded",
108
+ "trial.confirmed",
109
+ "trial.cancelled",
110
+ "community.post_reviewed",
111
+ "community.video_ready",
112
+ "booking.changed"
85
113
  ];
86
114
  var ERROR_REASONS = [
87
115
  "invalid_credentials",
@@ -102,7 +130,30 @@ var ERROR_REASONS = [
102
130
  "code_invalid",
103
131
  "redirect_uri_mismatch",
104
132
  "origin_not_trusted",
105
- "ip_not_allowed"
133
+ "ip_not_allowed",
134
+ "org_not_installed",
135
+ "minor_not_allowed",
136
+ "birth_year_required",
137
+ "feature_disabled",
138
+ "app_post_stopped",
139
+ "consent_required",
140
+ "video_not_ready",
141
+ "video_failed",
142
+ "post_rate_limited",
143
+ "app_user_daily_limit",
144
+ "post_in_progress",
145
+ "no_org_account",
146
+ "teacher_busy",
147
+ "online_only",
148
+ "venue_unavailable",
149
+ "venue_not_found",
150
+ "slot_overlap",
151
+ "slot_taken",
152
+ "not_draft",
153
+ "not_editable",
154
+ "org_not_verified",
155
+ "opening_ended",
156
+ "opening_incomplete"
106
157
  ];
107
158
  var OPERATION_TOKENS = {
108
159
  getUserInfo: ["user"],
@@ -115,6 +166,8 @@ var OPERATION_TOKENS = {
115
166
  addClassMember: ["tenant", "user"],
116
167
  removeClassMember: ["tenant", "user"],
117
168
  listClassSessions: ["tenant", "user"],
169
+ createClassSessions: ["tenant", "user"],
170
+ setClassSessionPlace: ["tenant", "user"],
118
171
  cancelClassSession: ["tenant", "user"],
119
172
  rescheduleClassSession: ["tenant", "user"],
120
173
  getSessionAttendance: ["tenant", "user"],
@@ -142,16 +195,40 @@ var OPERATION_TOKENS = {
142
195
  listTrialSlots: ["tenant"],
143
196
  createTrialSlots: ["tenant"],
144
197
  cancelTrialSlot: ["tenant"],
198
+ setTrialTeacher: ["tenant"],
145
199
  listTrialBookings: ["tenant"],
146
200
  confirmTrialBooking: ["tenant"],
201
+ listVenues: ["tenant"],
202
+ createVenue: ["tenant"],
203
+ getVenue: ["tenant"],
204
+ listOpenings: ["tenant", "user"],
205
+ getOpening: ["tenant", "user"],
206
+ getInsights: ["tenant"],
147
207
  listTradeOrders: ["tenant"],
208
+ listBookings: ["tenant"],
209
+ getBooking: ["tenant"],
148
210
  listTenantReviews: ["tenant"],
149
211
  replyReview: ["tenant"],
150
212
  listPublicContent: ["app", "tenant", "user"],
151
213
  getUserInterests: ["user"],
152
214
  addInterestSignal: ["user"],
153
215
  getUserLearningProfile: ["user"],
154
- reportOnBehalf: ["user"]
216
+ reportOnBehalf: ["user"],
217
+ createCommunityImageUpload: ["user", "tenant"],
218
+ createCommunityVideoUpload: ["user", "tenant"],
219
+ completeCommunityVideoUpload: ["user", "tenant"],
220
+ getCommunityVideo: ["user", "tenant"],
221
+ listCommunityPosts: ["user", "tenant"],
222
+ createCommunityPost: ["user", "tenant"],
223
+ getCommunityPost: ["user", "tenant"],
224
+ deleteCommunityPost: ["user", "tenant"],
225
+ listCommunityCategories: ["user", "tenant"],
226
+ getMySchedule: ["user"],
227
+ listMyCourses: ["user"],
228
+ listMyWords: ["user"],
229
+ addMyWord: ["user"],
230
+ listClassExams: ["tenant"],
231
+ listClassExamResults: ["tenant"]
155
232
  };
156
233
  var SCOPE_OPENID = "openid";
157
234
  var SCOPE_PROFILE = "profile";
@@ -176,11 +253,21 @@ var SCOPE_JOB = "job";
176
253
  var SCOPE_JOB_CANDIDATE_READONLY = "job:candidate:readonly";
177
254
  var SCOPE_TRIAL = "trial";
178
255
  var SCOPE_TRADE_ORDER_READONLY = "trade:order:readonly";
256
+ var SCOPE_TRADE_BOOKING_READONLY = "trade:booking:readonly";
257
+ var SCOPE_VENUE_READONLY = "venue:readonly";
258
+ var SCOPE_VENUE = "venue";
259
+ var SCOPE_OPENING_READONLY = "opening:readonly";
179
260
  var SCOPE_REVIEW = "review";
180
261
  var SCOPE_PROFILE_INTEREST_READONLY = "profile:interest:readonly";
181
262
  var SCOPE_PROFILE_INTEREST = "profile:interest";
182
263
  var SCOPE_PROFILE_LEARNING_READONLY = "profile:learning:readonly";
183
264
  var SCOPE_REPORT_CREATE = "report:create";
265
+ var SCOPE_COMMUNITY_POST = "community:post";
266
+ var SCOPE_ME_SCHEDULE_READONLY = "me:schedule:readonly";
267
+ var SCOPE_ME_LEARNING_READONLY = "me:learning:readonly";
268
+ var SCOPE_ME_WORDS_READONLY = "me:words:readonly";
269
+ var SCOPE_ME_WORDS = "me:words";
270
+ var SCOPE_INSIGHTS_READONLY = "insights:readonly";
184
271
 
185
272
  // src/gen/operations.ts
186
273
  var Operations = class {
@@ -240,6 +327,14 @@ var Operations = class {
240
327
  listClassSessionsAll(classId, query, options) {
241
328
  return this.iterate({ id: "listClassSessions", method: "GET", path: "/classes/{class_id}/sessions", pathParams: { class_id: classId }, query, tokenTypes: ["tenant", "user"] }, options);
242
329
  }
330
+ /** Schedule a class's sessions (once, or repeating weekly), online or at one of the institution's teaching sites (`POST /classes/{class_id}/sessions`; tenant / user token; scopes: schedule) */
331
+ createClassSessions(classId, body, options) {
332
+ return this.call({ id: "createClassSessions", method: "POST", path: "/classes/{class_id}/sessions", pathParams: { class_id: classId }, body, tokenTypes: ["tenant", "user"] }, options);
333
+ }
334
+ /** Change where one session (or this one and the later ones of its series) is held (`PUT /classes/{class_id}/sessions/{session_id}/place`; tenant / user token; scopes: schedule) */
335
+ setClassSessionPlace(classId, sessionId, body, options) {
336
+ return this.call({ id: "setClassSessionPlace", method: "PUT", path: "/classes/{class_id}/sessions/{session_id}/place", pathParams: { class_id: classId, session_id: sessionId }, body, tokenTypes: ["tenant", "user"] }, options);
337
+ }
243
338
  /** Cancel a class session (members are notified, the same as cancelling in the timetable) (`POST /classes/{class_id}/sessions/{session_id}/cancel`; tenant / user token; scopes: schedule) */
244
339
  cancelClassSession(classId, sessionId, options) {
245
340
  return this.call({ id: "cancelClassSession", method: "POST", path: "/classes/{class_id}/sessions/{session_id}/cancel", pathParams: { class_id: classId, session_id: sessionId }, tokenTypes: ["tenant", "user"] }, options);
@@ -384,7 +479,7 @@ var Operations = class {
384
479
  listTrialVenuesAll(query, options) {
385
480
  return this.iterate({ id: "listTrialVenues", method: "GET", path: "/trial/venues", pathParams: {}, query, tokenTypes: ["tenant"] }, options);
386
481
  }
387
- /** Trial lesson slots (by default only those not over yet, by time) (`GET /trial/slots`; tenant token; scopes: trial) */
482
+ /** Trial times (a trial lesson's own times at teaching sites; by default only those not over yet, by time) (`GET /trial/slots`; tenant token; scopes: trial) */
388
483
  listTrialSlots(query, options) {
389
484
  return this.call({ id: "listTrialSlots", method: "GET", path: "/trial/slots", pathParams: {}, query, tokenTypes: ["tenant"] }, options);
390
485
  }
@@ -392,14 +487,18 @@ var Operations = class {
392
487
  listTrialSlotsAll(query, options) {
393
488
  return this.iterate({ id: "listTrialSlots", method: "GET", path: "/trial/slots", pathParams: {}, query, tokenTypes: ["tenant"] }, options);
394
489
  }
395
- /** Schedule trial lesson slots (they can repeat weekly); the teaching site must be published (`POST /trial/slots`; tenant token; scopes: trial) */
490
+ /** Open trial times (they can repeat weekly): add them to an existing trial lesson, or open a new trial draft (`POST /trial/slots`; tenant token; scopes: trial) */
396
491
  createTrialSlots(body, options) {
397
492
  return this.call({ id: "createTrialSlots", method: "POST", path: "/trial/slots", pathParams: {}, body, tokenTypes: ["tenant"] }, options);
398
493
  }
399
- /** Cancel a trial lesson slot (whoever booked it is notified as usual; the booking is cancelled with it) (`POST /trial/slots/{slot_id}/cancel`; tenant token; scopes: trial) */
494
+ /** Close a trial time (no more bookings; bookings already made stay as they are, and nobody is notified) (`POST /trial/slots/{slot_id}/cancel`; tenant token; scopes: trial) */
400
495
  cancelTrialSlot(slotId, body, options) {
401
496
  return this.call({ id: "cancelTrialSlot", method: "POST", path: "/trial/slots/{slot_id}/cancel", pathParams: { slot_id: slotId }, body, tokenTypes: ["tenant"] }, options);
402
497
  }
498
+ /** Fill in (or change) the teacher of a trial draft (`PUT /trial/openings/{opening_id}/teacher`; tenant token; scopes: trial) */
499
+ setTrialTeacher(openingId, body, options) {
500
+ return this.call({ id: "setTrialTeacher", method: "PUT", path: "/trial/openings/{opening_id}/teacher", pathParams: { opening_id: openingId }, body, tokenTypes: ["tenant"] }, options);
501
+ }
403
502
  /** Trial lesson bookings at the institution's teaching sites (`GET /trial/bookings`; tenant token; scopes: trial) */
404
503
  listTrialBookings(query, options) {
405
504
  return this.call({ id: "listTrialBookings", method: "GET", path: "/trial/bookings", pathParams: {}, query, tokenTypes: ["tenant"] }, options);
@@ -412,6 +511,38 @@ var Operations = class {
412
511
  confirmTrialBooking(bookingId, options) {
413
512
  return this.call({ id: "confirmTrialBooking", method: "POST", path: "/trial/bookings/{booking_id}/confirm", pathParams: { booking_id: bookingId }, tokenTypes: ["tenant"] }, options);
414
513
  }
514
+ /** The institution's teaching sites (campuses), newest first (`GET /venues`; tenant token; scopes: venue:readonly, venue, trial) */
515
+ listVenues(query, options) {
516
+ return this.call({ id: "listVenues", method: "GET", path: "/venues", pathParams: {}, query, tokenTypes: ["tenant"] }, options);
517
+ }
518
+ /** Every item of `listVenues`, page after page. */
519
+ listVenuesAll(query, options) {
520
+ return this.iterate({ id: "listVenues", method: "GET", path: "/venues", pathParams: {}, query, tokenTypes: ["tenant"] }, options);
521
+ }
522
+ /** Create a draft teaching site (campus); it is published in the platform and reviewed as usual (`POST /venues`; tenant token; scopes: venue) */
523
+ createVenue(body, options) {
524
+ return this.call({ id: "createVenue", method: "POST", path: "/venues", pathParams: {}, body, tokenTypes: ["tenant"] }, options);
525
+ }
526
+ /** One teaching site (of this institution only) (`GET /venues/{venue_id}`; tenant token; scopes: venue:readonly, venue, trial) */
527
+ getVenue(venueId, options) {
528
+ return this.call({ id: "getVenue", method: "GET", path: "/venues/{venue_id}", pathParams: { venue_id: venueId }, tokenTypes: ["tenant"] }, options);
529
+ }
530
+ /** Openings: every class and course opened (group classes, one-to-ones, open classes, trial lessons, video courses), newest first (`GET /openings`; tenant / user token; scopes: opening:readonly) */
531
+ listOpenings(query, options) {
532
+ return this.call({ id: "listOpenings", method: "GET", path: "/openings", pathParams: {}, query, tokenTypes: ["tenant", "user"] }, options);
533
+ }
534
+ /** Every item of `listOpenings`, page after page. */
535
+ listOpeningsAll(query, options) {
536
+ return this.iterate({ id: "listOpenings", method: "GET", path: "/openings", pathParams: {}, query, tokenTypes: ["tenant", "user"] }, options);
537
+ }
538
+ /** One opening (`GET /openings/{opening_id}`; tenant / user token; scopes: opening:readonly) */
539
+ getOpening(openingId, options) {
540
+ return this.call({ id: "getOpening", method: "GET", path: "/openings/{opening_id}", pathParams: { opening_id: openingId }, tokenTypes: ["tenant", "user"] }, options);
541
+ }
542
+ /** One page of the institution's dashboard (read only): enrolment, teaching, teaching sites, income, community and events (`GET /insights`; tenant token; scopes: insights:readonly) */
543
+ getInsights(query, options) {
544
+ return this.call({ id: "getInsights", method: "GET", path: "/insights", pathParams: {}, query, tokenTypes: ["tenant"] }, options);
545
+ }
415
546
  /** The institution's orders (read-only, for reconciliation). Write operations involving money are not open (`GET /trade/orders`; tenant token; scopes: trade:order:readonly) */
416
547
  listTradeOrders(query, options) {
417
548
  return this.call({ id: "listTradeOrders", method: "GET", path: "/trade/orders", pathParams: {}, query, tokenTypes: ["tenant"] }, options);
@@ -420,6 +551,18 @@ var Operations = class {
420
551
  listTradeOrdersAll(query, options) {
421
552
  return this.iterate({ id: "listTradeOrders", method: "GET", path: "/trade/orders", pathParams: {}, query, tokenTypes: ["tenant"] }, options);
422
553
  }
554
+ /** The institution's bookings (one-to-ones and trial lessons, read only for reconciliation), latest lesson first (`GET /bookings`; tenant token; scopes: trade:booking:readonly) */
555
+ listBookings(query, options) {
556
+ return this.call({ id: "listBookings", method: "GET", path: "/bookings", pathParams: {}, query, tokenTypes: ["tenant"] }, options);
557
+ }
558
+ /** Every item of `listBookings`, page after page. */
559
+ listBookingsAll(query, options) {
560
+ return this.iterate({ id: "listBookings", method: "GET", path: "/bookings", pathParams: {}, query, tokenTypes: ["tenant"] }, options);
561
+ }
562
+ /** One booked lesson (`GET /bookings/{booking_id}`; tenant token; scopes: trade:booking:readonly) */
563
+ getBooking(bookingId, options) {
564
+ return this.call({ id: "getBooking", method: "GET", path: "/bookings/{booking_id}", pathParams: { booking_id: bookingId }, tokenTypes: ["tenant"] }, options);
565
+ }
423
566
  /** Reviews of this institution (and its courses and teaching sites) (`GET /reviews`; tenant token; scopes: review) */
424
567
  listTenantReviews(query, options) {
425
568
  return this.call({ id: "listTenantReviews", method: "GET", path: "/reviews", pathParams: {}, query, tokenTypes: ["tenant"] }, options);
@@ -456,6 +599,90 @@ var Operations = class {
456
599
  reportOnBehalf(body, options) {
457
600
  return this.call({ id: "reportOnBehalf", method: "POST", path: "/reports", pathParams: {}, body, tokenTypes: ["user"] }, options);
458
601
  }
602
+ /** Get an upload URL for an image (it is re-encoded and its EXIF removed when the post is made) (`POST /community/images`; user / tenant token; scopes: community:post) */
603
+ createCommunityImageUpload(body, options) {
604
+ return this.call({ id: "createCommunityImageUpload", method: "POST", path: "/community/images", pathParams: {}, body, tokenTypes: ["user", "tenant"] }, options);
605
+ }
606
+ /** Get an upload URL for a video (up to 4 GB, 1 hour) (`POST /community/videos`; user / tenant token; scopes: community:post) */
607
+ createCommunityVideoUpload(body, options) {
608
+ return this.call({ id: "createCommunityVideoUpload", method: "POST", path: "/community/videos", pathParams: {}, body, tokenTypes: ["user", "tenant"] }, options);
609
+ }
610
+ /** The upload is done; start transcoding (calling it again has no side effects) (`POST /community/videos/{asset_id}/complete`; user / tenant token; scopes: community:post) */
611
+ completeCommunityVideoUpload(assetId, options) {
612
+ return this.call({ id: "completeCommunityVideoUpload", method: "POST", path: "/community/videos/{asset_id}/complete", pathParams: { asset_id: assetId }, tokenTypes: ["user", "tenant"] }, options);
613
+ }
614
+ /** Transcoding status (only for videos this app uploaded for this person) (`GET /community/videos/{asset_id}`; user / tenant token; scopes: community:post) */
615
+ getCommunityVideo(assetId, options) {
616
+ return this.call({ id: "getCommunityVideo", method: "GET", path: "/community/videos/{asset_id}", pathParams: { asset_id: assetId }, tokenTypes: ["user", "tenant"] }, options);
617
+ }
618
+ /** Posts this app made in this person's name (the person's own posts and other apps' posts are not visible) (`GET /community/posts`; user / tenant token; scopes: community:post) */
619
+ listCommunityPosts(query, options) {
620
+ return this.call({ id: "listCommunityPosts", method: "GET", path: "/community/posts", pathParams: {}, query, tokenTypes: ["user", "tenant"] }, options);
621
+ }
622
+ /** Every item of `listCommunityPosts`, page after page. */
623
+ listCommunityPostsAll(query, options) {
624
+ return this.iterate({ id: "listCommunityPosts", method: "GET", path: "/community/posts", pathParams: {}, query, tokenTypes: ["user", "tenant"] }, options);
625
+ }
626
+ /** Publish a public post in this person's name (text, up to 9 images or 1 video, up to 10 topics) (`POST /community/posts`; user / tenant token; scopes: community:post) */
627
+ createCommunityPost(body, options) {
628
+ return this.call({ id: "createCommunityPost", method: "POST", path: "/community/posts", pathParams: {}, body, tokenTypes: ["user", "tenant"] }, options);
629
+ }
630
+ /** The status of one post (only posts this app made; anything else is 404) (`GET /community/posts/{post_id}`; user / tenant token; scopes: community:post) */
631
+ getCommunityPost(postId, options) {
632
+ return this.call({ id: "getCommunityPost", method: "GET", path: "/community/posts/{post_id}", pathParams: { post_id: postId }, tokenTypes: ["user", "tenant"] }, options);
633
+ }
634
+ /** Delete a post (only posts this app made; anything else is 404) (`DELETE /community/posts/{post_id}`; user / tenant token; scopes: community:post) */
635
+ deleteCommunityPost(postId, options) {
636
+ return this.call({ id: "deleteCommunityPost", method: "DELETE", path: "/community/posts/{post_id}", pathParams: { post_id: postId }, tokenTypes: ["user", "tenant"] }, options);
637
+ }
638
+ /** Categories a post can be filed under (those enabled in this region) (`GET /community/categories`; user / tenant token; scopes: community:post) */
639
+ listCommunityCategories(options) {
640
+ return this.call({ id: "listCommunityCategories", method: "GET", path: "/community/categories", pathParams: {}, tokenTypes: ["user", "tenant"] }, options);
641
+ }
642
+ /** This person's timetable: their sessions, the events they signed up for, confirmed trial lessons and exams (for calendar apps; read-only) (`GET /me/schedule`; user token; scopes: me:schedule:readonly) */
643
+ getMySchedule(query, options) {
644
+ return this.call({ id: "getMySchedule", method: "GET", path: "/me/schedule", pathParams: {}, query, tokenTypes: ["user"] }, options);
645
+ }
646
+ /** Every item of `getMySchedule`, page after page. */
647
+ getMyScheduleAll(query, options) {
648
+ return this.iterate({ id: "getMySchedule", method: "GET", path: "/me/schedule", pathParams: {}, query, tokenTypes: ["user"] }, options);
649
+ }
650
+ /** The courses this person is studying and their progress (lessons completed, last studied; level and goals belong to the gold-only profile and are not here) (`GET /me/courses`; user token; scopes: me:learning:readonly) */
651
+ listMyCourses(query, options) {
652
+ return this.call({ id: "listMyCourses", method: "GET", path: "/me/courses", pathParams: {}, query, tokenTypes: ["user"] }, options);
653
+ }
654
+ /** Every item of `listMyCourses`, page after page. */
655
+ listMyCoursesAll(query, options) {
656
+ return this.iterate({ id: "listMyCourses", method: "GET", path: "/me/courses", pathParams: {}, query, tokenTypes: ["user"] }, options);
657
+ }
658
+ /** This person's word bank: words, pinyin, review state (no meanings, and review records cannot be changed) (`GET /me/words`; user token; scopes: me:words:readonly, me:words) */
659
+ listMyWords(query, options) {
660
+ return this.call({ id: "listMyWords", method: "GET", path: "/me/words", pathParams: {}, query, tokenTypes: ["user"] }, options);
661
+ }
662
+ /** Every item of `listMyWords`, page after page. */
663
+ listMyWordsAll(query, options) {
664
+ return this.iterate({ id: "listMyWords", method: "GET", path: "/me/words", pathParams: {}, query, tokenTypes: ["user"] }, options);
665
+ }
666
+ /** Add a word to this person's word bank (unchanged if it is already there; its source is recorded as this app) (`POST /me/words`; user token; scopes: me:words) */
667
+ addMyWord(body, options) {
668
+ return this.call({ id: "addMyWord", method: "POST", path: "/me/words", pathParams: {}, body, tokenTypes: ["user"] }, options);
669
+ }
670
+ /** A class's exams (tenant tokens only, own institution) (`GET /classes/{class_id}/exams`; tenant token; scopes: grade:readonly) */
671
+ listClassExams(classId, query, options) {
672
+ return this.call({ id: "listClassExams", method: "GET", path: "/classes/{class_id}/exams", pathParams: { class_id: classId }, query, tokenTypes: ["tenant"] }, options);
673
+ }
674
+ /** Every item of `listClassExams`, page after page. */
675
+ listClassExamsAll(classId, query, options) {
676
+ return this.iterate({ id: "listClassExams", method: "GET", path: "/classes/{class_id}/exams", pathParams: { class_id: classId }, query, tokenTypes: ["tenant"] }, options);
677
+ }
678
+ /** Each learner's result in one exam (scores only once released; tenant tokens only, own institution) (`GET /classes/{class_id}/exams/{exam_id}/results`; tenant token; scopes: grade:readonly) */
679
+ listClassExamResults(classId, examId, query, options) {
680
+ return this.call({ id: "listClassExamResults", method: "GET", path: "/classes/{class_id}/exams/{exam_id}/results", pathParams: { class_id: classId, exam_id: examId }, query, tokenTypes: ["tenant"] }, options);
681
+ }
682
+ /** Every item of `listClassExamResults`, page after page. */
683
+ listClassExamResultsAll(classId, examId, query, options) {
684
+ return this.iterate({ id: "listClassExamResults", method: "GET", path: "/classes/{class_id}/exams/{exam_id}/results", pathParams: { class_id: classId, exam_id: examId }, query, tokenTypes: ["tenant"] }, options);
685
+ }
459
686
  };
460
687
 
461
688
  // src/client.ts
@@ -537,6 +764,129 @@ var Client = class extends Operations {
537
764
  clearTokenCache() {
538
765
  this.tokens.clear();
539
766
  }
767
+ // ── posting in a person's name (D105), and as the institution (M6.21 K2) ─────
768
+ /**
769
+ * Uploads an image for a post: asks for an upload slot, PUTs the bytes there and returns the `key` to
770
+ * put in `images[].key`. Up to 10 MB; jpeg, png, webp or gif.
771
+ */
772
+ uploadImage(userAccessToken, data, contentType, options = {}) {
773
+ return this.uploadImageWith({ userAccessToken, signal: options.signal }, data, contentType);
774
+ }
775
+ /** `uploadImage` for a post of the institution (its tenant token, `community:post` granted by it). */
776
+ uploadImageAsOrg(tenantKey, data, contentType, options = {}) {
777
+ return this.uploadImageWith({ tenantKey, signal: options.signal }, data, contentType);
778
+ }
779
+ /**
780
+ * Uploads a video for a post (up to 4 GB, 1 hour): asks for an upload slot, PUTs the file there and
781
+ * starts transcoding. A large file goes as a Blob that reads from disk (`await fs.openAsBlob(path)`).
782
+ * Returns the video's status (`asset_id`); wait for `ready` with `waitVideoReady` before posting.
783
+ */
784
+ uploadVideo(userAccessToken, data, file, options = {}) {
785
+ return this.uploadVideoWith({ userAccessToken, signal: options.signal }, data, file);
786
+ }
787
+ /** `uploadVideo` for a post of the institution. */
788
+ uploadVideoAsOrg(tenantKey, data, file, options = {}) {
789
+ return this.uploadVideoWith({ tenantKey, signal: options.signal }, data, file);
790
+ }
791
+ /**
792
+ * Waits until a video has been transcoded and returns it (`status: ready`). A failed transcode throws
793
+ * an OpenApiError with reason `video_failed` (upload another file); running out of time throws one
794
+ * with code TIMEOUT (reason `video_not_ready`). You also get the `community.video_ready` event.
795
+ */
796
+ waitVideoReady(userAccessToken, assetId, options = {}) {
797
+ return this.waitVideoWith({ userAccessToken }, assetId, options);
798
+ }
799
+ /** `waitVideoReady` for a video uploaded for the institution. */
800
+ waitVideoReadyAsOrg(tenantKey, assetId, options = {}) {
801
+ return this.waitVideoWith({ tenantKey }, assetId, options);
802
+ }
803
+ /**
804
+ * Posts in the person's name. Without a `uuid` the SDK makes one, so its own retries never post twice;
805
+ * give your own `uuid` to make your retries safe too (one post per `uuid` within an hour). Errors keep the server's reason: `consent_required`
806
+ * (send the person to `error.consentUrl`), `video_not_ready`, `video_failed`, `post_rate_limited`,
807
+ * `app_user_daily_limit`, `daily_quota_exceeded`, `minor_not_allowed`, `app_post_stopped`, …
808
+ */
809
+ createPost(userAccessToken, post, options = {}) {
810
+ return this.createPostWith({ userAccessToken, signal: options.signal }, post);
811
+ }
812
+ /**
813
+ * Posts as the institution, with its community account (its name and logo; the post says it came from
814
+ * this app). The institution must have installed the app and granted `community:post`; before its
815
+ * homepage was first published there is no account yet (409 `no_org_account`). The same limits,
816
+ * review and `uuid` handling as `createPost`.
817
+ */
818
+ createPostAsOrg(tenantKey, post, options = {}) {
819
+ return this.createPostWith({ tenantKey, signal: options.signal }, post);
820
+ }
821
+ // ── the institution's dashboard (M6.21 K2) ────────────────────────────────
822
+ /**
823
+ * Every page of the institution's dashboard the API offers (`GET /insights`, `insights:readonly`): the
824
+ * default page first, then each other tab it lists, with the same query (range, dates, filters). Returns
825
+ * tab → page. Fewer than five people come back as `null` with `withheld: true`; money is per currency.
826
+ */
827
+ async getAllInsights(tenantKey, query = {}, options = {}) {
828
+ const who = { tenantKey, signal: options.signal };
829
+ const first = await this.getInsights(query, who);
830
+ const out = { [first.tab]: first };
831
+ for (const tab of first.tabs) {
832
+ if (out[tab]) continue;
833
+ out[tab] = await this.getInsights({ ...query, tab }, who);
834
+ }
835
+ return out;
836
+ }
837
+ async uploadImageWith(who, data, contentType) {
838
+ const slot = await this.createCommunityImageUpload({ content_type: contentType, size_bytes: sizeOf(data) }, who);
839
+ await this.putUpload(slot.upload_url, slot.headers, data, who.signal);
840
+ return slot.key;
841
+ }
842
+ async uploadVideoWith(who, data, file) {
843
+ const slot = await this.createCommunityVideoUpload(
844
+ { filename: file.filename, content_type: file.contentType, size_bytes: sizeOf(data) },
845
+ who
846
+ );
847
+ await this.putUpload(slot.upload.url, slot.upload.headers, data, who.signal);
848
+ return this.send(
849
+ "POST",
850
+ `/community/videos/${encodeURIComponent(slot.asset_id)}/complete`,
851
+ { auth: this.postingAuth("completeCommunityVideoUpload", who), retryPost: true }
852
+ );
853
+ }
854
+ async waitVideoWith(who, assetId, options) {
855
+ const deadline = Date.now() + (options.timeoutMs ?? 30 * 6e4);
856
+ let interval = options.intervalMs ?? 2e3;
857
+ const maxInterval = options.maxIntervalMs ?? 15e3;
858
+ for (; ; ) {
859
+ const video = await this.getCommunityVideo(assetId, { ...who, signal: options.signal });
860
+ if (video.status === "ready") return video;
861
+ if (video.status === "failed") {
862
+ throw new OpenApiError({
863
+ status: 409,
864
+ code: "CONFLICT",
865
+ message: `video ${assetId} could not be transcoded${video.error ? ` (${video.error})` : ""}`,
866
+ details: { reason: "video_failed", asset_id: assetId, error: video.error ?? null }
867
+ });
868
+ }
869
+ const left = deadline - Date.now();
870
+ if (left <= 0) {
871
+ throw new OpenApiError({
872
+ status: 0,
873
+ code: "TIMEOUT",
874
+ message: `video ${assetId} is not ready yet (${video.status})`,
875
+ details: { reason: "video_not_ready", asset_id: assetId, status: video.status }
876
+ });
877
+ }
878
+ await this.sleep(Math.min(interval, left), options.signal);
879
+ interval = Math.min(interval * 2, maxInterval);
880
+ }
881
+ }
882
+ createPostWith(who, post) {
883
+ const body = post.uuid ? post : { ...post, uuid: crypto.randomUUID() };
884
+ return this.send("POST", "/community/posts", {
885
+ body,
886
+ auth: this.postingAuth("createCommunityPost", who),
887
+ retryPost: true
888
+ });
889
+ }
540
890
  // ── calls ─────────────────────────────────────────────────────────────────
541
891
  /** Any open API call by method and path (`/tenant/members`), for what the generated methods do not cover. */
542
892
  request(method, path, init = {}, options) {
@@ -578,6 +928,31 @@ var Client = class extends Operations {
578
928
  credentials() {
579
929
  return { app_id: this.appId, app_secret: this.appSecret };
580
930
  }
931
+ postingAuth(id, who) {
932
+ const spec = { id, method: "POST", path: "", pathParams: {}, tokenTypes: ["user", "tenant"] };
933
+ return { spec, options: who };
934
+ }
935
+ /**
936
+ * PUTs an upload to the slot the platform gave (object storage, not the open API: no token, its own
937
+ * headers). Not limited by `timeoutMs`, a large video takes as long as it takes; cancel with the signal.
938
+ */
939
+ async putUpload(url, headers, data, signal) {
940
+ let res;
941
+ try {
942
+ res = await this.fetchFn(url, { method: "PUT", headers, body: data, signal });
943
+ } catch (err) {
944
+ if (signal?.aborted) throw err;
945
+ throw new OpenApiError({ status: 0, code: "NETWORK", message: `PUT upload: ${err.message}` });
946
+ }
947
+ if (!res.ok) {
948
+ throw new OpenApiError({
949
+ status: res.status,
950
+ code: "UPLOAD_FAILED",
951
+ message: `PUT upload: ${res.status} ${res.statusText}`,
952
+ details: { reason: "upload_failed", body: (await res.text().catch(() => "")).slice(0, 500) }
953
+ });
954
+ }
955
+ }
581
956
  async cached(key, fetchToken) {
582
957
  const hit = this.tokens.get(key);
583
958
  if (hit && hit.expiresAt - this.refreshBefore * 1e3 > Date.now()) return hit;
@@ -648,7 +1023,7 @@ var Client = class extends Operations {
648
1023
  const timeout = AbortSignal.timeout(this.timeoutMs);
649
1024
  const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
650
1025
  const canRetry = attempt < this.maxRetries;
651
- const idempotent = IDEMPOTENT.has(method) || method === "POST" && init.retryPost !== false && !init.auth;
1026
+ const idempotent = IDEMPOTENT.has(method) || method === "POST" && (init.retryPost === true || init.retryPost !== false && !init.auth);
652
1027
  let res;
653
1028
  try {
654
1029
  res = await this.fetchFn(this.url(path, init.query), {
@@ -698,6 +1073,7 @@ var Client = class extends Operations {
698
1073
  }
699
1074
  }
700
1075
  };
1076
+ var sizeOf = (data) => data instanceof Uint8Array ? data.byteLength : data.size;
701
1077
  async function toError(res) {
702
1078
  let body = {};
703
1079
  try {
@@ -783,6 +1159,7 @@ export {
783
1159
  SCOPE_ATTENDANCE_READONLY,
784
1160
  SCOPE_CLASS_MEMBER,
785
1161
  SCOPE_CLASS_READONLY,
1162
+ SCOPE_COMMUNITY_POST,
786
1163
  SCOPE_COURSE,
787
1164
  SCOPE_COURSEWORK,
788
1165
  SCOPE_EVENT,
@@ -790,12 +1167,18 @@ export {
790
1167
  SCOPE_GRADE_READONLY,
791
1168
  SCOPE_IM_CHAT_READONLY,
792
1169
  SCOPE_IM_MESSAGE_SEND_AS_BOT,
1170
+ SCOPE_INSIGHTS_READONLY,
793
1171
  SCOPE_JOB,
794
1172
  SCOPE_JOB_CANDIDATE_READONLY,
795
1173
  SCOPE_LEARNING_PROGRESS_READONLY,
796
1174
  SCOPE_LIVE_RECORDING_READONLY,
1175
+ SCOPE_ME_LEARNING_READONLY,
1176
+ SCOPE_ME_SCHEDULE_READONLY,
1177
+ SCOPE_ME_WORDS,
1178
+ SCOPE_ME_WORDS_READONLY,
797
1179
  SCOPE_NOTIFY_SEND,
798
1180
  SCOPE_OPENID,
1181
+ SCOPE_OPENING_READONLY,
799
1182
  SCOPE_ORG_MEMBER,
800
1183
  SCOPE_ORG_MEMBER_READONLY,
801
1184
  SCOPE_PROFILE,
@@ -806,8 +1189,11 @@ export {
806
1189
  SCOPE_REVIEW,
807
1190
  SCOPE_SCHEDULE,
808
1191
  SCOPE_SCHEDULE_READONLY,
1192
+ SCOPE_TRADE_BOOKING_READONLY,
809
1193
  SCOPE_TRADE_ORDER_READONLY,
810
1194
  SCOPE_TRIAL,
1195
+ SCOPE_VENUE,
1196
+ SCOPE_VENUE_READONLY,
811
1197
  VERSION,
812
1198
  challengeResponse,
813
1199
  decrypt,
package/dist/types.d.ts CHANGED
@@ -42,6 +42,17 @@ export interface ClientOptions {
42
42
  /** Appended to the User-Agent header. */
43
43
  userAgent?: string;
44
44
  }
45
+ /** How `waitVideoReady` polls a video's status. */
46
+ export interface WaitVideoOptions {
47
+ /** Give up after this many milliseconds (an OpenApiError, code TIMEOUT). Default 30 minutes. */
48
+ timeoutMs?: number;
49
+ /** First wait between two looks, in milliseconds; it doubles up to `maxIntervalMs`. Default 2 000. */
50
+ intervalMs?: number;
51
+ /** Longest wait between two looks. Default 15 000. */
52
+ maxIntervalMs?: number;
53
+ /** Stops waiting. */
54
+ signal?: AbortSignal;
55
+ }
45
56
  export interface UserAccessToken {
46
57
  access_token: string;
47
58
  token_type: 'Bearer';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ichinese/open-sdk",
3
- "version": "1.0.0-rc.2",
3
+ "version": "1.0.0-rc.4",
4
4
  "description": "Server SDK for the iChinese open platform (open API, tokens, events, web login): Node.js 20+",
5
5
  "license": "MIT",
6
6
  "type": "module",