neekuro 2.1.0 → 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 +38 -4
- package/package.json +6 -5
- package/sources/classes/SFW.js +292 -47
- package/sources/classes/user/BaseUser.js +17 -6
- package/sources/rest/APIClient.js +3 -2
- package/sources/typings/index.d.ts +34 -15
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
|
-
|
|
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
|
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.
|
|
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,10 +20,11 @@
|
|
|
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
29
|
"cacao0027 <kmzkuro@gmail.com>"
|
|
29
30
|
],
|
package/sources/classes/SFW.js
CHANGED
|
@@ -1,47 +1,292 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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;
|
|
@@ -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
|
|
@@ -15,16 +16,26 @@ class BaseUser {
|
|
|
15
16
|
if (typeof token !== 'string' || !token.trim()) {
|
|
16
17
|
throw new TypeError('El token debe ser una cadena no vacía');
|
|
17
18
|
}
|
|
18
|
-
|
|
19
|
+
const normalizedToken = token.trim();
|
|
20
|
+
if (tokenGlobal !== normalizedToken) tokenVersion += 1;
|
|
21
|
+
tokenGlobal = normalizedToken;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
/**
|
|
22
25
|
* Obten el token del usuario si este ya se estableció
|
|
23
26
|
* @returns {string} Token de usuario
|
|
24
27
|
*/
|
|
25
|
-
static getToken() {
|
|
26
|
-
return tokenGlobal;
|
|
27
|
-
}
|
|
28
|
-
|
|
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
|
+
}
|
|
29
40
|
|
|
30
41
|
module.exports = BaseUser;
|
|
@@ -51,7 +51,8 @@ class APIClient {
|
|
|
51
51
|
/**
|
|
52
52
|
* Obtiene el objeto compartido por la api a traves de la ruta y con autorizacion por token
|
|
53
53
|
* @param {string} endpoint establece la ruta a la que se quiere comunicar
|
|
54
|
-
* @
|
|
54
|
+
* @template {import('../typings').ApiResponse} [T=GifResponse]
|
|
55
|
+
* @returns {Promise<T>}
|
|
55
56
|
* @throws {APIError} solo si sucede un error entre solicitudes
|
|
56
57
|
*/
|
|
57
58
|
async get(endpoint, options = {}) {
|
|
@@ -80,7 +81,7 @@ class APIClient {
|
|
|
80
81
|
throw new APIError(endpoint, response, data);
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
return data
|
|
84
|
+
return /** @type {T} */ (data)
|
|
84
85
|
} catch (error) {
|
|
85
86
|
if (error instanceof APIError) {
|
|
86
87
|
throw error;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* // obtiene el objeto SFW
|
|
8
8
|
* const SFW = require('neekuro').SFW
|
|
9
9
|
*/
|
|
10
|
-
export const SFW: {
|
|
10
|
+
export const SFW: {
|
|
11
11
|
/**
|
|
12
12
|
* Funcion principal para obtener gifs de cierta categoria
|
|
13
13
|
* @param category category: corresponde a la subcategoria de SFW
|
|
@@ -23,8 +23,18 @@ export const SFW: {
|
|
|
23
23
|
* // obtener anime
|
|
24
24
|
* cuddle.getAnime();
|
|
25
25
|
*/
|
|
26
|
-
getGif<T extends SfwCategories>(category: T, gif: GifMap[T]): Promise<NekoGif>
|
|
27
|
-
|
|
26
|
+
getGif<T extends SfwCategories>(category: T, gif: GifMap[T]): Promise<NekoGif>
|
|
27
|
+
/** Obtiene todos los GIF disponibles y utiliza la caché interna. */
|
|
28
|
+
getGifs<T extends SfwCategories>(category: T, gif: GifMap[T]): Promise<NekoGif[]>
|
|
29
|
+
/** Limpia toda la caché de imágenes. */
|
|
30
|
+
clearCache(): void
|
|
31
|
+
/** Limpia la caché de una categoría completa o de una subcategoría. */
|
|
32
|
+
clearCache<T extends SfwCategories>(category: T, gif?: GifMap[T]): void
|
|
33
|
+
/** Cambia la URL HTTPS utilizada para consultar SFW. */
|
|
34
|
+
setBaseURL(url: string): void
|
|
35
|
+
/** Configura el TTL en milisegundos. El valor cero desactiva la caché. */
|
|
36
|
+
setCacheTTL(milliseconds: number): void
|
|
37
|
+
}
|
|
28
38
|
|
|
29
39
|
//#endregion
|
|
30
40
|
|
|
@@ -136,10 +146,15 @@ export interface WelcomeData {
|
|
|
136
146
|
};
|
|
137
147
|
}
|
|
138
148
|
|
|
139
|
-
export interface NekoGifInterface {
|
|
140
|
-
url: string | null
|
|
141
|
-
anime?: string | null
|
|
142
|
-
}
|
|
149
|
+
export interface NekoGifInterface {
|
|
150
|
+
url: string | null
|
|
151
|
+
anime?: string | null
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface GifData {
|
|
155
|
+
url: string
|
|
156
|
+
anime: string
|
|
157
|
+
}
|
|
143
158
|
|
|
144
159
|
export declare interface GifMap {
|
|
145
160
|
action: ActionGifs
|
|
@@ -147,11 +162,12 @@ export declare interface GifMap {
|
|
|
147
162
|
}
|
|
148
163
|
|
|
149
164
|
export interface GifResponse extends ApiResponse {
|
|
150
|
-
data:
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
165
|
+
data: GifData
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface GifListResponse extends ApiResponse {
|
|
169
|
+
data: GifData[]
|
|
170
|
+
}
|
|
155
171
|
|
|
156
172
|
export interface ApiResponse {
|
|
157
173
|
message?: string,
|
|
@@ -303,7 +319,10 @@ export class BaseUser {
|
|
|
303
319
|
* Obten el token del usuario si este ya se estableció
|
|
304
320
|
* @returns {string} Token de usuario
|
|
305
321
|
*/
|
|
306
|
-
public static getToken(): string
|
|
322
|
+
public static getToken(): string | null
|
|
323
|
+
|
|
324
|
+
/** Versión interna que cambia cuando se establece un token diferente. */
|
|
325
|
+
public static getTokenVersion(): number
|
|
307
326
|
|
|
308
327
|
/**
|
|
309
328
|
* Establece el token "global" para todo el paquete
|
|
@@ -330,8 +349,8 @@ export class APIClient {
|
|
|
330
349
|
* @param endpoint establece la ruta a la que se quiere comunicar en la api
|
|
331
350
|
* @throws {APIError} solo si sucede un error entre solicitudes
|
|
332
351
|
*/
|
|
333
|
-
public get(endpoint: string, options?: { timeout?: number }): Promise<
|
|
334
|
-
}
|
|
352
|
+
public get<T extends ApiResponse = GifResponse>(endpoint: string, options?: { timeout?: number }): Promise<T>
|
|
353
|
+
}
|
|
335
354
|
|
|
336
355
|
/**
|
|
337
356
|
* Clase APIError
|