aboutsummaryrefslogtreecommitdiffstats
path: root/src/session.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/session.js
downloadwa-cli-d80640d01eeb84907e36ff4af33add9081eda17c.tar.gz
wa-cli-d80640d01eeb84907e36ff4af33add9081eda17c.zip
wa-cli: WhatsApp from the terminal, with a neomutt Maildir bridgeHEADmain
Diffstat (limited to 'src/session.js')
-rw-r--r--src/session.js69
1 files changed, 69 insertions, 0 deletions
diff --git a/src/session.js b/src/session.js
new file mode 100644
index 0000000..9197636
--- /dev/null
+++ b/src/session.js
@@ -0,0 +1,69 @@
+// CLI side: find (or start) the account daemon and talk to it.
+
+import { spawn } from 'node:child_process';
+import fs from 'node:fs';
+import { fileURLToPath } from 'node:url';
+import { loadConfig, paths } from './config.js';
+import { WaError, call, ping } from './rpc.js';
+
+const BIN = fileURLToPath(new URL('../bin/wa.js', import.meta.url));
+const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
+
+export class Session {
+ constructor(account, { autostart } = {}) {
+ this.account = account;
+ this.paths = paths(account);
+ this.config = loadConfig(account);
+ this.autostart = autostart ?? this.config.autostart;
+ }
+
+ running() {
+ return ping(this.paths.socket);
+ }
+
+ async call(method, params = {}, opts = {}) {
+ await this.ensureDaemon();
+ try {
+ return await call(this.paths.socket, method, params, opts);
+ } catch (e) {
+ if (e.syscall === 'connect') throw new WaError('EDAEMON', `could not connect to the daemon (${e.code})`);
+ throw e;
+ }
+ }
+
+ async ensureDaemon() {
+ if (await this.running()) return;
+ if (!this.autostart) {
+ throw new WaError('EDAEMON', `the daemon for account "${this.account}" is not running: ` +
+ `\`wa daemon start\` or \`systemctl --user start wa-daemon@${this.account}\``);
+ }
+ await this.startBackground();
+ }
+
+ async startBackground() {
+ const P = this.paths;
+ fs.mkdirSync(P.stateDir, { recursive: true, mode: 0o700 });
+ const logFd = fs.openSync(P.logFile, 'a');
+ if (process.stderr.isTTY) process.stderr.write(`wa: starting the daemon (account ${this.account}), log in ${P.logFile}\n`);
+ const child = spawn(process.execPath, [BIN, 'daemon', 'start', '--foreground', '--account', this.account], {
+ detached: true,
+ stdio: ['ignore', logFd, logFd],
+ env: { ...process.env, WA_CLI_ACCOUNT: this.account },
+ });
+ fs.closeSync(logFd);
+ let exited = null;
+ child.on('exit', (code) => { exited = code; });
+ child.unref();
+
+ const deadline = Date.now() + 30000;
+ while (Date.now() < deadline) {
+ if (await this.running()) return;
+ if (exited !== null) {
+ const tail = fs.readFileSync(P.logFile, 'utf8').trimEnd().split('\n').slice(-5).join('\n');
+ throw new WaError('EDAEMON', `the daemon exited while starting (code ${exited}):\n${tail}`);
+ }
+ await sleep(150);
+ }
+ throw new WaError('EDAEMON', `the daemon did not open the socket within 30 s (see ${P.logFile})`);
+ }
+}