warriors-names/src/main.rs

213 lines
5.2 KiB
Rust

use anyhow::{anyhow, Result};
use rand::{thread_rng, Rng};
use reqwest::{header::HeaderMap, Client};
use std::{path::Path, time::Duration};
use tokio::{fs::File, prelude::*, time::interval};
use tokio_compat_02::FutureExt;
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<Token> {
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<Path>) -> Result<Token> {
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<Path>, 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<Path>) -> Result<Self> {
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<Path>,
name: name::Config,
description: description::Config,
) -> Result<State> {
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()
.compat()
.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.to_state("Token.toml", name, description).await?;
let mut rng = thread_rng();
loop {
ticker.tick().await;
state.post(&mut rng).await?;
}
}