Skip to content

Commit

Permalink
Merge pull request #1078 from camelid/close-msg
Browse files Browse the repository at this point in the history
Notify Zulip when issue is closed or reopened
  • Loading branch information
spastorino authored Aug 26, 2021
2 parents d547e8b + e3c3985 commit 201b5d9
Show file tree
Hide file tree
Showing 2 changed files with 137 additions and 60 deletions.
2 changes: 2 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
}
Expand Down
195 changes: 135 additions & 60 deletions src/handlers/notify_zulip.rs
Original file line number Diff line number Diff line change
@@ -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(())
}

0 comments on commit 201b5d9

Please sign in to comment.