2020-10-22 18:34:28 +00:00
|
|
|
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<String>,
|
|
|
|
suffix: Vec<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
struct State {
|
|
|
|
server: Url,
|
|
|
|
client: Client,
|
|
|
|
prefix: Vec<String>,
|
|
|
|
suffix: Vec<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[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();
|
|
|
|
|
|
|
|
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>) -> Result<State> {
|
|
|
|
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,
|
2020-10-22 18:38:55 +00:00
|
|
|
visibility: "unlisted",
|
2020-10-22 18:34:28 +00:00
|
|
|
})
|
|
|
|
.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?;
|
|
|
|
}
|
|
|
|
}
|