Skip to main content

tryaudex_core/
notify.rs

1use crate::error::{AvError, Result};
2use crate::session::Session;
3
4/// Supported notification targets.
5pub enum NotifyTarget {
6    Slack(String),
7    Discord(String),
8    Generic(String),
9}
10
11impl NotifyTarget {
12    /// Parse a notification URL like "slack://webhook-url" or a plain HTTPS URL.
13    pub fn parse(url: &str) -> Result<Self> {
14        if let Some(webhook) = url.strip_prefix("slack://") {
15            Ok(Self::Slack(webhook.to_string()))
16        } else if let Some(webhook) = url.strip_prefix("discord://") {
17            Ok(Self::Discord(webhook.to_string()))
18        } else if url.starts_with("https://") || url.starts_with("http://") {
19            Ok(Self::Generic(url.to_string()))
20        } else {
21            Err(AvError::InvalidPolicy(format!(
22                "Invalid notify URL '{}'. Use slack://WEBHOOK_URL, discord://WEBHOOK_URL, or an HTTPS URL",
23                url
24            )))
25        }
26    }
27}
28
29/// Send a session start notification.
30pub async fn notify_start(target: &NotifyTarget, session: &Session) -> Result<()> {
31    let actions: Vec<String> = session.policy.actions.iter().map(|a| a.to_iam_action()).collect();
32    let text = format!(
33        "Audex session started\n\u{2022} Session: `{}`\n\u{2022} Role: `{}`\n\u{2022} Actions: `{}`\n\u{2022} Command: `{}`\n\u{2022} TTL: {}s",
34        &session.id[..8],
35        session.role_arn,
36        actions.join(", "),
37        session.command.join(" "),
38        session.ttl_seconds,
39    );
40    send(target, &text).await
41}
42
43/// Send a session end notification.
44pub async fn notify_end(target: &NotifyTarget, session: &Session, exit_code: Option<i32>) -> Result<()> {
45    let emoji = match session.status.to_string().as_str() {
46        "completed" => "\u{2705}",
47        "failed" => "\u{274c}",
48        _ => "\u{26a0}\u{fe0f}",
49    };
50    let text = format!(
51        "{} Audex session ended\n\u{2022} Session: `{}`\n\u{2022} Status: {}\n\u{2022} Command: `{}`{}",
52        emoji,
53        &session.id[..8],
54        session.status,
55        session.command.join(" "),
56        exit_code.map_or(String::new(), |c| format!("\n\u{2022} Exit code: {}", c)),
57    );
58    send(target, &text).await
59}
60
61async fn send(target: &NotifyTarget, text: &str) -> Result<()> {
62    let client = reqwest::Client::new();
63
64    let (url, body) = match target {
65        NotifyTarget::Slack(webhook) => {
66            let url = if webhook.starts_with("https://") {
67                webhook.clone()
68            } else {
69                format!("https://{}", webhook)
70            };
71            (url, serde_json::json!({ "text": text }))
72        }
73        NotifyTarget::Discord(webhook) => {
74            let url = if webhook.starts_with("https://") {
75                webhook.clone()
76            } else {
77                format!("https://{}", webhook)
78            };
79            (url, serde_json::json!({ "content": text }))
80        }
81        NotifyTarget::Generic(url) => {
82            (url.clone(), serde_json::json!({
83                "text": text,
84                "source": "audex",
85            }))
86        }
87    };
88
89    client
90        .post(&url)
91        .header("content-type", "application/json")
92        .json(&body)
93        .send()
94        .await
95        .map_err(|e| AvError::Sts(format!("Webhook notification failed: {}", e)))?;
96
97    Ok(())
98}