Skip to content

Commit 201b5d9

Browse files
authored
Merge pull request #1078 from camelid/close-msg
Notify Zulip when issue is closed or reopened
2 parents d547e8b + e3c3985 commit 201b5d9

File tree

2 files changed

+137
-60
lines changed

2 files changed

+137
-60
lines changed

src/config.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@ pub(crate) struct NotifyZulipLabelConfig {
131131
pub(crate) topic: String,
132132
pub(crate) message_on_add: Option<String>,
133133
pub(crate) message_on_remove: Option<String>,
134+
pub(crate) message_on_close: Option<String>,
135+
pub(crate) message_on_reopen: Option<String>,
134136
#[serde(default)]
135137
pub(crate) required_labels: Vec<String>,
136138
}

src/handlers/notify_zulip.rs

Lines changed: 135 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,96 +1,171 @@
11
use crate::{
2-
config::NotifyZulipConfig,
3-
github::{IssuesAction, IssuesEvent},
2+
config::{NotifyZulipConfig, NotifyZulipLabelConfig},
3+
github::{Issue, IssuesAction, IssuesEvent, Label},
44
handlers::Context,
55
};
66

77
pub(super) struct NotifyZulipInput {
88
notification_type: NotificationType,
9+
/// Label that triggered this notification.
10+
///
11+
/// For example, if an `I-prioritize` issue is closed,
12+
/// this field will be `I-prioritize`.
13+
label: Label,
914
}
1015

1116
pub(super) enum NotificationType {
1217
Labeled,
1318
Unlabeled,
19+
Closed,
20+
Reopened,
1421
}
1522

1623
pub(super) fn parse_input(
1724
_ctx: &Context,
1825
event: &IssuesEvent,
1926
config: Option<&NotifyZulipConfig>,
20-
) -> Result<Option<NotifyZulipInput>, String> {
21-
if let IssuesAction::Labeled | IssuesAction::Unlabeled = event.action {
22-
let applied_label = &event.label.as_ref().expect("label").name;
23-
if let Some(config) = config.and_then(|c| c.labels.get(applied_label)) {
24-
for label in &config.required_labels {
25-
let pattern = match glob::Pattern::new(label) {
26-
Ok(pattern) => pattern,
27-
Err(err) => {
28-
log::error!("Invalid glob pattern: {}", err);
29-
continue;
30-
}
31-
};
32-
if !event
33-
.issue
34-
.labels()
35-
.iter()
36-
.any(|l| pattern.matches(&l.name))
37-
{
38-
// Issue misses a required label, ignore this event
39-
return Ok(None);
27+
) -> Result<Option<Vec<NotifyZulipInput>>, String> {
28+
let config = match config {
29+
Some(config) => config,
30+
None => return Ok(None),
31+
};
32+
33+
match event.action {
34+
IssuesAction::Labeled | IssuesAction::Unlabeled => {
35+
let applied_label = event.label.as_ref().expect("label").clone();
36+
Ok(config
37+
.labels
38+
.get(&applied_label.name)
39+
.and_then(|label_config| {
40+
parse_label_change_input(event, applied_label, label_config)
41+
})
42+
.map(|input| vec![input]))
43+
}
44+
IssuesAction::Closed | IssuesAction::Reopened => {
45+
Ok(Some(parse_close_reopen_input(event, config)))
46+
}
47+
_ => Ok(None),
48+
}
49+
}
50+
51+
fn parse_label_change_input(
52+
event: &IssuesEvent,
53+
label: Label,
54+
config: &NotifyZulipLabelConfig,
55+
) -> Option<NotifyZulipInput> {
56+
if !has_all_required_labels(&event.issue, config) {
57+
// Issue misses a required label, ignore this event
58+
return None;
59+
}
60+
61+
match event.action {
62+
IssuesAction::Labeled if config.message_on_add.is_some() => Some(NotifyZulipInput {
63+
notification_type: NotificationType::Labeled,
64+
label,
65+
}),
66+
IssuesAction::Unlabeled if config.message_on_remove.is_some() => Some(NotifyZulipInput {
67+
notification_type: NotificationType::Unlabeled,
68+
label,
69+
}),
70+
_ => None,
71+
}
72+
}
73+
74+
fn parse_close_reopen_input(
75+
event: &IssuesEvent,
76+
global_config: &NotifyZulipConfig,
77+
) -> Vec<NotifyZulipInput> {
78+
event
79+
.issue
80+
.labels
81+
.iter()
82+
.cloned()
83+
.filter_map(|label| {
84+
global_config
85+
.labels
86+
.get(&label.name)
87+
.map(|config| (label, config))
88+
})
89+
.flat_map(|(label, config)| {
90+
if !has_all_required_labels(&event.issue, config) {
91+
// Issue misses a required label, ignore this event
92+
return None;
93+
}
94+
95+
match event.action {
96+
IssuesAction::Closed if config.message_on_close.is_some() => {
97+
Some(NotifyZulipInput {
98+
notification_type: NotificationType::Closed,
99+
label,
100+
})
101+
}
102+
IssuesAction::Reopened if config.message_on_reopen.is_some() => {
103+
Some(NotifyZulipInput {
104+
notification_type: NotificationType::Reopened,
105+
label,
106+
})
40107
}
108+
_ => None,
41109
}
110+
})
111+
.collect()
112+
}
42113

43-
if event.action == IssuesAction::Labeled && config.message_on_add.is_some() {
44-
return Ok(Some(NotifyZulipInput {
45-
notification_type: NotificationType::Labeled,
46-
}));
47-
} else if config.message_on_remove.is_some() {
48-
return Ok(Some(NotifyZulipInput {
49-
notification_type: NotificationType::Unlabeled,
50-
}));
114+
fn has_all_required_labels(issue: &Issue, config: &NotifyZulipLabelConfig) -> bool {
115+
for req_label in &config.required_labels {
116+
let pattern = match glob::Pattern::new(req_label) {
117+
Ok(pattern) => pattern,
118+
Err(err) => {
119+
log::error!("Invalid glob pattern: {}", err);
120+
continue;
51121
}
122+
};
123+
if !issue.labels().iter().any(|l| pattern.matches(&l.name)) {
124+
return false;
52125
}
53126
}
54-
Ok(None)
127+
128+
true
55129
}
56130

57131
pub(super) async fn handle_input<'a>(
58132
ctx: &Context,
59133
config: &NotifyZulipConfig,
60134
event: &IssuesEvent,
61-
input: NotifyZulipInput,
135+
inputs: Vec<NotifyZulipInput>,
62136
) -> anyhow::Result<()> {
63-
let config = config
64-
.labels
65-
.get(&event.label.as_ref().unwrap().name)
66-
.unwrap();
67-
68-
let mut topic = config.topic.clone();
69-
topic = topic.replace("{number}", &event.issue.number.to_string());
70-
topic = topic.replace("{title}", &event.issue.title);
71-
// Truncate to 60 chars (a Zulip limitation)
72-
let mut chars = topic.char_indices().skip(59);
73-
if let (Some((len, _)), Some(_)) = (chars.next(), chars.next()) {
74-
topic.truncate(len);
75-
topic.push('…');
76-
}
137+
for input in inputs {
138+
let config = &config.labels[&input.label.name];
77139

78-
let mut msg = match input.notification_type {
79-
NotificationType::Labeled => config.message_on_add.as_ref().unwrap().clone(),
80-
NotificationType::Unlabeled => config.message_on_remove.as_ref().unwrap().clone(),
81-
};
140+
let mut topic = config.topic.clone();
141+
topic = topic.replace("{number}", &event.issue.number.to_string());
142+
topic = topic.replace("{title}", &event.issue.title);
143+
// Truncate to 60 chars (a Zulip limitation)
144+
let mut chars = topic.char_indices().skip(59);
145+
if let (Some((len, _)), Some(_)) = (chars.next(), chars.next()) {
146+
topic.truncate(len);
147+
topic.push('…');
148+
}
82149

83-
msg = msg.replace("{number}", &event.issue.number.to_string());
84-
msg = msg.replace("{title}", &event.issue.title);
150+
let mut msg = match input.notification_type {
151+
NotificationType::Labeled => config.message_on_add.as_ref().unwrap().clone(),
152+
NotificationType::Unlabeled => config.message_on_remove.as_ref().unwrap().clone(),
153+
NotificationType::Closed => config.message_on_close.as_ref().unwrap().clone(),
154+
NotificationType::Reopened => config.message_on_reopen.as_ref().unwrap().clone(),
155+
};
85156

86-
let zulip_req = crate::zulip::MessageApiRequest {
87-
recipient: crate::zulip::Recipient::Stream {
88-
id: config.zulip_stream,
89-
topic: &topic,
90-
},
91-
content: &msg,
92-
};
93-
zulip_req.send(&ctx.github.raw()).await?;
157+
msg = msg.replace("{number}", &event.issue.number.to_string());
158+
msg = msg.replace("{title}", &event.issue.title);
159+
160+
let zulip_req = crate::zulip::MessageApiRequest {
161+
recipient: crate::zulip::Recipient::Stream {
162+
id: config.zulip_stream,
163+
topic: &topic,
164+
},
165+
content: &msg,
166+
};
167+
zulip_req.send(&ctx.github.raw()).await?;
168+
}
94169

95170
Ok(())
96171
}

0 commit comments

Comments
 (0)