mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
Rename examples/ to tools-src/
Update doc references in CLAUDE.md and slack README. Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
9d156411fc
commit
f34a80191e
@@ -0,0 +1,2 @@
|
||||
Cargo.lock
|
||||
target/
|
||||
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "slack-tool"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Slack integration tool for IronClaw (WASM component)"
|
||||
license = "MIT OR Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
wit-bindgen = "0.36"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
[profile.release]
|
||||
opt-level = "s"
|
||||
lto = true
|
||||
strip = true
|
||||
codegen-units = 1
|
||||
@@ -0,0 +1,238 @@
|
||||
# Slack WASM Tool
|
||||
|
||||
A standalone WASM component that provides Slack integration for IronClaw. This serves as both a functional tool and a template for building custom WASM tools.
|
||||
|
||||
## Features
|
||||
|
||||
- **send_message**: Send messages to channels or threads
|
||||
- **list_channels**: List channels the bot has access to
|
||||
- **get_channel_history**: Retrieve recent messages from a channel
|
||||
- **post_reaction**: Add emoji reactions to messages
|
||||
- **get_user_info**: Get information about Slack users
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Rust toolchain** with WASM target:
|
||||
```bash
|
||||
rustup target add wasm32-wasip2
|
||||
```
|
||||
|
||||
2. **cargo-component** for building WASM components:
|
||||
```bash
|
||||
cargo install cargo-component
|
||||
```
|
||||
|
||||
3. **Slack Bot Token** with the following OAuth scopes:
|
||||
- `chat:write` - Send messages
|
||||
- `channels:read` - List public channels
|
||||
- `channels:history` - Read channel history
|
||||
- `groups:read` - List private channels
|
||||
- `groups:history` - Read private channel history
|
||||
- `reactions:write` - Add reactions
|
||||
- `users:read` - Get user information
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
cd tools-src/wasm-tools/slack
|
||||
cargo component build --release
|
||||
```
|
||||
|
||||
The compiled WASM component will be at:
|
||||
```
|
||||
target/wasm32-wasip2/release/slack_tool.wasm
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
### Option A: File-based (Development)
|
||||
|
||||
Copy the WASM and capabilities files to the agent's tools directory:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.ironclaw/tools
|
||||
cp target/wasm32-wasip2/release/slack_tool.wasm ~/.ironclaw/tools/slack.wasm
|
||||
cp slack.capabilities.json ~/.ironclaw/tools/
|
||||
```
|
||||
|
||||
### Option B: Database Storage (Production)
|
||||
|
||||
Use the agent CLI or API to store the tool:
|
||||
|
||||
```bash
|
||||
ironclaw tool install \
|
||||
--name slack \
|
||||
--wasm target/wasm32-wasip2/release/slack_tool.wasm \
|
||||
--capabilities slack.capabilities.json
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Store your Slack bot token as a secret:
|
||||
|
||||
```bash
|
||||
ironclaw secret set slack_bot_token "xoxb-your-token-here"
|
||||
```
|
||||
|
||||
Or via SQL:
|
||||
```sql
|
||||
INSERT INTO secrets (user_id, name, encrypted_value, key_salt)
|
||||
VALUES ('your_user_id', 'slack_bot_token', ...);
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Send a Message
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "send_message",
|
||||
"channel": "#general",
|
||||
"text": "Hello from IronClaw!"
|
||||
}
|
||||
```
|
||||
|
||||
### Reply in a Thread
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "send_message",
|
||||
"channel": "C1234567890",
|
||||
"text": "This is a thread reply",
|
||||
"thread_ts": "1234567890.123456"
|
||||
}
|
||||
```
|
||||
|
||||
### List Channels
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "list_channels",
|
||||
"limit": 50
|
||||
}
|
||||
```
|
||||
|
||||
### Get Channel History
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "get_channel_history",
|
||||
"channel": "C1234567890",
|
||||
"limit": 10
|
||||
}
|
||||
```
|
||||
|
||||
### Add a Reaction
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "post_reaction",
|
||||
"channel": "C1234567890",
|
||||
"timestamp": "1234567890.123456",
|
||||
"emoji": "thumbsup"
|
||||
}
|
||||
```
|
||||
|
||||
### Get User Info
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "get_user_info",
|
||||
"user_id": "U1234567890"
|
||||
}
|
||||
```
|
||||
|
||||
## Security Model
|
||||
|
||||
This tool runs in a sandboxed WASM environment with strict capability controls:
|
||||
|
||||
1. **HTTP Allowlist**: Can only access `slack.com/api/*`
|
||||
2. **Credential Injection**: The bot token is injected by the host runtime; the WASM code never sees it
|
||||
3. **Rate Limiting**: 50 requests/minute, 1000 requests/hour
|
||||
4. **No Filesystem Access**: Cannot read/write files except through workspace capability
|
||||
5. **No Network Access**: Beyond the allowlisted endpoints
|
||||
|
||||
## Capabilities File
|
||||
|
||||
The `slack.capabilities.json` file declares what this tool needs:
|
||||
|
||||
```json
|
||||
{
|
||||
"http": {
|
||||
"allowlist": [
|
||||
{ "host": "slack.com", "path_prefix": "/api/", "methods": ["GET", "POST"] }
|
||||
],
|
||||
"credentials": {
|
||||
"slack_bot_token": {
|
||||
"secret_name": "slack_bot_token",
|
||||
"location": { "type": "bearer" },
|
||||
"host_patterns": ["slack.com"]
|
||||
}
|
||||
},
|
||||
"rate_limit": { "requests_per_minute": 50, "requests_per_hour": 1000 }
|
||||
},
|
||||
"secrets": {
|
||||
"allowed_names": ["slack_bot_token"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Building Your Own Tool
|
||||
|
||||
Use this as a template for creating new WASM tools:
|
||||
|
||||
1. Copy this directory
|
||||
2. Update `Cargo.toml` with your tool name
|
||||
3. Modify `src/types.rs` with your action types
|
||||
4. Implement API calls in `src/api.rs`
|
||||
5. Update the action dispatch in `src/lib.rs`
|
||||
6. Create your `*.capabilities.json` file
|
||||
7. Build with `cargo component build --release`
|
||||
|
||||
### Key Files
|
||||
|
||||
- `Cargo.toml` - Rust package config with WASM target
|
||||
- `src/lib.rs` - WIT bindings and main dispatch
|
||||
- `src/types.rs` - Request/response types
|
||||
- `src/api.rs` - API implementation
|
||||
- `*.capabilities.json` - Security capabilities declaration
|
||||
|
||||
### WIT Interface
|
||||
|
||||
Tools implement the `sandboxed-tool` world from `wit/tool.wit`:
|
||||
|
||||
```wit
|
||||
world sandboxed-tool {
|
||||
import host; // log, http-request, secret-exists, etc.
|
||||
export tool; // execute, schema, description
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Slack bot token not configured"
|
||||
|
||||
Ensure you've stored the secret:
|
||||
```bash
|
||||
ironclaw secret set slack_bot_token "xoxb-..."
|
||||
```
|
||||
|
||||
### "Endpoint not in allowlist"
|
||||
|
||||
Check that `slack.capabilities.json` includes the endpoint you're trying to access.
|
||||
|
||||
### "Rate limit exceeded"
|
||||
|
||||
The tool has a default rate limit of 50 requests/minute. Wait and retry.
|
||||
|
||||
### Build errors
|
||||
|
||||
Ensure you have the WASM target and cargo-component installed:
|
||||
```bash
|
||||
rustup target add wasm32-wasip2
|
||||
cargo install cargo-component
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT OR Apache-2.0
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"http": {
|
||||
"allowlist": [
|
||||
{
|
||||
"host": "slack.com",
|
||||
"path_prefix": "/api/",
|
||||
"methods": ["GET", "POST"]
|
||||
}
|
||||
],
|
||||
"credentials": {
|
||||
"slack_bot_token": {
|
||||
"secret_name": "slack_bot_token",
|
||||
"location": { "type": "bearer" },
|
||||
"host_patterns": ["slack.com"]
|
||||
}
|
||||
},
|
||||
"rate_limit": {
|
||||
"requests_per_minute": 50,
|
||||
"requests_per_hour": 1000
|
||||
},
|
||||
"timeout_secs": 30
|
||||
},
|
||||
"secrets": {
|
||||
"allowed_names": ["slack_bot_token"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
//! Slack Web API implementation.
|
||||
//!
|
||||
//! All API calls go through the host's HTTP capability, which handles
|
||||
//! credential injection and rate limiting. The WASM tool never sees
|
||||
//! the actual bot token.
|
||||
|
||||
use crate::near::agent::host;
|
||||
use crate::types::*;
|
||||
|
||||
const SLACK_API_BASE: &str = "https://slack.com/api";
|
||||
|
||||
/// Make a Slack API call.
|
||||
fn slack_api_call(method: &str, endpoint: &str, body: Option<&str>) -> Result<String, String> {
|
||||
let url = format!("{}/{}", SLACK_API_BASE, endpoint);
|
||||
|
||||
// Content-Type header for POST requests
|
||||
let headers = if body.is_some() {
|
||||
r#"{"Content-Type": "application/json; charset=utf-8"}"#
|
||||
} else {
|
||||
"{}"
|
||||
};
|
||||
|
||||
let body_bytes = body.map(|b| b.as_bytes().to_vec());
|
||||
|
||||
host::log(host::LogLevel::Debug, &format!("Slack API: {} {}", method, endpoint));
|
||||
|
||||
let response = host::http_request(method, &url, headers, body_bytes.as_deref())?;
|
||||
|
||||
if response.status < 200 || response.status >= 300 {
|
||||
return Err(format!(
|
||||
"Slack API returned status {}: {}",
|
||||
response.status,
|
||||
String::from_utf8_lossy(&response.body)
|
||||
));
|
||||
}
|
||||
|
||||
String::from_utf8(response.body).map_err(|e| format!("Invalid UTF-8 in response: {}", e))
|
||||
}
|
||||
|
||||
/// Send a message to a Slack channel.
|
||||
pub fn send_message(
|
||||
channel: &str,
|
||||
text: &str,
|
||||
thread_ts: Option<&str>,
|
||||
) -> Result<SendMessageResult, String> {
|
||||
let mut payload = serde_json::json!({
|
||||
"channel": channel,
|
||||
"text": text,
|
||||
});
|
||||
|
||||
if let Some(ts) = thread_ts {
|
||||
payload["thread_ts"] = serde_json::Value::String(ts.to_string());
|
||||
}
|
||||
|
||||
let body = serde_json::to_string(&payload).map_err(|e| e.to_string())?;
|
||||
let response = slack_api_call("POST", "chat.postMessage", Some(&body))?;
|
||||
|
||||
let parsed: serde_json::Value =
|
||||
serde_json::from_str(&response).map_err(|e| format!("Failed to parse response: {}", e))?;
|
||||
|
||||
if !parsed["ok"].as_bool().unwrap_or(false) {
|
||||
let error = parsed["error"].as_str().unwrap_or("unknown_error");
|
||||
return Err(format!("Slack API error: {}", error));
|
||||
}
|
||||
|
||||
Ok(SendMessageResult {
|
||||
ok: true,
|
||||
channel: parsed["channel"].as_str().unwrap_or(channel).to_string(),
|
||||
ts: parsed["ts"].as_str().unwrap_or("").to_string(),
|
||||
message: parsed.get("message").map(|m| MessageInfo {
|
||||
text: m["text"].as_str().unwrap_or("").to_string(),
|
||||
user: m["user"].as_str().map(|s| s.to_string()),
|
||||
ts: m["ts"].as_str().unwrap_or("").to_string(),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
/// List channels the bot has access to.
|
||||
pub fn list_channels(limit: u32) -> Result<ListChannelsResult, String> {
|
||||
let url = format!(
|
||||
"conversations.list?types=public_channel,private_channel&limit={}",
|
||||
limit
|
||||
);
|
||||
|
||||
let response = slack_api_call("GET", &url, None)?;
|
||||
|
||||
let parsed: serde_json::Value =
|
||||
serde_json::from_str(&response).map_err(|e| format!("Failed to parse response: {}", e))?;
|
||||
|
||||
if !parsed["ok"].as_bool().unwrap_or(false) {
|
||||
let error = parsed["error"].as_str().unwrap_or("unknown_error");
|
||||
return Err(format!("Slack API error: {}", error));
|
||||
}
|
||||
|
||||
let channels = parsed["channels"]
|
||||
.as_array()
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.map(|c| Channel {
|
||||
id: c["id"].as_str().unwrap_or("").to_string(),
|
||||
name: c["name"].as_str().unwrap_or("").to_string(),
|
||||
is_private: c["is_private"].as_bool().unwrap_or(false),
|
||||
is_member: c["is_member"].as_bool().unwrap_or(false),
|
||||
topic: c["topic"]["value"].as_str().map(|s| s.to_string()),
|
||||
purpose: c["purpose"]["value"].as_str().map(|s| s.to_string()),
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(ListChannelsResult { ok: true, channels })
|
||||
}
|
||||
|
||||
/// Get message history from a channel.
|
||||
pub fn get_channel_history(channel: &str, limit: u32) -> Result<ChannelHistoryResult, String> {
|
||||
let url = format!("conversations.history?channel={}&limit={}", channel, limit);
|
||||
|
||||
let response = slack_api_call("GET", &url, None)?;
|
||||
|
||||
let parsed: serde_json::Value =
|
||||
serde_json::from_str(&response).map_err(|e| format!("Failed to parse response: {}", e))?;
|
||||
|
||||
if !parsed["ok"].as_bool().unwrap_or(false) {
|
||||
let error = parsed["error"].as_str().unwrap_or("unknown_error");
|
||||
return Err(format!("Slack API error: {}", error));
|
||||
}
|
||||
|
||||
let messages = parsed["messages"]
|
||||
.as_array()
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.map(|m| HistoryMessage {
|
||||
ts: m["ts"].as_str().unwrap_or("").to_string(),
|
||||
text: m["text"].as_str().unwrap_or("").to_string(),
|
||||
user: m["user"].as_str().map(|s| s.to_string()),
|
||||
msg_type: m["type"].as_str().unwrap_or("message").to_string(),
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(ChannelHistoryResult { ok: true, messages })
|
||||
}
|
||||
|
||||
/// Add a reaction to a message.
|
||||
pub fn post_reaction(channel: &str, timestamp: &str, emoji: &str) -> Result<PostReactionResult, String> {
|
||||
let payload = serde_json::json!({
|
||||
"channel": channel,
|
||||
"timestamp": timestamp,
|
||||
"name": emoji,
|
||||
});
|
||||
|
||||
let body = serde_json::to_string(&payload).map_err(|e| e.to_string())?;
|
||||
let response = slack_api_call("POST", "reactions.add", Some(&body))?;
|
||||
|
||||
let parsed: serde_json::Value =
|
||||
serde_json::from_str(&response).map_err(|e| format!("Failed to parse response: {}", e))?;
|
||||
|
||||
if !parsed["ok"].as_bool().unwrap_or(false) {
|
||||
let error = parsed["error"].as_str().unwrap_or("unknown_error");
|
||||
// "already_reacted" is not really an error
|
||||
if error != "already_reacted" {
|
||||
return Err(format!("Slack API error: {}", error));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(PostReactionResult { ok: true })
|
||||
}
|
||||
|
||||
/// Get information about a user.
|
||||
pub fn get_user_info(user_id: &str) -> Result<GetUserInfoResult, String> {
|
||||
let url = format!("users.info?user={}", user_id);
|
||||
|
||||
let response = slack_api_call("GET", &url, None)?;
|
||||
|
||||
let parsed: serde_json::Value =
|
||||
serde_json::from_str(&response).map_err(|e| format!("Failed to parse response: {}", e))?;
|
||||
|
||||
if !parsed["ok"].as_bool().unwrap_or(false) {
|
||||
let error = parsed["error"].as_str().unwrap_or("unknown_error");
|
||||
return Err(format!("Slack API error: {}", error));
|
||||
}
|
||||
|
||||
let user = &parsed["user"];
|
||||
let profile = &user["profile"];
|
||||
|
||||
Ok(GetUserInfoResult {
|
||||
ok: true,
|
||||
user: UserInfo {
|
||||
id: user["id"].as_str().unwrap_or("").to_string(),
|
||||
name: user["name"].as_str().unwrap_or("").to_string(),
|
||||
real_name: profile["real_name"].as_str().map(|s| s.to_string()),
|
||||
display_name: profile["display_name"].as_str().map(|s| s.to_string()),
|
||||
email: profile["email"].as_str().map(|s| s.to_string()),
|
||||
is_bot: user["is_bot"].as_bool().unwrap_or(false),
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
//! Slack WASM Tool for IronClaw.
|
||||
//!
|
||||
//! This is a standalone WASM component that provides Slack integration.
|
||||
//! It demonstrates how to build external tools that can be dynamically
|
||||
//! loaded by the agent runtime.
|
||||
//!
|
||||
//! # Capabilities Required
|
||||
//!
|
||||
//! - HTTP: `slack.com/api/*` (GET, POST)
|
||||
//! - Secrets: `slack_bot_token` (injected automatically)
|
||||
//!
|
||||
//! # Supported Actions
|
||||
//!
|
||||
//! - `send_message`: Send a message to a channel
|
||||
//! - `list_channels`: List channels the bot has access to
|
||||
//! - `get_channel_history`: Get recent messages from a channel
|
||||
//! - `post_reaction`: Add an emoji reaction to a message
|
||||
//! - `get_user_info`: Get information about a Slack user
|
||||
//!
|
||||
//! # Example Usage
|
||||
//!
|
||||
//! ```json
|
||||
//! {"action": "send_message", "channel": "#general", "text": "Hello from the agent!"}
|
||||
//! ```
|
||||
|
||||
mod api;
|
||||
mod types;
|
||||
|
||||
use types::SlackAction;
|
||||
|
||||
// Generate bindings from the WIT interface.
|
||||
// This creates the `bindings` module with types and traits.
|
||||
wit_bindgen::generate!({
|
||||
world: "sandboxed-tool",
|
||||
path: "../../../wit/tool.wit",
|
||||
});
|
||||
|
||||
/// Implementation of the tool interface.
|
||||
struct SlackTool;
|
||||
|
||||
impl exports::near::agent::tool::Guest for SlackTool {
|
||||
fn execute(req: exports::near::agent::tool::Request) -> exports::near::agent::tool::Response {
|
||||
match execute_inner(&req.params) {
|
||||
Ok(result) => exports::near::agent::tool::Response {
|
||||
output: Some(result),
|
||||
error: None,
|
||||
},
|
||||
Err(e) => exports::near::agent::tool::Response {
|
||||
output: None,
|
||||
error: Some(e),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn schema() -> String {
|
||||
// JSON Schema for the tool's parameters
|
||||
r#"{
|
||||
"type": "object",
|
||||
"required": ["action"],
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"action": { "const": "send_message" },
|
||||
"channel": {
|
||||
"type": "string",
|
||||
"description": "Channel ID or name (e.g., '#general' or 'C1234567890')"
|
||||
},
|
||||
"text": {
|
||||
"type": "string",
|
||||
"description": "Message text (supports Slack mrkdwn formatting)"
|
||||
},
|
||||
"thread_ts": {
|
||||
"type": "string",
|
||||
"description": "Optional thread timestamp to reply in a thread"
|
||||
}
|
||||
},
|
||||
"required": ["action", "channel", "text"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": { "const": "list_channels" },
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximum number of channels to return (default: 100)",
|
||||
"default": 100
|
||||
}
|
||||
},
|
||||
"required": ["action"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": { "const": "get_channel_history" },
|
||||
"channel": {
|
||||
"type": "string",
|
||||
"description": "Channel ID (e.g., 'C1234567890')"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximum number of messages to return (default: 20)",
|
||||
"default": 20
|
||||
}
|
||||
},
|
||||
"required": ["action", "channel"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": { "const": "post_reaction" },
|
||||
"channel": {
|
||||
"type": "string",
|
||||
"description": "Channel ID containing the message"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"description": "Timestamp of the message to react to"
|
||||
},
|
||||
"emoji": {
|
||||
"type": "string",
|
||||
"description": "Emoji name without colons (e.g., 'thumbsup')"
|
||||
}
|
||||
},
|
||||
"required": ["action", "channel", "timestamp", "emoji"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": { "const": "get_user_info" },
|
||||
"user_id": {
|
||||
"type": "string",
|
||||
"description": "User ID (e.g., 'U1234567890')"
|
||||
}
|
||||
},
|
||||
"required": ["action", "user_id"]
|
||||
}
|
||||
]
|
||||
}"#
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn description() -> String {
|
||||
"Slack integration tool for sending messages, listing channels, reading history, \
|
||||
adding reactions, and getting user information. Requires a Slack bot token with \
|
||||
appropriate scopes (chat:write, channels:read, channels:history, reactions:write, \
|
||||
users:read)."
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Inner execution logic with proper error handling.
|
||||
fn execute_inner(params: &str) -> Result<String, String> {
|
||||
// Check if the Slack token is configured
|
||||
if !crate::near::agent::host::secret_exists("slack_bot_token") {
|
||||
return Err(
|
||||
"Slack bot token not configured. Please add the 'slack_bot_token' secret.".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
// Parse the action from JSON
|
||||
let action: SlackAction =
|
||||
serde_json::from_str(params).map_err(|e| format!("Invalid parameters: {}", e))?;
|
||||
|
||||
crate::near::agent::host::log(
|
||||
crate::near::agent::host::LogLevel::Info,
|
||||
&format!("Executing Slack action: {:?}", action),
|
||||
);
|
||||
|
||||
// Dispatch to the appropriate handler
|
||||
let result = match action {
|
||||
SlackAction::SendMessage {
|
||||
channel,
|
||||
text,
|
||||
thread_ts,
|
||||
} => {
|
||||
let result = api::send_message(&channel, &text, thread_ts.as_deref())?;
|
||||
serde_json::to_string(&result).map_err(|e| e.to_string())?
|
||||
}
|
||||
|
||||
SlackAction::ListChannels { limit } => {
|
||||
let result = api::list_channels(limit)?;
|
||||
serde_json::to_string(&result).map_err(|e| e.to_string())?
|
||||
}
|
||||
|
||||
SlackAction::GetChannelHistory { channel, limit } => {
|
||||
let result = api::get_channel_history(&channel, limit)?;
|
||||
serde_json::to_string(&result).map_err(|e| e.to_string())?
|
||||
}
|
||||
|
||||
SlackAction::PostReaction {
|
||||
channel,
|
||||
timestamp,
|
||||
emoji,
|
||||
} => {
|
||||
let result = api::post_reaction(&channel, ×tamp, &emoji)?;
|
||||
serde_json::to_string(&result).map_err(|e| e.to_string())?
|
||||
}
|
||||
|
||||
SlackAction::GetUserInfo { user_id } => {
|
||||
let result = api::get_user_info(&user_id)?;
|
||||
serde_json::to_string(&result).map_err(|e| e.to_string())?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
// Export the tool implementation.
|
||||
export!(SlackTool);
|
||||
@@ -0,0 +1,147 @@
|
||||
//! Types for Slack API requests and responses.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Input parameters for the Slack tool.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(tag = "action", rename_all = "snake_case")]
|
||||
pub enum SlackAction {
|
||||
/// Send a message to a channel.
|
||||
SendMessage {
|
||||
/// Channel ID or name (e.g., "#general" or "C1234567890").
|
||||
channel: String,
|
||||
/// Message text (supports Slack mrkdwn formatting).
|
||||
text: String,
|
||||
/// Optional thread timestamp to reply in a thread.
|
||||
#[serde(default)]
|
||||
thread_ts: Option<String>,
|
||||
},
|
||||
|
||||
/// List channels the bot has access to.
|
||||
ListChannels {
|
||||
/// Maximum number of channels to return (default: 100).
|
||||
#[serde(default = "default_limit")]
|
||||
limit: u32,
|
||||
},
|
||||
|
||||
/// Get message history from a channel.
|
||||
GetChannelHistory {
|
||||
/// Channel ID (e.g., "C1234567890").
|
||||
channel: String,
|
||||
/// Maximum number of messages to return (default: 20).
|
||||
#[serde(default = "default_history_limit")]
|
||||
limit: u32,
|
||||
},
|
||||
|
||||
/// Add a reaction (emoji) to a message.
|
||||
PostReaction {
|
||||
/// Channel ID containing the message.
|
||||
channel: String,
|
||||
/// Timestamp of the message to react to.
|
||||
timestamp: String,
|
||||
/// Emoji name without colons (e.g., "thumbsup").
|
||||
emoji: String,
|
||||
},
|
||||
|
||||
/// Get information about a user.
|
||||
GetUserInfo {
|
||||
/// User ID (e.g., "U1234567890").
|
||||
user_id: String,
|
||||
},
|
||||
}
|
||||
|
||||
fn default_limit() -> u32 {
|
||||
100
|
||||
}
|
||||
|
||||
fn default_history_limit() -> u32 {
|
||||
20
|
||||
}
|
||||
|
||||
/// Result from send_message.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct SendMessageResult {
|
||||
pub ok: bool,
|
||||
pub channel: String,
|
||||
pub ts: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub message: Option<MessageInfo>,
|
||||
}
|
||||
|
||||
/// Basic message info.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct MessageInfo {
|
||||
pub text: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub user: Option<String>,
|
||||
pub ts: String,
|
||||
}
|
||||
|
||||
/// A Slack channel.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Channel {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub is_private: bool,
|
||||
pub is_member: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub topic: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub purpose: Option<String>,
|
||||
}
|
||||
|
||||
/// Result from list_channels.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ListChannelsResult {
|
||||
pub ok: bool,
|
||||
pub channels: Vec<Channel>,
|
||||
}
|
||||
|
||||
/// Result from get_channel_history.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ChannelHistoryResult {
|
||||
pub ok: bool,
|
||||
pub messages: Vec<HistoryMessage>,
|
||||
}
|
||||
|
||||
/// A message from channel history.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct HistoryMessage {
|
||||
pub ts: String,
|
||||
pub text: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub user: Option<String>,
|
||||
#[serde(rename = "type")]
|
||||
pub msg_type: String,
|
||||
}
|
||||
|
||||
/// Result from post_reaction.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct PostReactionResult {
|
||||
pub ok: bool,
|
||||
}
|
||||
|
||||
/// User information.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct UserInfo {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub real_name: Option<String>,
|
||||
pub display_name: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub is_bot: bool,
|
||||
}
|
||||
|
||||
/// Result from get_user_info.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct GetUserInfoResult {
|
||||
pub ok: bool,
|
||||
pub user: UserInfo,
|
||||
}
|
||||
|
||||
/// Generic Slack API error response.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SlackApiError {
|
||||
pub ok: bool,
|
||||
pub error: String,
|
||||
}
|
||||
Reference in New Issue
Block a user