diff --git a/extensions/irc/src/monitor.test.ts b/extensions/irc/src/monitor.test.ts new file mode 100644 index 0000000000..b8af37265e --- /dev/null +++ b/extensions/irc/src/monitor.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from "vitest"; +import { resolveIrcInboundTarget } from "./monitor.js"; + +describe("irc monitor inbound target", () => { + it("keeps channel target for group messages", () => { + expect( + resolveIrcInboundTarget({ + target: "#openclaw", + senderNick: "alice", + }), + ).toEqual({ + isGroup: true, + target: "#openclaw", + rawTarget: "#openclaw", + }); + }); + + it("maps DM target to sender nick and preserves raw target", () => { + expect( + resolveIrcInboundTarget({ + target: "openclaw-bot", + senderNick: "alice", + }), + ).toEqual({ + isGroup: false, + target: "alice", + rawTarget: "openclaw-bot", + }); + }); + + it("falls back to raw target when sender nick is empty", () => { + expect( + resolveIrcInboundTarget({ + target: "openclaw-bot", + senderNick: " ", + }), + ).toEqual({ + isGroup: false, + target: "openclaw-bot", + rawTarget: "openclaw-bot", + }); + }); +}); diff --git a/extensions/irc/src/monitor.ts b/extensions/irc/src/monitor.ts index 1489b5f9b5..bcfd88138e 100644 --- a/extensions/irc/src/monitor.ts +++ b/extensions/irc/src/monitor.ts @@ -16,6 +16,20 @@ export type IrcMonitorOptions = { onMessage?: (message: IrcInboundMessage, client: IrcClient) => void | Promise; }; +export function resolveIrcInboundTarget(params: { target: string; senderNick: string }): { + isGroup: boolean; + target: string; + rawTarget: string; +} { + const rawTarget = params.target; + const isGroup = isChannelTarget(rawTarget); + if (isGroup) { + return { isGroup: true, target: rawTarget, rawTarget }; + } + const senderNick = params.senderNick.trim(); + return { isGroup: false, target: senderNick || rawTarget, rawTarget }; +} + export async function monitorIrcProvider(opts: IrcMonitorOptions): Promise<{ stop: () => void }> { const core = getIrcRuntime(); const cfg = opts.config ?? (core.config.loadConfig() as CoreConfig); @@ -83,16 +97,20 @@ export async function monitorIrcProvider(opts: IrcMonitorOptions): Promise<{ sto return; } - const isGroup = isChannelTarget(event.target); + const inboundTarget = resolveIrcInboundTarget({ + target: event.target, + senderNick: event.senderNick, + }); const message: IrcInboundMessage = { messageId: makeIrcMessageId(), - target: event.target, + target: inboundTarget.target, + rawTarget: inboundTarget.rawTarget, senderNick: event.senderNick, senderUser: event.senderUser, senderHost: event.senderHost, text: event.text, timestamp: Date.now(), - isGroup, + isGroup: inboundTarget.isGroup, }; core.channel.activity.record({ diff --git a/extensions/irc/src/onboarding.test.ts b/extensions/irc/src/onboarding.test.ts index be3c7b6ee9..400e34fc73 100644 --- a/extensions/irc/src/onboarding.test.ts +++ b/extensions/irc/src/onboarding.test.ts @@ -72,4 +72,47 @@ describe("irc onboarding", () => { expect(result.cfg.channels?.irc?.groupPolicy).toBe("allowlist"); expect(Object.keys(result.cfg.channels?.irc?.groups ?? {})).toEqual(["#openclaw", "#ops"]); }); + + it("writes DM allowFrom to top-level config for non-default account prompts", async () => { + const prompter: WizardPrompter = { + intro: vi.fn(async () => {}), + outro: vi.fn(async () => {}), + note: vi.fn(async () => {}), + select: vi.fn(async () => "allowlist"), + multiselect: vi.fn(async () => []), + text: vi.fn(async ({ message }: { message: string }) => { + if (message === "IRC allowFrom (nick or nick!user@host)") { + return "Alice, Bob!ident@example.org"; + } + throw new Error(`Unexpected prompt: ${message}`); + }) as WizardPrompter["text"], + confirm: vi.fn(async () => false), + progress: vi.fn(() => ({ update: vi.fn(), stop: vi.fn() })), + }; + + const promptAllowFrom = ircOnboardingAdapter.dmPolicy?.promptAllowFrom; + expect(promptAllowFrom).toBeTypeOf("function"); + + const cfg: CoreConfig = { + channels: { + irc: { + accounts: { + work: { + host: "irc.libera.chat", + nick: "openclaw-work", + }, + }, + }, + }, + }; + + const updated = (await promptAllowFrom?.({ + cfg, + prompter, + accountId: "work", + })) as CoreConfig; + + expect(updated.channels?.irc?.allowFrom).toEqual(["alice", "bob!ident@example.org"]); + expect(updated.channels?.irc?.accounts?.work?.allowFrom).toBeUndefined(); + }); }); diff --git a/extensions/irc/src/onboarding.ts b/extensions/irc/src/onboarding.ts index 5fe158893d..6f0508f676 100644 --- a/extensions/irc/src/onboarding.ts +++ b/extensions/irc/src/onboarding.ts @@ -105,8 +105,17 @@ function setIrcDmPolicy(cfg: CoreConfig, dmPolicy: DmPolicy): CoreConfig { }; } -function setIrcAllowFrom(cfg: CoreConfig, accountId: string, allowFrom: string[]): CoreConfig { - return updateIrcAccountConfig(cfg, accountId, { allowFrom }); +function setIrcAllowFrom(cfg: CoreConfig, allowFrom: string[]): CoreConfig { + return { + ...cfg, + channels: { + ...cfg.channels, + irc: { + ...cfg.channels?.irc, + allowFrom, + }, + }, + }; } function setIrcNickServ( @@ -157,9 +166,7 @@ async function promptIrcAllowFrom(params: { prompter: WizardPrompter; accountId?: string; }): Promise { - const accountId = params.accountId?.trim() || resolveDefaultIrcAccountId(params.cfg); - const resolved = resolveIrcAccount({ cfg: params.cfg, accountId }); - const existing = resolved.config.allowFrom ?? []; + const existing = params.cfg.channels?.irc?.allowFrom ?? []; await params.prompter.note( [ @@ -188,7 +195,7 @@ async function promptIrcAllowFrom(params: { .filter(Boolean), ), ]; - return setIrcAllowFrom(params.cfg, accountId, normalized); + return setIrcAllowFrom(params.cfg, normalized); } async function promptIrcNickServConfig(params: { diff --git a/extensions/irc/src/types.ts b/extensions/irc/src/types.ts index b01350f73c..124b240ee5 100644 --- a/extensions/irc/src/types.ts +++ b/extensions/irc/src/types.ts @@ -77,7 +77,10 @@ export type CoreConfig = OpenClawConfig & { export type IrcInboundMessage = { messageId: string; + /** Conversation peer id: channel name for groups, sender nick for DMs. */ target: string; + /** Raw IRC PRIVMSG target (bot nick for DMs, channel for groups). */ + rawTarget?: string; senderNick: string; senderUser?: string; senderHost?: string; diff --git a/src/config/types.channels.ts b/src/config/types.channels.ts index 073fccf326..465e5de284 100644 --- a/src/config/types.channels.ts +++ b/src/config/types.channels.ts @@ -1,8 +1,8 @@ import type { GroupPolicy } from "./types.base.js"; import type { DiscordConfig } from "./types.discord.js"; -import type { IrcConfig } from "./types.irc.js"; import type { GoogleChatConfig } from "./types.googlechat.js"; import type { IMessageConfig } from "./types.imessage.js"; +import type { IrcConfig } from "./types.irc.js"; import type { MSTeamsConfig } from "./types.msteams.js"; import type { SignalConfig } from "./types.signal.js"; import type { SlackConfig } from "./types.slack.js";