lib/patreon: refresh token support

This should hopefully make the patrons page work consistently and no
longer require me to manually update the patreon token once per month.
Why didn't I do this age ago??????

Hacked up live on twitch: https://twitch.tv/princessxen

Closes #442

Signed-off-by: Xe <me@christine.website>
This commit is contained in:
Cadey Ratio 2022-04-02 16:15:10 +00:00
parent 0c0c5875e6
commit 1c8c3396a7
6 changed files with 93 additions and 14 deletions

1
.gitignore vendored
View File

@ -6,3 +6,4 @@ cw.tar
/result /result
.#* .#*
/target /target
.patreon.json

1
Cargo.lock generated
View File

@ -1514,6 +1514,7 @@ dependencies = [
"tokio", "tokio",
"tracing", "tracing",
"tracing-futures", "tracing-futures",
"url",
] ]
[[package]] [[package]]

View File

@ -14,6 +14,7 @@ serde = { version = "1", features = ["derive"] }
thiserror = "1" thiserror = "1"
tracing = "0.1" tracing = "0.1"
tracing-futures = "0.2" tracing-futures = "0.2"
url = "2"
[dev-dependencies] [dev-dependencies]
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }

View File

@ -1,7 +1,10 @@
use std::{fs, io, path::Path};
use chrono::prelude::*; use chrono::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
use tracing::{debug, error, instrument}; use tracing::{debug, error, instrument};
use url::Url;
pub type Campaigns = Vec<Object<Campaign>>; pub type Campaigns = Vec<Object<Campaign>>;
pub type Pledges = Vec<Object<Pledge>>; pub type Pledges = Vec<Object<Pledge>>;
@ -61,14 +64,30 @@ pub struct User {
pub url: String, pub url: String,
} }
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct RefreshGrant {
pub access_token: String,
pub refresh_token: String,
pub expires_in: serde_json::Value,
pub scope: serde_json::Value,
pub token_type: String,
}
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum Error { pub enum Error {
#[error("json error: {0:?}")] #[error("json error: {0}")]
Json(#[from] serde_json::Error), Json(#[from] serde_json::Error),
#[error("request error: {0:?}")]
#[error("request error: {0}")]
Request(#[from] reqwest::Error), Request(#[from] reqwest::Error),
#[error("{0}")]
IO(#[from] io::Error),
#[error("url parse error: {0}")]
URLParse(#[from] url::ParseError),
} }
#[derive(Debug, Serialize, Deserialize, Clone, Default)] #[derive(Debug, Serialize, Deserialize, Clone, Default)]
@ -105,12 +124,20 @@ pub struct Links {
} }
impl Client { impl Client {
pub fn new(creds: Credentials) -> Self { pub fn new(creds: Credentials) -> Result<Self> {
Self { let mut creds = creds.clone();
let p = Path::new(".patreon.json");
if p.exists() {
let config = fs::read_to_string(p)?;
creds = serde_json::from_str(&config)?;
}
Ok(Self {
cli: reqwest::Client::new(), cli: reqwest::Client::new(),
base_url: "https://api.patreon.com".into(), base_url: "https://api.patreon.com".into(),
creds: creds, creds: creds,
} })
} }
#[instrument(skip(self))] #[instrument(skip(self))]
@ -157,4 +184,57 @@ impl Client {
let data: Data<Vec<Object<Pledge>>, Object<User>> = serde_json::from_str(&data)?; let data: Data<Vec<Object<Pledge>>, Object<User>> = serde_json::from_str(&data)?;
Ok(data.included.unwrap()) Ok(data.included.unwrap())
} }
/*
POST www.patreon.com/api/oauth2/token
?grant_type=refresh_token
&refresh_token=<the users refresh_token>
&client_id=<your client id>
&client_secret=<your client secret>
1. grab new creds
2. serialize new creds to disk
3. reload current creds in ram
4. ???
5. profit!
*/
#[instrument(skip(self))]
pub async fn refresh_token(&mut self) -> Result<()> {
let mut u = Url::parse(&self.base_url)?;
u.set_path("/api/oauth2/token");
u.query_pairs_mut()
.append_pair("grant_type", "refresh_token")
.append_pair("refresh_token", &self.creds.refresh_token)
.append_pair("client_id", &self.creds.client_id)
.append_pair("client_secret", &self.creds.client_secret);
let rg: RefreshGrant = self
.cli
.post(&u.to_string())
.header(
"Authorization",
format!("Bearer {}", self.creds.access_token),
)
.send()
.await?
.error_for_status()?
.json()
.await?;
let mut creds = self.creds.clone();
creds.access_token = rg.access_token;
creds.refresh_token = rg.refresh_token;
let p = Path::new(".patreon.json");
if p.exists() {
fs::remove_file(p)?;
}
let mut fout = fs::File::create(p)?;
serde_json::to_writer(&mut fout, &creds)?;
self.creds = creds;
Ok(())
}
} }

View File

@ -9,14 +9,9 @@ pub mod poke;
#[derive(Clone, Deserialize)] #[derive(Clone, Deserialize)]
pub struct Config { pub struct Config {
#[serde(rename = "clackSet")]
pub(crate) clack_set: Vec<String>,
pub(crate) signalboost: Vec<Person>, pub(crate) signalboost: Vec<Person>,
pub(crate) port: u16,
#[serde(rename = "resumeFname")] #[serde(rename = "resumeFname")]
pub(crate) resume_fname: PathBuf, pub(crate) resume_fname: PathBuf,
#[serde(rename = "webMentionEndpoint")]
pub(crate) webmention_url: String,
#[serde(rename = "miToken")] #[serde(rename = "miToken")]
pub(crate) mi_token: String, pub(crate) mi_token: String,
} }
@ -27,7 +22,9 @@ async fn patrons() -> Result<Option<patreon::Users>> {
let creds: Credentials = envy::prefixed("PATREON_") let creds: Credentials = envy::prefixed("PATREON_")
.from_env() .from_env()
.unwrap_or(Credentials::default()); .unwrap_or(Credentials::default());
let cli = Client::new(creds); let mut cli = Client::new(creds)?;
cli.refresh_token().await?;
match cli.campaign().await { match cli.campaign().await {
Ok(camp) => { Ok(camp) => {

View File

@ -36,7 +36,7 @@ impl Into<jsonfeed::Item> for Post {
.date_published(self.date.to_rfc3339()) .date_published(self.date.to_rfc3339())
.author( .author(
jsonfeed::Author::new() jsonfeed::Author::new()
.name("Christine Dodrill") .name("Xe Iaso")
.url("https://christine.website") .url("https://christine.website")
.avatar("https://christine.website/static/img/avatar.png"), .avatar("https://christine.website/static/img/avatar.png"),
); );
@ -83,8 +83,7 @@ impl Post {
async fn read_post(dir: &str, fname: PathBuf, cli: &Option<mi::Client>) -> Result<Post> { async fn read_post(dir: &str, fname: PathBuf, cli: &Option<mi::Client>) -> Result<Post> {
debug!( debug!(
"loading {}/{}", "loading {}",
dir,
fname.clone().into_os_string().into_string().unwrap() fname.clone().into_os_string().into_string().unwrap()
); );