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 }