vortaroboto

Log | Files | Refs | README

main.rs (17469B)


      1 use chrono::{prelude::*, Duration};
      2 use futures::prelude::*;
      3 use irc::client::prelude::*;
      4 use std::{collections::HashMap, str::FromStr};
      5 use std::thread::sleep;
      6 use std::time::Duration as StdDuration;
      7 
      8 use log::{debug, info, error};
      9 use simple_logger::SimpleLogger;
     10 
     11 #[macro_use]
     12 mod colors;
     13 use colors::{ColorKind, Formatted, FormattingKind};
     14 
     15 mod requests;
     16 use requests::{difinu, trovu, Difino, Traduko, Trovo, Vortfarado, Vorto};
     17 
     18 mod pack;
     19 use pack::{pack_data, unpack_data};
     20 
     21 mod cmds;
     22 use cmds::stats::{add_words, stats};
     23 
     24 mod tradukhelpu;
     25 
     26 const FONTLIGILO: &'static str = "https://git.vigoux.giize.com/vortaroboto/";
     27 const NICK: &'static str = "vortaroboto";
     28 const LINEO_SEP: &'static str = "\r\n";
     29 const DORM_TEMPO: u64 = 500;
     30 
     31 lazy_static::lazy_static!(
     32     static ref TRAD_REG: regex::Regex = regex::Regex::new("\"(?P<lang>[a-z]{3}):(?P<txt>[^\"]+)\"").unwrap();
     33 );
     34 
     35 enum NumerSelektilo {
     36     Cxiuj,
     37     Numero(usize),
     38 }
     39 
     40 impl FromStr for NumerSelektilo {
     41     type Err = String;
     42 
     43     fn from_str(s: &str) -> Result<Self, Self::Err> {
     44         match s {
     45             "*" => Ok(Self::Cxiuj),
     46             s => Ok(Self::Numero(s.parse::<usize>().map_err(|e| e.to_string())?)),
     47         }
     48     }
     49 }
     50 
     51 impl Default for NumerSelektilo {
     52     fn default() -> Self {
     53         Self::Numero(0)
     54     }
     55 }
     56 
     57 #[tokio::main]
     58 async fn main() -> irc::error::Result<()> {
     59     SimpleLogger::new()
     60         .with_level(log::LevelFilter::Debug)
     61         .init()
     62         .unwrap();
     63 
     64     let config = Config {
     65         nickname: Some(NICK.to_owned()),
     66         password: Some(std::env::args().next_back().unwrap()),
     67         server: Some("irc.libera.chat".to_owned()),
     68         channels: vec!["##esperanto".to_owned()],
     69         max_messages_in_burst: Some(5),
     70         burst_window_length: Some(1),
     71         // ping_timeout: Some(600),
     72         ..Config::default()
     73     };
     74 
     75     loop {
     76 
     77         let mut client = Client::from_config(config.clone()).await;
     78         while let Err(_) = client {
     79             client = Client::from_config(config.clone()).await;
     80             error!("Could not connect the client.");
     81             sleep(StdDuration::from_millis(1000));
     82         }
     83 
     84         let mut client = if let Ok(c) = client {
     85             c
     86         } else {
     87             unreachable!();
     88         };
     89 
     90         while let Err(_) = client.identify() {
     91             error!("Could not identify.");
     92             sleep(StdDuration::from_millis(1000));
     93         };
     94 
     95         let mut stream = client.stream()?;
     96         let sender = client.sender();
     97 
     98         loop {
     99             match stream.next().await.transpose() {
    100                 Ok(Some(message)) => {
    101                     debug!("{}", message);
    102                     match message.command {
    103                         Command::PRIVMSG(ref target, ref msg) => {
    104                             let is_channel = target.starts_with('#');
    105                             let target = if is_channel {
    106                                 target
    107                             } else {
    108                                 message.source_nickname().unwrap_or(target)
    109                             };
    110 
    111                             if let Some(cmd) = msg.strip_prefix('!') {
    112                                 if let Some(answer) = handle_command(target, cmd).await {
    113                                     let r = match (is_channel, message.source_nickname()) {
    114                                         (true, Some(n)) => format!("{}:\r\n{}", n, answer),
    115                                         _ => answer,
    116                                     };
    117                                     sender.send_privmsg(target, r)?;
    118                                 }
    119                                 continue;
    120                             }
    121 
    122                             // TODO: make this local to a channel / query
    123                             tokio::task::spawn(add_words(msg.to_string()));
    124                             if let Some(nomo) = message.source_nickname() {
    125                                 vidita_update(target, nomo, msg);
    126                             }
    127 
    128                             // Donu bonaj respondoj ofte
    129                             if msg.contains(client.current_nickname()) {
    130                                 let msg = match msg.split_ascii_whitespace().next() {
    131                                     Some("saluton" | "Saluton") => {
    132                                         format!(
    133                                 "Saluton ! Mi estas {} la roboto kiun vi povas demandi pri vortoj kaj tradukoj ! Tajpu !helpu por pli'sciiĝi !",
    134                                 irc_fmt!(FormattingKind::Bold => client.current_nickname())
    135                                 )
    136                                     }
    137                                     Some("dankon" | "Dankon") => {
    138                                         format!(
    139                                             "Nedankinde {} !",
    140                                             message.source_nickname().unwrap_or("vi")
    141                                         )
    142                                     }
    143                                     _ => {
    144                                         continue;
    145                                     }
    146                                 };
    147                                 sender.send_privmsg(target, msg)?;
    148                             } else {
    149                                 for m in TRAD_REG.captures_iter(msg.as_str()) {
    150                                     let srclang = m.name("lang").unwrap().as_str();
    151                                     let txt = m.name("txt").unwrap().as_str();
    152 
    153                                     if let Ok(trad) = tradukhelpu::trad(txt, srclang).await {
    154                                         sender.send_privmsg(target, format!("{}: por diri \"{}\" en esperanto vi povus diri \"{}\"", message.source_nickname().unwrap(), txt, trad));
    155                                     }
    156                                 }
    157                             }
    158                         }
    159                         _ => (),
    160                     }
    161                 }
    162                 Ok(None) => {},
    163                 Err(irc::error::Error::PingTimeout) => {
    164                     info!("Ping timeout, reconnecting...");
    165                     sleep(StdDuration::from_millis(DORM_TEMPO));
    166                     break;
    167                 }
    168                 Err(e) => {
    169                     error!("{}", e.to_string());
    170                     break;
    171                 }
    172             }
    173         }
    174     }
    175 }
    176 
    177 const VIDITA_PATH: &'static str = "vidita.bincode";
    178 fn vidita_update(channel: &str, nomo: &str, msg: &str) {
    179     let mut h: HashMap<String, HashMap<String, (String, DateTime<Utc>)>> =
    180         unpack_data(VIDITA_PATH).unwrap_or_default();
    181 
    182     // Make a little dance to create the channel index if it's not here yet
    183     let inner = if let Some(inner) = h.get_mut(channel) {
    184         inner
    185     } else {
    186         h.insert(channel.to_owned(), HashMap::default());
    187         h.get_mut(channel).unwrap()
    188     };
    189 
    190     inner.insert(nomo.to_owned(), (msg.to_owned(), Utc::now()));
    191 
    192     pack_data(VIDITA_PATH, &h).unwrap();
    193 }
    194 
    195 fn formato_dauxro(dato: Duration) -> String {
    196     let inter: Vec<(i64, &'static str)> = vec![
    197         (dato.num_days(), "tago"),
    198         (dato.num_hours() % 24, "horo"),
    199         (dato.num_minutes() % 60, "minuto"),
    200         (dato.num_seconds() % 60, "sekondo"),
    201     ];
    202 
    203     let inter: Vec<String> = inter
    204         .iter()
    205         .filter_map(|(n, v)| {
    206             if *n == 1 {
    207                 Some(format!("{} {}", n, v))
    208             } else if *n > 1 {
    209                 Some(format!("{} {}j", n, v))
    210             } else {
    211                 None
    212             }
    213         })
    214         .collect();
    215 
    216     inter.join(" ")
    217 }
    218 
    219 fn vidita_display(channel: &str, nomo: &str) -> Option<String> {
    220     if nomo == NICK {
    221         return Some(format!(
    222             "Eh, mi {} {}!",
    223             irc_fmt!(FormattingKind::Italics => "estas"),
    224             NICK
    225         ));
    226     }
    227     let h: HashMap<String, HashMap<String, (String, DateTime<Utc>)>> =
    228         unpack_data(VIDITA_PATH).ok()?;
    229     let inner = h.get(channel)?;
    230     let (msg, time) = inner.get(nomo)?;
    231 
    232     Some(format!(
    233         "{} estis laste vidita antaŭ {}: '{}'",
    234         nomo,
    235         formato_dauxro(Utc::now() - *time),
    236         msg
    237     ))
    238 }
    239 
    240 macro_rules! parse_or_default {
    241     ($t:ty, $arg:ident) => {
    242         if let Some(s) = $arg.next() {
    243             s.parse::<$t>().unwrap_or_default()
    244         } else {
    245             <$t>::default()
    246         }
    247     };
    248 }
    249 
    250 macro_rules! wrap_handler {
    251     ($f:ident; $w:expr; $($args:expr),*) => {
    252         match $f($w, $($args),*).await {
    253             Ok(r) => Some(r),
    254             Err(e) => Some(format!("Nenio trovata pri: {} ({})", $w, e.to_string())),
    255         }
    256     };
    257 }
    258 
    259 async fn handle_command(source: &str, cmd: &str) -> Option<String> {
    260     let mut splitted = cmd.split_ascii_whitespace();
    261     match splitted.next() {
    262         Some("helpu" | "h") => Some(helpu()),
    263         Some("difinu" | "d") => {
    264             if let Some(w) = splitted.next() {
    265                 let index = parse_or_default!(NumerSelektilo, splitted);
    266                 wrap_handler!(define_word; w; index)
    267             } else {
    268                 Some(String::from("Uzo: difinu [vorto] [numero]"))
    269             }
    270         }
    271         Some("traduku" | "trad" | "t") => {
    272             if let Some(w) = splitted.next() {
    273                 wrap_handler!(traduki; w; splitted.next())
    274             } else {
    275                 Some(String::from("Uzo: traduku [vorto] [lingv'kodo]"))
    276             }
    277         }
    278         Some("vortfarado" | "v" | "vf") => {
    279             if let Some(w) = splitted.next() {
    280                 let index = parse_or_default!(NumerSelektilo, splitted);
    281                 wrap_handler!(vortfarado; w; index)
    282             } else {
    283                 Some(String::from("Uzo: vortfarado {vorto} [numero]"))
    284             }
    285         }
    286         Some("etimologio" | "etim" | "e") => {
    287             if let Some(w) = splitted.next() {
    288                 wrap_handler!(etimologio; w;)
    289             } else {
    290                 Some(String::from("Uzo: etimologio {vorto}"))
    291             }
    292         }
    293         Some("statistikoj" | "stat") => stats().ok(),
    294         Some("vidita" | "vid") => {
    295             if let Some(w) = splitted.next() {
    296                 vidita_display(source, w).or_else(|| Some(format!("Neniam vidita {}.", w)))
    297             } else {
    298                 Some(String::from("Uzo: vidita {uzanto}"))
    299             }
    300         }
    301         Some("fontkodo") => {
    302             Some(format!("Mi fontkodo estas tie: {}", FONTLIGILO))
    303         }
    304         Some(u) => Some(format!("Mi ne scias kiel respondi al: {}", u)),
    305         None => None,
    306     }
    307 }
    308 
    309 fn helpu() -> String {
    310     const KOMANDOJ: &[(&'static str, &[&'static str], &[&'static str], &'static str)] = &[
    311         ("helpu", &[], &[], "montri ĉi tiun mesaĝon"),
    312         (
    313             "difinu",
    314             &["vorto"],
    315             &["numero"],
    316             "difini {vorto}, uzas la eblan [numero] difinon",
    317         ),
    318         (
    319             "traduku",
    320             &["vorto"],
    321             &["lingvokodo"],
    322             "traduki {vorto}, uzas la eblan [numero]",
    323         ),
    324         (
    325             "vortfarado",
    326             &["vorto"],
    327             &["numero"],
    328             "doni la vortfaradon [numero] por {vorto}",
    329         ),
    330         (
    331             "etimologio",
    332             &["vorto"],
    333             &[],
    334             "Doni la etimologion de {vorto}",
    335         ),
    336         ("statistiko", &[], &[], "Donu la 10 vortojn plej uzatajn."),
    337         (
    338             "vidita",
    339             &["unzanto"],
    340             &[],
    341             "Donu la lastan fojon kiam uzanto estis vidita.",
    342         ),
    343     ];
    344 
    345     fn format_args(am: &[&str], left: char, right: char, c: ColorKind) -> String {
    346         am.iter()
    347             .map(|a| {
    348                 format!(
    349                     "{}{}{}",
    350                     left,
    351                     irc_fmt!(FormattingKind::Color { fg: c, bg: None } => a),
    352                     right,
    353                 )
    354             })
    355             .collect::<Vec<String>>()
    356             .join(" ")
    357     }
    358 
    359     let lineoj: Vec<String> = KOMANDOJ
    360         .iter()
    361         .map(|e| match e {
    362             (k, &[], &[], h) => {
    363                 format!(
    364                     "{}: {}",
    365                     irc_fmt!(FormattingKind::Color {fg: ColorKind::Green, bg: None} => k),
    366                     h
    367                 )
    368             }
    369             (k, am, &[], h) => {
    370                 format!(
    371                     "{} {}: {}",
    372                     irc_fmt!(FormattingKind::Color {fg: ColorKind::Green, bg: None} => k),
    373                     format_args(am, '{', '}', ColorKind::Red),
    374                     h
    375                 )
    376             }
    377             (k, &[], ao, h) => {
    378                 format!(
    379                     "{} {}: {}",
    380                     irc_fmt!(FormattingKind::Color {fg: ColorKind::Green, bg: None} => k),
    381                     format_args(ao, '[', ']', ColorKind::Blue),
    382                     h
    383                 )
    384             }
    385             (k, am, ao, h) => {
    386                 format!(
    387                     "{} {} {}: {}",
    388                     irc_fmt!(FormattingKind::Color {fg: ColorKind::Green, bg: None} => k),
    389                     format_args(am, '{', '}', ColorKind::Red),
    390                     format_args(ao, '[', ']', ColorKind::Blue),
    391                     h
    392                 )
    393             }
    394         })
    395         .collect();
    396 
    397     lineoj.join(LINEO_SEP)
    398 }
    399 
    400 async fn etimologio(vorto: &str) -> Result<String, String> {
    401     let h: HashMap<String, String> = unpack_data("etim.bincode")?;
    402 
    403     Ok(h.get(vorto)
    404         .map(|e| e.to_owned())
    405         .unwrap_or(format!("Nenio trovata por {}", vorto)))
    406 }
    407 
    408 async fn define_word(vorto: &str, difino: NumerSelektilo) -> Result<String, String> {
    409     let res: Vorto = trovu(vorto.to_owned()).await?;
    410 
    411     fn format_difino(d: &Difino, index: usize, len: usize, vorto: &str, por: bool) -> String {
    412         if por {
    413             format!(
    414                 "Difino {} el {} por {}: {}",
    415                 index + 1,
    416                 len,
    417                 vorto,
    418                 d.difino
    419             )
    420         } else {
    421             format!("Difino {} el {}: {}", index + 1, len, d.difino)
    422         }
    423     }
    424 
    425     if res.difinoj.is_empty() {
    426         return Err(format!("{} ne havas difinon.", vorto));
    427     }
    428 
    429     if res.difinoj.len() == 1 {
    430         if let Some(d) = res.difinoj.get(0) {
    431             return Ok(format!("Nura difino por {}: {}", vorto, d.difino));
    432         } else {
    433             unreachable!();
    434         }
    435     }
    436 
    437     if let NumerSelektilo::Numero(i) = difino {
    438         let index = i.clamp(1, res.difinoj.len()) - 1;
    439         if let Some(d) = res.difinoj.get(index) {
    440             Ok(format_difino(d, index, res.difinoj.len(), vorto, true))
    441         } else {
    442             unreachable!();
    443         }
    444     } else {
    445         let difj = res
    446             .difinoj
    447             .iter()
    448             .enumerate()
    449             .map(|(index, d)| format_difino(d, index, res.difinoj.len(), vorto, false))
    450             .collect::<Vec<String>>()
    451             .join(LINEO_SEP);
    452 
    453         Ok(format!("Difinoj por {}:\r\n{}", vorto, difj))
    454     }
    455 }
    456 
    457 async fn traduki(vorto: &str, fonto: Option<&str>) -> Result<String, String> {
    458     let res: Trovo = difinu(vorto.to_owned()).await?;
    459 
    460     let tradukoj = res.tradukoj.iter();
    461     let tradukoj: String = if let Some(f) = fonto {
    462         tradukoj
    463             .filter_map(|t: &Traduko| {
    464                 if let Some(v) = &t.vorto {
    465                     if f == t.kodo {
    466                         Some(v.clone())
    467                     } else {
    468                         None
    469                     }
    470                 } else {
    471                     None
    472                 }
    473             })
    474             .collect::<Vec<String>>()
    475             .join(", ")
    476     } else {
    477         tradukoj
    478             .filter_map(|t: &Traduko| {
    479                 if let Some(v) = &t.vorto {
    480                     Some(format!("{} \"{}\": {}", t.lingvo, t.traduko, v))
    481                 } else {
    482                     None
    483                 }
    484             })
    485             .collect::<Vec<String>>()
    486             .join(LINEO_SEP)
    487     };
    488 
    489     if tradukoj.is_empty() {
    490         return Err(format!("Neniu traduko trovita por {}", vorto));
    491     }
    492 
    493     Ok(tradukoj)
    494 }
    495 
    496 async fn vortfarado(vorto: &str, index: NumerSelektilo) -> Result<String, String> {
    497     let res: Trovo = difinu(vorto.to_owned()).await?;
    498 
    499     let vf = res.vortfarado;
    500 
    501     if vf.is_empty() {
    502         return Err(format!("{} ne havas vort'faradon.", vorto));
    503     }
    504 
    505     fn make_partoj(v: &Vortfarado) -> String {
    506         v.partoj
    507             .iter()
    508             .map(|p| {
    509                 if let Some(pv) = &p.vorto {
    510                     format!("{} ({})", p.parto, pv)
    511                 } else {
    512                     p.parto.to_owned()
    513                 }
    514             })
    515             .collect::<Vec<String>>()
    516             .join(" + ")
    517     }
    518 
    519     if let NumerSelektilo::Numero(i) = index {
    520         let index = i.clamp(1, vf.len()) - 1;
    521 
    522         if let Some(v) = vf.get(index) {
    523             let v = make_partoj(v);
    524             Ok(format!(
    525                 "Vortfarado {} el {} por \"{}\":\r\n{}",
    526                 index + 1,
    527                 vf.len(),
    528                 vorto,
    529                 v
    530             ))
    531         } else {
    532             unreachable!();
    533         }
    534     } else {
    535         // Donu cxiuj trancxajxoj
    536         let vj = vf
    537             .iter()
    538             .enumerate()
    539             .map(|(index, v)| format!("{}: {}", index + 1, make_partoj(v)))
    540             .collect::<Vec<String>>()
    541             .join(LINEO_SEP);
    542         Ok(format!("Vortfaradoj por \"{}\":\r\n{}", vorto, vj))
    543     }
    544 }