fix(channels): complete SendMessage migration after rebase

This commit is contained in:
Chummy 2026-02-17 23:25:52 +08:00
parent dbebd48dfe
commit cd0dd13476
7 changed files with 57 additions and 67 deletions

View file

@ -250,7 +250,10 @@ async fn process_channel_message(ctx: Arc<ChannelRuntimeContext>, msg: traits::C
); );
if let Some(channel) = target_channel.as_ref() { if let Some(channel) = target_channel.as_ref() {
let _ = channel let _ = channel
.send(&SendMessage::new(format!("⚠️ Error: {e}"), &msg.reply_target)) .send(&SendMessage::new(
format!("⚠️ Error: {e}"),
&msg.reply_target,
))
.await; .await;
} }
} }

View file

@ -1,4 +1,4 @@
use super::traits::{Channel, ChannelMessage}; use super::traits::{Channel, ChannelMessage, SendMessage};
use async_trait::async_trait; use async_trait::async_trait;
use futures_util::{SinkExt, StreamExt}; use futures_util::{SinkExt, StreamExt};
use serde_json::json; use serde_json::json;
@ -162,25 +162,28 @@ impl Channel for QQChannel {
"qq" "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?; let token = self.get_token().await?;
// Determine if this is a group or private message based on recipient format // Determine if this is a group or private message based on recipient format
// Format: "user:{openid}" or "group:{group_openid}" // 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"), format!("{QQ_API_BASE}/v2/groups/{group_id}/messages"),
json!({ json!({
"content": message, "content": &message.content,
"msg_type": 0, "msg_type": 0,
}), }),
) )
} else { } 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"), format!("{QQ_API_BASE}/v2/users/{user_id}/messages"),
json!({ json!({
"content": message, "content": &message.content,
"msg_type": 0, "msg_type": 0,
}), }),
) )

View file

@ -1,4 +1,4 @@
use crate::channels::traits::{Channel, ChannelMessage}; use crate::channels::traits::{Channel, ChannelMessage, SendMessage};
use async_trait::async_trait; use async_trait::async_trait;
use futures_util::StreamExt; use futures_util::StreamExt;
use reqwest::Client; use reqwest::Client;
@ -269,17 +269,17 @@ impl Channel for SignalChannel {
"signal" "signal"
} }
async fn send(&self, message: &str, recipient: &str) -> anyhow::Result<()> { async fn send(&self, message: &SendMessage) -> anyhow::Result<()> {
let params = match Self::parse_recipient_target(recipient) { let params = match Self::parse_recipient_target(&message.recipient) {
RecipientTarget::Direct(number) => serde_json::json!({ RecipientTarget::Direct(number) => serde_json::json!({
"recipient": [number], "recipient": [number],
"message": message, "message": &message.content,
"account": self.account, "account": &self.account,
}), }),
RecipientTarget::Group(group_id) => serde_json::json!({ RecipientTarget::Group(group_id) => serde_json::json!({
"groupId": group_id, "groupId": group_id,
"message": message, "message": &message.content,
"account": self.account, "account": &self.account,
}), }),
}; };
@ -423,11 +423,11 @@ impl Channel for SignalChannel {
let params = match Self::parse_recipient_target(recipient) { let params = match Self::parse_recipient_target(recipient) {
RecipientTarget::Direct(number) => serde_json::json!({ RecipientTarget::Direct(number) => serde_json::json!({
"recipient": [number], "recipient": [number],
"account": self.account, "account": &self.account,
}), }),
RecipientTarget::Group(group_id) => serde_json::json!({ RecipientTarget::Group(group_id) => serde_json::json!({
"groupId": group_id, "groupId": group_id,
"account": self.account, "account": &self.account,
}), }),
}; };
self.rpc_request("sendTyping", params).await?; self.rpc_request("sendTyping", params).await?;

View file

@ -380,10 +380,10 @@ impl TelegramChannel {
match self.persist_allowed_identity(&identity).await { match self.persist_allowed_identity(&identity).await {
Ok(()) => { Ok(()) => {
let _ = self let _ = self
.send( .send(&SendMessage::new(
"✅ Telegram account bound successfully. You can talk to ZeroClaw now.", "✅ Telegram account bound successfully. You can talk to ZeroClaw now.",
&chat_id, &chat_id,
) ))
.await; .await;
tracing::info!( tracing::info!(
"Telegram: paired and allowlisted identity={identity}" "Telegram: paired and allowlisted identity={identity}"
@ -394,45 +394,45 @@ impl TelegramChannel {
"Telegram: failed to persist allowlist after bind: {e}" "Telegram: failed to persist allowlist after bind: {e}"
); );
let _ = self 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.", "⚠️ Bound for this runtime, but failed to persist config. Access may be lost after restart; check config file permissions.",
&chat_id, &chat_id,
) ))
.await; .await;
} }
} }
} else { } else {
let _ = self let _ = self
.send( .send(&SendMessage::new(
"❌ Could not identify your Telegram account. Ensure your account has a username or stable user ID, then retry.", "❌ Could not identify your Telegram account. Ensure your account has a username or stable user ID, then retry.",
&chat_id, &chat_id,
) ))
.await; .await;
} }
} }
Ok(None) => { Ok(None) => {
let _ = self let _ = self
.send( .send(&SendMessage::new(
"❌ Invalid binding code. Ask operator for the latest code and retry.", "❌ Invalid binding code. Ask operator for the latest code and retry.",
&chat_id, &chat_id,
) ))
.await; .await;
} }
Err(lockout_secs) => { Err(lockout_secs) => {
let _ = self let _ = self
.send( .send(&SendMessage::new(
&format!("⏳ Too many invalid attempts. Retry in {lockout_secs}s."), format!("⏳ Too many invalid attempts. Retry in {lockout_secs}s."),
&chat_id, &chat_id,
) ))
.await; .await;
} }
} }
} else { } else {
let _ = self let _ = self
.send( .send(&SendMessage::new(
" Telegram pairing is not active. Ask operator to update allowlist in config.toml.", " Telegram pairing is not active. Ask operator to update allowlist in config.toml.",
&chat_id, &chat_id,
) ))
.await; .await;
} }
return; return;
@ -456,23 +456,20 @@ Allowlist Telegram username (without '@') or numeric user ID.",
.unwrap_or_else(|| "YOUR_TELEGRAM_ID".to_string()); .unwrap_or_else(|| "YOUR_TELEGRAM_ID".to_string());
let _ = self let _ = self
.send( .send(&SendMessage::new(
&format!( format!(
"🔐 This bot requires operator approval.\n\n\ "🔐 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."
Copy this command to operator terminal:\n\
`zeroclaw channel bind-telegram {suggested_identity}`\n\n\
After operator runs it, send your message again."
), ),
&chat_id, &chat_id,
) ))
.await; .await;
if self.pairing_code_active() { if self.pairing_code_active() {
let _ = self let _ = self
.send( .send(&SendMessage::new(
" If operator provides a one-time pairing code, you can also run `/bind <code>`.", " If operator provides a one-time pairing code, you can also run `/bind <code>`.",
&chat_id, &chat_id,
) ))
.await; .await;
} }
} }
@ -1066,7 +1063,8 @@ impl Channel for TelegramChannel {
} }
if let Some(attachment) = parse_path_only_attachment(&message.content) { 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(()); return Ok(());
} }
@ -1369,7 +1367,7 @@ mod tests {
"username": "alice" "username": "alice"
}, },
"chat": { "chat": {
"id": -100200300 "id": -100_200_300
} }
} }
}); });

View file

@ -12,7 +12,7 @@ pub struct ChannelMessage {
} }
/// Message to send through a channel /// Message to send through a channel
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone)]
pub struct SendMessage { pub struct SendMessage {
pub content: String, pub content: String,
pub recipient: 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 /// Core channel trait — implement for any messaging platform
#[async_trait] #[async_trait]
pub trait Channel: Send + Sync { pub trait Channel: Send + Sync {
@ -152,7 +132,10 @@ mod tests {
assert!(channel.health_check().await); assert!(channel.health_check().await);
assert!(channel.start_typing("bob").await.is_ok()); assert!(channel.start_typing("bob").await.is_ok());
assert!(channel.stop_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] #[tokio::test]

View file

@ -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::config::Config;
use crate::cron::{ use crate::cron::{
due_jobs, next_run_for_schedule, record_last_run, record_run, remove_job, reschedule_after_run, 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() .as_ref()
.ok_or_else(|| anyhow::anyhow!("telegram channel not configured"))?; .ok_or_else(|| anyhow::anyhow!("telegram channel not configured"))?;
let channel = TelegramChannel::new(tg.bot_token.clone(), tg.allowed_users.clone()); 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" => { "discord" => {
let dc = config let dc = config
@ -247,7 +247,7 @@ async fn deliver_if_configured(config: &Config, job: &CronJob, output: &str) ->
dc.listen_to_bots, dc.listen_to_bots,
dc.mention_only, dc.mention_only,
); );
channel.send(output, target).await?; channel.send(&SendMessage::new(output, target)).await?;
} }
"slack" => { "slack" => {
let sl = config let sl = config
@ -260,7 +260,7 @@ async fn deliver_if_configured(config: &Config, job: &CronJob, output: &str) ->
sl.channel_id.clone(), sl.channel_id.clone(),
sl.allowed_users.clone(), sl.allowed_users.clone(),
); );
channel.send(output, target).await?; channel.send(&SendMessage::new(output, target)).await?;
} }
other => anyhow::bail!("unsupported delivery channel: {other}"), other => anyhow::bail!("unsupported delivery channel: {other}"),
} }

View file

@ -704,7 +704,10 @@ async fn handle_whatsapp_message(
{ {
Ok(response) => { Ok(response) => {
// Send reply via WhatsApp // 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}"); tracing::error!("Failed to send WhatsApp reply: {e}");
} }
} }