lib.rs 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. #![allow(clippy::new_without_default)]
  2. #[macro_use]
  3. extern crate lazy_static;
  4. use crate::github::PullRequestDetails;
  5. use anyhow::Context;
  6. use handlers::HandlerError;
  7. use interactions::ErrorComment;
  8. use std::fmt;
  9. use tracing as log;
  10. pub mod actions;
  11. pub mod agenda;
  12. mod changelogs;
  13. pub mod config;
  14. pub mod db;
  15. pub mod github;
  16. pub mod handlers;
  17. pub mod http_client;
  18. pub mod interactions;
  19. pub mod jobs;
  20. pub mod notification_listing;
  21. pub mod payload;
  22. pub mod rfcbot;
  23. pub mod team;
  24. mod team_data;
  25. pub mod triage;
  26. pub mod zulip;
  27. /// The name of a webhook event.
  28. #[derive(Debug)]
  29. pub enum EventName {
  30. /// Pull request activity.
  31. ///
  32. /// This gets translated to [`github::Event::Issue`] when sent to a handler.
  33. ///
  34. /// <https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#pull_request>
  35. PullRequest,
  36. /// Pull request review activity.
  37. ///
  38. /// This gets translated to [`github::Event::IssueComment`] when sent to a handler.
  39. ///
  40. /// <https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#pull_request_review>
  41. PullRequestReview,
  42. /// A comment on a pull request review.
  43. ///
  44. /// This gets translated to [`github::Event::IssueComment`] when sent to a handler.
  45. ///
  46. /// <https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#pull_request_review_comment>
  47. PullRequestReviewComment,
  48. /// An issue or PR comment.
  49. ///
  50. /// This gets translated to [`github::Event::IssueComment`] when sent to a handler.
  51. ///
  52. /// <https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#issue_comment>
  53. IssueComment,
  54. /// Issue activity.
  55. ///
  56. /// This gets translated to [`github::Event::Issue`] when sent to a handler.
  57. ///
  58. /// <https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#issues>
  59. Issue,
  60. /// One or more commits are pushed to a repository branch or tag.
  61. ///
  62. /// This gets translated to [`github::Event::Push`] when sent to a handler.
  63. ///
  64. /// <https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#push>
  65. Push,
  66. /// A Git branch or tag is created.
  67. ///
  68. /// This gets translated to [`github::Event::Create`] when sent to a handler.
  69. ///
  70. /// <https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#create>
  71. Create,
  72. /// All other unhandled webhooks.
  73. Other,
  74. }
  75. impl std::str::FromStr for EventName {
  76. type Err = std::convert::Infallible;
  77. fn from_str(s: &str) -> Result<EventName, Self::Err> {
  78. Ok(match s {
  79. "pull_request_review" => EventName::PullRequestReview,
  80. "pull_request_review_comment" => EventName::PullRequestReviewComment,
  81. "issue_comment" => EventName::IssueComment,
  82. "pull_request" => EventName::PullRequest,
  83. "issues" => EventName::Issue,
  84. "push" => EventName::Push,
  85. "create" => EventName::Create,
  86. _ => EventName::Other,
  87. })
  88. }
  89. }
  90. impl fmt::Display for EventName {
  91. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  92. write!(
  93. f,
  94. "{}",
  95. match self {
  96. EventName::PullRequestReview => "pull_request_review",
  97. EventName::PullRequestReviewComment => "pull_request_review_comment",
  98. EventName::IssueComment => "issue_comment",
  99. EventName::Issue => "issues",
  100. EventName::PullRequest => "pull_request",
  101. EventName::Push => "push",
  102. EventName::Create => "create",
  103. EventName::Other => "other",
  104. }
  105. )
  106. }
  107. }
  108. #[derive(Debug)]
  109. pub struct WebhookError(anyhow::Error);
  110. impl From<anyhow::Error> for WebhookError {
  111. fn from(e: anyhow::Error) -> WebhookError {
  112. WebhookError(e)
  113. }
  114. }
  115. pub fn deserialize_payload<T: serde::de::DeserializeOwned>(v: &str) -> anyhow::Result<T> {
  116. let mut deserializer = serde_json::Deserializer::from_str(&v);
  117. let res: Result<T, _> = serde_path_to_error::deserialize(&mut deserializer);
  118. match res {
  119. Ok(r) => Ok(r),
  120. Err(e) => {
  121. let ctx = format!("at {:?}", e.path());
  122. Err(e.into_inner()).context(ctx)
  123. }
  124. }
  125. }
  126. pub async fn webhook(
  127. event: EventName,
  128. payload: String,
  129. ctx: &handlers::Context,
  130. ) -> Result<bool, WebhookError> {
  131. let event = match event {
  132. EventName::PullRequestReview => {
  133. let mut payload = deserialize_payload::<github::PullRequestReviewEvent>(&payload)
  134. .context("PullRequestReview failed to deserialize")
  135. .map_err(anyhow::Error::from)?;
  136. log::info!("handling pull request review comment {:?}", payload);
  137. payload.pull_request.pull_request = Some(PullRequestDetails {});
  138. // Treat pull request review comments exactly like pull request
  139. // review comments.
  140. github::Event::IssueComment(github::IssueCommentEvent {
  141. action: match payload.action {
  142. github::PullRequestReviewAction::Submitted => {
  143. github::IssueCommentAction::Created
  144. }
  145. github::PullRequestReviewAction::Edited => github::IssueCommentAction::Edited,
  146. github::PullRequestReviewAction::Dismissed => {
  147. github::IssueCommentAction::Deleted
  148. }
  149. },
  150. changes: payload.changes,
  151. issue: payload.pull_request,
  152. comment: payload.review,
  153. repository: payload.repository,
  154. })
  155. }
  156. EventName::PullRequestReviewComment => {
  157. let mut payload = deserialize_payload::<github::PullRequestReviewComment>(&payload)
  158. .context("PullRequestReview(Comment) failed to deserialize")
  159. .map_err(anyhow::Error::from)?;
  160. payload.issue.pull_request = Some(PullRequestDetails {});
  161. log::info!("handling pull request review comment {:?}", payload);
  162. // Treat pull request review comments exactly like pull request
  163. // review comments.
  164. github::Event::IssueComment(github::IssueCommentEvent {
  165. action: payload.action,
  166. changes: payload.changes,
  167. issue: payload.issue,
  168. comment: payload.comment,
  169. repository: payload.repository,
  170. })
  171. }
  172. EventName::IssueComment => {
  173. let payload = deserialize_payload::<github::IssueCommentEvent>(&payload)
  174. .context("IssueCommentEvent failed to deserialize")
  175. .map_err(anyhow::Error::from)?;
  176. log::info!("handling issue comment {:?}", payload);
  177. github::Event::IssueComment(payload)
  178. }
  179. EventName::Issue | EventName::PullRequest => {
  180. let mut payload = deserialize_payload::<github::IssuesEvent>(&payload)
  181. .context(format!("{:?} failed to deserialize", event))
  182. .map_err(anyhow::Error::from)?;
  183. if matches!(event, EventName::PullRequest) {
  184. payload.issue.pull_request = Some(PullRequestDetails {});
  185. }
  186. log::info!("handling issue event {:?}", payload);
  187. github::Event::Issue(payload)
  188. }
  189. EventName::Push => {
  190. let payload = deserialize_payload::<github::PushEvent>(&payload)
  191. .with_context(|| format!("{:?} failed to deserialize", event))
  192. .map_err(anyhow::Error::from)?;
  193. log::info!("handling push event {:?}", payload);
  194. github::Event::Push(payload)
  195. }
  196. EventName::Create => {
  197. let payload = deserialize_payload::<github::CreateEvent>(&payload)
  198. .with_context(|| format!("{:?} failed to deserialize", event))
  199. .map_err(anyhow::Error::from)?;
  200. log::info!("handling create event {:?}", payload);
  201. github::Event::Create(payload)
  202. }
  203. // Other events need not be handled
  204. EventName::Other => {
  205. return Ok(false);
  206. }
  207. };
  208. let errors = handlers::handle(&ctx, &event).await;
  209. let mut other_error = false;
  210. let mut message = String::new();
  211. for err in errors {
  212. match err {
  213. HandlerError::Message(msg) => {
  214. if !message.is_empty() {
  215. message.push_str("\n\n");
  216. }
  217. message.push_str(&msg);
  218. }
  219. HandlerError::Other(err) => {
  220. log::error!("handling event failed: {:?}", err);
  221. other_error = true;
  222. }
  223. }
  224. }
  225. if !message.is_empty() {
  226. if let Some(issue) = event.issue() {
  227. let cmnt = ErrorComment::new(issue, message);
  228. cmnt.post(&ctx.github).await?;
  229. }
  230. }
  231. if other_error {
  232. Err(WebhookError(anyhow::anyhow!(
  233. "handling failed, error logged",
  234. )))
  235. } else {
  236. Ok(true)
  237. }
  238. }