mrsbfh

Matrix-Rust-SDK-Bot-Framework-Helper
git clone git://archive.git.mtrnord.blog/MTRNord/mrsbfh.git
Log | Files | Refs | README

lib.rs (10798B)


      1 pub(crate) mod utils;
      2 use crate::utils::get_arg;
      3 use convert_case::{Case, Casing};
      4 use proc_macro::TokenStream;
      5 use quote::quote;
      6 use syn::parse_macro_input;
      7 use 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]
     19 pub 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]
     62 pub 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)]
    183 pub 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]
    221 pub 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 }