rdzobot 0.1.0

Modular, but monolithic Matrix bot
Documentation
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-FileCopyrightText: 2025 Wojtek Porczyk <woju@hackerspace.pl>

//! Module `newag`: czy pojazdy jejżdżą?

use crate::prelude::*;
use anyhow::anyhow;


#[derive(Debug, Deserialize)]
#[serde(default)]
#[doc(hidden)]
pub struct Config {
    enabled: bool,
}

impl Default for Config {
    fn default() -> Self { Self { enabled: true } }
}

#[doc(hidden)]
pub fn load(bot: Rdzobot) {
    if !bot.config().module.newag.enabled {
        return;
    }

    bot.add_command(clap::Command::new("!rozprawa"), on_cmd_rozprawa);
}


type ParseError = String;

#[derive(Debug)]
#[allow(dead_code)]
struct Rozprawa {
    wydział: String,
    miejsce: String,
    sygnatura: String,
    data: String,
    przewodniczący: String,
}

#[derive(Default)]
struct BuilderRozprawy {
    wydział: Option<String>,
    miejsce: Option<String>,
    sygnatura: Option<String>,
    data: Option<String>,
    przewodniczący: Option<String>,
}

impl Rozprawa {
    fn build() -> BuilderRozprawy { BuilderRozprawy::default() }

    async fn fetch(http: &reqwest::Client) -> anyhow::Result<Self> {
        // TODO config
        let fragment = scraper::Html::parse_document(&http.get(
            "https://bip.warszawa.so.gov.pl/e-wokanda/szukaj?keyword=&signature=XXII+GW+493%2F24&department_id=&start_time=&recaptcha_response=")
            .send()
            .await?
            .text()
            .await?);
        let selector_tr = scraper::Selector::parse("table caption ~ * tr").unwrap();
        let selector_th = scraper::Selector::parse("th").unwrap();
        let selector_td = scraper::Selector::parse("td").unwrap();

        let mut builder = Rozprawa::build();

        for tr in fragment.select(&selector_tr) {
            #[rustfmt::skip]
            let th: String = tr
                .select(&selector_th)
                .next()
                .unwrap()
                .inner_html()
                .trim()
                .into();
            let td: String = tr
                .select(&selector_td)
                .next()
                .unwrap()
                .inner_html()
                .split_whitespace()
                .collect::<Vec<_>>()
                .join(" ");

            match th.as_str() {
                "Wydział" => builder.wydział(td),
                "Miejsce rozprawy" => builder.miejsce(td),
                "Sygnatura" => builder.sygnatura(td),
                "Data" => builder.data(td),
                "Przewodniczący" => builder.przewodniczący(td),
                _ => &builder,
            };
        }

        match builder.build() {
            Ok(this) => Ok(this),
            Err(e) => Err(anyhow!(e)),
        }
    }
}

impl BuilderRozprawy {
    fn wydział(&mut self, wydział: String) -> &Self {
        if self.wydział.is_none() {
            self.wydział = Some(wydział);
        }
        self
    }

    fn miejsce(&mut self, miejsce: String) -> &Self {
        if self.miejsce.is_none() {
            self.miejsce = Some(miejsce);
        }
        self
    }

    fn sygnatura(&mut self, sygnatura: String) -> &Self {
        if self.sygnatura.is_none() {
            self.sygnatura = Some(sygnatura);
        }
        self
    }

    fn data(&mut self, data: String) -> &Self {
        if self.data.is_none() {
            self.data = Some(data);
        }
        self
    }

    fn przewodniczący(&mut self, przewodniczący: String) -> &Self {
        if self.przewodniczący.is_none() {
            self.przewodniczący = Some(przewodniczący);
        }
        self
    }

    fn build(self) -> Result<Rozprawa, ParseError> {
        Ok(Rozprawa {
            wydział: self.wydział.ok_or("missing 'wydział'")?,
            miejsce: self.miejsce.ok_or("missing 'miejsce'")?,
            sygnatura: self.sygnatura.ok_or("missing 'sygnatura'")?,
            data: self.data.ok_or("missing 'data'")?,
            przewodniczący: self.przewodniczący.ok_or("missing 'przewodniczący'")?,
        })
    }
}

async fn on_cmd_rozprawa(
    _arg_matches: clap::ArgMatches,
    _event: OriginalSyncRoomMessageEvent,
    client: Client,
    room: Room,
    _bot: Rdzobot,
) -> anyhow::Result<()> {
    let rozprawa = Rozprawa::fetch(client.http_client()).await?;

    let content = RoomMessageEventContent::notice_plain(format!(
        "następna rozprawa: {} {}",
        &rozprawa.data, &rozprawa.miejsce
    ));
    room.send(content).await?;

    Ok(())
}