use anyhow::{anyhow, Result}; use reqwest::{Client, header::HeaderMap}; use std::{path::Path, time::Duration,}; use tokio::{fs::File, time::interval, prelude::*}; use tokio_compat_02::FutureExt; use url::Url; const DEFAULT_PREFIX: &str = "fire"; const DEFAULT_SUFFIX: &str = "paw"; const USER_AGENT: &str = "warriors-cats"; const TOKEN_PATH: &str = "/oauth/token"; const STATUS_URL: &str = "/api/v1/statuses"; #[derive(serde::Deserialize)] struct Config { names: Names, 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, } #[derive(serde::Deserialize)] struct Names { prefix: Vec, suffix: Vec, } struct State { server: Url, client: Client, prefix: Vec, suffix: Vec, } #[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, } 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() .compat() .await?; if !res.status().is_success() { return Err(anyhow!("Failed to fetch access token:\n{}", res.text().await?)); } let token: Token = res.json().compat().await?; Ok(token) } } async fn read_token(path: impl AsRef) -> Result { let mut file = File::open(path).await?; let mut contents = vec![]; file.read_to_end(&mut contents).await?; let token: Token = toml::from_slice(&contents)?; Ok(token) } async fn write_token(path: impl AsRef, token: &Token) -> Result<()> { let token_bytes = toml::to_vec(token)?; let mut file = File::create(path).await?; file.write_all(&token_bytes).await?; Ok(()) } impl Config { async fn open(path: impl AsRef) -> Result { let mut file = File::open(path).await?; let mut contents = vec![]; file.read_to_end(&mut contents).await?; let config: Config = toml::from_slice(&contents)?; Ok(config) } async fn to_state(self, token_path: impl AsRef) -> Result { let Config { names, 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, prefix: names.prefix, suffix: names.suffix, }) } } impl State { async fn post(&self) -> Result<()> { let name = self.generate(); let mut url = self.server.clone(); url.set_path(STATUS_URL); let res = self.client.post(url.as_str()) .form(&Status { status: name, }) .send() .compat() .await?; if !res.status().is_success() { return Err(anyhow!("Failed to post status:\n{}", res.text().await?)); } Ok(()) } fn generate(&self) -> String { use rand::{thread_rng, seq::SliceRandom}; let mut rng = thread_rng(); let prefix = self.prefix.choose(&mut rng).map(|s| s.as_str()).unwrap_or(DEFAULT_PREFIX); let mut suffix; while { suffix = self.suffix.choose(&mut rng).map(|s| s.as_str()).unwrap_or(DEFAULT_SUFFIX); suffix == prefix } {} let (first, rest) = prefix.split_at(1); first.to_uppercase() + rest + &suffix } } #[tokio::main] async fn main() -> Result<()> { let config = Config::open("Config.toml").await?; let duration = Duration::from_secs(60) * config.timing.duration; let mut ticker = interval(duration); let state = config.to_state("Token.toml").await?; loop { ticker.tick().await; state.post().await?; } }