diff --git a/src/channels/mod.rs b/src/channels/mod.rs index e74f631..9a8e75a 100644 --- a/src/channels/mod.rs +++ b/src/channels/mod.rs @@ -250,7 +250,10 @@ async fn process_channel_message(ctx: Arc, msg: traits::C ); if let Some(channel) = target_channel.as_ref() { let _ = channel - .send(&SendMessage::new(format!("⚠️ Error: {e}"), &msg.reply_target)) + .send(&SendMessage::new( + format!("⚠️ Error: {e}"), + &msg.reply_target, + )) .await; } } diff --git a/src/channels/qq.rs b/src/channels/qq.rs index 814288d..3391fd7 100644 --- a/src/channels/qq.rs +++ b/src/channels/qq.rs @@ -1,4 +1,4 @@ -use super::traits::{Channel, ChannelMessage}; +use super::traits::{Channel, ChannelMessage, SendMessage}; use async_trait::async_trait; use futures_util::{SinkExt, StreamExt}; use serde_json::json; @@ -162,25 +162,28 @@ impl Channel for QQChannel { "qq" } - async fn send(&self, message: &str, recipient: &str) -> anyhow::Result<()> { + async fn send(&self, message: &SendMessage) -> anyhow::Result<()> { let token = self.get_token().await?; // Determine if this is a group or private message based on recipient format // Format: "user:{openid}" or "group:{group_openid}" - let (url, body) = if let Some(group_id) = recipient.strip_prefix("group:") { + let (url, body) = if let Some(group_id) = message.recipient.strip_prefix("group:") { ( format!("{QQ_API_BASE}/v2/groups/{group_id}/messages"), json!({ - "content": message, + "content": &message.content, "msg_type": 0, }), ) } else { - let user_id = recipient.strip_prefix("user:").unwrap_or(recipient); + let user_id = message + .recipient + .strip_prefix("user:") + .unwrap_or(&message.recipient); ( format!("{QQ_API_BASE}/v2/users/{user_id}/messages"), json!({ - "content": message, + "content": &message.content, "msg_type": 0, }), ) diff --git a/src/channels/signal.rs b/src/channels/signal.rs index 3bcaf56..2cbbc84 100644 --- a/src/channels/signal.rs +++ b/src/channels/signal.rs @@ -1,4 +1,4 @@ -use crate::channels::traits::{Channel, ChannelMessage}; +use crate::channels::traits::{Channel, ChannelMessage, SendMessage}; use async_trait::async_trait; use futures_util::StreamExt; use reqwest::Client; @@ -269,17 +269,17 @@ impl Channel for SignalChannel { "signal" } - async fn send(&self, message: &str, recipient: &str) -> anyhow::Result<()> { - let params = match Self::parse_recipient_target(recipient) { + async fn send(&self, message: &SendMessage) -> anyhow::Result<()> { + let params = match Self::parse_recipient_target(&message.recipient) { RecipientTarget::Direct(number) => serde_json::json!({ "recipient": [number], - "message": message, - "account": self.account, + "message": &message.content, + "account": &self.account, }), RecipientTarget::Group(group_id) => serde_json::json!({ "groupId": group_id, - "message": message, - "account": self.account, + "message": &message.content, + "account": &self.account, }), }; @@ -423,11 +423,11 @@ impl Channel for SignalChannel { let params = match Self::parse_recipient_target(recipient) { RecipientTarget::Direct(number) => serde_json::json!({ "recipient": [number], - "account": self.account, + "account": &self.account, }), RecipientTarget::Group(group_id) => serde_json::json!({ "groupId": group_id, - "account": self.account, + "account": &self.account, }), }; self.rpc_request("sendTyping", params).await?; diff --git a/src/channels/telegram.rs b/src/channels/telegram.rs index b08f843..553654d 100644 --- a/src/channels/telegram.rs +++ b/src/channels/telegram.rs @@ -380,10 +380,10 @@ impl TelegramChannel { match self.persist_allowed_identity(&identity).await { Ok(()) => { let _ = self - .send( + .send(&SendMessage::new( "✅ Telegram account bound successfully. You can talk to ZeroClaw now.", &chat_id, - ) + )) .await; tracing::info!( "Telegram: paired and allowlisted identity={identity}" @@ -394,45 +394,45 @@ impl TelegramChannel { "Telegram: failed to persist allowlist after bind: {e}" ); let _ = self - .send( + .send(&SendMessage::new( "⚠️ Bound for this runtime, but failed to persist config. Access may be lost after restart; check config file permissions.", &chat_id, - ) + )) .await; } } } else { let _ = self - .send( + .send(&SendMessage::new( "❌ Could not identify your Telegram account. Ensure your account has a username or stable user ID, then retry.", &chat_id, - ) + )) .await; } } Ok(None) => { let _ = self - .send( + .send(&SendMessage::new( "❌ Invalid binding code. Ask operator for the latest code and retry.", &chat_id, - ) + )) .await; } Err(lockout_secs) => { let _ = self - .send( - &format!("⏳ Too many invalid attempts. Retry in {lockout_secs}s."), + .send(&SendMessage::new( + format!("⏳ Too many invalid attempts. Retry in {lockout_secs}s."), &chat_id, - ) + )) .await; } } } else { let _ = self - .send( + .send(&SendMessage::new( "ℹ️ Telegram pairing is not active. Ask operator to update allowlist in config.toml.", &chat_id, - ) + )) .await; } return; @@ -456,23 +456,20 @@ Allowlist Telegram username (without '@') or numeric user ID.", .unwrap_or_else(|| "YOUR_TELEGRAM_ID".to_string()); let _ = self - .send( - &format!( - "🔐 This bot requires operator approval.\n\n\ -Copy this command to operator terminal:\n\ -`zeroclaw channel bind-telegram {suggested_identity}`\n\n\ -After operator runs it, send your message again." + .send(&SendMessage::new( + format!( + "🔐 This bot requires operator approval.\n\nCopy this command to operator terminal:\n`zeroclaw channel bind-telegram {suggested_identity}`\n\nAfter operator runs it, send your message again." ), &chat_id, - ) + )) .await; if self.pairing_code_active() { let _ = self - .send( + .send(&SendMessage::new( "ℹ️ If operator provides a one-time pairing code, you can also run `/bind `.", &chat_id, - ) + )) .await; } } @@ -1066,7 +1063,8 @@ impl Channel for TelegramChannel { } if let Some(attachment) = parse_path_only_attachment(&message.content) { - self.send_attachment(&message.recipient, &attachment).await?; + self.send_attachment(&message.recipient, &attachment) + .await?; return Ok(()); } @@ -1369,7 +1367,7 @@ mod tests { "username": "alice" }, "chat": { - "id": -100200300 + "id": -100_200_300 } } }); diff --git a/src/channels/traits.rs b/src/channels/traits.rs index 069496f..1731ba8 100644 --- a/src/channels/traits.rs +++ b/src/channels/traits.rs @@ -12,7 +12,7 @@ pub struct ChannelMessage { } /// Message to send through a channel -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone)] pub struct SendMessage { pub content: String, pub recipient: String, @@ -43,26 +43,6 @@ impl SendMessage { } } -impl From<&str> for SendMessage { - fn from(content: &str) -> Self { - Self { - content: content.to_string(), - recipient: String::new(), - subject: None, - } - } -} - -impl From<(String, String)> for SendMessage { - fn from(value: (String, String)) -> Self { - Self { - content: value.0, - recipient: value.1, - subject: None, - } - } -} - /// Core channel trait — implement for any messaging platform #[async_trait] pub trait Channel: Send + Sync { @@ -152,7 +132,10 @@ mod tests { assert!(channel.health_check().await); assert!(channel.start_typing("bob").await.is_ok()); assert!(channel.stop_typing("bob").await.is_ok()); - assert!(channel.send(&SendMessage::new("hello", "bob")).await.is_ok()); + assert!(channel + .send(&SendMessage::new("hello", "bob")) + .await + .is_ok()); } #[tokio::test] diff --git a/src/cron/scheduler.rs b/src/cron/scheduler.rs index 4562dba..dc53047 100644 --- a/src/cron/scheduler.rs +++ b/src/cron/scheduler.rs @@ -1,4 +1,4 @@ -use crate::channels::{Channel, DiscordChannel, SlackChannel, TelegramChannel}; +use crate::channels::{Channel, DiscordChannel, SendMessage, SlackChannel, TelegramChannel}; use crate::config::Config; use crate::cron::{ due_jobs, next_run_for_schedule, record_last_run, record_run, remove_job, reschedule_after_run, @@ -232,7 +232,7 @@ async fn deliver_if_configured(config: &Config, job: &CronJob, output: &str) -> .as_ref() .ok_or_else(|| anyhow::anyhow!("telegram channel not configured"))?; let channel = TelegramChannel::new(tg.bot_token.clone(), tg.allowed_users.clone()); - channel.send(output, target).await?; + channel.send(&SendMessage::new(output, target)).await?; } "discord" => { let dc = config @@ -247,7 +247,7 @@ async fn deliver_if_configured(config: &Config, job: &CronJob, output: &str) -> dc.listen_to_bots, dc.mention_only, ); - channel.send(output, target).await?; + channel.send(&SendMessage::new(output, target)).await?; } "slack" => { let sl = config @@ -260,7 +260,7 @@ async fn deliver_if_configured(config: &Config, job: &CronJob, output: &str) -> sl.channel_id.clone(), sl.allowed_users.clone(), ); - channel.send(output, target).await?; + channel.send(&SendMessage::new(output, target)).await?; } other => anyhow::bail!("unsupported delivery channel: {other}"), } diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 59ae3b0..988b780 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -704,7 +704,10 @@ async fn handle_whatsapp_message( { Ok(response) => { // Send reply via WhatsApp - if let Err(e) = wa.send(&SendMessage::new(response, &msg.reply_target)).await { + if let Err(e) = wa + .send(&SendMessage::new(response, &msg.reply_target)) + .await + { tracing::error!("Failed to send WhatsApp reply: {e}"); } }