remove strip markdown config, enabled by default
This commit is contained in:
parent
be3a16f294
commit
ad828b9f0b
@ -289,9 +289,6 @@ If you see "token refresh disabled (no refresh token)":
|
||||
- `allowedRoles` - Role-based access control (`"moderator" | "owner" | "vip" | "subscriber" | "all"`)
|
||||
- `requireMention` - Require @mention (default: `false`)
|
||||
|
||||
**Plugin config:**
|
||||
- `stripMarkdown` - Strip markdown from outbound (default: `true`)
|
||||
|
||||
**Provider options:**
|
||||
- `channels.twitch.enabled` - Enable/disable channel startup
|
||||
- `channels.twitch.username` - Bot username (simplified single-account config)
|
||||
@ -333,13 +330,6 @@ Full example:
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
twitch: {
|
||||
stripMarkdown: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -36,20 +36,22 @@ const TwitchAccountSchema = z.object({
|
||||
obtainmentTimestamp: z.number().optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Base configuration properties shared by both single and multi-account modes
|
||||
*/
|
||||
const TwitchConfigBaseSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
markdown: MarkdownConfigSchema.optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Simplified single-account configuration schema
|
||||
*
|
||||
* Use this for single-account setups. Properties are at the top level,
|
||||
* creating an implicit "default" account.
|
||||
*/
|
||||
const simplifiedSchema = z.intersection(
|
||||
z.object({
|
||||
name: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
markdown: MarkdownConfigSchema.optional(),
|
||||
}),
|
||||
TwitchAccountSchema,
|
||||
);
|
||||
const simplifiedSchema = z.intersection(TwitchConfigBaseSchema, TwitchAccountSchema);
|
||||
|
||||
/**
|
||||
* Multi-account configuration schema
|
||||
@ -57,11 +59,7 @@ const simplifiedSchema = z.intersection(
|
||||
* Use this for multi-account setups. Each key is an account ID (e.g., "default", "secondary").
|
||||
*/
|
||||
const multiAccountSchema = z.intersection(
|
||||
z.object({
|
||||
name: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
markdown: MarkdownConfigSchema.optional(),
|
||||
}),
|
||||
TwitchConfigBaseSchema,
|
||||
z
|
||||
.object({
|
||||
/** Per-account configuration (for multi-account setups) */
|
||||
|
||||
@ -1,33 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { getAccountConfig, parsePluginConfig } from "./config.js";
|
||||
|
||||
describe("parsePluginConfig", () => {
|
||||
it("parses valid config with stripMarkdown true", () => {
|
||||
const result = parsePluginConfig({ stripMarkdown: true });
|
||||
expect(result.stripMarkdown).toBe(true);
|
||||
});
|
||||
|
||||
it("parses valid config with stripMarkdown false", () => {
|
||||
const result = parsePluginConfig({ stripMarkdown: false });
|
||||
expect(result.stripMarkdown).toBe(false);
|
||||
});
|
||||
|
||||
it("defaults to stripMarkdown true when not specified", () => {
|
||||
const result = parsePluginConfig({});
|
||||
expect(result.stripMarkdown).toBe(true);
|
||||
});
|
||||
|
||||
it("handles undefined config", () => {
|
||||
const result = parsePluginConfig(undefined as unknown as Record<string, unknown>);
|
||||
expect(result).toEqual({ stripMarkdown: true }); // Returns defaults when value is falsy
|
||||
});
|
||||
|
||||
it("handles null config", () => {
|
||||
const result = parsePluginConfig(null as unknown as Record<string, unknown>);
|
||||
expect(result).toEqual({ stripMarkdown: true }); // Returns defaults when value is null
|
||||
});
|
||||
});
|
||||
import { getAccountConfig } from "./config.js";
|
||||
|
||||
describe("getAccountConfig", () => {
|
||||
const mockMultiAccountConfig = {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
|
||||
import type { TwitchAccountConfig, TwitchPluginConfig } from "./types.js";
|
||||
import type { TwitchAccountConfig } from "./types.js";
|
||||
|
||||
/**
|
||||
* Default account ID for Twitch
|
||||
@ -24,49 +24,52 @@ export function getAccountConfig(
|
||||
return null;
|
||||
}
|
||||
|
||||
const cfg = coreConfig as Record<string, unknown>;
|
||||
const channels = cfg.channels as Record<string, unknown> | undefined;
|
||||
const twitch = channels?.twitch as Record<string, unknown> | undefined;
|
||||
const accounts = twitch?.accounts as Record<string, unknown> | undefined;
|
||||
const cfg = coreConfig as ClawdbotConfig;
|
||||
const twitch = cfg.channels?.twitch;
|
||||
// Access accounts via unknown to handle union type (single-account vs multi-account)
|
||||
const twitchRaw = twitch as Record<string, unknown> | undefined;
|
||||
const accounts = twitchRaw?.accounts as Record<string, TwitchAccountConfig> | undefined;
|
||||
|
||||
// For default account, check base-level config first
|
||||
if (accountId === DEFAULT_ACCOUNT_ID) {
|
||||
const accountFromAccounts = accounts?.[DEFAULT_ACCOUNT_ID] as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
const accountFromAccounts = accounts?.[DEFAULT_ACCOUNT_ID];
|
||||
|
||||
// Base-level properties that can form an implicit default account
|
||||
const baseLevel = {
|
||||
username: typeof twitch?.username === "string" ? twitch.username : undefined,
|
||||
accessToken: typeof twitch?.accessToken === "string" ? twitch.accessToken : undefined,
|
||||
clientId: typeof twitch?.clientId === "string" ? twitch.clientId : undefined,
|
||||
channel: typeof twitch?.channel === "string" ? twitch.channel : undefined,
|
||||
enabled: typeof twitch?.enabled === "boolean" ? twitch.enabled : undefined,
|
||||
allowFrom: Array.isArray(twitch?.allowFrom) ? twitch.allowFrom : undefined,
|
||||
allowedRoles: Array.isArray(twitch?.allowedRoles) ? twitch.allowedRoles : undefined,
|
||||
username: typeof twitchRaw?.username === "string" ? twitchRaw.username : undefined,
|
||||
accessToken: typeof twitchRaw?.accessToken === "string" ? twitchRaw.accessToken : undefined,
|
||||
clientId: typeof twitchRaw?.clientId === "string" ? twitchRaw.clientId : undefined,
|
||||
channel: typeof twitchRaw?.channel === "string" ? twitchRaw.channel : undefined,
|
||||
enabled: typeof twitchRaw?.enabled === "boolean" ? twitchRaw.enabled : undefined,
|
||||
allowFrom: Array.isArray(twitchRaw?.allowFrom) ? twitchRaw.allowFrom : undefined,
|
||||
allowedRoles: Array.isArray(twitchRaw?.allowedRoles) ? twitchRaw.allowedRoles : undefined,
|
||||
requireMention:
|
||||
typeof twitch?.requireMention === "boolean" ? twitch.requireMention : undefined,
|
||||
clientSecret: typeof twitch?.clientSecret === "string" ? twitch.clientSecret : undefined,
|
||||
refreshToken: typeof twitch?.refreshToken === "string" ? twitch.refreshToken : undefined,
|
||||
expiresIn: typeof twitch?.expiresIn === "number" ? twitch.expiresIn : undefined,
|
||||
typeof twitchRaw?.requireMention === "boolean" ? twitchRaw.requireMention : undefined,
|
||||
clientSecret:
|
||||
typeof twitchRaw?.clientSecret === "string" ? twitchRaw.clientSecret : undefined,
|
||||
refreshToken:
|
||||
typeof twitchRaw?.refreshToken === "string" ? twitchRaw.refreshToken : undefined,
|
||||
expiresIn: typeof twitchRaw?.expiresIn === "number" ? twitchRaw.expiresIn : undefined,
|
||||
obtainmentTimestamp:
|
||||
typeof twitch?.obtainmentTimestamp === "number" ? twitch.obtainmentTimestamp : undefined,
|
||||
typeof twitchRaw?.obtainmentTimestamp === "number"
|
||||
? twitchRaw.obtainmentTimestamp
|
||||
: undefined,
|
||||
};
|
||||
|
||||
// Merge: base-level takes precedence over accounts.default
|
||||
const merged = {
|
||||
const merged: Partial<TwitchAccountConfig> = {
|
||||
...accountFromAccounts,
|
||||
...baseLevel,
|
||||
} as Record<string, unknown>;
|
||||
} as Partial<TwitchAccountConfig>;
|
||||
|
||||
// Only return if we have at least username
|
||||
if (merged.username) {
|
||||
return merged as unknown as TwitchAccountConfig;
|
||||
return merged as TwitchAccountConfig;
|
||||
}
|
||||
|
||||
// Fall through to accounts.default if no base-level username
|
||||
if (accountFromAccounts) {
|
||||
return accountFromAccounts as unknown as TwitchAccountConfig;
|
||||
return accountFromAccounts;
|
||||
}
|
||||
|
||||
return null;
|
||||
@ -80,29 +83,16 @@ export function getAccountConfig(
|
||||
return accounts[accountId] as TwitchAccountConfig | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse plugin config
|
||||
*/
|
||||
export function parsePluginConfig(value: unknown): TwitchPluginConfig {
|
||||
if (!value || typeof value !== "object") {
|
||||
return { stripMarkdown: true };
|
||||
}
|
||||
|
||||
const raw = value as Record<string, unknown>;
|
||||
return {
|
||||
stripMarkdown: typeof raw.stripMarkdown === "boolean" ? raw.stripMarkdown : true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* List all configured account IDs
|
||||
*
|
||||
* Includes both explicit accounts and implicit "default" from base-level config
|
||||
*/
|
||||
export function listAccountIds(cfg: ClawdbotConfig): string[] {
|
||||
const accounts = (cfg as Record<string, unknown>).channels as Record<string, unknown> | undefined;
|
||||
const twitch = accounts?.twitch as Record<string, unknown> | undefined;
|
||||
const accountMap = twitch?.accounts as Record<string, unknown> | undefined;
|
||||
const twitch = cfg.channels?.twitch;
|
||||
// Access accounts via unknown to handle union type (single-account vs multi-account)
|
||||
const twitchRaw = twitch as Record<string, unknown> | undefined;
|
||||
const accountMap = twitchRaw?.accounts as Record<string, unknown> | undefined;
|
||||
|
||||
const ids: string[] = [];
|
||||
|
||||
@ -113,10 +103,10 @@ export function listAccountIds(cfg: ClawdbotConfig): string[] {
|
||||
|
||||
// Add implicit "default" if base-level config exists and "default" not already present
|
||||
const hasBaseLevelConfig =
|
||||
twitch &&
|
||||
(typeof twitch.username === "string" ||
|
||||
typeof twitch.accessToken === "string" ||
|
||||
typeof twitch.channel === "string");
|
||||
twitchRaw &&
|
||||
(typeof twitchRaw.username === "string" ||
|
||||
typeof twitchRaw.accessToken === "string" ||
|
||||
typeof twitchRaw.channel === "string");
|
||||
|
||||
if (hasBaseLevelConfig && !ids.includes(DEFAULT_ACCOUNT_ID)) {
|
||||
ids.push(DEFAULT_ACCOUNT_ID);
|
||||
|
||||
@ -5,10 +5,9 @@
|
||||
* resolves agent routes, and handles replies.
|
||||
*/
|
||||
|
||||
import type { ReplyPayload } from "clawdbot/plugin-sdk";
|
||||
import type { ReplyPayload, ClawdbotConfig } from "clawdbot/plugin-sdk";
|
||||
import type { TwitchAccountConfig, TwitchChatMessage } from "./types.js";
|
||||
import { checkTwitchAccessControl } from "./access-control.js";
|
||||
import { parsePluginConfig } from "./config.js";
|
||||
import { getTwitchRuntime } from "./runtime.js";
|
||||
import { getOrCreateClientManager } from "./client-manager-registry.js";
|
||||
import { stripMarkdownForTwitch } from "./utils/markdown.js";
|
||||
@ -46,9 +45,10 @@ async function processTwitchMessage(params: {
|
||||
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
|
||||
}): Promise<void> {
|
||||
const { message, account, accountId, config, runtime, core, statusSink } = params;
|
||||
const cfg = config as ClawdbotConfig;
|
||||
|
||||
const route = core.channel.routing.resolveAgentRoute({
|
||||
cfg: config as Parameters<typeof core.channel.routing.resolveAgentRoute>[0]["cfg"],
|
||||
cfg,
|
||||
channel: "twitch",
|
||||
accountId,
|
||||
peer: {
|
||||
@ -62,9 +62,7 @@ async function processTwitchMessage(params: {
|
||||
channel: "Twitch",
|
||||
from: message.displayName ?? message.username,
|
||||
timestamp: message.timestamp?.getTime(),
|
||||
envelope: core.channel.reply.resolveEnvelopeFormatOptions(
|
||||
config as Parameters<typeof core.channel.reply.resolveEnvelopeFormatOptions>[0],
|
||||
),
|
||||
envelope: core.channel.reply.resolveEnvelopeFormatOptions(cfg),
|
||||
body: rawBody,
|
||||
});
|
||||
|
||||
@ -89,7 +87,7 @@ async function processTwitchMessage(params: {
|
||||
});
|
||||
|
||||
const storePath = core.channel.session.resolveStorePath(
|
||||
(config as Parameters<typeof core.channel.session.resolveStorePath>[0]["cfg"])?.session?.store,
|
||||
config as Parameters<typeof core.channel.session.resolveStorePath>[0],
|
||||
{ agentId: route.agentId },
|
||||
);
|
||||
await core.channel.session.recordInboundSession({
|
||||
@ -102,16 +100,14 @@ async function processTwitchMessage(params: {
|
||||
});
|
||||
|
||||
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
||||
cfg: config as Parameters<typeof core.channel.text.resolveMarkdownTableMode>[0]["cfg"],
|
||||
cfg,
|
||||
channel: "twitch",
|
||||
accountId,
|
||||
});
|
||||
|
||||
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
||||
ctx: ctxPayload,
|
||||
cfg: config as Parameters<
|
||||
typeof core.channel.reply.dispatchReplyWithBufferedBlockDispatcher
|
||||
>[0]["cfg"],
|
||||
cfg,
|
||||
dispatcherOptions: {
|
||||
deliver: async (payload) => {
|
||||
await deliverTwitchReply({
|
||||
@ -168,9 +164,7 @@ async function deliverTwitchReply(params: {
|
||||
return;
|
||||
}
|
||||
|
||||
const pluginCfg = parsePluginConfig((config as any).pluginConfig ?? {});
|
||||
const textToSend =
|
||||
(pluginCfg.stripMarkdown ?? true) ? stripMarkdownForTwitch(payload.text) : payload.text;
|
||||
const textToSend = stripMarkdownForTwitch(payload.text);
|
||||
|
||||
await client.say(channel, textToSend);
|
||||
statusSink?.({ lastOutboundAt: Date.now() });
|
||||
|
||||
@ -17,7 +17,6 @@ import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
|
||||
vi.mock("./config.js", () => ({
|
||||
DEFAULT_ACCOUNT_ID: "default",
|
||||
getAccountConfig: vi.fn(),
|
||||
parsePluginConfig: vi.fn(() => ({ stripMarkdown: true })),
|
||||
}));
|
||||
|
||||
vi.mock("./send.js", () => ({
|
||||
@ -294,35 +293,6 @@ describe("outbound", () => {
|
||||
}),
|
||||
).rejects.toThrow("Connection lost");
|
||||
});
|
||||
|
||||
it("should respect stripMarkdown config", async () => {
|
||||
const { getAccountConfig } = await import("./config.js");
|
||||
const { parsePluginConfig } = await import("./config.js");
|
||||
const { sendMessageTwitchInternal } = await import("./send.js");
|
||||
|
||||
vi.mocked(getAccountConfig).mockReturnValue(mockAccount);
|
||||
vi.mocked(parsePluginConfig).mockReturnValue({ stripMarkdown: false });
|
||||
vi.mocked(sendMessageTwitchInternal).mockResolvedValue({
|
||||
ok: true,
|
||||
messageId: "msg-789",
|
||||
});
|
||||
|
||||
await twitchOutbound.sendText({
|
||||
cfg: mockConfig,
|
||||
to: "#testchannel",
|
||||
text: "**Bold** text",
|
||||
accountId: "default",
|
||||
});
|
||||
|
||||
expect(sendMessageTwitchInternal).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
false, // stripMarkdown disabled
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendMedia", () => {
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
* Supports text and media (URL) sending with markdown stripping and chunking.
|
||||
*/
|
||||
|
||||
import { DEFAULT_ACCOUNT_ID, getAccountConfig, parsePluginConfig } from "./config.js";
|
||||
import { DEFAULT_ACCOUNT_ID, getAccountConfig } from "./config.js";
|
||||
import { sendMessageTwitchInternal } from "./send.js";
|
||||
import type {
|
||||
ChannelOutboundAdapter,
|
||||
@ -127,17 +127,12 @@ export const twitchOutbound: ChannelOutboundAdapter = {
|
||||
throw new Error("No channel specified and no default channel in account config");
|
||||
}
|
||||
|
||||
const pluginCfg = parsePluginConfig(
|
||||
// biome-ignore lint/suspicious/noExplicitAny: pluginConfig is not part of CoreConfig
|
||||
(cfg as any).pluginConfig ?? {},
|
||||
);
|
||||
|
||||
const result = await sendMessageTwitchInternal(
|
||||
normalizeTwitchChannel(channel),
|
||||
text,
|
||||
cfg,
|
||||
resolvedAccountId,
|
||||
pluginCfg.stripMarkdown ?? true,
|
||||
true, // stripMarkdown
|
||||
console,
|
||||
);
|
||||
|
||||
|
||||
@ -35,14 +35,6 @@ import type { RuntimeEnv } from "../../../src/runtime.js";
|
||||
*/
|
||||
export type TwitchRole = "moderator" | "owner" | "vip" | "subscriber" | "all";
|
||||
|
||||
/**
|
||||
* Plugin configuration passed from Clawdbot
|
||||
*/
|
||||
export interface TwitchPluginConfig {
|
||||
/** Strip markdown from outbound messages before sending to Twitch (default true). */
|
||||
stripMarkdown?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Account configuration for a Twitch channel
|
||||
*/
|
||||
@ -140,5 +132,10 @@ export type {
|
||||
OutboundDeliveryResult,
|
||||
};
|
||||
|
||||
// Import and re-export the schema type
|
||||
import type { TwitchConfigSchema } from "./config-schema.js";
|
||||
import type { z } from "zod";
|
||||
export type TwitchConfig = z.infer<typeof TwitchConfigSchema>;
|
||||
|
||||
export type { ClawdbotConfig };
|
||||
export type { RuntimeEnv };
|
||||
|
||||
Loading…
Reference in New Issue
Block a user