mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-03 03:03:24 -04:00
chore: Enable "curly" rule to avoid single-statement if confusion/errors.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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})`);
|
||||
|
||||
@@ -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 ");
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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]`;
|
||||
|
||||
@@ -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("+")) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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") ||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 ??
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user