feat: unify main session and icon cues

This commit is contained in:
Peter Steinberger
2025-12-06 23:16:23 +01:00
parent 460d8fc094
commit 4b6325908b
15 changed files with 238 additions and 24 deletions

View File

@@ -6,8 +6,8 @@ import { loadConfig, type WarelayConfig } from "../config/config.js";
import {
DEFAULT_IDLE_MINUTES,
DEFAULT_RESET_TRIGGER,
deriveSessionKey,
loadSessionStore,
resolveSessionKey,
resolveStorePath,
type SessionEntry,
saveSessionStore,
@@ -210,6 +210,7 @@ export async function getReplyFromConfig(
// Optional session handling (conversation reuse + /new resets)
const sessionCfg = reply?.session;
const mainKey = sessionCfg?.mainKey ?? "main";
const resetTriggers = sessionCfg?.resetTriggers?.length
? sessionCfg.resetTriggers
: [DEFAULT_RESET_TRIGGER];
@@ -261,7 +262,7 @@ export async function getReplyFromConfig(
}
}
sessionKey = deriveSessionKey(sessionScope, ctx);
sessionKey = resolveSessionKey(sessionScope, ctx, mainKey);
sessionStore = loadSessionStore(storePath);
const entry = sessionStore[sessionKey];
const idleMs = idleMinutes * 60_000;

View File

@@ -12,6 +12,7 @@ export type MsgContext = {
GroupMembers?: string;
SenderName?: string;
SenderE164?: string;
Surface?: string;
};
export type TemplateContext = MsgContext & {

View File

@@ -15,8 +15,8 @@ import { type CliDeps, createDefaultDeps } from "../cli/deps.js";
import { loadConfig, type WarelayConfig } from "../config/config.js";
import {
DEFAULT_IDLE_MINUTES,
deriveSessionKey,
loadSessionStore,
resolveSessionKey,
resolveStorePath,
type SessionEntry,
saveSessionStore,
@@ -67,6 +67,7 @@ function resolveSession(opts: {
}): SessionResolution {
const sessionCfg = opts.replyCfg?.session;
const scope = sessionCfg?.scope ?? "per-sender";
const mainKey = sessionCfg?.mainKey ?? "main";
const idleMinutes = Math.max(
sessionCfg?.idleMinutes ?? DEFAULT_IDLE_MINUTES,
1,
@@ -78,7 +79,7 @@ function resolveSession(opts: {
let sessionKey: string | undefined =
sessionStore && opts.to
? deriveSessionKey(scope, { From: opts.to } as MsgContext)
? resolveSessionKey(scope, { From: opts.to } as MsgContext, mainKey)
: undefined;
let sessionEntry =
sessionKey && sessionStore ? sessionStore[sessionKey] : undefined;

View File

@@ -23,6 +23,7 @@ export type SessionConfig = {
sessionIntro?: string;
typingIntervalSeconds?: number;
heartbeatMinutes?: number;
mainKey?: string;
};
export type LoggingConfig = {
@@ -135,6 +136,7 @@ const ReplySchema = z
sendSystemOnce: z.boolean().optional(),
sessionIntro: z.string().optional(),
typingIntervalSeconds: z.number().int().positive().optional(),
mainKey: z.string().optional(),
})
.optional(),
heartbeatMinutes: z.number().int().nonnegative().optional(),

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { deriveSessionKey } from "./sessions.js";
import { deriveSessionKey, resolveSessionKey } from "./sessions.js";
describe("sessions", () => {
it("returns normalized per-sender key", () => {
@@ -22,4 +22,16 @@ describe("sessions", () => {
"group:12345-678@g.us",
);
});
it("maps direct chats to main key when provided", () => {
expect(
resolveSessionKey("per-sender", { From: "whatsapp:+1555" }, "main"),
).toBe("main");
});
it("leaves groups untouched even with main key", () => {
expect(
resolveSessionKey("per-sender", { From: "12345-678@g.us" }, "main"),
).toBe("group:12345-678@g.us");
});
});

View File

@@ -77,3 +77,20 @@ export function deriveSessionKey(scope: SessionScope, ctx: MsgContext) {
}
return from || "unknown";
}
/**
* Resolve the session key with an optional canonical direct-chat key (e.g., "main").
* All non-group direct chats collapse to `mainKey` when provided, keeping group isolation.
*/
export function resolveSessionKey(
scope: SessionScope,
ctx: MsgContext,
mainKey?: string,
) {
const raw = deriveSessionKey(scope, ctx);
if (scope === "global") return raw;
const canonical = (mainKey ?? "").trim();
const isGroup = raw.startsWith("group:") || raw.includes("@g.us");
if (!isGroup && canonical) return canonical;
return raw;
}

View File

@@ -12,6 +12,7 @@ import { loadConfig } from "./config/config.js";
import {
deriveSessionKey,
loadSessionStore,
resolveSessionKey,
resolveStorePath,
saveSessionStore,
} from "./config/sessions.js";
@@ -52,6 +53,7 @@ export {
normalizeE164,
PortInUseError,
promptYesNo,
resolveSessionKey,
resolveStorePath,
runCommandWithTimeout,
runExec,

View File

@@ -5,8 +5,8 @@ import { waitForever } from "../cli/wait.js";
import { loadConfig } from "../config/config.js";
import {
DEFAULT_IDLE_MINUTES,
deriveSessionKey,
loadSessionStore,
resolveSessionKey,
resolveStorePath,
saveSessionStore,
} from "../config/sessions.js";
@@ -213,11 +213,15 @@ export async function runWebHeartbeatOnce(opts: {
});
const cfg = cfgOverride ?? loadConfig();
const sessionCfg = cfg.inbound?.reply?.session;
const mainKey = sessionCfg?.mainKey ?? "main";
const sessionScope = sessionCfg?.scope ?? "per-sender";
const sessionKey = resolveSessionKey(sessionScope, { From: to }, mainKey);
if (sessionId) {
const storePath = resolveStorePath(cfg.inbound?.reply?.session?.store);
const store = loadSessionStore(storePath);
store[to] = {
...(store[to] ?? {}),
store[sessionKey] = {
...(store[sessionKey] ?? {}),
sessionId,
updatedAt: Date.now(),
};
@@ -432,7 +436,11 @@ function getSessionSnapshot(
) {
const sessionCfg = cfg.inbound?.reply?.session;
const scope = sessionCfg?.scope ?? "per-sender";
const key = deriveSessionKey(scope, { From: from, To: "", Body: "" });
const key = resolveSessionKey(
scope,
{ From: from, To: "", Body: "" },
sessionCfg?.mainKey ?? "main",
);
const store = loadSessionStore(resolveStorePath(sessionCfg?.store));
const entry = store[key];
const idleMinutes = Math.max(
@@ -790,6 +798,7 @@ export async function monitorWebProvider(
GroupMembers: latest.groupParticipants?.join(", "),
SenderName: latest.senderName,
SenderE164: latest.senderE164,
Surface: "whatsapp",
},
{
onReplyStart: latest.sendComposing,