use std::{collections::HashSet, error::Error, fmt};
use aok::Result;
use futures::{stream::FuturesUnordered, StreamExt};
use reqwest::{Method, StatusCode};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct AccessTokenResponse {
pub access_token: String,
pub token_type: String,
pub scope: String,
}
#[derive(Debug)]
pub struct GitHubError {
pub code: StatusCode,
pub msg: String,
}
impl fmt::Display for GitHubError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "GitHub {} : {}", self.code, self.msg,)
}
}
impl Error for GitHubError {}
async fn request(token: &str, method: Method, url: &str) -> Result<String> {
let url = format!("https://api.github.com/{url}");
loop {
let res = ireq::REQ
.request(method.clone(), &url)
.header("Authorization", format!("token {}", token))
.header("User-Agent", "i")
.send()
.await?;
let status = res.status();
if status.is_success() {
return Ok(res.text().await?);
} else if status == StatusCode::TOO_MANY_REQUESTS {
tracing::warn!("too many requests {url} : {}", res.text().await?);
tokio::time::sleep(tokio::time::Duration::from_secs(60)).await;
continue;
} else {
return Err(
GitHubError {
code: res.status(),
msg: res.text().await?,
}
.into(),
);
}
}
}
async fn json<T: serde::de::DeserializeOwned>(token: &str, method: Method, url: &str) -> Result<T> {
let text = request(token, method, url).await?;
Ok(sonic_rs::from_str(&text)?)
}
#[derive(Debug, Clone)]
pub struct User {
pub token: String,
}
#[derive(Debug, Deserialize)]
pub struct UserInfo {
pub login: String,
pub id: u64,
pub followers: u64,
pub following: u64,
pub created_at: String,
pub updated_at: String,
pub twitter_username: Option<String>,
pub name: Option<String>,
pub company: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct UserEmail {
pub email: String,
pub primary: bool,
}
#[derive(Debug, Deserialize)]
pub struct _UserEmail {
pub email: String,
pub primary: bool,
pub verified: bool,
pub visibility: Option<String>,
}
impl User {
pub fn new(token: impl Into<String>) -> User {
User {
token: token.into(),
}
}
pub async fn star_user_repo<S1: AsRef<str>, S2: AsRef<str>>(
&self,
user_li: impl IntoIterator<Item = S1>,
repo_li: impl IntoIterator<Item = S2>,
) -> Result<()> {
macro_rules! run {
($li:ident, $func:ident) => {{
async move {
let mut ing = FuturesUnordered::new();
for i in $li {
let i = i.as_ref().to_owned();
tracing::info!("{i}");
ing.push(self.$func(i));
}
while let Some(r) = ing.next().await {
if let Err(ref err) = r {
tracing::error!("{} {}", stringify!($func), err);
}
}
}
}};
}
tokio::join!(run!(user_li, follow), run!(repo_li, star_repo));
Ok(())
}
pub async fn request(&self, method: Method, url: impl AsRef<str>) -> Result<String> {
request(&self.token, method, url.as_ref()).await
}
pub async fn json<T: serde::de::DeserializeOwned>(&self, url: impl AsRef<str>) -> Result<T> {
json(&self.token, Method::GET, url.as_ref()).await
}
pub async fn info(&self) -> Result<UserInfo> {
self.json("user").await
}
pub async fn _emails(&self) -> Result<Vec<_UserEmail>> {
self.json("user/emails").await
}
pub async fn emails(&self) -> Result<Vec<UserEmail>> {
Ok(
self
._emails()
.await?
.into_iter()
.filter_map(|e| {
if e.email.ends_with("@users.noreply.github.com") {
return None;
}
if e.verified {
Some(UserEmail {
email: e.email,
primary: e.primary,
})
} else {
None
}
})
.collect(),
)
}
pub async fn star_repo(&self, owner_repo: impl AsRef<str>) -> Result<()> {
let url = format!("user/starred/{}", owner_repo.as_ref());
self.request(Method::PUT, url).await?;
Ok(())
}
pub async fn follow(&self, username: impl AsRef<str>) -> Result<()> {
let url = format!("user/following/{}", username.as_ref());
self.request(Method::PUT, &url).await?;
Ok(())
}
}
pub fn verify_access_token(access_token: AccessTokenResponse, scope_li: &[&str]) -> Option<String> {
if access_token.token_type == "bearer" {
let set: HashSet<&str> = HashSet::from_iter(access_token.scope.split(','));
for i in scope_li {
if !set.contains(i) {
return None;
}
}
return Some(access_token.access_token);
}
None
}
pub async fn access_token(
client_id: impl AsRef<str>,
client_secret: impl AsRef<str>,
code: impl AsRef<str>,
) -> Result<AccessTokenResponse> {
let params = [
("client_id", client_id.as_ref()),
("client_secret", client_secret.as_ref()),
("code", code.as_ref()),
];
let res = ireq::REQ
.post("https://github.com/login/oauth/access_token")
.header("Accept", "application/json")
.form(¶ms)
.send()
.await?
.text()
.await?;
let res = sonic_rs::from_str::<AccessTokenResponse>(&res)?;
Ok(res)
}