use anyhow::{anyhow, Result}; use rand::{thread_rng, Rng}; use reqwest::{header::HeaderMap, Client}; use std::{path::Path, time::Duration}; use tokio::{fs::File, io::AsyncReadExt, io::AsyncWriteExt, time::interval}; use url::Url; mod description; mod name; const USER_AGENT: &str = "warriors-cats"; const TOKEN_PATH: &str = "/oauth/token"; const STATUS_URL: &str = "/api/v1/statuses"; #[derive(serde::Deserialize)] struct Config { timing: Timing, mastodon: Mastodon, } #[derive(serde::Deserialize)] struct Mastodon { server: Url, client_id: String, client_secret: String, authorization_code: String, } #[derive(serde::Deserialize)] struct Timing { duration: u32, } struct State { server: Url, client: Client, name: name::Config, description: description::Config, } #[derive(serde::Serialize)] struct OauthParams { client_id: String, client_secret: String, code: String, redirect_uri: &'static str, grant_type: &'static str, scope: &'static str, } #[derive(serde::Deserialize, serde::Serialize)] struct Token { access_token: String, } #[derive(serde::Serialize)] struct Status { status: String, visibility: &'static str, } impl Mastodon { async fn authenticate(self) -> Result { let server = self.server; let mut oauth_url = server.clone(); oauth_url.set_path(TOKEN_PATH); let client = Client::new(); let res = client .post(oauth_url.as_str()) .header("User-Agent", USER_AGENT) .form(&OauthParams { client_id: self.client_id, client_secret: self.client_secret, code: self.authorization_code, redirect_uri: "urn:ietf:wg:oauth:2.0:oob", grant_type: "authorization_code", scope: "read write", }) .send() .await?; if !res.status().is_success() { return Err(anyhow!( "Failed to fetch access token:\n{}", res.text().await? )); } let token: Token = res.json().await?; Ok(token) } } async fn read_token(path: impl AsRef) -> Result { let mut file = File::open(path).await?; let mut contents = String::new(); file.read_to_string(&mut contents).await?; let token: Token = toml::from_str(&contents)?; Ok(token) } async fn write_token(path: impl AsRef, token: &Token) -> Result<()> { let token_bytes = toml::to_string(token)?; let mut file = File::create(path).await?; file.write_all(token_bytes.as_bytes()).await?; Ok(()) } impl Config { async fn open(path: impl AsRef) -> Result { let mut file = File::open(path).await?; let mut contents = String::new(); file.read_to_string(&mut contents).await?; let config: Config = toml::from_str(&contents)?; Ok(config) } async fn into_state( self, token_path: impl AsRef, name: name::Config, description: description::Config, ) -> Result { let Config { timing: _, mastodon, } = self; let server = mastodon.server.clone(); let token = match read_token(token_path.as_ref()).await { Ok(token) => token, _ => { let token = mastodon.authenticate().await?; write_token(token_path.as_ref(), &token).await?; token } }; let mut headers = HeaderMap::new(); headers.insert( "Authorization", format!("Bearer {}", token.access_token).parse()?, ); let client = Client::builder() .user_agent(USER_AGENT) .default_headers(headers) .build()?; Ok(State { server, client, name, description, }) } } impl State { async fn post(&self, rng: &mut impl Rng) -> Result<()> { let mut url = self.server.clone(); url.set_path(STATUS_URL); let res = self .client .post(url.as_str()) .form(&Status { status: self.gen(rng), visibility: "unlisted", }) .send() .await?; if !res.status().is_success() { return Err(anyhow!("Failed to post status:\n{}", res.text().await?)); } Ok(()) } fn gen(&self, rng: &mut impl Rng) -> String { let name = self.name.gen(rng); let description = self.description.gen(rng); format!("{} - {}", name, description) } } #[tokio::main] async fn main() -> Result<()> { let config = Config::open("Config.toml").await?; let name = name::config("Name.json").await?; let description = description::config("Description.json").await?; let duration = Duration::from_secs(60) * config.timing.duration; let mut ticker = interval(duration); let state = config.into_state("Token.toml", name, description).await?; let mut rng = thread_rng(); loop { ticker.tick().await; state.post(&mut rng).await?; } }