diff --git a/Cargo.lock b/Cargo.lock index 77e95d1..0a528e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -391,9 +391,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "js-sys", @@ -613,6 +613,40 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + [[package]] name = "date_header" version = "1.0.5" @@ -690,6 +724,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", + "serde_core", ] [[package]] @@ -788,6 +823,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "ed25519" version = "2.2.3" @@ -1135,6 +1176,20 @@ dependencies = [ "web-sys", ] +[[package]] +name = "grist-client" +version = "0.0.1" +source = "git+https://github.com/QazCetelic/grist-client-rs#a5e41d8a0debbb06bf2ff87c0a8aa80734251a32" +dependencies = [ + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serde_with", + "url", + "uuid", +] + [[package]] name = "growable-bloom-filter" version = "2.1.1" @@ -1159,13 +1214,19 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", "tracing", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.15.5" @@ -1226,6 +1287,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hkdf" version = "0.12.4" @@ -1496,6 +1563,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.1.0" @@ -1571,6 +1644,17 @@ dependencies = [ "quote", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -1878,7 +1962,7 @@ dependencies = [ "gloo-timers", "http", "imbl", - "indexmap", + "indexmap 2.14.0", "itertools 0.14.0", "js_int", "language-tags", @@ -2141,6 +2225,16 @@ version = "0.1.54" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbf6f36070878c42c5233846cd3de24cf9016828fd47bc22957a687298bb21fc" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2676,6 +2770,26 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "regex" version = "1.12.3" @@ -2725,6 +2839,7 @@ dependencies = [ "hyper-util", "js-sys", "log", + "mime_guess", "native-tls", "percent-encoding", "pin-project-lite", @@ -2832,7 +2947,7 @@ dependencies = [ "form_urlencoded", "getrandom 0.2.17", "http", - "indexmap", + "indexmap 2.14.0", "js-sys", "js_int", "konst", @@ -2861,7 +2976,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dbdeccb62cb4ffe3282325de8ba28cbc0fdce7c78a3f11b7241fbfdb9cb9907" dependencies = [ "as_variant", - "indexmap", + "indexmap 2.14.0", "js_int", "js_option", "percent-encoding", @@ -3044,6 +3159,30 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -3148,7 +3287,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f" dependencies = [ "form_urlencoded", - "indexmap", + "indexmap 2.14.0", "itoa", "ryu", "serde_core", @@ -3178,6 +3317,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "serde_spanned" version = "1.1.1" @@ -3199,6 +3349,38 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "sha1" version = "0.10.6" @@ -3235,10 +3417,12 @@ name = "shift_bot" version = "0.1.0" dependencies = [ "anyhow", + "chrono", "clap", "dirs", "either", "futures-util", + "grist-client", "log", "matrix-sdk", "pulldown-cmark", @@ -3666,7 +3850,7 @@ version = "0.25.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" dependencies = [ - "indexmap", + "indexmap 2.14.0", "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "winnow 1.0.3", @@ -4076,7 +4260,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap", + "indexmap 2.14.0", "wasm-encoder", "wasmparser", ] @@ -4120,7 +4304,7 @@ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags", "hashbrown 0.15.5", - "indexmap", + "indexmap 2.14.0", "semver", ] @@ -4353,7 +4537,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck", - "indexmap", + "indexmap 2.14.0", "prettyplease", "syn 2.0.117", "wasm-metadata", @@ -4384,7 +4568,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", "bitflags", - "indexmap", + "indexmap 2.14.0", "log", "serde", "serde_derive", @@ -4403,7 +4587,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap", + "indexmap 2.14.0", "log", "semver", "serde", diff --git a/Cargo.toml b/Cargo.toml index a937ed8..e0bda78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,3 +21,5 @@ log = "^0.4" simple_logger = "^5.2" either = "^1.15" shlex = "2.0.1" +grist-client = { git = "https://github.com/QazCetelic/grist-client-rs", version = "0.0.1" } +chrono = "0.4.45" diff --git a/src/basic/conf.rs b/src/basic/conf.rs index d0702d5..ba1aac4 100644 --- a/src/basic/conf.rs +++ b/src/basic/conf.rs @@ -64,10 +64,12 @@ pub fn get_serve() -> Arc { pub struct Main { /// The data directory. static_dir: PathBuf, + /// Grist API KEY + grist_api_key: String, } impl Main { /// Constructor. - pub fn new() -> Self { + pub fn new(grist_api_key: impl Into) -> Self { let static_dir_root = match dirs::data_dir().context("no `static_dir` directory found") { Ok(root) => root, Err(e) => { @@ -77,6 +79,7 @@ impl Main { }; Self { static_dir: static_dir_root.join("shift_bot"), + grist_api_key: grist_api_key.into(), } } @@ -96,11 +99,16 @@ impl Main { pub fn session_file(&self) -> PathBuf { self.from_static_dir(Self::SESSION_FILE_NAME) } + + /// Grist API KEY + pub fn grist_api_key(&self) -> &str { + &self.grist_api_key + } } impl Default for Main { fn default() -> Self { - Self::new() + Self::new("") } } diff --git a/src/command.rs b/src/command.rs index 70d4088..b62ad39 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,5 +1,4 @@ -use std::fmt::Write; - +use crate::grist::Shift; use clap::{CommandFactory, Parser, Subcommand}; use matrix_sdk::{Client, Room}; @@ -27,19 +26,37 @@ enum Action { Aide, } -/// Sends a message to a room. pub async fn send_room_msg(_client: Client, room: Room, content: String) -> Res { use matrix_sdk::ruma::events::room::message::RoomMessageEventContent; - // let md_parser = pulldown_cmark::Parser::new(&content); - // let mut html_body = String::with_capacity(250); - // pulldown_cmark::html::push_html(&mut html_body, md_parser); - // let content = RoomMessageEventContent::text_html(content, html_body); let content = RoomMessageEventContent::text_plain(content); let sent = room.send(content).await?; Ok(sent.event_id) } -// pub fn handle(command: impl Into) -> Res<()> { +pub async fn send_room_msg_html( + _client: Client, + room: Room, + html_body: String, +) -> Res { + use matrix_sdk::ruma::events::room::message::RoomMessageEventContent; + // let md_parser = pulldown_cmark::Parser::new(&content); + // let mut html_body = String::with_capacity(250); + // pulldown_cmark::html::push_html(&mut html_body, md_parser); + let content = RoomMessageEventContent::text_html(html_body.clone(), html_body); + let sent = room.send(content).await?; + Ok(sent.event_id) +} + +pub fn format_shifts(shifts: Vec) -> String { + let mut output = String::new(); + output.push_str("Shifts :\n
    "); + for shift in shifts { + output.push_str(format!("
  • {}
  • ", shift).as_ref()); + } + output.push_str("
"); + output +} + pub async fn handle(command: impl AsRef, room: Room, client: Client) -> Res<()> { debug!("handling command: {}", command.as_ref()); @@ -50,6 +67,9 @@ pub async fn handle(command: impl AsRef, room: Room, client: Client) -> Res match clap.action { Action::Lister => { debug!("user asked for list"); + let shifts = crate::grist::list_shifts().await?; + debug!("{:?}", shifts); + send_room_msg_html(client, room, format_shifts(shifts)).await?; } Action::Ajouter { nom, infos } => { debug!("user asked for add"); diff --git a/src/grist.rs b/src/grist.rs new file mode 100644 index 0000000..ed1663f --- /dev/null +++ b/src/grist.rs @@ -0,0 +1,220 @@ +prelude! {} + +use chrono::DateTime; +use grist_client::apis::configuration::Configuration; +use grist_client::apis::records_api::list_records; +use grist_client::models::{RecordsList, RecordsListRecordsInner}; + +pub const API_URL: &str = "https://grist.interhacker.space/api"; +pub const SHIFTS_DOC: &str = "iwSaC82TsQiuD3MYSG9RXX"; +pub const SHIFTS_TABLE: &str = "Shifts"; +pub const SHIFT_TYPES_TABLE: &str = "Types_de_shifts"; +pub const INSCRIPTIONS_TABLE: &str = "Inscriptions"; + +#[derive(Clone, Debug)] +pub enum Priority { + Prioritaire, + Secondaire, + Bonus, + Urgent, +} + +impl Display for Priority { + fn fmt(&self, f: &mut Fmt<'_>) -> FmtRes { + match self { + Priority::Prioritaire => write!(f, "Prioritaire"), + Priority::Secondaire => write!(f, "Secondaire"), + Priority::Bonus => write!(f, "Bonus"), + Priority::Urgent => write!(f, "Prioritaire"), + } + } +} + +#[derive(Clone, Debug)] +pub struct ShiftType { + pub name: String, + pub priority: Priority, + pub frequency: u64, + pub persons: u64, + pub duration: u64, + pub guide: String, +} +impl ShiftType { + pub fn new( + name: impl Into, + priority: Priority, + frequency: u64, + persons: u64, + duration: u64, + guide: impl Into, + ) -> Self { + Self { + name: name.into(), + priority: priority, + frequency: frequency, + persons: persons, + duration: duration, + guide: guide.into(), + } + } +} + +impl Display for ShiftType { + fn fmt(&self, f: &mut Fmt<'_>) -> FmtRes { + write!( + f, + "{} ({}): {} x/j, {} personnes, {} h", + self.name, self.priority, self.frequency, self.persons, self.duration + ) + } +} + +// {"Autres_infos": Null, "Heure_de_debut": Number(1782144000), "Inscriptions": Array [String("L"), Number(1)], "Type_de_shift": Number(23)} +#[derive(Clone, Debug)] +pub struct Shift { + pub shift_type: ShiftType, + pub start_time: u64, + pub inscriptions: Vec, +} +impl Shift { + pub fn new(shift_type: ShiftType, start_time: u64, inscriptions: Vec) -> Self { + Self { + shift_type: shift_type, + start_time: start_time.into(), + inscriptions: inscriptions, + } + } +} + +impl Display for Shift { + fn fmt(&self, f: &mut Fmt<'_>) -> FmtRes { + let start_time = DateTime::from_timestamp(self.start_time as i64, 0) + .expect("failed to parse timestamp") + .format("%H:%M"); + write!( + f, + "{} ({}): à {}, durée {} h, inscrit·es : {}/{}", + self.shift_type.name, + self.shift_type.priority, + start_time, + self.shift_type.duration, + self.inscriptions.len(), + self.shift_type.persons + ) + } +} + +pub fn get_number(record: &RecordsListRecordsInner, column: &str) -> Res { + record + .fields + .get(column) + .ok_or(anyhow!("Failed to get column {}", column))? + .as_u64() + .ok_or(anyhow!("Failed to parse number")) +} + +pub fn get_string(record: &RecordsListRecordsInner, column: &str) -> Res { + record + .fields + .get(column) + .ok_or(anyhow!("Failed to get column {}", column))? + .as_str() + .ok_or(anyhow!("Failed to parse number")) + .map(|s| s.into()) +} + +pub fn get_priority(record: &RecordsListRecordsInner, column: &str) -> Res { + match get_string(record, column)? { + s if s.starts_with("1") => Ok(Priority::Prioritaire), + s if s.starts_with("2") => Ok(Priority::Secondaire), + s if s.starts_with("3") => Ok(Priority::Bonus), + s if s.starts_with("0") => Ok(Priority::Urgent), + _ => Err(anyhow!("can't parse priority")), + } +} + +pub fn get_number_array(record: RecordsListRecordsInner, column: &str) -> Res> { + let val_col = record + .fields + .get(column) + .ok_or(anyhow!("Failed to get column {}", column))?; + if val_col.is_null() { + return Ok(vec![]); + } + let mut val_array = val_col + .as_array() + .ok_or(anyhow!("Failed to parse array"))? + .clone(); + val_array.remove(0); + debug!("{:?}", val_array); + let mut u64_array = Vec::new(); + for val in val_array { + let u = val.as_u64().ok_or(anyhow!("Failed to parse number"))?; + u64_array.push(u); + } + Ok(u64_array) +} + +pub async fn get_records(doc: &str, table: &str) -> Res { + let conf = conf::get(); + let api_key = conf.grist_api_key(); + let grist_conf = Configuration::new(API_URL.into(), Some(api_key.into())); + list_records(&grist_conf, doc, table, None, None, None, None, None, None) + .await + .context("Failed to list records") +} + +pub async fn get_shift_type(index: u64) -> Res { + let record_list = get_records(SHIFTS_DOC, SHIFT_TYPES_TABLE).await?; + let record = record_list + .records + .iter() + .find(|record| record.id == index) + .ok_or(anyhow!("can't find shift type"))?; + Ok(ShiftType::new( + get_string(&record, "Nom")?, + get_priority(&record, "Priorite")?, + get_number(&record, "Frequence_jour")?, + get_number(&record, "Personnes_shift")?, + get_number(&record, "Duree_h_")?, + get_string(&record, "Guide")?, + )) +} + +pub async fn get_inscription(index: u64) -> Res { + let record_list = get_records(SHIFTS_DOC, INSCRIPTIONS_TABLE).await?; + let record = record_list + .records + .iter() + .find(|record| record.id == index) + .ok_or(anyhow!("can't find inscription"))? + .clone(); + let name = get_string(&record, "Personne")?.into(); + Ok(name) +} + +pub async fn list_shifts() -> Res> { + let record_list = get_records(SHIFTS_DOC, SHIFTS_TABLE).await?; + debug!("{:?}", record_list); + let mut shifts: Vec = Vec::new(); + for record in record_list.records { + let shift_type_ref = + get_number(&record, "Type_de_shift").context("getting shift type ref")?; + let shift_type = get_shift_type(shift_type_ref).await?; + let start_time = get_number(&record, "Heure_de_debut")?; + let inscriptions_refs = + get_number_array(record, "Inscriptions").context("getting inscriptions refs")?; + // let inscriptions = inscriptions_refs + // .iter() + // .filter_map(async |i| get_inscription(i).await.ok()) + // .collect(); + let mut inscriptions = Vec::new(); + for inscription_ref in inscriptions_refs { + inscriptions.push(get_inscription(inscription_ref).await?); + } + let shift = Shift::new(shift_type, start_time, inscriptions); + shifts.push(shift); + } + + Ok(shifts) +} diff --git a/src/lib.rs b/src/lib.rs index 164e65d..0cda956 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,6 +41,8 @@ pub mod serve; pub mod command; +pub mod grist; + prelude! {} /// Warmup, runs before anything. diff --git a/src/main.rs b/src/main.rs index f499cb4..37c6ed8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,7 @@ WARNING: The contents of this file may NOT be used to train AI/LLM models. See N details. */ -use clap::{Parser}; +use clap::Parser; use shift_bot::*; @@ -39,6 +39,7 @@ struct Clap { default_value = "shift_bot_dev" )] username: String, + /// Homeserver. #[arg( short = 's', @@ -47,9 +48,14 @@ struct Clap { default_value = "matrix.org" )] homeserver: String, + /// Password of the element bot account. #[arg(short, long, value_name = "PASSWORD")] password: String, + + /// Grist API KEY + #[arg(short, long)] + grist_api_key: String, } impl Clap { @@ -69,7 +75,7 @@ impl Clap { async fn run(self) -> Res<()> { simple_logger::init_with_level(self.verb_level()) .context("something when wrong while initializing logger")?; - conf::set(conf::Main::new()); + conf::set(conf::Main::new(self.grist_api_key)); warmup(self.wipe)?; let bot = MatrixId::new(self.username, self.homeserver); let conf = conf::Serve::new(bot, self.password);