1#[cfg(feature = "im")]
2use base64::{prelude::BASE64_STANDARD, Engine};
3#[cfg(feature = "im")]
4use hmac::{Hmac, Mac};
5#[cfg(feature = "im")]
6use serde_json::{json, Value};
7#[cfg(feature = "im")]
8use sha2::Sha256;
9
10#[cfg(feature = "im")]
11use crate::core::{
12 api_resp::{BaseResponse, RawResponse},
13 http::Transport,
14 SDKResult,
15};
16
17#[cfg(feature = "im")]
18use crate::service::im::v1::message::{MessageCardTemplate, SendMessageTrait};
19
20#[allow(dead_code)]
24pub struct CustomBot<'a> {
25 webhook_url: &'a str,
27 secret: Option<&'a str>,
29 client: reqwest::Client,
30}
31
32impl<'a> CustomBot<'a> {
33 pub fn new(webhook_url: &'a str, secret: Option<&'a str>) -> Self {
34 CustomBot {
35 webhook_url,
36 secret,
37 client: reqwest::Client::new(),
38 }
39 }
40}
41
42impl CustomBot<'_> {
43 #[cfg(feature = "im")]
44 pub async fn send_message<T>(&self, message: T) -> SDKResult<BaseResponse<RawResponse>>
45 where
46 T: SendMessageTrait,
47 {
48 let mut json = json!({
49 "msg_type": message.msg_type(),
50 "content": message.content()
51 });
52 self.check_sign(&mut json);
53 Transport::do_send(
54 self.client.post(self.webhook_url),
55 json.to_string().into(),
56 false,
57 )
58 .await
59 }
60
61 #[cfg(feature = "im")]
63 pub async fn send_card(
64 &self,
65 message: MessageCardTemplate,
66 ) -> SDKResult<BaseResponse<RawResponse>> {
67 let mut json = json!({
68 "msg_type": message.msg_type(),
69 "card": message.content()
70 });
71
72 self.check_sign(&mut json);
73
74 Transport::do_send(
75 self.client.post(self.webhook_url),
76 json.to_string().into_bytes(),
77 false,
78 )
79 .await
80 }
81
82 #[cfg(feature = "im")]
84 fn check_sign(&self, json: &mut Value) {
85 if let Some(secret) = self.secret.as_ref() {
86 let now = chrono::Local::now().timestamp();
87 json["timestamp"] = serde_json::to_value(now).unwrap();
88 let sign = CustomBot::sign(now, secret);
89 json["sign"] = serde_json::to_value(sign).unwrap();
90 }
91 }
92
93 #[cfg(feature = "im")]
95 fn sign(timestamp: i64, secret: &str) -> String {
96 let string_to_sign = format!("{timestamp}\n{secret}");
97 let hmac: Hmac<Sha256> = Hmac::new_from_slice(string_to_sign.as_bytes()).unwrap();
98 let hmac_code = hmac.finalize().into_bytes();
99 BASE64_STANDARD.encode(hmac_code)
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn test_custom_bot_creation_with_secret() {
109 let webhook_url = "https://open.feishu.cn/open-apis/bot/v2/hook/test";
110 let secret = Some("test_secret");
111
112 let bot = CustomBot::new(webhook_url, secret);
113
114 assert_eq!(bot.webhook_url, webhook_url);
115 assert_eq!(bot.secret, secret);
116 }
117
118 #[test]
119 fn test_custom_bot_creation_without_secret() {
120 let webhook_url = "https://open.feishu.cn/open-apis/bot/v2/hook/test";
121
122 let bot = CustomBot::new(webhook_url, None);
123
124 assert_eq!(bot.webhook_url, webhook_url);
125 assert!(bot.secret.is_none());
126 }
127
128 #[test]
129 fn test_custom_bot_creation_with_empty_webhook() {
130 let webhook_url = "";
131 let secret = Some("test_secret");
132
133 let bot = CustomBot::new(webhook_url, secret);
134
135 assert_eq!(bot.webhook_url, "");
136 assert_eq!(bot.secret, secret);
137 }
138
139 #[test]
140 fn test_custom_bot_creation_with_different_urls() {
141 let test_urls = [
142 "https://open.feishu.cn/open-apis/bot/v2/hook/test1",
143 "https://open.larksuite.com/open-apis/bot/v2/hook/test2",
144 "http://localhost:8080/webhook",
145 "https://example.com/hook",
146 ];
147
148 for url in &test_urls {
149 let bot = CustomBot::new(url, None);
150 assert_eq!(bot.webhook_url, *url);
151 }
152 }
153
154 #[cfg(feature = "im")]
155 #[test]
156 fn test_sign_basic() {
157 let timestamp = 1609459200; let secret = "test_secret";
159
160 let signature = CustomBot::sign(timestamp, secret);
161
162 assert!(!signature.is_empty());
164 assert!(base64::prelude::BASE64_STANDARD.decode(&signature).is_ok());
165 }
166
167 #[cfg(feature = "im")]
168 #[test]
169 fn test_sign_consistency() {
170 let timestamp = 1609459200;
171 let secret = "test_secret";
172
173 let signature1 = CustomBot::sign(timestamp, secret);
174 let signature2 = CustomBot::sign(timestamp, secret);
175
176 assert_eq!(signature1, signature2);
178 }
179
180 #[cfg(feature = "im")]
181 #[test]
182 fn test_sign_different_inputs() {
183 let timestamp1 = 1609459200;
184 let timestamp2 = 1609459201;
185 let secret = "test_secret";
186
187 let signature1 = CustomBot::sign(timestamp1, secret);
188 let signature2 = CustomBot::sign(timestamp2, secret);
189
190 assert_ne!(signature1, signature2);
192
193 let signature3 = CustomBot::sign(timestamp1, "different_secret");
194
195 assert_ne!(signature1, signature3);
197 }
198
199 #[cfg(feature = "im")]
200 #[test]
201 fn test_sign_with_empty_secret() {
202 let timestamp = 1609459200;
203 let secret = "";
204
205 let signature = CustomBot::sign(timestamp, secret);
206
207 assert!(!signature.is_empty());
209 assert!(base64::prelude::BASE64_STANDARD.decode(&signature).is_ok());
210 }
211
212 #[cfg(feature = "im")]
213 #[test]
214 fn test_sign_with_special_characters() {
215 let timestamp = 1609459200;
216 let secret = "test_secret!@#$%^&*()_+";
217
218 let signature = CustomBot::sign(timestamp, secret);
219
220 assert!(!signature.is_empty());
222 assert!(base64::prelude::BASE64_STANDARD.decode(&signature).is_ok());
223 }
224
225 #[cfg(feature = "im")]
226 #[test]
227 fn test_sign_with_unicode() {
228 let timestamp = 1609459200;
229 let secret = "测试密钥🔐";
230
231 let signature = CustomBot::sign(timestamp, secret);
232
233 assert!(!signature.is_empty());
235 assert!(base64::prelude::BASE64_STANDARD.decode(&signature).is_ok());
236 }
237
238 #[cfg(feature = "im")]
239 #[test]
240 fn test_check_sign_with_secret() {
241 let webhook_url = "https://test.webhook.url";
242 let secret = Some("test_secret");
243 let bot = CustomBot::new(webhook_url, secret);
244
245 let mut json = json!({
246 "msg_type": "text",
247 "content": "test message"
248 });
249
250 bot.check_sign(&mut json);
251
252 assert!(json["timestamp"].is_i64());
254 assert!(json["sign"].is_string());
255 assert!(!json["sign"].as_str().unwrap().is_empty());
256 }
257
258 #[cfg(feature = "im")]
259 #[test]
260 fn test_check_sign_without_secret() {
261 let webhook_url = "https://test.webhook.url";
262 let bot = CustomBot::new(webhook_url, None);
263
264 let mut json = json!({
265 "msg_type": "text",
266 "content": "test message"
267 });
268 let original_json = json.clone();
269
270 bot.check_sign(&mut json);
271
272 assert_eq!(json, original_json);
274 assert!(json["timestamp"].is_null());
275 assert!(json["sign"].is_null());
276 }
277
278 #[cfg(feature = "im")]
279 #[test]
280 fn test_check_sign_preserves_existing_fields() {
281 let webhook_url = "https://test.webhook.url";
282 let secret = Some("test_secret");
283 let bot = CustomBot::new(webhook_url, secret);
284
285 let mut json = json!({
286 "msg_type": "text",
287 "content": "test message",
288 "existing_field": "existing_value"
289 });
290
291 bot.check_sign(&mut json);
292
293 assert_eq!(json["msg_type"], "text");
295 assert_eq!(json["content"], "test message");
296 assert_eq!(json["existing_field"], "existing_value");
297
298 assert!(json["timestamp"].is_i64());
300 assert!(json["sign"].is_string());
301 }
302
303 #[test]
304 fn test_custom_bot_is_send_sync() {
305 fn assert_send<T: Send>() {}
307 fn assert_sync<T: Sync>() {}
308
309 assert_send::<CustomBot>();
310 assert_sync::<CustomBot>();
311 }
312
313 #[test]
314 fn test_custom_bot_lifetime() {
315 let webhook_url = String::from("https://test.webhook.url");
316 let secret_str = String::from("test_secret");
317
318 let bot = CustomBot::new(&webhook_url, Some(&secret_str));
320
321 assert_eq!(bot.webhook_url, webhook_url.as_str());
322 assert_eq!(bot.secret, Some(secret_str.as_str()));
323 }
324
325 #[test]
326 fn test_custom_bot_multiple_instances() {
327 let webhook_url1 = "https://test1.webhook.url";
328 let webhook_url2 = "https://test2.webhook.url";
329 let secret1 = Some("secret1");
330 let secret2 = Some("secret2");
331
332 let bot1 = CustomBot::new(webhook_url1, secret1);
333 let bot2 = CustomBot::new(webhook_url2, secret2);
334
335 assert_eq!(bot1.webhook_url, webhook_url1);
336 assert_eq!(bot1.secret, secret1);
337 assert_eq!(bot2.webhook_url, webhook_url2);
338 assert_eq!(bot2.secret, secret2);
339
340 assert_ne!(bot1.webhook_url, bot2.webhook_url);
342 assert_ne!(bot1.secret, bot2.secret);
343 }
344
345 #[test]
346 fn test_custom_bot_debug_representation() {
347 let webhook_url = "https://test.webhook.url";
348 let secret = Some("test_secret");
349 let bot = CustomBot::new(webhook_url, secret);
350
351 let debug_str = format!("{:?}", bot.client);
353 assert!(debug_str.contains("Client"));
354 }
355
356 #[cfg(feature = "im")]
357 #[test]
358 fn test_sign_boundary_values() {
359 let test_cases = [
361 (0, "secret"), (i64::MAX, "secret"), (1609459200, ""), (-1, "secret"), ];
366
367 for (timestamp, secret) in &test_cases {
368 let signature = CustomBot::sign(*timestamp, secret);
369 assert!(
370 !signature.is_empty(),
371 "Failed for timestamp: {}, secret: '{}'",
372 timestamp,
373 secret
374 );
375 assert!(
376 base64::prelude::BASE64_STANDARD.decode(&signature).is_ok(),
377 "Invalid base64 for timestamp: {}, secret: '{}'",
378 timestamp,
379 secret
380 );
381 }
382 }
383
384 #[cfg(feature = "im")]
385 #[test]
386 fn test_sign_very_long_secret() {
387 let timestamp = 1609459200;
388 let long_secret = "a".repeat(1000); let signature = CustomBot::sign(timestamp, &long_secret);
391
392 assert!(!signature.is_empty());
393 assert!(base64::prelude::BASE64_STANDARD.decode(&signature).is_ok());
394 }
395}