@learnpack/learnpack 5.0.309 → 5.0.311
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +409 -409
- package/lib/commands/audit.js +15 -15
- package/lib/commands/breakToken.js +19 -19
- package/lib/commands/clean.js +3 -3
- package/lib/commands/logout.js +3 -3
- package/lib/commands/publish.js +16 -6
- package/lib/commands/serve.js +16 -16
- package/lib/creatorDist/assets/{index-B37w_ZhT.js → index-BI7U47zy.js} +13186 -13013
- package/lib/creatorDist/index.html +1 -1
- package/lib/managers/config/index.js +77 -77
- package/lib/utils/creatorUtilities.js +14 -14
- package/lib/utils/templates/isolated/exercises/01-hello-world/README.es.md +26 -26
- package/lib/utils/templates/isolated/exercises/01-hello-world/README.md +26 -26
- package/lib/utils/templates/scorm/adlcp_rootv1p2.xsd +110 -110
- package/lib/utils/templates/scorm/config/index.html +209 -209
- package/lib/utils/templates/scorm/ims_xml.xsd +1 -1
- package/lib/utils/templates/scorm/imscp_rootv1p1p2.xsd +345 -345
- package/lib/utils/templates/scorm/imsmanifest.xml +38 -38
- package/lib/utils/templates/scorm/imsmd_rootv1p2p1.xsd +573 -573
- package/package.json +1 -1
- package/src/commands/audit.ts +487 -487
- package/src/commands/breakToken.ts +67 -67
- package/src/commands/clean.ts +30 -30
- package/src/commands/logout.ts +38 -38
- package/src/commands/publish.ts +39 -23
- package/src/commands/serve.ts +3179 -3179
- package/src/commands/start.ts +333 -333
- package/src/commands/translate.ts +123 -123
- package/src/creator/README.md +54 -54
- package/src/creator/package-lock.json +6621 -6621
- package/src/creator/package.json +55 -55
- package/src/creator/src/App.tsx +611 -608
- package/src/creator/src/components/FileUploader.tsx +340 -302
- package/src/creator/src/components/Icon.tsx +18 -18
- package/src/creator/src/components/LessonItem.tsx +152 -152
- package/src/creator/src/components/Login.tsx +259 -259
- package/src/creator/src/components/ParamsChecker.tsx +25 -25
- package/src/creator/src/components/Uploader.tsx +3 -6
- package/src/creator/src/components/syllabus/ContentIndex.tsx +323 -323
- package/src/creator/src/components/syllabus/SyllabusEditor.tsx +341 -337
- package/src/creator/src/i18n.ts +28 -28
- package/src/creator/src/locales/en.json +139 -138
- package/src/creator/src/locales/es.json +139 -138
- package/src/creator/src/utils/configTypes.ts +122 -122
- package/src/creator/src/utils/constants.ts +13 -13
- package/src/creator/src/utils/creatorUtils.ts +46 -46
- package/src/creator/src/utils/eventBus.ts +2 -2
- package/src/creator/src/utils/rigo.ts +1 -1
- package/src/creator/src/utils/socket.ts +61 -61
- package/src/creator/src/utils/store.ts +222 -222
- package/src/creator/src/vite-env.d.ts +1 -1
- package/src/creator/vite.config.ts +13 -13
- package/src/creatorDist/assets/{index-B37w_ZhT.js → index-BI7U47zy.js} +13186 -13013
- package/src/creatorDist/index.html +1 -1
- package/src/managers/config/defaults.ts +49 -49
- package/src/managers/config/exercise.ts +364 -364
- package/src/managers/config/index.ts +775 -775
- package/src/managers/file.ts +236 -236
- package/src/managers/server/routes.ts +554 -554
- package/src/managers/telemetry.ts +188 -188
- package/src/models/action.ts +13 -13
- package/src/models/config-manager.ts +28 -28
- package/src/models/config.ts +106 -106
- package/src/models/exercise-obj.ts +30 -30
- package/src/models/session.ts +39 -39
- package/src/models/socket.ts +61 -61
- package/src/models/status.ts +16 -16
- package/src/ui/_app/app.css +1 -1
- package/src/ui/_app/app.js +477 -407
- package/src/ui/app.tar.gz +0 -0
- package/src/utils/BaseCommand.ts +56 -56
- package/src/utils/api.ts +665 -665
- package/src/utils/audit.ts +392 -392
- package/src/utils/checkNotInstalled.ts +267 -267
- package/src/utils/convertCreds.js +34 -34
- package/src/utils/creatorUtilities.ts +504 -504
- package/src/utils/export/README.md +178 -178
- package/src/utils/incrementVersion.js +74 -74
- package/src/utils/misc.ts +58 -58
- package/src/utils/sidebarGenerator.ts +195 -195
- package/src/utils/templates/isolated/exercises/01-hello-world/README.es.md +26 -26
- package/src/utils/templates/isolated/exercises/01-hello-world/README.md +26 -26
- package/src/utils/templates/scorm/adlcp_rootv1p2.xsd +110 -110
- package/src/utils/templates/scorm/config/index.html +209 -209
- package/src/utils/templates/scorm/ims_xml.xsd +1 -1
- package/src/utils/templates/scorm/imscp_rootv1p1p2.xsd +345 -345
- package/src/utils/templates/scorm/imsmanifest.xml +38 -38
- 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
|
+
}
|