1use anyhow::{Context, Result, anyhow, bail};
7use std::hash::{Hash, Hasher};
8use std::io::{self, Write as IoWrite};
9use std::path::Path;
10use tracing::{debug, warn};
11
12use super::config::ChannelConfig;
13
14#[derive(Debug, Clone)]
16pub struct InboundMessage {
17 pub from_user_id: i64,
18 #[allow(dead_code)] pub chat_id: i64,
20 pub text: String,
21}
22
23pub struct TelegramBot {
25 bot_token: String,
26 allowed_user_ids: Vec<i64>,
27 last_update_offset: i64,
28}
29
30impl TelegramBot {
31 pub fn new(bot_token: String, allowed_user_ids: Vec<i64>) -> Self {
33 Self {
34 bot_token,
35 allowed_user_ids,
36 last_update_offset: 0,
37 }
38 }
39
40 pub fn from_config(config: &ChannelConfig) -> Option<Self> {
46 let token = config
47 .bot_token
48 .clone()
49 .or_else(|| std::env::var("BATTY_TELEGRAM_BOT_TOKEN").ok());
50
51 token.map(|t| Self::new(t, config.allowed_user_ids.clone()))
52 }
53
54 #[allow(dead_code)] pub fn is_authorized(&self, user_id: i64) -> bool {
59 self.allowed_user_ids.contains(&user_id)
60 }
61
62 const MAX_MESSAGE_LEN: usize = 4096;
64
65 pub fn send_message(&self, chat_id: &str, text: &str) -> Result<()> {
70 let chunks = split_message(text, Self::MAX_MESSAGE_LEN);
71 let total_chunks = chunks.len();
72
73 for (index, chunk) in chunks.into_iter().enumerate() {
74 let chunk_id = outbound_chunk_message_id(chat_id, chunk, index, total_chunks);
75 debug!(
76 chat_id,
77 chunk_index = index,
78 total_chunks,
79 chunk_id,
80 "sending telegram message chunk"
81 );
82 self.send_message_chunk(chat_id, chunk)?;
83 }
84 Ok(())
85 }
86
87 fn send_message_chunk(&self, chat_id: &str, text: &str) -> Result<()> {
89 let url = format!("https://api.telegram.org/bot{}/sendMessage", self.bot_token);
90
91 let body = serde_json::json!({
92 "chat_id": chat_id,
93 "text": text,
94 });
95
96 let resp = ureq::post(&url)
97 .set("Content-Type", "application/json")
98 .send_string(&body.to_string());
99
100 match resp {
101 Ok(r) => {
102 let status = r.status();
103 let body: serde_json::Value = r
104 .into_json()
105 .context("failed to parse sendMessage response body")?;
106 let message_id = parse_send_message_response(&body)?;
107 debug!(status, message_id, "sendMessage accepted by Telegram API");
108 Ok(())
109 }
110 Err(ureq::Error::Status(status, response)) => {
111 let detail = response.into_string().unwrap_or_default();
112 warn!(status, detail = %detail, "sendMessage failed");
113 bail!("Telegram sendMessage failed with status {status}: {detail}");
114 }
115 Err(ureq::Error::Transport(error)) => {
116 warn!(error = %error, "sendMessage transport failed");
117 bail!("Telegram sendMessage transport failed: {error}");
118 }
119 }
120 }
121
122 pub fn poll_updates(&mut self) -> Result<Vec<InboundMessage>> {
129 let url = format!(
130 "https://api.telegram.org/bot{}/getUpdates?offset={}&timeout=0",
131 self.bot_token, self.last_update_offset
132 );
133
134 let resp = ureq::get(&url).call();
135
136 let body: serde_json::Value = match resp {
137 Ok(r) => r.into_json()?,
138 Err(e) => {
139 warn!(error = %e, "getUpdates failed");
140 bail!("Telegram getUpdates failed: {e}");
141 }
142 };
143
144 let (messages, new_offset) =
145 parse_updates_response(&body, &self.allowed_user_ids, self.last_update_offset);
146 self.last_update_offset = new_offset;
147
148 Ok(messages)
149 }
150}
151
152fn parse_send_message_response(json: &serde_json::Value) -> Result<i64> {
153 if json["ok"].as_bool() != Some(true) {
154 return Err(anyhow!("Telegram API returned ok=false"));
155 }
156
157 json.get("result")
158 .and_then(|result| result.get("message_id"))
159 .and_then(|message_id| message_id.as_i64())
160 .ok_or_else(|| anyhow!("Telegram sendMessage response missing result.message_id"))
161}
162
163fn outbound_chunk_message_id(
164 chat_id: &str,
165 chunk: &str,
166 chunk_index: usize,
167 total_chunks: usize,
168) -> u64 {
169 let mut hasher = std::collections::hash_map::DefaultHasher::new();
170 chat_id.hash(&mut hasher);
171 chunk.hash(&mut hasher);
172 chunk_index.hash(&mut hasher);
173 total_chunks.hash(&mut hasher);
174 hasher.finish()
175}
176
177fn parse_updates_response(
181 json: &serde_json::Value,
182 allowed: &[i64],
183 current_offset: i64,
184) -> (Vec<InboundMessage>, i64) {
185 let mut messages = Vec::new();
186 let mut new_offset = current_offset;
187
188 let results = match json.get("result").and_then(|r| r.as_array()) {
189 Some(arr) => arr,
190 None => return (messages, new_offset),
191 };
192
193 for update in results {
194 if let Some(update_id) = update.get("update_id").and_then(|v| v.as_i64()) {
196 if update_id + 1 > new_offset {
197 new_offset = update_id + 1;
198 }
199 }
200
201 let message = match update.get("message") {
202 Some(m) => m,
203 None => continue,
204 };
205
206 let from_id = message
207 .get("from")
208 .and_then(|f| f.get("id"))
209 .and_then(|v| v.as_i64());
210
211 let chat_id = message
212 .get("chat")
213 .and_then(|c| c.get("id"))
214 .and_then(|v| v.as_i64());
215
216 let text = message
217 .get("text")
218 .and_then(|t| t.as_str())
219 .map(|s| s.to_string());
220
221 let (Some(from_id), Some(chat_id), Some(text)) = (from_id, chat_id, text) else {
222 continue;
223 };
224
225 if !allowed.contains(&from_id) {
226 debug!(from_id, "dropping message from unauthorized user");
227 continue;
228 }
229
230 messages.push(InboundMessage {
231 from_user_id: from_id,
232 chat_id,
233 text,
234 });
235 }
236
237 (messages, new_offset)
238}
239
240pub fn setup_telegram(project_root: &Path) -> Result<()> {
243 let config_path = project_root
244 .join(".batty")
245 .join("team_config")
246 .join("team.yaml");
247 if !config_path.exists() {
248 bail!(
249 "no team config found at {}; run `batty init` first",
250 config_path.display()
251 );
252 }
253
254 println!("Telegram Bot Setup");
255 println!("==================\n");
256
257 println!("Step 1: Bot Token");
259 println!(" 1) Open Telegram and chat with @BotFather");
260 println!(" 2) Send /newbot (or /mybots to reuse an existing bot)");
261 println!(" 3) Copy the token (looks like 123456:ABC-DEF...)");
262 println!();
263 let bot_token = prompt_bot_token()?;
264
265 println!("Step 2: Your Telegram User ID");
267 println!(" Option A: DM @userinfobot — it replies with your numeric ID");
268 println!(" Option B: DM @getidsbot");
269 println!(" Option C: Call https://api.telegram.org/bot<TOKEN>/getUpdates");
270 println!(" after sending your bot a message, and read message.from.id");
271 println!();
272 let user_id = prompt_user_id()?;
273
274 println!("Step 3: Test Message");
276 println!(" IMPORTANT: You must DM your bot first (send /start in Telegram)");
277 println!(" before the bot can message you. Telegram blocks bots from initiating");
278 println!(" conversations with users who haven't messaged the bot.");
279 println!();
280 let send_test = prompt_yes_no("Send a test message? [Y/n]: ", true)?;
281 if send_test {
282 let bot = TelegramBot::new(bot_token.clone(), vec![user_id]);
283 let chat_id = user_id.to_string();
284 match bot.send_message(&chat_id, "Batty Telegram bridge configured!") {
285 Ok(()) => println!("Test message sent. Check your Telegram.\n"),
286 Err(e) => {
287 println!("Warning: test message failed: {e}");
288 println!(" Did you DM the bot first? Open your bot in Telegram and send /start,");
289 println!(" then run `batty telegram` again.\n");
290 }
291 }
292 }
293
294 update_team_yaml(&config_path, &bot_token, user_id)?;
296
297 println!("Telegram configured successfully.");
298 println!("Restart the daemon with: batty stop && batty start");
299
300 Ok(())
301}
302
303fn prompt_bot_token() -> Result<String> {
305 loop {
306 let token = prompt("Enter your Telegram bot token (from @BotFather): ")?;
307 if token.is_empty() {
308 println!("Token cannot be empty. Try again.\n");
309 continue;
310 }
311
312 match validate_bot_token(&token) {
313 Ok(username) => {
314 println!("Bot validated: @{username}\n");
315 return Ok(token);
316 }
317 Err(e) => {
318 println!("Validation failed: {e}");
319 let retry = prompt_yes_no("Try again? [Y/n]: ", true)?;
320 if !retry {
321 bail!("setup cancelled");
322 }
323 }
324 }
325 }
326}
327
328fn validate_bot_token(token: &str) -> Result<String> {
331 let url = format!("https://api.telegram.org/bot{token}/getMe");
332 let resp: serde_json::Value = ureq::get(&url).call()?.into_json()?;
333
334 if resp["ok"].as_bool() != Some(true) {
335 bail!("Telegram API returned ok=false");
336 }
337
338 resp["result"]["username"]
339 .as_str()
340 .map(|s| s.to_string())
341 .ok_or_else(|| anyhow::anyhow!("missing username in getMe response"))
342}
343
344fn prompt_user_id() -> Result<i64> {
346 loop {
347 let input = prompt("Enter your Telegram user ID (from @userinfobot): ")?;
348 match input.trim().parse::<i64>() {
349 Ok(id) if id > 0 => return Ok(id),
350 _ => {
351 println!("Invalid user ID — must be a positive number. Try again.\n");
352 }
353 }
354 }
355}
356
357fn prompt(msg: &str) -> Result<String> {
359 print!("{msg}");
360 io::stdout().flush()?;
361 let mut input = String::new();
362 io::stdin().read_line(&mut input)?;
363 Ok(input.trim().to_string())
364}
365
366fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
368 let input = prompt(msg)?;
369 if input.is_empty() {
370 return Ok(default_yes);
371 }
372 Ok(input.starts_with('y') || input.starts_with('Y'))
373}
374
375#[allow(dead_code)] pub fn parse_get_me_response(json: &serde_json::Value) -> Option<String> {
379 if json["ok"].as_bool() != Some(true) {
380 return None;
381 }
382 json["result"]["username"].as_str().map(|s| s.to_string())
383}
384
385fn update_team_yaml(path: &Path, bot_token: &str, user_id: i64) -> Result<()> {
388 let content = std::fs::read_to_string(path)
389 .with_context(|| format!("failed to read {}", path.display()))?;
390
391 let mut doc: serde_yaml::Value = serde_yaml::from_str(&content)
392 .with_context(|| format!("failed to parse {}", path.display()))?;
393
394 let user_id_str = user_id.to_string();
395
396 let roles = doc
398 .get_mut("roles")
399 .and_then(|r| r.as_sequence_mut())
400 .ok_or_else(|| anyhow::anyhow!("no 'roles' sequence in team.yaml"))?;
401
402 let user_role = roles.iter_mut().find(|r| {
403 r.get("role_type")
404 .and_then(|v| v.as_str())
405 .map(|s| s == "user")
406 .unwrap_or(false)
407 });
408
409 if let Some(role) = user_role {
410 role["channel"] = serde_yaml::Value::String("telegram".into());
412
413 if role.get("channel_config").is_none() {
415 if let Some(role_map) = role.as_mapping_mut() {
416 role_map.insert(
417 serde_yaml::Value::String("channel_config".into()),
418 serde_yaml::Value::Mapping(serde_yaml::Mapping::new()),
419 );
420 }
421 }
422
423 let cc = &mut role["channel_config"];
424
425 if let Some(mapping) = cc.as_mapping_mut() {
426 mapping.insert(
427 serde_yaml::Value::String("target".into()),
428 serde_yaml::Value::String(user_id_str),
429 );
430 mapping.insert(
431 serde_yaml::Value::String("bot_token".into()),
432 serde_yaml::Value::String(bot_token.into()),
433 );
434 mapping.insert(
435 serde_yaml::Value::String("provider".into()),
436 serde_yaml::Value::String("openclaw".into()),
437 );
438 let ids = vec![serde_yaml::Value::Number(serde_yaml::Number::from(user_id))];
440 mapping.insert(
441 serde_yaml::Value::String("allowed_user_ids".into()),
442 serde_yaml::Value::Sequence(ids),
443 );
444 }
445 } else {
446 let mut new_role = serde_yaml::Mapping::new();
448 new_role.insert("name".into(), "human".into());
449 new_role.insert("role_type".into(), "user".into());
450 new_role.insert("channel".into(), "telegram".into());
451
452 let mut cc = serde_yaml::Mapping::new();
453 cc.insert("target".into(), serde_yaml::Value::String(user_id_str));
454 cc.insert(
455 "bot_token".into(),
456 serde_yaml::Value::String(bot_token.into()),
457 );
458 cc.insert("provider".into(), "openclaw".into());
459 let ids = vec![serde_yaml::Value::Number(serde_yaml::Number::from(user_id))];
460 cc.insert("allowed_user_ids".into(), serde_yaml::Value::Sequence(ids));
461 new_role.insert("channel_config".into(), serde_yaml::Value::Mapping(cc));
462
463 let talks_to = vec!["architect".into()];
464 new_role.insert("talks_to".into(), serde_yaml::Value::Sequence(talks_to));
465
466 roles.push(serde_yaml::Value::Mapping(new_role));
467 }
468
469 let output = serde_yaml::to_string(&doc)?;
470 std::fs::write(path, &output).with_context(|| format!("failed to write {}", path.display()))?;
471
472 Ok(())
473}
474
475fn split_message(text: &str, max_len: usize) -> Vec<&str> {
480 if text.len() <= max_len {
481 return vec![text];
482 }
483
484 let mut chunks = Vec::new();
485 let mut remaining = text;
486
487 while !remaining.is_empty() {
488 if remaining.len() <= max_len {
489 chunks.push(remaining);
490 break;
491 }
492
493 let split_at = remaining[..max_len]
495 .rfind('\n')
496 .map(|pos| pos + 1) .unwrap_or(max_len); chunks.push(&remaining[..split_at]);
500 remaining = &remaining[split_at..];
501 }
502
503 chunks
504}
505
506#[cfg(test)]
507mod tests {
508 use super::*;
509
510 #[test]
511 fn new_constructor_sets_fields() {
512 let bot = TelegramBot::new("token123".into(), vec![111, 222]);
513 assert_eq!(bot.bot_token, "token123");
514 assert_eq!(bot.allowed_user_ids, vec![111, 222]);
515 assert_eq!(bot.last_update_offset, 0);
516 }
517
518 #[test]
519 fn is_authorized_allows_listed_id() {
520 let bot = TelegramBot::new("t".into(), vec![100, 200, 300]);
521 assert!(bot.is_authorized(100));
522 assert!(bot.is_authorized(200));
523 assert!(bot.is_authorized(300));
524 }
525
526 #[test]
527 fn is_authorized_rejects_unlisted_id() {
528 let bot = TelegramBot::new("t".into(), vec![100, 200]);
529 assert!(!bot.is_authorized(999));
530 }
531
532 #[test]
533 fn is_authorized_empty_list_denies_all() {
534 let bot = TelegramBot::new("t".into(), vec![]);
535 assert!(!bot.is_authorized(100));
536 assert!(!bot.is_authorized(0));
537 }
538
539 #[test]
540 fn from_config_returns_none_without_token() {
541 let config = ChannelConfig {
547 target: "12345".into(),
548 provider: "telegram".into(),
549 bot_token: None,
550 allowed_user_ids: vec![],
551 };
552
553 if std::env::var("BATTY_TELEGRAM_BOT_TOKEN").is_err() {
555 assert!(TelegramBot::from_config(&config).is_none());
556 }
557 }
558
559 #[test]
560 fn from_config_returns_some_with_config_token() {
561 let config = ChannelConfig {
562 target: "12345".into(),
563 provider: "telegram".into(),
564 bot_token: Some("bot-tok-from-config".into()),
565 allowed_user_ids: vec![42],
566 };
567
568 let bot = TelegramBot::from_config(&config).expect("should return Some");
569 assert_eq!(bot.bot_token, "bot-tok-from-config");
570 assert_eq!(bot.allowed_user_ids, vec![42]);
571 }
572
573 #[test]
574 fn from_config_prefers_config_token_over_env() {
575 let config = ChannelConfig {
577 target: "12345".into(),
578 provider: "telegram".into(),
579 bot_token: Some("from-config".into()),
580 allowed_user_ids: vec![],
581 };
582
583 let bot = TelegramBot::from_config(&config).unwrap();
584 assert_eq!(bot.bot_token, "from-config");
585 }
586
587 #[test]
588 fn parse_updates_authorized_message() {
589 let json: serde_json::Value = serde_json::json!({
590 "ok": true,
591 "result": [
592 {
593 "update_id": 123456,
594 "message": {
595 "from": {"id": 12345678},
596 "chat": {"id": 12345678},
597 "text": "hello from telegram"
598 }
599 }
600 ]
601 });
602
603 let allowed = vec![12345678_i64];
604 let (msgs, new_offset) = parse_updates_response(&json, &allowed, 0);
605
606 assert_eq!(msgs.len(), 1);
607 assert_eq!(msgs[0].from_user_id, 12345678);
608 assert_eq!(msgs[0].chat_id, 12345678);
609 assert_eq!(msgs[0].text, "hello from telegram");
610 assert_eq!(new_offset, 123457); }
612
613 #[test]
614 fn parse_updates_unauthorized_user_filtered() {
615 let json: serde_json::Value = serde_json::json!({
616 "ok": true,
617 "result": [
618 {
619 "update_id": 100,
620 "message": {
621 "from": {"id": 99999},
622 "chat": {"id": 99999},
623 "text": "sneaky message"
624 }
625 }
626 ]
627 });
628
629 let allowed = vec![12345678_i64];
630 let (msgs, new_offset) = parse_updates_response(&json, &allowed, 0);
631
632 assert!(msgs.is_empty(), "unauthorized message should be filtered");
633 assert_eq!(new_offset, 101); }
635
636 #[test]
637 fn parse_updates_empty_result() {
638 let json: serde_json::Value = serde_json::json!({
639 "ok": true,
640 "result": []
641 });
642
643 let (msgs, new_offset) = parse_updates_response(&json, &[42], 50);
644 assert!(msgs.is_empty());
645 assert_eq!(new_offset, 50); }
647
648 #[test]
649 fn parse_updates_multiple_messages_mixed_auth() {
650 let json: serde_json::Value = serde_json::json!({
651 "ok": true,
652 "result": [
653 {
654 "update_id": 200,
655 "message": {
656 "from": {"id": 111},
657 "chat": {"id": 111},
658 "text": "authorized msg"
659 }
660 },
661 {
662 "update_id": 201,
663 "message": {
664 "from": {"id": 999},
665 "chat": {"id": 999},
666 "text": "unauthorized msg"
667 }
668 },
669 {
670 "update_id": 202,
671 "message": {
672 "from": {"id": 222},
673 "chat": {"id": 222},
674 "text": "also authorized"
675 }
676 }
677 ]
678 });
679
680 let allowed = vec![111_i64, 222];
681 let (msgs, new_offset) = parse_updates_response(&json, &allowed, 0);
682
683 assert_eq!(msgs.len(), 2);
684 assert_eq!(msgs[0].text, "authorized msg");
685 assert_eq!(msgs[1].text, "also authorized");
686 assert_eq!(new_offset, 203);
687 }
688
689 #[test]
690 fn parse_updates_skips_non_message_updates() {
691 let json: serde_json::Value = serde_json::json!({
692 "ok": true,
693 "result": [
694 {
695 "update_id": 300,
696 "edited_message": {
697 "from": {"id": 42},
698 "chat": {"id": 42},
699 "text": "edited"
700 }
701 }
702 ]
703 });
704
705 let (msgs, new_offset) = parse_updates_response(&json, &[42], 0);
706 assert!(msgs.is_empty());
707 assert_eq!(new_offset, 301);
708 }
709
710 #[test]
711 fn parse_updates_skips_message_without_text() {
712 let json: serde_json::Value = serde_json::json!({
713 "ok": true,
714 "result": [
715 {
716 "update_id": 400,
717 "message": {
718 "from": {"id": 42},
719 "chat": {"id": 42}
720 }
721 }
722 ]
723 });
724
725 let (msgs, new_offset) = parse_updates_response(&json, &[42], 0);
726 assert!(msgs.is_empty());
727 assert_eq!(new_offset, 401);
728 }
729
730 #[test]
731 fn parse_get_me_valid_response() {
732 let json: serde_json::Value = serde_json::json!({
733 "ok": true,
734 "result": {
735 "id": 123456789,
736 "is_bot": true,
737 "first_name": "Test Bot",
738 "username": "test_bot"
739 }
740 });
741 assert_eq!(parse_get_me_response(&json), Some("test_bot".to_string()));
742 }
743
744 #[test]
745 fn parse_get_me_invalid_response() {
746 let json: serde_json::Value = serde_json::json!({
747 "ok": false,
748 "description": "Not Found"
749 });
750 assert_eq!(parse_get_me_response(&json), None);
751 }
752
753 #[test]
754 fn parse_send_message_response_requires_api_acceptance() {
755 let json: serde_json::Value = serde_json::json!({
756 "ok": true,
757 "result": {
758 "message_id": 42
759 }
760 });
761
762 assert_eq!(parse_send_message_response(&json).unwrap(), 42);
763 }
764
765 #[test]
766 fn parse_send_message_response_rejects_missing_confirmation() {
767 let json: serde_json::Value = serde_json::json!({
768 "ok": true,
769 "result": {}
770 });
771
772 assert!(
773 parse_send_message_response(&json)
774 .unwrap_err()
775 .to_string()
776 .contains("message_id")
777 );
778 }
779
780 #[test]
781 fn update_team_yaml_updates_existing_user_role() {
782 let tmp = tempfile::tempdir().unwrap();
783 let path = tmp.path().join("team.yaml");
784 std::fs::write(
785 &path,
786 r#"
787name: test-team
788roles:
789 - name: human
790 role_type: user
791 channel: telegram
792 channel_config:
793 target: "placeholder"
794 provider: openclaw
795 talks_to: [architect]
796 - name: architect
797 role_type: architect
798 agent: claude
799"#,
800 )
801 .unwrap();
802
803 update_team_yaml(&path, "123:abc-token", 99887766).unwrap();
804
805 let content = std::fs::read_to_string(&path).unwrap();
806 assert!(content.contains("123:abc-token"));
807 assert!(content.contains("99887766"));
808 }
809
810 #[test]
811 fn update_team_yaml_creates_user_role_if_missing() {
812 let tmp = tempfile::tempdir().unwrap();
813 let path = tmp.path().join("team.yaml");
814 std::fs::write(
815 &path,
816 r#"
817name: test-team
818roles:
819 - name: architect
820 role_type: architect
821 agent: claude
822"#,
823 )
824 .unwrap();
825
826 update_team_yaml(&path, "456:xyz-token", 11223344).unwrap();
827
828 let content = std::fs::read_to_string(&path).unwrap();
829 assert!(content.contains("456:xyz-token"));
830 assert!(content.contains("11223344"));
831 assert!(content.contains("user"));
832 assert!(content.contains("human"));
833 }
834
835 #[test]
836 fn setup_telegram_bails_without_config() {
837 let tmp = tempfile::tempdir().unwrap();
838 let result = setup_telegram(tmp.path());
839 assert!(result.is_err());
840 assert!(result.unwrap_err().to_string().contains("batty init"));
841 }
842
843 #[test]
844 fn split_message_short_text_returns_single_chunk() {
845 let chunks = split_message("hello", 4096);
846 assert_eq!(chunks, vec!["hello"]);
847 }
848
849 #[test]
850 fn split_message_exact_limit_returns_single_chunk() {
851 let text = "a".repeat(4096);
852 let chunks = split_message(&text, 4096);
853 assert_eq!(chunks.len(), 1);
854 assert_eq!(chunks[0].len(), 4096);
855 }
856
857 #[test]
858 fn split_message_splits_on_newline() {
859 let line = "a".repeat(2000);
860 let text = format!("{line}\n{line}\n{line}");
861 let chunks = split_message(&text, 4096);
862 assert_eq!(chunks.len(), 2);
863 assert!(chunks[0].len() <= 4096);
865 assert!(chunks[1].len() <= 4096);
866 let reassembled: String = chunks.iter().copied().collect();
868 assert_eq!(reassembled, text);
869 }
870
871 #[test]
872 fn split_message_hard_splits_long_line() {
873 let text = "a".repeat(5000);
874 let chunks = split_message(&text, 4096);
875 assert_eq!(chunks.len(), 2);
876 assert_eq!(chunks[0].len(), 4096);
877 assert_eq!(chunks[1].len(), 904);
878 }
879
880 #[test]
881 fn split_message_multiple_chunks() {
882 let text = "a".repeat(10000);
883 let chunks = split_message(&text, 4096);
884 assert_eq!(chunks.len(), 3);
885 assert_eq!(chunks[0].len(), 4096);
886 assert_eq!(chunks[1].len(), 4096);
887 assert_eq!(chunks[2].len(), 1808);
888 let reassembled: String = chunks.iter().copied().collect();
889 assert_eq!(reassembled, text);
890 }
891
892 #[test]
893 fn split_message_empty_text() {
894 let chunks = split_message("", 4096);
895 assert_eq!(chunks, vec![""]);
896 }
897
898 #[test]
899 fn outbound_chunk_message_id_changes_per_chunk() {
900 let text = format!("{}\n{}", "a".repeat(4096), "b".repeat(200));
901 let chunks = split_message(&text, 4096);
902 assert_eq!(chunks.len(), 2);
903
904 let first = outbound_chunk_message_id("12345", chunks[0], 0, chunks.len());
905 let second = outbound_chunk_message_id("12345", chunks[1], 1, chunks.len());
906
907 assert_ne!(first, second);
908 }
909}