ping.rs 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. //! Purpose: Allow any user to ping a pre-selected group of people on GitHub via comments.
  2. //!
  3. //! The set of "teams" which can be pinged is intentionally restricted via configuration.
  4. //!
  5. //! Parsing is done in the `parser::command::ping` module.
  6. use crate::{
  7. config::PingConfig,
  8. github::{self, Event},
  9. handlers::{Context, Handler},
  10. interactions::ErrorComment,
  11. };
  12. use futures::future::{BoxFuture, FutureExt};
  13. use parser::command::ping::PingCommand;
  14. use parser::command::{Command, Input};
  15. pub(super) struct PingHandler;
  16. impl Handler for PingHandler {
  17. type Input = PingCommand;
  18. type Config = PingConfig;
  19. fn parse_input(
  20. &self,
  21. ctx: &Context,
  22. event: &Event,
  23. _: Option<&Self::Config>,
  24. ) -> Result<Option<Self::Input>, String> {
  25. let body = if let Some(b) = event.comment_body() {
  26. b
  27. } else {
  28. // not interested in other events
  29. return Ok(None);
  30. };
  31. if let Event::Issue(e) = event {
  32. if !matches!(e.action, github::IssuesAction::Opened | github::IssuesAction::Edited) {
  33. // skip events other than opening or editing the issue to avoid retriggering commands in the
  34. // issue body
  35. return Ok(None);
  36. }
  37. }
  38. let mut input = Input::new(&body, &ctx.username);
  39. let command = input.parse_command();
  40. if let Some(previous) = event.comment_from() {
  41. let mut prev_input = Input::new(&previous, &ctx.username);
  42. let prev_command = prev_input.parse_command();
  43. if command == prev_command {
  44. return Ok(None);
  45. }
  46. }
  47. match command {
  48. Command::Ping(Ok(command)) => Ok(Some(command)),
  49. Command::Ping(Err(err)) => {
  50. return Err(format!(
  51. "Parsing ping command in [comment]({}) failed: {}",
  52. event.html_url().expect("has html url"),
  53. err
  54. ));
  55. }
  56. _ => Ok(None),
  57. }
  58. }
  59. fn handle_input<'a>(
  60. &self,
  61. ctx: &'a Context,
  62. config: &'a PingConfig,
  63. event: &'a Event,
  64. input: PingCommand,
  65. ) -> BoxFuture<'a, anyhow::Result<()>> {
  66. handle_input(ctx, config, event, input.team).boxed()
  67. }
  68. }
  69. async fn handle_input(
  70. ctx: &Context,
  71. config: &PingConfig,
  72. event: &Event,
  73. team_name: String,
  74. ) -> anyhow::Result<()> {
  75. let is_team_member = if let Err(_) | Ok(false) = event.user().is_team_member(&ctx.github).await
  76. {
  77. false
  78. } else {
  79. true
  80. };
  81. if !is_team_member {
  82. let cmnt = ErrorComment::new(
  83. &event.issue().unwrap(),
  84. format!("Only Rust team members can ping teams."),
  85. );
  86. cmnt.post(&ctx.github).await?;
  87. return Ok(());
  88. }
  89. let (gh_team, config) = match config.get_by_name(&team_name) {
  90. Some(v) => v,
  91. None => {
  92. let cmnt = ErrorComment::new(
  93. &event.issue().unwrap(),
  94. format!(
  95. "This team (`{}`) cannot be pinged via this command;\
  96. it may need to be added to `triagebot.toml` on the master branch.",
  97. team_name,
  98. ),
  99. );
  100. cmnt.post(&ctx.github).await?;
  101. return Ok(());
  102. }
  103. };
  104. let team = github::get_team(&ctx.github, &gh_team).await?;
  105. let team = match team {
  106. Some(team) => team,
  107. None => {
  108. let cmnt = ErrorComment::new(
  109. &event.issue().unwrap(),
  110. format!(
  111. "This team (`{}`) does not exist in the team repository.",
  112. team_name,
  113. ),
  114. );
  115. cmnt.post(&ctx.github).await?;
  116. return Ok(());
  117. }
  118. };
  119. if let Some(label) = config.label.clone() {
  120. let issue_labels = event.issue().unwrap().labels();
  121. if !issue_labels.iter().any(|l| l.name == label) {
  122. let mut issue_labels = issue_labels.to_owned();
  123. issue_labels.push(github::Label { name: label });
  124. event
  125. .issue()
  126. .unwrap()
  127. .set_labels(&ctx.github, issue_labels)
  128. .await?;
  129. }
  130. }
  131. let mut users = Vec::new();
  132. if let Some(gh) = team.github {
  133. let repo = event.issue().expect("has issue").repository();
  134. // Ping all github teams associated with this team repo team that are in this organization.
  135. // We cannot ping across organizations, but this should not matter, as teams should be
  136. // sync'd to the org for which triagebot is configured.
  137. for gh_team in gh.teams.iter().filter(|t| t.org == repo.organization) {
  138. users.push(format!("@{}/{}", gh_team.org, gh_team.name));
  139. }
  140. } else {
  141. for member in &team.members {
  142. users.push(format!("@{}", member.github));
  143. }
  144. }
  145. let ping_msg = if users.is_empty() {
  146. format!("no known users to ping?")
  147. } else {
  148. format!("cc {}", users.join(" "))
  149. };
  150. let comment = format!("{}\n\n{}", config.message, ping_msg);
  151. event
  152. .issue()
  153. .expect("issue")
  154. .post_comment(&ctx.github, &comment)
  155. .await?;
  156. Ok(())
  157. }