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 blogwatcher skill for RSS/Atom monitoring — thanks @Hyaxia.
|
||||||
- Skills: add Notion API skill — thanks @scald.
|
- 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.
|
- 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 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.
|
- 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.
|
- 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
|
- **Peter Steinberger** - Benevolent Dictator
|
||||||
- GitHub: [@steipete](https://github.com/steipete) · X: [@steipete](https://x.com/steipete)
|
- 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)
|
- GitHub: [@4shadowed](https://github.com/4shadowed) · X: [@4shad0wed](https://x.com/4shad0wed)
|
||||||
|
|
||||||
- **Jos** - Telegram, API, Nix mode
|
- **Jos** - Telegram, API, Nix mode
|
||||||
|
|||||||
@@ -292,6 +292,64 @@ Reaction notification modes:
|
|||||||
- `all`: all reactions on all messages.
|
- `all`: all reactions on all messages.
|
||||||
- `allowlist`: reactions from `guilds.<id>.users` on all messages (empty list disables).
|
- `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)
|
### `imessage` (imsg CLI)
|
||||||
|
|
||||||
Clawdis spawns `imsg rpc` (JSON-RPC over stdio). No daemon or port required.
|
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"] },
|
"kick": { "label": "kick", "detailKeys": ["guildId", "userId"] },
|
||||||
"ban": { "label": "ban", "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[]) {
|
function setWhatsAppAllowFrom(cfg: ClawdisConfig, allowFrom?: string[]) {
|
||||||
return {
|
return {
|
||||||
...cfg,
|
...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")) {
|
if (selection.includes("signal")) {
|
||||||
let resolvedCliPath = signalCliPath;
|
let resolvedCliPath = signalCliPath;
|
||||||
let cliDetected = signalCliDetected;
|
let cliDetected = signalCliDetected;
|
||||||
@@ -550,3 +724,4 @@ export async function setupProviders(
|
|||||||
|
|
||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ const GROUP_LABELS: Record<string, string> = {
|
|||||||
talk: "Talk",
|
talk: "Talk",
|
||||||
telegram: "Telegram",
|
telegram: "Telegram",
|
||||||
discord: "Discord",
|
discord: "Discord",
|
||||||
|
slack: "Slack",
|
||||||
signal: "Signal",
|
signal: "Signal",
|
||||||
imessage: "iMessage",
|
imessage: "iMessage",
|
||||||
whatsapp: "WhatsApp",
|
whatsapp: "WhatsApp",
|
||||||
@@ -65,6 +66,7 @@ const GROUP_ORDER: Record<string, number> = {
|
|||||||
talk: 130,
|
talk: 130,
|
||||||
telegram: 140,
|
telegram: 140,
|
||||||
discord: 150,
|
discord: 150,
|
||||||
|
slack: 155,
|
||||||
signal: 160,
|
signal: 160,
|
||||||
imessage: 170,
|
imessage: 170,
|
||||||
whatsapp: 180,
|
whatsapp: 180,
|
||||||
@@ -92,6 +94,8 @@ const FIELD_LABELS: Record<string, string> = {
|
|||||||
"talk.apiKey": "Talk API Key",
|
"talk.apiKey": "Talk API Key",
|
||||||
"telegram.botToken": "Telegram Bot Token",
|
"telegram.botToken": "Telegram Bot Token",
|
||||||
"discord.token": "Discord Bot Token",
|
"discord.token": "Discord Bot Token",
|
||||||
|
"slack.botToken": "Slack Bot Token",
|
||||||
|
"slack.appToken": "Slack App Token",
|
||||||
"signal.account": "Signal Account",
|
"signal.account": "Signal Account",
|
||||||
"imessage.cliPath": "iMessage CLI Path",
|
"imessage.cliPath": "iMessage CLI Path",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ export type HookMappingConfig = {
|
|||||||
| "whatsapp"
|
| "whatsapp"
|
||||||
| "telegram"
|
| "telegram"
|
||||||
| "discord"
|
| "discord"
|
||||||
|
| "slack"
|
||||||
| "signal"
|
| "signal"
|
||||||
| "imessage";
|
| "imessage";
|
||||||
to?: string;
|
to?: string;
|
||||||
@@ -290,6 +291,64 @@ export type DiscordConfig = {
|
|||||||
guilds?: Record<string, DiscordGuildEntry>;
|
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 = {
|
export type SignalConfig = {
|
||||||
/** If false, do not start the Signal provider. Default: true. */
|
/** If false, do not start the Signal provider. Default: true. */
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
@@ -356,6 +415,7 @@ export type QueueModeBySurface = {
|
|||||||
whatsapp?: QueueMode;
|
whatsapp?: QueueMode;
|
||||||
telegram?: QueueMode;
|
telegram?: QueueMode;
|
||||||
discord?: QueueMode;
|
discord?: QueueMode;
|
||||||
|
slack?: QueueMode;
|
||||||
signal?: QueueMode;
|
signal?: QueueMode;
|
||||||
imessage?: QueueMode;
|
imessage?: QueueMode;
|
||||||
webchat?: QueueMode;
|
webchat?: QueueMode;
|
||||||
@@ -642,6 +702,7 @@ export type ClawdisConfig = {
|
|||||||
| "whatsapp"
|
| "whatsapp"
|
||||||
| "telegram"
|
| "telegram"
|
||||||
| "discord"
|
| "discord"
|
||||||
|
| "slack"
|
||||||
| "signal"
|
| "signal"
|
||||||
| "imessage"
|
| "imessage"
|
||||||
| "none";
|
| "none";
|
||||||
@@ -731,6 +792,7 @@ export type ClawdisConfig = {
|
|||||||
whatsapp?: WhatsAppConfig;
|
whatsapp?: WhatsAppConfig;
|
||||||
telegram?: TelegramConfig;
|
telegram?: TelegramConfig;
|
||||||
discord?: DiscordConfig;
|
discord?: DiscordConfig;
|
||||||
|
slack?: SlackConfig;
|
||||||
signal?: SignalConfig;
|
signal?: SignalConfig;
|
||||||
imessage?: IMessageConfig;
|
imessage?: IMessageConfig;
|
||||||
cron?: CronConfig;
|
cron?: CronConfig;
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ const QueueModeBySurfaceSchema = z
|
|||||||
whatsapp: QueueModeSchema.optional(),
|
whatsapp: QueueModeSchema.optional(),
|
||||||
telegram: QueueModeSchema.optional(),
|
telegram: QueueModeSchema.optional(),
|
||||||
discord: QueueModeSchema.optional(),
|
discord: QueueModeSchema.optional(),
|
||||||
|
slack: QueueModeSchema.optional(),
|
||||||
signal: QueueModeSchema.optional(),
|
signal: QueueModeSchema.optional(),
|
||||||
imessage: QueueModeSchema.optional(),
|
imessage: QueueModeSchema.optional(),
|
||||||
webchat: QueueModeSchema.optional(),
|
webchat: QueueModeSchema.optional(),
|
||||||
@@ -163,6 +164,7 @@ const HeartbeatSchema = z
|
|||||||
z.literal("whatsapp"),
|
z.literal("whatsapp"),
|
||||||
z.literal("telegram"),
|
z.literal("telegram"),
|
||||||
z.literal("discord"),
|
z.literal("discord"),
|
||||||
|
z.literal("slack"),
|
||||||
z.literal("signal"),
|
z.literal("signal"),
|
||||||
z.literal("imessage"),
|
z.literal("imessage"),
|
||||||
z.literal("none"),
|
z.literal("none"),
|
||||||
@@ -225,6 +227,7 @@ const HookMappingSchema = z
|
|||||||
z.literal("whatsapp"),
|
z.literal("whatsapp"),
|
||||||
z.literal("telegram"),
|
z.literal("telegram"),
|
||||||
z.literal("discord"),
|
z.literal("discord"),
|
||||||
|
z.literal("slack"),
|
||||||
z.literal("signal"),
|
z.literal("signal"),
|
||||||
z.literal("imessage"),
|
z.literal("imessage"),
|
||||||
])
|
])
|
||||||
@@ -619,6 +622,59 @@ export const ClawdisSchema = z.object({
|
|||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
.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
|
signal: z
|
||||||
.object({
|
.object({
|
||||||
enabled: z.boolean().optional(),
|
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,
|
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 = {
|
state.signalForm = {
|
||||||
enabled: typeof signal.enabled === "boolean" ? signal.enabled : true,
|
enabled: typeof signal.enabled === "boolean" ? signal.enabled : true,
|
||||||
account: typeof signal.account === "string" ? signal.account : "",
|
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;
|
const configInvalid = snapshot.valid === false ? "Config invalid." : null;
|
||||||
state.telegramConfigStatus = configInvalid;
|
state.telegramConfigStatus = configInvalid;
|
||||||
state.discordConfigStatus = configInvalid;
|
state.discordConfigStatus = configInvalid;
|
||||||
|
state.slackConfigStatus = configInvalid;
|
||||||
state.signalConfigStatus = configInvalid;
|
state.signalConfigStatus = configInvalid;
|
||||||
state.imessageConfigStatus = configInvalid;
|
state.imessageConfigStatus = configInvalid;
|
||||||
|
|
||||||
@@ -405,3 +492,4 @@ function removePathValue(
|
|||||||
delete (current as Record<string, unknown>)[lastKey];
|
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) {
|
export async function saveSignalConfig(state: ConnectionsState) {
|
||||||
if (!state.client || !state.connected) return;
|
if (!state.client || !state.connected) return;
|
||||||
if (state.signalSaving) return;
|
if (state.signalSaving) return;
|
||||||
@@ -529,3 +670,4 @@ export async function saveIMessageConfig(state: ConnectionsState) {
|
|||||||
state.imessageSaving = false;
|
state.imessageSaving = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,41 @@ export type DiscordActionForm = {
|
|||||||
moderation: boolean;
|
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 = {
|
export const defaultDiscordActions: DiscordActionForm = {
|
||||||
reactions: true,
|
reactions: true,
|
||||||
stickers: true,
|
stickers: true,
|
||||||
@@ -78,6 +113,14 @@ export const defaultDiscordActions: DiscordActionForm = {
|
|||||||
moderation: false,
|
moderation: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const defaultSlackActions: SlackActionForm = {
|
||||||
|
reactions: true,
|
||||||
|
messages: true,
|
||||||
|
pins: true,
|
||||||
|
memberInfo: true,
|
||||||
|
emojiList: true,
|
||||||
|
};
|
||||||
|
|
||||||
export type SignalForm = {
|
export type SignalForm = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
account: string;
|
account: string;
|
||||||
@@ -125,3 +168,4 @@ export type CronFormState = {
|
|||||||
timeoutSeconds: string;
|
timeoutSeconds: string;
|
||||||
postToMainPrefix: string;
|
postToMainPrefix: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,14 @@ const discordActionOptions = [
|
|||||||
{ key: "moderation", label: "Moderation" },
|
{ key: "moderation", label: "Moderation" },
|
||||||
] satisfies Array<{ key: keyof DiscordActionForm; label: string }>;
|
] 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 = {
|
export type ConnectionsProps = {
|
||||||
connected: boolean;
|
connected: boolean;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
@@ -1347,3 +1355,4 @@ function renderProvider(
|
|||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user