github.rs 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. use failure::{Error, ResultExt};
  2. use reqwest::header::{AUTHORIZATION, USER_AGENT};
  3. use reqwest::{Client, Error as HttpError, RequestBuilder, Response, StatusCode};
  4. use std::io::Read;
  5. #[derive(Debug, serde::Deserialize)]
  6. pub struct User {
  7. pub login: String,
  8. }
  9. impl User {
  10. pub fn current(client: &GithubClient) -> Result<Self, Error> {
  11. Ok(client
  12. .get("https://api.github.com/user")
  13. .send_req()?
  14. .json()?)
  15. }
  16. pub fn is_team_member(&self, client: &GithubClient) -> Result<bool, Error> {
  17. let client = client.raw();
  18. let url = format!("{}/teams.json", rust_team_data::v1::BASE_URL);
  19. let permission: rust_team_data::v1::Teams = client
  20. .get(&url)
  21. .send()
  22. .and_then(Response::error_for_status)
  23. .and_then(|mut r| r.json())
  24. .context("could not get team data")?;
  25. let map = permission.teams;
  26. Ok(map["all"].members.iter().any(|g| g.github == self.login))
  27. }
  28. }
  29. #[derive(Debug, Clone, serde::Deserialize)]
  30. pub struct Label {
  31. pub name: String,
  32. }
  33. impl Label {
  34. fn exists(&self, repo_api_prefix: &str, client: &GithubClient) -> bool {
  35. #[allow(clippy::redundant_pattern_matching)]
  36. match client
  37. .get(&format!("{}/labels/{}", repo_api_prefix, self.name))
  38. .send_req()
  39. {
  40. Ok(_) => true,
  41. // XXX: Error handling if the request failed for reasons beyond 'label didn't exist'
  42. Err(_) => false,
  43. }
  44. }
  45. }
  46. #[derive(Debug, serde::Deserialize)]
  47. pub struct Issue {
  48. pub number: u64,
  49. title: String,
  50. user: User,
  51. labels: Vec<Label>,
  52. assignees: Vec<User>,
  53. // API URL
  54. repository_url: String,
  55. comments_url: String,
  56. }
  57. #[derive(Debug, serde::Deserialize)]
  58. pub struct Comment {
  59. pub body: String,
  60. pub html_url: String,
  61. pub user: User,
  62. }
  63. impl Issue {
  64. pub fn post_comment(&self, client: &GithubClient, body: &str) -> Result<(), Error> {
  65. #[derive(serde::Serialize)]
  66. struct PostComment<'a> {
  67. body: &'a str,
  68. }
  69. client
  70. .post(&self.comments_url)
  71. .json(&PostComment { body })
  72. .send_req()
  73. .context("failed to post comment")?;
  74. Ok(())
  75. }
  76. pub fn set_labels(&self, client: &GithubClient, mut labels: Vec<Label>) -> Result<(), Error> {
  77. // PUT /repos/:owner/:repo/issues/:number/labels
  78. // repo_url = https://api.github.com/repos/Codertocat/Hello-World
  79. let url = format!(
  80. "{repo_url}/issues/{number}/labels",
  81. repo_url = self.repository_url,
  82. number = self.number
  83. );
  84. labels.retain(|label| label.exists(&self.repository_url, &client));
  85. #[derive(serde::Serialize)]
  86. struct LabelsReq {
  87. labels: Vec<String>,
  88. }
  89. client
  90. .put(&url)
  91. .json(&LabelsReq {
  92. labels: labels.iter().map(|l| l.name.clone()).collect(),
  93. })
  94. .send_req()
  95. .context("failed to set labels")?;
  96. Ok(())
  97. }
  98. pub fn labels(&self) -> &[Label] {
  99. &self.labels
  100. }
  101. pub fn add_assignee(&self, client: &GithubClient, user: &str) -> Result<(), Error> {
  102. let url = format!(
  103. "{repo_url}/issues/{number}/assignees",
  104. repo_url = self.repository_url,
  105. number = self.number
  106. );
  107. let check_url = format!(
  108. "{repo_url}/assignees/{name}",
  109. repo_url = self.repository_url,
  110. name = user,
  111. );
  112. match client.get(&check_url).send() {
  113. Ok(resp) => {
  114. if resp.status() == reqwest::StatusCode::NO_CONTENT {
  115. // all okay
  116. } else if resp.status() == reqwest::StatusCode::NOT_FOUND {
  117. failure::bail!("invalid assignee {:?}", user);
  118. }
  119. }
  120. Err(e) => failure::bail!("unable to check assignee validity: {:?}", e),
  121. }
  122. #[derive(serde::Serialize)]
  123. struct AssigneeReq<'a> {
  124. assignees: &'a [&'a str],
  125. }
  126. client
  127. .post(&url)
  128. .json(&AssigneeReq { assignees: &[user] })
  129. .send_req()
  130. .context("failed to add assignee")?;
  131. Ok(())
  132. }
  133. }
  134. trait RequestSend: Sized {
  135. fn configure(self, g: &GithubClient) -> Self;
  136. fn send_req(self) -> Result<Response, HttpError>;
  137. }
  138. impl RequestSend for RequestBuilder {
  139. fn configure(self, g: &GithubClient) -> RequestBuilder {
  140. self.header(USER_AGENT, "rust-lang-triagebot")
  141. .header(AUTHORIZATION, format!("token {}", g.token))
  142. }
  143. fn send_req(self) -> Result<Response, HttpError> {
  144. match self.send() {
  145. Ok(r) => r.error_for_status(),
  146. Err(e) => Err(e),
  147. }
  148. }
  149. }
  150. #[derive(Clone)]
  151. pub struct GithubClient {
  152. token: String,
  153. client: Client,
  154. }
  155. impl GithubClient {
  156. pub fn new(client: Client, token: String) -> Self {
  157. GithubClient { client, token }
  158. }
  159. pub fn raw(&self) -> &Client {
  160. &self.client
  161. }
  162. pub fn raw_file(&self, repo: &str, branch: &str, path: &str) -> Result<Option<Vec<u8>>, Error> {
  163. let url = format!(
  164. "https://raw.githubusercontent.com/{}/{}/{}",
  165. repo, branch, path
  166. );
  167. let mut resp = self.get(&url).send()?;
  168. match resp.status() {
  169. StatusCode::OK => {
  170. let mut buf = Vec::with_capacity(resp.content_length().unwrap_or(4) as usize);
  171. resp.read_to_end(&mut buf)?;
  172. Ok(Some(buf))
  173. }
  174. StatusCode::NOT_FOUND => Ok(None),
  175. status => failure::bail!("failed to GET {}: {}", url, status),
  176. }
  177. }
  178. fn get(&self, url: &str) -> RequestBuilder {
  179. log::trace!("get {:?}", url);
  180. self.client.get(url).configure(self)
  181. }
  182. fn post(&self, url: &str) -> RequestBuilder {
  183. log::trace!("post {:?}", url);
  184. self.client.post(url).configure(self)
  185. }
  186. fn put(&self, url: &str) -> RequestBuilder {
  187. log::trace!("put {:?}", url);
  188. self.client.put(url).configure(self)
  189. }
  190. }