diff --git a/.cspell.json b/.cspell.json index 346de7c..fa718c4 100644 --- a/.cspell.json +++ b/.cspell.json @@ -4,6 +4,7 @@ "aionbot", "chronos", "Deque", + "Hasher", "onebot", "serde" ] diff --git a/Cargo.lock b/Cargo.lock index 6bd94ab..040db87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,11 +26,22 @@ dependencies = [ "memchr", ] +[[package]] +name = "aionbot" +version = "0.1.0" +dependencies = [ + "aionbot-adapter-onebot", + "aionbot-core", + "aionbot-macros", + "tokio", +] + [[package]] name = "aionbot-adapter-onebot" version = "0.1.0" dependencies = [ "aionbot-core", + "anyhow", "onebot_v11", ] @@ -45,6 +56,15 @@ dependencies = [ "serde_json", ] +[[package]] +name = "aionbot-macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "anyhow" version = "1.0.89" diff --git a/crates/aionbot-adapter-onebot/Cargo.toml b/crates/aionbot-adapter-onebot/Cargo.toml index 205c8fa..cf13aca 100644 --- a/crates/aionbot-adapter-onebot/Cargo.toml +++ b/crates/aionbot-adapter-onebot/Cargo.toml @@ -5,4 +5,5 @@ edition = "2021" [dependencies] aionbot-core = { version = "0.1.0", path = "../aionbot-core" } +anyhow = "1.0.89" onebot_v11 = "0.1.5" diff --git a/crates/aionbot-adapter-onebot/src/lib.rs b/crates/aionbot-adapter-onebot/src/lib.rs index e2e8d70..cb642b6 100644 --- a/crates/aionbot-adapter-onebot/src/lib.rs +++ b/crates/aionbot-adapter-onebot/src/lib.rs @@ -1 +1,15 @@ +use anyhow::Result; + pub extern crate aionbot_core; + +pub trait Adapter { + fn reply(&self, message: &str) -> impl std::future::Future> + Send; +} + +impl Adapter for aionbot_core::event::Event { + async fn reply(&self, message: &str) -> Result<()> { + let _ = message; + // let ws = onebot_v11::connect::ws_reverse::ReverseWsConnect::new(config); + unimplemented!() + } +} diff --git a/crates/aionbot-core/src/core.rs b/crates/aionbot-core/src/core.rs deleted file mode 100644 index 68b6631..0000000 --- a/crates/aionbot-core/src/core.rs +++ /dev/null @@ -1,18 +0,0 @@ -use std::sync::Arc; - -use anyhow::Result; - -use crate::{event::Event, handler::Handler}; - -pub struct Core { - handlers: Vec, -} - -impl Core { - pub async fn input(&mut self, event: Arc) -> Result<()> { - for handler in self.handlers.iter() { - handler.input(&event).await?; - } - Ok(()) - } -} diff --git a/crates/aionbot-core/src/entry.rs b/crates/aionbot-core/src/entry.rs index 7d87f4f..ed521f4 100644 --- a/crates/aionbot-core/src/entry.rs +++ b/crates/aionbot-core/src/entry.rs @@ -4,10 +4,10 @@ use crate::{router::Router, types::Callback}; #[derive(Clone)] pub struct Entry { - id: String, - priority: i8, - router: Arc>, - callback: Arc, + pub id: &'static str, + pub priority: i8, + pub router: Arc>, + pub callback: Arc, } impl Entry { diff --git a/crates/aionbot-core/src/handler.rs b/crates/aionbot-core/src/handler.rs index 94606b5..72fb2c8 100644 --- a/crates/aionbot-core/src/handler.rs +++ b/crates/aionbot-core/src/handler.rs @@ -20,14 +20,14 @@ impl Handler { pub async fn input(&self, event: &Arc) -> Result<()> { let mut queue = self.matches(event); while let Some(entry) = queue.pop() { - entry.get_handler()(event).await?; + entry.get_handler()(event.clone()).await?; } Ok(()) } + #[inline] pub fn matches(&self, event: &Arc) -> EventQueue { let mut queue = EventQueue::new(); - for entry in self.entries.iter() { if entry.get_router().matches(event) { queue.push(entry.get_priority(), entry.clone()); diff --git a/crates/aionbot-core/src/lib.rs b/crates/aionbot-core/src/lib.rs index 513a614..9ae57fd 100644 --- a/crates/aionbot-core/src/lib.rs +++ b/crates/aionbot-core/src/lib.rs @@ -1,7 +1,7 @@ -pub mod core; pub mod entry; pub mod event; pub mod handler; +pub mod prelude; pub mod queue; pub mod router; pub mod types; diff --git a/crates/aionbot-core/src/prelude.rs b/crates/aionbot-core/src/prelude.rs new file mode 100644 index 0000000..101e033 --- /dev/null +++ b/crates/aionbot-core/src/prelude.rs @@ -0,0 +1,4 @@ +pub use crate::entry::Entry; +pub use crate::event::Event; +pub use crate::router::*; +pub use crate::types::*; diff --git a/crates/aionbot-core/src/queue.rs b/crates/aionbot-core/src/queue.rs index 2eb3ae0..d43a199 100644 --- a/crates/aionbot-core/src/queue.rs +++ b/crates/aionbot-core/src/queue.rs @@ -87,16 +87,6 @@ impl EventQueue { pub fn is_empty(&self) -> bool { self.heap.is_empty() } - - pub fn generate(&mut self) -> impl Iterator> + '_ { - std::iter::from_fn(move || { - if !self.is_empty() { - Some(self.pop()) - } else { - None - } - }) - } } impl Default for EventQueue { diff --git a/crates/aionbot-core/src/router.rs b/crates/aionbot-core/src/router.rs index 04b868d..2b9abd6 100644 --- a/crates/aionbot-core/src/router.rs +++ b/crates/aionbot-core/src/router.rs @@ -10,53 +10,60 @@ impl Router for str { } } +impl Router for &str { + fn matches(&self, event: &Event) -> bool { + self == &event.plain_data.to_string() + } +} + impl Router for String { - fn matches(&self, message: &Event) -> bool { - self == &message.plain_data.to_string() + fn matches(&self, event: &Event) -> bool { + self == &event.plain_data.to_string() } } -// pub struct ExactMatchRouter { -// pattern: String, -// ignore_spaces: bool, -// } +pub struct ExactMatchRouter { + pattern: String, + ignore_spaces: bool, +} -// impl ExactMatchRouter { -// pub fn new(pattern: &str, ignore_spaces: bool) -> Self { -// Self { -// pattern: pattern.to_string(), -// ignore_spaces, -// } -// } -// } +impl ExactMatchRouter { + pub fn new(pattern: &str, ignore_spaces: bool) -> Self { + Self { + pattern: pattern.to_string(), + ignore_spaces, + } + } +} -// impl Router for ExactMatchRouter { -// fn matches(&self, message: &str) -> bool { -// if self.ignore_spaces { -// message.replace(" ", "") == self.pattern.replace(" ", "") -// } else { -// message == self.pattern -// } -// } -// } +impl Router for ExactMatchRouter { + fn matches(&self, event: &Event) -> bool { + let message = event.plain_data.to_string(); + if self.ignore_spaces { + message.replace(" ", "") == self.pattern.replace(" ", "") + } else { + message == self.pattern + } + } +} -// pub struct RegexRouter { -// pattern: regex::Regex, -// } +pub struct RegexRouter { + pattern: regex::Regex, +} -// impl RegexRouter { -// pub fn new(pattern: &str) -> Self { -// Self { -// pattern: regex::Regex::new(pattern).unwrap(), -// } -// } -// } +impl RegexRouter { + pub fn new(pattern: &str) -> Self { + Self { + pattern: regex::Regex::new(pattern).unwrap(), + } + } +} -// impl Router for RegexRouter { -// fn matches(&self, message: &str) -> bool { -// self.pattern.is_match(message) -// } -// } +impl Router for RegexRouter { + fn matches(&self, event: &Event) -> bool { + self.pattern.is_match(&event.plain_data.to_string()) + } +} // pub struct StartsWithRouter { // pattern: String, diff --git a/crates/aionbot-core/src/types.rs b/crates/aionbot-core/src/types.rs index b66d49a..425be50 100644 --- a/crates/aionbot-core/src/types.rs +++ b/crates/aionbot-core/src/types.rs @@ -1,6 +1,9 @@ +use std::sync::Arc; + use anyhow::Result; use futures::future::BoxFuture; use crate::event::Event; -pub type Callback = fn(&Event) -> BoxFuture<'static, Result>; +pub type HandlerCallback = BoxFuture<'static, Result<()>>; +pub type Callback = fn(Arc) -> HandlerCallback; diff --git a/crates/aionbot-macros/Cargo.toml b/crates/aionbot-macros/Cargo.toml new file mode 100644 index 0000000..35506cc --- /dev/null +++ b/crates/aionbot-macros/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "aionbot-macros" +version = "0.1.0" +edition = "2021" + +[dependencies] +proc-macro2 = "1.0.86" +quote = "1.0.37" +syn = { version = "2.0.77", features = ["full"] } + +[lib] +proc-macro = true diff --git a/crates/aionbot-macros/src/lib.rs b/crates/aionbot-macros/src/lib.rs new file mode 100644 index 0000000..7ba3ab6 --- /dev/null +++ b/crates/aionbot-macros/src/lib.rs @@ -0,0 +1,105 @@ +use std::hash::{DefaultHasher, Hash, Hasher}; + +use proc_macro::TokenStream; +use quote::quote; +use syn::{meta::ParseNestedMeta, parse_macro_input, Result}; + +struct HandlerArgs { + priority: syn::LitInt, + router: Option, +} + +impl Default for HandlerArgs { + fn default() -> Self { + Self { + priority: syn::LitInt::new("0", proc_macro2::Span::call_site()), + router: None, + } + } +} + +impl HandlerArgs { + fn parse(&mut self, meta: ParseNestedMeta) -> Result<()> { + if let Some(ident) = meta.path.get_ident() { + match ident.to_string().as_str() { + "router" => { + self.router = Some(meta.value()?.parse()?); + Ok(()) + } + "priority" => { + self.priority = meta.value()?.parse()?; + Ok(()) + } + _ => Err(meta.error("msg")), + } + } else { + Err(meta.error("msg")) + } + } + + fn is_empty(&self) -> bool { + self.router.is_none() + } +} + +#[proc_macro_attribute] +pub fn register(attr: TokenStream, item: TokenStream) -> TokenStream { + let input = parse_macro_input!(item as syn::ItemFn); + + let mut attrs = HandlerArgs::default(); + let parser = syn::meta::parser(|meta| attrs.parse(meta)); + parse_macro_input!(attr with parser); + + let vis = &input.vis; + match vis { + syn::Visibility::Public(_) => {} + _ => { + return TokenStream::from( + quote! { compile_error!("Only public functions can be registered"); }, + ) + } + } + + let origin_ident = &input.sig.ident; + let mut fn_name = input.sig.ident.to_string(); + let mut hasher = DefaultHasher::new(); + fn_name.hash(&mut hasher); + let hash_id = hasher.finish().to_string(); + fn_name.extend("_".chars().chain(hash_id.chars())); + + let fn_name_ident = syn::Ident::new(&fn_name, input.sig.ident.span()); + + let fn_args = &input.sig.inputs; + let fn_body = &input.block; + + let default_router = quote! { "default" }.into(); + let default_router = parse_macro_input!(default_router as syn::Expr); + let router = attrs.router.as_ref().unwrap_or(&default_router); + + if attrs.is_empty() { + return TokenStream::from( + quote! { compile_error!("Missing `#[register(router = \"...\")]` attribute"); }, + ); + }; + + let expanded = quote! { + use std::sync::*; + use std::cell::*; + use aionbot_core::prelude::*; + + pub fn #fn_name_ident(#fn_args) -> HandlerCallback { + Box::pin(async move { #fn_body }) + } + + pub fn #origin_ident() -> Entry { + Entry { + id: #hash_id, + priority: 0, + router: Arc::new(Box::new(#router)), + callback: Arc::new(#fn_name_ident), + } + } + }; + + TokenStream::from(expanded) +} diff --git a/crates/aionbot/Cargo.toml b/crates/aionbot/Cargo.toml new file mode 100644 index 0000000..98c697a --- /dev/null +++ b/crates/aionbot/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "aionbot" +version = "0.1.0" +edition = "2021" + +[dependencies] +aionbot-adapter-onebot = { version = "0.1.0", path = "../aionbot-adapter-onebot" } +aionbot-core = { version = "0.1.0", path = "../aionbot-core" } +aionbot-macros = { version = "0.1.0", path = "../aionbot-macros" } +tokio = { version = "1.40.0", features = ["full"] } diff --git a/crates/aionbot/src/main.rs b/crates/aionbot/src/main.rs new file mode 100644 index 0000000..a101fe0 --- /dev/null +++ b/crates/aionbot/src/main.rs @@ -0,0 +1,19 @@ +use std::sync::Arc; + +use aionbot_adapter_onebot::Adapter; +use aionbot_macros::register; + +#[register(router = ExactMatchRouter::new("hello", false))] +pub async fn hello_world(event: Arc) -> Result { + println!("{}", &event.plain_data.to_string()); + event.reply("Hello, world!").await?; + Ok(()) +} + +#[tokio::main] +async fn main() { + let entries = vec![hello_world()]; + for entry in entries { + println!("{}", entry.id); + } +}