import { ensureRecord, getRecord, isRecord, type LegacyConfigMigration, mergeMissing, } from "./legacy.shared.js"; export const LEGACY_CONFIG_MIGRATIONS_PART_1: LegacyConfigMigration[] = [ { id: "bindings.match.provider->bindings.match.channel", describe: "Move bindings[].match.provider to bindings[].match.channel", apply: (raw, changes) => { const bindings = Array.isArray(raw.bindings) ? raw.bindings : null; if (!bindings) return; let touched = false; for (const entry of bindings) { if (!isRecord(entry)) continue; const match = getRecord(entry.match); if (!match) continue; if (typeof match.channel === "string" && match.channel.trim()) continue; const provider = typeof match.provider === "string" ? match.provider.trim() : ""; if (!provider) continue; match.channel = provider; delete match.provider; entry.match = match; touched = true; } if (touched) { raw.bindings = bindings; changes.push("Moved bindings[].match.provider → bindings[].match.channel."); } }, }, { id: "bindings.match.accountID->bindings.match.accountId", describe: "Move bindings[].match.accountID to bindings[].match.accountId", apply: (raw, changes) => { const bindings = Array.isArray(raw.bindings) ? raw.bindings : null; if (!bindings) return; let touched = false; for (const entry of bindings) { if (!isRecord(entry)) continue; const match = getRecord(entry.match); if (!match) continue; if (match.accountId !== undefined) continue; const accountID = typeof match.accountID === "string" ? match.accountID.trim() : match.accountID; if (!accountID) continue; match.accountId = accountID; delete match.accountID; entry.match = match; touched = true; } if (touched) { raw.bindings = bindings; changes.push("Moved bindings[].match.accountID → bindings[].match.accountId."); } }, }, { id: "session.sendPolicy.rules.match.provider->match.channel", describe: "Move session.sendPolicy.rules[].match.provider to match.channel", apply: (raw, changes) => { const session = getRecord(raw.session); if (!session) return; const sendPolicy = getRecord(session.sendPolicy); if (!sendPolicy) return; const rules = Array.isArray(sendPolicy.rules) ? sendPolicy.rules : null; if (!rules) return; let touched = false; for (const rule of rules) { if (!isRecord(rule)) continue; const match = getRecord(rule.match); if (!match) continue; if (typeof match.channel === "string" && match.channel.trim()) continue; const provider = typeof match.provider === "string" ? match.provider.trim() : ""; if (!provider) continue; match.channel = provider; delete match.provider; rule.match = match; touched = true; } if (touched) { sendPolicy.rules = rules; session.sendPolicy = sendPolicy; raw.session = session; changes.push("Moved session.sendPolicy.rules[].match.provider → match.channel."); } }, }, { id: "messages.queue.byProvider->byChannel", describe: "Move messages.queue.byProvider to messages.queue.byChannel", apply: (raw, changes) => { const messages = getRecord(raw.messages); if (!messages) return; const queue = getRecord(messages.queue); if (!queue) return; if (queue.byProvider === undefined) return; if (queue.byChannel === undefined) { queue.byChannel = queue.byProvider; changes.push("Moved messages.queue.byProvider → messages.queue.byChannel."); } else { changes.push("Removed messages.queue.byProvider (messages.queue.byChannel already set)."); } delete queue.byProvider; messages.queue = queue; raw.messages = messages; }, }, { id: "providers->channels", describe: "Move provider config sections to channels.*", apply: (raw, changes) => { const legacyKeys = [ "whatsapp", "telegram", "discord", "slack", "signal", "imessage", "msteams", ]; const legacyEntries = legacyKeys.filter((key) => isRecord(raw[key])); if (legacyEntries.length === 0) return; const channels = ensureRecord(raw, "channels"); for (const key of legacyEntries) { const legacy = getRecord(raw[key]); if (!legacy) continue; const channelEntry = ensureRecord(channels, key); const hadEntries = Object.keys(channelEntry).length > 0; mergeMissing(channelEntry, legacy); channels[key] = channelEntry; delete raw[key]; changes.push( hadEntries ? `Merged ${key} → channels.${key}.` : `Moved ${key} → channels.${key}.`, ); } raw.channels = channels; }, }, { id: "routing.allowFrom->channels.whatsapp.allowFrom", describe: "Move routing.allowFrom to channels.whatsapp.allowFrom", apply: (raw, changes) => { const routing = raw.routing; if (!routing || typeof routing !== "object") return; const allowFrom = (routing as Record).allowFrom; if (allowFrom === undefined) return; const channels = getRecord(raw.channels); const whatsapp = channels ? getRecord(channels.whatsapp) : null; if (!whatsapp) { delete (routing as Record).allowFrom; if (Object.keys(routing as Record).length === 0) { delete raw.routing; } changes.push("Removed routing.allowFrom (channels.whatsapp not configured)."); return; } if (whatsapp.allowFrom === undefined) { whatsapp.allowFrom = allowFrom; changes.push("Moved routing.allowFrom → channels.whatsapp.allowFrom."); } else { changes.push("Removed routing.allowFrom (channels.whatsapp.allowFrom already set)."); } delete (routing as Record).allowFrom; if (Object.keys(routing as Record).length === 0) { delete raw.routing; } channels!.whatsapp = whatsapp; raw.channels = channels!; }, }, { id: "routing.groupChat.requireMention->groups.*.requireMention", describe: "Move routing.groupChat.requireMention to channels.whatsapp/telegram/imessage groups", apply: (raw, changes) => { const routing = raw.routing; if (!routing || typeof routing !== "object") return; const groupChat = (routing as Record).groupChat && typeof (routing as Record).groupChat === "object" ? ((routing as Record).groupChat as Record) : null; if (!groupChat) return; const requireMention = groupChat.requireMention; if (requireMention === undefined) return; const channels = ensureRecord(raw, "channels"); const applyTo = ( key: "whatsapp" | "telegram" | "imessage", options?: { requireExisting?: boolean }, ) => { if (options?.requireExisting && !isRecord(channels[key])) return; const section = channels[key] && typeof channels[key] === "object" ? (channels[key] as Record) : {}; const groups = section.groups && typeof section.groups === "object" ? (section.groups as Record) : {}; const defaultKey = "*"; const entry = groups[defaultKey] && typeof groups[defaultKey] === "object" ? (groups[defaultKey] as Record) : {}; if (entry.requireMention === undefined) { entry.requireMention = requireMention; groups[defaultKey] = entry; section.groups = groups; channels[key] = section; changes.push( `Moved routing.groupChat.requireMention → channels.${key}.groups."*".requireMention.`, ); } else { changes.push( `Removed routing.groupChat.requireMention (channels.${key}.groups."*" already set).`, ); } }; applyTo("whatsapp", { requireExisting: true }); applyTo("telegram"); applyTo("imessage"); delete groupChat.requireMention; if (Object.keys(groupChat).length === 0) { delete (routing as Record).groupChat; } if (Object.keys(routing as Record).length === 0) { delete raw.routing; } raw.channels = channels; }, }, { id: "gateway.token->gateway.auth.token", describe: "Move gateway.token to gateway.auth.token", apply: (raw, changes) => { const gateway = raw.gateway; if (!gateway || typeof gateway !== "object") return; const token = (gateway as Record).token; if (token === undefined) return; const gatewayObj = gateway as Record; const auth = gatewayObj.auth && typeof gatewayObj.auth === "object" ? (gatewayObj.auth as Record) : {}; if (auth.token === undefined) { auth.token = token; if (!auth.mode) auth.mode = "token"; changes.push("Moved gateway.token → gateway.auth.token."); } else { changes.push("Removed gateway.token (gateway.auth.token already set)."); } delete gatewayObj.token; if (Object.keys(auth).length > 0) { gatewayObj.auth = auth; } raw.gateway = gatewayObj; }, }, { id: "telegram.requireMention->channels.telegram.groups.*.requireMention", describe: "Move telegram.requireMention to channels.telegram.groups.*.requireMention", apply: (raw, changes) => { const channels = ensureRecord(raw, "channels"); const telegram = channels.telegram; if (!telegram || typeof telegram !== "object") return; const requireMention = (telegram as Record).requireMention; if (requireMention === undefined) return; const groups = (telegram as Record).groups && typeof (telegram as Record).groups === "object" ? ((telegram as Record).groups as Record) : {}; const defaultKey = "*"; const entry = groups[defaultKey] && typeof groups[defaultKey] === "object" ? (groups[defaultKey] as Record) : {}; if (entry.requireMention === undefined) { entry.requireMention = requireMention; groups[defaultKey] = entry; (telegram as Record).groups = groups; changes.push( 'Moved telegram.requireMention → channels.telegram.groups."*".requireMention.', ); } else { changes.push('Removed telegram.requireMention (channels.telegram.groups."*" already set).'); } delete (telegram as Record).requireMention; channels.telegram = telegram as Record; raw.channels = channels; }, }, ];