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:
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() {