@bck-inc/nsl-core
Version:
SDK officiel pour l'API NSL (Néon Spinellia LuckyScale) - 100 % fetch natif
316 lines (315 loc) • 13.9 kB
JavaScript
;
// src/NSLCore.ts – SDK officiel NSL (TypeScript)
// ==================================================
// NSL = Néon Spinellia LuckyScale
// --------------------------------------------------
// • fetch natif : Node ≥ 18 (polyfill node‑fetch@3 possible sur 16).
// • Couche transport / auth / log — cache TTL + rate‑limit intégrés.
// • Évènements : request · response · cacheHit · validationError ·
// moduleStateChanged · ping · error.
// --------------------------------------------------
// 2024‑##‑## – Auteur : DragClover <dragclover@gmail.com>
Object.defineProperty(exports, "__esModule", { value: true });
exports.NSLCore = void 0;
const types_1 = require("./types");
const utils_1 = require("./utils");
const guard_1 = require("./guard");
const node_events_1 = require("node:events");
const errors_1 = require("./errors");
/***************************************************************************
* Classe : NSLCore
* -------------------------------------------------------------------------
* Expose un SDK minimaliste pour consommer l’API NSL depuis n’importe quel
* bot ou service Node.js. Toutes les méthodes retournent une Promesse et
* propagent les erreurs réseau/HTTP sous forme d’`Error` JavaScript.
*
* ⚠️ Comprend un cache mémoire TTL ; aucun back‑off ni suivi de quota
* n’est implémenté. Gérez ces aspects en amont si nécessaire.
***************************************************************************/
class NSLCore extends node_events_1.EventEmitter {
/* ----------------------------------------------------------------------- */
/* CONSTRUCTOR */
/* ----------------------------------------------------------------------- */
constructor({ token, botId, baseURL = 'https://api.neon-inc.fr/api/nsl', debug = false }) {
super();
this.cache = new Map();
/** Durée du cache mémoire (ms) pour les requêtes GET). */
this.defaultTTL = 1500; // 60 s
this.token = token;
this.botId = botId;
// Supprime un éventuel `/` final pour éviter les doubles slash.
this.baseURL = baseURL.replace(/\/$/, '');
this.debug = debug;
}
on(event, listener) {
return super.on(event, listener);
}
once(event, listener) {
return super.once(event, listener);
}
emit(event, ...args) {
return super.emit(event, ...args);
}
/* ----------------------------------------------------------------------- */
/* Helper interne : reject après N ms */
/* ----------------------------------------------------------------------- */
timeout(ms) {
return new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), ms));
}
/**
* Mesure la latence RTT de l’API NSL puis émet l’évènement {@link NSLCoreEvents.ping ping}.
*
* @param timeout Timeout max (ms) avant de renvoyer ‑1. 2000 par défaut.
* @returns RTT en millisecondes ou ‑1 si timeout/erreur.
*/
async getPing(timeout = 2000) {
const start = Date.now();
let rtt = -1;
try {
await Promise.race([
this.request('ping', 'GET'),
this.timeout(timeout)
]);
rtt = Date.now() - start;
}
finally {
this.emit('ping', { rtt });
}
return rtt;
}
/* ----------------------------------------------------------------------- */
/* PUBLIC API */
/* ----------------------------------------------------------------------- */
/**
* GET `/modules/get`.
* Les réponses sont automatiquement mises en cache pendant `defaultTTL`.
*
* @param query Filtres bot_id / server_id / module_name …
* @return Objet {@link NSLData} typé ou erreur si la validation échoue
* (évènement `validationError` émis).
*/
async fetchModules(query) {
const url = new URL(`${this.baseURL}/modules/get`);
Object.entries(query).forEach(([k, v]) => {
if (v !== undefined && v !== null)
url.searchParams.append(k, String(v));
});
const res = await this.request(url.toString(), 'GET', undefined, guard_1.assertIsNSLData);
// On vérifie seulement si le backend a répondu VALIDE
if (res.rep.code === types_1.NSLCodes.VALID) {
(0, guard_1.assertIsModuleArray)(res.data);
}
return res;
}
/**
* Insère un nouveau module : POST `/modules/insert`.
* @returns Promesse d’un objet **{@link NSLData<T\>}**.
*/
insertModule(body) {
return this.request('modules/insert', 'POST', body);
}
/**
* Met à jour un module existant : PATCH `/modules/update`.
* @returns Promesse d’un objet **{@link NSLData<T\>}**.
*/
updateModule(body) {
return this.request('modules/update', 'PATCH', body);
}
/**
* Active ou désactive un module.
* Émet toujours `moduleStateChanged` quand l’opération est validée (INSERT/UPDATE).
* @param params Objet métier décrivant le module ciblé.
* @param more { debug?:0\|1, log?:function } – contrôle du verbosité/log.
* @returns Promesse d’un objet **{@link NSLSetModuleStateReturn}**.
*/
async setModuleState(params, more = {}) {
const { newState, guild, module_name, bot_id, server_id, coexist, color, description } = params;
const { debug, log } = more;
const logFn = more.log ?? (typeof utils_1.co === 'function' ? utils_1.co : console.log);
const dbg = more.debug ?? (this.debug ? 0 : 1);
// Recherche pré‑existante.
const res = await this.fetchModules({ bot_id, server_id, module_name, coexist, description, color });
const r = {};
if (!res.data && res.rep.code === types_1.NSLCodes.NOT_FOUND) {
// Pas trouvé → insertion.
if (debug === 0)
logFn(`§6[§4NSLCore§6] ℹ️ §l§bModule §1§o${guild ? `${guild.id}:${module_name}` : module_name}§r §l§bnon trouvé, insertion...`, "debug");
await this.insertModule({ bot_id, module_name, server_id, coexist, color, description, state: newState }).then((e) => {
if (e.rep.code === types_1.NSLCodes.VALID) {
if (debug === 0)
logFn(" §e╚> ✅ §l§aInsertion réussie.", "debug");
this.emit('moduleStateChanged', {
module: module_name,
type: 'INSERT', // ou 'UPDATE' selon le bloc courant
newState
});
r.module_name = `§a${module_name}`;
r.type = "INSERT";
r.code = e.rep.code;
r.message = e.rep.message;
}
else {
if (debug === 0)
logFn(` §e╚> ❌ §l§cInsertion échoué.§r §e§o(${e.rep.message})`, "debug");
r.module_name = `§c${module_name}`;
r.type = "INSERT";
r.code = e.rep.code;
r.message = e.rep.message;
}
});
}
else {
// Trouvé → mise à jour de l’état.
if (debug === 0)
logFn(`§6[§4NSLCore§6] ℹ️ §l§bModule §1§o${guild ? `${guild.id}:${module_name}` : module_name}§r §l§btrouvé, mise à jour de l'état...`, "debug");
await this.updateModule({ bot_id, server_id, module_name, fields: { state: newState, coexist, color, description } }).then((e) => {
if (e.rep.code === types_1.NSLCodes.VALID) {
if (debug === 0)
logFn(" §e╚> ✅ §l§aMise à jour réussie.", "debug");
this.emit('moduleStateChanged', {
module: module_name,
type: 'UPDATE', // ou 'UPDATE' selon le bloc courant
newState
});
r.module_name = `§a${module_name}`;
r.type = "UPDATE";
r.code = e.rep.code;
r.message = e.rep.message;
}
else {
if (debug === 0)
logFn(" §e╚> ❌ §l§cMise à jour échoué.", "debug");
r.module_name = `§c${module_name}`;
r.type = "UPDATE";
r.code = e.rep.code;
r.message = e.rep.message;
}
});
}
return r;
}
/**
* Vérifie si un module est « standardisé » côté NSL.
* Retourne **true** uniquement quand :
* • l’endpoint répond 200 (code VALID) et
* • la propriété `data.isStandard` vaut `true`.
*/
async isStandardized(module_name) {
const url = new URL(`${this.baseURL}/modules/is-standardized`);
url.searchParams.set('module_name', module_name);
const res = await this.request(url.toString(), 'GET');
return (res?.rep?.code === types_1.NSLCodes.VALID &&
res?.data?.isStandard === true);
}
/**
* Active un module NSL ― simple sucrage syntaxique autour de
* {@link setModuleState setModuleState()} avec `newState = NSLState.enabled`.
*
* @param p - Identifiants du module à modifier (bot_id, server_id, module_name…).
* Tous les champs de {@link NSLSetModuleStateParams} sauf `newState`.
* @returns La même structure de réponse que {@link setModuleState}.
*
* @example
* ```ts
* await core.enableModule({
* bot_id: client.user!.id,
* server_id: guild.id,
* module_name:"welcome"
* });
* ```
*/
async enableModule(p) {
return this.setModuleState({ ...p, newState: types_1.NSLState.enabled });
}
/**
* Désactive un module NSL — équivalent de
* {@link setModuleState setModuleState()} avec `newState = NSLState.disabled`.
*
* @param p - Mêmes paramètres que {@link enableModule} (sans `newState`).
* @returns L’objet renvoyé par {@link setModuleState}.
*
* @example
* ```ts
* await core.disableModule({
* bot_id: client.user!.id,
* server_id: guild.id,
* module_name:"music"
* });
* ```
*/
async disableModule(p) {
return this.setModuleState({ ...p, newState: types_1.NSLState.disabled });
}
/* ----------------------------------------------------------------------- */
/* PRIVATE HELPERS */
/* ----------------------------------------------------------------------- */
/**
* Point d’entrée unique pour toutes les requêtes HTTP NSL.
*
* Workflow :
* 1. Vérifie cache mémoire → émet `cacheHit` si trouvé.
* 2. Fait le fetch et émet :
* • `request` juste avant,
* • `response` à la réception des headers.
* 3. Valide le JSON : si échec → `validationError`.
* 4. Stocke dans le cache, retourne la donnée typée.
*
* Toute exception est relayée via `error`.
*
* @internal
*/
async request(endpoint, method = 'GET', body, validate) {
if (endpoint !== "ping" && !this.token)
throw new Error('NSLCore ➜ token manquant');
if (endpoint !== "ping" && !this.botId)
throw new Error('NSLCore ➜ botId manquant (header nsl-bot-id)');
// 1) Résolution de l’URL complète.
const url = String(endpoint).startsWith('http')
? String(endpoint)
: `${this.baseURL}/${String(endpoint).replace(/^\//, '')}`;
// 2) Prépare l’init fetch.
const init = {
method,
headers: {
Authorization: `Bearer ${this.token}`,
'nsl-bot-id': this.botId,
...(body ? { 'Content-Type': 'application/json' } : {})
},
body: body ? JSON.stringify(body) : undefined
};
try {
this.emit('request', { url, method });
const cacheKey = `${method}:${url}:${body ? JSON.stringify(body) : ''}`;
const cached = this.cache.get(cacheKey);
if (cached && cached.expires > Date.now()) {
this.emit('cacheHit', { url, ttlLeft: cached.expires - Date.now() });
return cached.value;
}
const res = await fetch(url, init);
this.emit('response', { url, status: res.status });
if (!res.ok)
new errors_1.NSLHTTPError(res.status);
const json = await res.json();
try {
// @ts-ignore
if (validate)
validate(json);
}
catch (ve) {
const issues = ve?.message ? [ve.message] : ['validation failed'];
this.emit('validationError', { url, issues });
throw ve;
}
this.cache.set(cacheKey, { expires: Date.now() + this.defaultTTL, value: json });
// 4) Debug facultatif.
if (this.debug)
(0, utils_1.co)(`§6[§4NSLCore§6] ℹ️ §l§a${method} ? modules : §6${JSON.stringify(json)}`, "debug");
return json;
}
catch (err) {
this.emit('error', err);
throw err;
}
}
}
exports.NSLCore = NSLCore;