neekuro 2.0.4 → 2.2.0
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/CHANGELOG.md +51 -0
- package/README.md +74 -6
- package/package.json +17 -11
- package/sources/classes/SFW.js +292 -45
- package/sources/classes/errors/{APIerror.js → APIError.js} +14 -12
- package/sources/classes/errors/Error.js +5 -4
- package/sources/classes/package/NekoGif.js +7 -4
- package/sources/classes/package/{Welcome.js → welcome.js} +162 -96
- package/sources/classes/user/BaseUser.js +21 -7
- package/sources/classes/user/User.js +7 -7
- package/sources/main/index.js +6 -4
- package/sources/rest/APIClient.js +73 -31
- package/sources/typings/index.d.ts +53 -29
- package/sources/utils/imageLoader.js +146 -0
|
@@ -2,7 +2,15 @@
|
|
|
2
2
|
const Canvas = require('canvas');
|
|
3
3
|
const NekoError = require('../errors/Error.js');
|
|
4
4
|
const hex_reg = require('hex-color-regex')({ strict: true })
|
|
5
|
-
const path = require('path');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const { loadImageSource, parseRemoteURL, validateImageBuffer } = require('../../utils/imageLoader.js');
|
|
8
|
+
|
|
9
|
+
const DEFAULT_WIDTH = 1140;
|
|
10
|
+
const DEFAULT_HEIGHT = 520;
|
|
11
|
+
const MIN_DIMENSION = 128;
|
|
12
|
+
const MAX_DIMENSION = 4096;
|
|
13
|
+
const registeredFonts = new Set();
|
|
6
14
|
|
|
7
15
|
/**
|
|
8
16
|
* @typedef {import('../../typings').FontNames} FontNames
|
|
@@ -29,7 +37,8 @@ class Welcome {
|
|
|
29
37
|
constructor() {
|
|
30
38
|
/**
|
|
31
39
|
* @property {Object} data - Configuración actual de la imagen
|
|
32
|
-
* @property {string} data.font - Fuente actualmente seleccionada
|
|
40
|
+
* @property {string} data.font - Fuente actualmente seleccionada
|
|
41
|
+
* @property {'center'|'manual'} data.layout - Distribución de los elementos
|
|
33
42
|
* @property {number} data.width - Ancho actual de la imagen
|
|
34
43
|
* @property {number} data.height - Alto actual de la imagen
|
|
35
44
|
* @property {Object} data.background - Configuración del fondo
|
|
@@ -54,10 +63,11 @@ class Welcome {
|
|
|
54
63
|
* @property {number} data.description.font_size - Tamaño de fuente de la descripción
|
|
55
64
|
* @property {string} data.description.text_color - Color del texto de la descripción
|
|
56
65
|
*/
|
|
57
|
-
this.data = {
|
|
58
|
-
font: 'arial',
|
|
59
|
-
|
|
60
|
-
|
|
66
|
+
this.data = {
|
|
67
|
+
font: 'arial',
|
|
68
|
+
layout: 'center',
|
|
69
|
+
width: DEFAULT_WIDTH,
|
|
70
|
+
height: DEFAULT_HEIGHT,
|
|
61
71
|
background: {
|
|
62
72
|
type: 'color',
|
|
63
73
|
value: '#23272A'
|
|
@@ -83,8 +93,9 @@ class Welcome {
|
|
|
83
93
|
font_size: 35,
|
|
84
94
|
text_color: "#F7F7F7"
|
|
85
95
|
}
|
|
86
|
-
}
|
|
87
|
-
|
|
96
|
+
}
|
|
97
|
+
this.setFont('arial');
|
|
98
|
+
}
|
|
88
99
|
|
|
89
100
|
/**
|
|
90
101
|
* Establece la resolución de la imagen de bienvenida
|
|
@@ -99,22 +110,28 @@ class Welcome {
|
|
|
99
110
|
* // Restablecer a valores por defecto
|
|
100
111
|
* welcome.setResolution('default', 'default');
|
|
101
112
|
*/
|
|
102
|
-
setResolution(width, height) {
|
|
113
|
+
setResolution(width, height) {
|
|
103
114
|
if (!width) throw new NekoError('ResolutionError', 'el parametro <width> no puede quedar vacio');
|
|
104
115
|
if (!height) throw new NekoError('ResolutionError', 'el parametro <height> no puede quedar vacio');
|
|
105
116
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
117
|
+
this.data.width = width === 'default' ? DEFAULT_WIDTH : validateDimension(width, 'width');
|
|
118
|
+
this.data.height = height === 'default' ? DEFAULT_HEIGHT : validateDimension(height, 'height');
|
|
119
|
+
|
|
120
|
+
return this
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Selecciona la distribución automática centrada o las coordenadas manuales.
|
|
125
|
+
* @param {'center'|'manual'} layout
|
|
126
|
+
* @returns {Welcome}
|
|
127
|
+
*/
|
|
128
|
+
setLayout(layout) {
|
|
129
|
+
if (!['center', 'manual'].includes(layout)) {
|
|
130
|
+
throw new NekoError('LayoutError', 'El layout debe ser "center" o "manual"');
|
|
131
|
+
}
|
|
132
|
+
this.data.layout = layout;
|
|
133
|
+
return this;
|
|
134
|
+
}
|
|
118
135
|
|
|
119
136
|
/**
|
|
120
137
|
* Configura la tipografía para el texto de la imagen
|
|
@@ -145,13 +162,18 @@ class Welcome {
|
|
|
145
162
|
case 'custom':
|
|
146
163
|
if (!dir) throw new NekoError('FontError', 'el parametro <dir> no puede quedar vacio en un tipo de fuente customizada');
|
|
147
164
|
if (typeof dir != 'string') throw new NekoError('TypeError', 'el parametro <dir> solo acepta tipo \'string\', recibió ' + typeof dir);
|
|
148
|
-
if (
|
|
149
|
-
ruta = path.
|
|
165
|
+
if (path.extname(dir).toLowerCase() !== '.ttf') throw new NekoError('FontError', 'se necesita un archivo TTF en el parametro <dir>')
|
|
166
|
+
ruta = path.resolve(dir);
|
|
150
167
|
break;
|
|
151
168
|
default: throw new NekoError('FontError', 'el parametro <name> no acepta ningun tipo diferente a los preterminados');
|
|
152
169
|
}
|
|
153
170
|
try {
|
|
154
|
-
|
|
171
|
+
if (!fs.existsSync(ruta)) throw new Error('Font not found');
|
|
172
|
+
const fontKey = `${name}:${ruta}`;
|
|
173
|
+
if (!registeredFonts.has(fontKey)) {
|
|
174
|
+
Canvas.registerFont(ruta, { family: name });
|
|
175
|
+
registeredFonts.add(fontKey);
|
|
176
|
+
}
|
|
155
177
|
} catch (error) {
|
|
156
178
|
throw new NekoError('DirError', 'no se puede encontrar el directorio especificado')
|
|
157
179
|
}
|
|
@@ -206,18 +228,20 @@ class Welcome {
|
|
|
206
228
|
}
|
|
207
229
|
|
|
208
230
|
// validacion de posiciones
|
|
209
|
-
if (data.x) {
|
|
231
|
+
if (data.x !== undefined) {
|
|
210
232
|
if (typeof data.x !== 'number') {
|
|
211
233
|
throw new NekoError('TypeError', `La propiedad <x> solo acepta tipo 'number', recibió ${typeof data.x}`);
|
|
212
234
|
}
|
|
213
|
-
this.data.title.x = data.x;
|
|
235
|
+
this.data.title.x = data.x;
|
|
236
|
+
this.data.layout = 'manual';
|
|
214
237
|
}
|
|
215
238
|
|
|
216
|
-
if (data.y) {
|
|
239
|
+
if (data.y !== undefined) {
|
|
217
240
|
if (typeof data.y !== 'number') {
|
|
218
241
|
throw new NekoError('TypeError', `La propiedad <y> solo acepta tipo 'number', recibió ${typeof data.y}`);
|
|
219
242
|
}
|
|
220
|
-
this.data.title.y = data.y;
|
|
243
|
+
this.data.title.y = data.y;
|
|
244
|
+
this.data.layout = 'manual';
|
|
221
245
|
}
|
|
222
246
|
}
|
|
223
247
|
|
|
@@ -270,18 +294,20 @@ class Welcome {
|
|
|
270
294
|
}
|
|
271
295
|
|
|
272
296
|
// validacion de posiciones
|
|
273
|
-
if (data.x) {
|
|
297
|
+
if (data.x !== undefined) {
|
|
274
298
|
if (typeof data.x !== 'number') {
|
|
275
299
|
throw new NekoError('TypeError', `La propiedad <x> solo acepta tipo 'number', recibió ${typeof data.x}`);
|
|
276
300
|
}
|
|
277
|
-
this.data.description.x = data.x;
|
|
301
|
+
this.data.description.x = data.x;
|
|
302
|
+
this.data.layout = 'manual';
|
|
278
303
|
}
|
|
279
304
|
|
|
280
|
-
if (data.y) {
|
|
305
|
+
if (data.y !== undefined) {
|
|
281
306
|
if (typeof data.y !== 'number') {
|
|
282
307
|
throw new NekoError('TypeError', `La propiedad <y> solo acepta tipo 'number', recibió ${typeof data.y}`);
|
|
283
308
|
}
|
|
284
|
-
this.data.description.y = data.y;
|
|
309
|
+
this.data.description.y = data.y;
|
|
310
|
+
this.data.layout = 'manual';
|
|
285
311
|
}
|
|
286
312
|
}
|
|
287
313
|
|
|
@@ -327,11 +353,9 @@ class Welcome {
|
|
|
327
353
|
}
|
|
328
354
|
|
|
329
355
|
if (typeof value == 'string') {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
throw new NekoError('BackgroundError', 'La URL proporcionada no es válida');
|
|
334
|
-
}
|
|
356
|
+
parseRemoteURL(value, 'el fondo');
|
|
357
|
+
} else {
|
|
358
|
+
validateImageBuffer(value, 'el fondo');
|
|
335
359
|
}
|
|
336
360
|
|
|
337
361
|
this.data.background = { type, value };
|
|
@@ -369,56 +393,33 @@ class Welcome {
|
|
|
369
393
|
}
|
|
370
394
|
|
|
371
395
|
if (typeof source === 'string') {
|
|
372
|
-
|
|
373
|
-
const url = new URL(source);
|
|
374
|
-
|
|
375
|
-
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
376
|
-
throw new NekoError('AvatarError', 'La URL debe usar HTTP/HTTPS');
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
const extension = url.pathname.toLowerCase().split('.').pop();
|
|
380
|
-
if (!['jpg', 'jpeg', 'png', 'gif'].includes(extension)) {
|
|
381
|
-
throw new NekoError('AvatarError', 'Formato de imagen no soportado. Usa JPEG, PNG o GIF');
|
|
382
|
-
}
|
|
383
|
-
} catch (error) {
|
|
384
|
-
if (error instanceof NekoError) throw error;
|
|
385
|
-
throw new NekoError('AvatarError', 'La URL del avatar no es válida');
|
|
386
|
-
}
|
|
396
|
+
parseRemoteURL(source, 'el avatar');
|
|
387
397
|
}
|
|
388
398
|
|
|
389
399
|
if (Buffer.isBuffer(source)) {
|
|
390
|
-
|
|
391
|
-
throw new NekoError('AvatarError', 'El Buffer de imagen es demasiado pequeño');
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
const header = source.toString('hex', 0, 8);
|
|
395
|
-
const isJPEG = header.startsWith('ffd8ff');
|
|
396
|
-
const isPNG = header.startsWith('89504e470d0a1a0a');
|
|
397
|
-
const isGIF = header.startsWith('47494638');
|
|
398
|
-
|
|
399
|
-
if (!isJPEG && !isPNG && !isGIF) {
|
|
400
|
-
throw new NekoError('AvatarError', 'Formato de imagen no soportado. El Buffer debe ser JPEG, PNG o GIF');
|
|
401
|
-
}
|
|
400
|
+
validateImageBuffer(source, 'el avatar');
|
|
402
401
|
}
|
|
403
402
|
|
|
404
403
|
this.data.avatar.source = source;
|
|
405
404
|
|
|
406
405
|
if (data) {
|
|
407
|
-
if (data.x) {
|
|
406
|
+
if (data.x !== undefined) {
|
|
408
407
|
if (typeof data.x != 'number') {
|
|
409
408
|
throw new NekoError('TypeError', 'La propiedad <x> debe ser un número');
|
|
410
409
|
}
|
|
411
410
|
if (data.x < 0) {
|
|
412
411
|
throw new NekoError('AvatarError', 'La posición X no puede ser negativa');
|
|
413
412
|
}
|
|
414
|
-
this.data.avatar.x = data.x;
|
|
413
|
+
this.data.avatar.x = data.x;
|
|
414
|
+
this.data.layout = 'manual';
|
|
415
415
|
}
|
|
416
416
|
|
|
417
|
-
if (data.y) {
|
|
417
|
+
if (data.y !== undefined) {
|
|
418
418
|
if (typeof data.y != 'number') {
|
|
419
419
|
throw new NekoError('TypeError', 'La propiedad <y> debe ser un número');
|
|
420
420
|
}
|
|
421
|
-
this.data.avatar.y = data.y;
|
|
421
|
+
this.data.avatar.y = data.y;
|
|
422
|
+
this.data.layout = 'manual';
|
|
422
423
|
}
|
|
423
424
|
|
|
424
425
|
if (data.border) {
|
|
@@ -434,10 +435,13 @@ class Welcome {
|
|
|
434
435
|
this.data.avatar.border = data.border;
|
|
435
436
|
}
|
|
436
437
|
|
|
437
|
-
if (data.radio) {
|
|
438
|
-
if (typeof data.radio != 'number') {
|
|
439
|
-
throw new NekoError('TypeError', 'La propiedad <radio> debe ser un número');
|
|
440
|
-
}
|
|
438
|
+
if (data.radio !== undefined) {
|
|
439
|
+
if (typeof data.radio != 'number') {
|
|
440
|
+
throw new NekoError('TypeError', 'La propiedad <radio> debe ser un número');
|
|
441
|
+
}
|
|
442
|
+
if (!Number.isFinite(data.radio) || data.radio <= 0 || data.radio > Math.min(this.data.width, this.data.height) / 2) {
|
|
443
|
+
throw new NekoError('AvatarError', 'El radio debe ser positivo y caber dentro de la imagen');
|
|
444
|
+
}
|
|
441
445
|
this.data.avatar.radio = data.radio;
|
|
442
446
|
}
|
|
443
447
|
}
|
|
@@ -464,8 +468,8 @@ class Welcome {
|
|
|
464
468
|
|
|
465
469
|
try {
|
|
466
470
|
const [bg, avatar] = await Promise.all([
|
|
467
|
-
this.data.background.type == 'image' ?
|
|
468
|
-
|
|
471
|
+
this.data.background.type == 'image' ? loadImageSource(this.data.background.value, 'el fondo') : null,
|
|
472
|
+
loadImageSource(this.data.avatar.source, 'el avatar')
|
|
469
473
|
])
|
|
470
474
|
|
|
471
475
|
const canvas = Canvas.createCanvas(this.data.width, this.data.height)
|
|
@@ -481,35 +485,43 @@ class Welcome {
|
|
|
481
485
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
482
486
|
}
|
|
483
487
|
|
|
484
|
-
const maxTitleWidth = this.data.width * 0.8;
|
|
485
|
-
let titleFontSize = this.data.
|
|
486
|
-
|
|
488
|
+
const maxTitleWidth = this.data.width * 0.8;
|
|
489
|
+
let titleFontSize = this.data.layout === 'center'
|
|
490
|
+
? Math.min(this.data.title.font_size, this.data.height * 0.14)
|
|
491
|
+
: this.data.title.font_size;
|
|
492
|
+
ctx.font = `${titleFontSize}px ${this.data.font}`;
|
|
487
493
|
|
|
488
494
|
while (ctx.measureText(this.data.title.content).width > maxTitleWidth && titleFontSize > 10) {
|
|
489
495
|
titleFontSize -= 1;
|
|
490
496
|
ctx.font = `${titleFontSize}px ${this.data.font}`;
|
|
491
497
|
}
|
|
492
498
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
let descFontSize = this.data.description.font_size;
|
|
499
|
-
ctx.font = `${descFontSize}px ${this.data.font}`;
|
|
499
|
+
const maxDescWidth = this.data.width * 0.9;
|
|
500
|
+
let descFontSize = this.data.layout === 'center'
|
|
501
|
+
? Math.min(this.data.description.font_size, this.data.height * 0.09)
|
|
502
|
+
: this.data.description.font_size;
|
|
503
|
+
ctx.font = `${descFontSize}px ${this.data.font}`;
|
|
500
504
|
|
|
501
505
|
while (ctx.measureText(this.data.description.content).width > maxDescWidth && descFontSize > 8) {
|
|
502
506
|
descFontSize -= 1;
|
|
503
507
|
ctx.font = `${descFontSize}px ${this.data.font}`;
|
|
504
508
|
}
|
|
505
509
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
510
|
+
const layout = this.data.layout === 'center'
|
|
511
|
+
? calculateCenteredLayout(this.data, titleFontSize, descFontSize)
|
|
512
|
+
: {
|
|
513
|
+
avatarX: this.data.avatar.x,
|
|
514
|
+
avatarY: this.data.avatar.y,
|
|
515
|
+
radio: this.data.avatar.radio,
|
|
516
|
+
titleX: this.data.title.x,
|
|
517
|
+
titleY: this.data.title.y,
|
|
518
|
+
descriptionX: this.data.description.x,
|
|
519
|
+
descriptionY: this.data.description.y
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
const x = layout.avatarX;
|
|
523
|
+
const y = layout.avatarY;
|
|
524
|
+
const radio = layout.radio;
|
|
513
525
|
|
|
514
526
|
if (this.data.avatar.border) {
|
|
515
527
|
ctx.beginPath();
|
|
@@ -524,8 +536,18 @@ class Welcome {
|
|
|
524
536
|
ctx.arc(x + radio, y + radio, radio, 0, Math.PI * 2, true);
|
|
525
537
|
ctx.closePath();
|
|
526
538
|
ctx.clip();
|
|
527
|
-
ctx.drawImage(avatar, x, y, radio * 2, radio * 2);
|
|
528
|
-
ctx.restore();
|
|
539
|
+
ctx.drawImage(avatar, x, y, radio * 2, radio * 2);
|
|
540
|
+
ctx.restore();
|
|
541
|
+
|
|
542
|
+
ctx.textAlign = 'center';
|
|
543
|
+
ctx.textBaseline = this.data.layout === 'center' ? 'middle' : 'alphabetic';
|
|
544
|
+
ctx.font = `${titleFontSize}px ${this.data.font}`;
|
|
545
|
+
ctx.fillStyle = this.data.title.text_color;
|
|
546
|
+
ctx.fillText(this.data.title.content, layout.titleX, layout.titleY);
|
|
547
|
+
|
|
548
|
+
ctx.font = `${descFontSize}px ${this.data.font}`;
|
|
549
|
+
ctx.fillStyle = this.data.description.text_color;
|
|
550
|
+
ctx.fillText(this.data.description.content, layout.descriptionX, layout.descriptionY);
|
|
529
551
|
|
|
530
552
|
return canvas.toBuffer();
|
|
531
553
|
} catch (error) {
|
|
@@ -535,6 +557,50 @@ class Welcome {
|
|
|
535
557
|
throw new NekoError('GenerateError', 'Error al generar la imagen: '+error.message)
|
|
536
558
|
}
|
|
537
559
|
}
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function calculateCenteredLayout(data, titleFontSize, descFontSize) {
|
|
563
|
+
const borderSize = data.avatar.border ? 6 : 0;
|
|
564
|
+
const avatarGap = clamp(data.height * 0.045, 16, 32);
|
|
565
|
+
const textGap = clamp(data.height * 0.018, 8, 16);
|
|
566
|
+
const availableHeight = data.height * 0.9;
|
|
567
|
+
const maxRadio = Math.max(
|
|
568
|
+
16,
|
|
569
|
+
(availableHeight - titleFontSize - descFontSize - avatarGap - textGap) / 2 - borderSize
|
|
570
|
+
);
|
|
571
|
+
const radio = Math.min(data.avatar.radio, maxRadio);
|
|
572
|
+
const avatarOuterSize = (radio + borderSize) * 2;
|
|
573
|
+
const contentHeight = avatarOuterSize + avatarGap + titleFontSize + textGap + descFontSize;
|
|
574
|
+
const startY = (data.height - contentHeight) / 2;
|
|
575
|
+
const centerX = data.width / 2;
|
|
576
|
+
const avatarX = centerX - radio;
|
|
577
|
+
const avatarY = startY + borderSize;
|
|
578
|
+
const titleY = startY + avatarOuterSize + avatarGap + titleFontSize / 2;
|
|
579
|
+
const descriptionY = titleY + titleFontSize / 2 + textGap + descFontSize / 2;
|
|
580
|
+
|
|
581
|
+
return {
|
|
582
|
+
avatarX,
|
|
583
|
+
avatarY,
|
|
584
|
+
radio,
|
|
585
|
+
titleX: centerX,
|
|
586
|
+
titleY,
|
|
587
|
+
descriptionX: centerX,
|
|
588
|
+
descriptionY
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function clamp(value, min, max) {
|
|
593
|
+
return Math.min(Math.max(value, min), max);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function validateDimension(value, name) {
|
|
597
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || !Number.isFinite(value)) {
|
|
598
|
+
throw new NekoError('TypeError', `el parametro <${name}> debe ser un número entero`);
|
|
599
|
+
}
|
|
600
|
+
if (value < MIN_DIMENSION || value > MAX_DIMENSION) {
|
|
601
|
+
throw new NekoError('ResolutionError', `<${name}> debe estar entre ${MIN_DIMENSION} y ${MAX_DIMENSION} píxeles`);
|
|
602
|
+
}
|
|
603
|
+
return value;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
module.exports = Welcome;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
let tokenGlobal = null;
|
|
3
|
+
let tokenGlobal = null;
|
|
4
|
+
let tokenVersion = 0;
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Clase BaseUser
|
|
@@ -11,17 +12,30 @@ class BaseUser {
|
|
|
11
12
|
* Establece el token "global" en todo el paquete.
|
|
12
13
|
* @param {string} token token a establecer.
|
|
13
14
|
*/
|
|
14
|
-
static setToken(token) {
|
|
15
|
-
|
|
15
|
+
static setToken(token) {
|
|
16
|
+
if (typeof token !== 'string' || !token.trim()) {
|
|
17
|
+
throw new TypeError('El token debe ser una cadena no vacía');
|
|
18
|
+
}
|
|
19
|
+
const normalizedToken = token.trim();
|
|
20
|
+
if (tokenGlobal !== normalizedToken) tokenVersion += 1;
|
|
21
|
+
tokenGlobal = normalizedToken;
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
/**
|
|
19
25
|
* Obten el token del usuario si este ya se estableció
|
|
20
26
|
* @returns {string} Token de usuario
|
|
21
27
|
*/
|
|
22
|
-
static getToken() {
|
|
23
|
-
return tokenGlobal;
|
|
24
|
-
}
|
|
25
|
-
|
|
28
|
+
static getToken() {
|
|
29
|
+
return tokenGlobal;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Identificador interno que cambia cada vez que se establece otro token.
|
|
34
|
+
* @returns {number}
|
|
35
|
+
*/
|
|
36
|
+
static getTokenVersion() {
|
|
37
|
+
return tokenVersion;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
26
40
|
|
|
27
41
|
module.exports = BaseUser;
|
|
@@ -12,9 +12,9 @@ class User {
|
|
|
12
12
|
* Puedes establecer el token aqui, sin embargo, recomiendo hacerlo en el metodo dedicado
|
|
13
13
|
* @param {string} token Token de la api
|
|
14
14
|
*/
|
|
15
|
-
constructor(token) {
|
|
16
|
-
this.token_ =
|
|
17
|
-
|
|
15
|
+
constructor(token) {
|
|
16
|
+
this.token_ = undefined;
|
|
17
|
+
if (token !== undefined) this.token(token);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
@@ -23,9 +23,9 @@ class User {
|
|
|
23
23
|
* @throws {NekoError} si el token no fue dado o no es una cadena
|
|
24
24
|
*/
|
|
25
25
|
token(token) {
|
|
26
|
-
if(
|
|
27
|
-
if(
|
|
28
|
-
this.token_ = token;
|
|
26
|
+
if(typeof(token) !== 'string') throw new NekoError('InvalidType', `El token debe ser string, no ${typeof(token)}`);
|
|
27
|
+
if(!token.trim()) throw new NekoError('NoToken', 'el parametro <token> es requerido');
|
|
28
|
+
this.token_ = token.trim();
|
|
29
29
|
setToken(this.#getToken());
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -38,4 +38,4 @@ class User {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
module.exports = User
|
|
41
|
+
module.exports = User
|
package/sources/main/index.js
CHANGED
|
@@ -8,7 +8,7 @@ exports.User = require('../classes/user/User.js')
|
|
|
8
8
|
|
|
9
9
|
exports.NekoGif = require('../classes/package/NekoGif.js')
|
|
10
10
|
exports.BaseUser = require('../classes/user/BaseUser.js')
|
|
11
|
-
exports.Welcome = require('../classes/package/
|
|
11
|
+
exports.Welcome = require('../classes/package/welcome.js')
|
|
12
12
|
|
|
13
13
|
// rest
|
|
14
14
|
|
|
@@ -16,9 +16,11 @@ exports.APIClient = require('../rest/APIClient.js')
|
|
|
16
16
|
|
|
17
17
|
// errores
|
|
18
18
|
|
|
19
|
-
exports.APIError = require('../classes/errors/
|
|
20
|
-
exports.
|
|
19
|
+
exports.APIError = require('../classes/errors/APIError.js')
|
|
20
|
+
exports.NekoError = require('../classes/errors/Error.js')
|
|
21
|
+
// Alias conservado por compatibilidad con versiones 2.0.x.
|
|
22
|
+
exports.Error = exports.NekoError
|
|
21
23
|
|
|
22
24
|
//#region "constantes"
|
|
23
25
|
|
|
24
|
-
exports.SFW = require('../classes/SFW.js')
|
|
26
|
+
exports.SFW = require('../classes/SFW.js')
|
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* @typedef {import('../typings').GifResponse} GifResponse
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
const APIError = require('../classes/errors/
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
const APIError = require('../classes/errors/APIError.js');
|
|
9
|
+
const NekoError = require('../classes/errors/Error.js');
|
|
10
|
+
|
|
11
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Clase APIClient
|
|
@@ -19,12 +20,28 @@ class APIClient {
|
|
|
19
20
|
* @param {string} baseURL link principal de la api
|
|
20
21
|
* @param {APIClientData} data objeto para las solicitudes
|
|
21
22
|
*/
|
|
22
|
-
constructor(baseURL, data) {
|
|
23
|
+
constructor(baseURL, data) {
|
|
24
|
+
if (typeof baseURL !== 'string' || !baseURL) {
|
|
25
|
+
throw new NekoError('InvalidBaseURL', 'El parámetro <baseURL> debe ser una URL válida');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let parsedBaseURL;
|
|
29
|
+
try {
|
|
30
|
+
parsedBaseURL = new URL(baseURL);
|
|
31
|
+
} catch {
|
|
32
|
+
throw new NekoError('InvalidBaseURL', 'El parámetro <baseURL> debe ser una URL válida');
|
|
33
|
+
}
|
|
34
|
+
if (parsedBaseURL.protocol !== 'https:') {
|
|
35
|
+
throw new NekoError('InvalidBaseURL', 'La API debe utilizar HTTPS');
|
|
36
|
+
}
|
|
37
|
+
if (!data || typeof data.token !== 'string' || !data.token.trim()) {
|
|
38
|
+
throw new NekoError('NoToken', 'Se requiere un token para utilizar la API');
|
|
39
|
+
}
|
|
23
40
|
/**
|
|
24
41
|
* url principal
|
|
25
42
|
* @type {string}
|
|
26
43
|
*/
|
|
27
|
-
this.baseURL = baseURL
|
|
44
|
+
this.baseURL = baseURL.replace(/\/$/, '')
|
|
28
45
|
/**
|
|
29
46
|
* data
|
|
30
47
|
* @type {APIClientData}
|
|
@@ -34,32 +51,57 @@ class APIClient {
|
|
|
34
51
|
/**
|
|
35
52
|
* Obtiene el objeto compartido por la api a traves de la ruta y con autorizacion por token
|
|
36
53
|
* @param {string} endpoint establece la ruta a la que se quiere comunicar
|
|
37
|
-
* @
|
|
54
|
+
* @template {import('../typings').ApiResponse} [T=GifResponse]
|
|
55
|
+
* @returns {Promise<T>}
|
|
38
56
|
* @throws {APIError} solo si sucede un error entre solicitudes
|
|
39
57
|
*/
|
|
40
|
-
async get(endpoint) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
async get(endpoint, options = {}) {
|
|
59
|
+
if (typeof endpoint !== 'string' || !endpoint.startsWith('/')) {
|
|
60
|
+
throw new NekoError('InvalidEndpoint', 'El endpoint debe comenzar con "/"');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const timeout = Number.isFinite(options.timeout) && options.timeout > 0
|
|
64
|
+
? options.timeout
|
|
65
|
+
: DEFAULT_TIMEOUT_MS;
|
|
66
|
+
const controller = new AbortController();
|
|
67
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const response = await fetch(`${this.baseURL}${endpoint}`, {
|
|
71
|
+
method: 'GET',
|
|
72
|
+
headers: {
|
|
73
|
+
'Authorization': `${this.data.token}`,
|
|
74
|
+
'Content-Type': 'application/json'
|
|
75
|
+
},
|
|
76
|
+
signal: controller.signal
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const data = await readJson(response);
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
throw new APIError(endpoint, response, data);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return /** @type {T} */ (data)
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error instanceof APIError) {
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
const message = error?.name === 'AbortError'
|
|
90
|
+
? `La solicitud excedió el tiempo límite de ${timeout} ms`
|
|
91
|
+
: error?.message ?? 'No fue posible conectar con la API';
|
|
92
|
+
throw APIError.fromNetworkError(endpoint, `${this.baseURL}${endpoint}`, message);
|
|
93
|
+
} finally {
|
|
94
|
+
clearTimeout(timeoutId);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function readJson(response) {
|
|
100
|
+
try {
|
|
101
|
+
return await response.json();
|
|
102
|
+
} catch {
|
|
103
|
+
return { code: response.status, message: 'La API devolvió una respuesta que no es JSON' };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
64
106
|
|
|
65
|
-
module.exports = APIClient;
|
|
107
|
+
module.exports = APIClient;
|