1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
// 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 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 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 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 text message sent to a joined room;
//! - [`crate::utils::nie_zesraj_się`] to loudly decline to answer a 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 guard for `std::sync::Mutex` (or a 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 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*;