use mastodon_async::entities::visibility::Visibility; use mastodon_async::helpers::toml as masto_toml; use mastodon_async::prelude::*; use mastodon_async::{helpers::cli, Result}; use reqwest; use serde::Deserialize; use std::collections::HashSet; use std::io::Cursor; use std::io::Write; use std::process::Command; use toml; const CONFIG_FILENAME: &str = "config.toml"; #[derive(Deserialize)] struct Config { bot: Bot, files: Files, errors: Errors, } #[derive(Deserialize)] struct Bot { name: String, instance: String, bio: String, } #[derive(Deserialize)] struct Files { posted: String, urls: String, tempfile: String, } #[derive(Deserialize)] struct Errors { out_of_images: String, } /// Parses the given filename to a config struct fn parse_config(filename: &str) -> Config { let toml_file = std::fs::read_to_string(filename) .expect("No config file, consider getting the original one and modifing it"); toml::from_str(&toml_file).expect("Malformed config file, check the original one for reference") } #[tokio::main] // requires `features = ["mt"] async fn main() -> Result<()> { let config: Config = parse_config(CONFIG_FILENAME); let mastodon = if let Ok(data) = masto_toml::from_file("mastodon-data.toml") { Mastodon::from(data) } else { register(&config).await? }; match get_next_url(&config) { Some(url) => { post_image(&mastodon, &url, &config).await; update_bio(&mastodon, &config).await; } None => post(&mastodon, &config.errors.out_of_images).await, }; Ok(()) } fn get_next_url(config: &Config) -> Option { let binding = std::fs::read_to_string(&config.files.posted).expect("Posted file not found"); let posted = binding.lines().collect::>(); let binding = std::fs::read_to_string(&config.files.urls).expect("Urls file not found"); let urls = binding.lines().collect::>(); let urls = urls.difference(&posted).collect::>(); if urls.is_empty() { None } else { let mut file = std::fs::OpenOptions::new() .write(true) .append(true) // This is needed to append to file .open(&config.files.posted) .expect("Cannot open posted file"); // Maybe we should retry just in case write!(file, "{}\n", urls[0]).expect("Cannot write to posted file"); // maybe we should retry tbh Some(urls[0].to_string().clone()) } } async fn post_image(account: &Mastodon, url: &String, config: &Config) { fetch_url(url.to_string(), &config.files.tempfile) .await .expect("Error fetching url"); let attachment = account .media(&config.files.tempfile, Some(url.to_string())) .await .expect("Attachment upload error"); let attachment = account .wait_for_processing(attachment, Default::default()) .await .expect("Attachment processing error"); let status = StatusBuilder::new() .media_ids(&[attachment.id]) .visibility(Visibility::Unlisted) .sensitive(true) .build() .expect("Could not build status"); // we should retry account.new_status(status).await.expect("Error generating status"); // we should retry or delete last url in posted println!("Status posted") } async fn update_bio(account: &Mastodon, config: &Config) { let binding = std::fs::read_to_string(&config.files.posted).expect("Posted file not found"); let posted = binding.lines().collect::>(); let binding = std::fs::read_to_string(&config.files.urls).expect("Url file not found"); let urls = binding.lines().collect::>(); let remaining = urls.difference(&posted).count(); Command::new("curl") .args([ "-X", "PATCH", &format!("{}/api/v1/accounts/update_credentials", config.bot.instance), "-H", &format!("Authorization:Bearer {}", account.data.token), "-d", &format!( "note={}\n\n{} new images remaining", config.bot.bio, remaining ), ]) .spawn() .expect("Could not spawn curl") .wait() .expect("Curl failed"); } async fn post(account: &Mastodon, msg: &str) { let status = StatusBuilder::new() .visibility(Visibility::Direct) .status(msg) .build() .expect("Error building error status"); account.new_status(status).await.expect("Error posting error status"); println!("Error status posted") } async fn fetch_url(url: String, file_name: &String) -> Result<()> { let response = reqwest::get(url).await?; let mut file = std::fs::File::create(file_name)?; let mut content = Cursor::new(response.bytes().await?); std::io::copy(&mut content, &mut file)?; Ok(()) } async fn register(config: &Config) -> Result { let registration = Registration::new(&config.bot.instance) .client_name(&config.bot.name) .scopes(Scopes::write_all()) .build() .await?; let mastodon = cli::authenticate(registration).await?; // Save app data for using on the next run. masto_toml::to_file(&mastodon.data, "mastodon-data.toml")?; Ok(mastodon) }