diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6985cf1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9a54104 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "neo" +version = "0.1.0" +authors = ["Woljix"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +matrix-sdk = "0.2.0" +url = "2.2.1" +tokio = { version = "0.2.25", features = ["full"] } +mime = "0.3.16" +serde = { version = "1.0.126", features = ["derive"] } +serde_json = "1.0.64" +indexmap = { version = "1.6.2", features = ["serde-1"] } +rand = "0.8.3" + +#[profile.release] +#opt-level = 'z' +#lto = 'thin' diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..c1b6f69 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,38 @@ +use indexmap::IndexMap; +use serde::{Serialize, Deserialize}; + +#[derive(Serialize, Deserialize)] +#[serde(default)] +pub struct ConfigLogin { + pub username: String, + pub password: String, + pub device_id: String, +} + +impl Default for ConfigLogin { + fn default() -> ConfigLogin { + ConfigLogin { + username: "".to_string(), + password: "".to_string(), + device_id: "".to_string(), + } + } +} + +#[derive(Serialize, Deserialize)] +#[serde(default)] +pub struct ConfigImages { + pub last_image: String, + pub files: Vec, + pub cache: IndexMap +} + +impl Default for ConfigImages { + fn default() -> ConfigImages { + ConfigImages { + last_image: "".to_string(), + files: Vec::new(), + cache: IndexMap::new(), + } + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..dc57805 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,147 @@ +use matrix_sdk::{self, Client}; +use rand::prelude::SliceRandom; +use std::{ + fs::File, + io::{Error, ErrorKind}, + path::PathBuf, + process, +}; +use url::Url; + +mod config; +use config::{ConfigImages, ConfigLogin}; +mod settings; +use settings::Settings; + +const HOME_SERVER: &str = "https://e1m1.xyz"; + +#[tokio::main] +async fn main() { + if let Err(e) = run().await { + println!("Application error: {}", e); + process::exit(1); + } +} + +async fn run() -> Result<(), Box> { + let dir_exe: PathBuf = { + let exe = std::env::current_exe().unwrap(); + let parent = exe.parent().unwrap(); + parent.to_path_buf() + }; + + let dir_login = &dir_exe.join("login.json"); + let dir_images = &dir_exe.join("images.json"); + + let settings_login: Settings = + Settings::::load(&dir_login).unwrap(); + + let mut settings_images: Settings = + Settings::::load(&dir_images).unwrap(); + + let conf_login = &settings_login.loaded; + let conf_images = &mut settings_images.loaded; + + let mut flag_debug: bool = false; + let mut flag_generate: bool = false; + + for x in std::env::args() { + match x.as_str() { + "-debug" => { + flag_debug = true; + break; + } + "generate" => { + flag_generate = true; + break; + } + _ => {} + } + } + + if flag_generate { + println!("Generating config files.."); + Settings::::new(ConfigLogin::default()) + .unwrap() + .save(&dir_login.with_extension("json.template").clone()) + .unwrap(); + Settings::::new(ConfigImages::default()) + .unwrap() + .save(&dir_images.with_extension("json.template").clone()) + .unwrap(); + + return Ok(()); + } + + if let Ok(client) = login( + conf_login.username.as_str(), + conf_login.password.as_str(), + conf_login.device_id.as_str(), + ) + .await + { + let selection = { + if flag_debug { + // Note: This assumes that "last_image" is not empty and has a valid image path. + conf_images.last_image.to_string() + } else { + let mut rng = rand::thread_rng(); + conf_images.files.choose(&mut rng).unwrap().to_string() + } + }; + + println!("Selection was: '{}'", selection); + + if conf_images.cache.contains_key(&selection) { + if let Some(url) = conf_images.cache.get(&selection) { + println!("Setting Avatar URL: '{}'", url); + client.set_avatar_url(Some(url)).await.unwrap(); + } + } else { + let path = dir_exe.join(&selection); + if path.exists() { + conf_images.last_image = selection.clone(); + + let mut image = File::open(&path).unwrap(); + match client.upload_avatar(&mime::IMAGE_JPEG, &mut image).await { + Ok(_) => { + println!("Successfully uploaded avatar!"); + if let Some(avatar_url) = client.avatar_url().await.unwrap() { + conf_images.cache.insert(selection.clone(), avatar_url); + } + } + Err(err) => { + return Err(Box::new(Error::new( + ErrorKind::Other, + format!("Error occurred while uploading avatar: '{}'", err), + ))); + } + } + } else { + println!("Error: file does not exist!"); + } + } + + settings_images.save(&dir_images).unwrap(); + } else { + return Err(Box::new(Error::new(ErrorKind::Other, "Login failed!"))); + } + + Ok(()) +} + +async fn login( + username: &str, + password: &str, + device_id: &str, +) -> Result { + let client = Client::new(Url::parse(HOME_SERVER).unwrap()).unwrap(); + + match client + .login(username, password, Some(device_id), Some("NEO bot")) + .await + { + Ok(_) => return Ok(client), + Err(e) => return Err(e), + } +} diff --git a/src/settings.rs b/src/settings.rs new file mode 100644 index 0000000..2c0a87d --- /dev/null +++ b/src/settings.rs @@ -0,0 +1,45 @@ +use std::{fs::OpenOptions}; +use std::path::Path; +use std::io::{self, Write, prelude::*}; +use serde::Serialize; +use serde::de::DeserializeOwned; +use serde_json; + +pub struct Settings { + pub loaded: T +} + +impl Settings { + pub fn new(import: T) -> io::Result { + Ok(Self { + loaded: import + }) + } + + pub fn load(file: &Path) -> io::Result { + let mut f = match OpenOptions::new().read(true).write(false).open(&file) { + Ok(file) => file, + Err(error) => panic!("Error opening this file: {:?}", error) + }; + + let mut contents = String::new(); + f.read_to_string(&mut contents).unwrap(); + + Ok(Self { + loaded: serde_json::from_str(contents.as_str()).unwrap() + }) + } + + pub fn save(&self, file: &Path) -> io::Result<()> { + let json = serde_json::to_string_pretty(&self.loaded).unwrap(); + + let mut f = match OpenOptions::new().read(true).write(true).create(true).open(&file) { + Ok(file) => file, + Err(error) => panic!("Error saving this file: {:?}", error) + }; + + f.write_all(json.as_bytes()).unwrap(); + + Ok(()) + } +}