Browse Source

Merge pull request #1078 from camelid/close-msg

Notify Zulip when issue is closed or reopened
Santiago Pastorino 3 years ago
parent
commit
201b5d90d3
2 changed files with 137 additions and 60 deletions
  1. 2 0
      src/config.rs
  2. 135 60
      src/handlers/notify_zulip.rs

+ 2 - 0
src/config.rs

@@ -131,6 +131,8 @@ pub(crate) struct NotifyZulipLabelConfig {
     pub(crate) topic: String,
     pub(crate) message_on_add: Option<String>,
     pub(crate) message_on_remove: Option<String>,
+    pub(crate) message_on_close: Option<String>,
+    pub(crate) message_on_reopen: Option<String>,
     #[serde(default)]
     pub(crate) required_labels: Vec<String>,
 }

+ 135 - 60
src/handlers/notify_zulip.rs

@@ -1,96 +1,171 @@
 use crate::{
-    config::NotifyZulipConfig,
-    github::{IssuesAction, IssuesEvent},
+    config::{NotifyZulipConfig, NotifyZulipLabelConfig},
+    github::{Issue, IssuesAction, IssuesEvent, Label},
     handlers::Context,
 };
 
 pub(super) struct NotifyZulipInput {
     notification_type: NotificationType,
+    /// Label that triggered this notification.
+    ///
+    /// For example, if an `I-prioritize` issue is closed,
+    /// this field will be `I-prioritize`.
+    label: Label,
 }
 
 pub(super) enum NotificationType {
     Labeled,
     Unlabeled,
+    Closed,
+    Reopened,
 }
 
 pub(super) fn parse_input(
     _ctx: &Context,
     event: &IssuesEvent,
     config: Option<&NotifyZulipConfig>,
-) -> Result<Option<NotifyZulipInput>, String> {
-    if let IssuesAction::Labeled | IssuesAction::Unlabeled = event.action {
-        let applied_label = &event.label.as_ref().expect("label").name;
-        if let Some(config) = config.and_then(|c| c.labels.get(applied_label)) {
-            for label in &config.required_labels {
-                let pattern = match glob::Pattern::new(label) {
-                    Ok(pattern) => pattern,
-                    Err(err) => {
-                        log::error!("Invalid glob pattern: {}", err);
-                        continue;
-                    }
-                };
-                if !event
-                    .issue
-                    .labels()
-                    .iter()
-                    .any(|l| pattern.matches(&l.name))
-                {
-                    // Issue misses a required label, ignore this event
-                    return Ok(None);
+) -> Result<Option<Vec<NotifyZulipInput>>, String> {
+    let config = match config {
+        Some(config) => config,
+        None => return Ok(None),
+    };
+
+    match event.action {
+        IssuesAction::Labeled | IssuesAction::Unlabeled => {
+            let applied_label = event.label.as_ref().expect("label").clone();
+            Ok(config
+                .labels
+                .get(&applied_label.name)
+                .and_then(|label_config| {
+                    parse_label_change_input(event, applied_label, label_config)
+                })
+                .map(|input| vec![input]))
+        }
+        IssuesAction::Closed | IssuesAction::Reopened => {
+            Ok(Some(parse_close_reopen_input(event, config)))
+        }
+        _ => Ok(None),
+    }
+}
+
+fn parse_label_change_input(
+    event: &IssuesEvent,
+    label: Label,
+    config: &NotifyZulipLabelConfig,
+) -> Option<NotifyZulipInput> {
+    if !has_all_required_labels(&event.issue, config) {
+        // Issue misses a required label, ignore this event
+        return None;
+    }
+
+    match event.action {
+        IssuesAction::Labeled if config.message_on_add.is_some() => Some(NotifyZulipInput {
+            notification_type: NotificationType::Labeled,
+            label,
+        }),
+        IssuesAction::Unlabeled if config.message_on_remove.is_some() => Some(NotifyZulipInput {
+            notification_type: NotificationType::Unlabeled,
+            label,
+        }),
+        _ => None,
+    }
+}
+
+fn parse_close_reopen_input(
+    event: &IssuesEvent,
+    global_config: &NotifyZulipConfig,
+) -> Vec<NotifyZulipInput> {
+    event
+        .issue
+        .labels
+        .iter()
+        .cloned()
+        .filter_map(|label| {
+            global_config
+                .labels
+                .get(&label.name)
+                .map(|config| (label, config))
+        })
+        .flat_map(|(label, config)| {
+            if !has_all_required_labels(&event.issue, config) {
+                // Issue misses a required label, ignore this event
+                return None;
+            }
+
+            match event.action {
+                IssuesAction::Closed if config.message_on_close.is_some() => {
+                    Some(NotifyZulipInput {
+                        notification_type: NotificationType::Closed,
+                        label,
+                    })
+                }
+                IssuesAction::Reopened if config.message_on_reopen.is_some() => {
+                    Some(NotifyZulipInput {
+                        notification_type: NotificationType::Reopened,
+                        label,
+                    })
                 }
+                _ => None,
             }
+        })
+        .collect()
+}
 
-            if event.action == IssuesAction::Labeled && config.message_on_add.is_some() {
-                return Ok(Some(NotifyZulipInput {
-                    notification_type: NotificationType::Labeled,
-                }));
-            } else if config.message_on_remove.is_some() {
-                return Ok(Some(NotifyZulipInput {
-                    notification_type: NotificationType::Unlabeled,
-                }));
+fn has_all_required_labels(issue: &Issue, config: &NotifyZulipLabelConfig) -> bool {
+    for req_label in &config.required_labels {
+        let pattern = match glob::Pattern::new(req_label) {
+            Ok(pattern) => pattern,
+            Err(err) => {
+                log::error!("Invalid glob pattern: {}", err);
+                continue;
             }
+        };
+        if !issue.labels().iter().any(|l| pattern.matches(&l.name)) {
+            return false;
         }
     }
-    Ok(None)
+
+    true
 }
 
 pub(super) async fn handle_input<'a>(
     ctx: &Context,
     config: &NotifyZulipConfig,
     event: &IssuesEvent,
-    input: NotifyZulipInput,
+    inputs: Vec<NotifyZulipInput>,
 ) -> anyhow::Result<()> {
-    let config = config
-        .labels
-        .get(&event.label.as_ref().unwrap().name)
-        .unwrap();
-
-    let mut topic = config.topic.clone();
-    topic = topic.replace("{number}", &event.issue.number.to_string());
-    topic = topic.replace("{title}", &event.issue.title);
-    // Truncate to 60 chars (a Zulip limitation)
-    let mut chars = topic.char_indices().skip(59);
-    if let (Some((len, _)), Some(_)) = (chars.next(), chars.next()) {
-        topic.truncate(len);
-        topic.push('…');
-    }
+    for input in inputs {
+        let config = &config.labels[&input.label.name];
 
-    let mut msg = match input.notification_type {
-        NotificationType::Labeled => config.message_on_add.as_ref().unwrap().clone(),
-        NotificationType::Unlabeled => config.message_on_remove.as_ref().unwrap().clone(),
-    };
+        let mut topic = config.topic.clone();
+        topic = topic.replace("{number}", &event.issue.number.to_string());
+        topic = topic.replace("{title}", &event.issue.title);
+        // Truncate to 60 chars (a Zulip limitation)
+        let mut chars = topic.char_indices().skip(59);
+        if let (Some((len, _)), Some(_)) = (chars.next(), chars.next()) {
+            topic.truncate(len);
+            topic.push('…');
+        }
 
-    msg = msg.replace("{number}", &event.issue.number.to_string());
-    msg = msg.replace("{title}", &event.issue.title);
+        let mut msg = match input.notification_type {
+            NotificationType::Labeled => config.message_on_add.as_ref().unwrap().clone(),
+            NotificationType::Unlabeled => config.message_on_remove.as_ref().unwrap().clone(),
+            NotificationType::Closed => config.message_on_close.as_ref().unwrap().clone(),
+            NotificationType::Reopened => config.message_on_reopen.as_ref().unwrap().clone(),
+        };
 
-    let zulip_req = crate::zulip::MessageApiRequest {
-        recipient: crate::zulip::Recipient::Stream {
-            id: config.zulip_stream,
-            topic: &topic,
-        },
-        content: &msg,
-    };
-    zulip_req.send(&ctx.github.raw()).await?;
+        msg = msg.replace("{number}", &event.issue.number.to_string());
+        msg = msg.replace("{title}", &event.issue.title);
+
+        let zulip_req = crate::zulip::MessageApiRequest {
+            recipient: crate::zulip::Recipient::Stream {
+                id: config.zulip_stream,
+                topic: &topic,
+            },
+            content: &msg,
+        };
+        zulip_req.send(&ctx.github.raw()).await?;
+    }
 
     Ok(())
 }