chore: Enable "curly" rule to avoid single-statement if confusion/errors.

This commit is contained in:
cpojer
2026-01-31 16:19:20 +09:00
parent 009b16fab8
commit 5ceff756e1
1266 changed files with 27871 additions and 9393 deletions

View File

@@ -32,7 +32,9 @@ export type ResolvedWhatsAppAccount = {
function listConfiguredAccountIds(cfg: OpenClawConfig): string[] {
const accounts = cfg.channels?.whatsapp?.accounts;
if (!accounts || typeof accounts !== "object") return [];
if (!accounts || typeof accounts !== "object") {
return [];
}
return Object.keys(accounts).filter(Boolean);
}
@@ -49,7 +51,9 @@ export function listWhatsAppAuthDirs(cfg: OpenClawConfig): string[] {
try {
const entries = fs.readdirSync(whatsappDir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory()) continue;
if (!entry.isDirectory()) {
continue;
}
authDirs.add(path.join(whatsappDir, entry.name));
}
} catch {
@@ -65,13 +69,17 @@ export function hasAnyWhatsAppAuth(cfg: OpenClawConfig): boolean {
export function listWhatsAppAccountIds(cfg: OpenClawConfig): string[] {
const ids = listConfiguredAccountIds(cfg);
if (ids.length === 0) return [DEFAULT_ACCOUNT_ID];
if (ids.length === 0) {
return [DEFAULT_ACCOUNT_ID];
}
return ids.toSorted((a, b) => a.localeCompare(b));
}
export function resolveDefaultWhatsAppAccountId(cfg: OpenClawConfig): string {
const ids = listWhatsAppAccountIds(cfg);
if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID;
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
return DEFAULT_ACCOUNT_ID;
}
return ids[0] ?? DEFAULT_ACCOUNT_ID;
}
@@ -80,7 +88,9 @@ function resolveAccountConfig(
accountId: string,
): WhatsAppAccountConfig | undefined {
const accounts = cfg.channels?.whatsapp?.accounts;
if (!accounts || typeof accounts !== "object") return undefined;
if (!accounts || typeof accounts !== "object") {
return undefined;
}
const entry = accounts[accountId] as WhatsAppAccountConfig | undefined;
return entry;
}

View File

@@ -36,9 +36,13 @@ export function hasWebCredsSync(authDir: string): boolean {
function readCredsJsonRaw(filePath: string): string | null {
try {
if (!fsSync.existsSync(filePath)) return null;
if (!fsSync.existsSync(filePath)) {
return null;
}
const stats = fsSync.statSync(filePath);
if (!stats.isFile() || stats.size <= 1) return null;
if (!stats.isFile() || stats.size <= 1) {
return null;
}
return fsSync.readFileSync(filePath, "utf-8");
} catch {
return null;
@@ -58,7 +62,9 @@ export function maybeRestoreCredsFromBackup(authDir: string): void {
}
const backupRaw = readCredsJsonRaw(backupPath);
if (!backupRaw) return;
if (!backupRaw) {
return;
}
// Ensure backup is parseable before restoring.
JSON.parse(backupRaw);
@@ -80,7 +86,9 @@ export async function webAuthExists(authDir: string = resolveDefaultWebAuthDir()
}
try {
const stats = await fs.stat(credsPath);
if (!stats.isFile() || stats.size <= 1) return false;
if (!stats.isFile() || stats.size <= 1) {
return false;
}
const raw = await fs.readFile(credsPath, "utf-8");
JSON.parse(raw);
return true;
@@ -92,15 +100,25 @@ export async function webAuthExists(authDir: string = resolveDefaultWebAuthDir()
async function clearLegacyBaileysAuthState(authDir: string) {
const entries = await fs.readdir(authDir, { withFileTypes: true });
const shouldDelete = (name: string) => {
if (name === "oauth.json") return false;
if (name === "creds.json" || name === "creds.json.bak") return true;
if (!name.endsWith(".json")) return false;
if (name === "oauth.json") {
return false;
}
if (name === "creds.json" || name === "creds.json.bak") {
return true;
}
if (!name.endsWith(".json")) {
return false;
}
return /^(app-state-sync|session|sender-key|pre-key)-/.test(name);
};
await Promise.all(
entries.map(async (entry) => {
if (!entry.isFile()) return;
if (!shouldDelete(entry.name)) return;
if (!entry.isFile()) {
return;
}
if (!shouldDelete(entry.name)) {
return;
}
await fs.rm(path.join(authDir, entry.name), { force: true });
}),
);

View File

@@ -230,10 +230,14 @@ describe("partial reply gating", () => {
string,
{ lastChannel?: string; lastTo?: string }
>;
if (stored[mainSessionKey]?.lastChannel && stored[mainSessionKey]?.lastTo) break;
if (stored[mainSessionKey]?.lastChannel && stored[mainSessionKey]?.lastTo) {
break;
}
await new Promise((resolve) => setTimeout(resolve, 5));
}
if (!stored) throw new Error("store not loaded");
if (!stored) {
throw new Error("store not loaded");
}
expect(stored[mainSessionKey]?.lastChannel).toBe("whatsapp");
expect(stored[mainSessionKey]?.lastTo).toBe("+1000");
@@ -295,11 +299,14 @@ describe("partial reply gating", () => {
stored[groupSessionKey]?.lastChannel &&
stored[groupSessionKey]?.lastTo &&
stored[groupSessionKey]?.lastAccountId
)
) {
break;
}
await new Promise((resolve) => setTimeout(resolve, 5));
}
if (!stored) throw new Error("store not loaded");
if (!stored) {
throw new Error("store not loaded");
}
expect(stored[groupSessionKey]?.lastChannel).toBe("whatsapp");
expect(stored[groupSessionKey]?.lastTo).toBe("123@g.us");
expect(stored[groupSessionKey]?.lastAccountId).toBe("work");

View File

@@ -28,11 +28,17 @@ import { elide } from "./util.js";
function resolveHeartbeatReplyPayload(
replyResult: ReplyPayload | ReplyPayload[] | undefined,
): ReplyPayload | undefined {
if (!replyResult) return undefined;
if (!Array.isArray(replyResult)) return replyResult;
if (!replyResult) {
return undefined;
}
if (!Array.isArray(replyResult)) {
return replyResult;
}
for (let idx = replyResult.length - 1; idx >= 0; idx -= 1) {
const payload = replyResult[idx];
if (!payload) continue;
if (!payload) {
continue;
}
if (payload.text || payload.mediaUrl || (payload.mediaUrls && payload.mediaUrls.length > 0)) {
return payload;
}
@@ -221,7 +227,9 @@ export async function runWebHeartbeatOnce(opts: {
store[sessionSnapshot.key].updatedAt = sessionSnapshot.entry.updatedAt;
await updateSessionStore(storePath, (nextStore) => {
const nextEntry = nextStore[sessionSnapshot.key];
if (!nextEntry) return;
if (!nextEntry) {
return;
}
nextStore[sessionSnapshot.key] = {
...nextEntry,
updatedAt: sessionSnapshot.entry.updatedAt,

View File

@@ -45,10 +45,14 @@ export function isBotMentionedFromTargets(
const hasMentions = (msg.mentionedJids?.length ?? 0) > 0;
if (hasMentions && !isSelfChat) {
if (targets.selfE164 && targets.normalizedMentions.includes(targets.selfE164)) return true;
if (targets.selfE164 && targets.normalizedMentions.includes(targets.selfE164)) {
return true;
}
if (targets.selfJid) {
// Some mentions use the bare JID; match on E.164 to be safe.
if (targets.normalizedMentions.includes(targets.selfJid)) return true;
if (targets.normalizedMentions.includes(targets.selfJid)) {
return true;
}
}
// If the message explicitly mentions someone else, do not fall back to regex matches.
return false;
@@ -56,17 +60,23 @@ export function isBotMentionedFromTargets(
// Self-chat mode: ignore WhatsApp @mention JIDs, otherwise @mentioning the owner in group chats triggers the bot.
}
const bodyClean = clean(msg.body);
if (mentionCfg.mentionRegexes.some((re) => re.test(bodyClean))) return true;
if (mentionCfg.mentionRegexes.some((re) => re.test(bodyClean))) {
return true;
}
// Fallback: detect body containing our own number (with or without +, spacing)
if (targets.selfE164) {
const selfDigits = targets.selfE164.replace(/\D/g, "");
if (selfDigits) {
const bodyDigits = bodyClean.replace(/[^\d]/g, "");
if (bodyDigits.includes(selfDigits)) return true;
if (bodyDigits.includes(selfDigits)) {
return true;
}
const bodyNoSpace = msg.body.replace(/[\s-]/g, "");
const pattern = new RegExp(`\\+?${selfDigits}`, "i");
if (pattern.test(bodyNoSpace)) return true;
if (pattern.test(bodyNoSpace)) {
return true;
}
}
}

View File

@@ -141,7 +141,9 @@ export async function monitorWebChannel(
let reconnectAttempts = 0;
while (true) {
if (stopRequested()) break;
if (stopRequested()) {
break;
}
const connectionId = newConnectionId();
const startedAt = Date.now();
@@ -175,9 +177,15 @@ export async function monitorWebChannel(
const inboundDebounceMs = resolveInboundDebounceMs({ cfg, channel: "whatsapp" });
const shouldDebounce = (msg: WebInboundMsg) => {
if (msg.mediaPath || msg.mediaType) return false;
if (msg.location) return false;
if (msg.replyToId || msg.replyToBody) return false;
if (msg.mediaPath || msg.mediaType) {
return false;
}
if (msg.location) {
return false;
}
if (msg.replyToId || msg.replyToBody) {
return false;
}
return !hasControlCommand(msg.body, cfg);
};
@@ -219,7 +227,9 @@ export async function monitorWebChannel(
setActiveWebListener(account.accountId, listener);
unregisterUnhandled = registerUnhandledRejectionHandler((reason) => {
if (!isLikelyWhatsAppCryptoError(reason)) return false;
if (!isLikelyWhatsAppCryptoError(reason)) {
return false;
}
const errorStr = formatError(reason);
reconnectLogger.warn(
{ connectionId, error: errorStr },
@@ -239,8 +249,12 @@ export async function monitorWebChannel(
unregisterUnhandled();
unregisterUnhandled = null;
}
if (heartbeat) clearInterval(heartbeat);
if (watchdogTimer) clearInterval(watchdogTimer);
if (heartbeat) {
clearInterval(heartbeat);
}
if (watchdogTimer) {
clearInterval(watchdogTimer);
}
if (backgroundTasks.size > 0) {
await Promise.allSettled(backgroundTasks);
backgroundTasks.clear();
@@ -279,9 +293,13 @@ export async function monitorWebChannel(
}, heartbeatSeconds * 1000);
watchdogTimer = setInterval(() => {
if (!lastMessageAt) return;
if (!lastMessageAt) {
return;
}
const timeSinceLastMessage = Date.now() - lastMessageAt;
if (timeSinceLastMessage <= MESSAGE_TIMEOUT_MS) return;
if (timeSinceLastMessage <= MESSAGE_TIMEOUT_MS) {
return;
}
const minutesSinceLastMessage = Math.floor(timeSinceLastMessage / 60000);
heartbeatLogger.warn(
{

View File

@@ -17,7 +17,9 @@ export function maybeSendAckReaction(params: {
info: (obj: unknown, msg: string) => void;
warn: (obj: unknown, msg: string) => void;
}) {
if (!params.msg.id) return;
if (!params.msg.id) {
return;
}
const ackConfig = params.cfg.channels?.whatsapp?.ackReaction;
const emoji = (ackConfig?.emoji ?? "").trim();
@@ -45,7 +47,9 @@ export function maybeSendAckReaction(params: {
groupActivated: activation === "always",
});
if (!shouldSendReaction()) return;
if (!shouldSendReaction()) {
return;
}
params.info(
{ chatId: params.msg.chatId, messageId: params.msg.id, emoji },

View File

@@ -29,8 +29,12 @@ export async function maybeBroadcastMessage(params: {
) => Promise<boolean>;
}) {
const broadcastAgents = params.cfg.broadcast?.[params.peerId];
if (!broadcastAgents || !Array.isArray(broadcastAgents)) return false;
if (broadcastAgents.length === 0) return false;
if (!broadcastAgents || !Array.isArray(broadcastAgents)) {
return false;
}
if (broadcastAgents.length === 0) {
return false;
}
const strategy = params.cfg.broadcast?.strategy || "parallel";
whatsappInboundLog.info(`Broadcasting message to ${broadcastAgents.length} agents (${strategy})`);

View File

@@ -1,6 +1,8 @@
export function isStatusCommand(body: string) {
const trimmed = body.trim().toLowerCase();
if (!trimmed) return false;
if (!trimmed) {
return false;
}
return trimmed === "/status" || trimmed === "status" || trimmed.startsWith("/status ");
}

View File

@@ -25,13 +25,17 @@ export function createEchoTracker(params: {
const trim = () => {
while (recentlySent.size > maxItems) {
const firstKey = recentlySent.values().next().value;
if (!firstKey) break;
if (!firstKey) {
break;
}
recentlySent.delete(firstKey);
}
};
const rememberText: EchoTracker["rememberText"] = (text, opts) => {
if (!text) return;
if (!text) {
return;
}
recentlySent.add(text);
if (opts.combinedBody && opts.combinedBodySessionKey) {
recentlySent.add(

View File

@@ -21,7 +21,9 @@ export type GroupHistoryEntry = {
function isOwnerSender(baseMentionConfig: MentionConfig, msg: WebInboundMsg) {
const sender = normalizeE164(msg.senderE164 ?? "");
if (!sender) return false;
if (!sender) {
return false;
}
const owners = resolveOwnerList(baseMentionConfig, msg.selfE164 ?? undefined);
return owners.includes(sender);
}

View File

@@ -6,10 +6,14 @@ export function noteGroupMember(
e164?: string,
name?: string,
) {
if (!e164 || !name) return;
if (!e164 || !name) {
return;
}
const normalized = normalizeE164(e164);
const key = normalized ?? e164;
if (!key) return;
if (!key) {
return;
}
let roster = groupMemberNames.get(conversationId);
if (!roster) {
roster = new Map();
@@ -28,9 +32,13 @@ export function formatGroupMembers(params: {
const ordered: string[] = [];
if (participants?.length) {
for (const entry of participants) {
if (!entry) continue;
if (!entry) {
continue;
}
const normalized = normalizeE164(entry) ?? entry;
if (!normalized || seen.has(normalized)) continue;
if (!normalized || seen.has(normalized)) {
continue;
}
seen.add(normalized);
ordered.push(normalized);
}
@@ -38,16 +46,22 @@ export function formatGroupMembers(params: {
if (roster) {
for (const entry of roster.keys()) {
const normalized = normalizeE164(entry) ?? entry;
if (!normalized || seen.has(normalized)) continue;
if (!normalized || seen.has(normalized)) {
continue;
}
seen.add(normalized);
ordered.push(normalized);
}
}
if (ordered.length === 0 && fallbackE164) {
const normalized = normalizeE164(fallbackE164) ?? fallbackE164;
if (normalized) ordered.push(normalized);
if (normalized) {
ordered.push(normalized);
}
}
if (ordered.length === 0) {
return undefined;
}
if (ordered.length === 0) return undefined;
return ordered
.map((entry) => {
const name = roster?.get(entry);

View File

@@ -51,7 +51,9 @@ export function updateLastRouteInBackground(params: {
}
export function awaitBackgroundTasks(backgroundTasks: Set<Promise<unknown>>) {
if (backgroundTasks.size === 0) return Promise.resolve();
if (backgroundTasks.size === 0) {
return Promise.resolve();
}
return Promise.allSettled(backgroundTasks).then(() => {
backgroundTasks.clear();
});

View File

@@ -4,7 +4,9 @@ import type { loadConfig } from "../../../config/config.js";
import type { WebInboundMsg } from "../types.js";
export function formatReplyContext(msg: WebInboundMsg) {
if (!msg.replyToBody) return null;
if (!msg.replyToBody) {
return null;
}
const sender = msg.replyToSender ?? "unknown sender";
const idPart = msg.replyToId ? ` id:${msg.replyToId}` : "";
return `[Replying to ${sender}${idPart}]\n${msg.replyToBody}\n[/Replying]`;

View File

@@ -138,7 +138,9 @@ export function createWebOnMessageHandler(params: {
logVerbose,
replyLogger: params.replyLogger,
});
if (!gating.shouldProcess) return;
if (!gating.shouldProcess) {
return;
}
} else {
// Ensure `peerId` for DMs is stable and stored as E.164 when possible.
if (!msg.senderE164 && peerId && peerId.startsWith("+")) {

View File

@@ -2,8 +2,14 @@ import { jidToE164, normalizeE164 } from "../../../utils.js";
import type { WebInboundMsg } from "../types.js";
export function resolvePeerId(msg: WebInboundMsg) {
if (msg.chatType === "group") return msg.conversationId ?? msg.from;
if (msg.senderE164) return normalizeE164(msg.senderE164) ?? msg.senderE164;
if (msg.from.includes("@")) return jidToE164(msg.from) ?? msg.from;
if (msg.chatType === "group") {
return msg.conversationId ?? msg.from;
}
if (msg.senderE164) {
return normalizeE164(msg.senderE164) ?? msg.senderE164;
}
if (msg.from.includes("@")) {
return jidToE164(msg.from) ?? msg.from;
}
return normalizeE164(msg.from) ?? msg.from;
}

View File

@@ -60,13 +60,17 @@ async function resolveWhatsAppCommandAuthorized(params: {
msg: WebInboundMsg;
}): Promise<boolean> {
const useAccessGroups = params.cfg.commands?.useAccessGroups !== false;
if (!useAccessGroups) return true;
if (!useAccessGroups) {
return true;
}
const isGroup = params.msg.chatType === "group";
const senderE164 = normalizeE164(
isGroup ? (params.msg.senderE164 ?? "") : (params.msg.senderE164 ?? params.msg.from ?? ""),
);
if (!senderE164) return false;
if (!senderE164) {
return false;
}
const configuredAllowFrom = params.cfg.channels?.whatsapp?.allowFrom ?? [];
const configuredGroupAllowFrom =
@@ -74,8 +78,12 @@ async function resolveWhatsAppCommandAuthorized(params: {
(configuredAllowFrom.length > 0 ? configuredAllowFrom : undefined);
if (isGroup) {
if (!configuredGroupAllowFrom || configuredGroupAllowFrom.length === 0) return false;
if (configuredGroupAllowFrom.some((v) => String(v).trim() === "*")) return true;
if (!configuredGroupAllowFrom || configuredGroupAllowFrom.length === 0) {
return false;
}
if (configuredGroupAllowFrom.some((v) => String(v).trim() === "*")) {
return true;
}
return normalizeAllowFromE164(configuredGroupAllowFrom).includes(senderE164);
}
@@ -89,7 +97,9 @@ async function resolveWhatsAppCommandAuthorized(params: {
: params.msg.selfE164
? [params.msg.selfE164]
: [];
if (allowFrom.some((v) => String(v).trim() === "*")) return true;
if (allowFrom.some((v) => String(v).trim() === "*")) {
return true;
}
return normalizeAllowFromE164(allowFrom).includes(senderE164);
}
@@ -221,9 +231,13 @@ export async function processMessage(params: {
const dmRouteTarget =
params.msg.chatType !== "group"
? (() => {
if (params.msg.senderE164) return normalizeE164(params.msg.senderE164);
if (params.msg.senderE164) {
return normalizeE164(params.msg.senderE164);
}
// In direct chats, `msg.from` is already the canonical conversation id.
if (params.msg.from.includes("@")) return jidToE164(params.msg.from);
if (params.msg.from.includes("@")) {
return jidToE164(params.msg.from);
}
return normalizeE164(params.msg.from);
})()
: undefined;

View File

@@ -1,13 +1,21 @@
export function elide(text?: string, limit = 400) {
if (!text) return text;
if (text.length <= limit) return text;
if (!text) {
return text;
}
if (text.length <= limit) {
return text;
}
return `${text.slice(0, limit)}… (truncated ${text.length - limit} chars)`;
}
export function isLikelyWhatsAppCryptoError(reason: unknown) {
const formatReason = (value: unknown): string => {
if (value == null) return "";
if (typeof value === "string") return value;
if (value == null) {
return "";
}
if (typeof value === "string") {
return value;
}
if (value instanceof Error) {
return `${value.message}\n${value.stack ?? ""}`;
}
@@ -18,11 +26,21 @@ export function isLikelyWhatsAppCryptoError(reason: unknown) {
return Object.prototype.toString.call(value);
}
}
if (typeof value === "number") return String(value);
if (typeof value === "boolean") return String(value);
if (typeof value === "bigint") return String(value);
if (typeof value === "symbol") return value.description ?? value.toString();
if (typeof value === "function") return value.name ? `[function ${value.name}]` : "[function]";
if (typeof value === "number") {
return String(value);
}
if (typeof value === "boolean") {
return String(value);
}
if (typeof value === "bigint") {
return String(value);
}
if (typeof value === "symbol") {
return value.description ?? value.toString();
}
if (typeof value === "function") {
return value.name ? `[function ${value.name}]` : "[function]";
}
return Object.prototype.toString.call(value);
};
const raw =
@@ -31,7 +49,9 @@ export function isLikelyWhatsAppCryptoError(reason: unknown) {
const hasAuthError =
haystack.includes("unsupported state or unable to authenticate data") ||
haystack.includes("bad mac");
if (!hasAuthError) return false;
if (!hasAuthError) {
return false;
}
return (
haystack.includes("@whiskeysockets/baileys") ||
haystack.includes("baileys") ||

View File

@@ -128,7 +128,9 @@ describe("web inbound media saves with extension", () => {
// Allow a brief window for the async handler to fire on slower hosts.
for (let i = 0; i < 50; i++) {
if (onMessage.mock.calls.length > 0) break;
if (onMessage.mock.calls.length > 0) {
break;
}
await new Promise((resolve) => setTimeout(resolve, 10));
}
@@ -179,7 +181,9 @@ describe("web inbound media saves with extension", () => {
realSock.ev.emit("messages.upsert", upsert);
for (let i = 0; i < 50; i++) {
if (onMessage.mock.calls.length > 0) break;
if (onMessage.mock.calls.length > 0) {
break;
}
await new Promise((resolve) => setTimeout(resolve, 10));
}
@@ -219,7 +223,9 @@ describe("web inbound media saves with extension", () => {
realSock.ev.emit("messages.upsert", upsert);
for (let i = 0; i < 50; i++) {
if (onMessage.mock.calls.length > 0) break;
if (onMessage.mock.calls.length > 0) {
break;
}
await new Promise((resolve) => setTimeout(resolve, 10));
}

View File

@@ -15,14 +15,18 @@ function unwrapMessage(message: proto.IMessage | undefined): proto.IMessage | un
}
function extractContextInfo(message: proto.IMessage | undefined): proto.IContextInfo | undefined {
if (!message) return undefined;
if (!message) {
return undefined;
}
const contentType = getContentType(message);
const candidate = contentType ? (message as Record<string, unknown>)[contentType] : undefined;
const contextInfo =
candidate && typeof candidate === "object" && "contextInfo" in candidate
? (candidate as { contextInfo?: proto.IContextInfo }).contextInfo
: undefined;
if (contextInfo) return contextInfo;
if (contextInfo) {
return contextInfo;
}
const fallback =
message.extendedTextMessage?.contextInfo ??
message.imageMessage?.contextInfo ??
@@ -36,19 +40,29 @@ function extractContextInfo(message: proto.IMessage | undefined): proto.IContext
message.interactiveResponseMessage?.contextInfo ??
message.buttonsMessage?.contextInfo ??
message.listMessage?.contextInfo;
if (fallback) return fallback;
if (fallback) {
return fallback;
}
for (const value of Object.values(message)) {
if (!value || typeof value !== "object") continue;
if (!("contextInfo" in value)) continue;
if (!value || typeof value !== "object") {
continue;
}
if (!("contextInfo" in value)) {
continue;
}
const candidateContext = (value as { contextInfo?: proto.IContextInfo }).contextInfo;
if (candidateContext) return candidateContext;
if (candidateContext) {
return candidateContext;
}
}
return undefined;
}
export function extractMentionedJids(rawMessage: proto.IMessage | undefined): string[] | undefined {
const message = unwrapMessage(rawMessage);
if (!message) return undefined;
if (!message) {
return undefined;
}
const candidates: Array<string[] | null | undefined> = [
message.extendedTextMessage?.contextInfo?.mentionedJid,
@@ -64,34 +78,46 @@ export function extractMentionedJids(rawMessage: proto.IMessage | undefined): st
];
const flattened = candidates.flatMap((arr) => arr ?? []).filter(Boolean);
if (flattened.length === 0) return undefined;
if (flattened.length === 0) {
return undefined;
}
return Array.from(new Set(flattened));
}
export function extractText(rawMessage: proto.IMessage | undefined): string | undefined {
const message = unwrapMessage(rawMessage);
if (!message) return undefined;
if (!message) {
return undefined;
}
const extracted = extractMessageContent(message);
const candidates = [message, extracted && extracted !== message ? extracted : undefined];
for (const candidate of candidates) {
if (!candidate) continue;
if (!candidate) {
continue;
}
if (typeof candidate.conversation === "string" && candidate.conversation.trim()) {
return candidate.conversation.trim();
}
const extended = candidate.extendedTextMessage?.text;
if (extended?.trim()) return extended.trim();
if (extended?.trim()) {
return extended.trim();
}
const caption =
candidate.imageMessage?.caption ??
candidate.videoMessage?.caption ??
candidate.documentMessage?.caption;
if (caption?.trim()) return caption.trim();
if (caption?.trim()) {
return caption.trim();
}
}
const contactPlaceholder =
extractContactPlaceholder(message) ??
(extracted && extracted !== message
? extractContactPlaceholder(extracted as proto.IMessage | undefined)
: undefined);
if (contactPlaceholder) return contactPlaceholder;
if (contactPlaceholder) {
return contactPlaceholder;
}
return undefined;
}
@@ -99,18 +125,32 @@ export function extractMediaPlaceholder(
rawMessage: proto.IMessage | undefined,
): string | undefined {
const message = unwrapMessage(rawMessage);
if (!message) return undefined;
if (message.imageMessage) return "<media:image>";
if (message.videoMessage) return "<media:video>";
if (message.audioMessage) return "<media:audio>";
if (message.documentMessage) return "<media:document>";
if (message.stickerMessage) return "<media:sticker>";
if (!message) {
return undefined;
}
if (message.imageMessage) {
return "<media:image>";
}
if (message.videoMessage) {
return "<media:video>";
}
if (message.audioMessage) {
return "<media:audio>";
}
if (message.documentMessage) {
return "<media:document>";
}
if (message.stickerMessage) {
return "<media:sticker>";
}
return undefined;
}
function extractContactPlaceholder(rawMessage: proto.IMessage | undefined): string | undefined {
const message = unwrapMessage(rawMessage);
if (!message) return undefined;
if (!message) {
return undefined;
}
const contact = message.contactMessage ?? undefined;
if (contact) {
const { name, phones } = describeContact({
@@ -120,7 +160,9 @@ function extractContactPlaceholder(rawMessage: proto.IMessage | undefined): stri
return formatContactPlaceholder(name, phones);
}
const contactsArray = message.contactsArrayMessage?.contacts ?? undefined;
if (!contactsArray || contactsArray.length === 0) return undefined;
if (!contactsArray || contactsArray.length === 0) {
return undefined;
}
const labels = contactsArray
.map((entry) => describeContact({ displayName: entry.displayName, vcard: entry.vcard }))
.map((entry) => formatContactLabel(entry.name, entry.phones))
@@ -140,7 +182,9 @@ function describeContact(input: { displayName?: string | null; vcard?: string |
function formatContactPlaceholder(name?: string, phones?: string[]): string {
const label = formatContactLabel(name, phones);
if (!label) return "<contact>";
if (!label) {
return "<contact>";
}
return `<contact: ${label}>`;
}
@@ -158,17 +202,25 @@ function formatContactsPlaceholder(labels: string[], total: number): string {
function formatContactLabel(name?: string, phones?: string[]): string | undefined {
const phoneLabel = formatPhoneList(phones);
const parts = [name, phoneLabel].filter((value): value is string => Boolean(value));
if (parts.length === 0) return undefined;
if (parts.length === 0) {
return undefined;
}
return parts.join(", ");
}
function formatPhoneList(phones?: string[]): string | undefined {
const cleaned = phones?.map((phone) => phone.trim()).filter(Boolean) ?? [];
if (cleaned.length === 0) return undefined;
if (cleaned.length === 0) {
return undefined;
}
const { shown, remaining } = summarizeList(cleaned, cleaned.length, 1);
const [primary] = shown;
if (!primary) return undefined;
if (remaining === 0) return primary;
if (!primary) {
return undefined;
}
if (remaining === 0) {
return primary;
}
return `${primary} (+${remaining} more)`;
}
@@ -186,7 +238,9 @@ export function extractLocationData(
rawMessage: proto.IMessage | undefined,
): NormalizedLocation | null {
const message = unwrapMessage(rawMessage);
if (!message) return null;
if (!message) {
return null;
}
const live = message.liveLocationMessage ?? undefined;
if (live) {
@@ -242,15 +296,21 @@ export function describeReplyContext(rawMessage: proto.IMessage | undefined): {
senderE164?: string;
} | null {
const message = unwrapMessage(rawMessage);
if (!message) return null;
if (!message) {
return null;
}
const contextInfo = extractContextInfo(message);
const quoted = normalizeMessageContent(contextInfo?.quotedMessage as proto.IMessage | undefined);
if (!quoted) return null;
if (!quoted) {
return null;
}
const location = extractLocationData(quoted);
const locationText = location ? formatLocationText(location) : undefined;
const text = extractText(quoted);
let body: string | undefined = [text, locationText].filter(Boolean).join("\n").trim();
if (!body) body = extractMediaPlaceholder(quoted);
if (!body) {
body = extractMediaPlaceholder(quoted);
}
if (!body) {
const quotedType = quoted ? getContentType(quoted) : undefined;
logVerbose(

View File

@@ -13,7 +13,9 @@ export async function downloadInboundMedia(
sock: Awaited<ReturnType<typeof createWaSocket>>,
): Promise<{ buffer: Buffer; mimetype?: string } | undefined> {
const message = unwrapMessage(msg.message as proto.IMessage | undefined);
if (!message) return undefined;
if (!message) {
return undefined;
}
const mimetype =
message.imageMessage?.mimetype ??
message.videoMessage?.mimetype ??

View File

@@ -48,7 +48,9 @@ export async function monitorWebInbox(options: {
onCloseResolve = resolve;
});
const resolveClose = (reason: WebListenerCloseReason) => {
if (!onCloseResolve) return;
if (!onCloseResolve) {
return;
}
const resolver = onCloseResolve;
onCloseResolve = null;
resolver(reason);
@@ -56,7 +58,9 @@ export async function monitorWebInbox(options: {
try {
await sock.sendPresenceUpdate("available");
if (shouldLogVerbose()) logVerbose("Sent global 'available' presence on connect");
if (shouldLogVerbose()) {
logVerbose("Sent global 'available' presence on connect");
}
} catch (err) {
logVerbose(`Failed to send 'available' presence on connect: ${String(err)}`);
}
@@ -70,21 +74,27 @@ export async function monitorWebInbox(options: {
msg.chatType === "group"
? (msg.senderJid ?? msg.senderE164 ?? msg.senderName ?? msg.from)
: msg.from;
if (!senderKey) return null;
if (!senderKey) {
return null;
}
const conversationKey = msg.chatType === "group" ? msg.chatId : msg.from;
return `${msg.accountId}:${conversationKey}:${senderKey}`;
},
shouldDebounce: options.shouldDebounce,
onFlush: async (entries) => {
const last = entries.at(-1);
if (!last) return;
if (!last) {
return;
}
if (entries.length === 1) {
await options.onMessage(last);
return;
}
const mentioned = new Set<string>();
for (const entry of entries) {
for (const jid of entry.mentionedJids ?? []) mentioned.add(jid);
for (const jid of entry.mentionedJids ?? []) {
mentioned.add(jid);
}
}
const combinedBody = entries
.map((entry) => entry.body)
@@ -114,7 +124,9 @@ export async function monitorWebInbox(options: {
const getGroupMeta = async (jid: string) => {
const cached = groupMetaCache.get(jid);
if (cached && cached.expires > Date.now()) return cached;
if (cached && cached.expires > Date.now()) {
return cached;
}
try {
const meta = await sock.groupMetadata(jid);
const participants =
@@ -140,7 +152,9 @@ export async function monitorWebInbox(options: {
};
const handleMessagesUpsert = async (upsert: { type?: string; messages?: Array<WAMessage> }) => {
if (upsert.type !== "notify" && upsert.type !== "append") return;
if (upsert.type !== "notify" && upsert.type !== "append") {
return;
}
for (const msg of upsert.messages ?? []) {
recordChannelActivity({
channel: "whatsapp",
@@ -149,17 +163,25 @@ export async function monitorWebInbox(options: {
});
const id = msg.key?.id ?? undefined;
const remoteJid = msg.key?.remoteJid;
if (!remoteJid) continue;
if (remoteJid.endsWith("@status") || remoteJid.endsWith("@broadcast")) continue;
if (!remoteJid) {
continue;
}
if (remoteJid.endsWith("@status") || remoteJid.endsWith("@broadcast")) {
continue;
}
const group = isJidGroup(remoteJid) === true;
if (id) {
const dedupeKey = `${options.accountId}:${remoteJid}:${id}`;
if (isRecentInboundMessage(dedupeKey)) continue;
if (isRecentInboundMessage(dedupeKey)) {
continue;
}
}
const participantJid = msg.key?.participant ?? undefined;
const from = group ? remoteJid : await resolveInboundJid(remoteJid);
if (!from) continue;
if (!from) {
continue;
}
const senderE164 = group
? participantJid
? await resolveInboundJid(participantJid)
@@ -190,7 +212,9 @@ export async function monitorWebInbox(options: {
sock: { sendMessage: (jid, content) => sock.sendMessage(jid, content) },
remoteJid,
});
if (!access.allowed) continue;
if (!access.allowed) {
continue;
}
if (id && !access.isSelfChat && options.sendReadReceipts !== false) {
const participant = msg.key?.participant;
@@ -209,7 +233,9 @@ export async function monitorWebInbox(options: {
}
// If this is history/offline catch-up, mark read above but skip auto-reply.
if (upsert.type === "append") continue;
if (upsert.type === "append") {
continue;
}
const location = extractLocationData(msg.message ?? undefined);
const locationText = location ? formatLocationText(location) : undefined;
@@ -219,7 +245,9 @@ export async function monitorWebInbox(options: {
}
if (!body) {
body = extractMediaPlaceholder(msg.message ?? undefined);
if (!body) continue;
if (!body) {
continue;
}
}
const replyContext = describeReplyContext(msg.message as proto.IMessage | undefined);

View File

@@ -66,18 +66,24 @@ function attachLoginWaiter(accountId: string, login: ActiveLogin) {
login.waitPromise = waitForWaConnection(login.sock)
.then(() => {
const current = activeLogins.get(accountId);
if (current?.id === login.id) current.connected = true;
if (current?.id === login.id) {
current.connected = true;
}
})
.catch((err) => {
const current = activeLogins.get(accountId);
if (current?.id !== login.id) return;
if (current?.id !== login.id) {
return;
}
current.error = formatError(err);
current.errorStatus = getStatusCode(err);
});
}
async function restartLoginSocket(login: ActiveLogin, runtime: RuntimeEnv) {
if (login.restartAttempted) return false;
if (login.restartAttempted) {
return false;
}
login.restartAttempted = true;
runtime.log(
info("WhatsApp asked for a restart after pairing (code 515); retrying connection once…"),
@@ -151,10 +157,14 @@ export async function startWebLoginWithQr(
sock = await createWaSocket(false, Boolean(opts.verbose), {
authDir: account.authDir,
onQr: (qr: string) => {
if (pendingQr) return;
if (pendingQr) {
return;
}
pendingQr = qr;
const current = activeLogins.get(account.accountId);
if (current && !current.qr) current.qr = qr;
if (current && !current.qr) {
current.qr = qr;
}
clearTimeout(qrTimer);
runtime.log(info("WhatsApp QR received."));
resolveQr?.(qr);
@@ -180,7 +190,9 @@ export async function startWebLoginWithQr(
verbose: Boolean(opts.verbose),
};
activeLogins.set(account.accountId, login);
if (pendingQr && !login.qr) login.qr = pendingQr;
if (pendingQr && !login.qr) {
login.qr = pendingQr;
}
attachLoginWaiter(account.accountId, login);
let qr: string;

View File

@@ -118,7 +118,9 @@ describe("web media loading", () => {
if (name === "content-disposition") {
return 'attachment; filename="report.pdf"';
}
if (name === "content-type") return "application/pdf";
if (name === "content-type") {
return "application/pdf";
}
return null;
},
},

View File

@@ -43,15 +43,23 @@ function formatCapReduce(label: string, cap: number, size: number): string {
}
function isHeicSource(opts: { contentType?: string; fileName?: string }): boolean {
if (opts.contentType && HEIC_MIME_RE.test(opts.contentType.trim())) return true;
if (opts.fileName && HEIC_EXT_RE.test(opts.fileName.trim())) return true;
if (opts.contentType && HEIC_MIME_RE.test(opts.contentType.trim())) {
return true;
}
if (opts.fileName && HEIC_EXT_RE.test(opts.fileName.trim())) {
return true;
}
return false;
}
function toJpegFileName(fileName?: string): string | undefined {
if (!fileName) return undefined;
if (!fileName) {
return undefined;
}
const trimmed = fileName.trim();
if (!trimmed) return fileName;
if (!trimmed) {
return fileName;
}
const parsed = path.parse(trimmed);
if (!parsed.ext || HEIC_EXT_RE.test(parsed.ext)) {
return path.format({ dir: parsed.dir, name: parsed.name || trimmed, ext: ".jpg" });
@@ -69,8 +77,12 @@ type OptimizedImage = {
};
function logOptimizedImage(params: { originalSize: number; optimized: OptimizedImage }): void {
if (!shouldLogVerbose()) return;
if (params.optimized.optimizedSize >= params.originalSize) return;
if (!shouldLogVerbose()) {
return;
}
if (params.optimized.optimizedSize >= params.originalSize) {
return;
}
if (params.optimized.format === "png") {
logVerbose(
`Optimized PNG (preserving alpha) from ${formatMb(params.originalSize)}MB to ${formatMb(params.optimized.optimizedSize)}MB (side≤${params.optimized.resizeSide}px)`,
@@ -207,7 +219,9 @@ async function loadWebMediaInternal(
let fileName = path.basename(mediaUrl) || undefined;
if (fileName && !path.extname(fileName) && mime) {
const ext = extensionForMime(mime);
if (ext) fileName = `${fileName}${ext}`;
if (ext) {
fileName = `${fileName}${ext}`;
}
}
return await clampAndFinalize({
buffer: data,

View File

@@ -111,7 +111,9 @@ export async function renderQrPngBase64(
const buf = Buffer.alloc(size * size * 4, 255);
for (let row = 0; row < modules; row += 1) {
for (let col = 0; col < modules; col += 1) {
if (!qr.isDark(row, col)) continue;
if (!qr.isDark(row, col)) {
continue;
}
const startX = (col + marginModules) * scale;
const startY = (row + marginModules) * scale;
for (let y = 0; y < scale; y += 1) {

View File

@@ -21,7 +21,9 @@ const clamp = (val: number, min: number, max: number) => Math.max(min, Math.min(
export function resolveHeartbeatSeconds(cfg: OpenClawConfig, overrideSeconds?: number): number {
const candidate = overrideSeconds ?? cfg.web?.heartbeatSeconds;
if (typeof candidate === "number" && candidate > 0) return candidate;
if (typeof candidate === "number" && candidate > 0) {
return candidate;
}
return DEFAULT_HEARTBEAT_SECONDS;
}

View File

@@ -62,7 +62,9 @@ describe("web session", () => {
it("logWebSelfId prints cached E.164 when creds exist", () => {
const existsSpy = vi.spyOn(fsSync, "existsSync").mockImplementation((p) => {
if (typeof p !== "string") return false;
if (typeof p !== "string") {
return false;
}
return p.endsWith("creds.json");
});
const readSpy = vi.spyOn(fsSync, "readFileSync").mockImplementation((p) => {
@@ -110,7 +112,9 @@ describe("web session", () => {
const copySpy = vi.spyOn(fsSync, "copyFileSync").mockImplementation(() => {});
const existsSpy = vi.spyOn(fsSync, "existsSync").mockImplementation((p) => {
if (typeof p !== "string") return false;
if (typeof p !== "string") {
return false;
}
return p.endsWith(credsSuffix);
});
const statSpy = vi.spyOn(fsSync, "statSync").mockImplementation((p) => {
@@ -193,7 +197,9 @@ describe("web session", () => {
const copySpy = vi.spyOn(fsSync, "copyFileSync").mockImplementation(() => {});
const existsSpy = vi.spyOn(fsSync, "existsSync").mockImplementation((p) => {
if (typeof p !== "string") return false;
if (typeof p !== "string") {
return false;
}
return p.endsWith(credsSuffix);
});
const statSpy = vi.spyOn(fsSync, "statSync").mockImplementation((p) => {

View File

@@ -46,9 +46,13 @@ function enqueueSaveCreds(
function readCredsJsonRaw(filePath: string): string | null {
try {
if (!fsSync.existsSync(filePath)) return null;
if (!fsSync.existsSync(filePath)) {
return null;
}
const stats = fsSync.statSync(filePath);
if (!stats.isFile() || stats.size <= 1) return null;
if (!stats.isFile() || stats.size <= 1) {
return null;
}
return fsSync.readFileSync(filePath, "utf-8");
} catch {
return null;
@@ -197,7 +201,9 @@ function safeStringify(value: unknown, limit = 800): string {
const raw = JSON.stringify(
value,
(_key, v) => {
if (typeof v === "bigint") return v.toString();
if (typeof v === "bigint") {
return v.toString();
}
if (typeof v === "function") {
const maybeName = (v as { name?: unknown }).name;
const name =
@@ -205,14 +211,18 @@ function safeStringify(value: unknown, limit = 800): string {
return `[Function ${name}]`;
}
if (typeof v === "object" && v) {
if (seen.has(v)) return "[Circular]";
if (seen.has(v)) {
return "[Circular]";
}
seen.add(v);
}
return v;
},
2,
);
if (!raw) return String(value);
if (!raw) {
return String(value);
}
return raw.length > limit ? `${raw.slice(0, limit)}` : raw;
} catch {
return String(value);
@@ -224,11 +234,15 @@ function extractBoomDetails(err: unknown): {
error?: string;
message?: string;
} | null {
if (!err || typeof err !== "object") return null;
if (!err || typeof err !== "object") {
return null;
}
const output = (err as { output?: unknown })?.output as
| { statusCode?: unknown; payload?: unknown }
| undefined;
if (!output || typeof output !== "object") return null;
if (!output || typeof output !== "object") {
return null;
}
const payload = (output as { payload?: unknown }).payload as
| { error?: unknown; message?: unknown; statusCode?: unknown }
| undefined;
@@ -240,14 +254,22 @@ function extractBoomDetails(err: unknown): {
: undefined;
const error = typeof payload?.error === "string" ? payload.error : undefined;
const message = typeof payload?.message === "string" ? payload.message : undefined;
if (!statusCode && !error && !message) return null;
if (!statusCode && !error && !message) {
return null;
}
return { statusCode, error, message };
}
export function formatError(err: unknown): string {
if (err instanceof Error) return err.message;
if (typeof err === "string") return err;
if (!err || typeof err !== "object") return String(err);
if (err instanceof Error) {
return err.message;
}
if (typeof err === "string") {
return err;
}
if (!err || typeof err !== "object") {
return String(err);
}
// Baileys frequently wraps errors under `error` with a Boom-like shape.
const boom =
@@ -271,12 +293,22 @@ export function formatError(err: unknown): string {
const message = messageCandidates[0];
const pieces: string[] = [];
if (typeof status === "number") pieces.push(`status=${status}`);
if (boom?.error) pieces.push(boom.error);
if (message) pieces.push(message);
if (codeText) pieces.push(`code=${codeText}`);
if (typeof status === "number") {
pieces.push(`status=${status}`);
}
if (boom?.error) {
pieces.push(boom.error);
}
if (message) {
pieces.push(message);
}
if (codeText) {
pieces.push(`code=${codeText}`);
}
if (pieces.length > 0) return pieces.join(" ");
if (pieces.length > 0) {
return pieces.join(" ");
}
return safeStringify(err);
}

View File

@@ -37,7 +37,9 @@ vi.mock("../config/config.js", async (importOriginal) => {
...actual,
loadConfig: () => {
const getter = (globalThis as Record<symbol, unknown>)[CONFIG_KEY];
if (typeof getter === "function") return getter();
if (typeof getter === "function") {
return getter();
}
return DEFAULT_CONFIG;
},
};
@@ -84,7 +86,11 @@ export function resetBaileysMocks() {
export function getLastSocket(): MockBaileysSocket {
const getter = (globalThis as Record<PropertyKey, unknown>)[Symbol.for("openclaw:lastSocket")];
if (typeof getter === "function") return (getter as () => MockBaileysSocket)();
if (!getter) throw new Error("Baileys mock not initialized");
if (typeof getter === "function") {
return (getter as () => MockBaileysSocket)();
}
if (!getter) {
throw new Error("Baileys mock not initialized");
}
throw new Error("Invalid Baileys socket getter");
}

View File

@@ -6,23 +6,35 @@ type ParsedVcard = {
const ALLOWED_VCARD_KEYS = new Set(["FN", "N", "TEL"]);
export function parseVcard(vcard?: string): ParsedVcard {
if (!vcard) return { phones: [] };
if (!vcard) {
return { phones: [] };
}
const lines = vcard.split(/\r?\n/);
let nameFromN: string | undefined;
let nameFromFn: string | undefined;
const phones: string[] = [];
for (const rawLine of lines) {
const line = rawLine.trim();
if (!line) continue;
if (!line) {
continue;
}
const colonIndex = line.indexOf(":");
if (colonIndex === -1) continue;
if (colonIndex === -1) {
continue;
}
const key = line.slice(0, colonIndex).toUpperCase();
const rawValue = line.slice(colonIndex + 1).trim();
if (!rawValue) continue;
if (!rawValue) {
continue;
}
const baseKey = normalizeVcardKey(key);
if (!baseKey || !ALLOWED_VCARD_KEYS.has(baseKey)) continue;
if (!baseKey || !ALLOWED_VCARD_KEYS.has(baseKey)) {
continue;
}
const value = cleanVcardValue(rawValue);
if (!value) continue;
if (!value) {
continue;
}
if (baseKey === "FN" && !nameFromFn) {
nameFromFn = normalizeVcardName(value);
continue;
@@ -33,7 +45,9 @@ export function parseVcard(vcard?: string): ParsedVcard {
}
if (baseKey === "TEL") {
const phone = normalizeVcardPhone(value);
if (phone) phones.push(phone);
if (phone) {
phones.push(phone);
}
}
}
return { name: nameFromFn ?? nameFromN, phones };
@@ -41,7 +55,9 @@ export function parseVcard(vcard?: string): ParsedVcard {
function normalizeVcardKey(key: string): string | undefined {
const [primary] = key.split(";");
if (!primary) return undefined;
if (!primary) {
return undefined;
}
const segments = primary.split(".");
return segments[segments.length - 1] || undefined;
}
@@ -56,7 +72,9 @@ function normalizeVcardName(value: string): string {
function normalizeVcardPhone(value: string): string {
const trimmed = value.trim();
if (!trimmed) return "";
if (!trimmed) {
return "";
}
if (trimmed.toLowerCase().startsWith("tel:")) {
return trimmed.slice(4).trim();
}