songy

telegram bot as a songbook
git clone git://git.relim.de/songy.git
Log | Files | Refs | README | LICENSE

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() + "/" + &timestamp.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 }