wip type changes
This commit is contained in:
parent
ebf7dcc054
commit
9693933b9e
@ -25,6 +25,7 @@ Text is supported everywhere; media and reactions vary by channel.
|
|||||||
- [Matrix](/channels/matrix) — Matrix protocol (plugin, installed separately).
|
- [Matrix](/channels/matrix) — Matrix protocol (plugin, installed separately).
|
||||||
- [Nostr](/channels/nostr) — Decentralized DMs via NIP-04 (plugin, installed separately).
|
- [Nostr](/channels/nostr) — Decentralized DMs via NIP-04 (plugin, installed separately).
|
||||||
- [Tlon](/channels/tlon) — Urbit-based messenger (plugin, installed separately).
|
- [Tlon](/channels/tlon) — Urbit-based messenger (plugin, installed separately).
|
||||||
|
- [Twitch](/channels/twitch) — Twitch chat via IRC connection (plugin, installed separately).
|
||||||
- [Zalo](/channels/zalo) — Zalo Bot API; Vietnam's popular messenger (plugin, installed separately).
|
- [Zalo](/channels/zalo) — Zalo Bot API; Vietnam's popular messenger (plugin, installed separately).
|
||||||
- [Zalo Personal](/channels/zalouser) — Zalo personal account via QR login (plugin, installed separately).
|
- [Zalo Personal](/channels/zalouser) — Zalo personal account via QR login (plugin, installed separately).
|
||||||
- [WebChat](/web/webchat) — Gateway WebChat UI over WebSocket.
|
- [WebChat](/web/webchat) — Gateway WebChat UI over WebSocket.
|
||||||
|
|||||||
459
docs/channels/twitch.md
Normal file
459
docs/channels/twitch.md
Normal file
@ -0,0 +1,459 @@
|
|||||||
|
---
|
||||||
|
summary: "Twitch chat bot support status, capabilities, and configuration"
|
||||||
|
read_when:
|
||||||
|
- Setting up Twitch chat integration for Clawdbot
|
||||||
|
- Configuring Twitch bot permissions and access control
|
||||||
|
---
|
||||||
|
# Twitch (plugin)
|
||||||
|
|
||||||
|
Twitch chat support via IRC connection. Clawdbot connects as a Twitch user (bot account) to receive and send messages in channels. Requires a Twitch application with OAuth token.
|
||||||
|
|
||||||
|
Status: ready for Twitch chat via IRC connection with @twurple.
|
||||||
|
|
||||||
|
## Plugin required
|
||||||
|
|
||||||
|
Twitch ships as a plugin and is not bundled with the core install.
|
||||||
|
|
||||||
|
Install via CLI (npm registry):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot plugins install @clawdbot/twitch
|
||||||
|
```
|
||||||
|
|
||||||
|
Local checkout (when running from a git repo):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot plugins install ./extensions/twitch
|
||||||
|
```
|
||||||
|
|
||||||
|
Details: [Plugins](/plugin)
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1) Install the Twitch plugin:
|
||||||
|
- From npm: `clawdbot plugins install @clawdbot/twitch`
|
||||||
|
- From a local checkout: `clawdbot plugins install ./extensions/twitch`
|
||||||
|
|
||||||
|
2) Create a Twitch application:
|
||||||
|
- Go to [Twitch Developer Console](https://dev.twitch.tv/console)
|
||||||
|
- Click "Register Your Application"
|
||||||
|
- Set Application Type to "Chat Bot"
|
||||||
|
- Copy the Client ID
|
||||||
|
|
||||||
|
3) Generate your OAuth token:
|
||||||
|
- Use [Twitch Token Generator](https://twitchtokengenerator.com/) or [TwitchApps TMI](https://twitchapps.com/tmi/)
|
||||||
|
- Select scopes: `chat:read` and `chat:write`
|
||||||
|
- Copy the token (starts with `oauth:`)
|
||||||
|
|
||||||
|
4) Configure credentials:
|
||||||
|
- Env: `CLAWDBOT_TWITCH_ACCESS_TOKEN=...` (default account only)
|
||||||
|
- Or config: `channels.twitch.accounts.default.token`
|
||||||
|
- If both are set, config takes precedence (env fallback is default-account only)
|
||||||
|
|
||||||
|
5) Start the gateway. Twitch starts when a token is resolved (config first, env fallback).
|
||||||
|
|
||||||
|
6) Invite the bot to your channel (it will auto-join the channel specified in config).
|
||||||
|
|
||||||
|
Minimal config (default account):
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
channels: {
|
||||||
|
twitch: {
|
||||||
|
enabled: true,
|
||||||
|
accounts: {
|
||||||
|
default: {
|
||||||
|
username: "mybot",
|
||||||
|
token: "oauth:your_token_here",
|
||||||
|
clientId: "your_client_id_here"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Env-only setup:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export CLAWDBOT_TWITCH_ACCESS_TOKEN=oauth:your_token_here
|
||||||
|
```
|
||||||
|
|
||||||
|
With env, you still need `clientId` in config (or use the minimal config above without `token`).
|
||||||
|
|
||||||
|
## Token setup
|
||||||
|
|
||||||
|
### Option 1: Static token (simple)
|
||||||
|
|
||||||
|
Use [Twitch Token Generator](https://twitchtokengenerator.com/):
|
||||||
|
|
||||||
|
1. Select scopes: `chat:read` and `chat:write`
|
||||||
|
2. Copy the token (starts with `oauth:`)
|
||||||
|
3. Add to config:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
channels: {
|
||||||
|
twitch: {
|
||||||
|
accounts: {
|
||||||
|
default: {
|
||||||
|
username: "mybot",
|
||||||
|
token: "oauth:abc123...",
|
||||||
|
clientId: "your_client_id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: Automatic token refresh (recommended)
|
||||||
|
|
||||||
|
For long-running bots, configure RefreshingAuthProvider:
|
||||||
|
|
||||||
|
1. Use [Twitch Token Generator](https://twitchtokengenerator.com/) with **"Include Refresh Token"** checked
|
||||||
|
2. Get your Client Secret from [Twitch Developer Console](https://dev.twitch.tv/console)
|
||||||
|
3. Add to config:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
channels: {
|
||||||
|
twitch: {
|
||||||
|
accounts: {
|
||||||
|
default: {
|
||||||
|
username: "mybot",
|
||||||
|
token: "oauth:abc123...",
|
||||||
|
clientId: "your_client_id",
|
||||||
|
clientSecret: "your_client_secret",
|
||||||
|
refreshToken: "your_refresh_token",
|
||||||
|
expiresIn: 14400,
|
||||||
|
obtainmentTimestamp: 1706092800000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The bot will automatically refresh tokens before they expire and log refresh events.
|
||||||
|
|
||||||
|
## Access control
|
||||||
|
|
||||||
|
### Allowlist by User ID (recommended)
|
||||||
|
|
||||||
|
Most secure: only allow specific Twitch user IDs:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
channels: {
|
||||||
|
twitch: {
|
||||||
|
accounts: {
|
||||||
|
default: {
|
||||||
|
username: "mybot",
|
||||||
|
token: "oauth:...",
|
||||||
|
clientId: "...",
|
||||||
|
allowFrom: ["123456789", "987654321"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why user IDs instead of usernames?** Twitch usernames can change, which could allow someone to hijack another user's access. User IDs are permanent.
|
||||||
|
|
||||||
|
Find your Twitch user ID at: https://www.streamweasels.com/tools/convert-your-twitch-username-to-user-id/
|
||||||
|
|
||||||
|
### Role-based restrictions
|
||||||
|
|
||||||
|
Restrict access to specific roles:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
channels: {
|
||||||
|
twitch: {
|
||||||
|
accounts: {
|
||||||
|
default: {
|
||||||
|
username: "mybot",
|
||||||
|
token: "oauth:...",
|
||||||
|
clientId: "...",
|
||||||
|
allowedRoles: ["moderator", "vip"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Available roles:**
|
||||||
|
- `"moderator"` - Channel moderators
|
||||||
|
- `"owner"` - Channel owner/broadcaster
|
||||||
|
- `"vip"` - VIPs
|
||||||
|
- `"subscriber"` - Subscribers
|
||||||
|
- `"all"` - Anyone in chat
|
||||||
|
|
||||||
|
### Combined allowlist + roles
|
||||||
|
|
||||||
|
Users in `allowFrom` bypass role checks:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
channels: {
|
||||||
|
twitch: {
|
||||||
|
accounts: {
|
||||||
|
default: {
|
||||||
|
username: "mybot",
|
||||||
|
token: "oauth:...",
|
||||||
|
clientId: "...",
|
||||||
|
allowFrom: ["123456789"],
|
||||||
|
allowedRoles: ["moderator"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example:
|
||||||
|
- User `123456789` can always message (bypasses role check)
|
||||||
|
- All moderators can message
|
||||||
|
- Everyone else is blocked
|
||||||
|
|
||||||
|
### Require @mention
|
||||||
|
|
||||||
|
Only respond when the bot is mentioned:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
channels: {
|
||||||
|
twitch: {
|
||||||
|
accounts: {
|
||||||
|
default: {
|
||||||
|
username: "mybot",
|
||||||
|
token: "oauth:...",
|
||||||
|
clientId: "...",
|
||||||
|
requireMention: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Multiple accounts
|
||||||
|
|
||||||
|
Configure multiple Twitch channels:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
channels: {
|
||||||
|
twitch: {
|
||||||
|
accounts: {
|
||||||
|
main: {
|
||||||
|
username: "mybot",
|
||||||
|
token: "oauth:...",
|
||||||
|
clientId: "...",
|
||||||
|
channel: "streamer1"
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
username: "mybot",
|
||||||
|
token: "oauth:...",
|
||||||
|
clientId: "...",
|
||||||
|
channel: "streamer2",
|
||||||
|
allowedRoles: ["moderator"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
For the default account, you can use environment variables instead of config:
|
||||||
|
|
||||||
|
- `CLAWDBOT_TWITCH_ACCESS_TOKEN` - OAuth token (with `oauth:` prefix)
|
||||||
|
|
||||||
|
Env fallback only works for the default account. For multi-account setups, use config.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export CLAWDBOT_TWITCH_ACCESS_TOKEN=oauth:abc123def456...
|
||||||
|
```
|
||||||
|
|
||||||
|
Config:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
channels: {
|
||||||
|
twitch: {
|
||||||
|
enabled: true,
|
||||||
|
accounts: {
|
||||||
|
default: {
|
||||||
|
username: "mybot",
|
||||||
|
clientId: "your_client_id"
|
||||||
|
// token will be read from CLAWDBOT_TWITCH_ACCESS_TOKEN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Priority: account config > base config > env var (for default account only).
|
||||||
|
|
||||||
|
## Plugin options
|
||||||
|
|
||||||
|
Control markdown stripping behavior:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
plugins: {
|
||||||
|
entries: {
|
||||||
|
twitch: {
|
||||||
|
stripMarkdown: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `stripMarkdown` (default: `true`) - Remove markdown formatting before sending to Twitch
|
||||||
|
|
||||||
|
Twitch doesn't support markdown, so this is enabled by default. Disable if you want to send markdown as-is (it will appear as plain text with markdown symbols).
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
First, run diagnostic commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot doctor
|
||||||
|
clawdbot channels status --probe
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bot doesn't respond to messages
|
||||||
|
|
||||||
|
**Check access control:**
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
channels: {
|
||||||
|
twitch: {
|
||||||
|
accounts: {
|
||||||
|
default: {
|
||||||
|
username: "mybot",
|
||||||
|
token: "oauth:...",
|
||||||
|
clientId: "...",
|
||||||
|
// Temporary: allow everyone
|
||||||
|
allowedRoles: ["all"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Check the bot is in the channel:** The bot must join the channel (either `channel: "target_channel"` or defaults to `username`).
|
||||||
|
|
||||||
|
### Token issues
|
||||||
|
|
||||||
|
**"Failed to connect" or authentication errors:**
|
||||||
|
- Verify token starts with `oauth:`
|
||||||
|
- Check token has `chat:read` and `chat:write` scopes
|
||||||
|
- If using RefreshingAuthProvider, verify `clientSecret` and `refreshToken` are set
|
||||||
|
|
||||||
|
### Token refresh not working
|
||||||
|
|
||||||
|
**Check logs for refresh events:**
|
||||||
|
```
|
||||||
|
[twitch] Using env token source for mybot
|
||||||
|
[twitch] Access token refreshed for user 123456 (expires in 14400s)
|
||||||
|
```
|
||||||
|
|
||||||
|
If you see "token refresh disabled (no refresh token)":
|
||||||
|
- Ensure `clientSecret` is provided
|
||||||
|
- Ensure `refreshToken` is provided (from Twitch Token Generator with "Include Refresh Token" checked)
|
||||||
|
|
||||||
|
## Capabilities & limits
|
||||||
|
|
||||||
|
**Supported:**
|
||||||
|
- ✅ Channel messages (group chat)
|
||||||
|
- ✅ Whispers/DMs (received but replies not supported - Twitch doesn't allow bots to send whispers)
|
||||||
|
- ✅ Markdown stripping (automatically applied)
|
||||||
|
- ✅ Message chunking (500 char limit)
|
||||||
|
- ✅ Access control (user ID allowlist, role-based)
|
||||||
|
- ✅ @mention requirement
|
||||||
|
- ✅ Automatic token refresh (with RefreshingAuthProvider)
|
||||||
|
- ✅ Multi-account support
|
||||||
|
|
||||||
|
**Not supported:**
|
||||||
|
- ❌ Native reactions
|
||||||
|
- ❌ Threaded replies
|
||||||
|
- ❌ Message editing
|
||||||
|
- ❌ Message deletion
|
||||||
|
- ❌ Rich embeds/media uploads (sends media URLs as text)
|
||||||
|
|
||||||
|
## Config reference
|
||||||
|
|
||||||
|
### Account config
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
username: string, // Bot username (required)
|
||||||
|
token: string, // OAuth token with chat:read and chat:write (required)
|
||||||
|
clientId: string, // Twitch Client ID (required)
|
||||||
|
channel?: string, // Channel to join (default: username)
|
||||||
|
enabled?: boolean, // Enable this account (default: true)
|
||||||
|
clientSecret?: string, // For RefreshingAuthProvider
|
||||||
|
refreshToken?: string, // For RefreshingAuthProvider
|
||||||
|
expiresIn?: number, // Token expiry in seconds
|
||||||
|
obtainmentTimestamp?: number, // Token obtained timestamp
|
||||||
|
allowFrom?: string[], // User ID allowlist
|
||||||
|
allowedRoles?: TwitchRole[], // Role-based access control
|
||||||
|
requireMention?: boolean // Require @mention (default: false)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**TwitchRole:** `"moderator"` | `"owner"` | `"vip"` | `"subscriber"` | `"all"`
|
||||||
|
|
||||||
|
### Plugin config
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
stripMarkdown?: boolean // Strip markdown from outbound (default: true)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tool actions
|
||||||
|
|
||||||
|
The agent can call `twitch` with action:
|
||||||
|
|
||||||
|
- `send` - Send a message to a channel
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
"action": "twitch",
|
||||||
|
"params": {
|
||||||
|
"message": "Hello Twitch!",
|
||||||
|
"to": "#mychannel"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Safety & ops
|
||||||
|
|
||||||
|
- **Treat tokens like passwords** - Never commit tokens to git
|
||||||
|
- **Use RefreshingAuthProvider** for long-running bots
|
||||||
|
- **Use user ID allowlists** instead of usernames for access control
|
||||||
|
- **Monitor logs** for token refresh events and connection status
|
||||||
|
- **Scope tokens minimally** - Only request `chat:read` and `chat:write`
|
||||||
|
- **If stuck**: Restart the gateway after confirming no other process owns the session
|
||||||
|
|
||||||
|
## Message limits
|
||||||
|
|
||||||
|
- **500 characters** per message (Twitch limit)
|
||||||
|
- Messages are automatically chunked at word boundaries
|
||||||
|
- Markdown is stripped before chunking to avoid breaking patterns
|
||||||
|
- No rate limiting (uses Twitch's built-in rate limits)
|
||||||
18
extensions/twitch/index.ts
Normal file
18
extensions/twitch/index.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
||||||
|
import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
|
import { twitchPlugin } from "./src/plugin.js";
|
||||||
|
import { setTwitchRuntime } from "./src/runtime.js";
|
||||||
|
|
||||||
|
const plugin = {
|
||||||
|
id: "twitch",
|
||||||
|
name: "Twitch",
|
||||||
|
description: "Twitch channel plugin",
|
||||||
|
configSchema: emptyPluginConfigSchema(),
|
||||||
|
register(api: ClawdbotPluginApi) {
|
||||||
|
setTwitchRuntime(api.runtime);
|
||||||
|
api.registerChannel({ plugin: twitchPlugin });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default plugin;
|
||||||
@ -9,9 +9,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sinclair/typebox": "0.34.47",
|
|
||||||
"@twurple/api": "^8.0.3",
|
"@twurple/api": "^8.0.3",
|
||||||
"@twurple/auth": "^8.0.3",
|
"@twurple/auth": "^8.0.3",
|
||||||
"@twurple/chat": "^8.0.3"
|
"@twurple/chat": "^8.0.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"clawdbot": "workspace:*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
|
import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
|
||||||
import type {
|
import type {
|
||||||
CoreConfig,
|
|
||||||
TwitchAccountConfig,
|
TwitchAccountConfig,
|
||||||
TwitchPluginConfig,
|
TwitchPluginConfig,
|
||||||
} from "./types.js";
|
} from "./types.js";
|
||||||
@ -50,7 +50,7 @@ export function parsePluginConfig(value: unknown): TwitchPluginConfig {
|
|||||||
/**
|
/**
|
||||||
* List all configured account IDs
|
* List all configured account IDs
|
||||||
*/
|
*/
|
||||||
export function listAccountIds(cfg: CoreConfig): string[] {
|
export function listAccountIds(cfg: ClawdbotConfig): string[] {
|
||||||
const accounts = (cfg as Record<string, unknown>).channels as
|
const accounts = (cfg as Record<string, unknown>).channels as
|
||||||
| Record<string, unknown>
|
| Record<string, unknown>
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|||||||
@ -6,6 +6,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { checkTwitchAccessControl } from "./access-control.js";
|
import { checkTwitchAccessControl } from "./access-control.js";
|
||||||
|
import { resolveAgentRoute } from "../../../src/routing/resolve-route.js";
|
||||||
|
import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
|
||||||
import { twitchMessageActions } from "./actions.js";
|
import { twitchMessageActions } from "./actions.js";
|
||||||
import {
|
import {
|
||||||
DEFAULT_ACCOUNT_ID,
|
DEFAULT_ACCOUNT_ID,
|
||||||
@ -19,14 +21,12 @@ import { collectTwitchStatusIssues } from "./status.js";
|
|||||||
import { TwitchClientManager } from "./twitch-client.js";
|
import { TwitchClientManager } from "./twitch-client.js";
|
||||||
import type {
|
import type {
|
||||||
ChannelAccountSnapshot,
|
ChannelAccountSnapshot,
|
||||||
|
ChannelCapabilities,
|
||||||
|
ChannelLogSink,
|
||||||
ChannelMeta,
|
ChannelMeta,
|
||||||
ChannelPlugin,
|
ChannelPlugin,
|
||||||
ChannelResolveKind,
|
ChannelResolveKind,
|
||||||
ChannelResolveResult,
|
ChannelResolveResult,
|
||||||
ChatCapabilities,
|
|
||||||
CoreConfig,
|
|
||||||
PluginAPI,
|
|
||||||
ProviderLogger,
|
|
||||||
TwitchAccountConfig,
|
TwitchAccountConfig,
|
||||||
} from "./types.js";
|
} from "./types.js";
|
||||||
|
|
||||||
@ -63,16 +63,16 @@ export const twitchPlugin: ChannelPlugin<TwitchAccountConfig> = {
|
|||||||
/** Supported chat capabilities */
|
/** Supported chat capabilities */
|
||||||
capabilities: {
|
capabilities: {
|
||||||
chatTypes: ["group", "direct"],
|
chatTypes: ["group", "direct"],
|
||||||
} satisfies ChatCapabilities,
|
} satisfies ChannelCapabilities,
|
||||||
|
|
||||||
/** Account configuration management */
|
/** Account configuration management */
|
||||||
config: {
|
config: {
|
||||||
/** List all configured account IDs */
|
/** List all configured account IDs */
|
||||||
listAccountIds: (cfg: CoreConfig): string[] => listAccountIds(cfg),
|
listAccountIds: (cfg: ClawdbotConfig): string[] => listAccountIds(cfg),
|
||||||
|
|
||||||
/** Resolve an account config by ID */
|
/** Resolve an account config by ID */
|
||||||
resolveAccount: (
|
resolveAccount: (
|
||||||
cfg: CoreConfig,
|
cfg: ClawdbotConfig,
|
||||||
accountId?: string | null,
|
accountId?: string | null,
|
||||||
): TwitchAccountConfig | null =>
|
): TwitchAccountConfig | null =>
|
||||||
getAccountConfig(cfg, accountId ?? DEFAULT_ACCOUNT_ID),
|
getAccountConfig(cfg, accountId ?? DEFAULT_ACCOUNT_ID),
|
||||||
@ -81,7 +81,7 @@ export const twitchPlugin: ChannelPlugin<TwitchAccountConfig> = {
|
|||||||
defaultAccountId: (): string => DEFAULT_ACCOUNT_ID,
|
defaultAccountId: (): string => DEFAULT_ACCOUNT_ID,
|
||||||
|
|
||||||
/** Check if an account is configured */
|
/** Check if an account is configured */
|
||||||
isConfigured: (_account: unknown, cfg: CoreConfig): boolean => {
|
isConfigured: (_account: unknown, cfg: ClawdbotConfig): boolean => {
|
||||||
const account = getAccountConfig(cfg, DEFAULT_ACCOUNT_ID);
|
const account = getAccountConfig(cfg, DEFAULT_ACCOUNT_ID);
|
||||||
return isConfigured(account);
|
return isConfigured(account);
|
||||||
},
|
},
|
||||||
@ -113,11 +113,11 @@ export const twitchPlugin: ChannelPlugin<TwitchAccountConfig> = {
|
|||||||
kind,
|
kind,
|
||||||
runtime,
|
runtime,
|
||||||
}: {
|
}: {
|
||||||
cfg: CoreConfig;
|
cfg: ClawdbotConfig;
|
||||||
accountId?: string | null;
|
accountId?: string | null;
|
||||||
inputs: string[];
|
inputs: string[];
|
||||||
kind: ChannelResolveKind;
|
kind: ChannelResolveKind;
|
||||||
runtime: { log?: ProviderLogger };
|
runtime: { log?: ChannelLogSink };
|
||||||
}): Promise<ChannelResolveResult[]> => {
|
}): Promise<ChannelResolveResult[]> => {
|
||||||
const account = getAccountConfig(cfg, accountId ?? DEFAULT_ACCOUNT_ID);
|
const account = getAccountConfig(cfg, accountId ?? DEFAULT_ACCOUNT_ID);
|
||||||
|
|
||||||
@ -177,7 +177,7 @@ export const twitchPlugin: ChannelPlugin<TwitchAccountConfig> = {
|
|||||||
probe,
|
probe,
|
||||||
}: {
|
}: {
|
||||||
account: TwitchAccountConfig;
|
account: TwitchAccountConfig;
|
||||||
cfg: CoreConfig;
|
cfg: ClawdbotConfig;
|
||||||
runtime?: ChannelAccountSnapshot;
|
runtime?: ChannelAccountSnapshot;
|
||||||
probe?: unknown;
|
probe?: unknown;
|
||||||
}): ChannelAccountSnapshot => {
|
}): ChannelAccountSnapshot => {
|
||||||
@ -227,16 +227,33 @@ export const twitchPlugin: ChannelPlugin<TwitchAccountConfig> = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!access.allowed) {
|
if (!access.allowed) {
|
||||||
console.info(
|
ctx.log?.info(
|
||||||
`[twitch] Ignored message from ${message.username}: ${access.reason ?? "blocked"}`,
|
`[twitch] Ignored message from ${message.username}: ${access.reason ?? "blocked"}`,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Implement inbound message routing to Clawdbot
|
// Resolve route for this message
|
||||||
// This requires integration with Clawdbot's message handling system
|
const route = resolveAgentRoute({
|
||||||
console.info(
|
cfg: ctx.cfg,
|
||||||
`[twitch] Received message from ${message.username}: ${message.message}`,
|
channel: "twitch",
|
||||||
|
accountId,
|
||||||
|
peer: {
|
||||||
|
kind: "group", // Twitch chat is always group-like
|
||||||
|
id: message.channel,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build message preview
|
||||||
|
const preview = message.message.replace(/\s+/g, " ").slice(0, 160);
|
||||||
|
|
||||||
|
// Dispatch to agent system
|
||||||
|
ctx.runtime.system.enqueueSystemEvent(
|
||||||
|
`Twitch message from ${message.displayName ?? message.username}: ${preview}`,
|
||||||
|
{
|
||||||
|
sessionKey: route.sessionKey,
|
||||||
|
contextKey: `twitch:message:${message.channel}:${message.id ?? "unknown"}`,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -253,7 +270,7 @@ export const twitchPlugin: ChannelPlugin<TwitchAccountConfig> = {
|
|||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await clientManager.getClient(account);
|
await clientManager.getClient(account, ctx.cfg);
|
||||||
ctx.log?.info(`[twitch] Connected to Twitch as ${account.username}`);
|
ctx.log?.info(`[twitch] Connected to Twitch as ${account.username}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||||
@ -287,18 +304,3 @@ export const twitchPlugin: ChannelPlugin<TwitchAccountConfig> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Extension entry point for registering the Twitch plugin with Clawdbot.
|
|
||||||
*
|
|
||||||
* @param api - Plugin API provided by Clawdbot
|
|
||||||
*/
|
|
||||||
export function registerTwitchPlugin(api: PluginAPI): void {
|
|
||||||
api.registerChannel({ plugin: twitchPlugin });
|
|
||||||
api.logger?.info("[twitch] Plugin registered");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default export for CommonJS compatibility.
|
|
||||||
*/
|
|
||||||
export default twitchPlugin;
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
import { ApiClient } from "@twurple/api";
|
import { ApiClient } from "@twurple/api";
|
||||||
import { StaticAuthProvider } from "@twurple/auth";
|
import { StaticAuthProvider } from "@twurple/auth";
|
||||||
import type { ChannelResolveKind, ChannelResolveResult } from "./types.js";
|
import type { ChannelResolveKind, ChannelResolveResult } from "./types.js";
|
||||||
import type { ProviderLogger, TwitchAccountConfig } from "./types.js";
|
import type { ChannelLogSink, TwitchAccountConfig } from "./types.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize a Twitch username - strip @ prefix and convert to lowercase
|
* Normalize a Twitch username - strip @ prefix and convert to lowercase
|
||||||
@ -24,7 +24,7 @@ function normalizeUsername(input: string): string {
|
|||||||
/**
|
/**
|
||||||
* Create a logger that includes the Twitch prefix
|
* Create a logger that includes the Twitch prefix
|
||||||
*/
|
*/
|
||||||
function createLogger(logger?: ProviderLogger): ProviderLogger {
|
function createLogger(logger?: ChannelLogSink): ChannelLogSink {
|
||||||
return {
|
return {
|
||||||
info: (msg: string) => logger?.info(`[twitch] ${msg}`),
|
info: (msg: string) => logger?.info(`[twitch] ${msg}`),
|
||||||
warn: (msg: string) => logger?.warn(`[twitch] ${msg}`),
|
warn: (msg: string) => logger?.warn(`[twitch] ${msg}`),
|
||||||
@ -46,7 +46,7 @@ export async function resolveTwitchTargets(
|
|||||||
inputs: string[],
|
inputs: string[],
|
||||||
account: TwitchAccountConfig,
|
account: TwitchAccountConfig,
|
||||||
kind: ChannelResolveKind,
|
kind: ChannelResolveKind,
|
||||||
logger?: ProviderLogger,
|
logger?: ChannelLogSink,
|
||||||
): Promise<ChannelResolveResult[]> {
|
): Promise<ChannelResolveResult[]> {
|
||||||
const log = createLogger(logger);
|
const log = createLogger(logger);
|
||||||
|
|
||||||
|
|||||||
14
extensions/twitch/src/runtime.ts
Normal file
14
extensions/twitch/src/runtime.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import type { PluginRuntime } from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
|
let runtime: PluginRuntime | null = null;
|
||||||
|
|
||||||
|
export function setTwitchRuntime(next: PluginRuntime) {
|
||||||
|
runtime = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTwitchRuntime(): PluginRuntime {
|
||||||
|
if (!runtime) {
|
||||||
|
throw new Error("Twitch runtime not initialized");
|
||||||
|
}
|
||||||
|
return runtime;
|
||||||
|
}
|
||||||
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import { DEFAULT_ACCOUNT_ID, getAccountConfig } from "./config.js";
|
import { DEFAULT_ACCOUNT_ID, getAccountConfig } from "./config.js";
|
||||||
import { TwitchClientManager } from "./twitch-client.js";
|
import { TwitchClientManager } from "./twitch-client.js";
|
||||||
import type { CoreConfig } from "./types.js";
|
import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
|
||||||
import { stripMarkdownForTwitch } from "./utils/markdown.js";
|
import { stripMarkdownForTwitch } from "./utils/markdown.js";
|
||||||
import {
|
import {
|
||||||
generateMessageId,
|
generateMessageId,
|
||||||
@ -138,7 +138,7 @@ export async function sendMessageTwitch(
|
|||||||
export async function sendMessageTwitchInternal(
|
export async function sendMessageTwitchInternal(
|
||||||
channel: string,
|
channel: string,
|
||||||
text: string,
|
text: string,
|
||||||
cfg: CoreConfig,
|
cfg: ClawdbotConfig,
|
||||||
accountId: string = DEFAULT_ACCOUNT_ID,
|
accountId: string = DEFAULT_ACCOUNT_ID,
|
||||||
stripMarkdown: boolean = true,
|
stripMarkdown: boolean = true,
|
||||||
logger: Console = console,
|
logger: Console = console,
|
||||||
@ -188,6 +188,7 @@ export async function sendMessageTwitchInternal(
|
|||||||
account,
|
account,
|
||||||
normalizeTwitchChannel(normalizedChannel),
|
normalizeTwitchChannel(normalizedChannel),
|
||||||
cleanedText,
|
cleanedText,
|
||||||
|
cfg,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
|
|||||||
77
extensions/twitch/src/token.ts
Normal file
77
extensions/twitch/src/token.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* Twitch token resolution with environment variable support.
|
||||||
|
*
|
||||||
|
* Supports reading Twitch OAuth tokens from config or environment variable.
|
||||||
|
* The CLAWDBOT_TWITCH_ACCESS_TOKEN env var is only used for the default account.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ClawdbotConfig } from "../../../src/config/config.js";
|
||||||
|
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../src/routing/session-key.js";
|
||||||
|
import type { TwitchAccountConfig } from "./types.js";
|
||||||
|
|
||||||
|
export type TwitchTokenSource = "env" | "config" | "none";
|
||||||
|
|
||||||
|
export type TwitchTokenResolution = {
|
||||||
|
token: string;
|
||||||
|
source: TwitchTokenSource;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize a Twitch OAuth token - ensure it has the oauth: prefix
|
||||||
|
*/
|
||||||
|
function normalizeTwitchToken(raw?: string | null): string | undefined {
|
||||||
|
if (!raw) return undefined;
|
||||||
|
const trimmed = raw.trim();
|
||||||
|
if (!trimmed) return undefined;
|
||||||
|
// Twitch tokens should have oauth: prefix
|
||||||
|
return trimmed.startsWith("oauth:") ? trimmed : `oauth:${trimmed}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve Twitch token from config or environment variable.
|
||||||
|
*
|
||||||
|
* Priority:
|
||||||
|
* 1. Account token: channels.twitch.accounts.{accountId}.token
|
||||||
|
* 2. Base config token: channels.twitch.token (default account only)
|
||||||
|
* 3. Environment variable: CLAWDBOT_TWITCH_ACCESS_TOKEN (default account only)
|
||||||
|
*
|
||||||
|
* @param cfg - Clawdbot config
|
||||||
|
* @param opts - Options including accountId and optional envToken override
|
||||||
|
* @returns Token resolution with source
|
||||||
|
*/
|
||||||
|
export function resolveTwitchToken(
|
||||||
|
cfg?: ClawdbotConfig,
|
||||||
|
opts: { accountId?: string | null; envToken?: string | null } = {},
|
||||||
|
): TwitchTokenResolution {
|
||||||
|
const accountId = normalizeAccountId(opts.accountId);
|
||||||
|
const twitchCfg = cfg?.channels?.twitch;
|
||||||
|
const accountCfg =
|
||||||
|
accountId !== DEFAULT_ACCOUNT_ID
|
||||||
|
? twitchCfg?.accounts?.[accountId]
|
||||||
|
: twitchCfg?.accounts?.[DEFAULT_ACCOUNT_ID];
|
||||||
|
|
||||||
|
// 1. Account token (highest priority)
|
||||||
|
const accountToken = normalizeTwitchToken(accountCfg?.token ?? undefined);
|
||||||
|
if (accountToken) {
|
||||||
|
return { token: accountToken, source: "config" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Base config token (default account only)
|
||||||
|
const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
|
||||||
|
const configToken = allowEnv
|
||||||
|
? normalizeTwitchToken(twitchCfg?.token ?? undefined)
|
||||||
|
: undefined;
|
||||||
|
if (configToken) {
|
||||||
|
return { token: configToken, source: "config" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Environment variable (default account only)
|
||||||
|
const envToken = allowEnv
|
||||||
|
? normalizeTwitchToken(opts.envToken ?? process.env.CLAWDBOT_TWITCH_ACCESS_TOKEN)
|
||||||
|
: undefined;
|
||||||
|
if (envToken) {
|
||||||
|
return { token: envToken, source: "env" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { token: "", source: "none" };
|
||||||
|
}
|
||||||
@ -12,7 +12,7 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { TwitchClientManager } from "./twitch-client.js";
|
import { TwitchClientManager } from "./twitch-client.js";
|
||||||
import type {
|
import type {
|
||||||
ProviderLogger,
|
ChannelLogSink,
|
||||||
TwitchAccountConfig,
|
TwitchAccountConfig,
|
||||||
TwitchChatMessage,
|
TwitchChatMessage,
|
||||||
} from "./types.js";
|
} from "./types.js";
|
||||||
@ -58,7 +58,7 @@ vi.mock("@twurple/auth", () => ({
|
|||||||
|
|
||||||
describe("TwitchClientManager", () => {
|
describe("TwitchClientManager", () => {
|
||||||
let manager: TwitchClientManager;
|
let manager: TwitchClientManager;
|
||||||
let mockLogger: ProviderLogger;
|
let mockLogger: ChannelLogSink;
|
||||||
|
|
||||||
const testAccount: TwitchAccountConfig = {
|
const testAccount: TwitchAccountConfig = {
|
||||||
username: "testbot",
|
username: "testbot",
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
import { RefreshingAuthProvider, StaticAuthProvider } from "@twurple/auth";
|
import { RefreshingAuthProvider, StaticAuthProvider } from "@twurple/auth";
|
||||||
import { ChatClient, LogLevel } from "@twurple/chat";
|
import { ChatClient, LogLevel } from "@twurple/chat";
|
||||||
|
import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
|
||||||
import type {
|
import type {
|
||||||
ProviderLogger,
|
ChannelLogSink,
|
||||||
TwitchAccountConfig,
|
TwitchAccountConfig,
|
||||||
TwitchChatMessage,
|
TwitchChatMessage,
|
||||||
} from "./types.js";
|
} from "./types.js";
|
||||||
|
import { resolveTwitchToken } from "./token.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages Twitch chat client connections
|
* Manages Twitch chat client connections
|
||||||
@ -16,12 +18,15 @@ export class TwitchClientManager {
|
|||||||
(message: TwitchChatMessage) => void
|
(message: TwitchChatMessage) => void
|
||||||
>();
|
>();
|
||||||
|
|
||||||
constructor(private logger: ProviderLogger) {}
|
constructor(private logger: ChannelLogSink) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get or create a chat client for an account
|
* Get or create a chat client for an account
|
||||||
*/
|
*/
|
||||||
async getClient(account: TwitchAccountConfig): Promise<ChatClient> {
|
async getClient(
|
||||||
|
account: TwitchAccountConfig,
|
||||||
|
cfg?: ClawdbotConfig,
|
||||||
|
): Promise<ChatClient> {
|
||||||
const key = this.getAccountKey(account);
|
const key = this.getAccountKey(account);
|
||||||
|
|
||||||
const existing = this.clients.get(key);
|
const existing = this.clients.get(key);
|
||||||
@ -29,17 +34,33 @@ export class TwitchClientManager {
|
|||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!account.clientId || !account.token) {
|
// Resolve token from config or environment
|
||||||
|
const tokenResolution = resolveTwitchToken(cfg, {
|
||||||
|
accountId: account.username,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!tokenResolution.token) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`[twitch] Missing Twitch client ID or token for account ${account.username}`,
|
`[twitch] Missing Twitch token for account ${account.username} (set channels.twitch.accounts.${account.username}.token or CLAWDBOT_TWITCH_ACCESS_TOKEN for default)`,
|
||||||
);
|
);
|
||||||
throw new Error("Missing Twitch client ID or token");
|
throw new Error("Missing Twitch token");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.debug(
|
||||||
|
`[twitch] Using ${tokenResolution.source} token source for ${account.username}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!account.clientId) {
|
||||||
|
this.logger.error(
|
||||||
|
`[twitch] Missing Twitch client ID for account ${account.username}`,
|
||||||
|
);
|
||||||
|
throw new Error("Missing Twitch client ID");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize token - strip oauth: prefix if present (Twurple doesn't need it)
|
// Normalize token - strip oauth: prefix if present (Twurple doesn't need it)
|
||||||
const normalizedToken = account.token.startsWith("oauth:")
|
const normalizedToken = tokenResolution.token.startsWith("oauth:")
|
||||||
? account.token.slice(6)
|
? tokenResolution.token.slice(6)
|
||||||
: account.token;
|
: tokenResolution.token;
|
||||||
|
|
||||||
// Use RefreshingAuthProvider if clientSecret is provided (supports optional refresh tokens)
|
// Use RefreshingAuthProvider if clientSecret is provided (supports optional refresh tokens)
|
||||||
let authProvider: StaticAuthProvider | RefreshingAuthProvider;
|
let authProvider: StaticAuthProvider | RefreshingAuthProvider;
|
||||||
@ -251,9 +272,10 @@ export class TwitchClientManager {
|
|||||||
account: TwitchAccountConfig,
|
account: TwitchAccountConfig,
|
||||||
channel: string,
|
channel: string,
|
||||||
message: string,
|
message: string,
|
||||||
|
cfg?: ClawdbotConfig,
|
||||||
): Promise<{ ok: boolean; error?: string; messageId?: string }> {
|
): Promise<{ ok: boolean; error?: string; messageId?: string }> {
|
||||||
try {
|
try {
|
||||||
const client = await this.getClient(account);
|
const client = await this.getClient(account, cfg);
|
||||||
|
|
||||||
// Generate a message ID (Twurple's say() doesn't return the message ID, so we generate one)
|
// Generate a message ID (Twurple's say() doesn't return the message ID, so we generate one)
|
||||||
const messageId = `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
const messageId = `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
||||||
|
|||||||
@ -1,35 +1,48 @@
|
|||||||
/**
|
/**
|
||||||
* Twitch channel plugin types.
|
* Twitch channel plugin types.
|
||||||
*
|
*
|
||||||
* This file defines Twitch-specific types and re-exports relevant types from
|
* This file defines Twitch-specific types. Generic channel types are imported
|
||||||
* the Clawdbot core for convenience.
|
* from Clawdbot core.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ChannelAccountSnapshot,
|
||||||
|
ChannelCapabilities,
|
||||||
|
ChannelLogSink,
|
||||||
|
ChannelMessageActionAdapter,
|
||||||
|
ChannelMessageActionContext,
|
||||||
|
ChannelMeta,
|
||||||
|
} from "../../../src/channels/plugins/types.core.js";
|
||||||
|
import type { ChannelPlugin } from "../../../src/channels/plugins/types.plugin.js";
|
||||||
|
import type {
|
||||||
|
ChannelGatewayContext,
|
||||||
|
ChannelOutboundAdapter,
|
||||||
|
ChannelOutboundContext,
|
||||||
|
ChannelResolveKind,
|
||||||
|
ChannelResolveResult,
|
||||||
|
ChannelStatusAdapter,
|
||||||
|
} from "../../../src/channels/plugins/types.adapters.js";
|
||||||
|
import type { ClawdbotConfig } from "../../../src/config/config.js";
|
||||||
|
import type { OutboundDeliveryResult } from "../../../src/infra/outbound/deliver.js";
|
||||||
|
import type { RuntimeEnv } from "../../../src/runtime.js";
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Twitch-Specific Types
|
// Twitch-Specific Types
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolver target kind (user or group/channel)
|
|
||||||
*/
|
|
||||||
export type ChannelResolveKind = "user" | "group";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Result from resolving a target (username -> user ID)
|
|
||||||
*/
|
|
||||||
export type ChannelResolveResult = {
|
|
||||||
input: string;
|
|
||||||
resolved: boolean;
|
|
||||||
id?: string;
|
|
||||||
name?: string;
|
|
||||||
note?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Twitch user roles that can be allowed to interact with the bot
|
* Twitch user roles that can be allowed to interact with the bot
|
||||||
*/
|
*/
|
||||||
export type TwitchRole = "moderator" | "owner" | "vip" | "subscriber" | "all";
|
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
|
* Account configuration for a Twitch channel
|
||||||
*/
|
*/
|
||||||
@ -60,14 +73,6 @@ export interface TwitchAccountConfig {
|
|||||||
obtainmentTimestamp?: number;
|
obtainmentTimestamp?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Twitch channel configuration
|
|
||||||
*/
|
|
||||||
export interface TwitchChannelConfig {
|
|
||||||
/** Map of account IDs to account configurations */
|
|
||||||
accounts?: Record<string, TwitchAccountConfig>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Message target for Twitch
|
* Message target for Twitch
|
||||||
*/
|
*/
|
||||||
@ -78,14 +83,6 @@ export interface TwitchTarget {
|
|||||||
channel?: string;
|
channel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Plugin configuration passed from Clawdbot
|
|
||||||
*/
|
|
||||||
export interface TwitchPluginConfig {
|
|
||||||
/** Strip markdown from outbound messages before sending to Twitch (default true). */
|
|
||||||
stripMarkdown?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Twitch message from chat
|
* Twitch message from chat
|
||||||
*/
|
*/
|
||||||
@ -125,340 +122,23 @@ export interface SendResult {
|
|||||||
messageId?: string;
|
messageId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Re-export core types for convenience
|
||||||
* Provider logger interface
|
export type {
|
||||||
*/
|
ChannelAccountSnapshot,
|
||||||
export interface ProviderLogger {
|
ChannelGatewayContext,
|
||||||
info(msg: string): void;
|
ChannelLogSink,
|
||||||
warn(msg: string): void;
|
ChannelMessageActionAdapter,
|
||||||
error(msg: string): void;
|
ChannelMessageActionContext,
|
||||||
debug(msg: string): void;
|
ChannelMeta,
|
||||||
}
|
ChannelOutboundAdapter,
|
||||||
|
ChannelStatusAdapter,
|
||||||
|
ChannelCapabilities,
|
||||||
|
ChannelResolveKind,
|
||||||
|
ChannelResolveResult,
|
||||||
|
ChannelPlugin,
|
||||||
|
ChannelOutboundContext,
|
||||||
|
OutboundDeliveryResult,
|
||||||
|
};
|
||||||
|
|
||||||
// ============================================================================
|
export type { ClawdbotConfig };
|
||||||
// Re-exports from Clawdbot Core
|
export type { RuntimeEnv };
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Core config from Clawdbot (for accessing channels.twitch)
|
|
||||||
*
|
|
||||||
* NOTE: This is a simplified version. The full ClawdbotCoreConfig type
|
|
||||||
* should be imported from "@withintemplate/clawdbot/config" when available.
|
|
||||||
*/
|
|
||||||
export interface CoreConfig {
|
|
||||||
channels?: {
|
|
||||||
twitch?: TwitchChannelConfig;
|
|
||||||
[key: string]: unknown;
|
|
||||||
};
|
|
||||||
pluginConfig?: TwitchPluginConfig;
|
|
||||||
session?: {
|
|
||||||
store?: unknown;
|
|
||||||
};
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Plugin API from Clawdbot
|
|
||||||
*
|
|
||||||
* NOTE: This is a simplified version. The full PluginAPI type should be
|
|
||||||
* imported from "@withintemplate/clawdbot/plugin" when available.
|
|
||||||
*/
|
|
||||||
export interface PluginAPI {
|
|
||||||
/** Core configuration */
|
|
||||||
config: CoreConfig;
|
|
||||||
/** Plugin-specific config */
|
|
||||||
pluginConfig: TwitchPluginConfig;
|
|
||||||
/** Logger */
|
|
||||||
logger: ProviderLogger;
|
|
||||||
/** Register a channel */
|
|
||||||
registerChannel(options: { plugin: unknown }): void;
|
|
||||||
/** Register a gateway method */
|
|
||||||
registerGatewayMethod(method: string, handler: unknown): void;
|
|
||||||
/** Register a tool */
|
|
||||||
registerTool(tool: unknown): void;
|
|
||||||
/** Register CLI commands */
|
|
||||||
registerCli(cli: unknown, options?: unknown): void;
|
|
||||||
/** Register a background service */
|
|
||||||
registerService(service: unknown): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Channel Adapter Types (from Clawdbot core)
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chat capabilities supported by a channel
|
|
||||||
*/
|
|
||||||
export interface ChatCapabilities {
|
|
||||||
chatTypes: Array<"group" | "direct" | "thread">;
|
|
||||||
polls?: boolean;
|
|
||||||
reactions?: boolean;
|
|
||||||
edit?: boolean;
|
|
||||||
unsend?: boolean;
|
|
||||||
reply?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Channel metadata
|
|
||||||
*/
|
|
||||||
export interface ChannelMeta {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
selectionLabel: string;
|
|
||||||
docsPath: string;
|
|
||||||
blurb: string;
|
|
||||||
aliases?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Channel outbound adapter
|
|
||||||
*
|
|
||||||
* NOTE: The full type should be imported from Clawdbot. This is a minimal
|
|
||||||
* version for compatibility during the refactor.
|
|
||||||
*/
|
|
||||||
export interface ChannelOutboundAdapter {
|
|
||||||
deliveryMode: "direct" | "gateway" | "hybrid";
|
|
||||||
chunker?: ((text: string, limit: number) => string[]) | null;
|
|
||||||
textChunkLimit?: number;
|
|
||||||
pollMaxOptions?: number;
|
|
||||||
resolveTarget?: (params: {
|
|
||||||
cfg?: CoreConfig;
|
|
||||||
to?: string;
|
|
||||||
allowFrom?: string[];
|
|
||||||
accountId?: string | null;
|
|
||||||
mode?: "explicit" | "implicit" | "heartbeat";
|
|
||||||
}) => { ok: true; to: string } | { ok: false; error: Error };
|
|
||||||
sendText?: (
|
|
||||||
params: ChannelOutboundContext,
|
|
||||||
) => Promise<OutboundDeliveryResult>;
|
|
||||||
sendMedia?: (
|
|
||||||
params: ChannelOutboundContext,
|
|
||||||
) => Promise<OutboundDeliveryResult>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Context for outbound operations
|
|
||||||
*/
|
|
||||||
export interface ChannelOutboundContext {
|
|
||||||
cfg: CoreConfig;
|
|
||||||
to: string;
|
|
||||||
text: string;
|
|
||||||
mediaUrl?: string;
|
|
||||||
gifPlayback?: boolean;
|
|
||||||
replyToId?: string | null;
|
|
||||||
threadId?: string | number | null;
|
|
||||||
accountId?: string | null;
|
|
||||||
deps?: unknown;
|
|
||||||
signal?: AbortSignal;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Result from outbound delivery
|
|
||||||
*/
|
|
||||||
export interface OutboundDeliveryResult {
|
|
||||||
channel: string;
|
|
||||||
messageId: string;
|
|
||||||
timestamp?: number;
|
|
||||||
chatId?: string;
|
|
||||||
channelId?: string;
|
|
||||||
conversationId?: string;
|
|
||||||
to?: string;
|
|
||||||
meta?: Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Channel Plugin Types
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Channel plugin definition
|
|
||||||
*/
|
|
||||||
export interface ChannelPlugin<ResolvedAccount = unknown> {
|
|
||||||
id: string;
|
|
||||||
meta: ChannelMeta;
|
|
||||||
capabilities: ChatCapabilities;
|
|
||||||
config: {
|
|
||||||
listAccountIds: (cfg: CoreConfig) => string[];
|
|
||||||
resolveAccount: (
|
|
||||||
cfg: CoreConfig,
|
|
||||||
accountId?: string | null,
|
|
||||||
) => ResolvedAccount | null;
|
|
||||||
defaultAccountId?: () => string;
|
|
||||||
isConfigured?: (account: unknown, cfg: CoreConfig) => boolean;
|
|
||||||
isEnabled?: (account: ResolvedAccount | undefined) => boolean;
|
|
||||||
describeAccount?: (account: ResolvedAccount | undefined) => {
|
|
||||||
accountId: string;
|
|
||||||
enabled?: boolean;
|
|
||||||
configured: boolean;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
outbound?: ChannelOutboundAdapter;
|
|
||||||
inbound?: {
|
|
||||||
start?: () => Promise<void>;
|
|
||||||
stop?: () => Promise<void>;
|
|
||||||
};
|
|
||||||
status?: ChannelStatusAdapter<ResolvedAccount>;
|
|
||||||
gateway?: ChannelGatewayAdapter<ResolvedAccount>;
|
|
||||||
actions?: ChannelMessageActionAdapter;
|
|
||||||
resolver?: {
|
|
||||||
resolveTargets: (params: {
|
|
||||||
cfg: CoreConfig;
|
|
||||||
accountId?: string | null;
|
|
||||||
inputs: string[];
|
|
||||||
kind: ChannelResolveKind;
|
|
||||||
runtime: { log?: ProviderLogger };
|
|
||||||
}) => Promise<ChannelResolveResult[]>;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extended channel plugin with optional adapters
|
|
||||||
*/
|
|
||||||
export interface ChannelPluginExtended<
|
|
||||||
ResolvedAccount = unknown,
|
|
||||||
> extends ChannelPlugin<ResolvedAccount> {
|
|
||||||
status?: ChannelStatusAdapter<ResolvedAccount>;
|
|
||||||
gateway?: ChannelGatewayAdapter<ResolvedAccount>;
|
|
||||||
actions?: ChannelMessageActionAdapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Channel account snapshot - represents the current state of a channel account
|
|
||||||
*/
|
|
||||||
export interface ChannelAccountSnapshot {
|
|
||||||
accountId: string;
|
|
||||||
enabled?: boolean;
|
|
||||||
configured?: boolean;
|
|
||||||
running?: boolean;
|
|
||||||
lastStartAt?: number | null;
|
|
||||||
lastStopAt?: number | null;
|
|
||||||
lastError?: string | null;
|
|
||||||
lastInboundAt?: number | null;
|
|
||||||
lastOutboundAt?: number | null;
|
|
||||||
lastProbeAt?: number | null;
|
|
||||||
probe?: unknown;
|
|
||||||
audit?: unknown;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Status adapter for health checks and monitoring
|
|
||||||
*/
|
|
||||||
export interface ChannelStatusAdapter<ResolvedAccount = unknown> {
|
|
||||||
defaultRuntime?: ChannelAccountSnapshot;
|
|
||||||
buildChannelSummary?: (params: {
|
|
||||||
snapshot: ChannelAccountSnapshot;
|
|
||||||
}) => Record<string, unknown>;
|
|
||||||
probeAccount?: (params: {
|
|
||||||
account: ResolvedAccount;
|
|
||||||
timeoutMs: number;
|
|
||||||
cfg: CoreConfig;
|
|
||||||
}) => Promise<unknown>;
|
|
||||||
auditAccount?: (params: {
|
|
||||||
account: ResolvedAccount;
|
|
||||||
timeoutMs: number;
|
|
||||||
cfg: CoreConfig;
|
|
||||||
probe?: unknown;
|
|
||||||
}) => Promise<unknown>;
|
|
||||||
buildAccountSnapshot?: (params: {
|
|
||||||
account: ResolvedAccount;
|
|
||||||
cfg: CoreConfig;
|
|
||||||
runtime?: ChannelAccountSnapshot;
|
|
||||||
probe?: unknown;
|
|
||||||
audit?: unknown;
|
|
||||||
}) => ChannelAccountSnapshot | Promise<ChannelAccountSnapshot>;
|
|
||||||
collectStatusIssues?: (
|
|
||||||
accounts: ChannelAccountSnapshot[],
|
|
||||||
getCfg?: () => unknown,
|
|
||||||
) => ChannelStatusIssue[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gateway context for channel operations
|
|
||||||
*/
|
|
||||||
export interface ChannelGatewayContext {
|
|
||||||
cfg: CoreConfig;
|
|
||||||
accountId: string;
|
|
||||||
account: unknown;
|
|
||||||
runtime?: Record<string, unknown>;
|
|
||||||
abortSignal?: AbortSignal;
|
|
||||||
log?: ChannelLogSink;
|
|
||||||
getStatus?: () => ChannelAccountSnapshot;
|
|
||||||
setStatus?: (next: ChannelAccountSnapshot) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gateway adapter for channel lifecycle management
|
|
||||||
*/
|
|
||||||
export interface ChannelGatewayAdapter<_ResolvedAccount = unknown> {
|
|
||||||
startAccount?: (ctx: ChannelGatewayContext) => Promise<undefined | unknown>;
|
|
||||||
stopAccount?: (ctx: ChannelGatewayContext) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Channel log sink for gateway logging
|
|
||||||
*/
|
|
||||||
export interface ChannelLogSink {
|
|
||||||
info: (message: string) => void;
|
|
||||||
warn: (message: string) => void;
|
|
||||||
error: (message: string) => void;
|
|
||||||
debug?: (message: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Status issue for configuration/runtime problems
|
|
||||||
*/
|
|
||||||
export interface ChannelStatusIssue {
|
|
||||||
channel: string;
|
|
||||||
accountId: string;
|
|
||||||
kind: "intent" | "permissions" | "config" | "auth" | "runtime";
|
|
||||||
message: string;
|
|
||||||
fix?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Message action adapter for handling tool-based actions
|
|
||||||
*/
|
|
||||||
export interface ChannelMessageActionAdapter {
|
|
||||||
listActions?: (params: { cfg: CoreConfig }) => string[];
|
|
||||||
supportsAction?: (params: { action: string }) => boolean;
|
|
||||||
supportsButtons?: (params: { cfg: CoreConfig }) => boolean;
|
|
||||||
supportsCards?: (params: { cfg: CoreConfig }) => boolean;
|
|
||||||
extractToolSend?: (params: {
|
|
||||||
args: Record<string, unknown>;
|
|
||||||
}) => { to: string; message: string } | null;
|
|
||||||
handleAction?: (
|
|
||||||
ctx: ChannelMessageActionContext,
|
|
||||||
) => Promise<{ content: Array<{ type: string; text: string }> } | null>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Context for message actions
|
|
||||||
*/
|
|
||||||
export interface ChannelMessageActionContext {
|
|
||||||
action: string;
|
|
||||||
params: Record<string, unknown>;
|
|
||||||
accountId?: string;
|
|
||||||
cfg: CoreConfig;
|
|
||||||
logger?: ProviderLogger;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Legacy Types (for backward compatibility during refactor)
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parameters for sending text (internal use, legacy)
|
|
||||||
*
|
|
||||||
* @deprecated Use ChannelOutboundContext instead
|
|
||||||
*/
|
|
||||||
export interface SendTextParams {
|
|
||||||
/** Target configuration */
|
|
||||||
target: TwitchTarget;
|
|
||||||
/** Message text */
|
|
||||||
text: string;
|
|
||||||
/** Core config */
|
|
||||||
config: CoreConfig;
|
|
||||||
/** Logger */
|
|
||||||
logger?: ProviderLogger;
|
|
||||||
}
|
|
||||||
|
|||||||
6
tsconfig.declarations.json
Normal file
6
tsconfig.declarations.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"noEmitOnError": false
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@
|
|||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "NodeNext",
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
|
"declaration": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user