WhatsApp: guard main DM last-route to single owner

This commit is contained in:
webdevtodayjason
2026-03-02 13:35:27 -06:00
committed by Peter Steinberger
parent f534ea9906
commit ab0b2c21f3
2 changed files with 113 additions and 1 deletions

View File

@@ -344,4 +344,76 @@ describe("web processMessage inbound contract", () => {
expect(updateLastRouteMock).not.toHaveBeenCalled();
});
it("does not update main last route for non-owner sender when main DM scope is pinned", async () => {
const updateLastRouteMock = vi.mocked(updateLastRouteInBackground);
updateLastRouteMock.mockClear();
const args = makeProcessMessageArgs({
routeSessionKey: "agent:main:main",
groupHistoryKey: "+3000",
cfg: {
channels: {
whatsapp: {
allowFrom: ["+1000"],
},
},
messages: {},
session: { store: sessionStorePath, dmScope: "main" },
} as unknown as ReturnType<typeof import("../../../config/config.js").loadConfig>,
msg: {
id: "msg-last-route-3",
from: "+3000",
to: "+2000",
chatType: "direct",
body: "hello",
senderE164: "+3000",
},
});
args.route = {
...args.route,
sessionKey: "agent:main:main",
mainSessionKey: "agent:main:main",
};
await processMessage(args);
expect(updateLastRouteMock).not.toHaveBeenCalled();
});
it("updates main last route for owner sender when main DM scope is pinned", async () => {
const updateLastRouteMock = vi.mocked(updateLastRouteInBackground);
updateLastRouteMock.mockClear();
const args = makeProcessMessageArgs({
routeSessionKey: "agent:main:main",
groupHistoryKey: "+1000",
cfg: {
channels: {
whatsapp: {
allowFrom: ["+1000"],
},
},
messages: {},
session: { store: sessionStorePath, dmScope: "main" },
} as unknown as ReturnType<typeof import("../../../config/config.js").loadConfig>,
msg: {
id: "msg-last-route-4",
from: "+1000",
to: "+2000",
chatType: "direct",
body: "hello",
senderE164: "+1000",
},
});
args.route = {
...args.route,
sessionKey: "agent:main:main",
mainSessionKey: "agent:main:main",
};
await processMessage(args);
expect(updateLastRouteMock).toHaveBeenCalledTimes(1);
});
});

View File

@@ -107,6 +107,28 @@ async function resolveWhatsAppCommandAuthorized(params: {
return access.commandAuthorized;
}
function resolvePinnedMainDmRecipient(params: {
cfg: ReturnType<typeof loadConfig>;
msg: WebInboundMsg;
}): string | null {
if ((params.cfg.session?.dmScope ?? "main") !== "main") {
return null;
}
const account = resolveWhatsAppAccount({ cfg: params.cfg, accountId: params.msg.accountId });
const rawAllowFrom = account.allowFrom ?? [];
if (rawAllowFrom.includes("*")) {
return null;
}
const normalizedOwners = Array.from(
new Set(
rawAllowFrom
.map((entry) => normalizeE164(String(entry)))
.filter((entry): entry is string => Boolean(entry)),
),
);
return normalizedOwners.length === 1 ? normalizedOwners[0] : null;
}
export async function processMessage(params: {
cfg: ReturnType<typeof loadConfig>;
msg: WebInboundMsg;
@@ -320,7 +342,17 @@ export async function processMessage(params: {
// Only update main session's lastRoute when DM actually IS the main session.
// When dmScope="per-channel-peer", the DM uses an isolated sessionKey,
// and updating mainSessionKey would corrupt routing for the session owner.
if (dmRouteTarget && params.route.sessionKey === params.route.mainSessionKey) {
const pinnedMainDmRecipient = resolvePinnedMainDmRecipient({
cfg: params.cfg,
msg: params.msg,
});
const shouldUpdateMainLastRoute =
!pinnedMainDmRecipient || pinnedMainDmRecipient === dmRouteTarget;
if (
dmRouteTarget &&
params.route.sessionKey === params.route.mainSessionKey &&
shouldUpdateMainLastRoute
) {
updateLastRouteInBackground({
cfg: params.cfg,
backgroundTasks: params.backgroundTasks,
@@ -332,6 +364,14 @@ export async function processMessage(params: {
ctx: ctxPayload,
warn: params.replyLogger.warn.bind(params.replyLogger),
});
} else if (
dmRouteTarget &&
params.route.sessionKey === params.route.mainSessionKey &&
pinnedMainDmRecipient
) {
logVerbose(
`Skipping main-session last route update for ${dmRouteTarget} (pinned owner ${pinnedMainDmRecipient})`,
);
}
const metaTask = recordSessionMetaFromInbound({