|
1 | 1 | use crate::{
|
2 |
| - config::NotifyZulipConfig, |
3 |
| - github::{IssuesAction, IssuesEvent}, |
| 2 | + config::{NotifyZulipConfig, NotifyZulipLabelConfig}, |
| 3 | + github::{Issue, IssuesAction, IssuesEvent, Label}, |
4 | 4 | handlers::Context,
|
5 | 5 | };
|
6 | 6 |
|
7 | 7 | pub(super) struct NotifyZulipInput {
|
8 | 8 | 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, |
9 | 14 | }
|
10 | 15 |
|
11 | 16 | pub(super) enum NotificationType {
|
12 | 17 | Labeled,
|
13 | 18 | Unlabeled,
|
| 19 | + Closed, |
| 20 | + Reopened, |
14 | 21 | }
|
15 | 22 |
|
16 | 23 | pub(super) fn parse_input(
|
17 | 24 | _ctx: &Context,
|
18 | 25 | event: &IssuesEvent,
|
19 | 26 | 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 | + }) |
40 | 107 | }
|
| 108 | + _ => None, |
41 | 109 | }
|
| 110 | + }) |
| 111 | + .collect() |
| 112 | +} |
42 | 113 |
|
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; |
51 | 121 | }
|
| 122 | + }; |
| 123 | + if !issue.labels().iter().any(|l| pattern.matches(&l.name)) { |
| 124 | + return false; |
52 | 125 | }
|
53 | 126 | }
|
54 |
| - Ok(None) |
| 127 | + |
| 128 | + true |
55 | 129 | }
|
56 | 130 |
|
57 | 131 | pub(super) async fn handle_input<'a>(
|
58 | 132 | ctx: &Context,
|
59 | 133 | config: &NotifyZulipConfig,
|
60 | 134 | event: &IssuesEvent,
|
61 |
| - input: NotifyZulipInput, |
| 135 | + inputs: Vec<NotifyZulipInput>, |
62 | 136 | ) -> 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]; |
77 | 139 |
|
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 | + } |
82 | 149 |
|
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 | + }; |
85 | 156 |
|
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 | + } |
94 | 169 |
|
95 | 170 | Ok(())
|
96 | 171 | }
|
0 commit comments