@learnpack/learnpack 5.0.275 → 5.0.276

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 (71) 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/init.js +41 -41
  6. package/lib/commands/logout.js +3 -3
  7. package/lib/commands/publish.js +5 -10
  8. package/lib/commands/serve.js +3 -2
  9. package/lib/creatorDist/assets/index-BfLyIQVh.js +10343 -10224
  10. package/lib/managers/config/index.js +77 -77
  11. package/lib/utils/api.d.ts +1 -1
  12. package/lib/utils/api.js +12 -9
  13. package/lib/utils/creatorUtilities.js +14 -14
  14. package/package.json +1 -1
  15. package/src/commands/audit.ts +487 -487
  16. package/src/commands/breakToken.ts +67 -67
  17. package/src/commands/clean.ts +30 -30
  18. package/src/commands/init.ts +650 -650
  19. package/src/commands/logout.ts +38 -38
  20. package/src/commands/publish.ts +20 -25
  21. package/src/commands/serve.ts +8 -3
  22. package/src/commands/start.ts +333 -333
  23. package/src/commands/translate.ts +123 -123
  24. package/src/creator/README.md +54 -54
  25. package/src/creator/eslint.config.js +28 -28
  26. package/src/creator/src/components/syllabus/ContentIndex.tsx +312 -312
  27. package/src/creator/src/i18n.ts +28 -28
  28. package/src/creator/src/index.css +217 -217
  29. package/src/creator/src/locales/en.json +126 -126
  30. package/src/creator/src/locales/es.json +126 -126
  31. package/src/creator/src/utils/configTypes.ts +122 -122
  32. package/src/creator/src/utils/constants.ts +13 -13
  33. package/src/creator/src/utils/creatorUtils.ts +46 -46
  34. package/src/creator/src/utils/eventBus.ts +2 -2
  35. package/src/creator/src/utils/lib.ts +468 -468
  36. package/src/creator/src/utils/socket.ts +61 -61
  37. package/src/creator/src/utils/store.ts +222 -222
  38. package/src/creator/src/vite-env.d.ts +1 -1
  39. package/src/creator/vite.config.ts +13 -13
  40. package/src/creatorDist/assets/index-BfLyIQVh.js +10343 -10224
  41. package/src/managers/config/defaults.ts +49 -49
  42. package/src/managers/config/exercise.ts +364 -364
  43. package/src/managers/config/index.ts +775 -775
  44. package/src/managers/file.ts +236 -236
  45. package/src/managers/server/routes.ts +554 -554
  46. package/src/managers/session.ts +182 -182
  47. package/src/managers/telemetry.ts +188 -188
  48. package/src/models/action.ts +13 -13
  49. package/src/models/config-manager.ts +28 -28
  50. package/src/models/config.ts +106 -106
  51. package/src/models/creator.ts +47 -47
  52. package/src/models/exercise-obj.ts +30 -30
  53. package/src/models/session.ts +39 -39
  54. package/src/models/socket.ts +61 -61
  55. package/src/models/status.ts +16 -16
  56. package/src/ui/_app/app.css +1 -1
  57. package/src/ui/_app/app.js +366 -363
  58. package/src/ui/app.tar.gz +0 -0
  59. package/src/utils/BaseCommand.ts +56 -56
  60. package/src/utils/api.ts +53 -39
  61. package/src/utils/audit.ts +392 -392
  62. package/src/utils/checkNotInstalled.ts +267 -267
  63. package/src/utils/configBuilder.ts +82 -82
  64. package/src/utils/convertCreds.js +34 -34
  65. package/src/utils/creatorUtilities.ts +504 -504
  66. package/src/utils/incrementVersion.js +74 -74
  67. package/src/utils/misc.ts +58 -58
  68. package/src/utils/rigoActions.ts +500 -500
  69. package/src/utils/sidebarGenerator.ts +195 -195
  70. package/src/utils/templates/isolated/exercises/01-hello-world/README.es.md +26 -26
  71. package/src/utils/templates/isolated/exercises/01-hello-world/README.md +26 -26
@@ -1,554 +1,554 @@
1
- import Console from "../../utils/console"
2
- import * as express from "express"
3
- import * as fs from "fs"
4
- import * as bodyParser from "body-parser"
5
- import socket from "../socket"
6
- import queue from "../../utils/fileQueue"
7
- // import gitpod from '../gitpod'
8
- import { detect, filterFiles } from "../config/exercise"
9
- import { IFile } from "../../models/file"
10
- import { IConfigObj, TEntries } from "../../models/config"
11
- import { IConfigManager } from "../../models/config-manager"
12
- import { IExercise } from "../../models/exercise-obj"
13
- import SessionManager from "../../managers/session"
14
- import TelemetryManager from "../telemetry"
15
- import { saveTranslatedReadme } from "../../utils/creatorUtilities"
16
- import { translateExercise } from "../../utils/rigoActions"
17
- import { addExerciseToSidebar } from "../../utils/sidebarGenerator"
18
- import path = require("path")
19
- // import { eventManager } from "../../utils/osOperations"
20
-
21
- const withHandler =
22
- (func: (req: express.Request, res: express.Response) => void) =>
23
- (req: express.Request, res: express.Response) => {
24
- try {
25
- func(req, res)
26
- } catch (error) {
27
- Console.debug(error)
28
- const _err = {
29
- message: (error as TypeError).message || "There has been an error",
30
- status: (error as any).status || 500,
31
- type: (error as any).type || null,
32
- }
33
- Console.error(_err.message)
34
-
35
- // send rep to the server
36
- res.status(_err.status)
37
- res.json(_err)
38
- }
39
- }
40
-
41
- export default async function (
42
- app: express.Application,
43
- configObject: IConfigObj,
44
- configManager: IConfigManager
45
- ) {
46
- const { config, exercises } = configObject
47
- const session = await SessionManager.get(configManager?.get())
48
-
49
- const dispatcher = queue.dispatcher({
50
- create: true,
51
- path: `${config?.dirPath}/vscode_queue.json`,
52
- })
53
-
54
- app.get(
55
- "/config",
56
- withHandler((_: express.Request, res: express.Response) => {
57
- const confObject = configManager.get()
58
- res.json(confObject)
59
- })
60
- )
61
-
62
- // Added this line to parse the json body
63
-
64
- const jsonBodyParser = bodyParser.json({ limit: "10mb" })
65
- // Trying to log in from frontend
66
- app.post(
67
- "/login",
68
- jsonBodyParser,
69
- withHandler(async (req: express.Request, res: express.Response) => {
70
- const email = req.body.email
71
- const password = req.body.password
72
-
73
- SessionManager.destroy()
74
- const payload = await SessionManager.loginWeb(email, password)
75
-
76
- res.json(payload)
77
- })
78
- )
79
-
80
- app.post(
81
- "/logout",
82
- jsonBodyParser,
83
- withHandler(async (req: express.Request, res: express.Response) => {
84
- SessionManager.destroy()
85
- res.json({ message: "You've logged out from Breathecode and Rigobot" })
86
- })
87
- )
88
-
89
- app.post(
90
- "/set-rigobot-token",
91
- jsonBodyParser,
92
- withHandler(async (req: express.Request, res: express.Response) => {
93
- const token = req.body.token
94
- // Ensure token is provided in the request body
95
- if (!token) {
96
- return res.status(400).json({ error: "Token is required" })
97
- }
98
-
99
- try {
100
- const tokenSaved = await SessionManager.setRigoToken(token)
101
- // Check if the token was saved successfully
102
- if (tokenSaved) {
103
- res.json({ status: "ok" })
104
- } else {
105
- res.status(500).json({ error: "Failed to save the token" })
106
- }
107
- } catch {
108
- // Handle any unexpected errors during the process
109
- res.status(500).json({ error: "Internal server error" })
110
- }
111
- })
112
- )
113
- app.post(
114
- "/set-tab-hash",
115
- jsonBodyParser,
116
- withHandler(async (req: express.Request, res: express.Response) => {
117
- const hash = req.body.hash
118
- if (!hash) {
119
- return res.status(400).json({ error: "Token is required" })
120
- }
121
-
122
- try {
123
- const hashSaved = await SessionManager.setTabHash(hash)
124
- if (hashSaved) {
125
- res.json({ status: "ok" })
126
- } else {
127
- res.status(500).json({ error: "Failed to save the token" })
128
- }
129
- } catch {
130
- res.status(500).json({ error: "Internal server error" })
131
- }
132
- })
133
- )
134
- app.post(
135
- "/set-session-key",
136
- jsonBodyParser,
137
- withHandler(async (req: express.Request, res: express.Response) => {
138
- const sessionKey = req.body.sessionKey
139
- if (!sessionKey) {
140
- return res.status(400).json({ error: "Token is required" })
141
- }
142
-
143
- try {
144
- const sessionSaved = await SessionManager.setSessionKey(sessionKey)
145
- if (sessionSaved) {
146
- res.json({ status: "ok" })
147
- } else {
148
- res.status(500).json({ error: "Failed to save the token" })
149
- }
150
- } catch {
151
- res.status(500).json({ error: "Internal server error" })
152
- }
153
- })
154
- )
155
- app.post(
156
- "/set-session",
157
- jsonBodyParser,
158
- withHandler(async (req: express.Request, res: express.Response) => {
159
- const payload = req.body.payload
160
- if (!payload) {
161
- return res.status(400).json({ error: "Token is required" })
162
- }
163
-
164
- try {
165
- const payloadSaved = await SessionManager.setPayload(payload)
166
- if (payloadSaved) {
167
- res.json({ status: "ok" })
168
- socket.sessionRefreshed(payload)
169
- } else {
170
- res.status(500).json({ error: "Failed to save the token" })
171
- }
172
- } catch {
173
- res.status(500).json({ error: "Internal server error" })
174
- }
175
- })
176
- )
177
-
178
- app.get("/telemetry", async (req, res) => {
179
- const telemetry = await TelemetryManager.retrieve()
180
- res.json(telemetry)
181
- })
182
-
183
- app.post("/telemetry", jsonBodyParser, async (req, res) => {
184
- const telemetry = req.body
185
- await TelemetryManager.save(telemetry)
186
- res.json({ message: "Telemetry saved successfully" })
187
- })
188
-
189
- app.get(
190
- "/check/rigo/status",
191
- withHandler(async (_: express.Request, res: express.Response) => {
192
- const payload = await SessionManager.getPayload()
193
-
194
- if (payload && payload.rigobot && payload.rigobot.key) {
195
- res.json({ rigoToken: payload.rigobot.key, payload: payload })
196
- } else {
197
- res
198
- .status(400)
199
- .json({ details: `Rigobot token not found, please log in first!` })
200
- }
201
- })
202
- )
203
-
204
- // symbolic link to maintain path compatiblity
205
- const fetchStaticAsset = withHandler((req, res) => {
206
- const filePath = `${config?.dirPath}/assets/${req.params.filePath}`
207
- if (!fs.existsSync(filePath)) throw new Error("File not found: " + filePath)
208
- const content = fs.readFileSync(filePath)
209
- res.write(content)
210
- res.end()
211
- })
212
-
213
- app.get(
214
- `${
215
- config?.dirPath.indexOf("./") === 0 ?
216
- config.dirPath.slice(1) :
217
- config?.dirPath
218
- }/assets/:filePath`,
219
- fetchStaticAsset
220
- )
221
-
222
- app.get("/assets/:filePath", fetchStaticAsset)
223
-
224
- app.get(
225
- "/exercise",
226
- withHandler((_: express.Request, res: express.Response) => {
227
- res.json(exercises)
228
- })
229
- )
230
-
231
- app.get(
232
- "/exercise/:slug/readme",
233
- withHandler(
234
- (
235
- { params: { slug }, query: { lang } }: express.Request,
236
- res: express.Response
237
- ) => {
238
- const exercise: IExercise = configManager.getExercise(slug)
239
-
240
- if (exercise && exercise.getReadme) {
241
- const readme = exercise.getReadme((lang as string) || null)
242
- res.json(readme)
243
- } else {
244
- res.status(400)
245
- }
246
- }
247
- )
248
- )
249
-
250
- app.get(
251
- "/exercise/:slug/report",
252
- withHandler(
253
- ({ params: { slug } }: express.Request, res: express.Response) => {
254
- const exercise = configManager.getExercise(slug)
255
- if (exercise && exercise.getTestReport) {
256
- const report = exercise.getTestReport()
257
- res.json(JSON.stringify(report))
258
- }
259
- }
260
- )
261
- )
262
-
263
- app.get(
264
- "/exercise/:slug",
265
- withHandler((req: express.Request, res: express.Response) => {
266
- // no need to re-start exercise if it's already started
267
- if (
268
- configObject.currentExercise &&
269
- req.params.slug === configObject.currentExercise
270
- ) {
271
- const exercise = configManager.getExercise(req.params.slug)
272
- res.json(exercise)
273
- if (exercise.position) {
274
- TelemetryManager.registerStepEvent(exercise.position, "open_step", {})
275
- }
276
-
277
- return
278
- }
279
-
280
- const exercise = configManager.startExercise(req.params.slug)
281
- if (exercise.position) {
282
- TelemetryManager.registerStepEvent(exercise.position, "open_step", {})
283
- }
284
-
285
- if (configObject.config?.editor.agent !== "os") {
286
- dispatcher.enqueue(dispatcher.events.START_EXERCISE, req.params.slug)
287
- }
288
-
289
- type TEntry = "python3" | "html" | "node" | "react" | "java"
290
-
291
- const entries = new Set(
292
- Object.keys(config?.entries!).map(
293
- lang => config?.entries[lang as TEntry]
294
- )
295
- )
296
-
297
- // if we are in incremental grading, the entry file can by dinamically detected
298
- // based on the changes the student is making during the exercise
299
- if (config?.grading === "incremental") {
300
- const scanedFiles = fs.readdirSync("./")
301
-
302
- // update the file hierarchy with updates
303
- exercise.files = [
304
- ...exercise.files.filter(f => f.name.includes("test.")),
305
- ...filterFiles(scanedFiles),
306
- ]
307
- Console.debug(`Exercise updated files: `, exercise.files)
308
- }
309
-
310
- const detected = detect(
311
- configObject,
312
- exercise.files
313
- .filter(fileName => entries.has(fileName.name))
314
- .map(f => f.name || f) as string[]
315
- )
316
-
317
- // if a new language for the testing engine is detected, we replace it
318
- // if not we leave it as it was before
319
- if (config?.language && !["", "auto"].includes(config?.language)) {
320
- Console.debug(
321
- `Exercise language ignored, instead imported from configuration ${config?.language}`
322
- )
323
- exercise.language = detected?.language
324
- } else if (
325
- detected?.language &&
326
- (!config?.language || config?.language === "auto")
327
- ) {
328
- Console.debug(
329
- `Switching to ${detected.language} engine in this exercise`
330
- )
331
- exercise.language = detected.language
332
- }
333
-
334
- // WARNING: has to be the FULL PATH to the entry path
335
- // We need to detect entry in both gradings: Incremental and Isolate
336
- exercise.entry = detected?.entry
337
- Console.debug(
338
- `Exercise detected entry: ${detected?.entry} and language ${exercise.language}`
339
- )
340
-
341
- // exercises.graded and exercises.disableGrading deprecated.
342
- if (
343
- !exercise.graded ||
344
- config?.disableGrading ||
345
- config?.disabledActions?.includes("test")
346
- ) {
347
- socket.removeAllowed("test")
348
- } else {
349
- socket.addAllowed("test")
350
- }
351
-
352
- if (!exercise.entry || config?.disabledActions?.includes("build")) {
353
- socket.removeAllowed("build")
354
- } else {
355
- socket.addAllowed("build")
356
- }
357
-
358
- if (
359
- exercise.files.filter(
360
- (f: IFile) =>
361
- !f.name.toLowerCase().includes("readme.") &&
362
- !f.name.toLowerCase().includes("test.")
363
- ).length === 0 ||
364
- config?.disabledActions?.includes("reset")
365
- ) {
366
- socket.removeAllowed("reset")
367
- } else if (!config?.disabledActions?.includes("reset")) {
368
- socket.addAllowed("reset")
369
- }
370
-
371
- socket.log("ready")
372
- res.json(exercise)
373
- })
374
- )
375
-
376
- app.get(
377
- "/exercise/:slug/file/:fileName",
378
- withHandler((req: express.Request, res: express.Response) => {
379
- const exercise = configManager.getExercise(req.params.slug)
380
- if (exercise && exercise.getFile) {
381
- res.write(exercise.getFile(req.params.fileName))
382
- res.end()
383
- }
384
- })
385
- )
386
-
387
- app.get(
388
- "/sidebar",
389
- withHandler((req: express.Request, res: express.Response) => {
390
- const sidebar = configManager.getSidebar()
391
- res.json(sidebar)
392
- })
393
- )
394
-
395
- app.delete(
396
- "/exercise/:slug/delete",
397
- withHandler(async (req: express.Request, res: express.Response) => {
398
- const exerciseDeleted = configManager.deleteExercise(req.params.slug)
399
- if (exerciseDeleted) {
400
- configManager.buildIndex()
401
- res.json({ status: "ok" })
402
- } else {
403
- res.status(500).json({ error: "Failed to delete exercise" })
404
- }
405
- })
406
- )
407
-
408
- app.post(
409
- "/actions/translate",
410
- jsonBodyParser,
411
- withHandler(async (req: express.Request, res: express.Response) => {
412
- const { exerciseSlugs, languages } = req.body
413
-
414
- const session = await SessionManager.getPayload()
415
- const rigoToken = session?.rigobot?.key
416
-
417
- const configDirPath = config?.dirPath
418
-
419
- if (!rigoToken) {
420
- return res.status(400).json({ error: "RigoToken not found" })
421
- }
422
-
423
- const languagesToTranslate: string[] = languages.split(",")
424
-
425
- try {
426
- await Promise.all(
427
- exerciseSlugs.map(async (slug: string) => {
428
- const exercise = configManager.getExercise(slug)
429
- if (!exercise) {
430
- throw new Error(`Exercise ${slug} not found`)
431
- }
432
-
433
- if (exercise.getReadme) {
434
- const readme = exercise.getReadme(null)
435
-
436
- await Promise.all(
437
- languagesToTranslate.map(async (language: string) => {
438
- const response = await translateExercise(
439
- rigoToken,
440
- {
441
- text_to_translate: readme.body,
442
- output_language: language,
443
- },
444
- `${process.env.HOST}/webhooks/translate-exercise`
445
- )
446
-
447
- await saveTranslatedReadme(
448
- slug,
449
- response.parsed.output_language_code,
450
- response.parsed.translation
451
- )
452
-
453
- addExerciseToSidebar(
454
- slug,
455
- response.parsed.output_language_code,
456
- response.parsed.translated_slug,
457
- configDirPath || ""
458
- )
459
-
460
- Console.success(
461
- `Translated ${slug} to ${language} successfully`
462
- )
463
- })
464
- )
465
- }
466
- })
467
- )
468
-
469
- configManager.buildIndex()
470
-
471
- return res.status(200).json({ message: "Translated exercises" })
472
- } catch (error) {
473
- console.log(error, "ERROR")
474
- return res.status(400).json({ error: (error as Error).message })
475
- }
476
- })
477
- )
478
-
479
- app.put(
480
- "/actions/rename",
481
- jsonBodyParser,
482
- withHandler(async (req: express.Request, res: express.Response) => {
483
- const { slug, newSlug } = req.body
484
- const exercise = configManager.getExercise(slug)
485
- if (!exercise) {
486
- return res.status(400).json({ error: "Exercise not found" })
487
- }
488
-
489
- try {
490
- if (exercise.renameFolder) {
491
- exercise.renameFolder(newSlug)
492
- res.json({ status: "ok" })
493
- } else {
494
- res.status(500).json({
495
- error:
496
- "Failed to rename exercise because it's not supported by the exercise",
497
- })
498
- }
499
- } catch {
500
- res.status(500).json({ error: "Failed to rename exercise" })
501
- }
502
- })
503
- )
504
-
505
- app.post(
506
- "/exercise/:slug/create",
507
- jsonBodyParser,
508
- withHandler(async (req: express.Request, res: express.Response) => {
509
- const { title, readme, language } = req.body
510
- const { slug } = req.params
511
-
512
- if (!title || !readme || !language) {
513
- return res.status(400).json({ error: "Missing required fields" })
514
- }
515
-
516
- try {
517
- const exerciseCreated = await configManager.createExercise(
518
- slug,
519
- readme,
520
- language
521
- )
522
- if (exerciseCreated) {
523
- configManager.buildIndex()
524
- res.json({ status: "ok" })
525
- } else {
526
- res.status(500).json({ error: "Failed to create exercise" })
527
- }
528
- } catch {
529
- res.status(500).json({ error: "Failed to create exercise" })
530
- }
531
- })
532
- )
533
-
534
- const textBodyParser = bodyParser.text()
535
- app.put(
536
- "/exercise/:slug/file/:fileName",
537
- textBodyParser,
538
- withHandler((req: express.Request, res: express.Response) => {
539
- const exercise = configManager.getExercise(req.params.slug)
540
- if (exercise && exercise.saveFile) {
541
- exercise.saveFile(req.params.fileName, req.body)
542
- res.end()
543
- }
544
- })
545
- )
546
-
547
- if (config?.outputPath) {
548
- app.use("/preview", express.static(config.outputPath))
549
- }
550
-
551
- app.use("/", express.static(`${config?.dirPath}/_app`))
552
-
553
- app.use("/creator", express.static(path.join(__dirname, "..", "creatorDist")))
554
- }
1
+ import Console from "../../utils/console"
2
+ import * as express from "express"
3
+ import * as fs from "fs"
4
+ import * as bodyParser from "body-parser"
5
+ import socket from "../socket"
6
+ import queue from "../../utils/fileQueue"
7
+ // import gitpod from '../gitpod'
8
+ import { detect, filterFiles } from "../config/exercise"
9
+ import { IFile } from "../../models/file"
10
+ import { IConfigObj, TEntries } from "../../models/config"
11
+ import { IConfigManager } from "../../models/config-manager"
12
+ import { IExercise } from "../../models/exercise-obj"
13
+ import SessionManager from "../../managers/session"
14
+ import TelemetryManager from "../telemetry"
15
+ import { saveTranslatedReadme } from "../../utils/creatorUtilities"
16
+ import { translateExercise } from "../../utils/rigoActions"
17
+ import { addExerciseToSidebar } from "../../utils/sidebarGenerator"
18
+ import path = require("path")
19
+ // import { eventManager } from "../../utils/osOperations"
20
+
21
+ const withHandler =
22
+ (func: (req: express.Request, res: express.Response) => void) =>
23
+ (req: express.Request, res: express.Response) => {
24
+ try {
25
+ func(req, res)
26
+ } catch (error) {
27
+ Console.debug(error)
28
+ const _err = {
29
+ message: (error as TypeError).message || "There has been an error",
30
+ status: (error as any).status || 500,
31
+ type: (error as any).type || null,
32
+ }
33
+ Console.error(_err.message)
34
+
35
+ // send rep to the server
36
+ res.status(_err.status)
37
+ res.json(_err)
38
+ }
39
+ }
40
+
41
+ export default async function (
42
+ app: express.Application,
43
+ configObject: IConfigObj,
44
+ configManager: IConfigManager
45
+ ) {
46
+ const { config, exercises } = configObject
47
+ const session = await SessionManager.get(configManager?.get())
48
+
49
+ const dispatcher = queue.dispatcher({
50
+ create: true,
51
+ path: `${config?.dirPath}/vscode_queue.json`,
52
+ })
53
+
54
+ app.get(
55
+ "/config",
56
+ withHandler((_: express.Request, res: express.Response) => {
57
+ const confObject = configManager.get()
58
+ res.json(confObject)
59
+ })
60
+ )
61
+
62
+ // Added this line to parse the json body
63
+
64
+ const jsonBodyParser = bodyParser.json({ limit: "10mb" })
65
+ // Trying to log in from frontend
66
+ app.post(
67
+ "/login",
68
+ jsonBodyParser,
69
+ withHandler(async (req: express.Request, res: express.Response) => {
70
+ const email = req.body.email
71
+ const password = req.body.password
72
+
73
+ SessionManager.destroy()
74
+ const payload = await SessionManager.loginWeb(email, password)
75
+
76
+ res.json(payload)
77
+ })
78
+ )
79
+
80
+ app.post(
81
+ "/logout",
82
+ jsonBodyParser,
83
+ withHandler(async (req: express.Request, res: express.Response) => {
84
+ SessionManager.destroy()
85
+ res.json({ message: "You've logged out from Breathecode and Rigobot" })
86
+ })
87
+ )
88
+
89
+ app.post(
90
+ "/set-rigobot-token",
91
+ jsonBodyParser,
92
+ withHandler(async (req: express.Request, res: express.Response) => {
93
+ const token = req.body.token
94
+ // Ensure token is provided in the request body
95
+ if (!token) {
96
+ return res.status(400).json({ error: "Token is required" })
97
+ }
98
+
99
+ try {
100
+ const tokenSaved = await SessionManager.setRigoToken(token)
101
+ // Check if the token was saved successfully
102
+ if (tokenSaved) {
103
+ res.json({ status: "ok" })
104
+ } else {
105
+ res.status(500).json({ error: "Failed to save the token" })
106
+ }
107
+ } catch {
108
+ // Handle any unexpected errors during the process
109
+ res.status(500).json({ error: "Internal server error" })
110
+ }
111
+ })
112
+ )
113
+ app.post(
114
+ "/set-tab-hash",
115
+ jsonBodyParser,
116
+ withHandler(async (req: express.Request, res: express.Response) => {
117
+ const hash = req.body.hash
118
+ if (!hash) {
119
+ return res.status(400).json({ error: "Token is required" })
120
+ }
121
+
122
+ try {
123
+ const hashSaved = await SessionManager.setTabHash(hash)
124
+ if (hashSaved) {
125
+ res.json({ status: "ok" })
126
+ } else {
127
+ res.status(500).json({ error: "Failed to save the token" })
128
+ }
129
+ } catch {
130
+ res.status(500).json({ error: "Internal server error" })
131
+ }
132
+ })
133
+ )
134
+ app.post(
135
+ "/set-session-key",
136
+ jsonBodyParser,
137
+ withHandler(async (req: express.Request, res: express.Response) => {
138
+ const sessionKey = req.body.sessionKey
139
+ if (!sessionKey) {
140
+ return res.status(400).json({ error: "Token is required" })
141
+ }
142
+
143
+ try {
144
+ const sessionSaved = await SessionManager.setSessionKey(sessionKey)
145
+ if (sessionSaved) {
146
+ res.json({ status: "ok" })
147
+ } else {
148
+ res.status(500).json({ error: "Failed to save the token" })
149
+ }
150
+ } catch {
151
+ res.status(500).json({ error: "Internal server error" })
152
+ }
153
+ })
154
+ )
155
+ app.post(
156
+ "/set-session",
157
+ jsonBodyParser,
158
+ withHandler(async (req: express.Request, res: express.Response) => {
159
+ const payload = req.body.payload
160
+ if (!payload) {
161
+ return res.status(400).json({ error: "Token is required" })
162
+ }
163
+
164
+ try {
165
+ const payloadSaved = await SessionManager.setPayload(payload)
166
+ if (payloadSaved) {
167
+ res.json({ status: "ok" })
168
+ socket.sessionRefreshed(payload)
169
+ } else {
170
+ res.status(500).json({ error: "Failed to save the token" })
171
+ }
172
+ } catch {
173
+ res.status(500).json({ error: "Internal server error" })
174
+ }
175
+ })
176
+ )
177
+
178
+ app.get("/telemetry", async (req, res) => {
179
+ const telemetry = await TelemetryManager.retrieve()
180
+ res.json(telemetry)
181
+ })
182
+
183
+ app.post("/telemetry", jsonBodyParser, async (req, res) => {
184
+ const telemetry = req.body
185
+ await TelemetryManager.save(telemetry)
186
+ res.json({ message: "Telemetry saved successfully" })
187
+ })
188
+
189
+ app.get(
190
+ "/check/rigo/status",
191
+ withHandler(async (_: express.Request, res: express.Response) => {
192
+ const payload = await SessionManager.getPayload()
193
+
194
+ if (payload && payload.rigobot && payload.rigobot.key) {
195
+ res.json({ rigoToken: payload.rigobot.key, payload: payload })
196
+ } else {
197
+ res
198
+ .status(400)
199
+ .json({ details: `Rigobot token not found, please log in first!` })
200
+ }
201
+ })
202
+ )
203
+
204
+ // symbolic link to maintain path compatiblity
205
+ const fetchStaticAsset = withHandler((req, res) => {
206
+ const filePath = `${config?.dirPath}/assets/${req.params.filePath}`
207
+ if (!fs.existsSync(filePath)) throw new Error("File not found: " + filePath)
208
+ const content = fs.readFileSync(filePath)
209
+ res.write(content)
210
+ res.end()
211
+ })
212
+
213
+ app.get(
214
+ `${
215
+ config?.dirPath.indexOf("./") === 0 ?
216
+ config.dirPath.slice(1) :
217
+ config?.dirPath
218
+ }/assets/:filePath`,
219
+ fetchStaticAsset
220
+ )
221
+
222
+ app.get("/assets/:filePath", fetchStaticAsset)
223
+
224
+ app.get(
225
+ "/exercise",
226
+ withHandler((_: express.Request, res: express.Response) => {
227
+ res.json(exercises)
228
+ })
229
+ )
230
+
231
+ app.get(
232
+ "/exercise/:slug/readme",
233
+ withHandler(
234
+ (
235
+ { params: { slug }, query: { lang } }: express.Request,
236
+ res: express.Response
237
+ ) => {
238
+ const exercise: IExercise = configManager.getExercise(slug)
239
+
240
+ if (exercise && exercise.getReadme) {
241
+ const readme = exercise.getReadme((lang as string) || null)
242
+ res.json(readme)
243
+ } else {
244
+ res.status(400)
245
+ }
246
+ }
247
+ )
248
+ )
249
+
250
+ app.get(
251
+ "/exercise/:slug/report",
252
+ withHandler(
253
+ ({ params: { slug } }: express.Request, res: express.Response) => {
254
+ const exercise = configManager.getExercise(slug)
255
+ if (exercise && exercise.getTestReport) {
256
+ const report = exercise.getTestReport()
257
+ res.json(JSON.stringify(report))
258
+ }
259
+ }
260
+ )
261
+ )
262
+
263
+ app.get(
264
+ "/exercise/:slug",
265
+ withHandler((req: express.Request, res: express.Response) => {
266
+ // no need to re-start exercise if it's already started
267
+ if (
268
+ configObject.currentExercise &&
269
+ req.params.slug === configObject.currentExercise
270
+ ) {
271
+ const exercise = configManager.getExercise(req.params.slug)
272
+ res.json(exercise)
273
+ if (exercise.position) {
274
+ TelemetryManager.registerStepEvent(exercise.position, "open_step", {})
275
+ }
276
+
277
+ return
278
+ }
279
+
280
+ const exercise = configManager.startExercise(req.params.slug)
281
+ if (exercise.position) {
282
+ TelemetryManager.registerStepEvent(exercise.position, "open_step", {})
283
+ }
284
+
285
+ if (configObject.config?.editor.agent !== "os") {
286
+ dispatcher.enqueue(dispatcher.events.START_EXERCISE, req.params.slug)
287
+ }
288
+
289
+ type TEntry = "python3" | "html" | "node" | "react" | "java"
290
+
291
+ const entries = new Set(
292
+ Object.keys(config?.entries!).map(
293
+ lang => config?.entries[lang as TEntry]
294
+ )
295
+ )
296
+
297
+ // if we are in incremental grading, the entry file can by dinamically detected
298
+ // based on the changes the student is making during the exercise
299
+ if (config?.grading === "incremental") {
300
+ const scanedFiles = fs.readdirSync("./")
301
+
302
+ // update the file hierarchy with updates
303
+ exercise.files = [
304
+ ...exercise.files.filter(f => f.name.includes("test.")),
305
+ ...filterFiles(scanedFiles),
306
+ ]
307
+ Console.debug(`Exercise updated files: `, exercise.files)
308
+ }
309
+
310
+ const detected = detect(
311
+ configObject,
312
+ exercise.files
313
+ .filter(fileName => entries.has(fileName.name))
314
+ .map(f => f.name || f) as string[]
315
+ )
316
+
317
+ // if a new language for the testing engine is detected, we replace it
318
+ // if not we leave it as it was before
319
+ if (config?.language && !["", "auto"].includes(config?.language)) {
320
+ Console.debug(
321
+ `Exercise language ignored, instead imported from configuration ${config?.language}`
322
+ )
323
+ exercise.language = detected?.language
324
+ } else if (
325
+ detected?.language &&
326
+ (!config?.language || config?.language === "auto")
327
+ ) {
328
+ Console.debug(
329
+ `Switching to ${detected.language} engine in this exercise`
330
+ )
331
+ exercise.language = detected.language
332
+ }
333
+
334
+ // WARNING: has to be the FULL PATH to the entry path
335
+ // We need to detect entry in both gradings: Incremental and Isolate
336
+ exercise.entry = detected?.entry
337
+ Console.debug(
338
+ `Exercise detected entry: ${detected?.entry} and language ${exercise.language}`
339
+ )
340
+
341
+ // exercises.graded and exercises.disableGrading deprecated.
342
+ if (
343
+ !exercise.graded ||
344
+ config?.disableGrading ||
345
+ config?.disabledActions?.includes("test")
346
+ ) {
347
+ socket.removeAllowed("test")
348
+ } else {
349
+ socket.addAllowed("test")
350
+ }
351
+
352
+ if (!exercise.entry || config?.disabledActions?.includes("build")) {
353
+ socket.removeAllowed("build")
354
+ } else {
355
+ socket.addAllowed("build")
356
+ }
357
+
358
+ if (
359
+ exercise.files.filter(
360
+ (f: IFile) =>
361
+ !f.name.toLowerCase().includes("readme.") &&
362
+ !f.name.toLowerCase().includes("test.")
363
+ ).length === 0 ||
364
+ config?.disabledActions?.includes("reset")
365
+ ) {
366
+ socket.removeAllowed("reset")
367
+ } else if (!config?.disabledActions?.includes("reset")) {
368
+ socket.addAllowed("reset")
369
+ }
370
+
371
+ socket.log("ready")
372
+ res.json(exercise)
373
+ })
374
+ )
375
+
376
+ app.get(
377
+ "/exercise/:slug/file/:fileName",
378
+ withHandler((req: express.Request, res: express.Response) => {
379
+ const exercise = configManager.getExercise(req.params.slug)
380
+ if (exercise && exercise.getFile) {
381
+ res.write(exercise.getFile(req.params.fileName))
382
+ res.end()
383
+ }
384
+ })
385
+ )
386
+
387
+ app.get(
388
+ "/sidebar",
389
+ withHandler((req: express.Request, res: express.Response) => {
390
+ const sidebar = configManager.getSidebar()
391
+ res.json(sidebar)
392
+ })
393
+ )
394
+
395
+ app.delete(
396
+ "/exercise/:slug/delete",
397
+ withHandler(async (req: express.Request, res: express.Response) => {
398
+ const exerciseDeleted = configManager.deleteExercise(req.params.slug)
399
+ if (exerciseDeleted) {
400
+ configManager.buildIndex()
401
+ res.json({ status: "ok" })
402
+ } else {
403
+ res.status(500).json({ error: "Failed to delete exercise" })
404
+ }
405
+ })
406
+ )
407
+
408
+ app.post(
409
+ "/actions/translate",
410
+ jsonBodyParser,
411
+ withHandler(async (req: express.Request, res: express.Response) => {
412
+ const { exerciseSlugs, languages } = req.body
413
+
414
+ const session = await SessionManager.getPayload()
415
+ const rigoToken = session?.rigobot?.key
416
+
417
+ const configDirPath = config?.dirPath
418
+
419
+ if (!rigoToken) {
420
+ return res.status(400).json({ error: "RigoToken not found" })
421
+ }
422
+
423
+ const languagesToTranslate: string[] = languages.split(",")
424
+
425
+ try {
426
+ await Promise.all(
427
+ exerciseSlugs.map(async (slug: string) => {
428
+ const exercise = configManager.getExercise(slug)
429
+ if (!exercise) {
430
+ throw new Error(`Exercise ${slug} not found`)
431
+ }
432
+
433
+ if (exercise.getReadme) {
434
+ const readme = exercise.getReadme(null)
435
+
436
+ await Promise.all(
437
+ languagesToTranslate.map(async (language: string) => {
438
+ const response = await translateExercise(
439
+ rigoToken,
440
+ {
441
+ text_to_translate: readme.body,
442
+ output_language: language,
443
+ },
444
+ `${process.env.HOST}/webhooks/translate-exercise`
445
+ )
446
+
447
+ await saveTranslatedReadme(
448
+ slug,
449
+ response.parsed.output_language_code,
450
+ response.parsed.translation
451
+ )
452
+
453
+ addExerciseToSidebar(
454
+ slug,
455
+ response.parsed.output_language_code,
456
+ response.parsed.translated_slug,
457
+ configDirPath || ""
458
+ )
459
+
460
+ Console.success(
461
+ `Translated ${slug} to ${language} successfully`
462
+ )
463
+ })
464
+ )
465
+ }
466
+ })
467
+ )
468
+
469
+ configManager.buildIndex()
470
+
471
+ return res.status(200).json({ message: "Translated exercises" })
472
+ } catch (error) {
473
+ console.log(error, "ERROR")
474
+ return res.status(400).json({ error: (error as Error).message })
475
+ }
476
+ })
477
+ )
478
+
479
+ app.put(
480
+ "/actions/rename",
481
+ jsonBodyParser,
482
+ withHandler(async (req: express.Request, res: express.Response) => {
483
+ const { slug, newSlug } = req.body
484
+ const exercise = configManager.getExercise(slug)
485
+ if (!exercise) {
486
+ return res.status(400).json({ error: "Exercise not found" })
487
+ }
488
+
489
+ try {
490
+ if (exercise.renameFolder) {
491
+ exercise.renameFolder(newSlug)
492
+ res.json({ status: "ok" })
493
+ } else {
494
+ res.status(500).json({
495
+ error:
496
+ "Failed to rename exercise because it's not supported by the exercise",
497
+ })
498
+ }
499
+ } catch {
500
+ res.status(500).json({ error: "Failed to rename exercise" })
501
+ }
502
+ })
503
+ )
504
+
505
+ app.post(
506
+ "/exercise/:slug/create",
507
+ jsonBodyParser,
508
+ withHandler(async (req: express.Request, res: express.Response) => {
509
+ const { title, readme, language } = req.body
510
+ const { slug } = req.params
511
+
512
+ if (!title || !readme || !language) {
513
+ return res.status(400).json({ error: "Missing required fields" })
514
+ }
515
+
516
+ try {
517
+ const exerciseCreated = await configManager.createExercise(
518
+ slug,
519
+ readme,
520
+ language
521
+ )
522
+ if (exerciseCreated) {
523
+ configManager.buildIndex()
524
+ res.json({ status: "ok" })
525
+ } else {
526
+ res.status(500).json({ error: "Failed to create exercise" })
527
+ }
528
+ } catch {
529
+ res.status(500).json({ error: "Failed to create exercise" })
530
+ }
531
+ })
532
+ )
533
+
534
+ const textBodyParser = bodyParser.text()
535
+ app.put(
536
+ "/exercise/:slug/file/:fileName",
537
+ textBodyParser,
538
+ withHandler((req: express.Request, res: express.Response) => {
539
+ const exercise = configManager.getExercise(req.params.slug)
540
+ if (exercise && exercise.saveFile) {
541
+ exercise.saveFile(req.params.fileName, req.body)
542
+ res.end()
543
+ }
544
+ })
545
+ )
546
+
547
+ if (config?.outputPath) {
548
+ app.use("/preview", express.static(config.outputPath))
549
+ }
550
+
551
+ app.use("/", express.static(`${config?.dirPath}/_app`))
552
+
553
+ app.use("/creator", express.static(path.join(__dirname, "..", "creatorDist")))
554
+ }