use std::sync::mpsc;
use std::sync::mpsc::{Receiver, SendError, Sender, TryRecvError};
use std::time::{SystemTime, UNIX_EPOCH};
use curl::easy::Easy;
use crate::cache;
use crate::commit::Oid;
use std::thread;
use tinyjson::JsonValue;
use url::Url;
use super::ActorThread;
pub struct GitHubRequest {
pub oid: Oid,
pub url: Url,
pub pr_id: String,
}
pub struct GitHubResponse {
pub oid: Oid,
pub subject: String,
}
pub struct GitHubThread(ActorThread<GitHubRequest, GitHubResponse>);
impl GitHubThread {
#[allow(clippy::too_many_lines)]
pub(crate) fn new() -> Self {
let (tx_1, receiver): (Sender<GitHubResponse>, Receiver<GitHubResponse>) = mpsc::channel();
let (sender, rx_2): (Sender<GitHubRequest>, Receiver<GitHubRequest>) = mpsc::channel();
let thread = thread::spawn(move || {
let mut rate_limit_remaining = 60;
let mut rate_limit_reset = u64::MAX;
while let Ok(v) = rx_2.recv() {
if !Self::can_handle(&v.url) {
log::debug!("Can not handle url {}", &v.url);
continue;
}
let pr_id = v.pr_id;
if rate_limit_remaining == 0 {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
if now < rate_limit_reset {
log::info!(
"Skipping lookup #{} Rate limited for {:?} seconds",
pr_id,
rate_limit_reset.checked_sub(now)
);
continue;
}
}
let domain = v.url.domain().expect("Url with a domain name");
let mut segments = v.url.path_segments().unwrap();
let owner = segments.next().unwrap();
let repo = segments.next().unwrap();
let oid = v.oid;
log::debug!(
"Looking up PR #{} for {}/{}/{}",
pr_id,
owner,
repo,
&oid.0[0..7]
);
let url = format!(
"https://api.github.com/repos/{}/{}/pulls/{}",
owner, repo, pr_id
);
let mut easy = Easy::new();
easy.url(&url).unwrap();
if let Some((response_code, headers, body)) = crate::utils::transfer(easy, domain) {
{
if let Some(value) = headers.get("X-RateLimit-Remaining") {
if let Ok(number) = value.parse::<u32>() {
log::trace!("RateLimit-Remaining: {}", number);
rate_limit_remaining = number;
}
}
if let Some(value) = headers.get("X-RateLimit-Reset") {
if let Ok(since_epoch) = value.parse::<u64>() {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
log::trace!(
"RateLimit-Reset in {:?} seconds",
since_epoch.checked_sub(now)
);
rate_limit_reset = since_epoch;
}
}
}
log::trace!("Response {} {}", response_code, url);
match response_code {
200 => {
if let Some(title) = Self::title_from_json(&body) {
log::debug!(
"PR #{} (RL {}) ⇒ «{}»",
pr_id,
rate_limit_remaining,
title
);
if let Err(err) = cache::store_api_response(
&v.url,
&format!("{}.json", pr_id),
&body,
) {
log::warn!("PR #{}, {}", pr_id, err);
}
tx_1.send(GitHubResponse {
oid,
subject: format!("{} (#{})", title, pr_id),
})
.unwrap();
} else {
log::warn!("Got invalid JSON for #{}", pr_id);
log::debug!("{}", body);
}
}
403 => {
log::warn!("We are asked to rate limit our selfs");
log::debug!("{}", body);
rate_limit_remaining = 0;
}
_ => {
log::warn!("Unexpected API Response {}", response_code);
log::debug!("{}", body);
}
}
}
}
});
Self(ActorThread::new(thread, receiver, sender))
}
pub(crate) fn send(&self, req: GitHubRequest) -> Result<(), SendError<GitHubRequest>> {
self.0.send(req)
}
pub(crate) fn try_recv(&self) -> Result<GitHubResponse, TryRecvError> {
self.0.try_recv()
}
pub(crate) fn can_handle(url: &Url) -> bool {
if let Some(domain) = url.domain() {
return domain == "github.com";
}
false
}
pub fn from_cache(url: &Url, pr_id: &str) -> Option<String> {
let json_data = match cache::fetch_api_response(url, &format!("{}.json", pr_id)) {
Ok(v) => v,
Err(err) => {
log::warn!("PR #{}, {}", pr_id, err);
None
}
}?;
Self::title_from_json(&json_data)
}
fn title_from_json(body: &str) -> Option<String> {
let json = body.parse::<JsonValue>().ok()?;
if let JsonValue::String(title) = &json["title"] {
return Some(title.to_string());
}
None
}
}