rustyclaw_core/messengers/
google_chat.rs1use super::{Message, Messenger, SendOptions};
8use anyhow::{Context, Result};
9use async_trait::async_trait;
10
11pub struct GoogleChatMessenger {
13 name: String,
14 webhook_url: Option<String>,
16 credentials_path: Option<String>,
18 spaces: Vec<String>,
20 connected: bool,
21 http: reqwest::Client,
22}
23
24impl GoogleChatMessenger {
25 pub fn new(name: String) -> Self {
26 Self {
27 name,
28 webhook_url: None,
29 credentials_path: None,
30 spaces: Vec::new(),
31 connected: false,
32 http: reqwest::Client::new(),
33 }
34 }
35
36 pub fn with_webhook_url(mut self, url: String) -> Self {
38 self.webhook_url = Some(url);
39 self
40 }
41
42 pub fn with_credentials(mut self, path: String) -> Self {
44 self.credentials_path = Some(path);
45 self
46 }
47
48 pub fn with_spaces(mut self, spaces: Vec<String>) -> Self {
50 self.spaces = spaces;
51 self
52 }
53}
54
55#[async_trait]
56impl Messenger for GoogleChatMessenger {
57 fn name(&self) -> &str {
58 &self.name
59 }
60
61 fn messenger_type(&self) -> &str {
62 "google_chat"
63 }
64
65 async fn initialize(&mut self) -> Result<()> {
66 if self.webhook_url.is_none() && self.credentials_path.is_none() {
68 anyhow::bail!(
69 "Google Chat requires either 'webhook_url' or 'credentials_path' \
70 (service account JSON)"
71 );
72 }
73
74 self.connected = true;
75 tracing::info!(
76 has_webhook = self.webhook_url.is_some(),
77 has_credentials = self.credentials_path.is_some(),
78 spaces = ?self.spaces,
79 "Google Chat initialized"
80 );
81
82 Ok(())
83 }
84
85 async fn send_message(&self, space: &str, content: &str) -> Result<String> {
86 if let Some(ref webhook_url) = self.webhook_url {
88 let payload = serde_json::json!({
89 "text": content
90 });
91
92 let resp = self
93 .http
94 .post(webhook_url)
95 .json(&payload)
96 .send()
97 .await
98 .context("Google Chat webhook POST failed")?;
99
100 if resp.status().is_success() {
101 let data: serde_json::Value = resp.json().await?;
102 return Ok(data["name"].as_str().unwrap_or("sent").to_string());
103 }
104 anyhow::bail!("Google Chat webhook returned {}", resp.status());
105 }
106
107 if self.credentials_path.is_some() {
109 anyhow::bail!(
112 "Google Chat API (service account) send not yet implemented for space '{}'. \
113 Use webhook_url or the claw-me-maybe skill.",
114 space
115 );
116 }
117
118 anyhow::bail!("No Google Chat send method configured")
119 }
120
121 async fn send_message_with_options(&self, opts: SendOptions<'_>) -> Result<String> {
122 self.send_message(opts.recipient, opts.content).await
125 }
126
127 async fn receive_messages(&self) -> Result<Vec<Message>> {
128 Ok(Vec::new())
133 }
134
135 fn is_connected(&self) -> bool {
136 self.connected
137 }
138
139 async fn disconnect(&mut self) -> Result<()> {
140 self.connected = false;
141 Ok(())
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn test_google_chat_creation() {
151 let m = GoogleChatMessenger::new("test".to_string())
152 .with_webhook_url("https://chat.googleapis.com/v1/spaces/XXX/messages?key=YYY".to_string());
153 assert_eq!(m.name(), "test");
154 assert_eq!(m.messenger_type(), "google_chat");
155 assert!(!m.is_connected());
156 assert!(m.webhook_url.is_some());
157 }
158
159 #[test]
160 fn test_with_credentials() {
161 let m = GoogleChatMessenger::new("test".to_string())
162 .with_credentials("/path/to/sa.json".to_string())
163 .with_spaces(vec!["spaces/AAAA".to_string()]);
164 assert_eq!(m.credentials_path, Some("/path/to/sa.json".to_string()));
165 assert_eq!(m.spaces, vec!["spaces/AAAA"]);
166 }
167}