github.rs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. use failure::{Error, ResultExt};
  2. use futures::{
  3. compat::{Future01CompatExt, Stream01CompatExt},
  4. stream::{FuturesUnordered, StreamExt},
  5. };
  6. use reqwest::header::{AUTHORIZATION, USER_AGENT};
  7. use reqwest::{
  8. r#async::{Client, RequestBuilder, Response},
  9. StatusCode,
  10. };
  11. use std::fmt;
  12. #[derive(Debug, PartialEq, Eq, serde::Deserialize)]
  13. pub struct User {
  14. pub login: String,
  15. }
  16. impl GithubClient {
  17. async fn _send_req(&self, req: RequestBuilder) -> Result<(Response, String), Error> {
  18. let req = req
  19. .build()
  20. .with_context(|_| format!("failed to build request"))?;
  21. let req_dbg = format!("{:?}", req);
  22. let resp = self.client.execute(req).compat().await.context(req_dbg.clone())?;
  23. resp.error_for_status_ref().context(req_dbg.clone())?;
  24. Ok((resp, req_dbg))
  25. }
  26. async fn send_req(&self, req: RequestBuilder) -> Result<Vec<u8>, Error> {
  27. let (resp, req_dbg) = self._send_req(req).await?;
  28. let mut body = Vec::new();
  29. let mut stream = resp.into_body().compat();
  30. while let Some(chunk) = stream.next().await {
  31. let chunk = chunk
  32. .context("reading stream failed")
  33. .map_err(Error::from)
  34. .context(req_dbg.clone())?;
  35. body.extend_from_slice(&chunk);
  36. }
  37. Ok(body)
  38. }
  39. async fn json<T>(&self, req: RequestBuilder) -> Result<T, Error>
  40. where
  41. T: serde::de::DeserializeOwned,
  42. {
  43. let (mut resp, req_dbg) = self._send_req(req).await?;
  44. Ok(resp.json().compat().await.context(req_dbg)?)
  45. }
  46. }
  47. impl User {
  48. pub async fn current(client: &GithubClient) -> Result<Self, Error> {
  49. client.json(client.get("https://api.github.com/user")).await
  50. }
  51. pub async fn is_team_member<'a>(&'a self, client: &'a GithubClient) -> Result<bool, Error> {
  52. let url = format!("{}/teams.json", rust_team_data::v1::BASE_URL);
  53. let permission: rust_team_data::v1::Teams = client
  54. .json(client.raw().get(&url))
  55. .await
  56. .context("could not get team data")?;
  57. let map = permission.teams;
  58. Ok(map["all"].members.iter().any(|g| g.github == self.login)
  59. || map
  60. .get("wg-triage")
  61. .map_or(false, |w| w.members.iter().any(|g| g.github == self.login)))
  62. }
  63. }
  64. #[derive(Debug, Clone, serde::Deserialize)]
  65. pub struct Label {
  66. pub name: String,
  67. }
  68. impl Label {
  69. async fn exists<'a>(&'a self, repo_api_prefix: &'a str, client: &'a GithubClient) -> bool {
  70. #[allow(clippy::redundant_pattern_matching)]
  71. match client
  72. .send_req(client.get(&format!("{}/labels/{}", repo_api_prefix, self.name)))
  73. .await
  74. {
  75. Ok(_) => true,
  76. // XXX: Error handling if the request failed for reasons beyond 'label didn't exist'
  77. Err(_) => false,
  78. }
  79. }
  80. }
  81. #[derive(Debug, serde::Deserialize)]
  82. pub struct Issue {
  83. pub number: u64,
  84. pub body: String,
  85. title: String,
  86. html_url: String,
  87. user: User,
  88. labels: Vec<Label>,
  89. assignees: Vec<User>,
  90. // API URL
  91. repository_url: String,
  92. comments_url: String,
  93. }
  94. #[derive(Debug, serde::Deserialize)]
  95. pub struct Comment {
  96. pub body: String,
  97. pub html_url: String,
  98. pub user: User,
  99. }
  100. #[derive(Debug)]
  101. pub enum AssignmentError {
  102. InvalidAssignee,
  103. Http(Error),
  104. }
  105. pub enum Selection<'a, T> {
  106. All,
  107. One(&'a T),
  108. }
  109. impl fmt::Display for AssignmentError {
  110. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  111. match self {
  112. AssignmentError::InvalidAssignee => write!(f, "invalid assignee"),
  113. AssignmentError::Http(e) => write!(f, "cannot assign: {}", e),
  114. }
  115. }
  116. }
  117. impl std::error::Error for AssignmentError {}
  118. impl Issue {
  119. pub async fn get_comment(&self, client: &GithubClient, id: usize) -> Result<Comment, Error> {
  120. let comment_url = format!("{}/issues/comments/{}", self.repository_url, id);
  121. let comment = client.json(client.get(&comment_url)).await?;
  122. Ok(comment)
  123. }
  124. pub async fn edit_body(&self, client: &GithubClient, body: &str) -> Result<(), Error> {
  125. let edit_url = format!("{}/issues/{}", self.repository_url, self.number);
  126. #[derive(serde::Serialize)]
  127. struct ChangedIssue<'a> {
  128. body: &'a str,
  129. }
  130. client._send_req(client
  131. .patch(&edit_url)
  132. .json(&ChangedIssue { body })).await
  133. .context("failed to edit issue body")?;
  134. Ok(())
  135. }
  136. pub async fn edit_comment(
  137. &self,
  138. client: &GithubClient,
  139. id: usize,
  140. new_body: &str,
  141. ) -> Result<(), Error> {
  142. let comment_url = format!("{}/issues/comments/{}", self.repository_url, id);
  143. #[derive(serde::Serialize)]
  144. struct NewComment<'a> {
  145. body: &'a str,
  146. }
  147. client._send_req(client
  148. .patch(&comment_url)
  149. .json(&NewComment { body: new_body }))
  150. .await
  151. .context("failed to edit comment")?;
  152. Ok(())
  153. }
  154. pub async fn post_comment(&self, client: &GithubClient, body: &str) -> Result<(), Error> {
  155. #[derive(serde::Serialize)]
  156. struct PostComment<'a> {
  157. body: &'a str,
  158. }
  159. client._send_req(client
  160. .post(&self.comments_url)
  161. .json(&PostComment { body }))
  162. .await
  163. .context("failed to post comment")?;
  164. Ok(())
  165. }
  166. pub async fn set_labels(&self, client: &GithubClient, labels: Vec<Label>) -> Result<(), Error> {
  167. // PUT /repos/:owner/:repo/issues/:number/labels
  168. // repo_url = https://api.github.com/repos/Codertocat/Hello-World
  169. let url = format!(
  170. "{repo_url}/issues/{number}/labels",
  171. repo_url = self.repository_url,
  172. number = self.number
  173. );
  174. let mut stream = labels.into_iter().map(|label| {
  175. async { (label.exists(&self.repository_url, &client).await, label) }
  176. }).collect::<FuturesUnordered<_>>();
  177. let mut labels = Vec::new();
  178. while let Some((true, label)) = stream.next().await {
  179. labels.push(label);
  180. }
  181. #[derive(serde::Serialize)]
  182. struct LabelsReq {
  183. labels: Vec<String>,
  184. }
  185. client._send_req(client
  186. .put(&url)
  187. .json(&LabelsReq {
  188. labels: labels.iter().map(|l| l.name.clone()).collect(),
  189. })).await
  190. .context("failed to set labels")?;
  191. Ok(())
  192. }
  193. pub fn labels(&self) -> &[Label] {
  194. &self.labels
  195. }
  196. pub fn contain_assignee(&self, user: &User) -> bool {
  197. self.assignees.contains(user)
  198. }
  199. pub async fn remove_assignees(
  200. &self,
  201. client: &GithubClient,
  202. selection: Selection<'_, User>,
  203. ) -> Result<(), AssignmentError> {
  204. let url = format!(
  205. "{repo_url}/issues/{number}/assignees",
  206. repo_url = self.repository_url,
  207. number = self.number
  208. );
  209. let assignees = match selection {
  210. Selection::All => self
  211. .assignees
  212. .iter()
  213. .map(|u| u.login.as_str())
  214. .collect::<Vec<_>>(),
  215. Selection::One(user) => vec![user.login.as_str()],
  216. };
  217. #[derive(serde::Serialize)]
  218. struct AssigneeReq<'a> {
  219. assignees: &'a [&'a str],
  220. }
  221. client._send_req(client
  222. .delete(&url)
  223. .json(&AssigneeReq {
  224. assignees: &assignees[..],
  225. })).await
  226. .map_err(AssignmentError::Http)?;
  227. Ok(())
  228. }
  229. pub async fn set_assignee(&self, client: &GithubClient, user: &str) -> Result<(), AssignmentError> {
  230. let url = format!(
  231. "{repo_url}/issues/{number}/assignees",
  232. repo_url = self.repository_url,
  233. number = self.number
  234. );
  235. let check_url = format!(
  236. "{repo_url}/assignees/{name}",
  237. repo_url = self.repository_url,
  238. name = user,
  239. );
  240. match client._send_req(client.get(&check_url)).await {
  241. Ok((resp, _)) => {
  242. if resp.status() == reqwest::StatusCode::NO_CONTENT {
  243. // all okay
  244. } else if resp.status() == reqwest::StatusCode::NOT_FOUND {
  245. return Err(AssignmentError::InvalidAssignee);
  246. }
  247. }
  248. Err(e) => return Err(AssignmentError::Http(e)),
  249. }
  250. self.remove_assignees(client, Selection::All).await?;
  251. #[derive(serde::Serialize)]
  252. struct AssigneeReq<'a> {
  253. assignees: &'a [&'a str],
  254. }
  255. client._send_req(client
  256. .post(&url)
  257. .json(&AssigneeReq { assignees: &[user] }))
  258. .await
  259. .map_err(AssignmentError::Http)?;
  260. Ok(())
  261. }
  262. }
  263. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  264. #[serde(rename_all = "lowercase")]
  265. pub enum IssueCommentAction {
  266. Created,
  267. Edited,
  268. Deleted,
  269. }
  270. #[derive(Debug, serde::Deserialize)]
  271. pub struct IssueCommentEvent {
  272. pub action: IssueCommentAction,
  273. pub issue: Issue,
  274. pub comment: Comment,
  275. pub repository: Repository,
  276. }
  277. #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
  278. #[serde(rename_all = "lowercase")]
  279. pub enum IssuesAction {
  280. Opened,
  281. Edited,
  282. Deleted,
  283. Transferred,
  284. Pinned,
  285. Unpinned,
  286. Closed,
  287. Reopened,
  288. Assigned,
  289. Unassigned,
  290. Labeled,
  291. Unlabeled,
  292. Locked,
  293. Unlocked,
  294. Milestoned,
  295. Demilestoned,
  296. }
  297. #[derive(Debug, serde::Deserialize)]
  298. pub struct IssuesEvent {
  299. pub action: IssuesAction,
  300. pub issue: Issue,
  301. pub repository: Repository,
  302. }
  303. #[derive(Debug, serde::Deserialize)]
  304. pub struct Repository {
  305. pub full_name: String,
  306. }
  307. #[derive(Debug)]
  308. pub enum Event {
  309. IssueComment(IssueCommentEvent),
  310. Issue(IssuesEvent),
  311. }
  312. impl Event {
  313. pub fn repo_name(&self) -> &str {
  314. match self {
  315. Event::IssueComment(event) => &event.repository.full_name,
  316. Event::Issue(event) => &event.repository.full_name,
  317. }
  318. }
  319. pub fn issue(&self) -> Option<&Issue> {
  320. match self {
  321. Event::IssueComment(event) => Some(&event.issue),
  322. Event::Issue(event) => Some(&event.issue),
  323. }
  324. }
  325. /// This will both extract from IssueComment events but also Issue events
  326. pub fn comment_body(&self) -> Option<&str> {
  327. match self {
  328. Event::Issue(e) => Some(&e.issue.body),
  329. Event::IssueComment(e) => Some(&e.comment.body),
  330. }
  331. }
  332. pub fn html_url(&self) -> Option<&str> {
  333. match self {
  334. Event::Issue(e) => Some(&e.issue.html_url),
  335. Event::IssueComment(e) => Some(&e.comment.html_url),
  336. }
  337. }
  338. pub fn user(&self) -> &User {
  339. match self {
  340. Event::Issue(e) => &e.issue.user,
  341. Event::IssueComment(e) => &e.comment.user,
  342. }
  343. }
  344. }
  345. trait RequestSend: Sized {
  346. fn configure(self, g: &GithubClient) -> Self;
  347. }
  348. impl RequestSend for RequestBuilder {
  349. fn configure(self, g: &GithubClient) -> RequestBuilder {
  350. self.header(USER_AGENT, "rust-lang-triagebot")
  351. .header(AUTHORIZATION, format!("token {}", g.token))
  352. }
  353. }
  354. #[derive(Clone)]
  355. pub struct GithubClient {
  356. token: String,
  357. client: Client,
  358. }
  359. impl GithubClient {
  360. pub fn new(client: Client, token: String) -> Self {
  361. GithubClient { client, token }
  362. }
  363. pub fn raw(&self) -> &Client {
  364. &self.client
  365. }
  366. pub async fn raw_file(&self, repo: &str, branch: &str, path: &str) -> Result<Option<Vec<u8>>, Error> {
  367. let url = format!(
  368. "https://raw.githubusercontent.com/{}/{}/{}",
  369. repo, branch, path
  370. );
  371. let req = self.get(&url);
  372. let req_dbg = format!("{:?}", req);
  373. let req = req
  374. .build()
  375. .with_context(|_| format!("failed to build request {:?}", req_dbg))?;
  376. let resp = self.client.execute(req).compat().await.context(req_dbg.clone())?;
  377. let status = resp.status();
  378. match status {
  379. StatusCode::OK => {
  380. let mut buf = Vec::with_capacity(resp.content_length().unwrap_or(4) as usize);
  381. let mut stream = resp.into_body().compat();
  382. while let Some(chunk) = stream.next().await {
  383. let chunk = chunk
  384. .context("reading stream failed")
  385. .map_err(Error::from)
  386. .context(req_dbg.clone())?;
  387. buf.extend_from_slice(&chunk);
  388. }
  389. Ok(Some(buf))
  390. }
  391. StatusCode::NOT_FOUND => Ok(None),
  392. status => failure::bail!("failed to GET {}: {}", url, status),
  393. }
  394. }
  395. fn get(&self, url: &str) -> RequestBuilder {
  396. log::trace!("get {:?}", url);
  397. self.client.get(url).configure(self)
  398. }
  399. fn patch(&self, url: &str) -> RequestBuilder {
  400. log::trace!("patch {:?}", url);
  401. self.client.patch(url).configure(self)
  402. }
  403. fn delete(&self, url: &str) -> RequestBuilder {
  404. log::trace!("delete {:?}", url);
  405. self.client.delete(url).configure(self)
  406. }
  407. fn post(&self, url: &str) -> RequestBuilder {
  408. log::trace!("post {:?}", url);
  409. self.client.post(url).configure(self)
  410. }
  411. fn put(&self, url: &str) -> RequestBuilder {
  412. log::trace!("put {:?}", url);
  413. self.client.put(url).configure(self)
  414. }
  415. }