chore: migrate to oxlint and oxfmt
Co-authored-by: Christoph Nakazawa <christoph.pojer@gmail.com>
This commit is contained in:
@@ -20,15 +20,7 @@ export async function deliverWebReply(params: {
|
||||
connectionId?: string;
|
||||
skipLog?: boolean;
|
||||
}) {
|
||||
const {
|
||||
replyResult,
|
||||
msg,
|
||||
maxMediaBytes,
|
||||
textLimit,
|
||||
replyLogger,
|
||||
connectionId,
|
||||
skipLog,
|
||||
} = params;
|
||||
const { replyResult, msg, maxMediaBytes, textLimit, replyLogger, connectionId, skipLog } = params;
|
||||
const replyStarted = Date.now();
|
||||
const textChunks = chunkMarkdownText(replyResult.text || "", textLimit);
|
||||
const mediaList = replyResult.mediaUrls?.length
|
||||
@@ -37,14 +29,9 @@ export async function deliverWebReply(params: {
|
||||
? [replyResult.mediaUrl]
|
||||
: [];
|
||||
|
||||
const sleep = (ms: number) =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms));
|
||||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
const sendWithRetry = async (
|
||||
fn: () => Promise<unknown>,
|
||||
label: string,
|
||||
maxAttempts = 3,
|
||||
) => {
|
||||
const sendWithRetry = async (fn: () => Promise<unknown>, label: string, maxAttempts = 3) => {
|
||||
let lastErr: unknown;
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
@@ -53,9 +40,7 @@ export async function deliverWebReply(params: {
|
||||
lastErr = err;
|
||||
const errText = formatError(err);
|
||||
const isLast = attempt === maxAttempts;
|
||||
const shouldRetry = /closed|reset|timed\\s*out|disconnect/i.test(
|
||||
errText,
|
||||
);
|
||||
const shouldRetry = /closed|reset|timed\\s*out|disconnect/i.test(errText);
|
||||
if (!shouldRetry || isLast) {
|
||||
throw err;
|
||||
}
|
||||
@@ -103,17 +88,14 @@ export async function deliverWebReply(params: {
|
||||
|
||||
// Media (with optional caption on first item)
|
||||
for (const [index, mediaUrl] of mediaList.entries()) {
|
||||
const caption =
|
||||
index === 0 ? remainingText.shift() || undefined : undefined;
|
||||
const caption = index === 0 ? remainingText.shift() || undefined : undefined;
|
||||
try {
|
||||
const media = await loadWebMedia(mediaUrl, maxMediaBytes);
|
||||
if (shouldLogVerbose()) {
|
||||
logVerbose(
|
||||
`Web auto-reply media size: ${(media.buffer.length / (1024 * 1024)).toFixed(2)}MB`,
|
||||
);
|
||||
logVerbose(
|
||||
`Web auto-reply media source: ${mediaUrl} (kind ${media.kind})`,
|
||||
);
|
||||
logVerbose(`Web auto-reply media source: ${mediaUrl} (kind ${media.kind})`);
|
||||
}
|
||||
if (media.kind === "image") {
|
||||
await sendWithRetry(
|
||||
@@ -178,24 +160,15 @@ export async function deliverWebReply(params: {
|
||||
"auto-reply sent (media)",
|
||||
);
|
||||
} catch (err) {
|
||||
whatsappOutboundLog.error(
|
||||
`Failed sending web media to ${msg.from}: ${formatError(err)}`,
|
||||
);
|
||||
whatsappOutboundLog.error(`Failed sending web media to ${msg.from}: ${formatError(err)}`);
|
||||
replyLogger.warn({ err, mediaUrl }, "failed to send web media reply");
|
||||
if (index === 0) {
|
||||
const warning =
|
||||
err instanceof Error
|
||||
? `⚠️ Media failed: ${err.message}`
|
||||
: "⚠️ Media failed.";
|
||||
const fallbackTextParts = [
|
||||
remainingText.shift() ?? caption ?? "",
|
||||
warning,
|
||||
].filter(Boolean);
|
||||
err instanceof Error ? `⚠️ Media failed: ${err.message}` : "⚠️ Media failed.";
|
||||
const fallbackTextParts = [remainingText.shift() ?? caption ?? "", warning].filter(Boolean);
|
||||
const fallbackText = fallbackTextParts.join("\n");
|
||||
if (fallbackText) {
|
||||
whatsappOutboundLog.warn(
|
||||
`Media skipped; sent text-only to ${msg.from}`,
|
||||
);
|
||||
whatsappOutboundLog.warn(`Media skipped; sent text-only to ${msg.from}`);
|
||||
await msg.reply(fallbackText);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,11 +31,7 @@ function resolveHeartbeatReplyPayload(
|
||||
for (let idx = replyResult.length - 1; idx >= 0; idx -= 1) {
|
||||
const payload = replyResult[idx];
|
||||
if (!payload) continue;
|
||||
if (
|
||||
payload.text ||
|
||||
payload.mediaUrl ||
|
||||
(payload.mediaUrls && payload.mediaUrls.length > 0)
|
||||
) {
|
||||
if (payload.text || payload.mediaUrl || (payload.mediaUrls && payload.mediaUrls.length > 0)) {
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
@@ -52,14 +48,7 @@ export async function runWebHeartbeatOnce(opts: {
|
||||
overrideBody?: string;
|
||||
dryRun?: boolean;
|
||||
}) {
|
||||
const {
|
||||
cfg: cfgOverride,
|
||||
to,
|
||||
verbose = false,
|
||||
sessionId,
|
||||
overrideBody,
|
||||
dryRun = false,
|
||||
} = opts;
|
||||
const { cfg: cfgOverride, to, verbose = false, sessionId, overrideBody, dryRun = false } = opts;
|
||||
const replyResolver = opts.replyResolver ?? getReplyFromConfig;
|
||||
const sender = opts.sender ?? sendMessageWhatsApp;
|
||||
const runId = newConnectionId();
|
||||
@@ -127,9 +116,7 @@ export async function runWebHeartbeatOnce(opts: {
|
||||
},
|
||||
"manual heartbeat message sent",
|
||||
);
|
||||
whatsappHeartbeatLog.info(
|
||||
`manual heartbeat sent to ${to} (id ${sendResult.messageId})`,
|
||||
);
|
||||
whatsappHeartbeatLog.info(`manual heartbeat sent to ${to} (id ${sendResult.messageId})`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -147,9 +134,7 @@ export async function runWebHeartbeatOnce(opts: {
|
||||
|
||||
if (
|
||||
!replyPayload ||
|
||||
(!replyPayload.text &&
|
||||
!replyPayload.mediaUrl &&
|
||||
!replyPayload.mediaUrls?.length)
|
||||
(!replyPayload.text && !replyPayload.mediaUrl && !replyPayload.mediaUrls?.length)
|
||||
) {
|
||||
heartbeatLogger.info(
|
||||
{
|
||||
@@ -163,13 +148,10 @@ export async function runWebHeartbeatOnce(opts: {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasMedia = Boolean(
|
||||
replyPayload.mediaUrl || (replyPayload.mediaUrls?.length ?? 0) > 0,
|
||||
);
|
||||
const hasMedia = Boolean(replyPayload.mediaUrl || (replyPayload.mediaUrls?.length ?? 0) > 0);
|
||||
const ackMaxChars = Math.max(
|
||||
0,
|
||||
cfg.agents?.defaults?.heartbeat?.ackMaxChars ??
|
||||
DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
|
||||
cfg.agents?.defaults?.heartbeat?.ackMaxChars ?? DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
|
||||
);
|
||||
const stripped = stripHeartbeatToken(replyPayload.text, {
|
||||
mode: "heartbeat",
|
||||
@@ -193,21 +175,13 @@ export async function runWebHeartbeatOnce(opts: {
|
||||
}
|
||||
|
||||
if (hasMedia) {
|
||||
heartbeatLogger.warn(
|
||||
{ to },
|
||||
"heartbeat reply contained media; sending text only",
|
||||
);
|
||||
heartbeatLogger.warn({ to }, "heartbeat reply contained media; sending text only");
|
||||
}
|
||||
|
||||
const finalText = stripped.text || replyPayload.text || "";
|
||||
if (dryRun) {
|
||||
heartbeatLogger.info(
|
||||
{ to, reason: "dry-run", chars: finalText.length },
|
||||
"heartbeat dry-run",
|
||||
);
|
||||
whatsappHeartbeatLog.info(
|
||||
`[dry-run] heartbeat -> ${to}: ${elide(finalText, 200)}`,
|
||||
);
|
||||
heartbeatLogger.info({ to, reason: "dry-run", chars: finalText.length }, "heartbeat dry-run");
|
||||
whatsappHeartbeatLog.info(`[dry-run] heartbeat -> ${to}: ${elide(finalText, 200)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
buildMentionRegexes,
|
||||
normalizeMentionText,
|
||||
} from "../../auto-reply/reply/mentions.js";
|
||||
import { buildMentionRegexes, normalizeMentionText } from "../../auto-reply/reply/mentions.js";
|
||||
import type { loadConfig } from "../../config/config.js";
|
||||
import { isSelfChatMode, jidToE164, normalizeE164 } from "../../utils.js";
|
||||
import type { WebInboundMsg } from "./types.js";
|
||||
@@ -25,18 +22,12 @@ export function buildMentionConfig(
|
||||
return { mentionRegexes, allowFrom: cfg.channels?.whatsapp?.allowFrom };
|
||||
}
|
||||
|
||||
export function resolveMentionTargets(
|
||||
msg: WebInboundMsg,
|
||||
authDir?: string,
|
||||
): MentionTargets {
|
||||
export function resolveMentionTargets(msg: WebInboundMsg, authDir?: string): MentionTargets {
|
||||
const jidOptions = authDir ? { authDir } : undefined;
|
||||
const normalizedMentions = msg.mentionedJids?.length
|
||||
? msg.mentionedJids
|
||||
.map((jid) => jidToE164(jid, jidOptions) ?? jid)
|
||||
.filter(Boolean)
|
||||
? msg.mentionedJids.map((jid) => jidToE164(jid, jidOptions) ?? jid).filter(Boolean)
|
||||
: [];
|
||||
const selfE164 =
|
||||
msg.selfE164 ?? (msg.selfJid ? jidToE164(msg.selfJid, jidOptions) : null);
|
||||
const selfE164 = msg.selfE164 ?? (msg.selfJid ? jidToE164(msg.selfJid, jidOptions) : null);
|
||||
const selfJid = msg.selfJid ? msg.selfJid.replace(/:\\d+/, "") : null;
|
||||
return { normalizedMentions, selfE164, selfJid };
|
||||
}
|
||||
@@ -53,11 +44,7 @@ export function isBotMentionedFromTargets(
|
||||
const isSelfChat = isSelfChatMode(targets.selfE164, mentionCfg.allowFrom);
|
||||
|
||||
if (msg.mentionedJids?.length && !isSelfChat) {
|
||||
if (
|
||||
targets.selfE164 &&
|
||||
targets.normalizedMentions.includes(targets.selfE164)
|
||||
)
|
||||
return true;
|
||||
if (targets.selfE164 && targets.normalizedMentions.includes(targets.selfE164)) return true;
|
||||
if (targets.selfJid && targets.selfE164) {
|
||||
// Some mentions use the bare JID; match on E.164 to be safe.
|
||||
if (targets.normalizedMentions.includes(targets.selfJid)) return true;
|
||||
@@ -106,17 +93,10 @@ export function debugMention(
|
||||
return { wasMentioned: result, details };
|
||||
}
|
||||
|
||||
export function resolveOwnerList(
|
||||
mentionCfg: MentionConfig,
|
||||
selfE164?: string | null,
|
||||
) {
|
||||
export function resolveOwnerList(mentionCfg: MentionConfig, selfE164?: string | null) {
|
||||
const allowFrom = mentionCfg.allowFrom;
|
||||
const raw =
|
||||
Array.isArray(allowFrom) && allowFrom.length > 0
|
||||
? allowFrom
|
||||
: selfE164
|
||||
? [selfE164]
|
||||
: [];
|
||||
Array.isArray(allowFrom) && allowFrom.length > 0 ? allowFrom : selfE164 ? [selfE164] : [];
|
||||
return raw
|
||||
.filter((entry): entry is string => Boolean(entry && entry !== "*"))
|
||||
.map((entry) => normalizeE164(entry))
|
||||
|
||||
@@ -25,11 +25,7 @@ import { whatsappHeartbeatLog, whatsappLog } from "./loggers.js";
|
||||
import { buildMentionConfig } from "./mentions.js";
|
||||
import { createEchoTracker } from "./monitor/echo.js";
|
||||
import { createWebOnMessageHandler } from "./monitor/on-message.js";
|
||||
import type {
|
||||
WebChannelStatus,
|
||||
WebInboundMsg,
|
||||
WebMonitorTuning,
|
||||
} from "./types.js";
|
||||
import type { WebChannelStatus, WebInboundMsg, WebMonitorTuning } from "./types.js";
|
||||
import { isLikelyWhatsAppCryptoError } from "./util.js";
|
||||
|
||||
export async function monitorWebChannel(
|
||||
@@ -58,9 +54,7 @@ export async function monitorWebChannel(
|
||||
const emitStatus = () => {
|
||||
tuning.statusSink?.({
|
||||
...status,
|
||||
lastDisconnect: status.lastDisconnect
|
||||
? { ...status.lastDisconnect }
|
||||
: null,
|
||||
lastDisconnect: status.lastDisconnect ? { ...status.lastDisconnect } : null,
|
||||
});
|
||||
};
|
||||
emitStatus();
|
||||
@@ -94,10 +88,7 @@ export async function monitorWebChannel(
|
||||
typeof configuredMaxMb === "number" && configuredMaxMb > 0
|
||||
? configuredMaxMb * 1024 * 1024
|
||||
: DEFAULT_WEB_MEDIA_BYTES;
|
||||
const heartbeatSeconds = resolveHeartbeatSeconds(
|
||||
cfg,
|
||||
tuning.heartbeatSeconds,
|
||||
);
|
||||
const heartbeatSeconds = resolveHeartbeatSeconds(cfg, tuning.heartbeatSeconds);
|
||||
const reconnectPolicy = resolveReconnectPolicy(cfg, tuning.reconnect);
|
||||
const baseMentionConfig = buildMentionConfig(cfg);
|
||||
const groupHistoryLimit =
|
||||
@@ -120,8 +111,7 @@ export async function monitorWebChannel(
|
||||
|
||||
const sleep =
|
||||
tuning.sleep ??
|
||||
((ms: number, signal?: AbortSignal) =>
|
||||
sleepWithAbort(ms, signal ?? abortSignal));
|
||||
((ms: number, signal?: AbortSignal) => sleepWithAbort(ms, signal ?? abortSignal));
|
||||
const stopRequested = () => abortSignal?.aborted === true;
|
||||
const abortPromise =
|
||||
abortSignal &&
|
||||
@@ -208,10 +198,9 @@ export async function monitorWebChannel(
|
||||
channel: "whatsapp",
|
||||
accountId: account.accountId,
|
||||
});
|
||||
enqueueSystemEvent(
|
||||
`WhatsApp gateway connected${selfE164 ? ` as ${selfE164}` : ""}.`,
|
||||
{ sessionKey: connectRoute.sessionKey },
|
||||
);
|
||||
enqueueSystemEvent(`WhatsApp gateway connected${selfE164 ? ` as ${selfE164}` : ""}.`, {
|
||||
sessionKey: connectRoute.sessionKey,
|
||||
});
|
||||
|
||||
setActiveWebListener(account.accountId, listener);
|
||||
unregisterUnhandled = registerUnhandledRejectionHandler((reason) => {
|
||||
@@ -268,10 +257,7 @@ export async function monitorWebChannel(
|
||||
};
|
||||
|
||||
if (minutesSinceLastMessage && minutesSinceLastMessage > 30) {
|
||||
heartbeatLogger.warn(
|
||||
logData,
|
||||
"⚠️ web gateway heartbeat - no messages in 30+ minutes",
|
||||
);
|
||||
heartbeatLogger.warn(logData, "⚠️ web gateway heartbeat - no messages in 30+ minutes");
|
||||
} else {
|
||||
heartbeatLogger.info(logData, "web gateway heartbeat");
|
||||
}
|
||||
@@ -281,9 +267,7 @@ export async function monitorWebChannel(
|
||||
if (!lastMessageAt) return;
|
||||
const timeSinceLastMessage = Date.now() - lastMessageAt;
|
||||
if (timeSinceLastMessage <= MESSAGE_TIMEOUT_MS) return;
|
||||
const minutesSinceLastMessage = Math.floor(
|
||||
timeSinceLastMessage / 60000,
|
||||
);
|
||||
const minutesSinceLastMessage = Math.floor(timeSinceLastMessage / 60000);
|
||||
heartbeatLogger.warn(
|
||||
{
|
||||
connectionId,
|
||||
@@ -319,10 +303,7 @@ export async function monitorWebChannel(
|
||||
|
||||
const reason = await Promise.race([
|
||||
listener.onClose?.catch((err) => {
|
||||
reconnectLogger.error(
|
||||
{ error: formatError(err) },
|
||||
"listener.onClose rejected",
|
||||
);
|
||||
reconnectLogger.error({ error: formatError(err) }, "listener.onClose rejected");
|
||||
return { status: 500, isLoggedOut: false, error: err };
|
||||
}) ?? waitForever(),
|
||||
abortPromise ?? waitForever(),
|
||||
@@ -374,10 +355,9 @@ export async function monitorWebChannel(
|
||||
"web reconnect: connection closed",
|
||||
);
|
||||
|
||||
enqueueSystemEvent(
|
||||
`WhatsApp gateway disconnected (status ${statusCode ?? "unknown"})`,
|
||||
{ sessionKey: connectRoute.sessionKey },
|
||||
);
|
||||
enqueueSystemEvent(`WhatsApp gateway disconnected (status ${statusCode ?? "unknown"})`, {
|
||||
sessionKey: connectRoute.sessionKey,
|
||||
});
|
||||
|
||||
if (loggedOut) {
|
||||
runtime.error(
|
||||
@@ -390,10 +370,7 @@ export async function monitorWebChannel(
|
||||
reconnectAttempts += 1;
|
||||
status.reconnectAttempts = reconnectAttempts;
|
||||
emitStatus();
|
||||
if (
|
||||
reconnectPolicy.maxAttempts > 0 &&
|
||||
reconnectAttempts >= reconnectPolicy.maxAttempts
|
||||
) {
|
||||
if (reconnectPolicy.maxAttempts > 0 && reconnectAttempts >= reconnectPolicy.maxAttempts) {
|
||||
reconnectLogger.warn(
|
||||
{
|
||||
connectionId,
|
||||
|
||||
@@ -69,8 +69,6 @@ export function maybeSendAckReaction(params: {
|
||||
},
|
||||
"failed to send ack reaction",
|
||||
);
|
||||
logVerbose(
|
||||
`WhatsApp ack reaction failed for chat ${params.msg.chatId}: ${formatError(err)}`,
|
||||
);
|
||||
logVerbose(`WhatsApp ack reaction failed for chat ${params.msg.chatId}: ${formatError(err)}`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -33,13 +33,9 @@ export async function maybeBroadcastMessage(params: {
|
||||
if (broadcastAgents.length === 0) return false;
|
||||
|
||||
const strategy = params.cfg.broadcast?.strategy || "parallel";
|
||||
whatsappInboundLog.info(
|
||||
`Broadcasting message to ${broadcastAgents.length} agents (${strategy})`,
|
||||
);
|
||||
whatsappInboundLog.info(`Broadcasting message to ${broadcastAgents.length} agents (${strategy})`);
|
||||
|
||||
const agentIds = params.cfg.agents?.list?.map((agent) =>
|
||||
normalizeAgentId(agent.id),
|
||||
);
|
||||
const agentIds = params.cfg.agents?.list?.map((agent) => normalizeAgentId(agent.id));
|
||||
const hasKnownAgents = (agentIds?.length ?? 0) > 0;
|
||||
const groupHistorySnapshot =
|
||||
params.msg.chatType === "group"
|
||||
@@ -49,9 +45,7 @@ export async function maybeBroadcastMessage(params: {
|
||||
const processForAgent = async (agentId: string): Promise<boolean> => {
|
||||
const normalizedAgentId = normalizeAgentId(agentId);
|
||||
if (hasKnownAgents && !agentIds?.includes(normalizedAgentId)) {
|
||||
whatsappInboundLog.warn(
|
||||
`Broadcast agent ${agentId} not found in agents.list; skipping`,
|
||||
);
|
||||
whatsappInboundLog.warn(`Broadcast agent ${agentId} not found in agents.list; skipping`);
|
||||
return false;
|
||||
}
|
||||
const agentRoute = {
|
||||
@@ -72,19 +66,12 @@ export async function maybeBroadcastMessage(params: {
|
||||
};
|
||||
|
||||
try {
|
||||
return await params.processMessage(
|
||||
params.msg,
|
||||
agentRoute,
|
||||
params.groupHistoryKey,
|
||||
{
|
||||
groupHistory: groupHistorySnapshot,
|
||||
suppressGroupHistoryClear: true,
|
||||
},
|
||||
);
|
||||
return await params.processMessage(params.msg, agentRoute, params.groupHistoryKey, {
|
||||
groupHistory: groupHistorySnapshot,
|
||||
suppressGroupHistoryClear: true,
|
||||
});
|
||||
} catch (err) {
|
||||
whatsappInboundLog.error(
|
||||
`Broadcast agent ${agentId} failed: ${formatError(err)}`,
|
||||
);
|
||||
whatsappInboundLog.error(`Broadcast agent ${agentId} failed: ${formatError(err)}`);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -95,12 +82,8 @@ export async function maybeBroadcastMessage(params: {
|
||||
if (await processForAgent(agentId)) didSendReply = true;
|
||||
}
|
||||
} else {
|
||||
const results = await Promise.allSettled(
|
||||
broadcastAgents.map(processForAgent),
|
||||
);
|
||||
didSendReply = results.some(
|
||||
(result) => result.status === "fulfilled" && result.value,
|
||||
);
|
||||
const results = await Promise.allSettled(broadcastAgents.map(processForAgent));
|
||||
didSendReply = results.some((result) => result.status === "fulfilled" && result.value);
|
||||
}
|
||||
|
||||
if (params.msg.chatType === "group" && didSendReply) {
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
export function isStatusCommand(body: string) {
|
||||
const trimmed = body.trim().toLowerCase();
|
||||
if (!trimmed) return false;
|
||||
return (
|
||||
trimmed === "/status" ||
|
||||
trimmed === "status" ||
|
||||
trimmed.startsWith("/status ")
|
||||
);
|
||||
return trimmed === "/status" || trimmed === "status" || trimmed.startsWith("/status ");
|
||||
}
|
||||
|
||||
export function stripMentionsForCommand(
|
||||
|
||||
@@ -9,10 +9,7 @@ export type EchoTracker = {
|
||||
) => void;
|
||||
has: (key: string) => boolean;
|
||||
forget: (key: string) => void;
|
||||
buildCombinedKey: (params: {
|
||||
sessionKey: string;
|
||||
combinedBody: string;
|
||||
}) => string;
|
||||
buildCombinedKey: (params: { sessionKey: string; combinedBody: string }) => string;
|
||||
};
|
||||
|
||||
export function createEchoTracker(params: {
|
||||
|
||||
@@ -10,10 +10,7 @@ import {
|
||||
resolveStorePath,
|
||||
} from "../../../config/sessions.js";
|
||||
|
||||
export function resolveGroupPolicyFor(
|
||||
cfg: ReturnType<typeof loadConfig>,
|
||||
conversationId: string,
|
||||
) {
|
||||
export function resolveGroupPolicyFor(cfg: ReturnType<typeof loadConfig>, conversationId: string) {
|
||||
const groupId = resolveGroupSessionKey({
|
||||
From: conversationId,
|
||||
ChatType: "group",
|
||||
@@ -53,10 +50,7 @@ export function resolveGroupActivationFor(params: {
|
||||
});
|
||||
const store = loadSessionStore(storePath);
|
||||
const entry = store[params.sessionKey];
|
||||
const requireMention = resolveGroupRequireMentionFor(
|
||||
params.cfg,
|
||||
params.conversationId,
|
||||
);
|
||||
const requireMention = resolveGroupRequireMentionFor(params.cfg, params.conversationId);
|
||||
const defaultActivation = requireMention === false ? "always" : "mention";
|
||||
return normalizeGroupActivation(entry?.groupActivation) ?? defaultActivation;
|
||||
}
|
||||
|
||||
@@ -2,17 +2,10 @@ import { parseActivationCommand } from "../../../auto-reply/group-activation.js"
|
||||
import type { loadConfig } from "../../../config/config.js";
|
||||
import { normalizeE164 } from "../../../utils.js";
|
||||
import type { MentionConfig } from "../mentions.js";
|
||||
import {
|
||||
buildMentionConfig,
|
||||
debugMention,
|
||||
resolveOwnerList,
|
||||
} from "../mentions.js";
|
||||
import { buildMentionConfig, debugMention, resolveOwnerList } from "../mentions.js";
|
||||
import type { WebInboundMsg } from "../types.js";
|
||||
import { isStatusCommand, stripMentionsForCommand } from "./commands.js";
|
||||
import {
|
||||
resolveGroupActivationFor,
|
||||
resolveGroupPolicyFor,
|
||||
} from "./group-activation.js";
|
||||
import { resolveGroupActivationFor, resolveGroupPolicyFor } from "./group-activation.js";
|
||||
import { noteGroupMember } from "./group-members.js";
|
||||
|
||||
export type GroupHistoryEntry = {
|
||||
@@ -47,9 +40,7 @@ export function applyGroupGating(params: {
|
||||
}) {
|
||||
const groupPolicy = resolveGroupPolicyFor(params.cfg, params.conversationId);
|
||||
if (groupPolicy.allowlistEnabled && !groupPolicy.allowed) {
|
||||
params.logVerbose(
|
||||
`Skipping group message ${params.conversationId} (not in allowlist)`,
|
||||
);
|
||||
params.logVerbose(`Skipping group message ${params.conversationId} (not in allowlist)`);
|
||||
return { shouldProcess: false };
|
||||
}
|
||||
|
||||
@@ -69,13 +60,10 @@ export function applyGroupGating(params: {
|
||||
const activationCommand = parseActivationCommand(commandBody);
|
||||
const owner = isOwnerSender(params.baseMentionConfig, params.msg);
|
||||
const statusCommand = isStatusCommand(commandBody);
|
||||
const shouldBypassMention =
|
||||
owner && (activationCommand.hasCommand || statusCommand);
|
||||
const shouldBypassMention = owner && (activationCommand.hasCommand || statusCommand);
|
||||
|
||||
if (activationCommand.hasCommand && !owner) {
|
||||
params.logVerbose(
|
||||
`Ignoring /activation from non-owner in group ${params.conversationId}`,
|
||||
);
|
||||
params.logVerbose(`Ignoring /activation from non-owner in group ${params.conversationId}`);
|
||||
return { shouldProcess: false };
|
||||
}
|
||||
|
||||
|
||||
@@ -23,9 +23,7 @@ export function buildInboundLine(params: {
|
||||
});
|
||||
const prefixStr = messagePrefix ? `${messagePrefix} ` : "";
|
||||
const senderLabel =
|
||||
msg.chatType === "group"
|
||||
? `${msg.senderName ?? msg.senderE164 ?? "Someone"}: `
|
||||
: "";
|
||||
msg.chatType === "group" ? `${msg.senderName ?? msg.senderE164 ?? "Someone"}: ` : "";
|
||||
const replyContext = formatReplyContext(msg);
|
||||
const baseLine = `${prefixStr}${senderLabel}${msg.body}${
|
||||
replyContext ? `\n\n${replyContext}` : ""
|
||||
@@ -34,8 +32,7 @@ export function buildInboundLine(params: {
|
||||
// Wrap with standardized envelope for the agent.
|
||||
return formatAgentEnvelope({
|
||||
channel: "WhatsApp",
|
||||
from:
|
||||
msg.chatType === "group" ? msg.from : msg.from?.replace(/^whatsapp:/, ""),
|
||||
from: msg.chatType === "group" ? msg.from : msg.from?.replace(/^whatsapp:/, ""),
|
||||
timestamp: msg.timestamp,
|
||||
body: baseLine,
|
||||
});
|
||||
|
||||
@@ -25,9 +25,7 @@ export function createWebOnMessageHandler(params: {
|
||||
echoTracker: EchoTracker;
|
||||
backgroundTasks: Set<Promise<unknown>>;
|
||||
replyResolver: typeof getReplyFromConfig;
|
||||
replyLogger: ReturnType<
|
||||
typeof import("../../../logging.js")["getChildLogger"]
|
||||
>;
|
||||
replyLogger: ReturnType<(typeof import("../../../logging.js"))["getChildLogger"]>;
|
||||
baseMentionConfig: MentionConfig;
|
||||
account: { authDir?: string; accountId?: string };
|
||||
}) {
|
||||
@@ -90,9 +88,7 @@ export function createWebOnMessageHandler(params: {
|
||||
|
||||
// Skip if this is a message we just sent (echo detection)
|
||||
if (params.echoTracker.has(msg.body)) {
|
||||
logVerbose(
|
||||
"Skipping auto-reply: detected echo (message matches recently sent text)",
|
||||
);
|
||||
logVerbose("Skipping auto-reply: detected echo (message matches recently sent text)");
|
||||
params.echoTracker.forget(msg.body);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -53,10 +53,7 @@ export async function processMessage(params: {
|
||||
) => void;
|
||||
echoHas: (key: string) => boolean;
|
||||
echoForget: (key: string) => void;
|
||||
buildCombinedEchoKey: (p: {
|
||||
sessionKey: string;
|
||||
combinedBody: string;
|
||||
}) => string;
|
||||
buildCombinedEchoKey: (p: { sessionKey: string; combinedBody: string }) => string;
|
||||
maxMediaTextChunkLimit?: number;
|
||||
groupHistory?: GroupHistoryEntry[];
|
||||
suppressGroupHistoryClear?: boolean;
|
||||
@@ -70,12 +67,8 @@ export async function processMessage(params: {
|
||||
let shouldClearGroupHistory = false;
|
||||
|
||||
if (params.msg.chatType === "group") {
|
||||
const history =
|
||||
params.groupHistory ??
|
||||
params.groupHistories.get(params.groupHistoryKey) ??
|
||||
[];
|
||||
const historyWithoutCurrent =
|
||||
history.length > 0 ? history.slice(0, -1) : [];
|
||||
const history = params.groupHistory ?? params.groupHistories.get(params.groupHistoryKey) ?? [];
|
||||
const historyWithoutCurrent = history.length > 0 ? history.slice(0, -1) : [];
|
||||
if (historyWithoutCurrent.length > 0) {
|
||||
const lineBreak = "\\n";
|
||||
const historyText = historyWithoutCurrent
|
||||
@@ -142,8 +135,7 @@ export async function processMessage(params: {
|
||||
"inbound web message",
|
||||
);
|
||||
|
||||
const fromDisplay =
|
||||
params.msg.chatType === "group" ? conversationId : params.msg.from;
|
||||
const fromDisplay = params.msg.chatType === "group" ? conversationId : params.msg.from;
|
||||
const kindLabel = params.msg.mediaType ? `, ${params.msg.mediaType}` : "";
|
||||
whatsappInboundLog.info(
|
||||
`Inbound message ${fromDisplay} -> ${params.msg.to} (${params.msg.chatType}${kindLabel}, ${combinedBody.length} chars)`,
|
||||
@@ -173,9 +165,7 @@ export async function processMessage(params: {
|
||||
}
|
||||
}
|
||||
|
||||
const textLimit =
|
||||
params.maxMediaTextChunkLimit ??
|
||||
resolveTextChunkLimit(params.cfg, "whatsapp");
|
||||
const textLimit = params.maxMediaTextChunkLimit ?? resolveTextChunkLimit(params.cfg, "whatsapp");
|
||||
let didLogHeartbeatStrip = false;
|
||||
let didSendReply = false;
|
||||
const responsePrefix = resolveEffectiveMessagesConfig(
|
||||
@@ -242,8 +232,7 @@ export async function processMessage(params: {
|
||||
params.rememberSentText(payload.text, {});
|
||||
return;
|
||||
}
|
||||
const shouldLog =
|
||||
info.kind === "final" && payload.text ? true : undefined;
|
||||
const shouldLog = info.kind === "final" && payload.text ? true : undefined;
|
||||
params.rememberSentText(payload.text, {
|
||||
combinedBody,
|
||||
combinedBodySessionKey: params.route.sessionKey,
|
||||
@@ -251,21 +240,12 @@ export async function processMessage(params: {
|
||||
});
|
||||
if (info.kind === "final") {
|
||||
const fromDisplay =
|
||||
params.msg.chatType === "group"
|
||||
? conversationId
|
||||
: (params.msg.from ?? "unknown");
|
||||
const hasMedia = Boolean(
|
||||
payload.mediaUrl || payload.mediaUrls?.length,
|
||||
);
|
||||
whatsappOutboundLog.info(
|
||||
`Auto-replied to ${fromDisplay}${hasMedia ? " (media)" : ""}`,
|
||||
);
|
||||
params.msg.chatType === "group" ? conversationId : (params.msg.from ?? "unknown");
|
||||
const hasMedia = Boolean(payload.mediaUrl || payload.mediaUrls?.length);
|
||||
whatsappOutboundLog.info(`Auto-replied to ${fromDisplay}${hasMedia ? " (media)" : ""}`);
|
||||
if (shouldLogVerbose()) {
|
||||
const preview =
|
||||
payload.text != null ? elide(payload.text, 400) : "<media>";
|
||||
whatsappOutboundLog.debug(
|
||||
`Reply body: ${preview}${hasMedia ? " (media)" : ""}`,
|
||||
);
|
||||
const preview = payload.text != null ? elide(payload.text, 400) : "<media>";
|
||||
whatsappOutboundLog.debug(`Reply body: ${preview}${hasMedia ? " (media)" : ""}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -294,9 +274,7 @@ export async function processMessage(params: {
|
||||
if (shouldClearGroupHistory && didSendReply) {
|
||||
params.groupHistories.set(params.groupHistoryKey, []);
|
||||
}
|
||||
logVerbose(
|
||||
"Skipping auto-reply: silent token or no text/media returned from resolver",
|
||||
);
|
||||
logVerbose("Skipping auto-reply: silent token or no text/media returned from resolver");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,8 +27,6 @@ export function getSessionSnapshot(
|
||||
: sessionCfg?.idleMinutes) ?? DEFAULT_IDLE_MINUTES,
|
||||
1,
|
||||
);
|
||||
const fresh = !!(
|
||||
entry && Date.now() - entry.updatedAt <= idleMinutes * 60_000
|
||||
);
|
||||
const fresh = !!(entry && Date.now() - entry.updatedAt <= idleMinutes * 60_000);
|
||||
return { key, entry, fresh, idleMinutes };
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { monitorWebInbox } from "../inbound.js";
|
||||
import type { ReconnectPolicy } from "../reconnect.js";
|
||||
|
||||
export type WebInboundMsg = Parameters<
|
||||
typeof monitorWebInbox
|
||||
>[0]["onMessage"] extends (msg: infer M) => unknown
|
||||
export type WebInboundMsg = Parameters<typeof monitorWebInbox>[0]["onMessage"] extends (
|
||||
msg: infer M,
|
||||
) => unknown
|
||||
? M
|
||||
: never;
|
||||
|
||||
|
||||
@@ -22,14 +22,11 @@ export function isLikelyWhatsAppCryptoError(reason: unknown) {
|
||||
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 === "function") return value.name ? `[function ${value.name}]` : "[function]";
|
||||
return Object.prototype.toString.call(value);
|
||||
};
|
||||
const raw =
|
||||
reason instanceof Error
|
||||
? `${reason.message}\n${reason.stack ?? ""}`
|
||||
: formatReason(reason);
|
||||
reason instanceof Error ? `${reason.message}\n${reason.stack ?? ""}` : formatReason(reason);
|
||||
const haystack = raw.toLowerCase();
|
||||
const hasAuthError =
|
||||
haystack.includes("unsupported state or unable to authenticate data") ||
|
||||
|
||||
Reference in New Issue
Block a user