use std::collections::HashMap;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::{HumanDuration, StringOrBool, deserialize_string_or_bool_opt};
pub const DEFAULT_ANNOUNCE_DEADLINE: std::time::Duration = std::time::Duration::from_secs(90);
pub const MIN_ANNOUNCE_DEADLINE: std::time::Duration = std::time::Duration::from_secs(1);
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum AnnounceGate {
#[default]
RequiredPublishers,
AllPublishers,
None,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct AnnounceConfig {
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub skip: Option<StringOrBool>,
#[serde(rename = "if")]
pub if_condition: Option<String>,
#[serde(default)]
pub gate_on: AnnounceGate,
pub deadline: Option<HumanDuration>,
pub discord: Option<DiscordAnnounce>,
pub discourse: Option<DiscourseAnnounce>,
pub slack: Option<SlackAnnounce>,
pub webhook: Option<WebhookConfig>,
pub telegram: Option<TelegramAnnounce>,
pub teams: Option<TeamsAnnounce>,
pub mattermost: Option<MattermostAnnounce>,
#[serde(alias = "smtp")]
pub email: Option<EmailAnnounce>,
pub reddit: Option<RedditAnnounce>,
pub twitter: Option<TwitterAnnounce>,
pub mastodon: Option<MastodonAnnounce>,
pub bluesky: Option<BlueskyAnnounce>,
pub linkedin: Option<LinkedInAnnounce>,
pub opencollective: Option<OpenCollectiveAnnounce>,
}
impl AnnounceConfig {
pub fn deadline_duration(&self) -> std::time::Duration {
self.deadline
.map(|d| d.duration())
.unwrap_or(DEFAULT_ANNOUNCE_DEADLINE)
.max(MIN_ANNOUNCE_DEADLINE)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct BlueskyAnnounce {
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub enabled: Option<StringOrBool>,
pub username: Option<String>,
pub message_template: Option<String>,
pub pds_url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct DiscourseAnnounce {
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub enabled: Option<StringOrBool>,
pub server: Option<String>,
pub category_id: Option<u64>,
pub username: Option<String>,
pub title_template: Option<String>,
pub message_template: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct LinkedInAnnounce {
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub enabled: Option<StringOrBool>,
pub message_template: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct OpenCollectiveAnnounce {
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub enabled: Option<StringOrBool>,
pub slug: Option<String>,
pub title_template: Option<String>,
pub message_template: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct TwitterAnnounce {
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub enabled: Option<StringOrBool>,
pub message_template: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct MastodonAnnounce {
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub enabled: Option<StringOrBool>,
pub server: Option<String>,
pub message_template: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct DiscordAnnounce {
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub enabled: Option<StringOrBool>,
pub webhook_url: Option<String>,
pub message_template: Option<String>,
pub author: Option<String>,
pub color: Option<String>,
pub icon_url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct WebhookConfig {
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub enabled: Option<StringOrBool>,
pub endpoint_url: Option<String>,
pub headers: Option<HashMap<String, String>>,
pub content_type: Option<String>,
pub message_template: Option<String>,
pub skip_tls_verify: Option<bool>,
#[serde(default)]
pub expected_status_codes: Vec<u16>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct TelegramAnnounce {
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub enabled: Option<StringOrBool>,
pub bot_token: Option<String>,
pub chat_id: Option<String>,
pub message_template: Option<String>,
pub parse_mode: Option<String>,
pub message_thread_id: Option<String>,
}
pub const TEAMS_DEFAULT_TITLE_TEMPLATE: &str = "{{ ProjectName }} {{ Tag }} is out!";
fn default_teams_title_template() -> Option<String> {
Some(TEAMS_DEFAULT_TITLE_TEMPLATE.to_string())
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct TeamsAnnounce {
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub enabled: Option<StringOrBool>,
pub webhook_url: Option<String>,
pub message_template: Option<String>,
#[serde(default = "default_teams_title_template")]
pub title_template: Option<String>,
pub color: Option<String>,
pub icon_url: Option<String>,
}
impl Default for TeamsAnnounce {
fn default() -> Self {
Self {
enabled: None,
webhook_url: None,
message_template: None,
title_template: default_teams_title_template(),
color: None,
icon_url: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct MattermostAnnounce {
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub enabled: Option<StringOrBool>,
pub webhook_url: Option<String>,
pub channel: Option<String>,
pub username: Option<String>,
pub icon_url: Option<String>,
pub icon_emoji: Option<String>,
pub color: Option<String>,
pub message_template: Option<String>,
pub title_template: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct EmailAnnounce {
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub enabled: Option<StringOrBool>,
pub host: Option<String>,
pub port: Option<u16>,
pub username: Option<String>,
pub from: Option<String>,
#[serde(default)]
pub to: Vec<String>,
pub subject_template: Option<String>,
pub message_template: Option<String>,
pub insecure_skip_verify: Option<bool>,
pub encryption: Option<EmailEncryption>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum EmailEncryption {
#[default]
Auto,
Tls,
Starttls,
None,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct RedditAnnounce {
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub enabled: Option<StringOrBool>,
pub application_id: Option<String>,
pub username: Option<String>,
pub sub: Option<String>,
pub title_template: Option<String>,
pub url_template: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct SlackAnnounce {
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub enabled: Option<StringOrBool>,
pub webhook_url: Option<String>,
pub message_template: Option<String>,
pub channel: Option<String>,
pub username: Option<String>,
pub icon_emoji: Option<String>,
pub icon_url: Option<String>,
pub blocks: Option<Vec<SlackBlock>>,
pub attachments: Option<Vec<SlackAttachment>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
pub struct SlackBlock {
#[serde(rename = "type")]
pub block_type: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub text: Option<SlackTextObject>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub block_id: Option<String>,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct SlackTextObject {
#[serde(rename = "type")]
pub text_type: String,
pub text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub emoji: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub verbatim: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
pub struct SlackAttachment {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub color: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fallback: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pretext: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub footer: Option<String>,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::HumanDuration;
use std::time::Duration;
#[test]
fn deadline_unset_uses_default() {
let cfg = AnnounceConfig::default();
assert_eq!(cfg.deadline_duration(), DEFAULT_ANNOUNCE_DEADLINE);
}
#[test]
fn deadline_zero_is_clamped_to_floor() {
let cfg = AnnounceConfig {
deadline: Some(HumanDuration(Duration::ZERO)),
..Default::default()
};
assert_eq!(cfg.deadline_duration(), MIN_ANNOUNCE_DEADLINE);
}
#[test]
fn deadline_subsecond_is_clamped_to_floor() {
let cfg = AnnounceConfig {
deadline: Some(HumanDuration(Duration::from_millis(250))),
..Default::default()
};
assert_eq!(cfg.deadline_duration(), MIN_ANNOUNCE_DEADLINE);
}
#[test]
fn deadline_above_floor_is_preserved() {
let cfg = AnnounceConfig {
deadline: Some(HumanDuration(Duration::from_secs(45))),
..Default::default()
};
assert_eq!(cfg.deadline_duration(), Duration::from_secs(45));
}
}