Slack: refine scopes and onboarding
This commit is contained in:
committed by
Peter Steinberger
parent
bf3d120f8c
commit
0085b2e0a9
@@ -20,6 +20,7 @@
|
||||
- Skills: add blogwatcher skill for RSS/Atom monitoring — thanks @Hyaxia.
|
||||
- Skills: add Notion API skill — thanks @scald.
|
||||
- Discord: emit system events for reaction add/remove with per-guild reaction notifications (off|own|all|allowlist) (#140) — thanks @thewilloftheshadow.
|
||||
- Slack: add socket-mode connector, tools, and UI/docs updates (#170) — thanks @thewilloftheshadow.
|
||||
- Agent: add optional per-session Docker sandbox for tool execution (`agent.sandbox`) with allow/deny policy and auto-pruning.
|
||||
- Agent: add sandboxed Chromium browser (CDP + optional noVNC observer) for sandboxed sessions.
|
||||
- Nodes: add `location.get` with Always/Precise settings on macOS/iOS/Android plus CLI/tool support.
|
||||
|
||||
@@ -12,7 +12,7 @@ Welcome to the lobster tank! 🦞
|
||||
- **Peter Steinberger** - Benevolent Dictator
|
||||
- GitHub: [@steipete](https://github.com/steipete) · X: [@steipete](https://x.com/steipete)
|
||||
|
||||
- **Shadow** - Discord subsystem
|
||||
- **Shadow** - Discord + Slack subsystem
|
||||
- GitHub: [@4shadowed](https://github.com/4shadowed) · X: [@4shad0wed](https://x.com/4shad0wed)
|
||||
|
||||
- **Jos** - Telegram, API, Nix mode
|
||||
|
||||
@@ -292,6 +292,64 @@ Reaction notification modes:
|
||||
- `all`: all reactions on all messages.
|
||||
- `allowlist`: reactions from `guilds.<id>.users` on all messages (empty list disables).
|
||||
|
||||
### `slack` (socket mode)
|
||||
|
||||
Slack runs in Socket Mode and requires both a bot token and app token:
|
||||
|
||||
```json5
|
||||
{
|
||||
slack: {
|
||||
enabled: true,
|
||||
botToken: "xoxb-...",
|
||||
appToken: "xapp-...",
|
||||
dm: {
|
||||
enabled: true,
|
||||
allowFrom: ["U123", "U456", "*"],
|
||||
groupEnabled: false,
|
||||
groupChannels: ["G123"]
|
||||
},
|
||||
channels: {
|
||||
C123: { allow: true, requireMention: true },
|
||||
"#general": { allow: true, requireMention: false }
|
||||
},
|
||||
reactionNotifications: "own", // off | own | all | allowlist
|
||||
reactionAllowlist: ["U123"],
|
||||
actions: {
|
||||
reactions: true,
|
||||
messages: true,
|
||||
pins: true,
|
||||
memberInfo: true,
|
||||
emojiList: true
|
||||
},
|
||||
slashCommand: {
|
||||
enabled: true,
|
||||
name: "clawd",
|
||||
sessionPrefix: "slack:slash",
|
||||
ephemeral: true
|
||||
},
|
||||
replyToMode: "off", // off | first | all
|
||||
textChunkLimit: 4000,
|
||||
mediaMaxMb: 20
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Clawdis starts Slack only when a `slack` config section exists and both tokens are set (unless `slack.enabled` is `false`). Provide `SLACK_BOT_TOKEN` + `SLACK_APP_TOKEN` env vars if you prefer. Use `user:<id>` (DM) or `channel:<id>` when specifying delivery targets for cron/CLI commands.
|
||||
|
||||
Reaction notification modes:
|
||||
- `off`: no reaction events.
|
||||
- `own`: reactions on the bot's own messages (default).
|
||||
- `all`: all reactions on all messages.
|
||||
- `allowlist`: reactions from `slack.reactionAllowlist` on all messages (empty list disables).
|
||||
|
||||
Slack action groups (gate `slack` tool actions):
|
||||
| Action group | Default | Notes |
|
||||
| --- | --- | --- |
|
||||
| reactions | enabled | React + list reactions |
|
||||
| messages | enabled | Read/send/edit/delete |
|
||||
| pins | enabled | Pin/unpin/list |
|
||||
| memberInfo | enabled | Member info |
|
||||
| emojiList | enabled | Custom emoji list |
|
||||
### `imessage` (imsg CLI)
|
||||
|
||||
Clawdis spawns `imsg rpc` (JSON-RPC over stdio). No daemon or port required.
|
||||
|
||||
166
docs/slack.md
Normal file
166
docs/slack.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# Slack (socket mode)
|
||||
|
||||
## Setup
|
||||
1) Create a Slack app (From scratch) in https://api.slack.com/apps.
|
||||
2) **Socket Mode** → toggle on. Then go to **Basic Information** → **App-Level Tokens** → **Generate Token and Scopes** with scope `connections:write`. Copy the **App Token** (`xapp-...`).
|
||||
3) **OAuth & Permissions** → add bot token scopes (use the manifest below). Click **Install to Workspace**. Copy the **Bot User OAuth Token** (`xoxb-...`).
|
||||
4) **Event Subscriptions** → enable events and subscribe to:
|
||||
- `message.*` (includes edits/deletes/thread broadcasts)
|
||||
- `app_mention`
|
||||
- `reaction_added`, `reaction_removed`
|
||||
- `member_joined_channel`, `member_left_channel`
|
||||
- `channel_rename`
|
||||
- `pin_added`, `pin_removed`
|
||||
5) Invite the bot to channels you want it to read.
|
||||
6) Slash Commands → create the `/clawd` command (or your preferred name).
|
||||
7) App Home → enable the **Messages Tab** so users can DM the bot.
|
||||
|
||||
Use the manifest below so scopes and events stay in sync.
|
||||
|
||||
## Manifest (optional)
|
||||
Use this Slack app manifest to create the app quickly (adjust the name/command if you want).
|
||||
|
||||
```json
|
||||
{
|
||||
"display_information": {
|
||||
"name": "Clawdbot",
|
||||
"description": "Slack connector for Clawdbot"
|
||||
},
|
||||
"features": {
|
||||
"bot_user": {
|
||||
"display_name": "Clawdbot",
|
||||
"always_online": false
|
||||
},
|
||||
"app_home": {
|
||||
"messages_tab_enabled": true,
|
||||
"messages_tab_read_only_enabled": false
|
||||
},
|
||||
"slash_commands": [
|
||||
{
|
||||
"command": "/clawd",
|
||||
"description": "Send a message to Clawdbot",
|
||||
"should_escape": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"oauth_config": {
|
||||
"scopes": {
|
||||
"bot": [
|
||||
"chat:write",
|
||||
"channels:history",
|
||||
"channels:read",
|
||||
"groups:history",
|
||||
"im:history",
|
||||
"mpim:history",
|
||||
"users:read",
|
||||
"app_mentions:read",
|
||||
"reactions:read",
|
||||
"reactions:write",
|
||||
"pins:read",
|
||||
"pins:write",
|
||||
"emoji:read",
|
||||
"commands",
|
||||
"files:read",
|
||||
"files:write"
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"socket_mode_enabled": true,
|
||||
"event_subscriptions": {
|
||||
"bot_events": [
|
||||
"app_mention",
|
||||
"message.channels",
|
||||
"message.groups",
|
||||
"message.im",
|
||||
"message.mpim",
|
||||
"reaction_added",
|
||||
"reaction_removed",
|
||||
"member_joined_channel",
|
||||
"member_left_channel",
|
||||
"channel_rename",
|
||||
"pin_added",
|
||||
"pin_removed"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Config
|
||||
Slack uses Socket Mode only (no HTTP webhook server). Provide both tokens:
|
||||
|
||||
```json
|
||||
{
|
||||
"slack": {
|
||||
"enabled": true,
|
||||
"botToken": "xoxb-...",
|
||||
"appToken": "xapp-...",
|
||||
"dm": {
|
||||
"enabled": true,
|
||||
"allowFrom": ["U123", "U456", "*"],
|
||||
"groupEnabled": false,
|
||||
"groupChannels": ["G123"]
|
||||
},
|
||||
"channels": {
|
||||
"C123": { "allow": true, "requireMention": true },
|
||||
"#general": { "allow": true, "requireMention": false }
|
||||
},
|
||||
"reactionNotifications": "own",
|
||||
"reactionAllowlist": ["U123"],
|
||||
"actions": {
|
||||
"reactions": true,
|
||||
"messages": true,
|
||||
"pins": true,
|
||||
"memberInfo": true,
|
||||
"emojiList": true
|
||||
},
|
||||
"slashCommand": {
|
||||
"enabled": true,
|
||||
"name": "clawd",
|
||||
"sessionPrefix": "slack:slash",
|
||||
"ephemeral": true
|
||||
},
|
||||
"replyToMode": "off",
|
||||
"textChunkLimit": 4000,
|
||||
"mediaMaxMb": 20
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Tokens can also be supplied via env vars:
|
||||
- `SLACK_BOT_TOKEN`
|
||||
- `SLACK_APP_TOKEN`
|
||||
|
||||
## Sessions + routing
|
||||
- DMs share the `main` session (like WhatsApp/Telegram).
|
||||
- Channels map to `slack:channel:<channelId>` sessions.
|
||||
- Slash commands use `slack:slash:<userId>` sessions.
|
||||
|
||||
## Reply threading
|
||||
Slack replies can be threaded when reply tags are present and `slack.replyToMode` is enabled.
|
||||
|
||||
```json
|
||||
{ "slack": { "replyToMode": "first" } }
|
||||
```
|
||||
|
||||
## Delivery targets
|
||||
Use these with cron/CLI sends:
|
||||
- `user:<id>` for DMs
|
||||
- `channel:<id>` for channels
|
||||
|
||||
## Tool actions
|
||||
Slack tool actions can be gated with `slack.actions.*`:
|
||||
|
||||
| Action group | Default | Notes |
|
||||
| --- | --- | --- |
|
||||
| reactions | enabled | React + list reactions |
|
||||
| messages | enabled | Read/send/edit/delete |
|
||||
| pins | enabled | Pin/unpin/list |
|
||||
| memberInfo | enabled | Member info |
|
||||
| emojiList | enabled | Custom emoji list |
|
||||
|
||||
## Notes
|
||||
- Mention gating is controlled via `slack.channels` (set `requireMention` to `true`).
|
||||
- Reaction notifications follow `slack.reactionNotifications` (use `reactionAllowlist` with mode `allowlist`).
|
||||
- Attachments are downloaded to the media store when permitted and under the size limit.
|
||||
144
skills/slack/SKILL.md
Normal file
144
skills/slack/SKILL.md
Normal file
@@ -0,0 +1,144 @@
|
||||
---
|
||||
name: slack
|
||||
description: Use when you need to control Slack from Clawdis via the slack tool, including reacting to messages or pinning/unpinning items in Slack channels or DMs.
|
||||
---
|
||||
|
||||
# Slack Actions
|
||||
|
||||
## Overview
|
||||
|
||||
Use `slack` to react, manage pins, send/edit/delete messages, and fetch member info. The tool uses the bot token configured for Clawdis.
|
||||
|
||||
## Inputs to collect
|
||||
|
||||
- `channelId` and `messageId` (Slack message timestamp, e.g. `1712023032.1234`).
|
||||
- For reactions, an `emoji` (Unicode or `:name:`).
|
||||
- For message sends, a `to` target (`channel:<id>` or `user:<id>`) and `content`.
|
||||
- For searches, a `query` (optionally `channelIds` or `channelNames`).
|
||||
|
||||
Message context lines include `slack message id` and `channel` fields you can reuse directly.
|
||||
|
||||
## Actions
|
||||
|
||||
### Action groups
|
||||
|
||||
| Action group | Default | Notes |
|
||||
| --- | --- | --- |
|
||||
| reactions | enabled | React + list reactions |
|
||||
| messages | enabled | Read/send/edit/delete |
|
||||
| pins | enabled | Pin/unpin/list |
|
||||
| memberInfo | enabled | Member info |
|
||||
| emojiList | enabled | Custom emoji list |
|
||||
|
||||
### React to a message
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "react",
|
||||
"channelId": "C123",
|
||||
"messageId": "1712023032.1234",
|
||||
"emoji": "✅"
|
||||
}
|
||||
```
|
||||
|
||||
### List reactions
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "reactions",
|
||||
"channelId": "C123",
|
||||
"messageId": "1712023032.1234"
|
||||
}
|
||||
```
|
||||
|
||||
### Send a message
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "sendMessage",
|
||||
"to": "channel:C123",
|
||||
"content": "Hello from Clawdis"
|
||||
}
|
||||
```
|
||||
|
||||
### Edit a message
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "editMessage",
|
||||
"channelId": "C123",
|
||||
"messageId": "1712023032.1234",
|
||||
"content": "Updated text"
|
||||
}
|
||||
```
|
||||
|
||||
### Delete a message
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "deleteMessage",
|
||||
"channelId": "C123",
|
||||
"messageId": "1712023032.1234"
|
||||
}
|
||||
```
|
||||
|
||||
### Read recent messages
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "readMessages",
|
||||
"channelId": "C123",
|
||||
"limit": 20
|
||||
}
|
||||
```
|
||||
|
||||
### Pin a message
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "pinMessage",
|
||||
"channelId": "C123",
|
||||
"messageId": "1712023032.1234"
|
||||
}
|
||||
```
|
||||
|
||||
### Unpin a message
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "unpinMessage",
|
||||
"channelId": "C123",
|
||||
"messageId": "1712023032.1234"
|
||||
}
|
||||
```
|
||||
|
||||
### List pinned items
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "listPins",
|
||||
"channelId": "C123"
|
||||
}
|
||||
```
|
||||
|
||||
### Member info
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "memberInfo",
|
||||
"userId": "U123"
|
||||
}
|
||||
```
|
||||
|
||||
### Emoji list
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "emojiList"
|
||||
}
|
||||
```
|
||||
|
||||
## Ideas to try
|
||||
|
||||
- React with ✅ to mark completed tasks.
|
||||
- Pin key decisions or weekly status updates.
|
||||
@@ -209,6 +209,24 @@
|
||||
"kick": { "label": "kick", "detailKeys": ["guildId", "userId"] },
|
||||
"ban": { "label": "ban", "detailKeys": ["guildId", "userId"] }
|
||||
}
|
||||
},
|
||||
"slack": {
|
||||
"emoji": "💬",
|
||||
"title": "Slack",
|
||||
"actions": {
|
||||
"react": { "label": "react", "detailKeys": ["channelId", "messageId", "emoji"] },
|
||||
"reactions": { "label": "reactions", "detailKeys": ["channelId", "messageId"] },
|
||||
"sendMessage": { "label": "send", "detailKeys": ["to", "content"] },
|
||||
"editMessage": { "label": "edit", "detailKeys": ["channelId", "messageId"] },
|
||||
"deleteMessage": { "label": "delete", "detailKeys": ["channelId", "messageId"] },
|
||||
"readMessages": { "label": "read messages", "detailKeys": ["channelId", "limit"] },
|
||||
"pinMessage": { "label": "pin", "detailKeys": ["channelId", "messageId"] },
|
||||
"unpinMessage": { "label": "unpin", "detailKeys": ["channelId", "messageId"] },
|
||||
"listPins": { "label": "list pins", "detailKeys": ["channelId"] },
|
||||
"memberInfo": { "label": "member", "detailKeys": ["userId"] },
|
||||
"emojiList": { "label": "emoji list" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,90 @@ async function noteDiscordTokenHelp(prompter: WizardPrompter): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
function buildSlackManifest(botName: string) {
|
||||
const safeName = botName.trim() || "Clawdis";
|
||||
const manifest = {
|
||||
display_information: {
|
||||
name: safeName,
|
||||
description: `${safeName} connector for Clawdis`,
|
||||
},
|
||||
features: {
|
||||
bot_user: {
|
||||
display_name: safeName,
|
||||
always_online: false,
|
||||
},
|
||||
slash_commands: [
|
||||
{
|
||||
command: "/clawd",
|
||||
description: "Send a message to Clawdis",
|
||||
should_escape: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
oauth_config: {
|
||||
scopes: {
|
||||
bot: [
|
||||
"chat:write",
|
||||
"channels:history",
|
||||
"channels:read",
|
||||
"groups:history",
|
||||
"im:history",
|
||||
"mpim:history",
|
||||
"users:read",
|
||||
"app_mentions:read",
|
||||
"reactions:read",
|
||||
"pins:read",
|
||||
"pins:write",
|
||||
"emoji:read",
|
||||
"commands",
|
||||
"files:read",
|
||||
"files:write",
|
||||
],
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
socket_mode_enabled: true,
|
||||
event_subscriptions: {
|
||||
bot_events: [
|
||||
"app_mention",
|
||||
"message.channels",
|
||||
"message.groups",
|
||||
"message.im",
|
||||
"message.mpim",
|
||||
"reaction_added",
|
||||
"reaction_removed",
|
||||
"member_joined_channel",
|
||||
"member_left_channel",
|
||||
"channel_rename",
|
||||
"pin_added",
|
||||
"pin_removed",
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
return JSON.stringify(manifest, null, 2);
|
||||
}
|
||||
|
||||
async function noteSlackTokenHelp(
|
||||
prompter: WizardPrompter,
|
||||
botName: string,
|
||||
): Promise<void> {
|
||||
const manifest = buildSlackManifest(botName);
|
||||
await prompter.note(
|
||||
[
|
||||
"1) Slack API → Create App → From scratch",
|
||||
"2) Add Socket Mode + enable it to get the app-level token (xapp-...)",
|
||||
"3) OAuth & Permissions → install app to workspace (xoxb- bot token)",
|
||||
"4) Enable Event Subscriptions (socket) for message events",
|
||||
"Tip: set SLACK_BOT_TOKEN + SLACK_APP_TOKEN in your env.",
|
||||
"",
|
||||
"Manifest (JSON):",
|
||||
manifest,
|
||||
].join("\n"),
|
||||
"Slack socket mode tokens",
|
||||
);
|
||||
}
|
||||
|
||||
function setWhatsAppAllowFrom(cfg: ClawdisConfig, allowFrom?: string[]) {
|
||||
return {
|
||||
...cfg,
|
||||
@@ -374,6 +458,96 @@ export async function setupProviders(
|
||||
}
|
||||
}
|
||||
|
||||
if (selection.includes("slack")) {
|
||||
let botToken: string | null = null;
|
||||
let appToken: string | null = null;
|
||||
const slackBotName = String(
|
||||
await prompter.text({
|
||||
message: "Slack bot display name (used for manifest)",
|
||||
initialValue: "Clawdis",
|
||||
}),
|
||||
).trim();
|
||||
if (!slackConfigured) {
|
||||
await noteSlackTokenHelp(prompter, slackBotName);
|
||||
}
|
||||
if (
|
||||
slackBotEnv &&
|
||||
slackAppEnv &&
|
||||
(!cfg.slack?.botToken || !cfg.slack?.appToken)
|
||||
) {
|
||||
const keepEnv = await prompter.confirm({
|
||||
message: "SLACK_BOT_TOKEN + SLACK_APP_TOKEN detected. Use env vars?",
|
||||
initialValue: true,
|
||||
});
|
||||
if (keepEnv) {
|
||||
next = {
|
||||
...next,
|
||||
slack: {
|
||||
...next.slack,
|
||||
enabled: true,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
botToken = String(
|
||||
await prompter.text({
|
||||
message: "Enter Slack bot token (xoxb-...)",
|
||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||
}),
|
||||
).trim();
|
||||
appToken = String(
|
||||
await prompter.text({
|
||||
message: "Enter Slack app token (xapp-...)",
|
||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||
}),
|
||||
).trim();
|
||||
}
|
||||
} else if (cfg.slack?.botToken && cfg.slack?.appToken) {
|
||||
const keep = await prompter.confirm({
|
||||
message: "Slack tokens already configured. Keep them?",
|
||||
initialValue: true,
|
||||
});
|
||||
if (!keep) {
|
||||
botToken = String(
|
||||
await prompter.text({
|
||||
message: "Enter Slack bot token (xoxb-...)",
|
||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||
}),
|
||||
).trim();
|
||||
appToken = String(
|
||||
await prompter.text({
|
||||
message: "Enter Slack app token (xapp-...)",
|
||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||
}),
|
||||
).trim();
|
||||
}
|
||||
} else {
|
||||
botToken = String(
|
||||
await prompter.text({
|
||||
message: "Enter Slack bot token (xoxb-...)",
|
||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||
}),
|
||||
).trim();
|
||||
appToken = String(
|
||||
await prompter.text({
|
||||
message: "Enter Slack app token (xapp-...)",
|
||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||
}),
|
||||
).trim();
|
||||
}
|
||||
|
||||
if (botToken && appToken) {
|
||||
next = {
|
||||
...next,
|
||||
slack: {
|
||||
...next.slack,
|
||||
enabled: true,
|
||||
botToken,
|
||||
appToken,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (selection.includes("signal")) {
|
||||
let resolvedCliPath = signalCliPath;
|
||||
let cliDetected = signalCliDetected;
|
||||
@@ -550,3 +724,4 @@ export async function setupProviders(
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ const GROUP_LABELS: Record<string, string> = {
|
||||
talk: "Talk",
|
||||
telegram: "Telegram",
|
||||
discord: "Discord",
|
||||
slack: "Slack",
|
||||
signal: "Signal",
|
||||
imessage: "iMessage",
|
||||
whatsapp: "WhatsApp",
|
||||
@@ -65,6 +66,7 @@ const GROUP_ORDER: Record<string, number> = {
|
||||
talk: 130,
|
||||
telegram: 140,
|
||||
discord: 150,
|
||||
slack: 155,
|
||||
signal: 160,
|
||||
imessage: 170,
|
||||
whatsapp: 180,
|
||||
@@ -92,6 +94,8 @@ const FIELD_LABELS: Record<string, string> = {
|
||||
"talk.apiKey": "Talk API Key",
|
||||
"telegram.botToken": "Telegram Bot Token",
|
||||
"discord.token": "Discord Bot Token",
|
||||
"slack.botToken": "Slack Bot Token",
|
||||
"slack.appToken": "Slack App Token",
|
||||
"signal.account": "Signal Account",
|
||||
"imessage.cliPath": "iMessage CLI Path",
|
||||
};
|
||||
|
||||
@@ -144,6 +144,7 @@ export type HookMappingConfig = {
|
||||
| "whatsapp"
|
||||
| "telegram"
|
||||
| "discord"
|
||||
| "slack"
|
||||
| "signal"
|
||||
| "imessage";
|
||||
to?: string;
|
||||
@@ -290,6 +291,64 @@ export type DiscordConfig = {
|
||||
guilds?: Record<string, DiscordGuildEntry>;
|
||||
};
|
||||
|
||||
export type SlackDmConfig = {
|
||||
/** If false, ignore all incoming Slack DMs. Default: true. */
|
||||
enabled?: boolean;
|
||||
/** Allowlist for DM senders (ids). */
|
||||
allowFrom?: Array<string | number>;
|
||||
/** If true, allow group DMs (default: false). */
|
||||
groupEnabled?: boolean;
|
||||
/** Optional allowlist for group DM channels (ids or slugs). */
|
||||
groupChannels?: Array<string | number>;
|
||||
};
|
||||
|
||||
export type SlackChannelConfig = {
|
||||
allow?: boolean;
|
||||
requireMention?: boolean;
|
||||
};
|
||||
|
||||
export type SlackReactionNotificationMode = "off" | "own" | "all" | "allowlist";
|
||||
|
||||
export type SlackActionConfig = {
|
||||
reactions?: boolean;
|
||||
messages?: boolean;
|
||||
pins?: boolean;
|
||||
search?: boolean;
|
||||
permissions?: boolean;
|
||||
memberInfo?: boolean;
|
||||
channelInfo?: boolean;
|
||||
emojiList?: boolean;
|
||||
};
|
||||
|
||||
export type SlackSlashCommandConfig = {
|
||||
/** Enable handling for the configured slash command (default: false). */
|
||||
enabled?: boolean;
|
||||
/** Slash command name (default: "clawd"). */
|
||||
name?: string;
|
||||
/** Session key prefix for slash commands (default: "slack:slash"). */
|
||||
sessionPrefix?: string;
|
||||
/** Reply ephemerally (default: true). */
|
||||
ephemeral?: boolean;
|
||||
};
|
||||
|
||||
export type SlackConfig = {
|
||||
/** If false, do not start the Slack provider. Default: true. */
|
||||
enabled?: boolean;
|
||||
botToken?: string;
|
||||
appToken?: string;
|
||||
textChunkLimit?: number;
|
||||
replyToMode?: ReplyToMode;
|
||||
mediaMaxMb?: number;
|
||||
/** Reaction notification mode (off|own|all|allowlist). Default: own. */
|
||||
reactionNotifications?: SlackReactionNotificationMode;
|
||||
/** Allowlist for reaction notifications when mode is allowlist. */
|
||||
reactionAllowlist?: Array<string | number>;
|
||||
actions?: SlackActionConfig;
|
||||
slashCommand?: SlackSlashCommandConfig;
|
||||
dm?: SlackDmConfig;
|
||||
channels?: Record<string, SlackChannelConfig>;
|
||||
};
|
||||
|
||||
export type SignalConfig = {
|
||||
/** If false, do not start the Signal provider. Default: true. */
|
||||
enabled?: boolean;
|
||||
@@ -356,6 +415,7 @@ export type QueueModeBySurface = {
|
||||
whatsapp?: QueueMode;
|
||||
telegram?: QueueMode;
|
||||
discord?: QueueMode;
|
||||
slack?: QueueMode;
|
||||
signal?: QueueMode;
|
||||
imessage?: QueueMode;
|
||||
webchat?: QueueMode;
|
||||
@@ -642,6 +702,7 @@ export type ClawdisConfig = {
|
||||
| "whatsapp"
|
||||
| "telegram"
|
||||
| "discord"
|
||||
| "slack"
|
||||
| "signal"
|
||||
| "imessage"
|
||||
| "none";
|
||||
@@ -731,6 +792,7 @@ export type ClawdisConfig = {
|
||||
whatsapp?: WhatsAppConfig;
|
||||
telegram?: TelegramConfig;
|
||||
discord?: DiscordConfig;
|
||||
slack?: SlackConfig;
|
||||
signal?: SignalConfig;
|
||||
imessage?: IMessageConfig;
|
||||
cron?: CronConfig;
|
||||
|
||||
@@ -86,6 +86,7 @@ const QueueModeBySurfaceSchema = z
|
||||
whatsapp: QueueModeSchema.optional(),
|
||||
telegram: QueueModeSchema.optional(),
|
||||
discord: QueueModeSchema.optional(),
|
||||
slack: QueueModeSchema.optional(),
|
||||
signal: QueueModeSchema.optional(),
|
||||
imessage: QueueModeSchema.optional(),
|
||||
webchat: QueueModeSchema.optional(),
|
||||
@@ -163,6 +164,7 @@ const HeartbeatSchema = z
|
||||
z.literal("whatsapp"),
|
||||
z.literal("telegram"),
|
||||
z.literal("discord"),
|
||||
z.literal("slack"),
|
||||
z.literal("signal"),
|
||||
z.literal("imessage"),
|
||||
z.literal("none"),
|
||||
@@ -225,6 +227,7 @@ const HookMappingSchema = z
|
||||
z.literal("whatsapp"),
|
||||
z.literal("telegram"),
|
||||
z.literal("discord"),
|
||||
z.literal("slack"),
|
||||
z.literal("signal"),
|
||||
z.literal("imessage"),
|
||||
])
|
||||
@@ -619,6 +622,59 @@ export const ClawdisSchema = z.object({
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
slack: z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
botToken: z.string().optional(),
|
||||
appToken: z.string().optional(),
|
||||
textChunkLimit: z.number().int().positive().optional(),
|
||||
replyToMode: ReplyToModeSchema.optional(),
|
||||
mediaMaxMb: z.number().positive().optional(),
|
||||
reactionNotifications: z
|
||||
.enum(["off", "own", "all", "allowlist"])
|
||||
.optional(),
|
||||
reactionAllowlist: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
actions: z
|
||||
.object({
|
||||
reactions: z.boolean().optional(),
|
||||
messages: z.boolean().optional(),
|
||||
pins: z.boolean().optional(),
|
||||
search: z.boolean().optional(),
|
||||
permissions: z.boolean().optional(),
|
||||
memberInfo: z.boolean().optional(),
|
||||
channelInfo: z.boolean().optional(),
|
||||
emojiList: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
slashCommand: z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
name: z.string().optional(),
|
||||
sessionPrefix: z.string().optional(),
|
||||
ephemeral: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
dm: z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
groupEnabled: z.boolean().optional(),
|
||||
groupChannels: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
})
|
||||
.optional(),
|
||||
channels: z
|
||||
.record(
|
||||
z.string(),
|
||||
z
|
||||
.object({
|
||||
allow: z.boolean().optional(),
|
||||
requireMention: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
signal: z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
|
||||
184
src/slack/actions.ts
Normal file
184
src/slack/actions.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import { WebClient } from "@slack/web-api";
|
||||
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { sendMessageSlack } from "./send.js";
|
||||
import { resolveSlackBotToken } from "./token.js";
|
||||
|
||||
export type SlackActionClientOpts = {
|
||||
token?: string;
|
||||
client?: WebClient;
|
||||
};
|
||||
|
||||
export type SlackMessageSummary = {
|
||||
ts?: string;
|
||||
text?: string;
|
||||
user?: string;
|
||||
thread_ts?: string;
|
||||
reply_count?: number;
|
||||
reactions?: Array<{
|
||||
name?: string;
|
||||
count?: number;
|
||||
users?: string[];
|
||||
}>;
|
||||
};
|
||||
|
||||
export type SlackPin = {
|
||||
type?: string;
|
||||
message?: { ts?: string; text?: string };
|
||||
file?: { id?: string; name?: string };
|
||||
};
|
||||
|
||||
function resolveToken(explicit?: string) {
|
||||
const cfgToken = loadConfig().slack?.botToken;
|
||||
const token = resolveSlackBotToken(
|
||||
explicit ?? process.env.SLACK_BOT_TOKEN ?? cfgToken ?? undefined,
|
||||
);
|
||||
if (!token) {
|
||||
throw new Error(
|
||||
"SLACK_BOT_TOKEN or slack.botToken is required for Slack actions",
|
||||
);
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
function normalizeEmoji(raw: string) {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
throw new Error("Emoji is required for Slack reactions");
|
||||
}
|
||||
return trimmed.replace(/^:+|:+$/g, "");
|
||||
}
|
||||
|
||||
async function getClient(opts: SlackActionClientOpts = {}) {
|
||||
const token = resolveToken(opts.token);
|
||||
return opts.client ?? new WebClient(token);
|
||||
}
|
||||
|
||||
export async function reactSlackMessage(
|
||||
channelId: string,
|
||||
messageId: string,
|
||||
emoji: string,
|
||||
opts: SlackActionClientOpts = {},
|
||||
) {
|
||||
const client = await getClient(opts);
|
||||
await client.reactions.add({
|
||||
channel: channelId,
|
||||
timestamp: messageId,
|
||||
name: normalizeEmoji(emoji),
|
||||
});
|
||||
}
|
||||
|
||||
export async function listSlackReactions(
|
||||
channelId: string,
|
||||
messageId: string,
|
||||
opts: SlackActionClientOpts = {},
|
||||
): Promise<SlackMessageSummary["reactions"]> {
|
||||
const client = await getClient(opts);
|
||||
const result = await client.reactions.get({
|
||||
channel: channelId,
|
||||
timestamp: messageId,
|
||||
full: true,
|
||||
});
|
||||
const message = result.message as SlackMessageSummary | undefined;
|
||||
return message?.reactions ?? [];
|
||||
}
|
||||
|
||||
export async function sendSlackMessage(
|
||||
to: string,
|
||||
content: string,
|
||||
opts: SlackActionClientOpts & { mediaUrl?: string; replyTo?: string } = {},
|
||||
) {
|
||||
return await sendMessageSlack(to, content, {
|
||||
token: opts.token,
|
||||
mediaUrl: opts.mediaUrl,
|
||||
threadTs: opts.replyTo,
|
||||
client: opts.client,
|
||||
});
|
||||
}
|
||||
|
||||
export async function editSlackMessage(
|
||||
channelId: string,
|
||||
messageId: string,
|
||||
content: string,
|
||||
opts: SlackActionClientOpts = {},
|
||||
) {
|
||||
const client = await getClient(opts);
|
||||
await client.chat.update({
|
||||
channel: channelId,
|
||||
ts: messageId,
|
||||
text: content,
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteSlackMessage(
|
||||
channelId: string,
|
||||
messageId: string,
|
||||
opts: SlackActionClientOpts = {},
|
||||
) {
|
||||
const client = await getClient(opts);
|
||||
await client.chat.delete({
|
||||
channel: channelId,
|
||||
ts: messageId,
|
||||
});
|
||||
}
|
||||
|
||||
export async function readSlackMessages(
|
||||
channelId: string,
|
||||
opts: SlackActionClientOpts & {
|
||||
limit?: number;
|
||||
before?: string;
|
||||
after?: string;
|
||||
} = {},
|
||||
): Promise<{ messages: SlackMessageSummary[]; hasMore: boolean }> {
|
||||
const client = await getClient(opts);
|
||||
const result = await client.conversations.history({
|
||||
channel: channelId,
|
||||
limit: opts.limit,
|
||||
latest: opts.before,
|
||||
oldest: opts.after,
|
||||
});
|
||||
return {
|
||||
messages: (result.messages ?? []) as SlackMessageSummary[],
|
||||
hasMore: Boolean(result.has_more),
|
||||
};
|
||||
}
|
||||
|
||||
export async function getSlackMemberInfo(
|
||||
userId: string,
|
||||
opts: SlackActionClientOpts = {},
|
||||
) {
|
||||
const client = await getClient(opts);
|
||||
return await client.users.info({ user: userId });
|
||||
}
|
||||
|
||||
export async function listSlackEmojis(opts: SlackActionClientOpts = {}) {
|
||||
const client = await getClient(opts);
|
||||
return await client.emoji.list();
|
||||
}
|
||||
|
||||
export async function pinSlackMessage(
|
||||
channelId: string,
|
||||
messageId: string,
|
||||
opts: SlackActionClientOpts = {},
|
||||
) {
|
||||
const client = await getClient(opts);
|
||||
await client.pins.add({ channel: channelId, timestamp: messageId });
|
||||
}
|
||||
|
||||
export async function unpinSlackMessage(
|
||||
channelId: string,
|
||||
messageId: string,
|
||||
opts: SlackActionClientOpts = {},
|
||||
) {
|
||||
const client = await getClient(opts);
|
||||
await client.pins.remove({ channel: channelId, timestamp: messageId });
|
||||
}
|
||||
|
||||
export async function listSlackPins(
|
||||
channelId: string,
|
||||
opts: SlackActionClientOpts = {},
|
||||
): Promise<SlackPin[]> {
|
||||
const client = await getClient(opts);
|
||||
const result = await client.pins.list({ channel: channelId });
|
||||
return (result.items ?? []) as SlackPin[];
|
||||
}
|
||||
15
src/slack/index.ts
Normal file
15
src/slack/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export {
|
||||
deleteSlackMessage,
|
||||
editSlackMessage,
|
||||
getSlackMemberInfo,
|
||||
listSlackEmojis,
|
||||
listSlackPins,
|
||||
listSlackReactions,
|
||||
pinSlackMessage,
|
||||
reactSlackMessage,
|
||||
readSlackMessages,
|
||||
sendSlackMessage,
|
||||
unpinSlackMessage,
|
||||
} from "./actions.js";
|
||||
export { monitorSlackProvider } from "./monitor.js";
|
||||
export { sendMessageSlack } from "./send.js";
|
||||
1317
src/slack/monitor.ts
Normal file
1317
src/slack/monitor.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -235,6 +235,92 @@ export function applyConfigSnapshot(state: ConfigState, snapshot: ConfigSnapshot
|
||||
typeof slash.ephemeral === "boolean" ? slash.ephemeral : true,
|
||||
};
|
||||
|
||||
const slackDm = (slack.dm ?? {}) as Record<string, unknown>;
|
||||
const slackChannels = slack.channels;
|
||||
const slackSlash = (slack.slashCommand ?? {}) as Record<string, unknown>;
|
||||
const slackActions =
|
||||
(slack.actions ?? {}) as Partial<Record<keyof typeof defaultSlackActions, unknown>>;
|
||||
state.slackForm = {
|
||||
enabled: typeof slack.enabled === "boolean" ? slack.enabled : true,
|
||||
botToken: typeof slack.botToken === "string" ? slack.botToken : "",
|
||||
appToken: typeof slack.appToken === "string" ? slack.appToken : "",
|
||||
dmEnabled: typeof slackDm.enabled === "boolean" ? slackDm.enabled : true,
|
||||
allowFrom: toList(slackDm.allowFrom),
|
||||
groupEnabled:
|
||||
typeof slackDm.groupEnabled === "boolean" ? slackDm.groupEnabled : false,
|
||||
groupChannels: toList(slackDm.groupChannels),
|
||||
mediaMaxMb:
|
||||
typeof slack.mediaMaxMb === "number" ? String(slack.mediaMaxMb) : "",
|
||||
textChunkLimit:
|
||||
typeof slack.textChunkLimit === "number"
|
||||
? String(slack.textChunkLimit)
|
||||
: "",
|
||||
replyToMode:
|
||||
slack.replyToMode === "first" || slack.replyToMode === "all"
|
||||
? slack.replyToMode
|
||||
: "off",
|
||||
reactionNotifications:
|
||||
slack.reactionNotifications === "off" ||
|
||||
slack.reactionNotifications === "all" ||
|
||||
slack.reactionNotifications === "allowlist"
|
||||
? slack.reactionNotifications
|
||||
: "own",
|
||||
reactionAllowlist: toList(slack.reactionAllowlist),
|
||||
slashEnabled:
|
||||
typeof slackSlash.enabled === "boolean" ? slackSlash.enabled : false,
|
||||
slashName: typeof slackSlash.name === "string" ? slackSlash.name : "",
|
||||
slashSessionPrefix:
|
||||
typeof slackSlash.sessionPrefix === "string"
|
||||
? slackSlash.sessionPrefix
|
||||
: "",
|
||||
slashEphemeral:
|
||||
typeof slackSlash.ephemeral === "boolean" ? slackSlash.ephemeral : true,
|
||||
actions: {
|
||||
...defaultSlackActions,
|
||||
reactions:
|
||||
typeof slackActions.reactions === "boolean"
|
||||
? slackActions.reactions
|
||||
: defaultSlackActions.reactions,
|
||||
messages:
|
||||
typeof slackActions.messages === "boolean"
|
||||
? slackActions.messages
|
||||
: defaultSlackActions.messages,
|
||||
pins:
|
||||
typeof slackActions.pins === "boolean"
|
||||
? slackActions.pins
|
||||
: defaultSlackActions.pins,
|
||||
memberInfo:
|
||||
typeof slackActions.memberInfo === "boolean"
|
||||
? slackActions.memberInfo
|
||||
: defaultSlackActions.memberInfo,
|
||||
emojiList:
|
||||
typeof slackActions.emojiList === "boolean"
|
||||
? slackActions.emojiList
|
||||
: defaultSlackActions.emojiList,
|
||||
},
|
||||
channels: Array.isArray(slackChannels)
|
||||
? []
|
||||
: typeof slackChannels === "object" && slackChannels
|
||||
? Object.entries(slackChannels as Record<string, unknown>).map(
|
||||
([key, value]): SlackChannelForm => {
|
||||
const entry =
|
||||
value && typeof value === "object"
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
return {
|
||||
key,
|
||||
allow:
|
||||
typeof entry.allow === "boolean" ? entry.allow : true,
|
||||
requireMention:
|
||||
typeof entry.requireMention === "boolean"
|
||||
? entry.requireMention
|
||||
: false,
|
||||
};
|
||||
},
|
||||
)
|
||||
: [],
|
||||
};
|
||||
|
||||
state.signalForm = {
|
||||
enabled: typeof signal.enabled === "boolean" ? signal.enabled : true,
|
||||
account: typeof signal.account === "string" ? signal.account : "",
|
||||
@@ -281,6 +367,7 @@ export function applyConfigSnapshot(state: ConfigState, snapshot: ConfigSnapshot
|
||||
const configInvalid = snapshot.valid === false ? "Config invalid." : null;
|
||||
state.telegramConfigStatus = configInvalid;
|
||||
state.discordConfigStatus = configInvalid;
|
||||
state.slackConfigStatus = configInvalid;
|
||||
state.signalConfigStatus = configInvalid;
|
||||
state.imessageConfigStatus = configInvalid;
|
||||
|
||||
@@ -405,3 +492,4 @@ function removePathValue(
|
||||
delete (current as Record<string, unknown>)[lastKey];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -379,6 +379,147 @@ export async function saveDiscordConfig(state: ConnectionsState) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveSlackConfig(state: ConnectionsState) {
|
||||
if (!state.client || !state.connected) return;
|
||||
if (state.slackSaving) return;
|
||||
state.slackSaving = true;
|
||||
state.slackConfigStatus = null;
|
||||
try {
|
||||
const base = state.configSnapshot?.config ?? {};
|
||||
const config = { ...base } as Record<string, unknown>;
|
||||
const slack = { ...(config.slack ?? {}) } as Record<string, unknown>;
|
||||
const form = state.slackForm;
|
||||
|
||||
if (form.enabled) {
|
||||
delete slack.enabled;
|
||||
} else {
|
||||
slack.enabled = false;
|
||||
}
|
||||
|
||||
if (!state.slackTokenLocked) {
|
||||
const token = form.botToken.trim();
|
||||
if (token) slack.botToken = token;
|
||||
else delete slack.botToken;
|
||||
}
|
||||
if (!state.slackAppTokenLocked) {
|
||||
const token = form.appToken.trim();
|
||||
if (token) slack.appToken = token;
|
||||
else delete slack.appToken;
|
||||
}
|
||||
|
||||
const dm = { ...(slack.dm ?? {}) } as Record<string, unknown>;
|
||||
dm.enabled = form.dmEnabled;
|
||||
const allowFrom = parseList(form.allowFrom);
|
||||
if (allowFrom.length > 0) dm.allowFrom = allowFrom;
|
||||
else delete dm.allowFrom;
|
||||
if (form.groupEnabled) {
|
||||
dm.groupEnabled = true;
|
||||
} else {
|
||||
delete dm.groupEnabled;
|
||||
}
|
||||
const groupChannels = parseList(form.groupChannels);
|
||||
if (groupChannels.length > 0) dm.groupChannels = groupChannels;
|
||||
else delete dm.groupChannels;
|
||||
if (Object.keys(dm).length > 0) slack.dm = dm;
|
||||
else delete slack.dm;
|
||||
|
||||
const mediaMaxMb = Number.parseFloat(form.mediaMaxMb);
|
||||
if (Number.isFinite(mediaMaxMb) && mediaMaxMb > 0) {
|
||||
slack.mediaMaxMb = mediaMaxMb;
|
||||
} else {
|
||||
delete slack.mediaMaxMb;
|
||||
}
|
||||
|
||||
const textChunkLimit = Number.parseInt(form.textChunkLimit, 10);
|
||||
if (Number.isFinite(textChunkLimit) && textChunkLimit > 0) {
|
||||
slack.textChunkLimit = textChunkLimit;
|
||||
} else {
|
||||
delete slack.textChunkLimit;
|
||||
}
|
||||
|
||||
if (form.replyToMode === "off") delete slack.replyToMode;
|
||||
else slack.replyToMode = form.replyToMode;
|
||||
|
||||
if (form.reactionNotifications === "own") {
|
||||
delete slack.reactionNotifications;
|
||||
} else {
|
||||
slack.reactionNotifications = form.reactionNotifications;
|
||||
}
|
||||
const reactionAllowlist = parseList(form.reactionAllowlist);
|
||||
if (reactionAllowlist.length > 0) {
|
||||
slack.reactionAllowlist = reactionAllowlist;
|
||||
} else {
|
||||
delete slack.reactionAllowlist;
|
||||
}
|
||||
|
||||
const slash = { ...(slack.slashCommand ?? {}) } as Record<string, unknown>;
|
||||
if (form.slashEnabled) {
|
||||
slash.enabled = true;
|
||||
} else {
|
||||
delete slash.enabled;
|
||||
}
|
||||
if (form.slashName.trim()) slash.name = form.slashName.trim();
|
||||
else delete slash.name;
|
||||
if (form.slashSessionPrefix.trim())
|
||||
slash.sessionPrefix = form.slashSessionPrefix.trim();
|
||||
else delete slash.sessionPrefix;
|
||||
if (form.slashEphemeral) {
|
||||
delete slash.ephemeral;
|
||||
} else {
|
||||
slash.ephemeral = false;
|
||||
}
|
||||
if (Object.keys(slash).length > 0) slack.slashCommand = slash;
|
||||
else delete slack.slashCommand;
|
||||
|
||||
const actions: Partial<SlackActionForm> = {};
|
||||
const applyAction = (key: keyof SlackActionForm) => {
|
||||
const value = form.actions[key];
|
||||
if (value !== defaultSlackActions[key]) actions[key] = value;
|
||||
};
|
||||
applyAction("reactions");
|
||||
applyAction("messages");
|
||||
applyAction("pins");
|
||||
applyAction("memberInfo");
|
||||
applyAction("emojiList");
|
||||
if (Object.keys(actions).length > 0) {
|
||||
slack.actions = actions;
|
||||
} else {
|
||||
delete slack.actions;
|
||||
}
|
||||
|
||||
const channels = form.channels
|
||||
.map((entry): [string, Record<string, unknown>] | null => {
|
||||
const key = entry.key.trim();
|
||||
if (!key) return null;
|
||||
const record: Record<string, unknown> = {
|
||||
allow: entry.allow,
|
||||
requireMention: entry.requireMention,
|
||||
};
|
||||
return [key, record];
|
||||
})
|
||||
.filter((value): value is [string, Record<string, unknown>] => Boolean(value));
|
||||
if (channels.length > 0) {
|
||||
slack.channels = Object.fromEntries(channels);
|
||||
} else {
|
||||
delete slack.channels;
|
||||
}
|
||||
|
||||
if (Object.keys(slack).length > 0) {
|
||||
config.slack = slack;
|
||||
} else {
|
||||
delete config.slack;
|
||||
}
|
||||
|
||||
const raw = `${JSON.stringify(config, null, 2).trimEnd()}\n`;
|
||||
await state.client.request("config.set", { raw });
|
||||
state.slackConfigStatus = "Saved. Restart gateway if needed.";
|
||||
} catch (err) {
|
||||
state.slackConfigStatus = String(err);
|
||||
} finally {
|
||||
state.slackSaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveSignalConfig(state: ConnectionsState) {
|
||||
if (!state.client || !state.connected) return;
|
||||
if (state.signalSaving) return;
|
||||
@@ -529,3 +670,4 @@ export async function saveIMessageConfig(state: ConnectionsState) {
|
||||
state.imessageSaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,41 @@ export type DiscordActionForm = {
|
||||
moderation: boolean;
|
||||
};
|
||||
|
||||
export type SlackChannelForm = {
|
||||
key: string;
|
||||
allow: boolean;
|
||||
requireMention: boolean;
|
||||
};
|
||||
|
||||
export type SlackActionForm = {
|
||||
reactions: boolean;
|
||||
messages: boolean;
|
||||
pins: boolean;
|
||||
memberInfo: boolean;
|
||||
emojiList: boolean;
|
||||
};
|
||||
|
||||
export type SlackForm = {
|
||||
enabled: boolean;
|
||||
botToken: string;
|
||||
appToken: string;
|
||||
dmEnabled: boolean;
|
||||
allowFrom: string;
|
||||
groupEnabled: boolean;
|
||||
groupChannels: string;
|
||||
mediaMaxMb: string;
|
||||
textChunkLimit: string;
|
||||
replyToMode: "off" | "first" | "all";
|
||||
reactionNotifications: "off" | "own" | "all" | "allowlist";
|
||||
reactionAllowlist: string;
|
||||
slashEnabled: boolean;
|
||||
slashName: string;
|
||||
slashSessionPrefix: string;
|
||||
slashEphemeral: boolean;
|
||||
actions: SlackActionForm;
|
||||
channels: SlackChannelForm[];
|
||||
};
|
||||
|
||||
export const defaultDiscordActions: DiscordActionForm = {
|
||||
reactions: true,
|
||||
stickers: true,
|
||||
@@ -78,6 +113,14 @@ export const defaultDiscordActions: DiscordActionForm = {
|
||||
moderation: false,
|
||||
};
|
||||
|
||||
export const defaultSlackActions: SlackActionForm = {
|
||||
reactions: true,
|
||||
messages: true,
|
||||
pins: true,
|
||||
memberInfo: true,
|
||||
emojiList: true,
|
||||
};
|
||||
|
||||
export type SignalForm = {
|
||||
enabled: boolean;
|
||||
account: string;
|
||||
@@ -125,3 +168,4 @@ export type CronFormState = {
|
||||
timeoutSeconds: string;
|
||||
postToMainPrefix: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -28,6 +28,14 @@ const discordActionOptions = [
|
||||
{ key: "moderation", label: "Moderation" },
|
||||
] satisfies Array<{ key: keyof DiscordActionForm; label: string }>;
|
||||
|
||||
const slackActionOptions = [
|
||||
{ key: "reactions", label: "Reactions" },
|
||||
{ key: "messages", label: "Messages" },
|
||||
{ key: "pins", label: "Pins" },
|
||||
{ key: "memberInfo", label: "Member info" },
|
||||
{ key: "emojiList", label: "Emoji list" },
|
||||
] satisfies Array<{ key: keyof SlackActionForm; label: string }>;
|
||||
|
||||
export type ConnectionsProps = {
|
||||
connected: boolean;
|
||||
loading: boolean;
|
||||
@@ -1347,3 +1355,4 @@ function renderProvider(
|
||||
return nothing;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user