ChannelMessage.sender was used both for display (username) and as the reply target in Channel::send(). For Telegram, sender is the username (e.g. "unknown") while send() requires the numeric chat_id, causing "Bad Request: chat not found" errors. Add a dedicated reply_to field to ChannelMessage that stores the channel-specific reply address (Telegram chat_id, Discord channel_id, Slack channel, etc.). Update all channel implementations and dispatch code to use reply_to for send/start_typing/stop_typing calls. This also fixes the same latent bug in Discord and Slack channels where sender (user ID) was incorrectly passed as the reply target.
129 lines
3.9 KiB
Rust
129 lines
3.9 KiB
Rust
//! Example: Implementing a custom Channel for ZeroClaw
|
|
//!
|
|
//! Channels let ZeroClaw communicate through any messaging platform.
|
|
//! Implement the Channel trait, register it, and the agent works everywhere.
|
|
|
|
use anyhow::Result;
|
|
use async_trait::async_trait;
|
|
use tokio::sync::mpsc;
|
|
|
|
/// Mirrors src/channels/traits.rs
|
|
#[derive(Debug, Clone)]
|
|
pub struct ChannelMessage {
|
|
pub id: String,
|
|
pub sender: String,
|
|
/// Channel-specific reply address (e.g. Telegram chat_id, Discord channel_id).
|
|
pub reply_to: String,
|
|
pub content: String,
|
|
pub channel: String,
|
|
pub timestamp: u64,
|
|
}
|
|
|
|
#[async_trait]
|
|
pub trait Channel: Send + Sync {
|
|
fn name(&self) -> &str;
|
|
async fn send(&self, message: &str, recipient: &str) -> Result<()>;
|
|
async fn listen(&self, tx: mpsc::Sender<ChannelMessage>) -> Result<()>;
|
|
async fn health_check(&self) -> bool;
|
|
}
|
|
|
|
/// Example: Telegram channel via Bot API
|
|
pub struct TelegramChannel {
|
|
bot_token: String,
|
|
allowed_users: Vec<String>,
|
|
client: reqwest::Client,
|
|
}
|
|
|
|
impl TelegramChannel {
|
|
pub fn new(bot_token: &str, allowed_users: Vec<String>) -> Self {
|
|
Self {
|
|
bot_token: bot_token.to_string(),
|
|
allowed_users,
|
|
client: reqwest::Client::new(),
|
|
}
|
|
}
|
|
|
|
fn api_url(&self, method: &str) -> String {
|
|
format!("https://api.telegram.org/bot{}/{method}", self.bot_token)
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl Channel for TelegramChannel {
|
|
fn name(&self) -> &str {
|
|
"telegram"
|
|
}
|
|
|
|
async fn send(&self, message: &str, chat_id: &str) -> Result<()> {
|
|
self.client
|
|
.post(self.api_url("sendMessage"))
|
|
.json(&serde_json::json!({
|
|
"chat_id": chat_id,
|
|
"text": message,
|
|
"parse_mode": "Markdown",
|
|
}))
|
|
.send()
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn listen(&self, tx: mpsc::Sender<ChannelMessage>) -> Result<()> {
|
|
let mut offset: i64 = 0;
|
|
|
|
loop {
|
|
let resp = self
|
|
.client
|
|
.get(self.api_url("getUpdates"))
|
|
.query(&[("offset", offset.to_string()), ("timeout", "30".into())])
|
|
.send()
|
|
.await?
|
|
.json::<serde_json::Value>()
|
|
.await?;
|
|
|
|
if let Some(updates) = resp["result"].as_array() {
|
|
for update in updates {
|
|
if let Some(msg) = update.get("message") {
|
|
let sender = msg["from"]["username"]
|
|
.as_str()
|
|
.unwrap_or("unknown")
|
|
.to_string();
|
|
|
|
if !self.allowed_users.is_empty() && !self.allowed_users.contains(&sender) {
|
|
continue;
|
|
}
|
|
|
|
let chat_id = msg["chat"]["id"].to_string();
|
|
|
|
let channel_msg = ChannelMessage {
|
|
id: msg["message_id"].to_string(),
|
|
sender,
|
|
reply_to: chat_id,
|
|
content: msg["text"].as_str().unwrap_or("").to_string(),
|
|
channel: "telegram".into(),
|
|
timestamp: msg["date"].as_u64().unwrap_or(0),
|
|
};
|
|
|
|
if tx.send(channel_msg).await.is_err() {
|
|
return Ok(());
|
|
}
|
|
}
|
|
offset = update["update_id"].as_i64().unwrap_or(offset) + 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn health_check(&self) -> bool {
|
|
self.client
|
|
.get(self.api_url("getMe"))
|
|
.send()
|
|
.await
|
|
.map(|r| r.status().is_success())
|
|
.unwrap_or(false)
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
println!("This is an example — see CONTRIBUTING.md for integration steps.");
|
|
println!("Add your channel config to ChannelsConfig in src/config/schema.rs");
|
|
}
|