handlers.rs 9.1 KB

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