diff --git a/src/config/sessions/metadata.test.ts b/src/config/sessions/metadata.test.ts new file mode 100644 index 000000000..c532b862b --- /dev/null +++ b/src/config/sessions/metadata.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from "vitest"; + +import { deriveSessionMetaPatch } from "./metadata.js"; + +describe("deriveSessionMetaPatch", () => { + it("captures origin + group metadata", () => { + const patch = deriveSessionMetaPatch({ + ctx: { + Provider: "whatsapp", + ChatType: "group", + GroupSubject: "Family", + From: "123@g.us", + }, + sessionKey: "agent:main:whatsapp:group:123@g.us", + }); + + expect(patch?.origin?.label).toBe("Family id:123@g.us"); + expect(patch?.origin?.provider).toBe("whatsapp"); + expect(patch?.subject).toBe("Family"); + expect(patch?.channel).toBe("whatsapp"); + expect(patch?.groupId).toBe("123@g.us"); + }); +}); diff --git a/src/config/sessions/metadata.ts b/src/config/sessions/metadata.ts new file mode 100644 index 000000000..b22a110ef --- /dev/null +++ b/src/config/sessions/metadata.ts @@ -0,0 +1,124 @@ +import type { MsgContext } from "../../auto-reply/templating.js"; +import { normalizeChatType } from "../../channels/chat-type.js"; +import { resolveConversationLabel } from "../../channels/conversation-label.js"; +import { getChannelDock } from "../../channels/dock.js"; +import { normalizeChannelId } from "../../channels/plugins/index.js"; +import { normalizeMessageChannel } from "../../utils/message-channel.js"; +import { buildGroupDisplayName, resolveGroupSessionKey } from "./group.js"; +import type { GroupKeyResolution, SessionEntry, SessionOrigin } from "./types.js"; + +const mergeOrigin = ( + existing: SessionOrigin | undefined, + next: SessionOrigin | undefined, +): SessionOrigin | undefined => { + if (!existing && !next) return undefined; + const merged: SessionOrigin = existing ? { ...existing } : {}; + if (next?.label) merged.label = next.label; + if (next?.provider) merged.provider = next.provider; + if (next?.surface) merged.surface = next.surface; + if (next?.chatType) merged.chatType = next.chatType; + if (next?.from) merged.from = next.from; + if (next?.to) merged.to = next.to; + if (next?.accountId) merged.accountId = next.accountId; + if (next?.threadId != null && next.threadId !== "") merged.threadId = next.threadId; + return Object.keys(merged).length > 0 ? merged : undefined; +}; + +export function deriveSessionOrigin(ctx: MsgContext): SessionOrigin | undefined { + const label = resolveConversationLabel(ctx)?.trim(); + const providerRaw = + (typeof ctx.OriginatingChannel === "string" && ctx.OriginatingChannel) || + ctx.Surface || + ctx.Provider; + const provider = normalizeMessageChannel(providerRaw); + const surface = ctx.Surface?.trim().toLowerCase(); + const chatType = normalizeChatType(ctx.ChatType) ?? undefined; + const from = ctx.From?.trim(); + const to = + (typeof ctx.OriginatingTo === "string" ? ctx.OriginatingTo : ctx.To)?.trim() ?? undefined; + const accountId = ctx.AccountId?.trim(); + const threadId = ctx.MessageThreadId ?? undefined; + + const origin: SessionOrigin = {}; + if (label) origin.label = label; + if (provider) origin.provider = provider; + if (surface) origin.surface = surface; + if (chatType) origin.chatType = chatType; + if (from) origin.from = from; + if (to) origin.to = to; + if (accountId) origin.accountId = accountId; + if (threadId != null && threadId !== "") origin.threadId = threadId; + + return Object.keys(origin).length > 0 ? origin : undefined; +} + +export function snapshotSessionOrigin(entry?: SessionEntry): SessionOrigin | undefined { + if (!entry?.origin) return undefined; + return { ...entry.origin }; +} + +export function deriveGroupSessionPatch(params: { + ctx: MsgContext; + sessionKey: string; + existing?: SessionEntry; + groupResolution?: GroupKeyResolution | null; +}): Partial | null { + const resolution = params.groupResolution ?? resolveGroupSessionKey(params.ctx); + if (!resolution?.channel) return null; + + const channel = resolution.channel; + const subject = params.ctx.GroupSubject?.trim(); + const space = params.ctx.GroupSpace?.trim(); + const explicitChannel = params.ctx.GroupChannel?.trim(); + const normalizedChannel = normalizeChannelId(channel); + const isChannelProvider = Boolean( + normalizedChannel && + getChannelDock(normalizedChannel)?.capabilities.chatTypes.includes("channel"), + ); + const nextGroupChannel = + explicitChannel ?? + ((resolution.chatType === "channel" || isChannelProvider) && + subject && + subject.startsWith("#") + ? subject + : undefined); + const nextSubject = nextGroupChannel ? undefined : subject; + + const patch: Partial = { + chatType: resolution.chatType ?? "group", + channel, + groupId: resolution.id, + }; + if (nextSubject) patch.subject = nextSubject; + if (nextGroupChannel) patch.groupChannel = nextGroupChannel; + if (space) patch.space = space; + + const displayName = buildGroupDisplayName({ + provider: channel, + subject: nextSubject ?? params.existing?.subject, + groupChannel: nextGroupChannel ?? params.existing?.groupChannel, + space: space ?? params.existing?.space, + id: resolution.id, + key: params.sessionKey, + }); + if (displayName) patch.displayName = displayName; + + return patch; +} + +export function deriveSessionMetaPatch(params: { + ctx: MsgContext; + sessionKey: string; + existing?: SessionEntry; + groupResolution?: GroupKeyResolution | null; +}): Partial | null { + const groupPatch = deriveGroupSessionPatch(params); + const origin = deriveSessionOrigin(params.ctx); + if (!groupPatch && !origin) return null; + + const patch: Partial = groupPatch ? { ...groupPatch } : {}; + const mergedOrigin = mergeOrigin(params.existing?.origin, origin); + if (mergedOrigin) patch.origin = mergedOrigin; + + return Object.keys(patch).length > 0 ? patch : null; +}