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