handlers.rs 9.8 KB

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