aboutsummaryrefslogtreecommitdiffstats
path: root/src/rpc.js
diff options
context:
space:
mode:
authorElvis Claros Castro <elvis@claros.ar>2026-09-26 19:26:33 -0300
committerElvis Claros Castro <elvis@claros.ar>2026-09-26 19:26:33 -0300
commitd80640d01eeb84907e36ff4af33add9081eda17c (patch)
tree27698b34e06e28d111c2196700015d9c82f78274 /src/rpc.js
downloadwa-cli-main.tar.gz
wa-cli-main.zip
wa-cli: WhatsApp from the terminal, with a neomutt Maildir bridgeHEADmain
Diffstat (limited to 'src/rpc.js')
-rw-r--r--src/rpc.js83
1 files changed, 83 insertions, 0 deletions
diff --git a/src/rpc.js b/src/rpc.js
new file mode 100644
index 0000000..d8502e9
--- /dev/null
+++ b/src/rpc.js
@@ -0,0 +1,83 @@
+// CLI <-> daemon protocol: one JSON object per line over a Unix socket.
+//
+// -> {"id":1,"method":"messages.list","params":{...}}
+// <- {"id":1,"result":...} normal reply
+// <- {"id":1,"error":{"code":"EAUTH","message":"..."}}
+// <- {"id":1,"event":{...}} stream methods (events.watch), until closed
+//
+// Simple on purpose: any language can speak it (e.g. a Python bridge with
+// socket.AF_UNIX) without going through the `wa` binary.
+
+import net from 'node:net';
+
+export class WaError extends Error {
+ constructor(code, message, data) {
+ super(message);
+ this.code = code;
+ if (data !== undefined) this.data = data;
+ }
+}
+
+export function onLines(socket, fn) {
+ let buf = '';
+ socket.setEncoding('utf8');
+ socket.on('data', (chunk) => {
+ buf += chunk;
+ let i;
+ while ((i = buf.indexOf('\n')) >= 0) {
+ const line = buf.slice(0, i);
+ buf = buf.slice(i + 1);
+ if (line.trim()) fn(line);
+ }
+ });
+}
+
+export function send(socket, obj) {
+ if (!socket.destroyed && socket.writable) socket.write(JSON.stringify(obj) + '\n');
+}
+
+// Calls a method. With onEvent(event, close), it keeps listening for events until
+// either side closes.
+export function call(socketPath, method, params = {}, { onEvent } = {}) {
+ return new Promise((resolve, reject) => {
+ const sock = net.createConnection(socketPath);
+ let settled = false;
+ const done = (fn, v) => {
+ if (settled) return;
+ settled = true;
+ fn(v);
+ };
+ sock.on('connect', () => send(sock, { id: 1, method, params }));
+ onLines(sock, (line) => {
+ let msg;
+ try {
+ msg = JSON.parse(line);
+ } catch {
+ return done(reject, new WaError('EINTERNAL', `invalid reply from the daemon: ${line.slice(0, 200)}`));
+ }
+ if (msg.event !== undefined) {
+ onEvent?.(msg.event, () => sock.end());
+ } else if (msg.error) {
+ done(reject, new WaError(msg.error.code, msg.error.message, msg.error.data));
+ sock.end();
+ } else {
+ done(resolve, msg.result);
+ sock.end();
+ }
+ });
+ sock.on('error', (e) => done(reject, e));
+ sock.on('close', () => done(onEvent ? resolve : reject,
+ onEvent ? undefined : new WaError('EDAEMON', 'the daemon closed the connection')));
+ });
+}
+
+export function ping(socketPath) {
+ return new Promise((resolve) => {
+ const sock = net.createConnection(socketPath);
+ sock.on('connect', () => {
+ sock.destroy();
+ resolve(true);
+ });
+ sock.on('error', () => resolve(false));
+ });
+}