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:
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,
+ }
+ }
+}