fix(config): write built-in channel enabled state to channels, not plugins.entries
Built-in channels (telegram, slack, discord, etc.) were being auto-enabled via `plugins.entries.<channel>.enabled`, which fails config validation because these channels are not plugins. Now built-in channels are enabled via `channels.<channel>.enabled` and plugin channels continue to use `plugins.entries.<plugin>.enabled`. This also means built-in channels no longer need to be added to `plugins.allow` since they're not loaded through the plugin system. Fixes #3741
This commit is contained in:
parent
6372242da7
commit
253f6cff98
@ -11,21 +11,27 @@ describe("applyPluginAutoEnable", () => {
|
|||||||
env: {},
|
env: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.config.plugins?.entries?.slack?.enabled).toBe(true);
|
// Built-in channels (slack) are enabled via channels.<id>.enabled, not plugins.entries.
|
||||||
expect(result.config.plugins?.allow).toEqual(["telegram", "slack"]);
|
expect((result.config.channels as Record<string, unknown>)?.slack).toMatchObject({
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
// Built-in channels don't need plugins.allow entry.
|
||||||
|
expect(result.config.plugins?.allow).toEqual(["telegram"]);
|
||||||
expect(result.changes.join("\n")).toContain("Slack configured, not enabled yet.");
|
expect(result.changes.join("\n")).toContain("Slack configured, not enabled yet.");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("respects explicit disable", () => {
|
it("respects explicit disable", () => {
|
||||||
const result = applyPluginAutoEnable({
|
const result = applyPluginAutoEnable({
|
||||||
config: {
|
config: {
|
||||||
channels: { slack: { botToken: "x" } },
|
channels: { slack: { botToken: "x", enabled: false } },
|
||||||
plugins: { entries: { slack: { enabled: false } } },
|
|
||||||
},
|
},
|
||||||
env: {},
|
env: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.config.plugins?.entries?.slack?.enabled).toBe(false);
|
// Built-in channels check enabled in channels.<id>.enabled.
|
||||||
|
expect((result.config.channels as Record<string, unknown>)?.slack).toMatchObject({
|
||||||
|
enabled: false,
|
||||||
|
});
|
||||||
expect(result.changes).toEqual([]);
|
expect(result.changes).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -72,8 +78,12 @@ describe("applyPluginAutoEnable", () => {
|
|||||||
env: {},
|
env: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// bluebubbles is a plugin channel, so it goes to plugins.entries.
|
||||||
expect(result.config.plugins?.entries?.bluebubbles?.enabled).toBe(true);
|
expect(result.config.plugins?.entries?.bluebubbles?.enabled).toBe(true);
|
||||||
expect(result.config.plugins?.entries?.imessage?.enabled).toBeUndefined();
|
// imessage is a built-in channel, but it's skipped due to preferOver.
|
||||||
|
expect((result.config.channels as Record<string, unknown>)?.imessage).not.toMatchObject({
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
expect(result.changes.join("\n")).toContain("bluebubbles configured, not enabled yet.");
|
expect(result.changes.join("\n")).toContain("bluebubbles configured, not enabled yet.");
|
||||||
expect(result.changes.join("\n")).not.toContain("iMessage configured, not enabled yet.");
|
expect(result.changes.join("\n")).not.toContain("iMessage configured, not enabled yet.");
|
||||||
});
|
});
|
||||||
@ -83,15 +93,17 @@ describe("applyPluginAutoEnable", () => {
|
|||||||
config: {
|
config: {
|
||||||
channels: {
|
channels: {
|
||||||
bluebubbles: { serverUrl: "http://localhost:1234", password: "x" },
|
bluebubbles: { serverUrl: "http://localhost:1234", password: "x" },
|
||||||
imessage: { cliPath: "/usr/local/bin/imsg" },
|
imessage: { cliPath: "/usr/local/bin/imsg", enabled: true },
|
||||||
},
|
},
|
||||||
plugins: { entries: { imessage: { enabled: true } } },
|
|
||||||
},
|
},
|
||||||
env: {},
|
env: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.config.plugins?.entries?.bluebubbles?.enabled).toBe(true);
|
expect(result.config.plugins?.entries?.bluebubbles?.enabled).toBe(true);
|
||||||
expect(result.config.plugins?.entries?.imessage?.enabled).toBe(true);
|
// imessage was already enabled in channels, stays enabled.
|
||||||
|
expect((result.config.channels as Record<string, unknown>)?.imessage).toMatchObject({
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows imessage auto-enable when bluebubbles is explicitly disabled", () => {
|
it("allows imessage auto-enable when bluebubbles is explicitly disabled", () => {
|
||||||
@ -107,7 +119,10 @@ describe("applyPluginAutoEnable", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(result.config.plugins?.entries?.bluebubbles?.enabled).toBe(false);
|
expect(result.config.plugins?.entries?.bluebubbles?.enabled).toBe(false);
|
||||||
expect(result.config.plugins?.entries?.imessage?.enabled).toBe(true);
|
// imessage is a built-in channel, so it goes to channels.imessage.enabled.
|
||||||
|
expect((result.config.channels as Record<string, unknown>)?.imessage).toMatchObject({
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
expect(result.changes.join("\n")).toContain("iMessage configured, not enabled yet.");
|
expect(result.changes.join("\n")).toContain("iMessage configured, not enabled yet.");
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -124,7 +139,10 @@ describe("applyPluginAutoEnable", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(result.config.plugins?.entries?.bluebubbles?.enabled).toBeUndefined();
|
expect(result.config.plugins?.entries?.bluebubbles?.enabled).toBeUndefined();
|
||||||
expect(result.config.plugins?.entries?.imessage?.enabled).toBe(true);
|
// imessage is a built-in channel, so it goes to channels.imessage.enabled.
|
||||||
|
expect((result.config.channels as Record<string, unknown>)?.imessage).toMatchObject({
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("enables imessage normally when only imessage is configured", () => {
|
it("enables imessage normally when only imessage is configured", () => {
|
||||||
@ -135,7 +153,10 @@ describe("applyPluginAutoEnable", () => {
|
|||||||
env: {},
|
env: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.config.plugins?.entries?.imessage?.enabled).toBe(true);
|
// imessage is a built-in channel, so it goes to channels.imessage.enabled.
|
||||||
|
expect((result.config.channels as Record<string, unknown>)?.imessage).toMatchObject({
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
expect(result.changes.join("\n")).toContain("iMessage configured, not enabled yet.");
|
expect(result.changes.join("\n")).toContain("iMessage configured, not enabled yet.");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -270,10 +270,28 @@ function resolveConfiguredPlugins(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isPluginExplicitlyDisabled(cfg: MoltbotConfig, pluginId: string): boolean {
|
function isPluginExplicitlyDisabled(cfg: MoltbotConfig, pluginId: string): boolean {
|
||||||
|
// Built-in channels use channels.<id>.enabled, not plugins.entries.
|
||||||
|
const builtinChannelId = normalizeChatChannelId(pluginId);
|
||||||
|
if (builtinChannelId) {
|
||||||
|
const channels = cfg.channels as Record<string, unknown> | undefined;
|
||||||
|
const entry = channels?.[builtinChannelId];
|
||||||
|
return isRecord(entry) && entry.enabled === false;
|
||||||
|
}
|
||||||
const entry = cfg.plugins?.entries?.[pluginId];
|
const entry = cfg.plugins?.entries?.[pluginId];
|
||||||
return entry?.enabled === false;
|
return entry?.enabled === false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isPluginAlreadyEnabled(cfg: MoltbotConfig, pluginId: string): boolean {
|
||||||
|
// Built-in channels use channels.<id>.enabled, not plugins.entries.
|
||||||
|
const builtinChannelId = normalizeChatChannelId(pluginId);
|
||||||
|
if (builtinChannelId) {
|
||||||
|
const channels = cfg.channels as Record<string, unknown> | undefined;
|
||||||
|
const entry = channels?.[builtinChannelId];
|
||||||
|
return isRecord(entry) && entry.enabled === true;
|
||||||
|
}
|
||||||
|
return cfg.plugins?.entries?.[pluginId]?.enabled === true;
|
||||||
|
}
|
||||||
|
|
||||||
function isPluginDenied(cfg: MoltbotConfig, pluginId: string): boolean {
|
function isPluginDenied(cfg: MoltbotConfig, pluginId: string): boolean {
|
||||||
const deny = cfg.plugins?.deny;
|
const deny = cfg.plugins?.deny;
|
||||||
return Array.isArray(deny) && deny.includes(pluginId);
|
return Array.isArray(deny) && deny.includes(pluginId);
|
||||||
@ -317,7 +335,29 @@ function ensureAllowlisted(cfg: MoltbotConfig, pluginId: string): MoltbotConfig
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function enableBuiltinChannel(cfg: MoltbotConfig, channelId: string): MoltbotConfig {
|
||||||
|
const channels = cfg.channels as Record<string, unknown> | undefined;
|
||||||
|
const existingEntry = channels?.[channelId];
|
||||||
|
const channelEntry = isRecord(existingEntry) ? existingEntry : {};
|
||||||
|
return {
|
||||||
|
...cfg,
|
||||||
|
channels: {
|
||||||
|
...channels,
|
||||||
|
[channelId]: {
|
||||||
|
...channelEntry,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function enablePluginEntry(cfg: MoltbotConfig, pluginId: string): MoltbotConfig {
|
function enablePluginEntry(cfg: MoltbotConfig, pluginId: string): MoltbotConfig {
|
||||||
|
// Built-in channels should be enabled via channels.<id>.enabled, not plugins.entries.
|
||||||
|
const builtinChannelId = normalizeChatChannelId(pluginId);
|
||||||
|
if (builtinChannelId) {
|
||||||
|
return enableBuiltinChannel(cfg, builtinChannelId);
|
||||||
|
}
|
||||||
|
|
||||||
const entries = {
|
const entries = {
|
||||||
...cfg.plugins?.entries,
|
...cfg.plugins?.entries,
|
||||||
[pluginId]: {
|
[pluginId]: {
|
||||||
@ -366,12 +406,24 @@ export function applyPluginAutoEnable(params: {
|
|||||||
if (isPluginDenied(next, entry.pluginId)) continue;
|
if (isPluginDenied(next, entry.pluginId)) continue;
|
||||||
if (isPluginExplicitlyDisabled(next, entry.pluginId)) continue;
|
if (isPluginExplicitlyDisabled(next, entry.pluginId)) continue;
|
||||||
if (shouldSkipPreferredPluginAutoEnable(next, entry, configured)) continue;
|
if (shouldSkipPreferredPluginAutoEnable(next, entry, configured)) continue;
|
||||||
const allow = next.plugins?.allow;
|
|
||||||
const allowMissing = Array.isArray(allow) && !allow.includes(entry.pluginId);
|
const isBuiltinChannel = normalizeChatChannelId(entry.pluginId) !== null;
|
||||||
const alreadyEnabled = next.plugins?.entries?.[entry.pluginId]?.enabled === true;
|
const alreadyEnabled = isPluginAlreadyEnabled(next, entry.pluginId);
|
||||||
if (alreadyEnabled && !allowMissing) continue;
|
|
||||||
|
// For plugin channels, also check plugins.allow list.
|
||||||
|
if (!isBuiltinChannel) {
|
||||||
|
const allow = next.plugins?.allow;
|
||||||
|
const allowMissing = Array.isArray(allow) && !allow.includes(entry.pluginId);
|
||||||
|
if (alreadyEnabled && !allowMissing) continue;
|
||||||
|
} else {
|
||||||
|
if (alreadyEnabled) continue;
|
||||||
|
}
|
||||||
|
|
||||||
next = enablePluginEntry(next, entry.pluginId);
|
next = enablePluginEntry(next, entry.pluginId);
|
||||||
next = ensureAllowlisted(next, entry.pluginId);
|
// Built-in channels don't need plugins.allow entry.
|
||||||
|
if (!isBuiltinChannel) {
|
||||||
|
next = ensureAllowlisted(next, entry.pluginId);
|
||||||
|
}
|
||||||
changes.push(formatAutoEnableChange(entry));
|
changes.push(formatAutoEnableChange(entry));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user