123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172 |
- use crate::{
- config::{NotifyZulipConfig, NotifyZulipLabelConfig},
- github::{Issue, IssuesAction, IssuesEvent, Label},
- handlers::Context,
- };
- use tracing as log;
- 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) async fn parse_input(
- _ctx: &Context,
- event: &IssuesEvent,
- config: Option<&NotifyZulipConfig>,
- ) -> 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()
- }
- 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;
- }
- }
- true
- }
- pub(super) async fn handle_input<'a>(
- ctx: &Context,
- config: &NotifyZulipConfig,
- event: &IssuesEvent,
- inputs: Vec<NotifyZulipInput>,
- ) -> anyhow::Result<()> {
- for input in inputs {
- let config = &config.labels[&input.label.name];
- 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('…');
- }
- 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(),
- };
- 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(())
- }
|