Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(notify-zulip): support notify-zulip.<label> getting mapped to multiple actions #1853

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,17 @@ pub(crate) struct AutolabelLabelConfig {
#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
pub(crate) struct NotifyZulipConfig {
#[serde(flatten)]
pub(crate) labels: HashMap<String, NotifyZulipLabelConfig>,
pub(crate) labels: HashMap<String, NotifyZulipNameConfig>,
}

#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
pub(crate) struct NotifyZulipNameConfig {
// This keeps the default configuration for the label, e.g. `[notify-zulip."beta-nominated"]`
#[serde(flatten)]
pub(crate) default: Option<NotifyZulipLabelConfig>,
Copy link
Contributor

@apiraino apiraino Nov 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ismailarilik can you put a comment explaining what this field is (is this a label?) and why is it needed?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

// This keeps other named configurations for the label, e.g. `[notify-zulip."beta-nominated".compiler]`
#[serde(flatten)]
pub(crate) others: Option<HashMap<String, NotifyZulipLabelConfig>>,
}

#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
Expand Down
223 changes: 164 additions & 59 deletions src/handlers/notify_zulip.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{
config::{NotifyZulipConfig, NotifyZulipLabelConfig},
config::{NotifyZulipConfig, NotifyZulipLabelConfig, NotifyZulipNameConfig},
github::{Issue, IssuesAction, IssuesEvent, Label},
handlers::Context,
};
Expand All @@ -12,6 +12,8 @@ pub(super) struct NotifyZulipInput {
/// For example, if an `I-prioritize` issue is closed,
/// this field will be `I-prioritize`.
label: Label,
is_default_valid: bool,
names: Vec<String>,
}

pub(super) enum NotificationType {
Expand Down Expand Up @@ -52,26 +54,71 @@ pub(super) async fn parse_input(
fn parse_label_change_input(
event: &IssuesEvent,
label: Label,
config: &NotifyZulipLabelConfig,
config: &NotifyZulipNameConfig,
) -> Option<NotifyZulipInput> {
if !has_all_required_labels(&event.issue, config) {
// Issue misses a required label, ignore this event
let mut is_default_valid = false;
let mut names: Vec<String> = vec![];

match &config.default {
Some(label_config) => {
if has_all_required_labels(&event.issue, &label_config) {
match event.action {
IssuesAction::Labeled { .. } if !label_config.messages_on_add.is_empty() => {
is_default_valid = true;
}
IssuesAction::Unlabeled { .. }
if !label_config.messages_on_remove.is_empty() =>
{
is_default_valid = true;
}
_ => (),
}
}
}
None => (),
}

match &config.others {
Some(other_configs) => {
for (name, label_config) in other_configs {
if has_all_required_labels(&event.issue, &label_config) {
match event.action {
IssuesAction::Labeled { .. }
if !label_config.messages_on_add.is_empty() =>
{
names.push(name.to_string());
}
IssuesAction::Unlabeled { .. }
if !label_config.messages_on_remove.is_empty() =>
{
names.push(name.to_string());
}
_ => (),
}
}
}
}
None => (),
}

if !is_default_valid && names.is_empty() {
// It seems that there is no match between this event and any notify-zulip config, ignore this event
return None;
}

match event.action {
IssuesAction::Labeled { .. } if !config.messages_on_add.is_empty() => {
Some(NotifyZulipInput {
notification_type: NotificationType::Labeled,
label,
})
}
IssuesAction::Unlabeled { .. } if !config.messages_on_remove.is_empty() => {
Some(NotifyZulipInput {
notification_type: NotificationType::Unlabeled,
label,
})
}
IssuesAction::Labeled { .. } => Some(NotifyZulipInput {
notification_type: NotificationType::Labeled,
label,
is_default_valid,
names,
}),
IssuesAction::Unlabeled { .. } => Some(NotifyZulipInput {
notification_type: NotificationType::Unlabeled,
label,
is_default_valid,
names,
}),
_ => None,
}
}
Expand All @@ -92,24 +139,69 @@ fn parse_close_reopen_input(
.map(|config| (label, config))
})
.flat_map(|(label, config)| {
if !has_all_required_labels(&event.issue, config) {
// Issue misses a required label, ignore this event
let mut is_default_valid = false;
let mut names: Vec<String> = vec![];

match &config.default {
Some(label_config) => {
if has_all_required_labels(&event.issue, &label_config) {
match event.action {
IssuesAction::Closed if !label_config.messages_on_close.is_empty() => {
is_default_valid = true;
}
IssuesAction::Reopened
if !label_config.messages_on_reopen.is_empty() =>
{
is_default_valid = true;
}
_ => (),
}
}
}
None => (),
}

match &config.others {
Some(other_configs) => {
for (name, label_config) in other_configs {
if has_all_required_labels(&event.issue, &label_config) {
match event.action {
IssuesAction::Closed
if !label_config.messages_on_close.is_empty() =>
{
names.push(name.to_string());
}
IssuesAction::Reopened
if !label_config.messages_on_reopen.is_empty() =>
{
names.push(name.to_string());
}
_ => (),
}
}
}
}
None => (),
}

if !is_default_valid && names.is_empty() {
// It seems that there is no match between this event and any notify-zulip config, ignore this event
return None;
}

match event.action {
IssuesAction::Closed if !config.messages_on_close.is_empty() => {
Some(NotifyZulipInput {
notification_type: NotificationType::Closed,
label,
})
}
IssuesAction::Reopened if !config.messages_on_reopen.is_empty() => {
Some(NotifyZulipInput {
notification_type: NotificationType::Reopened,
label,
})
}
IssuesAction::Closed => Some(NotifyZulipInput {
notification_type: NotificationType::Closed,
label,
is_default_valid,
names,
}),
IssuesAction::Reopened => Some(NotifyZulipInput {
notification_type: NotificationType::Reopened,
label,
is_default_valid,
names,
}),
_ => None,
}
})
Expand Down Expand Up @@ -140,41 +232,54 @@ pub(super) async fn handle_input<'a>(
inputs: Vec<NotifyZulipInput>,
) -> anyhow::Result<()> {
for input in inputs {
let config = &config.labels[&input.label.name];

let topic = &config.topic;
let topic = topic.replace("{number}", &event.issue.number.to_string());
let mut 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('…');
let name_config = &config.labels[&input.label.name];

// Get valid label configs
let mut label_configs: Vec<&NotifyZulipLabelConfig> = vec![];
if input.is_default_valid {
label_configs.push(name_config.default.as_ref().unwrap());
}
for name in input.names {
label_configs.push(&name_config.others.as_ref().unwrap()[&name]);
}

let msgs = match input.notification_type {
NotificationType::Labeled => &config.messages_on_add,
NotificationType::Unlabeled => &config.messages_on_remove,
NotificationType::Closed => &config.messages_on_close,
NotificationType::Reopened => &config.messages_on_reopen,
};
for label_config in label_configs {
let config = label_config;

let recipient = crate::zulip::Recipient::Stream {
id: config.zulip_stream,
topic: &topic,
};
let topic = &config.topic;
let topic = topic.replace("{number}", &event.issue.number.to_string());
let mut 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 msg in msgs {
let msg = msg.replace("{number}", &event.issue.number.to_string());
let msg = msg.replace("{title}", &event.issue.title);
let msg = replace_team_to_be_nominated(&event.issue.labels, msg);
let msgs = match input.notification_type {
NotificationType::Labeled => &config.messages_on_add,
NotificationType::Unlabeled => &config.messages_on_remove,
NotificationType::Closed => &config.messages_on_close,
NotificationType::Reopened => &config.messages_on_reopen,
};

crate::zulip::MessageApiRequest {
recipient,
content: &msg,
let recipient = crate::zulip::Recipient::Stream {
id: config.zulip_stream,
topic: &topic,
};

for msg in msgs {
let msg = msg.replace("{number}", &event.issue.number.to_string());
let msg = msg.replace("{title}", &event.issue.title);
let msg = replace_team_to_be_nominated(&event.issue.labels, msg);

crate::zulip::MessageApiRequest {
recipient,
content: &msg,
}
.send(&ctx.github.raw())
.await?;
}
.send(&ctx.github.raw())
.await?;
}
}

Expand Down
Loading