implement list command

This commit is contained in:
Pierre de Lacroix 2026-06-27 13:32:01 +02:00
parent 8309d26c9f
commit 3210581a22
Signed by: lateralus23
GPG key ID: 53E0CEC29C24EF39
7 changed files with 467 additions and 25 deletions

View file

@ -64,10 +64,12 @@ pub fn get_serve() -> Arc<Serve> {
pub struct Main {
/// The data directory.
static_dir: PathBuf,
/// Grist APIKEY
grist_api_key: String,
}
impl Main {
/// Constructor.
pub fn new() -> Self {
pub fn new(grist_api_key: impl Into<String>) -> 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 APIKEY
pub fn grist_api_key(&self) -> &str {
&self.grist_api_key
}
}
impl Default for Main {
fn default() -> Self {
Self::new()
Self::new("")
}
}

View file

@ -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<OwnedEventId> {
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<String>) -> Res<()> {
pub async fn send_room_msg_html(
_client: Client,
room: Room,
html_body: String,
) -> Res<OwnedEventId> {
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<Shift>) -> String {
let mut output = String::new();
output.push_str("Shifts:\n<ul>");
for shift in shifts {
output.push_str(format!("<li>{}</li>", shift).as_ref());
}
output.push_str("</ul>");
output
}
pub async fn handle(command: impl AsRef<str>, room: Room, client: Client) -> Res<()> {
debug!("handling command: {}", command.as_ref());
@ -50,6 +67,9 @@ pub async fn handle(command: impl AsRef<str>, 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");

220
src/grist.rs Normal file
View file

@ -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, "<b>Prioritaire</b>"),
Priority::Secondaire => write!(f, "Secondaire"),
Priority::Bonus => write!(f, "Bonus"),
Priority::Urgent => write!(f, "<font color=\"red\">Prioritaire</font>"),
}
}
}
#[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<String>,
priority: Priority,
frequency: u64,
persons: u64,
duration: u64,
guide: impl Into<String>,
) -> 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<String>,
}
impl Shift {
pub fn new(shift_type: ShiftType, start_time: u64, inscriptions: Vec<String>) -> 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<u64> {
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<String> {
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<Priority> {
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<Vec<u64>> {
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<RecordsList> {
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<ShiftType> {
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<String> {
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<Vec<Shift>> {
let record_list = get_records(SHIFTS_DOC, SHIFTS_TABLE).await?;
debug!("{:?}", record_list);
let mut shifts: Vec<Shift> = 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)
}

View file

@ -41,6 +41,8 @@ pub mod serve;
pub mod command;
pub mod grist;
prelude! {}
/// Warmup, runs before anything.

View file

@ -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 APIKEY
#[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);