warriors-names/src/main.rs

213 lines
5.2 KiB
Rust
Raw Normal View History

2020-10-22 18:34:28 +00:00
use anyhow::{anyhow, Result};
use rand::{thread_rng, Rng};
2020-10-23 20:56:20 +00:00
use reqwest::{header::HeaderMap, Client};
use std::{path::Path, time::Duration};
use tokio::{fs::File, prelude::*, time::interval};
2020-10-22 18:34:28 +00:00
use tokio_compat_02::FutureExt;
use url::Url;
2020-10-23 20:56:20 +00:00
mod description;
mod name;
2020-10-23 20:56:20 +00:00
2020-10-22 18:34:28 +00:00
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,
2020-10-22 18:34:28 +00:00
}
#[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,
2020-10-22 18:38:55 +00:00
visibility: &'static str,
2020-10-22 18:34:28 +00:00
}
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();
2020-10-23 20:56:20 +00:00
let res = client
.post(oauth_url.as_str())
2020-10-22 18:34:28 +00:00
.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() {
2020-10-23 20:56:20 +00:00
return Err(anyhow!(
"Failed to fetch access token:\n{}",
res.text().await?
));
2020-10-22 18:34:28 +00:00
}
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> {
2020-10-22 18:34:28 +00:00
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
2020-10-23 20:56:20 +00:00
}
2020-10-22 18:34:28 +00:00
};
let mut headers = HeaderMap::new();
2020-10-23 20:56:20 +00:00
headers.insert(
"Authorization",
format!("Bearer {}", token.access_token).parse()?,
);
2020-10-22 18:34:28 +00:00
let client = Client::builder()
.user_agent(USER_AGENT)
.default_headers(headers)
.build()?;
Ok(State {
server,
client,
name,
description,
2020-10-22 18:34:28 +00:00
})
}
}
impl State {
2020-10-23 20:56:20 +00:00
async fn post(&self, rng: &mut impl Rng) -> Result<()> {
2020-10-22 18:34:28 +00:00
let mut url = self.server.clone();
url.set_path(STATUS_URL);
2020-10-23 20:56:20 +00:00
let res = self
.client
.post(url.as_str())
2020-10-22 18:34:28 +00:00
.form(&Status {
status: self.gen(rng),
2020-10-22 18:38:55 +00:00
visibility: "unlisted",
2020-10-22 18:34:28 +00:00
})
.send()
2020-10-23 20:56:20 +00:00
.compat()
.await?;
2020-10-22 18:34:28 +00:00
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)
2020-10-22 18:34:28 +00:00
}
}
#[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?;
2020-10-22 18:34:28 +00:00
let duration = Duration::from_secs(60) * config.timing.duration;
let mut ticker = interval(duration);
let state = config.to_state("Token.toml", name, description).await?;
2020-10-22 18:34:28 +00:00
2020-10-23 20:56:20 +00:00
let mut rng = thread_rng();
2020-10-22 18:34:28 +00:00
loop {
ticker.tick().await;
2020-10-23 20:56:20 +00:00
state.post(&mut rng).await?;
2020-10-22 18:34:28 +00:00
}
}