213 lines
5.2 KiB
Rust
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?;
|
|
}
|
|
}
|