vortaroboto

Log | Files | Refs | README

commit a70481d1ff12201f0930f84b59282969fc5aa312
parent 9c2cd5f53f3797ebf88596384101de1f989b6637
Author: tomvig38@gmail.com <tomvig38@gmail.com>
Date:   Mon, 25 Oct 2021 07:39:47 +0000

Aldonue statistikoj
Diffstat:
M.gitignore | 1+
MCargo.lock | 118+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MCargo.toml | 1+
Msrc/main.rs | 172++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Asrc/requests.rs | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 304 insertions(+), 76 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -1 +1,2 @@ /target +vorto.bincode diff --git a/Cargo.lock b/Cargo.lock @@ -3,6 +3,36 @@ version = 3 [[package]] +name = "async-mutex" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479db852db25d9dbf6204e6cb6253698f175c15726470f78af0d918e99d6156e" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-rwlock" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "261803dcc39ba9e72760ba6e16d0199b1eef9fc44e81bffabbebb9f5aea3906c" +dependencies = [ + "async-mutex", + "event-listener", +] + +[[package]] +name = "async-trait" +version = "0.1.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "atty" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -53,6 +83,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" [[package]] +name = "cached" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2bc2fd249a24a9cdd4276f3a3e0461713271ab63b0e9e656e200e8e21c8c927" +dependencies = [ + "async-mutex", + "async-rwlock", + "async-trait", + "cached_proc_macro", + "cached_proc_macro_types", + "futures", + "hashbrown", + "once_cell", +] + +[[package]] +name = "cached_proc_macro" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3531903b39df48a378a7ed515baee7c1fff32488489c7d0725eb1749b22a91" +dependencies = [ + "cached_proc_macro_types", + "darling", + "quote", + "syn", +] + +[[package]] +name = "cached_proc_macro_types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a4f925191b4367301851c6d99b09890311d74b0d43f274c0b34c86d308a3663" + +[[package]] name = "cc" version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -89,6 +153,41 @@ dependencies = [ ] [[package]] +name = "darling" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "757c0ded2af11d8e739c4daea1ac623dd1624b06c844cf3f5a39f1bdbd99bb12" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c34d8efb62d0c2d7f60ece80f75e5c63c1588ba68032740494b0b9a996466e3" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade7bff147130fe5e6d39f089c6bd49ec0250f35d70b2eebf72afdfc919f15cc" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] name = "encoding" version = "0.2.33" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -162,6 +261,12 @@ dependencies = [ ] [[package]] +name = "event-listener" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7531096570974c3a9dcf9e4b8e1cede1ec26cf5046219fb3b9d897503b9be59" + +[[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -379,6 +484,12 @@ dependencies = [ ] [[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] name = "idna" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -865,6 +976,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] name = "syn" version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1063,6 +1180,7 @@ name = "vortaroboto" version = "0.1.0" dependencies = [ "bincode", + "cached", "futures", "irc", "log", diff --git a/Cargo.toml b/Cargo.toml @@ -16,3 +16,4 @@ log = "0.4" simple_logger = "1.13.0" bincode = "1.3.3" snap = "1.0.5" +cached = "0.26.2" diff --git a/src/main.rs b/src/main.rs @@ -1,6 +1,6 @@ use futures::prelude::*; use irc::client::prelude::*; -use serde::Deserialize; +use serde::{de::DeserializeOwned, ser::Serialize}; use std::{collections::HashMap, str::FromStr}; use log::{debug, info}; @@ -10,56 +10,28 @@ use simple_logger::SimpleLogger; mod colors; use colors::{ColorKind, Formatted, FormattingKind}; +mod requests; +use requests::{Difino, Traduko, Trovo, Vorto, Vortfarado, difinu, trovu}; + const NICK: &'static str = "vortaroboto"; -const TROVI_URL: &'static str = "http://www.simplavortaro.org/api/v1/trovi"; -const VORTO_URL: &'static str = "http://www.simplavortaro.org/api/v1/vorto"; const LINEO_SEP: &'static str = "\r\n"; -#[derive(Deserialize, Debug)] -struct Vortpartoj { - vorto: Option<String>, - parto: String, -} - -#[derive(Deserialize, Debug)] -struct Vortfarado { - partoj: Vec<Vortpartoj>, - rezulto: String, -} - -#[derive(Deserialize, Debug)] -struct Traduko { - kodo: String, - vorto: Option<String>, - lingvo: String, - traduko: String, -} - -#[derive(Deserialize, Debug)] -struct Trovo { - malpreciza: Vec<String>, - vortfarado: Vec<Vortfarado>, - preciza: Vec<String>, - tradukoj: Vec<Traduko>, -} - -#[derive(Deserialize, Debug)] -struct Ekzemplo { - ekzemplo: String, - fonto: Option<String>, +fn unpack_data<T>(fname: &str) -> Result<T, String> +where + T: DeserializeOwned, +{ + let r = snap::read::FrameDecoder::new(std::fs::File::open(fname).map_err(|e| e.to_string())?); + bincode::deserialize_from(r).map_err(|e| e.to_string()) } -#[derive(Deserialize, Debug)] -struct Difino { - difino: String, - ekzemploj: Vec<Ekzemplo>, - tradukoj: Vec<Traduko>, -} - -#[derive(Deserialize, Debug)] -struct Vorto { - difinoj: Vec<Difino>, - vorto: String, +fn pack_data<T>(fname: &str, data: &T) -> Result<(), String> +where + T: Serialize, +{ + let f = std::fs::File::create(fname).map_err(|e| e.to_string())?; + let w = snap::write::FrameEncoder::new(f); + bincode::serialize_into(w, data).map_err(|e| e.to_string())?; + Ok(()) } enum NumerSelektilo { @@ -84,20 +56,6 @@ impl Default for NumerSelektilo { } } -async fn trovu(vorto: &str) -> Result<Vorto, String> { - reqwest::get(format!("{}/{}", VORTO_URL, vorto)) - .and_then(|j| j.json::<Vorto>()) - .map_err(|e| e.to_string()) - .await -} - -async fn difinu(vorto: &str) -> Result<Trovo, String> { - reqwest::get(format!("{}/{}", TROVI_URL, vorto)) - .and_then(|j| j.json::<Trovo>()) - .map_err(|e| e.to_string()) - .await -} - #[tokio::main] async fn main() -> irc::error::Result<()> { SimpleLogger::new() @@ -140,11 +98,28 @@ async fn main() -> irc::error::Result<()> { }; sender.send_privmsg(target, r)?; } - } else if msg.contains(client.current_nickname()) { - sender.send_privmsg(target, format!( - "Saluton ! Mi estas {} la roboto kiun vi povas demandi pri vortoj kaj tradukoj ! Tajpu !helpu por pli'sciiĝi !", - irc_fmt!(FormattingKind::Bold => client.current_nickname()) - ))?; + continue; + } + + tokio::task::spawn(add_words(msg.to_string())); + + // Donu bonaj respondoj ofte + if msg.contains(client.current_nickname()) { + let msg = match msg.split_ascii_whitespace().next() { + Some("saluton" | "Saluton") => { + format!( + "Saluton ! Mi estas {} la roboto kiun vi povas demandi pri vortoj kaj tradukoj ! Tajpu !helpu por pli'sciiĝi !", + irc_fmt!(FormattingKind::Bold => client.current_nickname()) + ) + } + Some("dankon" | "Dankon") => { + format!("Nedankinde {} !", message.source_nickname().unwrap_or("vi")) + } + _ => { + continue; + } + }; + sender.send_privmsg(target, msg)?; } } _ => (), @@ -154,6 +129,50 @@ async fn main() -> irc::error::Result<()> { Ok(()) } +const VORTO_PATH: &'static str = "vorto.bincode"; +async fn add_words(frazo: String) { + let mut h: HashMap<String, usize> = unpack_data(VORTO_PATH).unwrap_or_default(); + + for vorto in frazo.split_ascii_whitespace() { + if let Ok(tro) = difinu(vorto.to_owned()).await { + if let Some(vf) = tro + .vortfarado + .iter() + .filter(|vf| vf.partoj.len() > 1) + .next() + { + // Konstrui la vera vorto + let mut vorto = String::with_capacity(vorto.len()); + for p in vf.partoj.iter().take_while(|p| p.vorto.is_some()) { + vorto.push_str(p.parto.as_str()); + } + vorto.push('-'); + + if let Some(v) = h.get_mut(&vorto) { + *v += 1; + } else { + h.insert(vorto.to_owned(), 1); + } + } + } + } + + pack_data(VORTO_PATH, &h).unwrap(); // Tre neebla, sed... +} + +fn stats() -> Result<String, String> { + let mut h: HashMap<String, usize> = unpack_data(VORTO_PATH)?; + let mut h: Vec<(String, usize)> = h.drain().collect(); + h.sort_by_cached_key(|(_, s)| *s); + h.reverse(); + + Ok(h.iter() + .take(10) + .map(|(v, o)| format!("{}: {}", v, o)) + .collect::<Vec<String>>() + .join(", ")) +} + macro_rules! parse_or_default { ($t:ty, $arg:ident) => { if let Some(s) = $arg.next() { @@ -207,6 +226,7 @@ async fn handle_command(cmd: &str) -> Option<String> { Some(String::from("Uzo: etimologio {vorto}")) } } + Some("satistikoj" | "stat") => stats().ok(), Some(u) => Some(format!("Mi ne scias kiel respondi al: {}", u)), None => None, } @@ -233,10 +253,13 @@ fn helpu() -> String { &["numero"], "doni la vortfarado [numero] por {vorto}", ), - ("etimolodio", - &["vorto"], - &[], - "Doni la etimolodio de {vorto}") + ( + "etimologio", + &["vorto"], + &[], + "Doni la etimologio de {vorto}", + ), + ("statistiko", &[], &[], "Donu la 10 vortoj plej uzataj.."), ]; fn format_args(am: &[&str], left: char, right: char, c: ColorKind) -> String { @@ -295,10 +318,7 @@ fn helpu() -> String { } async fn etimologio(vorto: &str) -> Result<String, String> { - let r = snap::read::FrameDecoder::new( - std::fs::File::open("etim.bincode").map_err(|e| e.to_string())?, - ); - let h: HashMap<String, String> = bincode::deserialize_from(r).map_err(|e| e.to_string())?; + let h: HashMap<String, String> = unpack_data("etim.bincode")?; Ok(h.get(vorto) .map(|e| e.to_owned()) @@ -306,7 +326,7 @@ async fn etimologio(vorto: &str) -> Result<String, String> { } async fn define_word(vorto: &str, difino: NumerSelektilo) -> Result<String, String> { - let res: Vorto = trovu(vorto).await?; + let res: Vorto = trovu(vorto.to_owned()).await?; fn format_difino(d: &Difino, index: usize, len: usize, vorto: &str, por: bool) -> String { if por { @@ -355,7 +375,7 @@ async fn define_word(vorto: &str, difino: NumerSelektilo) -> Result<String, Stri } async fn traduki(vorto: &str, fonto: Option<&str>) -> Result<String, String> { - let res: Trovo = difinu(vorto).await?; + let res: Trovo = difinu(vorto.to_owned()).await?; let tradukoj = res.tradukoj.iter(); let tradukoj: Vec<String> = if let Some(f) = fonto { @@ -392,7 +412,7 @@ async fn traduki(vorto: &str, fonto: Option<&str>) -> Result<String, String> { } async fn vortfarado(vorto: &str, index: NumerSelektilo) -> Result<String, String> { - let res: Trovo = difinu(vorto).await?; + let res: Trovo = difinu(vorto.to_owned()).await?; let vf = res.vortfarado; diff --git a/src/requests.rs b/src/requests.rs @@ -0,0 +1,88 @@ +use futures::prelude::*; +use serde::{Deserialize, de::DeserializeOwned}; +use cached::proc_macro::cached; + +const TROVI_URL: &'static str = "http://www.simplavortaro.org/api/v1/trovi"; +const VORTO_URL: &'static str = "http://www.simplavortaro.org/api/v1/vorto"; + +#[derive(Deserialize, Debug, Clone)] +pub struct Vortpartoj { + pub vorto: Option<String>, + pub parto: String, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Vortfarado { + pub partoj: Vec<Vortpartoj>, + pub rezulto: String, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Traduko { + pub kodo: String, + pub vorto: Option<String>, + pub lingvo: String, + pub traduko: String, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Trovo { + pub malpreciza: Vec<String>, + pub vortfarado: Vec<Vortfarado>, + pub preciza: Vec<String>, + pub tradukoj: Vec<Traduko>, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Ekzemplo { + pub ekzemplo: String, + pub fonto: Option<String>, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Difino { + pub difino: String, + pub ekzemploj: Vec<Ekzemplo>, + pub tradukoj: Vec<Traduko>, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Vorto { + pub difinoj: Vec<Difino>, + pub vorto: String, +} + + +async fn req<T>(url: &str, vorto: String) -> Result<T, String> +where + T: DeserializeOwned, +{ + reqwest::get(format!("{}/{}", url, vorto.to_lowercase())) + .and_then(|j| j.json::<T>()) + .map_err(|e| e.to_string()) + .await +} + +fn prepare_word(vorto: String) -> String { + vorto + .to_lowercase() + .chars() + .filter(|c| c.is_alphabetic()) + .collect() +} + +macro_rules! q { + ($finternal:ident, $fname:ident, $t:ty, $url:expr) => { + #[cached] + async fn $finternal(vorto: String) -> Result<$t, String> { + req::<$t>($url, vorto).await + } + + pub async fn $fname(vorto: String) -> Result<$t, String> { + $finternal(prepare_word(vorto)).await + } + }; +} + +q! { t_int, trovu, Vorto, VORTO_URL } +q! { d_int, difinu, Trovo, TROVI_URL }