mrsbfh

Matrix-Rust-SDK-Bot-Framework-Helper
git clone git://archive.git.mtrnord.blog/MTRNord/mrsbfh.git
Log | Files | Refs | README

commit 346af5b8c1563ec3bff09d41b119ed431b2436cc
parent 035af787f086f2c40216e206f639f0f50d03f62f
Author: MTRNord <mtrnord1@gmail.com>
Date:   Thu, 31 Dec 2020 03:06:45 +0100

feat: Add most basic utils and a basic example bot.

Diffstat:
MCargo.toml | 3++-
Aexample-bot/Cargo.toml | 18++++++++++++++++++
Aexample-bot/src/commands/hello_world.rs | 18++++++++++++++++++
Aexample-bot/src/commands/mod.rs | 12++++++++++++
Aexample-bot/src/config.rs | 12++++++++++++
Aexample-bot/src/main.rs | 37+++++++++++++++++++++++++++++++++++++
Aexample-bot/src/matrix/mod.rs | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aexample-bot/src/matrix/sync.rs | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmrsbfh-macros/src/lib.rs | 131++++++++++++++++++++++++++++---------------------------------------------------
Amrsbfh-macros/src/utils.rs | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmrsbfh/Cargo.toml | 21+++++++++++++++++++++
Cmrsbfh/src/lib.rs -> mrsbfh/src/commands.rs | 0
Amrsbfh/src/config.rs | 12++++++++++++
Mmrsbfh/src/lib.rs | 16++++++++++++++--
Amrsbfh/src/utils.rs | 43+++++++++++++++++++++++++++++++++++++++++++
15 files changed, 470 insertions(+), 87 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -2,5 +2,6 @@ members = [ "mrsbfh", - "mrsbfh-macros" + "mrsbfh-macros", + "example-bot" ] diff --git a/example-bot/Cargo.toml b/example-bot/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "example-bot" +version = "0.1.0" +authors = ["MTRNord <mtrnord1@gmail.com>"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +mrsbfh = {path = "../mrsbfh"} +serde = "1.0" +tracing = "0.1" +tracing-subscriber = "0.2" +tracing-futures = "0.2.4" +tokio = { version = "0.2", features = ["full"] } +clap = "3.0.0-beta.2" +async-trait = "0.1.41" +\ No newline at end of file diff --git a/example-bot/src/commands/hello_world.rs b/example-bot/src/commands/hello_world.rs @@ -0,0 +1,18 @@ +use mrsbfh::commands::command; +use mrsbfh::config::Config; +use mrsbfh::matrix_sdk::events::{room::message::MessageEventContent, AnyMessageEventContent}; +use std::error::Error; + +#[command(help = "`!hello_world` - Prints \"hello world\".")] +pub async fn hello_world<C: Config>( + mut tx: mrsbfh::Sender, + _config: C, + _sender: String, + mut _args: Vec<&str>, +) -> Result<(), Box<dyn Error>> { + let content = + AnyMessageEventContent::RoomMessage(MessageEventContent::notice_plain("Hello World!")); + + tx.send(content).await?; + Ok(()) +} diff --git a/example-bot/src/commands/mod.rs b/example-bot/src/commands/mod.rs @@ -0,0 +1,11 @@ +use mrsbfh::commands::command_generate; + +pub mod hello_world; + +#[command_generate( + bot_name = "Example", + description = "This bot prints hello!" +)] +enum Commands { + HelloWorld +} +\ No newline at end of file diff --git a/example-bot/src/config.rs b/example-bot/src/config.rs @@ -0,0 +1,12 @@ +use mrsbfh::config::ConfigDerive; +use serde::{Deserialize, Serialize}; +use std::borrow::Cow; + +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone, ConfigDerive)] +pub struct Config<'a> { + pub homeserver_url: Cow<'a, str>, + pub mxid: Cow<'a, str>, + pub password: Cow<'a, str>, + pub store_path: Cow<'a, str>, + pub session_path: Cow<'a, str>, +} diff --git a/example-bot/src/main.rs b/example-bot/src/main.rs @@ -0,0 +1,37 @@ +use crate::config::Config; +use clap::Clap; +use mrsbfh::config::Config as _; +use std::error::Error; +use tracing::*; + +pub mod commands; +mod config; +mod matrix; + +#[derive(Clap)] +#[clap(version = "0.1.0", author = "MTRNord")] +struct Opts { + #[clap(short, long, default_value = "config.yml")] + config: String, +} + +#[tokio::main] +async fn main() -> Result<(), Box<dyn Error>> { + tracing_subscriber::fmt() + .pretty() + .with_thread_names(true) + .with_max_level(tracing::Level::INFO) + .init(); + + info!("Starting..."); + let opts: Opts = Opts::parse(); + + info!("Loading Configs..."); + let config = Config::load(opts.config)?; + info!("Setting up Client..."); + let client = &mut matrix::setup(config.clone()).await?; + info!("Starting Sync..."); + matrix::start_sync(client, config).await?; + + Ok(()) +} diff --git a/example-bot/src/matrix/mod.rs b/example-bot/src/matrix/mod.rs @@ -0,0 +1,84 @@ +use crate::config::Config; +use mrsbfh::matrix_sdk::{Client, ClientConfig, Session as SDKSession, SyncSettings}; +use mrsbfh::url::Url; +use mrsbfh::utils::Session; +use std::convert::TryFrom; +use std::error::Error; +use std::fs; +use std::path::Path; +use tracing::*; + +mod sync; + +pub async fn setup(config: Config<'_>) -> Result<Client, Box<dyn Error>> { + info!("Beginning Matrix Setup"); + let store_path_string = config.store_path.to_string(); + let store_path = Path::new(&store_path_string); + if !store_path.exists() { + fs::create_dir_all(store_path)?; + } + let client_config = ClientConfig::new().store_path(fs::canonicalize(&store_path)?); + + let homeserver_url = + Url::parse(&config.homeserver_url).expect("Couldn't parse the homeserver URL"); + + let client = Client::new_with_config(homeserver_url, client_config).unwrap(); + + if let Some(session) = Session::load(config.session_path.parse().unwrap()) { + info!("Starting relogin"); + + let session = SDKSession { + access_token: session.access_token, + device_id: session.device_id.into(), + user_id: mrsbfh::matrix_sdk::identifiers::UserId::try_from(session.user_id.as_str()) + .unwrap(), + }; + + if let Err(e) = client.restore_login(session).await { + error!("{}", e); + }; + info!("Finished relogin"); + } else { + info!("Starting login"); + let login_response = client + .login( + &config.mxid, + &config.password, + None, + Some(&"timetracking-bot".to_string()), + ) + .await; + match login_response { + Ok(login_response) => { + info!("Session: {:#?}", login_response); + let session = Session { + homeserver: client.homeserver().to_string(), + user_id: login_response.user_id.to_string(), + access_token: login_response.access_token, + device_id: login_response.device_id.into(), + }; + session.save(config.session_path.parse().unwrap())?; + } + Err(e) => error!("Error while login: {}", e), + } + info!("Finished login"); + } + + info!("logged in as {}", config.mxid); + + Ok(client) +} + +pub async fn start_sync( + client: &mut Client, + config: Config<'static>, +) -> Result<(), Box<dyn Error>> { + client + .add_event_emitter(Box::new(sync::Bot::new(client.clone(), config.clone()))) + .await; + + info!("Starting full Sync..."); + client.sync(SyncSettings::default()).await; + + Ok(()) +} diff --git a/example-bot/src/matrix/sync.rs b/example-bot/src/matrix/sync.rs @@ -0,0 +1,86 @@ +use crate::commands::match_command; +use crate::config::Config; +use mrsbfh::matrix_sdk::{ + events::{ + room::message::{MessageEventContent, TextMessageEventContent}, + SyncMessageEvent, + }, + Client, EventEmitter, SyncRoom, +}; +use mrsbfh::matrix_sdk_common_macros::async_trait; +use tokio::sync::mpsc; +use tracing::*; + +#[derive(Debug, Clone)] +pub struct Bot { + client: Client, + config: Config<'static>, +} + +impl Bot { + pub fn new(client: Client, config: Config<'static>) -> Self { + Self { + client, + config: config.clone(), + } + } +} + +#[async_trait] +impl EventEmitter for Bot { + async fn on_room_message(&self, room: SyncRoom, event: &SyncMessageEvent<MessageEventContent>) { + if let SyncRoom::Joined(room) = room { + let msg_body = if let SyncMessageEvent { + content: MessageEventContent::Text(TextMessageEventContent { body: msg_body, .. }), + .. + } = event + { + msg_body.clone() + } else { + String::new() + }; + if msg_body.is_empty() { + return; + } + + let sender = event.sender.clone().to_string(); + + let (tx, mut rx) = mpsc::channel(100); + let room_id = room.read().await.clone().room_id; + + let cloned_config = self.config.clone(); + tokio::spawn(async move { + let mut split = msg_body.split_whitespace(); + + let command_raw = split.next().expect("This is not a command"); + let command = command_raw.to_lowercase(); + info!("Got command: {}", command); + + // Make sure this is immutable + let args: Vec<&str> = split.collect(); + if let Err(e) = match_command( + command.replace("!", "").as_str(), + cloned_config.clone(), + tx, + sender, + args, + ) + .await + { + error!("{}", e); + } + }); + + while let Some(v) = rx.recv().await { + if let Err(e) = self + .client + .clone() + .room_send(&room_id.clone(), v, None) + .await + { + error!("{}", e); + } + } + } + } +} diff --git a/mrsbfh-macros/src/lib.rs b/mrsbfh-macros/src/lib.rs @@ -1,74 +1,17 @@ +pub(crate) mod utils; + +use crate::utils::get_arg; use convert_case::{Case, Casing}; use proc_macro::TokenStream; use quote::quote; use syn::parse_macro_input; use syn::spanned::Spanned; -fn get_arg<'a>( - input_span: proc_macro2::Span, - args: syn::AttributeArgs, - arg: &'a str, - expected: &'a str, - expected_args: usize, -) -> Result<syn::LitStr, TokenStream> { - if args.len() == expected_args { - let meta = args - .iter() - .filter_map(|x| { - if let syn::NestedMeta::Meta(ref meta) = x { - if meta.path().is_ident(&arg) { - return Some(meta); - } - } - None - }) - .next(); - if let Some(meta) = meta { - if let syn::Meta::NameValue(ref meta) = meta { - let meta_lit = meta.lit.clone(); - return match meta_lit { - syn::Lit::Str(s) => Ok(s), - _ => { - let error = syn::Error::new( - meta.lit.span(), - format!( - "expected `{}`\n\nThe field '{}' needs to be a str literal!", - expected, arg - ), - ) - .to_compile_error(); - Err(quote! {#error}.into()) - } - }; - } - } else { - let error = syn::Error::new( - meta.span(), - format!( - "1expected `{}`\n\nThe field '{}' is required!", - expected, arg - ), - ) - .to_compile_error(); - return Err(quote! {#error}.into()); - } - } - let error = syn::Error::new( - input_span, - format!( - "expected `{}` but not enough arguments where provided.", - expected - ), - ) - .to_compile_error(); - Err(quote! {#error}.into()) -} - /// Used to define a command /// /// ```compile_fail /// #[command(help = "Description")] -/// async fn r#in(mut tx: tokio::sync::mpsc::Sender<matrix_sdk::events::AnyMessageEventContent>, sender: String, mut args: Vec<&str>) -> Result<(), ParseErrors> {} +/// async fn hello_world<C: mrsbfh::config::Config>(mut tx: mrsbfh::Sender, config: C, sender: String, mut args: Vec<&str>) -> Result<(), Box<dyn std::error::Error>> {} /// ``` #[proc_macro_attribute] pub fn command(args: TokenStream, input: TokenStream) -> TokenStream { @@ -79,7 +22,7 @@ pub fn command(args: TokenStream, input: TokenStream) -> TokenStream { let help_const_name = syn::Ident::new( &format!( "{}_HELP", - input.sig.ident.to_string().to_uppercase().replace("R#", "") + input.sig.ident.to_string().to_uppercase().replace("r#", "") ), input.sig.span(), ); @@ -126,7 +69,7 @@ pub fn command_generate(args: TokenStream, input: TokenStream) -> TokenStream { .ident .to_string() .to_case(Case::Snake) - .split("_") + .split('_') .map(|x| x.chars().next().unwrap().to_string().to_lowercase()) .collect(); chars.join("") @@ -186,46 +129,66 @@ pub fn command_generate(args: TokenStream, input: TokenStream) -> TokenStream { let help_preamble = help_title + &description + commands_title; let code = quote! { - use const_concat::*; + use mrsbfh::const_concat::*; const HELP_MARKDOWN: &str = const_concat!(#help_preamble, #(#help_parts,)*); async fn help( - mut tx: tokio::sync::mpsc::Sender<matrix_sdk::events::AnyMessageEventContent>, - ) -> Result<(), crate::errors::ParseErrors> { - let options = pulldown_cmark::Options::empty(); - let parser = pulldown_cmark::Parser::new_ext(HELP_MARKDOWN, options); + mut tx: mrsbfh::Sender, + ) -> Result<(), std::boxed::Box<dyn std::error::Error>> { + let options = mrsbfh::pulldown_cmark::Options::empty(); + let parser = mrsbfh::pulldown_cmark::Parser::new_ext(HELP_MARKDOWN, options); let mut html = String::new(); - pulldown_cmark::html::push_html(&mut html, parser); + mrsbfh::pulldown_cmark::html::push_html(&mut html, parser); let owned_html = html.to_owned(); - tokio::spawn(async move { - let content = matrix_sdk::events::AnyMessageEventContent::RoomMessage( - matrix_sdk::events::room::message::MessageEventContent::notice_html( + mrsbfh::tokio::spawn(async move { + let content = mrsbfh::matrix_sdk::events::AnyMessageEventContent::RoomMessage( + mrsbfh::matrix_sdk::events::room::message::MessageEventContent::notice_html( HELP_MARKDOWN, owned_html, ), ); if let Err(e) = tx.send(content).await { - tracing::error!("Error: {}",e); + mrsbfh::tracing::error!("Error: {}",e); }; }); Ok(()) } - pub async fn match_command(cmd: &str, config: crate::config::Config<'_>, tx: tokio::sync::mpsc::Sender<matrix_sdk::events::AnyMessageEventContent>, sender: String, args: Vec<&str>,) -> Result<(), crate::errors::ParseErrors> { - match cmd { - #(#commands)* - "help" => { - help(tx).await - }, - "h" => { - help(tx).await - }, - _ => {Ok(())} - } + + pub async fn match_command<C: mrsbfh::config::Config>(cmd: &str, config: C, tx: mrsbfh::Sender, sender: String, args: Vec<&str>,) -> Result<(), std::boxed::Box<dyn std::error::Error>> { + match cmd { + #(#commands)* + "help" => { + help(tx).await + }, + "h" => { + help(tx).await + }, + _ => {Ok(())} } + } }; code.into() } + +#[proc_macro_derive(ConfigDerive)] +pub fn config_derive(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as syn::DeriveInput); + let name = &ast.ident; + + let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); + let expanded = quote! { + impl #impl_generics mrsbfh::config::Config for #name #ty_generics #where_clause { + fn load<P: AsRef<std::path::Path> + std::fmt::Debug>(path: P) -> Result<Self, Box<dyn std::error::Error>> { + let contents = std::fs::read_to_string(path).expect("Something went wrong reading the file"); + let config: Self = mrsbfh::serde_yaml::from_str(&contents)?; + Ok(config) + } + } + }; + + TokenStream::from(expanded) +} diff --git a/mrsbfh-macros/src/utils.rs b/mrsbfh-macros/src/utils.rs @@ -0,0 +1,63 @@ +use proc_macro::TokenStream; +use syn::spanned::Spanned; +use quote::quote; + +pub(crate) fn get_arg<'a>( + input_span: proc_macro2::Span, + args: syn::AttributeArgs, + arg: &'a str, + expected: &'a str, + expected_args: usize, +) -> Result<syn::LitStr, TokenStream> { + if args.len() == expected_args { + let meta = args + .iter() + .filter_map(|x| { + if let syn::NestedMeta::Meta(ref meta) = x { + if meta.path().is_ident(&arg) { + return Some(meta); + } + } + None + }) + .next(); + if let Some(meta) = meta { + if let syn::Meta::NameValue(ref meta) = meta { + let meta_lit = meta.lit.clone(); + return match meta_lit { + syn::Lit::Str(s) => Ok(s), + _ => { + let error = syn::Error::new( + meta.lit.span(), + format!( + "expected `{}`\n\nThe field '{}' needs to be a str literal!", + expected, arg + ), + ) + .to_compile_error(); + Err(quote! {#error}.into()) + } + }; + } + } else { + let error = syn::Error::new( + meta.span(), + format!( + "1expected `{}`\n\nThe field '{}' is required!", + expected, arg + ), + ) + .to_compile_error(); + return Err(quote! {#error}.into()); + } + } + let error = syn::Error::new( + input_span, + format!( + "expected `{}` but not enough arguments where provided.", + expected + ), + ) + .to_compile_error(); + Err(quote! {#error}.into()) +} +\ No newline at end of file diff --git a/mrsbfh/Cargo.toml b/mrsbfh/Cargo.toml @@ -6,7 +6,27 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[dependencies.matrix-sdk] +git = "https://github.com/matrix-org/matrix-rust-sdk.git" +#rev = "d4327d4cfcc0aa2dfb3d0c59cc55bc03634b5977" +branch = "fix-signatures" +[dependencies.matrix-sdk-common-macros] +git = "https://github.com/matrix-org/matrix-rust-sdk.git" +#rev = "d4327d4cfcc0aa2dfb3d0c59cc55bc03634b5977" +branch = "fix-signatures" + [dependencies] +url = "2.1.1" + # Command macros mrsbfh-macros = {path = "../mrsbfh-macros"} const-concat = {git = "https://github.com/Vurich/const-concat", rev = "f836e77a4ecadf6a11994340fdcbbdadcdc4906d"} + +tokio = { version = "0.2", features = ["full"] } +tracing = "0.1" + +serde = "1.0" +serde_yaml = "0.8.13" +serde_json = "1" + +pulldown-cmark = "0.8.0" # For generating the help text +\ No newline at end of file diff --git a/mrsbfh/src/lib.rs b/mrsbfh/src/commands.rs diff --git a/mrsbfh/src/config.rs b/mrsbfh/src/config.rs @@ -0,0 +1,12 @@ +use serde::de::DeserializeOwned; +use serde::Serialize; +use std::error::Error; +use std::path::Path; + +pub use mrsbfh_macros::ConfigDerive; + +pub trait Config { + fn load<P: AsRef<Path> + std::fmt::Debug>(path: P) -> Result<Self, Box<dyn Error>> + where + Self: Sized + Serialize + DeserializeOwned; +} diff --git a/mrsbfh/src/lib.rs b/mrsbfh/src/lib.rs @@ -1 +1,14 @@ -pub use mrsbfh_macros::{command, command_generate}; -\ No newline at end of file +pub mod commands; +pub mod config; +pub mod utils; + +pub type Sender = tokio::sync::mpsc::Sender<matrix_sdk::events::AnyMessageEventContent>; + +pub use const_concat; +pub use matrix_sdk; +pub use matrix_sdk_common_macros; +pub use pulldown_cmark; +pub use serde_yaml; +pub use tokio; +pub use tracing; +pub use url; diff --git a/mrsbfh/src/utils.rs b/mrsbfh/src/utils.rs @@ -0,0 +1,43 @@ +use serde::{Deserialize, Serialize}; +use std::error::Error; +use std::path::PathBuf; +use tracing::*; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Session { + /// The homeserver used for this session. + pub homeserver: String, + /// The access token used for this session. + pub access_token: String, + /// The user the access token was issued for. + pub user_id: String, + /// The ID of the client device + pub device_id: String, +} + +impl Session { + pub fn save(&self, session_path: PathBuf) -> Result<(), Box<dyn Error>> { + let mut session_path: PathBuf = session_path; + info!("SessionPath: {:?}", session_path); + std::fs::create_dir_all(&session_path)?; + session_path.push("session.json"); + serde_json::to_writer(&std::fs::File::create(session_path)?, self)?; + Ok(()) + } + + pub fn load(session_path: PathBuf) -> Option<Self> { + let mut session_path: PathBuf = session_path; + session_path.push("session.json"); + let file = std::fs::File::open(session_path); + match file { + Ok(file) => { + let session: Result<Self, serde_json::Error> = serde_json::from_reader(&file); + match session { + Ok(session) => Some(session), + Err(_) => None, + } + } + Err(_) => None, + } + } +}