ソースを参照

Implement ping command

Mark Rousskov 5 年 前
コミット
4093dbedb9
4 ファイル変更173 行追加4 行削除
  1. 14 0
      src/config.rs
  2. 17 4
      src/github.rs
  3. 1 0
      src/handlers.rs
  4. 141 0
      src/handlers/ping.rs

+ 14 - 0
src/config.rs

@@ -18,6 +18,20 @@ lazy_static::lazy_static! {
 pub(crate) struct Config {
     pub(crate) relabel: Option<RelabelConfig>,
     pub(crate) assign: Option<AssignConfig>,
+    pub(crate) ping: Option<PingConfig>,
+}
+
+#[derive(Debug, serde::Deserialize)]
+pub(crate) struct PingConfig {
+    // team name -> message
+    // message will have the cc string appended
+    pub(crate) teams: HashMap<String, PingTeamConfig>,
+}
+
+#[derive(Debug, serde::Deserialize)]
+pub(crate) struct PingTeamConfig {
+    pub(crate) message: String,
+    pub(crate) label: Option<String>,
 }
 
 #[derive(Debug, serde::Deserialize)]

+ 17 - 4
src/github.rs

@@ -75,6 +75,19 @@ impl User {
     }
 }
 
+pub async fn get_team(
+    client: &GithubClient,
+    team: &str,
+) -> Result<Option<rust_team_data::v1::Team>, Error> {
+    let url = format!("{}/teams.json", rust_team_data::v1::BASE_URL);
+    let permission: rust_team_data::v1::Teams = client
+        .json(client.raw().get(&url))
+        .await
+        .context("could not get team data")?;
+    let mut map = permission.teams;
+    Ok(map.swap_remove(team))
+}
+
 #[derive(Debug, Clone, serde::Deserialize)]
 pub struct Label {
     pub name: String,
@@ -145,9 +158,9 @@ impl fmt::Display for AssignmentError {
 impl std::error::Error for AssignmentError {}
 
 #[derive(Debug)]
-struct IssueRepository {
-    organization: String,
-    repository: String,
+pub struct IssueRepository {
+    pub organization: String,
+    pub repository: String,
 }
 
 impl fmt::Display for IssueRepository {
@@ -157,7 +170,7 @@ impl fmt::Display for IssueRepository {
 }
 
 impl Issue {
-    fn repository(&self) -> &IssueRepository {
+    pub fn repository(&self) -> &IssueRepository {
         self.repository.get_or_init(|| {
             log::trace!("get repository for {}", self.repository_url);
             let url = url::Url::parse(&self.repository_url).unwrap();

+ 1 - 0
src/handlers.rs

@@ -60,6 +60,7 @@ macro_rules! handlers {
 handlers! {
     assign = assign::AssignmentHandler,
     relabel = relabel::RelabelHandler,
+    ping = ping::PingHandler,
     //tracking_issue = tracking_issue::TrackingIssueHandler,
 }
 

+ 141 - 0
src/handlers/ping.rs

@@ -0,0 +1,141 @@
+//! Purpose: Allow any user to ping a pre-selected group of people on GitHub via comments.
+//!
+//! The set of "teams" which can be pinged is intentionally restricted via configuration.
+//!
+//! Parsing is done in the `parser::command::ping` module.
+
+use crate::{
+    config::PingConfig,
+    github::{self, Event},
+    handlers::{Context, Handler},
+    interactions::ErrorComment,
+};
+use failure::Error;
+use futures::future::{BoxFuture, FutureExt};
+use parser::command::ping::PingCommand;
+use parser::command::{Command, Input};
+
+pub(super) struct PingHandler;
+
+impl Handler for PingHandler {
+    type Input = PingCommand;
+    type Config = PingConfig;
+
+    fn parse_input(&self, ctx: &Context, event: &Event) -> Result<Option<Self::Input>, String> {
+        let body = if let Some(b) = event.comment_body() {
+            b
+        } else {
+            // not interested in other events
+            return Ok(None);
+        };
+
+        if let Event::Issue(e) = event {
+            if e.action != github::IssuesAction::Opened {
+                // skip events other than opening the issue to avoid retriggering commands in the
+                // issue body
+                return Ok(None);
+            }
+        }
+
+        let mut input = Input::new(&body, &ctx.username);
+        match input.parse_command() {
+            Command::Ping(Ok(command)) => Ok(Some(command)),
+            Command::Ping(Err(err)) => {
+                return Err(format!(
+                    "Parsing ping command in [comment]({}) failed: {}",
+                    event.html_url().expect("has html url"),
+                    err
+                ));
+            }
+            _ => Ok(None),
+        }
+    }
+
+    fn handle_input<'a>(
+        &self,
+        ctx: &'a Context,
+        config: &'a PingConfig,
+        event: &'a Event,
+        input: PingCommand,
+    ) -> BoxFuture<'a, Result<(), Error>> {
+        handle_input(ctx, config, event, input.team).boxed()
+    }
+}
+
+async fn handle_input(
+    ctx: &Context,
+    config: &PingConfig,
+    event: &Event,
+    team_name: String,
+) -> Result<(), Error> {
+    if !config.teams.contains_key(&team_name) {
+        let cmnt = ErrorComment::new(
+            &event.issue().unwrap(),
+            format!(
+                "This team (`{}`) cannot be pinged via this command;\
+                 it may need to be added to `triagebot.toml` on the master branch.",
+                team_name,
+            ),
+        );
+        cmnt.post(&ctx.github).await?;
+        return Ok(());
+    }
+    let team = github::get_team(&ctx.github, &team_name).await?;
+    let team = match team {
+        Some(team) => team,
+        None => {
+            let cmnt = ErrorComment::new(
+                &event.issue().unwrap(),
+                format!(
+                    "This team (`{}`) does not exist in the team repository.",
+                    team_name,
+                ),
+            );
+            cmnt.post(&ctx.github).await?;
+            return Ok(());
+        }
+    };
+
+    if let Some(label) = config.teams[&team_name].label.clone() {
+        let issue_labels = event.issue().unwrap().labels();
+        if !issue_labels.iter().any(|l| l.name == label) {
+            let mut issue_labels = issue_labels.to_owned();
+            issue_labels.push(github::Label { name: label });
+            event
+                .issue()
+                .unwrap()
+                .set_labels(&ctx.github, issue_labels)
+                .await?;
+        }
+    }
+
+    let mut users = Vec::new();
+
+    if let Some(gh) = team.github {
+        let repo = event.issue().expect("has issue").repository();
+        // Ping all github teams associated with this team repo team that are in this organization.
+        // We cannot ping across organizations, but this should not matter, as teams should be
+        // sync'd to the org for which triagebot is configured.
+        for gh_team in gh.teams.iter().filter(|t| t.org == repo.organization) {
+            users.push(format!("@{}/{}", gh_team.org, gh_team.name));
+        }
+    } else {
+        for member in &team.members {
+            users.push(format!("@{}", member.github));
+        }
+    }
+
+    let ping_msg = if users.is_empty() {
+        format!("no known users to ping?")
+    } else {
+        format!("cc {}", users.join(" "))
+    };
+    let comment = format!("{}\n\n{}", config.teams[&team_name].message, ping_msg);
+    event
+        .issue()
+        .expect("issue")
+        .post_comment(&ctx.github, &comment)
+        .await?;
+
+    Ok(())
+}