@learnpack/learnpack 5.0.335 → 5.0.339

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 (48) hide show
  1. package/bin/run +17 -17
  2. package/lib/commands/init.js +41 -41
  3. package/lib/commands/serve.js +589 -126
  4. package/lib/creatorDist/assets/index-BhqDgBS9.js +8448 -78631
  5. package/lib/creatorDist/assets/index-CjddKHB_.css +1 -1688
  6. package/lib/managers/config/exercise.js +2 -14
  7. package/lib/managers/readmeHistoryService.js +3 -1
  8. package/lib/managers/server/routes.js +2 -1
  9. package/lib/utils/configBuilder.js +2 -1
  10. package/lib/utils/creatorUtilities.js +14 -14
  11. package/lib/utils/exerciseFileOrder.d.ts +20 -0
  12. package/lib/utils/exerciseFileOrder.js +49 -0
  13. package/lib/utils/export/epub.js +26 -26
  14. package/lib/utils/readmeSanitizer.d.ts +8 -0
  15. package/lib/utils/readmeSanitizer.js +13 -0
  16. package/lib/utils/templates/epub/epub.css +146 -146
  17. package/lib/utils/templates/scorm/config/api.js +175 -175
  18. package/package.json +1 -1
  19. package/src/commands/init.ts +655 -655
  20. package/src/commands/publish.ts +670 -670
  21. package/src/commands/serve.ts +5853 -5216
  22. package/src/creator/eslint.config.js +28 -28
  23. package/src/creator/src/index.css +227 -227
  24. package/src/creator/src/utils/lib.ts +471 -471
  25. package/src/creatorDist/assets/index-BhqDgBS9.js +8448 -78631
  26. package/src/creatorDist/assets/index-CjddKHB_.css +1 -1688
  27. package/src/managers/config/exercise.ts +3 -15
  28. package/src/managers/readmeHistoryService.ts +3 -1
  29. package/src/managers/server/routes.ts +15 -6
  30. package/src/managers/session.ts +184 -184
  31. package/src/ui/_app/app.css +1 -1
  32. package/src/ui/_app/app.js +1950 -1878
  33. package/src/ui/app.tar.gz +0 -0
  34. package/src/utils/api.ts +675 -675
  35. package/src/utils/configBuilder.ts +102 -100
  36. package/src/utils/creatorUtilities.ts +536 -536
  37. package/src/utils/errors.ts +108 -108
  38. package/src/utils/exerciseFileOrder.ts +50 -0
  39. package/src/utils/export/epub.ts +553 -553
  40. package/src/utils/export/index.ts +4 -4
  41. package/src/utils/export/scorm.ts +121 -121
  42. package/src/utils/export/shared.ts +61 -61
  43. package/src/utils/export/types.ts +25 -25
  44. package/src/utils/export/zip.ts +55 -55
  45. package/src/utils/readmeSanitizer.ts +10 -0
  46. package/src/utils/rigoActions.ts +642 -642
  47. package/src/utils/templates/epub/epub.css +146 -146
  48. package/src/utils/templates/scorm/config/api.js +175 -175
package/src/utils/api.ts CHANGED
@@ -1,675 +1,675 @@
1
- import Console from "../utils/console"
2
- import * as storage from "node-persist"
3
- import cli from "cli-ux"
4
- import axios from "axios"
5
- import * as dotenv from "dotenv"
6
-
7
- dotenv.config()
8
-
9
- const HOST = "https://breathecode.herokuapp.com"
10
- export const RIGOBOT_HOST = "https://rigobot.herokuapp.com"
11
- export const RIGOBOT_REALTIME_HOST = "https://ai.4geeks.com"
12
- // export const RIGOBOT_REALTIME_HOST = "http://127.0.0.1:8003"
13
- // export const RIGOBOT_HOST = "https://rigobot-test-cca7d841c9d8.herokuapp.com"
14
- // export const RIGOBOT_HOST =
15
- // "https://8000-charlytoc-rigobot-bmwdeam7cev.ws-us118.gitpod.io"
16
-
17
- // eslint-disable-next-line
18
- const _fetch = require("node-fetch");
19
-
20
- interface IHeaders {
21
- "Content-Type"?: string;
22
- Authorization?: string;
23
- }
24
-
25
- interface IOptions {
26
- headers?: IHeaders;
27
- method?: string;
28
- body?: string;
29
- }
30
-
31
- const fetch = async (
32
- url: string,
33
- options: IOptions = {},
34
- returnAsJson = true
35
- ) => {
36
- const headers: IHeaders = { "Content-Type": "application/json" }
37
- Console.debug(`Fetching ${url}`)
38
- let session = null
39
- try {
40
- session = await storage.getItem("bc-payload")
41
- if (session.token && session.token !== "" && !url.includes("/token"))
42
- headers.Authorization = "Token " + session.token
43
- } catch {}
44
-
45
- try {
46
- const resp = await _fetch(url, {
47
- ...options,
48
- headers: { ...headers, ...options.headers },
49
- } as any)
50
-
51
- if (resp.status >= 200 && resp.status < 300) {
52
- return returnAsJson ? await resp.json() : await resp.text()
53
- }
54
-
55
- if (resp.status === 401)
56
- Console.debug("Invalid authentication credentials", `Code: 401`)
57
- // throw APIError("Invalid authentication credentials", 401)
58
- else if (resp.status === 404) throw APIError("Package not found", 404)
59
- else if (resp.status >= 500)
60
- throw APIError("Impossible to connect with the server", 500)
61
- else if (resp.status >= 400) {
62
- const error = await resp.json()
63
- if (error.detail || error.error) {
64
- throw APIError(error.detail || error.error)
65
- } else if (error.nonFieldErrors) {
66
- throw APIError(error.nonFieldErrors[0], error)
67
- } else if (typeof error === "object") {
68
- if (Object.keys(error).length > 0) {
69
- const key = error[Object.keys(error)[0]]
70
- throw APIError(`${key}: ${error[key][0]}`, error)
71
- }
72
- } else {
73
- throw APIError("Uknown error")
74
- }
75
- } else throw APIError("Uknown error")
76
- } catch (error) {
77
- Console.error((error as TypeError).message)
78
- throw error
79
- }
80
- }
81
-
82
- const login = async (identification: string, password: string) => {
83
- try {
84
- cli.action.start(`Looking for credentials with ${identification}`)
85
- await cli.wait(1000)
86
- const url = `${HOST}/v1/auth/login/`
87
-
88
- const res = await axios.post(url, {
89
- email: identification,
90
- password: password,
91
- })
92
- const data = res.data
93
-
94
- cli.action.stop("ready")
95
- let rigoPayload = null
96
- try {
97
- rigoPayload = await loginRigo(data.token)
98
- } catch {
99
- return { ...data, rigobot: null }
100
- }
101
-
102
- return { ...data, rigobot: rigoPayload }
103
- } catch (error) {
104
- cli.action.stop("error")
105
- Console.error((error as TypeError).message)
106
- Console.debug(error)
107
- }
108
- }
109
-
110
- const loginRigo = async (token: string) => {
111
- try {
112
- const rigoUrl = `${RIGOBOT_HOST}/v1/auth/me/token?breathecode_token=${token}`
113
- const rigoResp = await _fetch(rigoUrl)
114
- const rigobotJson = await rigoResp.json()
115
- return rigobotJson
116
- } catch (error) {
117
- // Handle the error as needed, for example log it or return a custom error message
118
- Console.error(
119
- "Error logging in to Rigo, did you already accepted Rigobot?:",
120
- error
121
- )
122
- throw new Error("Failed to log in to Rigo")
123
- }
124
- }
125
-
126
- const publish = async (config: any) => {
127
- const keys = [
128
- "difficulty",
129
- "language",
130
- "skills",
131
- "technologies",
132
- "slug",
133
- "repository",
134
- "author",
135
- "title",
136
- ]
137
-
138
- const payload: { [key: string]: string } = {}
139
- for (const k of keys) config[k] ? (payload[k] = config[k]) : null
140
- try {
141
- console.log("Package to publish:", payload)
142
- cli.action.start("Updating package information...")
143
- await cli.wait(1000)
144
- const data = await fetch(`${HOST}/v1/package/${config.slug}`, {
145
- method: "PUT",
146
- body: JSON.stringify(payload),
147
- })
148
- cli.action.stop("ready")
149
- return data
150
- } catch (error) {
151
- Console.error((error as TypeError).message)
152
- Console.debug(error)
153
- throw error
154
- }
155
- }
156
-
157
- const update = async (config: any) => {
158
- try {
159
- cli.action.start("Updating package information...")
160
- await cli.wait(1000)
161
- const data = await fetch(`${HOST}/v1/package/`, {
162
- method: "POST",
163
- body: JSON.stringify(config),
164
- })
165
- cli.action.stop("ready")
166
- return data
167
- } catch (error) {
168
- Console.error((error as any).message)
169
- Console.debug(error)
170
- throw error
171
- }
172
- }
173
-
174
- const getPackage = async (slug: string) => {
175
- try {
176
- cli.action.start("Downloading package information...")
177
- await cli.wait(1000)
178
- const data = await fetch(`${HOST}/v1/package/${slug}`)
179
- cli.action.stop("ready")
180
- return data
181
- } catch (error) {
182
- if ((error as any).status === 404)
183
- Console.error(`Package ${slug} does not exist`)
184
- else Console.error(`Package ${slug} does not exist`)
185
- Console.debug(error)
186
- throw error
187
- }
188
- }
189
-
190
- const getLangs = async () => {
191
- try {
192
- cli.action.start("Downloading language options...")
193
- await cli.wait(1000)
194
- const data = await fetch(`${HOST}/v1/package/language`)
195
- cli.action.stop("ready")
196
- return data
197
- } catch (error) {
198
- if ((error as any).status === 404)
199
- Console.error("Package slug does not exist")
200
- else Console.error("Package slug does not exist")
201
- Console.debug(error)
202
- throw error
203
- }
204
- }
205
-
206
- const getAllPackages = async ({
207
- lang = "",
208
- slug = "",
209
- }: {
210
- lang?: string;
211
- slug?: string;
212
- }) => {
213
- try {
214
- cli.action.start("Downloading packages...")
215
- await cli.wait(1000)
216
- const data = await fetch(
217
- `${HOST}/v1/package/all?limit=100&language=${lang}&slug=${slug}`
218
- )
219
- cli.action.stop("ready")
220
- return data
221
- } catch (error) {
222
- Console.error(`Package ${slug} does not exist`)
223
- Console.debug(error)
224
- throw error
225
- }
226
- }
227
-
228
- const APIError = (error: TypeError | string, code?: number) => {
229
- const message: string = (error as TypeError).message || (error as string)
230
- const _err = new Error(message) as any
231
- _err.status = code || 400
232
- return _err
233
- }
234
-
235
- const sendBatchTelemetry = async function (url: string, body: any) {
236
- if (!url) {
237
- return
238
- }
239
-
240
- const session = await storage.getItem("bc-payload")
241
- if (
242
- !session ||
243
- !Object.prototype.hasOwnProperty.call(session, "token") ||
244
- session.token === ""
245
- ) {
246
- Console.debug("No token found, skipping batch telemetry delivery")
247
- return
248
- }
249
-
250
- if (!session || !session.user_id || session.user_id === "") {
251
- Console.debug("No user_id found, skipping batch telemetry delivery")
252
- return
253
- }
254
-
255
- body.user_id = session.user_id
256
-
257
- fetch(
258
- url,
259
- {
260
- method: "POST",
261
- body: JSON.stringify(body),
262
- },
263
- false
264
- )
265
- .then(response => {
266
- Console.debug("Telemetry sent successfully")
267
- return response
268
- })
269
- .catch(error => {
270
- Console.debug("Error while sending batch Telemetry", error)
271
- })
272
- }
273
-
274
- const sendStreamTelemetry = async function (url: string, body: object) {
275
- if (!url) {
276
- return
277
- }
278
-
279
- const session = await storage.getItem("bc-payload")
280
- if (
281
- !session ||
282
- !Object.prototype.hasOwnProperty.call(session, "token") ||
283
- session.token === ""
284
- ) {
285
- Console.debug("No token found, skipping stream telemetry delivery")
286
- return
287
- }
288
-
289
- fetch(
290
- url,
291
- {
292
- method: "POST",
293
- body: JSON.stringify(body),
294
- },
295
- false
296
- )
297
- .then(response => {
298
- return response
299
- })
300
- .catch(error => {
301
- Console.debug("Error while sending stream Telemetry", error)
302
- })
303
- }
304
-
305
- type TConsumableSlug =
306
- | "ai-conversation-message"
307
- | "ai-compilation"
308
- | "ai-tutorial-generation"
309
- | "ai-generation"
310
- | "learnpack-publish";
311
-
312
- export const countConsumables = (
313
- consumables: any,
314
- consumableSlug: TConsumableSlug = "ai-tutorial-generation"
315
- ) => {
316
- // Find the void that matches the consumableSlug
317
-
318
- const consumable = consumables.voids.find(
319
- (voidItem: any) => voidItem.slug === consumableSlug
320
- )
321
-
322
- // Return the available units or 0 if not found
323
- return consumable ? consumable.balance.unit : 0
324
- }
325
-
326
- export const getConsumable = async (
327
- token: string,
328
- consumableSlug: TConsumableSlug = "ai-generation"
329
- ): Promise<any> => {
330
- const url = `${HOST}/v1/payments/me/service/consumable?virtual=true`
331
-
332
- const headers = {
333
- Authorization: `Token ${token}`,
334
- }
335
-
336
- try {
337
- const response = await axios.get(url, { headers })
338
-
339
- const count = countConsumables(response.data, consumableSlug)
340
-
341
- return { count }
342
- } catch (error) {
343
- console.error("Error fetching consumables:", error)
344
- throw error
345
- }
346
- }
347
-
348
- export interface TAcademy {
349
- id: number;
350
- name: string;
351
- slug: string;
352
- timezone: string;
353
- }
354
-
355
- const neededPermissions = [
356
- "add_asset",
357
- "change_asset",
358
- "view_asset",
359
- "delete_asset",
360
- ]
361
-
362
- export const listUserAcademies = async (
363
- breathecodeToken: string
364
- ): Promise<TAcademy[]> => {
365
- const url = "https://breathecode.herokuapp.com/v1/auth/user/me"
366
-
367
- try {
368
- const response = await axios.get(url, {
369
- headers: {
370
- Authorization: `Token ${breathecodeToken}`,
371
- },
372
- })
373
-
374
- const data = response.data
375
-
376
- const academiesMap = new Map<number, TAcademy>()
377
-
378
- for (const role of data.roles) {
379
- const academy = role.academy
380
- if (!academiesMap.has(academy.id)) {
381
- academiesMap.set(academy.id, academy)
382
- }
383
- }
384
-
385
- const permissions = new Set(data.permissions.map((p: any) => p.codename))
386
-
387
- // Validate if the user has ALL the needed permissions
388
- const hasAllPermissions = neededPermissions.every(permission =>
389
- permissions.has(permission)
390
- )
391
-
392
- if (!hasAllPermissions) {
393
- // The user does not have all the needed permissions
394
-
395
- return []
396
- }
397
-
398
- return [...academiesMap.values()]
399
- } catch (error) {
400
- console.error("Failed to fetch user academies:", error)
401
- return []
402
- }
403
- }
404
-
405
- export const validateToken = async (token: string) => {
406
- const url = "https://breathecode.herokuapp.com/v1/auth/user/me"
407
- const headers = {
408
- Authorization: `Token ${token}`,
409
- }
410
-
411
- try {
412
- const response = await axios.get(url, { headers })
413
- return response.data
414
- } catch {
415
- return false
416
- }
417
- }
418
-
419
- type TAssetMissing = {
420
- slug: string;
421
- title: string;
422
- lang: string;
423
- graded: boolean;
424
- url: string;
425
- description: string;
426
- learnpack_deploy_url: string;
427
- technologies: string[];
428
- category: number | string;
429
- owner: number;
430
- author: number;
431
- preview: string;
432
- readme_raw: string;
433
- all_translations: string[];
434
- academy_id?: number;
435
- };
436
-
437
- export const createAsset = async (token: string, asset: TAssetMissing) => {
438
- const body: any = {
439
- slug: asset.slug,
440
- title: asset.title,
441
- lang: asset.lang,
442
- asset_type: "EXERCISE",
443
- visibility: "PUBLIC",
444
- status: "DRAFT",
445
- url: asset.url,
446
- readme_url: null,
447
- difficulty: null,
448
- duration: null,
449
- graded: asset.graded,
450
- gitpod: true,
451
- category: asset.category,
452
- owner: asset.owner,
453
- author: asset.author,
454
- preview: asset.preview,
455
- description: asset.description,
456
- external: false,
457
- interactive: true,
458
- solution_video_url: null,
459
- intro_video_url: null,
460
- translations: [asset.lang],
461
- learnpack_deploy_url: asset.learnpack_deploy_url,
462
- technologies: asset.technologies,
463
- readme_raw: asset.readme_raw,
464
- all_translations: asset.all_translations,
465
- }
466
-
467
- let url = `https://breathecode.herokuapp.com/v1/registry/asset/me`
468
- const headers: any = {
469
- Authorization: `Token ${token}`,
470
- }
471
-
472
- // Use academy-specific endpoint if academy_id is provided
473
- if (asset.academy_id !== undefined) {
474
- url = `https://breathecode.herokuapp.com/v1/registry/academy/asset`
475
- headers.Academy = String(asset.academy_id)
476
- }
477
-
478
- try {
479
- const response = await axios.post(url, body, { headers })
480
- return response.data
481
- } catch (error: any) {
482
- console.error("Failed to create asset:", error)
483
- throw error.response.data
484
- }
485
- }
486
-
487
- export const doesAssetExists = async (
488
- token: string,
489
- assetSlug: string
490
- ): Promise<{ exists: boolean; academyId?: number }> => {
491
- const url = `https://breathecode.herokuapp.com/v1/registry/asset/${assetSlug}`
492
-
493
- const headers = {
494
- Authorization: `Token ${token}`,
495
- }
496
-
497
- try {
498
- const response = await axios.get(url, { headers })
499
- if (response.status === 200) {
500
- const academyId = response.data?.academy?.id || response.data?.academy_id
501
- return { exists: true, academyId }
502
- }
503
-
504
- return { exists: false }
505
- } catch {
506
- // console.error("Failed to get asset:", error)
507
- return { exists: false }
508
- }
509
- }
510
-
511
- const updateAsset = async (
512
- token: string,
513
- assetSlug: string,
514
- asset: Partial<TAssetMissing>
515
- ) => {
516
- const url = `https://breathecode.herokuapp.com/v1/registry/asset/me/${assetSlug}`
517
- const headers = {
518
- Authorization: `Token ${token}`,
519
- }
520
- try {
521
- const response = await axios.put(url, asset, { headers })
522
- return response.data
523
- } catch (error: any) {
524
- console.error("Failed to update asset:", error)
525
- // Try to print the data
526
- // console.log(error.response.data)
527
- throw error.response.data
528
- }
529
- }
530
-
531
- const getCategories = async (token: string) => {
532
- const url = `${HOST}/v1/registry/category`
533
- const headers = {
534
- Authorization: `Token ${token}`,
535
- }
536
- try {
537
- const response = await axios.get(url, { headers })
538
- return response.data
539
- } catch (error) {
540
- console.error("Failed to get categories:", error)
541
- throw error
542
- }
543
- }
544
-
545
- const updateRigoPackage = async (
546
- token: string,
547
- slug: string,
548
- updates: { asset_id?: number; new_slug?: string }
549
- ) => {
550
- const cleanToken = token.replace(/[\n\r]/g, "")
551
-
552
- const url = `${RIGOBOT_HOST}/v1/learnpack/package/${slug}/`
553
-
554
- try {
555
- const response = await axios.put(url, updates, {
556
- headers: {
557
- Authorization: "Token " + cleanToken,
558
- },
559
- })
560
- return response.data
561
- } catch (error) {
562
- console.error("Failed to update Rigo package:", error)
563
- throw error
564
- }
565
- }
566
-
567
- const createRigoPackage = async (token: string, slug: string, config: any) => {
568
- const url = `${RIGOBOT_HOST}/v1/learnpack/package`
569
- const cleanToken = token.replace(/[\n\r]/g, "")
570
-
571
- try {
572
- const response = await axios.post(
573
- url,
574
- { slug, config },
575
- {
576
- headers: {
577
- Authorization: "Token " + cleanToken,
578
- },
579
- }
580
- )
581
- return response.data
582
- } catch (error) {
583
- console.error("Failed to create Rigo package:", error)
584
- throw error
585
- }
586
- }
587
-
588
- type TTechnology = {
589
- slug: string;
590
- lang: string;
591
- };
592
-
593
- let technologiesCache: TTechnology[] = []
594
-
595
- export const fetchTechnologies = async () => {
596
- const BREATHECODE_PERMANENT_TOKEN = process.env.BREATHECODE_PERMANENT_TOKEN
597
- const LANGS = ["en", "es"]
598
-
599
- if (!BREATHECODE_PERMANENT_TOKEN) {
600
- throw new Error(
601
- "BREATHECODE_PERMANENT_TOKEN is not defined in environment variables"
602
- )
603
- }
604
-
605
- const headers = {
606
- Authorization: `Token ${BREATHECODE_PERMANENT_TOKEN}`,
607
- }
608
-
609
- const results = await Promise.all(
610
- LANGS.map(lang =>
611
- axios
612
- .get(`${HOST}/v1/registry/technology?lang=${lang}`, { headers })
613
- .then(res => {
614
- return res.data
615
- })
616
- .then(data =>
617
- data.map((item: any) => ({
618
- slug: item.slug,
619
- lang: lang,
620
- }))
621
- )
622
- )
623
- )
624
-
625
- const allItems = results.flat()
626
-
627
- // Remove duplicates by slug+lang combination
628
- const unique = []
629
- const seen = new Set()
630
- for (const item of allItems) {
631
- const key = `${item.slug}:${item.lang}`
632
- if (!seen.has(key)) {
633
- seen.add(key)
634
- unique.push(item)
635
- }
636
- }
637
-
638
- return unique
639
- }
640
-
641
- // Function to update the cache and schedule the next update
642
- async function updateTechnologiesPeriodically() {
643
- try {
644
- technologiesCache = await fetchTechnologies()
645
- // Uncomment for debugging:
646
- // console.log('Technologies list updated:', technologiesCache);
647
- } catch (error: any) {
648
- console.error("Error updating technologies list:", error)
649
- } finally {
650
- setTimeout(updateTechnologiesPeriodically, 24 * 60 * 60 * 1000)
651
- }
652
- }
653
-
654
- export const getCurrentTechnologies = () => technologiesCache
655
-
656
- export default {
657
- login,
658
- publish,
659
- update,
660
- getPackage,
661
- getLangs,
662
- getAllPackages,
663
- sendBatchTelemetry,
664
- sendStreamTelemetry,
665
- listUserAcademies,
666
- validateToken,
667
- createAsset,
668
- doesAssetExists,
669
- updateAsset,
670
- getCategories,
671
- updateRigoPackage,
672
- createRigoPackage,
673
- getCurrentTechnologies,
674
- updateTechnologiesPeriodically,
675
- }
1
+ import Console from "../utils/console"
2
+ import * as storage from "node-persist"
3
+ import cli from "cli-ux"
4
+ import axios from "axios"
5
+ import * as dotenv from "dotenv"
6
+
7
+ dotenv.config()
8
+
9
+ const HOST = "https://breathecode.herokuapp.com"
10
+ export const RIGOBOT_HOST = "https://rigobot.herokuapp.com"
11
+ export const RIGOBOT_REALTIME_HOST = "https://ai.4geeks.com"
12
+ // export const RIGOBOT_REALTIME_HOST = "http://127.0.0.1:8003"
13
+ // export const RIGOBOT_HOST = "https://rigobot-test-cca7d841c9d8.herokuapp.com"
14
+ // export const RIGOBOT_HOST =
15
+ // "https://8000-charlytoc-rigobot-bmwdeam7cev.ws-us118.gitpod.io"
16
+
17
+ // eslint-disable-next-line
18
+ const _fetch = require("node-fetch");
19
+
20
+ interface IHeaders {
21
+ "Content-Type"?: string;
22
+ Authorization?: string;
23
+ }
24
+
25
+ interface IOptions {
26
+ headers?: IHeaders;
27
+ method?: string;
28
+ body?: string;
29
+ }
30
+
31
+ const fetch = async (
32
+ url: string,
33
+ options: IOptions = {},
34
+ returnAsJson = true
35
+ ) => {
36
+ const headers: IHeaders = { "Content-Type": "application/json" }
37
+ Console.debug(`Fetching ${url}`)
38
+ let session = null
39
+ try {
40
+ session = await storage.getItem("bc-payload")
41
+ if (session.token && session.token !== "" && !url.includes("/token"))
42
+ headers.Authorization = "Token " + session.token
43
+ } catch {}
44
+
45
+ try {
46
+ const resp = await _fetch(url, {
47
+ ...options,
48
+ headers: { ...headers, ...options.headers },
49
+ } as any)
50
+
51
+ if (resp.status >= 200 && resp.status < 300) {
52
+ return returnAsJson ? await resp.json() : await resp.text()
53
+ }
54
+
55
+ if (resp.status === 401)
56
+ Console.debug("Invalid authentication credentials", `Code: 401`)
57
+ // throw APIError("Invalid authentication credentials", 401)
58
+ else if (resp.status === 404) throw APIError("Package not found", 404)
59
+ else if (resp.status >= 500)
60
+ throw APIError("Impossible to connect with the server", 500)
61
+ else if (resp.status >= 400) {
62
+ const error = await resp.json()
63
+ if (error.detail || error.error) {
64
+ throw APIError(error.detail || error.error)
65
+ } else if (error.nonFieldErrors) {
66
+ throw APIError(error.nonFieldErrors[0], error)
67
+ } else if (typeof error === "object") {
68
+ if (Object.keys(error).length > 0) {
69
+ const key = error[Object.keys(error)[0]]
70
+ throw APIError(`${key}: ${error[key][0]}`, error)
71
+ }
72
+ } else {
73
+ throw APIError("Uknown error")
74
+ }
75
+ } else throw APIError("Uknown error")
76
+ } catch (error) {
77
+ Console.error((error as TypeError).message)
78
+ throw error
79
+ }
80
+ }
81
+
82
+ const login = async (identification: string, password: string) => {
83
+ try {
84
+ cli.action.start(`Looking for credentials with ${identification}`)
85
+ await cli.wait(1000)
86
+ const url = `${HOST}/v1/auth/login/`
87
+
88
+ const res = await axios.post(url, {
89
+ email: identification,
90
+ password: password,
91
+ })
92
+ const data = res.data
93
+
94
+ cli.action.stop("ready")
95
+ let rigoPayload = null
96
+ try {
97
+ rigoPayload = await loginRigo(data.token)
98
+ } catch {
99
+ return { ...data, rigobot: null }
100
+ }
101
+
102
+ return { ...data, rigobot: rigoPayload }
103
+ } catch (error) {
104
+ cli.action.stop("error")
105
+ Console.error((error as TypeError).message)
106
+ Console.debug(error)
107
+ }
108
+ }
109
+
110
+ const loginRigo = async (token: string) => {
111
+ try {
112
+ const rigoUrl = `${RIGOBOT_HOST}/v1/auth/me/token?breathecode_token=${token}`
113
+ const rigoResp = await _fetch(rigoUrl)
114
+ const rigobotJson = await rigoResp.json()
115
+ return rigobotJson
116
+ } catch (error) {
117
+ // Handle the error as needed, for example log it or return a custom error message
118
+ Console.error(
119
+ "Error logging in to Rigo, did you already accepted Rigobot?:",
120
+ error
121
+ )
122
+ throw new Error("Failed to log in to Rigo")
123
+ }
124
+ }
125
+
126
+ const publish = async (config: any) => {
127
+ const keys = [
128
+ "difficulty",
129
+ "language",
130
+ "skills",
131
+ "technologies",
132
+ "slug",
133
+ "repository",
134
+ "author",
135
+ "title",
136
+ ]
137
+
138
+ const payload: { [key: string]: string } = {}
139
+ for (const k of keys) config[k] ? (payload[k] = config[k]) : null
140
+ try {
141
+ console.log("Package to publish:", payload)
142
+ cli.action.start("Updating package information...")
143
+ await cli.wait(1000)
144
+ const data = await fetch(`${HOST}/v1/package/${config.slug}`, {
145
+ method: "PUT",
146
+ body: JSON.stringify(payload),
147
+ })
148
+ cli.action.stop("ready")
149
+ return data
150
+ } catch (error) {
151
+ Console.error((error as TypeError).message)
152
+ Console.debug(error)
153
+ throw error
154
+ }
155
+ }
156
+
157
+ const update = async (config: any) => {
158
+ try {
159
+ cli.action.start("Updating package information...")
160
+ await cli.wait(1000)
161
+ const data = await fetch(`${HOST}/v1/package/`, {
162
+ method: "POST",
163
+ body: JSON.stringify(config),
164
+ })
165
+ cli.action.stop("ready")
166
+ return data
167
+ } catch (error) {
168
+ Console.error((error as any).message)
169
+ Console.debug(error)
170
+ throw error
171
+ }
172
+ }
173
+
174
+ const getPackage = async (slug: string) => {
175
+ try {
176
+ cli.action.start("Downloading package information...")
177
+ await cli.wait(1000)
178
+ const data = await fetch(`${HOST}/v1/package/${slug}`)
179
+ cli.action.stop("ready")
180
+ return data
181
+ } catch (error) {
182
+ if ((error as any).status === 404)
183
+ Console.error(`Package ${slug} does not exist`)
184
+ else Console.error(`Package ${slug} does not exist`)
185
+ Console.debug(error)
186
+ throw error
187
+ }
188
+ }
189
+
190
+ const getLangs = async () => {
191
+ try {
192
+ cli.action.start("Downloading language options...")
193
+ await cli.wait(1000)
194
+ const data = await fetch(`${HOST}/v1/package/language`)
195
+ cli.action.stop("ready")
196
+ return data
197
+ } catch (error) {
198
+ if ((error as any).status === 404)
199
+ Console.error("Package slug does not exist")
200
+ else Console.error("Package slug does not exist")
201
+ Console.debug(error)
202
+ throw error
203
+ }
204
+ }
205
+
206
+ const getAllPackages = async ({
207
+ lang = "",
208
+ slug = "",
209
+ }: {
210
+ lang?: string;
211
+ slug?: string;
212
+ }) => {
213
+ try {
214
+ cli.action.start("Downloading packages...")
215
+ await cli.wait(1000)
216
+ const data = await fetch(
217
+ `${HOST}/v1/package/all?limit=100&language=${lang}&slug=${slug}`
218
+ )
219
+ cli.action.stop("ready")
220
+ return data
221
+ } catch (error) {
222
+ Console.error(`Package ${slug} does not exist`)
223
+ Console.debug(error)
224
+ throw error
225
+ }
226
+ }
227
+
228
+ const APIError = (error: TypeError | string, code?: number) => {
229
+ const message: string = (error as TypeError).message || (error as string)
230
+ const _err = new Error(message) as any
231
+ _err.status = code || 400
232
+ return _err
233
+ }
234
+
235
+ const sendBatchTelemetry = async function (url: string, body: any) {
236
+ if (!url) {
237
+ return
238
+ }
239
+
240
+ const session = await storage.getItem("bc-payload")
241
+ if (
242
+ !session ||
243
+ !Object.prototype.hasOwnProperty.call(session, "token") ||
244
+ session.token === ""
245
+ ) {
246
+ Console.debug("No token found, skipping batch telemetry delivery")
247
+ return
248
+ }
249
+
250
+ if (!session || !session.user_id || session.user_id === "") {
251
+ Console.debug("No user_id found, skipping batch telemetry delivery")
252
+ return
253
+ }
254
+
255
+ body.user_id = session.user_id
256
+
257
+ fetch(
258
+ url,
259
+ {
260
+ method: "POST",
261
+ body: JSON.stringify(body),
262
+ },
263
+ false
264
+ )
265
+ .then(response => {
266
+ Console.debug("Telemetry sent successfully")
267
+ return response
268
+ })
269
+ .catch(error => {
270
+ Console.debug("Error while sending batch Telemetry", error)
271
+ })
272
+ }
273
+
274
+ const sendStreamTelemetry = async function (url: string, body: object) {
275
+ if (!url) {
276
+ return
277
+ }
278
+
279
+ const session = await storage.getItem("bc-payload")
280
+ if (
281
+ !session ||
282
+ !Object.prototype.hasOwnProperty.call(session, "token") ||
283
+ session.token === ""
284
+ ) {
285
+ Console.debug("No token found, skipping stream telemetry delivery")
286
+ return
287
+ }
288
+
289
+ fetch(
290
+ url,
291
+ {
292
+ method: "POST",
293
+ body: JSON.stringify(body),
294
+ },
295
+ false
296
+ )
297
+ .then(response => {
298
+ return response
299
+ })
300
+ .catch(error => {
301
+ Console.debug("Error while sending stream Telemetry", error)
302
+ })
303
+ }
304
+
305
+ type TConsumableSlug =
306
+ | "ai-conversation-message"
307
+ | "ai-compilation"
308
+ | "ai-tutorial-generation"
309
+ | "ai-generation"
310
+ | "learnpack-publish";
311
+
312
+ export const countConsumables = (
313
+ consumables: any,
314
+ consumableSlug: TConsumableSlug = "ai-tutorial-generation"
315
+ ) => {
316
+ // Find the void that matches the consumableSlug
317
+
318
+ const consumable = consumables.voids.find(
319
+ (voidItem: any) => voidItem.slug === consumableSlug
320
+ )
321
+
322
+ // Return the available units or 0 if not found
323
+ return consumable ? consumable.balance.unit : 0
324
+ }
325
+
326
+ export const getConsumable = async (
327
+ token: string,
328
+ consumableSlug: TConsumableSlug = "ai-generation"
329
+ ): Promise<any> => {
330
+ const url = `${HOST}/v1/payments/me/service/consumable?virtual=true`
331
+
332
+ const headers = {
333
+ Authorization: `Token ${token}`,
334
+ }
335
+
336
+ try {
337
+ const response = await axios.get(url, { headers })
338
+
339
+ const count = countConsumables(response.data, consumableSlug)
340
+
341
+ return { count }
342
+ } catch (error) {
343
+ console.error("Error fetching consumables:", error)
344
+ throw error
345
+ }
346
+ }
347
+
348
+ export interface TAcademy {
349
+ id: number;
350
+ name: string;
351
+ slug: string;
352
+ timezone: string;
353
+ }
354
+
355
+ const neededPermissions = [
356
+ "add_asset",
357
+ "change_asset",
358
+ "view_asset",
359
+ "delete_asset",
360
+ ]
361
+
362
+ export const listUserAcademies = async (
363
+ breathecodeToken: string
364
+ ): Promise<TAcademy[]> => {
365
+ const url = "https://breathecode.herokuapp.com/v1/auth/user/me"
366
+
367
+ try {
368
+ const response = await axios.get(url, {
369
+ headers: {
370
+ Authorization: `Token ${breathecodeToken}`,
371
+ },
372
+ })
373
+
374
+ const data = response.data
375
+
376
+ const academiesMap = new Map<number, TAcademy>()
377
+
378
+ for (const role of data.roles) {
379
+ const academy = role.academy
380
+ if (!academiesMap.has(academy.id)) {
381
+ academiesMap.set(academy.id, academy)
382
+ }
383
+ }
384
+
385
+ const permissions = new Set(data.permissions.map((p: any) => p.codename))
386
+
387
+ // Validate if the user has ALL the needed permissions
388
+ const hasAllPermissions = neededPermissions.every(permission =>
389
+ permissions.has(permission)
390
+ )
391
+
392
+ if (!hasAllPermissions) {
393
+ // The user does not have all the needed permissions
394
+
395
+ return []
396
+ }
397
+
398
+ return [...academiesMap.values()]
399
+ } catch (error) {
400
+ console.error("Failed to fetch user academies:", error)
401
+ return []
402
+ }
403
+ }
404
+
405
+ export const validateToken = async (token: string) => {
406
+ const url = "https://breathecode.herokuapp.com/v1/auth/user/me"
407
+ const headers = {
408
+ Authorization: `Token ${token}`,
409
+ }
410
+
411
+ try {
412
+ const response = await axios.get(url, { headers })
413
+ return response.data
414
+ } catch {
415
+ return false
416
+ }
417
+ }
418
+
419
+ type TAssetMissing = {
420
+ slug: string;
421
+ title: string;
422
+ lang: string;
423
+ graded: boolean;
424
+ url: string;
425
+ description: string;
426
+ learnpack_deploy_url: string;
427
+ technologies: string[];
428
+ category: number | string;
429
+ owner: number;
430
+ author: number;
431
+ preview: string;
432
+ readme_raw: string;
433
+ all_translations: string[];
434
+ academy_id?: number;
435
+ };
436
+
437
+ export const createAsset = async (token: string, asset: TAssetMissing) => {
438
+ const body: any = {
439
+ slug: asset.slug,
440
+ title: asset.title,
441
+ lang: asset.lang,
442
+ asset_type: "EXERCISE",
443
+ visibility: "PUBLIC",
444
+ status: "DRAFT",
445
+ url: asset.url,
446
+ readme_url: null,
447
+ difficulty: null,
448
+ duration: null,
449
+ graded: asset.graded,
450
+ gitpod: true,
451
+ category: asset.category,
452
+ owner: asset.owner,
453
+ author: asset.author,
454
+ preview: asset.preview,
455
+ description: asset.description,
456
+ external: false,
457
+ interactive: true,
458
+ solution_video_url: null,
459
+ intro_video_url: null,
460
+ translations: [asset.lang],
461
+ learnpack_deploy_url: asset.learnpack_deploy_url,
462
+ technologies: asset.technologies,
463
+ readme_raw: asset.readme_raw,
464
+ all_translations: asset.all_translations,
465
+ }
466
+
467
+ let url = `https://breathecode.herokuapp.com/v1/registry/asset/me`
468
+ const headers: any = {
469
+ Authorization: `Token ${token}`,
470
+ }
471
+
472
+ // Use academy-specific endpoint if academy_id is provided
473
+ if (asset.academy_id !== undefined) {
474
+ url = `https://breathecode.herokuapp.com/v1/registry/academy/asset`
475
+ headers.Academy = String(asset.academy_id)
476
+ }
477
+
478
+ try {
479
+ const response = await axios.post(url, body, { headers })
480
+ return response.data
481
+ } catch (error: any) {
482
+ console.error("Failed to create asset:", error)
483
+ throw error.response.data
484
+ }
485
+ }
486
+
487
+ export const doesAssetExists = async (
488
+ token: string,
489
+ assetSlug: string
490
+ ): Promise<{ exists: boolean; academyId?: number }> => {
491
+ const url = `https://breathecode.herokuapp.com/v1/registry/asset/${assetSlug}`
492
+
493
+ const headers = {
494
+ Authorization: `Token ${token}`,
495
+ }
496
+
497
+ try {
498
+ const response = await axios.get(url, { headers })
499
+ if (response.status === 200) {
500
+ const academyId = response.data?.academy?.id || response.data?.academy_id
501
+ return { exists: true, academyId }
502
+ }
503
+
504
+ return { exists: false }
505
+ } catch {
506
+ // console.error("Failed to get asset:", error)
507
+ return { exists: false }
508
+ }
509
+ }
510
+
511
+ const updateAsset = async (
512
+ token: string,
513
+ assetSlug: string,
514
+ asset: Partial<TAssetMissing>
515
+ ) => {
516
+ const url = `https://breathecode.herokuapp.com/v1/registry/asset/me/${assetSlug}`
517
+ const headers = {
518
+ Authorization: `Token ${token}`,
519
+ }
520
+ try {
521
+ const response = await axios.put(url, asset, { headers })
522
+ return response.data
523
+ } catch (error: any) {
524
+ console.error("Failed to update asset:", error)
525
+ // Try to print the data
526
+ // console.log(error.response.data)
527
+ throw error.response.data
528
+ }
529
+ }
530
+
531
+ const getCategories = async (token: string) => {
532
+ const url = `${HOST}/v1/registry/category`
533
+ const headers = {
534
+ Authorization: `Token ${token}`,
535
+ }
536
+ try {
537
+ const response = await axios.get(url, { headers })
538
+ return response.data
539
+ } catch (error) {
540
+ console.error("Failed to get categories:", error)
541
+ throw error
542
+ }
543
+ }
544
+
545
+ const updateRigoPackage = async (
546
+ token: string,
547
+ slug: string,
548
+ updates: { asset_id?: number; new_slug?: string }
549
+ ) => {
550
+ const cleanToken = token.replace(/[\n\r]/g, "")
551
+
552
+ const url = `${RIGOBOT_HOST}/v1/learnpack/package/${slug}/`
553
+
554
+ try {
555
+ const response = await axios.put(url, updates, {
556
+ headers: {
557
+ Authorization: "Token " + cleanToken,
558
+ },
559
+ })
560
+ return response.data
561
+ } catch (error) {
562
+ console.error("Failed to update Rigo package:", error)
563
+ throw error
564
+ }
565
+ }
566
+
567
+ const createRigoPackage = async (token: string, slug: string, config: any) => {
568
+ const url = `${RIGOBOT_HOST}/v1/learnpack/package`
569
+ const cleanToken = token.replace(/[\n\r]/g, "")
570
+
571
+ try {
572
+ const response = await axios.post(
573
+ url,
574
+ { slug, config },
575
+ {
576
+ headers: {
577
+ Authorization: "Token " + cleanToken,
578
+ },
579
+ }
580
+ )
581
+ return response.data
582
+ } catch (error) {
583
+ console.error("Failed to create Rigo package:", error)
584
+ throw error
585
+ }
586
+ }
587
+
588
+ type TTechnology = {
589
+ slug: string;
590
+ lang: string;
591
+ };
592
+
593
+ let technologiesCache: TTechnology[] = []
594
+
595
+ export const fetchTechnologies = async () => {
596
+ const BREATHECODE_PERMANENT_TOKEN = process.env.BREATHECODE_PERMANENT_TOKEN
597
+ const LANGS = ["en", "es"]
598
+
599
+ if (!BREATHECODE_PERMANENT_TOKEN) {
600
+ throw new Error(
601
+ "BREATHECODE_PERMANENT_TOKEN is not defined in environment variables"
602
+ )
603
+ }
604
+
605
+ const headers = {
606
+ Authorization: `Token ${BREATHECODE_PERMANENT_TOKEN}`,
607
+ }
608
+
609
+ const results = await Promise.all(
610
+ LANGS.map(lang =>
611
+ axios
612
+ .get(`${HOST}/v1/registry/technology?lang=${lang}`, { headers })
613
+ .then(res => {
614
+ return res.data
615
+ })
616
+ .then(data =>
617
+ data.map((item: any) => ({
618
+ slug: item.slug,
619
+ lang: lang,
620
+ }))
621
+ )
622
+ )
623
+ )
624
+
625
+ const allItems = results.flat()
626
+
627
+ // Remove duplicates by slug+lang combination
628
+ const unique = []
629
+ const seen = new Set()
630
+ for (const item of allItems) {
631
+ const key = `${item.slug}:${item.lang}`
632
+ if (!seen.has(key)) {
633
+ seen.add(key)
634
+ unique.push(item)
635
+ }
636
+ }
637
+
638
+ return unique
639
+ }
640
+
641
+ // Function to update the cache and schedule the next update
642
+ async function updateTechnologiesPeriodically() {
643
+ try {
644
+ technologiesCache = await fetchTechnologies()
645
+ // Uncomment for debugging:
646
+ // console.log('Technologies list updated:', technologiesCache);
647
+ } catch (error: any) {
648
+ console.error("Error updating technologies list:", error)
649
+ } finally {
650
+ setTimeout(updateTechnologiesPeriodically, 24 * 60 * 60 * 1000)
651
+ }
652
+ }
653
+
654
+ export const getCurrentTechnologies = () => technologiesCache
655
+
656
+ export default {
657
+ login,
658
+ publish,
659
+ update,
660
+ getPackage,
661
+ getLangs,
662
+ getAllPackages,
663
+ sendBatchTelemetry,
664
+ sendStreamTelemetry,
665
+ listUserAcademies,
666
+ validateToken,
667
+ createAsset,
668
+ doesAssetExists,
669
+ updateAsset,
670
+ getCategories,
671
+ updateRigoPackage,
672
+ createRigoPackage,
673
+ getCurrentTechnologies,
674
+ updateTechnologiesPeriodically,
675
+ }