main.rs (24109B)
1 use bytes::Bytes; 2 use clap::Parser; 3 use frankenstein::api_params::File; 4 use frankenstein::api_params::GetFileParams; 5 use frankenstein::api_params::InputFile; 6 use frankenstein::api_params::SendDocumentParams; 7 use frankenstein::objects::AllowedUpdate; 8 use frankenstein::objects::UpdateContent; 9 use frankenstein::Api; 10 use frankenstein::ChatId; 11 use frankenstein::GetUpdatesParams; 12 use frankenstein::Message; 13 use frankenstein::SendMessageParams; 14 use frankenstein::TelegramApi; 15 use std::collections::VecDeque; 16 use std::fs::DirEntry; 17 use std::io::Write; 18 use std::{fs, process, thread, time}; 19 mod i18n; 20 use config_file::FromConfigFile; 21 use i18n::I18n; 22 use serde::Deserialize; 23 24 /* 25 * 4096 is the max character length 26 * of the text parameter in the sendMessage 27 * method of the telegram bot api 28 * (char>=1byte ) 29 * see: https://core.telegram.org/bots/api#sendmessage 30 */ 31 const MAX_TEXT_LEN: usize = 4096; 32 33 #[derive(Parser, Debug, Deserialize)] 34 struct Config { 35 #[arg(short, long, help = "telegram bot api token")] 36 token: Option<String>, 37 #[arg(short, long, help = "path to folder with pdf files")] 38 songs_path: Option<String>, 39 #[arg( 40 short, 41 long, 42 help = "language that the bot speaks: 'en', 'de' or 'ro/md'" 43 )] 44 lang: Option<String>, 45 #[arg(short = 'f', long, help = "path to search file")] 46 search_file: Option<String>, 47 #[arg(short, long, help = "path to folder where reports will be saved")] 48 reports_path: Option<String>, 49 #[arg(short, long, help = "path to yml config file")] 50 config: Option<String>, 51 } 52 53 impl Config { 54 pub fn new() -> Self { 55 Self { 56 token: None, 57 songs_path: None, 58 lang: None, 59 search_file: None, 60 reports_path: None, 61 config: None, 62 } 63 } 64 } 65 66 struct SongNotFound { 67 message: String, 68 } 69 70 struct HandleArg { 71 api: Api, 72 msg: Option<Message>, 73 token: String, 74 reports_path: Option<String>, 75 i18n: I18n, 76 songs_path: String, 77 search_file: Option<String>, 78 } 79 80 struct HandleResult { 81 // wait_for_report: bool, 82 user_id_waiting_for_report: Option<u64>, 83 } 84 85 enum OutgoingTextMsg { 86 DirEntry(Vec<DirEntry>), 87 String(Vec<String>), 88 } 89 90 struct FindSongArgs { 91 songs_path: String, 92 i18n: I18n, 93 search_string: String, 94 search_type: SearchType, 95 search_file: String, 96 } 97 98 #[derive(Debug)] 99 enum SearchType { 100 Title, 101 FullText, 102 } 103 104 struct SearchResult { 105 ss_in_title: Vec<String>, 106 ss_in_lyrics: Vec<String>, 107 } 108 109 enum ReportFileType { 110 Voice(Bytes), 111 Text(String), 112 } 113 114 fn main() { 115 let config = get_config(); 116 let api = Api::new(&config.token.clone().unwrap().as_str()); 117 let is_reports_path = config.reports_path.is_some(); 118 let songs_path: String = add_ending_slash(config.songs_path.unwrap()); 119 let mut handle_arg = HandleArg { 120 api: api.clone(), 121 msg: None, 122 token: config.token.unwrap().clone(), 123 reports_path: config.reports_path.clone(), 124 i18n: I18n::new(config.lang.unwrap(), songs_path.clone()), 125 songs_path: songs_path.clone(), 126 search_file: config.search_file.clone(), 127 }; 128 let mut updates_params = GetUpdatesParams::builder() 129 .allowed_updates(vec![AllowedUpdate::Message]) 130 .build(); 131 let mut handle_res: Option<HandleResult>; 132 let mut user_ids_waiting_for_report = vec![]; 133 loop { 134 let dur = time::Duration::from_millis(500); 135 thread::sleep(dur); 136 let result = TelegramApi::get_updates(&api, &updates_params); 137 match result { 138 Ok(val) => { 139 for update in &val.result { 140 updates_params.offset = Some(i64::from(update.update_id) + 1); 141 match &update.content { 142 UpdateContent::Message(msg) => { 143 handle_arg.msg = Some(msg.clone()); 144 if is_reports_path && !user_ids_waiting_for_report.is_empty() { 145 let user_id = msg.from.as_ref().unwrap().id; 146 let mut remove_later: Option<usize> = None; 147 let mut skip = false; 148 for (i, id) in user_ids_waiting_for_report.iter().enumerate() { 149 if user_id == *id { 150 skip = true; 151 if handle_report(&handle_arg) { 152 remove_later = Some(i); 153 } 154 } 155 } 156 if let Some(id) = remove_later { 157 user_ids_waiting_for_report.swap_remove(id); 158 } 159 if skip { 160 continue; 161 } 162 } 163 if msg.text.is_some() { 164 handle_res = handle_text_message(&handle_arg); 165 if let Some(res) = handle_res { 166 if let Some(id) = res.user_id_waiting_for_report { 167 user_ids_waiting_for_report.push(id); 168 } 169 } 170 } 171 } 172 _ => {} 173 } 174 } 175 } 176 Err(_err) => { 177 eprintln!("Error receiving updates from Telegram Bot API."); 178 } 179 } 180 } 181 } 182 183 fn get_config() -> Config { 184 let mut config: Config = Config::new(); 185 let args = Config::parse(); 186 if let Some(conf) = args.config { 187 config = Config::from_config_file(conf).unwrap(); 188 } 189 if args.token.is_some() { 190 config.token = args.token; 191 } 192 if args.songs_path.is_some() { 193 config.songs_path = args.songs_path; 194 } 195 if args.lang.is_some() { 196 config.lang = args.lang; 197 } 198 if args.search_file.is_some() { 199 config.search_file = args.search_file; 200 } 201 if args.reports_path.is_some() { 202 config.reports_path = args.reports_path; 203 } 204 if config.token.is_none() || config.songs_path.is_none() { 205 eprintln!("Provide at least a --token and a --songs-path."); 206 process::exit(-1); 207 } 208 if config.lang.is_none() { 209 config.lang = Some(String::from("en")); 210 } 211 config 212 } 213 214 fn add_ending_slash(path: String) -> String { 215 if !path.ends_with("/") { 216 let mut new_path = path.to_owned(); 217 new_path.push_str("/"); 218 return new_path; 219 } else { 220 return path; 221 } 222 } 223 224 fn handle_text_message(args: &HandleArg) -> Option<HandleResult> { 225 let mut find_song_args = FindSongArgs { 226 search_string: String::new(), 227 songs_path: args.songs_path.clone(), 228 i18n: args.i18n.clone(), 229 search_type: SearchType::Title, 230 search_file: String::new(), 231 }; 232 if let Some(search_file) = args.search_file.as_ref() { 233 if fs::File::open(search_file).is_ok() { 234 find_song_args.search_type = SearchType::FullText; 235 find_song_args.search_file = search_file.to_string(); 236 } 237 } 238 let msg = args.msg.clone().unwrap(); 239 let text: &str = msg.text.as_ref().unwrap(); 240 let chat_id: u64 = msg.from.as_ref().unwrap().id; 241 let mut params = SendMessageParams::builder() 242 .chat_id(ChatId::Integer(chat_id.try_into().unwrap())) 243 .text("") 244 .build(); 245 match text { 246 "/start" => { 247 params.text = (args.i18n.start_msg).to_string(); 248 send_message(&args.api, &mut params); 249 } 250 "/list" => { 251 let songs = get_songs(&args.songs_path, None); 252 params.text = form_msg(OutgoingTextMsg::DirEntry(songs)); 253 send_message(&args.api, &mut params); 254 } 255 "/report" => { 256 params.text = args.i18n.report.msg.clone(); 257 send_message(&args.api, &mut params); 258 return Some(HandleResult { 259 user_id_waiting_for_report: Some(chat_id), 260 }); 261 } 262 _ => { 263 if text.starts_with("/") { 264 for name in i18n::get_folder_names(&args.songs_path) { 265 if text == "/".to_owned() + name.as_str() { 266 let songs = get_songs(&args.songs_path, Some(&name)); 267 params.text = form_msg(OutgoingTextMsg::DirEntry(songs)); 268 send_message(&args.api, &mut params); 269 return None; 270 } 271 } 272 let len = text.as_bytes().len(); 273 find_song_args.search_string = text[1..len].to_string(); 274 match title_search(&find_song_args) { 275 Ok(files) => { 276 let file = files.get(0); 277 let input_file = InputFile::builder().path(file.unwrap().path()).build(); 278 let send_document_params = SendDocumentParams::builder() 279 .chat_id(ChatId::Integer(chat_id.try_into().unwrap())) 280 .document(File::InputFile(input_file)) 281 .build(); 282 send_document(&args.api, &send_document_params); 283 } 284 Err(err) => { 285 eprintln!("{}", err.message); 286 params.text = (args.i18n.song_not_found).to_string(); 287 send_message(&args.api, &mut params); 288 } 289 } 290 } else { 291 find_song_args.search_string = text.to_string(); 292 match find_song_args.search_type { 293 SearchType::Title => match title_search(&find_song_args) { 294 Ok(files) => { 295 params.text = form_msg(OutgoingTextMsg::DirEntry(files)); 296 send_message(&args.api, &mut params); 297 } 298 Err(err) => { 299 eprintln!("{}", err.message); 300 params.text = (args.i18n.song_not_found).to_string(); 301 send_message(&args.api, &mut params); 302 } 303 }, 304 SearchType::FullText => match full_text_search(&find_song_args) { 305 Ok(search_result) => { 306 let mut only_one_result: Option<String> = None; 307 if search_result.ss_in_title.len() == 1 308 && search_result.ss_in_lyrics.len() == 0 309 { 310 only_one_result = Some(search_result.ss_in_title[0].clone()); 311 } else if search_result.ss_in_title.len() == 0 312 && search_result.ss_in_lyrics.len() == 1 313 { 314 only_one_result = Some(search_result.ss_in_lyrics[0].clone()); 315 } 316 if let Some(new_search_string) = only_one_result { 317 find_song_args.search_string = new_search_string; 318 match title_search(&find_song_args) { 319 Ok(files) => { 320 let file = files.get(0); 321 let input_file = 322 InputFile::builder().path(file.unwrap().path()).build(); 323 let send_document_params = SendDocumentParams::builder() 324 .chat_id(ChatId::Integer(chat_id.try_into().unwrap())) 325 .document(File::InputFile(input_file)) 326 .build(); 327 send_document(&args.api, &send_document_params); 328 } 329 Err(err) => { 330 /* 331 This can't be reached in theory 332 because we've already found a song previously 333 */ 334 eprintln!("{}", err.message); 335 params.text = (args.i18n.song_not_found).to_string(); 336 send_message(&args.api, &mut params); 337 } 338 } 339 } else { 340 let ss_in_title = 341 form_msg(OutgoingTextMsg::String(search_result.ss_in_title)); 342 let ss_in_lyrics = 343 form_msg(OutgoingTextMsg::String(search_result.ss_in_lyrics)); 344 params.text.push_str(&ss_in_title); 345 params.text.push_str(&ss_in_lyrics); 346 send_message(&args.api, &mut params); 347 } 348 } 349 Err(err) => { 350 eprintln!("{}", err.message); 351 params.text = (args.i18n.song_not_found).to_string(); 352 send_message(&args.api, &mut params); 353 } 354 }, 355 } 356 } 357 } 358 } 359 return None; 360 } 361 362 fn handle_report(args: &HandleArg) -> bool { 363 let msg = args.msg.clone().unwrap(); 364 let reports_path = args.reports_path.clone().unwrap(); 365 let chat_id: u64 = msg.from.as_ref().unwrap().id; 366 let mut params = SendMessageParams::builder() 367 .chat_id(ChatId::Integer(chat_id.try_into().unwrap())) 368 .text("") 369 .build(); 370 if let Some(voice) = msg.voice { 371 match args.api.get_file(&GetFileParams { 372 file_id: voice.file_id, 373 }) { 374 Ok(file) => { 375 let file_path = file.result.file_path.unwrap(); 376 match download_file(&args.token, &file_path) { 377 Ok(bytes) => save_file(ReportFileType::Voice(bytes), &reports_path), 378 Err(_) => {} 379 } 380 } 381 Err(_) => {} 382 } 383 params.text = args.i18n.report.success_msg.clone(); 384 send_message(&args.api, &mut params); 385 return true; 386 } else if let Some(text) = msg.text { 387 if text == String::from("/cancel") { 388 params.text = String::from(args.i18n.report.cancel_msg.clone()); 389 send_message(&args.api, &mut params); 390 return true; 391 } 392 params.text = args.i18n.report.success_msg.clone(); 393 send_message(&args.api, &mut params); 394 save_file(ReportFileType::Text(text), &reports_path); 395 return true; 396 } else { 397 params.text = args.i18n.report.error_msg.clone(); 398 send_message(&args.api, &mut params); 399 return false; 400 } 401 } 402 403 fn download_file(token: &String, file_path: &String) -> Result<Bytes, reqwest::Error> { 404 let url = format!("https://api.telegram.org/file/bot{token}/{file_path}"); 405 let bytes = reqwest::blocking::get(url)?.bytes()?; 406 return Ok(bytes); 407 } 408 409 fn save_file(t: ReportFileType, reports_path: &String) { 410 let timestamp = chrono::offset::Utc::now().timestamp_millis(); 411 let filepath = reports_path.to_owned() + "/" + ×tamp.to_string(); 412 match t { 413 ReportFileType::Voice(bytes) => match fs::File::create(filepath + ".ogg") { 414 Ok(mut file) => { 415 let _res = file.write(&bytes); 416 } 417 Err(_) => {} 418 }, 419 ReportFileType::Text(mut text) => match fs::File::create(filepath + ".txt") { 420 Ok(mut file) => { 421 text = text + "\n"; 422 let _res = file.write(text.as_bytes()); 423 } 424 Err(_) => {} 425 }, 426 } 427 } 428 429 fn send_document(api: &Api, params: &SendDocumentParams) { 430 let result = api.send_document(params); 431 match result { 432 Err(err) => { 433 eprintln!("send_document failed."); 434 dbg!(err); 435 } 436 Ok(_res) => {} 437 } 438 } 439 440 fn send_message(api: &Api, params: &mut SendMessageParams) { 441 let text_len = params.text.chars().count(); 442 let msg_count = text_len as f64 / MAX_TEXT_LEN as f64; 443 if msg_count <= 1.0 { 444 let result = api.send_message(params); 445 match result { 446 Err(err) => { 447 eprintln!("send_message failed."); 448 dbg!(err); 449 } 450 Ok(_res) => {} 451 } 452 } else { 453 let mut text: String = params.text.clone(); 454 let mut part: &str; 455 loop { 456 if text.chars().count() > MAX_TEXT_LEN { 457 match find_last_line_break(text.clone()) { 458 Ok(index) => { 459 part = &text[..index]; 460 params.text = part.to_string(); 461 let result = api.send_message(params); 462 match result { 463 Err(err) => { 464 eprintln!("send_message failed."); 465 dbg!(err); 466 } 467 Ok(_res) => {} 468 } 469 text = text[index + 1..].to_string(); 470 } 471 Err(_) => { 472 eprintln!("Dude, there's no line break. Deal with it."); 473 } 474 } 475 } else { 476 params.text = text; 477 let result = api.send_message(params); 478 match result { 479 Err(err) => { 480 eprintln!("send_message failed."); 481 dbg!(err); 482 } 483 Ok(_res) => {} 484 } 485 break; 486 } 487 } 488 } 489 } 490 491 fn find_last_line_break(text: String) -> Result<usize, usize> { 492 let mut i: usize = MAX_TEXT_LEN as usize; 493 loop { 494 if i == 0 { 495 return Err(i); 496 } 497 match text.chars().nth(i) { 498 Some(c) => { 499 if c == '\n' { 500 return Ok(i); 501 } 502 } 503 None => { 504 eprintln!("nth error"); 505 } 506 } 507 i -= 1; 508 } 509 } 510 511 fn form_msg(songs: OutgoingTextMsg) -> String { 512 let mut message = String::new(); 513 match songs { 514 OutgoingTextMsg::DirEntry(songs) => { 515 for song in songs { 516 let file_name = song.file_name(); 517 let filename = file_name.to_str().unwrap(); 518 let s: Vec<&str> = filename.split(".").collect(); 519 let name = s.get(0).unwrap(); 520 let mut command: String = "/".to_string(); 521 command.push_str(name); 522 command.push_str("\n"); 523 message.push_str(command.as_str()); 524 } 525 } 526 OutgoingTextMsg::String(songs) => { 527 for song in songs { 528 let mut command = String::from("/"); 529 command.push_str(&song); 530 command.push_str("\n"); 531 message.push_str(command.as_str()); 532 } 533 } 534 } 535 return message; 536 } 537 538 fn title_search(args: &FindSongArgs) -> Result<Vec<DirEntry>, SongNotFound> { 539 let mut exact_match: Option<DirEntry> = None; 540 let mut matches: VecDeque<DirEntry> = VecDeque::new(); 541 let mut filename: String; 542 let ss = args.i18n.format(&args.search_string).to_lowercase(); 543 544 for file in get_songs(&args.songs_path, None) { 545 filename = file.file_name().to_str().unwrap().to_string(); 546 let name = filename 547 .split(".") 548 .collect::<Vec<&str>>() 549 .get(0) 550 .unwrap() 551 .to_string() 552 .to_lowercase(); 553 if name == ss { 554 exact_match = Some(file); 555 } else if name.starts_with(&ss) { 556 matches.push_front(file); 557 } else if name.contains(&ss) { 558 matches.push_back(file); 559 } 560 } 561 let mut result: Vec<DirEntry> = Vec::new(); 562 if let Some(entry) = exact_match { 563 result.push(entry); 564 } 565 result.append(&mut matches.into_iter().collect()); 566 if result.len() > 0 { 567 return Ok(result); 568 } else { 569 return Err(SongNotFound { 570 message: String::from("Didn't find any song."), 571 }); 572 } 573 } 574 575 fn full_text_search(args: &FindSongArgs) -> Result<SearchResult, SongNotFound> { 576 let mut ss_in_title: Vec<String> = vec![]; 577 let mut ss_in_lyrics: Vec<String> = vec![]; 578 let ss = prepare_for_fulltext_search(&args.search_string); 579 let content = fs::read_to_string(&args.search_file).unwrap(); 580 for line in content.lines() { 581 let s_line: Vec<&str> = line.split(':').collect(); 582 let name = s_line.get(0).unwrap(); 583 let song_title = s_line.get(1).unwrap(); 584 let song_lyrics = s_line.get(2).unwrap(); 585 if song_title.starts_with(&ss) { 586 // move found song to the beginning 587 let mut temp = vec![name.to_string()]; 588 temp.append(&mut ss_in_title); 589 ss_in_title = temp; 590 } else if song_title.contains(&ss) { 591 ss_in_title.push(name.to_string()); 592 } else if song_lyrics.contains(&ss) { 593 ss_in_lyrics.push(name.to_string()); 594 } 595 } 596 if ss_in_title.len() == 0 && ss_in_lyrics.len() == 0 { 597 return Err(SongNotFound { 598 message: String::from("Didn't find any song."), 599 }); 600 } else { 601 return Ok(SearchResult { 602 ss_in_title, 603 ss_in_lyrics, 604 }); 605 } 606 } 607 608 fn prepare_for_fulltext_search(string: &String) -> String { 609 let mut res = String::new(); 610 // let mut is_last_line_break = false; 611 for c in string.chars() { 612 if c.is_alphabetic() { 613 res.push(c); 614 // is_last_line_break = false; 615 } 616 /* if c == '\n' || c == ' ' { 617 if !is_last_line_break { 618 res.push(' '); 619 is_last_line_break = true; 620 } 621 } */ 622 } 623 res = res.to_lowercase(); 624 return res; 625 } 626 627 fn get_songs(songs_path: &String, folder_name: Option<&String>) -> Vec<DirEntry> { 628 match folder_name { 629 Some(name) => { 630 return get_files_recursive(&(songs_path.to_owned() + name)); 631 } 632 None => { 633 return get_files_recursive(songs_path); 634 } 635 } 636 } 637 638 fn get_files_recursive(folder_path: &String) -> Vec<DirEntry> { 639 let path = fs::read_dir(folder_path); 640 let mut is_dir: bool; 641 let mut songs: Vec<DirEntry> = vec![]; 642 match path { 643 Ok(read_dir) => { 644 for r in read_dir { 645 match r { 646 Ok(dir_entry) => { 647 is_dir = dir_entry.file_type().unwrap().is_dir(); 648 if is_dir { 649 let path = dir_entry.path().to_str().unwrap().to_string(); 650 for song in get_files_recursive(&path) { 651 songs.push(song); 652 } 653 } else { 654 songs.push(dir_entry); 655 } 656 } 657 Err(err) => { 658 eprintln!("Cannot access filepath."); 659 dbg!(err); 660 } 661 } 662 } 663 } 664 Err(_) => { 665 eprintln!("Cannot open/read or what ever the path {}.", folder_path); 666 } 667 } 668 songs.sort_by_key(|name| name.file_name().into_string().unwrap().to_lowercase()); 669 return songs; 670 }