build: add local node bin to restart script PATH

This commit is contained in:
Peter Steinberger
2025-12-07 18:49:55 +01:00
parent 558af7a454
commit d463c82c95
31 changed files with 2089 additions and 1851 deletions

View File

@@ -16,15 +16,15 @@ import { isVerbose, logVerbose } from "../globals.js";
import { triggerWarelayRestart } from "../infra/restart.js";
import { runCommandWithTimeout } from "../process/exec.js";
import { defaultRuntime } from "../runtime.js";
import { resolveHeartbeatSeconds } from "../web/reconnect.js";
import { getWebAuthAgeMs, webAuthExists } from "../web/session.js";
import { runCommandReply } from "./command-reply.js";
import { buildStatusMessage } from "./status.js";
import {
applyTemplate,
type MsgContext,
type TemplateContext,
} from "./templating.js";
import { buildStatusMessage } from "./status.js";
import { resolveHeartbeatSeconds } from "../web/reconnect.js";
import { getWebAuthAgeMs, webAuthExists } from "../web/session.js";
import {
normalizeThinkLevel,
normalizeVerboseLevel,

View File

@@ -1,7 +1,6 @@
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { lookupContextTokens } from "../agents/context.js";
import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL } from "../agents/defaults.js";
@@ -79,9 +78,8 @@ const probeAgentCommand = (command?: string[]): AgentProbe => {
encoding: "utf-8",
timeout: 1500,
});
const found = res.status === 0 && res.stdout
? res.stdout.split("\n")[0]?.trim()
: "";
const found =
res.status === 0 && res.stdout ? res.stdout.split("\n")[0]?.trim() : "";
return {
ok: Boolean(found),
detail: found || "not in PATH",
@@ -115,7 +113,7 @@ export function buildStatusMessage(args: StatusArgs): string {
DEFAULT_CONTEXT_TOKENS;
const totalTokens =
entry?.totalTokens ??
((entry?.inputTokens ?? 0) + (entry?.outputTokens ?? 0));
(entry?.inputTokens ?? 0) + (entry?.outputTokens ?? 0);
const agentProbe = probeAgentCommand(args.reply?.command);
const thinkLevel =
@@ -138,7 +136,9 @@ export function buildStatusMessage(args: StatusArgs): string {
const sessionLine = [
`Session: ${args.sessionKey ?? "unknown"}`,
`scope ${args.sessionScope ?? "per-sender"}`,
entry?.updatedAt ? `updated ${formatAge(now - entry.updatedAt)}` : "no activity",
entry?.updatedAt
? `updated ${formatAge(now - entry.updatedAt)}`
: "no activity",
args.storePath ? `store ${abbreviatePath(args.storePath)}` : undefined,
]
.filter(Boolean)
@@ -155,7 +155,13 @@ export function buildStatusMessage(args: StatusArgs): string {
const helpersLine = "Shortcuts: /new reset | /restart relink";
return [ "⚙️ Status", webLine, agentLine, contextLine, sessionLine, optionsLine, helpersLine ].join(
"\n",
);
return [
"⚙️ Status",
webLine,
agentLine,
contextLine,
sessionLine,
optionsLine,
helpersLine,
].join("\n");
}

View File

@@ -1,12 +1,15 @@
import { logWebSelfId, sendMessageWeb } from "../providers/web/index.js";
import { logWebSelfId, sendMessageWhatsApp } from "../providers/web/index.js";
import { sendMessageTelegram } from "../telegram/send.js";
export type CliDeps = {
sendMessageWeb: typeof sendMessageWeb;
sendMessageWhatsApp: typeof sendMessageWhatsApp;
sendMessageTelegram: typeof sendMessageTelegram;
};
export function createDefaultDeps(): CliDeps {
return {
sendMessageWeb,
sendMessageWhatsApp,
sendMessageTelegram,
};
}

View File

@@ -7,6 +7,7 @@ const monitorWebProvider = vi.fn();
const logWebSelfId = vi.fn();
const waitForever = vi.fn();
const spawnRelayTmux = vi.fn().mockResolvedValue("clawdis-relay");
const monitorTelegramProvider = vi.fn();
const runtime = {
log: vi.fn(),
@@ -23,6 +24,9 @@ vi.mock("../provider-web.js", () => ({
loginWeb,
monitorWebProvider,
}));
vi.mock("../telegram/monitor.js", () => ({
monitorTelegramProvider,
}));
vi.mock("./deps.js", () => ({
createDefaultDeps: () => ({ waitForever }),
logWebSelfId,
@@ -86,6 +90,15 @@ describe("cli program", () => {
);
});
it("runs telegram relay when token set", async () => {
const program = buildProgram();
const prev = process.env.TELEGRAM_BOT_TOKEN;
process.env.TELEGRAM_BOT_TOKEN = "token123";
await program.parseAsync(["relay:telegram"], { from: "user" });
expect(monitorTelegramProvider).toHaveBeenCalled();
process.env.TELEGRAM_BOT_TOKEN = prev;
});
it("runs status command", async () => {
const program = buildProgram();
await program.parseAsync(["status"], { from: "user" });

View File

@@ -135,16 +135,20 @@ export function buildProgram() {
program
.command("send")
.description("Send a WhatsApp message (web provider)")
.description("Send a message (WhatsApp web or Telegram bot)")
.requiredOption(
"-t, --to <number>",
"Recipient number in E.164 (e.g. +15555550123)",
"Recipient: E.164 for WhatsApp (e.g. +15555550123) or Telegram chat id/@username",
)
.requiredOption("-m, --message <text>", "Message body")
.option(
"--media <path-or-url>",
"Attach media (image/audio/video/document). Accepts local paths or URLs.",
)
.option(
"--provider <provider>",
"Delivery provider: whatsapp|telegram (default: whatsapp)",
)
.option("--dry-run", "Print payload and skip sending", false)
.option("--json", "Output result as JSON", false)
.option("--verbose", "Verbose logging", false)
@@ -562,6 +566,42 @@ Examples:
}
});
program
.command("relay:telegram")
.description("Auto-reply to Telegram (Bot API, long-poll)")
.option("--verbose", "Verbose logging", false)
.addHelpText(
"after",
`
Examples:
clawdis relay:telegram # uses TELEGRAM_BOT_TOKEN env
TELEGRAM_BOT_TOKEN=xxx clawdis relay:telegram --verbose
`,
)
.action(async (opts) => {
setVerbose(Boolean(opts.verbose));
const token = process.env.TELEGRAM_BOT_TOKEN;
if (!token) {
defaultRuntime.error(
danger("Set TELEGRAM_BOT_TOKEN to use telegram relay"),
);
defaultRuntime.exit(1);
return;
}
try {
await import("../telegram/monitor.js").then((m) =>
m.monitorTelegramProvider({
verbose: Boolean(opts.verbose),
token,
runtime: defaultRuntime,
}),
);
} catch (err) {
defaultRuntime.error(danger(`Telegram relay failed: ${String(err)}`));
defaultRuntime.exit(1);
}
});
program
.command("status")
.description("Show web session health and recent session recipients")

View File

@@ -378,13 +378,13 @@ export async function agentCommand(
}
if (!sentViaIpc) {
if (text || media.length === 0) {
await deps.sendMessageWeb(targetTo, text, {
await deps.sendMessageWhatsApp(targetTo, text, {
verbose: false,
mediaUrl: media[0],
});
}
for (const extra of media.slice(1)) {
await deps.sendMessageWeb(targetTo, "", {
await deps.sendMessageWhatsApp(targetTo, "", {
verbose: false,
mediaUrl: extra,
});

View File

@@ -18,7 +18,8 @@ const runtime: RuntimeEnv = {
};
const makeDeps = (overrides: Partial<CliDeps> = {}): CliDeps => ({
sendMessageWeb: vi.fn(),
sendMessageWhatsApp: vi.fn(),
sendMessageTelegram: vi.fn(),
...overrides,
});
@@ -34,7 +35,7 @@ describe("sendCommand", () => {
deps,
runtime,
);
expect(deps.sendMessageWeb).not.toHaveBeenCalled();
expect(deps.sendMessageWhatsApp).not.toHaveBeenCalled();
});
it("uses IPC when available", async () => {
@@ -48,14 +49,16 @@ describe("sendCommand", () => {
deps,
runtime,
);
expect(deps.sendMessageWeb).not.toHaveBeenCalled();
expect(deps.sendMessageWhatsApp).not.toHaveBeenCalled();
expect(runtime.log).toHaveBeenCalledWith(expect.stringContaining("ipc1"));
});
it("falls back to direct send when IPC fails", async () => {
sendViaIpcMock.mockResolvedValueOnce({ success: false, error: "nope" });
const deps = makeDeps({
sendMessageWeb: vi.fn().mockResolvedValue({ messageId: "direct1" }),
sendMessageWhatsApp: vi
.fn()
.mockResolvedValue({ messageId: "direct1" }),
});
await sendCommand(
{
@@ -66,13 +69,34 @@ describe("sendCommand", () => {
deps,
runtime,
);
expect(deps.sendMessageWeb).toHaveBeenCalled();
expect(deps.sendMessageWhatsApp).toHaveBeenCalled();
});
it("routes to telegram provider", async () => {
const deps = makeDeps({
sendMessageTelegram: vi
.fn()
.mockResolvedValue({ messageId: "t1", chatId: "123" }),
});
await sendCommand(
{ to: "123", message: "hi", provider: "telegram" },
deps,
runtime,
);
expect(deps.sendMessageTelegram).toHaveBeenCalledWith(
"123",
"hi",
expect.objectContaining({ token: expect.any(String) }),
);
expect(deps.sendMessageWhatsApp).not.toHaveBeenCalled();
});
it("emits json output", async () => {
sendViaIpcMock.mockResolvedValueOnce(null);
const deps = makeDeps({
sendMessageWeb: vi.fn().mockResolvedValue({ messageId: "direct2" }),
sendMessageWhatsApp: vi
.fn()
.mockResolvedValue({ messageId: "direct2" }),
});
await sendCommand(
{

View File

@@ -7,6 +7,7 @@ export async function sendCommand(
opts: {
to: string;
message: string;
provider?: string;
json?: boolean;
dryRun?: boolean;
media?: string;
@@ -14,13 +15,44 @@ export async function sendCommand(
deps: CliDeps,
runtime: RuntimeEnv,
) {
const provider = (opts.provider ?? "whatsapp").toLowerCase();
if (opts.dryRun) {
runtime.log(
`[dry-run] would send via web -> ${opts.to}: ${opts.message}${opts.media ? ` (media ${opts.media})` : ""}`,
`[dry-run] would send via ${provider} -> ${opts.to}: ${opts.message}${opts.media ? ` (media ${opts.media})` : ""}`,
);
return;
}
if (provider === "telegram") {
const result = await deps.sendMessageTelegram(opts.to, opts.message, {
token: process.env.TELEGRAM_BOT_TOKEN,
mediaUrl: opts.media,
});
runtime.log(
success(
`✅ Sent via telegram. Message ID: ${result.messageId} (chat ${result.chatId})`,
),
);
if (opts.json) {
runtime.log(
JSON.stringify(
{
provider: "telegram",
via: "direct",
to: opts.to,
chatId: result.chatId,
messageId: result.messageId,
mediaUrl: opts.media ?? null,
},
null,
2,
),
);
}
return;
}
// Try to send via IPC to running relay first (avoids Signal session corruption)
const ipcResult = await sendViaIpc(opts.to, opts.message, opts.media);
if (ipcResult) {
@@ -55,7 +87,7 @@ export async function sendCommand(
// Fall back to direct connection (creates new Baileys socket)
const res = await deps
.sendMessageWeb(opts.to, opts.message, {
.sendMessageWhatsApp(opts.to, opts.message, {
verbose: false,
mediaUrl: opts.media,
})

View File

@@ -44,6 +44,15 @@ export type WebConfig = {
reconnect?: WebReconnectConfig;
};
export type TelegramConfig = {
botToken?: string;
requireMention?: boolean;
allowFrom?: Array<string | number>;
mediaMaxMb?: number;
proxy?: string;
webhookUrl?: string;
};
export type GroupChatConfig = {
requireMention?: boolean;
mentionPatterns?: string[];
@@ -89,6 +98,7 @@ export type WarelayConfig = {
};
};
web?: WebConfig;
telegram?: TelegramConfig;
};
// New branding path (preferred)
@@ -214,6 +224,16 @@ const WarelaySchema = z.object({
.optional(),
})
.optional(),
telegram: z
.object({
botToken: z.string().optional(),
requireMention: z.boolean().optional(),
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
mediaMaxMb: z.number().positive().optional(),
proxy: z.string().optional(),
webhookUrl: z.string().optional(),
})
.optional(),
});
export function loadConfig(): WarelayConfig {

View File

@@ -23,12 +23,30 @@ describe("sessions", () => {
);
});
it("collapses direct chats to main by default", () => {
expect(resolveSessionKey("per-sender", { From: "+1555" })).toBe("main");
});
it("collapses direct chats to main even when sender missing", () => {
expect(resolveSessionKey("per-sender", {})).toBe("main");
});
it("maps direct chats to main key when provided", () => {
expect(
resolveSessionKey("per-sender", { From: "whatsapp:+1555" }, "main"),
).toBe("main");
});
it("uses custom main key when provided", () => {
expect(resolveSessionKey("per-sender", { From: "+1555" }, "primary")).toBe(
"primary",
);
});
it("keeps global scope untouched", () => {
expect(resolveSessionKey("global", { From: "+1555" })).toBe("global");
});
it("leaves groups untouched even with main key", () => {
expect(
resolveSessionKey("per-sender", { From: "12345-678@g.us" }, "main"),

View File

@@ -79,8 +79,8 @@ export function deriveSessionKey(scope: SessionScope, ctx: MsgContext) {
}
/**
* Resolve the session key with an optional canonical direct-chat key (e.g., "main").
* All non-group direct chats collapse to `mainKey` when provided, keeping group isolation.
* Resolve the session key with a canonical direct-chat bucket (default: "main").
* All non-group direct chats collapse to this bucket; groups stay isolated.
*/
export function resolveSessionKey(
scope: SessionScope,
@@ -89,8 +89,9 @@ export function resolveSessionKey(
) {
const raw = deriveSessionKey(scope, ctx);
if (scope === "global") return raw;
const canonical = (mainKey ?? "").trim();
// Default to a single shared direct-chat session called "main"; groups stay isolated.
const canonical = (mainKey ?? "main").trim() || "main";
const isGroup = raw.startsWith("group:") || raw.includes("@g.us");
if (!isGroup && canonical) return canonical;
if (!isGroup) return canonical;
return raw;
}

View File

@@ -30,7 +30,7 @@ import { assertProvider, normalizeE164, toWhatsappJid } from "./utils.js";
dotenv.config({ quiet: true });
// Capture all console output into pino logs while keeping stdout/stderr behavior.
// Capture all console output into structured logs while keeping stdout/stderr behavior.
enableConsoleCapture();
import { buildProgram } from "./cli/program.js";

View File

@@ -2,7 +2,7 @@ import fs from "node:fs";
import path from "node:path";
import util from "node:util";
import pino, { type Bindings, type LevelWithSilent, type Logger } from "pino";
import { Logger as TsLogger } from "tslog";
import { loadConfig, type WarelayConfig } from "./config/config.js";
import { isVerbose } from "./globals.js";
@@ -15,7 +15,7 @@ const LOG_PREFIX = "clawdis";
const LOG_SUFFIX = ".log";
const MAX_LOG_AGE_MS = 24 * 60 * 60 * 1000; // 24h
const ALLOWED_LEVELS: readonly LevelWithSilent[] = [
const ALLOWED_LEVELS = [
"silent",
"fatal",
"error",
@@ -23,30 +23,32 @@ const ALLOWED_LEVELS: readonly LevelWithSilent[] = [
"info",
"debug",
"trace",
];
] as const;
type Level = (typeof ALLOWED_LEVELS)[number];
export type LoggerSettings = {
level?: LevelWithSilent;
level?: Level;
file?: string;
};
type LogObj = Record<string, unknown>;
type ResolvedSettings = {
level: LevelWithSilent;
level: Level;
file: string;
};
export type LoggerResolvedSettings = ResolvedSettings;
let cachedLogger: Logger | null = null;
let cachedLogger: TsLogger<LogObj> | null = null;
let cachedSettings: ResolvedSettings | null = null;
let overrideSettings: LoggerSettings | null = null;
let consolePatched = false;
function normalizeLevel(level?: string): LevelWithSilent {
function normalizeLevel(level?: string): Level {
if (isVerbose()) return "trace";
const candidate = level ?? "info";
return ALLOWED_LEVELS.includes(candidate as LevelWithSilent)
? (candidate as LevelWithSilent)
: "info";
return ALLOWED_LEVELS.includes(candidate as Level) ? (candidate as Level) : "info";
}
function resolveSettings(): ResolvedSettings {
@@ -62,28 +64,48 @@ function settingsChanged(a: ResolvedSettings | null, b: ResolvedSettings) {
return a.level !== b.level || a.file !== b.file;
}
function buildLogger(settings: ResolvedSettings): Logger {
function levelToMinLevel(level: Level): number {
// tslog level ordering: fatal=0, error=1, warn=2, info=3, debug=4, trace=5
const map: Record<Level, number> = {
fatal: 0,
error: 1,
warn: 2,
info: 3,
debug: 4,
trace: 5,
silent: Number.POSITIVE_INFINITY,
};
return map[level];
}
function buildLogger(settings: ResolvedSettings): TsLogger<LogObj> {
fs.mkdirSync(path.dirname(settings.file), { recursive: true });
// Clean up stale rolling logs when using a dated log filename.
if (isRollingPath(settings.file)) {
pruneOldRollingLogs(path.dirname(settings.file));
}
const destination = pino.destination({
dest: settings.file,
mkdir: true,
sync: true, // deterministic for tests; log volume is modest.
const logger = new TsLogger<LogObj>({
name: "clawdis",
minLevel: levelToMinLevel(settings.level),
type: "hidden", // no ansi formatting
});
return pino(
{
level: settings.level,
base: undefined,
timestamp: pino.stdTimeFunctions.isoTime,
},
destination,
logger.attachTransport(
(logObj) => {
try {
const time = (logObj as any)?.date?.toISOString?.() ?? new Date().toISOString();
const line = JSON.stringify({ ...logObj, time });
fs.appendFileSync(settings.file, line + "\n", { encoding: "utf8" });
} catch {
// never block on logging failures
}
}
);
return logger;
}
export function getLogger(): Logger {
export function getLogger(): TsLogger<LogObj> {
const settings = resolveSettings();
if (!cachedLogger || settingsChanged(cachedSettings, settings)) {
cachedLogger = buildLogger(settings);
@@ -93,12 +115,55 @@ export function getLogger(): Logger {
}
export function getChildLogger(
bindings?: Bindings,
opts?: { level?: LevelWithSilent },
): Logger {
return getLogger().child(bindings ?? {}, opts);
bindings?: Record<string, unknown>,
opts?: { level?: Level },
): TsLogger<LogObj> {
const base = getLogger();
const minLevel = opts?.level ? levelToMinLevel(opts.level) : undefined;
const name = bindings ? JSON.stringify(bindings) : undefined;
return base.getSubLogger({
name,
minLevel,
prefix: bindings ? [name ?? ""] : [],
});
}
export type LogLevel = Level;
// Baileys expects a pino-like logger shape. Provide a lightweight adapter.
export function toPinoLikeLogger(
logger: TsLogger<LogObj>,
level: Level,
): PinoLikeLogger {
const buildChild = (bindings?: Record<string, unknown>) =>
toPinoLikeLogger(
logger.getSubLogger({ name: bindings ? JSON.stringify(bindings) : undefined }),
level,
);
return {
level,
child: buildChild,
trace: (...args: unknown[]) => logger.trace(...args),
debug: (...args: unknown[]) => logger.debug(...args),
info: (...args: unknown[]) => logger.info(...args),
warn: (...args: unknown[]) => logger.warn(...args),
error: (...args: unknown[]) => logger.error(...args),
fatal: (...args: unknown[]) => logger.fatal(...args),
};
}
export type PinoLikeLogger = {
level: string;
child: (bindings?: Record<string, unknown>) => PinoLikeLogger;
trace: (...args: unknown[]) => void;
debug: (...args: unknown[]) => void;
info: (...args: unknown[]) => void;
warn: (...args: unknown[]) => void;
error: (...args: unknown[]) => void;
fatal: (...args: unknown[]) => void;
};
export function getResolvedLoggerSettings(): LoggerResolvedSettings {
return resolveSettings();
}
@@ -136,7 +201,7 @@ export function enableConsoleCapture(): void {
};
const forward =
(level: LevelWithSilent, orig: (...args: unknown[]) => void) =>
(level: Level, orig: (...args: unknown[]) => void) =>
(...args: unknown[]) => {
const formatted = util.format(...args);
try {

View File

@@ -8,7 +8,7 @@ import { CONFIG_DIR } from "../utils.js";
import { detectMime, extensionForMime } from "./mime.js";
const MEDIA_DIR = path.join(CONFIG_DIR, "media");
const MAX_BYTES = 5 * 1024 * 1024; // 5MB
const MAX_BYTES = 5 * 1024 * 1024; // 5MB default
const DEFAULT_TTL_MS = 2 * 60 * 1000; // 2 minutes
export function getMediaDir() {
@@ -159,9 +159,12 @@ export async function saveMediaBuffer(
buffer: Buffer,
contentType?: string,
subdir = "inbound",
maxBytes = MAX_BYTES,
): Promise<SavedMedia> {
if (buffer.byteLength > MAX_BYTES) {
throw new Error("Media exceeds 5MB limit");
if (buffer.byteLength > maxBytes) {
throw new Error(
`Media exceeds ${(maxBytes / (1024 * 1024)).toFixed(0)}MB limit`,
);
}
const dir = path.join(MEDIA_DIR, subdir);
await fs.mkdir(dir, { recursive: true });

View File

@@ -7,7 +7,7 @@ describe("provider-web barrel", () => {
expect(mod.createWaSocket).toBeTypeOf("function");
expect(mod.loginWeb).toBeTypeOf("function");
expect(mod.monitorWebProvider).toBeTypeOf("function");
expect(mod.sendMessageWeb).toBeTypeOf("function");
expect(mod.sendMessageWhatsApp).toBeTypeOf("function");
expect(mod.monitorWebInbox).toBeTypeOf("function");
expect(mod.pickProvider).toBeTypeOf("function");
expect(mod.WA_WEB_AUTH_DIR).toBeTruthy();

View File

@@ -19,7 +19,7 @@ export {
} from "./web/inbound.js";
export { loginWeb } from "./web/login.js";
export { loadWebMedia, optimizeImageToJpeg } from "./web/media.js";
export { sendMessageWeb } from "./web/outbound.js";
export { sendMessageWhatsApp } from "./web/outbound.js";
export {
createWaSocket,
formatError,

View File

@@ -11,7 +11,7 @@ describe("providers/web entrypoint", () => {
expect(entry.monitorWebInbox).toBe(impl.monitorWebInbox);
expect(entry.monitorWebProvider).toBe(impl.monitorWebProvider);
expect(entry.pickProvider).toBe(impl.pickProvider);
expect(entry.sendMessageWeb).toBe(impl.sendMessageWeb);
expect(entry.sendMessageWhatsApp).toBe(impl.sendMessageWhatsApp);
expect(entry.WA_WEB_AUTH_DIR).toBe(impl.WA_WEB_AUTH_DIR);
expect(entry.waitForWaConnection).toBe(impl.waitForWaConnection);
expect(entry.webAuthExists).toBe(impl.webAuthExists);

View File

@@ -6,7 +6,7 @@ export {
monitorWebInbox,
monitorWebProvider,
pickProvider,
sendMessageWeb,
sendMessageWhatsApp,
WA_WEB_AUTH_DIR,
waitForWaConnection,
webAuthExists,

View File

@@ -18,7 +18,7 @@ import {
runWebHeartbeatOnce,
stripHeartbeatToken,
} from "./auto-reply.js";
import type { sendMessageWeb } from "./outbound.js";
import type { sendMessageWhatsApp } from "./outbound.js";
import {
resetBaileysMocks,
resetLoadConfigMock,
@@ -157,7 +157,7 @@ describe("resolveHeartbeatRecipients", () => {
describe("runWebHeartbeatOnce", () => {
it("skips when heartbeat token returned", async () => {
const store = await makeSessionStore();
const sender: typeof sendMessageWeb = vi.fn();
const sender: typeof sendMessageWhatsApp = vi.fn();
const resolver = vi.fn(async () => ({ text: HEARTBEAT_TOKEN }));
await runWebHeartbeatOnce({
cfg: {
@@ -178,7 +178,7 @@ describe("runWebHeartbeatOnce", () => {
it("sends when alert text present", async () => {
const store = await makeSessionStore();
const sender: typeof sendMessageWeb = vi
const sender: typeof sendMessageWhatsApp = vi
.fn()
.mockResolvedValue({ messageId: "m1", toJid: "jid" });
const resolver = vi.fn(async () => ({ text: "ALERT" }));
@@ -201,7 +201,7 @@ describe("runWebHeartbeatOnce", () => {
it("falls back to most recent session when no to is provided", async () => {
const store = await makeSessionStore();
const storePath = store.storePath;
const sender: typeof sendMessageWeb = vi
const sender: typeof sendMessageWhatsApp = vi
.fn()
.mockResolvedValue({ messageId: "m1", toJid: "jid" });
const resolver = vi.fn(async () => ({ text: "ALERT" }));
@@ -239,7 +239,7 @@ describe("runWebHeartbeatOnce", () => {
};
await fs.writeFile(storePath, JSON.stringify(store));
const sender: typeof sendMessageWeb = vi.fn();
const sender: typeof sendMessageWhatsApp = vi.fn();
const resolver = vi.fn(async () => ({ text: HEARTBEAT_TOKEN }));
setLoadConfigMock({
inbound: {
@@ -365,7 +365,7 @@ describe("runWebHeartbeatOnce", () => {
it("sends overrideBody directly and skips resolver", async () => {
const store = await makeSessionStore();
const sender: typeof sendMessageWeb = vi
const sender: typeof sendMessageWhatsApp = vi
.fn()
.mockResolvedValue({ messageId: "m1", toJid: "jid" });
const resolver = vi.fn();
@@ -391,7 +391,7 @@ describe("runWebHeartbeatOnce", () => {
it("dry-run overrideBody prints and skips send", async () => {
const store = await makeSessionStore();
const sender: typeof sendMessageWeb = vi.fn();
const sender: typeof sendMessageWhatsApp = vi.fn();
const resolver = vi.fn();
await runWebHeartbeatOnce({
cfg: {

View File

@@ -19,7 +19,7 @@ import { jidToE164, normalizeE164 } from "../utils.js";
import { monitorWebInbox } from "./inbound.js";
import { sendViaIpc, startIpcServer, stopIpcServer } from "./ipc.js";
import { loadWebMedia } from "./media.js";
import { sendMessageWeb } from "./outbound.js";
import { sendMessageWhatsApp } from "./outbound.js";
import {
computeBackoff,
newConnectionId,
@@ -55,7 +55,7 @@ async function sendWithIpcFallback(
return { messageId: ipcResult.messageId, toJid: `${to}@s.whatsapp.net` };
}
// Fall back to direct send
return sendMessageWeb(to, message, opts);
return sendMessageWhatsApp(to, message, opts);
}
const DEFAULT_WEB_MEDIA_BYTES = 5 * 1024 * 1024;
@@ -194,7 +194,7 @@ export async function runWebHeartbeatOnce(opts: {
verbose?: boolean;
replyResolver?: typeof getReplyFromConfig;
runtime?: RuntimeEnv;
sender?: typeof sendMessageWeb;
sender?: typeof sendMessageWhatsApp;
sessionId?: string;
overrideBody?: string;
dryRun?: boolean;

View File

@@ -23,7 +23,7 @@ vi.mock("./media.js", () => ({
loadWebMedia: (...args: unknown[]) => loadWebMediaMock(...args),
}));
import { sendMessageWeb } from "./outbound.js";
import { sendMessageWhatsApp } from "./outbound.js";
const { createWaSocket } = await import("./session.js");
@@ -38,7 +38,7 @@ describe("web outbound", () => {
});
it("sends message via web and closes socket", async () => {
await sendMessageWeb("+1555", "hi", { verbose: false });
await sendMessageWhatsApp("+1555", "hi", { verbose: false });
const sock = await createWaSocket();
expect(sock.sendMessage).toHaveBeenCalled();
expect(sock.ws.close).toHaveBeenCalled();
@@ -51,7 +51,7 @@ describe("web outbound", () => {
contentType: "audio/ogg",
kind: "audio",
});
await sendMessageWeb("+1555", "voice note", {
await sendMessageWhatsApp("+1555", "voice note", {
verbose: false,
mediaUrl: "/tmp/voice.ogg",
});
@@ -74,7 +74,7 @@ describe("web outbound", () => {
contentType: "video/mp4",
kind: "video",
});
await sendMessageWeb("+1555", "clip", {
await sendMessageWhatsApp("+1555", "clip", {
verbose: false,
mediaUrl: "/tmp/video.mp4",
});
@@ -97,7 +97,7 @@ describe("web outbound", () => {
contentType: "image/jpeg",
kind: "image",
});
await sendMessageWeb("+1555", "pic", {
await sendMessageWhatsApp("+1555", "pic", {
verbose: false,
mediaUrl: "/tmp/pic.jpg",
});
@@ -121,7 +121,7 @@ describe("web outbound", () => {
kind: "document",
fileName: "file.pdf",
});
await sendMessageWeb("+1555", "doc", {
await sendMessageWhatsApp("+1555", "doc", {
verbose: false,
mediaUrl: "/tmp/file.pdf",
});

View File

@@ -9,7 +9,7 @@ import { toWhatsappJid } from "../utils.js";
import { loadWebMedia } from "./media.js";
import { createWaSocket, waitForWaConnection } from "./session.js";
export async function sendMessageWeb(
export async function sendMessageWhatsApp(
to: string,
body: string,
options: { verbose: boolean; mediaUrl?: string },

View File

@@ -13,7 +13,7 @@ import qrcode from "qrcode-terminal";
import { SESSION_STORE_DEFAULT } from "../config/sessions.js";
import { danger, info, success } from "../globals.js";
import { getChildLogger } from "../logging.js";
import { getChildLogger, toPinoLikeLogger } from "../logging.js";
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
import type { Provider } from "../utils.js";
import { CONFIG_DIR, ensureDir, jidToE164 } from "../utils.js";
@@ -26,17 +26,13 @@ export const WA_WEB_AUTH_DIR = path.join(CONFIG_DIR, "credentials");
* Consumers can opt into QR printing for interactive login flows.
*/
export async function createWaSocket(printQr: boolean, verbose: boolean) {
const logger = getChildLogger(
const baseLogger = getChildLogger(
{ module: "baileys" },
{
level: verbose ? "info" : "silent",
},
);
// Some Baileys internals call logger.trace even when silent; ensure it's present.
const loggerAny = logger as unknown as Record<string, unknown>;
if (typeof loggerAny.trace !== "function") {
loggerAny.trace = () => {};
}
const logger = toPinoLikeLogger(baseLogger, verbose ? "info" : "silent");
await ensureDir(WA_WEB_AUTH_DIR);
const { state, saveCreds } = await useMultiFileAuthState(WA_WEB_AUTH_DIR);
const { version } = await fetchLatestBaileysVersion();