UNPKG

@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
"use strict"; // 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;