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 `parcels`: qrcode generation for receiving parcels from Paczkomat®s

use matrix_sdk::ruma::EventId;
use matrix_sdk::ruma::events::reaction::OriginalSyncReactionEvent;

use crate::prelude::*;
use crate::utils::Location;

mod inpost;
mod orlen;


#[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(mut bot: Rdzobot) {
    if !bot.config().module.parcels.enabled {
        return;
    }

    bot.add_command(clap::Command::new("!paczki"), on_cmd_paczki);
    bot.add_regex(regex::Regex::new(inpost::RE_PARCEL_INPOST_1).unwrap(), inpost::on_regex_inpost);
    bot.add_regex(regex::Regex::new(orlen::RE_PARCEL_ORLEN).unwrap(), orlen::on_regex_orlen);
    bot.add_event_handler(on_reaction);
}


async fn on_cmd_paczki(
    _arg_matches: clap::ArgMatches,
    _event: OriginalSyncRoomMessageEvent,
    _client: Client,
    room: Room,
    bot: Rdzobot,
) -> anyhow::Result<()> {
    let room_id = room.room_id().to_owned();
    let (body, html_body) =
        tokio::task::spawn_blocking(move || -> anyhow::Result<(String, String)> {
            let sqlite = bot.sqlite();

            let mut body = String::new();
            let mut html_body = String::new();

            /* element on mobile can't render the table properly */
            html_body.push_str("<ul>");

            let mut stmt = sqlite.prepare(
                "SELECT
                    event_qr,
                    parcels.operator,
                    code,
                    parcels.ref,
                    lat,
                    lon,
                    time_end
                FROM parcels
                LEFT JOIN parcel_points ON
                    parcels.operator = parcel_points.operator AND parcels.ref = parcel_points.ref
                WHERE
                    room = ?1 AND
                    unixepoch('now') < unixepoch(time_end)
                ORDER BY unixepoch(time_end) ASC;",
            )?;

            for res in stmt.query_map((room_id.as_str(),), |row| {
                // TODO: match operator {
                let lat_res = row.get::<usize, f64>(4);
                let lon_res = row.get::<usize, f64>(5);
                Ok((
                    room_id
                        .matrix_to_event_uri(
                            EventId::parse(row.get::<usize, String>(0)?.as_str()).unwrap(),
                        )
                        .to_string(),
                    row.get::<usize, String>(2)?,
                    row.get::<usize, String>(3)?,
                    if let (Ok(lat), Ok(lon)) = (lat_res, lon_res) {
                        Some(Location::new(lat, lon))
                    } else {
                        None
                    },
                    row.get::<usize, chrono::DateTime<chrono::Utc>>(6)?,
                ))
            })? {
                let (href, id, ref_, location, time_end) = res?;

                body.push_str(format!("- {} {} {}\n", id, ref_, time_end).as_str());

                html_body.push_str(
                    format!(
                        "<li>{} <a href=\"{}\">[QR]</a> — {}{}{}",
                        id,
                        href,
                        ref_,
                        if let Some(l) = location {
                            format!(" <a href=\"{}\">[MAP]</a>", l.osm_shortlink(None, true))
                        } else {
                            "".to_string()
                        },
                        time_end.with_timezone(&bot.config().timezone),
                    )
                    .as_str(),
                );
            }

            html_body.push_str("</ul>");

            Ok((body, html_body))
        })
        .await??;

    if !body.is_empty() {
        room.send(RoomMessageEventContent::notice_html(body, html_body)).await?;
    } else {
        room.send(RoomMessageEventContent::notice_plain("Nie ma paczek.")).await?;
    }

    Ok(())
}

async fn on_reaction(
    event: OriginalSyncReactionEvent,
    room: Room,
    bot: Ctx<Rdzobot>,
) -> anyhow::Result<()> {
    tokio::task::spawn_blocking(move || {
        bot.sqlite()
            .prepare(
                "DELETE FROM parcels
                WHERE (
                    room = ?1 AND (event_orig = ?2 OR event_qr = ?2)
                ) OR unixepoch(time_end) < unixepoch('now');",
            )?
            .execute((room.room_id().as_str(), event.content.relates_to.event_id.as_str()))?;

        Ok(())
    })
    .await?
}