diff options
Diffstat (limited to 'macros/src')
| -rw-r--r-- | macros/src/lib.rs | 158 | ||||
| -rw-r--r-- | macros/src/macro_types/mod.rs | 249 |
2 files changed, 407 insertions, 0 deletions
diff --git a/macros/src/lib.rs b/macros/src/lib.rs new file mode 100644 index 0000000..222f14d --- /dev/null +++ b/macros/src/lib.rs @@ -0,0 +1,158 @@ +extern crate proc_macro; +use proc_macro::TokenStream; +use proc_macro2::{Ident, Span}; +use quote::quote; + +use syn::parse_macro_input; + +mod macro_types; + +use macro_types::*; + +fn generate_help(items: &Items) -> proc_macro2::TokenStream { + let command_help = items.inner.iter().filter_map(|x| { + if let Item::Command(command) = x { + let help = command.help(); + Some(quote! { + bot.send_notice(target, #help).unwrap(); + }) + } else { + None + } + }); + + let matcher_help = items.inner.iter().filter_map(|x| { + if let Item::Matcher(matcher) = x { + let help = matcher.help(); + Some(quote! { + bot.send_notice(target, #help).unwrap(); + }) + } else { + None + } + }); + + let hook_help = items.inner.iter().filter_map(|x| { + if let Item::Hook(hook) = x { + let help = hook.help(); + Some(quote! { + bot.send_notice(target, #help).unwrap(); + }) + } else { + None + } + }); + + let gen = quote! { + let target = message.source_nickname().unwrap(); + + bot.send_notice(target, "COMMANDS:").unwrap(); + #(#command_help)* + + bot.send_notice(target, "MATCHERS:").unwrap(); + #(#matcher_help)* + + bot.send_notice(target, "HOOKS:").unwrap(); + #(#hook_help)* + }; + gen.into() +} + +#[proc_macro] +pub fn catinator(tokens: TokenStream) -> TokenStream { + let items = parse_macro_input!(tokens as Items); + + let hooks = items.inner.iter().filter_map(|x| { + if let Item::Hook(hook) = x { + Some(hook.to_call()) + } else { + None + } + }); + + let commands = items.inner.iter().filter_map(|x| { + if let Item::Command(command) = x { + Some(command.to_call()) + } else { + None + } + }); + + let matchers = items.inner.iter().filter_map(|x| { + if let Item::Matcher(command) = x { + Some(command.to_call()) + } else { + None + } + }); + + let matchers_regex = items.inner.iter().filter_map(|x| { + if let Item::Matcher(matcher) = x { + let name = &matcher.name; + let regex = &matcher.matcher; + + let ident = Ident::new(&name.value(), Span::call_site()); + + Some(quote! { + let #ident = regex::Regex::new(#regex).unwrap(); + }) + } else { + None + } + }); + + let help = generate_help(&items); + + let gen = quote! { + use std::env; + + use futures::prelude::*; + use tracing::{info, debug, trace}; + + use irc::client::prelude::*; + use catinator::Bot; + + let config_path = env::var("CATINATOR_CONFIG").unwrap_or("config.toml".to_string()); + info!("starting bot with config file {}", config_path); + let mut bot = Bot::new(&config_path).await.unwrap(); + + if bot.config.server.sasl { + info!("initializing sasl"); + bot.sasl_init().await.unwrap() + } + + #(#matchers_regex)* + + info!("starting main event loop"); + let mut stream = bot.irc_client.stream().unwrap(); + + while let Some(message) = stream.next().await.transpose().unwrap() { + trace!("{:?}", message); + + let command = message.clone().command; + + #(#hooks)* + + match &command { + Command::PRIVMSG(_target, text) => { + let mut word = text.split_ascii_whitespace().next().unwrap().chars(); + let prefix = word.next().unwrap(); + let rest: String = word.collect(); + + if prefix == bot.config.settings.prefix { + if "help" == rest { + #help + } + + #(#commands)* + } else { + #(#matchers)* + } + } + _ => (), + } + } + }; + + return gen.into(); +} diff --git a/macros/src/macro_types/mod.rs b/macros/src/macro_types/mod.rs new file mode 100644 index 0000000..262b3df --- /dev/null +++ b/macros/src/macro_types/mod.rs @@ -0,0 +1,249 @@ +use proc_macro2::{Ident, Span}; +use quote::quote; + +use syn::{ + parenthesized, + parse::{Parse, ParseStream}, + Path, +}; +use syn::{LitStr, Token}; + +pub trait IrcItem { + fn to_call(&self) -> proc_macro2::TokenStream; + fn help(&self) -> String; +} + +#[derive(Debug)] +pub struct Items { + pub inner: Vec<Item>, +} + +impl Parse for Items { + fn parse(input: ParseStream) -> syn::Result<Self> { + let mut items: Vec<Item> = Vec::new(); + + while !input.is_empty() { + if input.peek(syn::Ident) && input.peek2(syn::token::Paren) { + items.push(input.parse()?) + } else { + return Err(input.error("line was not of expected format")); + } + } + + Ok(Self { inner: items }) + } +} + +#[derive(Debug)] +pub enum Item { + Command(Command), + Hook(Hook), + Matcher(Matcher), +} + +impl Parse for Item { + fn parse(input: ParseStream) -> syn::Result<Self> { + if input.peek(syn::Ident) { + let item: Ident = input.parse()?; + match item.to_string().as_str() { + "command" => input.parse().map(Item::Command), + "hook" => input.parse().map(Item::Hook), + "matcher" => input.parse().map(Item::Matcher), + _ => Err(input.error(format!( + "expected one of: command, hook or matcher not {}", + item.to_string() + ))), + } + } else { + Err(input.error("wrong type")) + } + } +} + +#[derive(Debug)] +pub struct Command { + pub name: LitStr, + pub description: LitStr, + pub function: Path, +} + +impl IrcItem for Command { + fn to_call(&self) -> proc_macro2::TokenStream { + let name = &self.name; + let function = &self.function; + + quote! { + if #name == rest { + debug!(target: "command", "{} with {:?}", #name, message); + let result = #function(message.clone()); + } + } + } + + fn help(&self) -> String { + format!(" {}: {}", self.name.value(), self.description.value()) + } +} + +impl Parse for Command { + fn parse(input: ParseStream) -> syn::Result<Self> { + let content; + parenthesized!(content in input); + + let mut _token: Token![,]; + + if input.peek(Token![,]) { + _token = input.parse()? + } + + let name = content.parse()?; + _token = content.parse()?; + let description = content.parse()?; + _token = content.parse()?; + let function = content.parse()?; + + Ok(Self { + name, + description, + function, + }) + } +} + +#[derive(Debug)] +pub struct Hook { + pub name: LitStr, + pub description: LitStr, + pub kind: Ident, + pub function: Path, +} + +impl IrcItem for Hook { + fn to_call(&self) -> proc_macro2::TokenStream { + let name = &self.name; + let kind = &self.kind; + let kind_str = &self.kind.to_string(); + let function = &self.function; + + quote! { + if let Command::#kind(..) = &command { + debug!(target: "hook", "{} of kind {} with {:?}", #name, #kind_str, message); + let result = #function(&bot, message.clone()); + } + } + } + + fn help(&self) -> String { + format!(" {}: {}", self.name.value(), self.description.value()) + } +} + +impl Parse for Hook { + fn parse(input: ParseStream) -> syn::Result<Self> { + let content; + parenthesized!(content in input); + + let mut _token: Token![,]; + + if input.peek(Token![,]) { + _token = input.parse()?; + } + + let name = content.parse()?; + _token = content.parse()?; + let description = content.parse()?; + _token = content.parse()?; + let kind: Ident = content.parse()?; + match kind.to_string().as_str() { + "PASS" | "NICK" | "USER" | "OPER" | "UserMODE" | "SERVICE" | "QUIT" | "SQUIT" + | "JOIN" | "PART" | "ChannelMODE" | "TOPIC" | "NAMES" | "LIST" | "INVITE" | "KICK" + | "PRIVMSG" | "NOTICE" | "MOTD" | "LUSERS" | "VERSION" | "STATS" | "LINKS" | "TIME" + | "CONNECT" | "TRACE" | "ADMIN" | "INFO" | "SERVLIST" | "SQUERY" | "WHO" | "WHOIS" + | "WHOWAS" | "KILL" | "PING" | "PONG" | "ERROR" | "AWAY" | "REHASH" | "DIE" + | "RESTART" | "SUMMON" | "USERS" | "WALLOPS" | "USERHOST" | "ISON" | "SAJOIN" + | "SAMODE" | "SANICK" | "SAPART" | "SAQUIT" | "NICKSERV" | "CHANSERV" | "OPERSERV" + | "BOTSERV" | "HOSTSERV" | "MEMOSERV" | "CAP" | "AUTHENTICATE" | "ACCOUNT" + | "METADATA" | "MONITOR" | "BATCH" | "CHGHOST" | "Response" | "Raw" => (), + _ => { + return Err(content.error(format!( + "expected one of: PASS, NICK, USER, OPER, UserMODE, SERVICE, QUIT, SQUIT + , JOIN, PART, ChannelMODE, TOPIC, NAMES, LIST, INVITE, KICK + , PRIVMSG, NOTICE, MOTD, LUSERS, VERSION, STATS, LINKS, TIME + , CONNECT, TRACE, ADMIN, INFO, SERVLIST, SQUERY, WHO, WHOIS + , WHOWAS, KILL, PING, PONG, ERROR, AWAY, REHASH, DIE + , RESTART, SUMMON, USERS, WALLOPS, USERHOST, ISON, SAJOIN + , SAMODE, SANICK, SAPART, SAQUIT, NICKSERV, CHANSERV, OPERSERV + , BOTSERV, HOSTSERV, MEMOSERV, CAP, AUTHENTICATE, ACCOUNT + , METADATA, MONITOR, BATCH, CHGHOST, Response, Raw not {}", + kind.to_string() + ))) + } + } + + _token = content.parse()?; + let function = content.parse()?; + + Ok(Self { + name, + description, + kind, + function, + }) + } +} + +#[derive(Debug)] +pub struct Matcher { + pub name: LitStr, + pub description: LitStr, + pub matcher: LitStr, + pub function: Path, +} + +impl IrcItem for Matcher { + fn to_call(&self) -> proc_macro2::TokenStream { + let name = &self.name; + let function = &self.function; + + let ident = Ident::new(&name.value(), Span::call_site()); + + quote! { + if #ident.is_match(text) { + debug!(target: "matcher", "{} with {:?}", #name, message); + let result = #function(&bot, message.clone()); + } + } + } + + fn help(&self) -> String { + format!(" {} ({}): {}", self.name.value(), self.matcher.value(), self.description.value()) + } +} + +impl Parse for Matcher { + fn parse(input: ParseStream) -> syn::Result<Self> { + let content; + parenthesized!(content in input); + + let mut _token: Token![,]; + + if input.peek(Token![,]) { + _token = input.parse()?; + } + + let name = content.parse()?; + _token = content.parse()?; + let description = content.parse()?; + _token = content.parse()?; + let matcher = content.parse()?; + _token = content.parse()?; + let function = content.parse()?; + + Ok(Self { + name, + description, + matcher, + function, + }) + } +} |
