aboutsummaryrefslogtreecommitdiff
path: root/macros/src
diff options
context:
space:
mode:
Diffstat (limited to 'macros/src')
-rw-r--r--macros/src/lib.rs158
-rw-r--r--macros/src/macro_types/mod.rs249
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,
+ })
+ }
+}