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>

//! # Writing modules for rdzobot.
//!
//! Use [`crate::prelude`]:
//!
//! ```compile_fail
//! use crate::prelude::*;
//! ```
//!
//! The prelude re-exports all types that are strictly required to implement a&nbsp;module.
//!
//! Create a struct for the module config. The struct needs to implement [`serde::Deserialize`], as
//! it will be deserialized from `config.toml`, from `[module.<module-name>]` key. Usually it needs
//! to at least contain `enabled=<bool>` key, which should default to `true`. Optimally the struct
//! should both have `#[serde(default)]` marker **and** an `impl Default`:
//!
//! ```
//! #[derive(Debug, serde::Deserialize)]
//! #[serde(default)]
//! #[doc(hidden)]
//! pub struct Config {
//!     enabled: bool,
//!
//!     //.. other fields, if you need them
//! }
//!
//! impl Default for Config {
//!     fn default() -> Self {
//!         Self {
//!             enabled: true,
//!         }
//!     }
//! }
//! ```
//!
//! `impl Default` will cover the case when owner didn't add `[module.<module>]` table to
//! `config.toml`, while `#[serde(default)]` helps when owner did add that table, but haven't put
//! all the entries. Optimally none of the keys should be required and should have sane defaults.
//!
//! Second, implement `fn load(bot: Rdzobot)` function, which will be called after the core of the
//! bot is initialised.
//!
//! ```
//! # use rdzobot::prelude::*;
//! #[doc(hidden)]
//! pub fn load(bot: Rdzobot) {
//!     if !bot.config().module.example.enabled {
//!         return;
//!     }
//!
//!     // bot.add_command(clap::Command("!example"), on_cmd_example);
//!     // bot.add_regex(regex::Regex::new("^example$"), on_regex_example);
//!     // bot.add_event_handler(on_example_event);
//!     // ...
//! }
//! ```
//!
//! ## Handling commands
//!
//! Create an async function that accepts arguments:
//! - `arg_matches: clap::ArgMatches` — the arguments that were parsed,
//! - `event: OriginalSyncRoomMessageEvent` — Matrix event that is being handled
//!   (`m.room.message`),
//! - `client: Client` — handle to [`matrix_sdk::Client`],
//! - `room: Room` — Matrix room, where the message with command was posted,
//! - `bot: Rdzobot` — handle for the bot object.
//!
//! And returns `anyhow::Result<()>`.
//!
//! Then pass this handler to [`bot.add_command()`][Rdzobot::add_command] in `load()`
//! function, as in example above as the second argument. The first argument to `add_command` needs
//! to be [`clap::Command`] — either instantiate it directly (`clap::Command::new("!command")`)
//! with clap's builder API, or if you're using derive API, `::command()` function that gets
//! derived unto your parser struct. In second case you can instantiate this struct from
//! `ArgMatches`.
//!
//! Derive API example:
//!
//! ```
//! # use rdzobot::prelude::*;
//! #[doc(hidden)]
//! pub fn load(bot: Rdzobot) {
//!     if !bot.config().module.example.enabled {
//!         return;
//!     }
//!
//!     bot.add_command(RcbArgs::command().name("!rcb"), on_cmd_rcb);
//! }
//!
//! #[derive(clap::Parser, Debug)]
//! struct RcbArgs {
//!     template: Option<String>,
//! }
//!
//! async fn on_cmd_rcb(
//!     mut arg_matches: clap::ArgMatches,
//!     event: OriginalSyncRoomMessageEvent,
//!     client: Client,
//!     room: Room,
//!     bot: Rdzobot,
//! ) -> anyhow::Result<()> {
//!     let args = RcbArgs::from_arg_matches_mut(&mut arg_matches).unwrap();
//!     if let Some(template) = args.template {
//!         // ...
//!     }
//!     Ok(())
//! }
//! ```
//!
//! ## Handling regular expressions
//!
//! Similar as above, there's [`Rdzobot::add_regex`], which accepts [`regex::Regex`] and
//! a&nbsp;handler. Handler is an `async fn(re: regex::Regex, body: String, event:
//! OriginalSyncRoomMessageEvent, _client: Client, room: Room, bot: Rdzobot) ->
//! anyhow::Result<()>`, — `body` is the (plain) content of the message, and `re` is the regex that
//! matched the message. You can run
//! [`re.captures_iter(body.as_str())`][regex::Regex::captures_iter] against the body to get the
//! parsed groups if you need them.
//!
//! Example:
//!
//! ```
//! # use rdzobot::prelude::*;
//! #[doc(hidden)]
//! pub fn load(mut bot: Rdzobot) {
//!     if !bot.config().module.example.enabled {
//!         return;
//!     }
//!
//!     bot.add_regex(regex::Regex::new(REGEX).unwrap(), on_regex_example);
//! }
//!
//! const REGEX: &str = "^[Hh]ello.*";
//!
//! async fn on_regex_example(
//!     re: regex::Regex,
//!     body: String,
//!     event: OriginalSyncRoomMessageEvent,
//!     _client: Client,
//!     room: Room,
//!     bot: Rdzobot,
//! ) -> anyhow::Result<()> {
//!     for captures in re.captures_iter(body.as_str()) {
//!         // ...
//!     }
//!
//!     Ok(())
//! }
//! ```
//!
//! ## Handling arbitrary events
//!
//! In the `load()` function you can register event handlers, e.g. to handle incoming messages,
//! reactions and all other kinds of Matrix events.
//!
//! The framework is designed to give maximum flexibility afforded by `matrix-rust-sdk`, while
//! maintaining less exciting stuff inside the framework. For this reason, you can write any
//! handlers that are possible to write by the Matrix SDK, with few convenience features. The main
//! difference is that you need to register the handlers through [`Rdzobot::add_event_handler`]
//! and/or [`Rdzobot::add_room_event_handler`], not through methods on [`matrix_sdk::Client`]. This
//! is because the handlers need to be accounted for when the module gets unloaded. If you directly
//! register (e.g. using `bot.client.add_event_handler`), the tasks won't get cancelled on reload.
//!
//! The bot adds itself as a&nbsp;context to the event handlers, so you can add `Ctx<Rdzobot>` to
//! the function signature:
//!
//! ```
//! # use rdzobot::prelude::*;
//! #[doc(hidden)]
//! pub fn load(bot: Rdzobot) {
//!     if !bot.config().module.example.enabled {
//!         return;
//!     }
//!
//!     bot.add_event_handler(on_room_message);
//! }
//!
//! async fn on_room_message(
//!     event: OriginalSyncRoomMessageEvent,
//!     client: Client,
//!     room: Room,
//!     bot: Ctx<Rdzobot>,
//! ) -> anyhow::Result<()> {
//!     let Some(body) = text_message_gate(&event, &client, &room) else {
//!         return Ok(());
//!     };
//!
//!     // the rest of the handler
//!
//!     Ok(())
//! }
//! ```
//!
//! ## Handling webhooks (REST API)
//!
//! There's common [`axum::Router`] available that serves all requests on common port (`5000` by
//! default). In `fn load()` you can create your own `Router` and merge it to the common one using
//! [`Rdzobot::merge_web_router()`]. Please use endpoins under `/api/<module>`.
//!
//! Example:
//!
//! ```
//! # use rdzobot::prelude::*;
//! #[doc(hidden)]
//! pub fn load(bot: Rdzobot) {
//!     if !bot.config().module.example.enabled {
//!         return;
//!     }
//!
//!     bot.merge_web_router(
//!         axum::Router::new().route("/api/example/hello", axum::routing::get(on_api_hello)),
//!     );
//! }
//!
//! async fn on_api_hello() -> &'static str {
//!     "Hello, world!"
//! }
//! ```
//!
//! ## Plugging into module loader
//!
//! - In `Cargo.toml`, add `mod-<module-name>` feature and all required dependencies.
//! - In `src/lib.rs` add the mod in the right place (3 times)
//! - In `migrations/000_init.sql` add whatever tables you need, if you use SQL.
//!
//! # Utilities available
//!
//! There are several utils available to simplify writing handlers for common cases:
//!
//! - [`crate::utils::text_message_gate`] `-> Option<String>`, where `Some(body)` contains message
//!   body if the event represents a&nbsp;text message sent to a&nbsp;joined room;
//! - [`crate::utils::nie_zesraj_się`] to loudly decline to answer a&nbsp;command, probably from an
//!   unauthorized submitter;
//! - [`Rdzobot::sqlite()`] for access to sqlite connection;
//!
//! ## sqlite
//!
//! TBD
//!
//! ## Auxiliary tokio tasks
//!
//! The tasks need to be aborted. This is currently TBD.
//!
//! ## Common !command handling
//!
//! TODO
//!
//! ## Common mistakes, aka WTF the borrow checker wants
//!
//! ### the trait `matrix_sdk::event_handler::EventHandler<_, _>` is not implemented for fn item
//!
//! There's a problem with the function that you attempted to pass as an event handler. One of the
//! common ones are to hold a&nbsp;guard for `std::sync::Mutex` (or a&nbsp;guard for one of related
//! synchronisation primitives) across `.await`. This causes the future that results from this
//! `async fn` to not be `Send`. If you try to do this with Rdzobot's [`add_command`][Rdzobot::add_command], you'll get
//! more or less clear message:
//!
//! ```text
//! error: future cannot be sent between threads safely
//!    --> src/bot.rs:190:55
//!     |
//! 190 |         this.add_command(clap::Command::new("!help"), on_cmd_help);
//!     |                                                       ^^^^^^^^^^^ future returned by `on_cmd_help` is not `Send`
//!     |
//!     = note: the full trait has been written to '/home/user/src/rdzobot/target/debug/deps/rdzobot-06e06058360229c8.long-type-15978233823613434038.txt'
//!     = help: within `impl Future<Output = Result<(), Error>>`, the trait `std::marker::Send` is not implemented for `std::sync::RwLockReadGuard<'_, clap::Command>`
//! note: future is not `Send` as this value is used across an await
//!    --> src/bot.rs:418:8
//!     |
//! 413 |     let command = bot.inner.command.read().unwrap();
//!     |         ------- has type `std::sync::RwLockReadGuard<'_, clap::Command>` which is not `Send`
//! ...
//! 418 |     )).await?;
//!     |        ^^^^^ await occurs here, with `command` maybe used later
//! note: required by a bound in `bot::Rdzobot::add_command`
//!    --> src/bot.rs:326:38
//!     |
//! 323 |     pub fn add_command(
//!     |            ----------- required by a bound in this associated function
//! ...
//! 326 |         handler: CommandHandler<impl HandlerFuture>,
//!     |                                      ^^^^^^^^^^^^^ required by this bound in `Rdzobot::add_command`
//! ```
//!
//! However, because of some type system magic that was committed by matrix-rust-sdk, the error
//! gets unreadable:
//!
//! ```text
//! error[E0277]: the trait bound `fn(matrix_sdk::ruma::ruma_events::OriginalSyncMessageLikeEvent<matrix_sdk::ruma::ruma_events::room::message::RoomMessageEventContent>, matrix_sdk::Client, matrix_sdk::Room, matrix_sdk::event_handler::Ctx<bot::Rdzobot>) -> impl std::future::Future<Output = std::result::Result<(), anyhow::Error>> {bot::on_room_message_command_or_regex}: matrix_sdk::event_handler::EventHandler<_, _>` is not satisfied
//!    --> src/bot.rs:229:39
//!     |
//! 229 |         self.client.add_event_handler(on_room_message_command_or_regex);
//!     |                     ----------------- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unsatisfied trait bound
//!     |                     |
//!     |                     required by a bound introduced by this call
//!     |
//!     = help: the trait `matrix_sdk::event_handler::EventHandler<_, _>` is not implemented for fn item `fn(OriginalSyncMessageLikeEvent<...>, ..., ..., ...) -> ... {on_room_message_command_or_regex}`
//! note: required by a bound in `matrix_sdk::Client::add_event_handler`
//!    --> /home/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/matrix-sdk-0.10.0/src/client/mod.rs:765:12
//!     |
//! 762 |     pub fn add_event_handler<Ev, Ctx, H>(&self, handler: H) -> EventHandlerHa...
//!     |            ----------------- required by a bound in this associated function
//! ...
//! 765 |         H: EventHandler<Ev, Ctx>,
//!     |            ^^^^^^^^^^^^^^^^^^^^^ required by this bound in `Client::add_event_handler`
//!
//! For more information about this error, try `rustc --explain E0277`.
//! ```
//!
//! One way to get close to solution is to comment out the respective `.add_event_handler()` call
//! and run the source through `clippy`, which has a&nbsp;warning against this exact problem:
//!
//! ```text
//! warning: this `MutexGuard` is held across an await point
//!    --> src/bot.rs:364:15
//!     |
//! 364 | ...   match bot.inner.command.read().unwrap().clone().try_get_matches_from_mu...
//!     |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//!     |
//!     = help: consider using an async-aware `Mutex` type or ensuring the `MutexGuard` is dropped before calling `await`
//! note: these are all the await points this lock is held through
//!    --> src/bot.rs:368:70
//!     |
//! 368 | ...nt, room, bot.0.clone()).await?;
//!     |                             ^^^^^
//! ...
//! 372 | ...ntContent::notice_plain(format!("{}", err.render()))).await?;
//!     |                                                          ^^^^^
//! 373 | ...
//! 374 | ...event, &room).await?,
//!     |                  ^^^^^
//!     = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#await_holding_lock
//!     = note: `#[warn(clippy::await_holding_lock)]` on by default
//! ```

use crate::prelude::*;