github.rs 16 KB

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