1
2
3
4
5
6
7
8
9
10
11
12
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
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;
}
|