handlers.rs 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. use crate::config::{self, Config, ConfigurationError};
  2. use crate::github::{Event, GithubClient, IssueCommentAction, IssuesAction, IssuesEvent};
  3. use octocrab::Octocrab;
  4. use parser::command::{Command, Input};
  5. use std::fmt;
  6. use std::sync::Arc;
  7. use tracing as log;
  8. #[derive(Debug)]
  9. pub enum HandlerError {
  10. Message(String),
  11. Other(anyhow::Error),
  12. }
  13. impl std::error::Error for HandlerError {}
  14. impl fmt::Display for HandlerError {
  15. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  16. match self {
  17. HandlerError::Message(msg) => write!(f, "{}", msg),
  18. HandlerError::Other(_) => write!(f, "An internal error occurred."),
  19. }
  20. }
  21. }
  22. mod assign;
  23. mod autolabel;
  24. mod close;
  25. mod github_releases;
  26. mod glacier;
  27. mod major_change;
  28. mod milestone_prs;
  29. mod nominate;
  30. mod note;
  31. mod notification;
  32. mod notify_zulip;
  33. mod ping;
  34. mod prioritize;
  35. mod relabel;
  36. mod review_submitted;
  37. mod rustc_commits;
  38. mod shortcut;
  39. pub async fn handle(ctx: &Context, event: &Event) -> Vec<HandlerError> {
  40. let config = config::get(&ctx.github, &event.repo_name()).await;
  41. let mut errors = Vec::new();
  42. if let (Ok(config), Event::Issue(event)) = (config.as_ref(), event) {
  43. handle_issue(ctx, event, config, &mut errors).await;
  44. }
  45. if let Some(body) = event.comment_body() {
  46. handle_command(ctx, event, &config, body, &mut errors).await;
  47. }
  48. if let Err(e) = notification::handle(ctx, event).await {
  49. log::error!(
  50. "failed to process event {:?} with notification handler: {:?}",
  51. event,
  52. e
  53. );
  54. }
  55. if let Err(e) = rustc_commits::handle(ctx, event).await {
  56. log::error!(
  57. "failed to process event {:?} with rustc_commits handler: {:?}",
  58. event,
  59. e
  60. );
  61. }
  62. if let Err(e) = milestone_prs::handle(ctx, event).await {
  63. log::error!(
  64. "failed to process event {:?} with milestone_prs handler: {:?}",
  65. event,
  66. e
  67. );
  68. }
  69. if let Some(config) = config
  70. .as_ref()
  71. .ok()
  72. .and_then(|c| c.review_submitted.as_ref())
  73. {
  74. if let Err(e) = review_submitted::handle(ctx, event, config).await {
  75. log::error!(
  76. "failed to process event {:?} with review_submitted handler: {:?}",
  77. event,
  78. e
  79. )
  80. }
  81. }
  82. if let Some(ghr_config) = config
  83. .as_ref()
  84. .ok()
  85. .and_then(|c| c.github_releases.as_ref())
  86. {
  87. if let Err(e) = github_releases::handle(ctx, event, ghr_config).await {
  88. log::error!(
  89. "failed to process event {:?} with github_releases handler: {:?}",
  90. event,
  91. e
  92. );
  93. }
  94. }
  95. errors
  96. }
  97. macro_rules! issue_handlers {
  98. ($($name:ident,)*) => {
  99. async fn handle_issue(
  100. ctx: &Context,
  101. event: &IssuesEvent,
  102. config: &Arc<Config>,
  103. errors: &mut Vec<HandlerError>,
  104. ) {
  105. $(
  106. match $name::parse_input(ctx, event, config.$name.as_ref()).await {
  107. Err(err) => errors.push(HandlerError::Message(err)),
  108. Ok(Some(input)) => {
  109. if let Some(config) = &config.$name {
  110. $name::handle_input(ctx, config, event, input).await.unwrap_or_else(|err| errors.push(HandlerError::Other(err)));
  111. } else {
  112. errors.push(HandlerError::Message(format!(
  113. "The feature `{}` is not enabled in this repository.\n\
  114. To enable it add its section in the `triagebot.toml` \
  115. in the root of the repository.",
  116. stringify!($name)
  117. )));
  118. }
  119. }
  120. Ok(None) => {}
  121. })*
  122. }
  123. }
  124. }
  125. // Handle events that happened on issues
  126. //
  127. // This is for events that happen only on issues (e.g. label changes).
  128. // Each module in the list must contain the functions `parse_input` and `handle_input`.
  129. issue_handlers! {
  130. autolabel,
  131. major_change,
  132. notify_zulip,
  133. }
  134. macro_rules! command_handlers {
  135. ($($name:ident: $enum:ident,)*) => {
  136. async fn handle_command(
  137. ctx: &Context,
  138. event: &Event,
  139. config: &Result<Arc<Config>, ConfigurationError>,
  140. body: &str,
  141. errors: &mut Vec<HandlerError>,
  142. ) {
  143. match event {
  144. Event::Issue(e) => if !matches!(e.action, IssuesAction::Opened | IssuesAction::Edited) {
  145. // no change in issue's body for these events, so skip
  146. log::debug!("skipping event, issue was {:?}", e.action);
  147. return;
  148. }
  149. Event::IssueComment(e) => if e.action == IssueCommentAction::Deleted {
  150. // don't execute commands again when comment is deleted
  151. log::debug!("skipping event, comment was {:?}", e.action);
  152. return;
  153. }
  154. Event::Push(_) | Event::Create(_) => {
  155. log::debug!("skipping unsupported event");
  156. return;
  157. }
  158. }
  159. let input = Input::new(&body, vec![&ctx.username, "triagebot"]);
  160. let commands = if let Some(previous) = event.comment_from() {
  161. let prev_commands = Input::new(&previous, vec![&ctx.username, "triagebot"]).collect::<Vec<_>>();
  162. input.filter(|cmd| !prev_commands.contains(cmd)).collect::<Vec<_>>()
  163. } else {
  164. input.collect()
  165. };
  166. if commands.is_empty() {
  167. return;
  168. }
  169. let config = match config {
  170. Ok(config) => config,
  171. Err(e @ ConfigurationError::Missing) => {
  172. return errors.push(HandlerError::Message(e.to_string()));
  173. }
  174. Err(e @ ConfigurationError::Toml(_)) => {
  175. return errors.push(HandlerError::Message(e.to_string()));
  176. }
  177. Err(e @ ConfigurationError::Http(_)) => {
  178. return errors.push(HandlerError::Other(e.clone().into()));
  179. }
  180. };
  181. for command in commands {
  182. match command {
  183. $(
  184. Command::$enum(Ok(command)) => {
  185. if let Some(config) = &config.$name {
  186. $name::handle_command(ctx, config, event, command)
  187. .await
  188. .unwrap_or_else(|err| errors.push(HandlerError::Other(err)));
  189. } else {
  190. errors.push(HandlerError::Message(format!(
  191. "The feature `{}` is not enabled in this repository.\n\
  192. To enable it add its section in the `triagebot.toml` \
  193. in the root of the repository.",
  194. stringify!($name)
  195. )));
  196. }
  197. }
  198. Command::$enum(Err(err)) => {
  199. errors.push(HandlerError::Message(format!(
  200. "Parsing {} command in [comment]({}) failed: {}",
  201. stringify!($name),
  202. event.html_url().expect("has html url"),
  203. err
  204. )));
  205. })*
  206. }
  207. }
  208. }
  209. }
  210. }
  211. // Handle commands in comments/issues body
  212. //
  213. // This is for handlers for commands parsed by the `parser` crate.
  214. // Each variant of `parser::command::Command` must be in this list,
  215. // preceded by the module containing the coresponding `handle_command` function
  216. command_handlers! {
  217. assign: Assign,
  218. glacier: Glacier,
  219. nominate: Nominate,
  220. ping: Ping,
  221. prioritize: Prioritize,
  222. relabel: Relabel,
  223. major_change: Second,
  224. shortcut: Shortcut,
  225. close: Close,
  226. note: Note,
  227. }
  228. pub struct Context {
  229. pub github: GithubClient,
  230. pub db: crate::db::ClientPool,
  231. pub username: String,
  232. pub octocrab: Octocrab,
  233. }