fix(web): allow group replies past allowFrom
This commit is contained in:
@@ -36,6 +36,7 @@
|
||||
## Agent-Specific Notes
|
||||
- Relay is managed by launchctl (label `com.steipete.warelay`). After code changes restart with `launchctl kickstart -k gui/$UID/com.steipete.warelay` and verify via `launchctl list | grep warelay`. Use tmux only if you spin up a temporary relay yourself and clean it up afterward.
|
||||
- Also read the shared guardrails at `~/Projects/oracle/AGENTS.md` and `~/Projects/agent-scripts/AGENTS.MD` before making changes; align with any cross-repo rules noted there.
|
||||
- When asked to open a “session” file, open the Pi/Tau session logs under `~/.pi/agent/sessions/warelay/*.jsonl` (newest unless a specific ID is given), not the default `sessions.json`.
|
||||
|
||||
## Exclamation Mark Escaping Workaround
|
||||
The Claude Code Bash tool escapes `!` to `\!` in command arguments. When using `warelay send` with messages containing exclamation marks, use heredoc syntax:
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
- Media server blocks symlinks and enforces path containment; logging rotates daily and prunes >24h.
|
||||
|
||||
### Bug Fixes
|
||||
- Web group chats now bypass the second `allowFrom` check (we still enforce it on the group participant at inbox ingest), so mentioned group messages reply even when the group JID isn’t in your allowlist.
|
||||
- `logVerbose` also writes to the configured Pino logger at debug level (without breaking stdout).
|
||||
- MIME sniffing and redirect handling for downloads/hosted media.
|
||||
- Response prefix applied to heartbeat alerts; heartbeat array payloads handled for both providers.
|
||||
- Tau RPC typing exposes `signal`/`killed`; NDJSON parsers normalized across agents.
|
||||
|
||||
@@ -348,6 +348,9 @@ export async function getReplyFromConfig(
|
||||
const from = (ctx.From ?? "").replace(/^whatsapp:/, "");
|
||||
const to = (ctx.To ?? "").replace(/^whatsapp:/, "");
|
||||
const isSamePhone = from && to && from === to;
|
||||
const isGroup =
|
||||
typeof ctx.From === "string" &&
|
||||
(ctx.From.includes("@g.us") || ctx.From.startsWith("group:"));
|
||||
const abortKey = sessionKey ?? (from || undefined) ?? (to || undefined);
|
||||
const rawBodyNormalized = (
|
||||
sessionCtx.BodyStripped ?? sessionCtx.Body ?? ""
|
||||
@@ -362,7 +365,7 @@ export async function getReplyFromConfig(
|
||||
// Same-phone mode (self-messaging) is always allowed
|
||||
if (isSamePhone) {
|
||||
logVerbose(`Allowing same-phone mode: from === to (${from})`);
|
||||
} else if (Array.isArray(allowFrom) && allowFrom.length > 0) {
|
||||
} else if (!isGroup && Array.isArray(allowFrom) && allowFrom.length > 0) {
|
||||
// Support "*" as wildcard to allow all senders
|
||||
if (!allowFrom.includes("*") && !allowFrom.includes(from)) {
|
||||
logVerbose(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import chalk from "chalk";
|
||||
import { getLogger } from "./logging.js";
|
||||
|
||||
let globalVerbose = false;
|
||||
let globalYes = false;
|
||||
@@ -12,7 +13,14 @@ export function isVerbose() {
|
||||
}
|
||||
|
||||
export function logVerbose(message: string) {
|
||||
if (globalVerbose) console.log(chalk.gray(message));
|
||||
if (globalVerbose) {
|
||||
console.log(chalk.gray(message));
|
||||
try {
|
||||
getLogger().debug({ message }, "verbose");
|
||||
} catch {
|
||||
// ignore logger failures to avoid breaking verbose printing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function setYes(v: boolean) {
|
||||
|
||||
@@ -145,6 +145,25 @@ describe("config and templating", () => {
|
||||
expect(result?.text).toBe("Reply: test");
|
||||
});
|
||||
|
||||
it("getReplyFromConfig allows group chats even when not in allowFrom", async () => {
|
||||
const cfg = {
|
||||
inbound: {
|
||||
allowFrom: ["+9999"],
|
||||
reply: {
|
||||
mode: "text" as const,
|
||||
text: "Group: {{From}}",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await index.getReplyFromConfig(
|
||||
{ Body: "hello", From: "120363422899103675@g.us", To: "+4475" },
|
||||
undefined,
|
||||
cfg,
|
||||
);
|
||||
expect(result?.text).toBe("Group: 120363422899103675@g.us");
|
||||
});
|
||||
|
||||
it("getReplyFromConfig rejects non-same-phone when not in allowFrom", async () => {
|
||||
const cfg = {
|
||||
inbound: {
|
||||
|
||||
@@ -98,6 +98,12 @@ function isBotMentioned(
|
||||
msg: WebInboundMsg,
|
||||
mentionCfg: MentionConfig,
|
||||
): boolean {
|
||||
const clean = (text: string) =>
|
||||
text
|
||||
// Remove zero-width and directionality markers WhatsApp injects around display names
|
||||
.replace(/[\u200b-\u200f\u202a-\u202e\u2060-\u206f]/g, "")
|
||||
.toLowerCase();
|
||||
|
||||
if (msg.mentionedJids?.length) {
|
||||
const normalizedMentions = msg.mentionedJids
|
||||
.map((jid) => jidToE164(jid) ?? jid)
|
||||
@@ -109,13 +115,14 @@ function isBotMentioned(
|
||||
if (normalizedMentions.includes(bareSelf)) return true;
|
||||
}
|
||||
}
|
||||
if (mentionCfg.mentionRegexes.some((re) => re.test(msg.body))) return true;
|
||||
const bodyClean = clean(msg.body);
|
||||
if (mentionCfg.mentionRegexes.some((re) => re.test(bodyClean))) return true;
|
||||
|
||||
// Fallback: detect body containing our own number (with or without +, spacing)
|
||||
if (msg.selfE164) {
|
||||
const selfDigits = msg.selfE164.replace(/\D/g, "");
|
||||
if (selfDigits) {
|
||||
const bodyDigits = msg.body.replace(/[^\d]/g, "");
|
||||
const bodyDigits = bodyClean.replace(/[^\d]/g, "");
|
||||
if (bodyDigits.includes(selfDigits)) return true;
|
||||
const bodyNoSpace = msg.body.replace(/[\s-]/g, "");
|
||||
const pattern = new RegExp(`\\+?${selfDigits}`, "i");
|
||||
@@ -126,6 +133,24 @@ function isBotMentioned(
|
||||
return false;
|
||||
}
|
||||
|
||||
function debugMention(
|
||||
msg: WebInboundMsg,
|
||||
mentionCfg: MentionConfig,
|
||||
): { wasMentioned: boolean; details: Record<string, unknown> } {
|
||||
const result = isBotMentioned(msg, mentionCfg);
|
||||
const details = {
|
||||
from: msg.from,
|
||||
body: msg.body,
|
||||
bodyClean: msg.body
|
||||
.replace(/[\u200b-\u200f\u202a-\u202e\u2060-\u206f]/g, "")
|
||||
.toLowerCase(),
|
||||
mentionedJids: msg.mentionedJids ?? null,
|
||||
selfJid: msg.selfJid ?? null,
|
||||
selfE164: msg.selfE164 ?? null,
|
||||
};
|
||||
return { wasMentioned: result, details };
|
||||
}
|
||||
|
||||
export function resolveReplyHeartbeatMinutes(
|
||||
cfg: ReturnType<typeof loadConfig>,
|
||||
overrideMinutes?: number,
|
||||
@@ -669,7 +694,6 @@ export async function monitorWebProvider(
|
||||
const batch = pendingBatches.get(conversationId);
|
||||
if (!batch || batch.messages.length === 0) return;
|
||||
if (getQueueSize() > 0) {
|
||||
// Wait until command queue is free to run the combined prompt.
|
||||
batch.timer = setTimeout(() => void processBatch(conversationId), 150);
|
||||
return;
|
||||
}
|
||||
@@ -814,8 +838,6 @@ export async function monitorWebProvider(
|
||||
const bucket = pendingBatches.get(key) ?? { messages: [] };
|
||||
bucket.messages.push(msg);
|
||||
pendingBatches.set(key, bucket);
|
||||
|
||||
// Process immediately when queue is free; otherwise wait until it drains.
|
||||
if (getQueueSize() === 0) {
|
||||
await processBatch(key);
|
||||
} else {
|
||||
@@ -859,10 +881,19 @@ export async function monitorWebProvider(
|
||||
while (history.length > groupHistoryLimit) history.shift();
|
||||
groupHistories.set(conversationId, history);
|
||||
|
||||
const wasMentioned = isBotMentioned(msg, mentionConfig);
|
||||
const mentionDebug = debugMention(msg, mentionConfig);
|
||||
replyLogger.debug(
|
||||
{
|
||||
conversationId,
|
||||
wasMentioned: mentionDebug.wasMentioned,
|
||||
...mentionDebug.details,
|
||||
},
|
||||
"group mention debug",
|
||||
);
|
||||
const wasMentioned = mentionDebug.wasMentioned;
|
||||
if (mentionConfig.requireMention && !wasMentioned) {
|
||||
logVerbose(
|
||||
`Group message stored for context (no mention detected) in ${conversationId}`,
|
||||
`Group message stored for context (no mention detected) in ${conversationId}: ${msg.body}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user