-
Notifications
You must be signed in to change notification settings - Fork 78
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1078 from camelid/close-msg
Notify Zulip when issue is closed or reopened
- Loading branch information
Showing
2 changed files
with
137 additions
and
60 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(()) | ||
} |