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#[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#[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#[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 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 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}