docs: finalize web refactor and coverage

This commit is contained in:
Peter Steinberger
2025-11-26 02:54:43 +01:00
parent 5c66e8273b
commit a48420d85f
4 changed files with 97 additions and 29 deletions

View File

@@ -18,7 +18,7 @@ Install from npm (global): `npm install -g warelay` (Node 22+). Then choose **on
**A) Personal WhatsApp Web (preferred: no Twilio creds, fastest setup)**
1. Link your account: `warelay login` (scan the QR).
2. Send a message: `warelay send --to +12345550000 --message "Hi from warelay"` (add `--provider web` if you want to force the web session).
3. Stay online & auto-reply: `warelay relay --verbose` (defaults to Web when logged in, falls back to Twilio otherwise).
3. Stay online & auto-reply: `warelay relay --verbose` (uses Web when you're logged in; if you're not linked, start it with `--provider twilio`). When a Web session drops, the relay exits instead of silently falling back so you notice and re-login.
**B) Twilio WhatsApp number (for delivery status + webhooks)**
1. Copy `.env.example``.env`; set `TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN` **or** `TWILIO_API_KEY`/`TWILIO_API_SECRET`, and `TWILIO_WHATSAPP_FROM=whatsapp:+19995550123` (optional `TWILIO_SENDER_SID`).
@@ -79,8 +79,8 @@ Install from npm (global): `npm install -g warelay` (Node 22+). Then choose **on
## Providers
- **Twilio (default):** needs `.env` creds + WhatsApp-enabled number; supports delivery tracking, polling, webhooks, and auto-reply typing indicators.
- **Web (`--provider web`):** uses your personal WhatsApp via Baileys; supports send/receive + auto-reply, but no delivery-status wait; cache lives in `~/.warelay/credentials/` (rerun `login` if logged out).
- **Auto-select (`relay` only):** `--provider auto` uses Web when logged in, otherwise Twilio polling.
- **Web (`--provider web`):** uses your personal WhatsApp via Baileys; supports send/receive + auto-reply, but no delivery-status wait; cache lives in `~/.warelay/credentials/` (rerun `login` if logged out). If the Web socket closes, the relay exits instead of pivoting to Twilio.
- **Auto-select (`relay` only):** `--provider auto` picks Web when a cache exists at start, otherwise Twilio polling. It will not swap from Web to Twilio mid-run if the Web session drops.
Best practice: use a dedicated WhatsApp account (separate SIM/eSIM or business account) for automation instead of your primary personal account to avoid unexpected logouts or rate limits.
@@ -171,6 +171,11 @@ Templating tokens: `{{Body}}`, `{{BodyStripped}}`, `{{From}}`, `{{To}}`, `{{Mess
- Web provider dropped: rerun `pnpm warelay login`; credentials live in `~/.warelay/credentials/`.
- Tailscale Funnel errors: update tailscale/tailscaled; check admin console that Funnel is enabled for this device.
### Maintainer notes (web provider internals)
- Web logic lives under `src/web/`: `session.ts` (auth/cache + provider pick), `login.ts` (QR login/logout), `outbound.ts`/`inbound.ts` (send/receive plumbing), `auto-reply.ts` (relay loop + reconnect/backoff), `media.ts` (download/resize helpers), and `reconnect.ts` (shared retry math). `test-helpers.ts` provides fixtures.
- The public surface remains the `src/provider-web.ts` barrel so existing imports keep working.
- Reconnects are capped and logged; no Twilio fallback occurs after a Web disconnect—restart the relay after re-linking.
## FAQ & Safety
- Twilio errors: **63016 “permission to send an SMS has not been enabled”** → ensure your number is WhatsApp-enabled; **63007 template not approved** → send a free-form session message within 24h or use an approved template; **63112 policy violation** → adjust content, shorten to <1600 chars, avoid links that trigger spam filters. Re-run `pnpm warelay status` to see the exact Twilio response body.
- Does this store my messages? warelay only writes `~/.warelay/warelay.json` (config), `~/.warelay/credentials/` (WhatsApp Web auth), and `~/.warelay/sessions.json` (session IDs + timestamps). It does **not** persist message bodies beyond the session store. Logs stream to stdout/stderr and also `/tmp/warelay/warelay.log` (configurable via `logging.file`).

View File

@@ -1,26 +0,0 @@
# Web Provider Refactor (Nov 26, 2025)
Context: `src/provider-web.ts` was a 900+ line ball of mud mixing session management, outbound sends, inbound handling, auto-replies, and media helpers. We split it into focused modules under `src/web/` and adjusted tests/CLI behavior.
## What changed
- New modules: `session.ts`, `login.ts`, `outbound.ts`, `inbound.ts`, `auto-reply.ts`, `media.ts`; barrel remains `src/provider-web.ts`.
- CLI adds `warelay logout` to clear `~/.warelay/credentials`; tested in `src/web/logout.test.ts`.
- Relay now **exits instead of falling back to Twilio** when the web provider fails (even in `--provider auto`), so outages are visible.
- Tests split accordingly; all suites green.
- Structured logging + heartbeats: web relay now emits structured logs with `runId`/`connectionId` plus periodic heartbeats (default every 60s) that include auth age and message counts.
- Bounded reconnects: web relay uses capped exponential backoff (default 2s→30s, max 12 attempts). CLI knobs `--web-retries`, `--web-retry-initial`, `--web-retry-max`, `--web-heartbeat` and config `web.reconnect`/`web.heartbeatSeconds` tune the behavior.
- Backoff reset after healthy uptime; logged-out state still exits immediately.
- Extracted reconnect/heartbeat helpers to `src/web/reconnect.ts` with unit tests.
- Added troubleshooting guide at `docs/refactor/web-relay-troubleshooting.md` (common errors, knobs, logs).
## How to use
- Link: `warelay login --provider web`
- Logout: `warelay logout` (deletes `~/.warelay/credentials`)
- Run relay web-only: `warelay relay --provider web --verbose`
## Follow-ups worth doing
- Document the new module boundaries in README/docs; add a one-liner explaining the no-fallback behavior.
- Add bounded backoff/jitter in `monitorWebProvider` reconnect loop with clearer exit codes. ✅
- Tighten config validation (`mediaMaxMb`, etc.) on load. ✅ (schema now includes `web.*` knobs)
- Emit structured logs for reconnect/close reasons to help ops triage (status, isLoggedOut). ✅
- Add quick troubleshooting snippets (how to read logs, restart relay, rotate creds).

View File

@@ -0,0 +1,15 @@
import { describe, expect, it } from "vitest";
import * as mod from "./provider-web.js";
describe("provider-web barrel", () => {
it("exports the expected web helpers", () => {
expect(mod.createWaSocket).toBeTypeOf("function");
expect(mod.loginWeb).toBeTypeOf("function");
expect(mod.monitorWebProvider).toBeTypeOf("function");
expect(mod.sendMessageWeb).toBeTypeOf("function");
expect(mod.monitorWebInbox).toBeTypeOf("function");
expect(mod.pickProvider).toBeTypeOf("function");
expect(mod.WA_WEB_AUTH_DIR).toBeTruthy();
});
});

View File

@@ -0,0 +1,74 @@
import fs from "node:fs/promises";
import { DisconnectReason } from "@whiskeysockets/baileys";
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.useFakeTimers();
const rmMock = vi.spyOn(fs, "rm");
vi.mock("./session.js", () => {
const sockA = { ws: { close: vi.fn() } };
const sockB = { ws: { close: vi.fn() } };
const createWaSocket = vi.fn(async () =>
createWaSocket.mock.calls.length === 0 ? sockA : sockB,
);
const waitForWaConnection = vi.fn();
const formatError = vi.fn((err: unknown) => `formatted:${String(err)}`);
return {
createWaSocket,
waitForWaConnection,
formatError,
WA_WEB_AUTH_DIR: "/tmp/wa-creds",
};
});
const { createWaSocket, waitForWaConnection, formatError } = await import(
"./session.js"
);
const { loginWeb } = await import("./login.js");
describe("loginWeb coverage", () => {
beforeEach(() => {
vi.clearAllMocks();
rmMock.mockClear();
});
it("restarts once when WhatsApp requests code 515", async () => {
waitForWaConnection
.mockRejectedValueOnce({ output: { statusCode: 515 } })
.mockResolvedValueOnce(undefined);
const runtime = { log: vi.fn(), error: vi.fn() } as never;
await loginWeb(false, waitForWaConnection as never, runtime);
expect(createWaSocket).toHaveBeenCalledTimes(2);
const firstSock = await createWaSocket.mock.results[0].value;
expect(firstSock.ws.close).toHaveBeenCalled();
vi.runAllTimers();
const secondSock = await createWaSocket.mock.results[1].value;
expect(secondSock.ws.close).toHaveBeenCalled();
});
it("clears creds and throws when logged out", async () => {
waitForWaConnection.mockRejectedValueOnce({
output: { statusCode: DisconnectReason.loggedOut },
});
await expect(loginWeb(false, waitForWaConnection as never)).rejects.toThrow(
/cache cleared/i,
);
expect(rmMock).toHaveBeenCalledWith("/tmp/wa-creds", {
recursive: true,
force: true,
});
});
it("formats and rethrows generic errors", async () => {
waitForWaConnection.mockRejectedValueOnce(new Error("boom"));
await expect(loginWeb(false, waitForWaConnection as never)).rejects.toThrow(
"formatted:Error: boom",
);
expect(formatError).toHaveBeenCalled();
});
});