aboutsummaryrefslogtreecommitdiffstats
path: root/src/api/index.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/api/index.js')
-rw-r--r--src/api/index.js111
1 files changed, 111 insertions, 0 deletions
diff --git a/src/api/index.js b/src/api/index.js
new file mode 100644
index 0000000..3fdf430
--- /dev/null
+++ b/src/api/index.js
@@ -0,0 +1,111 @@
+// 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 <resource> <method>`.
+
+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;
+}