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 }