Files
optimclaw/examples/wasm-tools/slack/src/api.rs
T
Illia PolosukhinandClaude Opus 4.5 4ae59ef52c Fix TuiChannel integration and enable in main.rs
- Store event_tx in Arc<Mutex<>> so respond() can send to TUI
- Fix run_event_loop to take owned receiver
- Switch main.rs from SimpleCliChannel to TuiChannel
- Add proper terminal cleanup on TUI error

Co-Authored-By: Claude Opus 4.5 <[email protected]>
2026-02-02 23:47:41 -08:00

199 lines
6.9 KiB
Rust

//! 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::bindings::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),
},
})
}