@learnpack/learnpack 5.0.308 → 5.0.310

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 (88) hide show
  1. package/README.md +409 -409
  2. package/lib/commands/audit.js +15 -15
  3. package/lib/commands/breakToken.js +19 -19
  4. package/lib/commands/clean.js +3 -3
  5. package/lib/commands/logout.js +3 -3
  6. package/lib/commands/serve.js +16 -16
  7. package/lib/creatorDist/assets/{index-B37w_ZhT.js → index-BI7U47zy.js} +13186 -13013
  8. package/lib/creatorDist/index.html +1 -1
  9. package/lib/managers/config/index.js +77 -77
  10. package/lib/utils/creatorUtilities.js +14 -14
  11. package/lib/utils/templates/isolated/exercises/01-hello-world/README.es.md +26 -26
  12. package/lib/utils/templates/isolated/exercises/01-hello-world/README.md +26 -26
  13. package/lib/utils/templates/scorm/adlcp_rootv1p2.xsd +110 -110
  14. package/lib/utils/templates/scorm/config/index.html +209 -209
  15. package/lib/utils/templates/scorm/ims_xml.xsd +1 -1
  16. package/lib/utils/templates/scorm/imscp_rootv1p1p2.xsd +345 -345
  17. package/lib/utils/templates/scorm/imsmanifest.xml +38 -38
  18. package/lib/utils/templates/scorm/imsmd_rootv1p2p1.xsd +573 -573
  19. package/package.json +1 -1
  20. package/src/commands/audit.ts +487 -487
  21. package/src/commands/breakToken.ts +67 -67
  22. package/src/commands/clean.ts +30 -30
  23. package/src/commands/logout.ts +38 -38
  24. package/src/commands/publish.ts +517 -517
  25. package/src/commands/serve.ts +3179 -3179
  26. package/src/commands/start.ts +333 -333
  27. package/src/commands/translate.ts +123 -123
  28. package/src/creator/README.md +54 -54
  29. package/src/creator/package-lock.json +6621 -6621
  30. package/src/creator/package.json +55 -55
  31. package/src/creator/src/App.tsx +611 -608
  32. package/src/creator/src/components/FileUploader.tsx +340 -302
  33. package/src/creator/src/components/Icon.tsx +18 -18
  34. package/src/creator/src/components/LessonItem.tsx +152 -152
  35. package/src/creator/src/components/Login.tsx +259 -259
  36. package/src/creator/src/components/ParamsChecker.tsx +25 -25
  37. package/src/creator/src/components/Uploader.tsx +3 -6
  38. package/src/creator/src/components/syllabus/ContentIndex.tsx +323 -323
  39. package/src/creator/src/components/syllabus/SyllabusEditor.tsx +341 -337
  40. package/src/creator/src/i18n.ts +28 -28
  41. package/src/creator/src/locales/en.json +139 -138
  42. package/src/creator/src/locales/es.json +139 -138
  43. package/src/creator/src/utils/configTypes.ts +122 -122
  44. package/src/creator/src/utils/constants.ts +13 -13
  45. package/src/creator/src/utils/creatorUtils.ts +46 -46
  46. package/src/creator/src/utils/eventBus.ts +2 -2
  47. package/src/creator/src/utils/lib.ts +172 -172
  48. package/src/creator/src/utils/rigo.ts +1 -1
  49. package/src/creator/src/utils/socket.ts +61 -61
  50. package/src/creator/src/utils/store.ts +222 -222
  51. package/src/creator/src/vite-env.d.ts +1 -1
  52. package/src/creator/vite.config.ts +13 -13
  53. package/src/creatorDist/assets/{index-B37w_ZhT.js → index-BI7U47zy.js} +13186 -13013
  54. package/src/creatorDist/index.html +1 -1
  55. package/src/managers/config/defaults.ts +49 -49
  56. package/src/managers/config/exercise.ts +364 -364
  57. package/src/managers/config/index.ts +775 -775
  58. package/src/managers/file.ts +236 -236
  59. package/src/managers/server/routes.ts +554 -554
  60. package/src/managers/telemetry.ts +188 -188
  61. package/src/models/action.ts +13 -13
  62. package/src/models/config-manager.ts +28 -28
  63. package/src/models/config.ts +106 -106
  64. package/src/models/exercise-obj.ts +30 -30
  65. package/src/models/session.ts +39 -39
  66. package/src/models/socket.ts +61 -61
  67. package/src/models/status.ts +16 -16
  68. package/src/ui/_app/app.css +1 -1
  69. package/src/ui/_app/app.js +477 -407
  70. package/src/ui/app.tar.gz +0 -0
  71. package/src/utils/BaseCommand.ts +56 -56
  72. package/src/utils/api.ts +665 -665
  73. package/src/utils/audit.ts +392 -392
  74. package/src/utils/checkNotInstalled.ts +267 -267
  75. package/src/utils/convertCreds.js +34 -34
  76. package/src/utils/creatorUtilities.ts +504 -504
  77. package/src/utils/export/README.md +178 -178
  78. package/src/utils/incrementVersion.js +74 -74
  79. package/src/utils/misc.ts +58 -58
  80. package/src/utils/sidebarGenerator.ts +195 -195
  81. package/src/utils/templates/isolated/exercises/01-hello-world/README.es.md +26 -26
  82. package/src/utils/templates/isolated/exercises/01-hello-world/README.md +26 -26
  83. package/src/utils/templates/scorm/adlcp_rootv1p2.xsd +110 -110
  84. package/src/utils/templates/scorm/config/index.html +209 -209
  85. package/src/utils/templates/scorm/ims_xml.xsd +1 -1
  86. package/src/utils/templates/scorm/imscp_rootv1p1p2.xsd +345 -345
  87. package/src/utils/templates/scorm/imsmanifest.xml +38 -38
  88. package/src/utils/templates/scorm/imsmd_rootv1p2p1.xsd +573 -573
package/src/utils/api.ts CHANGED
@@ -1,665 +1,665 @@
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
- url: string
424
- description: string
425
- learnpack_deploy_url: string
426
- technologies: string[]
427
- category: number | string
428
- owner: number
429
- author: number
430
- preview: string
431
- readme_raw: string
432
- all_translations: string[]
433
- }
434
-
435
- export const createAsset = async (token: string, asset: TAssetMissing) => {
436
- const body = {
437
- slug: asset.slug,
438
- title: asset.title,
439
- lang: asset.lang,
440
- asset_type: "EXERCISE",
441
- visibility: "PUBLIC",
442
- status: "DRAFT",
443
- url: asset.url,
444
- readme_url: null,
445
- difficulty: null,
446
- duration: null,
447
- graded: false,
448
- gitpod: true,
449
- category: asset.category,
450
- owner: asset.owner,
451
- author: asset.author,
452
- preview: asset.preview,
453
- description: asset.description,
454
- external: false,
455
- interactive: true,
456
- solution_video_url: null,
457
- intro_video_url: null,
458
- translations: [asset.lang],
459
- learnpack_deploy_url: asset.learnpack_deploy_url,
460
- technologies: asset.technologies,
461
- readme_raw: asset.readme_raw,
462
- all_translations: asset.all_translations,
463
- }
464
- const url = `https://breathecode.herokuapp.com/v1/registry/asset/me`
465
- const headers = {
466
- Authorization: `Token ${token}`,
467
- }
468
-
469
- try {
470
- const response = await axios.post(url, body, { headers })
471
- return response.data
472
- } catch (error: any) {
473
- console.error("Failed to create asset:", error)
474
- throw error.response.data
475
- }
476
- }
477
-
478
- export const doesAssetExists = async (
479
- token: string,
480
- assetSlug: string
481
- ): Promise<{ exists: boolean; academyId?: number }> => {
482
- const url = `https://breathecode.herokuapp.com/v1/registry/asset/${assetSlug}`
483
-
484
- const headers = {
485
- Authorization: `Token ${token}`,
486
- }
487
-
488
- try {
489
- const response = await axios.get(url, { headers })
490
- if (response.status === 200) {
491
- return { exists: true }
492
- }
493
-
494
- return { exists: false }
495
- } catch {
496
- // console.error("Failed to get asset:", error)
497
- return { exists: false }
498
- }
499
- }
500
-
501
- const updateAsset = async (
502
- token: string,
503
- assetSlug: string,
504
- asset: Partial<TAssetMissing>
505
- ) => {
506
- const url = `https://breathecode.herokuapp.com/v1/registry/asset/me/${assetSlug}`
507
- const headers = {
508
- Authorization: `Token ${token}`,
509
- }
510
- try {
511
- const response = await axios.put(url, asset, { headers })
512
- return response.data
513
- } catch (error: any) {
514
- console.error("Failed to update asset:", error)
515
- // Try to print the data
516
- // console.log(error.response.data)
517
- throw error.response.data
518
- }
519
- }
520
-
521
- const getCategories = async (token: string) => {
522
- const url = `${HOST}/v1/registry/category`
523
- const headers = {
524
- Authorization: `Token ${token}`,
525
- }
526
- try {
527
- const response = await axios.get(url, { headers })
528
- return response.data
529
- } catch (error) {
530
- console.error("Failed to get categories:", error)
531
- throw error
532
- }
533
- }
534
-
535
- const updateRigoPackage = async (
536
- token: string,
537
- slug: string,
538
- updates: { asset_id?: number; new_slug?: string }
539
- ) => {
540
- const cleanToken = token.replace(/[\n\r]/g, "")
541
-
542
- const url = `${RIGOBOT_HOST}/v1/learnpack/package/${slug}/`
543
-
544
- try {
545
- const response = await axios.put(url, updates, {
546
- headers: {
547
- Authorization: "Token " + cleanToken,
548
- },
549
- })
550
- return response.data
551
- } catch (error) {
552
- console.error("Failed to update Rigo package:", error)
553
- throw error
554
- }
555
- }
556
-
557
- const createRigoPackage = async (token: string, slug: string, config: any) => {
558
- const url = `${RIGOBOT_HOST}/v1/learnpack/package`
559
- const cleanToken = token.replace(/[\n\r]/g, "")
560
-
561
- try {
562
- const response = await axios.post(
563
- url,
564
- { slug, config },
565
- {
566
- headers: {
567
- Authorization: "Token " + cleanToken,
568
- },
569
- }
570
- )
571
- return response.data
572
- } catch (error) {
573
- console.error("Failed to create Rigo package:", error)
574
- throw error
575
- }
576
- }
577
-
578
- type TTechnology = {
579
- slug: string
580
- lang: string
581
- }
582
-
583
- let technologiesCache: TTechnology[] = []
584
-
585
- export const fetchTechnologies = async () => {
586
- const BREATHECODE_PERMANENT_TOKEN = process.env.BREATHECODE_PERMANENT_TOKEN
587
- const LANGS = ["en", "es"]
588
-
589
- if (!BREATHECODE_PERMANENT_TOKEN) {
590
- throw new Error(
591
- "BREATHECODE_PERMANENT_TOKEN is not defined in environment variables"
592
- )
593
- }
594
-
595
- const headers = {
596
- Authorization: `Token ${BREATHECODE_PERMANENT_TOKEN}`,
597
- }
598
-
599
- const results = await Promise.all(
600
- LANGS.map(lang =>
601
- axios
602
- .get(`${HOST}/v1/registry/technology?lang=${lang}`, { headers })
603
- .then(res => {
604
- return res.data
605
- })
606
- .then(data =>
607
- data.map((item: any) => ({
608
- slug: item.slug,
609
- lang: lang,
610
- }))
611
- )
612
- )
613
- )
614
-
615
- const allItems = results.flat()
616
-
617
- // Remove duplicates by slug+lang combination
618
- const unique = []
619
- const seen = new Set()
620
- for (const item of allItems) {
621
- const key = `${item.slug}:${item.lang}`
622
- if (!seen.has(key)) {
623
- seen.add(key)
624
- unique.push(item)
625
- }
626
- }
627
-
628
- return unique
629
- }
630
-
631
- // Function to update the cache and schedule the next update
632
- async function updateTechnologiesPeriodically() {
633
- try {
634
- technologiesCache = await fetchTechnologies()
635
- // Uncomment for debugging:
636
- // console.log('Technologies list updated:', technologiesCache);
637
- } catch (error: any) {
638
- console.error("Error updating technologies list:", error)
639
- } finally {
640
- setTimeout(updateTechnologiesPeriodically, 24 * 60 * 60 * 1000)
641
- }
642
- }
643
-
644
- export const getCurrentTechnologies = () => technologiesCache
645
-
646
- export default {
647
- login,
648
- publish,
649
- update,
650
- getPackage,
651
- getLangs,
652
- getAllPackages,
653
- sendBatchTelemetry,
654
- sendStreamTelemetry,
655
- listUserAcademies,
656
- validateToken,
657
- createAsset,
658
- doesAssetExists,
659
- updateAsset,
660
- getCategories,
661
- updateRigoPackage,
662
- createRigoPackage,
663
- getCurrentTechnologies,
664
- updateTechnologiesPeriodically,
665
- }
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
+ url: string
424
+ description: string
425
+ learnpack_deploy_url: string
426
+ technologies: string[]
427
+ category: number | string
428
+ owner: number
429
+ author: number
430
+ preview: string
431
+ readme_raw: string
432
+ all_translations: string[]
433
+ }
434
+
435
+ export const createAsset = async (token: string, asset: TAssetMissing) => {
436
+ const body = {
437
+ slug: asset.slug,
438
+ title: asset.title,
439
+ lang: asset.lang,
440
+ asset_type: "EXERCISE",
441
+ visibility: "PUBLIC",
442
+ status: "DRAFT",
443
+ url: asset.url,
444
+ readme_url: null,
445
+ difficulty: null,
446
+ duration: null,
447
+ graded: false,
448
+ gitpod: true,
449
+ category: asset.category,
450
+ owner: asset.owner,
451
+ author: asset.author,
452
+ preview: asset.preview,
453
+ description: asset.description,
454
+ external: false,
455
+ interactive: true,
456
+ solution_video_url: null,
457
+ intro_video_url: null,
458
+ translations: [asset.lang],
459
+ learnpack_deploy_url: asset.learnpack_deploy_url,
460
+ technologies: asset.technologies,
461
+ readme_raw: asset.readme_raw,
462
+ all_translations: asset.all_translations,
463
+ }
464
+ const url = `https://breathecode.herokuapp.com/v1/registry/asset/me`
465
+ const headers = {
466
+ Authorization: `Token ${token}`,
467
+ }
468
+
469
+ try {
470
+ const response = await axios.post(url, body, { headers })
471
+ return response.data
472
+ } catch (error: any) {
473
+ console.error("Failed to create asset:", error)
474
+ throw error.response.data
475
+ }
476
+ }
477
+
478
+ export const doesAssetExists = async (
479
+ token: string,
480
+ assetSlug: string
481
+ ): Promise<{ exists: boolean; academyId?: number }> => {
482
+ const url = `https://breathecode.herokuapp.com/v1/registry/asset/${assetSlug}`
483
+
484
+ const headers = {
485
+ Authorization: `Token ${token}`,
486
+ }
487
+
488
+ try {
489
+ const response = await axios.get(url, { headers })
490
+ if (response.status === 200) {
491
+ return { exists: true }
492
+ }
493
+
494
+ return { exists: false }
495
+ } catch {
496
+ // console.error("Failed to get asset:", error)
497
+ return { exists: false }
498
+ }
499
+ }
500
+
501
+ const updateAsset = async (
502
+ token: string,
503
+ assetSlug: string,
504
+ asset: Partial<TAssetMissing>
505
+ ) => {
506
+ const url = `https://breathecode.herokuapp.com/v1/registry/asset/me/${assetSlug}`
507
+ const headers = {
508
+ Authorization: `Token ${token}`,
509
+ }
510
+ try {
511
+ const response = await axios.put(url, asset, { headers })
512
+ return response.data
513
+ } catch (error: any) {
514
+ console.error("Failed to update asset:", error)
515
+ // Try to print the data
516
+ // console.log(error.response.data)
517
+ throw error.response.data
518
+ }
519
+ }
520
+
521
+ const getCategories = async (token: string) => {
522
+ const url = `${HOST}/v1/registry/category`
523
+ const headers = {
524
+ Authorization: `Token ${token}`,
525
+ }
526
+ try {
527
+ const response = await axios.get(url, { headers })
528
+ return response.data
529
+ } catch (error) {
530
+ console.error("Failed to get categories:", error)
531
+ throw error
532
+ }
533
+ }
534
+
535
+ const updateRigoPackage = async (
536
+ token: string,
537
+ slug: string,
538
+ updates: { asset_id?: number; new_slug?: string }
539
+ ) => {
540
+ const cleanToken = token.replace(/[\n\r]/g, "")
541
+
542
+ const url = `${RIGOBOT_HOST}/v1/learnpack/package/${slug}/`
543
+
544
+ try {
545
+ const response = await axios.put(url, updates, {
546
+ headers: {
547
+ Authorization: "Token " + cleanToken,
548
+ },
549
+ })
550
+ return response.data
551
+ } catch (error) {
552
+ console.error("Failed to update Rigo package:", error)
553
+ throw error
554
+ }
555
+ }
556
+
557
+ const createRigoPackage = async (token: string, slug: string, config: any) => {
558
+ const url = `${RIGOBOT_HOST}/v1/learnpack/package`
559
+ const cleanToken = token.replace(/[\n\r]/g, "")
560
+
561
+ try {
562
+ const response = await axios.post(
563
+ url,
564
+ { slug, config },
565
+ {
566
+ headers: {
567
+ Authorization: "Token " + cleanToken,
568
+ },
569
+ }
570
+ )
571
+ return response.data
572
+ } catch (error) {
573
+ console.error("Failed to create Rigo package:", error)
574
+ throw error
575
+ }
576
+ }
577
+
578
+ type TTechnology = {
579
+ slug: string
580
+ lang: string
581
+ }
582
+
583
+ let technologiesCache: TTechnology[] = []
584
+
585
+ export const fetchTechnologies = async () => {
586
+ const BREATHECODE_PERMANENT_TOKEN = process.env.BREATHECODE_PERMANENT_TOKEN
587
+ const LANGS = ["en", "es"]
588
+
589
+ if (!BREATHECODE_PERMANENT_TOKEN) {
590
+ throw new Error(
591
+ "BREATHECODE_PERMANENT_TOKEN is not defined in environment variables"
592
+ )
593
+ }
594
+
595
+ const headers = {
596
+ Authorization: `Token ${BREATHECODE_PERMANENT_TOKEN}`,
597
+ }
598
+
599
+ const results = await Promise.all(
600
+ LANGS.map(lang =>
601
+ axios
602
+ .get(`${HOST}/v1/registry/technology?lang=${lang}`, { headers })
603
+ .then(res => {
604
+ return res.data
605
+ })
606
+ .then(data =>
607
+ data.map((item: any) => ({
608
+ slug: item.slug,
609
+ lang: lang,
610
+ }))
611
+ )
612
+ )
613
+ )
614
+
615
+ const allItems = results.flat()
616
+
617
+ // Remove duplicates by slug+lang combination
618
+ const unique = []
619
+ const seen = new Set()
620
+ for (const item of allItems) {
621
+ const key = `${item.slug}:${item.lang}`
622
+ if (!seen.has(key)) {
623
+ seen.add(key)
624
+ unique.push(item)
625
+ }
626
+ }
627
+
628
+ return unique
629
+ }
630
+
631
+ // Function to update the cache and schedule the next update
632
+ async function updateTechnologiesPeriodically() {
633
+ try {
634
+ technologiesCache = await fetchTechnologies()
635
+ // Uncomment for debugging:
636
+ // console.log('Technologies list updated:', technologiesCache);
637
+ } catch (error: any) {
638
+ console.error("Error updating technologies list:", error)
639
+ } finally {
640
+ setTimeout(updateTechnologiesPeriodically, 24 * 60 * 60 * 1000)
641
+ }
642
+ }
643
+
644
+ export const getCurrentTechnologies = () => technologiesCache
645
+
646
+ export default {
647
+ login,
648
+ publish,
649
+ update,
650
+ getPackage,
651
+ getLangs,
652
+ getAllPackages,
653
+ sendBatchTelemetry,
654
+ sendStreamTelemetry,
655
+ listUserAcademies,
656
+ validateToken,
657
+ createAsset,
658
+ doesAssetExists,
659
+ updateAsset,
660
+ getCategories,
661
+ updateRigoPackage,
662
+ createRigoPackage,
663
+ getCurrentTechnologies,
664
+ updateTechnologiesPeriodically,
665
+ }