IRC: fix DM target + onboarding allowFrom consistency

This commit is contained in:
Vignesh Natarajan
2026-02-07 14:06:27 -08:00
parent 4f577a4a13
commit b5bdf7b26f
6 changed files with 124 additions and 10 deletions

View File

@@ -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",
});
});
});

View File

@@ -16,6 +16,20 @@ export type IrcMonitorOptions = {
onMessage?: (message: IrcInboundMessage, client: IrcClient) => void | Promise<void>;
};
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({

View File

@@ -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();
});
});

View File

@@ -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<CoreConfig> {
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: {

View File

@@ -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;

View File

@@ -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";