// Registry of `resource.method` methods. // // Each module exports an object { 'resource.method': definition }. A definition: // description text for `wa schema` / --help // params { name: { type, required?, default?, description, enum? } } // type: string | integer | number | boolean | array | object | path | chat // path -> the CLI resolves it to an absolute path (relative to the CLI cwd) // chat -> the daemon resolves it to a chatId (JID, number or exact name) // columns default columns for --format table/csv // ready false if it works without a ready session (default true) // stream true if it emits events until the client disconnects // handler async (ctx, params, conn) => result (only used by the daemon) // // Adding a method only takes adding it here: the daemon exposes it on the socket and // the CLI lists it in the help and accepts it as `wa `. import path from 'node:path'; import { WaError } from '../rpc.js'; import chats from './chats.js'; import contacts from './contacts.js'; import messages from './messages.js'; import session from './session.js'; export const METHODS = { ...session, ...chats, ...messages, ...contacts }; export function resources() { const out = {}; for (const [name, def] of Object.entries(METHODS)) { const [res, meth] = name.split('.'); (out[res] ||= {})[meth] = def; } return out; } export function getMethod(name) { const def = METHODS[name]; if (!def) { throw new WaError('EINVAL', `unknown method: ${name} (see \`wa schema\`)`); } return def; } // Serializable description (no handler), for `wa schema`. export function schema(name) { const { handler, ...rest } = getMethod(name); return { method: name, ready: rest.ready !== false, ...rest }; } // Spanish yes (si/sí) is accepted as well. const TRUE = new Set(['true', '1', 'yes', 'si', 'sí']); const FALSE = new Set(['false', '0', 'no']); // Validates and normalizes parameters. `cwd` is used to resolve path-typed ones. export function validate(name, params = {}, { cwd } = {}) { const def = getMethod(name); const spec = def.params || {}; if (params === null || typeof params !== 'object' || Array.isArray(params)) { throw new WaError('EINVAL', `${name}: --params must be a JSON object`); } const out = {}; for (const key of Object.keys(params)) { if (!spec[key]) { const valid = Object.keys(spec); throw new WaError('EINVAL', `${name}: unknown parameter "${key}"` + (valid.length ? ` (valid: ${valid.join(', ')})` : ' (takes no parameters)')); } } for (const [key, p] of Object.entries(spec)) { let v = params[key]; if (v === undefined || v === null) { if (p.required) throw new WaError('EINVAL', `${name}: missing parameter "${key}"`); if (p.default !== undefined) out[key] = p.default; continue; } const bad = () => new WaError('EINVAL', `${name}: "${key}" must be ${p.type}`); switch (p.type) { case 'string': case 'chat': if (typeof v === 'number') v = String(v); if (typeof v !== 'string' || v === '') throw bad(); break; case 'path': if (typeof v !== 'string' || v === '') throw bad(); if (cwd) v = path.resolve(cwd, v.replace(/^~(?=$|\/)/, process.env.HOME)); break; case 'integer': case 'number': if (typeof v === 'string' && v.trim() !== '') v = Number(v); if (typeof v !== 'number' || Number.isNaN(v)) throw bad(); if (p.type === 'integer' && !Number.isInteger(v)) throw bad(); break; case 'boolean': if (typeof v === 'string' && TRUE.has(v.toLowerCase())) v = true; else if (typeof v === 'string' && FALSE.has(v.toLowerCase())) v = false; if (typeof v !== 'boolean') throw bad(); break; case 'array': if (typeof v === 'string') v = v.split(',').map((s) => s.trim()).filter(Boolean); if (!Array.isArray(v)) throw bad(); break; case 'object': if (typeof v !== 'object' || Array.isArray(v)) throw bad(); break; } if (p.enum && !p.enum.includes(v)) { throw new WaError('EINVAL', `${name}: "${key}" must be one of: ${p.enum.join(', ')}`); } out[key] = v; } return out; }