@learnpack/learnpack 5.0.262 → 5.0.266

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.
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.processImage = exports.createLearnJson = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const command_1 = require("@oclif/command");
6
+ const buffer_1 = require("buffer");
6
7
  const youtube_transcript_1 = require("youtube-transcript");
7
8
  const express = require("express");
8
9
  const cors = require("cors");
@@ -64,7 +65,7 @@ const createLearnJson = (courseInfo) => {
64
65
  exports.createLearnJson = createLearnJson;
65
66
  const uploadFileToBucket = async (bucket, file, path) => {
66
67
  const fileRef = bucket.file(path);
67
- await fileRef.save(Buffer.from(file, "utf8"));
68
+ await fileRef.save(buffer_1.Buffer.from(file, "utf8"));
68
69
  };
69
70
  const PARAMS = {
70
71
  expected_grade_level: "8",
@@ -123,7 +124,7 @@ const createMultiLangAsset = async (bucket, rigoToken, bcToken, courseSlug, cour
123
124
  // eslint-disable-next-line no-await-in-loop
124
125
  const [indexReadmeContent] = await indexReadme.download();
125
126
  const indexReadmeString = indexReadmeContent.toString();
126
- const b64IndexReadme = Buffer.from(indexReadmeString).toString("base64");
127
+ const b64IndexReadme = buffer_1.Buffer.from(indexReadmeString).toString("base64");
127
128
  // eslint-disable-next-line no-await-in-loop
128
129
  const asset = await (0, publish_1.handleAssetCreation)({ token: bcToken, rigobotToken: rigoToken.trim() }, courseJson, lang, deployUrl, b64IndexReadme, all_translations);
129
130
  if (!asset) {
@@ -236,7 +237,7 @@ class ServeCommand extends SessionCommand_1.default {
236
237
  if (!bucket) {
237
238
  return res.status(500).send("Upload failed");
238
239
  }
239
- const buffer = Buffer.from(content, "utf-8");
240
+ const buffer = buffer_1.Buffer.from(content, "utf-8");
240
241
  const file = bucket.file(destination);
241
242
  const stream = file.createWriteStream({
242
243
  resumable: false,
@@ -330,39 +331,77 @@ class ServeCommand extends SessionCommand_1.default {
330
331
  app.post("/webhooks/:courseSlug/initial-readme-processor", async (req, res) => {
331
332
  const { courseSlug } = req.params;
332
333
  const body = req.body;
333
- // Save the file as courses/courseSlug/README.md
334
- const filePath = `courses/${courseSlug}/README${(0, creatorUtilities_1.getReadmeExtension)(body.parsed.language_code)}`;
335
- await uploadFileToBucket(bucket, body.parsed.content, filePath);
334
+ console.log("RECEIVING INITIAL README WEBHOOK", body);
335
+ const langCode = body.parsed.language_code || body.parsed.output_language_code || "";
336
+ const content = body.parsed.content || body.parsed.translation || "";
337
+ if (!content || !langCode) {
338
+ console.log("No content or language code to save", body);
339
+ return res.status(400).json({
340
+ status: "ERROR",
341
+ message: "No content to save",
342
+ });
343
+ }
344
+ const filePath = `courses/${courseSlug}/README${(0, creatorUtilities_1.getReadmeExtension)(langCode)}`;
345
+ await uploadFileToBucket(bucket, content, filePath);
336
346
  res.json({ status: "SUCCESS" });
337
347
  });
338
348
  app.post("/webhooks/:courseSlug/images/:imageId", async (req, res) => {
339
349
  const { courseSlug, imageId } = req.params;
340
350
  const body = req.body;
341
- console.log("RECEIVING IMAGE WEBHOOK", body);
342
- const imageUrl = body.image_url;
343
- const imagePath = `courses/${courseSlug}/.learn/assets/${imageId}`;
344
- const imageFile = bucket.file(imagePath);
345
- const [exists] = await imageFile.exists();
346
- if (!exists) {
347
- // Descargar la imagen
348
- const response = await fetch(imageUrl);
349
- if (!response.ok) {
350
- return res.status(400).json({
351
+ try {
352
+ if (body.error) {
353
+ (0, creatorSocket_1.emitToNotification)(imageId, {
351
354
  status: "ERROR",
352
- message: "No se pudo descargar la imagen",
355
+ message: "Error generating image",
353
356
  });
357
+ return res.json({ status: "ERROR" });
358
+ }
359
+ fs.writeFileSync(`image-${imageId}.json`, JSON.stringify(body, null, 2));
360
+ const imageUrl = body.image_url;
361
+ const format = body.format;
362
+ const imagePath = `courses/${courseSlug}/.learn/assets/${imageId}`;
363
+ const imageFile = bucket.file(imagePath);
364
+ const [exists] = await imageFile.exists();
365
+ if (!exists) {
366
+ let buffer;
367
+ if (format === "b64" || imageUrl.startsWith("data:image/")) {
368
+ const base64Data = imageUrl.includes(",") ?
369
+ imageUrl.split(",")[1] :
370
+ imageUrl;
371
+ buffer = buffer_1.Buffer.from(base64Data, "base64");
372
+ }
373
+ else if (imageUrl.startsWith("http")) {
374
+ const response = await fetch(imageUrl);
375
+ if (!response.ok) {
376
+ return res.status(400).json({
377
+ status: "ERROR",
378
+ message: "Could not download the image",
379
+ });
380
+ }
381
+ buffer = new Uint8Array(await response.arrayBuffer());
382
+ }
383
+ else {
384
+ return res.status(400).json({
385
+ status: "ERROR",
386
+ message: "Image_url format not supported",
387
+ });
388
+ }
389
+ await imageFile.save(buffer, { contentType: "image/png" });
354
390
  }
355
- const buffer = await response.arrayBuffer();
356
- // Guardar la imagen en el bucket
357
- await imageFile.save(new Uint8Array(buffer), {
358
- contentType: "image/png",
391
+ (0, creatorSocket_1.emitToNotification)(imageId, {
392
+ status: "SUCCESS",
393
+ message: "Image generated successfully",
359
394
  });
395
+ res.json({ status: "SUCCESS" });
396
+ }
397
+ catch (error) {
398
+ (0, creatorSocket_1.emitToNotification)(imageId, {
399
+ status: "ERROR",
400
+ message: "Error receiving image webhook",
401
+ });
402
+ console.error("❌ Error receiving image webhook:", error);
403
+ res.status(500).json({ error: error.message });
360
404
  }
361
- (0, creatorSocket_1.emitToNotification)(imageId, {
362
- status: "SUCCESS",
363
- message: "Image generated successfully",
364
- });
365
- res.json({ status: "SUCCESS" });
366
405
  });
367
406
  app.post("/webhooks/:courseSlug/exercise-processor/:lessonID/:rigoToken", async (req, res) => {
368
407
  // console.log("Receiving a webhook to exercise processor")
@@ -438,6 +477,15 @@ class ServeCommand extends SessionCommand_1.default {
438
477
  await uploadFileToBucket(bucket, JSON.stringify(newSyllabus), `courses/${courseSlug}/.learn/initialSyllabus.json`);
439
478
  res.json({ status: "SUCCESS" });
440
479
  });
480
+ // The following endpoint is used to store an incoming translation where it supposed to be
481
+ app.post("/webhooks/:courseSlug/:exSlug/save-translation", async (req, res) => {
482
+ const { courseSlug, exSlug } = req.params;
483
+ const body = req.body;
484
+ console.log("RECEIVING TRANSLATION WEBHOOK", body);
485
+ const readmePath = `courses/${courseSlug}/exercises/${exSlug}/README${(0, creatorUtilities_1.getReadmeExtension)(body.parsed.output_language_code)}`;
486
+ await uploadFileToBucket(bucket, body.parsed.translation, readmePath);
487
+ res.json({ status: "SUCCESS" });
488
+ });
441
489
  app.get("/check-preview-image/:slug", async (req, res) => {
442
490
  const { slug } = req.params;
443
491
  const file = bucket.file(`courses/${slug}/preview.png`);
@@ -623,65 +671,79 @@ class ServeCommand extends SessionCommand_1.default {
623
671
  return res.status(400).json({ error: "RigoToken not found" });
624
672
  }
625
673
  const languagesToTranslate = languages.split(",");
626
- const course = await bucket
627
- .file(`courses/${courseSlug}/learn.json`)
628
- .download();
629
- const courseJson = JSON.parse(course.toString());
630
- const languageCodes = new Set();
674
+ const languageCodesRes = await (0, rigoActions_1.getLanguageCodes)(rigoToken, {
675
+ raw_languages: languagesToTranslate.join(","),
676
+ });
677
+ const languageCodes = languageCodesRes.parsed.language_codes;
631
678
  try {
632
679
  await Promise.all(exerciseSlugs.map(async (slug) => {
633
680
  const readmePath = `courses/${courseSlug}/exercises/${slug}/README${(0, creatorUtilities_1.getReadmeExtension)(currentLanguage)}`;
634
681
  const readme = await bucket.file(readmePath).download();
635
- await Promise.all(languagesToTranslate.map(async (language) => {
636
- const response = await (0, rigoActions_1.translateExercise)(rigoToken, {
682
+ await Promise.all(languageCodes.map(async (language) => {
683
+ // verify if the translation already exists
684
+ const translationPath = `courses/${courseSlug}/exercises/${slug}/README${(0, creatorUtilities_1.getReadmeExtension)(language)}`;
685
+ const [exists] = await bucket.file(translationPath).exists();
686
+ if (exists) {
687
+ console.log(`Translation in ${language} already exists for exercise ${slug}`);
688
+ return;
689
+ }
690
+ await (0, rigoActions_1.translateExercise)(rigoToken, {
637
691
  text_to_translate: readme.toString(),
638
692
  output_language: language,
639
- exercise_slug: slug,
640
- });
641
- const translatedReadme = await bucket.file(`courses/${courseSlug}/exercises/${slug}/README${(0, creatorUtilities_1.getReadmeExtension)(response.parsed.output_language_code)}`);
642
- await translatedReadme.save(response.parsed.translation);
643
- languageCodes.add(response.parsed.output_language_code);
693
+ }, `${process.env.HOST}/webhooks/${courseSlug}/${slug}/save-translation`);
644
694
  }));
645
695
  }));
646
- const currentLanguages = Object.keys(courseJson.title);
647
- for (const languageCode of currentLanguages) {
648
- if (languageCodes.has(languageCode)) {
649
- languageCodes.delete(languageCode);
696
+ const course = await bucket
697
+ .file(`courses/${courseSlug}/learn.json`)
698
+ .download();
699
+ const courseJson = JSON.parse(course.toString());
700
+ const neededLanguages = new Set();
701
+ let currentLanguages = Object.keys(courseJson.title);
702
+ for (const languageCode of languageCodes) {
703
+ if (!currentLanguages.includes(languageCode)) {
704
+ neededLanguages.add(languageCode);
650
705
  }
651
706
  }
652
- if ([...languageCodes].length > 0) {
653
- const translatedCourseMetadata = await Promise.all([...languageCodes].map(async (languageCode) => {
654
- const result = await (0, rigoActions_1.translateCourseMetadata)(rigoToken, {
655
- title: courseJson.title[currentLanguage],
656
- description: courseJson.description[currentLanguage],
657
- destination_lang_code: languageCode,
658
- });
659
- return {
660
- languageCode,
661
- title: result.parsed.title,
662
- description: result.parsed.description,
663
- };
664
- }));
665
- for (const metadata of translatedCourseMetadata) {
666
- courseJson.title[metadata.languageCode] = metadata.title;
667
- courseJson.description[metadata.languageCode] =
668
- metadata.description;
669
- }
707
+ const neededLanguagesList = [...neededLanguages]
708
+ .map((lang) => lang.toLowerCase())
709
+ .sort();
710
+ if (neededLanguagesList.length > 0) {
711
+ const result = await (0, rigoActions_1.translateCourseMetadata)(rigoToken, {
712
+ title: JSON.stringify(courseJson.title),
713
+ description: JSON.stringify(courseJson.description),
714
+ new_languages: neededLanguagesList.join(","),
715
+ });
716
+ const translateTitle = JSON.parse(result.parsed.title);
717
+ const translateDescription = JSON.parse(result.parsed.description);
718
+ courseJson.title = translateTitle;
719
+ courseJson.description = translateDescription;
670
720
  await uploadFileToBucket(bucket, JSON.stringify(courseJson), `courses/${courseSlug}/learn.json`);
671
- const previewReadme = await bucket.file(`courses/${courseSlug}/README${(0, creatorUtilities_1.getReadmeExtension)(currentLanguage)}`);
672
- const [previewReadmeContent] = await previewReadme.download();
673
- const previewReadmeContentString = previewReadmeContent.toString();
674
- await Promise.all([...languageCodes].map(async (languageCode) => {
675
- const translatedPreviewReadme = await (0, rigoActions_1.translateExercise)(rigoToken, {
676
- text_to_translate: previewReadmeContentString,
677
- output_language: languageCode,
678
- exercise_slug: "preview-readme",
679
- });
680
- await bucket
681
- .file(`courses/${courseSlug}/README${(0, creatorUtilities_1.getReadmeExtension)(languageCode)}`)
682
- .save(translatedPreviewReadme.parsed.translation);
683
- }));
721
+ currentLanguages = Object.keys(courseJson.title);
684
722
  }
723
+ // Check that all the READMEs exists
724
+ const missingReadmeTranslations = [];
725
+ let firstAvailable = "";
726
+ for (const languageCode of currentLanguages) {
727
+ // eslint-disable-next-line no-await-in-loop
728
+ const previewReadme = await bucket.file(`courses/${courseSlug}/README${(0, creatorUtilities_1.getReadmeExtension)(languageCode)}`);
729
+ // eslint-disable-next-line no-await-in-loop
730
+ const [exists] = await previewReadme.exists();
731
+ if (!exists) {
732
+ missingReadmeTranslations.push(languageCode);
733
+ }
734
+ else {
735
+ // eslint-disable-next-line no-await-in-loop
736
+ const [previewReadmeContent] = await previewReadme.download();
737
+ const previewReadmeContentString = previewReadmeContent.toString();
738
+ firstAvailable = previewReadmeContentString;
739
+ }
740
+ }
741
+ await Promise.all(missingReadmeTranslations.map(async (languageCode) => {
742
+ await (0, rigoActions_1.translateExercise)(rigoToken, {
743
+ text_to_translate: firstAvailable,
744
+ output_language: languageCode,
745
+ }, `${process.env.HOST}/webhooks/${courseSlug}/initial-readme-processor`);
746
+ }));
685
747
  return res.status(200).json({ message: "Translated exercises" });
686
748
  }
687
749
  catch (error) {
@@ -1019,7 +1081,7 @@ class ServeCommand extends SessionCommand_1.default {
1019
1081
  const { link } = req.params;
1020
1082
  try {
1021
1083
  // 1) Decode the URL
1022
- const decoded = Buffer.from(link, "base64url").toString("utf-8");
1084
+ const decoded = buffer_1.Buffer.from(link, "base64url").toString("utf-8");
1023
1085
  const ytMatch = decoded.match(YT_REGEX);
1024
1086
  if (ytMatch) {
1025
1087
  const videoId = ytMatch[1];
@@ -1087,7 +1149,7 @@ class ServeCommand extends SessionCommand_1.default {
1087
1149
  return res.status(400).json({ error: "URL is required" });
1088
1150
  }
1089
1151
  try {
1090
- const decodedUrl = Buffer.from(url, "base64url").toString("utf-8");
1152
+ const decodedUrl = buffer_1.Buffer.from(url, "base64url").toString("utf-8");
1091
1153
  const response = await axios_1.default.get(decodedUrl, {
1092
1154
  responseType: "arraybuffer",
1093
1155
  });
@@ -78,8 +78,7 @@ class BuildCommand extends SessionCommand_1.default {
78
78
  const response = await (0, rigoActions_1.translateExercise)(rigoToken, {
79
79
  text_to_translate: readme,
80
80
  output_language: language,
81
- exercise_slug: exercise,
82
- });
81
+ }, `${process.env.HOST}/webhooks/translate-exercise`);
83
82
  await (0, creatorUtilities_1.saveTranslatedReadme)(exercise, response.parsed.output_language_code, response.parsed.translation);
84
83
  console_1.default.success(`Translated ${exercise} to ${language} successfully`);
85
84
  }));