ruma-bot

git clone git://archive.git.mtrnord.blog/MTRNord/ruma-bot.git
Log | Files | Refs

commit daa9349cfe2c7b22b762762c89cbe0fc2a97a2fe
parent c148d48597648bca8b97bc550d55d35a1fb5899e
Author: Jonas Platte <jplatte+git@posteo.de>
Date:   Sun, 28 Jul 2019 01:11:41 +0200

Get the basic machinery for calling handler fns into place

Diffstat:
MCargo.toml | 10++++++++--
Mmacro/src/lib.rs | 49++++++++++++++++++++++++++++++++++++++++++-------
Asrc/command_handler_fn.rs | 44++++++++++++++++++++++++++++++++++++++++++++
Msrc/lib.rs | 194++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Mtests/echo.rs | 5++++-
5 files changed, 277 insertions(+), 25 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -6,6 +6,13 @@ edition = "2018" [dependencies] anymap = "0.12.1" +failure = "0.1.5" +futures-preview = "0.3.0-alpha.17" +js_int = "0.1.1" +hyper = { git = "https://github.com/hyperium/hyper" } +hyper-tls = { git = "https://github.com/hyperium/hyper-tls" } +ruma-client = { path = "../ruma-client" } +url = "2.0.0" [dependencies.ruma-bot-macro] path = "macro" @@ -14,4 +21,4 @@ path = "macro" git = "https://github.com/tokio-rs/tokio" [workspace] -members = ["macro"] -\ No newline at end of file +members = ["macro"] diff --git a/macro/src/lib.rs b/macro/src/lib.rs @@ -1,9 +1,11 @@ extern crate proc_macro; use proc_macro::TokenStream; +use std::mem; +use proc_macro2::Span; use quote::quote; -use syn::{parse_macro_input, ItemFn}; +use syn::{parse_macro_input, Ident, ItemFn}; use args::Args; @@ -11,29 +13,62 @@ mod args; #[proc_macro_attribute] pub fn command_handler(args: TokenStream, input: TokenStream) -> TokenStream { - let handler_fn = parse_macro_input!(input as ItemFn); + let mut handler_fn = parse_macro_input!(input as ItemFn); let macro_args = parse_macro_input!(args as Args); assert!( + handler_fn.decl.variadic.is_none(), + "ruma_bot handler functions are not allowed to be variadic", + ); + + assert!( handler_fn.unsafety.is_none(), - "ruma_bot handler functions are not allowed to be `unsafe`" + "ruma_bot handler functions are not allowed to be `unsafe`", ); - let ident = handler_fn.ident; + let infer_type = syn::Type::Infer(syn::TypeInfer { + underscore_token: Default::default(), + }); + let fn_type = syn::TypeBareFn { + lifetimes: None, + unsafety: None, + abi: handler_fn.abi.clone(), + fn_token: Default::default(), + paren_token: Default::default(), + inputs: (0..handler_fn.decl.inputs.len()) + .map(|_| syn::BareFnArg { + name: None, + ty: infer_type.clone(), + }) + .collect(), + variadic: None, + output: syn::ReturnType::Type(Default::default(), Box::new(infer_type)), + }; + + let ident = mem::replace( + &mut handler_fn.ident, + Ident::new("command_handler_impl", Span::call_site()), + ); // TODO let commands = vec![ident.to_string()]; TokenStream::from(quote! { #[allow(non_camel_case_types)] + #[derive(Clone, Copy)] struct #ident; impl ruma_bot::CommandHandler for #ident { - fn commands(&self) -> &'static [&'static str] { + fn commands() -> &'static [&'static str] { &[#(#commands),*] } - fn get_fn(&self) -> Box<dyn ruma_bot::CommandHandlerFn> { - unimplemented!() + fn call( + &mut self, + bot: &ruma_bot::Bot, + ) -> Box<dyn futures::Future<Output = Result<(), failure::Error>>> { + #handler_fn + + ruma_bot::CommandHandlerFn::call(command_handler_impl as #fn_type, bot) } } }) diff --git a/src/command_handler_fn.rs b/src/command_handler_fn.rs @@ -0,0 +1,44 @@ +use failure::Error; +use futures::{Future, FutureExt}; + +use crate::Bot; + +pub trait CommandHandlerFn { + fn call(self, bot: &Bot) -> Box<dyn Future<Output = Result<(), Error>> + Send>; +} + +/*impl<R> CommandHandlerFn for fn() -> R +where + R: Future<Output = ()> + Send + 'static, // TODO: 'static bound reasonable? +{ + fn call(self, _bot: &Bot) -> Box<dyn Future<Output = Result<(), Error>> + Send> { + Box::new((self)().map(|_| Ok(()))) + } +} + +impl<R> CommandHandlerFn for fn(&Bot) -> R +where + R: Future<Output = ()> + Send + 'static, // TODO: 'static bound reasonable? +{ + fn call(self, bot: &Bot) -> Box<dyn Future<Output = Result<(), Error>> + Send> { + Box::new((self)(bot).map(|_| Ok(()))) + } +}*/ + +impl<R> CommandHandlerFn for fn() -> R +where + R: Future<Output = Result<(), Error>> + Send + 'static, // TODO: 'static bound reasonable? +{ + fn call(self, _bot: &Bot) -> Box<dyn Future<Output = Result<(), Error>> + Send> { + Box::new((self)()) + } +} + +impl<R> CommandHandlerFn for fn(&Bot) -> R +where + R: Future<Output = Result<(), Error>> + Send + 'static, // TODO: 'static bound reasonable? +{ + fn call(self, bot: &Bot) -> Box<dyn Future<Output = Result<(), Error>> + Send> { + Box::new((self)(bot)) + } +} diff --git a/src/lib.rs b/src/lib.rs @@ -11,60 +11,224 @@ use std::{collections::HashMap, sync::Arc}; use anymap::any::Any; +use failure::{err_msg, Fallible}; +use futures::{Future, TryStreamExt}; +use js_int::UInt; +use ruma_client::{api, HttpsClient as MatrixClient}; +use url::Url; + type AnyMap = anymap::Map<dyn Any + Send + Sync>; +type HandlerFnMap = HashMap<&'static str, Box<dyn CommandHandler>>; pub use ruma_bot_macro::command_handler; +mod command_handler_fn; mod state; +#[doc(hidden)] +pub use command_handler_fn::CommandHandlerFn; + +fn env_var(name: &'static str) -> Option<String> { + std::env::var(name) + .map_err(|e| { + if let std::env::VarError::NotUnicode(_) = e { + panic!("ruma_bot: {} contains non-unicode bytes", name); + } + }) + .ok() +} + /// A function (usually `async`) annotated with `#[ruma_bot::command_handler] -pub trait CommandHandler { +pub trait CommandHandler: Send + Sync { /// The command(s) this function handles - fn commands(&self) -> &'static [&'static str]; + fn commands() -> &'static [&'static str] + where + Self: Sized; - fn get_fn(&self) -> Box<dyn CommandHandlerFn>; + /// Used to call the command handler with the necessary application state and command context + // TODO: Additional parameter(s) for command details + fn call(&mut self, bot: &Bot) -> Box<dyn Future<Output = Result<(), failure::Error>>>; } -pub trait CommandHandlerFn: Send + Sync { - //fn +struct ConnectionDetails { + //homeserver_url: Url, + username: String, + password: String, } pub struct BotBuilder { - handlers: HashMap<&'static str, Box<dyn CommandHandlerFn>>, + handlers: HandlerFnMap, + homeserver_url: Option<Url>, + username: Option<String>, + password: Option<String>, } impl BotBuilder { pub fn new() -> Self { Self { handlers: HashMap::new(), + homeserver_url: None, + username: None, + password: None, } } /// Register a command handler - pub fn register(mut self, handler: impl CommandHandler) -> Self { - for command in handler.commands() { - let _old_value = self.handlers.insert(command, handler.get_fn()); - // TODO: Log a warning if _old_value is Some + pub fn register<T>(mut self, handler: T) -> Self + where + T: CommandHandler + Copy + 'static, + { + for command in T::commands() { + let old_value = self.handlers.insert(command, Box::new(handler)); + assert!( + old_value.is_none(), + "ruma_bot: Tried to register a command handler for '{}' when one was already registered", + command, + ); } self } - pub fn build(self) -> Bot { - Bot { + pub fn homeserver_url(mut self, homeserver_url: Url) -> Self { + self.homeserver_url = Some(homeserver_url); + self + } + + pub fn username(mut self, username: String) -> Self { + self.username = Some(username); + self + } + + pub fn password(mut self, password: String) -> Self { + self.password = Some(password); + self + } + + pub fn build(self) -> Fallible<Bot> { + Ok(Bot { + client: MatrixClient::https( + self.homeserver_url + .or_else(|| { + env_var("RUMA_BOT_HOMESERVER_URL") + .map(|s| s.parse().expect("valid url for RUMA_BOT_HOMESERVER_URL")) + }) + .ok_or(err_msg("ruma_bot: homeserver_url not configured"))?, + None, + )?, handlers: Arc::new(self.handlers), state: AnyMap::new(), - } + connection_details: Some(ConnectionDetails { + username: self + .username + .or_else(|| env_var("RUMA_BOT_USERNAME")) + .ok_or(err_msg("ruma_bot: username not configured"))?, + password: self + .password + .or_else(|| env_var("RUMA_BOT_PASSWORD")) + .ok_or(err_msg("ruma_bot: password not configured"))?, + }), + }) } } pub struct Bot { - handlers: Arc<HashMap<&'static str, Box<dyn CommandHandlerFn>>>, + client: MatrixClient, + handlers: Arc<HandlerFnMap>, state: AnyMap, + connection_details: Option<ConnectionDetails>, } impl Bot { - pub async fn run(&self) -> Result<(), ()> { + pub async fn run(mut self) -> Result<(), ruma_client::Error> { + // A Bot is always instantiated with connection_details = Some(...) and this method (that + // can only be called once) is the only one method that uses connection_details. Thus, this + // .unwrap() will never fail. + let cd = self.connection_details.take().unwrap(); + + self.client.log_in(cd.username, cd.password, None).await?; + + use api::r0::{ + filter::{Filter, FilterDefinition, RoomEventFilter, RoomFilter}, + sync::sync_events::Filter as SyncFilter, + }; + + let mut sync0 = Box::pin(self.client.sync( + Some(SyncFilter::FilterDefinition(FilterDefinition { + event_fields: Vec::new(), + event_format: None, + account_data: Some(Filter { + limit: Some(UInt::from(0u32)), + //..Filter::default() + senders: Vec::new(), + not_senders: Vec::new(), + types: Vec::new(), + not_types: Vec::new(), + }), + room: Some(RoomFilter { + account_data: Some(RoomEventFilter { + limit: Some(UInt::from(0u32)), + //..RoomEventFilter::default() + rooms: Vec::new(), + not_rooms: Vec::new(), + senders: Vec::new(), + not_senders: Vec::new(), + types: Vec::new(), + not_types: Vec::new(), + }), + timeline: Some(RoomEventFilter { + limit: Some(UInt::from(0u32)), + //..RoomEventFilter::default() + rooms: Vec::new(), + not_rooms: Vec::new(), + senders: Vec::new(), + not_senders: Vec::new(), + types: Vec::new(), + not_types: Vec::new(), + }), + ephemeral: Some(RoomEventFilter { + limit: Some(UInt::from(0u32)), + //..RoomEventFilter::default() + rooms: Vec::new(), + not_rooms: Vec::new(), + senders: Vec::new(), + not_senders: Vec::new(), + types: Vec::new(), + not_types: Vec::new(), + }), + state: Some(RoomEventFilter { + limit: Some(UInt::from(0u32)), + //..RoomEventFilter::default() + rooms: Vec::new(), + not_rooms: Vec::new(), + senders: Vec::new(), + not_senders: Vec::new(), + types: Vec::new(), + not_types: Vec::new(), + }), + //..RoomFilter::default() + include_leave: None, + rooms: Vec::new(), + not_rooms: Vec::new(), + }), + presence: Some(Filter { + limit: Some(UInt::from(0u32)), + //..Filter::default() + senders: Vec::new(), + not_senders: Vec::new(), + types: Vec::new(), + not_types: Vec::new(), + }), + })), + None, + false, + )); + + let sync_start = sync0.try_next().await?.expect("sync response").next_batch; + let mut sync_stream = Box::pin(self.client.sync(None, Some(sync_start), true)); + + while let Some(res) = sync_stream.try_next().await? {} + Ok(()) } } diff --git a/tests/echo.rs b/tests/echo.rs @@ -1,9 +1,12 @@ #![feature(async_await)] +use failure::Fallible; use ruma_bot::{command_handler, BotBuilder}; #[command_handler(command = "help")] -async fn help() {} +async fn help() -> Fallible<()> { + Ok(()) +} #[tokio::main] async fn main() {