mrsbfh_macros/
lib.rs

1pub(crate) mod utils;
2use crate::utils::get_arg;
3use convert_case::{Case, Casing};
4use proc_macro::TokenStream;
5use quote::quote;
6use syn::parse_macro_input;
7use syn::spanned::Spanned;
8
9/// Used to define a command
10///
11/// ```compile_fail
12/// use std::sync::Arc;
13/// use tokio::sync::Mutex;
14///
15/// #[command(help = "Description")]
16/// async fn hello_world(mut tx: mrsbfh::Sender, config: Arc<Mutex<Config>>, sender: String, mut args: Vec<&str>) -> Result<(), Box<dyn std::error::Error>> where Config: mrsbfh::config::Loader + Clone {}
17/// ```
18#[proc_macro_attribute]
19pub fn command(args: TokenStream, input: TokenStream) -> TokenStream {
20    let input = parse_macro_input!(input as syn::ItemFn);
21
22    let args = parse_macro_input!(args as syn::AttributeArgs);
23
24    let help_const_name = syn::Ident::new(
25        &format!(
26            "{}_HELP",
27            input.sig.ident.to_string().to_uppercase().replace("R#", "")
28        ),
29        input.sig.span(),
30    );
31    let help_description = match get_arg(
32        input.span(),
33        args,
34        "help",
35        "#[command(help = \"<description>\")]",
36        1,
37    ) {
38        Ok(v) => syn::LitStr::new(&format!("* {}\n", v.value()), v.span()),
39        Err(e) => return e,
40    };
41
42    let code = quote! {
43        #input
44        pub(crate) const #help_const_name: &str = #help_description;
45
46    };
47    code.into()
48}
49
50/// Used to generate the match case and help text
51///
52/// ```compile_fail
53/// #[command_generate(bot_name = "botless", description = "Is it a bot or is it not?")]
54/// enum Commands {
55///     In,
56///     Out
57/// }
58/// ```
59///
60/// **Note**: The defined enum will NOT be present at runtime. It gets replaced fully
61#[proc_macro_attribute]
62pub fn command_generate(args: TokenStream, input: TokenStream) -> TokenStream {
63    let input = parse_macro_input!(input as syn::ItemEnum);
64
65    let args = parse_macro_input!(args as syn::AttributeArgs);
66
67    let commands = input.variants.iter().map(|v| {
68        let command_string = v.ident.to_string().to_lowercase();
69        let command_short = {
70            let chars: Vec<String> = v
71                .ident
72                .to_string()
73                .to_case(Case::Snake)
74                .split('_')
75                .map(|x| x.chars().next().unwrap().to_string().to_lowercase())
76                .collect();
77            chars.join("")
78        };
79        let command = quote::format_ident!(
80            "r#{}",
81            syn::Ident::new(&v.ident.to_string().to_case(Case::Snake), v.span())
82        );
83
84        quote! {
85            #command_string => {
86                #command::#command(client, tx, config, sender, room_id, args).await
87            },
88            #command_short => {
89                #command::#command(client, tx, config, sender, room_id, args).await
90            },
91        }
92    });
93
94    let help_parts = input.variants.iter().map(|v| {
95        let command_string = v.ident.to_string().to_case(Case::Snake);
96        let help_command =
97            syn::Ident::new(&format!("{}_HELP", command_string.to_uppercase()), v.span());
98        let command = quote::format_ident!(
99            "r#{}",
100            syn::Ident::new(&v.ident.to_string().to_case(Case::Snake), v.span())
101        );
102
103        quote! {
104            #command::#help_command
105        }
106    });
107    let mut help_format_string = String::from("{}");
108    input.variants.iter().for_each(|_| {
109        help_format_string = format!("{}{}", help_format_string, "{}");
110    });
111
112    let bot_name = match get_arg(
113        input.span(),
114        args.clone(),
115        "bot_name",
116        "#[command_generate(bot_name = \"<bot name>\", description = \"<bot description>\")]",
117        2,
118    ) {
119        Ok(v) => v.value(),
120        Err(e) => return e,
121    };
122    let description = match get_arg(
123        input.span(),
124        args,
125        "description",
126        "#[command_generate(bot_name = \"<bot name>\", description = \"<bot description>\")]",
127        2,
128    ) {
129        Ok(v) => format!("{}\n\n", v.value()),
130        Err(e) => return e,
131    };
132
133    let help_title = format!("# Help for the {} Bot\n\n", bot_name);
134    let commands_title = "## Commands\n";
135    let help_preamble = help_title + &description + commands_title;
136
137    let code = quote! {
138
139        async fn help(
140            mut tx: mrsbfh::Sender,
141        ) -> Result<(), Error> {
142            let options = mrsbfh::pulldown_cmark::Options::empty();
143            let help_markdown = format!(#help_format_string, #help_preamble, #(#help_parts,)*);
144            let parser = mrsbfh::pulldown_cmark::Parser::new_ext(&help_markdown, options);
145            let mut html = String::new();
146            mrsbfh::pulldown_cmark::html::push_html(&mut html, parser);
147            let owned_html = html.to_owned();
148
149            mrsbfh::tokio::spawn(async move {
150                let content = matrix_sdk::ruma::events::AnyMessageEventContent::RoomMessage(
151                    matrix_sdk::ruma::events::room::message::MessageEventContent::notice_html(
152                        &help_markdown,
153                        owned_html,
154                    ),
155                );
156
157                if let Err(e) = tx.send(content).await {
158                    mrsbfh::tracing::error!("Error: {}",e);
159                };
160            });
161
162            Ok(())
163        }
164
165        pub async fn match_command<'a>(cmd: &str, client: matrix_sdk::Client, config: std::sync::Arc<tokio::sync::Mutex<Config<'a>>>, tx: mrsbfh::Sender, sender: String, room_id: matrix_sdk::ruma::RoomId, args: Vec<&str>,) -> Result<(), Error> where Config<'a>: mrsbfh::config::Loader + Clone {
166            match cmd {
167                #(#commands)*
168                "help" => {
169                    help(tx).await
170                },
171                "h" => {
172                    help(tx).await
173                },
174                _ => {Ok(())}
175            }
176        }
177
178    };
179    code.into()
180}
181
182#[proc_macro_derive(ConfigDerive)]
183pub fn config_derive(input: TokenStream) -> TokenStream {
184    let ast = parse_macro_input!(input as syn::DeriveInput);
185    let name = &ast.ident;
186
187    let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
188    let expanded = quote! {
189        impl #impl_generics mrsbfh::config::Loader for #name #ty_generics #where_clause {
190            fn load<P: AsRef<std::path::Path> + std::fmt::Debug>(path: P) -> Result<Self, mrsbfh::errors::ConfigError> {
191                let contents = std::fs::read_to_string(path)?;
192                let config: Self = mrsbfh::serde_yaml::from_str(&contents)?;
193                Ok(config)
194            }
195        }
196    };
197
198    TokenStream::from(expanded)
199}
200
201/// Used to generate code to detect commands when we get a message for the bot
202///
203/// Requirements:
204///
205/// * Tokio
206/// * Tokio tracing
207/// * Naming of arguments needs to be EXACTLY like in the example
208/// * the async_trait macro needs to be BELOW the commands macro
209/// * The match_command MUST be imported
210///
211/// ```compile_fail
212/// use crate::commands::match_command;
213///
214/// #[mrsbfh::commands::commands]
215/// async fn on_room_message(event: SyncMessageEvent<MessageEventContent>, room: Room) {
216///         // Your own logic. (Executed BEFORE the commands matching)
217/// }
218/// ```
219///
220#[proc_macro_attribute]
221pub fn commands(_: TokenStream, input: TokenStream) -> TokenStream {
222    let mut method = parse_macro_input!(input as syn::ItemFn);
223
224    if method.sig.ident == "on_room_message" {
225        let original = method.block.clone();
226        let new_block = syn::parse_quote! {
227            {
228                #original
229
230                // Command matching logic
231                if let matrix_sdk::room::Room::Joined(room) = room {
232                    let msg_body = if let matrix_sdk::ruma::events::SyncMessageEvent {
233                        content: matrix_sdk::ruma::events::room::message::MessageEventContent {
234                            msgtype: matrix_sdk::ruma::events::room::message::MessageType::Text(matrix_sdk::ruma::events::room::message::TextMessageEventContent { body: msg_body, .. }),
235                            ..
236                        },
237                        ..
238                    } = event
239                    {
240                        msg_body.clone()
241                    } else {
242                        String::new()
243                    };
244                    if msg_body.is_empty() {
245                        return;
246                    }
247
248                    let sender = event.sender.clone().to_string();
249
250                    let (tx, mut rx) = tokio::sync::mpsc::channel(100);
251                    let room_id = room.room_id().clone();
252
253                    let cloned_config = config.clone();
254                    let cloned_client = client.clone();
255                    tokio::spawn(async move {
256                        let normalized_body = mrsbfh::commands::command_utils::WHITESPACE_DEDUPLICATOR_MAGIC.replace_all(&msg_body, " ");
257                        let mut split = msg_body.split_whitespace();
258
259                        let command_raw = split.next().expect("This is not a command").to_lowercase();
260                        let command = mrsbfh::commands::command_utils::COMMAND_MATCHER_MAGIC.captures(command_raw.as_str())
261                                                           .map_or(String::new(), |caps| {
262                                                                caps.get(1)
263                                                                    .map_or(String::new(),
264                                                                            |m| String::from(m.as_str()))
265                                                           });
266                        if !command.is_empty() {
267                           tracing::info!("Got command: {}", command);
268                        }
269                        // Make sure this is immutable
270                        let args: Vec<&str> = split.collect();
271                        if let Err(e) = match_command(
272                            command.as_str(),
273                            cloned_client.clone(),
274                            cloned_config.clone(),
275                            tx,
276                            sender,
277                            room_id,
278                            args,
279                        )
280                        .await
281                        {
282                            tracing::error!("{}", e);
283                        }
284
285                    });
286
287                    while let Some(v) = rx.recv().await {
288                        if let Err(e) = room.send(v, None)
289                            .await
290                        {
291                            tracing::error!("{}", e);
292                        }
293                    }
294                }
295            }
296        };
297        method.block = new_block;
298    }
299
300    TokenStream::from(quote! {#method})
301}