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 ADDED
@@ -0,0 +1,51 @@
1
+ # Changelog
2
+
3
+ ## [2.2.0] - 28-08-2026
4
+
5
+ ### Added
6
+ - `SFW.getGifs()` permite obtener todas las imágenes de una subcategoría.
7
+ - `SFW.clearCache()` permite invalidar toda la caché, una categoría o una subcategoría.
8
+ - `SFW.setBaseURL()` permite configurar un endpoint HTTPS alternativo.
9
+ - `SFW.setCacheTTL()` permite configurar o desactivar el tiempo de caché.
10
+
11
+ ### Changed
12
+ - `SFW.getGif()` conserva listas en memoria durante cinco minutos y selecciona aleatoriamente en cada llamada.
13
+ - Las solicitudes concurrentes para la misma subcategoría se agrupan en una sola petición HTTP.
14
+ - Cuando existen varias imágenes se evita repetir inmediatamente la selección anterior.
15
+ - Cambiar el token invalida automáticamente las listas obtenidas con el token previo.
16
+ - El cliente REST admite respuestas tipadas distintas a un único GIF.
17
+
18
+ ### Compatibility
19
+ - `SFW.getGif(category, gif)` mantiene la misma firma y continúa devolviendo `NekoGif`.
20
+ - La caché solamente cambia la frecuencia de consulta; no fija una imagen durante el TTL.
21
+
22
+ ## [2.1.0] - 28-08-2026
23
+
24
+ ### Changed
25
+ - Se corrigieron imports sensibles a mayúsculas para funcionar en Linux.
26
+ - Se eliminó la dependencia circular del paquete hacia sí mismo y `node-fetch`.
27
+ - `Welcome` valida resolución, coordenadas, radios, fuentes e imágenes con límites seguros.
28
+ - Avatar, título y descripción utilizan una columna centrada automática con modo manual opcional.
29
+ - Las imágenes remotas requieren HTTPS, tienen timeout, límite de 10 MB y bloqueo de redes privadas.
30
+ - El cliente REST usa `fetch` nativo, timeout y conserva el error concreto de red.
31
+ - Se alinearon los exports, la implementación y las declaraciones TypeScript.
32
+
33
+ ### Compatibility
34
+ - Se conserva la API encadenable de `Welcome`.
35
+ - `Error` continúa disponible como alias de `NekoError`.
36
+ - Las URLs HTTP de imágenes dejan de aceptarse; deben utilizar HTTPS.
37
+
38
+ ## [2.0.3] - 07-04-2025
39
+
40
+ ### Fixed
41
+ - **Error de Importaciones**
42
+ - Corregido el error de importaciones require('...')`
43
+ - Causaba que no se pudiera usar el paquete
44
+ - Se uso el mismo nombre para todas las importaciones respecto al archivo
45
+ - Afectaba a todos los usuarios que hacian uso de la api
46
+
47
+ **Impacto**:
48
+ - La actualizacion prevee que resuelva el error
49
+ - No requiere cambios en implementaciones existentes
50
+
51
+ Commit: (1e7380a)
package/README.md CHANGED
@@ -56,7 +56,7 @@ const neekuro = require('neekuro');
56
56
  */
57
57
 
58
58
  // es necesario escribir todo siempre dentro de una funcion asincrona
59
- async function ObtenerImagen() {
59
+ async function ObtenerImagen() {
60
60
  // obtenemos un gif con el metodo getGif de SFW
61
61
  const cuddle = await neekuro.SFW.getGif('action', 'cuddle');
62
62
 
@@ -65,11 +65,45 @@ async function ObtenerImagen() {
65
65
  }
66
66
 
67
67
  // ejecutamos la funcion
68
- ObtenerImagen();
68
+ ObtenerImagen();
69
69
 
70
70
  // nota: no te aguites por el gif y subcategoria, todo viene en documentado con typescript, solo tienes que completar los pasos :)
71
- ```
72
- #### Ejemplo de imagenes de bienvenida
71
+ ```
72
+
73
+ `getGif()` selecciona una imagen nuevamente en cada llamada. Internamente, NeeKuro
74
+ conserva durante cinco minutos la lista de la subcategoría para reducir solicitudes
75
+ HTTP; no conserva un único GIF durante ese tiempo. Cuando existen varias imágenes,
76
+ evita mostrar la misma dos veces consecutivas.
77
+
78
+ También puedes obtener la lista completa:
79
+
80
+ ```javascript
81
+ const gifs = await neekuro.SFW.getGifs('action', 'cuddle');
82
+ console.log(gifs.map(gif => gif.getUrl()));
83
+ ```
84
+
85
+ Si actualizaste el catálogo y necesitas consultarlo inmediatamente, puedes limpiar
86
+ una subcategoría, una categoría completa o toda la caché:
87
+
88
+ ```javascript
89
+ neekuro.SFW.clearCache('action', 'cuddle');
90
+ neekuro.SFW.clearCache('action');
91
+ neekuro.SFW.clearCache();
92
+ ```
93
+
94
+ El TTL se expresa en milisegundos y un valor de cero desactiva la caché:
95
+
96
+ ```javascript
97
+ neekuro.SFW.setCacheTTL(5 * 60 * 1000);
98
+ ```
99
+
100
+ Para pruebas o instalaciones propias de la API puede configurarse otra URL HTTPS:
101
+
102
+ ```javascript
103
+ neekuro.SFW.setBaseURL('https://api.example.com/api/sfw');
104
+ ```
105
+
106
+ #### Ejemplo de imagenes de bienvenida
73
107
  Las imagenes de bienvenida son otra de las alternativas que otorga NeeKuro, puedes hacer una imagen de la siguiente manera:
74
108
  ```javascript
75
109
  // obtenemos directamente la clase del paquete
@@ -83,13 +117,47 @@ const welcome = new Welcome()
83
117
  .setDescription('Al servidor de Discord');
84
118
 
85
119
  // Genera y guarda la imagen
86
- welcome.build()
120
+ welcome.build()
87
121
  .then(image => fs.writeFileSync('bienvenida.png', image))
88
122
  .then(() => console.log('Imagen creada correctamente!'))
89
123
  .catch(err => console.error('Error:', err.message));
90
124
 
91
125
  // nota: como antes, todo viene bien documentado en el paquete, solo sigue los pasos y explora por ti ;3
92
- ```
126
+ ```
127
+
128
+ Las imágenes remotas deben usar HTTPS. El paquete limita cada descarga a 10 MB,
129
+ aplica un tiempo máximo de 10 segundos, comprueba el formato y bloquea direcciones
130
+ locales o privadas. También puedes proporcionar un `Buffer` JPEG, PNG, GIF o WebP.
131
+
132
+ La resolución admite valores enteros entre 128 y 4096 píxeles. Para regresar al
133
+ tamaño original de 1140 × 520 puedes usar:
134
+
135
+ ```javascript
136
+ welcome.setResolution('default', 'default');
137
+ ```
138
+
139
+ Por defecto, el avatar, el título y la descripción forman una columna centrada
140
+ horizontal y verticalmente. El tamaño del avatar y de los textos se adapta cuando
141
+ la resolución es pequeña. Si necesitas coordenadas personalizadas puedes activar
142
+ el modo manual directamente o proporcionar `x`/`y` en cualquiera de los elementos:
143
+
144
+ ```javascript
145
+ welcome.setLayout('manual');
146
+
147
+ // Para regresar a la distribución automática:
148
+ welcome.setLayout('center');
149
+ ```
150
+
151
+ ## Desarrollo
152
+
153
+ ```sh
154
+ pnpm install
155
+ pnpm run check
156
+ pnpm run pack:check
157
+ ```
158
+
159
+ `pnpm run check` ejecuta las pruebas funcionales locales y comprueba las
160
+ declaraciones TypeScript. Ninguna de estas pruebas consulta la API remota.
93
161
  <hr>
94
162
  <center>
95
163
  <p>Resultado esperado</p>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "neekuro",
4
- "version": "2.0.4",
4
+ "version": "2.2.0",
5
5
  "description": "Una biblioteca de imágenes con temática de anime para bots de Discord.",
6
6
  "main": "sources/main/index.js",
7
7
  "types": "sources/typings/index.d.ts",
@@ -20,14 +20,15 @@
20
20
  "directories": {
21
21
  "test": "test"
22
22
  },
23
- "files": [
24
- "sources",
25
- "README.md"
26
- ],
23
+ "files": [
24
+ "sources",
25
+ "README.md",
26
+ "CHANGELOG.md"
27
+ ],
27
28
  "contributors": [
28
- "cacao0027 <kzmkuro@gmail.com>"
29
+ "cacao0027 <kmzkuro@gmail.com>"
29
30
  ],
30
- "author": "cacao0027 <kzmkuro@gmail.com>",
31
+ "author": "cacao0027 <kmzkuro@gmail.com>",
31
32
  "keywords": [
32
33
  "anime",
33
34
  "acción",
@@ -40,17 +41,22 @@
40
41
  "images"
41
42
  ],
42
43
  "license": "ISC",
44
+ "packageManager": "pnpm@11.23.0",
43
45
  "scripts": {
44
- "test": "nodemon node ./test/main.test.js"
46
+ "test": "node --test test/*.test.js",
47
+ "check": "node --check sources/main/index.js && node --test test/*.test.js && tsc -p test/tsconfig.json",
48
+ "pack:check": "npm pack --dry-run"
45
49
  },
46
50
  "engines": {
47
51
  "node": ">=20.12.2"
48
52
  },
49
53
  "dependencies": {
50
54
  "canvas": "^3.1.0",
51
- "hex-color-regex": "^1.1.0",
52
- "neekuro": "file:",
53
- "node-fetch": "^3.3.2"
55
+ "hex-color-regex": "^1.1.0"
56
+ },
57
+ "devDependencies": {
58
+ "@types/node": "^24.0.0",
59
+ "typescript": "^5.9.0"
54
60
  },
55
61
  "repository": {
56
62
  "type": "git",
@@ -1,45 +1,292 @@
1
- /**
2
- * @typedef {import('../typings').SfwCategories} SfwCategories
3
- * @typedef {import('../typings').GifMap} GifMap
4
- */
5
-
6
- const APIClient = require('../rest/APIClient.js');
7
- const NekoError = require('./errors/Error.js');
8
- const NekoGif = require('./package/NekoGif.js');
9
- const { getToken } = require('./user/BaseUser.js');
10
-
11
- // url base de la api para SFW
12
- const base = 'https://www.nexatdc.work.gd/api/sfw'
13
-
14
- /**
15
- * Clase SFW
16
- * No se necesita instanciar para acceder a sus metodos
17
- */
18
- class SFW {
19
- /**
20
- * Funcion principal para obtener gifs de cierta categoria
21
- * @template {SfwCategories} T
22
- * @param {T} cat corresponde a la subcategoria de SFW
23
- * @param {GifMap[T]} gif corresponde al gif que quieras obtener por categoria
24
- * @returns {Promise<NekoGif>}
25
- */
26
- static async getGif(cat, gif) {
27
- if (!cat) throw new NekoError('NoInput', 'el parametro <cat> es requerido');
28
- if (!gif) throw new NekoError('NoInput', 'el parametro <gif> es requerido');
29
-
30
- let solicitud = new APIClient(base, { token: getToken() }), response = null;
31
- switch (cat) {
32
- case 'action':
33
- response = solicitud.get('/action/' + gif + '?random=true')
34
- break;
35
- case 'reaction':
36
- response = solicitud.get('/reaction/' + gif + '?random=true')
37
- break;
38
- default:
39
- throw new NekoError('InvalidInput', 'la categoria que ingresas es invalida')
40
- }
41
- return new NekoGif(await response);
42
- }
43
- }
44
-
45
- module.exports = SFW
1
+ 'use strict';
2
+
3
+ /**
4
+ * @typedef {import('../typings').SfwCategories} SfwCategories
5
+ * @typedef {import('../typings').GifMap} GifMap
6
+ * @typedef {import('../typings').GifData} GifData
7
+ * @typedef {import('../typings').GifListResponse} GifListResponse
8
+ */
9
+
10
+ const APIClient = require('../rest/APIClient.js');
11
+ const NekoError = require('./errors/Error.js');
12
+ const NekoGif = require('./package/NekoGif.js');
13
+ const { getToken, getTokenVersion } = require('./user/BaseUser.js');
14
+
15
+ const DEFAULT_BASE_URL = 'https://www.nexatdc.work.gd/api/sfw';
16
+ const DEFAULT_CACHE_TTL = 5 * 60 * 1000;
17
+ const MAX_CACHE_ENTRIES = 100;
18
+
19
+ let baseURL = DEFAULT_BASE_URL;
20
+ let cacheTTL = DEFAULT_CACHE_TTL;
21
+ let globalCacheVersion = 0;
22
+
23
+ /** @type {Map<string, {items: GifData[], expiresAt: number, tokenVersion: number}>} */
24
+ const gifCache = new Map();
25
+ /** @type {Map<string, {promise: Promise<GifData[]>, tokenVersion: number}>} */
26
+ const gifRequests = new Map();
27
+ /** @type {Map<string, number>} */
28
+ const cacheVersions = new Map();
29
+ /** @type {Map<string, number>} */
30
+ const lastSelections = new Map();
31
+
32
+ /** Cliente para obtener imágenes SFW. */
33
+ class SFW {
34
+ /**
35
+ * Obtiene un GIF aleatorio. La lista puede provenir de caché, pero la
36
+ * selección se realiza nuevamente en cada llamada.
37
+ * @template {SfwCategories} T
38
+ * @param {T} category categoría principal
39
+ * @param {GifMap[T]} gif subcategoría solicitada
40
+ * @returns {Promise<NekoGif>}
41
+ */
42
+ static async getGif(category, gif) {
43
+ const key = getCacheKey(category, gif);
44
+ const items = await getGifData(category, gif, key);
45
+ const previousIndex = lastSelections.get(key);
46
+ const index = getRandomIndex(items.length, previousIndex);
47
+
48
+ lastSelections.set(key, index);
49
+ return createNekoGif(items[index]);
50
+ }
51
+
52
+ /**
53
+ * Obtiene todos los GIF disponibles para una subcategoría.
54
+ * Se devuelven instancias nuevas para impedir que el consumidor modifique
55
+ * accidentalmente los datos conservados en caché.
56
+ * @template {SfwCategories} T
57
+ * @param {T} category categoría principal
58
+ * @param {GifMap[T]} gif subcategoría solicitada
59
+ * @returns {Promise<NekoGif[]>}
60
+ */
61
+ static async getGifs(category, gif) {
62
+ const key = getCacheKey(category, gif);
63
+ const items = await getGifData(category, gif, key);
64
+ return items.map(createNekoGif);
65
+ }
66
+
67
+ /**
68
+ * Limpia toda la caché, una categoría principal o una subcategoría.
69
+ * @template {SfwCategories} T
70
+ * @param {T} [category]
71
+ * @param {GifMap[T]} [gif]
72
+ */
73
+ static clearCache(category, gif) {
74
+ if (category === undefined) {
75
+ globalCacheVersion += 1;
76
+ gifCache.clear();
77
+ gifRequests.clear();
78
+ cacheVersions.clear();
79
+ lastSelections.clear();
80
+ return;
81
+ }
82
+
83
+ validateCategory(category);
84
+ if (gif === undefined) {
85
+ const prefix = `${category}:`;
86
+ for (const key of collectKnownKeys(prefix)) clearCacheKey(key);
87
+ return;
88
+ }
89
+
90
+ clearCacheKey(getCacheKey(category, gif));
91
+ }
92
+
93
+ /**
94
+ * Cambia la URL base utilizada por SFW y vacía la caché anterior.
95
+ * @param {string} url
96
+ */
97
+ static setBaseURL(url) {
98
+ baseURL = validateBaseURL(url);
99
+ this.clearCache();
100
+ }
101
+
102
+ /**
103
+ * Configura la duración de la caché. Un valor de cero la desactiva.
104
+ * @param {number} milliseconds
105
+ */
106
+ static setCacheTTL(milliseconds) {
107
+ if (!Number.isFinite(milliseconds) || milliseconds < 0) {
108
+ throw new NekoError('InvalidCacheTTL', 'El tiempo de caché debe ser un número mayor o igual a cero');
109
+ }
110
+ cacheTTL = Math.floor(milliseconds);
111
+ this.clearCache();
112
+ }
113
+ }
114
+
115
+ /**
116
+ * @template {SfwCategories} T
117
+ * @param {T} category
118
+ * @param {GifMap[T]} gif
119
+ * @param {string} key
120
+ * @returns {Promise<GifData[]>}
121
+ */
122
+ async function getGifData(category, gif, key) {
123
+ const token = getToken();
124
+ if (!token) throw new NekoError('NoToken', 'Debes establecer un token antes de solicitar un GIF');
125
+ const currentTokenVersion = getTokenVersion();
126
+
127
+ const cached = gifCache.get(key);
128
+ if (
129
+ cached
130
+ && cached.tokenVersion === currentTokenVersion
131
+ && cached.expiresAt > Date.now()
132
+ ) return cached.items;
133
+ if (cached) gifCache.delete(key);
134
+
135
+ const pending = gifRequests.get(key);
136
+ if (pending?.tokenVersion === currentTokenVersion) return pending.promise;
137
+
138
+ const requestGlobalVersion = globalCacheVersion;
139
+ const requestKeyVersion = cacheVersions.get(key) ?? 0;
140
+ const request = requestGifList(category, gif, token)
141
+ .then(items => {
142
+ if (
143
+ cacheTTL > 0
144
+ && requestGlobalVersion === globalCacheVersion
145
+ && requestKeyVersion === (cacheVersions.get(key) ?? 0)
146
+ && currentTokenVersion === getTokenVersion()
147
+ ) {
148
+ storeCacheEntry(key, items, currentTokenVersion);
149
+ }
150
+ return items;
151
+ })
152
+ .finally(() => {
153
+ if (gifRequests.get(key)?.promise === request) gifRequests.delete(key);
154
+ });
155
+
156
+ gifRequests.set(key, { promise: request, tokenVersion: currentTokenVersion });
157
+ return request;
158
+ }
159
+
160
+ /**
161
+ * @template {SfwCategories} T
162
+ * @param {T} category
163
+ * @param {GifMap[T]} gif
164
+ * @param {string} token
165
+ * @returns {Promise<GifData[]>}
166
+ */
167
+ async function requestGifList(category, gif, token) {
168
+ const client = new APIClient(baseURL, { token });
169
+ const endpoint = `/${category}/${encodeURIComponent(String(gif))}`;
170
+ /** @type {GifListResponse} */
171
+ const response = await client.get(endpoint);
172
+
173
+ if (!Array.isArray(response?.data) || response.data.length === 0) {
174
+ throw new NekoError('EmptyResponse', `La API no devolvió GIF para ${category}/${gif}`);
175
+ }
176
+
177
+ return response.data.map((item, index) => normalizeGif(item, category, gif, index));
178
+ }
179
+
180
+ /**
181
+ * @param {unknown} item
182
+ * @param {string} category
183
+ * @param {string} gif
184
+ * @param {number} index
185
+ * @returns {GifData}
186
+ */
187
+ function normalizeGif(item, category, gif, index) {
188
+ if (!item || typeof item !== 'object' || typeof item.url !== 'string' || !item.url.trim()) {
189
+ throw new NekoError('InvalidResponse', `El GIF ${index} de ${category}/${gif} no contiene una URL válida`);
190
+ }
191
+
192
+ return Object.freeze({
193
+ url: item.url.trim(),
194
+ anime: typeof item.anime === 'string' && item.anime.trim()
195
+ ? item.anime.trim()
196
+ : 'Desconocido'
197
+ });
198
+ }
199
+
200
+ /** @param {GifData} item @returns {NekoGif} */
201
+ function createNekoGif(item) {
202
+ return new NekoGif({ code: 200, data: { ...item } });
203
+ }
204
+
205
+ /**
206
+ * Elige uniformemente entre todas las posiciones, excepto la selección
207
+ * anterior cuando existen al menos dos alternativas.
208
+ * @param {number} length
209
+ * @param {number|undefined} previousIndex
210
+ */
211
+ function getRandomIndex(length, previousIndex) {
212
+ if (length === 1) return 0;
213
+ if (previousIndex === undefined || previousIndex < 0 || previousIndex >= length) {
214
+ return Math.floor(Math.random() * length);
215
+ }
216
+
217
+ const index = Math.floor(Math.random() * (length - 1));
218
+ return index >= previousIndex ? index + 1 : index;
219
+ }
220
+
221
+ /**
222
+ * @template {SfwCategories} T
223
+ * @param {T} category
224
+ * @param {GifMap[T]} gif
225
+ */
226
+ function getCacheKey(category, gif) {
227
+ validateCategory(category);
228
+ if (typeof gif !== 'string' || !gif.trim()) {
229
+ throw new NekoError('NoInput', 'el parámetro <gif> es requerido');
230
+ }
231
+ return `${category}:${gif}`;
232
+ }
233
+
234
+ /** @param {unknown} category */
235
+ function validateCategory(category) {
236
+ if (category === undefined || category === null || category === '') {
237
+ throw new NekoError('NoInput', 'el parámetro <category> es requerido');
238
+ }
239
+ if (category !== 'action' && category !== 'reaction') {
240
+ throw new NekoError('InvalidInput', 'la categoría que ingresas es inválida');
241
+ }
242
+ }
243
+
244
+ /** @param {string} url */
245
+ function validateBaseURL(url) {
246
+ if (typeof url !== 'string' || !url.trim()) {
247
+ throw new NekoError('InvalidBaseURL', 'La URL base debe ser una cadena HTTPS válida');
248
+ }
249
+
250
+ let parsed;
251
+ try {
252
+ parsed = new URL(url);
253
+ } catch {
254
+ throw new NekoError('InvalidBaseURL', 'La URL base debe ser una cadena HTTPS válida');
255
+ }
256
+ if (parsed.protocol !== 'https:' || parsed.username || parsed.password) {
257
+ throw new NekoError('InvalidBaseURL', 'La URL base debe utilizar HTTPS y no incluir credenciales');
258
+ }
259
+ return url.trim().replace(/\/$/, '');
260
+ }
261
+
262
+ /** @param {string} key @param {GifData[]} items @param {number} tokenVersion */
263
+ function storeCacheEntry(key, items, tokenVersion) {
264
+ if (!gifCache.has(key) && gifCache.size >= MAX_CACHE_ENTRIES) {
265
+ const oldest = gifCache.keys().next().value;
266
+ if (oldest) {
267
+ gifCache.delete(oldest);
268
+ lastSelections.delete(oldest);
269
+ }
270
+ }
271
+ gifCache.set(key, { items, expiresAt: Date.now() + cacheTTL, tokenVersion });
272
+ }
273
+
274
+ /** @param {string} key */
275
+ function clearCacheKey(key) {
276
+ gifCache.delete(key);
277
+ gifRequests.delete(key);
278
+ lastSelections.delete(key);
279
+ cacheVersions.set(key, (cacheVersions.get(key) ?? 0) + 1);
280
+ }
281
+
282
+ /** @param {string} prefix */
283
+ function collectKnownKeys(prefix) {
284
+ return new Set([
285
+ ...[...gifCache.keys()].filter(key => key.startsWith(prefix)),
286
+ ...[...gifRequests.keys()].filter(key => key.startsWith(prefix)),
287
+ ...[...lastSelections.keys()].filter(key => key.startsWith(prefix)),
288
+ ...[...cacheVersions.keys()].filter(key => key.startsWith(prefix))
289
+ ]);
290
+ }
291
+
292
+ module.exports = SFW;
@@ -2,7 +2,7 @@
2
2
 
3
3
  /**
4
4
  * @typedef {import('../../typings').ApiResponse} ApiResponse
5
- * @typedef {import('node-fetch').Response} Response
5
+ * @typedef {{url?: string, status?: number}} ResponseLike
6
6
  */
7
7
 
8
8
  /**
@@ -14,15 +14,13 @@ class APIError extends Error {
14
14
  /**
15
15
  * Crea la instancia del error
16
16
  * @param {string} endpoint la ruta a la que se realizo la peticion
17
- * @param {Response} response respuesta de node-fetch
17
+ * @param {ResponseLike} response respuesta HTTP o representación de un error de red
18
18
  * @param {ApiResponse} data respuesta de la api
19
19
  */
20
- constructor(endpoint, response, data) {
21
- super();
20
+ constructor(endpoint, response, data) {
21
+ super(APIError.determineMessage(data));
22
22
 
23
23
  this.name = `NekoREST Error [${endpoint}]`
24
- this.message = this.#determineMessage(data);
25
-
26
24
  /**
27
25
  * @type {string}
28
26
  */
@@ -31,7 +29,7 @@ class APIError extends Error {
31
29
  /**
32
30
  * @type {number}
33
31
  */
34
- this.statusCode = parseInt(response?.status ?? 500);
32
+ this.statusCode = Number.parseInt(response?.status ?? 500, 10);
35
33
 
36
34
  /**
37
35
  * @type {ApiResponse}
@@ -45,7 +43,7 @@ class APIError extends Error {
45
43
  * @param {ApiResponse} response respuesta de la api
46
44
  * @returns {string} mensaje de error
47
45
  */
48
- #determineMessage(response) {
46
+ static determineMessage(response = {}) {
49
47
  if(response.code === 403) {
50
48
  return 'TOKEN ERROR: el token que haz proporcionado es invalido'
51
49
  } else
@@ -54,7 +52,11 @@ class APIError extends Error {
54
52
  } else {
55
53
  return response.message ?? 'descripcion no dada'
56
54
  }
57
- }
58
- }
59
-
60
- module.exports = APIError
55
+ }
56
+
57
+ static fromNetworkError(endpoint, url, message) {
58
+ return new APIError(endpoint, { url, status: 503 }, { code: 503, message });
59
+ }
60
+ }
61
+
62
+ module.exports = APIError
@@ -10,16 +10,17 @@ class NekoError extends Error {
10
10
  * @param {string} name Corresponde al nombre del error
11
11
  * @param {string} message Mensaje de error
12
12
  */
13
- constructor(name, message) {
14
- super(message);
13
+ constructor(name, message) {
14
+ super(message);
15
15
 
16
16
  this.name = `NeeKuro Error [${name}]`
17
17
 
18
18
  /**
19
19
  * @type {string}
20
20
  */
21
- this.message = message
21
+ this.message = message
22
+ Error.captureStackTrace?.(this, NekoError);
22
23
  }
23
24
  }
24
25
 
25
- module.exports = NekoError
26
+ module.exports = NekoError
@@ -14,19 +14,22 @@ class NekoGif {
14
14
  * Recibe la respuesta de la api
15
15
  * @param {GifResponse} response respuesta de la api
16
16
  */
17
- constructor(response) {
17
+ constructor(response) {
18
+ if (!response || typeof response !== 'object' || !response.data) {
19
+ throw new TypeError('La respuesta no contiene datos de imagen');
20
+ }
18
21
 
19
22
  /**
20
23
  * URL del gif
21
24
  * @type {string|null}
22
25
  */
23
- this.url = response.data.url || null
26
+ this.url = typeof response.data.url === 'string' ? response.data.url : null
24
27
 
25
28
  /**
26
29
  * Anime del gif
27
30
  * @type {string|null}
28
31
  */
29
- this.anime = response.data.anime || null
32
+ this.anime = typeof response.data.anime === 'string' ? response.data.anime : null
30
33
  }
31
34
 
32
35
  /**
@@ -47,4 +50,4 @@ class NekoGif {
47
50
 
48
51
  }
49
52
 
50
- module.exports = NekoGif
53
+ module.exports = NekoGif