ping.rs 4.8 KB

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