github.rs 5.3 KB

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