diff --git a/Cargo.lock b/Cargo.lock index dc32a09..007b60c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -437,6 +437,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -667,6 +677,7 @@ dependencies = [ "js-sys", "log", "mime", + "mime_guess", "native-tls", "once_cell", "percent-encoding", @@ -1009,6 +1020,15 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.13" @@ -1047,6 +1067,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "want" version = "0.3.1" diff --git a/Cargo.toml b/Cargo.toml index fd0ff99..21ce04c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ edition = "2021" dirs = "5.0.1" serde = { version = "1.0.192", features = ["derive"] } serde_json = "1.0" -reqwest = { version = "0.11.22", features = ["json"] } +reqwest = { version = "0.11.22", features = ["json", "multipart"] } rofi = "0.3.0" tokio = { version = "1", features = ["full"] } diff --git a/README.org b/README.org index 3a98281..2041922 100644 --- a/README.org +++ b/README.org @@ -13,16 +13,21 @@ On the first run, if it does not exist, PluralSync will create a configuration d { "pk_key": "// Pluralkit token", "sp_key": "// Simplplural token", - "pfp_module": { + "avatar_module": { "enabled": false, - "pfp_folder": "// Folder to grab profile pictures, they follow they member1member2...memberX.png format", - "pfp_output_path": "// Path for the copied selected pfp" + "avatar_folder": "// Folder to grab profile pictures, they follow they member1member2...memberX.png format", + "avatar_output_path": "// Path for the copied selected avatar" }, "disc_module": { "enabled": false, "token": "// Discord user token", "python_path": "// Path to the python executable", - "script_path": "// Path to updatepfp.py" + "script_path": "// Path to updatediscordavatar.py" + }, + "fedi_module": { + "enabled": false, + "instance" : "// Fedi instance url", + "token": "// Fedi bearer token" } } #+end_src @@ -43,17 +48,22 @@ Just fill it with PluralKit and SimplyPlural tokens associated to your account. - *Memberlist* - Writes the known list of members based on the =system.json= file. ** Modules -*** PFP module +*** Avatar module Will get a profile picture from a folder based on the currently fronting members and will copy it to the desired location. I recommend something it's easy to have at hand like =~/face=. The pictures in the path should be named according to the front. For example, if Bob and Alice are fronting, the module will copy ={PATH}/bobalice.png= to the location, or if it's only Bob it'll be =bob.png=. -Notice this will cover all combinations, if the main front is Alice the combination =alicebob.png= instead. I recommend the use of a python script to generate the possible combinations. +Notice this will cover all combinations, if the main front is Alice the combination =alicebob.png= instead. I personally use a python script to generate my set. *** Discord module **Use of this module goes against discord terms of service. USE AT YOUR OWN RISK.** -Requires the =PFP module= to be enabled and the latest version of [[https://github.com/dolfies/discord.py-self/][discord.py-self]]. -Will change your discord avatar with the one selected by the PFP module. +Requires the =avatar module= to be enabled and the latest version of [[https://github.com/dolfies/discord.py-self/][discord.py-self]]. +Will change your discord avatar with the one selected by the avatar module. + +*** Fedi module +Should be compatible with Mastodon, Pleroma and Akkoma. + +Requires the =avatar module= to be enabled. Will set the avatar of your fediverse account to the one selected by the avatar module. * Installation Just build with cargo and move or symlink the executable wherever is more comofortable for you. @@ -65,5 +75,6 @@ cargo build -r * Future Given the nature of the project as a learning exercise, there are currently a set of features and improvements I'd like to add eventually. - [X] Proper error handling (What I have currently is simply a mess) -- [ ] Rofi and zenity support -- [ ] Proper way to handle front conflicts between PluralKit and SimplyPlural +- [ ] Allow the user to set which front is used in cased of mismatch between PK and SP +- [ ] Implement clap for command inputs +- [ ] Actually separate the modules as actual modules diff --git a/config.example.json b/config.example.json index 5c5949f..aceeac3 100644 --- a/config.example.json +++ b/config.example.json @@ -1,15 +1,20 @@ { "pk_key": "// Pluralkit token", "sp_key": "// Simplplural token", - "pfp_module": { + "avatar_module": { "enabled": false, - "pfp_folder": "// Folder to grab profile pictures, they follow they member1member2.png format", - "pfp_output_path": "// Path for the copied selected pfp" + "avatar_folder": "// Folder to grab profile pictures, they follow they member1member2...memberX.png format", + "avatar_output_path": "// Path for the copied selected avatar" }, "disc_module": { "enabled": false, "token": "// Discord user token", "python_path": "// Path to the python executable", - "script_path": "// Path to updatepfp.py" + "script_path": "// Path to updatediscordavatar.py" + }, + "fedi_module": { + "enabled": false, + "instance" : "// Fedi instance url", + "token": "// Fedi bearer token" } } diff --git a/src/main.rs b/src/main.rs index 910eeb1..ce629f0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ use std::collections::HashMap; use std::process::Command; use reqwest::header::{USER_AGENT, AUTHORIZATION}; +use reqwest::multipart::{Part, Form}; use serde::{Serialize, Deserialize}; use serde_json::Value; @@ -15,15 +16,16 @@ use dirs; struct Config { pk_key: String, sp_key: String, - pfp_module: PfpModule, - disc_module: DiscModule + avatar_module: AvatarModule, + disc_module: DiscModule, + fedi_module: FediModule, } #[derive(Debug, Serialize, Deserialize)] -struct PfpModule { +struct AvatarModule { enabled: bool, - pfp_folder: String, - pfp_output_path: String, + avatar_folder: String, + avatar_output_path: String, } #[derive(Debug, Serialize, Deserialize)] @@ -34,6 +36,13 @@ struct DiscModule { script_path: String, } +#[derive(Debug, Serialize, Deserialize)] +struct FediModule { + enabled: bool, + instance: String, + token: String, +} + #[derive(Debug, Serialize, Deserialize, Clone)] struct System { pk_userid: String, @@ -65,19 +74,24 @@ const PK_URL: &str = "https://api.pluralkit.me/v2"; const SP_URL: &str = "https://api.apparyllis.com/v1"; const EXAMPLE_JSON: &str = r#"{ - "pk_key": "// Pluralkit token", - "sp_key": "// Simplplural token", - "pfp_module": { - "enabled": false, - "pfp_folder": "// Folder to grab profile pictures, they follow they member1member2.png format", - "pfp_output_path": "// Path for the copied selected pfp" - }, - "disc_module": { - "enabled": false, - "token": "// Discord user token", - "python_path": "// Path to the python executable", - "script_path": "// Path to updatepfp.py" - } + "pk_key": "// Pluralkit token", + "sp_key": "// Simplplural token", + "avatar_module": { + "enabled": false, + "avatar_folder": "// Folder to grab profile pictures, they follow they member1member2...memberX.png format", + "avatar_output_path": "// Path for the copied selected avatar" + }, + "disc_module": { + "enabled": false, + "token": "// Discord user token", + "python_path": "// Path to the python executable", + "script_path": "// Path to updatediscordavatar.py" + }, + "fedi_module": { + "enabled": false, + "instance" : "// Fedi instance url", + "token": "// Fedi bearer token" + } }"#; const HELP_STRING: &str = r#"PluralSync @@ -293,16 +307,16 @@ fn get(config_path: String) -> Result<(), &'static str> { println!("Currently fronting: {}", fronters); let _ = fs::write(format!("{}/.front", config_path), fronters); - if config.pfp_module.enabled { - let pfpnames = names.join("").to_lowercase() + ".png"; + if config.avatar_module.enabled { + let avatarnames = names.join("").to_lowercase() + ".png"; - if Path::new(&config.pfp_module.pfp_output_path).exists() { - let _ = remove_file(&config.pfp_module.pfp_output_path); + if Path::new(&config.avatar_module.avatar_output_path).exists() { + let _ = remove_file(&config.avatar_module.avatar_output_path); } if cfg!(target_os = "windows") { - let cmdaug = format!("copy {}\\{} {}", &config.pfp_module.pfp_folder, pfpnames, &config.pfp_module.pfp_output_path); - Command::new("cmd").args(["/C", &cmdaug]).output().expect("PFP module error"); + let cmdaug = format!("copy {}\\{} {}", &config.avatar_module.avatar_folder, avatarnames, &config.avatar_module.avatar_output_path); + Command::new("cmd").args(["/C", &cmdaug]).output().expect("Avatar module error"); if config.disc_module.enabled { let mut c = Command::new("cmd").args(["/C", format!("{} {}", &config.disc_module.python_path, &config.disc_module.script_path).as_str()]).spawn().expect("Discord module error"); @@ -310,12 +324,16 @@ fn get(config_path: String) -> Result<(), &'static str> { } } else { - Command::new("sh").arg("-c").arg(format!("cp {}/{} {}", &config.pfp_module.pfp_folder, pfpnames, &config.pfp_module.pfp_output_path)).output().expect("PFP module error"); + Command::new("sh").arg("-c").arg(format!("cp {}/{} {}", &config.avatar_module.avatar_folder, avatarnames, &config.avatar_module.avatar_output_path)).output().expect("Avatar module error"); if config.disc_module.enabled { let mut c = Command::new("sh").arg("-c").arg(format!("{} {}", &config.disc_module.python_path, &config.disc_module.script_path)).spawn().expect("Discord module error"); let _ = c.wait().expect("Error"); } + + if config.fedi_module.enabled { + let _ = http_patch_request_fedi(config.fedi_module.instance + "/api/v1/accounts/update_credentials", format!("Bearer {}", &config.fedi_module.token).as_str(), config.avatar_module.avatar_output_path); + } } } @@ -620,3 +638,17 @@ async fn http_patch_request(url: String, key: &str, body: String ) -> Result<(), .await?; Ok(()) } + +#[tokio::main] +async fn http_patch_request_fedi(url: String, key: &str, file_path: String ) -> Result<(), Box> { + let client = reqwest::Client::new(); + let form = Form::new().part("avatar", Part::bytes(fs::read(file_path).unwrap()).file_name("face.png").mime_str("image/png").unwrap()); + let c = client + .patch(url) + .multipart(form) + .header(USER_AGENT, "Pluralsync") + .header(AUTHORIZATION, key); + let _res = c.send() + .await?; + Ok(()) +} diff --git a/updatepfp.py b/updatediscordavatar.py similarity index 73% rename from updatepfp.py rename to updatediscordavatar.py index ad68504..9f55985 100644 --- a/updatepfp.py +++ b/updatediscordavatar.py @@ -13,10 +13,10 @@ json = json.load(f) class MyClient(discord.Client): async def on_ready(self): print('Logged on as', self.user) - pfp_path = json["pfp_module"]["pfp_output_path"] - fp = open(pfp_path, 'rb') - pfp = fp.read() - await self.user.edit(avatar=pfp) + avatar_path = json["avatar_module"]["avatar_output_path"] + fp = open(avatar_path, 'rb') + avatar = fp.read() + await self.user.edit(avatar=avatar) print('Discord module finished') await self.close()